- 链路②: admin审核台真UI走查闭合(CDP-on-mini harness: admin_walk.py/run_walk.sh)—— 创作者提交→admin点9137「通过」→项目PUBLISHED(4)+feed_rank写入实证(队列3→2+DB);附带修admin构建腐化(vite8→钉5.1.4清装) - Wave4评审版 HJ-WAVE4-001 + 评审纪要(两轮对抗评审R1 23+R2 6全接受) + 创始人拍板D-A/D-B/D-C回填+ERRATA回流 - Wave4 execution版 HJ-WAVE4-EXEC-001(4路深度recon→起草→三镜头评审逮aigc回调blocker+PII口径订正;主agent抽查file:line属实) - 总账§3链路②行+§6波次史、作战清单⑩ 同步收口 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
83 KiB
Wave4(community + biz)· execution 版
- 文档编号:HJ-WAVE4-EXEC-001
- 日期:2026-06-11
- 状态:待实现 agent 认领 + 主 agent 验收(评审版三项战略决策 D-A/D-B/D-C 已创始人拍板,见 §0)
- 唯一设计依据:
docs/agent-specs/2026-06-11-Wave4-community-biz-review.md(编号 HJ-WAVE4-001,已两轮对抗评审定稿)+docs/agent-specs/2026-06-11-Wave4-community-biz-review-评审纪要.md(R1 24 条 + R2 6 条逐条处置)。本文不得与评审版矛盾;执行中发现矛盾,停工上报,以评审版为准。 - 读者:实现 agent 分工认领(建议 community 先、biz 后,单序列资源由 §0 单 agent 收口);主 agent 验收。
- 环境铁律(继承项目记忆
internal-build-infra-servers/m1-runtime-bringup-state/frontend-spine-built):- 重型构建/测试一律上 mini-desktop(
ssh mini-desktop,15G 内存,有 java17/mvn/docker);本机严禁 mvn/npm build(小内存会 OOM);源码同步走git push(阿里云 Giteassh://git@101.200.34.71:2222)→ mini-desktopgit pull(scp 源码树被分类器拦)。 - staging app 在 mini-desktop 隔离容器
http://100.64.0.7:48080(对内http://localhost:48080)。 - 含中文 SQL 在 mini-desktop 执行务必
--default-character-set=utf8mb4(防乱码,记忆铁律)。 - 「骨架编译过 ≠ 真实可验收」(评审版 R10 铁律 + 记忆
golden-loop-b1-done/frontend-link-ui-walk-publish-fix):完成条件强制要求真实写链路 staging 实测(§9.3 两条系统身份写链路)+ biz/community UI 入口真人走查(§9.4),不以批跑/API 全绿替代。
- 重型构建/测试一律上 mini-desktop(
0. 拍板前提(评审版 §0.1 / §7,本文全按拍板结果起草)
| 决策 | 拍板结果(本文落点) |
|---|---|
| D-A biz 形态 | 轻量 lead form 先行——只交付「询单→报价→进度→demo 确认」信息流闭环,资金流(收款/分账/在线签章)留 M4(§7 biz 建设 + §10 M4 留债) |
| D-B community 功能优先级 | 通知底座最小集——T-CMU-10 编排 + T-CMU-06 站内信 + T-CMU-11 等级引擎(+可选短信 T-CMU-09),一次覆盖 5/5 P0;SOC/GRW 域仅留接口桩、不实现(§6 community 建设) |
| D-C 是否本波接 ip seam | 不接、维持 D5 推迟——本波 P-BIZ-02 直接复用 aigc 4 模板 + runtime 沙箱(绕开 T-IP-09 模板市场),无需 ip seam |
| 建设次序(execution 单 agent 定) | 先 community 后 biz(community 触达机制最独立、复用面集中、风险最低;biz 外部对接多、受 pay 桩制约,适合后段) |
| P-INC-01 计数权威源(execution §0 单 agent 拍) | (a) project 发布成功后同进程 notify community 计数(与三上游同范式;实查 ProjectApi 无 countPublishedByCreator 方法,方案 b 需 project 新增端点,故取 a 上游单点改动)。详见 §6.4 |
1. 目标与范围边界
1.1 目标
交付 Wave4 总 11 P0 = community 5 + biz 6 可验证,不破坏已建 11 模块(错误码段/Flyway 序号/前缀/契约文件名零冲突)。
- community(5 P0 = P-NTF-01/02/03/05 + P-INC-01):通知底座(编排 + 站内信 + 等级引擎)。触达机制 = 同进程
-apinotify-push(评审版 §3.3 定型):aigc 生成完成 / compliance·project 审核出结果 / trade 收益变动后,由各自 service 在本地事务提交后同进程调用 community 暴露的CommunityNotifyApi(复用@Primary本地实现),写站内信。不新建 MQ、不动events.schema.json。 - biz(6 P0 = P-BIZ-01/02/03/04/08/12):轻量 lead form 信息流闭环(询单→报价→进度看板→demo 试玩确认),复用 aigc 4 模板 / runtime 取包 / admin 队列 / project 轻量状态机范式。资金流(收款/分账/在线签章)留 M4。
1.2 明确不做(继承评审版 §1 非目标,实现 agent 不得擅自扩界)
- biz 不收款结算(T-BIZ-08 收费、T-BIZ-03 在线电子签章)→ 本波只到「状态机走通 + 线下签署后台标记兜底」,资金流并入 M4。
- community 不全量铺开 T-CMU 全家桶——SOC/GRW 域(关系图谱/排行/成就/扇出/邮件,全 P1/P2)只留接口桩、不实现。
- 不引入 Flowable / yudao-bpm(当前注释未装配)——复用 project 已落地的轻量状态机范式(
ProjectStatusEnum+ProjectServiceImpl写前流转校验,无独立ProjectStateService.java)。 - community「记入钱包/流量包」本波不跨模块写——实查
feed.yaml仅boost/pinned(无流量包发放端点)、trade.yaml无 grant/incentive 端点,两个跨模块写 seam 当前均不存在;本波只写 community 自有 reward 触发记录表,真实记账/到账留 M4。 - P-BIZ-11 学生防沉迷不在 biz——经 Doc C 已划归 compliance(
T-CMP-36/37),由 compliance 承接(不计入 biz)。 - 本波两类留债分开记账(评审版 R1,勿混):①资金流债(biz 签署/收款、community reward 真实记账)→ 统一并入 M4;②前端 UI 入口债(biz 运营代客发起=WS4 / 客户进度看板=WS3)→ 跨工位协调项,可与本波后端并行推进,不与 M4 资金流债捆绑延期(§10)。
events.schema.json本波零改(community 走 notify-push、不新增遥测事件)——主动规避「只改契约不改TelemetryEventEnum= 事件静默 rejected」双处同步坑(评审版 §3.3 保留口径)。
2. 涉及模块与文件路径(克隆 + 接线全清单)
路径前缀统一
/root/games-development-ai/;包名固定cn.wanxiang.game.module.{community|biz}.{层}(克隆黄金模块game-module-project范式,禁cn.iocoder业务包名)。
2.1 新建模块(克隆黄金模块)
| 模块 | 子模块 | 关键文件(举黄金模块 project 实例为样板,file:line) |
|---|---|---|
| community | game-module-community-api |
pom.xml(artifactId→game-module-community-api)/ enums/ApiConstants.java(NAME→community-server、PREFIX→RPC_API_PREFIX+"/community")/ enums/ErrorCodeConstants.java(段 1_107_000_000 起)/ api/CommunityNotifyApi.java(Feign 接口,照 ProjectApi.java:23 @FeignClient(name=ApiConstants.NAME))/ dto/(跨模块 DTO) |
game-module-community-server |
pom.xml(artifactId→game-module-community-server)/ controller/app/message/AppCommunityMessageController.java(@RequestMapping("/community"),前缀 /app-api 框架自动加,照 AppProjectController.java:33-37)/ controller/admin/notification/AdminCommunityNotificationController.java(mock-trigger,照 AdminProjectController.java:31-35)/ service/{message,level,notify}/(业务规则 + @Transactional 只加此层)/ dal/dataobject/community/{MessageDO,LevelDO,RewardDO}.java(extends TenantBaseDO,照 ProjectDO.java:18-83)/ dal/mysql/community/(@Mapper extends BaseMapperX,显式列禁裸 select*,照 ProjectMapper.java:16-87)/ convert/(BeanUtils+手写 static,非 MapStruct,照 ProjectConvert.java)/ api/CommunityNotifyApiImpl.java(@RestController @Validated @Primary implements CommunityNotifyApi,照 ProjectApiImpl.java:22-25)/ src/test/java/.../service/.../*ServiceImplTest.java(extends BaseMockitoUnitTest) |
|
| biz | game-module-biz-api |
同 community-api 结构(artifactId→-biz-api、NAME→biz-server、段 1_110_000_000 起)+ enums/BizLeadStatusEnum.java(照 ProjectStatusEnum.java:23-68) |
game-module-biz-server |
同 community-server 结构(artifactId→-biz-server);controller 偏 controller/admin/biz/(运营为主)+ 少量 controller/app/biz/(客户提单 + 我的订单);service/lead/BizLeadServiceImpl.java(流转校验内联,照 ProjectServiceImpl.java:111-116/350-370) |
2.2 公共装配点(⚠️ 两处,缺任一处克隆即卡 compile,实现 agent 不得以旧 skill 为唯一依据)
评审版 §2.4 + R2-2 硬提示:
add-business-module.md步骤表当前仅列「步骤5=yudao-server/pom.xml 加依赖」,未把 rootgame-cloud/pom.xml的<modules>注册列为独立步(开头还明写「无需改 Application/yaml,只需在 yudao-server/pom.xml 加依赖」)。该 root pom 注册步计划本波收口后才回写进 skill。故本波克隆以本节「装配点=两处」为准——只读 skill 会漏掉 root pom 的<module>注册,触发「缺此步父 pom 找不到聚合定义 →mvn -pl … compile失败」。
| # | 文件(亲核 2026-06-11 dev/2.0.0) | 改动 | 缺此步的后果 |
|---|---|---|---|
| ① | game-cloud/pom.xml(root 聚合 pom)<modules> 块(实查 :10 <modules> … :34 末为 game-module-studio … :35 </modules>) |
在 line34 之后 / line35 </modules> 之前追加 <module>game-module-community</module> + <module>game-module-biz</module> |
mvn -pl game-module-community compile 找不到父 pom 聚合定义 → compile 失败 |
| ② | game-cloud/yudao-server/pom.xml <dependencies> 块(实查 :23 <dependencies> … :80 末 game-module 依赖为 game-module-studio-server) |
在 studio-server <dependency> 块之后追加两个 <dependency>(groupId=cn.iocoder.cloud / artifactId=game-module-community-server、game-module-biz-server / version=${revision}) |
单体不把模块编进 JAR → 启动后无此模块、Swagger 无分组 |
| ③ | YudaoServerApplication.java / application.yaml |
零改(YudaoServerApplication 组件扫描 + @MapperScan + application.yaml type-aliases 已通配 cn.wanxiang.game.module;端前缀按 controller.app/controller.admin 包名自动加) |
—(明确写入本文降认知负荷) |
2.3 契约文件(新建,独占无冲突)
contracts/db-schemas/V12.0.0__create_game_community.sql+game-cloud/yudao-server/src/main/resources/db/migration/V12.0.0__create_game_community.sql(两处字节一致,见 §0/§5)contracts/db-schemas/V13.0.0__create_game_biz.sql+game-cloud/yudao-server/src/main/resources/db/migration/V13.0.0__create_game_biz.sql(两处字节一致)contracts/api-schemas/community.yaml、contracts/api-schemas/biz.yaml(独占新文件,实查均不存在)contracts/README.md §四:community=107 / biz=110 已在册(:79,只读确认,不改)
2.4 上游 notify 接线点(本波在上游各新增 1 处同进程调用 + 上游 -server/pom.xml 加 game-module-community-api 依赖)
| 挂点 | 文件:line(亲核 2026-06-11) | 接线说明(详见 §6.5) |
|---|---|---|
| 挂点1 P-NTF-02 生成完成 | game-module-aigc/.../service/callback/DifyCallbackServiceImpl.java:~52(return txService.handleCallbackTx(reqVO); 内层 @Transactional 三表写入链于此提交) |
改为先接 Boolean result,提交后经 aigcTaskMapper.selectByTraceId(reqVO.getTraceId()) 回查任务,仅 task.status==SUCCEEDED 且 versionId!=null(真实成功终态,排除 config_invalid/failed/幂等无新版本)才调 notifyGenerateDone(task.getCreatorUserId(), task.getGameId(), task.getVersionId()),再 return(实参三字段均取自回查任务,不走 projectApi.getCreatorUserId) |
| 挂点2 P-NTF-03 审核结果 + P-INC-01 发布计数 | 外层 caller game-module-project/.../controller/admin/project/AdminProjectController.java:55-57(projectService.reviewProject(reviewReqVO, reviewerUserId);service 本身 @Transactional 于 ProjectServiceImpl.java:207) |
service 返回后调 notifyReviewResult(...);isApprove 时多调一次 notifyProjectPublished(...)(合并在同一 caller) |
| 挂点3 P-NTF-05 收益变动 | game-module-trade/.../service/settle/SettlementServiceImpl.java:~78(settle 循环内 firstRecord 分支 / newlyRecorded++;recordIncome 为 @Transactional 单条已提交,循环体非事务) |
if (firstRecord) {...} 分支内调 notifyIncomeChanged(...)(仅首次真实入账才通知,幂等复跑不重发) |
⚠️ 行号漂移声明:以上 file:line 基于 dev/2.0.0 工作区快照(亲核日 2026-06-11)。落地时若上游模块有改动,以语义定位为准(aigc=
handleCallbackTx返回处、project=AdminProjectController.reviewProject调用返回后、trade=settle 循环firstRecord分支)。
3. 数据流与依赖(一图看清边界)
graph TB
subgraph UP["上游(本波各加 1 处提交后同进程 notify)"]
AGC["aigc DifyCallbackServiceImpl<br/>handleCallbackTx 提交后"]
PRJ["project AdminProjectController<br/>reviewProject 返回后"]
TRD["trade SettlementServiceImpl<br/>settle firstRecord 分支"]
end
subgraph CMU["community(V12 / 段 1-107 / CommunityNotifyApi @Primary)"]
NAPI["CommunityNotifyApi 入口<br/>(系统身份注入 LoginUser(id=0)+finally clearContext)"]
MSG["game_community_message 站内信"]
LVL["game_community_level 等级<br/>(published_count 原子自增)"]
RWD["game_community_reward 触发记录<br/>(uk 幂等;发放留 M4)"]
end
subgraph BIZ["biz(V13 / 段 1-110)"]
LEAD["game_biz_lead 询单(状态机)"]
QUOTE["game_biz_quote 报价"]
PROG["game_biz_progress 进度工单"]
ACC["game_biz_acceptance 验收"]
end
subgraph REUSE["只读复用(biz 不反写)"]
RTPKG["runtime 取包双路由<br/>/app-api/runtime/package/{versionId}(+/manifest)"]
TPL["aigc 4 模板 clicker/merge/idle/tycoon"]
end
subgraph M4["M4 留债(明确推迟)"]
PAY["pay 收款 / 在线签章 / trade IncentiveCreditApi / feed 流量包-api / 现金到账"]
end
AGC -.notifyGenerateDone.-> NAPI
PRJ -.notifyReviewResult + notifyProjectPublished.-> NAPI
TRD -.notifyIncomeChanged.-> NAPI
NAPI --> MSG
NAPI --> LVL --> RWD
LEAD --> QUOTE
LEAD --> PROG
LEAD --> ACC
RTPKG -.demo 可预览判定(只读).-> BIZ
TPL -.模板复用(只读).-> BIZ
RWD -.真实记账/到账.-> M4
ACC -.签署/收款.-> M4
classDef deb fill:#fee,stroke:#c33;
class PAY deb;
classDef new fill:#eef,stroke:#36c;
class AGC,PRJ,TRD new;
事务边界铁律(评审版 §3.3/§4 红线):三上游写方法本身均 @Transactional,notify 必须在本地事务提交后调用(=外层 caller 或循环内已提交分支),避免「通知失败回滚已提交业务」。三个 notify 调用点统一**try-catch 吞异常 + 仅记 error log**,不得让通知失败中断业务/结算循环(详见 §6.5、§8.3)。
4. 接口与数据契约(DDL + 端点 + CommunityNotifyApi)
DDL 套黄金模块公共列模板(recon
errorcode-flyway-ddlconv实证范式):InnoDB + utf8mb4;显式列 + 列级中文 COMMENT;状态机用TINYINT,非法流转由 Service 校验、DO 层不承载;每表强制 6 审计列(creator/create_time/updater/update_time/deleted BIT(1) DEFAULT b'0'/tenant_id BIGINT DEFAULT 0,全 NOT NULL);唯一键含(业务键, deleted, tenant_id);业务归属列creator_user_id/user_id(归属隔离边界)区别于 Yudao 审计列creator(VARCHAR64)。
⚠️ 归属隔离口径钉实(R-medium PII 红线,实查黄金模块未用 @DataPermission):实查
game-module-project的 DAL/Mapper 零@DataPermission注解(全 game-module grep 无命中),其归属隔离实为 Mapper 查询显式以creator_user_id/user_id作 WHERE 谓词、且该 userId 取自SecurityFrameworkUtils.getLoginUserId()(token 解析、非前端入参)——范本=ProjectMapper.selectMyPage「强制按当前用户过滤」、TradeController各 app 端点getLoginUserId()。故 community/biz 客户侧端点(/app-api/community/messages、/app-api/biz/leads/my等)必须在 Mapper 查询里强制以登录态 user_id 作谓词 enforce 隔离,不得依赖「黄金模块并未启用的 @DataPermission 框架机制」(照搬「靠 DataPermission」会既不加注解、又误以为框架已隔离 → 用户可读他人站内信/收益数额/审核拒绝原因等 PII)。隐私隔离必须在后端查询层 enforce(可信边界),前端不够。
4.1 community DDL — V12.0.0__create_game_community.sql(三表)
文件头注释块须含:契约#2 DB 迁移标识 + 模块名 community + owner WS5 / 文件名 + Flyway 纪律(只新增、已合入禁改、回滚写补偿迁移 V12.0.1)/ 表清单 + 职责(引架构 Doc B community 章节)/ 错误码段
community = 1-107-***-***/ 约定(InnoDB+utf8mb4、显式列、Yudao 6 审计列(creator/create_time/updater/update_time/deleted/tenant_id,与 §4 表头及黄金模块 V1/V9 头注对齐,勿误作「七」去找幽灵第 7 列)、状态机 tinyint、中文注释、禁裸 select*)。
-- 表1:game_community_message 站内消息中心(P-NTF-01/02/03/05 投递落点)
CREATE TABLE `game_community_message` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '消息 ID',
`user_id` BIGINT NOT NULL COMMENT '收件人用户 ID(归属隔离:Mapper 强制以 getLoginUserId() 作 WHERE 谓词,用户只见自己消息)',
`type` TINYINT NOT NULL COMMENT '消息类型:1系统 2公告 3审核 4收益',
`biz_ref` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '业务关联键(如 gameId/taskId/settleId,供前端跳转)',
`title` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '消息标题',
`content` VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '消息正文(审核拒绝含原因)',
`read_status` TINYINT NOT NULL DEFAULT 0 COMMENT '已读状态:0未读 1已读',
`read_time` DATETIME NULL COMMENT '已读时间',
`creator` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建者(Yudao 审计列;系统身份投递=0)',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '更新者(系统身份写=0,防 NOT NULL 拒绝)',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '逻辑删除:0未删 1已删',
`tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户 ID(MVP 单租户=0)',
PRIMARY KEY (`id`),
KEY `idx_user_read` (`user_id`, `read_status`, `create_time`) COMMENT '我的消息中心列表(按已读态+时间倒序)',
KEY `idx_user_type` (`user_id`, `type`, `create_time`) COMMENT '按四类聚合查询'
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '站内消息中心(四类聚合,T-CMU-06)';
-- 表2:game_community_level 创作者等级(P-INC-01 计数权威落点 published_count)
CREATE TABLE `game_community_level` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '等级记录 ID',
`creator_user_id` BIGINT NOT NULL COMMENT '创作者用户 ID',
`level` TINYINT NOT NULL DEFAULT 1 COMMENT '创作者等级:1小白(MVP 仅维护计数+新人里程碑,分层留 P1)',
`published_count` INT NOT NULL DEFAULT 0 COMMENT '已发布作品数(计数权威源=project 发布成功 notify 原子累加)',
`last_milestone` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '末次已触发里程碑键(如 publish_3,幂等防重复触发)',
`last_milestone_time` DATETIME NULL COMMENT '末次里程碑触发时间',
`creator` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建者',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '更新者',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '逻辑删除',
`tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户 ID',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_creator` (`creator_user_id`, `deleted`, `tenant_id`) COMMENT '一创作者一条等级记录(幂等初始化)'
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '创作者等级(T-CMU-11 等级引擎)';
-- 表3:game_community_reward 新人奖励触发记录(P-INC-01;本波只写触发记录,发放留 M4)
CREATE TABLE `game_community_reward` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '奖励触发记录 ID',
`creator_user_id` BIGINT NOT NULL COMMENT '创作者用户 ID',
`milestone` VARCHAR(32) NOT NULL COMMENT '里程碑键(如 publish_3 发满3作品)',
`reward_type` TINYINT NOT NULL COMMENT '奖励类型:1流量包 2现金',
`reward_amount` BIGINT NOT NULL DEFAULT 0 COMMENT '奖励额度(流量包=曝光量;现金=分,BIGINT 防浮点;与 trade 同口径)',
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '发放状态:0已触发待发放(本波终态) 1已发放(M4 回写)',
`granted_ref` VARCHAR(64) NOT NULL DEFAULT '' COMMENT 'M4 发放回执(trade 流水 id / feed 流量包 id),本波空',
`creator` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建者(系统身份触发=0)',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '更新者(系统身份写=0)',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '逻辑删除',
`tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户 ID',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_creator_milestone` (`creator_user_id`, `milestone`, `deleted`, `tenant_id`) COMMENT '同创作者同里程碑只触发一次(幂等防重复发钱)',
KEY `idx_status` (`status`) COMMENT 'M4 待发放扫描'
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '新人奖励触发记录(P-INC-01,发放留 M4)';
4.2 biz DDL — V13.0.0__create_game_biz.sql(四表)
文件头注释块同 §4.1 范式(模块名 biz / owner WS5 / 错误码段
biz = 1-110-***-***/ 资金流边界:本波只写自有表,不写 pay/trade,签署/收款/分账留 M4)。
-- 表1:game_biz_lead B/G 端定制询单主单(含轻量状态机;CRM=工单)
-- 状态机 status:0待跟进 →1已报价 →2制作中 →3待验收 →4已交付(单线性五态,BizLeadService 写前校验)
CREATE TABLE `game_biz_lead` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '定制询单 ID',
`customer_user_id` BIGINT NULL COMMENT '提单客户用户 ID(B 端自助提单时落;运营代客发起可空)',
`owner_user_id` BIGINT NULL COMMENT '跟进负责人用户 ID(运营,admin 队列认领)',
`biz_type` TINYINT NOT NULL DEFAULT 3 COMMENT '定制场景:1品牌营销 2文旅 3通用定制',
`contact_name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '联系人',
`contact_phone` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '联系电话',
`company` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '企业/机构名称',
`title` VARCHAR(120) NOT NULL DEFAULT '' COMMENT '需求标题',
`scene_desc` VARCHAR(2000) NOT NULL DEFAULT '' COMMENT '场景需求描述(B/G 端在线提交的定制需求正文)',
`template_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '选定的玩法模板 ID(复用 aigc 4 模板 clicker/merge/idle/tycoon)',
`demo_version_id` BIGINT NULL COMMENT 'demo 预览版本 ID(runtime game_version.id;取包走 /app-api/runtime/package/{versionId})',
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态机:0待跟进 1已报价 2制作中 3待验收 4已交付',
`creator` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建者(Yudao 审计列)',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '更新者(系统/异步写须注入 LoginUser(id=0),否则 NOT NULL 拒绝)',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '逻辑删除:0未删 1已删',
`tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户 ID(MVP 单租户=0)',
PRIMARY KEY (`id`),
KEY `idx_owner_status` (`owner_user_id`, `status`) COMMENT 'admin 处理队列:按负责人+状态',
KEY `idx_customer` (`customer_user_id`, `create_time`) COMMENT '客户侧我的订单看板(/app-api/biz/leads/my)',
KEY `idx_status_update` (`status`, `update_time`) COMMENT '运营队列按状态+时间'
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'B端定制询单主单(轻量状态机)';
-- 表2:game_biz_quote 报价表(报价≠可支付订单,参数化估价输出;收款留 M4)
CREATE TABLE `game_biz_quote` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '报价 ID',
`lead_id` BIGINT NOT NULL COMMENT '所属询单 ID(game_biz_lead.id)',
`quote_no` INT NOT NULL DEFAULT 1 COMMENT '报价版本号(同询单内可多轮报价,递增;唯一性由 service 以 max(quote_no)+1 串行生成保证、DB 不强约束——与黄金模块 game_version.version_no 同范式,非唯一键。若后续需 DB 级唯一可加 uk_lead_quote(lead_id,quote_no,deleted,tenant_id))',
`amount_fen` BIGINT NOT NULL DEFAULT 0 COMMENT '报价金额(单位:分,与 trade 同口径;本波仅展示不收款)',
`service_items` VARCHAR(1000) NOT NULL DEFAULT '' COMMENT '服务项明细(编排内容,参数化估价表单产出)',
`valid_days` INT NOT NULL DEFAULT 30 COMMENT '报价有效天数',
`remark` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '备注',
`creator` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建者',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '更新者',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '逻辑删除',
`tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户 ID',
PRIMARY KEY (`id`),
KEY `idx_lead` (`lead_id`, `quote_no`) COMMENT '按询单查报价历史'
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'B端定制报价表(报价≠下单,收款留 M4)';
-- 表3:game_biz_progress 进度工单表(状态机各节点流转记录,驱动定制进度看板)
CREATE TABLE `game_biz_progress` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '进度工单 ID',
`lead_id` BIGINT NOT NULL COMMENT '所属询单 ID',
`from_status` TINYINT NULL COMMENT '流转前状态(首条为 null)',
`to_status` TINYINT NOT NULL COMMENT '流转后状态(0-4)',
`operator_user_id` BIGINT NULL COMMENT '操作人用户 ID(运营推进;系统推进可空)',
`note` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '进度说明(看板时间线展示)',
`creator` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建者',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '更新者',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '逻辑删除',
`tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户 ID',
PRIMARY KEY (`id`),
KEY `idx_lead_time` (`lead_id`, `create_time`) COMMENT '按询单取进度时间线(看板)'
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'B端定制进度工单表(看板时间线)';
-- 表4:game_biz_acceptance 交付验收表(试玩→反馈→确认三态;签署/收款留 M4)
CREATE TABLE `game_biz_acceptance` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '验收记录 ID',
`lead_id` BIGINT NOT NULL COMMENT '所属询单 ID',
`demo_version_id` BIGINT NOT NULL COMMENT '交付 demo 版本 ID(runtime game_version.id,试玩取包用)',
`feedback` VARCHAR(1000) NOT NULL DEFAULT '' COMMENT '客户试玩反馈',
`confirm_status` TINYINT NOT NULL DEFAULT 0 COMMENT '确认态:0待确认 1已确认',
`confirmed_time` DATETIME NULL COMMENT '客户确认时间',
`signed_offline` BIT(1) NOT NULL DEFAULT b'0' COMMENT '线下签署后台标记兜底(M4 接在线签章前过渡:0未签 1已线下签)',
`creator` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建者',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '更新者',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '逻辑删除',
`tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户 ID',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_lead_version` (`lead_id`, `demo_version_id`, `deleted`, `tenant_id`) COMMENT '同询单同 demo 版本一条验收(含 deleted/tenant 维度)',
KEY `idx_lead` (`lead_id`) COMMENT '按询单查验收'
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'B端定制交付验收表(试玩/反馈/确认;签署收款留 M4)';
4.3 CommunityNotifyApi 方法签名(@Primary 本地实现)
路径:
game-module-community-api/.../api/CommunityNotifyApi.java(接口,@FeignClient(name=ApiConstants.NAME)、ApiConstants.NAME="community-server"、PREFIX=RpcConstants.RPC_API_PREFIX+"/community")+community-server/.../api/CommunityNotifyApiImpl.java(@RestController @Validated @Primary implements CommunityNotifyApi,照ProjectApiImpl.java:22-25)。实现入口注入系统身份LoginUser(id=0)+finallyclearContext(§8.2)。统一CommonResult信封;幂等以(userId,type,bizRef)去重(重复投递不重复落库)。
public interface CommunityNotifyApi {
// P-NTF-02 生成完成通知:aigc 在 DifyCallbackServiceImpl.handleCallback 提交后调
// 触发判定 = 提交后回查任务为 SUCCEEDED 终态且 versionId 已回填(非 result==TRUE,见 §6.5 挂点1);
// 三实参均取自回查的 AigcTaskDO(creatorUserId/gameId/versionId)
CommonResult<Boolean> notifyGenerateDone(@RequestParam("creatorUserId") Long creatorUserId,
@RequestParam("gameId") Long gameId,
@RequestParam("versionId") Long versionId);
// P-NTF-03 审核结果通知:project 在 reviewProject 提交后调;
// decision 三态(1通过/2拒绝/3下架,对齐 ReviewDecisionEnum)——本波仅对 1/2 发通知,
// decision=3 下架(含封禁联动)本波不发(caller 侧以 decision∈{1,2} 守门,见 §6.5 挂点2);reject(2) 须带 reason
CommonResult<Boolean> notifyReviewResult(@RequestParam("creatorUserId") Long creatorUserId,
@RequestParam("gameId") Long gameId,
@RequestParam("decision") Integer decision,
@RequestParam(value = "reason", required = false) String reason);
// P-NTF-05 收益变动通知:trade 在 SettlementServiceImpl.settle 入账成功后调
// netAmount=创作者实得净额(分);settle 作用域无现成 net(recordIncome 内部算、不返回),
// 须在 settle 内现算 BigDecimal.valueOf(gross).multiply(creatorShare).setScale(0,DOWN)(见 §6.5 挂点3)
CommonResult<Boolean> notifyIncomeChanged(@RequestParam("creatorUserId") Long creatorUserId,
@RequestParam("source") Integer source,
@RequestParam("sourceRef") String sourceRef,
@RequestParam("netAmount") Long netAmount);
// P-INC-01 发布计数(方案a):project 发布成功后同进程 notify,累加 published_count 并触发里程碑
CommonResult<Boolean> notifyProjectPublished(@RequestParam("creatorUserId") Long creatorUserId,
@RequestParam("gameId") Long gameId);
}
4.4 community.yaml 端点清单(contracts/api-schemas/community.yaml,段注释 1-107)
| 方法 路由 | P0 | 说明 |
|---|---|---|
GET /app-api/community/messages |
P-NTF-01 | 站内信分页列表(query: type 1系统/2公告/3审核/4收益 可选, readStatus 可选, cursor/size) |
POST /app-api/community/messages/{id}/read |
P-NTF-01 | 标记单条已读(落 read_status=1+read_time) |
GET /app-api/community/messages/unread-count |
P-NTF-01 | 未读数角标(按 type 分组计数) |
GET /app-api/community/level |
P-INC-01 | 当前创作者等级 + published_count + 下一里程碑进度(如 2/3 距新人奖励) |
GET /app-api/community/rewards |
P-INC-01 | 我的奖励触发记录列表(status 触发态/发放态可见) |
[RPC] /rpc-api/community/* |
— | CommunityNotifyApi 四方法,供上游同进程 @Primary 调用,非对外业务路由 |
[staging only] POST /admin-api/community/notifications/{type}/mock-trigger |
— | 上游未接通前独立验收四类投递,仅 dev/staging 生效(@Profile({"dev","staging"}) 或网关层关闭,生产关闭)。⚠️ 实查全 game-module 无 @Profile 先例(现有 dev/staging 差异化手段是 @ConditionalOnProperty 灰度开关,如 aigc AIGC_EXECUTOR_ENABLED)——若用 @Profile 须知是本仓首例、在类或方法级标注、并验证 staging profile 下 Bean 注册生效;亦可改用 @ConditionalOnProperty(name="community.mock-trigger.enabled", havingValue="true") 与现有灰度开关风格一致。二选一钉死 |
4.5 biz.yaml 端点清单(contracts/api-schemas/biz.yaml,段注释 1-110;端前缀框架按包名自动加,@RequestMapping 只写 /biz)
客户侧 /app-api/biz/*(game-studio,用户 Token + Mapper 强制 getLoginUserId() 归属谓词,WS3)
| 方法 路由 | P0 | 说明 |
|---|---|---|
POST /app-api/biz/leads |
P-BIZ-01 | 提交定制需求询单(落库即开状态机=待跟进0 + 落 progress 首条 to_status=0);req=BizLeadCreateReqVO(bizType/title/sceneDesc/contactName/contactPhone/company);resp=CommonResult<Long> |
GET /app-api/biz/leads/my |
P-BIZ-03 | 我的定制订单列表(customer_user_id 过滤,客户只见自己) |
GET /app-api/biz/leads/{id} |
— | 询单详情(含当前状态/最新报价/进度时间线/demo 可预览判定) |
GET /app-api/biz/leads/{id}/progress |
P-BIZ-03 | 进度看板时间线(game_biz_progress 按时间) |
GET /app-api/biz/templates |
P-BIZ-02 | 按场景检索可选模板(复用 aigc 4 模板);query=bizType |
POST /app-api/biz/acceptances/{leadId}/feedback |
P-BIZ-04 | 客户试玩后提交反馈;req=BizFeedbackReqVO(demoVersionId/feedback) |
POST /app-api/biz/acceptances/{leadId}/confirm |
P-BIZ-04 | 客户确认验收(confirm_status=1,状态机 3→4 已交付) |
demo 试玩取包不在 biz.yaml 自建端点——直接复用 runtime 真实双路由:
GET /app-api/runtime/package/{versionId}(清单 meta,CommonResult<RuntimePackageRespVO>,biz 看板「是否可预览」判定走此条)+GET /app-api/runtime/package/{versionId}/manifest(原始 JSON,宿主 sha256 比对后注入)。两条须写全(评审版 R2-3)。
admin /admin-api/biz/*(game-admin,RBAC @PreAuthorize,WS4)
| 方法 路由 | P0 | 权限点 | 说明 |
|---|---|---|---|
GET /admin-api/biz/leads/page |
— | biz:lead:query |
处理队列(按 owner/status/bizType 筛选) |
POST /admin-api/biz/leads |
P-BIZ-08/12 | biz:lead:create |
运营代客发起定制单(customer_user_id 可空) |
PUT /admin-api/biz/leads/{id}/assign |
— | biz:lead:assign |
认领/指派负责人(owner_user_id) |
POST /admin-api/biz/leads/{id}/quotes |
P-BIZ-08 | biz:quote:create |
报价输出(参数化估价,报价≠可支付订单;状态机 0→1) |
GET /admin-api/biz/leads/{id}/quotes |
— | biz:quote:query |
报价历史 |
POST /admin-api/biz/leads/{id}/advance |
P-BIZ-03 | biz:lead:advance |
推进状态机(→2制作中/→3待验收,→3 守 demo 可预览);req=BizAdvanceReqVO(toStatus/note/demoVersionId) |
PUT /admin-api/biz/acceptances/{id}/sign-offline |
P-BIZ-04 | biz:acceptance:sign |
线下签署后台标记兜底(signed_offline=1) |
权限点登记(§7.4 必做):
biz:lead:query/create/assign/advance、biz:quote:create/query、biz:acceptance:sign需在 yudaosystem_menu登记后 RBAC 才放行(范本=project:review:query登记方式)。
§0. 全局单序列资源预占登记(⚠️ 开篇第一节,单 agent 一次性收口提交)
目的(评审版 §8 §0 / R1-5 / R6):防 V12/V13 双占、文件名碰撞、错误码撞段。这是一个可勾的中央登记动作,由单 agent 收口提交(建 community 的 agent 在动任何代码前先做完本节并提交,biz agent 从该提交 pull)。
§0.1 预占前置校验(落地时实跑,证据入提交说明)
cd /root/games-development-ai
# ① Flyway 文件名占用校验(按文件名,非内容;应全空、exit≠0=未占用)
ls contracts/db-schemas/V12* contracts/db-schemas/V13* \
game-cloud/yudao-server/src/main/resources/db/migration/V12* \
game-cloud/yudao-server/src/main/resources/db/migration/V13* 2>/dev/null; echo "exit=$? # 期望 exit=2(无此文件)"
# ⚠️ 不要用 `grep -r 'V12\|V13'` 判占用——它是内容匹配,会命中
# V11.0.0__create_passport_player_invite.sql 的让位声明注释(两处副本各一行,实测返 2 行),命中仅此注释属预期、非真实占用。
# ② 错误码段占用校验(应全返 0)
grep -rn '1_107\|1_110' game-cloud/ | wc -l # 期望 0
# ③ 契约 yaml 占用校验(应不存在)
ls contracts/api-schemas/community.yaml contracts/api-schemas/biz.yaml 2>/dev/null; echo "exit=$? # 期望 exit=2"
亲核 2026-06-11(dev/2.0.0):① exit=2(V12/V13 均无);② 返 0;③ exit=2。三项均空 = 可安全预占。
§0.2 预占登记表(单 agent 写死锁定)
| 资源 | community | biz | 一致性要求 |
|---|---|---|---|
| Flyway 主版本 | V12.0.0__create_game_community.sql |
V13.0.0__create_game_biz.sql |
两处副本(contracts/db-schemas/ + yudao-server/.../db/migration/)字节一致(md5sum 全等;命名 V{x.y.z}__{desc}.sql,只增不改旧——改 V1–V11 任一 → flyway validate 阻断 CI) |
| 错误码段 | 1_107_***_***(-api 的 ErrorCodeConstants.java) |
1_110_***_***(同) |
contracts/README.md §四:79 已在册(复用不改);段内子码按 new ErrorCode(1_107_00X_***, "中文") 分块、文件头列全 13 段防重叠 |
| 契约 yaml | contracts/api-schemas/community.yaml |
contracts/api-schemas/biz.yaml |
独占新文件 |
| W4 埋点加列占位 | — | — | V14 预留给 W4 埋点加列(本波不写,仅在登记表占号防后续撞 V12/V13) |
Flyway 副本数裁决(recon
errorcode-flyway-ddlconv必裁项):取「两副本」(contracts/db-schemas/授权源 +yudao-server/.../db/migration/唯一执行副本),不放单模块-server/db/migration/。依据:V10/V11(跨模块件)已开「两副本」先例;单体部署下 yudao-server 聚合全部 migration,单模块副本是冗余且多一处漂移风险;放进单模块 classpath jar 反而可能触发同版本 Flyway 在多 jar 重复校验失败(V11 守门①根因)。community/biz-server下不放任何 sql。
§0.3 收口动作
单 agent 一次性创建并提交:两个 V12/V13 SQL(各两副本字节一致)+ 两个空 yaml 骨架(或含完整端点)+ root game-cloud/pom.xml 的 <module> 注册 + yudao-server pom 依赖。先提交 push,community/biz 实现 agent 再从此提交 pull,避免并行同改 root pom/Flyway 冲突。
5. 逐模块克隆关键步骤(community 先,biz 后)
通用「克隆 5 步骨架」(评审版 §8):cp 黄金模块 → 改 3 个 pom artifactId → ① root pom 注册
<module>+ ② yudao-server pom 注册-server依赖 → -api 写ErrorCodeConstants+ApiConstants+枚举+DTO+Feign → -server 写 controller/service/dal/convert/ApiImpl@Primary。装配=两处(§2.2),克隆开篇先复述,勿以旧 skill 为唯一依据。
5.1 community 建设步骤(13 步,逐步可勾)
| # | 步骤 | 关键点 / 防雷 |
|---|---|---|
| C0 | 完成 §0 预占收口(V12 两副本 + community.yaml + root pom <module> + yudao-server pom 依赖),提交 push |
单 agent 收口;biz 从此提交 pull |
| C1 | cp -r game-module-project game-module-community,包路径 project→community、artifactId 三处改(api/server/聚合 pom) |
包名固定 cn.wanxiang.game.module.community.*;⚠️ 克隆后清理 community-server pom 中 project 发布编排专用、community 不依赖的跨模块 -api(compliance-api/runtime-api/feed-api/system PlayerApi 等)——community 作为通知收件 + 等级引擎自包含、不调任何跨模块 -api(卫生项,留孤儿依赖与「最小变更」相悖;与黄金模块列集一致由 §0 装配点保障,非 pom 依赖集) |
| C2 | 改 ApiConstants.java:NAME="community-server"、PREFIX=RPC_API_PREFIX+"/community" |
⚠️ 不改则 Feign 服务名撞 project-server(R8) |
| C3 | 改 ErrorCodeConstants.java:段 1_107_000_000 起,文件头列全 13 段防重叠 |
⚠️ 顺手复制 project 100 段忘改 → 撞段(R8) |
| C4 | 写 CommunityNotifyApi.java(四方法,§4.3 签名)+ DTO |
@FeignClient(name=ApiConstants.NAME) |
| C5 | 写 CommunityNotifyApiImpl.java(@RestController @Validated @Primary),入口注入 LoginUser(id=0)+finally clearContext |
⚠️ R3 系统身份写雷(§8.2),单测 mock 测不到 |
| C6 | 写三表 DO(extends TenantBaseDO,@TableName 改对应新表名+@KeySequence 名随表改如 game_community_message_seq 或删除)+ Mapper(BaseMapperX,显式列禁裸 select*,客户侧查询强制 getLoginUserId() 归属谓词,published_count 原子自增方法)+ Convert |
⚠️ 克隆继承 @KeySequence("game_project_seq")(实查 ProjectDO:19,MySQL 忽略但残留语义错乱)须改名或删;level 自增用 UPDATE … SET published_count=published_count+1 WHERE creator_user_id=?(§6.4 防并发丢更新) |
| C7 | 写 service/message/(投递/已读/未读数)+ service/notify/(编排:路由四类→去重→写站内信)+ service/level/(计数累加→里程碑判定→插 reward) |
@Transactional 只加 service 层 |
| C8 | 写 controller/app/message/(5 个 app 端点,§4.4,Mapper 强制 getLoginUserId() 归属谓词)+ controller/admin/notification/(mock-trigger,@Profile({"dev","staging"}) 本仓首例 或 @ConditionalOnProperty 灰度开关,§4.4/§8.4 二选一) |
mock-trigger 须限定 staging,否则成匿名伪造投递入口(§8.4);客户侧端点隔离靠显式 user_id 谓词,非 @DataPermission(黄金模块未用) |
| C9 | 接挂点1(aigc):DifyCallbackServiceImpl succeeded 后调 notifyGenerateDone(§6.5);aigc -server/pom.xml 加 game-module-community-api 依赖 |
try-catch 吞异常 |
| C10 | 接挂点2(project):AdminProjectController.reviewProject 返回后调 notifyReviewResult,isApprove 时多调 notifyProjectPublished(§6.5);project -server/pom.xml 加 community-api 依赖 |
reject 须传 reqVO.getReason() |
| C11 | 接挂点3(trade):SettlementServiceImpl settle 循环 firstRecord 分支调 notifyIncomeChanged(§6.5);trade -server/pom.xml 加 community-api 依赖 |
仅首次入账才通知,幂等不重发 |
| C12 | Nacos 配置注册(§7.3)+ 单测(extends BaseMockitoUnitTest,mock insert/updateById 用 any(XxxDO.class) 消歧) |
单测覆盖:投递/已读/计数累加/里程碑触发幂等 |
| C13 | mini-desktop 工程门 + staging 链路①实测(§9.2/§9.3) | 「编译过≠可验收」 |
5.2 biz 建设步骤(11 步)
| # | 步骤 | 关键点 / 防雷 |
|---|---|---|
| B0 | 从 §0 收口提交 pull(V13 两副本 + biz.yaml + root pom + yudao-server pom 已就位) | — |
| B1 | cp -r game-module-project game-module-biz,包/artifactId 改 biz |
包名 cn.wanxiang.game.module.biz.* |
| B2 | 改 ApiConstants.java:NAME="biz-server"、PREFIX=…+"/biz";ErrorCodeConstants.java 段 1_110 |
⚠️ R8 同 community |
| B3 | 写 BizLeadStatusEnum.java(单线性五态 + of/canTransit,§6.6 范本)+ 错误码(BIZ_LEAD_NOT_EXISTS/BIZ_LEAD_STATUS_ILLEGAL_TRANSITION/BIZ_DEMO_NOT_READY) |
合法流转矩阵:仅 current→current+1 单跳 |
| B4 | 写四表 DO + Mapper + Convert(§4.2 DDL 对应) | ⚠️ 客户侧查询 Mapper 强制以 getLoginUserId()=customer_user_id 作 WHERE 谓词(范本 ProjectMapper.selectMyPage,黄金模块无 @DataPermission 可抄,勿假定框架已隔离);克隆后清理 biz-server pom 中 project 发布编排专用、biz 不依赖的跨模块 -api(compliance-api/feed-api/system PlayerApi 等),保留 runtime-api(demo 取包用,project-server pom :44 已携带、克隆即继承);DO 上 @KeySequence 名随表改(如 game_biz_lead_seq)或删除(本工程恒 MySQL,该注解被忽略),@TableName 改对应新表名 |
| B5 | 写 service/lead/BizLeadServiceImpl.java:提单/报价/advanceStatus(写前 canTransit 校验,→3 额外 validateDemoPreviewable,§6.6) |
流转校验内联,不引 Flowable |
| B6 | validateDemoPreviewable:同进程注入 RuntimePackageApi(@Primary 本地实现)调 getStatus(versionId) 判 demo 可预览(§6.7 负路径) |
⚠️ demo 取包失败降级,不透传 500 |
| B7 | 写 controller/app/biz/(7 个客户端点,§4.5)+ controller/admin/biz/(7 个 admin 端点,@PreAuthorize("@ss.hasPermission('biz:...')")) |
端前缀框架自动加 |
| B8 | 写 BizTemplateService:返 aigc 4 可玩模板(clicker/merge/idle/tycoon);dodge/runner/match 仅契约未实现不返 |
§6.7 |
| B9 | 权限点 SQL 登记(system_menu,§7.4)+ Nacos 配置(§7.3)+ 单测(BizLeadServiceImplTest:流转合法/非法/→3 无 demo 拒) |
mock RuntimePackageApi.getStatus 测负路径 |
| B10 | mini-desktop 工程门 + staging 链路②实测(§9.2/§9.3)+ 客户/admin UI 入口走查跟进项(§9.4) | 「编译过≠可验收」 |
6. 关键实现细节(业务逻辑 + 接线)
6.1 community 通知编排(T-CMU-10)
四类消息(type 1系统/2公告/3审核/4收益)统一经 notify service:路由 type → 组装 title/content(审核拒绝含 reason)→ 幂等去重 (user_id,type,biz_ref)(重复投递不重复落库)→ mapper.insert(MessageDO)。
6.2 community 站内信(T-CMU-06)
AppCommunityMessageController 5 端点:列表(按 type/readStatus 过滤、cursor 分页)/ 标记已读(read_status=1+read_time=now)/ 未读数(按 type 分组 count)/ 等级查询 / 奖励记录列表。归属隔离=Mapper 查询强制以 SecurityFrameworkUtils.getLoginUserId() 作 user_id WHERE 谓词(范本 ProjectMapper.selectMyPage「强制按当前用户过滤」,黄金模块无 @DataPermission;标记已读须先校验该消息 user_id==当前登录 id 再落 read,防越权改他人消息已读态)。
6.3 community 等级引擎(T-CMU-11)
level service:notifyProjectPublished 触发 → 原子自增 published_count(§6.4)→ 同事务内判 published_count == publish-threshold(默认 3)→ 达标且 uk_creator_milestone 不存在则插一条 game_community_reward(milestone=publish_3、status=0 已触发待发放、reward_type/reward_amount 按配置)。本波终态=触发可观测+本地记账,不跨模块发放。
6.4 P-INC-01 计数权威源(方案 a 落地,§0 单 agent 已拍)
- 计数权威源 = project 发布成功(非 feed 互动回流)。实查 ProjectApi 无
countPublishedByCreator方法(只有 getCreatorUserId/getCurrentVersionId/getStatus/getFeedMeta/createProject/unlistIfPublished),故方案 b 需 project 新增端点;取方案 a = project 发布成功后同进程 notify community 计数(上游单点改动、与三上游 notify 同范式)。 - ⚠️ 计数交付口径 = 「尽力计数」(R-high 对齐,承认 at-most-once 边界):方案 a 的 notify 在事务提交后调用且 try-catch 吞异常(§6.5 挂点2),本质是 at-most-once 投递——notify 因瞬时故障/重启丢一次,
published_count不会再被补发(project 侧无重放、ProjectApi 无countPublishedByCreator兜底真相源)。因此本波明确把published_count定为「尽力计数」:允许极端情况下少计/漏触发里程碑,不宣称「绝对权威强一致」(与 reward 的uk_creator_milestone强幂等是两件事——后者只保证「到达阈值后只发一次」,不保证「计数一定到得了阈值」)。M4 真实记账时由 project 补countPublishedByCreator只读端点 + community 读时对账重建,届时升为可对账权威源(§10.2 ①资金流债已含此项)。AC-CMU-5 据此订正为「计数到达阈值即触发」(§9.1)。 - 并发安全(recon 必落项):
notifyProjectPublished并发到达同一 creator 时,published_count累加须 DB 原子UPDATE … SET published_count=published_count+1 WHERE creator_user_id=?(Mapper default 方法),避免读改写丢更新导致计数偏少漏触发里程碑;累加后同事务判count==threshold才插 reward,uk_creator_milestone兜底幂等(重复发布不重触发)。 - 首次初始化:
game_community_level一创作者一条(uk_creator),首次 notify 时INSERT … ON DUPLICATE KEY或先查后插(幂等)。
6.5 三上游 notify 挂点接线(事务提交后 + try-catch 吞异常)
通用规则:三上游写方法本身均
@Transactional,notify 必在本地事务提交后调用(外层 caller 或循环内已提交分支),避免通知失败回滚业务。统一try-catch包裹、仅记 error log,不得让通知失败中断业务/结算循环。
- 挂点1 P-NTF-02(aigc):
DifyCallbackServiceImpl.handleCallback(亲核:50-71return txService.handleCallbackTx(reqVO);,内层@Transactional三表写入链于此提交;成功回填动作=内层DifyCallbackTxService.handleCallbackTx步骤⑦调aigcTaskService.completeWithVersion(taskId, versionId),不是DifyCallbackService的「成功路径」——勿误引)。- ⚠️ 三处硬约束(R-blocker,照字面无法落地,必须按下方处方):
handleCallbackTx返回值无法判别真成功:实查内层 succeeded(:204)/ config_invalid 业务性失败(:166)/ failed(:155)/ 幂等短路(:122)全部return Boolean.TRUE,故result==TRUE不能作为 succeeded 判据(照搬会对生成失败/重复回调误发「生成完成」)。- 外层作用域无 gameId/versionId/creatorUserId:
DifyCallbackReqVO字段仅traceId/status/templateId/gameConfig/assets/qualityScore/failureReason(无三者);versionId是内层事务步骤④新建的局部变量、不随Boolean返回;gameId/creatorUserId是内层经aigcTaskMapper.selectByTraceId反查的task字段。三个 notify 实参在外层均无来源。 - 不走
projectApi.getCreatorUserId(gameId):该路径前提gameId本身在外层不可达;且AigcTaskDO(实查 :37/:41/:45)已自带creatorUserId/gameId/versionId三字段,回查任务一次即全得,更直接。
- 可落地处方(实查
selectByTraceId已存在、AigcTaskDO三字段齐备,aigc 自有AigcTaskMapper可直接注入):内层事务提交后,在外层handleCallback末尾按 traceId 回查任务,仅真实成功终态才发:Boolean result = txService.handleCallbackTx(reqVO); // 内层 @Transactional 已提交 // 提交后回查任务:仅 SUCCEEDED 且 versionId 已回填 = 真实成功终态, // 天然排除 config_invalid/failed(status=FAILED)与幂等短路无新版本场景 try { AigcTaskDO task = aigcTaskMapper.selectByTraceId(reqVO.getTraceId()); if (task != null && Objects.equals(task.getStatus(), AigcTaskStatusEnum.SUCCEEDED.getStatus()) && task.getVersionId() != null) { communityNotifyApi.notifyGenerateDone(task.getCreatorUserId(), task.getGameId(), task.getVersionId()); } } catch (Exception e) { log.error("[handleCallback] 生成完成通知失败(不回滚业务)traceId={}", reqVO.getTraceId(), e); } return result;注:
notifyGenerateDone的触发判定不是result==TRUE,而是「回查任务为 SUCCEEDED 终态且 versionId 已回填」(§4.3 签名注释同步修正)。aigc-server已依赖 project/runtime-api,本处只新增对 community-api 的依赖;AigcTaskMapper/AigcTaskStatusEnum为 aigc 自有,直接注入/引用。
- ⚠️ 三处硬约束(R-blocker,照字面无法落地,必须按下方处方):
- 挂点2 P-NTF-03 + P-INC-01(project):
ProjectServiceImpl.reviewProject(亲核:206-242@Transactional,落 review_record + 状态机 +(APPROVE)发布编排 :240)。notify 不可放方法内(会被回滚牵连)。真实位置=外层 callerAdminProjectController.reviewProject(亲核:55-59,projectService.reviewProject(reviewReqVO, reviewerUserId))。- ⚠️ 作用域处方(R-medium,caller 侧
creatorUserId/isApprove均不在原作用域,须显式取得):实查AdminProjectController.reviewProject方法体只有reviewReqVO(字段gameId/versionId/decision/reason,无 creatorUserId)与reviewerUserId;isApprove概念仅存在于ProjectServiceImpl内部(:213),控制器取不到。caller 须自行派生(AdminProjectController增注入ProjectApi,project-server 已依赖 project-api,自调@PrimaryProjectApiImpl 可达):projectService.reviewProject(reviewReqVO, reviewerUserId); // service @Transactional 已提交 try { Long creatorUserId = projectApi.getCreatorUserId(reviewReqVO.getGameId()).getCheckedData(); if (creatorUserId != null) { // 游戏不存在/已删时 null,跳过 notify Integer decision = reviewReqVO.getDecision(); boolean isApprove = Objects.equals(decision, ReviewDecisionEnum.APPROVE.getDecision()); // ★ 仅对「通过(1)/拒绝(2)」发审核结果通知;下架(3, 含封禁联动)本波不发(消除未定义文案分支) if (isApprove || Objects.equals(decision, ReviewDecisionEnum.REJECT.getDecision())) { communityNotifyApi.notifyReviewResult(creatorUserId, reviewReqVO.getGameId(), decision, reviewReqVO.getReason()); } if (isApprove) { communityNotifyApi.notifyProjectPublished(creatorUserId, reviewReqVO.getGameId()); } } } catch (Exception e) { log.error("[reviewProject] 审核/发布通知失败(不回滚审核)gameId={}", reviewReqVO.getGameId(), e); } - ⚠️ owner 钉准(评审版 §6.2 注 / recon
community-designrisk):Doc C 标 P-NTF-03 辅技术功能=compliance「审核状态机事件源」,但实查审核结果落库点是 project 侧ProjectServiceImpl.reviewProject(写 review_record + 状态机),complianceComplianceGateServiceImpl.evaluate只做 Gate 二态不碰 project 审核态。故 notify 挂 project.reviewProject 外层 caller,不挂 compliance(否则 reason 取不到)。该改挂与「唯一设计依据」评审版 §2.1/§2.2/AC-CMU-3 的 compliance 上游源矛盾,已回流评审版勘误#1(§11)订正——验收以 project 挂点为准。 - ⚠️ UNLIST 第二调用路径漏通知(R-medium,已知不通知项):
ProjectApi.unlistIfPublished(ProjectServiceImpl:282-297)以SYSTEM_REVIEWER_ID直接调 service 层reviewProject(reqVO, SYSTEM_REVIEWER_ID)(:295)绕过 AdminProjectController,供 compliance 封禁联动调用。本波 notify 钩子只在 controller,故经封禁联动/系统下架路径一律不发通知——属本波非 P0 的已知不通知项(下架通知本就不在 community 5 P0 内),不补钩子(补则须下沉 service 用TransactionSynchronization.afterCommit,超本波边界)。 - ⚠️ published_count 计数幂等口径(R-high,承认边界):
notifyProjectPublished仅在isApprove分支调用,而reviewProject受状态机「仅审核中(REVIEWING)可通过」约束(再审被挡死),故正常每款游戏只产生一次 approve→一次计数。但 P-INC-01 计数权威源建立在「事务提交后 notify + try-catch 吞异常」=at-most-once 投递上,存在两类已知风险:① notify 因瞬时故障/重启丢失 → 该次发布永久少计;② framework 重试/前端重复提交致同一 gameId 二次 approve 通知 → 计数偏大。uk_creator_milestone只防「重复发钱」、不防「count 本身偏差」。本波取「尽力计数」口径(见 §6.4 收紧 + AC-CMU-5 订正),不引入按 gameId 的计数级幂等表(属 M4 真实记账时统一对账范畴)。
- ⚠️ 作用域处方(R-medium,caller 侧
- 挂点3 P-NTF-05(trade):
SettlementServiceImpl.settle(亲核 :55-89 循环内if (firstRecord) { newlyRecorded++; }分支;recordIncome为@Transactional单条已提交,循环体非事务)。- ⚠️
net是幽灵变量(R-medium,照字面编译不过):实查 settle 循环作用域只有revenue(AdRevenueRespDTO,含getRevenueAmount()=gross 毛额 /getCreatorUserId()/getId())与creatorShare(@Value("${trade.creator-share:0.80}"));net在IncomeServiceImpl.recordIncome内部由calcNet(gross, shareRate)算(BigDecimal.valueOf(gross).multiply(shareRate).setScale(0, DOWN),:86-89),且recordIncome返回boolean(不返 net)。settle 局部无net这个名字。 - 可落地处方(二选一,钉准取 a 现算——口径与 recordIncome 落账完全一致、零跨改):
- (a) settle 内现算 net(推荐):
firstRecord分支内先long net = BigDecimal.valueOf(revenue.getRevenueAmount()).multiply(creatorShare).setScale(0, java.math.RoundingMode.DOWN).longValue();(与IncomeServiceImpl.calcNet同公式同舍入,保证通知净额=落账净额),再try { communityNotifyApi.notifyIncomeChanged(revenue.getCreatorUserId(), TradeIncomeSourceEnum.AD.getSource(), String.valueOf(revenue.getId()), net); } catch (Exception e) { log.error("[settle] 收益变动通知失败(不中断结算循环)revenueId={}", revenue.getId(), e); }(仅首次真实入账才通知,幂等复跑不重发)。 - (b) 改
recordIncome返回入账净额(或入账 DO 含 net),settle 据返回值传——但recordIncome返回值语义(首次入账 boolean)被结算回标逻辑依赖,改签名需回归 trade 现有调用方,故不取。
- (a) settle 内现算 net(推荐):
- 访问器核实:
TradeIncomeSourceEnum.AD(实查 :22AD(1,"广告"),@Getter于字段source)访问器为getSource()(文档用法正确)。 - ⚠️ 事务边界 nuance(recon
community-designrisk):settle 是定时任务、循环体非事务、内层recordIncome单条已提交;firstRecord分支调 notify 满足「入账已提交」,但若后续条目或回标失败,已发 notify 无法回滚——评审版接受此口径(通知失败不回滚业务、反之亦然),故 notify 异常须 try-catch 吞掉 + 告警日志,不得中断结算循环。
- ⚠️
6.6 biz 轻量状态机(范本 project ProjectStatusEnum/ProjectServiceImpl)
// game-module-biz-api/.../enums/BizLeadStatusEnum.java
// 合法流转:0待跟进→1已报价→2制作中→3待验收→4已交付(仅相邻正向单跳,不可跳级/回退)
// 非法流转由 BizLeadService 写前拒(抛 BIZ_LEAD_STATUS_ILLEGAL_TRANSITION)
// 硬规则:→3待验收 必须已有可玩 demo(demo_version_id 非空且 runtime 取包可预览),否则抛 BIZ_DEMO_NOT_READY
@Getter @AllArgsConstructor
public enum BizLeadStatusEnum {
PENDING(0, "待跟进"), QUOTED(1, "已报价"), PRODUCING(2, "制作中"), ACCEPTING(3, "待验收"), DELIVERED(4, "已交付");
private final Integer status; private final String name;
public static BizLeadStatusEnum of(Integer status){ return Arrays.stream(values()).filter(e->Objects.equals(e.status,status)).findFirst().orElse(null); }
/** 合法流转矩阵:仅允许 current→current+1 单跳(单线性,5 态 4 条边) */
public static boolean canTransit(Integer from, Integer to){ return from!=null && to!=null && to == from + 1 && of(from)!=null && of(to)!=null; }
}
// game-module-biz-server/.../service/lead/BizLeadServiceImpl.java(流转校验骨架,范本 ProjectServiceImpl:207 同款事务边界)
// ★ 必须 @Transactional:两写(改 lead.status + insert progress 进度行)须原子,否则崩溃会留下
// 「状态已翻但时间线缺该节点行」或反之的不一致,看板与状态机错位(事务边界铁律 §3 红线)
@Transactional(rollbackFor = Exception.class)
public void advanceStatus(Long leadId, Integer toStatus, Long operatorUserId){
BizLeadDO lead = bizLeadMapper.selectById(leadId);
if (lead == null) throw exception(BIZ_LEAD_NOT_EXISTS);
Integer from = lead.getStatus();
if (!BizLeadStatusEnum.canTransit(from, toStatus)) throw exception(BIZ_LEAD_STATUS_ILLEGAL_TRANSITION); // 写前拒非法流转
// ★ →3待验收 守门:必须有可玩 demo(复用 runtime 取包判定,无产物/编译失败/versionId 缺失即拒)
if (Objects.equals(toStatus, BizLeadStatusEnum.ACCEPTING.getStatus())) { validateDemoPreviewable(lead.getDemoVersionId()); }
BizLeadDO update = new BizLeadDO(); update.setId(leadId); update.setStatus(toStatus); bizLeadMapper.updateById(update);
// 落进度工单(看板时间线);若由非 Web 线程/系统身份触发,入口须 LoginUser(id=0)+finally clearContext(否则 updater NOT NULL 拒绝 500)
BizProgressDO p = new BizProgressDO(); p.setLeadId(leadId); p.setFromStatus(from); p.setToStatus(toStatus); p.setOperatorUserId(operatorUserId); bizProgressMapper.insert(p);
}
confirm 端点跨表原子性(R-medium,必规约):客户
POST /app-api/biz/acceptances/{leadId}/confirm(§4.5)涉及两表写 + 状态机 3→4——置game_biz_acceptance.confirm_status=1+confirmed_time与advanceStatus(leadId, 4, ...)(推进 lead 3→4 + 落 progress 行)必须在同一@Transactional(rollbackFor=Exception.class)内,任一失败整体回滚,杜绝「已确认验收但 lead 未推进到已交付」的跨表不一致。前置约束:canTransit仅允许相邻正向单跳,confirm 走 3→4 合法,但前置态须确为 3(待验收),否则canTransit(非3, 4)抛BIZ_LEAD_STATUS_ILLEGAL_TRANSITION——端点文案须说明「仅待验收态可确认」。
6.7 biz demo 预览取包接线(复用 runtime 双路由 + 负路径降级)
- 模板列表 P-BIZ-02:
BizTemplateService返 aigc 4 可玩模板(clicker/merge/idle/tycoon,contracts/templates/已有 runtime 真实实现);dodge/runner/match 仅契约落盘未实现,不返。 - demo 取包:复用 runtime 双路由
GET /app-api/runtime/package/{versionId}(清单 meta,biz 看板「是否可预览」走此条)+/manifest(原始 JSON)。 validateDemoPreviewable(demoVersionId)(→3 守门 + 负路径):同进程注入RuntimePackageApi(@Primary本地实现)调getStatus(versionId);判定口径=status∈{0预览就绪,1已发布}即可预览,null=无就绪包=不可预览。负路径(R1 必验):demoVersionId缺失/非法、无编译产物(runtime 抛RUNTIME_PACKAGE_NOT_READY1-102-001-001)、编译失败时,抛BIZ_DEMO_NOT_READY(看板降级显示「该模板暂不可预览」),不透传 500;状态机不允许在无可玩 demo 时推进到「待验收」。- ⚠️ 预览鉴权口径缺口(R-low 拆分表述,勿合并为单一警告):两条路径不同,须分别处理:
- 「是否可预览」判定(
validateDemoPreviewable走RuntimePackageApi.getStatus)= owner-agnostic、不受 owner 校验影响。实查RuntimePackageApi.getStatus(:60-63)委托RuntimePackageService纯只查 status 单列、不做 owner/scene 校验,故此判定路径不会撞RUNTIME_PACKAGE_PREVIEW_NOT_OWNER——实现 agent 勿误以为 getStatus 会被 owner 拦。 - 客户真实试玩取包(走控制器
GET /app-api/runtime/package/{versionId})才有 owner+scene 门禁:preview 场景userId==null即拒;biz 客户(customer_user_id)≠版本创作者(运营代建)时可能触发RUNTIME_PACKAGE_PREVIEW_NOT_OWNER(1-102-001-003)。建议:biz demo 走 play 场景(status=1 已发布) 而非 preview 场景,或 biz demo 先发布到 status=1 后再供客户试玩(避免 owner 校验拦截)。此 owner 风险仅存在于真实试玩取包路径,与上一条 getStatus 判定无关。落地前与 runtime owner 确认此口径。
- 「是否可预览」判定(
7. 配置与权限登记
7.1 装配点(§2.2 复述)
两处:① root game-cloud/pom.xml <modules> 注册 <module>game-module-community</module>+<module>game-module-biz</module>(line34 后);② yudao-server/pom.xml <dependencies> 注册两个 -server 依赖(studio-server 后)。
7.2 上游 -server pom 加 community-api 依赖
aigc / project / trade 三个 -server/pom.xml 各加 game-module-community-api 依赖(照 project-server/pom.xml:34-53 加 compliance-api/runtime-api/feed-api 的写法)。
7.3 Nacos 模块配置注册(对齐 add-business-module.md 步骤6)
| 配置项 | 默认值 | 用途 |
|---|---|---|
community.sms.enabled |
false |
短信发送开关(收益高优可选短信;默认关,站内信成功即 AC 达标) |
community.level.publish-threshold |
3 |
新人里程碑阈值(发满 3 作品触发 reward) |
biz.state-machine.timeout-hours |
(评审版列) | 状态机节点超时(如需;硬编码则可跳过) |
deploy/nacos/ 加配置项并导入,验证 Nacos 控制台可见。若 community/biz 通知模板/阈值硬编码无模块级配置,本步可精简(评审版未强制要求新增 Nacos 配置)。
7.4 biz admin 权限点登记(⚠️ recon biz-design 必做缺口)
/admin-api/biz/* 用 @PreAuthorize("@ss.hasPermission('biz:...')"),权限点 biz:lead:query/create/assign/advance、biz:quote:create/query、biz:acceptance:sign 需在 yudao system_menu 登记后 RBAC 才放行——否则 admin 端点建好但管理员无权限调用(403)。本文要求:execution 落地时以 V13 或单独菜单初始化 SQL 登记这些权限点(范本=project:review:query 在 yudao 的登记方式)。
8. 边角失败路径(必落)
8.1 事务提交后 notify + try-catch 吞异常
见 §6.5:三上游 notify 均在事务提交后调用 + try-catch 仅记 error log,通知失败不回滚业务、不中断结算循环。
8.2 系统身份写防 updater NOT NULL 雷(R3,单测 mock 测不到)
community 通知投递 / reward 记账 / biz 状态机推进若由非 Web 线程或系统身份触发 → 无登录态 →
updaterNOT NULL 拒绝 → 链路 500/任务卡死。单测 mock 测不到此雷(项目记忆铁律)。
范本(照 EventIngestServiceImpl.injectSystemIdentityIfAnonymous,game-module-telemetry/.../service/event/EventIngestServiceImpl.java:187-198 + finally :144-147,canonical 源头=AigcGenerateExecutor.executeWithSystemIdentity):
// CommunityNotifyApiImpl 四方法入口 + BizLeadServiceImpl 系统/异步触发入口
LoginUser loginUser = new LoginUser().setId(0L).setUserType(UserTypeEnum.ADMIN.getValue()).setTenantId(1L);
SecurityContextHolder.setContext(new SecurityContextImpl(...)); // 注入系统身份
try {
// ... 业务写(DefaultDBFieldHandler 自动填 creator/updater='0')
} finally {
SecurityContextHolder.clearContext(); // 防 Web 线程上下文泄漏
}
8.3 demo 取包失败降级
见 §6.7:validateDemoPreviewable 对取包失败/无产物/versionId 缺失抛 BIZ_DEMO_NOT_READY、看板降级「该模板暂不可预览」、状态机拒推进到待验收,不透传 500。
8.4 mock-trigger 安全边界
/admin-api/community/notifications/{type}/mock-trigger 须 @Profile({"dev","staging"})(本仓首例,全 game-module 无 @Profile 先例,须验证 staging profile 下 Bean 注册生效)或 @ConditionalOnProperty(与现有 AIGC_EXECUTOR_ENABLED 灰度开关风格一致,推荐)或网关层关闭,生产一律关闭——否则成匿名伪造投递入口(安全须在可信边界 enforce,前端不够)。三者二选一钉死给最小代码范本。
8.5 @Primary Bean 冲突(R7)
新模块 Feign 接口 + 本地 @Primary 实现可能 Bean 冲突致启动失败(M1 实证 4 个 CommonApi)。ApiImpl 标 @RestController @Primary;仍冲突则给 @FeignClient 补 primary=false(修法 commit d7fac90);启动盯 BeanDefinition 冲突日志。
9. 验证方法(community 5 + biz 6 P0 AC + 工程门 + staging 实测 + UI 走查)
9.1 AC 逐条口径
community(5 P0)
| # | 口径 |
|---|---|
| AC-CMU-1 P-NTF-01 | 四类消息(系统/公告/审核/收益)可投递、/app-api/community/messages 列表可读、{id}/read 后 read_status=1 且 unread-count 递减 |
| AC-CMU-2 P-NTF-02 | aigc 生成完成后 notifyGenerateDone 被调→通知到达消息中心(前提:aigc 已接挂点1 或经 mock-trigger 验投递) |
| AC-CMU-3 P-NTF-03 | 审核出结果后 notifyReviewResult 被调→落站内信。仅 decision=1通过/2拒绝 发通知;decision=3下架(含封禁联动经 unlistIfPublished 系统身份路径)本波不发通知(非 P0 已知边界,caller 以 decision∈{1,2} 守门,§6.5 挂点2)。decision=2拒绝须带 reason——但实查 ReviewReqVO.reason 仅 @Size(max=500)、无 @NotNull,全链路不强制 reject 时 reason 非空,故 community 侧 notify service 对 decision=2 且 reason 空须给兜底文案(如「未提供原因」)或记 warn,避免落「拒绝原因为空」站内信(配合 P-OPN-02) |
| AC-CMU-4 P-NTF-05 | trade 结算入账后 notifyIncomeChanged 被调→站内信(+community.sms.enabled=true 时可选短信)送达 |
| AC-CMU-5 P-INC-01 | 计数到达阈值(默认 3,community.level.publish-threshold)即触发:计数权威源=project 发布成功 notifyProjectPublished 累加 published_count(尽力计数口径——at-most-once 投递,承认丢通知会漏触发,§6.4),到达阈值后等级引擎自动写一条 game_community_reward(uk_creator_milestone 幂等,重复发布不重触发);本波终态=触发可观测+本地记账,status=0待发放;记入钱包/流量包+现金真实到账、以及计数可对账重建(project 补 countPublishedByCreator 兜底)留 M4 |
biz(6 P0)
| # | 口径 |
|---|---|
| AC-BIZ-1 P-BIZ-01 | POST /app-api/biz/leads 提交即落 game_biz_lead 一行 status=0待跟进 + 落 game_biz_progress 首条 to_status=0。Swagger/curl 可验 |
| AC-BIZ-2 P-BIZ-02 | GET /app-api/biz/templates 返 aigc 4 可玩模板;选定后经 runtime 双路由预览(/package/{versionId} + /manifest)。★负路径(R1 必验):versionId 缺失/非法、无编译产物、编译失败(RUNTIME_PACKAGE_NOT_READY)时看板降级「该模板暂不可预览」而非透传 500;advanceStatus 到 toStatus=3 时 validateDemoPreviewable 拒→抛 BIZ_DEMO_NOT_READY |
| AC-BIZ-3 P-BIZ-03 | 状态机各节点可经 /admin-api/biz/leads/{id}/advance 推进、/app-api/biz/leads/{id}/progress 取看板时间线(后端 API 层=本波验收)。客户侧看板(game-studio WS3)+admin 队列(WS4)=完成条件增强项,随对应工位排期走真人 UI 走查 |
| AC-BIZ-4 P-BIZ-04 | 客户「试玩+反馈+确认」三态走通——feedback 落 game_biz_acceptance、confirm 置 confirm_status=1 且状态机 3→4。签署/收款留 M4,本波线下签署+sign-offline 兜底 |
| AC-BIZ-5 P-BIZ-08 | 经 /admin-api/biz/leads(代客发起)+/admin-api/biz/leads/{id}/quotes(报价)Swagger/curl 可发起品牌营销定制单并完成报价(报价≠可支付订单)。运营代客发起 UI(WS4)=独立跟进项 |
| AC-BIZ-6 P-BIZ-12 | 文旅场景(bizType=2)可发起定制单并走验收。P0 子集=场景需求 CRUD+模板复用+专属看板必交(与 P-BIZ-01/03 同能力栈);P1「快速交付增强」列 M4 |
9.2 工程门精确 CLI(mini-desktop 执行,逐项可勾)
# 1) 编译(community;biz 换路径)
mvn -DskipTests clean install -pl game-cloud/game-module-community/game-module-community-server -am
# 2) 单测(继承 BaseMockitoUnitTest 绿)
# community: 投递/已读/计数累加/里程碑触发幂等;biz: BizLeadServiceImplTest 流转合法/非法/→3 无 demo 拒
# 3) 启动 yudao-server → 应用层 Flyway 自动 migrate V12/V13(两处副本字节一致校验:md5sum 全等)
# 4) Swagger 可见(返回非空且含新端点)
curl http://localhost:48080/v3/api-docs?group=community # biz 换 group=biz
# 5) doc.html 可见 community/biz 分组;Spring 启动无 @Primary Bean 冲突日志
9.3 staging 两条系统身份写链路实测(⚠️ R3 必测,单测 mock 测不到,「骨架编译过≠可验收」)
- 链路①=community 通知投递:上游 notify(或 staging
mock-trigger)→ 写game_community_message,验系统身份注入LoginUser(id=0)生效、updater落'0'、不 500。 - 链路②=biz 状态机推进:admin 推进定制单状态(
/admin-api/biz/leads/{id}/advance)→ 写game_biz_lead/game_biz_progress(含 reward 触发记账若由非 Web 线程触发),验非 Web 线程/系统身份写updaterNOT NULL 不 500。
9.4 UI 入口真人走查(完成条件增强项,跨工位绑定,不阻塞本波后端收口)
项目记忆铁律「编排器旁路/批跑 API 全绿掩盖 UI 缺陷」(
frontend-link-ui-walk-publish-fix)——UI 走查是唯一手段。
- community:消息中心列表/已读/未读角标在 game-studio(WS3)真人走查(6c6g CDP 走查配方:
run_in_background起 chrome +player_cdp.CdpSession,记忆frontend-link-ui-walk-publish-fix)。 - biz:客户侧进度看板(game-studio
/biz-orders/my-orders或邀请链,WS3)+ 运营代客发起/处理队列(game-admin,WS4,权限点 §7.4 登记后)真人走查。
10. 完成条件 + M4 留债 + 回滚
10.1 完成条件(全勾才算 done,「无验证证据不得 claim 完成」)
- §0 单序列资源预占收口提交(V12/V13 两副本字节一致 + 错误码段 + yaml + 两处装配点),三项前置校验证据入提交说明。
- community/biz 各自
mvn -DskipTests clean install -pl … -am通过(mini-desktop)+ 单测绿。 - 启动 yudao-server,Flyway migrate V12/V13 成功(两副本 md5 全等)+
curl …group=community/group=biz非空含新端点 + doc.html 可见 + 无 @Primary Bean 冲突。 - community 5 P0 + biz 6 P0 AC(§9.1)逐条满足(含 AC-BIZ-2 负路径、AC-CMU-5 幂等)。
- staging 两条系统身份写链路实测通过(§9.3,
updater落 '0' 不 500)——这是「骨架编译过≠真实可验收」的硬验收。 - UI 入口真人走查(§9.4)作为完成条件增强项跟进(绑 WS3/WS4 排期,不阻塞本波后端收口)。
- 收口后按知识沉淀铁律回写
.agents/skills/add-business-module.md(107/110 升「已落地」、补 root pom 注册独立步、补「跨模块通知优先同进程-apinotify-push 而非 MQ」范式)+ 新踩坑,更新.agents/README.md;更新docs/mvp/MVP进度总账.md+ 两模块.agent。
10.2 M4 留债(两类债分离,评审版 R1)
①资金流债(受 pay 桩 + ICP/支付进件日历闸门制约)
| 留债 | 说明 |
|---|---|
| biz T-BIZ-08 收费 | 报价转可支付订单 → pay 进件后接 PayOrderApi 收款(amount_fen 已按「分」口径预留,与 trade 同,直接复用) |
| biz T-BIZ-03 在线电子签章 | 三方签章;本波 game_biz_acceptance.signed_offline 线下兜底过渡 |
| community reward 真实记账/到账 | 记入钱包/流量包+现金到账=trade 暴露 IncentiveCreditApi + feed 暴露流量包分配 -api(当前均不存在)后补 seam;本波只写 game_community_reward.status=0,M4 回写 granted_ref+status=1 |
②前端 UI 入口债(跨工位协调,可与本波后端并行,非 M4 资金流债)
| 留债 | 工位 |
|---|---|
客户侧进度看板 game-studio /biz-orders/my-orders 或邀请链 |
WS3 |
| 运营代客发起 + 处理队列 game-admin(权限点 §7.4 登记) | WS4 |
| community 消息中心 UI | WS3 |
10.3 回滚
- Flyway:已合入迁移禁止修改,回滚只写新补偿迁移(
V12.0.1/V13.0.1DROP,两副本字节一致);CI 跑flyway validate阻断改旧。 - 代码:两模块纯新增(克隆),回滚=root pom 撤
<module>+ yudao-server pom 撤依赖 + 删两模块目录 + 撤三上游 notify 调用点(三处 try-catch 块)+ 撤 community.yaml/biz.yaml;对已建 11 模块零行为变更,回滚面收敛。 - 上游接线回滚:三个 notify 调用点均 try-catch 独立块,注释/删除即恢复上游原行为(不影响 aigc/project/trade 既有事务)。
11. 与评审版的勘误回流登记(治理铁律:以评审版为准 → 矛盾须回流不得单方改写)
本文 line6 自定铁律「本文不得与评审版矛盾;执行中发现矛盾,停工上报,以评审版为准」。落地实查发现评审版 3 处结论与真实代码不符,已回流评审版勘误段(HJ-WAVE4-001 顶部「⚠️ 勘误 ERRATA」+ §2.1/§2.2/AC-CMU-3 inline 标注),本文相应处方与之一致,验收口径唯一。
| 勘误# | 评审版原结论 | 实查订正 | 本文落点 |
|---|---|---|---|
| 勘误#1 | P-NTF-03 上游源 = compliance「审核状态机事件源」(§2.1/§2.2 Mermaid + P0 表 + AC-CMU-3) | 审核态权威在 project ProjectServiceImpl.reviewProject(写 review_record + 状态机 + reason);compliance 不持审核态(Gate 二态 + 封禁)。notify 挂 project AdminProjectController.reviewProject 外层 caller |
§6.5 挂点2(含作用域处方)+ §2.4 挂点表 |
| 勘误#2 | (评审版未展开 decision 态数)approve/reject 两态 | ReviewDecisionEnum 三态(1通过/2拒绝/3下架);本波仅 1/2 发通知,3下架(含 unlistIfPublished 系统身份第二路径)不发 |
§4.3 签名注释 + §6.5 挂点2 + AC-CMU-3 |
| 勘误#3 | P-INC-01 计数权威源(隐含强一致语义) | at-most-once 投递→「尽力计数」,丢通知会漏触发;可对账重建留 M4 | §6.4 + AC-CMU-5 |
未采纳项(实现 agent 已能直接定、无需回流评审版的实现细节):aigc 挂点1 的「succeeded 判别 + ID 取数」走提交后
selectByTraceId回查(评审版未涉及代码级取数,属 execution 细节);trade 挂点3 的net现算(同上);biz advanceStatus/confirm 的@Transactional边界(同上)。这些是 execution 版职责内的可落地化,不构成与评审版的结论矛盾,故只在本文钉准、不回流。
证据边界(2026-06-11 亲核 dev/2.0.0):root pom <modules> :10-35(末 game-module-studio :34);yudao-server pom <dependencies> :23-…(末 game-module 依赖 studio-server :80);Flyway 两副本 ceiling=V11(V1–V11 各两副本,最高 V11.0.0__create_passport_player_invite.sql;V10 golden-loop + V11 passport 均在两处),V12/V13 文件名占用 ls exit=2(未占用);错误码段 grep '1_107\|1_110' game-cloud/ 返 0;community.yaml/biz.yaml 不存在(exit=2);ProjectApi 现有方法=getCreatorUserId/getCurrentVersionId/getStatus/getFeedMeta/createProject/unlistIfPublished(无 countPublishedByCreator,故 P-INC-01 取方案 a);aigc DifyCallbackServiceImpl.handleCallback 内 return txService.handleCallbackTx(reqVO)(内层 @Transactional 三表写入链);project AdminProjectController.reviewProject:55-57 调 projectService.reviewProject(service @Transactional :207,APPROVE 同事务发布编排 :237-239);trade SettlementServiceImpl.settle 循环 firstRecord/newlyRecorded++ 分支(recordIncome @Transactional 单条提交、循环体非事务)。设计依据=评审版 HJ-WAVE4-001 + 两轮评审纪要 + 四份深度 recon(含可直接入文 DDL/端点/挂点 artifacts)。行号漂移以语义定位为准。本文为 execution 设计产出,未实跑 mvn/Flyway/单测——落地后须经 §9 工程门 + staging 两链路实测 + UI 走查验证方可 claim 完成(评审版 R10 铁律)。