lili b31cc37a4f chore(brand): 造梦AI→绘境AI 全活层品牌不变量收口(441 文件)
把活层退役旧品牌「造梦AI」统一改为现行「绘境AI」,brand-invariant 门红线归零。构成:
~412 Java @author 署名 + game-studio/index.html 浏览器标题 + game-runtime/package.json 描述
+ game-admin/.env VITE_APP_TITLE 运营后台标题 + contracts API title/@Schema 用户协议示例/events 描述
+ docs-design mockup 品牌位 + 各模块注释。全是注释/显示/元数据,零逻辑改动。
zaomeng 拼音持久化标识符按 2026-06-24 评审 GR-05 不动(347 处保留)。
白名单(docs/ip 法律备案 / _archive / 带日期留痕)与 AGENTS.md 门定义自身不动。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 08:29:11 -07:00

470 lines
31 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 | 模块:ad(game-module-ad,Wave2 变现域「广告引擎」)| owner:WS5 主笔,全员 review
# 职责(架构 Doc B):联盟接入 / 广告位 AI 植入 / 曝光计费 / eCPM / 归因。不做结算提现(归 trade)。
# MVP provider 默认 mock(真实 csj 穿山甲 / gdt 优量汇 走审核闸门后注入,见契约#7 ad-slot 与执行 spec 风险#6)。
# Provider 用 SPI 思路(AdProvider:mock/csj/gdt):本契约层只暴露统一形态,不绑死联盟;provider!=mock 时由人提供 providerSlotId(审核闸门后注入)。
# 端:/app-api(产品端 game-studio,SDK Plugin.Ad,用户/匿名 Token) /admin-api(管理端 game-admin,RBAC);前缀由 huijing 框架按 controller.app/admin 包名自动添加
# 错误码段:ad = 1-111-***-***(本模块独占,禁止与他模块重叠:100 project/101 aigc/102 runtime/103 feed/104 telemetry/106 trade)
# 响应统一 Huijing CommonResult 信封:{ code, data, msg };code=0 成功
# 脊柱(钱财闭环上游):SDK 拉广告位(#7) → 有效曝光/激励完成上报(计费级,幂等) → mock eCPM 计收入 → 落广告收入台账 game_ad_revenue(settle_status=0 未结算) → 经 x-feign-contracts 暴露给 trade 做 T+1 分账,trade 入账后回标 settle_status=1
# 计费 vs 分析埋点:本模块 report/impression、report/reward = 计费级(落 game_ad_revenue,决定收入),区别于 telemetry(#5 ad_impression/ad_reward) 的分析埋点(落 game_telemetry_event,仅统计);二者并存、口径不同、不可互相替代
# 计费幂等键:game_ad_revenue.uk_trace=(trace_id, event_type, tenant_id)。同 (traceId,event_type) 重复上报幂等去重;同一 traceId 的 impression 与 reward 各落一条、互不覆盖(防 reward 被 impression 静默吞掉)
# 收入口径(避免同一次展示重复计收入):type=rewarded 收入计在 reward(impression 若上报 revenue_amount=0);type=interstitial/banner 收入计在 impression。MVP SDK 仅 showRewarded(一次广告交互一个 traceId),故 rewarded 一次展示即一条 reward 计费
# 与契约#7 对齐:广告位 CRUD 字段严格对齐 ad-slot.schema.json(slotId/type/provider/placement/providerSlotId/ecpmFloor/enabled/compliance),不重造广告位结构
# 与契约#5 对齐:不重造 ad_impression/ad_reward/income_settled 事件(那是 telemetry 分析埋点);本模块只新增计费上报接口与收入台账
info:
title: 绘境AI ad 模块 API
version: 1.0.0
description: >-
广告引擎("赚得到"上游)。SDK Plugin.Ad 拉取启用广告位(契约#7) → 有效曝光/激励视频完成计费上报 →
mock provider 算 eCPM 收入 → 落广告收入台账 game_ad_revenue → 经 Feign 暴露未结算收入给 trade 做 T+1 分账。
MVP 全 mock provider(真实联盟走 SPI + 审核闸门后注入);前端据此 vite-plugin-mock 自动生成 mock。
servers:
- url: http://localhost:48080
description: 本地(Swagger/Knife4j http://localhost:48080/doc.html)
paths:
# ===========================================================================
# 产品端 /app-api(SDK Plugin.Ad,用户 Token 或匿名 anon Token)
# ===========================================================================
/app-api/ad/slot/list-enabled:
get:
tags: [app-ad]
summary: 拉取启用的广告位配置(SDK Plugin.Ad 初始化用;返回 AdSlotConfig 列表,对齐契约#7)
description: >-
SDK Plugin.Ad 据此渲染广告位。返回 enabled=true 的广告位配置,字段对齐契约#7 ad-slot.schema.json
(slotId/type/provider/placement/ecpmFloor/compliance)。
安全:provider!=mock 时 providerSlotId 为联盟侧真实广告位 ID,非前端渲染必需则不下发明文(默认不下发,仅服务端调联盟时使用;本接口 providerSlotId 恒为空串占位,真实联盟由服务端代理拉广告)。
可选按 placement 过滤(feed 间隙/本局结束/暂停),便于 SDK 按场景取位。
parameters:
- { name: placement, in: query, required: false, schema: { type: string, enum: [game_end, pause, feed] }, description: '触发场景过滤(对齐#7 placement);空则返回全部启用广告位' }
responses:
'200':
description: 成功(启用广告位配置列表)
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultAdSlotConfigList' }
/app-api/ad/report/impression:
post:
tags: [app-ad]
summary: 有效曝光上报(计费级,T-AD-01;幂等键=traceId,mock 算 eCPM 收入落台账)
description: >-
有效曝光计费上报(区别于 telemetry 分析埋点 ad_impression)。服务端按 slotId 找广告位配置,
校验合规(compliance.blockMinor=未成年不计费、maxPerSession 单会话上限),mock provider 用广告位 ecpmFloor 算单次曝光收入
(mock 口径:revenue = ecpmFloor / 1000,单位分,向下取整;见 decisions),落广告收入台账 game_ad_revenue(event_type=impression, settle_status=0 未结算)。
幂等:以 traceId 为幂等键(对应 game_ad_revenue.uk_trace),重复上报同一 traceId 只计一次收入(命中唯一键即跳过,返回上次受理结果)。
收入归因:creatorUserId 由 gameId 反查 project.game_project.creator_user_id(收入归到游戏作者),落台账 creator_user_id 供 trade 按创作者聚合分账。
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/AdImpressionReqVO' }
responses:
'200':
description: 受理结果(计费收入 + 是否首次计费)
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultAdReportResult' }
/app-api/ad/report/reward:
post:
tags: [app-ad]
summary: 激励视频完成校验(T-AD-02;幂等键=traceId,mock 直接校验通过并计激励收入)
description: >-
激励视频(type=rewarded)观看完成校验,决定是否发奖(rewarded=true 时前端发放游戏内奖励)。
mock provider 直接校验通过返回 rewarded=true(真实 provider later 走服务端回调校验防作弊,见 decisions;本接口为前端上报入口,真实校验由服务端→联盟回调链路 /app-api/ad/reward/callback 替换/增强)。
【M4】provider=callback(sandbox 自回调桩)/csj/gdt 时,发奖凭据从「前端自报」升级为「服务端回调验签」(AdProvider.verifyReward 演进为携带回调上下文的验签形态);mock 仍直通。
校验通过同时按 reward 计费落 game_ad_revenue(event_type=reward, settle_status=0),收入归因同 impression。
幂等:以 traceId 为幂等键(uk_trace),重复上报同一 traceId 只发奖一次、只计费一次。
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/AdRewardReqVO' }
responses:
'200':
description: 校验结果(rewarded + 计费收入)
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultAdRewardResult' }
/app-api/ad/reward/callback:
post:
tags: [app-ad]
summary: 激励视频服务端回调校验(T-AD-09;联盟回调入站端点,签名校验防作弊)
description: >-
【M4 真实化】激励视频「服务端→联盟回调校验」入站端点(防作弊,替代「前端上报直通发奖」)。
真实联盟(csj/gdt)在用户看完激励视频后由联盟服务端回调本端点,携带 transId(联盟侧交易号)+ sign(联盟签名)+ 业务参数;
服务端用联盟密钥校验 sign 通过后,方按 reward 计费落 game_ad_revenue(event_type=reward)并标记该 traceId 可发奖。
provider=mock/callback(sandbox 自回调桩) 时签名校验走桩直通;真实 csj/gdt 由 SPI 实现注入真实验签(审核闸门后)。
幂等:以 traceId(uk_trace)去重,重复回调只计费一次、只发奖一次。签名校验失败返回 1-111-002-004(回调签名校验失败),不计费不发奖。
本端点为服务端可信入站(联盟→平台),与 /app-api/ad/report/reward(前端上报入口)形态不同:报奖凭据从「前端自报」升级为「联盟回调+验签」。
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/AdRewardCallbackReqVO' }
responses:
'200':
description: 回调受理结果(验签通过并计费=accepted:true)
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultAdRewardCallbackResult' }
# ===========================================================================
# 管理端 /admin-api(运营/管理员 RBAC):广告位 CRUD + 广告收入台账对账
# ===========================================================================
/admin-api/ad/slot/create:
post:
tags: [admin-ad]
summary: 新建广告位(T-AD-03;落 game_ad_slot,字段对齐契约#7)
description: 新建广告位配置。slotId 业务唯一(uk_slot),重复报错(1-111-001-001)。provider!=mock 时 providerSlotId 必填(审核闸门后由人填写联盟真实广告位 ID)。
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/AdSlotSaveReqVO' }
responses:
'200':
description: 返回新建广告位主键 ID
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultLong' }
/admin-api/ad/slot/update:
put:
tags: [admin-ad]
summary: 更新广告位(T-AD-03;按主键 id 更新,slotId 不可改)
description: 更新广告位配置(type/provider/placement/providerSlotId/ecpmFloor/enabled/compliance)。slotId 为业务唯一键不可改(改则用新建)。
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/AdSlotSaveReqVO' }
responses:
'200':
description: 成功
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultBoolean' }
/admin-api/ad/slot/delete:
delete:
tags: [admin-ad]
summary: 删除广告位(T-AD-03;逻辑删除 deleted=1)
parameters:
- { name: id, in: query, required: true, schema: { type: integer, format: int64 }, description: 广告位主键 ID }
responses:
'200':
description: 成功
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultBoolean' }
/admin-api/ad/slot/get:
get:
tags: [admin-ad]
summary: 广告位详情(T-AD-03;按主键 id)
parameters:
- { name: id, in: query, required: true, schema: { type: integer, format: int64 }, description: 广告位主键 ID }
responses:
'200':
description: 成功
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultAdSlot' }
/admin-api/ad/slot/page:
get:
tags: [admin-ad]
summary: 广告位分页(T-AD-03;按 type/provider/enabled 筛选)
parameters:
- { name: type, in: query, required: false, schema: { type: string, enum: [rewarded, interstitial, banner] }, description: 广告形式过滤 }
- { name: provider, in: query, required: false, schema: { type: string, enum: [csj, gdt, mock, callback] }, description: 联盟过滤 }
- { name: enabled, in: query, required: false, schema: { type: boolean }, description: 启用状态过滤 }
- { 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/CommonResultAdSlotPage' }
/admin-api/ad/revenue/page:
get:
tags: [admin-ad]
summary: 广告收入台账分页(T-AD-04;归因/计费对账,只读)
description: >-
广告收入台账查询(game_ad_revenue),供运营对账(按广告位/游戏/创作者/结算状态/归集日筛选)。
这是 ad↔trade seam 的人工对账视图;与 trade 侧 game_trade_income.source_ref 通过 game_ad_revenue.id + trace_id 对齐。
parameters:
- { name: slotId, in: query, required: false, schema: { type: string }, description: 广告位逻辑 ID 过滤 }
- { name: gameId, in: query, required: false, schema: { type: integer, format: int64 }, description: 游戏 ID 过滤 }
- { name: creatorUserId, in: query, required: false, schema: { type: integer, format: int64 }, description: 收入归因到的创作者 ID 过滤 }
- { name: eventType, in: query, required: false, schema: { type: string, enum: [impression, reward] }, description: 计费事件类型过滤 }
- { name: settleStatus, in: query, required: false, schema: { type: integer, enum: [0, 1] }, description: '结算状态过滤:0未结算 1已结算' }
- { name: statDate, 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/CommonResultAdRevenuePage' }
components:
schemas:
# ---- Huijing CommonResult 信封 ----
CommonResultBoolean:
type: object
properties:
code: { type: integer, description: '0=成功,非0=错误码 1-111-***-***' }
data: { type: boolean }
msg: { type: string }
CommonResultLong:
type: object
properties:
code: { type: integer }
data: { type: integer, format: int64, description: 新建广告位主键 ID }
msg: { type: string }
CommonResultAdSlot:
type: object
properties:
code: { type: integer }
data: { $ref: '#/components/schemas/AdSlotRespVO' }
msg: { type: string }
CommonResultAdSlotPage:
type: object
properties:
code: { type: integer }
data:
type: object
properties:
list: { type: array, items: { $ref: '#/components/schemas/AdSlotRespVO' } }
total: { type: integer, format: int64 }
msg: { type: string }
CommonResultAdSlotConfigList:
type: object
properties:
code: { type: integer }
data: { type: array, items: { $ref: '#/components/schemas/AdSlotConfigVO' } }
msg: { type: string }
CommonResultAdReportResult:
type: object
properties:
code: { type: integer }
data: { $ref: '#/components/schemas/AdReportResultVO' }
msg: { type: string }
CommonResultAdRewardResult:
type: object
properties:
code: { type: integer }
data: { $ref: '#/components/schemas/AdRewardResultVO' }
msg: { type: string }
CommonResultAdRevenuePage:
type: object
properties:
code: { type: integer }
data:
type: object
properties:
list: { type: array, items: { $ref: '#/components/schemas/AdRevenueRespVO' } }
total: { type: integer, format: int64 }
msg: { type: string }
# ---- 计费上报入参(app-api,计费级,幂等键=traceId)----
AdImpressionReqVO:
type: object
description: 有效曝光计费上报入参(区别于 telemetry 分析埋点)。traceId 为幂等键,重复上报只计一次收入。
required: [slotId, gameId, traceId]
properties:
slotId: { type: string, description: '广告位逻辑 ID(对齐契约#7 slotId;据此找配置算 eCPM)' }
gameId: { type: integer, format: int64, description: '当前试玩游戏 ID(project.game_project.id);服务端据此反查 creator_user_id 做收入归因' }
traceId: { type: string, minLength: 1, description: '全链路追踪 ID = 计费幂等键(对应 game_ad_revenue.uk_trace,重复上报只计一次)' }
sessionId: { type: string, description: '会话 ID(统计 maxPerSession 单会话上限用;对齐 #5 session_id 口径)' }
AdRewardReqVO:
type: object
description: 激励视频完成校验入参。mock 直接通过;traceId 为幂等键,重复上报只发奖+计费一次。
required: [slotId, gameId, traceId]
properties:
slotId: { type: string, description: '广告位逻辑 ID(type 应为 rewarded)' }
gameId: { type: integer, format: int64, description: '当前试玩游戏 ID;收入归因到该游戏创作者' }
traceId: { type: string, minLength: 1, description: '全链路追踪 ID = 发奖/计费幂等键(对应 game_ad_revenue.uk_trace)' }
sessionId: { type: string, description: '会话 ID(防作弊/会话上限用)' }
# ---- 计费上报出参 ----
AdReportResultVO:
type: object
description: 有效曝光计费受理结果
properties:
accepted: { type: boolean, description: '是否受理计费(合规拦截/会话超限时为 false,不计费)' }
firstBilling: { type: boolean, description: '是否首次计费(幂等:重复 traceId 为 false,表示命中已存在台账、未重复计收入)' }
revenueAmount: { type: integer, format: int64, description: '本次计入的广告收入(单位:分;mock=ecpmFloor/1000 向下取整);未受理时为 0' }
traceId: { type: string, description: '回执 traceId(便于对账排障)' }
AdRewardResultVO:
type: object
description: 激励视频完成校验结果
properties:
rewarded: { type: boolean, description: '是否发奖(mock 恒 true;真实 provider 由服务端回调校验决定)' }
firstBilling: { type: boolean, description: '是否首次计费(幂等:重复 traceId 为 false)' }
revenueAmount: { type: integer, format: int64, description: '本次计入的广告收入(单位:分);未发奖时为 0' }
traceId: { type: string, description: '回执 traceId' }
# ---- M4 新增:激励视频服务端回调校验(T-AD-09 防作弊回调入站)----
AdRewardCallbackReqVO:
type: object
description: 激励视频服务端回调入参(联盟→平台;服务端验签后计费发奖)
required: [slotId, gameId, traceId, sign]
properties:
slotId: { type: string, minLength: 1, description: '广告位逻辑 ID(type 应为 rewarded;据此取 provider 选验签实现)' }
gameId: { type: integer, format: int64, description: '游戏 ID(收入归因到该游戏创作者)' }
traceId: { type: string, minLength: 1, description: '全链路追踪 ID = 发奖/计费幂等键(uk_trace)' }
transId: { type: string, description: '联盟侧交易号(真实联盟回调携带;mock/callback 桩可空)' }
sign: { type: string, minLength: 1, description: '联盟回调签名(服务端用联盟密钥校验;mock/callback 桩走桩直通)' }
timestamp: { type: integer, format: int64, description: '回调时间戳(防重放,可选;真实联盟验签纳入)' }
extra: { type: string, description: '联盟透传业务参数(JSON 串,可空)' }
AdRewardCallbackResultVO:
type: object
description: 激励视频回调受理结果
properties:
accepted: { type: boolean, description: '是否受理(验签通过并计费=true;验签失败已由错误码 1-111-002-004 拦截,不达此)' }
rewarded: { type: boolean, description: '是否可发奖(验签通过即 true)' }
firstBilling: { type: boolean, description: '是否首次计费(幂等:重复 traceId 为 false)' }
revenueAmount: { type: integer, format: int64, description: '本次计入的广告收入(单位:分)' }
traceId: { type: string, description: '回执 traceId' }
CommonResultAdRewardCallbackResult:
type: object
properties:
code: { type: integer }
data: { $ref: '#/components/schemas/AdRewardCallbackResultVO' }
msg: { type: string }
# ---- 广告位配置(对齐契约#7 ad-slot.schema.json)----
AdSlotConfigVO:
type: object
description: >-
SDK 下发的广告位配置(对齐契约#7 ad-slot.schema.json AdSlotConfig)。
安全:providerSlotId 默认不下发明文(恒空串),真实联盟由服务端代理拉广告,前端不持有联盟广告位 ID。
properties:
slotId: { type: string, description: '广告位逻辑 ID(平台内唯一,埋点 ad_impression.slot_id 引用)' }
type: { type: string, enum: [rewarded, interstitial, banner], description: '广告形式(对齐#7 type)' }
provider: { type: string, enum: [csj, gdt, mock, callback], description: '联盟:csj 穿山甲/gdt 优量汇/mock 桩(MVP 默认 mock)' }
placement: { type: string, enum: [game_end, pause, feed], description: '触发场景:game_end 本局结束/pause 暂停/feed 游戏流间隙' }
ecpmFloor: { type: number, minimum: 0, description: 'eCPM 底价(分),低于此不展示' }
compliance: { $ref: '#/components/schemas/AdComplianceVO' }
AdComplianceVO:
type: object
description: 合规植入规则(对齐契约#7 compliance;未成年人保护)
properties:
blockMinor: { type: boolean, default: true, description: '识别为未成年则不展示/不计费' }
maxPerSession: { type: integer, minimum: 0, description: '单会话最大展示次数(计费上报按此限流)' }
# ---- 广告位 admin CRUD VO(对齐契约#7 全字段)----
AdSlotSaveReqVO:
type: object
description: 广告位新建/更新入参(字段严格对齐契约#7 ad-slot.schema.json)。新建无 id;更新带 id,slotId 不可改。
required: [slotId, type, provider, enabled]
properties:
id: { type: integer, format: int64, description: '主键 ID(更新必填、新建不传)' }
slotId: { type: string, minLength: 1, description: '广告位逻辑 ID(业务唯一键 uk_slot;新建唯一、更新不可改)' }
type: { type: string, enum: [rewarded, interstitial, banner], description: '广告形式(对齐#7)' }
provider: { type: string, enum: [csj, gdt, mock, callback], description: '联盟(MVP 默认 mock;csj/gdt 走审核闸门后注入)' }
placement: { type: string, enum: [game_end, pause, feed], description: '触发场景' }
providerSlotId: { type: string, description: '联盟侧真实广告位 ID(provider!=mock 时必填,由人提供——审核闸门后注入;mock 时留空)' }
ecpmFloor: { type: number, minimum: 0, description: 'eCPM 底价(分),低于此不展示;mock 计费亦用此算收入' }
enabled: { type: boolean, description: 是否启用 }
compliance: { $ref: '#/components/schemas/AdComplianceVO' }
AdSlotRespVO:
type: object
description: 广告位详情(admin;含 #7 全字段 + 主键 + 审计时间)
properties:
id: { type: integer, format: int64, description: 主键 ID }
slotId: { type: string, description: 广告位逻辑 ID(业务唯一键) }
type: { type: string, enum: [rewarded, interstitial, banner] }
provider: { type: string, enum: [csj, gdt, mock, callback] }
placement: { type: string, enum: [game_end, pause, feed] }
providerSlotId: { type: string, description: '联盟侧真实广告位 ID(admin 可见明文,供运营核对)' }
ecpmFloor: { type: number, minimum: 0, description: 'eCPM 底价(分)' }
enabled: { type: boolean }
compliance: { $ref: '#/components/schemas/AdComplianceVO' }
createTime: { type: string, format: date-time }
updateTime: { type: string, format: date-time }
# ---- 广告收入台账 VO(admin 对账只读)----
AdRevenueRespVO:
type: object
description: >-
广告收入台账记录(game_ad_revenue)。这是 ad↔trade seam 的数据源:
settleStatus=0 的记录由 trade 经 AdRevenueApi.getUnsettledRevenue 拉取做 T+1 分账,分账后回标 settleStatus=1。
properties:
id: { type: integer, format: int64, description: '台账记录 ID(= trade 侧 game_trade_income.source_ref 对账锚点)' }
slotId: { type: string, description: 广告位逻辑 ID }
gameId: { type: integer, format: int64, description: 产生收入的游戏 ID }
creatorUserId: { type: integer, format: int64, description: '收入归因到的创作者用户 ID(trade 按此聚合分账)' }
eventType: { type: string, enum: [impression, reward], description: '计费事件类型:impression 有效曝光/reward 激励完成' }
provider: { type: string, enum: [csj, gdt, mock, callback], description: 产生收入的联盟 }
ecpm: { type: integer, format: int64, description: 'eCPM(分;mock 取广告位 ecpmFloor)' }
revenueAmount: { type: integer, format: int64, description: '本条广告收入(单位:分,BIGINT;金额一律用分,禁浮点)' }
traceId: { type: string, description: '计费幂等键 / 对账 traceId' }
settleStatus: { type: integer, enum: [0, 1], description: '结算状态机:0未结算 1已结算(trade 分账成功后经 markSettled 回标 1)' }
statDate: { type: string, format: date, description: '归集日(yyyy-MM-dd,按日归集供 trade 按日扫未结算)' }
createTime: { type: string, format: date-time }
# ===========================================================================
# ad↔trade seam(Phase 0 锁死的跨模块 Feign 契约;两侧 agent 据此实现,不得各自发挥)
# 仿 telemetry.yaml x-mq-contracts 写法;跨模块只依赖 -api 走 Feign(见 add-business-module 规范)
# ===========================================================================
x-feign-contracts:
description: >-
ad 模块通过 -api 包暴露给 trade 模块的 Feign 同步契约(AdRevenueApi)。
用途:trade 的 T+1 结算 job 据此拉取未结算广告收入做分账,并在入账成功后回标已结算。
跨模块只依赖 ad 的 -api(com.wanxiang.huijing.game.module.ad.api),禁止 trade 依赖 ad 的 -server,避免编译耦合。
全链路对账:广告计费产生的 game_ad_revenue.id + trace_id 透传到 trade 的 game_trade_income.source_ref + trace_id,可端到端对账。
provider:
module: ad
apiInterface: 'com.wanxiang.huijing.game.module.ad.api.AdRevenueApi'
consumedBy: 'trade(T+1 结算 job:com.wanxiang.huijing.game.module.trade.job.SettlementJob,仅契约约定,trade 侧实现)'
operations:
- name: getUnsettledRevenue
signature: 'List<AdRevenueRespDTO> getUnsettledRevenue(LocalDate statDate)'
semantics: >-
返回指定归集日 statDate 下 settle_status=0(未结算)的广告收入列表。
trade 据此按 creatorUserId 聚合后 ×creator_share 分账。
只读、幂等(重复调用返回当前仍未结算的集合);按 statDate 命中 game_ad_revenue.idx_settle_date 索引扫描,不裸 select*。
input:
statDate: 'LocalDate —— 归集日(yyyy-MM-dd),通常为 T+1 结算的 T 日'
output:
itemType: AdRevenueRespDTO
itemFields:
- 'id: Long —— 台账记录 ID(trade 侧作 source_ref 对账锚点,uk(source, source_ref) 防重分账)'
- 'creatorUserId: Long —— 收入归因到的创作者用户 ID(trade 按此聚合)'
- 'revenueAmount: Long —— 本条广告收入(单位:分,原始 gross,trade ×share_rate 得 net)'
- 'statDate: LocalDate —— 归集日(透传,trade 作 settle_date)'
- 'traceId: String —— 全链路 traceId(透传到 game_trade_income.trace_id 供对账)'
- name: markSettled
signature: 'boolean markSettled(List<Long> revenueIds)'
semantics: >-
trade 分账入账(写 game_trade_income + 累加 game_trade_account.balance)成功后回调,
把这些 game_ad_revenue 记录 settle_status 0→1(已结算)。
幂等:已是 settle_status=1 的记录跳过(不报错、不重复处理),保证至少一次回调下不重复置位;
仅做状态位更新(UPDATE ... WHERE id IN (...) AND settle_status=0),不删数据、可追溯。
input:
revenueIds: 'List<Long> —— 已成功分账入账的 game_ad_revenue.id 列表(即 getUnsettledRevenue 返回项的 id)'
output:
result: 'boolean —— 回标是否成功(部分已为 1 仍视为成功,幂等语义)'
settlementFlow:
description: >-
T+1 结算流(trade 侧 SettlementJob 编排,ad 仅提供数据与回标):
1) trade job 调 AdRevenueApi.getUnsettledRevenue(statDate=T 日) 拉未结算广告收入;
2) 按 creatorUserId 聚合 gross 总额;
3) ×creator_share(Nacos 配置 trade.creator-share,MVP=0.80)算 net;
4) 写 game_trade_income(uk(source, source_ref) 防同一笔收入重复分账,source=ad、source_ref=game_ad_revenue.id)
+ 累加 game_trade_account.balance(账户为流水物化汇总,余额变更必有流水);
5) 调 AdRevenueApi.markSettled(revenueIds) 把 ad 台账回标 settle_status=1。
全链路用 trace_id(ad.traceId → trade.trace_id)+ source_ref(ad.id → trade.source_ref)可对账。
失败处理:步骤 4 成功但步骤 5 markSettled 失败 → 下轮 job 因 ad 仍为 settle_status=0 会重拉,但 trade 侧 uk(source, source_ref) 拦截重复入账(幂等),下轮重试 markSettled 即可补偿,不会重复发钱。