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:
1 parent
b447c41f13
commit
1a9a2762f3
19 files changed
+1835
-86
No files matched your search
@@ -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