lili c7a58b8c45 feat(project,telemetry): P-OPN-08 创作者公开聚合API@PermitAll(仅status==4·聚合统计无PII)
新增创作者公开主页两只读端点(契约+实现+单测,全 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>
2026-06-18 13:43:08 -07:00

337 lines
21 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 查看板数据源);前缀由 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 排序信号'