feat(payment): 新增「售后策略」配置与前台售后弹窗链路
- 后台「支付&付费 → 售后策略」:售后功能开关、申请时效、超时效提示(HTML)、 预置原因、申请售后说明、所需材料、申请说明占位文案、可上传图片开关; PHP schema 与 SPA schema 1:1 对齐,group 字段支持拖拽排序 - PaymentRuntime 新增售后设置解析(缺键回退 schema 默认、HTML 走 kses、 中文原因稳定 slug)与 after_sale_expired 时效判定(支付时间起算) - 售后工单支持举证图片(order_aftersale.images 列,最多 3 张, 复用评论图片上传校验)与上传路由;订单视图模型输出 after_sale 按钮状态 - 前台申请 / 超时效提示 / 售后详细三个独立弹窗(modal-mini + 蓝色炫彩头), 订单详情弹窗不再内嵌售后表单并清理相关死代码 - 新增 after-sale-policy 契约测试与运行时 harness,527 项契约测试通过
This commit is contained in:
@@ -504,6 +504,34 @@ class SchemaPayment {
|
||||
]],
|
||||
],
|
||||
],
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* 售后策略
|
||||
* ------------------------------------------------------------------ */
|
||||
'after_sale' => [
|
||||
'key' => 'after_sale',
|
||||
'title' => '售后策略',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'main', 'title' => '售后功能', 'icon' => 'fa fas fa-undo-alt', 'fields' => [
|
||||
['key' => 'after_sale_s', 'label' => '售后功能', 'type' => 'switch', 'default' => true, 'help' => '开启后用户可以申请售后;关闭后不再处理售后,前台「我的订单」不再显示申请售后按钮'],
|
||||
['key' => 'after_sale_time_limit', 'label' => '申请售后时效(天)', 'type' => 'number', 'default' => 7, 'min' => 0, 'max' => 3650, 'help' => '订单支付后多少天内可以申请售后,超过时效将无法申请;0 表示不限制'],
|
||||
['key' => 'after_sale_expired_text', 'label' => '超过时效提示', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '<div>抱歉,您的订单已超过售后申请时效</div>', 'rows' => 3, 'help' => '订单超过售后时效后,用户点击申请售后时弹窗提示的内容(支持HTML代码,请注意代码规范)'],
|
||||
]],
|
||||
['key' => 'form', 'title' => '申请表单', 'icon' => 'fa fas fa-file-alt', 'fields' => [
|
||||
['key' => 'after_sale_reasons', 'label' => '预置原因', 'type' => 'group', 'button_title' => '添加原因', 'help' => '用户申请售后时可选择的原因', 'default' => [['t' => '质量问题'], ['t' => '假冒品牌'], ['t' => '其他原因']], 'fields' => [
|
||||
['key' => 't', 'label' => '原因', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'after_sale_desc', 'label' => '申请售后说明', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '', 'rows' => 5, 'help' => '用户申请售后时显示的内容(支持HTML代码,请注意代码规范)'],
|
||||
['key' => 'after_sale_materials', 'label' => '申请所需材料', 'type' => 'group', 'button_title' => '添加材料', 'help' => '用户提交售后申请时需要提供的资料,如联系邮箱、联系电话;此处的信息收集,仅用于售后联系与审核', 'default' => [['t' => '联系邮箱'], ['t' => '联系电话']], 'fields' => [
|
||||
['key' => 't', 'label' => '资料项', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'after_sale_reason_placeholder', 'label' => '申请说明占位文案', 'type' => 'input', 'default' => '必填,请您详细填写申请原因', 'help' => '前台申请售后弹窗中「申请说明」输入框的提示文字'],
|
||||
['key' => 'after_sale_img_s', 'label' => '申请售后可上传图片', 'type' => 'switch', 'default' => true, 'help' => '开启后申请售后弹窗中出现图片上传入口(最多上传3张)'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -211,6 +211,7 @@ trait DBMigratorPart2 {
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
aftersale_type VARCHAR(24) NOT NULL DEFAULT 'refund',
|
||||
reason TEXT NOT NULL,
|
||||
images LONGTEXT DEFAULT NULL,
|
||||
refund_amount VARCHAR(32) NOT NULL DEFAULT '0',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
admin_note TEXT DEFAULT NULL,
|
||||
|
||||
@@ -407,6 +407,18 @@ trait ScriptsPart1b {
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
// 售后弹窗(申请 / 超时效提示 / 售后详细):与订单中心同面板、同条件入队。
|
||||
$order_after_sale_js = get_template_directory() . '/assets/js/order-after-sale.js';
|
||||
if (file_exists($order_after_sale_js)) {
|
||||
wp_enqueue_script(
|
||||
'aksrt-order-after-sale',
|
||||
$base_uri . '/assets/js/order-after-sale.js',
|
||||
['loader_js'],
|
||||
(string) filemtime($order_after_sale_js),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$identity_js = get_template_directory() . '/assets/js/routes/user-center-auth.js';
|
||||
|
||||
@@ -498,6 +498,24 @@ trait ScriptsPart1c {
|
||||
'desc' => (string) ($report['desc'] ?? ''),
|
||||
];
|
||||
})(),
|
||||
// 售后策略数据(order-after-sale.js):预置原因、所需材料、申请说明与
|
||||
// 占位文案、图片开关、超时效提示均读取后台「支付&付费 → 售后策略」。
|
||||
'afterSale' => (function () {
|
||||
if (!class_exists('\AKSRT\Settings\PaymentRuntime')) {
|
||||
return ['enabled' => false, 'reasons' => [], 'materials' => [], 'desc' => '', 'imgEnabled' => false, 'maxImages' => 3, 'reasonPlaceholder' => '', 'expiredText' => ''];
|
||||
}
|
||||
$after_sale = \AKSRT\Settings\PaymentRuntime::get_after_sale_settings();
|
||||
return [
|
||||
'enabled' => !empty($after_sale['enabled']),
|
||||
'reasons' => array_values((array) ($after_sale['reasons'] ?? [])),
|
||||
'materials' => array_values((array) ($after_sale['materials'] ?? [])),
|
||||
'desc' => (string) ($after_sale['desc'] ?? ''),
|
||||
'imgEnabled' => !empty($after_sale['img_enabled']),
|
||||
'maxImages' => 3,
|
||||
'reasonPlaceholder' => (string) ($after_sale['reason_placeholder'] ?? ''),
|
||||
'expiredText' => (string) ($after_sale['expired_text'] ?? ''),
|
||||
];
|
||||
})(),
|
||||
'stickyEnabled' => class_exists('\AKSRT\Settings\Settings') ? !empty(\AKSRT\Settings\Settings::get_post_list_style_settings()['sticky_s']) : true,
|
||||
'stickyPages' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_post_list_style_settings()['sticky_s'] : ['home', 'cat'],
|
||||
'badgeShow' => class_exists('\AKSRT\Settings\Settings') ? !empty(\AKSRT\Settings\Settings::get_post_list_style_settings()['list_badge_show']) : true,
|
||||
|
||||
@@ -30,6 +30,9 @@ final class AfterSaleService {
|
||||
['methods' => \WP_REST_Server::CREATABLE, 'callback' => [$this, 'rest_submit'], 'permission_callback' => [$this, 'require_user'], 'args' => [
|
||||
'order_number' => ['required' => true, 'sanitize_callback' => 'sanitize_text_field'],
|
||||
'reason' => ['required' => true, 'sanitize_callback' => 'sanitize_textarea_field'],
|
||||
'images' => ['required' => false, 'type' => 'array', 'validate_callback' => static function ($value): bool {
|
||||
return is_array($value) && count($value) <= 3;
|
||||
}],
|
||||
]],
|
||||
['methods' => \WP_REST_Server::READABLE, 'callback' => [$this, 'rest_list'], 'permission_callback' => [$this, 'require_user'], 'args' => [
|
||||
'order_number' => ['required' => true, 'sanitize_callback' => 'sanitize_text_field'],
|
||||
@@ -38,6 +41,37 @@ final class AfterSaleService {
|
||||
'order_number' => ['required' => true, 'sanitize_callback' => 'sanitize_text_field'],
|
||||
]],
|
||||
]);
|
||||
// 售后举证图片上传:复用评论图片上传链路(大小 / 类型 / 内容审核同一套校验)。
|
||||
register_rest_route('aksrt/v1', '/orders/' . $number . '/after-sale/upload-image', [
|
||||
['methods' => \WP_REST_Server::CREATABLE, 'callback' => [$this, 'rest_upload_image'], 'permission_callback' => [$this, 'require_after_sale_image']],
|
||||
]);
|
||||
}
|
||||
|
||||
/** 售后举证图片上传权限:登录 + nonce + 售后开启 + 图片开关开启 + 订单归属本人。 */
|
||||
public function require_after_sale_image(\WP_REST_Request $request) {
|
||||
$user = $this->require_user($request);
|
||||
if (is_wp_error($user)) {
|
||||
return $user;
|
||||
}
|
||||
if (!class_exists('\AKSRT\Settings\PaymentRuntime')) {
|
||||
return new \WP_Error('aksrt_after_sale_forbidden', __('售后功能暂未开启', 'aksrt'), ['status' => 403]);
|
||||
}
|
||||
$settings = \AKSRT\Settings\PaymentRuntime::get_after_sale_settings();
|
||||
if (empty($settings['enabled']) || empty($settings['img_enabled'])) {
|
||||
return new \WP_Error('aksrt_after_sale_forbidden', __('售后图片上传未开启', 'aksrt'), ['status' => 403]);
|
||||
}
|
||||
$number = sanitize_text_field((string) $request->get_param('order_number'));
|
||||
if (!OrderRepository::get_instance()->get_user_order(get_current_user_id(), $number)) {
|
||||
return new \WP_Error('aksrt_order_missing', __('订单不存在', 'aksrt'), ['status' => 404]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rest_upload_image(\WP_REST_Request $request) {
|
||||
if (!class_exists('\AKSRT\Admin\UserController')) {
|
||||
return new \WP_Error('aksrt_after_sale_upload', __('上传服务不可用', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
return \AKSRT\Admin\UserController::get_instance()->rest_upload_comment_image($request);
|
||||
}
|
||||
|
||||
public function require_user(\WP_REST_Request $request) {
|
||||
@@ -47,7 +81,8 @@ final class AfterSaleService {
|
||||
}
|
||||
|
||||
public function rest_submit(\WP_REST_Request $request) {
|
||||
$result = $this->create(get_current_user_id(), (string) $request->get_param('order_number'), (string) $request->get_param('reason'));
|
||||
$images = $request->get_param('images');
|
||||
$result = $this->create(get_current_user_id(), (string) $request->get_param('order_number'), (string) $request->get_param('reason'), is_array($images) ? $images : []);
|
||||
return is_wp_error($result) ? $result : rest_ensure_response(['success' => true, 'data' => $result]);
|
||||
}
|
||||
|
||||
@@ -62,8 +97,9 @@ final class AfterSaleService {
|
||||
return is_wp_error($result) ? $result : rest_ensure_response(['success' => true, 'data' => $result]);
|
||||
}
|
||||
|
||||
public function create(int $user_id, string $order_number, string $reason) {
|
||||
public function create(int $user_id, string $order_number, string $reason, array $images = []) {
|
||||
$reason = sanitize_textarea_field($reason);
|
||||
$images = $this->sanitize_images($images);
|
||||
if ($user_id <= 0 || !preg_match('/^(?:\d{20}|\d{22})$/', $order_number) || $reason === '' || mb_strlen($reason) > 2000) {
|
||||
return new \WP_Error('aksrt_after_sale_invalid', __('请填写有效的售后原因', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
@@ -84,15 +120,37 @@ final class AfterSaleService {
|
||||
if ($amount === null) return new \WP_Error('aksrt_after_sale_amount', __('订单退款金额无效', 'aksrt'), ['status' => 400]);
|
||||
$inserted = $wpdb->insert($this->table(), [
|
||||
'order_number' => $order_number, 'user_id' => $user_id, 'aftersale_type' => 'refund',
|
||||
'reason' => $reason, 'refund_amount' => $amount, 'status' => self::STATUS_PENDING,
|
||||
'created_at' => current_time('mysql'),
|
||||
], ['%s', '%d', '%s', '%s', '%s', '%s', '%s']);
|
||||
'reason' => $reason, 'images' => wp_json_encode($images), 'refund_amount' => $amount,
|
||||
'status' => self::STATUS_PENDING, 'created_at' => current_time('mysql'),
|
||||
], ['%s', '%d', '%s', '%s', '%s', '%s', '%s', '%s']);
|
||||
return $inserted === 1 ? $this->get((int) $wpdb->insert_id) : new \WP_Error('aksrt_after_sale_db', __('售后申请保存失败', 'aksrt'), ['status' => 500]);
|
||||
} finally {
|
||||
$wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $lock));
|
||||
}
|
||||
}
|
||||
|
||||
/** 售后举证图片:最多 3 条,统一走 esc_url_raw;非字符串与空值直接丢弃。 */
|
||||
private function sanitize_images(array $images): array {
|
||||
$clean = [];
|
||||
foreach ($images as $url) {
|
||||
if (!is_string($url) || count($clean) >= 3) {
|
||||
continue;
|
||||
}
|
||||
$url = esc_url_raw(trim($url));
|
||||
if ($url !== '') {
|
||||
$clean[] = $url;
|
||||
}
|
||||
}
|
||||
return $clean;
|
||||
}
|
||||
|
||||
/** 最新一条售后工单(列表视图只取最新态,避免整表拉取)。 */
|
||||
public function latest_for_order(string $order_number): ?array {
|
||||
global $wpdb;
|
||||
$row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$this->table()} WHERE order_number = %s ORDER BY id DESC LIMIT 1", $order_number), ARRAY_A);
|
||||
return is_array($row) ? $this->normalize($row) : null;
|
||||
}
|
||||
|
||||
public function cancel_for_user(int $user_id, string $order_number) {
|
||||
if ($user_id <= 0 || !preg_match('/^(?:\d{20}|\d{22})$/', $order_number)) return new \WP_Error('aksrt_after_sale_invalid', __('售后申请无效', 'aksrt'), ['status' => 400]);
|
||||
global $wpdb;
|
||||
@@ -550,5 +608,13 @@ final class AfterSaleService {
|
||||
private function table(): string { return \AKSRT\Core\DBMigrator::table('order_aftersale'); }
|
||||
|
||||
/** @param array<string,mixed> $row */
|
||||
private function normalize(array $row): array { $row['id'] = (int) ($row['id'] ?? 0); $row['user_id'] = (int) ($row['user_id'] ?? 0); return $row; }
|
||||
private function normalize(array $row): array {
|
||||
$row['id'] = (int) ($row['id'] ?? 0);
|
||||
$row['user_id'] = (int) ($row['user_id'] ?? 0);
|
||||
$images = json_decode((string) ($row['images'] ?? ''), true);
|
||||
$row['images'] = is_array($images)
|
||||
? array_values(array_filter($images, static fn($url): bool => is_string($url) && $url !== ''))
|
||||
: [];
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,8 +195,9 @@ class UserOrders {
|
||||
$can_pay = $group === OrderCenterQuery::STATUS_PENDING;
|
||||
$can_cancel = $group === OrderCenterQuery::STATUS_PENDING;
|
||||
$order_number = (string) ($order['order_number'] ?? '');
|
||||
$after_sales = $detail && class_exists(AfterSaleService::class) ? AfterSaleService::get_instance()->list_for_order($order_number) : [];
|
||||
$has_open_after_sale = (bool) array_filter($after_sales, static fn(array $item): bool => in_array((string) ($item['status'] ?? ''), ['pending', 'processing', 'approved'], true));
|
||||
$after_sale_service = class_exists(AfterSaleService::class) ? AfterSaleService::get_instance() : null;
|
||||
$after_sales = $detail && $after_sale_service ? $after_sale_service->list_for_order($order_number) : [];
|
||||
$after_sale_state = $this->after_sale_state($group, $order_number, (string) ($order['paid_at'] ?? ''), $after_sale_service, $after_sales);
|
||||
$updated_at = $this->resolve_updated_at($order);
|
||||
|
||||
$view = [
|
||||
@@ -224,8 +225,8 @@ class UserOrders {
|
||||
'allowed_actions' => [
|
||||
'can_pay' => $can_pay,
|
||||
'can_cancel' => $can_cancel,
|
||||
'can_after_sale' => $group === OrderCenterQuery::STATUS_PAID && !$has_open_after_sale,
|
||||
],
|
||||
'after_sale' => $after_sale_state,
|
||||
'time_left' => $this->get_order_countdown((string) ($order['expires_at'] ?? ''), $status),
|
||||
'updated_at' => $updated_at,
|
||||
'status_version' => $this->resolve_status_version($order, $updated_at),
|
||||
@@ -245,6 +246,66 @@ class UserOrders {
|
||||
return $view;
|
||||
}
|
||||
|
||||
/**
|
||||
* 售后入口状态——前台「申请售后 / 售后详细」按钮与三个弹窗的唯一数据源。
|
||||
*
|
||||
* 列表模式只拉最新一条工单(latest_for_order);详情模式直接复用已取到的
|
||||
* 全量工单(list_for_order 按 id DESC,首条即最新)。
|
||||
*
|
||||
* @param array<int,array<string,mixed>> $records
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function after_sale_state(string $group, string $order_number, string $paid_at, ?AfterSaleService $service, array $records = []): array {
|
||||
$settings = class_exists('\AKSRT\Settings\PaymentRuntime')
|
||||
? \AKSRT\Settings\PaymentRuntime::get_after_sale_settings()
|
||||
: [];
|
||||
$settings = is_array($settings) ? $settings : [];
|
||||
|
||||
$open_statuses = ['pending', 'processing', 'approved'];
|
||||
$latest = null;
|
||||
$open = false;
|
||||
if ($service !== null) {
|
||||
if ($records) {
|
||||
foreach ($records as $record) {
|
||||
if ($latest === null) {
|
||||
$latest = $record;
|
||||
}
|
||||
if (in_array((string) ($record['status'] ?? ''), $open_statuses, true)) {
|
||||
$open = true;
|
||||
}
|
||||
}
|
||||
} elseif ($order_number !== '') {
|
||||
$latest = $service->latest_for_order($order_number);
|
||||
$open = $latest !== null && in_array((string) ($latest['status'] ?? ''), $open_statuses, true);
|
||||
}
|
||||
}
|
||||
|
||||
// 售后只对已支付订单开放;开关关闭时整体不处理售后。
|
||||
$enabled = $group === OrderCenterQuery::STATUS_PAID && !empty($settings['enabled']);
|
||||
$expired = $enabled && class_exists('\AKSRT\Settings\PaymentRuntime')
|
||||
&& \AKSRT\Settings\PaymentRuntime::after_sale_expired($paid_at);
|
||||
$has_application = $latest !== null;
|
||||
|
||||
return [
|
||||
'enabled' => $enabled,
|
||||
'expired' => (bool) $expired,
|
||||
'has_application' => $has_application,
|
||||
'open' => $open,
|
||||
'can_apply' => $enabled && !$expired && !$open,
|
||||
'can_reapply' => $enabled && !$expired && !$open && $has_application,
|
||||
'latest' => $has_application ? [
|
||||
'status' => (string) ($latest['status'] ?? ''),
|
||||
'reason' => (string) ($latest['reason'] ?? ''),
|
||||
'images' => array_values((array) ($latest['images'] ?? [])),
|
||||
'refund_amount' => (string) ($latest['refund_amount'] ?? '0'),
|
||||
'created_at' => (string) ($latest['created_at'] ?? ''),
|
||||
'reviewed_at' => (string) ($latest['reviewed_at'] ?? ''),
|
||||
'completed_at' => (string) ($latest['completed_at'] ?? ''),
|
||||
'admin_note' => (string) ($latest['admin_note'] ?? ''),
|
||||
] : null,
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function empty_view(OrderCenterQuery $query): array {
|
||||
return [
|
||||
|
||||
@@ -553,6 +553,7 @@ class PaymentRuntime {
|
||||
$wechat = 'null';
|
||||
}
|
||||
$alipay = sanitize_key((string) ($settings['pay_alipay_sdk_options'] ?? 'null'));
|
||||
|
||||
if (!in_array($alipay, ['official_alipay', 'epay', 'null'], true)) {
|
||||
$alipay = 'null';
|
||||
}
|
||||
@@ -583,6 +584,87 @@ class PaymentRuntime {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 售后策略(后台「支付&付费 → 售后策略」分区)的运行时读取。
|
||||
*
|
||||
* read_domain 走 SchemaProvider 的带默认读取:用户从未保存过该分区时,
|
||||
* 预置原因 / 所需材料 / 文案等键仍取 schema 默认值(语义对齐
|
||||
* UserRuntime::ban_settings 对禁封/举报分区的读取)。
|
||||
*
|
||||
* @return array{
|
||||
* enabled:bool,time_limit:int,expired_text:string,
|
||||
* reasons:array<int,array{slug:string,name:string}>,materials:string[],
|
||||
* desc:string,img_enabled:bool,reason_placeholder:string
|
||||
* }
|
||||
*/
|
||||
public static function get_after_sale_settings(): array {
|
||||
$s = class_exists('\AKSRT\Admin\SchemaProvider')
|
||||
? Settings::read_domain('payment', 'after_sale')
|
||||
: Settings::get_all();
|
||||
if (!is_array($s)) {
|
||||
$s = [];
|
||||
}
|
||||
|
||||
$reasons = [];
|
||||
foreach ((array) ($s['after_sale_reasons'] ?? []) as $row) {
|
||||
$text = trim((string) (is_array($row) ? ($row['t'] ?? '') : $row));
|
||||
if ($text === '') {
|
||||
continue;
|
||||
}
|
||||
$reasons[] = ['slug' => self::after_sale_slug($text), 'name' => sanitize_text_field($text)];
|
||||
}
|
||||
|
||||
$materials = [];
|
||||
foreach ((array) ($s['after_sale_materials'] ?? []) as $row) {
|
||||
$text = trim((string) (is_array($row) ? ($row['t'] ?? '') : $row));
|
||||
if ($text !== '') {
|
||||
$materials[] = sanitize_text_field($text);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'enabled' => !isset($s['after_sale_s']) || !empty($s['after_sale_s']),
|
||||
'time_limit' => max(0, min(3650, (int) ($s['after_sale_time_limit'] ?? 7))),
|
||||
'expired_text' => wp_kses_post((string) ($s['after_sale_expired_text'] ?? '')),
|
||||
'reasons' => $reasons,
|
||||
'materials' => $materials,
|
||||
'desc' => wp_kses_post((string) ($s['after_sale_desc'] ?? '')),
|
||||
'img_enabled' => !empty($s['after_sale_img_s']),
|
||||
'reason_placeholder' => sanitize_text_field((string) ($s['after_sale_reason_placeholder'] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单是否已超过售后申请时效(自支付时间起算;时效 <=0 或缺支付时间视为未过期)。
|
||||
*
|
||||
* 时区口径与 UserOrders::get_order_countdown 保持一致:均以站点本地时间
|
||||
* 比较存储的 DATETIME 与当前时间。
|
||||
*/
|
||||
public static function after_sale_expired(string $paid_at): bool {
|
||||
$limit = (int) (self::get_after_sale_settings()['time_limit'] ?? 0);
|
||||
if ($limit <= 0) {
|
||||
return false;
|
||||
}
|
||||
$paid_at = trim($paid_at);
|
||||
if ($paid_at === '') {
|
||||
return false;
|
||||
}
|
||||
$paid_timestamp = strtotime($paid_at);
|
||||
if ($paid_timestamp === false) {
|
||||
return false;
|
||||
}
|
||||
return current_time('timestamp') > $paid_timestamp + $limit * DAY_IN_SECONDS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 预置原因 slug:sanitize_key 无法处理中文标签(会被清洗成空串),
|
||||
* 与 UserRuntime::ban_settings 同策略——ASCII 标签取 key,其余取稳定短哈希。
|
||||
*/
|
||||
private static function after_sale_slug(string $text): string {
|
||||
$key = sanitize_key($text);
|
||||
return $key !== '' ? $key : substr(md5($text), 0, 12);
|
||||
}
|
||||
|
||||
private static function count_structured_lines(string $raw): int {
|
||||
$lines = preg_split('/[\r\n]+/', $raw, -1, PREG_SPLIT_NO_EMPTY);
|
||||
$count = 0;
|
||||
|
||||
Reference in New Issue
Block a user