feat(api): 完善家谱系统API客户端契约

- 实现家谱管理相关方法,包括创建、详情、概览、我的家谱和选项查询
- 添加家谱加入申请功能,支持申请、审核、取消和待审核列表操作
- 集成通知详情获取方法和通知ID安全验证机制
- 完善功德记录、谱文、相册、视频、祭祀活动的完整CRUD操作契约
- 实现家谱成员管理功能,包含成员列表、更新、移除和转让所有者操作
- 优化路径ID验证逻辑,拒绝不安全的数值ID并提供明确错误提示
- 更新测试用例以验证所有新增API方法的路径和请求体白名单机制
This commit is contained in:
fizzleaf
2026-07-29 16:57:27 +08:00
parent 59a72fb22b
commit fb1743aa2a
74 changed files with 13166 additions and 1320 deletions
+498 -51
View File
@@ -156,13 +156,39 @@
| C11 | `completed` 只有 string 类型,无枚举 | 不能决定复选框提交 `0/1`、true/false 或其他值 | Apifox 增加枚举和中文含义 |
| C12 | `feedType``recordType``registerSource` 等无枚举 | 无法判断文本输入还是选择项 | 明确为自由文本,或补全枚举 |
| C13 | 多个日期字段没有 format/时区规则 | 页面值和后端解析可能不一致 | 明确 date 或 date-time,并统一时区 |
| C14 | 只有配额,没有我的家谱/成员入口 | 无法取得 `genealogyId`、成员或受邀用户选项 | 增加 PC 家谱列表/选项和成员选项接口,或明确外部上下文契约 |
| C14 | 已由后端 PC 实链接决 | `GET /genealogy/pc/genealogies/mine``/options` 返回完整 `AppGenealogyVo``genealogyId` 从响应取得 | 前端只从真实列表选择,不允许手填 ID |
| C15 | `bindingMode=SPECIFIED` 需要 `appUserId`,但无业务用户选项接口 | 不能提供合法选择器 | 增加家谱成员/业务用户选项接口 |
| C16 | 邀约的 `inviteeUserIds` 没有选项接口 | 不能让管理员选择受邀人 | 增加同一家谱成员选项接口 |
| C17 | 视频、成长、亲友、备忘、功德、通知响应未展开 | 无法安全展示、编辑或单条操作 | 分别增加资源 View 和列表/详情响应 |
| C18 | 更新接口未说明缺省、null、空串语义 | 可选字段可能无法清空 | Apifox 对每个可清空字段写明更新规则 |
| C19 | 多数 View 没有 `canEdit` / `canDelete` 等权限字段 | 页面无法可靠决定按钮可见性 | 增加能力字段,或提供明确且可实现的角色规则 |
| C20 | 文章、相册、祭祀只有删除操作 | 没有真实 ID 来源和完整资源流程 | 补全 PC 列表/详情/新增/修改后才开放页面 |
| C21 | 家族圈动态只返回 `mediaOssIds`,PC 没有 OSS ID 批量解析/访问 URL 接口 | 已上传图片只能统计数量,不能在列表和详情安全回显缩略图 | PC 增加按 OSS ID 返回授权 URL、文件名和媒体类型的接口,或让动态 View 直接返回媒体对象数组 |
| C22 | 停用动态不会出现在普通列表,普通详情接口也拒绝读取 | 管理员可以停用,但停用后无法从 PC 页面重新读取、恢复或验证最终状态 | 增加包含停用记录的管理列表/详情,或增加独立的状态恢复接口 |
| C23 | `VideoVo` 只返回 `coverOssId` / `videoOssId`,没有 PC 文件访问 URL 或解析接口 | 可以维护视频记录,但不能安全播放视频或回显封面 | 视频 View 直接返回授权 URL,或增加按 OSS ID 获取文件访问地址的 PC 接口 |
| C24 | 视频列表只返回正常记录,详情拒绝读取停用记录 | `status=1` 后无法从页面重新读取和恢复 | 增加视频 management 列表/详情,或增加独立状态恢复接口 |
| C25 | `GrowthRecordVo` 只返回附件 `mediaOssIds`,没有 PC 文件访问 URL 或解析接口 | 可以维护附件引用,但不能安全预览、下载或回显文件名 | 成长记录 View 返回附件对象,或增加按 OSS ID 获取授权地址、文件名和类型的 PC 接口 |
| C26 | 成长记录列表只返回正常记录,详情拒绝读取停用记录 | `status=1` 后无法写后重读、恢复或再次编辑 | 增加成长记录 management 列表/详情,或增加独立状态恢复接口 |
| C27 | `giftAmount` 只有 `number` / `BigDecimal` 类型,没有精度、范围和正负规则 | 前端不能自行限定两位小数、最大金额或禁止负数 | Apifox 明确 scale、precision、minimum/maximum 和负数业务含义 |
| C28 | `RelativeRecordVo` 只返回附件 `mediaOssIds`,没有 PC 文件访问 URL 或解析接口 | 可以维护附件引用,但不能安全预览、下载或回显文件名 | 亲友记录 View 返回附件对象,或增加按 OSS ID 获取授权地址、文件名和类型的 PC 接口 |
| C29 | 亲友记录列表只返回正常记录,详情拒绝读取停用记录 | `status=1` 后无法写后重读、恢复或再次编辑 | 增加亲友记录 management 列表/详情,或增加独立状态恢复接口 |
| C30 | `MemoVo` 只返回附件 `mediaOssIds`,没有 PC 文件访问 URL 或解析接口 | 可以维护附件引用,但不能安全预览、下载或回显文件名 | 备忘录 View 返回附件对象,或增加按 OSS ID 获取授权地址、文件名和类型的 PC 接口 |
| C31 | 备忘录列表只返回正常记录,详情拒绝读取停用记录 | `status=1` 后无法写后重读、恢复或再次编辑 | 增加备忘录 management 列表/详情,或增加独立状态恢复接口 |
| C32 | 功德金额只有 `number` / `BigDecimal` 类型,没有精度、范围和正负规则 | 前端不能自行限定两位小数、最大金额或禁止负数 | Apifox 明确 scale、precision、minimum/maximum 和负数业务含义 |
| C33 | 功德记录列表只返回正常记录,详情拒绝读取停用记录 | `status=1` 后无法写后重读、恢复或再次编辑 | 增加功德记录 management 列表/详情,或增加独立状态恢复接口 |
| C34 | YAML `NotificationView` 声明了家谱编号/名称、发送人昵称/手机号和业务摘要,但 PC `toVo` 未填充这些字段 | 页面可能长期显示空白,导出契约与真实 PC 响应不一致 | 后端补齐字段,或从 PC Schema 删除不会返回的字段并明确隐私策略 |
| C35 | PC 谱文后端列表支持可选 `categoryId`,YAML 未声明该 query,且没有文章分类选项接口 | 前端无法可靠提供分类筛选或编辑分类 | YAML 补齐 query,并提供当前家谱文章分类选项接口 |
| C36 | `ArticleVo` 只返回 `coverOssId`,没有 PC 文件访问 URL | 谱文可维护封面引用,但不能安全回显封面图片 | Article View 返回封面 URL,或增加 OSS ID 授权解析接口 |
| C37 | 谱文列表只返回正常记录,详情拒绝读取停用记录 | `status=1` 后无法写后重读或恢复 | 增加谱文 management 列表/详情,或独立状态恢复接口 |
| C38 | `AlbumVo` / `AlbumPhotoVo` 只返回 `coverOssId` / `ossId`,没有 PC 文件访问 URL | 相册和照片记录可维护,但不能安全回显封面或照片内容 | View 返回授权 URL,或增加 OSS ID 授权解析接口 |
| C39 | 相册和照片列表只返回正常记录,且没有停用记录的 management 入口 | `status=1` 后无法从页面重读、恢复或再次编辑 | 增加相册/照片 management 列表,或独立状态恢复接口 |
| C40 | `CeremonyVo` 只返回 `coverOssId`,没有 PC 文件访问 URL | 活动记录可维护,但不能安全回显封面 | View 返回封面授权 URL,或增加 OSS ID 授权解析接口 |
| C41 | 活动列表和详情只允许读取正常记录 | `status=1` 后无法从页面重读、恢复或再次编辑 | 增加活动 management 列表/详情,或独立状态恢复接口 |
| C42 | `CeremonyGiftBody.giftAmount` 的 YAML 没有精度和最大值,后端实际拒绝负数 | 页面可按后端约束拒绝负数,但不能自行限定小数位或最大金额 | YAML 补充 minimum、scale、precision 和 maximum |
| C43 | YAML 成员更新角色枚举含 `owner/visitor`PC 后端 DTO 只接受 `admin/editor/member` | 按 YAML 展示会产生必然失败的写入 | YAML 收紧枚举;本轮前端按 PC 后端校验冻结为三种可分配角色 |
| C44 | `GenealogyMemberVo` 声明昵称、手机号、家谱和世系人物名称等字段,PC `toVo` 实际只填充 ID、成员名、角色、关系、来源和时间 | 列表部分说明可能长期为空,手机号还有隐私风险 | 后端补齐非敏感展示字段并删除手机号,或从 PC Schema 删除不会返回的字段 |
| C45 | PC 没有世系关系解除接口 | 页面无法合法解除父母、配偶、子女或兄弟姐妹关系 | 后端增加带权限和关系完整性校验的解除接口 |
| C46 | 成员更新只在 `lineagePersonId != null` 时写入,没有解除成员与世系人物绑定的语义 | 下拉留空只能表示“不修改”,不能完成解绑 | 增加明确解绑接口或约定可验证的 null/空值语义 |
当前复核状态:
@@ -175,8 +201,28 @@
| C01/C08 | 前端按后端 PC 实链解决 | 只调用 `/genealogy/pc/region/*`;列表按 `RegionSelectVo``regionCode/regionName/regionLevel` 读取,不保留公共路径 fallback |
| C09 | 前端按后端 PC 实链解决 | `SysOssResumableInitVo` 明确提供 `uploadId/instant/ossId/url/fileName/uploadedChunks`,完成响应使用 `SysOssUploadVo` |
| C10 | 前端边界解决 | 后端初始化 `ossId` 为 Long、完成响应为 string;浏览器统一转十进制字符串,隐藏回填且不转 `Number` |
| C11(备忘录) | 本轮按后端 PC 实链解决 | YAML 仍只有 stringPC `AppMemoServiceImpl` 明确校验 `completed` 只能为 `0/1`,含义为否/是,前端冻结为未完成/已完成 |
| C13 | 后续日期模块仍阻断 | 本阶段上传与区划响应不消费日期;其他模块仍需逐字段核验 Java 日期类型 |
| C17(成长记录) | 本轮按后端 PC 实链解决 | YAML 响应仍为泛型;PC `PcGrowthRecordController` 明确返回 `GrowthRecordVo`,前端只冻结该 PC 响应,不读取 APP 路径 |
| C17(亲友记录) | 本轮按后端 PC 实链解决 | YAML 响应仍为泛型;PC `PcRelativeRecordController` 明确返回 `RelativeRecordVo`,前端只冻结该 PC 响应,不读取 APP 路径 |
| C17(备忘录) | 本轮按后端 PC 实链解决 | YAML 响应仍为泛型;PC `PcMemoController` 复用的 PC 支持层明确返回 `MemoVo`,前端只冻结该 PC 响应,不读取 APP 路径 |
| C17(功德记录) | 本轮按后端 PC 实链解决 | YAML 响应仍为泛型;PC `PcMeritRecordController` 复用的 PC 支持层明确返回 `MeritRecordVo`,前端只冻结该 PC 响应,不读取 APP 路径 |
| C17(消息通知) | 本轮按 YAML 详情 DTO 和后端 PC 实链解决 | 详情为完整 `NotificationView`;列表导出仍是泛型数组,但 PC 控制器实际返回同一 `NotificationVo`。前端只接受直接数组,不读取 APP 路径 |
| C20(谱文) | 本轮按后端 PC 实链解决 | PC 控制器实际具备列表、详情、新增、修改、删除并返回 `ArticleVo`;页面不再只开放删除 |
| C18 | 阻断 | 未说明可选更新字段的省略、`null`、空串语义 |
| C34 | 阻断非核心展示 | PC 实际未填充 `genealogyNo/genealogyName/senderNickName/senderPhone/bizSummary`;页面兼容空值并始终隐藏手机号,核心通知读取和已读闭环不依赖这些字段 |
| C35 | 阻断分类能力 | 本轮不发送 YAML 未声明的 `categoryId` query,也不提供手填分类 ID |
| C36/C37 | 阻断非核心展示/停用管理 | 本轮不猜封面 URL,并固定提交 `status=0`,避免产生不可重读谱文 |
| C20(相册/照片) | 本轮按后端 PC 实链接决 | PC 控制器具备相册列表、新增、修改、删除和照片列表、新增、删除;页面只从响应取得稳定 ID |
| C38/C39 | 阻断非核心展示/停用管理 | 本轮不猜文件 URL,并固定提交 `status=0`,避免产生不可重读的相册或照片 |
| C20(祭祀活动/祭品) | 本轮按后端 PC 实链接决 | PC 控制器具备活动完整 CRUD 和祭品列表、新增、删除;页面只使用响应中的稳定 ID |
| C16 | 本轮按后端 PC 实链接决 | `members/options` 返回同一家谱成员及真实 `appUserId`;管理员邀约名单只允许选择正常且已绑定账号的成员 |
| C15 | 本轮按后端 PC 实链接决 | `members/options` 是当前家谱真实业务账号的唯一选择源;`SPECIFIED` 只提交正常且已绑定账号成员的 `appUserId` |
| C40/C41 | 阻断非核心展示/停用管理 | 本轮不猜封面 URL,并固定提交 `status=0`,避免产生不可重读活动 |
| C42 | 阻断金额精度/上限 | 前端只执行后端已确认的非负校验,不自行限制小数位和最大金额 |
| C43 | 前端按后端 PC 实链收紧 | 成员角色选择器只允许 `admin/editor/member`;不发送 YAML 中后端拒绝的 `owner/visitor` |
| C44 | 阻断非核心 enrichment | 页面兼容未填充名称,始终隐藏成员、邀请人手机号和内部用户 ID;核心成员管理不依赖这些字段 |
| C45/C46 | 阻断 | 页面不提供世系关系解除,也不把空 `lineagePersonId` 解释为解绑 |
阶段 0 验收标准:
@@ -376,12 +422,13 @@
`-1` 显示为“不限”,不能参与普通减法或显示负数。
配额响应不包含 `genealogyId`,不能替代“我的家谱”。在 C14 完成前
配额响应不包含 `genealogyId`,不能替代“我的家谱”。当前由 `GET /genealogy/pc/genealogies/mine` 返回完整 `AppGenealogyVo`
- 所有家谱内页面缺少真实 `genealogyId` 时必须阻止请求
- 不显示静态家谱卡片作为真实数据
- 不从 APP 接口获取家谱
- 不允许用户手工输入家谱 ID
- `profile-families.html` 从真实列表生成家谱入口
- `genealogyId` 在浏览器边界保持字符串,并由 `profile-common.js` 统一读取和传播
- 所有家谱内页面缺少真实 `genealogyId` 时必须阻止请求并回到家谱选择入口
-显示静态家谱卡片,不从 APP 接口获取家谱,不允许用户手工输入家谱 ID
- 切换家谱通过进入新的带 `genealogyId` URL 完成整页导航,旧页面模块状态不会复用。
### 7.5 行政区划
@@ -455,6 +502,17 @@
4. 点赞、取消、评论、删除成功后以接口返回和重新读取结果校正计数。
5. 删除有回复的评论后保留占位,不从 DOM 直接移除整棵回复。
2026-07-28 家族圈实施状态:
- `ApiClient` 的 14 个家族圈 PC 操作已由契约测试锁定;页面首屏只调用 `/feeds/page`,评论和回复分别调用各自的 `/page`
- 已新增 `profile-feed-detail.html`,以 `genealogyId + feedId` 形成稳定详情深链;列表、详情和编辑链接都保留字符串 ID。
- 列表和详情消费 `AppFamilyFeedVo` 的发布人、点赞状态与计数、评论计数、置顶、状态和时间字段;评论消费 `FamilyFeedCommentVo` 的归属、父评论、层级、删除占位、回复计数和时间字段。
- `GET /genealogy/pc/genealogies/{genealogyId}``canEditContent/canManage` 控制编辑和高级设置,`GET /genealogy/pc/auth/profile``userId` 结合响应中的发布人/评论人 ID 控制本人删除按钮;后端仍执行最终权限校验。
- 发布图片只允许文件选择;`mediaOssIds` 为隐藏的上传派生字段,多文件结果使用英文逗号连接,不允许手填 OSS ID。
- 新增、修改、点赞、取消点赞、评论和删除均有同资源动作锁;成功后重新读取详情、分页列表或评论分页以校正页面状态。
- `status=1` 使用后端定义的“停用”语义,不再显示为“草稿”。C21、C22 未解决前,图片缩略图回显和停用记录恢复仍属于后端阻断,不宣称这两项闭环完成。
- 已在真实 Chrome 登录态验证:当前账号的 `/genealogies/mine` 返回空列表;无家谱上下文时家族圈不发业务请求,所有发布入口统一改写到 `profile-families.html?next=profile-feed-edit.html`。发布表单实测为多文件选择、隐藏 OSS ID、普通用户隐藏且禁用管理字段。由于账号没有家谱,本轮不能进行真实动态列表/详情读取或写操作联调。
### 7.7 字辈谱
`profile-generation.html` 同时提供“单条维护”和“批量维护”。
@@ -486,6 +544,14 @@
普通展示列表使用正常字辈接口;管理页面使用 management 接口以包含停用项。
当前实现状态:
- 页面先读取家谱详情,以 `canEditContent` 决定调用正常列表还是 management 列表;无维护权限时不渲染新增、编辑、停用和批量维护控件,后端继续执行最终权限校验。
- `genealogyId` 只来自家谱上下文,`poemId` 只来自列表响应;所有业务 ID 在浏览器边界保持字符串,不提供手工 ID 输入。
- 单条新增、修改和状态切换成功后重新读取列表;批量维护严格执行 preview → 当前请求体签名确认 → save → 重新读取列表。
- 批量预览逐项校验 `action`、状态、必需的新旧文本和内部 ID,并核对四类汇总计数;勾选 `disableMissing` 时保存确认明确说明只停用后续世代、不删除历史。
- 已在真实 Chrome 登录态验证无家谱上下文分支:不发送字辈业务请求,新增按钮和批量面板隐藏,家谱上下文入口统一返回家谱选择页。当前账号家谱列表为空,因此正常/management 列表、单条写入和批量写入仍缺少真实家谱数据联调证据。
### 7.8 世系人物
全部操作集中在 `profile-tree.html`,详情和编辑可使用抽屉/弹窗,但所有字段都要有明确控件。
@@ -493,7 +559,7 @@
| 字段 | 必填性 | 类型 | 页面处理 |
| --- | --- | --- | --- |
| `bindingMode` | 必填 | S | NONE 不绑定、SELF 当前账号、SPECIFIED 指定账号 |
| `appUserId` | SPECIFIED 时必填 | S/I | 从真实业务用户选项选择;没有接口时禁用 SPECIFIED,禁止文本 ID |
| `appUserId` | SPECIFIED 时必填 | S/I | 从当前家谱 `members/options` 的正常且已绑定账号成员中选择;禁止文本 ID |
| `personNo` | 可选 | U/A | 高级设置可填;留空由服务端生成 |
| `name` | 必填 | U | 姓名 |
| `aliasName` | 可选 | U | 别名/曾用名 |
@@ -543,6 +609,19 @@
`LineagePersonTreeView` 补全前,不得继续依靠多个字段 fallback 猜树结构。
当前实现状态:
- 已接入 PC 世系树、普通列表、分页列表、人物选项、详情、新增、修改、逻辑停用,以及父母、子女、兄弟姐妹、配偶四类关系新增;成功写入后统一重新读取树、列表、分页和选项,返回稳定 `personId` 时再读取详情。
- 页面先读取家谱详情,`canEditContent` 只控制新增、编辑、停用和关系维护;树、列表、详情、筛选和分页保持只读可用,后端继续执行最终查看和编辑权限校验。
- `LineagePersonBody` 的全部字段均有明确来源:世代从正常字辈列表选择并自动回填字辈,父母从世系人物选项选择,头像通过统一上传组件产生隐藏 `avatarOssId`,所有业务 ID 在浏览器边界保持字符串且不可手填。
- 新增和编辑人物开放 `NONE``SELF``SPECIFIED``SPECIFIED` 选择器只消费当前家谱 `members/options`,过滤未绑定账号、停用成员和当前账号,不显示手机号,不允许手填 `appUserId`
- 编辑已绑定其他账号的人物时,用详情响应的 `appUserId` 精确匹配同一成员选项;真实选项不存在时不伪造回填,保存会被前端校验阻止。
- 分页查询已接入 `keyword``generation``personStatus``pageNum/pageSize`;详情展示家谱、绑定账号、父母、配偶、出生、逝世、安葬、人物状态、简介和备注等响应字段。
- DELETE 确认明确使用“停用”语义,并提示存在正常子女时后端会拒绝;页面没有伪造解除关系或物理删除。
- 已在真实 Chrome 登录态验证无家谱上下文分支:不发送世系业务请求,读区域显示选择家谱提示,新增、关系维护和完整表单隐藏,分页与配偶专用字段不误显示,所有家谱链接返回家谱选择页。当前账号家谱列表为空,因此真实树、列表、详情和写操作仍缺少有数据账号联调证据。
- C18 仍适用于本模块:可选字段清空语义未明确,本轮仅提交非空值,不宣称可选字段清空闭环。
- `SPECIFIED` 账号绑定相关聚焦测试 42/42 通过;真实账号页面验证统一留到阶段 6 收尾执行。
### 7.9 视频
计划新增 `profile-video-edit.html``profile-video.html` 负责列表。
@@ -560,7 +639,25 @@
流程:选视频 → 分片上传 → 得到 `videoOssId` → 可选封面上传 → 填标题说明 → 保存 → 重新读取详情。
当前视频列表、详情、新增和修改均返回泛型对象/列表。C17 完成前可以完成静态表单布局和 `ApiClient` 契约测试,但不得把真实 CRUD 页面标记为已完成。
YAML 的视频响应仍使用泛型 `ObjectResult` / `ListResult`,但后端已存在独立 `PcVideoController`,其 PC method/path 与 YAML 一致,并明确返回 `VideoVo`。根据本轮“后端源码可作为可靠补充契约”的约定,页面响应字段由该 PC 控制器及 `VideoVo` 冻结:
- I`videoId``genealogyId``coverOssId``videoOssId``publisherUserId`
- R`genealogyNo``genealogyName``surname``publisherNickName``publisherPhone``publishTime``viewCount``remark`
- U/R`videoTitle``videoDesc`
- F/A/R`durationSeconds`
- U/S/R`sortOrder`
- A/R`status`,当前固定提交 `0`
当前接入状态:
- `profile-video.html` 已接入正常视频列表、详情读取、权限化编辑/删除入口、空错状态和无家谱上下文分支;
- 新增 `profile-video-edit.html`,新增和修改只提交 `VideoBody` 七个字段;视频与封面都通过统一分片上传产生隐藏 OSS ID,不允许手填;
- 选择视频文件后由浏览器媒体元数据自动读取时长,`durationSeconds` 只读;
- 保存成功后使用稳定 `videoId` 重新读取详情,再返回列表;删除确认明确会释放视频和封面文件引用,删除后重读列表;
- 后端 `BigNumberSerializer` 对超出 JavaScript 安全整数范围的 `Long` 输出字符串,对安全整数输出 number;前端接受两种输入并统一保留为字符串;
- C23 未解决前只显示文件“已上传”,不猜测 OSS URL,不宣称播放或封面预览完成;
- C24 未解决前不开放 `status=1`,避免产生无法重新读取和恢复的停用视频;
- 2026-07-29 已在真实登录 Chrome 验证无家谱上下文:视频列表不发业务请求,发布入口及编辑表单保持隐藏,所有上下文链接返回家谱选择页,控制台无错误。当前账号没有家谱,真实列表、详情、上传和 CRUD 写入仍待有数据账号验证。
### 7.10 贺礼邀约与祭祀
@@ -583,6 +680,8 @@
“我的邀请”接口有完整响应,可独立实现接受/拒绝流程。管理端替换受邀人依赖 C16,且必须先有真实活动 ID。
当前接入状态:`profile-gift.html` 已使用 `GET /genealogy/pc/genealogies/ceremony-invitations/mine` 展示当前账号的邀请列表和响应内详情;仅 `PENDING` 邀请显示接受/拒绝,并向 `PUT /genealogy/pc/genealogies/{genealogyId}/ceremonies/{ceremonyId}/invitations/me` 提交唯一字段 `inviteStatus`,成功后重读列表。内部 ID 和坐标不直接展示,坐标仅用于生成地图链接。2026-07-29 使用真实账号联调,PC “我的邀请”接口成功返回空数组,页面空状态和鉴权正常;由于当前账号没有待响应邀请,真实接受/拒绝写入仍待有邀请数据时复核。后端仓库中的同组 `AppCeremonyController` 仍以 `/genealogy/app/genealogies` 为基础路径,与冻结 YAML 的 `/genealogy/pc` 不一致,但线上 PC mine 路径已实际可用,前端继续以 YAML 为唯一契约,不回退 APP 路径。
当前没有祭祀活动列表、详情、新增、修改和献礼列表/新增接口。现有 `profile-gift-edit.html` 中的 `ceremonyType``ceremonyTitle``ceremonyTime``location``ceremonyDesc``coverOssId` 不属于任何现有 PC 写入 DTO,必须继续保持禁用,不得向猜测路径提交。
两条删除接口只能在后端补齐列表和详情、产生稳定 ID 后开放:
@@ -605,9 +704,27 @@
| `remindTime` | 可选 | S | 日期时间控件 |
| `mediaOssIds` | 可选 | F/I | 多附件上传 |
| `sortOrder` | 可选 | U/S | 管理高级设置 |
| `status` | 可选 | S | 0 正常、1 停用 |
| `status` | 可选 | A | 当前固定提交 0 正常;C26 解决前不开放 1 停用 |
列表、详情、新增和修改响应仍是泛型对象。补齐 `GrowthRecordView` 前不得展示原始 JSON,也不得猜 `recordId` 开启编辑和删除。
YAML 的列表、详情、新增和修改响应仍是泛型对象,但后端已存在独立 `PcGrowthRecordController`,其 PC method/path 与 YAML 一致,并明确返回 `GrowthRecordVo`。根据本轮“后端源码可作为可靠补充契约”的约定,页面响应字段由该 PC 控制器及 `GrowthRecordVo` 冻结:
- I`recordId``genealogyId``appUserId``lineagePersonId``mediaOssIds`
- R`genealogyNo``genealogyName``surname``appUserNickName``appUserPhone``lineagePersonNo``lineagePersonName``remark`
- U/R`recordType``recordTitle``recordContent`
- S/R`recordDate``remindTime`
- U/S/R`sortOrder`
- A/R`status`,当前固定提交 `0`
当前接入状态:
- `profile-growth.html` 已接入当前账号成长记录列表、详情、编辑和删除入口;列表接口不传后端额外的 `all` 参数,只消费当前 YAML 声明的请求;
- `profile-growth-edit.html` 已接入新增和修改;`lineagePersonId` 只从真实世系人物选项接口选择,`recordId` 只来自列表响应或 URL,均按字符串处理且不能手填;
- `recordType` 后端 DTO 和服务均为不带枚举的自由字符串,因此保留可选文本输入;`recordDate` 提交 `yyyy-MM-dd``remindTime``datetime-local` 转为后端 `Date` 接受的 `yyyy-MM-dd HH:mm:ss`
- 多附件通过统一分片上传产生隐藏 `mediaOssIds`,支持保留、追加和清除附件选择,不显示 OSS ID;
- 新增和修改成功后必须使用稳定 `recordId` 重读同一条详情并校验身份,再返回列表;删除成功后重读列表,确认文案明确逻辑删除且会释放附件引用;
- C18 未解决前,标题以外可选文本、人物和日期字段清空不宣称闭环;前端对空可选字段保持省略,附件清除使用后端已明确的空引用处理;
- C25 未解决前只显示附件数量,不猜测 OSS URL、文件名或下载地址;C26 未解决前固定 `status=0`,不产生无法重新读取和恢复的停用记录;
- 2026-07-29 已在真实登录 Chrome 验证无家谱上下文:列表和详情不发成长记录业务请求,编辑表单及页头保存按钮隐藏,入口统一返回家谱选择页。当前账号没有家谱,真实列表、详情、世系选项、上传和 CRUD 写入仍待有数据账号验证。
### 7.12 亲友记录
@@ -619,13 +736,33 @@
| `relationName` | 可选 | U | 关系名称 |
| `eventName` | 可选 | U | 事件名称 |
| `eventTime` | 可选 | S | 日期时间选择器 |
| `giftAmount` | 可选 | U | 金额输入,明确精度和非负规则后校验 |
| `giftAmount` | 可选 | U | 金额输入;只提交 JSON 序列化后数值不变的有限数字,C27 解决前不猜精度、范围或非负规则 |
| `recordContent` | 可选 | U | 内容 |
| `mediaOssIds` | 可选 | F/I | 多附件上传 |
| `sortOrder` | 可选 | U/S | 管理高级设置 |
| `status` | 可选 | S | 0 正常、1 停用 |
| `status` | 可选 | A | 当前固定提交 0 正常;C29 解决前不开放 1 停用 |
补齐 `RelativeRecordView` 前只完成页面字段布局和请求契约,不开放真实列表、编辑和删除。
YAML 的列表、详情、新增和修改响应仍是泛型对象,但后端已存在独立 `PcRelativeRecordController`,其 PC method/path 与 YAML 一致,并明确返回 `RelativeRecordVo`。根据本轮“后端源码可作为可靠补充契约”的约定,页面响应字段由该 PC 控制器及 `RelativeRecordVo` 冻结:
- I`relativeId``genealogyId``appUserId``mediaOssIds`
- R`genealogyNo``genealogyName``surname``appUserNickName``appUserPhone``remark`
- U/R`relativeName``relationName``eventName``recordContent`
- S/R`eventTime`
- U/R`giftAmount`,后端 `BigDecimal` 响应按字符串保留;
- U/S/R`sortOrder`
- A/R`status`,当前固定提交 `0`
当前接入状态:
- `profile-relative.html` 已接入当前账号亲友记录列表、详情、编辑和删除入口;列表接口不传后端额外的 `all` 参数,只消费当前 YAML 声明的请求;
- `profile-relative-edit.html` 已接入新增和修改;`relativeId` 只来自列表响应或 URL,按字符串处理且不能手填;
- `eventTime` 使用 `datetime-local`,提交前转为后端 `Date` 接受的 `yyyy-MM-dd HH:mm:ss`,并校验真实月日和时分秒;
- `giftAmount` 请求按 YAML 的 `number` 提交;提交前比较用户十进制输入与 `JSON.stringify(Number(value))` 的精确数值,序列化会改值的超大或高精度金额直接拒绝。后端 `BigDecimal` 响应按原字符串展示,不转回 `Number`
- 多附件通过统一分片上传产生隐藏 `mediaOssIds`,支持保留、追加和清除附件选择,不显示 OSS ID;
- 新增和修改成功后必须使用稳定 `relativeId` 重读同一条详情并校验身份,再返回列表;删除成功后重读列表,确认文案明确逻辑删除且会释放附件引用;
- C18 未解决前,可选文本、时间和金额字段的清空不宣称闭环;附件清除使用后端已明确的空引用处理;
- C27 未解决前不增加两位小数、最大金额或非负限制;C28 未解决前只显示附件数量;C29 未解决前固定 `status=0`,并拒绝编辑停用响应,避免意外恢复;
- 2026-07-29 已在真实登录 Chrome 验证无家谱上下文:列表和详情不发亲友记录业务请求,编辑表单及页头保存按钮隐藏,入口统一返回家谱选择页。当前账号没有家谱,真实列表、详情、上传和 CRUD 写入仍待有数据账号验证。
### 7.13 备忘录
@@ -635,13 +772,31 @@
| --- | --- | --- | --- |
| `memoTitle` | 必填 | U | 标题 |
| `memoContent` | 可选 | U | 内容 |
| `remindTime` | 可选 | S | 日期时间 |
| `completed` | 可选 | S | 应为开关;C11 明确提交值前保持禁用 |
| `mediaOssIds` | 可选 | F/I | 多附件上传 |
| `sortOrder` | 可选 | U/S | 管理高级设置 |
| `status` | 可选 | S | 0 正常、1 停用 |
| `remindTime` | 可选 | S | `datetime-local` 选择,提交为 `yyyy-MM-dd HH:mm:ss` 并校验真实日期 |
| `completed` | 可选 | S | PC 后端已核验:`0` 未完成、`1` 已完成;创建默认 `0` |
| `mediaOssIds` | 可选 | F/I | 多附件上传自动产生;隐藏保存,允许追加和清除,不允许手填 |
| `sortOrder` | 可选 | U/S | 安全整数;创建默认 `0` |
| `status` | 可选 | I/A | 页面固定隐藏提交 `0`;不开放不可重读的停用状态 |
`completed` 表示业务完成状态,`status` 表示记录正常/停用,两者不能合并。补齐 `MemoView` 前不得猜 `memoId` 响应字段
PC `MemoVo` 响应字段及页面用途:
| 字段 | 类型 | 标记 | 页面处理 |
| --- | --- | --- | --- |
| `memoId``genealogyId``appUserId` | Long | I | 仅从 PC 响应取得并按十进制字符串保存;用于详情、编辑、删除和写后重读,不显示、不手填 |
| `genealogyNo``genealogyName``surname` | string | R | 家谱上下文只读信息;当前备忘录页不重复展示 |
| `appUserNickName` | string | R | 详情显示记录人 |
| `appUserPhone` | string | I | 隐私字段,不渲染 |
| `memoTitle``memoContent``remindTime``completed` | string | R | 列表摘要、详情展示和编辑回填 |
| `mediaOssIds` | string | I | 只统计附件数量并回填隐藏上传字段,不暴露原始 OSS ID |
| `sortOrder` | Long | I | 编辑回填;列表顺序由后端负责 |
| `status` | string | I | 仅接受正常记录 `0`;停用响应拒绝进入编辑器 |
| `remark` | string | R | 详情只读展示,MemoBody 不含此字段,因此不提交 |
`completed` 表示业务完成状态,`status` 表示记录正常/停用,两者不能合并。列表、详情、新增、修改和删除只调用 `/genealogy/pc/genealogies/{genealogyId}/memos` 及其 `/{memoId}` 子路径;列表不发送后端管理专用的 `all` 查询。新增/修改成功后使用稳定 `memoId` 重读同一详情,删除后重读列表。
- C18 未解决前,可选内容和提醒时间的清空不宣称闭环;附件清除使用后端已明确的空引用处理;
- C30 未解决前只显示附件数量;C31 未解决前固定 `status=0`,拒绝编辑停用响应,避免意外恢复;
- 2026-07-29 已在真实登录 Chrome 验证无家谱上下文:列表和详情显示明确提示,编辑表单及页头保存按钮隐藏,控制台无错误。当前账号没有可用家谱,真实列表、详情、上传和 CRUD 写入仍待有数据账号验证。
### 7.14 功德记录
@@ -653,12 +808,33 @@
| `meritType` | 可选 | S | donation 捐赠、repair 修祠、public 公益、other 其他 |
| `meritTitle` | 必填 | U | 标题 |
| `meritContent` | 可选 | U | 内容 |
| `amount` | 可选 | U | 金额 |
| `meritTime` | 可选 | S | date-time |
| `sortOrder` | 可选 | U/S | 管理高级设置 |
| `status` | 可选 | S | 0 正常、1 停用 |
| `amount` | 可选 | U | YAML `number`;提交前拒绝 JSON 序列化会改变精确数值的输入,不自行限制小数位、范围或正负 |
| `meritTime` | 可选 | S | `datetime-local` 选择,提交为后端示例和 `Date` 接受的 `yyyy-MM-dd HH:mm:ss` |
| `sortOrder` | 可选 | U/S | 安全整数;创建默认 `0` |
| `status` | 可选 | I/A | 页面固定隐藏提交 `0`;不开放不可重读的停用状态 |
现有页面错误地使用了 `service`,但契约枚举中不存在该值;必须删除 `service`,增加 `repair``public`。现有页面还缺少 `sortOrder``status`
PC `MeritRecordVo` 响应字段及页面用途:
| 字段 | 类型 | 标记 | 页面处理 |
| --- | --- | --- | --- |
| `meritId``genealogyId``appUserId` | Long | I | 仅从 PC 响应取得并按十进制字符串保存;用于详情、编辑、删除和写后重读,不显示、不手填 |
| `genealogyNo``genealogyName``surname` | string | R | 家谱上下文只读信息;当前功德页不重复展示 |
| `appUserNickName` | string | R | 详情显示记录人 |
| `appUserPhone` | string | I | 隐私字段,不渲染 |
| `donorName``meritType``meritTitle``meritContent` | string | R | 列表摘要、详情展示和编辑回填;HTML 内容转义后展示 |
| `amount` | BigDecimal | R | 按响应原始字符串展示,不转回 `Number` |
| `meritTime` | Date | R | 列表、详情展示和 `datetime-local` 编辑回填 |
| `sortOrder` | Long | I | 编辑回填;列表顺序由后端负责 |
| `status` | string | I | 仅接受正常记录 `0`;停用响应拒绝进入编辑器 |
| `remark` | string | R | 详情只读展示,`MeritRecordBody` 不含此字段,因此不提交 |
现有页面中的契约外 `service` 已删除,补齐 `repair``public`;由于 `MeritRecordBody` 没有附件字段,功德页不借用其他模块的 `mediaOssIds` 或上传控件。ApiClient 的新增和修改方法再次按 8 个 YAML 字段白名单过滤,调用者额外传入 `appUserId``mediaOssIds` 等字段也不会发送。
- 列表、详情、新增、修改和删除只调用 `/genealogy/pc/genealogies/{genealogyId}/merit-records` 及其 `/{meritId}` 子路径;
- 新增/修改成功后使用稳定 `meritId` 重读同一详情,删除成功后重读列表;
- C18 未解决前,可选内容、金额和时间的清空不宣称闭环;C32 未解决前不增加两位小数、最大金额或非负限制;C33 未解决前固定 `status=0` 并拒绝编辑停用响应;
- PC 写操作最终由 `assertCanEditContent` 鉴权;当前 View 没有 `canEdit/canDelete`,页面保留操作入口并明确处理 403,不猜角色字段;
- 2026-07-29 已在真实登录 Chrome 验证无家谱上下文:列表和详情显示明确提示,编辑表单及页头保存按钮隐藏,契约外类型和附件控件不存在,控制台无错误。当前账号没有可用家谱,真实列表、详情和 CRUD 写入仍待有数据账号验证。
列表、详情、新增和修改仍是泛型响应。补齐 `MeritRecordView` 后再开放完整 CRUD。
@@ -666,28 +842,276 @@
页面:`profile-messages.html`
| 字段 | 必填性 | 类型 | 页面处理 |
接口冻结:
- `GET /genealogy/pc/notifications`:可选 query `readStatus=0|1`,返回非分页直接数组;全部通知时省略 query。
- `GET /genealogy/pc/notifications/unread-count`:返回非负 int64 未读数量。
- `GET /genealogy/pc/notifications/{notificationId}`:读取当前账号可见的正常通知详情。
- `POST /genealogy/pc/notifications/{notificationId}/read`:将当前账号的一条正常未读通知标记已读。
- `POST /genealogy/pc/notifications/read-all`:将当前账号全部未读通知标记已读。
- 五个接口均由 `ApiClient` 自动携带 Bearer 和 `clientid`;页面不采集 Header。YAML 未声明业务错误码;前端统一处理未登录/401、403、404、业务失败和网络失败。
| 字段 | 必填性 / 类型 | 标记 | 页面用途、来源与提交时机 |
| --- | --- | --- | --- |
| `readStatus` | 可选 query | S | 全部=省略、未读=0、已读=1 |
| `notificationId` | 单条已读必填 path | I/A | 从被点击通知记录带入 |
| 未读数量 `data` | 响应 | R | 顶部徽标,int64 按安全数值/字符串处理 |
| query `readStatus` | 可选 string;枚举 `0/1` | S | 消息页筛选选择;全部=省略、未读=0、已读=1;切换筛选时提交 |
| path `notificationId` | 详情/单条已读必填 int64 | I/A | 从列表响应的稳定十进制字符串取得;点击详情或“标记已读”时自动进入 path,不允许手填或转 `Number` |
| `notificationId` | 响应 int64 | I | 列表行内部关联详情和已读动作;不在可见页面展示 |
| `genealogyId` | 响应 int64,可空 | I | 仅保留为响应内部上下文;不展示、不提交、不允许手填 |
| `genealogyNo` | 响应 string | R | 详情只读候选字段;当前 PC 未填充,不作为功能依赖 |
| `genealogyName` | 响应 string | R | 详情只读展示;当前 PC 可能为空 |
| `senderUserId` | 响应 int64,可空 | I | 内部关联字段;不展示、不提交 |
| `senderNickName` | 响应 string | R | 详情只读展示;当前 PC 可能为空 |
| `senderPhone` | 响应 string | I | 隐私字段,页面始终隐藏 |
| `noticeType` | 响应 string,无枚举/默认值 | R | 列表和详情只读展示;不据此猜测跳转 |
| `noticeTitle` | 响应 string | R | 列表和详情标题,转义后展示 |
| `noticeContent` | 响应 string | R | 列表摘要和详情正文,转义后展示 |
| `bizType` | 响应 string,无枚举/默认值 | R | 详情只读展示;不据此拼业务 URL |
| `bizId` | 响应 int64,可空 | I | 内部业务关联字段;PC 未提供目标路由契约前不展示、不跳转 |
| `bizSummary` | 响应 string | R | 详情只读展示;当前 PC 可能为空 |
| `publishTime` | 响应 string `date-time` | R | 列表和详情只读展示,不由页面提交 |
| `readStatus` | 响应 string;枚举 `0/1` | R | 0=未读、1=已读;仅正常未读记录显示单条已读按钮 |
| `readTime` | 响应 string `date-time`,可空 | R | 详情只读展示,未读时为空 |
| `status` | 响应 string;枚举 `0/1` | I | 仅用于确认正常状态 `0` 才能执行单条已读,不提供页面编辑 |
| `remark` | 响应 string | R | 详情只读展示 |
| 未读数量 `data` | 响应 int64 | R | 顶部计数;只接受非负安全整数 |
通知列表当前是 `ListResult<object>`,没有通知 ID、标题、正文、类型、时间、已读状态和目标深链。补齐 `NotificationView`
响应和权限边界
- 可以调用未读数量和全部已读;
- 不得展示原始对象;
- 不得猜 `notificationId` 开放单条已读;
- 不得根据未声明字段拼详情跳转
- YAML 的详情对象没有声明字段级 `required`、长度、范围或默认值;页面不补造限制,并兼容可选展示字段为空。
- PC 后端按当前 `APP_USER` 登录账号隔离通知,只返回 `status=0` 的正常通知;列表按接收记录倒序,非分页。
- 详情和单条已读必须命中当前账号的接收记录;重复单条已读保持已读状态,页面仍做写操作防重复。
- C34 未解决前,`genealogyNo/genealogyName/senderNickName/senderPhone/bizSummary` 不能被视为稳定可用;手机号即使后端补齐也不展示
- 页面不展示原始 JSON,不使用 APP 接口,不开放删除,不根据 `bizType/bizId` 猜业务深链。
- 2026-07-29 已在真实登录 Chrome 验证当前账号:未读数为 0,全部和仅未读筛选均成功同步,空列表/空详情状态正确。账号暂无通知,因此有数据详情和单条/全部已读真实写入仍待有数据时复核。
### 7.16 当前只有删除能力的模块
### 7.16 谱文
| 模块 | 当前操作 | 页面策略 |
页面:`profile-article.html``profile-article-edit.html`
| 请求字段 | 必填性 / 类型 | 标记 | 页面来源与提交时机 |
| --- | --- | --- | --- |
| `categoryId` | 可选 int64 | I/S | C35 未解决前省略;没有真实分类选项时不提供输入框 |
| `articleTitle` | 必填 string | U | 编辑页标题;保存时提交 |
| `articleSummary` | 可选 string | U | 编辑页摘要;非空时提交 |
| `coverOssId` | 可选 string/int64 | F/I | 选择封面并完成统一上传后自动产生;隐藏保存,禁止手填 |
| `articleContent` | 必填 string | U | wangEditor 正文;同步到 textarea 后提交 |
| `authorName` | 可选 string | U | 编辑页落款;非空时提交 |
| `sortOrder` | 可选 int64 | U/S | 编辑页安全整数;非空时提交 |
| `status` | 可选 string `0/1` | A/I | C37 未解决前固定隐藏提交 `0` |
| `genealogyId``articleId` | path int64 | I/A | 家谱上下文和列表/详情响应;按十进制字符串处理,不显示、不手填 |
PC `ArticleVo` 响应字段及页面用途:
| 字段 | 类型 | 标记 | 页面处理 |
| --- | --- | --- | --- |
| `articleId``genealogyId` | Long | I | 列表、详情、编辑、删除和写后重读的稳定 ID |
| `genealogyNo``genealogyName``surname` | string | R | 家谱只读上下文 |
| `categoryId` | Long,可空 | I | 仅内部保留;无分类选项源时不回填为可编辑 ID |
| `categoryName``categoryCode` | string | R | 详情分类只读展示 |
| `articleTitle``articleSummary``articleContent``authorName` | string | R/U | 列表、详情展示和编辑回填;可见内容均转义 |
| `coverOssId` | Long,可空 | I | 编辑隐藏回填;C36 未解决前不拼封面 URL |
| `publishTime``viewCount` | Date/Long | R | 详情发布时间和浏览量 |
| `sortOrder` | Long | I/U | 编辑回填;列表顺序由后端负责 |
| `status` | string `0/1` | I | 只允许正常记录进入编辑器 |
| `remark` | string | R | 详情只读展示,不进入 ArticleBody |
接入边界:
- 列表、详情、新增、修改和删除只调用 `/genealogy/pc/genealogies/{genealogyId}/articles` 及其 `/{articleId}` 子路径;
- YAML 响应仍为泛型包装,但 PC 控制器明确返回 `ArticleVo`,前端严格校验该 VO,不读取 APP 路径;
- 新增/修改成功后使用稳定 `articleId` 重读同一详情;删除后重读列表;所有写操作防重复;
- 页面先读取家谱详情,只有 `canEditContent/canManage` 显示新增、编辑和删除;后端继续执行最终权限校验;
- C35 未解决前不开放分类筛选/编辑;C36 未解决前不猜封面地址;C37 未解决前不开放停用;
- 相关聚焦测试 49/49 通过;真实账号页面验证统一留到阶段 6 收尾执行。
### 7.17 相册与照片
页面:`profile-album.html``profile-album-edit.html``profile-album-detail.html`
相册请求字段:
| 请求字段 | 必填性 / 类型 | 标记 | 页面来源与提交时机 |
| --- | --- | --- | --- |
| `albumName` | 必填 string | U | 编辑页相册名称;保存时提交 |
| `albumDesc` | 可选 string | U | 编辑页描述;非空时提交 |
| `coverOssId` | 可选 string/int64 | F/I | 选择封面并完成统一上传后自动产生;隐藏保存,禁止手填 |
| `sortOrder` | 可选 int64 | U/S | 编辑页安全整数;非空时提交 |
| `status` | 可选 string `0/1` | A/I | C39 未解决前固定隐藏提交 `0` |
| `genealogyId``albumId` | path int64 | I/A | 家谱上下文及相册响应;按十进制字符串处理,不显示、不手填 |
照片请求字段:
| 请求字段 | 必填性 / 类型 | 标记 | 页面来源与提交时机 |
| --- | --- | --- | --- |
| `ossId` | 必填 string/int64 | F/I | 选择照片并完成统一上传后自动产生;隐藏保存,禁止手填 |
| `photoTitle``photoDesc``photographer` | 可选 string | U | 照片表单;非空时提交 |
| `shootTime` | 可选 date | S | 日期选择器;非空时提交 `YYYY-MM-DD` |
| `sortOrder` | 可选 int64 | U/S | 照片表单安全整数;非空时提交 |
| `status` | 可选 string `0/1` | A/I | C39 未解决前固定隐藏提交 `0` |
| `albumId``photoId` | path int64 | I/A | 相册和照片响应;按十进制字符串处理,不显示、不手填 |
PC 响应字段及页面用途:
| 对象 | 字段 | 标记 | 页面处理 |
| --- | --- | --- | --- |
| `AlbumVo` | `albumId``genealogyId` | I | 列表、编辑、详情、删除和写后重读的稳定 ID |
| `AlbumVo` | `genealogyNo``genealogyName``surname` | R | 家谱只读上下文 |
| `AlbumVo` | `albumName``albumDesc` | R/U | 列表/详情展示和编辑回填;可见内容均转义 |
| `AlbumVo` | `coverOssId` | F/I | 编辑隐藏回填;C38 未解决前不拼封面 URL |
| `AlbumVo` | `photoCount` | R | 列表和详情照片数量 |
| `AlbumVo` | `sortOrder``status` | I/U | 编辑回填;只允许正常记录进入编辑器 |
| `AlbumVo` | `remark` | R | 详情只读展示,不进入请求 Body |
| `AlbumPhotoVo` | `photoId``genealogyId``albumId` | I | 照片删除和写后重读的稳定 ID |
| `AlbumPhotoVo` | `genealogyNo``genealogyName``surname``albumName` | R | 家谱与相册只读上下文 |
| `AlbumPhotoVo` | `ossId` | F/I | 文件引用;C38 未解决前不拼照片 URL |
| `AlbumPhotoVo` | `photoTitle``photoDesc``photographer``shootTime` | R/U | 照片列表展示;可见内容均转义 |
| `AlbumPhotoVo` | `sortOrder``status` | I | 列表顺序和正常状态校验 |
| `AlbumPhotoVo` | `remark` | R | 只读展示,不进入请求 Body |
接入边界:
- 相册只调用 `/genealogy/pc/genealogies/{genealogyId}/albums` 及其 `/{albumId}` 子路径;照片只调用该相册下的 `/photos``/{photoId}`
- 后端没有单独的相册详情接口,详情和编辑页必须重读相册列表并用响应中的稳定 `albumId` 精确匹配;
- YAML 响应仍为泛型包装,前端只接受后端 PC 控制器实际返回的 `AlbumVo` / `AlbumPhotoVo` 直接数组,不读取 APP 路径;
- 新增/修改相册后重读相册列表;新增/删除照片后重读照片列表;所有写操作防重复;
- 页面先读取家谱详情,只有 `canEditContent/canManage` 显示新增、编辑、删除和照片维护;
- C38 未解决前不猜文件地址;C39 未解决前不开放停用;
- 相关聚焦测试 49/49 通过;真实账号页面验证统一留到阶段 6 收尾执行。
### 7.18 祭祀活动、祭品与管理员受邀名单
页面:`profile-ceremony.html``profile-gift-edit.html``profile-ceremony-detail.html`;当前账号收到的邀请继续由 `profile-gift.html` 负责。
活动请求字段:
| 请求字段 | 必填性 / 类型 | 标记 | 页面来源与提交时机 |
| --- | --- | --- | --- |
| `ceremonyType` | 必填 string,无枚举 | U | 编辑页活动类型;保存时提交,不借用其他端枚举 |
| `ceremonyTitle` | 必填 string | U | 编辑页标题;保存时提交 |
| `ceremonyDesc` | 可选 string | U | 编辑页说明;非空时提交 |
| `ceremonyTime` | 可选 date-time | S | 日期时间选择器;转换为后端 Date 可解析格式后提交 |
| `location` | 可选 string | U | 编辑页地点名称;非空时提交 |
| `locationAddress` | 可选 string,最长 300 | U | 编辑页详细地址;非空时提交 |
| `longitude` | 可选 number`[-180, 180]` | U/S | 与纬度同时填写并保存 |
| `latitude` | 可选 number`[-90, 90]` | U/S | 与经度同时填写并保存 |
| `coverOssId` | 可选 string/int64 | F/I | 选择封面并完成统一上传后自动产生;隐藏保存,禁止手填 |
| `sortOrder` | 可选 int64 | U/S | 编辑页安全整数;非空时提交 |
| `status` | 可选 string `0/1` | A/I | C41 未解决前固定隐藏提交 `0` |
| `genealogyId``ceremonyId` | path int64 | I/A | 家谱上下文和活动响应;按十进制字符串处理,不显示、不手填 |
祭品与受邀名单请求字段:
| 请求字段 | 必填性 / 类型 | 标记 | 页面来源与提交时机 |
| --- | --- | --- | --- |
| `giverName` | 可选 string | U | 祭品表单赠送人姓名;非空时提交 |
| `giftAmount` | 必填 number,后端要求非负 | U | 祭品表单金额;添加祭品时提交;C42 未解决前不限制精度和最大值 |
| `giftMessage` | 可选 string | U | 祭品留言;非空时提交 |
| `inviteeUserIds` | 必填、唯一 int64 数组,可为空 | S/I | 从 `members/options` 的正常且已绑定账号成员多选产生;更新名单时提交完整数组,禁止手填 |
| `giftId``invitationId``inviteeUserId` | path/response int64 | I/A | 只从 PC 响应取得;用于删除、名单匹配和写后重读,不显示原值 |
PC 响应字段及页面用途:
| 对象 | 字段 | 标记 | 页面处理 |
| --- | --- | --- | --- |
| `CeremonyVo` | `ceremonyId``genealogyId``sponsorUserId` | I | 活动 CRUD、详情和写后重读的稳定 ID |
| `CeremonyVo` | `genealogyNo``genealogyName``surname` | R | 家谱只读上下文 |
| `CeremonyVo` | `sponsorNickName` | R | 发起人只读展示 |
| `CeremonyVo` | `sponsorPhone` | I | 隐私字段,始终不展示 |
| `CeremonyVo` | `ceremonyType``ceremonyTitle``ceremonyDesc``ceremonyTime``location``locationAddress` | R/U | 列表/详情展示和编辑回填;可见内容均转义 |
| `CeremonyVo` | `longitude``latitude` | I/S | 仅用于生成安全地图链接,不直接展示数值 |
| `CeremonyVo` | `coverOssId` | F/I | 编辑隐藏回填;C40 未解决前不拼封面 URL |
| `CeremonyVo` | `giftCount``giftAmount` | R | 详情祭品数量与礼金合计 |
| `CeremonyVo` | `sortOrder``status` | I/U | 编辑回填;只允许正常记录进入编辑器 |
| `CeremonyVo` | `remark` | R | 详情只读展示,不进入请求 Body |
| `CeremonyGiftVo` | `giftId``genealogyId``ceremonyId``giverUserId` | I | 祭品删除、关联和写后重读的稳定 ID |
| `CeremonyGiftVo` | `genealogyNo``genealogyName``surname``ceremonyTitle` | R | 家谱与活动只读上下文 |
| `CeremonyGiftVo` | `giverNickName``giverName``giftAmount``giftMessage``giftTime` | R | 祭品列表展示;可见内容均转义 |
| `CeremonyGiftVo` | `giverPhone` | I | 隐私字段,始终不展示 |
| `CeremonyGiftVo` | `status``remark` | R/I | 正常状态校验和只读备注 |
| `CeremonyInvitationVo` | 全部 ID、版本 | I | 邀请状态匹配和更新后的重读校验,不显示原值 |
| `CeremonyInvitationVo` | `inviteStatus`、投递/阅读/响应时间 | R | 管理员邀请列表只读展示 |
| `GenealogyMemberVo` 选项 | `appUserId` | I/S | 受邀人的唯一真实业务用户 ID 来源 |
| `GenealogyMemberVo` 选项 | `memberName``appUserNickName` | R/S | 受邀人选择器标签;手机号不展示 |
接入边界:
- 活动只调用 `/genealogy/pc/genealogies/{genealogyId}/ceremonies``/{ceremonyId}`;祭品只调用其 `/gifts` 子路径;
- 管理员邀请列表和替换只调用同一活动下的 `/invitations``/invitees`,受邀人来源只调用当前家谱 `/members/options`
- 新增/修改活动后重读同一详情,新增/删除祭品后重读祭品列表,更新受邀人后重读邀请列表;所有写操作防重复;
- 页面先读取家谱详情,只有 `canEditContent/canManage` 显示活动、祭品和受邀名单维护;
- 空数组允许取消全部尚未响应邀请;响应过的历史邀请由后端保留状态,前端不伪造删除;
- C40 未解决前不猜封面地址;C41 未解决前不开放停用;C42 未解决前不猜金额精度和上限;
- 相关聚焦测试 56/56 通过;真实账号页面验证统一留到阶段 6 收尾执行。
### 7.19 家谱成员管理、退出与谱主转移
页面:`profile-family-admin.html`
成员修改请求字段:
| 请求字段 | 必填性 / 类型 | 标记 | 页面来源与提交时机 |
| --- | --- | --- | --- |
| `memberName` | 可选 string,最长 50 | U | 管理员编辑表单;非空时提交 |
| `relationName` | 可选 string,最长 100 | U | 管理员编辑表单;后端明确接收空串时可清空 |
| `roleType` | 可选 string`admin/editor/member` | S | 按当前管理者和目标角色过滤;保存时提交,不发送 `owner/visitor` |
| `lineagePersonId` | 可选 int64 | S/I | 从当前家谱世系人物选项选择;非空时提交,留空表示不修改而不是解绑 |
| `genealogyId``memberId` | path int64 | I/A | 家谱上下文和成员列表响应;按字符串处理,不显示、不手填 |
退出与谱主转移:
| 字段/操作 | 必填性 / 类型 | 标记 | 页面来源与提交时机 |
| --- | --- | --- | --- |
| `DELETE /members/me` | 无 Body | S/A | 当前正常成员二次确认后提交;谱主不显示退出按钮 |
| `targetMemberId` | 必填 int64 | S/I | 谱主从当前正常成员列表选择新谱主;禁止选择自己和手填 ID |
| `DELETE /members/{memberId}` | path int64 | S/I | 管理员从真实成员行执行“移出家谱”;谱主不可被移出 |
PC `GenealogyMemberVo` 响应字段及页面用途:
| 字段 | 标记 | 页面处理 |
| --- | --- | --- |
| 内容文章 | 删除文章 | `profile-article*` 保持设计;没有列表/详情时不开放删除 |
| 相册 | 删除相册、删除照片 | `profile-album.html` 保持设计;不使用静态 ID 调删除 |
| 祭祀 | 删除活动、删除祭品 | 活动 CRUD 和礼物列表补齐前保持禁用 |
| `memberId``genealogyId``appUserId``lineagePersonId``inviterUserId` | I | 当前成员匹配、修改、移出、转让和世系绑定的稳定 ID;不显示原值 |
| `genealogyNo``genealogyName``surname` | R | C44 未解决前允许为空的家谱只读上下文 |
| `appUserNickName``lineagePersonNo``lineagePersonName``memberName` | R/U | 成员列表标签和编辑回填;可见内容均转义 |
| `appUserPhone``inviterPhone` | I | 隐私字段,始终不展示 |
| `roleType` | R/S | 响应可含 `owner/admin/editor/member/visitor`;修改只提交三种后端可分配角色 |
| `relationName` | R/U | 列表展示和编辑回填 |
| `joinSource``inviterNickName``joinTime` | R | 加入来源只读说明;C44 未填充时允许为空 |
| `status` | I | 列表只接受后端返回的正常状态 `0` |
删除按钮必须由真实响应中的 ID 和权限字段产生。禁止用 DOM 序号、示例 ID 或 URL 猜测资源 ID。
权限与状态边界:
- 所有账号可按家谱查看权限读取正常成员;只有家谱详情 `canManage=true` 时显示修改和移出;
- 谱主不能直接修改或移出;只有谱主可管理管理员、分配 `admin` 或转让谱主;管理员只能管理非管理员成员并分配 `editor/member`
- 普通成员、编辑和管理员可退出家谱;谱主必须先转让谱主。退出和移出只停用成员关系,不删除世系人物;
- 更新后重读成员列表并核对同一 `memberId`;移出后核对该成员不再出现在正常列表;谱主转移后重读家谱详情和成员列表;
- 页面没有直接新增成员接口;新增成员必须通过后端已有的加入/邀请业务闭环;
- C44 未解决前不依赖 enrichment 字段并隐藏手机号;C45/C46 未解决前不伪造关系解除或成员-世系人物解绑;
- 相关聚焦测试 49/49 通过;真实账号页面验证统一留到阶段 6 收尾执行。
### 7.20 家谱创建与加入申请契约
页面:`profile-create-family.html`;加入申请和审核页面留到下一批开放。
`AppGenealogyCreateBody`
| 字段 | 类型 | 必填 | 来源 | 页面与提交时机 |
| --- | --- | --- | --- | --- |
| `genealogyName` | string | 是 | U | 创建页谱名;提交时发送 |
| `surname` | string | 是 | U | 创建页姓氏;提交时发送 |
| `regionCode` | string | 是 | S | 省市区选择器自动取最后一级;提交时发送 |
| `ancestralHall``originPlace``addressDetail``intro` | string | 否 | U | 创建页可选输入;非空时发送 |
| `coverOssId` | int64/string | 否 | F | 封面上传成功后自动产生;不允许手填 |
| `visibility` | string enum `0/1/2` | 否 | S | 私密/公开/成员可见 |
| `joinMode` | string enum `0/1/2` | 否 | S | 关闭/审核/邀请模式 |
实现边界:
- 创建只调用 `POST /genealogy/pc/genealogies`,提交前读取 `GET /genealogy/pc/genealogies/quota`
- 地区选项复用 `/genealogy/pc/region/children?parentCode=...`,不手写地区编码;
- 创建成功后使用响应中的稳定 `genealogyId` 重读同一家谱,匹配后进入家谱主页;
- ApiClient 已同时冻结加入申请、我的申请、待审核、审核和撤销的 PC path/body 白名单,但本批未提前开放加入 UI;
- `inviterUserId` 没有安全 PC 邀请来源,本轮加入申请契约不发送该字段;
- 创建页面无家谱 ID、申请 ID、用户 ID 或 OSS ID 文本输入,所有写操作防重复。
## 8. 分阶段执行顺序
@@ -769,6 +1193,8 @@
3. 世系人物;
4. 我的贺礼邀请。
当前进度:阶段 4 已按顺序完成。家族圈的列表、正常状态详情、新增、修改、删除、点赞、评论、回复、权限显隐、空/错状态、防重复和写后重读已接入,C21 图片 URL 回显和 C22 停用记录管理继续阻断;字辈谱的正常/management 权限分流、单条新增修改和状态切换、批量预览保存、响应校验、防重复、写后重读及无家谱上下文分支已接入;世系人物的树、列表、分页、详情、完整字段表单、权限显隐、四类关系新增、逻辑停用、防重复和写后重读已接入,C15 继续阻断新增 `SPECIFIED` 账号绑定,C18 继续阻断可选字段清空闭环;我的贺礼邀请已接入当前账号列表、响应内详情、PENDING 接受/拒绝、长 ID 校验、权限/空错状态、防重复和写后重读,C16 继续阻断管理员替换受邀人。真实账号已验证邀请读取和空状态;该账号没有待响应邀请,接受/拒绝真实写入仍待有数据时复核。下一阶段为阶段 5,按顺序先核对视频 DTO 是否已完整。
每个模块采用同一闭环:
1. `ApiClient` 方法和契约测试;
@@ -792,19 +1218,36 @@
任何模块的 View/List DTO 未补齐时,只完成表单布局和契约测试,不宣称真实功能完成。
### 阶段 6:后端补齐后再开放的模块
当前进度:阶段 5 已按顺序完成。视频、成长记录、亲友记录、备忘录、功德记录和消息通知均已按对应 PC 契约开放;各模块的文件 URL、停用记录、可选字段清空和金额业务边界仍按 C18、C23–C34 保持阻断,不通过 APP 接口或猜测字段补齐。
范围:
### 阶段 6:当前 PC 契约可闭环模块
- 文章;
- 相册/照片;
- 祭祀活动/祭品;
- 管理员邀约名单;
- 指定账号绑定世系人物;
- 家谱和成员管理;
- 世系关系解除。
已完成:
这些模块不允许通过 APP 接口或猜测路径提前实现。
- 谱文列表、详情、新增、修改和删除;
- 相册列表、新增、修改、删除,以及照片列表、新增和删除;
- 祭祀活动列表、详情、新增、修改和删除,祭品列表、新增和删除;
- 管理员受邀名单读取和完整替换,当前账号邀请读取与响应;
- 从同家谱正常成员选项中选择账号完成 `SPECIFIED` 世系人物绑定;
- 家谱成员列表、修改、移出、当前成员退出和谱主转移;
- 个人中心、家谱首页和家谱内容页的稳定入口。
契约收口:
- `replaceCeremonyInvitees(genealogyId, ceremonyId, inviteeUserIds)` 只接收真实成员选项产生的 ID 数组,`utils/ApiClient.js` 唯一封装 `{ inviteeUserIds }`
- `/members/options` 严格按 YAML 不发送 Query;后端存在但 YAML 未定义的 `keyword` 不进入前端契约;
- 成员角色构造边界只接受 `admin/editor/member``owner/visitor` 在请求构造前失败;
- 所有业务 ID 来自 URL、上下文或响应选项,页面不提供手填 ID/OSS ID。
仍阻断:
- C35–C46 中记录的分类选项、文件 URL、停用记录、金额业务精度/上限、响应 enrichment 与解绑契约;
- 世系关系解除没有 PC 接口;成员移出/退出不会伪造成世系关系解除或成员与世系人物解绑;
- 真实账号 `19181970173` 已验证登录和无家谱上下文分支:阶段 6 页面均阻止业务请求、隐藏管理操作并返回家谱选择页,浏览器控制台无错误。该账号当前无家谱数据,因此列表有数据态、详情以及真实写操作仍待有数据账号复核,未制造家谱或业务记录。
### 阶段 7:剩余 PC 页面分批开放
首批已完成家谱生命周期 ApiClient 契约和创建家谱页面:额度、三级地区、封面上传派生、严格 DTO 校验、防重复与创建后重读均已接入。加入申请/审核、反馈/工单、VIP 和家谱主页按独立后续批次实施;成员邀请、分享和资料完善提醒没有 PC Controller,继续阻断。
## 9. 文件级施工建议
@@ -822,9 +1265,13 @@
| `public/js/auth-pages.js` / `security-pages.js` | 认证业务流程 |
| `public/js/feed-pages.js` | 家族圈 |
| `public/js/generation-pages.js` | 字辈 |
| `public/js/lineage-pages.js` | 世系 |
| `public/js/lineage-pages.js` | 世系与指定账号绑定 |
| `public/js/video-pages.js` | 视频列表、详情、编辑、上传元数据和 CRUD |
| `public/js/growth-pages.js``relative-pages.js``memo-pages.js` | 对应族务记录 |
| 新增专用视频/功德/邀约脚本 | 仅在 DTO 完整后新增 |
| `public/js/ceremony-pages.js` | 当前用户贺礼邀请列表、响应内详情和接受/拒绝 |
| `public/js/article-pages.js``album-pages.js` | 谱文、相册和照片 |
| `public/js/ceremony-admin-pages.js` | 祭祀活动、祭品与管理员邀约 |
| `public/js/member-admin-pages.js` | 成员修改、移出、退出和谱主转移 |
不得创建一个包揽所有业务的巨型页面脚本,也不得为了单一页面引入通用框架。
@@ -33,7 +33,7 @@
- `auditGenealogyJoinApply(genealogyId, applyId, body)`
- `cancelGenealogyJoinApply(applyId)`
- [ ] **Step 1: 写失败的路径和 body 白名单测试**
- [x] **Step 1: 写失败的路径和 body 白名单测试**
测试固定以下请求:
@@ -69,7 +69,7 @@ await client.auditGenealogyJoinApply(genealogyId, applyId, {
- 审核 body 只保留 `status/auditRemark`
- 所有 path ID 拒绝不安全数字。
- [ ] **Step 2: 运行 RED**
- [x] **Step 2: 运行 RED**
Run:
@@ -79,11 +79,11 @@ node --test --test-name-pattern "genealogy lifecycle" tests/api-client-contract.
Expected: FAIL,新方法尚不存在。
- [ ] **Step 3: 最小实现方法和白名单**
- [x] **Step 3: 最小实现方法和白名单**
使用现有 `request()``pickDefined()``toRequiredPathId()`;不增加兼容别名或 APP fallback。
- [ ] **Step 4: 运行 GREEN**
- [x] **Step 4: 运行 GREEN**
Run:
@@ -110,7 +110,7 @@ Expected: PASS。
- `normalizeCreatedGenealogy(item)`
- `buildCreatedGenealogyUrl(item)`
- [ ] **Step 1: 写失败的创建行为测试**
- [x] **Step 1: 写失败的创建行为测试**
```js
assert.deepEqual(GenealogyEntryPages.buildGenealogyCreateBody({
@@ -141,7 +141,7 @@ assert.deepEqual(GenealogyEntryPages.buildGenealogyCreateBody({
- 创建响应必须有稳定 `genealogyId`、谱名和姓氏。
- 跳转地址只能由响应 ID 产生:`profile-family-home.html?genealogyId=...`
- [ ] **Step 2: 运行 RED**
- [x] **Step 2: 运行 RED**
Run:
@@ -151,11 +151,11 @@ node --test tests/genealogy-entry-pages.test.js
Expected: FAIL,纯函数不存在。
- [ ] **Step 3: 最小实现纯函数**
- [x] **Step 3: 最小实现纯函数**
严格构造 `AppGenealogyCreateBody`,可选空值省略,不读取页面外字段。
- [ ] **Step 4: 运行 GREEN**
- [x] **Step 4: 运行 GREEN**
Run:
@@ -182,7 +182,7 @@ Expected: PASS。
- Consumes: Task 1 和 Task 2、`RegionPages``UploadPages`
- Produces: `initGenealogyCreatePage()`
- [ ] **Step 1: 写失败的页面测试**
- [x] **Step 1: 写失败的页面测试**
断言页面:
@@ -193,7 +193,7 @@ Expected: PASS。
- 表单只包含后端创建 DTO 字段和地区选择辅助字段。
- 创建按钮、状态区域和额度区域可由脚本控制。
- [ ] **Step 2: 运行 RED**
- [x] **Step 2: 运行 RED**
Run:
@@ -203,7 +203,7 @@ node --test tests/genealogy-entry-pages.test.js tests/pending-pages.test.js test
Expected: FAIL,页面仍 pending 且缺上传/初始化行为。
- [ ] **Step 3: 实现页面初始化和提交**
- [x] **Step 3: 实现页面初始化和提交**
初始化顺序:
@@ -218,7 +218,7 @@ Expected: FAIL,页面仍 pending 且缺上传/初始化行为。
401 清登录态;403 保留登录态;业务错误显示在表单状态区域。
- [ ] **Step 4: 运行 GREEN**
- [x] **Step 4: 运行 GREEN**
Run:
@@ -237,11 +237,11 @@ Expected: PASS。
- Modify: `docs/PC接口对接规划.md`
- Modify: `docs/superpowers/plans/2026-07-29-genealogy-create-first-batch.md`
- [ ] **Step 1: 更新契约和阶段状态**
- [x] **Step 1: 更新契约和阶段状态**
记录 `AppGenealogyCreateBody``AppGenealogyVo`、额度、权限、上传派生字段、加入申请 ApiClient 契约及仍未开放的加入 UI。
- [ ] **Step 2: 聚焦和全量验证**
- [x] **Step 2: 聚焦和全量验证**
Run:
@@ -253,6 +253,6 @@ git -c safe.directory=D:/WorkSpace/Web/jiapu diff --check
Expected: 全部 PASS。
- [ ] **Step 3: 浏览器验证并报告**
- [x] **Step 3: 浏览器验证并报告**
使用真实账号验证创建页登录态、额度、地区加载、无手填 ID、控制台错误和提交前校验。不得为测试消耗真实创建额度;除非用户明确授权,不执行最终创建写操作。
@@ -0,0 +1,509 @@
# 阶段 6 PC 功能页面 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. Project rules reserve code edits for the primary agent; subagents may only perform read-only exploration and review.
**Goal:** 实现所有当前 PC 接口已具备闭环但前端缺失或未开放的谱文、相册/照片、祭祀/祭品、管理员邀约、指定账号绑定和家谱成员管理页面。
**Architecture:** `utils/ApiClient.js` 唯一拥有 PC method/path/query/body;每个业务使用独立 UMD 页面脚本,页面只消费脚本导出的规范化、校验、渲染和初始化接口。`profile-common.js` 提供家谱上下文,`upload-pages.js` 产生 OSS ID,所有写操作使用稳定字符串 ID、防重复提交并在成功后重读。
**Tech Stack:** 静态 HTML、原生 JavaScript UMD、Axios 请求封装、Node `node:test`、现有 profile CSS、wangEditor v5、统一分片上传。
## Global Constraints
- 后端目录 `D:/WorkSpace/Java/Genealogy` 严格只读。
- 只使用 `/genealogy/pc/**`,不得使用 APP 接口或旧路径 fallback。
- 不允许手填业务 ID、用户 ID 或 OSS ID;int64 在浏览器边界保存为十进制字符串。
- 普通列表无法重读停用记录的模块固定提交 `status=0`
- 每个模块严格执行 RED → GREEN → 聚焦测试 → 真实账号非破坏性验证 → 规划更新。
- 当前工作树已有前几阶段改动;不得重置、覆盖或提交无关文件。除非用户另行要求,本计划不创建 Git commit。
---
### Task 1: 扩展 PC ApiClient 契约
**Files:**
- Modify: `utils/ApiClient.js`
- Modify: `tests/api-client-contract.test.js`
**Interfaces:**
- Produces:
- `articles(genealogyId)`, `articleDetail(genealogyId, articleId)`, `createArticle(genealogyId, body)`, `updateArticle(genealogyId, articleId, body)`
- `albums(genealogyId)`, `createAlbum(genealogyId, body)`, `updateAlbum(genealogyId, albumId, body)`, `albumPhotos(genealogyId, albumId)`, `createAlbumPhoto(genealogyId, albumId, body)`
- `ceremonies(genealogyId)`, `ceremonyDetail(genealogyId, ceremonyId)`, `createCeremony`, `updateCeremony`, `ceremonyGifts`, `createCeremonyGift`
- `replaceCeremonyInvitees(genealogyId, ceremonyId, inviteeUserIds)`
- `genealogyMembers(genealogyId)`, `genealogyMemberOptions(genealogyId)`, `updateGenealogyMember`, `removeGenealogyMember`, `leaveGenealogy`, `transferGenealogyOwner`
- [x] **Step 1: 写失败的路径和 body 白名单测试**
```js
await client.createArticle('9007199254740993001', {
articleTitle: '族史',
articleContent: '<p>正文</p>',
coverOssId: '9007199254740993002',
appUserId: 'must-drop',
status: '0'
});
assert.deepEqual(calls[0].data, {
articleTitle: '族史',
articleContent: '<p>正文</p>',
coverOssId: '9007199254740993002',
status: '0'
});
await client.replaceCeremonyInvitees(
'9007199254740993001',
'9007199254740993003',
['9007199254740993004']
);
assert.deepEqual(calls.at(-1).data, {
inviteeUserIds: ['9007199254740993004']
});
```
- [x] **Step 2: 运行 RED**
Run: `node --test --test-name-pattern "article|album|ceremony admin|genealogy member" tests/api-client-contract.test.js`
Expected: FAIL,原因是新方法不存在或仍只有删除方法。
- [x] **Step 3: 最小实现所有方法和白名单**
```js
var ARTICLE_FIELDS = [
'categoryId', 'articleTitle', 'articleSummary', 'coverOssId',
'articleContent', 'authorName', 'sortOrder', 'status'
];
var ALBUM_FIELDS = ['albumName', 'albumDesc', 'coverOssId', 'sortOrder', 'status'];
var ALBUM_PHOTO_FIELDS = [
'ossId', 'photoTitle', 'photoDesc', 'photographer',
'shootTime', 'sortOrder', 'status'
];
var CEREMONY_FIELDS = [
'ceremonyType', 'ceremonyTitle', 'ceremonyDesc', 'ceremonyTime',
'location', 'locationAddress', 'longitude', 'latitude',
'coverOssId', 'sortOrder', 'status'
];
var CEREMONY_GIFT_FIELDS = ['giverName', 'giftAmount', 'giftMessage'];
var MEMBER_UPDATE_FIELDS = ['memberName', 'relationName', 'roleType', 'lineagePersonId'];
```
每个方法只通过既有 `request()``pickDefined()``toRequiredPathId()` 和路径 builder 发请求;成员 options 按 YAML 不发送 Query。
- [x] **Step 4: 运行 GREEN 和语法检查**
Run: `node --test tests/api-client-contract.test.js`
Run: `node --check utils/ApiClient.js`
Expected: PASS。
---
### Task 2: 谱文列表、详情、创建、修改和删除
**Files:**
- Create: `public/js/article-pages.js`
- Create: `tests/article-pages.test.js`
- Modify: `profile-article.html`
- Modify: `profile-article-edit.html`
- Modify: `tests/pending-pages.test.js`
- Modify: `tests/pc-scope.test.js`
**Interfaces:**
- Consumes: Task 1 article ApiClient methods、`ProfileUI.requireGenealogyContext()``ProfileUpload.bindUploadField()``RichEditorPages`
- Produces: `buildArticleBody`, `normalizeArticle`, `normalizeArticles`, `matchesArticle`, `renderArticleList`, `renderArticleDetail`, `initArticleListPage`, `initArticleEditPage`
- [x] **Step 1: 写失败的谱文行为测试**
```js
assert.deepEqual(ArticlePages.buildArticleBody({
articleTitle: '族史',
articleContent: '<p>正文</p>',
coverOssId: '9007199254740993002',
categoryId: '',
status: '1'
}), {
articleTitle: '族史',
articleContent: '<p>正文</p>',
coverOssId: '9007199254740993002',
status: '0'
});
assert.equal(
ArticlePages.normalizeArticle({ articleId: Number.MAX_SAFE_INTEGER + 1 }),
null
);
assert.doesNotMatch(
ArticlePages.renderArticleDetail({
articleId: '9007199254740993003',
articleTitle: '<img src=x onerror=alert(1)>',
articleContent: '<script>alert(1)</script>',
status: '0'
}),
/<script|onerror/
);
```
页面测试必须断言两个 HTML 不再 pending、加载 `article-pages.js`、没有手填 `articleId/categoryId/coverOssId/status`,列表页存在详情区域,编辑页存在标题/摘要/正文/作者/封面上传。
- [x] **Step 2: 运行 RED**
Run: `node --test tests/article-pages.test.js tests/api-client-contract.test.js tests/pending-pages.test.js tests/pc-scope.test.js`
Expected: FAIL,原因是模块不存在且页面仍为 pending。
- [x] **Step 3: 实现谱文脚本和页面**
```js
function buildArticleBody(values) {
return compact({
articleTitle: requiredText(values.articleTitle, '请输入谱文标题'),
articleSummary: optionalText(values.articleSummary),
coverOssId: normalizeNullableId(values.coverOssId),
articleContent: requiredText(values.articleContent, '请输入谱文正文'),
authorName: optionalText(values.authorName),
sortOrder: normalizeOptionalSafeInteger(values.sortOrder),
status: '0'
});
}
```
列表从直接数组读取;详情按 `articleId` 匹配;列表没有独立分类选项源时不渲染 `categoryId` 输入。新增/修改成功后用返回 ID 或 URL ID 重读详情,删除后重读列表。所有可见文本转义,正文使用现有安全富文本展示规则。
- [x] **Step 4: 运行 GREEN**
Run: `node --test tests/article-pages.test.js tests/api-client-contract.test.js tests/pending-pages.test.js tests/pc-scope.test.js`
Run: `node --check public/js/article-pages.js`
Expected: PASS。
- [x] **Step 5: 更新规划并报告谱文阶段**
`docs/PC接口对接规划.md` 记录 ArticleBody/ArticleVo 字段、C20 谱文状态、`categoryId` query 差异、封面 URL 和停用记录阻断。
---
### Task 3: 相册列表、相册编辑和照片管理
**Files:**
- Create: `profile-album-edit.html`
- Create: `profile-album-detail.html`
- Create: `public/js/album-pages.js`
- Create: `tests/album-pages.test.js`
- Modify: `profile-album.html`
- Modify: `tests/pending-pages.test.js`
**Interfaces:**
- Consumes: Task 1 album ApiClient methods、ProfileUI、ProfileUpload
- Produces: `buildAlbumBody`, `buildAlbumPhotoBody`, `normalizeAlbum`, `normalizeAlbumPhoto`, `findAlbumById`, `renderAlbumList`, `renderPhotoList`, three page initializers
- [x] **Step 1: 写失败的相册/照片测试**
```js
assert.deepEqual(AlbumPages.buildAlbumPhotoBody({
ossId: '9007199254740993010',
photoTitle: '祠堂合影',
shootTime: '2026-07-29T10:30',
status: '1'
}), {
ossId: '9007199254740993010',
photoTitle: '祠堂合影',
shootTime: '2026-07-29 10:30:00',
status: '0'
});
assert.equal(AlbumPages.findAlbumById(
[{ albumId: '9007199254740993011', albumName: '旧影', status: '0' }],
'9007199254740993011'
).albumName, '旧影');
```
页面测试断言 OSS ID 只能是 hidden/upload target,列表链接传播 `genealogyId + albumId`,详情页具有照片上传和真实删除入口。
- [x] **Step 2: 运行 RED**
Run: `node --test tests/album-pages.test.js tests/api-client-contract.test.js tests/pending-pages.test.js`
Expected: FAIL,原因是脚本和新增页面不存在。
- [x] **Step 3: 实现三个页面和脚本**
```js
function buildAlbumBody(values) {
return compact({
albumName: requiredText(values.albumName, '请输入相册名称'),
albumDesc: optionalText(values.albumDesc),
coverOssId: normalizeNullableId(values.coverOssId),
sortOrder: normalizeOptionalSafeInteger(values.sortOrder),
status: '0'
});
}
```
相册编辑/详情通过重新读取相册列表并按稳定 `albumId` 匹配;照片列表调用 `/photos`。照片新增后重读照片列表;删除照片后重读并更新相册计数;删除相册后返回并重读相册列表。
- [x] **Step 4: 运行 GREEN**
Run: `node --test tests/album-pages.test.js tests/api-client-contract.test.js tests/pending-pages.test.js`
Run: `node --check public/js/album-pages.js`
Expected: PASS。
- [x] **Step 5: 更新规划并报告相册阶段**
记录 AlbumBody、AlbumPhotoBody、AlbumVo、AlbumPhotoVo、文件 URL 缺口和停用记录阻断。
---
### Task 4: 祭祀活动、祭品和管理员邀约
**Files:**
- Create: `profile-ceremony.html`
- Create: `profile-ceremony-detail.html`
- Create: `public/js/ceremony-admin-pages.js`
- Create: `tests/ceremony-admin-pages.test.js`
- Modify: `profile-gift-edit.html`
- Modify: `profile-gift.html`
- Modify: `tests/pending-pages.test.js`
**Interfaces:**
- Consumes: Task 1 ceremony/member ApiClient methods、ProfileUI、ProfileUpload
- Produces: `buildCeremonyBody`, `buildCeremonyGiftBody`, `normalizeCeremony`, `normalizeCeremonyGift`, `normalizeInviteeOption`, `buildInviteeBody`, list/detail/edit initializers
- [x] **Step 1: 写失败的祭祀测试**
```js
assert.deepEqual(CeremonyAdminPages.buildCeremonyGiftBody({
giverName: '宗亲',
giftAmount: '88.50',
giftMessage: '敬献'
}), {
giverName: '宗亲',
giftAmount: 88.5,
giftMessage: '敬献'
});
assert.throws(() => CeremonyAdminPages.buildCeremonyGiftBody({
giftAmount: '-1'
}));
assert.deepEqual(CeremonyAdminPages.buildInviteeBody([
'9007199254740993020',
'9007199254740993020',
'9007199254740993021'
]), {
inviteeUserIds: ['9007199254740993020', '9007199254740993021']
});
```
页面测试断言个人邀请脚本和管理员脚本职责分离;活动编辑没有手填封面 ID;详情页有祭品和受邀成员选择器。
- [x] **Step 2: 运行 RED**
Run: `node --test tests/ceremony-admin-pages.test.js tests/ceremony-pages.test.js tests/api-client-contract.test.js tests/pending-pages.test.js`
Expected: FAIL,原因是管理模块和页面不存在。
- [x] **Step 3: 实现活动、祭品和邀约管理**
```js
function buildInviteeBody(ids) {
var values = Array.from(new Set(ids.map(normalizeId).filter(Boolean)));
return { inviteeUserIds: values };
}
```
活动固定 `status=0`;经纬度必须成对且范围合法;祭品金额非负并通过精确 JSON 数值检查。管理员候选过滤空 `appUserId`,提交空数组时显示取消全部待响应邀请确认。各写操作完成后重读活动详情、祭品或邀请名单。
- [x] **Step 4: 运行 GREEN**
Run: `node --test tests/ceremony-admin-pages.test.js tests/ceremony-pages.test.js tests/api-client-contract.test.js tests/pending-pages.test.js`
Run: `node --check public/js/ceremony-admin-pages.js`
Expected: PASS。
- [x] **Step 5: 更新规划并报告祭祀阶段**
记录 CeremonyBody、CeremonyGiftBody、CeremonyInviteesBody、权限与金额/文件/停用阻断。
---
### Task 5: 启用 SPECIFIED 世系账号绑定
**Files:**
- Modify: `public/js/lineage-pages.js`
- Modify: `profile-tree.html`
- Modify: `tests/lineage-pages.test.js`
**Interfaces:**
- Consumes: Task 1 `genealogyMemberOptions(genealogyId)`
- Produces: `normalizeBindingMemberOption`, `renderBindingMemberOptions`;扩展现有表单初始化和 `buildLineagePersonBody`
- [x] **Step 1: 写失败的绑定测试**
```js
assert.deepEqual(LineagePages.normalizeBindingMemberOption({
memberId: '9007199254740993030',
appUserId: '9007199254740993031',
memberName: '族员甲',
appUserNickName: '账号甲',
status: '0'
}), {
value: '9007199254740993031',
label: '族员甲(账号甲)'
});
assert.equal(LineagePages.normalizeBindingMemberOption({
memberId: '9007199254740993032',
appUserId: null,
status: '0'
}), null);
```
页面测试断言 `SPECIFIED` 不再 disabled、只显示成员选择器且不存在 appUserId 文本输入。
- [x] **Step 2: 运行 RED**
Run: `node --test tests/lineage-pages.test.js tests/api-client-contract.test.js`
Expected: FAIL,原因是选项仍禁用且规范化函数不存在。
- [x] **Step 3: 最小启用成员选项绑定**
只在 `canManage` 时显示 `SPECIFIED`;选择后内部保存字符串 `appUserId``NONE/SELF` 必须删除 body 中的 `appUserId``SPECIFIED` 必须选择有效 option;403 作为权限错误处理,不清登录态。
- [x] **Step 4: 运行 GREEN**
Run: `node --test tests/lineage-pages.test.js tests/api-client-contract.test.js`
Run: `node --check public/js/lineage-pages.js`
Expected: PASS。
- [x] **Step 5: 更新规划并报告绑定阶段**
把 C15 更新为“由同家谱正常成员 options 解决;未绑定账号的成员不可选”,保留没有任意 AppUser 搜索能力的边界。
---
### Task 6: 家谱成员管理、退出和所有权转移
**Files:**
- Create: `public/js/member-admin-pages.js`
- Create: `tests/member-admin-pages.test.js`
- Modify: `profile-family-admin.html`
- Modify: `tests/pending-pages.test.js`
**Interfaces:**
- Consumes: Task 1 member ApiClient methods、`lineagePersonOptions`、ProfileUI
- Produces: `normalizeMember`, `normalizeMemberOptions`, `buildMemberUpdateBody`, `canEditMember`, `canRemoveMember`, `canTransferOwner`, `renderMemberList`, `initMemberAdminPage`
- [x] **Step 1: 写失败的成员管理测试**
```js
assert.deepEqual(MemberAdminPages.buildMemberUpdateBody({
memberName: '族员甲',
relationName: '侄',
roleType: 'editor',
lineagePersonId: '9007199254740993040',
appUserId: 'must-drop'
}), {
memberName: '族员甲',
relationName: '侄',
roleType: 'editor',
lineagePersonId: '9007199254740993040'
});
assert.throws(() => MemberAdminPages.buildMemberUpdateBody({
roleType: 'owner'
}));
```
渲染测试断言手机号和内部 ID 不可见;owner 不显示移除按钮;当前 owner 不显示退出按钮而显示所有权转移。
- [x] **Step 2: 运行 RED**
Run: `node --test tests/member-admin-pages.test.js tests/api-client-contract.test.js tests/pending-pages.test.js`
Expected: FAIL,原因是脚本不存在且页面 pending。
- [x] **Step 3: 实现成员管理页面**
成员列表支持关键词刷新;编辑只提交 `memberName/relationName/roleType/lineagePersonId`。角色选择只有 `admin/editor/member`。移除、退出、转让都二次确认并使用响应中的 memberId;成功后重读成员列表,退出成功后清除当前家谱上下文并返回家谱选择页。
- [x] **Step 4: 运行 GREEN**
Run: `node --test tests/member-admin-pages.test.js tests/api-client-contract.test.js tests/pending-pages.test.js`
Run: `node --check public/js/member-admin-pages.js`
Expected: PASS。
- [x] **Step 5: 更新规划并报告成员阶段**
记录成员更新字段、角色差异、退出/移除/转让权限与世系关系解除继续阻断。
---
### Task 7: 导航开放、真实验证和阶段收尾
**Files:**
- Modify: `profile.html`
- Modify: `profile-content.html`
- Modify: `profile-families.html`
- Modify: `docs/PC接口对接规划.md`
- Modify: `tests/pending-pages.test.js`
- Modify: `tests/pc-scope.test.js`
**Interfaces:**
- Consumes: Tasks 16 completed pages
- Produces: 所有功能的稳定导航入口和阶段 6 完成记录
- [x] **Step 1: 写失败的入口行为测试**
```js
for (const page of [
'profile-article.html',
'profile-album.html',
'profile-ceremony.html',
'profile-family-admin.html'
]) {
assert.match(navigationHtml, new RegExp(page.replace('.', '\\.')));
}
```
测试同时断言新增/开放页面不加载 `pending-pages.js`,不存在 APP path、手填 ID 或原始 JSON。
- [x] **Step 2: 运行 RED**
Run: `node --test tests/pending-pages.test.js tests/pc-scope.test.js`
Expected: FAIL,原因是导航尚未开放。
- [x] **Step 3: 开放入口并更新规划**
个人中心和家谱内容入口链接到谱文、相册和祭祀;家谱管理入口链接到成员管理。规划逐模块记录字段表、验证结果、契约差异和唯一未实现的世系关系解除。
- [x] **Step 4: 聚焦和全量自动验证**
Run:
```powershell
node --test tests/article-pages.test.js tests/album-pages.test.js
node --test tests/ceremony-admin-pages.test.js tests/ceremony-pages.test.js
node --test tests/lineage-pages.test.js tests/member-admin-pages.test.js
node --test tests/api-client-contract.test.js tests/pending-pages.test.js tests/pc-scope.test.js
npm test
node --check public/js/article-pages.js
node --check public/js/album-pages.js
node --check public/js/ceremony-admin-pages.js
node --check public/js/member-admin-pages.js
git -c safe.directory=D:/WorkSpace/Web/jiapu diff --check
```
Expected: 全部 PASS。
- [x] **Step 5: 真实账号浏览器验证**
逐页验证登录态、家谱上下文、无数据空状态、筛选、隐私隐藏和控制台错误。真实账号没有家谱时,确认业务请求被阻止且所有入口回到 `profile-families.html?next=...`;不得制造家谱或业务记录。
- [x] **Step 6: 独立只读复核和最终报告**
安排默认子代理只读检查所有阶段 6 文件,主代理按 file:line 抽查。报告使用 `Changed / Verified / Blocked / Next`,明确世系关系解除、文件 URL、停用记录和缺真实数据写入验证的剩余风险。
@@ -0,0 +1,119 @@
# 阶段 6 PC 功能页面设计
## 目标
把当前 PC YAML 与只读后端已经具备完整业务闭环、但前端页面缺失或仍处于待开发状态的功能全部落到现有个人中心中。不得使用 APP 接口、静态业务 ID、手填用户 ID/OSS ID、猜测响应字段或不可重读的停用流程。
## 实施范围
### 1. 谱文
- 开放 `profile-article.html`,提供正常谱文列表、分类筛选、详情和权限化操作入口。
- 开放 `profile-article-edit.html`,提供新增与修改。
- 新增成功后使用响应中的稳定 `articleId` 重读详情;修改后重读同一详情;删除后重读列表。
- `categoryId` 只能来自真实文章分类响应;如果 PC 没有分类列表/选项源,则分类保持省略,不能输入 ID。
- 封面只能通过文件选择和上传产生 `coverOssId`
- 普通列表和详情只读取正常状态,因此创建、修改固定提交 `status=0`,不开放停用。
### 2. 相册与照片
- 开放 `profile-album.html` 作为相册列表入口。
- 新增 `profile-album-edit.html` 负责相册新增、修改。
- 新增 `profile-album-detail.html` 负责相册详情、照片列表、照片上传和照片删除。
- 相册封面与照片文件都必须通过统一分片上传产生 OSS ID。
- 相册或照片写入后重新读取相册/照片列表;删除后重新读取并校正数量。
- 普通接口只返回正常记录,因此不开放相册或照片停用状态。
- 在没有 PC 文件访问 URL 时,只显示上传状态、标题和业务元数据,不拼接 OSS 地址。
### 3. 祭祀活动与祭品
- 保留 `profile-gift.html` 的“我的邀请”能力,并增加进入祭祀活动管理的入口。
- 开放 `profile-gift-edit.html`,负责祭祀活动新增、修改。
- 新增 `profile-ceremony.html`,负责活动列表和详情。
- 新增 `profile-ceremony-detail.html`,负责活动详情、祭品列表、新增祭品、删除祭品和管理员邀约名单。
- 活动封面通过统一上传产生 `coverOssId`
- 祭品金额按 YAML `number` 和后端 `BigDecimal` 边界处理;拒绝负数以及 JSON 数值序列化会改变输入值的金额。
- 活动与祭品写操作成功后重新读取对应详情/列表。
- 普通活动列表只返回正常记录,因此不开放活动停用。
### 4. 管理员邀约名单
- 邀约管理放在 `profile-ceremony-detail.html`,不与当前用户“我的邀请”响应流程混合。
- 候选人来自 `/genealogy/pc/genealogies/{genealogyId}/members/options`
- 只允许选择拥有稳定 `appUserId` 的同家谱正常成员;当前登录人由后端拒绝,前端不猜测替代账号。
- 保存时一次提交完整 `inviteeUserIds`;空数组表示取消所有仍未响应邀请,提交前必须二次确认。
- 保存成功后重新读取活动邀请名单。
### 5. 指定账号绑定世系人物
-`profile-tree.html` 启用 `SPECIFIED`,仅对具有家谱管理权限的用户开放。
- 账号候选来自家谱成员选项;过滤没有 `appUserId` 的成员。
- 用户看到成员昵称/成员名称,不看到或手填 `appUserId`
- `NONE``SELF``SPECIFIED` 继续遵守互斥请求规则;后端负责最终租户、账号状态和重复绑定校验。
### 6. 家谱成员管理
- 开放 `profile-family-admin.html`
- 提供成员列表、关键词搜索、成员详情摘要、成员资料/角色修改、成员移除、当前用户退出家谱和所有权转移。
- 角色编辑只发送后端实际接受的 `admin/editor/member`,不允许把 `owner/visitor` 作为更新值。
- 世系人物关联只能来自真实世系人物选项。
- 所有权转移只能从当前正常成员记录中选择稳定 `memberId`
- 移除、退出和所有权转移必须有明确影响说明与二次确认;成功后重新读取成员列表和家谱上下文。
## 明确保留的阻断
- PC 没有世系关系解除接口,不创建“解除父母/配偶/兄弟关系”按钮。
- PC 没有统一 OSS ID 访问地址解析能力;谱文封面、相册照片、祭祀封面只显示后端已直接返回的 URL,否则不猜 URL。
- 普通谱文、相册、照片和祭祀列表过滤停用记录,详情也不能稳定读取停用项;前端固定提交正常状态。
- 谱文 YAML 遗漏后端可选 `categoryId` 列表 query,且文章分类选项来源需要单独核实;未确认前不发送该 query。
- YAML 中部分 OSS ID 为 string、Java DTO 为 Long;浏览器始终保存十进制字符串,上传响应是唯一来源。
- YAML 的文章、相册和祭祀响应仍为泛型包装;页面只消费对应 PC 控制器明确返回的 `ArticleVo``AlbumVo``AlbumPhotoVo``CeremonyVo``CeremonyGiftVo`,不读取 APP 路径。
## 前端结构
- `utils/ApiClient.js` 是所有 method/path/query/body 的唯一 owner,并为每个 Body 使用字段白名单。
- 每个业务模块使用独立脚本:
- `public/js/article-pages.js`
- `public/js/album-pages.js`
- `public/js/ceremony-admin-pages.js`
- `public/js/member-admin-pages.js`
- `public/js/ceremony-pages.js` 继续只负责当前账号的邀请读取与接受/拒绝,避免管理端与个人端状态混在一起。
- `public/js/profile-common.js` 继续作为 `genealogyId` 的唯一上下文 owner。
- `public/js/upload-pages.js` 继续作为 OSS ID 的唯一前端产生来源。
## 数据流与状态
1. 页面从 URL 和 `profile-common.js` 取得真实家谱上下文。
2. 页面先读取家谱详情,按 `canView/canEditContent/canManage` 控制可见入口;后端继续执行最终权限校验。
3. 列表记录返回稳定字符串 ID,用户点击后进入详情或编辑。
4. 表单只提交 YAML/后端 PC DTO 声明的字段;可选空值默认省略。
5. 写操作使用模块级提交锁,完成后重新读取服务端状态。
6. 页面分别展示加载、空、校验失败、403、404、业务失败和网络失败状态。
7. 所有响应文本经过转义;手机号、内部 ID、OSS ID 和原始 JSON不进入可见页面。
## 页面入口
- 个人中心和家谱管理导航开放谱文、相册、祭祀活动、成员管理入口。
- 页面缺少 `genealogyId` 时返回家谱选择页,并通过 `next` 保留目标页面。
- 已有待开发页面在功能完成并通过测试后移除 `data-feature-status="pending"``pending-pages.js`
- 新增页面沿用现有个人中心头部、侧边栏、按钮、卡片和移动端断点,不引入新的前端框架。
## 测试与验收
每个模块单独执行:
1. 先新增 `ApiClient` 路径、query、body 白名单和长 ID 失败测试并观察 RED。
2. 新增规范化、权限、渲染转义、写后重读、防重复和页面字段测试并观察 RED。
3. 最小实现后运行模块聚焦测试直到 GREEN。
4. 使用真实登录账号验证读取、空状态和非破坏性筛选。
5. 有真实业务数据时验证详情和写操作;没有数据时明确记录未覆盖项,不制造示例数据。
6. 每个模块完成后运行相关测试并更新 `docs/PC接口对接规划.md`
7. 阶段完成后运行 `npm test`、JavaScript 语法检查、`git diff --check` 和凭据扫描。
## 完成标准
- 所有当前 PC 可闭环功能都有可访问页面和导航入口。
- 所有请求字段有真实页面来源,所有 ID 均由响应、选择器或上传产生。
- 不存在 APP 路径、旧路径 fallback、手填业务 ID、原始 JSON或静态业务数据。
- 所有写操作防重复并在成功后重新读取。
- 仍缺后端能力的功能保持明确禁用并写入规划阻断项。