新增创作者公开主页两只读端点(契约+实现+单测,全 additive):
1) GET /app-api/project/public/creator/{creatorId} @PermitAll
→ PageResult<PublicCreatorProjectRespVO>{id,title,summary,coverUrl,ageRating,
playCount,likeCount,publishTime,creatorId,creatorName};仅 status==4 已发布作品。
2) GET /app-api/telemetry/public/creator/{creatorId}/summary @PermitAll
→ {creatorId,publishedCount,totalPlayCount,totalLikeCount};对该创作者 status==4 作品聚合。
跨创作者公开读(#1 正确性风险)处理:本仓 DataPermission 仅注册 system DeptDataPermissionRule
(仅作用 AdminUserDO/DeptDO 表),game_project 无任何 DataPermission 规则 → MP 拦截器不追加过滤;
project 的"创作者只见自己数据"一向由 Service/Mapper 手动 eq(creatorUserId) 强制而非拦截器。
故公开读只需按"目标 creatorId(路径参数,非登录人)"+status==4 等值即天然跨创作者,无需 @DataPermission(enable=false)。
口径:publishTime 取 update_time 近似(项目表无 publish_time 列,同 count-published 口径);
公开统计经 project-api getCreatorPublishedSummary 取(发布态权威+play/like 计数列均在 project);
creatorName 经既有 AdminUserApi.getUser().nickname 尽力解析(查不到为 null,无 PII)。
单测(Mockito,test goal GREEN):
- ProjectMapperTest(新):SQL 形态守卫——公开查询恒 status==PUBLISHED(4)+按 creator_user_id 过滤,不放行下架(5)/封禁(6);
- ProjectServiceImplTest(+5):跨创作者读(走 selectPublishedPageByCreator 非 selectMyPage)/creatorName 兜底/空/聚合/零;
- GameStatServiceImplTest(+2):跨创作者经 project-api 聚合(不走"我的作品"归属链)/null DTO 兜零。
mvn -pl game-module-project/...-server,game-module-telemetry/...-server -am test → project 32/32 + telemetry 46/46 绿。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
337 lines
21 KiB
YAML
337 lines
21 KiB
YAML
openapi: 3.0.3
|
||
# 契约 #1 API | 模块:telemetry(照抄黄金模板 project 风格)| owner:WS5 主笔,全员 review
|
||
# 端:/app-api(产品端 game-studio,用户/匿名 Token 上报) /admin-api(管理端 game-admin,RBAC 查看板数据源);前缀由 huijing 框架按 controller.app/admin 包名自动添加
|
||
# 错误码段:telemetry = 1-104-***-***
|
||
# 响应统一 Huijing 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' }
|
||
|
||
# ===========================================================================
|
||
# 创作者看板数据源(产品端,登录态;跨"我的作品"只读聚合,供 studio 看板取数)
|
||
# 归属隔离:creatorUserId = getLoginUserId()(token 解析、非前端入参)→ 经 project 取"我的作品" gameId 集合 → 仅聚合这些 gameId,严防越权读他人数据。
|
||
# ad/income 口径:本模块聚合表 game_telemetry_game_stat 不含广告/收益列(Wave2 变现范围外,见文件头 ad/income 范围说明);adRevenueTotal 为预留字段,
|
||
# 待 ad/trade 收益对接点接通前恒为 0(非遗漏,避免越 trade 钱口径硬边界)。
|
||
# ===========================================================================
|
||
/app-api/telemetry/my/summary:
|
||
get:
|
||
tags: [app-telemetry]
|
||
summary: 我的作品汇总聚合(创作者看板;跨"我的全部作品"累计 试玩/完玩/完玩率/广告收益)
|
||
description: >-
|
||
登录创作者维度:先经 project 取"我的作品" gameId 集合,再跨这些 gameId 累加 game_telemetry_game_stat 全历史聚合,
|
||
得到 { totalPlay, totalComplete, completionRate, adRevenueTotal }。
|
||
completionRate = totalComplete / totalPlay(totalPlay=0 兜 0,避免除零);
|
||
adRevenueTotal 当前恒为 0(聚合表无收益列,待 ad/trade 收益对接点,见文件头说明)。
|
||
无作品或无聚合数据时各计数返回 0(不报错)。
|
||
responses:
|
||
'200':
|
||
description: 成功(创作者无作品时各字段为 0)
|
||
content:
|
||
application/json:
|
||
schema: { $ref: '#/components/schemas/CommonResultMyStatSummary' }
|
||
|
||
/app-api/telemetry/my/game-stat/trend:
|
||
get:
|
||
tags: [app-telemetry]
|
||
summary: 我的作品近 N 日趋势(创作者看板;跨"我的全部作品"按日聚合)
|
||
description: >-
|
||
登录创作者维度:先经 project 取"我的作品" gameId 集合,再跨这些 gameId 按 stat_date 聚合近 days 日
|
||
(含今日,倒推 days-1 天;按统计日升序返回,便于前端画折线)。某日无数据则该日不出现在 list(前端按 statDate 对齐补零)。
|
||
无作品时返回空 list(不报错)。
|
||
parameters:
|
||
- { name: days, in: query, required: false, schema: { type: integer, default: 7, minimum: 1, maximum: 90 }, description: 趋势天数(含今日倒推;默认 7,最大 90,越界由服务端夹紧) }
|
||
responses:
|
||
'200':
|
||
description: 成功(创作者无作品时 list 为空数组)
|
||
content:
|
||
application/json:
|
||
schema: { $ref: '#/components/schemas/CommonResultMyTrend' }
|
||
|
||
/app-api/telemetry/public/creator/{creatorId}/summary:
|
||
get:
|
||
tags: [app-telemetry]
|
||
summary: 创作者公开统计聚合(P-OPN-08,@PermitAll 跨创作者公开读)
|
||
description: >-
|
||
创作者主页对外展示其公开统计:对该创作者 status==4(已发布) 作品聚合 { publishedCount, totalPlayCount, totalLikeCount }。
|
||
匿名可访问(@PermitAll),按 creatorId 路径参数取任意创作者(非登录人,不受归属过滤);公开 = 仅 status==4 一态,无 PII。
|
||
统计口径取 game_project 表的 play_count/like_count 计数列(同 feed 卡 / 创作者主页列表口径,发布态权威 + 计数同源)。
|
||
无公开作品时各计数返回 0(不报错)。
|
||
parameters:
|
||
- { name: creatorId, in: path, required: true, schema: { type: integer, format: int64 }, description: 创作者用户 ID }
|
||
responses:
|
||
'200':
|
||
description: 成功(创作者无公开作品时各字段为 0)
|
||
content:
|
||
application/json:
|
||
schema: { $ref: '#/components/schemas/CommonResultPublicCreatorSummary' }
|
||
|
||
# ===========================================================================
|
||
# 看板数据源(管理端 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:
|
||
# ---- Huijing 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 }
|
||
CommonResultMyStatSummary:
|
||
type: object
|
||
properties:
|
||
code: { type: integer }
|
||
data: { $ref: '#/components/schemas/MyStatSummaryRespVO' }
|
||
msg: { type: string }
|
||
CommonResultMyTrend:
|
||
type: object
|
||
properties:
|
||
code: { type: integer }
|
||
data:
|
||
type: object
|
||
properties:
|
||
list: { type: array, items: { $ref: '#/components/schemas/MyTrendItemVO' } }
|
||
msg: { type: string }
|
||
CommonResultPublicCreatorSummary:
|
||
type: object
|
||
properties:
|
||
code: { type: integer }
|
||
data: { $ref: '#/components/schemas/PublicCreatorSummaryRespVO' }
|
||
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 }
|
||
|
||
# ---- 创作者看板 VO(跨"我的作品"聚合,归属隔离)----
|
||
MyStatSummaryRespVO:
|
||
type: object
|
||
description: >-
|
||
创作者"我的作品"汇总(跨我的全部 gameId 累计全历史聚合)。
|
||
adRevenueTotal 当前恒为 0(聚合表无收益列,待 ad/trade 收益对接点;见文件头 ad/income 范围说明)。
|
||
properties:
|
||
totalPlay: { type: integer, format: int64, description: '我的全部作品累计试玩数(跨 gameId 求和 play_count)' }
|
||
totalComplete: { type: integer, format: int64, description: '我的全部作品累计完玩数(跨 gameId 求和 completed_count)' }
|
||
completionRate: { type: number, format: float, description: '完玩率 = totalComplete / totalPlay(totalPlay=0 兜 0,0-1 比值)' }
|
||
adRevenueTotal: { type: number, format: float, description: '我的全部作品累计广告收益(当前恒为 0,预留字段;待 ad/trade 收益对接点,避免越钱口径硬边界)' }
|
||
# P-OPN-08 创作者公开统计聚合(对外展示,无 PII;口径 = 该创作者 status==4 已发布作品的 play_count/like_count 累计)
|
||
PublicCreatorSummaryRespVO:
|
||
type: object
|
||
description: >-
|
||
创作者公开主页统计:对该创作者 status==4(已发布) 作品聚合发布数 + 累计试玩 + 累计点赞。
|
||
统计源 = game_project 表的 play_count/like_count 计数列(与 feed 卡 / 创作者公开作品列表同源;发布态权威在 project)。
|
||
无公开作品时各计数为 0(不报错)。
|
||
properties:
|
||
creatorId: { type: integer, format: int64, description: 创作者用户 ID(必返,作主页归属锚) }
|
||
publishedCount: { type: integer, format: int64, description: '该创作者已发布(status==4)作品数' }
|
||
totalPlayCount: { type: integer, format: int64, description: '该创作者已发布作品累计试玩数(跨作品求和 play_count)' }
|
||
totalLikeCount: { type: integer, format: int64, description: '该创作者已发布作品累计点赞数(跨作品求和 like_count)' }
|
||
MyTrendItemVO:
|
||
type: object
|
||
description: 我的作品某统计日的跨作品按日聚合项(趋势折线一个点)
|
||
properties:
|
||
statDate: { type: string, format: date, description: 统计日(yyyy-MM-dd) }
|
||
playCount: { type: integer, format: int64, description: '该日我的全部作品试玩数之和' }
|
||
completedCount: { type: integer, format: int64, description: '该日我的全部作品完玩数之和' }
|
||
playEndCount: { type: integer, format: int64, description: '该日我的全部作品试玩结束数之和' }
|
||
likeCount: { type: integer, format: int64, description: '该日我的全部作品点赞数之和' }
|
||
shareCount: { type: integer, format: int64, description: '该日我的全部作品分享数之和' }
|
||
|
||
# ===========================================================================
|
||
# 外部依赖对接点契约(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 排序信号'
|