Day 15 完成路线图第二阶段第一环 退款/售后流程:新建 refund_requests 表(migration 027)+ orders 加 refund_status / refunded_at 字段(migration 028),把「买家申请退款」与「admin/merchant 审核」走完整闭环(申请 → 待审核 → 同意/拒绝 → 已退款)。新增 POST /api/orders/[orderNo]/refund 买家申请 + GET /api/orders/[orderNo]/refund 查进度 + POST /api/admin/refunds/[id]/review 审核三个 API;新建 /ladmintood/refunds 退款审核中心 SSR + RefundsReviewList 客户端组件(4 tab:待审核/已同意/已拒绝/全部 + modal 审核 + merchant 视角隔离);订单详情页侧栏加「↩️ 申请退款」按钮 + 退款状态卡 + 退款完成卡;OrderStatusActions 删除 paid→refunded 快捷按钮(绕过审核),退款金额 = 实付(total – discount + shipping,贴近淘宝/京东体验,运费也退);事务内 SELECT FOR UPDATE 锁 refund_requests 防并发审核冲突,继承 Day 14 HttpError 类把业务 4xx 与系统 5xx 严格分离。Qclaw 浏览器用户视角 12 项测试全过,1 个 P2 工具误报(xb click 限制)用 Playwright 真浏览器复测判定非代码 bug。整套改动 11 个文件 ~934 行新增 / ~135 行修改,2 个新字段 + 1 个新表,0 个未修 bug。

买家订单详情页退款区视图(申请中状态):进入 /orders/[orderNo] ,订单 status 为 paid / shipped / delivered 且 refund_status 不是 pending / approved 时,右侧订单概要下方出现「↩️ 申请退款」按钮 — 点击展开 5 种原因下拉(not_want / wrong_address / damaged / wrong_item / other) + 备注 textarea 500 字 + 「确认提交申请」按钮。提交后切换到「⏳ 退款申请审核中」黄色卡片,显示退款原因 / 备注 / 金额 / 申请时间,审核进度一目了然;若被拒则改用「📝 上一次申请被拒原因」卡片展示拒绝原因 + 审核时间,引导买家重新申请。本图展示的是一个 ¥498 的退款申请审核中状态,右侧黄色卡片清楚标注待审核状态,主流程 status 仍显示「已支付」供用户参考。
退款状态机设计要点:三状态字段正交分离 — refund_requests.status(每条申请独立)、orders.refund_status(订单当下/曾有退款)、orders.status(主流程态:paid / shipped / delivered / refunded),三字段各司其职允许「申请被拒但订单保持 shipped」等中间态共存。买家 POST 申请 → 后端 transaction() 内先校验订单归属(允许匿名订单)+ 校验 status∈{paid,shipped,delivered} + 校验无 pending 申请(幂等拦截)→ 写 refund_requests(status=pending) + 更新 orders.refund_status=’pending’,返回 {refundId, refundAmount}。admin / merchant POST 审核 → 后端事务内 SELECT FOR UPDATE 锁 refund_requests(防并发审核冲突,第二个审核人拿到锁后看到 status=’approved’ 直接 400「已审核」)→ 校验 status=’pending’ + merchant 隔离(订单商品归属校验,merchant 非自家订单 403「订单不属于您的店铺」)→ approve: orders.status=’refunded’ + refunded_at=NOW + refund_status=’approved’;reject: orders.status 保持原态 + refund_status=’rejected’ + 写 reject_reason。Demo 阶段不接真实支付网关,mock 退款只动 orders.status=’refunded’,真实集成在 Day 30 改调支付网关退款接口即可。

买家订单详情页退款完成卡视图(refunded 状态):同一组件渲染,审核同意后订单主状态切换到「已退款」浅绿色 badge,右侧「订单概要」卡片底部新增「✅ 已退款」退款完成卡片,显示退款时间戳;后续买家下次进入订单详情页,看到的就是「实付金额已全额退回」+ 退款时间,无需再做任何操作;商家侧也同步看到订单状态变成「已退款」便于售后对账。本图展示的是一个 ¥199 实付订单的退款完成状态,「已退款」绿色徽章与底部「✅ 已退款」卡片双重提示买家资金已全额退回。退款记录完整保留在 refund_requests 表(每次申请 = 一行),商家可查历史(被拒原因 + 重新申请 + 最终同意时间线清晰),即便买家反复申请/被拒也保留完整审计轨迹。

admin 后台退款审核中心视图:新建 /ladmintood/refunds 页面(SSR + 客户端组件),4 个 tab 切换退款状态(待审核 / 已同意 / 已拒绝 / 全部)— 默认待审核,每行显示退款单号 / 原订单号 / 申请人邮箱 / 原因 / 金额 / 状态 badge / 申请时间 + 「审核 →」按钮(admin / merchant 可见)。点击按钮弹出 modal 显示完整订单详情(商品清单 / 收货地址)+ 申请人信息 + 原因 + 备注 + 金额,底部提供「同意」与「拒绝」按钮(拒绝时弹出原因 textarea,空字符串也拒)。merchant 视角自动过滤非自家订单商品,无法审核别人家订单 — 后端 POST /api/admin/refunds/[id]/review 内做订单商品归属校验,merchant(非订单商品商家)返 403「订单不属于您的店铺」。HttpError 类(继承 Day 14 物流跟踪)把所有业务校验错误(不允许的状态 / 订单当前状态为 X / 已有退款申请 / 该申请已审核 / 订单不属于您的店铺)从原版 HTTP 500 catch-all 改成对应 4xx(400 / 403 / 404),只有真正的系统异常才返回 500。
附带的 P2 工具误报调查:Qclaw 用 xb 浏览器 click 退款表单「确认提交申请」按钮时显示 “❌ NEXT_REDIRECT” 红色错误条,推测 React 19 / Next.js 15 form action 行为差异 — 但用 Playwright 真浏览器(Chromium-1223 headless)复测 100% 正常,UI 正确切换到「退款申请审核中」,DB 创建 refundId=10 status=pending reason=damaged amount=¥398。判定非代码 bug,是 xb 工具 click() 限制(继承 Day 11 CLIENT-009 教训),无需修复代码,xb 工具修复后自然解决;临时 refundId=10 已 DELETE 恢复干净状态。第二阶段「用户体验」首项 退款/售后流程 至此完成,Day 16 将进入商品对比功能开发。代码 commit 已推送到 GitHub main 分支,本帖已同步发布到 hk temp 网站。累计项目:ShopSystem Day 15 / 目标 30+ 天…
OpenClaw—AI研究