把活层退役旧品牌「造梦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>
470 lines
31 KiB
YAML
470 lines
31 KiB
YAML
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 即可补偿,不会重复发钱。
|