zizi 43fce856d7 feat: Wave2 变现域 ad+trade 后端建成验证 + game-admin 运营台脊柱
后端(主 agent 独立复跑验证,非采信子 agent 自报):
- 新建 game-module-ad(广告引擎)+ game-module-trade(分账/结算/提现),克隆黄金模板 project
- 契约先行已锁:ad.yaml/trade.yaml + Flyway V6/V7;ad 计费 uk_trace 收紧为 (trace_id,event_type,tenant_id) 防 reward 被 impression 静默吞掉
- ad↔trade Feign seam(AdRevenueApi.getUnsettledRevenue/markSettled)+ project-api 归因(getCreatorUserId,game 模块首个跨模块 Feign,仿 yudao DictDataApi)
- AdProvider SPI(mock,留 csj/gdt 注入位);资金安全:金额用分/uk 幂等/状态机 CAS/防超扣/T+1 结算补偿
- pay 不建,复用 yudao-pay 后置(Doc B:收单归 pay、钱包提现归 trade;MVP 钱财闭环 ad→trade 不经收单)
- 验证:mvn -pl yudao-server -am compile 全量 BUILD SUCCESS;ad 15 + trade 24 = 39 单测绿;huijing 残留=0、裸 select*=0

前端 game-admin(运营后台):
- 克隆 yudao-ui-admin-vue3 裁剪(删 crm/erp/mall/mes/iot 等无关业务模块)+ 5 个 MVP 运营页接 admin-api(审核/精选/看板/广告位/提现审核)+ mock 兜底
- 构建验证移至 mini-desktop(本机 5.8G 内存 OOM,见执行 spec)

文档:HJ-PAR-002 review+execution 双 spec + Workflow 编排脚本

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 16:20:17 +00:00

357 lines
22 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

