跳转到主要内容

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 元」
金额 >100000400「单笔金额过大」
settings 值非数字—Go 回退默认 vs 冻结 NaN→JSON null(PRD ④)
bind 传字符串数值400echo 默认(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「缺少二维码图片」
超 8MB500「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 不存在窗口200addTolerant 吞 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)节
1GET /recharge/config(:159)F8.01
2GET /recharge/orders(:160)F8.01
3POST /recharge/orders(:161)F8.01
4POST /recharge/orders/:id/proof(:162,+up8)F8.02
5POST /admin/recharge-orders/:id/approve(:163,amw+adminMw)F8.03
6POST /admin/recharge-orders/:id/reject(:164,amw+adminMw)F8.03
7GET /admin/recharge-orders(:165,amw+adminMw)F8.03
8POST /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 判定在外层」——纯谓词重排真值表同构无副作用差异,不立目。