# 家谱加入申请与审核 Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 开放家谱申请加入、我的申请、撤销待审核申请,以及家谱管理员审核申请的完整 PC 页面闭环。 **Architecture:** `utils/ApiClient.js` 已拥有本批全部 PC path/body。新增 `join-pages.js` 负责可申请家谱选择、申请表单、我的申请和撤销;新增 `join-review-pages.js` 负责当前家谱待审核列表和通过/拒绝。两个 UMD 模块只使用响应中的字符串 ID,写入后重读对应列表。 **Tech Stack:** 静态 HTML、原生 JavaScript UMD、Axios、Node `node:test`。 ## Global Constraints - 后端 `D:\WorkSpace\Java\Genealogy` 全程只读。 - 只调用 `/genealogy/pc/**`。 - 家谱 ID 和申请 ID 必须来自 PC 响应或当前家谱上下文,不能手填。 - `inviterUserId` 没有安全 PC 来源,不展示、不发送。 - 所有写操作防重复,401 清登录态,403 保留登录态。 - 每项生产行为先运行失败测试,再最小实现。 --- ### Task 1: 申请数据边界 **Files:** - Create: `tests/join-pages.test.js` - Create: `public/js/join-pages.js` **Interfaces:** - Consumes: `genealogyOptions(query)`、`applyToGenealogy(genealogyId, body)`、`myGenealogyJoinApplies()`、`cancelGenealogyJoinApply(applyId)` - Produces: - `normalizeJoinGenealogy(item)` - `buildJoinApplyBody(values)` - `validateJoinApplyBody(body)` - `normalizeJoinApply(item)` - `normalizeJoinApplies(data)` - `renderJoinGenealogyOptions(data)` - `renderMyJoinApplies(data)` - [ ] **Step 1: 写失败的纯行为测试** 用完整 `AppGenealogyVo` 和 `GenealogyJoinApplyVo` 字面量验证: ```js assert.deepEqual(JoinPages.buildJoinApplyBody({ applicantName: ' 叶子 ', phone: ' 19181970173 ', relationDesc: ' 族亲 ', applyReason: ' 申请加入 ', inviterUserId: 'must-drop' }), { applicantName: '叶子', phone: '19181970173', relationDesc: '族亲', applyReason: '申请加入' }); ``` 断言: - `applicantName <= 50`、`phone <= 30`、`relationDesc <= 100`、`applyReason <= 500`。 - 可申请家谱必须有稳定 `genealogyId/genealogyName/surname`,停用家谱拒绝。 - 申请必须有稳定 `applyId/genealogyId` 和 `status`;状态只允许 `0/1/2/3`。 - 安全数字长 ID 只接受字符串;不安全 number 拒绝。 - 普通用户列表不展示内部用户 ID、邀请人 ID、审核人 ID或原始 JSON。 - 只有 `status=0` 渲染撤销按钮。 - [ ] **Step 2: 运行 RED** Run: ```powershell node --test tests/join-pages.test.js ``` Expected: FAIL,模块不存在。 - [ ] **Step 3: 最小实现纯函数** 所有可见文本使用 `escapeHtml`;响应任一必需字段无效时拒绝该条,数组包含非法元素时整批返回空数组。 - [ ] **Step 4: 运行 GREEN** Run: ```powershell node --test tests/join-pages.test.js node --check public/js/join-pages.js ``` Expected: PASS。 --- ### Task 2: 申请页面与我的申请 **Files:** - Modify: `join-genealogy.html` - Modify: `profile-join-family.html` - Modify: `public/js/join-pages.js` - Modify: `tests/join-pages.test.js` - Modify: `tests/pending-pages.test.js` - Modify: `tests/pc-scope.test.js` **Interfaces:** - Produces: `initJoinApplyPage()`、`initMyJoinAppliesPage()` - [ ] **Step 1: 写失败的页面行为测试** 断言: - 两页均不再 pending,并加载 `join-pages.js`。 - `join-genealogy.html` 有关键词搜索、后端家谱选项、申请资料表单;不存在 `genealogyId/inviterUserId/applyId` 文本输入。 - `profile-join-family.html` 有我的申请列表和刷新入口;申请 ID 只存在于响应渲染按钮属性。 - 页面没有邀请码输入或“分享码”伪语义。 - [ ] **Step 2: 运行 RED** Run: ```powershell node --test tests/join-pages.test.js tests/pending-pages.test.js tests/pc-scope.test.js ``` Expected: FAIL,页面仍 pending/缺少真实行为。 - [ ] **Step 3: 实现申请和撤销** `join-genealogy.html`: 1. 读取 `genealogyOptions({keyword})`。 2. 用户只能点击响应卡片选择家谱,内部保存字符串 `genealogyId`。 3. 构造并校验申请 body。 4. 提交后重读 `myGenealogyJoinApplies()`,必须找到同一响应 `applyId`。 5. 跳转 `profile-join-family.html`。 `profile-join-family.html`: 1. 读取并渲染我的申请。 2. 只有待审核申请显示撤销。 3. 二次确认后调用撤销接口并重读列表,确保该申请不再为待审核。 - [ ] **Step 4: 运行 GREEN** Run: ```powershell node --test tests/join-pages.test.js tests/pending-pages.test.js tests/pc-scope.test.js node --check public/js/join-pages.js ``` Expected: PASS。 --- ### Task 3: 管理员审核 **Files:** - Create: `tests/join-review-pages.test.js` - Create: `public/js/join-review-pages.js` - Modify: `profile-join-review.html` - Modify: `tests/pending-pages.test.js` - Modify: `tests/pc-scope.test.js` **Interfaces:** - Consumes: `genealogyDetail(genealogyId)`、`pendingGenealogyJoinApplies(genealogyId)`、`auditGenealogyJoinApply(genealogyId, applyId, body)` - Produces: - `getCurrentGenealogyId(search)` - `buildJoinAuditBody(values)` - `validateJoinAuditBody(body)` - `renderPendingJoinApplies(data, canManage)` - `initJoinReviewPage()` - [ ] **Step 1: 写失败的审核行为测试** ```js assert.deepEqual(JoinReviewPages.buildJoinAuditBody({ status: '2', auditRemark: ' 资料不一致 ', applyId: 'must-drop' }), { status: '2', auditRemark: '资料不一致' }); ``` 断言: - `status` 只允许 `1/2`,`auditRemark <= 500`。 - 页面只从 URL/`ProfileUI` 读取家谱 ID。 - 只有 `genealogyDetail.canManage=true` 显示审核动作。 - 待审核列表显示申请人名称、申请手机号、关系和原因,但隐藏内部用户 ID、邀请人/审核人 ID。 - 通过和拒绝按钮使用响应中的稳定 `applyId`。 - [ ] **Step 2: 运行 RED** Run: ```powershell node --test tests/join-review-pages.test.js ``` Expected: FAIL,模块不存在且页面仍 pending。 - [ ] **Step 3: 实现审核页面** 加载当前家谱详情和待审核列表;没有上下文时阻止请求并返回家谱选择入口。点击通过使用 `{status:'1'}`;拒绝要求输入可选审核说明并使用 `{status:'2', auditRemark}`。审核成功后重读待审核列表,确保同一 `applyId` 不再存在。 - [ ] **Step 4: 运行 GREEN** Run: ```powershell node --test tests/join-review-pages.test.js tests/pending-pages.test.js tests/pc-scope.test.js node --check public/js/join-review-pages.js ``` Expected: PASS。 --- ### Task 4: 导航、规划与验证 **Files:** - Modify: `profile-families.html` - Modify: `profile-family-admin.html` - Modify: `profile.html` - Modify: `tests/stage6-navigation.test.js` - Modify: `docs/PC接口对接规划.md` - Modify: `docs/superpowers/plans/2026-07-29-genealogy-join-review.md` - [ ] **Step 1: 写失败的导航测试** 断言我的家谱入口可达申请页,家谱管理入口携带上下文进入审核页;不存在指向手填家谱 ID 或邀请码页面的入口。 - [ ] **Step 2: 运行 RED** Run: ```powershell node --test tests/stage6-navigation.test.js tests/pending-pages.test.js ``` Expected: FAIL,审核导航尚未稳定携带家谱上下文。 - [ ] **Step 3: 开放导航并更新规划** 记录申请/审核 DTO、VO、权限、状态、字段来源、写后重读和 `inviterUserId` 阻断。 - [ ] **Step 4: 聚焦和全量验证** Run: ```powershell node --test tests/join-pages.test.js tests/join-review-pages.test.js tests/api-client-contract.test.js tests/pending-pages.test.js tests/pc-scope.test.js tests/stage6-navigation.test.js npm test node --check public/js/join-pages.js node --check public/js/join-review-pages.js git -c safe.directory=D:/WorkSpace/Web/jiapu diff --check ``` Expected: 全部 PASS。 - [ ] **Step 5: 浏览器验证并报告** 真实账号验证可申请家谱列表、我的申请空状态、无家谱上下文审核分支、隐私和控制台错误。未经用户明确授权,不提交或审核真实申请。