FSD M7 · 分销提现
← 总则 | 附录 → | 上游:PRD §4 M7 实现落点:
backend/internal/domain/distribution/(model 181/service 233/repo 214)+cmd/server/distribution_handlers.go(184 行 11 handler)+cmd/server/routes.go:137-151(挂载)。事实基准=Go 代码;冻结版qmtv/server/index.js:598-638(用户面 7)+:708-737(状态机 4)两段不连续(中间隔 admin 段 :640-:703)仅差异对拍参照。 域内要点:零域铁律——withdrawals/exchanges 全域不触 users.credits/credit_records/通知/SSE(「返佣写入方在 M8 approve 链」model.go:4-8,唯一写入方 M8 F8.03);available 分母无 status 过滤(cancelled/rejected 永不释放额度——:715 note「收益已解锁」冻结谎言照抄);settings 回退双 shape 不可合并(overview 三键 vs 提现面一键);affiliate_threshold 980 硬编码与 M8 不同源勿统一;W_FLOW 严格邻接状态机(终态无键恒 400);admin PUT 不限属主 vs cancel/confirm 属主强制。bind 形态:POST/PUT 三处裸传 return err(echo 默认 400 无统一信封——与 m12「请求体格式错误」两域不同制)。PRD §4 M7 已登记差异 ①~⑩(prd-qmtv-v1.md:180-184)引用不重立。
M7 分销提现
11 端点全量(用户面 9 条 amw + admin 2 条 amw+adminMw 双挂——物理在 /admin/ 前缀但归本域,M12 裁决⑩)。
F7.01 分销总览(overview)
对应 PRD:§4 M7 · GET /api/distribution/overview | 状态:实现 | 实现落点:distribution_handlers.go:28-37、service.go:34-70、repo.go:57-98 | UI:DistributionView.vue(Promise.all 五请求聚合单 query
['dist-all']保「任一失败单 toast」语义)| 场景挂钩:无专项 e2e(E2E-WD-01 待实现);DistributionChain 第七链集成锚
1 功能定义
下级数/累计佣金/在途提现/分销设置/推广码/开通标志/阈值——四查询七键总览。
2 触发条件与前置状态
登录态(amw)+DB 就绪;无行级前置。
3 输入与校验
无 body/query。
4 处理流程
①CountDownlines(referral_relations JOIN users 语义保留勿简化单表 COUNT——孤儿 invitee 面 COUNT 与冻结 all().length 逐位一致,repo.go:57-58)②SumCommissions(COALESCE 0)③PendingWithdrawSum(冻结双 IN 冗余 → Go 等价单 IN 三态 ('pending','reviewing','topay')——paid 被 AND 掉,repo.go:72-78 结果逐位一致)④GetDistSettings(ErrNoRows → SettingsFallback{0.2,0.05,100} 冻结 :602 字面量)⑤GetUserDist(users 两列窄投影 referral_code,affiliate_enabled)。
5 输出与结果状态
OverviewDoc 七键序 downline_count,total_commission,pending_withdraw,settings,referral_code,affiliate_enabled,affiliate_threshold(model.go:160-168)。settings 双 shape:有行=SettingsRow 四键 id,commission_rate,level2_rate,min_withdraw;无行=SettingsFallback 三键 {commission_rate:0.2,level2_rate:0.05,min_withdraw:100} 无 id 键——两 shape 不可合并。referral_code/affiliate_enabled 在 UserDist 为 json:"-" 经 OverviewDoc 再出。
6 状态流转
只读零写。
7 边界与异常
settings 表错误非 ErrNoRows → 500;其余仅 DB 错误透传。零业务 4xx 面。
8 权限与数据规则
数据严格限本人(inviter_id=?/user_id=?);affiliate_threshold 恒 980 硬编码(model.go:158-159/service.go:70)——与 M8 recharge 读 affiliate_threshold_cny 刻意不同源勿统一(PRD M7 ⑤=M8 ⑤ 双登记)。
F7.02 下线与佣金流水(downlines/records)
对应 PRD:§4 M7 · GET /api/distribution/downlines、GET /api/distribution/records | 状态:实现 | 实现落点:distribution_handlers.go:39-60、service.go:73-82、repo.go:103-117 | UI:DistributionView.vue(聚合 query 内)| 场景挂钩:DistributionChain(seed 对拍 40 佣金断言)
1 功能定义
下线列表(含次级下线数 sub_count)+佣金流水(含来源人昵称 from_name)。
2 触发条件与前置状态
登录态;commission_records 空表时 records 为 [](零域 quirk——唯一写入方 M8 approve 链,repo.go:6 注)。
3 输入与校验
无 body/query。
4 处理流程
downlines 单 SELECT(id,nickname,email,created_at+sub_count 相关子查询,无 ORDER BY 无 LIMIT=冻结 :606 同形);records 单 SELECT(c.* 显式 8 列+u.nickname AS from_name 尾插,ORDER BY created_at DESC **LIMIT 100**)。
5 输出与结果状态
downlines 行键 id,nickname,email,created_at,sub_count;records 行键 id,user_id,from_user_id,order_amount,commission,level,status,created_at,from_name(order_amount/commission float64——0004 DECIMAL 后 int64 对小数行恒 scan error→500,二十二期 A1 收口)。LIMIT 分化:域内仅 records 有 LIMIT 100。
6 状态流转
无写。
7 边界与异常
零行业务异常;空列表 JSON [](gorp nil→MakeSlice 与冻结 .all() 逐位一致)。
8 权限与数据规则
本人维度;from_name JOIN 可能因 from_user 无行而少行(INNER JOIN 同构冻结 :609)。
F7.03 提现列表与申请(withdrawals GET/POST)
对应 PRD:§4 M7 · GET、POST /api/distribution/withdrawals | 状态:实现 | 实现落点:distribution_handlers.go:62-93、service.go:85-123+:155-167、repo.go:121-125+:144-166 | UI:DistributionView.vue(wd 提现表单)| 场景挂钩:E2E-WD-01(待实现——主干「余额划转」措辞与差异①相悖,引用时勘正为「佣金台账划转不动 users.credits」);DistributionChain(⑨⑩ 双断言)
1 功能定义
本人提现工单列表+按可提现额度(Σ佣金−Σ提现)申请提现(佣金台账面非余额)。
2 触发条件与前置状态
POST 前置=金额 ≥ min_withdraw 且 ≤ available。
3 输入与校验
POST body credits float64,bind 裸传 return err(handlers:85-87——echo 默认 400 无统一信封);校验序:①credits==0 || credits<min_withdraw → 400 动态文案(!credits 语义=0)②credits>available → 400「可提现积分不足」。
4 处理流程
GET 单 SELECT 7 列 ORDER BY created_at DESC 无 LIMIT。POST:①读 settings 取 min_withdraw——回退仅 {min_withdraw:100} 一键(与 overview 回退三键不同 shape 不共用,PRD ④)②算 available=Σcommission−Σwithdrawals(分母 SUM 无 status 过滤——cancelled/rejected 永不释放额度,:715 note「收益已解锁」冻结谎言照抄,PRD ②)③w_ id → INSERT 三列 (id,user_id,credits),status/created_at 走 DDL DEFAULT(‘pending’)。
5 输出与结果状态
WithdrawalRow 行键序 id,user_id,credits,status,created_at,note,reviewed_at(note/reviewed_at 可空→null 透传);POST {"id":"w_..."}。
6 状态流转
INSERT 即建 pending 态工单(后续流转 F7.05/F7.06)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 金额 0 或 < min_withdraw | 400 | 最低提现 %d 积分(动态 min_withdraw 插值=冻结 :619 模板串) |
| 超出可提现额度 | 400 | 「可提现积分不足」 |
8 权限与数据规则
credits 绑定 float64=冻结 +req.body.credits 强转(PRD ⑥——冻结接受 JSON 数字串 Go bind 拒之);零域:不触 users.credits/credit_records/通知/SSE。
F7.04 兑换(exchanges GET/POST)
对应 PRD:§4 M7 · GET、POST /api/distribution/exchanges | 状态:实现 | 实现落点:distribution_handlers.go:95-124、service.go:128-148、repo.go:128-132+:152-172 | UI:DistributionView.vue(兑换表单)| 场景挂钩:DistributionChain(死态断言)
1 功能定义
佣金积分兑换列表+申请兑换(佣金台账面,min=10 常量不读 settings)。
2 触发条件与前置状态
POST 前置=金额 ≥10 且 ≤(Σ佣金−Σ兑换)。
3 输入与校验
POST body credits float64,bind 裸传 return err;校验序:①credits==0 || credits<10 → 400 ②credits>available → 400。
4 处理流程
GET 单 SELECT 5 列无 LIMIT。POST:Σcommissions → ΣexchangesAll(同构无 status 过滤)→ ex_ id → INSERT 三列,status 走 DDL DEFAULT ‘pending’——全冻结无任何 UPDATE 死态照抄(PRD ⑨)。
5 输出与结果状态
ExchangeRow 行键 id,user_id,credits,status,created_at;POST {"id":"ex_..."}。
6 状态流转
exchanges.status 恒 ‘pending’(死态——集成断言钉死)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 金额 0 或 <10 | 400 | 「最低兑换 10 积分」 |
| 超出可兑换额度 | 400 | 「可兑换积分不足」 |
8 权限与数据规则
amw 本人维度;兑换不扣 withdrawals 面——available 独立再减 Σexchanges(service.go:130-131 注=冻结 :630-631)。
F7.05 撤回与确认到账(cancel/confirm)
对应 PRD:§4 M7 · POST /api/distribution/withdrawals/:id/cancel、POST …/:id/confirm | 状态:实现 | 实现落点:distribution_handlers.go:126-145、service.go:181-201、repo.go:177-207 | UI:DistributionView.vue(工单操作)| 场景挂钩:DistributionChain
1 功能定义
属主自撤(仅 pending/reviewing)+打款后属主确认(仅 paid)——状态机用户面两臂。
2 触发条件与前置状态
cancel 前置 status ∈ {pending,reviewing};confirm 前置 status=‘paid’;均先过属主 404 关(WHERE id=? AND user_id=?)。
3 输入与校验
无 body;路径 :id。
4 处理流程
cancel:属主查行 → 状态谓词 → UPDATE ... SET status='cancelled', note='用户撤回(收益已解锁)'——不写 reviewed_at、不回补余额(released 纯展示,repo.go:196-197 注)→ 响应。confirm:属主查行 → status!=="paid" 谓词 → UPDATE ... SET status='done', note='用户确认到账'(同样不写 reviewed_at)→ ok(true)。
5 输出与结果状态
cancel {"cancelled":true,"released":<float>};confirm data:true。
6 状态流转
pending/reviewing →(cancel)→ cancelled;paid →(confirm)→ done。cancelled/done 均终态(W_FLOW 无键恒 400)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 工单不存在/非本人 | 404 | 「工单不存在」(cancel/confirm/admin PUT 三处共享同文案同哨兵) |
| 非 pending/reviewing 撤回 | 400 | 「当前状态不可撤回(已进入打款流程)」(全角括号照抄) |
| 非 paid 确认 | 400 | 「仅在「已打款」状态下可确认到账」(直角引号照抄) |
8 权限与数据规则
amw+属主强制(与 admin PUT 不限属主形成对照);cancel 不写 reviewed_at;released 纯展示不回补额度(「收益已解锁」谎言见 F7.03)。
F7.06 管理端审核状态机(admin withdrawals PUT/GET)
对应 PRD:§4 M7 · PUT、GET /api/admin/withdrawals(/:id) | 状态:实现 | 实现落点:distribution_handlers.go:147-171、service.go:204-233、model.go:44-71、repo.go:135-140+:186-194+:209-214 | UI:AdminView.vue(提现 tab 消费本域端点——wdSet/wdReject 合并 mutation)| 场景挂钩:无专项 e2e(E2E-WD-01 待实现);DistributionChain(逐态迁移+终态 400+bogus 态三断言)
1 功能定义
admin 严格邻接流转提现工单+全量工单列表(归 M7 域系 M12 裁决⑩——物理在 /admin/ 前缀但域逻辑/册归属均本域)。
2 触发条件与前置状态
amw+adminMw 双挂(全组唯二,其余 9 条仅 amw);PUT 前置=工单存在(不限属主 WHERE id=?)且当前态在 W_FLOW 有键且 next ∈ 邻接表。
3 输入与校验
PUT body {"status":string,"note":string|null}(Note *string;note 空串→nil COALESCE 保留旧值=冻结 :732 req.body.note || null);bind 裸传 return err。GET 无参数(无 status 过滤——与 m08 admin list 双分支不同制)。
4 处理流程
PUT:①WithdrawalByID 不限属主 ②W_FLOW[w.Status] miss→nil 等价空表(=冻结 ||[])③线性扫邻接不合 → StatusError{400, 不允许从「X」流转到「Y」}(zhOr miss 原样回显英文态=冻结 W_ZH[s]||s)④UPDATE ... SET status=?, note=COALESCE(?,note), reviewed_at=?——reviewed_at 每次合法流转覆写,值=newUTCLocalTime() UTC 墙钟(与 auth LocalTime 本地时刻意不同源,PRD ⑦)⑤响应。GET:w.* 显式 7 列+email 尾插,ORDER BY created_at DESC 无 LIMIT。
5 输出与结果状态
PUT {"status":next,"zh":"<W_ZH[next]>"};GET 行键序 id,user_id,credits,status,created_at,note,reviewed_at,email。
6 状态流转
W_FLOW 邻接表全表(model.go:47-52=冻结 index.js:708 字面量直译勿优化):
| 当前态 | 可流转 next | 说明 |
|---|---|---|
| pending | reviewing, rejected | 待审核臂 |
| reviewing | topay, rejected | 审核中臂 |
| topay | paid, rejected | 待打款臂 |
| paid | done | 打款完成臂 |
| done / rejected / cancelled | (无键→nil→恒 400) | 三终态禁出 |
W_ZH:pending=待审核/reviewing=审核中/topay=待打款/paid=已打款/done=已完成/rejected=已驳回/cancelled=已撤回。完整迁移链 pending→reviewing→topay→paid→done,任意臂可旁路 rejected,用户臂 cancelled。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 工单不存在 | 404 | 「工单不存在」(三处共享哨兵) |
| 非邻接流转/终态流转/bogus 态 | 400 | 不允许从「%s」流转到「%s」(严格邻接禁跳转——W_ZH miss 原样回显英文态,PRD ⑧) |
| 非 admin | 403 | “forbidden”(adminMw) |
8 权限与数据规则
quirk:PUT 不限属主 vs cancel/confirm 属主(F7.05 对照);note COALESCE 缺省保留;reviewed_at UTC 每次合法流转覆写(cancel/confirm 不写——不对称)。
册尾端点覆盖对账(11/11)
对账 backend/cmd/server/routes.go:141-151 亲数 11 条 ↔ 本册 F7.01~F7.06。口径脚注:冻结源物理排布两段不连续(用户面 7 条 index.js:598-638 + 状态机 4 条 :708-737,中间隔 admin 段);Go 物理分组单块连续 11 条;M12 组注释「41 物理 − withdrawals 2 条归 M7」(routes.go:268-270)——#10/#11 物理在 /admin/ 前缀但归本域(M12 裁决⑩)。
| # | 端点(routes.go:line) | 节 |
|---|---|---|
| 1 | GET /distribution/overview(:141) | F7.01 |
| 2 | GET /distribution/downlines(:142) | F7.02 |
| 3 | GET /distribution/records(:143) | F7.02 |
| 4 | GET /distribution/withdrawals(:144) | F7.03 |
| 5 | POST /distribution/withdrawals(:145) | F7.03 |
| 6 | GET /distribution/exchanges(:146) | F7.04 |
| 7 | POST /distribution/exchanges(:147) | F7.04 |
| 8 | POST /distribution/withdrawals/:id/cancel(:148) | F7.05 |
| 9 | POST /distribution/withdrawals/:id/confirm(:149) | F7.05 |
| 10 | PUT /admin/withdrawals/:id(:150,amw+adminMw) | F7.06 |
| 11 | GET /admin/withdrawals(:151,amw+adminMw) | F7.06 |
差异纪律:PRD §4 M7 ①~⑩ 全引用不重立(含 DECIMAL 收口⑥/UTC reviewed_at ⑦/W_FLOW ⑧/死态⑨/M12 归属⑩);本册零新立 DIF(19 端点 wire 面键集/键序/文案/状态码/校验序与冻结逐位一致——撰写期双侧对拍判定)。注记:Go 注释冻结行号微偏三处(model.go:27-28 引 :633/:636 实 :632/:633、repo.go:168-171 引 :637 实 :635、service.go:130 引 :628-638 实 628-636)——行为零差勘误候选非 DIF。