feat(order): 重构退款全流程并新增申请退款中状态

- 订单状态新增「6 申请退款中」,用户申请退款原子流转并落待审核退款流水
- 退款失败回退订单至已支付并恢复被取消的权益发放任务
- 新增虚拟支付退款查单兜底任务,反查超时订单并自动订正
- 会员包来源额度不叠加,扣减/回退/回收口径统一为只取生效一份
- 退款回收流水按吊销前后有效剩余差值记账,避免无效回收记录
- 同步更新 README 与 Stitch 设计文档
This commit is contained in:
2026-09-16 03:44:09 +08:00
parent 89b59c7b91
commit b94dd6dffb
29 changed files with 2141 additions and 158 deletions
+13 -8
View File
@@ -337,7 +337,10 @@ stateDiagram-v2
pending --> expired : 超时 15 分钟(后台每分钟扫描)
pending --> cancelled : 用户主动取消(同步关闭微信订单)
expired --> paid : 过期后付款到账(补发货)
cancelled --> refunded : 已付款的取消订单自动发起退款
cancelled --> refunded : 已付款的取消订单生成待审核退款单(管理员手动退款)
paid --> refunding : 用户申请退款(同事务写待审核退款单)
refunding --> refunded : 退款成功回调(回收权益 → 通知)
refunding --> paid : 退款失败/被驳回(流水置失败,可重新申请)
paid --> refunded : 退款成功回调(回收权益 → 通知)
state "1 待支付" as pending
@@ -345,6 +348,7 @@ stateDiagram-v2
state "3 已取消" as cancelled
state "4 已过期" as expired
state "5 已退款" as refunded
state "6 申请退款中" as refunding
```
处理流程:
@@ -352,11 +356,11 @@ stateDiagram-v2
1. **商品**:5 类商品(`1` 会员包 / `2` 上传包 / `3` AI 推荐包 / `4` 衣柜分享包 / `5` 搭配分享包),价格单位为**分**;仅 `1 上架` 状态可购买。
2. **下单**:实际支付金额取 `discount_price`;下单时把商品权益快照(会员天数/各类额度)冗余进订单,防止商品后续变更导致错发;先落本地待支付订单再调微信 JSAPI,微信下单失败时订单保留并靠超时自动过期。同一用户可并存多个待支付订单(无防重限制)。
3. **支付回调**:验签 + 解密 → 仅处理 `SUCCESS` → 幂等锁 30s → **金额校验**(回调金额与订单不一致则拒绝流转、人工介入)→ 待支付 → 已支付 → 发放权益 + 写支付信息 + 发订单通知。竞态兜底:已过期但已付款的订单补发货;已取消但已付款的订单生成**待审核退款单**,由管理员手动退款。
4. **权益发放**:会员天数实际发放(叠加规则 `vip_expire = max(vip_expire, now) + 天数×86400`,续费顺延,会员类型按**续费后最终到期时长**推导——剩余 ≥365 天为年度会员、否则月度会员,避免年度会员续月包被降级);Cos / AI / 分享额度改为写入**额度凭证** `quota_grants`——会员包 `expire_time=0`(跟随 VIP)、加量包按 `goods.valid_days` 独立到期,幂等由 `(ref_id, quota_type)` 唯一索引保证;补偿任务 `order_no + benefit_type` 唯一保证不重发。
4. **权益发放**:会员天数实际发放(叠加规则 `vip_expire = max(vip_expire, now) + 天数×86400`,续费顺延,会员类型按**续费后最终到期时长**推导——剩余 ≥365 天为年度会员、否则月度会员,避免年度会员续月包被降级);Cos / AI / 分享额度改为写入**额度凭证** `quota_grants`——会员包 `expire_time=0`(跟随 VIP)、加量包按 `goods.valid_days` 独立到期,幂等由 `(ref_id, quota_type)` 唯一索引保证;补偿任务 `order_no + benefit_type` 唯一保证不重发。**会员包来源(source=1)额度不叠加**:月度包/年度包同时持有时同类额度只取 amount 最大的一份(用尽后自动顺延下一份),避免额度翻倍。
5. **超时过期**:后台任务**每分钟**扫描 `待支付且 expire_time < now` 的订单批量置为已过期(条件更新天然幂等);过期后无权益可回收。
6. **退款**:**所有退款均需管理员操作**——用户申请退款仅登记待审核退款单(`0 待处理`),运营审核后在微信后台手动退款,退款回调更新状态(`0 待处理 → 1 退款中 → 2 成功 / 3 失败`);「已取消但已付款」的订单同样生成待审核退款单而非自动退款。退款成功回调将订单置已退款、**回收权益**(会员天数不回退到过去;赠送额度通过失效凭证回收未用部分,已用不追回;已取消订单权益从未发放,跳过回收)并发通知。
6. **退款**:**所有退款均需管理员操作**——用户申请退款受理成功后将订单置为 `6 申请退款中` 并登记待审核退款单(流水 `0 待处理`,同一事务 + 订单行锁,重复/并发申请仅受理一次),运营审核后在微信后台手动退款,退款回调更新流水状态(`0 待处理 → 2 成功 / 3 失败`);退款成功 → 订单 `6 → 5`,退款失败 → 订单回退 `2 已支付`(可重新申请)。**虚拟支付退款推送(`xpay_refund_notify`)按官方字段解析**:订单号取 `MchOrderId`、结果取 `RetCode`(0=成功,失败原因见 `RetMsg`),关键字段缺失或结果无法判定时**应答失败**触发微信重试(最多 15 次)而非静默成功;成功后仍反查 `query_order` 确认真实退款与金额一致。**查单兜底任务**每 60 秒反查停留超 10 分钟的「申请退款中」订单:微信侧已退款自动置 `5` 并回收权益,仍未退款则保持状态、超 24 小时告警人工。「已取消但已付款」的订单同样生成待审核退款单而非自动退款,订单保持 `3 已取消`。退款成功回调将订单置已退款、**回收权益**(会员天数不回退到过去;赠送额度通过失效凭证回收未用部分,已用不追回;已取消订单权益从未发放,跳过回收)并发通知。
7. **发放失败补偿**:权益发放失败自动落 pending 任务,后台任务**每分钟**扫描重试(Redis 锁保证单实例),每轮最多 100 个,重试 3 次仍失败则标记失败 + 告警人工介入;重试前先查发放流水幂等。
8. **额度变更统一收口**:Cos 永久额度扣减/回退封装在事务内「凭证扣减(先到期先用)+ 免费额度扣减 + 写流水」强一致;每日额度(AI/分享/衣柜分享)由 Redis 原子限流计数(当天 0 点自然过期),MySQL `user_daily_usages` 兜底统计。
8. **额度变更统一收口**:Cos 永久额度扣减/回退封装在事务内「凭证扣减(非会员包先到期先用 → 会员包生效份)+ 免费额度扣减 + 写流水」强一致(会员包来源不叠加,未生效份不参与扣减/回退);每日额度(AI/分享/衣柜分享)由 Redis 原子限流计数(当天 0 点自然过期),MySQL `user_daily_usages` 兜底统计。
### 6. AI 推荐
@@ -641,7 +645,7 @@ flowchart TD
| goods_name | string | 商品名称 |
| unit_price | int64 | 单价(分) |
| pay_amount | int64 | 实付金额(分) |
| order_status | int | 1=待支付 2=已支付 3=已取消 4=已过期 5=已退款 |
| order_status | int | 1=待支付 2=已支付 3=已取消 4=已过期 5=已退款 6=申请退款中 |
| member_days | int64 | 会员天数 |
| expire_time | int64 | 支付截止时间戳(秒) |
| pay_time | int64 | 支付时间戳(秒) |
@@ -1152,7 +1156,7 @@ multipart 上传,字段名 `file`;仅支持图片(jpeg/png/gif/webp),
#### 10. 订单模块
订单状态枚举:`order_status` 1=待支付 2=已支付 3=已取消 4=已过期 5=已退款;退款状态 `status` 0=待处理 1=退款中 2=退款成功 3=退款失败。
订单状态枚举:`order_status` 1=待支付 2=已支付 3=已取消 4=已过期 5=已退款 6=申请退款中;退款状态 `status` 0=待处理 1=退款中 2=退款成功 3=退款失败。
**POST `/order` —— 创建订单并发起微信支付(需鉴权)**
@@ -1180,7 +1184,7 @@ multipart 上传,字段名 `file`;仅支持图片(jpeg/png/gif/webp),
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| page / page_size | int | 否 | 分页 |
| status | int | 否 | 1=待支付 2=已支付 3=已取消 4=已过期 5=已退款;不传 = 全部 |
| status | int | 否 | 1=待支付 2=已支付 3=已取消 4=已过期 5=已退款 6=申请退款中;不传 = 全部 |
响应 `data.list` 元素 = 订单对象(见公共结构)。
@@ -1430,7 +1434,7 @@ sequenceDiagram
2. 前端用响应中的 `pay_params` 调 `wx.requestPayment`;`expire_time` 用于支付倒计时(15 分钟)。
3. 支付结果以微信回调为准(公开路由,服务端验签 + 解密);前端支付完成后应通过 `GET /order` 确认 `order_status=2`,不要自行判定支付成功。
4. 超时未支付订单由后台任务每分钟扫描并自动置为已过期(4)。
5. 退款:`POST /order/refund` 申请 → 微信异步处理 → 退款回调更新退款状态;可通过订单列表查看 `refund_time` / `refund_amount`。
5. 退款:`POST /order/refund` 申请成功后订单置 `6 申请退款中`(人工审核,非实时到账)→ 运营在微信后台手动退款 → 退款回调将订单置 `5 已退款`(失败则回退 `2 已支付`,可重新申请);虚拟支付退款推送字段为 `MchOrderId`/`MchRefundId`/`RetCode`(0=成功),推送丢失时由**查单兜底任务**自动订正;可通过订单列表查看 `refund_time` / `refund_amount`。
#### 流程 5:点赞 / 收藏切换
@@ -1600,6 +1604,7 @@ curl -X POST http://localhost:80/api/v1/ai-recommend \
| 每日运势生成 | 每日 0 点 + 启动兜底 | 大模型生成十二星座运势;Redis 分布式锁保证多实例仅执行一次;当日不足 12 条时按需兜底生成 |
| 订单超时过期 | 每 60 秒 | 将超过 15 分钟未支付的订单批量置为"已过期" |
| 权益发放补偿 | 每 60 秒 | 重试发放失败的权益任务(恢复僵死任务、幂等防重复发放、超限告警) |
| 虚拟支付退款查单兜底 | 每 60 秒 | 反查停留超 10 分钟的「申请退款中」虚拟支付订单:已退款自动置「已退款」并回收权益,仍未退款保持状态、超 24 小时告警人工 |
## 部署提示