Files
danyang-match/docs/superpowers/plans/2026-10-14-global-public-id-implementation.md
T

215 lines
16 KiB
Markdown
Raw Normal View History

# 全平台业务展示编号统一实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 让所有面向用户或跨角色展示的人员、任务、活动和业务记录 ID 使用 13 位稳定业务编号,并确保 Web、小程序和运营 API 的展示一致。
**Architecture:** 保留 PostgreSQL UUID 主键、外键、权限与路由寻址;为公开展示实体持久化业务编号。以 PostgreSQL 原子计数器按前缀和上海日期分配编号,在实体创建事务中写入;后端 DTO 同时提供内部 ID 与公开展示字段,前端只渲染公开字段。
**Tech Stack:** Java 21、Spring Boot、MyBatis-Plus、PostgreSQL、Flyway、Vue 3、TypeScript、微信原生小程序、Node.js 行为测试、Docker Compose。
**Spec:** `docs/superpowers/specs/2026-10-14-global-public-id-design.md`
## Global Constraints
- 公开编号严格使用 `2 位大写字母前缀 + Asia/Shanghai YYYYMMDD + 3 位序号`,共 13 位。
- 用户 `YH` 按注册日和性别分别排名,男性序号奇数、女性序号偶数;红娘 `HN`,其他工作人员 `GL`,任务 `RW`,活动 `HD`。
- UUID 主键、外键、JWT subject、数据范围、权限校验、幂等语义和既有 UUID API 路由保持不变。
- 推荐对象、被推荐人、被举报人和活动参与者统一展示用户 `YH` 编号。
- 任意用户可见页面、弹窗、导出和小程序不得渲染原始 UUID;不可用中文占位掩盖接口漏字段,必须修 DTO/查询。
- 不覆盖或回滚开始任务前工作区的任何修改;只提交本计划涉及的新改动。
- 不添加第三方依赖;后端唯一编号由 PostgreSQL 并发安全地产生。
---
## 文件结构与责任
- `backend/src/main/resources/db/migration/V060__add_global_public_display_ids.sql`:为业务实体增加公开编号列/计数器,并为现存数据确定性回填和建立唯一索引。
- `backend/src/main/java/com/danyang/match/platform/shared/id/`:编号前缀、长度、日期与原子分配服务;只新增小型领域工具,不重构 UUID 基础设施。
- 各领域 Entity、Mapper、Service、DTO:在创建事务中写入公开编号;查询以 JOIN 取关联用户/操作员的展示编号。
- `backend/src/test/java/com/danyang/match/platform/shared/id/` 与领域测试:格式、奇偶、并发、迁移、DTO 白名单和越权行为测试。
- `admin-web/src/modules/`:替换直接渲染 UUID 的列、详情、表单和导出;交互继续传内部 ID。
- `miniapp/pages/` 与 `miniapp/services/`:只显示公开编号,动作请求继续用内部 ID。
- `docs/DEMO-VALIDATION-2026-10-13.md`:追加编号回填与双端展示验收证据,不改写历史记录。
## Task 1: 建立全仓公开 ID 展示清单与锁定测试
2026-10-14 执行结果:模板/关键动态文案盘点与检查器已完成,清单见 `../specs/2026-10-14-global-public-id-display-inventory.md`。检查器 7 项自检 PASS;114 个模板发现 31 个审查候选,全局展示检查仍 FAIL。Java 响应行为覆盖测试移至 Task 3,避免仅断言源码字符串。预约 YY 前缀待用户确认后进入迁移实现。
**Files:**
- Create: `backend/src/test/java/com/danyang/match/platform/shared/id/PublicDisplayIdCoverageTest.java`
- Create: `tests/public-id-display-coverage.cjs`
- Inspect: `admin-web/src/modules/`、`miniapp/pages/`、各模块 Controller/DTO/Mapper、Flyway 迁移
- [ ] **Step 1: 建立清单和失败测试**
清单至少逐项记录:实体、UUID 列/关系、创建入口、API DTO、管理端页面、用户小程序页面、编号前缀、是否包含既有用户隐私权限。覆盖 `users`、`operators`、`activities`、`review_tasks`、`reports`、`matchmaker_services`、`matchmaker_recommendations`、`matchmaker_contact_authorizations`、`matchmaker_assignments`、`matchmaker_followups`、转介实体,以及搜索中发现的任何其他实际展示 UUID 的实体。
守卫测试扫描已登记的 Web Vue 与小程序 WXML/TS/JS 文件,拒绝把 `operatorId`、`userId`、`targetUserId`、`activityId`、`serviceId`、`recommendationId` 等内部 UUID 直接插值到可见模板;允许它们用于事件、路由和 API 参数。测试对明确的 display 字段白名单断言。
- [ ] **Step 2: 运行测试确认暴露当前差距**
Run: `node tests/public-id-display-coverage.cjs`
Expected: FAIL,报告仍直接显示 UUID 的具体文件与模板位置。
- [ ] **Step 3: 固化清单与覆盖测试**
将前置检查清单保存到 `docs/superpowers/specs/2026-10-14-global-public-id-display-inventory.md`;确认每个展示 ID 均映射到规格中已有前缀。若发现需新增前缀或改变既有 API 路由参数,暂停后续实现并修订规格等产品确认。
- [ ] **Step 4: 运行测试**
Run: `node tests/public-id-display-coverage.cjs`
Expected: 检查器自检 PASS;展示扫描在遗留问题修复前保持 FAIL,不将已知问题豁免为全局 PASS。
## Task 2: 编号分配器、字段迁移和历史回填
**Files:**
- Create: `backend/src/main/resources/db/migration/V060__add_global_public_display_ids.sql`
- Create: `backend/src/main/java/com/danyang/match/platform/shared/id/PublicDisplayId.java`
- Create: `backend/src/main/java/com/danyang/match/platform/shared/id/PublicDisplayIdAllocator.java`
- Create: `backend/src/main/java/com/danyang/match/platform/shared/id/PublicDisplayIdMapper.java`
- Modify: `backend/src/main/java/com/danyang/match/platform/user/UserDisplayNumber.java`
- Modify: 相关 entity,至少 `User.java`、`Operator.java`、`Activity.java`、审核/举报/红娘相关 entity
- Create: `backend/src/test/java/com/danyang/match/platform/shared/id/PublicDisplayIdTest.java`
- Create: `backend/src/test/java/com/danyang/match/platform/shared/id/PublicDisplayIdAllocatorTest.java`
- Modify: `backend/src/test/java/com/danyang/match/platform/deployment/MigrationValidationTest.java`
- Modify: `backend/src/test/resources/schema-h2.sql`
- [ ] **Step 1: 编写编号格式和分配器失败测试**
测试 `YH20261014001`、`YH20261014002` 的用户奇偶规则,普通记录格式 `RW20261014001`;非法前缀、日期、序号、用户性别、序号越过 999 必须拒绝。PostgreSQL mapper 测试以并发任务申请同一前缀同一天的编号,断言数量一致且全部唯一;禁止使用 `COUNT(*) + 1`。
- [ ] **Step 2: 运行定向测试确认失败**
Run: `cd backend; mvn -Dtest=PublicDisplayIdTest,PublicDisplayIdAllocatorTest test`
Expected: FAIL,因编号工具和 mapper 尚未实现。
- [ ] **Step 3: 添加持久化编号字段与回填**
新增迁移为清单内公开实体添加 `display_id`(用户沿用 `display_number`,工作人员沿用 `display_id`),建立 `public_display_id_counters(prefix, local_date, sequence_group, last_sequence)` 计数器表。迁移按 `Asia/Shanghai` 注册/创建日期及稳定次序回填:用户按实名性别分别排名并写入男奇女偶尾号;工作人员按角色决定 `HN/GL`;任务 `RW`;活动 `HD`;其余业务表按规格与覆盖清单中的前缀分表排名。回填完成后设置非空和部分唯一约束;软删除记录也保留其编号唯一性,不得复用。初始化计数器到每组已使用的最大序号。
- [ ] **Step 4: 实现原子分配器**
Mapper 使用 PostgreSQL 原子 UPSERT:普通编号按 `sequence_group=''` 每次加 1;用户编号按 `sequence_group=gender` 分别加 2,男性首值 1、女性首值 2。更新前检查生成值不超过 999;无返回值时抛明确容量异常。日期由 `Clock` 注入并按 `Asia/Shanghai` 转换。`UserDisplayNumber` 调整为接受已分配奇偶序号且支持三位尾号,避免自行计数。
- [ ] **Step 5: 运行后端定向测试与迁移验证**
Run: `cd backend; mvn -Dtest=PublicDisplayIdTest,PublicDisplayIdAllocatorTest,MigrationValidationTest test`
Expected: PASS;并发下无重复编号,迁移在空 H2 测试结构与 PostgreSQL 演示数据库验证通过。
## Task 3: 后端创建写入与展示 DTO 收口
**Files:**
- Modify: 各展示实体的 Mapper/Service/Controller/DTO,按 Task 1 清单逐项列出
- Modify: `backend/src/main/java/com/danyang/match/platform/matchmaker/mapper/MatchmakerContactAuthorizationMapper.java`
- Modify: `backend/src/main/java/com/danyang/match/platform/matchmaker/service/ContactAuthorizationService.java`
- Modify: `backend/src/main/java/com/danyang/match/platform/matchmaker/service/MatchmakerManagementService.java`
- Modify: 活动、审核、举报、审计、转介等清单中 Mapper 查询和响应 DTO
- Create: `backend/src/test/java/com/danyang/match/platform/shared/id/PublicDisplayIdResponseTest.java`
- Modify: 对应领域测试与 `backend/src/test/resources/schema-h2.sql`
- [ ] **Step 1: 为创建流程写回归测试**
对每类清单实体调用真实 Service 创建入口,断言同事务持久化 UUID 主键和正确前缀的公开编号;模拟编号分配失败,断言领域实体未写入。用户和工作人员 API 输出同时断言 `id` 仍为既有 UUID、`displayNumber/displayId` 是业务编号。
- [ ] **Step 2: 运行测试确认失败**
Run: `cd backend; mvn -Dtest=PublicDisplayIdResponseTest,MatchmakerContactAuthorizationTest,MatchmakerManagementReadTest test`
Expected: 新增编号断言失败,既有权限测试保持可执行。
- [ ] **Step 3: 接入创建事务与关联编号查询**
对每个实体创建 Service,在已有 `@Transactional` 中先分配编号再 insert;操作员编号依实际角色用 `HN/GL`。Mapper 查询只额外 JOIN 公开编号字段;DTO 增加语义明确的 `displayId`/`displayNumber` 和关联对象 `operatorDisplayId`、`userDisplayNumber`、`targetUserDisplayNumber`。contact authorization 不再把底层 `operatorId/userId` 作为展示字段使用。
推荐对象、被举报人、活动参与者和审核对象关联到 user profile 时,复用 `users.display_number`;查询必须保留现有数据范围、用户隐私白名单和防探测条件。列表只补展示字段,不扩大查询可见范围。
- [ ] **Step 4: 检查未覆盖 API 和 UUID 响应泄漏**
运行后端静态覆盖测试和针对 Controller JSON 的响应断言;确认审计记录实体引用只在有权限且可映射时返回业务编号,否则返回实体类型脱敏说明。搜索所有列表 DTO 对 UUID 的输出使用,逐项按清单修复,不修改内部写请求。
- [ ] **Step 5: 运行相关后端回归**
Run: `cd backend; mvn -Dtest=PublicDisplayId*,MatchmakerContactAuthorizationTest,MatchmakerAssignmentA1Test,MatchmakerManagementReadTest,Activity*,Review*,Report*,MigrationValidationTest test`
Expected: PASS;既有权限、事务、隐藏/软删除与错误语义不变。
## Task 4: 管理端只展示业务编号
**Files:**
- Modify: `admin-web/src/modules/matchmaker/MatchmakerManagementView.vue`
- Modify: `admin-web/src/modules/activity/ActivityInterestView.vue`
- Modify: `admin-web/src/modules/activity/ActivityCheckinView.vue`
- Modify: `admin-web/src/modules/operator/OperatorManagementView.vue`
- Modify: `admin-web/src/modules/audit/AuditLogView.vue`
- Modify: Task 1 清单识别的用户、举报、安全、审核、报名、活动、联系授权、转介等视图
- Modify: 对应 API 类型定义与 service adapter
- Modify: `tests/public-id-display-coverage.cjs`
- [ ] **Step 1: 为目标页面写展示行为测试**
使用虚拟 API fixture 分别提供 UUID 与展示编号;渲染断言应见 `HN...`、`YH...`、`HD...`、`RW...` 等展示编号且不见 UUID,点击详情/审批/筛选后请求仍携带原内部 UUID。
- [ ] **Step 2: 修复模板和交互控件**
表格/详情使用 DTO 的公开编号字段;推荐对象输入改为可搜索用户选择器,按公开用户编号和允许公开的基础资料搜索,提交时仍传选中对象内部 UUID。若缺展示编号,展示“编号待生成”并在接口测试中判为缺陷,不允许回退 UUID。
- [ ] **Step 3: 更新覆盖守卫并运行管理端检查**
Run: `cd admin-web; pnpm run type-check; pnpm run build; cd ..; node tests/public-id-display-coverage.cjs`
Expected: 类型检查、构建和 ID 展示守卫 PASS。
## Task 5: 小程序只展示业务编号
**Files:**
- Modify: Task 1 清单中用户卡片、活动详情/报名/签到、红娘工作台/服务详情/推荐进展、举报与安全、个人资料/认证等页面
- Modify: 必要的 `miniapp/services/*.js` response adapter
- Modify: `miniapp/types/index.ts` 与 `miniapp/types/index.d.ts` 的展示字段
- Create: `tests/miniapp/public_id_display_behavior_test.cjs`
- Modify: `tests/public-id-display-coverage.cjs`
- [ ] **Step 1: 写小程序字段展示失败测试**
使用接口响应 fixture 注入 `id/userId/targetUserId` UUID 和对应 `displayId/displayNumber`,断言 WXML 页面绑定展示字段、服务跳转和 Like/Pass/报名/举报动作仍提交 UUID;无展示编号时不把 UUID 作为空态回退值。
- [ ] **Step 2: 修正响应类型和页面绑定**
更新 TS 声明和各页面 WXML/JS,让列表、详情、确认弹窗、活动页面和举报确认中的公开文本只用业务编号;保留内部 ID 用于请求和路由。
- [ ] **Step 3: 运行小程序行为与结构检查**
Run: `node tests/miniapp/public_id_display_behavior_test.cjs; node tests/public-id-display-coverage.cjs; pnpm --dir miniapp run type-check`
Expected: PASS;另运行项目现有 WXML/WXSS 结构守卫和 matchmaker/活动相关测试。
## Task 6: 全量迁移、运行验收和文档证据
**Files:**
- Modify: `docs/DEMO-VALIDATION-2026-10-13.md`
- Modify: `docs/STATUS.md`
- Inspect: `infrastructure/docker-compose.yml`、运行中的 danyang Docker 服务
- [ ] **Step 1: 执行完整后端测试及迁移检查**
Run: `cd backend; mvn test`
Expected: 记录总数、失败、错误、跳过;全部失败/错误为 0。PostgreSQL 演示库迁移完成,检查每表编号无空值、无重复、长度正确,用户奇偶规则正确,计数器不小于已分配最大尾号。
- [ ] **Step 2: 执行 Web、小程序回归**
Run: `cd admin-web; pnpm run type-check; pnpm run build; cd ..; pnpm --dir miniapp run type-check; node tests/public-id-display-coverage.cjs`
Expected: 全部 PASS;执行现有小程序结构与相关行为测试。
- [ ] **Step 3: 演示环境 HTTP 和页面检查**
以演示账号读取联系授权、红娘服务/推荐、用户、活动、审核、举报接口,逐类抽查 UUID 与业务编号关系;确认权限拒绝样本仍拒绝。检查浏览器常用视图及微信开发者工具小程序核心页,页面无 UUID 可见文本,动作正常。
- [ ] **Step 4: 记录证据并完成阶段报告**
在 `docs/DEMO-VALIDATION-2026-10-13.md` 追加编号格式、各类数量、重复检测、旧号映射、测试输出摘要和目视验收结果;在 `docs/STATUS.md` 仅追加当前结论,不覆盖历史状态。运行 `git diff --check`,最终报告逐项 `PASS/PARTIAL/FAIL/NOT_VERIFIED`。
## 完成门禁
- 六个任务均有实现与独立测试证据;不得因前端隐藏列而遗漏后端公开 DTO。
- 演示 PostgreSQL 中公开编号检查 SQL 返回 0 空值(仅规格明确允许的未实名用户可空)、0 重复、0 格式错误。
- 联系授权截图展示 `HN...` 与 `YH...`;跨模块推荐、举报和活动同一用户编号相同。
- 所有 UUID 保留作内部键但不出现在面向用户的可见文本、导出或通知。
- 若既有客户端依赖公开 UUID 字段作为用户输入,应提供公开编号解析适配并做兼容测试;权限判断仍基于解析后的 UUID。
- 最终说明未执行的微信真机验收或不可执行测试,不将历史测试结果写成本轮证据。