zizi e670eaf476 feat(B1): 黄金闭环契约先行基座 — 接口/DTO/V10/契约 yaml/错误码/pom
contract-first 基座(B1 step①,编译验证待 mini-desktop,与 step② 合并后编译):
- feed-api: FeedApi.upsertRank + FeedRankUpsertReqDTO(UpsertMode 分流) + ApiConstants
- runtime-api: RuntimePackageApi.publish + ApiConstants
- project-api: ProjectVersionApi.createForPackage + DTO + 错误码 PROJECT_PUBLISH_PACKAGE_NOT_READY
- V10: game_runtime_package.package_json + game_telemetry_event.event_id/uk_event_id(替换 uk_dedup);仅 contracts 源 + yudao-server 单一执行副本(守门②)
- 契约 yaml: events/telemetry 信封加 eventId + runtime /manifest 端点(原样 JSON 不包 CommonResult)
- pom: project-server→feed/runtime-api、telemetry-server→feed-api(守门④无环已核)

主 agent 独立核验: V10 双副本一致 / uk_dedup 索引名 / 错误码无碰撞 / 守门①②④ / telemetry x-mq 幂等口径同步改 eventId 避免契约自相矛盾

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 13:50:30 +00:00

224 lines
13 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 | 模块:telemetry(照抄黄金模板 project 风格)| owner:WS5 主笔,全员 review
# 端:/app-api(产品端 game-studio,用户/匿名 Token 上报) /admin-api(管理端 game-admin,RBAC 查看板数据源);前缀由 yudao 框架按 controller.app/admin 包名自动添加
# 错误码段:telemetry = 1-104-***-***
# 响应统一 Yudao CommonResult 信封:{ code, data, msg };code=0 成功
# 脊柱:批量事件(#5 envelope) → /app-api/telemetry/events/batch → MQ 异步消费(幂等) → 原始事件表 game_telemetry_event + 聚合 game_telemetry_game_stat(play_count/like_count/quality_score) → 回灌 feed 与 project
# 量纲约定:telemetry 侧 quality_score/qualityScore = 运营质量分(0-100),≠ aigc 生成质量分(0-1),二者量纲不同、不可互相回灌
# play_count 权威源:= 本模块 game_play_start 事件聚合(runtime session 仅观测、不计数);二者勿混为计数源
# ad/income 范围:ad_impression/ad_reward/income_settled 事件原始表照常入库,但聚合 = Wave2 变现范围外,later 补列(非遗漏)
# 外部依赖:MQ(RocketMQ) 只定义"对接点契约"(见 components.x-mq-contracts),本契约不实现、不 mock
info:
title: 绘境AI telemetry 模块 API
version: 1.0.0
description: >-
遥测/事件批量入库 → MQ 异步 → 增量聚合 → quality_score → 回灌 feed/project 的数据回路。
上报通道接收 #5 events.schema.json 的事件信封(envelope);不渲染看板/建议/导出 UI(供数据给产品域)。
前端据此 vite-plugin-mock 自动生成 mock。
servers:
- url: http://localhost:48080
description: 本地(Swagger/Knife4j http://localhost:48080/doc.html)
paths:
# ===========================================================================
# 上报通道(产品端,用户 Token 或匿名 anon Token;高频写入)
# ===========================================================================
/app-api/telemetry/events/batch:
post:
tags: [app-telemetry]
summary: 事件批量摄取(T-TEL-01;快速 ACK 后投递 MQ 异步入库,不同步聚合)
description: >-
接收 #5 envelope 批量事件 { batch: Envelope[] }。服务端只做轻量校验(事件名在注册表、信封必填字段、批量上限)后
投递 RocketMQ(topic=telemetry-event,见 x-mq-contracts),立即返回受理结果;落原始表与聚合在消费侧异步完成(幂等)。
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/EventBatchReqVO' }
responses:
'200':
description: 受理结果(accepted/rejected 计数;非整批失败语义)
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultEventBatchResult' }
/app-api/telemetry/perf/beacon:
post:
tags: [app-telemetry]
summary: 性能埋点兜底通道(T-TEL-03 sendBeacon / T-TEL-11 加载耗时;页面卸载场景)
description: >-
sendBeacon 在页面卸载/跳转时上报首屏与加载耗时(perf_first_screen / game_load_failed 等)。
与 /events/batch 同样投递 MQ;单条信封,body 体积小,永远返回受理成功(不阻塞前端卸载)。
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/EnvelopeReqVO' }
responses:
'200':
description: 受理成功(恒为 true)
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultBoolean' }
# ===========================================================================
# 看板数据源(管理端 RBAC;只读聚合,供产品域 UI 取数)
# ===========================================================================
/admin-api/telemetry/game-stat/page:
get:
tags: [admin-telemetry]
summary: 游戏维度聚合分页(T-TEL-05;创作者/运营看板数据源,本模块只供数据不渲染 UI)
parameters:
- { name: gameId, in: query, required: false, schema: { type: integer, format: int64 }, description: 指定游戏;空则全量分页 }
- { 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/CommonResultGameStatPage' }
/admin-api/telemetry/game-stat/{gameId}:
get:
tags: [admin-telemetry]
summary: 单游戏最新聚合快照(含 quality_score=运营质量分(0-100),≠ aigc 生成质量分(0-1);feed 排序信号与看板共用读口)
parameters:
- { name: gameId, in: path, required: true, schema: { type: integer, format: int64 } }
responses:
'200':
description: 成功
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultGameStat' }
components:
schemas:
# ---- Yudao CommonResult 信封 ----
CommonResultBoolean:
type: object
properties:
code: { type: integer, description: '0=成功,非0=错误码 1-104-***-***' }
data: { type: boolean }
msg: { type: string }
CommonResultEventBatchResult:
type: object
properties:
code: { type: integer }
data: { $ref: '#/components/schemas/EventBatchResultVO' }
msg: { type: string }
CommonResultGameStat:
type: object
properties:
code: { type: integer }
data: { $ref: '#/components/schemas/GameStatRespVO' }
msg: { type: string }
CommonResultGameStatPage:
type: object
properties:
code: { type: integer }
data:
type: object
properties:
list: { type: array, items: { $ref: '#/components/schemas/GameStatRespVO' } }
total: { type: integer, format: int64 }
msg: { type: string }
# ---- 事件信封(对齐契约 #5 events.schema.json envelope;此处为上报入参镜像)----
EnvelopeReqVO:
type: object
description: 单条事件信封;字段语义与约束以 #5 events.schema.json 为单一事实源,此处仅做 OpenAPI 入参映射。
required: [event, schemaVersion, ts, traceId, eventId]
properties:
event: { type: string, description: '事件名(须在 #5 eventRegistry 中登记,否则消费侧丢弃并计 rejected)' }
schemaVersion: { type: string, pattern: '^v[0-9]+$', description: '事件 Schema 版本,如 v1/v2(对应 #5 schema_version)' }
ts: { type: integer, format: int64, description: '事件发生时间戳(毫秒)' }
traceId: { type: string, description: '全链路追踪 ID,贯穿生成→编译→加载→运行→上报(对应 #5 trace_id)' }
eventId: { type: string, description: '事件实例 UUID(必填,每事件实例唯一,幂等真身,对应 #5 eventId / game_telemetry_event.event_id 的 uk_event_id 唯一键)。前端 crypto.randomUUID() 生成;重放同 eventId 不重复计数' }
sessionId: { type: string, description: '会话 ID(一次游戏流消费/一次创作会话,对应 #5 session_id);来源=runtime sessionId(int64) 转 string,作 session 维度对账键' }
user:
type: object
description: 用户标识;匿名玩家用 anonId,登录后带 userId(对应 #5 user)
properties:
userId: { type: string }
anonId: { type: string, description: 匿名 token 派生的稳定匿名 ID }
context:
type: object
description: 上下文(对应 #5 context;game_id/version_id 是聚合落点关键)
properties:
gameId: { type: string }
versionId: { type: string }
channel: { type: string, description: 渠道归因(utm/channel 解析) }
deviceType: { type: string, enum: [mobile, desktop, miniapp] }
props:
type: object
description: 事件私有字段(结构见 #5 对应 eventRegistry 项,如 duration_ms/completed/slot_id/reason)
additionalProperties: true
EventBatchReqVO:
type: object
required: [batch]
properties:
batch:
type: array
description: 批量事件信封;单次上限 200 条(超限整体 rejected,前端分批重传)
minItems: 1
maxItems: 200
items: { $ref: '#/components/schemas/EnvelopeReqVO' }
# ---- 响应 VO ----
EventBatchResultVO:
type: object
description: 批量受理结果(部分成功语义:已受理投递 MQ 的条数与被拒条数)
properties:
accepted: { type: integer, description: 通过轻量校验并投递 MQ 的事件数 }
rejected: { type: integer, description: 校验失败被拒的事件数(事件名未登记/信封字段缺失等) }
traceId: { type: string, description: 本次批量受理回执 traceId(便于排障) }
GameStatRespVO:
type: object
description: >-
游戏维度聚合快照(消费侧增量聚合产出;play_count/like_count 回灌 project,quality_score 回灌 feed 排序)。
ad/income 范围说明:ad_impression/ad_reward/income_settled 事件原始表(game_telemetry_event)照常入库,
但聚合 = Wave2 变现范围外,later 补列(本 VO 暂不含广告/收益聚合字段,非遗漏)。
properties:
gameId: { type: integer, format: int64 }
statDate: { type: string, format: date, description: 统计日(yyyy-MM-dd;按日聚合粒度) }
playCount: { type: integer, format: int64, description: '当日累计试玩数;权威源=本模块 game_play_start 事件聚合(runtime session 仅观测、不计数)' }
playEndCount: { type: integer, format: int64, description: 当日试玩结束数(game_play_end) }
completedCount: { type: integer, format: int64, description: '当日完玩数(props.completed=true)' }
avgDurationMs: { type: integer, format: int64, description: 平均试玩时长(毫秒;quality_score 输入) }
likeCount: { type: integer, format: int64, description: 当日点赞数 }
favoriteCount: { type: integer, format: int64, description: 当日收藏数 }
shareCount: { type: integer, format: int64, description: 当日分享数 }
reportCount: { type: integer, format: int64, description: 当日举报数(举报阈值降权信号) }
loadFailCount: { type: integer, format: int64, description: 当日加载失败数(降权信号) }
qualityScore: { type: number, format: float, description: '运营质量分(0-100)(T-TEL-04;回灌 feed 排序);≠ aigc 生成质量分(0-1),量纲不同、不可互相回灌' }
updateTime: { type: string, format: date-time }
# ===========================================================================
# 外部依赖对接点契约(MQ 异步消费=脊柱对接点;只定义契约,不实现、不 mock)
# ===========================================================================
x-mq-contracts:
description: >-
telemetry 摄取与消费之间的 RocketMQ 对接点契约。生产者=上报通道(/events/batch、/perf/beacon),
消费者=本模块异步消费组(落 game_telemetry_event + 增量聚合 game_telemetry_game_stat + 回灌 feed/project)。
幂等键=信封 eventId(事件实例 UUID),对应原始表 uk_event_id 唯一约束,重复消息消费侧落库幂等(INSERT IGNORE 语义)。
(历史口径 (traceId,event,ts)/uk_dedup 已于 V10 被 uk_event_id 取代——同毫秒同名会误吞、重放会重计数,见 V10 迁移与 HJ-EXEC-006 §3.5 C5。)
producer:
topic: telemetry-event
tag: 'event | perf'
keys: traceId
messageBody: '{ envelope: EnvelopeReqVO } # 与上报入参同构,逐条投递'
orderly: false
note: 上报通道快速 ACK 后异步投递;投递失败本地落兜底表后定时补投(不丢事件)。
consumer:
group: telemetry-event-consumer
concurrency: 建议并发消费(无序);聚合写入按 gameId 行级原子累加避免覆盖
idempotency: 以 eventId(事件实例 UUID)为幂等键,命中 game_telemetry_event.uk_event_id 即跳过,保证至少一次投递下不重复聚合(V10 起;原 uk_dedup 已下线)
retry: 默认重试 ≤16 次仍失败转 DLQ(telemetry-event-dlq),由 T-TEL-19 数据质量监控告警
sideEffects:
- '落 game_telemetry_event(原始事件,幂等)'
- '增量聚合 game_telemetry_game_stat(play/like/quality 等按 gameId+statDate 累加)'
- '回灌:play_count/like_count → project(game_project.play_count/like_count);quality_score → feed 排序信号'