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:
@@ -0,0 +1,338 @@
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* 「售后策略」功能契约:
|
||||
* 1) 后台 schema(PHP SchemaPayment after_sale 分区)与 SPA JS schema 的键、顺序、
|
||||
* 类型、默认值 1:1 对齐,且 schema 与侧边页均已注册(/payment/after-sale 可达);
|
||||
* 2) 运行时(PaymentRuntime)缺键回退 schema 默认、HTML 走 kses、时效边界正确;
|
||||
* 3) 售后工单:images 列、create 图片清洗与上限、上传路由权限、latest_for_order;
|
||||
* 4) 订单视图模型输出 after_sale 状态(按钮矩阵的唯一数据源);
|
||||
* 5) 前台:申请/超时效/售后详细三个独立弹窗、modal-mini + 蓝色炫彩头、placeholder 与
|
||||
* 图片上传配置化、不可再申请提示原文、订单详情弹窗不再内嵌售后表单(死代码清理)。
|
||||
*/
|
||||
|
||||
const assert = require('node:assert/strict')
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const { spawnSync } = require('node:child_process')
|
||||
const test = require('node:test')
|
||||
|
||||
const root = path.resolve(__dirname, '..', '..')
|
||||
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8')
|
||||
|
||||
const FIELD_ORDER = [
|
||||
'after_sale_s',
|
||||
'after_sale_time_limit',
|
||||
'after_sale_expired_text',
|
||||
'after_sale_reasons',
|
||||
'after_sale_desc',
|
||||
'after_sale_materials',
|
||||
'after_sale_reason_placeholder',
|
||||
'after_sale_img_s',
|
||||
]
|
||||
|
||||
/** Extract the balanced [...] block assigned to a PHP array key inside a section body. */
|
||||
function phpSection(src, sectionKey) {
|
||||
const start = src.indexOf(`'${sectionKey}' => [`)
|
||||
assert.notEqual(start, -1, `PHP schema has a ${sectionKey} section`)
|
||||
const open = src.indexOf('[', start)
|
||||
let depth = 0
|
||||
for (let i = open; i < src.length; i++) {
|
||||
const ch = src[i]
|
||||
if (ch === "'" || ch === '"') {
|
||||
const quote = ch
|
||||
i++
|
||||
while (i < src.length) {
|
||||
if (src[i] === '\\') { i += 2; continue }
|
||||
if (src[i] === quote) break
|
||||
i++
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (ch === '[') depth++
|
||||
else if (ch === ']') {
|
||||
depth--
|
||||
if (depth === 0) return src.slice(open, i + 1)
|
||||
}
|
||||
}
|
||||
throw new Error(`unbalanced PHP section: ${sectionKey}`)
|
||||
}
|
||||
|
||||
test('after-sale schema is 1:1 between PHP and SPA', () => {
|
||||
const php = phpSection(read('src/Admin/Schema/SchemaPayment.php'), 'after_sale')
|
||||
const js = read('assets/admin/src/schema/sections/payment/afterSale.js')
|
||||
|
||||
assert.match(php, /'key'\s*=>\s*'after_sale'/)
|
||||
assert.match(php, /'title'\s*=>\s*'售后策略'/)
|
||||
assert.match(php, /'option_key'\s*=>\s*'aksrt_settings'/)
|
||||
|
||||
// 字段键、顺序、类型一致(JS 侧 defaults 与 groups 同序,按首次出现去重)
|
||||
const phpKeys = [...php.matchAll(/'key'\s*=>\s*'([a-z0-9_]+)'/g)].map((m) => m[1])
|
||||
const jsKeys = [...js.matchAll(/key:\s*'([a-z0-9_]+)'/g)].map((m) => m[1])
|
||||
const sectionKeys = (keys) => [...new Set(keys.filter((key) => FIELD_ORDER.includes(key)))]
|
||||
assert.deepEqual(sectionKeys(phpKeys), FIELD_ORDER)
|
||||
assert.deepEqual(sectionKeys(jsKeys), FIELD_ORDER)
|
||||
|
||||
for (const [phpType, jsType] of [
|
||||
['switch', 'switch'],
|
||||
['number', 'number'],
|
||||
['textarea', 'textarea'],
|
||||
['group', 'group'],
|
||||
['input', 'input'],
|
||||
]) {
|
||||
assert.ok(php.includes(`'type' => '${phpType}'`), `missing PHP field type ${phpType}`)
|
||||
assert.ok(js.includes(`type: '${jsType}'`), `missing JS field type ${jsType}`)
|
||||
}
|
||||
|
||||
// 默认值一致(开关/时效/占位文案/预置原因/所需材料)
|
||||
assert.match(php, /'after_sale_s', 'label' => '售后功能', 'type' => 'switch', 'default' => true/)
|
||||
assert.match(js, /after_sale_s:\s*true/)
|
||||
assert.match(js, /key: 'after_sale_s', label: '售后功能', type: 'switch', default: true/)
|
||||
assert.match(php, /'after_sale_time_limit', 'label' => '申请售后时效\(天\)', 'type' => 'number', 'default' => 7, 'min' => 0, 'max' => 3650/)
|
||||
assert.match(js, /key: 'after_sale_time_limit', label: '申请售后时效\(天\)', type: 'number', default: 7, min: 0, max: 3650/)
|
||||
assert.match(php, /'after_sale_reason_placeholder', 'label' => '申请说明占位文案', 'type' => 'input', 'default' => '必填,请您详细填写申请原因'/)
|
||||
assert.match(js, /key: 'after_sale_reason_placeholder', label: '申请说明占位文案', type: 'input', default: '必填,请您详细填写申请原因'/)
|
||||
assert.match(php, /'after_sale_img_s', 'label' => '申请售后可上传图片', 'type' => 'switch', 'default' => true/)
|
||||
assert.match(js, /key: 'after_sale_img_s', label: '申请售后可上传图片', type: 'switch', default: true/)
|
||||
|
||||
for (const reason of ['质量问题', '假冒品牌', '其他原因']) {
|
||||
assert.ok(php.includes(`'t' => '${reason}'`), `missing PHP preset reason ${reason}`)
|
||||
assert.ok(js.includes(`{ t: '${reason}' }`), `missing JS preset reason ${reason}`)
|
||||
}
|
||||
for (const material of ['联系邮箱', '联系电话']) {
|
||||
assert.ok(php.includes(`'t' => '${material}'`), `missing PHP material ${material}`)
|
||||
assert.ok(js.includes(`{ t: '${material}' }`), `missing JS material ${material}`)
|
||||
}
|
||||
|
||||
// HTML 配置项(超时效提示 / 申请说明)走 kses 通道
|
||||
assert.match(php, /'after_sale_expired_text', 'label' => '超过时效提示', 'type' => 'textarea', 'sanitize_type' => 'html'/)
|
||||
assert.match(php, /'after_sale_desc', 'label' => '申请售后说明', 'type' => 'textarea', 'sanitize_type' => 'html'/)
|
||||
assert.match(js, /key: 'after_sale_expired_text', label: '超过时效提示', type: 'textarea', sanitize_type: 'html'/)
|
||||
assert.match(js, /key: 'after_sale_desc', label: '申请售后说明', type: 'textarea', sanitize_type: 'html'/)
|
||||
})
|
||||
|
||||
test('after-sale schema and sidebar page are registered under 支付&付费', () => {
|
||||
const schemaIndex = read('assets/admin/src/schema/sections/payment/index.js')
|
||||
assert.match(schemaIndex, /import \{ afterSaleSchema \} from '\.\/afterSale\.js'/)
|
||||
assert.match(schemaIndex, /after_sale:\s*afterSaleSchema/)
|
||||
|
||||
const section = read('assets/admin/src/constants/sections/payment.js')
|
||||
assert.match(section, /key: 'payment-after-sale'/)
|
||||
assert.match(section, /name: 'PaymentAfterSale'/)
|
||||
assert.match(section, /label: '售后策略'/)
|
||||
assert.match(section, /route: '\/payment\/after-sale'/)
|
||||
assert.match(section, /canonicalDomain: 'payment'/)
|
||||
assert.match(section, /section: 'after_sale'/)
|
||||
|
||||
// 侧栏条目位于提现策略之后(支付&付费末位)
|
||||
assert.ok(section.indexOf("key: 'payment-after-sale'") > section.indexOf("key: 'payment-withdrawal'"))
|
||||
|
||||
const schemaRegistry = read('assets/admin/src/schema/index.js')
|
||||
assert.match(schemaRegistry, /paymentSchemas/)
|
||||
})
|
||||
|
||||
test('after-sale runtime resolves defaults, overrides and the expiry boundary matrix', () => {
|
||||
const result = spawnSync(process.env.PHP_BINARY || 'php', [path.join(__dirname, 'after-sale-policy-harness.php')], {
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
})
|
||||
assert.equal(result.status, 0, result.stderr || result.stdout)
|
||||
const payload = JSON.parse(result.stdout)
|
||||
|
||||
assert.deepEqual(payload.defaults, {
|
||||
enabled: true,
|
||||
time_limit: 7,
|
||||
expired_text: '<div>抱歉,您的订单已超过售后申请时效</div>',
|
||||
reasons: [
|
||||
{ slug: 'c440cdf71825', name: '质量问题' },
|
||||
{ slug: '2ead35a456a0', name: '假冒品牌' },
|
||||
{ slug: 'fba26eab6b2b', name: '其他原因' },
|
||||
],
|
||||
materials: ['联系邮箱', '联系电话'],
|
||||
desc: '',
|
||||
img_enabled: true,
|
||||
reason_placeholder: '必填,请您详细填写申请原因',
|
||||
})
|
||||
|
||||
assert.equal(payload.overridden.enabled, false)
|
||||
assert.equal(payload.overridden.time_limit, 30)
|
||||
assert.equal(payload.overridden.img_enabled, false)
|
||||
assert.equal(payload.overridden.reason_placeholder, '请描述问题')
|
||||
assert.deepEqual(payload.overridden.reasons, [
|
||||
{ slug: 'c440cdf71825', name: '质量问题' },
|
||||
{ slug: '327e0ab68567', name: '发错货' },
|
||||
])
|
||||
assert.deepEqual(payload.overridden.materials, ['联系邮箱'])
|
||||
// HTML 配置项剥离 script,保留 div
|
||||
assert.equal(payload.overridden.expired_text, '<div>已超过时效</div>')
|
||||
assert.equal(payload.overridden.desc, '<div>请保持商品完好</div>')
|
||||
|
||||
assert.deepEqual(payload.expired_ok, {
|
||||
within: true,
|
||||
exact_boundary: true,
|
||||
just_expired: true,
|
||||
long_expired: true,
|
||||
no_limit: true,
|
||||
missing_paid: true,
|
||||
})
|
||||
})
|
||||
|
||||
test('after-sale tickets persist evidence images and expose the latest row', () => {
|
||||
const schema = read('src/Core/DBMigratorPart2.php')
|
||||
assert.match(schema, /CREATE TABLE \{\$prefix\}aksrt_order_aftersale[\s\S]*?images LONGTEXT DEFAULT NULL[\s\S]*?refund_amount/)
|
||||
|
||||
const service = read('src/Payment/AfterSaleService.php')
|
||||
assert.match(service, /public function create\(int \$user_id, string \$order_number, string \$reason, array \$images = \[\]\)/)
|
||||
assert.match(service, /\$images = \$this->sanitize_images\(\$images\);/)
|
||||
assert.match(service, /'images' => wp_json_encode\(\$images\)/)
|
||||
assert.match(service, /private function sanitize_images\(array \$images\): array \{[\s\S]*?count\(\$clean\) >= 3[\s\S]*?esc_url_raw/)
|
||||
assert.match(service, /public function latest_for_order\(string \$order_number\): \?array \{[\s\S]*?ORDER BY id DESC LIMIT 1/)
|
||||
// normalize 输出 images 数组(坏 JSON / 空串回落空数组)
|
||||
assert.match(service, /json_decode\(\(string\) \(\$row\['images'\] \?\? ''\), true\)/)
|
||||
|
||||
// 图片上传路由:登录 + nonce + 售后/图片开关 + 订单归属;回调委托评论图片上传
|
||||
assert.match(service, /\/orders\/' \. \$number \. '\/after-sale\/upload-image'/)
|
||||
assert.match(service, /public function require_after_sale_image\(\\WP_REST_Request \$request\)/)
|
||||
assert.match(service, /get_after_sale_settings\(\)[\s\S]*?empty\(\$settings\['enabled'\]\)[\s\S]*?empty\(\$settings\['img_enabled'\]\)/)
|
||||
assert.match(service, /get_user_order\(get_current_user_id\(\), \$number\)/)
|
||||
assert.match(service, /UserController::get_instance\(\)->rest_upload_comment_image\(\$request\)/)
|
||||
assert.match(service, /'images' => \['required' => false, 'type' => 'array'[\s\S]*?count\(\$value\) <= 3/)
|
||||
|
||||
// 提交 REST 透传 images
|
||||
assert.match(service, /is_array\(\$images\) \? \$images : \[\]\)/)
|
||||
|
||||
// 既有售后资产契约(锁 / 退款 / 完成)不受影响
|
||||
assert.match(service, /GET_LOCK\(%s, 5\)/)
|
||||
assert.match(service, /Order::STATUS_PAID/)
|
||||
})
|
||||
|
||||
test('order view model exposes the after-sale button matrix', () => {
|
||||
const userOrders = read('src/Payment/UserOrders.php')
|
||||
assert.match(userOrders, /private function after_sale_state\(string \$group, string \$order_number, string \$paid_at, \?AfterSaleService \$service, array \$records = \[\]\): array \{/)
|
||||
|
||||
assert.match(userOrders, /'after_sale' => \$after_sale_state,/)
|
||||
assert.match(userOrders, /'enabled'\s*=> \$enabled,/)
|
||||
assert.match(userOrders, /'expired'\s*=> \(bool\) \$expired,/)
|
||||
assert.match(userOrders, /'has_application' => \$has_application,/)
|
||||
assert.match(userOrders, /'open'\s*=> \$open,/)
|
||||
assert.match(userOrders, /'can_apply'\s*=> \$enabled && !\$expired && !\$open,/)
|
||||
assert.match(userOrders, /'can_reapply'\s*=> \$enabled && !\$expired && !\$open && \$has_application,/)
|
||||
assert.match(userOrders, /'latest'\s*=> \$has_application \? \[/)
|
||||
|
||||
// 仅已支付订单开放;开关关闭整体不处理售后
|
||||
assert.match(userOrders, /\$enabled = \$group === OrderCenterQuery::STATUS_PAID && !empty\(\$settings\['enabled'\]\);/)
|
||||
// 时效以支付时间为准
|
||||
assert.match(userOrders, /\$this->after_sale_state\(\$group, \$order_number, \(string\) \(\$order\['paid_at'\] \?\? ''\), \$after_sale_service, \$after_sales\)/)
|
||||
// 列表模式单行查询,详情模式复用全量工单
|
||||
assert.match(userOrders, /\$after_sales = \$detail && \$after_sale_service \? \$after_sale_service->list_for_order\(\$order_number\) : \[\];/)
|
||||
assert.match(userOrders, /\$latest = \$service->latest_for_order\(\$order_number\);/)
|
||||
assert.match(userOrders, /\$view\['after_sales'\] = \$after_sales;/)
|
||||
|
||||
// 旧 can_after_sale 双路已移除
|
||||
assert.ok(!userOrders.includes('can_after_sale'), 'can_after_sale must be replaced by the after_sale state')
|
||||
})
|
||||
|
||||
test('frontend after-sale modals are standalone and config-driven', () => {
|
||||
const js = read('assets/js/order-after-sale.js')
|
||||
|
||||
// 三个独立弹窗 + modal-mini 几何 + 蓝色炫彩标题区
|
||||
assert.match(js, /className: 'aksrt-after-sale-apply-modal'/)
|
||||
assert.match(js, /className: 'aksrt-after-sale-detail-modal'/)
|
||||
assert.match(js, /className: 'aksrt-after-sale-expired-modal'/)
|
||||
assert.match(js, /modal-colorful-header colorful-bg jb-blue/)
|
||||
assert.match(js, /AksrtModalRuntime\.open\(\{/)
|
||||
|
||||
// 申请表单:预置原因徽章、所需资料、必填申请说明(placeholder 取后台配置)
|
||||
assert.match(js, /reasonOptions\(\)/)
|
||||
assert.match(js, /materialOptions\(\)/)
|
||||
assert.match(js, /data-aksrt-after-sale-reason=/)
|
||||
assert.match(js, /data-aksrt-after-sale-material=/)
|
||||
assert.match(js, /var placeholder = String\(cfg\.reasonPlaceholder \|\| '必填,请您详细填写申请原因'\)/)
|
||||
assert.match(js, /name="reason"/)
|
||||
assert.match(js, /notyf\('请填写申请说明', 'danger'\)/)
|
||||
assert.match(js, /notyf\('请选择售后原因', 'danger'\)/)
|
||||
|
||||
// 图片上传:开关控制渲染,最多 3 张,上传/删除/提交
|
||||
assert.match(js, /if \(cfg\.imgEnabled\) \{/)
|
||||
assert.match(js, /MAX_IMAGES = 3/)
|
||||
assert.match(js, /\/after-sale\/upload-image'/)
|
||||
assert.match(js, /data-aksrt-after-sale-image-remove/)
|
||||
assert.match(js, /images: collectImages\(shell\)/)
|
||||
|
||||
// 申请说明(HTML)+ 超时效提示(HTML)由后台配置直出
|
||||
assert.match(js, /var desc = String\(cfg\.desc \|\| ''\)/)
|
||||
assert.match(js, /var text = String\(settings\(\)\.expiredText \|\| '抱歉,您的订单已超过售后申请时效'\)/)
|
||||
|
||||
// 售后详细:状态徽章 / 明细行 / 图片缩略图;不可再申请提示原文 + 我知道了
|
||||
assert.match(js, /STATUS_LABELS = \{[\s\S]*?pending: '待审核'[\s\S]*?completed: '已完成',/)
|
||||
assert.match(js, /REAPPLY_BLOCKED_TEXT = '该无法再次申请售后,该订单已售后,暂不支持再次申请售后。'/)
|
||||
assert.match(js, /escapeHtml\(REAPPLY_BLOCKED_TEXT\)/)
|
||||
assert.match(js, /data-aksrt-modal-close>我知道了</)
|
||||
assert.match(js, /data-aksrt-after-sale-reapply=/)
|
||||
|
||||
// 提交后关闭弹窗并刷新订单列表
|
||||
assert.match(js, /AKSRTOrderCenter\.refresh\(orderNumber\)/)
|
||||
assert.match(js, /window\.AksrtModalRuntime\.close\('submit'\)/)
|
||||
|
||||
// 入口委托:申请 / 售后详细
|
||||
assert.match(js, /data-aksrt-after-sale-apply/)
|
||||
assert.match(js, /data-aksrt-after-sale-detail/)
|
||||
assert.match(js, /auto_fun\.aksrt-after-sale/)
|
||||
})
|
||||
|
||||
test('order center drops the embedded after-sale form and exposes refresh()', () => {
|
||||
const js = read('assets/js/order-center.js')
|
||||
|
||||
// 内嵌售后表单与两个 submit 处理器已彻底移除(死代码清理)
|
||||
assert.ok(!js.includes('data-aksrt-after-sale-form'), 'embedded after-sale form must be removed')
|
||||
assert.ok(!js.includes('提交售后申请'), 'embedded after-sale submit copy must be removed')
|
||||
assert.ok(!js.includes('onDocumentSubmit'), 'document submit handler for the after-sale form must be removed')
|
||||
|
||||
// 列表卡片与订单详情弹窗底部共用新的售后按钮
|
||||
assert.match(js, /function afterSaleButtons\(orderNumber, afterSale\)/)
|
||||
assert.match(js, /if \(afterSale\.enabled\) html \+= afterSaleButtons\(order\.order_number, afterSale\);/)
|
||||
assert.match(js, /html \+= afterSaleButtons\(order\.order_number, order\.after_sale \|\| \{\}\);/)
|
||||
assert.match(js, /data-aksrt-after-sale-detail="/)
|
||||
assert.match(js, /data-aksrt-after-sale-apply="/)
|
||||
|
||||
// 售后记录区与取消申请保留在订单详情弹窗
|
||||
assert.match(js, /售后记录/)
|
||||
assert.match(js, /data-aksrt-after-sale-cancel/)
|
||||
|
||||
// refresh 供售后弹窗提交后刷新列表与统计
|
||||
assert.match(js, /function refresh\(orderNumber\) \{/)
|
||||
assert.match(js, /window\.AKSRTOrderCenter = \{ init: init, destroy: destroy, refresh: refresh \}/)
|
||||
assert.match(js, /liveStates\.forEach\(function \(state\) \{/)
|
||||
})
|
||||
|
||||
test('after-sale assets, template buttons and runtime data are wired', () => {
|
||||
const scripts = read('src/Core/ScriptsPart1b.php')
|
||||
assert.match(scripts, /'aksrt-order-after-sale'/)
|
||||
assert.match(scripts, /\/assets\/js\/order-after-sale\.js'/)
|
||||
|
||||
const scripts1c = read('src/Core/ScriptsPart1c.php')
|
||||
assert.match(scripts1c, /'afterSale'\s*=> \(function \(\) \{/)
|
||||
assert.match(scripts1c, /PaymentRuntime::get_after_sale_settings\(\)/)
|
||||
assert.match(scripts1c, /'maxImages'\s*=> 3/)
|
||||
|
||||
const template = read('inc/helpers/user-center-renderers/profile-orders.php')
|
||||
assert.match(template, /\$after_sale = is_array\(\$order\['after_sale'\] \?\? null\) \? \$order\['after_sale'\] : \[\];/)
|
||||
assert.match(template, /data-aksrt-after-sale-detail="/)
|
||||
assert.match(template, /data-aksrt-after-sale-apply="/)
|
||||
assert.match(template, /esc_html_e\('售后详细', 'aksrt'\)/)
|
||||
assert.ok(!template.includes('can_after_sale'), 'template must not use the removed can_after_sale flag')
|
||||
|
||||
const css = read('assets/css/frontend/modal-runtime.css')
|
||||
assert.match(css, /\.aksrt-after-sale-apply-modal:not\(\.aksrt-modal-runtime--bottom\) \.aksrt-modal-runtime__dialog/)
|
||||
assert.match(css, /\.aksrt-after-sale-detail-modal:not\(\.aksrt-modal-runtime--bottom\) \.aksrt-modal-runtime__dialog/)
|
||||
assert.match(css, /\.aksrt-after-sale-expired-modal:not\(\.aksrt-modal-runtime--bottom\) \.aksrt-modal-runtime__dialog/)
|
||||
assert.match(css, /width: min\(400px, calc\(100vw - 32px\)\)/)
|
||||
assert.match(css, /\.aksrt-after-sale-apply-modal \.modal-colorful-header/)
|
||||
assert.match(css, /\.aksrt-after-sale-apply-modal \.colorful-bg > \.colorful-make/)
|
||||
|
||||
const field = read('assets/admin/src/components/form/AksField.vue')
|
||||
assert.match(field, /import draggable from 'vuedraggable'/)
|
||||
assert.match(field, /v-model="repeaterDragList"/)
|
||||
assert.match(field, /handle="\.aksrt-field__repeater-handle"/)
|
||||
assert.match(field, /function repeaterDragKey\(item\)/)
|
||||
})
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
/**
|
||||
* After-sale policy runtime harness.
|
||||
*
|
||||
* Covers the 「支付&付费 → 售后策略」 runtime resolution: schema defaults when the
|
||||
* section was never saved, stored overrides, HTML sanitisation, preset-reason
|
||||
* slugs, and the after_sale_expired boundary matrix.
|
||||
*/
|
||||
|
||||
namespace {
|
||||
define('ABSPATH', __DIR__);
|
||||
define('DAY_IN_SECONDS', 86400);
|
||||
|
||||
$GLOBALS['aksrt_test_options'] = [];
|
||||
$GLOBALS['aksrt_test_now'] = 0;
|
||||
|
||||
function sanitize_key($value): string {
|
||||
return preg_replace('/[^a-z0-9_\-]/', '', strtolower((string) $value)) ?? '';
|
||||
}
|
||||
|
||||
function sanitize_text_field($value): string {
|
||||
return trim(preg_replace('/[\r\n\t]+/', ' ', strip_tags((string) $value)) ?? '');
|
||||
}
|
||||
|
||||
function sanitize_textarea_field($value): string {
|
||||
return trim(strip_tags((string) $value));
|
||||
}
|
||||
|
||||
function wp_kses_post($value): string {
|
||||
$value = preg_replace('#<script\b[^>]*>.*?</script>#is', '', (string) $value);
|
||||
return strip_tags($value, '<a><b><strong><em><i><span><div><p><br><ul><ol><li>');
|
||||
}
|
||||
|
||||
function esc_url_raw($value): string {
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
function wp_json_encode($value): string {
|
||||
return (string) json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
function get_option(string $name, $default = false) {
|
||||
return $GLOBALS['aksrt_test_options'][$name] ?? $default;
|
||||
}
|
||||
|
||||
function update_option(string $name, $value): bool {
|
||||
$GLOBALS['aksrt_test_options'][$name] = $value;
|
||||
return true;
|
||||
}
|
||||
|
||||
function current_time(string $type, $gmt = false) {
|
||||
return $GLOBALS['aksrt_test_now'];
|
||||
}
|
||||
}
|
||||
|
||||
namespace AKSRT\Settings {
|
||||
class Settings {
|
||||
public static function get_all(): array {
|
||||
$stored = get_option('aksrt_settings', []);
|
||||
return is_array($stored) ? $stored : [];
|
||||
}
|
||||
|
||||
public static function read_domain(string $domain, string $section = ''): array {
|
||||
return \AKSRT\Admin\SchemaProvider::get_instance()->read_settings($domain, $section);
|
||||
}
|
||||
|
||||
public static function flush_settings_cache(): void {}
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
require_once dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaPayment.php';
|
||||
require_once dirname(__DIR__, 2) . '/src/Admin/SchemaProvider.php';
|
||||
require_once dirname(__DIR__, 2) . '/src/Settings/PaymentRuntime.php';
|
||||
|
||||
$GLOBALS['aksrt_test_now'] = strtotime('2026-03-01 12:00:00');
|
||||
|
||||
// 1) Section never saved: the runtime still resolves schema defaults.
|
||||
$defaults = \AKSRT\Settings\PaymentRuntime::get_after_sale_settings();
|
||||
|
||||
// 2) Stored values override defaults; HTML fields are kses'ed.
|
||||
update_option('aksrt_settings', [
|
||||
'after_sale_s' => false,
|
||||
'after_sale_time_limit' => 30,
|
||||
'after_sale_expired_text' => '<div>已超过时效</div><script>alert(1)</script>',
|
||||
'after_sale_reasons' => [['t' => '质量问题'], ['t' => '发错货']],
|
||||
'after_sale_desc' => '<div>请保持商品完好</div><script>alert(2)</script>',
|
||||
'after_sale_materials' => [['t' => '联系邮箱']],
|
||||
'after_sale_reason_placeholder' => '请描述问题',
|
||||
'after_sale_img_s' => false,
|
||||
]);
|
||||
$overridden = \AKSRT\Settings\PaymentRuntime::get_after_sale_settings();
|
||||
|
||||
// 3) Expiry boundary matrix: measured from paid_at, strictly beyond the limit.
|
||||
$matrix = [];
|
||||
$boundary_cases = [
|
||||
'within' => ['2026-02-25 00:00:00', 7, false],
|
||||
'exact_boundary' => ['2026-02-22 12:00:00', 7, false],
|
||||
'just_expired' => ['2026-02-22 11:59:59', 7, true],
|
||||
'long_expired' => ['2026-01-01 00:00:00', 7, true],
|
||||
'no_limit' => ['2020-01-01 00:00:00', 0, false],
|
||||
'missing_paid' => ['', 7, false],
|
||||
];
|
||||
$stored = get_option('aksrt_settings', []);
|
||||
foreach ($boundary_cases as $name => $case) {
|
||||
[$paid_at, $limit, $expected] = $case;
|
||||
$stored['after_sale_time_limit'] = $limit;
|
||||
update_option('aksrt_settings', $stored);
|
||||
$matrix[$name] = \AKSRT\Settings\PaymentRuntime::after_sale_expired($paid_at) === $expected;
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'defaults' => $defaults,
|
||||
'overridden' => $overridden,
|
||||
'expired_ok' => $matrix,
|
||||
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
Reference in New Issue
Block a user