# FSD M7 · 分销提现

LLMS 索引： [llms.txt](/llms.txt)

---

> [← 总则](/qmtv/fsd-qmtv/00-%E6%80%BB%E5%88%99/) ｜ [附录 →](/qmtv/fsd-qmtv/99-%E9%99%84%E5%BD%95/) ｜ 上游：[PRD §4 M7](/qmtv/product/prd-qmtv-v1/)
> 实现落点：`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。

---

反链：

- [QMTV 功能规格说明书（FSD）v1.0 · 总则](/qmtv/fsd-qmtv/00-%E6%80%BB%E5%88%99/)
- [FSD 附录 · PRD 对照 / 代码域索引 / 差异台账](/qmtv/fsd-qmtv/99-%E9%99%84%E5%BD%95/)
