FSD M8 · 充值
← 总则 | 附录 → | 上游:PRD §4 M8 实现落点:
backend/internal/domain/recharge/(model 118/repo 209/service 381)+cmd/server/recharge_handlers.go(161 行 8 handler)+cmd/server/routes.go:152-166(挂载,up8 := BodyLimit("8mb"):158 proof/pay-qrcode 共用)。事实基准=Go 代码;冻结版qmtv/server/recharge.js(8 条 :28-:120,admin 4 条 router 内写死 /admin/ 前缀)仅差异对拍参照。 域内要点:M8 是 M7 的返佣写入方(commission_records 唯一写入方在 approve 链——M7 零域的另一面);404 文案三态分叉(「订单不存在或已处理」vs proof 面「订单不存在」vs 400「已处理不可重复提交凭证」——model.go:21-32 三哨兵);approve 零事务补偿序七步(MarkPaid→到账→两级返佣→资格联动→通知→SSE——禁事务铁律顺合冻结补偿式一致性);reject 双截断不同源(落库 200/通知 100);orderNo=‘RCH’+毫秒+3 位随机非 uid 勿用 ids 包;recharge_orders.credits刻意不 DECIMAL(0004 七列外——到账恒整数);SSE recharge 仅发本人(M12 无 SSE 的对照面)。bind 形态:裸传 return err(与 m12「请求体格式错误」两域不同制)。PRD §4 M8 已登记差异 ①~⑧(prd-qmtv-v1.md:188)引用不重立。
M8 充值
8 端点全量(用户面 4 条 amw + admin 4 条 amw+adminMw 双挂)。
F8.01 充值配置与下单(config/orders GET+POST)
对应 PRD:§4 M8 · GET /api/recharge/config、GET、POST /api/recharge/orders | 状态:实现 | 实现落点:recharge_handlers.go:40-99、service.go:65-165、repo.go:92-97 | UI:RechargeView.vue(config staleTime 5min+凭证上传 mutate)| 场景挂钩:E2E-RCH-01(待实现,二十七期候选);RechargeChain 第十三链集成锚(33 元单 0.66 佣金)
1 功能定义
汇率/收款码/推广阈值三键配置面+本人订单列表+创建充值订单(金额两位舍入换算积分)。
2 触发条件与前置状态
config/orders 无前置;下单前置=舍入后金额 ∈ [1,100000](双 400)。
3 输入与校验
POST body amount_cny float64,bind 裸传 return err;校验序(service.go:151-158=冻结 :37-39):①amount = Round(amountCny×100)/100(正数域 half-away 与 Math.round 同形)②amount==0 || amount<1 → 400「充值金额至少 1 元」③amount>100000 → 400「单笔金额过大」。
4 处理流程
config:GetSetting(pay_qrcode) 空串/缺行→null + creditRate(默认 10)+ affiliateThreshold(默认 980)组装。orders GET:单 SELECT 9 列 NULLIF(proof_path,''),ORDER BY created_at DESC LIMIT 50。POST:舍入双 400 → orderNo = "RCH"+UnixMilli+3 位随机(非 uid 勿用 ids 包,service.go:149 注)→ credits = int64(Round(amount × creditRate())) → ro_ id → INSERT 5 列,status/created_at 走 DDL DEFAULT。
5 输出与结果状态
config 三键序 credit_per_cny,qrcode,affiliate_threshold(qrcode 空串→null);orders 行键序 id,order_no,credits,amount_cny,status,proof_path,note,created_at,reviewed_at(无 user_id;proof_path NULLIF 还原 null、reviewed_at NULL→null);POST 四键序 id,order_no,credits,amount_cny(amount_cny=舍入后两位小数值)。LIMIT 分化:用户面 50/admin 面 200。
6 状态流转
INSERT 建 pending 订单(proof/approve/reject 流转 F8.02/F8.03)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 金额 <1 元 | 400 | 「充值金额至少 1 元」 |
| 金额 >100000 | 400 | 「单笔金额过大」 |
| settings 值非数字 | — | Go 回退默认 vs 冻结 NaN→JSON null(PRD ④) |
| bind 传字符串数值 | 400 | echo 默认(DIF-31-4 扩域:冻结 +amount_cny 强转放行) |
8 权限与数据规则
orders 严格本人 user_id=?;config 全登录用户同值;threshold 980 与 M7 硬编码 980 两处刻意不同源勿统一(PRD ⑤=M7 ⑤);recharge_orders.credits BIGINT 恒整数(0004 刻意不动——到账恒整数)。
F8.02 凭证上传与收款码(proof/pay-qrcode)
对应 PRD:§4 M8 · POST /api/recharge/orders/:id/proof、POST /api/admin/pay-qrcode | 状态:实现 | 实现落点:recharge_handlers.go:18-38/:140-161、service.go:170-230+:364-372、repo.go:100-121 | UI:RechargeView.vue(凭证上传 mutate)/AdminView.vue(收款码设置)| 场景挂钩:RechargeChain(真落盘断言 :114-117)
1 功能定义
订单转账凭证图片上传(属主)+admin 收款二维码设置(落 system_settings KV)——共用同一 upload 函数+8MB 双保险。
2 触发条件与前置状态
proof 前置=订单存在且属主且 status=‘pending’(校验顺序 404→400 非 pending→400 无文件,service.go:170-183=冻结 :50→:51→:52);pay-qrcode 无前置。
3 输入与校验
multipart 单文件字段 file;8MB 双保险:路由级 up8+handler 单文件 8<<20,超限 500「File too large」 模拟 multer LIMIT_FILE_SIZE(handlers:23-26,M9 ④/M12 同制先例)。
4 处理流程
proof:GetOrderOfUser(id=? AND user_id=?)→ 非 pending 400 → 无文件 400 → upload(ObjectKey up_+extOf 默认 .png + store.Put + PathOf /files/+key)→ UPDATE recharge_orders SET proof_path=? 单列(不改 status)。pay-qrcode:无文件 400 → 同一 upload → SetSetting upsert(key 保留字反引号)。
5 输出与结果状态
proof {"proof":"<path>"};pay-qrcode {"qrcode":"<path>"};落库/响应形态 /files/...。
6 状态流转
recharge_orders.proof_path 单列覆写(status 不动);system_settings pay_qrcode upsert 不触订单。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 订单不存在/非本人 | 404 | 「订单不存在」(三态分叉之一——与 approve/reject 的「订单不存在或已处理」不同源) |
| 非 pending 再传凭证 | 400 | 「订单已处理,不可重复提交凭证」(approve 后亦然) |
| 无文件(proof) | 400 | 「缺少凭证图片」 |
| 无文件(pay-qrcode) | 400 | 「缺少二维码图片」 |
| 超 8MB | 500 | 「File too large」 |
8 权限与数据规则
proof=amw+属主;pay-qrcode=amw+adminMw;proof_path Go DDL NOT NULL DEFAULT ’’ vs 冻结可空——三处读侧 NULLIF 还原 null 透传(PRD ⑦)。
F8.03 审核与两级返佣(approve/reject/admin list)
对应 PRD:§4 M8 · POST /api/admin/recharge-orders/:id/approve、POST …/:id/reject、GET /api/admin/recharge-orders | 状态:实现 | 实现落点:recharge_handlers.go:100-139、service.go:234-361、repo.go:125-146+:151-162+:168-200 | UI:AdminView.vue(充值 tab approveRecharge——invalidate 缺口保留不刷 users·summary 照抄)| 场景挂钩:RechargeChain(approve 全链/幂等 404/33 元 0.66 佣金/资格联动四锚)
1 功能定义
admin 审核链——approve 到账+两级返佣+资格联动+通知+SSE(全站返佣唯一写入方);reject 驳回+通知;admin 全量订单列表。approveOrder 冻结导出「供重启复用」系死代码零消费方——Go 不建恢复链(PRD ⑥)。
2 触发条件与前置状态
approve/reject 共享 pending 谓词 WHERE id=? AND status='pending';非 pending(含二跑幂等)→ 404「订单不存在或已处理」(ErrOrderProcessed 两路同源共享哨兵——与 proof 面「订单不存在」不同源,三态分叉之二/一)。
3 输入与校验
approve 无 body;reject body note string(缺省→""),bind 裸传 return err;admin list query status。adminNickname:JWT 零查库经 auth.Me 取行,空 → 兜底 'admin'(=冻结 admin.nickname||'admin')。
4 处理流程(approve 零事务补偿序七步,service.go:236-298=冻结 :59-92 逐条直译)
①MarkPaid:status='paid', reviewed_by=?, reviewed_at=?(reviewed_at=newUTCLocalTime UTC,PRD ②)→ ②到账 addTolerant「付费充值 %s(%s 元)」(仅吞 ErrNoUser 对齐冻结 addCredits 静默,其余 DB 错误照传播——PRD ③)→ ③GetBuyer(affiliate_enabled 单列)→ ④两级返佣:GetReferrer → rel1 → pay(一级) → GetReferrer(rel1) → rel2 → pay(二级);pay:commission=Round(amount×rate×100)/100、<=0 跳过、INSERT commission_records 7 列 status='settled'(0004 后 order_amount/commission 原值入库——差异①终结)、addTolerant 入账「分销收益(一级|二级):下级充值 X 元,佣金 Y」+通知「分销收益到账」(from_user_id 恒=充值人非链上级,PRD ⑧)→ ⑤资格联动:!buyer.AffiliateEnabled && AmountCny>=threshold → EnableAffiliate + 通知「推广资格已开通」(已开通不重复)→ ⑥通知「充值到账」订单 %s 审核通过,%d 积分已到账。 → ⑦SSE hub.Emit(o.UserID,"recharge",{order_no,credits}) 仅发本人。reject(service.go:340-361):pending 404 同源 → notifyNote 缺省「凭证有误」→ MarkRejected 4 值(落库 note=truncateRune(note,200) 空串原样)→ 通知「充值未通过」订单 %s 未通过审核:%s(truncateRune(notifyNote,100)——双截断不同源)→ ok(true)。admin list:status!="" && status!="全部" 双 SQL 分支照抄勿合并(带 WHERE vs 不过滤,等价性勿优化)。
5 输出与结果状态
approve {"order_no","credits"};reject true;admin list 行键序 13 键 id,order_no,user_id,credits,amount_cny,status,proof_path,note,reviewed_by,reviewed_at,created_at,email,nickname(AdminOrderRow 11+2 尾插=冻结 r.* SQLite 列序;version 私货列不入投影)。LIMIT 200。
6 状态流转
pending →(approve)→ paid / →(reject)→ rejected;proof 不改 status;二级状态无出边(GetPendingOrder 谓词使 approve/reject 二跑恒 404——幂等性缺失)。写序=①~⑦ 顺序(MarkPaid 先行——到账/返佣失败时已 paid 的补偿式语义=冻结同构)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 非 pending(approve/reject,含二跑) | 404 | 「订单不存在或已处理」(三态分叉之二) |
| 非 pending(proof 面) | 400 | 「订单已处理,不可重复提交凭证」 |
| buyer 不存在窗口 | 200 | addTolerant 吞 ErrNoUser(PRD ③——对齐冻结静默) |
| 佣金计算 ≤0 | — | 静默跳过该级返佣(=冻结 :70) |
8 权限与数据规则
三端点均 amw+adminMw;adminNickname 空兜底 ‘admin’;reviewed_at UTC 刻意与 auth LocalTime 不同源(PRD ②,M7 ⑦ 同制);33 元单集成锚:二级佣金 0.66 原值入库+余额 base+2.66 DECIMAL 往返(integration_recharge_test.go:162-190)。
册尾端点覆盖对账(8/8)
对账 backend/cmd/server/routes.go:159-166 亲数 8 条 ↔ 本册 F8.01~F8.03。口径脚注:冻结 recharge.js 8 条(:28/:32/:36/:48 用户 4+:95/:99/:108/:115 admin 4——admin 靠 router 内写死 /admin/ 前缀,挂载点 index.js:27 app.use('/api', recharge.router));Go 拍平进同一 authed 组、admin 4 条挂 adminMw(routes.go:154-155 注)——冻结-Go 计数自洽。
| # | 端点(routes.go:line) | 节 |
|---|---|---|
| 1 | GET /recharge/config(:159) | F8.01 |
| 2 | GET /recharge/orders(:160) | F8.01 |
| 3 | POST /recharge/orders(:161) | F8.01 |
| 4 | POST /recharge/orders/:id/proof(:162,+up8) | F8.02 |
| 5 | POST /admin/recharge-orders/:id/approve(:163,amw+adminMw) | F8.03 |
| 6 | POST /admin/recharge-orders/:id/reject(:164,amw+adminMw) | F8.03 |
| 7 | GET /admin/recharge-orders(:165,amw+adminMw) | F8.03 |
| 8 | POST /admin/pay-qrcode(:166,amw+adminMw+up8) | F8.02 |
差异纪律:PRD §4 M8 ①~⑧ 全引用不重立(含 DECIMAL 收口①/UTC②/ErrNoUser 容错③/settings NaN④/threshold 双源⑤/死代码⑥/proof_path NULLIF⑦/from_user_id ⑧);本册零独立新立 DIF——下单 bind 强转(冻结 +amount_cny 宽收数值字符串 vs Go bind 400)并入 DIF-31-4 扩域(M7 ⑥ credits 面同族、m12 F12.02/04 同型)。注记:资格联动谓词排序冻结「金额判定在前」vs Go「buyer 判定在外层」——纯谓词重排真值表同构无副作用差异,不立目。