openapi: 3.0.3
# 契约 #1 API | 模块:trade(game-module-trade,Wave2 变现域「分账/结算/提现」)| owner:WS5 主笔,全员 review
# 职责(架构 Doc B):把多源收入按规则分账、归集、结算、对账;钱包打款/提现门槛与打款执行属 trade(不属 pay)。不做支付收单(归 pay)。
# MVP 钱财闭环(决策 D3):广告收入 → trade 分账(创作者得 80%)→ 创作者收益账户 → 提现(满 5 元)。会员订阅/支付收单=P1,本轮不做。
# 配置化(决策 D3,全配置走 Nacos/DB,严禁硬编码):
# - trade.creator-share = 0.80(MVP 全档统一 80% 分账;分层结构 creator_share_tier 保留、值后期填,见 V7.0.0 注释)
# - trade.withdraw-min = 500(单位:分,满 5 元可提现)
# 上述为 Nacos 配置项,本契约不建死值;接口内 ≥withdraw-min 校验读 Nacos。
# 上游 Feign 依赖:消费 ad 模块 AdRevenueApi(cn.wanxiang.game.module.ad.api.AdRevenueApi)——
# getUnsettledRevenue(statDate) 拉未结算广告收入、markSettled(revenueIds) 回标已结算(seam 契约详见 ad.yaml x-feign-contracts)。
# trade 侧 T+1 SettlementJob 据此分账入账;跨模块只依赖 ad 的 -api,禁依赖 ad 的 -server。
# 端:/app-api(产品端 game-studio,创作者,用户 Token + DataPermission 只见自己) /admin-api(管理端 game-admin,RBAC);前缀由 yudao 框架按 controller.app/admin 包名自动添加
# 错误码段:trade = 1-106-***-***(本模块独占,禁止与他模块重叠:100 project/101 aigc/102 runtime/103 feed/104 telemetry/111 ad)
# 响应统一 Yudao CommonResult 信封:{ code, data, msg };code=0 成功
# 资金严谨:金额一律用「分」(BIGINT),禁浮点;比例用 DECIMAL;所有资金写入有幂等键(biz_no/source_ref + uk);状态机用 tinyint,非法流转服务端拒绝。
# 与契约#5 对齐:不重造 income_settled 事件(那是 telemetry 分析埋点 props:amount/source);本模块落账走 game_trade_income,income_settled 仅为埋点镜像。
info:
title: 造梦AI trade 模块 API
version: 1.0.0
description: >-
分账/结算/提现域。消费上游 ad 未结算广告收入 → 按 creator_share(Nacos 0.80) T+1 分账入创作者收益账户 →
创作者发起提现(满 withdraw-min 分)→ 管理端审核 → mock 打款。
收益账户为流水物化汇总(余额变更必有流水);MVP 全 mock 打款,状态机/幂等/对账为真实接入留位。
前端据此 vite-plugin-mock 自动生成 mock。
servers:
- url: http://localhost:48080
description: 本地(Swagger/Knife4j http://localhost:48080/doc.html)
paths:
# ===========================================================================
# 产品端 /app-api(创作者,用户 Token + DataPermission:只见自己账户/流水/提现)
# ===========================================================================
/app-api/trade/account/mine:
get:
tags: [app-trade]
summary: 我的收益账户(T-TRD-01;余额/累计收益/累计提现/冻结中,金额单位分)
description: >-
返回当前创作者收益账户(game_trade_account,DataPermission 限本人 user_id)。
账户不存在时返回零值账户(首次有收益或首次查询时由服务端按需初始化,余额全 0)。
金额字段一律单位「分」。
responses:
'200':
description: 成功
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultTradeAccount' }
/app-api/trade/income/page:
get:
tags: [app-trade]
summary: 我的收益流水分页(T-TRD-02;source=ad/tip、gross/net/share_rate、结算日)
description: 返回当前创作者收益流水(game_trade_income,DataPermission 限本人)。供创作者查看每笔分账明细与对账。
parameters:
- { name: source, in: query, required: false, schema: { type: integer, enum: [1, 2] }, description: '收入来源过滤:1广告 2打赏' }
- { name: settleDateStart, in: query, required: false, schema: { type: string, format: date }, description: 结算日起(yyyy-MM-dd) }
- { name: settleDateEnd, in: query, required: false, schema: { type: string, format: date }, description: 结算日止(yyyy-MM-dd) }
- { name: pageNo, in: query, required: false, schema: { type: integer, default: 1 } }
- { name: pageSize, in: query, required: false, schema: { type: integer, default: 10 } }
responses:
'200':
description: 成功
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultIncomePage' }
/app-api/trade/withdraw/apply:
post:
tags: [app-trade]
summary: 发起提现(T-TRD-03;幂等键=bizNo,校验 ≥withdraw-min 且 ≤可提现余额,冻结余额)
description: >-
创作者发起提现申请。服务端校验:
1) amount ≥ Nacos trade.withdraw-min(满 5 元=500 分,否则 1-106-002-001 未达提现门槛);
2) amount ≤ game_trade_account.balance 可提现余额(否则 1-106-002-002 余额不足);
3) bizNo 幂等(对应 game_trade_withdraw.uk_biz_no,重复提交同一 bizNo 返回已有申请、不重复冻结,1-106-002-003 为非幂等的并发冲突时抛)。
校验通过:原子地从 balance 扣减并加到 frozen(冻结中=提现处理中),落 game_trade_withdraw(status=0 待审核),返回提现单 ID。
余额变更必有流水/可追溯:冻结即提现申请记录本身,打款/驳回时再调整 frozen 与 total_withdraw。
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/WithdrawApplyReqVO' }
responses:
'200':
description: 返回新建提现申请 ID
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultLong' }
/app-api/trade/withdraw/page:
get:
tags: [app-trade]
summary: 我的提现记录分页(T-TRD-04;状态机/金额/审核驳回理由)
description: 返回当前创作者提现申请记录(game_trade_withdraw,DataPermission 限本人)。
parameters:
- { name: status, in: query, required: false, schema: { type: integer, enum: [0, 1, 2, 3, 4] }, description: '提现状态过滤(见 status 状态机)' }
- { name: pageNo, in: query, required: false, schema: { type: integer, default: 1 } }
- { name: pageSize, in: query, required: false, schema: { type: integer, default: 10 } }
responses:
'200':
description: 成功
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultWithdrawPage' }
# ===========================================================================
# 管理端 /admin-api(运营/管理员 RBAC):提现审核 + 收益流水查询 + 平台营收报表
# ===========================================================================
/admin-api/trade/withdraw/page:
get:
tags: [admin-trade]
summary: 提现审核队列分页(T-TRD-05;跨创作者,按状态/用户筛选)
description: 管理端查看提现申请(默认取 status=0 待审核),用于审核处理。
parameters:
- { name: status, in: query, required: false, schema: { type: integer, enum: [0, 1, 2, 3, 4] }, description: '默认取 status=0 待审核' }
- { name: userId, in: query, required: false, schema: { type: integer, format: int64 }, description: 按创作者用户 ID 筛选 }
- { name: pageNo, in: query, required: false, schema: { type: integer, default: 1 } }
- { name: pageSize, in: query, required: false, schema: { type: integer, default: 10 } }
responses:
'200':
description: 成功
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultWithdrawPage' }
/admin-api/trade/withdraw/audit:
post:
tags: [admin-trade]
summary: 提现审核(T-TRD-06;通过/驳回,驱动状态机;非法流转服务端拒绝)
description: >-
管理端审核提现申请,驱动状态机(合法流转见 components.x-state-machines):
- 通过(decision=1):status 0→1(审核通过打款中),触发 mock 打款(MVP 直接置 2 已打款,扣 frozen、加 total_withdraw);
真实打款 later 走 pay 渠道异步回调,回调成功 1→2、失败 1→4(打款失败,frozen 退回 balance)。
- 驳回(decision=2):status 0→3(驳回),把冻结金额 frozen 退回 balance(资金原路退回,必填 rejectReason)。
仅 status=0 待审核可审核;非法流转(如已打款再审核)服务端拒绝(1-106-003-001 非法状态流转)。
幂等:以提现单 id + 当前 status 乐观校验,重复审核同一单不二次发钱/二次退款。
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/WithdrawAuditReqVO' }
responses:
'200':
description: 成功
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultBoolean' }
/admin-api/trade/income/page:
get:
tags: [admin-trade]
summary: 收益流水查询分页(T-TRD-07;跨创作者对账,按来源/创作者/结算日筛选,只读)
description: >-
管理端查看全量收益流水(game_trade_income),供平台对账。
对账锚点:source_ref(广告收入=game_ad_revenue.id)+ trace_id 与 ad 侧台账对齐。
parameters:
- { name: userId, in: query, required: false, schema: { type: integer, format: int64 }, description: 按创作者用户 ID 筛选 }
- { name: source, in: query, required: false, schema: { type: integer, enum: [1, 2] }, description: '收入来源:1广告 2打赏' }
- { name: settleDate, in: query, required: false, schema: { type: string, format: date }, description: 结算日(yyyy-MM-dd)筛选 }
- { name: pageNo, in: query, required: false, schema: { type: integer, default: 1 } }
- { name: pageSize, in: query, required: false, schema: { type: integer, default: 10 } }
responses:
'200':
description: 成功
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultIncomePage' }
/admin-api/trade/report/revenue:
get:
tags: [admin-trade]
summary: 平台营收报表聚合(T-TRD-08;按日聚合 gross/net/平台分成/提现,只读)
description: >-
平台营收报表(只读聚合):按结算日聚合收益流水的原始收入(gross)、创作者实得(net)、平台分成(gross-net),
及提现申请的打款总额。供运营看平台收入与分账结构。
parameters:
- { name: statDateStart, in: query, required: true, schema: { type: string, format: date }, description: 统计起始日(yyyy-MM-dd) }
- { name: statDateEnd, in: query, required: true, schema: { type: string, format: date }, description: 统计结束日(yyyy-MM-dd) }
responses:
'200':
description: 成功
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultRevenueReport' }
components:
schemas:
# ---- Yudao CommonResult 信封 ----
CommonResultBoolean:
type: object
properties:
code: { type: integer, description: '0=成功,非0=错误码 1-106-***-***' }
data: { type: boolean }
msg: { type: string }
CommonResultLong:
type: object
properties:
code: { type: integer }
data: { type: integer, format: int64, description: 新建提现申请 ID }
msg: { type: string }
CommonResultTradeAccount:
type: object
properties:
code: { type: integer }
data: { $ref: '#/components/schemas/TradeAccountRespVO' }
msg: { type: string }
CommonResultIncomePage:
type: object
properties:
code: { type: integer }
data:
type: object
properties:
list: { type: array, items: { $ref: '#/components/schemas/IncomeRespVO' } }
total: { type: integer, format: int64 }
msg: { type: string }
CommonResultWithdrawPage:
type: object
properties:
code: { type: integer }
data:
type: object
properties:
list: { type: array, items: { $ref: '#/components/schemas/WithdrawRespVO' } }
total: { type: integer, format: int64 }
msg: { type: string }
CommonResultRevenueReport:
type: object
properties:
code: { type: integer }
data: { $ref: '#/components/schemas/RevenueReportVO' }
msg: { type: string }
# ---- 请求 VO ----
WithdrawApplyReqVO:
type: object
description: 发起提现入参。bizNo 为幂等键,amount 单位分。
required: [amount, bizNo]
properties:
amount: { type: integer, format: int64, minimum: 1, description: '提现金额(单位:分)。服务端校验 ≥Nacos trade.withdraw-min(500=5元) 且 ≤可提现余额 balance' }
bizNo: { type: string, minLength: 1, description: '提现业务单号 = 幂等键(前端生成唯一,对应 game_trade_withdraw.uk_biz_no;重复提交返回已有申请、不重复冻结)' }
remark: { type: string, maxLength: 255, description: 备注(可空) }
WithdrawAuditReqVO:
type: object
description: 提现审核入参(管理端)。decision 驱动状态机;驳回必填 rejectReason。
required: [id, decision]
properties:
id: { type: integer, format: int64, description: 提现申请 ID }
decision: { type: integer, enum: [1, 2], description: '审核决策:1通过(0→1→mock 打款 2) 2驳回(0→3,冻结退回余额)' }
rejectReason: { type: string, maxLength: 255, description: '驳回理由(decision=2 必填,回填创作者)' }
remark: { type: string, maxLength: 255, description: 审核备注(可空) }
# ---- 响应 VO ----
TradeAccountRespVO:
type: object
description: >-
创作者收益账户(game_trade_account 物化汇总)。金额一律单位「分」。
约束:balance/frozen/total_income/total_withdraw 由分账入账与提现流程维护,余额变更必有对应流水/单据。
properties:
userId: { type: integer, format: int64, description: 创作者用户 ID }
balance: { type: integer, format: int64, description: '可提现余额(单位:分)' }
frozen: { type: integer, format: int64, description: '冻结中(提现处理中占用,单位:分)' }
totalIncome: { type: integer, format: int64, description: '累计收益(历史分账入账总额,单位:分)' }
totalWithdraw: { type: integer, format: int64, description: '累计已提现(已打款成功总额,单位:分)' }
updateTime: { type: string, format: date-time }
IncomeRespVO:
type: object
description: >-
收益流水(game_trade_income,分账后入账)。net = gross × shareRate(创作者实得)。
对账锚点:source + sourceRef 唯一(广告=ad + game_ad_revenue.id),与 ad 台账对齐。
properties:
id: { type: integer, format: int64, description: 收益流水 ID }
userId: { type: integer, format: int64, description: 创作者用户 ID }
source: { type: integer, enum: [1, 2], description: '收入来源:1广告(ad) 2打赏(tip)' }
sourceRef: { type: string, description: '来源单号(对账锚点):广告=game_ad_revenue.id;打赏=打赏单 id。与 source 组成唯一键防重复分账' }
grossAmount: { type: integer, format: int64, description: '原始收入(分账前,单位:分)' }
shareRate: { type: number, format: double, description: '分账比例(DECIMAL,如 0.8000=创作者得 80%;来自 Nacos trade.creator-share 快照)' }
netAmount: { type: integer, format: int64, description: '创作者实得 = gross × shareRate(单位:分;落账时定格,不随配置变动)' }
settleDate: { type: string, format: date, description: 'T+1 结算日(yyyy-MM-dd)' }
traceId: { type: string, description: '全链路 traceId(广告来源透传自 game_ad_revenue.trace_id,供对账)' }
createTime: { type: string, format: date-time }
WithdrawRespVO:
type: object
description: 提现申请(game_trade_withdraw)。status 状态机见 x-state-machines;金额单位分。
properties:
id: { type: integer, format: int64, description: 提现申请 ID }
userId: { type: integer, format: int64, description: 创作者用户 ID }
amount: { type: integer, format: int64, description: '提现金额(单位:分)' }
status: { type: integer, enum: [0, 1, 2, 3, 4], description: '状态机:0待审核 1审核通过打款中 2已打款 3驳回 4打款失败' }
bizNo: { type: string, description: '提现业务单号(幂等键)' }
channel: { type: string, description: '打款渠道(MVP=mock;真实 later 接 pay 渠道)' }
applyTime: { type: string, format: date-time, description: 申请时间 }
auditTime: { type: string, format: date-time, description: 审核时间(审核后写入) }
auditUserId: { type: integer, format: int64, description: 审核人用户 ID(管理端) }
payTime: { type: string, format: date-time, description: 打款时间(打款成功后写入) }
rejectReason: { type: string, description: 驳回理由(status=3 时) }
remark: { type: string, description: 备注 }
RevenueReportVO:
type: object
description: 平台营收报表聚合(按统计区间汇总;金额单位分)
properties:
statDateStart: { type: string, format: date, description: 统计起始日 }
statDateEnd: { type: string, format: date, description: 统计结束日 }
totalGross: { type: integer, format: int64, description: '区间内原始收入合计(分账前,单位:分)' }
totalNet: { type: integer, format: int64, description: '区间内创作者实得合计(单位:分)' }
totalPlatform: { type: integer, format: int64, description: '区间内平台分成合计 = totalGross - totalNet(单位:分)' }
totalWithdrawPaid: { type: integer, format: int64, description: '区间内已打款提现合计(单位:分)' }
# ===========================================================================
# 状态机契约(合法流转;非法流转由 Service 服务端拒绝,DO 层不承载校验)
# ===========================================================================
x-state-machines:
description: trade 模块资金状态机定义,供两侧 agent 实现一致;非法流转一律服务端拒绝(1-106-003-001)。
withdraw:
field: 'game_trade_withdraw.status'
states:
'0': 待审核(apply 落库初始态;冻结金额已从 balance 转入 frozen)
'1': 审核通过打款中(审核通过 0→1;mock 渠道随即置 2,真实渠道等待 pay 异步回调)
'2': 已打款(终态成功;1→2:frozen 扣减、total_withdraw 累加)
'3': 驳回(终态;0→3:frozen 退回 balance,资金原路退回)
'4': 打款失败(终态;1→4:frozen 退回 balance,可由创作者重新发起新单)
legalTransitions:
- '0 → 1(审核通过)'
- '0 → 3(审核驳回,frozen→balance)'
- '1 → 2(打款成功,frozen→total_withdraw)'
- '1 → 4(打款失败,frozen→balance)'
illegalExamples:
- '2 → * (已打款为终态,不可再变)'
- '3 → * / 4 → * (驳回/失败为终态)'
- '0 → 2(未经审核直接打款,拒绝)'
adRevenueSettle:
field: 'game_ad_revenue.settle_status(ad 侧表,trade 经 AdRevenueApi.markSettled 驱动)'
states:
'0': 未结算(ad 计费落台账初始态)
'1': 已结算(trade 分账入账成功后经 markSettled 回标)
legalTransitions:
- '0 → 1(trade 分账成功回标,幂等:已是 1 跳过)'
note: '该状态位权威表在 ad 模块 game_ad_revenue,trade 只经 Feign markSettled 驱动,不直接写 ad 表'
# ===========================================================================
# 上游 Feign 依赖(消费 ad 模块;seam 权威定义在 ad.yaml x-feign-contracts,此处仅声明消费关系)
# ===========================================================================
x-feign-dependencies:
description: trade 消费 ad 模块 -api 的 Feign 契约,用于 T+1 分账结算。权威签名见 ad.yaml x-feign-contracts,此处不重复定义、仅声明依赖与用法。
dependsOn:
module: ad
apiInterface: 'cn.wanxiang.game.module.ad.api.AdRevenueApi'
operations:
- 'getUnsettledRevenue(LocalDate statDate) → List<AdRevenueRespDTO>:拉指定归集日未结算广告收入'
- 'markSettled(List<Long> revenueIds) → boolean:分账入账成功后回标 ad 台账 settle_status=1(幂等)'
usedBy: 'cn.wanxiang.game.module.trade.job.SettlementJob(T+1 结算定时任务)'
flow: >-
SettlementJob:getUnsettledRevenue(T 日) → 按 creatorUserId 聚合 gross → ×Nacos trade.creator-share(0.80) 得 net →
写 game_trade_income(uk(source, source_ref) 防重) + 累加 game_trade_account.balance → markSettled 回标 ad。
全链路 trace_id(ad→trade) + source_ref(ad.id→trade) 可对账。
补偿:入账成功但 markSettled 失败时,下轮重拉但被 trade uk 拦截重复入账,仅重试回标,不重复发钱。