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

272 lines
12 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 | 模块:project(黄金模板)| owner:WS1 lead 主笔,全员 review
# 端:/app-api(产品端 game-studio,用户 Token+DataPermission) /admin-api(管理端 game-admin,RBAC);前缀由 huijing 框架按 controller.app/admin 包名自动添加
# 错误码段:project = 1-100-***-***
# 响应统一 Huijing CommonResult 信封:{ code, data, msg };code=0 成功
info:
title: 绘境AI project 模块 API
version: 1.0.0
description: 游戏项目 CRUD / 状态机 / 版本 / 草稿 / 发布编排 / 审核 / 专区。前端据此 vite-plugin-mock 自动生成 mock。
servers:
- url: http://localhost:48080
description: 本地(Swagger/Knife4j http://localhost:48080/doc.html)
paths:
/app-api/project/my:
get:
tags: [app-project]
summary: 我的项目列表(创作者只见自己数据)
parameters:
- { name: status, in: query, required: false, schema: { type: integer }, 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/CommonResultProjectPage' }
/app-api/project:
post:
tags: [app-project]
summary: 创建项目(初始 status=0 草稿)
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/ProjectCreateReqVO' }
responses:
'200':
description: 返回新建项目 ID
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultLong' }
/app-api/project/{id}:
get:
tags: [app-project]
summary: 项目详情
parameters:
- { name: id, in: path, required: true, schema: { type: integer, format: int64 } }
responses:
'200':
description: 成功
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultProject' }
/app-api/project/{id}/draft:
put:
tags: [app-project]
summary: 保存草稿(仅 status=0 可改;标题/简介/封面/标签/适龄)
parameters:
- { name: id, in: path, required: true, schema: { type: integer, format: int64 } }
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/ProjectDraftReqVO' }
responses:
'200':
description: 成功
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultBoolean' }
/app-api/project/{id}/publish:
post:
tags: [app-project]
summary: 提交发布(发布前门禁聚合 → 进审核 BPM;统一发布编排 compliance→runtime→feed 失败回滚 ←G6)
description: >-
归属/草稿态为安全前置,失败抛异常(code≠0)。发布前门禁聚合为 gates:全部通过则 admitted=true 并置 status=1 审核中;
任一拦截则 admitted=false 且不改状态机(无副作用),创作者据 gates 修复后重提。
parameters:
- { name: id, in: path, required: true, schema: { type: integer, format: int64 } }
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/ProjectPublishReqVO' }
responses:
'200':
description: 成功(含发布前门禁聚合结果 admitted + gates)
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultProjectPublish' }
/app-api/project/public/creator/{creatorId}:
get:
tags: [app-project]
summary: 创作者公开作品聚合(P-OPN-08,@PermitAll 跨创作者公开读)
description: >-
创作者主页对外展示其公开作品列表。公开 = 项目 status==4(已发布) 仅此一态(排除 5下架/6封禁/草稿/审核中等);
无 PII,仅作品基础信息 + 聚合计数。匿名可访问(@PermitAll),按 creatorId 路径参数取任意创作者数据(非登录人,
故不受创作者归属过滤)。creatorName 尽力解析(查不到为 null),creatorId 必返。
parameters:
- { name: creatorId, in: path, required: true, schema: { type: integer, format: int64 }, description: 创作者用户 ID }
- { 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: 成功(仅 status==4 已发布作品)
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultPublicCreatorProjectPage' }
/admin-api/project/review/page:
get:
tags: [admin-project]
summary: 审核队列(管理端 RBAC)
parameters:
- { name: status, in: query, required: false, schema: { type: integer }, description: 默认取 status=1 审核中 }
- { 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/CommonResultProjectPage' }
/admin-api/project/review:
post:
tags: [admin-project]
summary: 审核决策(通过/拒绝/下架;锁风门 Gate 二态聚合)
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/ReviewReqVO' }
responses:
'200':
description: 成功
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultBoolean' }
components:
schemas:
# ---- Huijing CommonResult 信封 ----
CommonResultBoolean:
type: object
properties:
code: { type: integer, description: '0=成功,非0=错误码 1-100-***-***' }
data: { type: boolean }
msg: { type: string }
CommonResultLong:
type: object
properties:
code: { type: integer }
data: { type: integer, format: int64, description: 新建项目 ID }
msg: { type: string }
CommonResultProject:
type: object
properties:
code: { type: integer }
data: { $ref: '#/components/schemas/ProjectRespVO' }
msg: { type: string }
CommonResultProjectPublish:
type: object
properties:
code: { type: integer }
data: { $ref: '#/components/schemas/ProjectPublishRespVO' }
msg: { type: string }
CommonResultProjectPage:
type: object
properties:
code: { type: integer }
data:
type: object
properties:
list: { type: array, items: { $ref: '#/components/schemas/ProjectRespVO' } }
total: { type: integer, format: int64 }
msg: { type: string }
CommonResultPublicCreatorProjectPage:
type: object
properties:
code: { type: integer }
data:
type: object
properties:
list: { type: array, items: { $ref: '#/components/schemas/PublicCreatorProjectRespVO' } }
total: { type: integer, format: int64 }
msg: { type: string }
# ---- 请求/响应 VO ----
ProjectCreateReqVO:
type: object
required: [title]
properties:
title: { type: string, maxLength: 60, description: 标题 }
templateId: { type: string, description: '玩法模板 ID(可选,缺省 generic:W-CLEAN 废玩法模板层后建项目无需选模板,入口将空 templateId 归一为 generic 单一通用口径)' }
ProjectDraftReqVO:
type: object
properties:
title: { type: string, maxLength: 60 }
summary: { type: string, maxLength: 500 }
coverUrl: { type: string }
tags: { type: array, items: { type: string }, maxItems: 10 }
ageRating: { type: string, enum: [all, '8+', '12+', '16+'] }
ProjectPublishReqVO:
type: object
required: [versionId, launchZoneId]
properties:
versionId: { type: integer, format: int64, description: 发布的版本 ID }
launchZoneId: { type: integer, format: int64, description: 发布去向专区(launchZone ←G1) }
GateResultVO:
type: object
required: [key, pass]
properties:
key: { type: string, description: '门禁键(稳定标识,如 age;Phase C 增 compliance)' }
label: { type: string, description: 门禁中文标签(展示用) }
pass: { type: boolean, description: 是否通过 }
code: { type: string, description: '拦截时错误码字符串(pass=true 时为 null;复用对应模块错误码段,不新增)' }
message: { type: string, description: 拦截时提示文案(pass=true 时为 null) }
ProjectPublishRespVO:
type: object
required: [admitted, gates]
properties:
admitted: { type: boolean, description: '是否准入(全部门禁通过=true;任一拦截=false 且不改状态机)' }
gates: { type: array, items: { $ref: '#/components/schemas/GateResultVO' }, description: 发布前门禁逐项结果 }
ReviewReqVO:
type: object
required: [gameId, versionId, decision]
properties:
gameId: { type: integer, format: int64 }
versionId: { type: integer, format: int64 }
decision: { type: integer, enum: [1, 2, 3], description: '1通过 2拒绝 3下架' }
reason: { type: string, maxLength: 500, description: 拒绝/下架理由(回填创作者) }
ProjectRespVO:
type: object
properties:
id: { type: integer, format: int64 }
title: { type: string }
summary: { type: string }
coverUrl: { type: string }
tags: { type: array, items: { type: string } }
templateId: { type: string }
ageRating: { type: string }
status: { type: integer, description: '0草稿 1审核中 2已通过 3已拒绝 4已发布 5已下架 6已封禁' }
currentVersionId: { type: integer, format: int64 }
launchZoneId: { type: integer, format: int64 }
featured: { type: boolean }
playCount: { type: integer, format: int64 }
likeCount: { type: integer, format: int64 }
createTime: { type: string, format: date-time }
# P-OPN-08 创作者公开作品卡(对外展示,无 PII;仅 status==4 已发布作品才会出现在列表)
PublicCreatorProjectRespVO:
type: object
properties:
id: { type: integer, format: int64, description: 游戏项目 ID }
title: { type: string, description: 标题 }
summary: { type: string, description: 简介 }
coverUrl: { type: string, description: 封面图 URL }
ageRating: { type: string, description: '适龄提示:all/8+/12+/16+' }
playCount: { type: integer, format: int64, description: 累计试玩数 }
likeCount: { type: integer, format: int64, description: 累计点赞数 }
publishTime: { type: string, format: date-time, description: '发布时间(项目表无独立 publish_time 列,取 update_time 近似,同 count-published 口径)' }
creatorId: { type: integer, format: int64, description: 创作者用户 ID(必返,作主页归属锚) }
creatorName: { type: string, description: 创作者昵称(尽力解析;查不到为 null,无 PII) }