交接文档
This commit is contained in:
@@ -1,140 +0,0 @@
|
||||
# PC 后端补齐清单(交给后端 GPT)
|
||||
|
||||
> 状态:待后端确认与实施
|
||||
>
|
||||
> 前端基线:`ac58235`(“验证20%”)之后的 PC 工作区。当前前端回归为 362 项通过。
|
||||
>
|
||||
> 本文目的:让后端直接知道 PC 端还缺什么能力、每项最小需要补什么,以及如何验收。它是 `PC接口对接规划.md` 中 C01–C46 的精简交接版;详细的历史证据仍以该文档为准。
|
||||
|
||||
## 0. 不可违反的边界
|
||||
|
||||
1. 这是 **PC** 契约补齐,不是把 `Jiapu-App` 的接口、参数、缓存键或数据结构搬过来。路径和 DTO 以 PC 后端为唯一 owner。
|
||||
2. 请先在 PC 的 Apifox/OpenAPI 中定义完整请求、响应、权限、枚举和错误码,再实现 Controller/Service;完成后重新导出契约。前端只会通过 `utils/ApiClient.js` 接入已确认的 PC 契约。
|
||||
3. 不要以泛型 `ObjectResult`、无元素类型的列表或“前端自行猜字段”作为交付。每个列表、详情和写入结果都必须有明确 DTO。
|
||||
4. 所有资源 ID、OSS ID、订单号等可能超过 JavaScript 安全整数的值,统一以十进制字符串传输和返回;不得要求用户手填内部 ID。
|
||||
5. 不返回或展示成员、邀请人、通知发送人的原始手机号。确有业务必要的内部用户 ID 只在受权限保护的选项或写入场景返回,PC 页面不会展示它。
|
||||
6. 不记录、更不要求提交真实测试账号、密码、令牌或真实家谱数据。角色验收使用既有隔离身份或后端准备的隔离夹具。
|
||||
|
||||
## 1. 已有能力,不要重复实现
|
||||
|
||||
以下 PC 能力已被当前前端使用:登录/账户基础资料、家谱列表与创建、申请加入与审核、成员名册与角色维护、成员选项、世系人物及四类关系新增、字辈、家族圈常规 CRUD、视频、成长/亲友/备忘/功德记录、谱文、相册/照片、祭祀活动/祭品/活动邀约、通知已读、反馈工单、会员套餐与会员订单、帮助文章、PC 推广和官网资讯。
|
||||
|
||||
本清单要求补的是这些能力的缺口或契约精度,不要求重新设计上述已有流程。已有写入接口仍必须由后端做最终权限校验,不能只依赖前端隐藏按钮。
|
||||
|
||||
## 2. P0:PC 当前完全缺失的业务契约
|
||||
|
||||
| 编号 | 前端位置与业务目标 | 后端必须补齐的最小能力 | 验收标准 |
|
||||
| --- | --- | --- | --- |
|
||||
| P0-01 | `profile-data-reminders.html`:查看当前家谱中待完善的成员资料 | 只读“资料缺项提醒”列表,按当前家谱和当前操作者权限过滤。每项至少返回稳定的成员/世系人物定位 ID、脱敏展示名、缺失字段枚举、可处理状态和更新时间;缺失字段至少能区分头像、出生日期、关系等。联系方式只能表达“缺失/已填写”,不能下发原始号码。 | 有权限角色能读取真实空列表和非空列表;无权限、无家谱、资源不存在分别有可区分失败;前端能从响应跳到已有成员/世系编辑页。 |
|
||||
| P0-02 | `profile-invite.html`、家谱主页:邀请家人加入家谱 | 家谱成员邀请的创建、活动邀请列表、撤销/失效、受邀人校验和受邀人接受流程。邀请方式如为链接或邀请码,响应需给出 `inviteId`、受限邀请凭据/链接、过期时间、使用次数或上限、状态及可展示的创建时间;不得把可枚举的内部 ID 当邀请码。 | 仅后端定义的可管理角色可创建和撤销;过期、撤销、次数用尽、非受邀人、重复接受均有稳定业务错误;接受后能在家谱成员/审核链路中重读结果。 |
|
||||
| P0-03 | `profile-family-home.html`:重要证件 | 家谱重要证件的列表、详情、新增、修改、删除(或明确仅支持其中一部分)。证件 DTO 至少含稳定 ID、家谱 ID、标题/类型、说明、文件对象、创建/更新时间、状态和权限能力。文件必须使用第 3.1 节的安全文件对象。 | 无权用户不能读写他人家谱证件;上传后重读同一条记录;删除后的可见性和是否可恢复有明确定义。 |
|
||||
| P0-04 | `profile-families.html`:我的家谱自定义排序 | 读取当前账号的家谱顺序及保存顺序的接口。保存请求应提交完整有序的“当前账号可见家谱 ID”数组,后端在一个事务内校验归属、去重和完整性,不接受跨账号或不可见家谱 ID。 | 重排后重新读取顺序一致;重复、缺失、越权 ID 返回明确校验/权限错误;并发保存不会产生重复排序值。 |
|
||||
| P0-05 | `profile-family-home.html`:删除家谱 | 先由产品/后端确定唯一语义:**逻辑归档** 或 **物理删除**,不能接口名叫删除而实际效果不明确。接口需只允许谱主执行,并返回影响范围(成员、人物、内容、文件、订单等)、最终状态和可否恢复;如需二次确认,应采用后端签发的一次性确认令牌或明确确认字段。 | 非谱主必定拒绝;成功后“我的家谱”重读不再出现或以归档状态出现;重复请求幂等;关联资源处理与定义一致。 |
|
||||
| P0-06 | `profile-share.html`、`profile-services.html`:应用分享、奖励、推广收益、提现 | 一套完整账户权益契约:分享规则/分享凭据、分享记录、奖励或收益流水、账户可用/冻结/累计余额、提现方式选项、提现申请及提现记录。金额和状态必须由服务端计算与驱动,前端不能传入奖励金额、余额或审核状态。 | 普通用户只能读取自己的记录;提现前校验可用余额、金额精度、最低/最高额、风控状态和收款方式;重复提交幂等;流水、余额与提现状态可重读且一致。 |
|
||||
| P0-07 | `profile-services.html`:个性化推荐开关 | 当前用户的推荐偏好读取与保存。至少定义可配置项枚举、默认值、保存后的版本/更新时间,以及未登录、无权限或功能未开通的处理。 | 未知枚举和值非法会被拒绝;保存后重读一致;不允许操作其他用户的偏好。 |
|
||||
| P0-08 | `profile-services.html`:会员订单在线支付 | 现有“创建订单/读取订单”之外,补支付发起、支付参数、安全回跳/异步通知后的订单状态查询和关闭/超时语义。`payType` 必须有正式枚举,订单状态机由后端维护,不能让前端提交“已支付”。 | 创建支付后返回可安全使用的支付会话参数;支付成功、失败、取消、超时、退款均能重读到准确状态;重复通知和用户刷新幂等。 |
|
||||
|
||||
## 3. P1:已有 PC 页面被契约限制的能力
|
||||
|
||||
### 3.1 统一安全文件访问能力(C21、C23、C25、C28、C30、C36、C38、C40)
|
||||
|
||||
下列资源目前大多只返回 `ossId` 或 `mediaOssIds`,PC 无法安全回显图片、封面、视频、附件名称或下载入口:家族圈、视频、成长记录、亲友记录、备忘录、谱文、相册/照片、祭祀活动。
|
||||
|
||||
请在资源 View 内直接返回文件对象数组,或提供一个受资源权限保护的 PC 文件解析接口。两种方案只保留一种,推荐统一文件对象:
|
||||
|
||||
| 字段 | 要求 |
|
||||
| --- | --- |
|
||||
| `ossId` | string,原始文件稳定标识 |
|
||||
| `fileName` | 可安全展示的文件名 |
|
||||
| `mediaType` | 明确枚举,例如 image/video/document |
|
||||
| `accessUrl` | 短时授权 URL;不能由前端用 OSS ID 拼接 |
|
||||
| `expiresAt` | 授权 URL 失效时间,使用统一日期时间格式 |
|
||||
|
||||
文件解析必须根据“该文件所属业务资源 + 当前操作者”校验权限,不能做成任意登录用户可按 OSS ID 探测或下载文件的接口。
|
||||
|
||||
### 3.2 停用记录的管理、详情与恢复(C22、C24、C26、C29、C31、C33、C37、C39、C41)
|
||||
|
||||
以下资源可以或可能写入 `status=1`,但普通列表不返回、普通详情拒绝读取,导致管理员无法恢复或验证:家族圈、视频、成长记录、亲友记录、备忘录、功德、谱文、相册/照片、祭祀活动。
|
||||
|
||||
请为每类资源提供唯一且一致的方案:
|
||||
|
||||
1. 给有管理权限的用户提供 `management` 列表和详情,包含正常/停用状态;或
|
||||
2. 提供受权限保护的独立状态读取与恢复接口。
|
||||
|
||||
必须明确 `status=0/1` 的中文业务含义、谁可停用/恢复、停用后文件和关联数据如何处理、普通用户是否仍可读取。前端在此完成前会持续固定提交正常状态,避免产生不可重读数据。
|
||||
|
||||
### 3.3 金额、分类与可清空字段(C18、C27、C32、C35、C42)
|
||||
|
||||
| 范围 | 缺失内容 | 必须定义 |
|
||||
| --- | --- | --- |
|
||||
| 亲友往来 `giftAmount`、功德金额、祭品金额 | 只有 number/BigDecimal,缺少精度和边界 | 货币单位、`precision`、`scale`、`minimum`、`maximum`、是否允许负数及其业务含义。建议金额响应使用十进制字符串,避免浮点误差。 |
|
||||
| 谱文分类 | 列表的 `categoryId` 未进入正式契约,且没有分类选项来源 | 正式 query、当前家谱文章分类选项接口、分类 DTO、是否允许无分类和分类停用后的处理。 |
|
||||
| 所有可选更新字段 | 未定义“不传字段”、`null`、空字符串、空数组的差别 | 每个更新 DTO 对每个可清空字段写明语义,并用后端测试覆盖。特别是备忘录提醒时间、说明、附件和成员绑定。 |
|
||||
|
||||
### 3.4 个人资料、成员、通知与关系契约(C04–C08、C11–C13、C19、C34、C43–C46)
|
||||
|
||||
| 范围 | 当前问题 | 后端补齐要求 |
|
||||
| --- | --- | --- |
|
||||
| 当前用户资料 | 资料读取仍可能是泛型响应;更新 DTO 没有地区字段 | 提供完整 `ProfileView`;如产品保留现居地区,更新/读取都使用 `regionCode`;明确头像 ID 类型和所有可清空字段语义。 |
|
||||
| 行政区划 | 历史上存在双路径和无元素 DTO | 只保留一套 PC 正式路径;`RegionView` 固定 `regionCode`、`regionName`、`regionLevel`、父级关系;删除旧路径。 |
|
||||
| 世系树 | `LineagePersonTreeView` 曾无属性 | OpenAPI 展开递归节点:人物 ID、基础展示资料、配偶、子女及必要的状态/权限。所有日期必须声明 `date` 或带时区规则的 `date-time`。 |
|
||||
| 枚举与能力 | `completed`、`feedType`、`recordType` 等可能无完整枚举;多数 View 缺 `canEdit/canDelete` | 明确自由文本或完整枚举;对页面需要的操作返回稳定的严格布尔能力字段,例如 `canManage`、`canEditContent`、`canEdit`、`canDelete`。后端仍做最终鉴权。 |
|
||||
| 通知 | 声明的家谱名、发送人、业务摘要等字段实际常为空,且含隐私风险 | 补齐真正可用的非敏感字段,或从 Schema 删除不会返回的字段;提供安全、明确的 `bizType/bizId` 深链规则,不能让前端猜路径;始终不返回发送人手机号。 |
|
||||
| 家谱成员 | `GenealogyMemberVo` 声明字段与实际填充不一致,角色枚举也不一致 | View 补齐真实需要的非敏感展示字段,或收紧 Schema;可分配角色正式收敛为后端实际支持的 `admin`、`editor`、`member`,不能接受 `owner`、`visitor`。 |
|
||||
| 世系关系解除 | 只有新增关系,没有解除关系 | 新增带家谱权限与关系完整性校验的解除接口;需明确父母、配偶、子女、兄弟姐妹解除后的双向影响。 |
|
||||
| 成员与世系人物解绑 | 当前更新中空 `lineagePersonId` 只代表不修改 | 提供专用解绑动作,或正式约定 `null`/空值表示解绑并给出可验证示例;不能由前端猜测。 |
|
||||
| 个人亲属关系 | `profile-data.html` 当前无个人关系数据 | 这是独立于家谱世系的产品决策:若保留个人关系页,请提供当前用户私有的列表/维护契约;若不做,请明确下线该需求,继续引导到家谱世系维护。 |
|
||||
|
||||
## 4. P2:契约质量与一致性必须同时修复
|
||||
|
||||
这些事项不一定新增页面,却会使 PC 前后端长期出现“接口能调但不知道如何正确用”的问题:
|
||||
|
||||
1. 删除/废弃缺少开头 `/` 的旧短信路径,只保留正式 PC 路径。
|
||||
2. 未登录可调用的注册、登录、短信、找回密码、验证接口在 OpenAPI 中标为公开;登录后接口标明 Bearer 鉴权。不要让公开接口错误继承 Authorization。
|
||||
3. 所有列表、详情、上传初始化/完成、写入结果都展开具体 Schema,尤其不能保留无元素的通用列表。上传初始化要明确 `uploadId`、秒传、已传分片、完成后的 OSS 信息。
|
||||
4. 用统一错误响应表达:未登录、无权限、资源不存在、字段校验失败、业务冲突、状态不可操作、频率/幂等冲突。每一类要有稳定 code 和用户可读 message;不要只返回模糊失败文本。
|
||||
5. 统一日期格式、时区、分页字段和排序字段;同一资源的列表、详情、创建、更新应使用同一 ID 类型和状态枚举。
|
||||
6. 更新 Apifox/OpenAPI、DTO、Controller、Service 和集成测试必须在同一提交完成;不要只改其中一处,也不要保留 APP 路径 fallback。
|
||||
|
||||
## 5. 权限与数据安全验收矩阵
|
||||
|
||||
后端实施每个新增或补齐接口后,至少用既有隔离的谱主、管理员/编辑、普通成员、非成员和未登录身份验证。不要为了验收在真实家谱中创建成员或写入数据。
|
||||
|
||||
| 场景 | 必须验证 |
|
||||
| --- | --- |
|
||||
| 未登录 | 仅公开认证接口可用;所有用户/家谱资源拒绝。 |
|
||||
| 非成员 | 不能读取私有家谱内容、成员、文件、邀请或管理列表。 |
|
||||
| 普通成员 | 只能使用后端允许的只读或本人操作,不能修改成员角色、家谱设置、排序、删除、证件或全局邀请。 |
|
||||
| 管理员/编辑 | 仅获得已声明能力范围内的内容或成员维护权限;不得越过谱主做家谱删除、谱主转让等操作。 |
|
||||
| 谱主 | 可执行后端定义的家谱级管理动作,但仍受资源状态、确认和业务完整性约束。 |
|
||||
|
||||
每个写入接口还要验证:重复请求幂等、写后可精确重读、越权返回明确错误、停用/恢复状态可验证、敏感字段不会越权泄漏。
|
||||
|
||||
## 6. 建议实施顺序
|
||||
|
||||
1. 先完成第 4 节的 OpenAPI/DTO 基础收口,并冻结唯一 PC 契约。
|
||||
2. 其次完成 3.1 文件访问和 3.2 停用管理;这两项能同时解除多个已有内容页的展示和状态闭环。
|
||||
3. 再完成 3.3、3.4 的金额、分类、资料、成员和关系精度。
|
||||
4. 最后按 P0-01 至 P0-08 补齐新业务;其中家谱删除、提现和支付必须先确认产品状态机与风控规则,不能只给一个无约束写接口。
|
||||
|
||||
## 7. 给后端 GPT 的执行指令
|
||||
|
||||
```text
|
||||
你负责为现有“家谱”项目补齐 PC API,不得复用或复制 Jiapu-App API。
|
||||
|
||||
先阅读 docs/PC后端补齐清单.md 与 docs/PC接口对接规划.md,确认当前 PC OpenAPI 和 Controller 的实际差异;不要重做“已有能力”章节列出的功能。
|
||||
|
||||
按第 6 节顺序实施。每个模块都要在同一改动中完成:PC OpenAPI/Apifox、请求 DTO、响应 View、Controller/Service、权限校验、错误码和集成测试。不要返回泛型 Object,不要让前端手填内部 ID,不要返回原始手机号,也不要新增 APP 路径兼容层。
|
||||
|
||||
实施前先输出:将新增/修改的 PC 接口、每个请求和响应字段、角色权限、状态机、数据库迁移(如有)以及与本清单的对应编号。实施后用隔离角色完成读写、越权、重复提交和写后重读验证,并重新导出 OpenAPI。
|
||||
```
|
||||
|
||||
## 8. 交付回执模板
|
||||
|
||||
后端完成后,请按以下格式回传,便于前端逐项接入:
|
||||
|
||||
| 清单编号 | 状态 | 正式 PC method/path | 请求 DTO | 响应 View | 权限/状态机 | OpenAPI 位置 | 集成测试证据 |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| 例如 P0-01 | 已完成/不做(附产品结论) | 仅填 PC 路径 | 字段和必填性 | 字段和类型 | 可用角色与错误码 | tag/schema 名 | 隔离测试名称,不含真实凭据 |
|
||||
|
||||
如某项决定不做,必须给出明确产品结论和对应前端页面应继续保留的不可操作文案;不能以“暂时没有接口”结束。
|
||||
-1666
File diff suppressed because it is too large
Load Diff
@@ -1,129 +0,0 @@
|
||||
# 内容与协作页面状态收口 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:** 让缺少正式接口的内容、协作、消息与提醒页面明确展示开发状态,同时保留家族圈动态的真实功能。
|
||||
|
||||
**Architecture:** 复用 `public/js/pending-pages.js` 的页面状态契约。每个待开发页面仅增加 `data-feature-status`、中文提示文案和状态脚本引用;脚本阻止主内容区未声明可用的业务链接,不新增接口调用、示例数据或业务脚本。
|
||||
|
||||
**Tech Stack:** 原生 JavaScript、静态 HTML/CSS、Node LTS `node:test`。
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- 保留现有页面、布局、视觉类名和导航结构,不删除用户设计。
|
||||
- `PC.openapi2.json` 中缺少正式接口的功能不得伪造请求或静态数据为真实业务。
|
||||
- 家族圈动态 `profile-feed.html` 与 `profile-feed-edit.html` 已接入正式接口,不能标记为待开发。
|
||||
- 所有新增注释使用中文;不写入测试账号或密码。
|
||||
- 当前工作区含用户未提交修改,不执行提交、重置或清理操作。
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 扩展页面状态测试
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/pending-pages.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: 每页的 `data-feature-status="pending"` 与 `public/js/pending-pages.js`。
|
||||
- Produces: 内容与协作待开发页面的静态约束,以及家族圈动态未被误标记的约束。
|
||||
|
||||
- [x] **Step 1: 写入失败测试**
|
||||
|
||||
在 `contentPendingPages` 中列出以下页面:
|
||||
|
||||
```js
|
||||
const contentPendingPages = [
|
||||
'profile-content.html', 'profile-article.html', 'profile-article-edit.html',
|
||||
'profile-album.html', 'profile-video.html', 'profile-gift.html',
|
||||
'profile-gift-edit.html', 'profile-growth.html', 'profile-growth-edit.html',
|
||||
'profile-memo.html', 'profile-memo-edit.html', 'profile-merit.html',
|
||||
'profile-merit-edit.html', 'profile-messages.html', 'profile-feedback.html',
|
||||
'profile-admin-permissions.html', 'profile-data-reminders.html'
|
||||
];
|
||||
```
|
||||
|
||||
同时断言 `profile-feed.html`、`profile-feed-edit.html` 不含待开发状态标记,并验证只有 `data-feature-link="available"` 的业务链接可跳转。
|
||||
|
||||
- [x] **Step 2: 运行失败测试**
|
||||
|
||||
Run: `$env:Path = 'C:\\Program Files\\nodejs;' + $env:Path; node --test tests/pending-pages.test.js`
|
||||
|
||||
Expected: 新增页面尚未标记,测试失败。
|
||||
|
||||
### Task 2: 标记内容与协作页面
|
||||
|
||||
**Files:**
|
||||
- Modify: `profile-content.html`
|
||||
- Modify: `profile-article.html`
|
||||
- Modify: `profile-article-edit.html`
|
||||
- Modify: `profile-album.html`
|
||||
- Modify: `profile-video.html`
|
||||
- Modify: `profile-gift.html`
|
||||
- Modify: `profile-gift-edit.html`
|
||||
- Modify: `profile-growth.html`
|
||||
- Modify: `profile-growth-edit.html`
|
||||
- Modify: `profile-memo.html`
|
||||
- Modify: `profile-memo-edit.html`
|
||||
- Modify: `profile-merit.html`
|
||||
- Modify: `profile-merit-edit.html`
|
||||
- Modify: `profile-messages.html`
|
||||
- Modify: `profile-feedback.html`
|
||||
- Modify: `profile-admin-permissions.html`
|
||||
- Modify: `profile-data-reminders.html`
|
||||
- Test: `tests/pending-pages.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `PageAvailability.init()` 读取的 `<body data-feature-status="pending" data-feature-message="..."></body>`。
|
||||
- Produces: 可见的“功能开发中”提示,并拦截主内容区提交与操作按钮。
|
||||
|
||||
- [x] **Step 1: 为每页增加状态契约**
|
||||
|
||||
在目标页面的 `body` 增加 `data-feature-status="pending"` 和与业务相符的中文 `data-feature-message`。例如谱文页面使用“谱文服务正在开发中,当前页面仅保留设计预览。”
|
||||
|
||||
- [x] **Step 2: 加载统一状态脚本**
|
||||
|
||||
有 `ApiClient.js` 的页面在其后加载:
|
||||
|
||||
```html
|
||||
<script src="utils/ApiClient.js"></script>
|
||||
<script src="public/js/pending-pages.js"></script>
|
||||
```
|
||||
|
||||
`profile-video.html` 与 `profile-data-reminders.html` 不依赖 `ApiClient.js`,在 `page-effects.js` 前加载状态脚本。
|
||||
|
||||
- [x] **Step 3: 收束业务入口跳转**
|
||||
|
||||
待开发页面主内容区的业务链接由状态脚本拦截并显示当前页面提示;侧栏保留页面浏览导航。`profile-content.html` 的家族圈动态入口使用 `data-feature-link="available"`,继续跳转到 `profile-feed.html`。
|
||||
|
||||
- [x] **Step 4: 运行测试确认通过**
|
||||
|
||||
Run: `$env:Path = 'C:\\Program Files\\nodejs;' + $env:Path; node --test tests/pending-pages.test.js`
|
||||
|
||||
Expected: 待开发页面均加载状态脚本,家族圈动态页面未被标记。
|
||||
|
||||
### Task 3: 更新进度并做静态验证
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/规划.md`
|
||||
- Modify: `docs/superpowers/plans/2026-07-11-content-page-state-closure.md`
|
||||
- Test: `tests/*.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: 全部页面的本地脚本引用。
|
||||
- Produces: 已完成第二轮进度与可重复的验证结果。
|
||||
|
||||
- [x] **Step 1: 更新总规划的第二轮状态**
|
||||
|
||||
将 `docs/规划.md` 中“第二轮:内容与协作页面状态收口”从未完成更新为已完成,并写明家族圈动态未受影响。
|
||||
|
||||
- [x] **Step 2: 执行完整测试**
|
||||
|
||||
Run: `$env:Path = 'C:\\Program Files\\nodejs;' + $env:Path; npm.cmd test`
|
||||
|
||||
Expected: 所有 Node 测试通过。
|
||||
|
||||
- [x] **Step 3: 执行静态检查**
|
||||
|
||||
Run: PowerShell 扫描全部 HTML 的 `src="*.js"`,随后运行 `git diff --check`。
|
||||
|
||||
Expected: 每个本地脚本文件存在,且没有空白格式错误。
|
||||
@@ -1,192 +0,0 @@
|
||||
# 家族圈动态接口接入 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.openapi2.json` 中已交付的 12 个家族圈动态接口接入既有动态页面,并保持未开发的家谱业务不调用不存在的接口。
|
||||
|
||||
**Architecture:** `PC.openapi2.json` 是接口唯一依据;`ApiClient` 提供一一对应的私有请求封装,`feed-pages.js` 只协调页面事件和已定义的接口方法。动态页从 URL 查询参数读取真实 `genealogyId`,不写入示例家谱 ID。
|
||||
|
||||
**Tech Stack:** 原生 JavaScript、Axios、Node LTS `node:test`、静态 HTML/CSS。
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- 所有新增或修改的注释使用清晰中文。
|
||||
- 不删除现有页面和样式;不接入尚未出现在 `PC.openapi2.json` 的业务。
|
||||
- 不提交当前工作区:其中含用户已有的未提交修改。
|
||||
- 动态读取响应目前只有通用 `ListResult`/`PageResult`,页面仅使用 `feedId` 与 `feedContent`;后端返回缺少这两个字段时展示明确错误,不猜测字段名。
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 更新接口边界测试和动态请求测试
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/api-client-contract.test.js`
|
||||
- Modify: `tests/pc-scope.test.js`
|
||||
- Create: `tests/feed-pages.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `PC.openapi2.json` 的 12 个 `/genealogy/pc/genealogies/{genealogyId}/feeds` 操作。
|
||||
- Produces: 对客户端路径、HTTP 方法、请求体和页面请求体转换的自动化约束。
|
||||
|
||||
- [ ] **Step 1: 写入失败测试**
|
||||
|
||||
```js
|
||||
test('家族圈动态方法使用文档定义的路径和请求体', async () => {
|
||||
const calls = [];
|
||||
const client = createClientWithRecorder(calls);
|
||||
await client.createFeed(900001001, { feedContent: '今天上传一张老照片。' });
|
||||
await client.feedCommentsPage(900001001, 7001, { pageNum: 1, pageSize: 10 });
|
||||
assert.deepEqual(calls.map((call) => [call.method, call.url]), [
|
||||
['post', '/genealogy/pc/genealogies/900001001/feeds'],
|
||||
['get', '/genealogy/pc/genealogies/900001001/feeds/7001/comments/page']
|
||||
]);
|
||||
});
|
||||
|
||||
test('动态表单只构造 FamilyFeedBody 字段', () => {
|
||||
assert.deepEqual(FeedPages.buildFeedBody({
|
||||
feedContent: '家谱动态', mediaOssIds: '101,102', status: '0'
|
||||
}), {
|
||||
feedType: 'text', feedContent: '家谱动态', mediaOssIds: '101,102', status: '0'
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 运行失败测试**
|
||||
|
||||
Run: `$env:Path = 'C:\\Program Files\\nodejs;' + $env:Path; npm.cmd test`
|
||||
|
||||
Expected: 动态方法或 `feed-pages.js` 尚不存在导致失败。
|
||||
|
||||
- [ ] **Step 3: 调整范围检查**
|
||||
|
||||
```js
|
||||
assert.match(contract, /\/genealogy\/pc\/genealogies\/\{genealogyId\}\/feeds/);
|
||||
assert.equal(read('profile-feed.html').includes('src="public/js/feed-pages.js"'), true);
|
||||
assert.equal(read('profile-feed-edit.html').includes('src="public/js/feed-pages.js"'), true);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 重新运行测试,确认仍处于预期失败状态**
|
||||
|
||||
Run: `$env:Path = 'C:\\Program Files\\nodejs;' + $env:Path; npm.cmd test`
|
||||
|
||||
Expected: 仅动态实现相关断言失败。
|
||||
|
||||
### Task 2: 增加 12 个家族圈 API 方法
|
||||
|
||||
**Files:**
|
||||
- Modify: `utils/ApiClient.js`
|
||||
- Modify: `tests/api-client-contract.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `genealogyId:number|string`、`feedId:number|string`、`commentId:number|string`。
|
||||
- Produces: `feeds`、`feedsPage`、`feedDetail`、`createFeed`、`updateFeed`、`deleteFeed`、`likeFeed`、`unlikeFeed`、`feedComments`、`feedCommentsPage`、`createFeedComment`、`deleteFeedComment`。
|
||||
|
||||
- [ ] **Step 1: 保持失败测试不变**
|
||||
|
||||
```js
|
||||
await client.createFeed(900001001, { feedContent: '家谱动态' });
|
||||
assert.equal(calls[0].url, '/genealogy/pc/genealogies/900001001/feeds');
|
||||
assert.deepEqual(calls[0].data, { feedContent: '家谱动态' });
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 实现最小路径构造**
|
||||
|
||||
```js
|
||||
function feedPath(genealogyId, suffix) {
|
||||
return '/genealogy/pc/genealogies/' + encodeURIComponent(genealogyId) + '/feeds' + suffix;
|
||||
}
|
||||
|
||||
createFeed: function (genealogyId, body) {
|
||||
return request('POST', feedPath(genealogyId, ''), { body: body });
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 运行测试,确认通过**
|
||||
|
||||
Run: `$env:Path = 'C:\\Program Files\\nodejs;' + $env:Path; npm.cmd test`
|
||||
|
||||
Expected: 动态客户端路径和请求体断言通过。
|
||||
|
||||
### Task 3: 恢复动态页的真实交互
|
||||
|
||||
**Files:**
|
||||
- Create: `public/js/feed-pages.js`
|
||||
- Modify: `profile-feed.html`
|
||||
- Modify: `profile-feed-edit.html`
|
||||
- Modify: `tests/feed-pages.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `GenealogyApi.defaultClient` 的 12 个动态方法和 URL 中的 `genealogyId`、可选 `feedId`。
|
||||
- Produces: 动态列表、发布/编辑、点赞/取消点赞、评论查看/发布/删除、动态删除事件。
|
||||
|
||||
- [ ] **Step 1: 使页面测试失败**
|
||||
|
||||
```js
|
||||
assert.equal(FeedPages.getCurrentGenealogyId('?genealogyId=900001001'), '900001001');
|
||||
assert.equal(FeedPages.getCurrentGenealogyId(''), '');
|
||||
assert.equal(FeedPages.getFeedId({ feedId: 7001 }), '7001');
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 实现页面数据转换和事件**
|
||||
|
||||
```js
|
||||
function buildFeedBody(values) {
|
||||
return {
|
||||
feedType: 'text',
|
||||
feedContent: String(values.feedContent || '').trim(),
|
||||
mediaOssIds: trimOrUndefined(values.mediaOssIds),
|
||||
status: trimOrUndefined(values.status)
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
列表只读取 `feedId`、`feedContent`,并在缺少其中任一字段时显示“动态响应缺少 feedId 或 feedContent,请联系后端补充 DTO”。编辑页以 `feedId` 查询详情并调用 `updateFeed`;列表操作调用点赞、取消点赞、评论、删除接口后重新加载当前页。
|
||||
|
||||
- [ ] **Step 3: 挂载脚本和契约字段**
|
||||
|
||||
```html
|
||||
<textarea id="feedContent" name="feedContent" required></textarea>
|
||||
<script src="public/js/feed-pages.js"></script>
|
||||
```
|
||||
|
||||
移除未在 `FamilyFeedBody` 中定义的“可见范围”提交字段;保持页面布局与既有视觉类名。
|
||||
|
||||
- [ ] **Step 4: 运行测试,确认通过**
|
||||
|
||||
Run: `$env:Path = 'C:\\Program Files\\nodejs;' + $env:Path; npm.cmd test`
|
||||
|
||||
Expected: `feed-pages.test.js` 和已有测试全部通过。
|
||||
|
||||
### Task 4: 更新文档来源与最终验证
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/pc-scope.test.js`
|
||||
- Modify: `PC.openapi.yaml`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `PC.openapi2.json` 的接口路径。
|
||||
- Produces: 明确标记旧 YAML 不再为接口源,并使静态检查以 JSON 为准。
|
||||
|
||||
- [ ] **Step 1: 写入失败检查**
|
||||
|
||||
```js
|
||||
const contract = JSON.parse(read('PC.openapi2.json'));
|
||||
assert.ok(contract.paths['/genealogy/pc/genealogies/{genealogyId}/feeds']);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 标记旧 YAML 失效**
|
||||
|
||||
在 `PC.openapi.yaml` 文件首行写入中文说明:该文件不再作为代码或测试接口源,正式接口源为 `PC.openapi2.json`。不复制或手改 JSON 中未定义的动态响应 DTO。
|
||||
|
||||
- [ ] **Step 3: 全量验证**
|
||||
|
||||
Run: `$env:Path = 'C:\\Program Files\\nodejs;' + $env:Path; npm.cmd test`
|
||||
|
||||
Expected: 全部测试通过,且所有动态页面加载的脚本文件都存在。
|
||||
|
||||
## Self-review
|
||||
|
||||
- 范围只覆盖最新文档已交付的家族圈动态接口,没有加入家谱创建、成员或世系树接口。
|
||||
- 所有请求体字段来自 `FamilyFeedBody` 或 `FamilyFeedCommentBody`。
|
||||
- 读取响应不使用历史字段兜底,不把未知字段猜成有效数据。
|
||||
- 任何 URL 缺少 `genealogyId` 的页面都会明确提示,不使用硬编码示例 ID。
|
||||
@@ -1,140 +0,0 @@
|
||||
# 家谱基础页面状态收口 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:** 让尚未获得后端家谱接口的基础页面明确展示开发状态,并阻止表单和操作按钮伪造成功结果。
|
||||
|
||||
**Architecture:** 仅在家谱列表、创建/加入、审核、成员管理、邀请、世系和字辈页面加载一个轻量状态脚本。页面通过 `data-feature-status` 和 `data-feature-message` 提供文案;脚本统一插入提示条,并拦截页面主内容区的提交与按钮操作。
|
||||
|
||||
**Tech Stack:** 原生 JavaScript、静态 HTML/CSS、Node LTS `node:test`。
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- 保留现有页面、布局、视觉类名和导航结构,不删除用户设计。
|
||||
- 不调用 `PC.openapi2.json` 中不存在的家谱、成员、世系或字辈接口。
|
||||
- 所有新增注释使用中文;不写入测试账号或密码。
|
||||
- 当前工作区含用户未提交修改,不执行提交、重置或清理操作。
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 建立失败测试
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/pending-pages.test.js`
|
||||
- Modify: `tests/pc-scope.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `PageAvailability.buildBanner(message)` 与 `PageAvailability.isPendingPage(body)`。
|
||||
- Produces: 对状态脚本、页面标记和脚本加载的约束。
|
||||
|
||||
- [x] **Step 1: 写入失败测试**
|
||||
|
||||
```js
|
||||
test('待开发页面生成明确且转义后的状态提示', () => {
|
||||
assert.match(PageAvailability.buildBanner('家谱基础服务正在开发中'), /家谱基础服务正在开发中/);
|
||||
assert.match(PageAvailability.buildBanner('<script>'), /<script>/);
|
||||
});
|
||||
|
||||
test('家谱基础页面显式加载状态脚本', () => {
|
||||
assert.equal(read('profile-families.html').includes('data-feature-status="pending"'), true);
|
||||
assert.equal(read('profile-tree.html').includes('src="public/js/pending-pages.js"'), true);
|
||||
});
|
||||
```
|
||||
|
||||
- [x] **Step 2: 运行失败测试**
|
||||
|
||||
Run: `$env:Path = 'C:\\Program Files\\nodejs;' + $env:Path; node --test tests/pending-pages.test.js tests/pc-scope.test.js`
|
||||
|
||||
Expected: `pending-pages.js` 不存在且页面未标记导致失败。
|
||||
|
||||
### Task 2: 实现统一状态脚本与样式
|
||||
|
||||
**Files:**
|
||||
- Create: `public/js/pending-pages.js`
|
||||
- Modify: `public/css/profile-module.css`
|
||||
- Test: `tests/pending-pages.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `<body data-feature-status="pending" data-feature-message="..."></body>`。
|
||||
- Produces: `PageAvailability.buildBanner(message)`、`PageAvailability.init()`。
|
||||
|
||||
- [x] **Step 1: 实现最小状态脚本**
|
||||
|
||||
```js
|
||||
function buildBanner(message) {
|
||||
return '<section class="feature-status-banner" role="status"><strong>功能开发中</strong><p>' + escapeHtml(message) + '</p></section>';
|
||||
}
|
||||
```
|
||||
|
||||
脚本只在 `data-feature-status="pending"` 页面运行,将提示条放在 `.module-main` 首位;拦截该页面 `main` 区域内的表单提交和非重置按钮,显示同一提示文案。
|
||||
|
||||
- [x] **Step 2: 增加复用样式**
|
||||
|
||||
```css
|
||||
.feature-status-banner {
|
||||
border: 1px solid rgba(196, 146, 69, .38);
|
||||
border-radius: 16px;
|
||||
background: #fff8e8;
|
||||
}
|
||||
```
|
||||
|
||||
- [x] **Step 3: 运行测试确认通过**
|
||||
|
||||
Run: `$env:Path = 'C:\\Program Files\\nodejs;' + $env:Path; node --test tests/pending-pages.test.js tests/pc-scope.test.js`
|
||||
|
||||
Expected: 新增状态脚本和页面标记断言通过。
|
||||
|
||||
### Task 3: 标记家谱基础页面
|
||||
|
||||
**Files:**
|
||||
- Modify: `profile-families.html`
|
||||
- Modify: `profile-create-family.html`
|
||||
- Modify: `profile-join-family.html`
|
||||
- Modify: `profile-join-review.html`
|
||||
- Modify: `profile-family-admin.html`
|
||||
- Modify: `profile-invite.html`
|
||||
- Modify: `profile-tree.html`
|
||||
- Modify: `profile-generation.html`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `pending-pages.js` 和每页 `data-feature-message`。
|
||||
- Produces: 明确状态提示,不提交文档外请求。
|
||||
|
||||
- [x] **Step 1: 在 body 标记状态**
|
||||
|
||||
```html
|
||||
<body class="page-profile-module" data-feature-status="pending" data-feature-message="家谱成员与权限服务正在开发中,当前页面仅保留设计预览。">
|
||||
```
|
||||
|
||||
- [x] **Step 2: 在 ApiClient 后加载状态脚本**
|
||||
|
||||
```html
|
||||
<script src="utils/ApiClient.js"></script>
|
||||
<script src="public/js/pending-pages.js"></script>
|
||||
<script src="public/js/page-effects.js"></script>
|
||||
```
|
||||
|
||||
- [x] **Step 3: 运行完整测试**
|
||||
|
||||
Run: `$env:Path = 'C:\\Program Files\\nodejs;' + $env:Path; npm.cmd test`
|
||||
|
||||
Expected: 全部测试通过。
|
||||
|
||||
### Task 4: 静态验证
|
||||
|
||||
**Files:**
|
||||
- Test: `tests/pc-scope.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: 8 个已标记页面。
|
||||
- Produces: 所有待开发页面有状态文案,所有页面脚本路径存在。
|
||||
|
||||
- [x] **Step 1: 执行脚本引用检查**
|
||||
|
||||
Run: PowerShell 扫描全部 HTML 的 `src="*.js"`,确认每个本地脚本文件存在。
|
||||
|
||||
- [x] **Step 2: 执行格式检查**
|
||||
|
||||
Run: `git diff --check`
|
||||
|
||||
Expected: 无缺失脚本和空白格式错误。
|
||||
@@ -1,139 +0,0 @@
|
||||
# 官网静态页面补齐 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:** 补齐官网新闻、法律文档入口、通用状态与工单页面,使无接口的线上服务不再伪装为可用功能。
|
||||
|
||||
**Architecture:** 新增静态 HTML 页面共用 `public/css/site-info.css` 和现有官网头尾布局。新闻页只链接现有静态文章与公告详情;法律页面明确标明待法务确认;工单页面复用 `pending-pages.js` 阻止无接口提交。
|
||||
|
||||
**Tech Stack:** 静态 HTML、原生 CSS、原生 JavaScript、Node LTS `node:test`。
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- 保留现有页面、布局、视觉类名和导航结构,不删除用户设计。
|
||||
- 不新增 `PC.openapi2.json` 外的接口调用,不伪造新闻接口、法律协议、工单列表或工单详情数据。
|
||||
- 法律页面只能显示“待法务确认”说明,发布前必须替换为主体与法务确认的正式文本。
|
||||
- 所有新增注释使用中文;不写入测试账号或密码。
|
||||
- 当前工作区含用户未提交修改,不执行提交、重置或清理操作。
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 建立静态页面约束测试
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/public-static-pages.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: 静态页面文件、`data-feature-status="pending"` 和本地 `href`。
|
||||
- Produces: 页面存在、关键说明、状态脚本与本地链接的可重复校验。
|
||||
|
||||
- [x] **Step 1: 写入失败测试**
|
||||
|
||||
测试必须断言存在以下文件:
|
||||
|
||||
```js
|
||||
const staticPages = [
|
||||
'news.html', 'privacy.html', 'children-privacy.html', 'terms.html',
|
||||
'not-found.html', 'forbidden.html', 'maintenance.html',
|
||||
'my-tickets.html', 'ticket-detail.html'
|
||||
];
|
||||
```
|
||||
|
||||
同时断言:`news.html` 含有新闻分类锚点并链接 `article-detail.html`、`notice-detail.html`;三个法律页面含“待法务确认”;`submit-ticket.html`、`my-tickets.html`、`ticket-detail.html` 有待开发状态和 `pending-pages.js`;新页面中的本地 `href` 全部存在。
|
||||
|
||||
- [x] **Step 2: 运行失败测试**
|
||||
|
||||
Run: `$env:Path = 'C:\\Program Files\\nodejs;' + $env:Path; node --test tests/public-static-pages.test.js`
|
||||
|
||||
Expected: 新页面不存在导致测试失败。
|
||||
|
||||
### Task 2: 建立通用静态页面外观与新闻、法律入口
|
||||
|
||||
**Files:**
|
||||
- Create: `public/css/site-info.css`
|
||||
- Create: `news.html`
|
||||
- Create: `privacy.html`
|
||||
- Create: `children-privacy.html`
|
||||
- Create: `terms.html`
|
||||
- Modify: `index.html`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: 现有 `public/css/public.css` 的 `site-header`、`footer`、`container`、`btn` 与 `card` 样式。
|
||||
- Produces: 新闻分类和详情跳转,以及能从首页底部访问的法律入口。
|
||||
|
||||
- [x] **Step 1: 创建复用样式**
|
||||
|
||||
`site-info.css` 提供 `.site-info-hero`、`.site-info-layout`、`.site-info-card`、`.site-info-status`、`.site-info-links`,仅处理本轮新增页面的排版和窄屏适配。
|
||||
|
||||
- [x] **Step 2: 创建新闻页面**
|
||||
|
||||
`news.html` 必须有“平台公告”“家谱文化”分类锚点,并将两条静态内容分别链接到 `notice-detail.html` 与 `article-detail.html`。
|
||||
|
||||
- [x] **Step 3: 创建法律页面并连接首页**
|
||||
|
||||
三个法律页面均写明“待法务确认”,并互相提供跳转链接。首页 `footer-legal` 增加三个页面链接。
|
||||
|
||||
### Task 3: 建立状态与工单边界
|
||||
|
||||
**Files:**
|
||||
- Create: `not-found.html`
|
||||
- Create: `forbidden.html`
|
||||
- Create: `maintenance.html`
|
||||
- Create: `my-tickets.html`
|
||||
- Create: `ticket-detail.html`
|
||||
- Modify: `submit-ticket.html`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `PageAvailability.init()` 读取的待开发页面属性。
|
||||
- Produces: 三种无接口提交的工单状态,和可返回首页、帮助中心的静态状态页。
|
||||
|
||||
- [x] **Step 1: 创建三个通用状态页**
|
||||
|
||||
三个页面分别显示“页面不存在”“无访问权限”“系统维护中”,并提供 `index.html` 返回入口。
|
||||
|
||||
- [x] **Step 2: 创建工单列表与详情状态页**
|
||||
|
||||
`my-tickets.html`、`ticket-detail.html` 均使用:
|
||||
|
||||
```html
|
||||
<body data-feature-status="pending" data-feature-message="在线工单查询服务正在开发中,当前页面仅保留设计预览。">
|
||||
```
|
||||
|
||||
并在 `public/js/pending-pages.js` 后仅放行帮助中心链接。
|
||||
|
||||
- [x] **Step 3: 收束提交工单表单**
|
||||
|
||||
`submit-ticket.html` 增加:
|
||||
|
||||
```html
|
||||
<body class="page-submit-ticket" data-feature-status="pending" data-feature-message="在线提交工单服务正在开发中,请先通过帮助中心了解常见问题。">
|
||||
```
|
||||
|
||||
在 `ApiClient.js` 后加载状态脚本,并为“返回帮助中心”链接添加 `data-feature-link="available"`。
|
||||
|
||||
### Task 4: 回写进度与验证
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/规划.md`
|
||||
- Modify: `docs/superpowers/plans/2026-07-11-public-static-pages.md`
|
||||
- Test: `tests/*.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: 新增静态页面和全部 HTML 本地脚本引用。
|
||||
- Produces: 第四轮进度记录和验证结果。
|
||||
|
||||
- [x] **Step 1: 写入第四轮完成状态**
|
||||
|
||||
在 `docs/规划.md` 的“实施进度”中增加静态官网页面补齐已完成,并注明法律文本需法务替换。
|
||||
|
||||
- [x] **Step 2: 执行完整测试**
|
||||
|
||||
Run: `$env:Path = 'C:\\Program Files\\nodejs;' + $env:Path; npm.cmd test`
|
||||
|
||||
Expected: 所有 Node 测试通过。
|
||||
|
||||
- [x] **Step 3: 执行静态检查**
|
||||
|
||||
Run: PowerShell 扫描全部 HTML 的 `src="*.js"`,随后运行 `git diff --check`。
|
||||
|
||||
Expected: 每个本地脚本文件存在,且没有空白格式错误。
|
||||
@@ -1,112 +0,0 @@
|
||||
# 剩余用户 PC 业务入口收口 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:** 复用 `public/js/pending-pages.js` 的状态契约和链接收束规则。仅标记 `profile-family-home.html`、`profile-share.html`、`profile-services.html`;账号主页、个人资料、安全设置和家族圈动态继续使用现有真实功能。
|
||||
|
||||
**Tech Stack:** 原生 JavaScript、静态 HTML/CSS、Node LTS `node:test`。
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- 保留现有页面、布局、视觉类名和导航结构,不删除用户设计。
|
||||
- `PC.openapi2.json` 中不存在家谱主页、邀请奖励和会员订单的正式接口,不得伪造请求或静态数据为真实业务。
|
||||
- `profile-family-home.html` 的家族圈动态入口继续跳转到 `profile-feed.html`。
|
||||
- 所有新增注释使用中文;不写入测试账号或密码。
|
||||
- 当前工作区含用户未提交修改,不执行提交、重置或清理操作。
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 扩展入口状态测试
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/pending-pages.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: 目标页面的 `data-feature-status="pending"`、状态脚本引用与 `data-feature-link="available"`。
|
||||
- Produces: 剩余业务入口的静态约束,及真实账户页面不被误标记的约束。
|
||||
|
||||
- [x] **Step 1: 写入失败测试**
|
||||
|
||||
新增以下数组并加入现有待开发页面检查:
|
||||
|
||||
```js
|
||||
const residualPendingPages = [
|
||||
'profile-family-home.html',
|
||||
'profile-share.html',
|
||||
'profile-services.html'
|
||||
];
|
||||
```
|
||||
|
||||
新增断言:`profile-family-home.html` 的 `profile-feed.html` 链接必须带有 `data-feature-link="available"`;`profile.html`、`profile-data.html`、`profile-security.html` 不得含待开发状态标记。
|
||||
|
||||
- [x] **Step 2: 运行失败测试**
|
||||
|
||||
Run: `$env:Path = 'C:\\Program Files\\nodejs;' + $env:Path; node --test tests/pending-pages.test.js`
|
||||
|
||||
Expected: 三个剩余入口尚未标记,测试失败。
|
||||
|
||||
### Task 2: 标记剩余业务入口
|
||||
|
||||
**Files:**
|
||||
- Modify: `profile-family-home.html`
|
||||
- Modify: `profile-share.html`
|
||||
- Modify: `profile-services.html`
|
||||
- Test: `tests/pending-pages.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `PageAvailability.init()` 读取的页面状态属性,和 `PageAvailability.shouldBlockLink(link)` 的可用入口约定。
|
||||
- Produces: 状态提示、业务链接拦截,以及唯一保留的动态入口。
|
||||
|
||||
- [x] **Step 1: 标记页面状态**
|
||||
|
||||
分别在三个 `body` 中增加 `data-feature-status="pending"` 和对应中文提示:家谱主页使用“家谱概览与管理服务正在开发中,当前页面仅保留设计预览。”;应用分享使用“应用分享与邀请奖励服务正在开发中,当前页面仅保留设计预览。”;帮助与服务使用“会员与在线服务正在开发中,当前页面仅保留设计预览。”
|
||||
|
||||
- [x] **Step 2: 加载统一状态脚本并放行动态入口**
|
||||
|
||||
每页都在 `utils/ApiClient.js` 后增加:
|
||||
|
||||
```html
|
||||
<script src="utils/ApiClient.js"></script>
|
||||
<script src="public/js/pending-pages.js"></script>
|
||||
```
|
||||
|
||||
在 `profile-family-home.html` 中将动态链接改为:
|
||||
|
||||
```html
|
||||
<a class="btn primary magnetic" href="profile-feed.html" data-feature-link="available">发布动态</a>
|
||||
```
|
||||
|
||||
- [x] **Step 3: 运行测试确认通过**
|
||||
|
||||
Run: `$env:Path = 'C:\\Program Files\\nodejs;' + $env:Path; node --test tests/pending-pages.test.js`
|
||||
|
||||
Expected: 全部状态页面和真实账户页面断言通过。
|
||||
|
||||
### Task 3: 更新进度与验证
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/规划.md`
|
||||
- Modify: `docs/superpowers/plans/2026-07-11-residual-pc-entry-closure.md`
|
||||
- Test: `tests/*.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: 全部 HTML 的本地脚本引用。
|
||||
- Produces: 已完成第三轮进度和可重复验证记录。
|
||||
|
||||
- [x] **Step 1: 写入第三轮完成状态**
|
||||
|
||||
在 `docs/规划.md` 的“实施进度”中增加已完成第三轮,说明三类剩余 PC 业务入口已收口,家族圈动态未受影响。
|
||||
|
||||
- [x] **Step 2: 执行完整测试**
|
||||
|
||||
Run: `$env:Path = 'C:\\Program Files\\nodejs;' + $env:Path; npm.cmd test`
|
||||
|
||||
Expected: 所有 Node 测试通过。
|
||||
|
||||
- [x] **Step 3: 执行静态检查**
|
||||
|
||||
Run: PowerShell 扫描全部 HTML 的 `src="*.js"`,随后运行 `git diff --check`。
|
||||
|
||||
Expected: 每个本地脚本文件存在,且没有空白格式错误。
|
||||
@@ -1,391 +0,0 @@
|
||||
# 应用下载页 PC 推广列表 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:** 在 `app.html` 现有推广区域读取并安全展示后端 PC 推广列表,同时保持应用下载页公开可访问。
|
||||
|
||||
**Architecture:** `utils/ApiClient.js` 独占 method/path/query 契约;新建 `public/js/app-promotion-pages.js` 独占 `AppPromotionVo` 规范化、链接安全、区域状态和渲染。页面只提供容器与脚本加载,不复制接口逻辑。
|
||||
|
||||
**Tech Stack:** 原生 JavaScript UMD、HTML/CSS、Node.js `node:test`、现有 `GenealogyApi` / `AxiosRequestUtil`。
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- 只调用 `GET /genealogy/pc/promotions`,不得调用 APP 或后台管理接口。
|
||||
- 后端控制器实际要求 PC 登录态;请求携带当前 token 和基础 `clientid`。
|
||||
- 页面初始化不发送 `platform`;客户端即使被其它调用方使用,也只允许可选 Query `platform`。
|
||||
- 页面不展示或拼接 `coverOssId`,不允许用户输入业务 ID 或 OSS ID。
|
||||
- `targetUrl` 只允许绝对 HTTP(S) URL;其它值按无链接卡片展示。
|
||||
- 页面本身保持公开;未登录和 401 只改变推广区域,不跳走整个下载页。
|
||||
- 旧 `promotion-pages.js` 不恢复,不保留兼容入口。
|
||||
- 后端项目 `D:\WorkSpace\Java\Genealogy` 只读。
|
||||
- 当前共享 `main` 工作区包含多批未提交改动;本计划不执行 Git stage、commit、merge 或 push。
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 冻结 PC 推广客户端契约
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/api-client-contract.test.js`
|
||||
- Modify: `utils/ApiClient.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: 私有 `request(method, path, options)` 与 `pickDefined(source, allowedFields)`。
|
||||
- Produces: `client.promotions(query?: { platform?: string }): Promise<Array<AppPromotionVo>>`。
|
||||
|
||||
- [ ] **Step 1: 在客户端允许方法集合中加入 `promotions`,并写失败测试**
|
||||
|
||||
测试使用带 `access-token` 的真实客户端边界,传入:
|
||||
|
||||
```js
|
||||
await client.promotions({
|
||||
platform: 'pc',
|
||||
keyword: 'must-drop'
|
||||
});
|
||||
```
|
||||
|
||||
手工断言请求为:
|
||||
|
||||
```js
|
||||
[
|
||||
'get',
|
||||
'/genealogy/pc/promotions',
|
||||
{ platform: 'pc' },
|
||||
'Bearer access-token'
|
||||
]
|
||||
```
|
||||
|
||||
该测试捕获错误 path、错误 method、契约外 Query 被透传或误设 `auth: false`。
|
||||
|
||||
- [ ] **Step 2: 运行客户端测试观察 RED**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/api-client-contract.test.js
|
||||
```
|
||||
|
||||
Expected: FAIL,提示 `promotions` 未导出或不是函数。
|
||||
|
||||
- [ ] **Step 3: 添加最小客户端实现**
|
||||
|
||||
在 `helpArticleDetail` 和 VIP 方法附近添加:
|
||||
|
||||
```js
|
||||
promotions: function (query) {
|
||||
return request('GET', '/genealogy/pc/promotions', {
|
||||
query: pickDefined(query, ['platform'])
|
||||
});
|
||||
},
|
||||
```
|
||||
|
||||
不得增加 `auth: false`、默认 `platform` 或旧路径 fallback。
|
||||
|
||||
- [ ] **Step 4: 运行客户端测试观察 GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/api-client-contract.test.js
|
||||
```
|
||||
|
||||
Expected: 全部通过。
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 实现推广响应和安全渲染边界
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/app-promotion-pages.test.js`
|
||||
- Create: `public/js/app-promotion-pages.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `GenealogyApi.defaultClient.promotions()`、`getToken()`、`clearToken()`。
|
||||
- Produces:
|
||||
- `normalizeTargetUrl(value): string`
|
||||
- `normalizePromotion(item): PromotionView | null`
|
||||
- `normalizePromotionList(data): PromotionView[]`
|
||||
- `renderPromotionList(data): string`
|
||||
- `renderPromotionLoginRequired(): string`
|
||||
- `loadPromotions(api): Promise<PromotionView[]>`
|
||||
- `isUnauthorized(error): boolean`
|
||||
- `init(): Promise<void>`
|
||||
|
||||
`PromotionView` 的唯一形状:
|
||||
|
||||
```js
|
||||
{
|
||||
promotionId: '2062179707935264769',
|
||||
promotionTitle: '下载移动端',
|
||||
promotionDesc: '随时查看家谱内容',
|
||||
targetUrl: 'https://example.com/download'
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 1: 写完整真实 DTO fixture 和失败测试**
|
||||
|
||||
Fixture 必须包含后端全部 10 个字段:
|
||||
|
||||
```js
|
||||
{
|
||||
promotionId: '2062179707935264769',
|
||||
promotionKey: 'app-download',
|
||||
promotionTitle: '下载移动端',
|
||||
promotionDesc: '随时查看家谱内容',
|
||||
coverOssId: '2062179707935264701',
|
||||
targetUrl: 'https://example.com/download',
|
||||
platform: 'all',
|
||||
sortOrder: 10,
|
||||
status: '0',
|
||||
remark: 'internal-only'
|
||||
}
|
||||
```
|
||||
|
||||
分别覆盖:
|
||||
|
||||
1. 规范化结果只保留 `PromotionView` 四字段;
|
||||
2. 不安全数字长 ID、空标题和 `status=1` 返回 `null`;
|
||||
3. 直接数组正常,`{ rows: [...] }` 和混入非法元素返回空数组;
|
||||
4. `https://`、`http://` 保留,`javascript:`、`data:`、相对路径和空值返回空字符串;
|
||||
5. 渲染转义标题与说明,不出现 `coverOssId`、`promotionKey`、`sortOrder`、`status`、`remark`;
|
||||
6. 合法 URL 使用 `target="_blank"` 和 `rel="noopener noreferrer"`,非法 URL 不生成 `<a>`;
|
||||
7. `loadPromotions(api)` 只调用一次 `api.promotions()`,不传 `platform`;
|
||||
8. `isUnauthorized` 仅把 HTTP/业务 401 视为登录失效,403 为普通错误。
|
||||
|
||||
- [ ] **Step 2: 运行模块测试观察 RED**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/app-promotion-pages.test.js
|
||||
```
|
||||
|
||||
Expected: FAIL,提示 `public/js/app-promotion-pages.js` 不存在。
|
||||
|
||||
- [ ] **Step 3: 实现 UMD 模块的纯函数**
|
||||
|
||||
实现稳定 ID:
|
||||
|
||||
```js
|
||||
function normalizeId(value) {
|
||||
if (typeof value === 'number' && !Number.isSafeInteger(value)) return '';
|
||||
var result = String(value == null ? '' : value).trim();
|
||||
return /^[1-9][0-9]*$/.test(result) ? result : '';
|
||||
}
|
||||
```
|
||||
|
||||
实现安全 URL:
|
||||
|
||||
```js
|
||||
function normalizeTargetUrl(value) {
|
||||
var text = String(value == null ? '' : value).trim();
|
||||
var parsed;
|
||||
if (!text) return '';
|
||||
try {
|
||||
parsed = new URL(text);
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
return parsed.protocol === 'http:' || parsed.protocol === 'https:' ? parsed.href : '';
|
||||
}
|
||||
```
|
||||
|
||||
`normalizePromotion` 必须要求稳定 `promotionId`、非空 `promotionTitle` 和 `status === '0'`,然后只返回 `PromotionView`。`normalizePromotionList` 必须拒绝非直接数组以及包含任一非法元素的数组。
|
||||
|
||||
- [ ] **Step 4: 实现渲染和读取函数**
|
||||
|
||||
有链接时结构:
|
||||
|
||||
```html
|
||||
<a class="promotion-card" data-promotion-id="..." href="..." target="_blank" rel="noopener noreferrer">
|
||||
<div><h3>...</h3><p>...</p><span>了解详情</span></div>
|
||||
</a>
|
||||
```
|
||||
|
||||
无链接时使用 `<article class="promotion-card">`,不输出 `href`、`target` 或 `rel`。空列表固定返回:
|
||||
|
||||
```html
|
||||
<div class="api-empty">当前暂无应用推广</div>
|
||||
```
|
||||
|
||||
未登录固定返回:
|
||||
|
||||
```html
|
||||
<div class="api-empty">登录后可查看应用推广。<a href="login.html">去登录</a></div>
|
||||
```
|
||||
|
||||
`loadPromotions(api)` 必须执行 `await api.promotions()`,并通过“原数组长度等于规范化后数组长度”确认响应完整。
|
||||
|
||||
- [ ] **Step 5: 实现区域级初始化**
|
||||
|
||||
`init()` 只在 `[data-promotion-page]` 和 `[data-promotion-list]` 同时存在时运行:
|
||||
|
||||
```js
|
||||
if (!api || !api.getToken || !api.getToken()) {
|
||||
list.innerHTML = renderPromotionLoginRequired();
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
有 token 时加载一次。401 调用 `api.clearToken()` 并渲染登录入口;403、网络失败或非法响应渲染“应用推广读取失败,请稍后重试”。不得修改 `root.location`。
|
||||
|
||||
- [ ] **Step 6: 运行模块测试观察 GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/app-promotion-pages.test.js tests/api-client-contract.test.js
|
||||
```
|
||||
|
||||
Expected: 全部通过。
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 开放应用下载页推广区域
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/app-promotion-pages.test.js`
|
||||
- Modify: `tests/pc-scope.test.js`
|
||||
- Modify: `app.html`
|
||||
- Modify only if required by rendered markup: `public/css/app.css`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `window.AppPromotionPages.init()` 的 DOMContentLoaded 自动初始化。
|
||||
- Produces: `app.html` 的真实推广列表入口。
|
||||
|
||||
- [ ] **Step 1: 写页面开放状态失败测试**
|
||||
|
||||
在 `tests/app-promotion-pages.test.js` 读取 `app.html`,断言:
|
||||
|
||||
```js
|
||||
assert.match(page, /data-promotion-page/);
|
||||
assert.match(page, /data-promotion-list/);
|
||||
assert.match(page, /public\/js\/app-promotion-pages\.js/);
|
||||
assert.doesNotMatch(page, /public\/js\/promotion-pages\.js/);
|
||||
assert.doesNotMatch(page, /name="(?:promotionId|coverOssId|platform)"/);
|
||||
```
|
||||
|
||||
在 `tests/pc-scope.test.js` 保留旧 `promotion-pages.js` deny-list,并新增 `app.html` 必须加载 `app-promotion-pages.js` 的断言。
|
||||
|
||||
- [ ] **Step 2: 运行页面测试观察 RED**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/app-promotion-pages.test.js tests/pc-scope.test.js
|
||||
```
|
||||
|
||||
Expected: FAIL,提示 `app.html` 尚未加载新脚本。
|
||||
|
||||
- [ ] **Step 3: 修改页面加载脚本**
|
||||
|
||||
在 `page-effects.js` 后加载:
|
||||
|
||||
```html
|
||||
<script src="public/js/app-promotion-pages.js"></script>
|
||||
```
|
||||
|
||||
保留现有 `data-promotion-page`、`data-promotion-list` 和初始加载文案。不得添加筛选器、刷新按钮、示例推广或隐藏 ID 输入。
|
||||
|
||||
- [ ] **Step 4: 仅在需要时补充无图片卡片样式**
|
||||
|
||||
如果 `<a class="promotion-card">` 不能继承卡片文字颜色与块级点击区域,只增加:
|
||||
|
||||
```css
|
||||
.promotion-card {
|
||||
display: block;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
```
|
||||
|
||||
不得调整首页广告样式或重做应用下载页布局。
|
||||
|
||||
- [ ] **Step 5: 运行页面与模块组合测试观察 GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/app-promotion-pages.test.js tests/api-client-contract.test.js tests/pc-scope.test.js tests/public-static-pages.test.js
|
||||
```
|
||||
|
||||
Expected: 全部通过。
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 更新规划并完成验证
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/PC接口对接规划.md`
|
||||
- Modify: `docs/superpowers/plans/2026-07-29-app-promotions.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: 已实现的 ApiClient、模块、页面和测试证据。
|
||||
- Produces: 阶段 7 第七批契约记录和验收报告。
|
||||
|
||||
- [ ] **Step 1: 在规划中新增推广字段表**
|
||||
|
||||
记录:
|
||||
|
||||
- method/path/内容推广目录;
|
||||
- `platform` 为可选 Query、当前页面省略;
|
||||
- Authorization 与 clientid 为 A;
|
||||
- Body 为空;
|
||||
- `promotionId`、`promotionKey`、`coverOssId`、`platform`、`sortOrder`、`status`、`remark` 为 I;
|
||||
- `promotionTitle`、`promotionDesc`、安全 `targetUrl` 为 R;
|
||||
- YAML `security: []` / 未导出 Query 与后端实际鉴权 / `platform` 的冲突;
|
||||
- 页面初始化、未登录、401、403、空数组、非法响应和真实列表时机。
|
||||
|
||||
- [ ] **Step 2: 运行专项测试**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/app-promotion-pages.test.js tests/api-client-contract.test.js tests/pc-scope.test.js tests/public-static-pages.test.js
|
||||
```
|
||||
|
||||
Expected: 0 failures。
|
||||
|
||||
- [ ] **Step 3: 运行语法和差异检查**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --check public/js/app-promotion-pages.js
|
||||
node --check utils/ApiClient.js
|
||||
git -c safe.directory=D:/WorkSpace/Web/jiapu diff --check
|
||||
```
|
||||
|
||||
Expected: 两个语法检查 exit 0;`diff --check` 无错误,CRLF warning 可记录但不算失败。
|
||||
|
||||
- [ ] **Step 4: 运行全量测试**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
npm test
|
||||
```
|
||||
|
||||
Expected: 0 failures。
|
||||
|
||||
- [ ] **Step 5: 浏览器验证真实分支**
|
||||
|
||||
只读验证:
|
||||
|
||||
1. 未登录打开 `app.html`,确认下载内容保留、推广区域显示登录入口、未发生推广请求;
|
||||
2. 使用已授权测试账号登录后打开 `app.html`;
|
||||
3. 后端返回空数组时显示“当前暂无应用推广”;有数据时检查标题、说明、安全外链和内部字段隐藏;
|
||||
4. 检查页面控制台;
|
||||
5. 不点击外部推广链接,不创建或修改后端数据;
|
||||
6. 浏览器控制不稳定时停止自动关闭标签页,只报告已取得的验证结果并清理本地临时服务。
|
||||
|
||||
- [ ] **Step 6: 标记计划状态并按阶段格式汇报**
|
||||
|
||||
报告必须包含:
|
||||
|
||||
```text
|
||||
Changed: ApiClient、推广模块、app.html 和规划新增内容。
|
||||
Verified: RED/GREEN、专项、全量和浏览器覆盖数量。
|
||||
Conflicts: YAML 匿名/无 Query 与后端真实鉴权/platform 的差异。
|
||||
Blocked: 缺少封面 URL、platform 枚举或真实推广数据时的剩余联调项。
|
||||
Next: 官网内容或公开家谱接口的下一最小批次。
|
||||
```
|
||||
@@ -1,376 +0,0 @@
|
||||
# 家谱主页 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:** 将 `profile-family-home.html` 从静态预览开放为真实 PC 家谱主页,展示当前家谱概览和世系树预览,并保留已接入业务页入口。
|
||||
|
||||
**Architecture:** `profile-common.js` 继续唯一拥有当前 `genealogyId`;页面只调用已有 `genealogyOverview(genealogyId)` 和 `lineageTree(genealogyId)`。`lineage-pages.js` 继续唯一拥有世系树节点规范化与 HTML 生成,新建 `family-home-pages.js` 只负责概览 DTO、权限显隐、并行读取和页面状态。
|
||||
|
||||
**Tech Stack:** 静态 HTML、原生 JavaScript UMD、Axios、Node `node:test`。
|
||||
|
||||
**Status:** 2026-07-29 已按 Task 1–4 实施;自动化与无家谱真实账号分支已验证,有家谱数据态等待具备真实家谱的账号复核。
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- 后端 `D:\WorkSpace\Java\Genealogy` 全程只读。
|
||||
- 只使用 PC 接口;不得调用 `/genealogy/dashboard/overview`、APP 或管理后台接口。
|
||||
- `genealogyId` 只能来自 `ProfileUI.getGenealogyId()`,始终按字符串处理。
|
||||
- `GET /genealogy/pc/genealogies/{genealogyId}/overview` 返回 `AppGenealogyVo`,它不是内容统计接口;不得制造文章、相册、视频或活动数量。
|
||||
- 世系树只读调用 `GET /genealogy/pc/genealogies/{genealogyId}/lineage/tree`。
|
||||
- 家谱管理入口仅在响应 `canManage=true` 时显示;内容页自身继续负责更细权限。
|
||||
- PC 没有成员邀请创建/分享接口;主页不得开放“邀请家人”操作。
|
||||
- 本批次没有写接口,不创建、修改或删除任何真实业务数据。
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 家谱主页 DTO 与页面契约
|
||||
|
||||
**Files:**
|
||||
- Create: `public/js/family-home-pages.js`
|
||||
- Create: `tests/family-home-pages.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `ProfileUI.getGenealogyId()`、`GenealogyApi.defaultClient`
|
||||
- Produces:
|
||||
- `normalizeFamilyOverview(item, expectedGenealogyId)`
|
||||
- `renderFamilyOverview(overview)`
|
||||
- `shouldShowFamilyManagement(overview)`
|
||||
- `shouldRedirectToLogin(api, error)`
|
||||
|
||||
- [ ] **Step 1: 写失败数据边界测试**
|
||||
|
||||
```js
|
||||
const overview = FamilyHomePages.normalizeFamilyOverview({
|
||||
genealogyId: '2062179707935264769',
|
||||
genealogyNo: 'G20260729001',
|
||||
genealogyName: '叶氏家谱',
|
||||
surname: '叶',
|
||||
ancestralHall: '南阳堂',
|
||||
originPlace: '四川成都',
|
||||
regionFullName: '四川省 成都市',
|
||||
memberCount: 12,
|
||||
personCount: 36,
|
||||
status: '0',
|
||||
canManage: true,
|
||||
canEditContent: true,
|
||||
ownerUserId: 'must-not-render',
|
||||
coverOssId: 'must-not-render'
|
||||
}, '2062179707935264769');
|
||||
|
||||
assert.deepEqual(overview, {
|
||||
genealogyId: '2062179707935264769',
|
||||
genealogyNo: 'G20260729001',
|
||||
genealogyName: '叶氏家谱',
|
||||
surname: '叶',
|
||||
ancestralHall: '南阳堂',
|
||||
originPlace: '四川成都',
|
||||
regionFullName: '四川省 成都市',
|
||||
memberCount: 12,
|
||||
personCount: 36,
|
||||
canManage: true,
|
||||
canEditContent: true
|
||||
});
|
||||
assert.equal(
|
||||
FamilyHomePages.normalizeFamilyOverview(
|
||||
{ genealogyId: Number.MAX_SAFE_INTEGER + 1, genealogyName: '非法', status: '0' },
|
||||
'2062179707935264769'
|
||||
),
|
||||
null
|
||||
);
|
||||
assert.equal(
|
||||
FamilyHomePages.normalizeFamilyOverview(
|
||||
{ genealogyId: '2', genealogyName: '串谱', status: '0' },
|
||||
'2062179707935264769'
|
||||
),
|
||||
null
|
||||
);
|
||||
```
|
||||
|
||||
同时断言:
|
||||
|
||||
- `genealogyName` 为空、`status!='0'`、负数/非安全计数均拒绝;
|
||||
- 渲染转义全部文本,不出现 `ownerUserId`、`coverOssId` 或原始 JSON;
|
||||
- 只有布尔值 `canManage===true` 才开放管理入口;
|
||||
- 401 清理登录态,403 不清理登录态。
|
||||
|
||||
- [ ] **Step 2: 运行 RED**
|
||||
|
||||
Run: `node --test tests/family-home-pages.test.js`
|
||||
|
||||
Expected: FAIL,`family-home-pages.js` 不存在。
|
||||
|
||||
- [ ] **Step 3: 最小实现概览规范化**
|
||||
|
||||
```js
|
||||
function normalizeFamilyOverview(item, expectedGenealogyId) {
|
||||
var source = item || {};
|
||||
var genealogyId = normalizeId(source.genealogyId);
|
||||
var expectedId = normalizeId(expectedGenealogyId);
|
||||
var memberCount = normalizeCount(source.memberCount);
|
||||
var personCount = normalizeCount(source.personCount);
|
||||
|
||||
if (!genealogyId || genealogyId !== expectedId ||
|
||||
!text(source.genealogyName) || String(source.status) !== '0' ||
|
||||
memberCount === null || personCount === null) return null;
|
||||
return {
|
||||
genealogyId: genealogyId,
|
||||
genealogyNo: text(source.genealogyNo),
|
||||
genealogyName: text(source.genealogyName),
|
||||
surname: text(source.surname),
|
||||
ancestralHall: text(source.ancestralHall),
|
||||
originPlace: text(source.originPlace),
|
||||
regionFullName: text(source.regionFullName),
|
||||
memberCount: memberCount,
|
||||
personCount: personCount,
|
||||
canManage: source.canManage === true,
|
||||
canEditContent: source.canEditContent === true
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 运行 GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/family-home-pages.test.js
|
||||
node --check public/js/family-home-pages.js
|
||||
```
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 复用世系树唯一渲染 owner
|
||||
|
||||
**Files:**
|
||||
- Modify: `public/js/lineage-pages.js`
|
||||
- Modify: `tests/lineage-pages.test.js`
|
||||
- Modify: `tests/family-home-pages.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `LineagePages.renderLineageTreeHtml(data)`
|
||||
- Consumes: 现有 `normalizeLineagePerson(item)`、`renderTreeNode(item, ancestry)`
|
||||
|
||||
- [ ] **Step 1: 写失败共享渲染测试**
|
||||
|
||||
```js
|
||||
const tree = [{
|
||||
personId: '2062179707935264770',
|
||||
genealogyId: '2062179707935264769',
|
||||
name: '<始祖>',
|
||||
status: '0',
|
||||
spouses: [],
|
||||
children: []
|
||||
}];
|
||||
|
||||
const html = LineagePages.renderLineageTreeHtml(tree);
|
||||
assert.match(html, /<始祖>/);
|
||||
assert.match(html, /data-lineage-person="2062179707935264770"/);
|
||||
assert.doesNotMatch(html, /2062179707935264769/);
|
||||
assert.match(LineagePages.renderLineageTreeHtml([]), /暂无世系树/);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 运行 RED**
|
||||
|
||||
Run: `node --test tests/lineage-pages.test.js tests/family-home-pages.test.js`
|
||||
|
||||
Expected: FAIL,`renderLineageTreeHtml` 未导出。
|
||||
|
||||
- [ ] **Step 3: 从现有 `renderTree` 提取纯 HTML owner**
|
||||
|
||||
```js
|
||||
function renderLineageTreeHtml(data) {
|
||||
var nodes = normalizeList(data)
|
||||
.map(function (item) { return renderTreeNode(item, {}); })
|
||||
.filter(Boolean);
|
||||
|
||||
return nodes.length
|
||||
? '<ul class="lineage-tree">' + nodes.join('') + '</ul>'
|
||||
: '<div class="api-empty">暂无世系树</div>';
|
||||
}
|
||||
|
||||
function renderTree(data) {
|
||||
var container = query('[data-lineage-tree]');
|
||||
if (container) container.innerHTML = renderLineageTreeHtml(data);
|
||||
}
|
||||
```
|
||||
|
||||
在 UMD 导出对象加入:
|
||||
|
||||
```js
|
||||
renderLineageTreeHtml: renderLineageTreeHtml
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 运行 GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/lineage-pages.test.js tests/family-home-pages.test.js
|
||||
node --check public/js/lineage-pages.js
|
||||
```
|
||||
|
||||
Expected: PASS,现有世系管理页输出不变。
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 开放真实家谱主页
|
||||
|
||||
**Files:**
|
||||
- Modify: `profile-family-home.html`
|
||||
- Modify: `public/js/family-home-pages.js`
|
||||
- Modify: `tests/family-home-pages.test.js`
|
||||
- Modify: `tests/pending-pages.test.js`
|
||||
- Modify: `tests/stage6-navigation.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes:
|
||||
- `api.genealogyOverview(genealogyId)`
|
||||
- `api.lineageTree(genealogyId)`
|
||||
- `LineagePages.renderLineageTreeHtml(data)`
|
||||
- Produces: `initFamilyHomePage()`、`init()`
|
||||
|
||||
- [ ] **Step 1: 写失败页面测试**
|
||||
|
||||
断言:
|
||||
|
||||
- `profile-family-home.html` 不再包含 `data-feature-status="pending"` 或 `pending-pages.js`;
|
||||
- 加载顺序为 `profile-common.js`、`lineage-pages.js`、`family-home-pages.js`;
|
||||
- 标题、摘要、计数、管理入口、世系预览均有稳定 `data-*` hook;
|
||||
- 硬编码“四川武胜汤氏族”被删除;
|
||||
- “邀请家人”不再是可点击业务入口,并明确提示“PC 暂未开放邀请”;
|
||||
- 谱文、相册、视频、功德、祭祀、世系、动态入口继续携带 `data-genealogy-context-link`;
|
||||
- 初始化只并行调用:
|
||||
|
||||
```js
|
||||
Promise.all([
|
||||
api.genealogyOverview(genealogyId),
|
||||
api.lineageTree(genealogyId)
|
||||
])
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 运行 RED**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/family-home-pages.test.js tests/pending-pages.test.js tests/stage6-navigation.test.js
|
||||
```
|
||||
|
||||
Expected: FAIL,主页仍为 pending 且包含硬编码家谱。
|
||||
|
||||
- [ ] **Step 3: 实现只读初始化流程**
|
||||
|
||||
```js
|
||||
async function initFamilyHomePage() {
|
||||
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
|
||||
var genealogyId = root.ProfileUI && root.ProfileUI.getGenealogyId();
|
||||
var results;
|
||||
var overview;
|
||||
|
||||
if (!genealogyId) {
|
||||
root.location.replace('profile-families.html?next=profile-family-home.html');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
results = await Promise.all([
|
||||
api.genealogyOverview(genealogyId),
|
||||
api.lineageTree(genealogyId)
|
||||
]);
|
||||
overview = normalizeFamilyOverview(results[0], genealogyId);
|
||||
if (!overview) throw new Error('家谱概览响应无效');
|
||||
renderFamilyOverview(overview);
|
||||
renderManagementAccess(overview.canManage);
|
||||
query('[data-lineage-home-tree]').innerHTML =
|
||||
root.LineagePages.renderLineageTreeHtml(results[1]);
|
||||
} catch (error) {
|
||||
if (shouldRedirectToLogin(api, error)) return redirectToLogin(api);
|
||||
renderFamilyHomeError(error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
页面只展示:
|
||||
|
||||
- 家谱名称、编号、姓氏、堂号、祖籍/地区;
|
||||
- `memberCount`、`personCount`;
|
||||
- 真实世系树及进入完整世系页的链接;
|
||||
- 后端已经接入的内容模块入口。
|
||||
|
||||
- [ ] **Step 4: 运行 GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/family-home-pages.test.js tests/pending-pages.test.js tests/stage6-navigation.test.js
|
||||
node --check public/js/family-home-pages.js
|
||||
```
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 规划记录与完整验收
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/PC接口对接规划.md`
|
||||
- Modify: `docs/superpowers/plans/2026-07-29-family-home.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1–3 的只读家谱主页闭环。
|
||||
|
||||
- [ ] **Step 1: 更新规划字段表**
|
||||
|
||||
新增家谱主页小节,逐字段记录:
|
||||
|
||||
| 字段 | 分类 | 页面用途 |
|
||||
| --- | --- | --- |
|
||||
| `genealogyId` | I/A | 当前上下文、两条请求 path、响应一致性校验 |
|
||||
| `genealogyNo`、`genealogyName`、`surname` | R | 标题和基础信息 |
|
||||
| `ancestralHall`、`originPlace`、`regionFullName` | R | 非空时展示 |
|
||||
| `memberCount`、`personCount` | R | 非负只读计数 |
|
||||
| `canManage`、`canEditContent` | I | 权限显隐,不作为用户输入 |
|
||||
| `ownerUserId`、`coverOssId` | I | 当前主页不展示、不手填 |
|
||||
| `LineagePersonTreeView.spouses/children` | R | 递归世系预览 |
|
||||
|
||||
同时记录:
|
||||
|
||||
- `/overview` 实际是详情别名,不包含内容聚合统计;
|
||||
- `/genealogy/dashboard/overview` 是后台权限接口,不进入 PC 前端;
|
||||
- 成员邀请没有 PC 接口,继续阻断。
|
||||
|
||||
- [ ] **Step 2: 聚焦验证**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/family-home-pages.test.js tests/lineage-pages.test.js tests/pending-pages.test.js tests/stage6-navigation.test.js tests/api-client-contract.test.js
|
||||
node --check public/js/family-home-pages.js
|
||||
node --check public/js/lineage-pages.js
|
||||
```
|
||||
|
||||
Expected: 全部 PASS。
|
||||
|
||||
- [ ] **Step 3: 全量和差异验证**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
npm test
|
||||
git -c safe.directory=D:/WorkSpace/Web/jiapu diff --check
|
||||
```
|
||||
|
||||
Expected: 0 failed,差异检查无错误。
|
||||
|
||||
- [ ] **Step 4: 浏览器真实只读验证**
|
||||
|
||||
使用真实登录态验证:
|
||||
|
||||
1. 无家谱上下文时只跳转选择页,不发家谱业务请求;
|
||||
2. 有真实家谱时标题、概览计数和世系树来自 PC 响应;
|
||||
3. 普通成员看不到管理按钮,管理者可见;
|
||||
4. 所有入口透传同一 `genealogyId`;
|
||||
5. 空世系、403、404、网络错误都有明确页面状态;
|
||||
6. 控制台无错误;
|
||||
7. 不创建或修改任何真实数据。
|
||||
@@ -1,310 +0,0 @@
|
||||
# 反馈与工单 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` 唯一拥有 `/genealogy/pc/feedback` 的 GET/POST 契约;新增 UMD 模块 `feedback-pages.js` 负责 DTO 构造、完整 VO 规范化、列表/详情渲染和四类页面初始化。“工单”只是反馈记录的帮助中心展示名称,详情通过 URL 中由列表响应产生的 `feedbackId` 在我的反馈列表中精确匹配。
|
||||
|
||||
**Tech Stack:** 静态 HTML、原生 JavaScript UMD、Axios、Node `node:test`。
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- 后端 `D:\WorkSpace\Java\Genealogy` 全程只读。
|
||||
- 只调用 `POST /genealogy/pc/feedback` 和 `GET /genealogy/pc/feedback`。
|
||||
- 请求只发送 `feedbackType`、`feedbackContent`、`contactInfo`;不发送 `feedbackTitle`。
|
||||
- `feedbackType` 只允许后端已确认字典值 `advice/bug/complaint/other`;空值省略并由后端默认 `advice`。
|
||||
- `feedbackId` 只能来自提交或列表响应,不提供文本输入。
|
||||
- 页面不展示 `appUserId`、`handlerId`、`appUserPhone`、原始 JSON。
|
||||
- 所有提交防重复;提交成功后必须重读列表并匹配同一 `feedbackId`。
|
||||
|
||||
---
|
||||
|
||||
### Task 1: ApiClient 反馈契约
|
||||
|
||||
**Files:**
|
||||
- Modify: `utils/ApiClient.js`
|
||||
- Modify: `tests/api-client-contract.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `submitFeedback(body)`、`myFeedback()`
|
||||
|
||||
- [ ] **Step 1: 写失败的契约测试**
|
||||
|
||||
增加测试,使用完整请求字面量断言:
|
||||
|
||||
```js
|
||||
await client.submitFeedback({
|
||||
feedbackType: 'bug',
|
||||
feedbackContent: '页面按钮无响应',
|
||||
contactInfo: '<测试联系电话>',
|
||||
feedbackTitle: 'must-drop',
|
||||
appUserId: 'must-drop'
|
||||
});
|
||||
await client.myFeedback();
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
{
|
||||
method: 'POST',
|
||||
url: '/genealogy/pc/feedback',
|
||||
body: {
|
||||
feedbackType: 'bug',
|
||||
feedbackContent: '页面按钮无响应',
|
||||
contactInfo: '<测试联系电话>'
|
||||
}
|
||||
},
|
||||
{ method: 'GET', url: '/genealogy/pc/feedback' }
|
||||
]);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 运行 RED**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/api-client-contract.test.js
|
||||
```
|
||||
|
||||
Expected: FAIL,`submitFeedback` 或 `myFeedback` 不存在。
|
||||
|
||||
- [ ] **Step 3: 最小实现**
|
||||
|
||||
在 `createClient()` 返回对象中增加:
|
||||
|
||||
```js
|
||||
submitFeedback: function (body) {
|
||||
return request('POST', '/genealogy/pc/feedback', {
|
||||
body: pickDefined(body, ['feedbackType', 'feedbackContent', 'contactInfo'])
|
||||
});
|
||||
},
|
||||
myFeedback: function () {
|
||||
return request('GET', '/genealogy/pc/feedback');
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 运行 GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/api-client-contract.test.js
|
||||
```
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 反馈 DTO、VO、列表和详情边界
|
||||
|
||||
**Files:**
|
||||
- Create: `public/js/feedback-pages.js`
|
||||
- Create: `tests/feedback-pages.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
- `getFeedbackId(search)`
|
||||
- `buildFeedbackBody(values)`
|
||||
- `validateFeedbackBody(body)`
|
||||
- `normalizeFeedback(item)`
|
||||
- `normalizeFeedbackList(data)`
|
||||
- `findFeedbackById(data, feedbackId)`
|
||||
- `buildFeedbackDetailUrl(feedbackId)`
|
||||
- `renderFeedbackList(data, options)`
|
||||
- `renderFeedbackDetail(item)`
|
||||
|
||||
- [ ] **Step 1: 写失败的纯行为测试**
|
||||
|
||||
用完整 `FeedbackVo` 字面量验证:
|
||||
|
||||
```js
|
||||
assert.deepEqual(FeedbackPages.buildFeedbackBody({
|
||||
feedbackType: ' bug ',
|
||||
feedbackContent: ' 页面按钮无响应 ',
|
||||
contactInfo: ' <测试联系电话> ',
|
||||
feedbackTitle: 'must-drop',
|
||||
appUserId: 'must-drop'
|
||||
}), {
|
||||
feedbackType: 'bug',
|
||||
feedbackContent: '页面按钮无响应',
|
||||
contactInfo: '<测试联系电话>'
|
||||
});
|
||||
```
|
||||
|
||||
同时断言:
|
||||
|
||||
- `feedbackContent` 为空时报错;
|
||||
- 类型只允许 `advice/bug/complaint/other`,空值省略;
|
||||
- 不安全 number ID、空内容、`handleStatus` 非 `0/1/2/3`、`status` 非 `0/1` 的响应拒绝;
|
||||
- 数组包含一个非法元素时整批返回空数组;
|
||||
- 详情只精确匹配同一字符串 `feedbackId`,不存在时不回退第一条;
|
||||
- 列表和详情转义可见文本,不出现内部用户/处理人 ID、账号手机号或原始 JSON;
|
||||
- 详情 URL 使用 `ticket-detail.html?feedbackId=...`。
|
||||
|
||||
- [ ] **Step 2: 运行 RED**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/feedback-pages.test.js
|
||||
```
|
||||
|
||||
Expected: FAIL,模块不存在。
|
||||
|
||||
- [ ] **Step 3: 最小实现纯函数**
|
||||
|
||||
`normalizeFeedback()` 返回且只返回:
|
||||
|
||||
```js
|
||||
{
|
||||
feedbackId,
|
||||
feedbackType,
|
||||
feedbackContent,
|
||||
contactInfo,
|
||||
handleStatus,
|
||||
handleResult,
|
||||
handleTime,
|
||||
status,
|
||||
remark
|
||||
}
|
||||
```
|
||||
|
||||
状态展示固定为:
|
||||
|
||||
```js
|
||||
{ '0': '待处理', '1': '处理中', '2': '已处理', '3': '已关闭' }
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 运行 GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/feedback-pages.test.js
|
||||
node --check public/js/feedback-pages.js
|
||||
```
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 四个页面的真实反馈闭环
|
||||
|
||||
**Files:**
|
||||
- Modify: `profile-feedback.html`
|
||||
- Modify: `submit-ticket.html`
|
||||
- Modify: `my-tickets.html`
|
||||
- Modify: `ticket-detail.html`
|
||||
- Modify: `public/js/feedback-pages.js`
|
||||
- Modify: `tests/feedback-pages.test.js`
|
||||
- Modify: `tests/pending-pages.test.js`
|
||||
- Modify: `tests/public-static-pages.test.js`
|
||||
- Modify: `tests/pc-scope.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `initFeedbackFormPage()`、`initFeedbackListPage()`、`initFeedbackDetailPage()`、`init()`
|
||||
|
||||
- [ ] **Step 1: 写失败的页面行为测试**
|
||||
|
||||
断言四个页面:
|
||||
|
||||
- 不再包含 `data-feature-status="pending"` 或 `pending-pages.js`;
|
||||
- 均加载 `feedback-pages.js` 和 ApiClient 依赖;
|
||||
- 两个提交页只提供 `feedbackType/feedbackContent/contactInfo`,不存在 `feedbackTitle` 或任意业务 ID 输入;
|
||||
- 列表页有 `data-feedback-list` 和刷新按钮;
|
||||
- 详情页只有 `data-feedback-detail`,不提供回复、追问、删除或关闭操作。
|
||||
|
||||
断言脚本提交后调用 `myFeedback()`,必须按提交响应的 `feedbackId` 重读匹配;详情按 URL `feedbackId` 精确匹配。
|
||||
|
||||
- [ ] **Step 2: 运行 RED**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/feedback-pages.test.js tests/pending-pages.test.js tests/public-static-pages.test.js tests/pc-scope.test.js
|
||||
```
|
||||
|
||||
Expected: FAIL,四个页面仍为 pending。
|
||||
|
||||
- [ ] **Step 3: 实现页面初始化**
|
||||
|
||||
提交页:
|
||||
|
||||
1. 构造并校验请求;
|
||||
2. `writePending` 锁和按钮禁用;
|
||||
3. 调用 `submitFeedback(body)`;
|
||||
4. 规范化提交响应;
|
||||
5. 重读 `myFeedback()`,精确找到同一 `feedbackId`;
|
||||
6. 个人反馈页刷新历史列表;工单页进入 `ticket-detail.html?feedbackId=...`。
|
||||
|
||||
列表页读取 `myFeedback()` 并生成详情链接。详情页从 URL 读取 ID、重读列表并精确匹配;未匹配显示“反馈记录不存在或无权查看”。
|
||||
|
||||
- [ ] **Step 4: 运行 GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/feedback-pages.test.js tests/pending-pages.test.js tests/public-static-pages.test.js tests/pc-scope.test.js
|
||||
node --check public/js/feedback-pages.js
|
||||
```
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 导航、规划和收尾验证
|
||||
|
||||
**Files:**
|
||||
- Modify: `help.html`
|
||||
- Modify: `profile-services.html`
|
||||
- Modify: `docs/PC接口对接规划.md`
|
||||
- Modify: `tests/stage6-navigation.test.js`
|
||||
- Modify: `docs/superpowers/plans/2026-07-29-feedback-tickets.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1–3 的真实反馈闭环。
|
||||
|
||||
- [ ] **Step 1: 写失败的导航测试**
|
||||
|
||||
断言:
|
||||
|
||||
- 帮助中心可直接进入提交工单和我的工单;
|
||||
- 服务中心可进入意见反馈;
|
||||
- 所有入口不再被 pending 状态拦截;
|
||||
- 不存在独立 ticket API、反馈标题或手填 `feedbackId` 的入口。
|
||||
|
||||
- [ ] **Step 2: 运行 RED**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/stage6-navigation.test.js tests/public-static-pages.test.js
|
||||
```
|
||||
|
||||
Expected: FAIL,帮助中心尚未开放我的工单入口或旧 pending 断言仍存在。
|
||||
|
||||
- [ ] **Step 3: 更新入口和规划**
|
||||
|
||||
规划记录:
|
||||
|
||||
- `AppFeedbackBody` 三个字段来源与提交时机;
|
||||
- `FeedbackVo` 可见字段、内部隐藏字段和四种处理状态;
|
||||
- 无独立工单 path,四个页面共用反馈记录;
|
||||
- 提交后重读、详情精确匹配、401/403 和隐私边界;
|
||||
- 后端没有用户侧回复、追问、关闭或删除接口,继续阻断。
|
||||
|
||||
- [ ] **Step 4: 聚焦和全量验证**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/feedback-pages.test.js tests/api-client-contract.test.js tests/pending-pages.test.js tests/public-static-pages.test.js tests/pc-scope.test.js tests/stage6-navigation.test.js
|
||||
npm test
|
||||
node --check public/js/feedback-pages.js
|
||||
git -c safe.directory=D:/WorkSpace/Web/jiapu diff --check
|
||||
```
|
||||
|
||||
Expected: 全部 PASS。
|
||||
|
||||
- [ ] **Step 5: 浏览器只读验证**
|
||||
|
||||
使用真实登录态验证四个页面、我的反馈列表、无记录详情、隐私和控制台错误。未经用户本轮明确授权,不提交真实反馈。
|
||||
@@ -1,258 +0,0 @@
|
||||
# 家谱创建首批 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 请求契约,并把 `profile-create-family.html` 从 pending 预览改成可用的真实创建家谱页面。
|
||||
|
||||
**Architecture:** `utils/ApiClient.js` 唯一拥有请求路径和 body 白名单;`public/js/genealogy-entry-pages.js` 扩展家谱创建的纯构造、校验、响应规范化和页面初始化;现有 `region-pages.js` 负责三级地区选项,`upload-pages.js` 负责生成 `coverOssId`。创建成功后只使用响应中的稳定 `genealogyId` 跳转到家谱主页。
|
||||
|
||||
**Tech Stack:** 静态 HTML、原生 JavaScript UMD、Axios、Node `node:test`。
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- 后端 `D:\WorkSpace\Java\Genealogy` 全程只读。
|
||||
- 只调用 `/genealogy/pc/**`,不调用 APP 或后台管理接口。
|
||||
- 不允许用户手填家谱 ID、申请 ID、用户 ID 或 OSS ID。
|
||||
- 每个生产行为必须先有失败测试并实际运行 RED。
|
||||
- 每个阶段完成后运行聚焦测试并更新 `docs/PC接口对接规划.md`。
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 冻结家谱生命周期 ApiClient 契约
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/api-client-contract.test.js`
|
||||
- Modify: `utils/ApiClient.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
- `createGenealogy(body)`
|
||||
- `applyToGenealogy(genealogyId, body)`
|
||||
- `myGenealogyJoinApplies()`
|
||||
- `pendingGenealogyJoinApplies(genealogyId)`
|
||||
- `auditGenealogyJoinApply(genealogyId, applyId, body)`
|
||||
- `cancelGenealogyJoinApply(applyId)`
|
||||
|
||||
- [x] **Step 1: 写失败的路径和 body 白名单测试**
|
||||
|
||||
测试固定以下请求:
|
||||
|
||||
```js
|
||||
await client.createGenealogy({
|
||||
genealogyName: '汤氏家谱',
|
||||
surname: '汤',
|
||||
regionCode: '511622',
|
||||
coverOssId: '2062179707935264769',
|
||||
legacyField: 'drop'
|
||||
});
|
||||
|
||||
await client.applyToGenealogy(genealogyId, {
|
||||
applicantName: '申请人',
|
||||
phone: '<测试联系电话>',
|
||||
relationDesc: '族亲',
|
||||
applyReason: '申请加入',
|
||||
inviterUserId: 'drop-without-safe-source'
|
||||
});
|
||||
|
||||
await client.auditGenealogyJoinApply(genealogyId, applyId, {
|
||||
status: '1',
|
||||
auditRemark: '资料一致',
|
||||
legacyField: 'drop'
|
||||
});
|
||||
```
|
||||
|
||||
断言:
|
||||
|
||||
- 创建请求为 `POST /genealogy/pc/genealogies`,只保留 `genealogyName/surname/regionCode/ancestralHall/originPlace/addressDetail/coverOssId/intro/visibility/joinMode`。
|
||||
- 加入请求为 `POST /genealogy/pc/genealogies/{genealogyId}/join-applies`,本轮安全 PC 页面只保留 `applicantName/phone/relationDesc/applyReason`,不发送没有安全来源的 `inviterUserId`。
|
||||
- 我的申请、待审核、审核和撤销使用后端 PC Controller 的完整路径。
|
||||
- 审核 body 只保留 `status/auditRemark`。
|
||||
- 所有 path ID 拒绝不安全数字。
|
||||
|
||||
- [x] **Step 2: 运行 RED**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test --test-name-pattern "genealogy lifecycle" tests/api-client-contract.test.js
|
||||
```
|
||||
|
||||
Expected: FAIL,新方法尚不存在。
|
||||
|
||||
- [x] **Step 3: 最小实现方法和白名单**
|
||||
|
||||
使用现有 `request()`、`pickDefined()` 和 `toRequiredPathId()`;不增加兼容别名或 APP fallback。
|
||||
|
||||
- [x] **Step 4: 运行 GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/api-client-contract.test.js
|
||||
node --check utils/ApiClient.js
|
||||
```
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 家谱创建纯行为
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/genealogy-entry-pages.test.js`
|
||||
- Modify: `public/js/genealogy-entry-pages.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `createGenealogy(body)`、`genealogyQuota()`
|
||||
- Produces:
|
||||
- `buildGenealogyCreateBody(values)`
|
||||
- `validateGenealogyCreateBody(body)`
|
||||
- `normalizeCreatedGenealogy(item)`
|
||||
- `buildCreatedGenealogyUrl(item)`
|
||||
|
||||
- [x] **Step 1: 写失败的创建行为测试**
|
||||
|
||||
```js
|
||||
assert.deepEqual(GenealogyEntryPages.buildGenealogyCreateBody({
|
||||
genealogyName: ' 汤氏家谱 ',
|
||||
surname: ' 汤 ',
|
||||
regionCode: '511622',
|
||||
ancestralHall: '',
|
||||
coverOssId: '2062179707935264769',
|
||||
visibility: '1',
|
||||
joinMode: '1',
|
||||
manualId: 'drop'
|
||||
}), {
|
||||
genealogyName: '汤氏家谱',
|
||||
surname: '汤',
|
||||
regionCode: '511622',
|
||||
coverOssId: '2062179707935264769',
|
||||
visibility: '1',
|
||||
joinMode: '1'
|
||||
});
|
||||
```
|
||||
|
||||
同时断言:
|
||||
|
||||
- 缺少谱名、姓氏或地区时返回明确校验错误。
|
||||
- `visibility` 只允许 `0/1/2`,`joinMode` 只允许 `0/1/2`。
|
||||
- `coverOssId` 必须是安全字符串 ID;没有文件可省略。
|
||||
- 额度 `createRemaining <= 0` 时阻止提交。
|
||||
- 创建响应必须有稳定 `genealogyId`、谱名和姓氏。
|
||||
- 跳转地址只能由响应 ID 产生:`profile-family-home.html?genealogyId=...`。
|
||||
|
||||
- [x] **Step 2: 运行 RED**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/genealogy-entry-pages.test.js
|
||||
```
|
||||
|
||||
Expected: FAIL,纯函数不存在。
|
||||
|
||||
- [x] **Step 3: 最小实现纯函数**
|
||||
|
||||
严格构造 `AppGenealogyCreateBody`,可选空值省略,不读取页面外字段。
|
||||
|
||||
- [x] **Step 4: 运行 GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/genealogy-entry-pages.test.js
|
||||
node --check public/js/genealogy-entry-pages.js
|
||||
```
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 开放创建家谱页面
|
||||
|
||||
**Files:**
|
||||
- Modify: `profile-create-family.html`
|
||||
- Modify: `public/js/genealogy-entry-pages.js`
|
||||
- Modify: `tests/genealogy-entry-pages.test.js`
|
||||
- Modify: `tests/pending-pages.test.js`
|
||||
- Modify: `tests/security-upload-scope.test.js`
|
||||
- Modify: `tests/pc-scope.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1 和 Task 2、`RegionPages`、`UploadPages`
|
||||
- Produces: `initGenealogyCreatePage()`
|
||||
|
||||
- [x] **Step 1: 写失败的页面测试**
|
||||
|
||||
断言页面:
|
||||
|
||||
- 不再包含 `data-feature-status="pending"` 和 `pending-pages.js`。
|
||||
- 加载 `region-pages.js`、`md5.js`、`upload-pages.js` 和 `genealogy-entry-pages.js`。
|
||||
- 存在隐藏 `coverOssId` 与文件选择控件,不能手填 OSS ID。
|
||||
- 不存在 `genealogyId/applyId/inviterUserId` 输入。
|
||||
- 表单只包含后端创建 DTO 字段和地区选择辅助字段。
|
||||
- 创建按钮、状态区域和额度区域可由脚本控制。
|
||||
|
||||
- [x] **Step 2: 运行 RED**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/genealogy-entry-pages.test.js tests/pending-pages.test.js tests/security-upload-scope.test.js tests/pc-scope.test.js
|
||||
```
|
||||
|
||||
Expected: FAIL,页面仍 pending 且缺上传/初始化行为。
|
||||
|
||||
- [x] **Step 3: 实现页面初始化和提交**
|
||||
|
||||
初始化顺序:
|
||||
|
||||
1. 校验登录态。
|
||||
2. 调用 `genealogyQuota()` 并展示真实额度。
|
||||
3. `RegionPages` 初始化三级地区。
|
||||
4. `UploadPages` 通过 `data-upload-target` 把封面上传结果写入隐藏 `coverOssId`。
|
||||
5. 提交前同步表单、构造并校验 body。
|
||||
6. 使用 `writePending` 阻止重复提交。
|
||||
7. 调用 `createGenealogy(body)`。
|
||||
8. 规范化响应并跳转到真实家谱主页。
|
||||
|
||||
401 清登录态;403 保留登录态;业务错误显示在表单状态区域。
|
||||
|
||||
- [x] **Step 4: 运行 GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/genealogy-entry-pages.test.js tests/api-client-contract.test.js tests/pending-pages.test.js tests/security-upload-scope.test.js tests/pc-scope.test.js
|
||||
node --check public/js/genealogy-entry-pages.js
|
||||
```
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 首批收口
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/PC接口对接规划.md`
|
||||
- Modify: `docs/superpowers/plans/2026-07-29-genealogy-create-first-batch.md`
|
||||
|
||||
- [x] **Step 1: 更新契约和阶段状态**
|
||||
|
||||
记录 `AppGenealogyCreateBody`、`AppGenealogyVo`、额度、权限、上传派生字段、加入申请 ApiClient 契约及仍未开放的加入 UI。
|
||||
|
||||
- [x] **Step 2: 聚焦和全量验证**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/api-client-contract.test.js tests/genealogy-entry-pages.test.js tests/pending-pages.test.js tests/security-upload-scope.test.js tests/pc-scope.test.js
|
||||
npm test
|
||||
git -c safe.directory=D:/WorkSpace/Web/jiapu diff --check
|
||||
```
|
||||
|
||||
Expected: 全部 PASS。
|
||||
|
||||
- [x] **Step 3: 浏览器验证并报告**
|
||||
|
||||
使用真实账号验证创建页登录态、额度、地区加载、无手填 ID、控制台错误和提交前校验。不得为测试消耗真实创建额度;除非用户明确授权,不执行最终创建写操作。
|
||||
@@ -1,266 +0,0 @@
|
||||
# 家谱加入申请与审核 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: ' <测试联系电话> ',
|
||||
relationDesc: ' 族亲 ',
|
||||
applyReason: ' 申请加入 ',
|
||||
inviterUserId: 'must-drop'
|
||||
}), {
|
||||
applicantName: '叶子',
|
||||
phone: '<测试联系电话>',
|
||||
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: 浏览器验证并报告**
|
||||
|
||||
真实账号验证可申请家谱列表、我的申请空状态、无家谱上下文审核分支、隐私和控制台错误。未经用户明确授权,不提交或审核真实申请。
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
# 帮助中心 PC 接口对接计划
|
||||
|
||||
目标:把现有 `help.html` 的示例问答替换为后端正式 PC 帮助文章列表和详情,不混入官网文章、推广或家谱业务。
|
||||
|
||||
## 全局约束
|
||||
|
||||
- 只调用 `GET /genealogy/pc/help-articles` 和 `GET /genealogy/pc/help-articles/{helpId}`。
|
||||
- 后端控制器未标记匿名访问,部署环境要求登录;两个接口发送当前 PC token。
|
||||
- `helpCategory` 是唯一 Query 字段;`helpId` 只能来自列表响应。
|
||||
- 展示字段仅为 `helpCategory`、`helpTitle`、`helpContent`、`viewCount`。
|
||||
- `coverOssId`、`sortOrder`、`status`、`remark` 不展示,不允许用户输入 ID。
|
||||
- 正文转义后展示,不执行响应中的 HTML。
|
||||
- 后端项目只读。
|
||||
|
||||
## 任务 1:冻结客户端契约
|
||||
|
||||
- 在 `tests/api-client-contract.test.js` 先增加失败测试。
|
||||
- 在 `utils/ApiClient.js` 增加 `helpArticles(query)` 与 `helpArticleDetail(helpId)`。
|
||||
- 验证 method、path、Query 白名单、登录鉴权和长 ID 字符串。
|
||||
|
||||
## 任务 2:实现帮助文章边界
|
||||
|
||||
- 新增 `tests/help-pages.test.js` 并先观察失败。
|
||||
- 新增 `public/js/help-pages.js`。
|
||||
- 校验完整 `HelpArticleVo` 输入,只向页面返回安全展示字段。
|
||||
- 列表任一元素非法时整批失败;详情必须与请求 ID 一致。
|
||||
- 转义标题、分类和正文。
|
||||
|
||||
## 任务 3:开放帮助中心
|
||||
|
||||
- 先用页面契约测试证明现有示例内容不符合真实接口状态。
|
||||
- 修改 `help.html`,提供加载、空、成功和失败状态。
|
||||
- 展开文章时调用详情接口,不制造编辑、邀请或后台操作。
|
||||
|
||||
## 任务 4:规划与验证
|
||||
|
||||
- 更新 `docs/PC接口对接规划.md` 的字段来源、页面时机与阶段 7 进度。
|
||||
- 运行帮助中心专项测试、全量测试、语法检查和 `diff --check`。
|
||||
- 浏览器验证真实列表/空状态和控制台。
|
||||
@@ -1,509 +0,0 @@
|
||||
# 阶段 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 1–6 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、停用记录和缺真实数据写入验证的剩余风险。
|
||||
@@ -1,249 +0,0 @@
|
||||
# VIP 套餐与订单 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:** 在服务中心开放 VIP 套餐查看、响应选项创建订单和我的订单刷新闭环,不制造支付或取消能力。
|
||||
|
||||
**Architecture:** `utils/ApiClient.js` 唯一拥有 `/genealogy/pc/vip/**` 的三个接口;新增 `vip-pages.js` 规范化套餐、家谱选项和订单,使用响应中的字符串 ID 构造 `AppVipOrderBody`。`profile-services.html` 作为唯一 VIP 页面,只提供套餐选择、可选家谱选择、创建订单和订单刷新。
|
||||
|
||||
**Tech Stack:** 静态 HTML、原生 JavaScript UMD、Axios、Node `node:test`。
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- 后端 `D:\WorkSpace\Java\Genealogy` 全程只读。
|
||||
- 只调用 `GET /genealogy/pc/vip/packages`、`POST /genealogy/pc/vip/orders`、`GET /genealogy/pc/vip/orders`。
|
||||
- `packageId` 只能来自套餐响应;`genealogyId` 只能来自 `genealogiesMine()` 响应,不提供文本输入。
|
||||
- 页面不提供未经确认的支付方式选择;请求省略 `payType`,由后端默认 `wechat`。
|
||||
- 不显示立即支付、模拟支付成功、取消订单、退款或关闭订单操作。
|
||||
- 创建成功后重读订单列表并精确匹配同一 `orderId`。
|
||||
- 不展示 `appUserId`、用户手机号、内部原始 JSON。
|
||||
|
||||
---
|
||||
|
||||
### Task 1: ApiClient VIP 契约
|
||||
|
||||
**Files:**
|
||||
- Modify: `utils/ApiClient.js`
|
||||
- Modify: `tests/api-client-contract.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `vipPackages()`、`createVipOrder(body)`、`vipOrders()`
|
||||
|
||||
- [ ] **Step 1: 写失败契约测试**
|
||||
|
||||
```js
|
||||
await client.vipPackages();
|
||||
await client.createVipOrder({
|
||||
packageId: '2062179707935264769',
|
||||
genealogyId: '2062179707935264770',
|
||||
payType: 'wechat',
|
||||
appUserId: 'must-drop',
|
||||
payStatus: 'must-drop'
|
||||
});
|
||||
await client.vipOrders();
|
||||
|
||||
assert.deepEqual(calls.map((config) => [config.method, config.url, config.data]), [
|
||||
['get', '/genealogy/pc/vip/packages', undefined],
|
||||
['post', '/genealogy/pc/vip/orders', {
|
||||
packageId: '2062179707935264769',
|
||||
genealogyId: '2062179707935264770',
|
||||
payType: 'wechat'
|
||||
}],
|
||||
['get', '/genealogy/pc/vip/orders', undefined]
|
||||
]);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 运行 RED**
|
||||
|
||||
Run: `node --test tests/api-client-contract.test.js`
|
||||
|
||||
Expected: FAIL,VIP 方法不存在。
|
||||
|
||||
- [ ] **Step 3: 最小实现**
|
||||
|
||||
```js
|
||||
vipPackages: function () {
|
||||
return request('GET', '/genealogy/pc/vip/packages');
|
||||
},
|
||||
createVipOrder: function (body) {
|
||||
return request('POST', '/genealogy/pc/vip/orders', {
|
||||
body: pickDefined(body, ['packageId', 'genealogyId', 'payType'])
|
||||
});
|
||||
},
|
||||
vipOrders: function () {
|
||||
return request('GET', '/genealogy/pc/vip/orders');
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 运行 GREEN**
|
||||
|
||||
Run: `node --test tests/api-client-contract.test.js`
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 套餐、家谱选项和订单数据边界
|
||||
|
||||
**Files:**
|
||||
- Create: `public/js/vip-pages.js`
|
||||
- Create: `tests/vip-pages.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
- `normalizeVipPackage(item)`
|
||||
- `normalizeVipPackages(data)`
|
||||
- `normalizeGenealogyOption(item)`
|
||||
- `buildVipOrderBody(values)`
|
||||
- `validateVipOrderBody(body)`
|
||||
- `normalizeVipOrder(item)`
|
||||
- `normalizeVipOrders(data)`
|
||||
- `renderVipPackages(data, selectedPackageId)`
|
||||
- `renderGenealogyOptions(data)`
|
||||
- `renderVipOrders(data)`
|
||||
|
||||
- [ ] **Step 1: 写失败纯行为测试**
|
||||
|
||||
使用完整 `VipPackageVo`、`VipOrderVo` 字面量断言:
|
||||
|
||||
- 套餐要求稳定字符串 `packageId`、非空名称、`packageType=vip/storage`、`durationUnit=permanent/day/month/year`、非负价格、`status=0`;
|
||||
- 停用套餐和不安全数字长 ID 不渲染;
|
||||
- 家谱选项要求稳定字符串 `genealogyId` 和非空 `genealogyName`;
|
||||
- body 只保留 `packageId/genealogyId/payType`,页面默认构造不包含 `payType`;
|
||||
- 必须选择有效套餐;可选家谱必须是安全字符串 ID;若显式提供 `payType` 只允许已确认 `wechat`;
|
||||
- 订单要求稳定 `orderId/packageId`、订单号、套餐名、非负金额、`payStatus=0/1/2/3`、`status=0/1`;
|
||||
- 渲染不出现用户 ID、手机号、原始 JSON,不出现支付/取消/退款按钮。
|
||||
|
||||
- [ ] **Step 2: 运行 RED**
|
||||
|
||||
Run: `node --test tests/vip-pages.test.js`
|
||||
|
||||
Expected: FAIL,模块不存在。
|
||||
|
||||
- [ ] **Step 3: 最小实现纯函数**
|
||||
|
||||
支付状态:
|
||||
|
||||
```js
|
||||
{ '0': '待支付', '1': '已支付', '2': '已关闭', '3': '已退款' }
|
||||
```
|
||||
|
||||
套餐类型:
|
||||
|
||||
```js
|
||||
{ vip: '会员套餐', storage: '存储扩容' }
|
||||
```
|
||||
|
||||
套餐和订单金额保留后端字符串语义,不进行浮点运算。
|
||||
|
||||
- [ ] **Step 4: 运行 GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/vip-pages.test.js
|
||||
node --check public/js/vip-pages.js
|
||||
```
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 服务中心真实 VIP 闭环
|
||||
|
||||
**Files:**
|
||||
- Modify: `profile-services.html`
|
||||
- Modify: `public/js/vip-pages.js`
|
||||
- Modify: `tests/vip-pages.test.js`
|
||||
- Modify: `tests/pending-pages.test.js`
|
||||
- Modify: `tests/pc-scope.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `initVipPage()`、`init()`
|
||||
|
||||
- [ ] **Step 1: 写失败页面行为测试**
|
||||
|
||||
断言:
|
||||
|
||||
- 服务中心不再 pending,加载 `vip-pages.js`;
|
||||
- 有套餐列表、只读选中提示、可选家谱下拉、订单表单、刷新订单和订单列表;
|
||||
- 不存在 `packageId/genealogyId/orderId` 文本或数字输入;
|
||||
- 不存在 `payType` 选择器以及支付、取消、退款、模拟成功按钮;
|
||||
- 脚本并行读取套餐、我的家谱和订单;
|
||||
- 创建后重读订单并精确匹配提交响应 `orderId`。
|
||||
|
||||
- [ ] **Step 2: 运行 RED**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/vip-pages.test.js tests/pending-pages.test.js tests/pc-scope.test.js
|
||||
```
|
||||
|
||||
Expected: FAIL,服务中心仍为 pending 且允许手填 ID。
|
||||
|
||||
- [ ] **Step 3: 实现页面初始化**
|
||||
|
||||
1. 401 跳转登录,403 保留登录态;
|
||||
2. `Promise.all([vipPackages(), genealogiesMine(), vipOrders()])` 读取页面数据;
|
||||
3. 点击套餐卡保存响应中的字符串 `packageId`;
|
||||
4. 家谱下拉只使用 `genealogiesMine()` 选项,空值表示不关联家谱;
|
||||
5. 表单提交使用 `writePending` 锁;
|
||||
6. 创建响应必须规范化;
|
||||
7. 重读 `vipOrders()` 并找到同一 `orderId`;
|
||||
8. 刷新订单列表并展示“订单已创建;当前 PC 暂未开放在线支付”。
|
||||
|
||||
- [ ] **Step 4: 运行 GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/vip-pages.test.js tests/pending-pages.test.js tests/pc-scope.test.js
|
||||
node --check public/js/vip-pages.js
|
||||
```
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 导航、规划和收尾验证
|
||||
|
||||
**Files:**
|
||||
- Modify: `profile.html`
|
||||
- Modify: `docs/PC接口对接规划.md`
|
||||
- Modify: `tests/stage6-navigation.test.js`
|
||||
- Modify: `docs/superpowers/plans/2026-07-29-vip-packages-orders.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1–3 的 VIP 套餐与订单闭环。
|
||||
|
||||
- [ ] **Step 1: 写失败导航测试**
|
||||
|
||||
断言个人中心服务入口可进入 `profile-services.html`,服务页真实开放并且所有 VIP 业务都留在该唯一 owner 页面。
|
||||
|
||||
- [ ] **Step 2: 运行 RED**
|
||||
|
||||
Run: `node --test tests/stage6-navigation.test.js tests/pending-pages.test.js`
|
||||
|
||||
Expected: FAIL,服务页仍 pending 或未加载真实脚本。
|
||||
|
||||
- [ ] **Step 3: 更新规划**
|
||||
|
||||
记录 `AppVipOrderBody`、`VipPackageVo`、`VipOrderVo` 的字段来源、枚举、隐藏字段、写后重读和支付能力阻断;阶段 7 标记 VIP 批次完成。
|
||||
|
||||
- [ ] **Step 4: 聚焦和全量验证**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/vip-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/vip-pages.js
|
||||
git -c safe.directory=D:/WorkSpace/Web/jiapu diff --check
|
||||
```
|
||||
|
||||
Expected: 全部 PASS。
|
||||
|
||||
- [ ] **Step 5: 浏览器只读验证**
|
||||
|
||||
真实登录态验证套餐、家谱下拉、订单空/有数据态、无伪支付动作和控制台错误。未经用户本轮明确授权,不创建真实订单。
|
||||
@@ -1,424 +0,0 @@
|
||||
# 官网资讯 PC 接口对接实施计划
|
||||
|
||||
> **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:** 将 `news.html` 与 `article-detail.html` 接入公开 PC 站点文章列表接口,交付可筛选列表与精确详情闭环。
|
||||
|
||||
**Architecture:** `utils/ApiClient.js` 独占 PC path、公开鉴权标记和 Query 校验;`public/js/site-news-pages.js` 独占 `SiteArticleVo` 规范化、渲染和页面初始化。后端没有单条详情接口,详情页使用 URL 中由列表响应生成的字符串 ID 重读列表并精确匹配。
|
||||
|
||||
**Tech Stack:** 原生 HTML/CSS/JavaScript、现有 Axios 请求层、Node `node:test`。
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- 后端 `D:\WorkSpace\Java\Genealogy` 只读,不得修改。
|
||||
- 只使用 `/genealogy/pc/site/articles`,不得调用 APP、后台管理或家谱谱文 CRUD 接口。
|
||||
- 本批只修改 `news.html` 与 `article-detail.html`,不混改其他静态内容页。
|
||||
- 业务 ID 始终保持字符串,不提供手填 ID 或 OSS ID。
|
||||
- `articleType` 只允许 `news/notice/download`;`limit` 固定为 100 且客户端只允许 1–100 的整数。
|
||||
- 公开请求设置 `auth: false`;其 401/403 不清理既有 token、不跳转登录。
|
||||
- 响应只接受直接 `SiteArticleVo[]`;旧字段、分页对象和任一非法元素均使整批失败。
|
||||
- 所有响应文本 HTML 转义;正文仅保留换行;外链只允许绝对 HTTP/HTTPS。
|
||||
- 当前工作区由用户明确授权直接施工;不得 stage、commit、push 或创建 PR。
|
||||
- 子代理仅用于探索和只读复核;业务代码修改、取舍与最终验证由主代理完成。
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 修正公开请求的登录态隔离
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/request-auth-state.test.js`
|
||||
- Modify: `utils/AxiosRequestUtil.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `createRequester({ getToken, onUnauthorized, axiosInstance })`
|
||||
- Produces: `request(method, path, { auth: false })` 不发送 Authorization,且 HTTP/业务 401 均不调用 `onUnauthorized`
|
||||
|
||||
- [ ] **Step 1: 写失败测试**
|
||||
|
||||
在 `tests/request-auth-state.test.js` 引入 `../utils/AxiosRequestUtil.js`,新增两个真实请求层用例:
|
||||
|
||||
```js
|
||||
test('public HTTP 401 neither sends nor clears the stored login token', async () => {
|
||||
let storedToken = 'access-token';
|
||||
let seenRequest;
|
||||
const requester = AxiosRequestUtil.createRequester({
|
||||
baseUrl: 'https://api.example.test',
|
||||
clientId: 'web-pc',
|
||||
getToken() { return storedToken; },
|
||||
onUnauthorized() { storedToken = ''; },
|
||||
axiosInstance: {
|
||||
request(config) {
|
||||
seenRequest = config;
|
||||
return Promise.reject({
|
||||
message: 'Request failed',
|
||||
response: { status: 401, data: { code: 401, msg: '认证失败' } }
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await assert.rejects(requester('GET', '/public', { auth: false }));
|
||||
assert.equal(seenRequest.headers.Authorization, undefined);
|
||||
assert.equal(storedToken, 'access-token');
|
||||
});
|
||||
|
||||
test('public business 401 preserves the stored login token', async () => {
|
||||
let storedToken = 'access-token';
|
||||
const requester = AxiosRequestUtil.createRequester({
|
||||
baseUrl: 'https://api.example.test',
|
||||
clientId: 'web-pc',
|
||||
getToken() { return storedToken; },
|
||||
onUnauthorized() { storedToken = ''; },
|
||||
axiosInstance: {
|
||||
request() {
|
||||
return Promise.resolve({
|
||||
data: { code: 401, msg: '认证失败' }
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await assert.rejects(requester('GET', '/public', { auth: false }));
|
||||
assert.equal(storedToken, 'access-token');
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 运行 RED**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/request-auth-state.test.js
|
||||
```
|
||||
|
||||
Expected: 新增用例因 `handleUnauthorized` 未区分 `auth: false` 而失败。
|
||||
|
||||
- [ ] **Step 3: 写最小实现**
|
||||
|
||||
在 `utils/AxiosRequestUtil.js` 的单次 `request` 闭包内,使 HTTP catch 与业务解包 catch 只在 `req.auth !== false` 时调用 `handleUnauthorized`:
|
||||
|
||||
```js
|
||||
if (req.auth !== false) handleUnauthorized(status);
|
||||
```
|
||||
|
||||
不得改变默认鉴权请求的现有行为。
|
||||
|
||||
- [ ] **Step 4: 运行 GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/request-auth-state.test.js
|
||||
```
|
||||
|
||||
Expected: 现有默认 401 清 token、403 保留 token,以及新增公开请求用例全部通过。
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 增加 PC 站点文章 ApiClient 契约
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/api-client-contract.test.js`
|
||||
- Modify: `utils/ApiClient.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `siteArticles(query?: { articleType?: 'news'|'notice'|'download', limit?: number }): Promise<Array>`
|
||||
- Request: `GET /genealogy/pc/site/articles`, `{ auth: false, query }`
|
||||
|
||||
- [ ] **Step 1: 写失败测试**
|
||||
|
||||
在公开方法清单中增加 `siteArticles`,并新增契约用例:
|
||||
|
||||
```js
|
||||
await client.siteArticles({
|
||||
articleType: 'notice',
|
||||
limit: 100,
|
||||
keyword: 'must-drop'
|
||||
});
|
||||
|
||||
assert.deepEqual(seen, {
|
||||
method: 'get',
|
||||
url: '/genealogy/pc/site/articles',
|
||||
params: { articleType: 'notice', limit: 100 },
|
||||
authorization: undefined
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() => client.siteArticles({ articleType: 'culture', limit: 100 }),
|
||||
/不支持的资讯类型/
|
||||
);
|
||||
assert.throws(
|
||||
() => client.siteArticles({ limit: 101 }),
|
||||
/资讯数量限制/
|
||||
);
|
||||
```
|
||||
|
||||
同时覆盖 `limit` 的 `0`、小数、字符串和负数,确保只接受 1–100 的 Number 整数;省略 `articleType` 时只发送 `{ limit: 100 }`。
|
||||
|
||||
- [ ] **Step 2: 运行 RED**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/api-client-contract.test.js
|
||||
```
|
||||
|
||||
Expected: `siteArticles` 不存在。
|
||||
|
||||
- [ ] **Step 3: 写最小实现**
|
||||
|
||||
在 `utils/ApiClient.js` 增加 `siteArticles`:
|
||||
|
||||
```js
|
||||
siteArticles: function (query) {
|
||||
var source = query || {};
|
||||
var articleType = source.articleType === undefined || source.articleType === null
|
||||
? ''
|
||||
: String(source.articleType).trim();
|
||||
var limit = source.limit;
|
||||
var params = {};
|
||||
|
||||
if (articleType && ['news', 'notice', 'download'].indexOf(articleType) === -1) {
|
||||
throw new Error('不支持的资讯类型:' + articleType);
|
||||
}
|
||||
if (limit !== undefined &&
|
||||
(typeof limit !== 'number' || !Number.isInteger(limit) || limit < 1 || limit > 100)) {
|
||||
throw new Error('资讯数量限制必须是 1 到 100 的整数');
|
||||
}
|
||||
if (articleType) params.articleType = articleType;
|
||||
if (limit !== undefined) params.limit = limit;
|
||||
return request('GET', '/genealogy/pc/site/articles', {
|
||||
auth: false,
|
||||
query: params
|
||||
});
|
||||
},
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 运行 GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/api-client-contract.test.js tests/request-auth-state.test.js
|
||||
```
|
||||
|
||||
Expected: 全部通过,且公开请求没有 Authorization Header。
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 实现站点资讯页面 owner
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/site-news-pages.test.js`
|
||||
- Create: `public/js/site-news-pages.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `GenealogyApi.defaultClient.siteArticles({ articleType?, limit: 100 })`
|
||||
- Produces:
|
||||
- `normalizeArticle(item)`
|
||||
- `normalizeArticleList(data)`
|
||||
- `normalizeArticleType(value)`
|
||||
- `normalizeExternalUrl(value)`
|
||||
- `readArticleId(search)`
|
||||
- `renderArticleList(data)`
|
||||
- `renderArticleDetail(item)`
|
||||
- `loadArticles(api, articleType?)`
|
||||
- `loadArticleDetail(api, articleId)`
|
||||
- `init()`
|
||||
|
||||
- [ ] **Step 1: 写完整 RED 测试**
|
||||
|
||||
创建 `tests/site-news-pages.test.js`,使用完整 `SiteArticleVo` fixture:
|
||||
|
||||
```js
|
||||
{
|
||||
articleId: '2062179707935264769',
|
||||
articleType: 'notice',
|
||||
articleTitle: '平台公告',
|
||||
articleSummary: '公告摘要',
|
||||
articleContent: '第一行\n第二行',
|
||||
coverOssId: '2062179707935264701',
|
||||
externalUrl: 'https://example.com/notice',
|
||||
publishTime: '2026-07-30 09:00:00',
|
||||
sortOrder: 1,
|
||||
status: '0',
|
||||
remark: 'internal-only'
|
||||
}
|
||||
```
|
||||
|
||||
必须覆盖:
|
||||
|
||||
- 安全长 ID 字符串保留;不安全 Number、零、空 ID 拒绝。
|
||||
- `articleType` 只接受 `news/notice/download`。
|
||||
- 标题必填、状态必须为 `0`。
|
||||
- 直接数组全有或全无;`{ rows: [...] }` 拒绝。
|
||||
- 规范化结果不包含 `coverOssId/sortOrder/status/remark`。
|
||||
- 列表 HTML 指向 `article-detail.html?articleId=2062179707935264769`。
|
||||
- 标题、摘要、正文转义;正文换行变为 `<br />`。
|
||||
- 危险、相对和 `data:` 外链不渲染;合法 HTTP/HTTPS 使用 `noopener noreferrer`。
|
||||
- `loadArticles(api, 'notice')` 调用 `{ articleType: 'notice', limit: 100 }`。
|
||||
- `loadArticles(api, '')` 调用 `{ limit: 100 }`。
|
||||
- `loadArticleDetail` 只返回同 ID;未匹配抛出“资讯不存在或已下线”。
|
||||
- 非法详情 ID 在调用 API 前抛出“资讯编号无效”。
|
||||
- `readArticleId('?articleId=...')` 只返回安全字符串。
|
||||
|
||||
- [ ] **Step 2: 运行 RED**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/site-news-pages.test.js
|
||||
```
|
||||
|
||||
Expected: 模块不存在。
|
||||
|
||||
- [ ] **Step 3: 写 UMD 模块最小实现**
|
||||
|
||||
创建 UMD 模块:Node 环境导出 `module.exports = factory(root)`;浏览器环境挂到 `root.SiteNewsPages`,并在 `DOMContentLoaded` 调用 `root.SiteNewsPages.init()`。关键行为:
|
||||
|
||||
```js
|
||||
async function loadArticles(api, articleType) {
|
||||
var type = normalizeArticleType(articleType);
|
||||
var query = { limit: 100 };
|
||||
var data;
|
||||
var items;
|
||||
|
||||
if (articleType && !type) throw new Error('资讯分类无效');
|
||||
if (type) query.articleType = type;
|
||||
data = await api.siteArticles(query);
|
||||
items = normalizeArticleList(data);
|
||||
if (!Array.isArray(data) || items.length !== data.length) {
|
||||
throw new Error('资讯列表响应无效');
|
||||
}
|
||||
return items;
|
||||
}
|
||||
```
|
||||
|
||||
`init()`:
|
||||
|
||||
- `[data-site-news-page]`:从 `location.search` 读取 `articleType`,加载并渲染 `[data-site-news-list]`。
|
||||
- `[data-site-article-page]`:读取 `articleId`;非法时不发请求;合法时精确重读并渲染 `[data-site-article-detail]`。
|
||||
- 所有失败只更新对应区域和 `[data-site-*-status]`,不跳转、不清 token。
|
||||
|
||||
- [ ] **Step 4: 运行 GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/site-news-pages.test.js tests/api-client-contract.test.js tests/request-auth-state.test.js
|
||||
```
|
||||
|
||||
Expected: 全部通过。
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 接入 HTML、规划并完成验收
|
||||
|
||||
**Files:**
|
||||
- Modify: `news.html`
|
||||
- Modify: `article-detail.html`
|
||||
- Modify: `tests/site-news-pages.test.js`
|
||||
- Modify: `tests/public-static-pages.test.js`
|
||||
- Modify: `tests/pc-scope.test.js`
|
||||
- Modify: `docs/PC接口对接规划.md`
|
||||
|
||||
**Interfaces:**
|
||||
- `news.html`: `[data-site-news-page]`, `[data-site-news-list]`, `[data-site-news-status]`
|
||||
- `article-detail.html`: `[data-site-article-page]`, `[data-site-article-detail]`, `[data-site-article-status]`
|
||||
|
||||
- [ ] **Step 1: 写 HTML 接入 RED 测试**
|
||||
|
||||
断言:
|
||||
|
||||
```js
|
||||
assert.match(newsPage, /data-site-news-page/);
|
||||
assert.match(newsPage, /data-site-news-list/);
|
||||
assert.match(newsPage, /news\.html\?articleType=news/);
|
||||
assert.match(newsPage, /news\.html\?articleType=notice/);
|
||||
assert.match(newsPage, /news\.html\?articleType=download/);
|
||||
assert.match(newsPage, /src="public\/js\/site-news-pages\.js"/);
|
||||
|
||||
assert.match(detailPage, /data-site-article-page/);
|
||||
assert.match(detailPage, /data-site-article-detail/);
|
||||
assert.match(detailPage, /src="public\/js\/site-news-pages\.js"/);
|
||||
assert.doesNotMatch(detailPage, /src="public\/js\/article-pages\.js"/);
|
||||
assert.doesNotMatch(newsPage + detailPage, /name="(?:articleId|coverOssId)"/);
|
||||
```
|
||||
|
||||
更新 `public-static-pages.test.js`,移除旧的固定 `#platform/#culture` 链接断言,改为真实分类入口和详情目标文件断言。
|
||||
|
||||
- [ ] **Step 2: 运行 RED**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/site-news-pages.test.js tests/public-static-pages.test.js tests/pc-scope.test.js
|
||||
```
|
||||
|
||||
Expected: HTML 尚未加载新模块、旧固定链接断言需迁移。
|
||||
|
||||
- [ ] **Step 3: 修改 HTML**
|
||||
|
||||
`news.html`:
|
||||
|
||||
- `<body data-site-news-page>`
|
||||
- 删除“当前为静态展示”文案和两条硬编码文章。
|
||||
- 列表容器初始化为“资讯内容加载中”。
|
||||
- 分类链接使用规格中的四个固定 URL。
|
||||
- 按顺序加载 `config.js`、`StorageUtil.js`、`axios.js`、`AxiosRequestUtil.js`、`ApiClient.js`、`page-effects.js`、`site-news-pages.js`。
|
||||
|
||||
`article-detail.html`:
|
||||
|
||||
- body 标记改为 `data-site-article-page`,避免与家谱谱文语义混用。
|
||||
- 将 hero 和正文合并到 `data-site-article-detail` 可替换容器。
|
||||
- 增加 `data-site-article-status`。
|
||||
- 加载 `site-news-pages.js`,不加载 `article-pages.js`。
|
||||
|
||||
- [ ] **Step 4: 更新规划**
|
||||
|
||||
在 `docs/PC接口对接规划.md` 第 7 节新增“官网资讯契约”,记录:
|
||||
|
||||
- method/path、公开鉴权、Query 枚举和 limit 范围。
|
||||
- `SiteArticleVo` 11 个字段的 R/I/S/A 分类、SQL 长度和默认值。
|
||||
- 列表直接数组、服务端排序、详情重读匹配。
|
||||
- YAML 只给通用响应且没有导出 `SiteArticleVo` Schema 的差异。
|
||||
- `coverOssId` 文件 URL 和站点文章单条详情接口仍阻断。
|
||||
|
||||
在阶段 7 增加第八批完成说明。
|
||||
|
||||
- [ ] **Step 5: 运行自动化验收**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --check public/js/site-news-pages.js
|
||||
node --check utils/ApiClient.js
|
||||
node --check utils/AxiosRequestUtil.js
|
||||
node --test tests/site-news-pages.test.js tests/api-client-contract.test.js tests/request-auth-state.test.js tests/public-static-pages.test.js tests/pc-scope.test.js
|
||||
node --test tests/*.test.js
|
||||
git -c safe.directory=D:/WorkSpace/Web/jiapu diff --check
|
||||
```
|
||||
|
||||
Expected: 所有命令退出码 0。
|
||||
|
||||
- [ ] **Step 6: 浏览器验收**
|
||||
|
||||
启动临时本地静态服务器,验证:
|
||||
|
||||
1. 未登录打开 `news.html`,页面不跳登录。
|
||||
2. 真实列表、分类或空状态正确。
|
||||
3. 有数据时点击一个由响应生成的详情链接,详情 ID 精确匹配;无数据时记录真实空状态。
|
||||
4. 控制台没有业务脚本错误。
|
||||
5. 不点击外部链接。
|
||||
6. 停止服务器并删除临时辅助文件。
|
||||
|
||||
- [ ] **Step 7: 阶段报告**
|
||||
|
||||
按以下格式报告并停下:
|
||||
|
||||
```text
|
||||
Changed: 官网资讯列表、详情、公开请求语义和规划补充。
|
||||
Verified: 专项/全量测试数量及真实浏览器覆盖。
|
||||
Conflicts: YAML 通用响应与后端完整 VO/SQL 的差异。
|
||||
Blocked: 单条详情接口、封面文件 URL、仍无 PC Controller 的 pending 页面。
|
||||
```
|
||||
@@ -1,71 +0,0 @@
|
||||
# 应用下载页 PC 推广列表设计
|
||||
|
||||
## 目标与范围
|
||||
|
||||
只在 `app.html` 现有“应用推广”区域接入:
|
||||
|
||||
`GET /genealogy/pc/promotions`
|
||||
|
||||
本批不修改首页广告位,不接入官网文章、帮助文章或公开家谱,不新增推广详情、编辑、发布和统计功能。
|
||||
|
||||
## 契约来源与冲突
|
||||
|
||||
后端 `PcPromotionApiController` 继承 `BusinessPromotionControllerSupport`,实际支持可选 Query `platform`,返回直接数组 `List<AppPromotionVo>`。服务端只返回 `status=0` 的记录,并按 `sortOrder` 升序、`promotionId` 降序排列。
|
||||
|
||||
Apifox YAML 将接口标记为 `security: []` 且未导出 `platform`;后端控制器没有 `@SaIgnore`,全局安全拦截器实际要求 PC 登录态和匹配的 `clientid`。本批以后端代码和部署行为为准:
|
||||
|
||||
- 请求携带当前 PC 登录 token;
|
||||
- 不发送 `platform`,因为后端没有提供可核验的枚举和值说明;
|
||||
- 页面不自行排序或筛选。
|
||||
|
||||
## 页面行为
|
||||
|
||||
`app.html` 本身保持公开,不因推广接口需要登录而强制跳转:
|
||||
|
||||
- 未登录:不发请求,推广区域显示“登录后查看应用推广”和登录链接;
|
||||
- 已登录:初始化时读取一次推广列表;
|
||||
- 返回空数组:显示“当前暂无应用推广”;
|
||||
- 返回有效记录:渲染现有三列推广卡片;
|
||||
- 401:清理失效登录态,推广区域改为登录入口;
|
||||
- 403、网络失败或响应非法:只在推广区域显示错误,不影响下载页其它内容;
|
||||
- 页面不提供手动刷新、筛选、编辑或业务 ID 输入。
|
||||
|
||||
## 字段边界
|
||||
|
||||
| 字段 | 分类 | 页面行为 |
|
||||
| --- | --- | --- |
|
||||
| `promotionId` | I | 要求为稳定非零十进制字符串,用作节点标识,不展示 |
|
||||
| `promotionKey` | I | 后台键,不展示 |
|
||||
| `promotionTitle` | R | 必填,转义后作为卡片标题 |
|
||||
| `promotionDesc` | R | 可空,转义后作为说明 |
|
||||
| `coverOssId` | I | 没有文件 URL 契约,不拼接、不展示、不手填 |
|
||||
| `targetUrl` | R | 可空;只允许绝对 `http`/`https` URL,合法时整张卡片可点击,并使用 `target="_blank"`、`rel="noopener noreferrer"` |
|
||||
| `platform` | I | 响应字段只用于边界保留,不显示、不作为前端筛选依据 |
|
||||
| `sortOrder` | I | 服务端排序 owner,前端不重新排序 |
|
||||
| `status` | I | 只接受 `0`;其它值使整条响应非法 |
|
||||
| `remark` | I | 后台备注,不展示 |
|
||||
|
||||
列表必须是直接数组。任一元素缺少稳定 `promotionId`、标题或正常状态时,整批响应失败,避免部分错误数据被误当成完整结果。
|
||||
|
||||
## 文件与所有权
|
||||
|
||||
- `utils/ApiClient.js`:method、完整 path 和 Query 白名单唯一 owner,新增 `promotions(query)`;虽然当前页面省略 Query,但客户端只允许 `platform`。
|
||||
- `public/js/app-promotion-pages.js`:响应规范化、URL 安全校验、卡片渲染、登录态和区域状态 owner。
|
||||
- `app.html`:保留现有 `data-promotion-list` 容器,加载推广脚本。
|
||||
- `public/css/app.css`:只在现有样式确实无法支持无封面卡片时做最小调整。
|
||||
- `tests/api-client-contract.test.js`、`tests/app-promotion-pages.test.js`、`tests/pc-scope.test.js`:契约、渲染、安全和页面开放状态。
|
||||
- `docs/PC接口对接规划.md`:记录字段来源、提交时机、鉴权冲突和阶段进度。
|
||||
|
||||
旧的 `promotion-pages.js` 不恢复;新的 owner 只服务应用下载页,避免与历史未定义脚本形成兼容分支。
|
||||
|
||||
## 测试与验收
|
||||
|
||||
必须按 RED → GREEN 验证:
|
||||
|
||||
1. 客户端只调用 `GET /genealogy/pc/promotions`,可选 Query 仅保留 `platform`,并携带登录 token。
|
||||
2. 长 ID 始终按字符串处理;不安全数字、停用记录、空标题和非法列表结构失败。
|
||||
3. 标题、说明和 URL 转义;`javascript:`、相对 URL 和非 HTTP(S) URL 不生成链接。
|
||||
4. 页面不显示 `coverOssId`、`promotionKey`、`sortOrder`、`status`、`remark` 或原始 JSON。
|
||||
5. 未登录不发推广请求;401、403、空列表和成功列表均有独立区域状态。
|
||||
6. 专项测试、全量测试、语法检查和 `git diff --check` 通过。
|
||||
7. 浏览器使用测试账号验证真实列表或空状态;没有推广数据时,不制造详情或示例推广。
|
||||
@@ -1,155 +0,0 @@
|
||||
# 剩余 PC 页面对接设计
|
||||
|
||||
## 目标
|
||||
|
||||
以 `D:\WorkSpace\Java\Genealogy` 中的 PC Controller、DTO、VO 和 Service 实现为本轮唯一运行时契约,开放当前仍为 pending、但后端 PC 能力已经形成闭环的页面。
|
||||
|
||||
后端目录全程只读。前端不得调用 `/genealogy/app/**`、后台管理端 `/genealogy/**`,不得猜测接口、字段、业务 ID、OSS ID、支付动作、邀请令牌或分享地址。
|
||||
|
||||
## 契约所有者
|
||||
|
||||
- `utils/ApiClient.js` 唯一拥有 PC method、path、query 和 body 白名单。
|
||||
- 页面脚本只接收 `ApiClient` 解包后的业务数据,不自行拼接接口路径。
|
||||
- 页面中的家谱、申请、反馈、套餐和订单 ID 只能来自 URL、当前家谱上下文或后端响应。
|
||||
- 后端 PC Controller 继承共用 Support 类时,以 PC Controller 的 `@RequestMapping` 作为路径,以 Support 类方法、DTO、VO 和 Service 作为字段与行为依据;这不构成借用 APP 接口。
|
||||
|
||||
## 范围
|
||||
|
||||
### 1. 家谱创建
|
||||
|
||||
页面:`profile-create-family.html`。
|
||||
|
||||
接入:
|
||||
|
||||
- `POST /genealogy/pc/genealogies`
|
||||
- `GET /genealogy/pc/genealogies/quota`
|
||||
- `GET /genealogy/pc/region/children?parentCode=0`
|
||||
- `GET /genealogy/pc/region/children?parentCode={regionCode}`
|
||||
|
||||
请求只提交 `AppGenealogyCreateBody`:
|
||||
|
||||
- 必填:`genealogyName`、`surname`、`regionCode`
|
||||
- 可选:`ancestralHall`、`originPlace`、`addressDetail`、`coverOssId`、`intro`、`visibility`、`joinMode`
|
||||
|
||||
`regionCode` 由省市区选择产生;`coverOssId` 只能由文件上传产生。成功后使用响应中的真实 `genealogyId` 写入当前家谱上下文并进入家谱主页。额度不足、未登录业务用户、地区无效和服务端创建失败均展示后端错误,不降级到虚假成功。
|
||||
|
||||
### 2. 加入申请与审核
|
||||
|
||||
页面:`profile-join-family.html`、`join-genealogy.html`、`profile-join-review.html`。
|
||||
|
||||
接入:
|
||||
|
||||
- `POST /genealogy/pc/genealogies/{genealogyId}/join-applies`
|
||||
- `GET /genealogy/pc/genealogies/join-applies/mine`
|
||||
- `DELETE /genealogy/pc/genealogies/join-applies/{applyId}`
|
||||
- `GET /genealogy/pc/genealogies/{genealogyId}/join-applies/pending`
|
||||
- `PUT /genealogy/pc/genealogies/{genealogyId}/join-applies/{applyId}/audit`
|
||||
|
||||
加入申请字段:
|
||||
|
||||
- `applicantName`:可选,最长 50
|
||||
- `phone`:可选,最长 30
|
||||
- `relationDesc`:可选,最长 100
|
||||
- `applyReason`:可选,最长 500
|
||||
- `inviterUserId`:仅后端 `INVITE` 模式使用;当前 PC 没有安全邀请来源,前端不展示、不提交
|
||||
|
||||
审核字段:
|
||||
|
||||
- `status`:必填,只允许 `1` 通过、`2` 拒绝
|
||||
- `auditRemark`:可选,最长 500
|
||||
|
||||
申请列表、审核列表和写入结果使用完整 `GenealogyJoinApplyVo`。页面隐藏申请人、邀请人和审核人的内部用户 ID;手机号只在家谱管理员审核场景按后端响应展示。撤销只允许当前申请人的待审核申请;审核只对当前家谱待审核申请开放。重复提交、已处理申请、已是成员、家谱关闭加入和权限不足均使用真实错误分支。
|
||||
|
||||
### 3. 意见反馈与工单
|
||||
|
||||
页面:`profile-feedback.html`、`submit-ticket.html`、`my-tickets.html`、`ticket-detail.html`。
|
||||
|
||||
接入:
|
||||
|
||||
- `POST /genealogy/pc/feedback`
|
||||
- `GET /genealogy/pc/feedback`
|
||||
|
||||
请求只提交 `AppFeedbackBody`:
|
||||
|
||||
- `feedbackContent`:必填
|
||||
- `feedbackType`:可选,空值由后端默认 `advice`
|
||||
- `contactInfo`:可选
|
||||
|
||||
现有页面里的 `feedbackTitle` 不属于后端 DTO,不发送。工单页面与意见反馈页面共用 `feedback-pages.js`;“工单”是同一反馈记录在帮助中心的展示名称,不制造独立 ticket path。
|
||||
|
||||
列表和详情使用完整 `FeedbackVo`。详情页从 PC 反馈列表按 URL 中真实 `feedbackId` 匹配,未匹配时显示不存在,不回退到第一条。用户只展示反馈类型、内容、联系方式、处理状态、处理结果、处理时间和备注,不展示 `appUserId`、`handlerId`、手机号 enrichment 或原始 JSON。提交成功后重读列表并核对同一反馈 ID。
|
||||
|
||||
### 4. VIP 套餐和订单
|
||||
|
||||
页面:`profile-services.html`。
|
||||
|
||||
接入:
|
||||
|
||||
- `GET /genealogy/pc/vip/packages`
|
||||
- `POST /genealogy/pc/vip/orders`
|
||||
- `GET /genealogy/pc/vip/orders`
|
||||
|
||||
订单请求只提交 `AppVipOrderBody`:
|
||||
|
||||
- `packageId`:必填,只能来自套餐响应
|
||||
- `genealogyId`:可选,只能来自用户家谱选项
|
||||
- `payType`:可选,空值由后端默认 `wechat`
|
||||
|
||||
页面展示完整 `VipPackageVo` 中的套餐名称、说明、价格、原价、有效期、家谱/成员/存储限制与正常状态;展示 `VipOrderVo` 中的订单号、套餐、关联家谱、金额、支付类型、支付状态和有效期。
|
||||
|
||||
后端当前只落订单,没有 PC 支付发起、支付回调或取消接口。页面只允许“创建订单”和“刷新订单”,不显示“立即支付”“模拟成功”“取消订单”。新订单按后端 `payStatus=0` 展示为待支付,并明确支付能力尚未开放。
|
||||
|
||||
### 5. 家谱主页与权限入口收口
|
||||
|
||||
页面:`profile-family-home.html`、`profile-admin-permissions.html`。
|
||||
|
||||
家谱主页使用:
|
||||
|
||||
- `GET /genealogy/pc/genealogies/{genealogyId}/overview`
|
||||
|
||||
主页展示 `AppGenealogyVo` 可确认的家谱名称、编号、姓氏、地区、成员数、人物数、当前角色和能力;继续保留已经开放的世系、谱文、相册、视频、功德、祭祀和家族圈入口。没有家谱上下文时阻止业务请求并返回家谱选择页。
|
||||
|
||||
管理员权限能力已经由 `profile-family-admin.html` 的成员角色修改、移出、退出和谱主转移覆盖。`profile-admin-permissions.html` 不再维护第二套表单,改为携带家谱上下文进入成员管理页;所有现有导航同步指向唯一 owner 页面。
|
||||
|
||||
### 6. 保持阻断的页面和能力
|
||||
|
||||
以下能力在后端没有 PC Controller,不实施假功能:
|
||||
|
||||
- `profile-invite.html`:没有成员邀请、邀请令牌或邀请链接接口
|
||||
- `profile-share.html`:没有家谱分享、二维码或奖励记录接口
|
||||
- `profile-data-reminders.html`:没有资料完善提醒查询或配置接口
|
||||
- 个人资料地区保存:`ProfileUpdateBody` 没有地区字段
|
||||
|
||||
后端备忘录和成长记录的 `remindTime` 继续由已经开放的对应页面维护,不把它们误作“资料完善提醒”接口。
|
||||
|
||||
## 页面脚本边界
|
||||
|
||||
- 扩展 `public/js/genealogy-entry-pages.js`:家谱创建、我的加入申请、撤销申请。
|
||||
- 新增 `public/js/join-review-pages.js`:待审核申请与审核。
|
||||
- 新增 `public/js/feedback-pages.js`:反馈提交、列表和基于列表的详情匹配。
|
||||
- 新增 `public/js/vip-pages.js`:套餐、订单创建和订单列表。
|
||||
- 新增 `public/js/family-home-pages.js`:家谱 overview 展示。
|
||||
- `public/js/member-admin-pages.js` 继续作为家谱成员权限唯一实现,不复制到旧权限页。
|
||||
|
||||
每个脚本导出纯规范化、校验、渲染函数和页面初始化函数,沿用 UMD 结构、`ProfileUI.requireGenealogyContext()`、401/403 分流、可见文本转义、稳定字符串 ID、防重复提交和写后重读。
|
||||
|
||||
## 错误和隐私
|
||||
|
||||
- 401 清理登录态;403 保留登录态并展示权限不足。
|
||||
- 没有家谱上下文时不发送家谱业务请求。
|
||||
- 所有写操作使用单一 `writePending` 锁,提交按钮同步禁用。
|
||||
- 后端响应缺少必需稳定 ID、返回不安全数字长 ID、状态不在已确认枚举内时,整条记录拒绝渲染。
|
||||
- 普通页面不展示内部用户 ID、处理人 ID、OSS ID、原始 JSON。
|
||||
- 创建、申请、审核、反馈和订单成功后均重读对应资源,不能仅凭成功提示宣称完成。
|
||||
|
||||
## 测试与阶段顺序
|
||||
|
||||
每个阶段严格执行 RED → GREEN → 聚焦回归:
|
||||
|
||||
1. ApiClient 契约:新增 method/path/query/body 白名单测试。
|
||||
2. 家谱创建:额度、地区选择、上传派生封面、成功上下文。
|
||||
3. 加入申请:申请、我的申请、撤销、审核和权限。
|
||||
4. 反馈与工单:共享 DTO、列表详情匹配、隐私和无独立 ticket path。
|
||||
5. VIP:套餐选择、订单白名单、待支付边界、无伪支付。
|
||||
6. 家谱主页与权限入口:overview、上下文传播、唯一成员管理入口。
|
||||
7. 更新 pending/PC scope/navigation 测试,运行全量测试并使用真实账号验证无家谱和有数据分支;没有测试数据时明确记录未覆盖,不制造记录。
|
||||
@@ -1,119 +0,0 @@
|
||||
# 阶段 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或静态业务数据。
|
||||
- 所有写操作防重复并在成功后重新读取。
|
||||
- 仍缺后端能力的功能保持明确禁用并写入规划阻断项。
|
||||
@@ -1,170 +0,0 @@
|
||||
# 官网资讯 PC 接口对接设计
|
||||
|
||||
## 1. 目标
|
||||
|
||||
把现有静态 `news.html` 和 `article-detail.html` 接入后端已经存在的 PC 站点文章接口,形成“资讯列表 → 文章详情”的公开只读闭环。
|
||||
|
||||
本批只处理官网资讯,不修改关于我们、家族文化、隐私政策、用户协议、姓氏百科及个人中心谱文 CRUD。
|
||||
|
||||
## 2. 契约来源
|
||||
|
||||
后端只读项目:`D:\WorkSpace\Java\Genealogy`。
|
||||
|
||||
接口:
|
||||
|
||||
| method | path | 参数 | 响应 |
|
||||
| --- | --- | --- | --- |
|
||||
| GET | `/genealogy/pc/site/articles` | Query `articleType?`、`limit?` | 直接 `SiteArticleVo[]` |
|
||||
|
||||
该操作在 YAML 中标记为 `security: []`,后端 `PcSiteContentController` 也没有页面侧登录要求。本批按公开接口处理:`ApiClient` 为该请求设置 `auth: false`,不携带或读取登录 token;公共请求层仍自动携带基础 `clientid` Header。
|
||||
|
||||
后端没有站点文章单条详情接口。详情页必须重新读取文章列表,再按 URL 中由列表链接产生的 `articleId` 精确匹配;不存在时显示明确的未找到状态,不得回退到第一条文章。
|
||||
|
||||
## 3. SQL 冻结值
|
||||
|
||||
文章类型字典 `gen_site_article_type`:
|
||||
|
||||
| 值 | 含义 |
|
||||
| --- | --- |
|
||||
| `news` | 新闻,默认 |
|
||||
| `notice` | 公告 |
|
||||
| `download` | 下载 |
|
||||
|
||||
`gen_site_article` 的页面相关约束:
|
||||
|
||||
| 字段 | SQL 约束 |
|
||||
| --- | --- |
|
||||
| `article_id` | `BIGINT NOT NULL`,主键 |
|
||||
| `article_type` | `varchar(50) NOT NULL DEFAULT 'news'` |
|
||||
| `article_title` | `varchar(200) NOT NULL` |
|
||||
| `article_summary` | `varchar(500) DEFAULT ''` |
|
||||
| `article_content` | `text`,可空 |
|
||||
| `cover_oss_id` | `BIGINT`,可空 |
|
||||
| `external_url` | `varchar(500) DEFAULT ''` |
|
||||
| `publish_time` | `datetime`,可空 |
|
||||
| `sort_order` | `BIGINT NOT NULL DEFAULT 0` |
|
||||
| `status` | `char(1) NOT NULL DEFAULT '0'`,`0` 正常、`1` 停用 |
|
||||
| `remark` | `varchar(500) DEFAULT NULL` |
|
||||
|
||||
服务端只返回正常状态记录,按 `sortOrder ASC`、`publishTime DESC`、`articleId DESC` 排序。正数 `limit` 最大按 100 执行;本批固定发送 `limit=100`。
|
||||
|
||||
## 4. 前端 owner
|
||||
|
||||
| 文件 | 职责 |
|
||||
| --- | --- |
|
||||
| `utils/ApiClient.js` | 唯一拥有 `/genealogy/pc/site/articles` 请求路径、Query 白名单与枚举校验 |
|
||||
| `utils/AxiosRequestUtil.js` | 确保 `auth: false` 的公开请求不携带 token,且其 401 不清理用户在其他页面建立的登录态 |
|
||||
| `public/js/site-news-pages.js` | 唯一拥有站点文章响应规范化、列表/详情渲染、URL ID 解析和页面初始化 |
|
||||
| `news.html` | 只提供资讯列表、分类入口及加载/空/失败挂载点 |
|
||||
| `article-detail.html` | 只提供公开文章详情挂载点 |
|
||||
|
||||
不得复用 `public/js/article-pages.js`。该脚本属于登录后的家谱谱文 CRUD,契约、权限和数据结构均不同。
|
||||
|
||||
## 5. 页面数据流
|
||||
|
||||
### 5.1 资讯列表
|
||||
|
||||
1. `news.html` 初始化读取 URL Query `articleType`。
|
||||
2. Query 为空时不发送 `articleType`,读取全部三类文章;非空时只接受 `news/notice/download`。
|
||||
URL 中出现其他值时显示“资讯分类无效”且不发请求。
|
||||
3. 请求固定携带 `limit=100`。
|
||||
4. 每个列表项使用响应中的字符串 `articleId` 生成 `article-detail.html?articleId=...`。
|
||||
5. 分类入口只生成本地固定链接:
|
||||
- 全部:`news.html`
|
||||
- 新闻:`news.html?articleType=news`
|
||||
- 公告:`news.html?articleType=notice`
|
||||
- 下载:`news.html?articleType=download`
|
||||
6. 空数组显示“当前暂无资讯”;畸形响应或网络失败显示区域错误,不跳转。
|
||||
|
||||
### 5.2 文章详情
|
||||
|
||||
1. `articleId` 只能来自 URL Query,并按非零十进制字符串校验,禁止转成 JavaScript Number。
|
||||
2. 页面调用同一个文章列表接口,固定 `limit=100`,不制造不存在的单条详情路径。
|
||||
3. 只接受 `articleId` 完全相同的文章。
|
||||
4. 缺失或非法 ID 显示“资讯编号无效”;合法但未匹配显示“资讯不存在或已下线”。
|
||||
5. 页面不提供 ID 输入框、编辑、发布、删除、状态切换或 OSS ID 输入。
|
||||
|
||||
## 6. 字段分类与展示
|
||||
|
||||
| 字段 | 分类 | 页面使用 |
|
||||
| --- | --- | --- |
|
||||
| `articleId` | I | 列表链接和详情精确匹配;不直接展示 |
|
||||
| `articleType` | R | 显示中文类型标签;只接受 SQL 字典枚举 |
|
||||
| `articleTitle` | R | 列表和详情标题,必须非空 |
|
||||
| `articleSummary` | R | 列表摘要和详情导语,可空 |
|
||||
| `articleContent` | R | 详情正文,可空 |
|
||||
| `coverOssId` | I | 没有文件 URL 契约,不显示、不拼接、不手填 |
|
||||
| `externalUrl` | R | 仅接受绝对 HTTP/HTTPS;合法时显示“查看外部内容” |
|
||||
| `publishTime` | R | 可空;按后端 `yyyy-MM-dd HH:mm:ss` 文本安全展示 |
|
||||
| `sortOrder` | I | 仅后端排序,不展示 |
|
||||
| `status` | I | 只接受 `0`,其他值使整批响应失败 |
|
||||
| `remark` | I | 后台备注,不展示 |
|
||||
|
||||
`articleType` 是列表页 Query 时分类为 S,由用户点击固定分类入口产生;`limit=100` 分类为 A,由系统自动提交。
|
||||
|
||||
## 7. 安全与渲染
|
||||
|
||||
- 所有业务 ID 始终保持字符串。
|
||||
- 标题、摘要、正文、类型和时间全部 HTML 转义。
|
||||
- 正文只保留换行,不执行后端返回的 HTML、脚本或事件属性。
|
||||
- `externalUrl` 只允许绝对 `http:` 或 `https:`,并使用 `target="_blank"` 与 `rel="noopener noreferrer"`。
|
||||
- 任一列表元素缺少稳定 ID、合法类型、标题或正常状态时,整批响应失败。
|
||||
- 响应不接受分页对象、`rows` 包装或旧字段别名。
|
||||
- 页面公开访问,请求显式使用 `auth: false`;公开请求的 401/403 不清理既有 token,也不跳转登录页。
|
||||
|
||||
## 8. HTML 迁移
|
||||
|
||||
`news.html` 删除两条硬编码新闻和固定分类卡片,改为:
|
||||
|
||||
- `data-site-news-page`
|
||||
- `data-site-news-list`
|
||||
- `data-site-news-status`
|
||||
- 固定四个分类链接
|
||||
- 加载完整 API 基础脚本和 `public/js/site-news-pages.js`
|
||||
|
||||
`article-detail.html` 保留现有视觉结构,将硬编码示例正文改为:
|
||||
|
||||
- `data-site-article-page`
|
||||
- `data-site-article-detail`
|
||||
- `data-site-article-status`
|
||||
- 加载 `public/js/site-news-pages.js`
|
||||
|
||||
现有 `notice-detail.html` 本批不改。后端的公告也是 `SiteArticleVo`,统一进入 `article-detail.html`,避免维护两个详情模板。
|
||||
|
||||
## 9. 错误与空状态
|
||||
|
||||
| 场景 | 行为 |
|
||||
| --- | --- |
|
||||
| 列表空数组 | 显示“当前暂无资讯” |
|
||||
| 列表响应畸形 | 显示“资讯列表响应无效” |
|
||||
| 网络或业务失败 | 显示“资讯读取失败,请稍后重试” |
|
||||
| 详情 ID 缺失或非法 | 显示“资讯编号无效”且不请求 |
|
||||
| 详情未匹配 | 显示“资讯不存在或已下线” |
|
||||
| 危险外链 | 不渲染外链入口,正文仍可查看 |
|
||||
| 401/403 | 作为公开内容读取失败处理,不清理登录态、不跳转 |
|
||||
|
||||
## 10. 测试与验收
|
||||
|
||||
必须以 TDD 覆盖:
|
||||
|
||||
1. `ApiClient` 使用公开 PC path,只发送 `articleType/limit`,设置 `auth: false`,拒绝非法类型与超范围 limit。
|
||||
2. 公共请求即使收到 401 也不清理既有登录 token。
|
||||
3. 长 ID 保持字符串,非法安全整数被拒绝。
|
||||
4. `SiteArticleVo` 全字段规范化与直接数组校验。
|
||||
5. 任一非法元素使整批失败。
|
||||
6. 列表链接只使用响应 ID,页面无手填 ID/OSS ID。
|
||||
7. 详情精确匹配,不回退。
|
||||
8. 正文转义、换行保留、危险外链拒绝。
|
||||
9. `news.html` 和 `article-detail.html` 加载新脚本,且不加载家谱谱文 CRUD 脚本。
|
||||
10. 先运行资讯专项测试,再运行全量 `node --test tests/*.test.js`。
|
||||
11. 使用浏览器验证公开列表、分类链接、详情或真实空状态,控制台无业务错误。
|
||||
|
||||
## 11. 明确不做
|
||||
|
||||
- 不修改后端。
|
||||
- 不新增文章管理能力。
|
||||
- 不接 APP 或后台管理接口。
|
||||
- 不把 `coverOssId` 猜成文件 URL。
|
||||
- 不接 `about/privacy/agreement` 固定页面。
|
||||
- 不修改 `culture.html` 和首页文章卡片;它们作为后续独立批次。
|
||||
- 不修改法律页“待法务确认”状态。
|
||||
@@ -1,58 +0,0 @@
|
||||
# 个人中心能力映射
|
||||
|
||||
## 使用边界
|
||||
|
||||
参考项目固定为 `C:\Users\Rain\Desktop\job\Jiapu-App`,只用于核对产品信息架构。本项目的真实读取、写入和权限判断只使用当前 PC 契约及 `utils/ApiClient.js`;不得复制 App 请求路径、参数、缓存编号或测试身份信息。
|
||||
|
||||
没有当前 PC 接口契约的能力必须保留明确的不可操作状态,不能以静态数据、二维码、金额或成功提示替代。
|
||||
|
||||
## 账户与服务
|
||||
|
||||
| 参考能力 | PC 页面 | 当前状态 |
|
||||
| --- | --- | --- |
|
||||
| 账户概览、个人资料 | `profile.html`、`profile-data.html` | 读取并编辑当前 PC 资料字段;未声明的地区和个人亲属列表不伪造。 |
|
||||
| 修改密码、换绑手机、注销、退出登录 | `profile-security.html` | 使用认证 PC 契约;表单具备校验、提交中和失败反馈。 |
|
||||
| 意见反馈、我的工单 | `profile-feedback.html`、`submit-ticket.html`、`my-tickets.html`、`ticket-detail.html` | 使用当前账号的反馈集合,提交后重读确认。 |
|
||||
| 帮助中心、平台联系与协议 | `help.html`、`profile-services.html` | 帮助文章读取使用 PC 契约;平台说明使用站内页面。 |
|
||||
| 会员套餐与订单 | `profile-services.html` | 读取购买能力、套餐与订单,创建微信 NATIVE 支付单并生成扫码二维码,可查询或关闭支付。 |
|
||||
| 推广收益、提现 | `profile-earnings.html`、`profile-share.html`、`profile-services.html` | 读取余额、不可变流水和提现记录;收款码由统一上传接口产生 OSS ID,提现请求使用稳定幂等号。 |
|
||||
| 分享奖励、推荐偏好 | `profile-share.html`、`profile-services.html` | 当前 PC 无对应读取或写入契约,明确不可操作。 |
|
||||
|
||||
## App 差异与承载决策
|
||||
|
||||
| App 能力或入口 | 核对结果 | PC 承载方式 |
|
||||
| --- | --- | --- |
|
||||
| 家谱功能菜单 | 字辈、世系、谱文、家族圈、相册、视频、功德、祭祀、成长、证件、备忘、亲友、成员管理、会员与订单均已有真实 PC 闭环。 | 保留在 `profile-family-home.html` 的桌面工作台,不新增重复总览页。 |
|
||||
| 发布动态、邀请家人 | PC 已有独立表单和一次性邀请闭环。 | 使用 `profile-feed-edit.html`、`profile-invite.html` 独立页面;两者都有多字段或需要上下文,不使用弹窗。 |
|
||||
| 应用分享与邀请奖励 | App 通过用户编号在客户端拼接分享地址;后端只有 App 推荐关系读取接口,没有 PC 分享链接契约。 | `profile-share.html` 只说明移动端分享边界,并引导到 `profile-earnings.html` 查看真实收益和提现;不伪造二维码,也不新增页面。 |
|
||||
| 个性化推荐开关 | App 页面没有持久化请求,不能视为已完成能力。 | `profile-services.html` 使用不可操作状态;待后端提供 PC 读写契约后再原位开放,不新增设置页。 |
|
||||
| 个人亲属关系 | App 对应页面是静态演示数据,真实关系能力属于家谱世系。 | `profile-data.html` 只做引导,维护操作进入 `profile-tree.html` 或 `profile-family-admin.html`;不复制演示页。 |
|
||||
| 分享展示页、祖先排行 | App 当前分别为静态占位和空页面。 | 不移植,不制造入口、假排名或假用户数据。 |
|
||||
| 余额、提现、购买记录 | App 与 PC 都有真实业务能力,PC 契约和桌面流程已完整。 | 收益与提现集中在 `profile-earnings.html`;会员、支付和订单集中在 `profile-services.html`。支付确认适合当前页扫码区,关闭订单使用确认弹窗。 |
|
||||
| 账号安全与注销 | PC 已有完整表单和危险操作确认。 | 集中在 `profile-security.html`;注销使用确认弹窗,短信、密码等多字段继续使用页面内表单。 |
|
||||
|
||||
## 家谱工作台
|
||||
|
||||
| 参考能力 | PC 页面 | 当前状态 |
|
||||
| --- | --- | --- |
|
||||
| 家谱列表、创建、加入、我的申请 | `profile-families.html`、`profile-create-family.html`、`profile-join-family.html` | 使用真实家谱列表、额度和申请契约;家谱编号只来自上下文或响应。 |
|
||||
| 当前家谱主页 | `profile-family-home.html` | 以概览和世系树组成桌面工作台,并透传当前家谱上下文。 |
|
||||
| 成员名册、角色、谱主转移、入谱审核 | `profile-family-admin.html`、`profile-admin-permissions.html`、`profile-join-review.html` | 成员维护和审核只由服务端能力字段决定。 |
|
||||
| 世系人物与父母、配偶、子女、兄弟姐妹关系 | `profile-tree.html` | 列表、详情、编辑和四类关系维护使用世系 PC 契约。 |
|
||||
| 字辈 | `profile-generation.html` | 字辈列表、单条和批量维护使用当前 PC 契约。 |
|
||||
| 家族圈 | `profile-feed.html`、`profile-feed-detail.html`、`profile-feed-edit.html` | 动态、评论、回复和点赞使用当前家谱上下文。 |
|
||||
| 谱文、相册、视频 | `profile-article.html`、`profile-album.html`、`profile-video.html` 及各自编辑/详情页 | 已提供列表、详情和编辑闭环;没有文件访问 URL 时只展示安全业务元数据。 |
|
||||
| 祭祀、贺礼邀约 | `profile-ceremony.html`、`profile-ceremony-detail.html`、`profile-gift.html`、`profile-gift-edit.html` | 活动、祭品和当前账号邀约使用当前 PC 契约。 |
|
||||
| 成长、亲友、备忘、功德记录 | 对应 `profile-*-edit.html` 与列表页 | 各自使用当前 PC CRUD 契约和真实家谱上下文。 |
|
||||
| 成员邀请 | `profile-invite.html` | 生成一次性邀请、预览/兑换、查看本人邀请及撤销待使用邀请。 |
|
||||
| 资料提醒 | `profile-data-reminders.html` | 按当前家谱读取成员资料缺项,并跳转世系维护。 |
|
||||
| 重要证件 | `profile-documents.html` | 按人物维护档案和附件,使用业务字典、统一上传、临时访问令牌与密码保护。 |
|
||||
| 家谱生命周期 | `profile-family-settings.html` | 支持归档、恢复;永久删除先读取能力并校验家谱全名和短信验证码。 |
|
||||
| 家谱排序 | `profile-families.html` | 使用“我的家谱”响应生成选择项,支持上移、下移并保存稳定 ID 顺序。 |
|
||||
| 个人亲属关系页 | `profile-data.html` | 当前 PC 未提供个人关系列表;页面明确引导至当前家谱的成员管理与世系维护。 |
|
||||
|
||||
## 验收边界
|
||||
|
||||
- 页面统一提供加载、空数据、请求失败和无权限状态;写入表单提供字段标签、校验、提交中和失败反馈。
|
||||
- 所有家谱内操作由同一当前家谱上下文提供稳定字符串 ID,不接受手工填写业务 ID 或 OSS ID。
|
||||
- 权限以 PC 家谱详情的严格布尔能力字段和服务端写入结果为准;没有隔离管理员或普通成员测试实体时,运行时角色验收记录原因后跳过,不创建测试数据。
|
||||
+45
-161
@@ -1,183 +1,67 @@
|
||||
# PC 接口对接交接文档
|
||||
# 家谱 PC 前端交接
|
||||
|
||||
> 更新:2026-07-25
|
||||
> 历史交接记录:本文的 79/85 条接口数量已过期,不再作为 AI 施工依据。当前接口基线、字段分类、阻断项和执行顺序统一以 [PC接口对接规划.md](PC接口对接规划.md) 为准;执行前仍须在 Apifox 实时复核并重新导出。
|
||||
> **失效范围:** 本文第二至第七节均为当日历史快照,可能包含已被现行 PC 契约和页面实现替代的接口、页面状态与验收结论;不得据此新增、关闭或阻断任何功能。
|
||||
更新日期:2026-09-17
|
||||
|
||||
## 一、必须遵守的范围
|
||||
## 项目是什么
|
||||
|
||||
1. Apifox 的 **PC 目录**是唯一正式契约源。
|
||||
2. 本地 OpenAPI 导出文件可能少接口,只能辅助查找,不能据此决定接口存在、路径或字段。
|
||||
3. 不使用 APP 接口,也不能把 `/app/` 改为 `/pc/` 后猜测使用。
|
||||
4. 继续任何新接口前,必须直接在 Apifox PC 详情核对 method、path、query、body、响应和权限。
|
||||
5. 家谱业务只能使用真实 `?genealogyId=...`;不写死编号、不从 APP 获取。
|
||||
6. 页面不直接调用 Axios、不自行拼请求路径,统一通过 `utils/ApiClient.js`。
|
||||
这是一个静态 HTML 多页面项目。页面直接加载 CSS 和 JavaScript,没有 React、Vue 或单页应用入口。
|
||||
|
||||
## 二、当前目录与历史施工范围
|
||||
官网页面在根目录,例如 `index.html`、`login.html`、`genealogy.html`。登录后的个人中心和家谱功能页面以 `profile-` 开头。
|
||||
|
||||
### 当前 PC 目录(2026-07-25)
|
||||
## 主要目录
|
||||
|
||||
| 分组 | 条数 | 当前处理状态 |
|
||||
| --- | ---: | --- |
|
||||
| 验证中心 | 3 | 已逐条复核并迁移 |
|
||||
| 认证登录 | 12 | 已逐条复核并迁移;其中两条短信发送定义同为当前 `operationCode` 路径 |
|
||||
| 文件上传 | 3 | 已逐条复核、`ApiClient` 仅保留分片三条;初始化响应未展开,头像上传保持阻止 |
|
||||
| 家谱 | 1 | 已核验并映射配额查询;入口页仅展示配额并阻止进入需 `genealogyId` 的业务页,不能伪造编号 |
|
||||
| 家族圈 | 14 | 已逐条复核;动态、点赞、评论、直接回复及两类分页均与当前实现一致 |
|
||||
| 行政区划 | 4 | 已逐条复核;当前 PC 目录下实际路径均为 `/genealogy/region/*`,与 `ApiClient` 一致 |
|
||||
| 字辈谱 | 6 | 已逐条复核;正常查询、维护、新增、批量预览/保存和修改/停用/恢复均与当前实现一致 |
|
||||
| 世系人物 | 12 | 已逐条复核;人物列表/分页/选项/树、CRUD/停用和四种新增关系均与当前实现一致 |
|
||||
| 内容文章 | 1 | 已核验;仅删除,页面继续关闭危险入口 |
|
||||
| 相册 | 2 | 已核验;仅删除,页面继续关闭危险入口 |
|
||||
| 视频 | 1 | 已核验;仅删除,页面继续关闭危险入口 |
|
||||
| 贺礼邀约 | 4 | 已逐条核验并映射;缺少活动列表/创建/详情,页面保持预览 |
|
||||
| 祭祀 | 2 | 已核验;仅删除活动/祭品,页面继续关闭危险入口 |
|
||||
| 族务记录 | 16 | 已逐条复核;成长记录、亲友往来、备忘录各 5 条与功德删除 1 条均保持当前契约 |
|
||||
| 消息通知 | 4 | 已逐条核验并映射;消息页已接入原始列表、未读数和全部已读 |
|
||||
| 合计 | 85 | 以桌面版 PC 目录为准 |
|
||||
|
||||
### 历史 79 条施工清单(2026-07-24,已被当前目录基线替代)
|
||||
|
||||
| 分组 | 条数 | 状态 |
|
||||
| --- | ---: | --- |
|
||||
| 验证中心 | 4 | 已核验、已接入 |
|
||||
| 认证登录 | 11 | 已核验、已接入 |
|
||||
| 文件上传 | 6 | 已核验;仅单文件上传页面闭环开放 |
|
||||
| 行政区划 | 4 | 已核验、已接入 |
|
||||
| 家族圈 | 14 | 已核验、已接入 |
|
||||
| 字辈谱 | 6 | 已核验、已接入 |
|
||||
| 世系人物 | 12 | 已核验、已接入 |
|
||||
| 族务记录·成长记录 | 5 | 已核验、`ApiClient` 已映射;页面开放列表与新增 |
|
||||
| 族务记录·亲友往来 | 5 | 已核验、`ApiClient` 已映射;页面开放列表与新增 |
|
||||
| 族务记录·备忘录 | 5 | 已核验、`ApiClient` 已映射;页面开放列表与新增 |
|
||||
| 族务记录·功德记录 | 1 | 已核验、`ApiClient` 已映射;缺少真实列表/详情,页面不开放删除 |
|
||||
| 内容文章 | 1 | 已核验、`ApiClient` 已映射;缺少真实列表/详情,页面不开放删除 |
|
||||
| 相册 | 2 | 已核验、`ApiClient` 已映射;缺少真实列表/详情,页面不开放删除 |
|
||||
| 视频 | 1 | 已核验、`ApiClient` 已映射;缺少真实列表/详情,页面不开放删除 |
|
||||
| 祭祀 | 2 | 已核验、`ApiClient` 已映射;缺少真实列表/详情,页面不开放删除 |
|
||||
| 合计 | 79 | |
|
||||
|
||||
完整映射见 [PC接口对接规划.md](PC接口对接规划.md)。
|
||||
|
||||
### 已核验关键规则
|
||||
|
||||
- 验证中心现为三条 PC 接口:`GET /genealogy/pc/auth/verification/{operationCode}/require`、`POST .../{operationCode}/challenge`、`POST .../{operationCode}/verify`。`operationCode` 仅允许 `password-login`、`sms-login`、`register`、`forgot-password`、`phone-change`、`account-deactivate`;前端不得提交旧 `sceneCode`。
|
||||
- `require` 的 query 为必填 `tenantId` 和可选 `subject`;challenge 的 body 为必填 `tenantId`、`subject`;verify 的 body 在此基础上增加 `challengeId`,可附 `providerCode`、`captchaType`、`payload`。三条都由请求头传 `clientid`,不得在 query/body 传 `clientId`。
|
||||
- 短信发送为 `POST /genealogy/pc/auth/sms/{operationCode}/code`,支持除 `password-login` 外的五个短信动作。body 使用 `grantType: "sms"`、`tenantId`、`phone` 和策略开启时的 `validToken`;`clientid` 只走请求头。
|
||||
- 当前认证登录的上述已核验接口一律要求 `clientid` 请求头,且 body 明确禁止 `clientId`。注册/密码登录/找回密码的 body 为 `grantType: "password"`、`tenantId` 加业务字段;密码登录在验证中心策略要求时还提交本次验证取得的 `validToken`;短信登录为 `grantType: "sms"`、`tenantId`、`phone`、`smsCode`;换绑为 `phone`、`smsCode`;注销为 `smsCode`。
|
||||
- 族务记录当前 16 条由成长记录、亲友往来、备忘录三套完整 CRUD(各 5 条)和 `DELETE .../merit-records/{meritId}` 组成。三套 CRUD 都要求 `genealogyId`,详情/修改/删除还要求各自记录 ID;列表元素与新增/详情响应 DTO 仍未展开,页面只展示原始记录并保持无稳定 ID 的编辑、详情、删除入口关闭。
|
||||
- 行政区划四条当前路径为 `GET /genealogy/region/children`(可选 `parentCode`)、`GET /genealogy/region/path/{regionCode}`、`GET /genealogy/region/search`(必填 `keyword`,可选 `level`、`limit`)、`GET /genealogy/region/{regionCode}`;均已与 `ApiClient` 核对一致。
|
||||
- 字辈谱 6 条当前路径分别为 `GET .../generation-poems`、`GET .../generation-poems/management`、`POST .../generation-poems`、`POST .../generation-poems/batch/preview`、`POST .../generation-poems/batch/save`、`PUT .../generation-poems/{poemId}`;实现仍只使用其明确 DTO 字段。
|
||||
- 世系人物 12 条当前包含 `persons` 列表/新增、`persons/page`、`persons/options`、`tree`、人物详情/修改/停用,以及 `children`、`parents`、`siblings`、`spouses` 四种以完整 `LineagePersonBody` 新建关系人物的接口;`genealogyId` 和 `personId` 均保持真实入口传入。
|
||||
- 内容文章、相册、视频和祭祀当前 6 条均为删除孤岛:`DELETE .../articles/{articleId}`、`.../albums/{albumId}`、`.../albums/{albumId}/photos/{photoId}`、`.../videos/{videoId}`、`.../ceremonies/{ceremonyId}`、`.../ceremonies/{ceremonyId}/gifts/{giftId}`。它们已映射但无当前 PC 列表/详情来源,不能开放页面删除。
|
||||
- 文件上传现仅有 `POST /genealogy/pc/files/resumable/init`、`POST .../chunk`、`POST .../complete`。所有文件统一先初始化;普通小文件可按初始化响应的 `instant=true` 直接取 OSS 信息,但该响应的字段 Schema 尚未展开,页面不得猜测 `uploadId`、`ossId` 或 `instant`,头像上传继续阻止。
|
||||
- 家谱当前只提供 `GET /genealogy/pc/genealogies/quota`,返回创建/加入的已用数量、上限、剩余额度以及 `canCreate`/`canJoin`。它不返回家谱对象或 `genealogyId`;只映射为 `genealogyQuota`,不据此开放家谱业务页面。
|
||||
- 贺礼邀约四条为替换受邀人、查询活动邀请名单、当前用户接受/拒绝、查询我的邀请。受邀人 body 只允许 `inviteeUserIds`,当前用户响应 body 只允许 `inviteStatus: ACCEPTED|DECLINED`。缺少活动列表/创建/详情,`profile-gift*.html` 继续为明确的待开发预览。
|
||||
- 消息通知为列表(可选 `readStatus: 0|1`)、未读数量、单条已读和全部已读。列表元素 DTO 未展开,`profile-messages.html` 只安全展示服务端原始记录,不猜测标题、内容、时间或单条 `notificationId`;未读数和全部已读已开放。
|
||||
- 认证相关均使用 PC 路径;密码为 32 位 MD5。改绑/注销只使用 PC DTO 所需字段,不擅自加 `tenantId`。
|
||||
- 家族圈 14 条当前路径为动态列表/分页/详情/增改删、点赞/取消点赞、一级评论列表/分页/发表/删除和直接回复列表/分页;均要求真实 `genealogyId`,现有 `ApiClient` 与页面调用已逐项比对一致。新评论只提交必填 `commentContent` 和可选 `parentCommentId`,不能发旧字段 `content`、`replyUserId`。
|
||||
- 字辈管理列表使用 `GET .../generation-poems/management`;批量操作使用 `poemText` 和可选 `disableMissing`。单项最多 50 字符、一次最多 500 代、总长最多 26000。
|
||||
- 世系分页 query:`pageNum`、`pageSize`、`keyword`、`generation`、`personStatus`;`keyword` 只查姓名、别名、人物编号。
|
||||
- 世系写入字段是 `name`、`generation`、`biography`,不是旧字段 `personName`、`generationNo`、`introduction`;`sortOrder`、`relationName` 是可选字段。
|
||||
- `DELETE .../lineage/persons/{personId}` 是逻辑停用,不是物理删除;有正常子女时后端会拒绝。
|
||||
- 新增父母/配偶/兄弟姐妹/子女四个关系接口均接收完整 `LineagePersonBody` 来新建关系人物,并非绑定两个已有 ID。
|
||||
- 已末次回到 Apifox 复核:世系分页 query、创建人物 body、`POST .../lineage/persons/{personId}/spouses` 和 `relationName`。
|
||||
- 成长记录 5 条均为登录接口,路径固定为 `/genealogy/pc/genealogies/{genealogyId}/growth-records` 及 `/{recordId}`;`genealogyId`、`recordId` 均是必填 int64 路径参数,`clientid` 为必填请求头。
|
||||
- 成长记录的 `GrowthRecordBody` 只有 `recordTitle` 必填;可选字段为 `lineagePersonId`、`recordType`、`recordContent`、`recordDate`、`remindTime`、`mediaOssIds`、`sortOrder`、`status`。`mediaOssIds` 只接受英文逗号分隔的正整数 OSS ID。
|
||||
- 成长记录列表响应是未展开元素 DTO 的 `ListResult`,详情/新增/修改为未展开元素 DTO 的 `ObjectResult`,删除为 `VoidResult`。不能据此猜测 `recordId`、标题或权限字段;页面仅作原始列表展示和新增,不开放编辑、详情或删除入口。
|
||||
- 亲友往来 5 条均为登录接口,路径固定为 `/genealogy/pc/genealogies/{genealogyId}/relative-records` 及 `/{relativeId}`;`RelativeRecordBody` 只有 `relativeName` 必填,可选 `relationName`、`eventName`、`eventTime`、`giftAmount`、`recordContent`、`mediaOssIds`、`sortOrder`、`status`。列表/详情元素 DTO 同样未展开,页面不开放编辑、详情或删除入口。
|
||||
- 备忘录 5 条均为登录接口,路径固定为 `/genealogy/pc/genealogies/{genealogyId}/memos` 及 `/{memoId}`;`genealogyId`、`memoId` 均是必填 int64 路径参数,`clientid` 为必填请求头。备忘录请求体只有 `memoTitle` 必填,可选 `memoContent`、`remindTime`、`completed`、`mediaOssIds`、`sortOrder`、`status`;`completed` 在 Apifox 中是 string,不能擅自改成布尔值。列表/详情元素 DTO 未展开,页面仅作原始列表展示和新增。
|
||||
- 功德记录在 PC 目录中当前只存在 `DELETE /genealogy/pc/genealogies/{genealogyId}/merit-records/{meritId}`;两个路径参数均为必填 int64,登录和 `clientid` 必填,响应为 `VoidResult`。`deleteMeritRecord` 已映射,但页面没有真实列表、详情或稳定 `meritId` 来源,必须保持删除入口关闭。
|
||||
- 其余 6 个删除孤岛接口也已核验并映射:`deleteArticle`、`deleteAlbum`、`deleteAlbumPhoto`、`deleteVideo`、`deleteCeremony`、`deleteCeremonyGift`。它们都要求登录、`clientid` 和 int64 路径 ID,响应均为 `VoidResult`;相册、视频与祭祀删除会由后端释放相应文件引用。由于没有 PC 列表/详情和稳定 ID 来源,所有相关页面继续保持预览,不能触发删除。
|
||||
|
||||
## 三、已经落地的页面
|
||||
|
||||
### 账号与资料
|
||||
|
||||
- `login.html`:密码登录、短信登录。
|
||||
- `register.html`:短信注册。
|
||||
- `forgot-password.html`:短信重置密码。
|
||||
- `profile-security.html`:改密码、改绑手机、注销账号。
|
||||
- `profile-data.html` / `profile.html`:资料读取保存、行政区划;头像上传等待当前 PC 分片初始化响应 Schema 补齐。
|
||||
|
||||
### 家谱业务
|
||||
|
||||
- `profile-feed.html` / `profile-feed-edit.html`:家族圈列表、详情、发布/修改、点赞、评论、回复。
|
||||
- `profile-generation.html` + `public/js/generation-pages.js`:字辈管理列表、新增、编辑、停用/恢复、批量预览/保存。所有写操作有防重复提交锁;批量示例要求用空格或支持的标点分隔。
|
||||
- `profile-tree.html` + `public/js/lineage-pages.js`:成员总览、世系树、分页搜索/翻页、下拉选择、详情、新增/编辑、逻辑停用、父母/配偶/兄弟姐妹/子女新增。所有写操作有防重复提交锁;响应 int64 ID 只以安全字符串用于后续请求。
|
||||
- `profile-growth.html` / `profile-growth-edit.html` + `public/js/growth-pages.js`:成长记录列表和新增;写入仅使用 `GrowthRecordBody`,并有防重复提交锁。列表/详情元素 DTO 尚未展开,编辑、详情和删除保持关闭。
|
||||
- `profile-relative.html` / `profile-relative-edit.html` + `public/js/relative-pages.js`:亲友往来列表和新增;写入仅使用 `RelativeRecordBody`,并有防重复提交锁。列表/详情元素 DTO 尚未展开,编辑、详情和删除保持关闭。
|
||||
- `profile-memo.html` / `profile-memo-edit.html` + `public/js/memo-pages.js`:备忘录列表和新增;写入仅使用已核验的备忘录请求体字段,并有防重复提交锁。列表/详情元素 DTO 尚未展开,编辑、详情和删除保持关闭。
|
||||
|
||||
### 关键代码文件
|
||||
|
||||
| 文件 | 责任 |
|
||||
| 位置 | 用途 |
|
||||
| --- | --- |
|
||||
| `utils/ApiClient.js` | 已核验 PC 路径、请求头、DTO |
|
||||
| `public/js/profile-common.js` | `genealogyId` 读取和链接传播的唯一 owner |
|
||||
| `public/js/genealogy-entry-pages.js` | 无真实家谱上下文时的统一入口页;仅查询 PC 配额并阻止业务跳转 |
|
||||
| `public/js/auth-pages.js`、`captcha-pages.js`、`security-pages.js` | 登录、注册、验证码、账号安全 |
|
||||
| `public/js/profile-pages.js`、`region-pages.js`、`upload-pages.js` | 资料、区划、头像 |
|
||||
| `public/js/feed-pages.js` | 家族圈 |
|
||||
| `public/js/generation-pages.js` | 字辈谱 |
|
||||
| `public/js/lineage-pages.js` | 世系人物 |
|
||||
| `public/js/growth-pages.js` | 成长记录 |
|
||||
| `public/js/relative-pages.js` | 亲友往来 |
|
||||
| `public/js/memo-pages.js` | 备忘录 |
|
||||
| 根目录 `*.html` | 各页面入口 |
|
||||
| `public/css/` | 页面样式,`public.css` 是全局样式 |
|
||||
| `public/js/` | 页面逻辑、公共交互和第三方脚本 |
|
||||
| `public/images/` | 官网图片素材 |
|
||||
| `utils/` | 请求、存储、表单和提示工具 |
|
||||
| `config.js` | 前端接口配置 |
|
||||
| `scripts/build-minified.mjs` | 构建脚本 |
|
||||
| `dist/` | 构建产物;当前已清理,需要时重新构建 |
|
||||
|
||||
## 四、当前阻塞和未完成范围
|
||||
## 已清理内容
|
||||
|
||||
### PC 家谱入口缺失
|
||||
本次已删除测试文件、测试截图、临时接口响应、设计审查记录和旧文档。`dist/`、接口快照和 `package.json` 也已清理。仓库不再保留自动化测试,也没有 `npm test` 命令。
|
||||
|
||||
PC 目录没有“我的家谱、家谱详情、创建/加入/选择家谱”接口。前端无法自行得到真实 `genealogyId`,因此当前正确行为是:个人中心和内容入口先统一进入 `profile-families.html`,该页只读取 `genealogyQuota()` 并明确说明当前不能选择家谱;缺少 ID 时阻止业务请求。静态家谱卡片已移除,不要伪造编号,也不要用 APP 补这个缺口。
|
||||
后续不要在仓库中保存测试截图、测试账号信息、接口返回样本或重复的过程记录。需要说明验证结果时,写在当前提交或任务交付里即可。
|
||||
|
||||
### 已核验但仍缺业务闭环的页面范围
|
||||
## 请求和登录
|
||||
|
||||
| 分组 | 已核验条数 | 当前原则 |
|
||||
| --- | ---: | --- |
|
||||
| 内容文章 | 1 | 仅删除;没有真实列表/详情来源时不开放删除 |
|
||||
| 相册 | 2 | 仅删除;不猜 DTO 或文件引用来源 |
|
||||
| 视频 | 1 | 仅删除;不猜 DTO 或文件引用来源 |
|
||||
| 祭祀 | 2 | 仅删除;不猜实体和献礼字段 |
|
||||
| 功德记录 | 1 | 仅删除;不猜实体和 `meritId` 来源 |
|
||||
接口地址和请求封装在 `utils/ApiClient.js`、`utils/AxiosRequestUtil.js`、`config.js`。
|
||||
|
||||
## 五、通用行为和安全约束
|
||||
页面不要直接发 Axios 请求,也不要在页面里拼接口地址。新增接口时先在 `ApiClient` 中统一定义,再由页面调用。
|
||||
|
||||
- `ApiClient` 统一加 `clientid`;登录后统一加 `Authorization: Bearer <token>`。
|
||||
- 401 清 token 跳转 `login.html`;403 保留登录态并展示无权限状态。
|
||||
- 浏览器中的 int64 ID 均按字符串保存,避免精度丢失。
|
||||
- 当前接口没有定义的删除、解绑、关系解除和业务文件引用值,保持禁用/待开发,不能猜测实现。
|
||||
- 这是脏工作树;不要使用 `git reset --hard`、`git checkout --`,也不要删除未跟踪文件。
|
||||
登录信息由 `StorageUtil.js` 管理。登录后的请求会携带 Bearer Token;401 会清除登录态并跳转到 `login.html`;403 保留登录态并显示无权限状态。
|
||||
|
||||
## 六、验证结果
|
||||
后端返回的长整型 ID 在浏览器里按字符串处理,避免数字精度丢失。
|
||||
|
||||
本轮已直接完成当前 PC 目录 85/85 条的详情复核,并逐项比对 `ApiClient`、对应页面和契约测试;历史 79 条记录不作为本轮验收依据。完整测试结果以本轮末次运行记录为准。
|
||||
## 家谱业务规则
|
||||
|
||||
已运行的检查包括:
|
||||
家谱业务必须使用真实的 `genealogyId`。没有家谱上下文时,应阻止请求或提示用户选择家谱,不能写死示例 ID。
|
||||
|
||||
- `node --check utils/ApiClient.js`
|
||||
- `node --check public/js/profile-common.js`
|
||||
- `node --check public/js/genealogy-entry-pages.js`
|
||||
- `node --check public/js/generation-pages.js`
|
||||
- `node --check public/js/lineage-pages.js`
|
||||
- `node --check public/js/growth-pages.js`
|
||||
- `node --check public/js/memo-pages.js`
|
||||
- `node --check public/js/captcha-pages.js`
|
||||
- `node --check public/js/auth-pages.js`
|
||||
- `node --check public/js/security-pages.js`
|
||||
- `node --test tests/api-client-contract.test.js tests/captcha-pages-contract.test.js tests/auth-pages.test.js tests/security-upload-scope.test.js tests/profile-pages.test.js`
|
||||
- `node --test tests/auth-pages.test.js tests/api-client-contract.test.js tests/profile-pages.test.js tests/security-upload-scope.test.js tests/feed-pages.test.js tests/generation-pages.test.js tests/lineage-pages.test.js tests/growth-pages.test.js tests/relative-pages.test.js tests/memo-pages.test.js tests/pending-pages.test.js tests/pc-scope.test.js`
|
||||
- `npm.cmd test`(91/91 通过)
|
||||
- `git diff --check`
|
||||
接口字段、权限和接口是否存在,以 Apifox 的 PC 目录为准。不要从 APP 接口推测 PC 接口,也不要根据旧文件猜字段。
|
||||
|
||||
本地运行态检查已使用 Chrome DevTools 逐页打开 `profile-feed*`、成长记录、亲友往来、备忘录、消息、资料、字辈和世系页面:12 个静态页面均返回 200;无登录态时需鉴权的页面转到登录且没有发送业务请求,三类族务编辑页缺少真实 `genealogyId` 时保持阻止且不发请求。已使用测试账号完成密码登录的真实滑块验证与登录;资料页实际修改昵称后已恢复原值,页面两次均显示保存成功;消息页实际完成列表和未读数读取,服务端返回空列表。个人中心到家谱入口页的真实点击已验证,入口页仅调用配额接口且在缺少真实 `genealogyId` 时不进入家族业务。当前仍没有 PC 家谱列表/详情返回的真实家谱上下文,因此未对需 `genealogyId` 的写接口发送提交。
|
||||
接口还缺少列表、详情或删除能力时,页面应保持禁用或提示状态,不要补造数据把入口打开。
|
||||
|
||||
当前完整测试集已运行并通过。
|
||||
## 常用位置
|
||||
|
||||
## 七、下一位 GPT 的执行顺序
|
||||
- 个人中心通用布局和导航:`public/js/profile-common.js`
|
||||
- 个人中心样式:`public/css/profile.css`、`public/css/profile-module.css`
|
||||
- 富文本编辑:`public/js/rich-editor.js`、`public/js/wangeditor5/`
|
||||
- 附件上传:`public/js/attachment-editor.js`、`public/js/upload-pages.js`
|
||||
- Layui 配置:`public/js/lay-config.js`
|
||||
|
||||
1. 读本文和 `docs/PC接口对接规划.md`。
|
||||
2. 先检查桌面版 Apifox PC 目录是否新增或变更;只逐条复核受影响接口,不要借用 APP 接口或猜 DTO。
|
||||
3. 每次只处理一个完整小分组:Apifox 核验 → `ApiClient` → 对应页面 → 聚焦测试;家谱配额接口不是入口,继续不写死 `genealogyId`。
|
||||
4. 只有删除能力、没有真实列表/详情来源的模块,不开放危险操作,只登记后端缺口。
|
||||
5. 每完成一组,更新本文、`PC接口对接规划.md` 和相关测试,并报告改动、验证和剩余阻塞。
|
||||
改公共样式或公共脚本前,先检查相关 `profile-*.html` 的引用,避免影响其他页面。
|
||||
|
||||
## 接手一个功能时
|
||||
|
||||
1. 找到页面入口、对应脚本和已有接口调用。
|
||||
2. 在 Apifox 的 PC 目录确认请求方法、参数、响应字段和权限。
|
||||
3. 修改请求封装和页面逻辑,保持加载、空数据、失败、无权限和禁用状态完整。
|
||||
4. JavaScript 改动后执行 `node --check 路径/文件.js`,并在浏览器走通受影响页面。
|
||||
5. 需要生成发布文件时执行 `node scripts/build-minified.mjs`。当前 `package.json` 已删除;如果后续重新引入 npm 脚本,需要同时恢复并维护对应配置。
|
||||
6. 提交前执行 `git diff --check`,确认没有格式问题。
|
||||
|
||||
工作区可能有未提交修改。不要使用 `git reset --hard` 或 `git checkout --` 覆盖现有文件。
|
||||
|
||||
-99
@@ -1,99 +0,0 @@
|
||||
# 规划
|
||||
|
||||
## 项目目标
|
||||
|
||||
将“代代相传”建设为由官网、用户 PC 管理端、用户 APP 端和平台后台组成的家谱服务。四个端共享家谱、成员、内容和权限规则,但分别交付,不能用用户 PC 页面替代 APP 或平台后台。
|
||||
|
||||
## 当前基线
|
||||
|
||||
- 当前工作区共有 55 个 HTML 页面,现有页面之间没有指向不存在 HTML 页的站内链接。
|
||||
- 本文保留项目级历史规划,不再维护接口数量和字段契约。当前 PC 接口基线、字段页面归属和 AI 执行顺序统一见 [PC接口对接规划.md](PC接口对接规划.md)。
|
||||
- 已完成真实接口闭环:注册、密码登录、短信登录、找回密码、个人资料、安全设置、头像上传、地区选择、家族圈动态。
|
||||
- 家谱列表、家谱创建/加入、成员、权限、世系、谱文、相册、视频、贺礼、成长、备忘、功德、消息、反馈等页面已接入当前 PC 契约;仍未声明 PC 契约的能力以 [个人中心能力映射.md](个人中心能力映射.md) 标注为不可操作,不得伪造请求或静态数据为真实业务。
|
||||
|
||||
## 实施进度
|
||||
|
||||
- [x] 第一轮:家谱基础页面状态收口。已覆盖家谱列表、创建与加入、审核、家族管理、邀请、世系和字辈页面;未开发功能会显示统一提示并阻止伪造操作。
|
||||
- [x] 第二轮:内容与协作页面状态收口。已覆盖除家族圈动态外的内容、记录、消息、反馈、管理员权限与资料提醒页面;家族圈动态保持真实功能并保留跳转入口。
|
||||
- [x] 第三轮:剩余用户 PC 业务入口收口。已覆盖家谱主页、应用分享和帮助与会员服务页面;家族圈动态入口保持真实跳转。
|
||||
- [x] 第四轮:官网静态页面补齐。已新增新闻、法律文档入口、通用状态和工单页面;在线工单功能明确处于开发中,法律正文必须在发布前由法务替换确认。
|
||||
|
||||
## 产品边界
|
||||
|
||||
### 官网
|
||||
|
||||
保留首页、数字家谱、家谱广场、姓氏百科、家族文化、应用下载、帮助中心、关于我们、搜索、公告和工单入口。
|
||||
|
||||
需要补齐的独立页面:
|
||||
|
||||
1. 新闻列表、新闻分类、新闻详情。
|
||||
2. 隐私政策、儿童个人信息处理规则、服务协议。
|
||||
3. 我的工单、工单详情。
|
||||
4. 404、无权限、系统维护等通用状态页。
|
||||
|
||||
### 用户 PC 管理端
|
||||
|
||||
现有页面覆盖了思维导图中的大部分视觉入口:我的家谱、家谱主页、世系图、字辈、谱文、相册、视频、家族圈、贺礼、成长日志、备忘录、功德录、家族管理、管理员权限、入谱审核、消息、个人资料和安全设置。
|
||||
|
||||
需要补齐的关键页面:
|
||||
|
||||
1. 家族成员名册与成员详情,用于按成员筛选、查看资料、维护角色;世系图不能替代成员名册。
|
||||
2. 家族圈动态详情,用于承载可分享链接、完整评论和通知跳转。
|
||||
3. 无家谱首次引导、无权限、空列表等状态视图。
|
||||
|
||||
### 用户 APP 端
|
||||
|
||||
APP 是独立交付物,负责移动端查看家谱、上传照片/视频、发布内容、消息和个人中心。当前 PC 项目只需共享接口契约和视觉规范,不直接复制为 APP 页面。
|
||||
|
||||
### 平台后台
|
||||
|
||||
平台后台是独立项目,负责全站家谱审核、内容治理、会员/VIP、系统设置和运营数据。它不应通过隐藏按钮混入用户 PC 端。
|
||||
|
||||
## 后端接口开发顺序
|
||||
|
||||
### 第一阶段:家谱基础
|
||||
|
||||
1. 我的家谱列表、家谱详情、创建家谱、加入家谱、加入审核。
|
||||
2. 家谱成员列表、成员详情、邀请、移除、角色与管理员权限。
|
||||
3. 家谱入口必须返回真实 `genealogyId`,让世系、内容和家族圈能从同一上下文进入。
|
||||
|
||||
### 第二阶段:世系与资料
|
||||
|
||||
1. 世系树查询。
|
||||
2. 成员新增、编辑、删除。
|
||||
3. 父母、配偶、子女等关系维护。
|
||||
4. 字辈列表与维护。
|
||||
|
||||
### 第三阶段:内容与协作
|
||||
|
||||
1. 谱文及分类。
|
||||
2. 相册、照片、视频。
|
||||
3. 贺礼、成长日志、备忘录、功德录。
|
||||
4. 消息中心、点赞评论通知、生日提醒、入谱审核通知。
|
||||
5. 意见反馈、工单列表与详情。
|
||||
|
||||
### 第四阶段:官网内容与合规
|
||||
|
||||
1. 新闻资讯、公告、公开内容检索。
|
||||
2. 隐私、未成年人保护和服务协议。
|
||||
3. 客服工单的提交、查询和回复。
|
||||
|
||||
## 契约要求
|
||||
|
||||
每个业务对象都必须提供明确 DTO,至少包括列表、详情和写入请求模型。禁止前端依赖泛型 `object` 或猜测字段名。
|
||||
|
||||
最优先需要后端补充的模型:
|
||||
|
||||
1. `GenealogyDTO`、`GenealogyMemberDTO`、`MemberRoleDTO`。
|
||||
2. `LineagePersonDTO`、`LineageRelationDTO`、`GenerationDTO`。
|
||||
3. `FeedDTO`、`CommentDTO`。
|
||||
4. `ArticleDTO`、`AlbumDTO`、`VideoDTO`、`CeremonyDTO`、`GrowthLogDTO`、`MemoDTO`、`MeritDTO`。
|
||||
5. `NotificationDTO`、`TicketDTO`、`NewsDTO`。
|
||||
|
||||
## 交付规则
|
||||
|
||||
1. 有正式接口和 DTO 的页面,才接入真实读写功能。
|
||||
2. 未开发接口对应的页面保留设计,但明确显示“功能开发中”或空状态,不伪造成功数据。
|
||||
3. 家谱业务全部从真实 `genealogyId` 进入,不写死示例家谱编号。
|
||||
4. 访客、普通成员、家谱管理员、平台管理员必须有独立权限边界。
|
||||
5. 测试账号仅用于运行时验收;账号和密码不写入仓库、文档、测试、日志或配置。
|
||||
@@ -1,43 +0,0 @@
|
||||
# 登录后体验验证 20% 记录
|
||||
|
||||
## 本轮目标
|
||||
|
||||
围绕“登录后页面层级不清、不同尺寸下显得拥挤”的反馈,先完成不改变业务接口的体验验证与 P0/P1 收口。
|
||||
|
||||
## 已完成
|
||||
|
||||
1. 登录后的真实去向
|
||||
- 仅有一部真实家谱时直接进入该家谱主页;多部或无家谱时进入“我的家谱”。
|
||||
- 个人中心不再展示无家谱上下文的演示数据或业务入口。
|
||||
2. 当前家谱上下文
|
||||
- 家谱业务页在页头展示当前家谱,并可回到“我的家谱”切换。
|
||||
- 个人中心、我的家谱、当前家谱主页使用可返回的三级路径。
|
||||
3. 响应式导航
|
||||
- 1320px 及以下的个人中心页面使用统一的折叠菜单,避免当前家谱、导航和操作按钮相互挤压。
|
||||
- 720px 及以下隐藏与顶部菜单重复的家谱侧栏,让实际内容进入首屏。
|
||||
- 折叠菜单显示当前所在页;“我的家谱”页移除页头重复的创建入口。
|
||||
|
||||
## 验证方式
|
||||
|
||||
- 自动回归:`npm test`,379 项通过。
|
||||
- 独立无界面 Chrome 点击回归:132 条流程通过。除原有认证、表单、权限和异常流程外,逐页覆盖 33 个家谱工作区页面、14 个账号工作区页面,实际点击 22 个家谱侧栏入口和 12 个账号侧栏入口,并检查顶部/左侧位置、宽度、吸顶行为、当前项、父级展开、家谱上下文以及 1440px、1024px、390px 三档横向溢出。
|
||||
- 真实流程:登录后进入真实家谱列表、选择家谱主页,并在 1249px 桌面与 390px 手机视口检查导航和首屏。
|
||||
- 验收使用隔离浏览器配置;测试账号、登录令牌及真实家谱标识不写入仓库。
|
||||
|
||||
## 20% 后续维护记录
|
||||
|
||||
- 个人中心继续以 `C:\Users\Rain\Desktop\job\Jiapu-App` 的账户与家谱能力为信息架构参考;真实读写仅使用当前 PC 客户端契约。
|
||||
- 已补齐当前家谱上下文传递、业务页统一加载/空数据/失败/无权限状态,以及账户、家谱和内容表单的标签与失败反馈。
|
||||
- 2026-08-27 按后端最新 `genealogy-pc-openapi.yaml` 复核后,资料提醒、家谱邀请、重要证件、家谱生命周期、推广收益与提现、VIP 微信扫码支付均已有正式 PC 契约并已接入;分享奖励、推荐偏好仍保持不可操作,不展示模拟数据。
|
||||
- 交给后端的完整补齐范围、最小 DTO、权限和验收要求见 `docs/PC后端补齐清单.md`;该清单只描述 PC 能力,不迁用 App 接口。
|
||||
- 本次当前全量回归为 379 项通过;测试账号、密码、令牌和真实家谱信息仍不写入代码、文档或 Git。
|
||||
- 全部根目录 HTML 的站内链接和页面锚点已做自动完整性检查;空锚点形式的退出操作已改为语义正确的按钮,避免被误判为导航和被自动巡检误触。
|
||||
- 活动页在没有任何可邀请成员时不再提交空名单,避免无意义写请求及误清空邀请;该边界已纳入无界面浏览器连续提交回归。
|
||||
- 角色运行时验收受阻:已用隔离身份只读确认谱主能力和未认证访问闭环,但该身份可访问的家谱中没有管理员或普通成员实体。未创建成员、未提交申请或写入家谱数据;解除条件是提供已有的隔离管理员、普通成员身份,或预先在隔离家谱中准备对应角色关系。
|
||||
- 最新只读复核跳过:隔离登录响应未提供可明确消费的令牌字段;为避免打印原始响应或猜测字段,未继续请求角色接口。该项不影响现有静态权限回归,待测试环境恢复标准 PC 登录响应后再验收。
|
||||
|
||||
## 后续优先级
|
||||
|
||||
1. 统一业务页的加载中、空数据、请求失败和无权限状态,减少页面之间的割裂感。
|
||||
2. 收口表单标签、必填提示、提交中与提交失败反馈,优先覆盖登录、创建/加入家谱和内容发布。
|
||||
3. 继续用真实权限角色验收成员、管理员与非成员的访问边界。
|
||||
Reference in New Issue
Block a user