openapi: 3.0.3 # 契约 #8 API | 模块:community(通知底座:站内信 + 创作者等级 + 新人奖励触发)| owner:WS5,全员 review # 设计依据:docs/agent-specs/2026-06-11-Wave4-community-biz-execution.md §4.3/§4.4(D-B 通知底座最小集,覆盖 5/5 P0)。 # 端:/app-api(产品端 game-studio:我的站内信中心 / 未读角标 / 我的等级 / 我的奖励,用户 Token + Mapper 强制 getLoginUserId() 归属谓词) # /rpc-api(CommunityNotifyApi 四方法,供上游 aigc/project/trade 同进程 @Primary 调用,非对外业务路由) # /admin-api(staging only:四类投递 mock-trigger,上游未接通前独立验收) # 前缀由 huijing 框架按 controller.app/admin 包名自动添加;本文件 path 直接写含前缀的完整对外路径。 # 错误码段(落 game 业务段 1-107-***-***,模块归属决定,contracts/README.md §四 已在册): # 1-107-001-*** = 站内信(消息不存在/非本人消息/已读状态非法…) # 1-107-002-*** = 创作者等级(等级记录不存在/计数累加异常…) # 1-107-003-*** = 新人奖励(里程碑重复触发被幂等拦截…) # 响应统一 Huijing CommonResult 信封:{ code, data, msg };code=0 成功。 # 触达机制(评审版 §3.3 定型):aigc 生成完成 / compliance·project 审核出结果 / trade 收益变动后, # 各自 service 在本地事务【提交后】同进程调用 community 暴露的 CommunityNotifyApi(复用 @Primary 本地实现)写站内信; # 不新建 MQ、不动 events.schema.json。CommunityNotifyApiImpl 入口注入系统身份 LoginUser(id=0) + finally clearContext。 # 幂等:站内信以 (userId,type,bizRef) 去重(重复投递不重复落库);里程碑以 (creator_user_id,milestone) 唯一键幂等。 # 归属隔离(R-medium PII 红线):客户侧端点必须在 Mapper 查询里强制以登录态 user_id 作谓词 enforce(可信边界,非 @DataPermission), # 前端不够;用户只见自己站内信/等级/奖励,不得读他人 PII。 # 边界 OUT:SOC/GRW 域(关系图谱/排行/成就/扇出/邮件,全 P1/P2)仅留接口桩、本波不实现; # reward 真实记账/到账(记入钱包/流量包)留 M4,本波只写自有 reward 触发记录表。 info: title: 绘境AI community 模块 API version: 1.0.0 description: >- 通知底座最小集:站内消息中心(系统/公告/审核/收益四类聚合 + 未读角标 + 标记已读)+ 创作者等级(published_count 计数权威源 = project 发布成功同进程 notify 累加,新人里程碑触发)+ 新人奖励触发记录(里程碑幂等触发,真实发放留 M4)。 上游 aigc/project/trade 经 CommunityNotifyApi(@Primary 本地实现)在本地事务提交后同进程投递站内信。 覆盖产品功能 P-NTF-01/02/03/05(通知触达)+ P-INC-01(创作者成长激励计数)。 前端据此 vite-plugin-mock 自动生成 mock。 servers: - url: http://localhost:48080 description: 本地(Swagger/Knife4j http://localhost:48080/doc.html) paths: # =========================================================================== # app 端(产品端 game-studio):我的站内信中心 / 未读角标 / 我的等级 / 我的奖励 # 全部用户 Token,Mapper 强制 getLoginUserId() 归属谓词,用户只见自己数据。 # =========================================================================== /app-api/community/messages: get: tags: [app-community] summary: 站内信分页列表(P-NTF-01) description: >- 我的消息中心列表,按已读态 + 创建时间倒序。 归属隔离:Mapper 强制 user_id = getLoginUserId() 谓词(可信边界),用户只见自己消息。 parameters: - { name: type, in: query, required: false, schema: { type: integer, enum: [1, 2, 3, 4] }, description: '消息类型:1系统 2公告 3审核 4收益(可选,不传查全部)' } - { name: readStatus, in: query, required: false, schema: { type: integer, enum: [0, 1] }, description: '已读状态:0未读 1已读(可选)' } - { name: cursor, in: query, required: false, schema: { type: integer, format: int64 }, description: '游标分页:上一页末条 id(可选,首页不传)' } - { name: size, in: query, required: false, schema: { type: integer, default: 20 }, description: '每页条数(默认 20)' } responses: '200': description: 站内信分页结果(CommonResult 信封) content: application/json: schema: type: object properties: code: { type: integer, example: 0 } data: type: object properties: list: { type: array, items: { $ref: '#/components/schemas/CommunityMessageRespVO' } } nextCursor: { type: integer, format: int64, nullable: true, description: '下一页游标,null 表示无更多' } msg: { type: string, example: '' } /app-api/community/messages/{id}/read: post: tags: [app-community] summary: 标记单条已读(P-NTF-01) description: >- 标记单条站内信已读,落 read_status=1 + read_time。 归属隔离:service 校验该消息 user_id == getLoginUserId(),非本人消息拒绝(1-107-001-***)。 parameters: - { name: id, in: path, required: true, schema: { type: integer, format: int64 }, description: '消息 ID' } responses: '200': description: 标记结果(CommonResult) content: application/json: schema: type: object properties: code: { type: integer, example: 0 } data: { type: boolean, example: true } msg: { type: string, example: '' } /app-api/community/messages/unread-count: get: tags: [app-community] summary: 未读数角标(P-NTF-01) description: 未读消息数角标,按 type 分组计数 + 总未读数。归属隔离同上。 responses: '200': description: 未读计数(CommonResult) content: application/json: schema: type: object properties: code: { type: integer, example: 0 } data: type: object properties: total: { type: integer, description: '总未读数' } byType: type: object description: '按四类分组未读数(key=type 1/2/3/4,value=count)' additionalProperties: { type: integer } msg: { type: string, example: '' } /app-api/community/level: get: tags: [app-community] summary: 当前创作者等级 + published_count + 下一里程碑进度(P-INC-01) description: >- 当前登录创作者的等级记录:level + published_count + 距下一新人里程碑进度(如 2/3)。 published_count 权威源 = project 发布成功后同进程 notify 原子累加。 归属隔离:creator_user_id = getLoginUserId()。无记录时返回初始化默认(level=1/count=0)。 responses: '200': description: 创作者等级(CommonResult) content: application/json: schema: type: object properties: code: { type: integer, example: 0 } data: { $ref: '#/components/schemas/CommunityLevelRespVO' } msg: { type: string, example: '' } /app-api/community/rewards: get: tags: [app-community] summary: 我的奖励触发记录列表(P-INC-01) description: >- 我的新人奖励触发记录列表,status 触发态(0已触发待发放)/发放态(1已发放,M4 回写)均可见。 归属隔离:creator_user_id = getLoginUserId()。 responses: '200': description: 奖励触发记录列表(CommonResult) content: application/json: schema: type: object properties: code: { type: integer, example: 0 } data: { type: array, items: { $ref: '#/components/schemas/CommunityRewardRespVO' } } msg: { type: string, example: '' } # =========================================================================== # app 端(U3 R-SOC 社区社交):评论 / 关注 / 排行 / 弹幕(additive,2026-06-18 plan002 U3) # 错误码段:评论 1-107-004 / 关注 1-107-005 / 排行 1-107-006 / 弹幕 1-107-007。 # 写端点(发评论/删评论/举报/关注/取关/发弹幕)需用户 Token,归属/发起方一律 getLoginUserId()(非前端入参)。 # 公开读:评论列表/计数、排行 TopN、弹幕回放无需登录。举报转 compliance:落本端举报态 + 事务提交后接缝(compliance 受理 -api 待提供,OPEN ITEM)。 # 弹幕实时接收走 WebSocket(type="danmaku",本契约仅描述 HTTP;WS 路径见后端 huijing.websocket.path),HTTP 发送为兜底入口。 # =========================================================================== /app-api/community/comments: post: tags: [app-community] summary: 发表评论(R-SOC) description: 需登录;作者 = 当前登录用户(token 解析,非前端入参)。内容非空。 requestBody: required: true content: application/json: schema: type: object required: [targetType, targetId, content] properties: targetType: { type: integer, enum: [1], default: 1, description: '评论目标类型:1游戏' } targetId: { type: integer, format: int64, description: '评论目标 ID(gameId)' } content: { type: string, maxLength: 1024, description: '评论正文' } parentId: { type: integer, format: int64, default: 0, description: '父评论 ID(0=一级,MVP 不展开树)' } responses: '200': description: 新评论 ID(CommonResult) content: application/json: schema: type: object properties: code: { type: integer, example: 0 } data: { type: integer, format: int64, example: 1 } msg: { type: string, example: '' } get: tags: [app-community] summary: 评论分页列表(R-SOC) description: 公开读;按 target_type+target_id 维度,id 倒序。 parameters: - { name: targetType, in: query, required: false, schema: { type: integer, enum: [1], default: 1 }, description: '目标类型:1游戏' } - { name: targetId, in: query, required: true, schema: { type: integer, format: int64 }, description: '目标 ID(gameId)' } - { name: pageNo, in: query, required: false, schema: { type: integer, default: 1 }, description: '页码,从 1 开始' } - { name: pageSize, in: query, required: false, schema: { type: integer, default: 10 }, description: '每页条数(最大 200)' } responses: '200': description: 评论分页结果(CommonResult) content: application/json: schema: type: object properties: code: { type: integer, example: 0 } data: type: object properties: list: { type: array, items: { $ref: '#/components/schemas/CommunityCommentRespVO' } } total: { type: integer, format: int64, example: 100 } msg: { type: string, example: '' } /app-api/community/comments/{id}: delete: tags: [app-community] summary: 删除评论(R-SOC) description: 需登录;先校验归属当前登录用户,防越权删他人评论(数据边界红线)。 parameters: - { name: id, in: path, required: true, schema: { type: integer, format: int64 }, description: '评论 ID' } responses: '200': description: CommonResult content: { application/json: { schema: { $ref: '#/components/schemas/CommonResultBoolean' } } } /app-api/community/comments/count: get: tags: [app-community] summary: 评论数(R-SOC) description: 某目标的评论总数(公开计数)。 parameters: - { name: targetType, in: query, required: false, schema: { type: integer, enum: [1], default: 1 } } - { name: targetId, in: query, required: true, schema: { type: integer, format: int64 }, description: '目标 ID(gameId)' } responses: '200': description: CommonResult content: application/json: schema: type: object properties: code: { type: integer, example: 0 } data: { type: integer, format: int64, example: 42 } msg: { type: string, example: '' } /app-api/community/comments/{id}/report: post: tags: [app-community] summary: 举报评论(R-SOC) description: >- 需登录;理由必填;落本端举报态(report_status=1)幂等 + 事务提交后转 compliance 受理。 OPEN ITEM:compliance 当前无举报受理 -api,本波仅留接缝桩,接通后经 -api 调用 + 幂等 + 补偿。 parameters: - { name: id, in: path, required: true, schema: { type: integer, format: int64 }, description: '被举报评论 ID' } requestBody: required: true content: application/json: schema: type: object required: [reason] properties: reason: { type: string, maxLength: 200, description: '举报理由(必填)' } responses: '200': description: CommonResult content: { application/json: { schema: { $ref: '#/components/schemas/CommonResultBoolean' } } } /app-api/community/follow/{targetUserId}: post: tags: [app-community] summary: 关注(R-SOC) description: 需登录;发起方 = 当前登录用户;幂等(重复关注同态不写);不能关注自己;并发由 DB 唯一键 CAS 收敛。 parameters: - { name: targetUserId, in: path, required: true, schema: { type: integer, format: int64 }, description: '被关注方用户 ID' } responses: '200': description: 关注结果(CommonResult) content: application/json: schema: type: object properties: code: { type: integer, example: 0 } data: { $ref: '#/components/schemas/CommunityFollowResultRespVO' } msg: { type: string, example: '' } delete: tags: [app-community] summary: 取关(R-SOC) description: 需登录;幂等(反激活 active=0,不删行)。 parameters: - { name: targetUserId, in: path, required: true, schema: { type: integer, format: int64 }, description: '被关注方用户 ID' } responses: '200': description: 取关结果(CommonResult) content: application/json: schema: type: object properties: code: { type: integer, example: 0 } data: { $ref: '#/components/schemas/CommunityFollowResultRespVO' } msg: { type: string, example: '' } /app-api/community/follow/stat/{userId}: get: tags: [app-community] summary: 关注统计(R-SOC) description: 某用户粉丝数/关注数 + 当前登录者是否已关注 ta(未登录则 followedByMe=false)。 parameters: - { name: userId, in: path, required: true, schema: { type: integer, format: int64 }, description: '查询的用户 ID' } responses: '200': description: 关注统计(CommonResult) content: application/json: schema: type: object properties: code: { type: integer, example: 0 } data: { $ref: '#/components/schemas/CommunityFollowStatRespVO' } msg: { type: string, example: '' } /app-api/community/rank: get: tags: [app-community] summary: 排行榜单 TopN(R-SOC) description: >- 公开读;维度须为已定义维度(game_hot 游戏热度 / creator_hot 创作者热度)。 优先读 Redis ZSET(快路径),ZSET 缺失自动从 DB 权威源回填重建(读路径不空窗,最终一致)。 parameters: - { name: dimension, in: query, required: true, schema: { type: string, enum: [game_hot, creator_hot] }, description: '排行维度' } - { name: topN, in: query, required: false, schema: { type: integer, default: 10 }, description: '取前 N 名' } responses: '200': description: 榜单(CommonResult) content: application/json: schema: type: object properties: code: { type: integer, example: 0 } data: { type: array, items: { $ref: '#/components/schemas/CommunityRankItemRespVO' } } msg: { type: string, example: '' } /app-api/community/danmaku: post: tags: [app-community] summary: 发送弹幕(R-SOC) description: >- 需登录;落库(可回放)+ 触发 WebSocket 房间(gameId)广播给在线观众;发送者 = 当前登录用户。 实时接收走 WebSocket(type="danmaku");本 HTTP 发送为不开 WS 客户端的兜底入口。 单实例边界:多实例需切 huijing.websocket.sender-type=redis(MQ future-state 未部署)。 requestBody: required: true content: application/json: schema: type: object required: [gameId, content] properties: gameId: { type: integer, format: int64, description: '游戏 ID(弹幕房间)' } content: { type: string, maxLength: 255, description: '弹幕正文' } playTime: { type: integer, default: 0, description: '播放进度(秒,MVP 可为 0)' } responses: '200': description: 已落库弹幕(CommonResult,同时是 WS 广播载荷) content: application/json: schema: type: object properties: code: { type: integer, example: 0 } data: { $ref: '#/components/schemas/CommunityDanmakuRespVO' } msg: { type: string, example: '' } /app-api/community/danmaku/recent: get: tags: [app-community] summary: 最近弹幕回放(R-SOC) description: 公开读;某游戏最近 N 条弹幕(进房补帧),按时间正序。 parameters: - { name: gameId, in: query, required: true, schema: { type: integer, format: int64 }, description: '游戏 ID(房间)' } - { name: limit, in: query, required: false, schema: { type: integer, default: 50 }, description: '取近 N 条' } responses: '200': description: 弹幕列表(CommonResult) content: application/json: schema: type: object properties: code: { type: integer, example: 0 } data: { type: array, items: { $ref: '#/components/schemas/CommunityDanmakuRespVO' } } msg: { type: string, example: '' } # =========================================================================== # RPC 端(/rpc-api/community/*):CommunityNotifyApi 四方法 # 供上游 aigc/project/trade 同进程 @Primary 调用,非对外业务路由(@FeignClient(name="community-server"))。 # 实现入口注入系统身份 LoginUser(id=0) + finally clearContext;统一 CommonResult 信封。 # =========================================================================== /rpc-api/community/notify/generate-done: post: tags: [rpc-community] summary: '[RPC] P-NTF-02 生成完成通知' description: >- aigc 在 DifyCallbackServiceImpl.handleCallback 提交后调;触发判定 = 提交后回查任务为 SUCCEEDED 终态且 versionId 已回填 (非 result==TRUE);三实参均取自回查的 AigcTaskDO(creatorUserId/gameId/versionId)。落 type=1 系统消息。 parameters: - { name: creatorUserId, in: query, required: true, schema: { type: integer, format: int64 }, description: '创作者用户 ID(取自回查任务)' } - { name: gameId, in: query, required: true, schema: { type: integer, format: int64 }, description: '游戏 ID(取自回查任务)' } - { name: versionId, in: query, required: true, schema: { type: integer, format: int64 }, description: '版本 ID(取自回查任务)' } responses: '200': description: CommonResult content: { application/json: { schema: { $ref: '#/components/schemas/CommonResultBoolean' } } } /rpc-api/community/notify/review-result: post: tags: [rpc-community] summary: '[RPC] P-NTF-03 审核结果通知' description: >- project 在 reviewProject 提交后调;decision 三态(1通过/2拒绝/3下架,对齐 ReviewDecisionEnum)—— 本波仅对 1/2 发通知(caller 侧以 decision∈{1,2} 守门),reject(2) 须带 reason。落 type=3 审核消息。 parameters: - { name: creatorUserId, in: query, required: true, schema: { type: integer, format: int64 }, description: '创作者用户 ID' } - { name: gameId, in: query, required: true, schema: { type: integer, format: int64 }, description: '游戏 ID' } - { name: decision, in: query, required: true, schema: { type: integer, enum: [1, 2] }, description: '审核决定:1通过 2拒绝(3下架本波不发)' } - { name: reason, in: query, required: false, schema: { type: string }, description: '拒绝原因(decision=2 必带)' } responses: '200': description: CommonResult content: { application/json: { schema: { $ref: '#/components/schemas/CommonResultBoolean' } } } /rpc-api/community/notify/income-changed: post: tags: [rpc-community] summary: '[RPC] P-NTF-05 收益变动通知' description: >- trade 在 SettlementServiceImpl.settle 入账成功后调(仅 firstRecord 首次真实入账才通知,幂等复跑不重发); netAmount=创作者实得净额(分)。落 type=4 收益消息。 parameters: - { name: creatorUserId, in: query, required: true, schema: { type: integer, format: int64 }, description: '创作者用户 ID' } - { name: source, in: query, required: true, schema: { type: integer }, description: '收益来源(对齐 trade 来源枚举)' } - { name: sourceRef, in: query, required: true, schema: { type: string }, description: '来源业务键(settleId 等)' } - { name: netAmount, in: query, required: true, schema: { type: integer, format: int64 }, description: '创作者实得净额(分)' } responses: '200': description: CommonResult content: { application/json: { schema: { $ref: '#/components/schemas/CommonResultBoolean' } } } /rpc-api/community/notify/project-published: post: tags: [rpc-community] summary: '[RPC] P-INC-01 发布计数(方案a)' description: >- project 发布成功后同进程 notify(与 notifyReviewResult 合并在同一 caller,isApprove 时多调一次); 累加 published_count 并触发新人里程碑(幂等)。落 type=1 系统消息(里程碑达成时)。 parameters: - { name: creatorUserId, in: query, required: true, schema: { type: integer, format: int64 }, description: '创作者用户 ID' } - { name: gameId, in: query, required: true, schema: { type: integer, format: int64 }, description: '游戏 ID' } responses: '200': description: CommonResult content: { application/json: { schema: { $ref: '#/components/schemas/CommonResultBoolean' } } } # =========================================================================== # admin 端(staging only):四类投递 mock-trigger # 上游未接通前独立验收四类投递,仅 dev/staging 生效(@ConditionalOnProperty(name="community.mock-trigger.enabled", # havingValue="true") 与现有灰度开关风格一致;或 @Profile({"dev","staging"}),二选一钉死),生产关闭。 # =========================================================================== /admin-api/community/notifications/{type}/mock-trigger: post: tags: [admin-community] summary: '[staging only] 四类投递 mock-trigger' description: >- 上游未接通前独立验收四类站内信投递,仅 dev/staging 生效,生产关闭。 实查全 game-module 无 @Profile 先例(现有差异化手段是 @ConditionalOnProperty 灰度开关),建设时二选一钉死。 parameters: - { name: type, in: path, required: true, schema: { type: integer, enum: [1, 2, 3, 4] }, description: '投递类型:1系统 2公告 3审核 4收益' } requestBody: required: false content: application/json: schema: type: object properties: userId: { type: integer, format: int64, description: '目标收件人 user_id(mock 指定)' } title: { type: string, description: '消息标题' } content: { type: string, description: '消息正文' } bizRef: { type: string, description: '业务关联键' } responses: '200': description: CommonResult content: { application/json: { schema: { $ref: '#/components/schemas/CommonResultBoolean' } } } components: schemas: CommonResultBoolean: type: object description: Huijing 统一响应信封(Boolean 数据) properties: code: { type: integer, example: 0 } data: { type: boolean, example: true } msg: { type: string, example: '' } CommunityMessageRespVO: type: object description: 站内消息(我的消息中心列表项) properties: id: { type: integer, format: int64, description: '消息 ID' } type: { type: integer, description: '消息类型:1系统 2公告 3审核 4收益' } bizRef: { type: string, description: '业务关联键(供前端跳转)' } title: { type: string, description: '消息标题' } content: { type: string, description: '消息正文(审核拒绝含原因)' } readStatus: { type: integer, description: '已读状态:0未读 1已读' } readTime: { type: string, format: date-time, nullable: true, description: '已读时间' } createTime: { type: string, format: date-time, description: '创建时间' } CommunityLevelRespVO: type: object description: 创作者等级 + 计数 + 下一里程碑进度 properties: level: { type: integer, description: '创作者等级:1小白(MVP)' } publishedCount: { type: integer, description: '已发布作品数(计数权威源)' } lastMilestone: { type: string, description: '末次已触发里程碑键(如 publish_3)' } nextMilestone: { type: string, nullable: true, description: '下一里程碑键(null 表示已达顶)' } nextMilestoneTarget: { type: integer, nullable: true, description: '下一里程碑目标作品数(如 3)' } CommunityRewardRespVO: type: object description: 新人奖励触发记录 properties: id: { type: integer, format: int64, description: '奖励触发记录 ID' } milestone: { type: string, description: '里程碑键(如 publish_3)' } rewardType: { type: integer, description: '奖励类型:1流量包 2现金' } rewardAmount: { type: integer, format: int64, description: '奖励额度(流量包=曝光量;现金=分)' } status: { type: integer, description: '发放状态:0已触发待发放(本波终态) 1已发放(M4)' } grantedRef: { type: string, description: 'M4 发放回执(本波空)' } createTime: { type: string, format: date-time, description: '触发时间' } # ====================== U3 R-SOC 社区社交(additive) ====================== CommunityCommentRespVO: type: object description: 社区评论(列表项) properties: id: { type: integer, format: int64, description: '评论 ID' } targetType: { type: integer, description: '评论目标类型:1游戏' } targetId: { type: integer, format: int64, description: '评论目标 ID(gameId)' } userId: { type: integer, format: int64, description: '评论作者用户 ID' } content: { type: string, description: '评论正文' } parentId: { type: integer, format: int64, description: '父评论 ID(0=一级评论)' } reportStatus: { type: integer, description: '举报态:0正常 1被举报' } createTime: { type: string, format: date-time, description: '创建时间' } CommunityFollowResultRespVO: type: object description: 关注/取关结果(回显切换后关注态 + 被关注方最新粉丝数) properties: targetUserId: { type: integer, format: int64, description: '被关注方用户 ID' } following: { type: boolean, description: '切换后是否已关注:true=已关注 false=已取关' } followerCount: { type: integer, format: int64, description: '被关注方最新粉丝数' } CommunityFollowStatRespVO: type: object description: 关注统计(粉丝数 + 关注数 + 当前登录者是否已关注 ta) properties: userId: { type: integer, format: int64, description: '查询的用户 ID' } followerCount: { type: integer, format: int64, description: '粉丝数(被多少 active 用户关注)' } followingCount: { type: integer, format: int64, description: '关注数(active 关注了多少人)' } followedByMe: { type: boolean, description: '当前登录者是否已关注 ta(未登录则恒 false)' } CommunityRankItemRespVO: type: object description: 排行榜单项 properties: rank: { type: integer, description: '名次(从 1 开始)' } targetId: { type: integer, format: int64, description: '排行对象 ID(game_hot=gameId / creator_hot=creatorUserId)' } score: { type: integer, format: int64, description: '排行分值' } CommunityDanmakuRespVO: type: object description: 弹幕(回放列表项 + WebSocket 广播载荷,type="danmaku" 的 content) properties: id: { type: integer, format: int64, description: '弹幕 ID' } gameId: { type: integer, format: int64, description: '游戏 ID(房间)' } userId: { type: integer, format: int64, description: '发送者用户 ID' } content: { type: string, description: '弹幕正文' } playTime: { type: integer, description: '播放进度(秒)' } createTime: { type: string, format: date-time, description: '发送时间' }