后端(主 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>
357 lines
22 KiB
YAML
357 lines
22 KiB
YAML
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 拦截重复入账,仅重试回标,不重复发钱。
|