feat(admin): 订单明细页已支付订单新增「退款」按钮

操作栏「详情」右侧为已支付订单渲染「退款」按钮:点击经 aksrt_pay_refund_order 把订单推到已退款。

退款沿既有的受闸路径执行:OrderRepository::update_status 要求 allow_refund,且 can_finalize_order_refund 要求该售后工单仍为待审核/处理中。因此工单号由服务端按订单号反查 find_active 得到,客户端只提交订单号;没有进行中的工单时整次操作中止并给出可操作提示,不会出现订单已退款但无工单依据的状态。

按钮仅在已支付订单上渲染——已退款是已支付的唯一后继状态,其他状态点击也只会在服务端被拒。附带清理 AfterSaleService::find_active 上方一处早已错配的 @param 文档注释(描述的是 \,与该函数签名无关)。
This commit is contained in:
2026-10-08 21:14:56 +08:00
parent 5fd8c6fc31
commit 83c164a427
4 changed files with 188 additions and 9 deletions
@@ -194,6 +194,29 @@ test('order detail page drops tabs; after-sales becomes a standalone submenu', (
assert.match(page, /ORDER_PRODUCT_READING[\s\S]*?付费阅读[\s\S]*?ORDER_PRODUCT_DOWNLOAD[\s\S]*?付费下载[\s\S]*?ORDER_PRODUCT_IMAGE[\s\S]*?付费图片[\s\S]*?ORDER_PRODUCT_VIDEO[\s\S]*?付费视频[\s\S]*?ORDER_PRODUCT_VIP[\s\S]*?购买会员[\s\S]*?ORDER_PRODUCT_RECHARGE[\s\S]*?余额充值[\s\S]*?ORDER_PRODUCT_POINTS[\s\S]*?购买积分/)
})
test('paid orders expose a refund button next to the detail link', () => {
const page = read('src/Admin/Native/PayOrders.php')
const service = read('src/Payment/AfterSaleService.php')
// 「退款」紧跟在「详情」右侧,仅已支付订单可见——已退款是已支付的唯一后继状态
assert.match(page, /详情', 'aksrt'\) \. '<\/a>';\s*\n\s*\/\/ 已支付是已退款的唯一前置状态[\s\S]*?aksrt_pay_refund_order/)
assert.ok(page.includes("if (\$status_key === \\AKSRT\\Core\\DomainModel::ORDER_STATUS_PAID) {"),
'refund button only renders for paid orders')
assert.ok(page.includes('aksrt_pay_refund_order_'), 'per-order nonce covers the refund')
assert.ok(page.includes("add_action('admin_post_aksrt_pay_refund_order', [\$this, 'handle_refund_order'])"),
'refund action registered')
assert.ok(page.includes('handle_refund_order'), 'refund handler exists')
// 工单号由服务端按订单号反查,客户端只能传订单号
assert.ok(page.includes("AfterSaleService::get_instance()->find_active(\$order_number)"),
'active after-sale application is resolved server-side')
assert.ok(!page.includes('after_sale_id\' => absint'), 'after-sale id is never taken from the request')
assert.ok(page.includes("'allow_refund' => true"), 'refund opts into the guarded transition')
// 各种失败都要给出可操作的提示,而不是静默失败
for (const code of ['order_refunded', 'order_refund_failed', 'order_refund_missing', 'order_refund_not_paid', 'order_refund_no_ticket']) {
assert.ok(page.includes(`'${code}'`), `flash code ${code} is mapped`)
}
assert.match(service, /public function find_active\(string \$number\)/, 'active application lookup is exposed')
})
test('order creation retries only on order-number unique key collisions', () => {
const repository = read('src/Payment/OrderRepositoryCore.php')
assert.match(repository, /private const ORDER_NUMBER_MAX_ATTEMPTS = \d+;/)