feat(api): 添加帮助中心反馈系统和VIP服务功能
- 在ApiClient中新增submitFeedback、myFeedback、helpArticles、helpArticleDetail、 siteArticles、promotions、vipPackages、createVipOrder、vipOrders等方法 - 添加帮助文章和站点资讯的参数验证逻辑 - 更新测试文件添加新的API方法测试用例 - 在HTML页面中添加反馈、帮助和VIP服务相关页面的脚本引用 - 更新加入家谱页面为完整的申请流程界面 - 修改资讯详情页面为站点资讯展示页面 - 更新AxiosRequestUtil中认证处理逻辑 - 添加世系树渲染的HTML生成函数用于页面复用 - 更新文档中的API契约说明和页面规划
This commit is contained in:
+318
-4
@@ -1090,7 +1090,7 @@ PC `GenealogyMemberVo` 响应字段及页面用途:
|
||||
|
||||
### 7.20 家谱创建与加入申请契约
|
||||
|
||||
页面:`profile-create-family.html`;加入申请和审核页面留到下一批开放。
|
||||
页面:`profile-create-family.html`、`join-genealogy.html`、`profile-join-family.html`、`profile-join-review.html`。
|
||||
|
||||
`AppGenealogyCreateBody`:
|
||||
|
||||
@@ -1109,9 +1109,323 @@ PC `GenealogyMemberVo` 响应字段及页面用途:
|
||||
- 创建只调用 `POST /genealogy/pc/genealogies`,提交前读取 `GET /genealogy/pc/genealogies/quota`;
|
||||
- 地区选项复用 `/genealogy/pc/region/children?parentCode=...`,不手写地区编码;
|
||||
- 创建成功后使用响应中的稳定 `genealogyId` 重读同一家谱,匹配后进入家谱主页;
|
||||
- ApiClient 已同时冻结加入申请、我的申请、待审核、审核和撤销的 PC path/body 白名单,但本批未提前开放加入 UI;
|
||||
- 申请页先从 `GET /genealogy/pc/genealogies/options` 选择响应中的家谱,再调用 `POST /genealogy/pc/genealogies/{genealogyId}/join-applies`;
|
||||
- 我的申请页调用 `GET /genealogy/pc/genealogies/join-applies/mine`;只有 `status=0` 的响应项可以调用 `DELETE /genealogy/pc/genealogies/join-applies/{applyId}`;
|
||||
- 审核页只从当前家谱上下文取得 `genealogyId`,先读取家谱详情并要求 `canManage=true`,再读取 `GET /genealogy/pc/genealogies/{genealogyId}/join-applies/pending`;
|
||||
- 审核只调用 `PUT /genealogy/pc/genealogies/{genealogyId}/join-applies/{applyId}/audit`,`status=1` 通过、`status=2` 拒绝;
|
||||
- `inviterUserId` 没有安全 PC 邀请来源,本轮加入申请契约不发送该字段;
|
||||
- 创建页面无家谱 ID、申请 ID、用户 ID 或 OSS ID 文本输入,所有写操作防重复。
|
||||
- 创建/申请/审核页面均无家谱 ID、申请 ID、用户 ID 或 OSS ID 文本输入,所有写操作防重复并在成功后重读对应资源。
|
||||
|
||||
`AppGenealogyJoinApplyBody`:
|
||||
|
||||
| 字段 | 类型/约束 | 必填 | 来源 | 页面与提交时机 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `applicantName` | string,最长 50 | 否 | U | 申请页姓名;非空时提交 |
|
||||
| `phone` | string,最长 30 | 否 | U | 申请页联系电话;非空时提交 |
|
||||
| `relationDesc` | string,最长 100 | 否 | U | 申请页关系说明;非空时提交 |
|
||||
| `applyReason` | string,最长 500 | 否 | U | 申请页申请理由;非空时提交 |
|
||||
| `inviterUserId` | int64/string | 邀请模式条件必填 | I | PC 无安全来源,不展示、不发送;邀请模式继续阻断 |
|
||||
|
||||
`GenealogyJoinApplyVo` 与审核:
|
||||
|
||||
| 字段 | 类型/枚举 | 来源 | 页面用途 |
|
||||
| --- | --- | --- | --- |
|
||||
| `applyId`、`genealogyId` | int64/string | R/I | 响应按钮属性和写入 path;不显示、不手填 |
|
||||
| `genealogyName`、`surname` | string | R | 我的申请列表只读展示 |
|
||||
| `applicantName`、`phone`、`relationDesc`、`applyReason` | string | R | 管理员待审核列表;只展示申请人主动提交的 `phone` |
|
||||
| `auditTime`、`auditRemark` | datetime/string | R | 我的申请审核结果展示 |
|
||||
| `status` | string `0/1/2/3` | R | 待审核/通过/拒绝/撤销;仅 `0` 可撤销 |
|
||||
| `appUserId`、`inviterUserId`、`auditUserId` | int64/string | I | 普通列表和审核列表均不展示 |
|
||||
| `appUserPhone`、`inviterPhone`、`auditPhone` | string | I | 账号/邀请/审核人员隐私字段均不展示 |
|
||||
| 审核 `status` | string `1/2` | S | 管理员点击通过或拒绝时提交 |
|
||||
| 审核 `auditRemark` | string,最长 500 | U | 拒绝时可选填写,非空时提交 |
|
||||
|
||||
### 7.21 意见反馈与工单契约
|
||||
|
||||
页面:`profile-feedback.html`、`submit-ticket.html`、`my-tickets.html`、`ticket-detail.html`。
|
||||
|
||||
后端只提供统一反馈集合:
|
||||
|
||||
- `POST /genealogy/pc/feedback`:当前业务用户提交反馈;
|
||||
- `GET /genealogy/pc/feedback`:读取当前业务用户自己的反馈列表;
|
||||
- 没有独立 ticket path;“工单”只是帮助中心对同一反馈记录的页面名称;
|
||||
- 没有用户侧回复、追问、关闭或删除接口,页面不制造这些操作。
|
||||
|
||||
`AppFeedbackBody`:
|
||||
|
||||
| 字段 | 类型/约束 | 必填 | 来源 | 页面与提交时机 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `feedbackType` | string enum `advice/bug/complaint/other` | 否 | S | 两个提交页选择;空值省略并由后端默认 `advice` |
|
||||
| `feedbackContent` | string,非空 | 是 | U | 问题或建议正文;提交时发送 |
|
||||
| `contactInfo` | string | 否 | U | 用户主动提供的联系方式;非空时发送 |
|
||||
| `feedbackTitle` | — | — | V | 旧页面字段不属于 DTO,已从表单移除且不发送 |
|
||||
|
||||
`FeedbackVo`:
|
||||
|
||||
| 字段 | 类型/枚举 | 来源 | 页面用途 |
|
||||
| --- | --- | --- | --- |
|
||||
| `feedbackId` | int64/string | R/I | 提交后重读匹配、列表详情链接和 URL;不展示、不手填 |
|
||||
| `feedbackType`、`feedbackContent`、`contactInfo` | string | R | 列表或详情只读展示 |
|
||||
| `handleStatus` | string `0/1/2/3` | R | 待处理/处理中/已处理/已关闭 |
|
||||
| `handleResult`、`handleTime`、`remark` | string/datetime | R | 后端存在时在详情展示 |
|
||||
| `status` | string `0/1` | I | 响应有效性校验,不提供用户修改入口 |
|
||||
| `appUserId`、`handlerId` | int64/string | I | 内部用户/处理人编号,不展示 |
|
||||
| `appUserNickName`、`appUserPhone` | string | I | 后台 enrichment,不在用户页面展示 |
|
||||
|
||||
实现边界:
|
||||
|
||||
- ApiClient 是 GET/POST path 和三字段 body 白名单唯一 owner;
|
||||
- 提交使用防重复锁;成功响应必须包含稳定字符串 `feedbackId`;
|
||||
- 提交后重读我的反馈列表并精确找到同一 `feedbackId`,否则不宣称成功;
|
||||
- 工单详情从 URL 读取列表响应产生的 `feedbackId`,只在我的列表中精确匹配,不回退第一条;
|
||||
- 401 清理登录态;403 保留登录态并显示后端权限错误;页面不展示原始 JSON。
|
||||
|
||||
### 7.22 VIP 套餐与会员订单契约
|
||||
|
||||
页面:`profile-services.html`。
|
||||
|
||||
PC 端只开放三条业务用户接口:
|
||||
|
||||
- `GET /genealogy/pc/vip/packages`:读取可购买套餐,成功响应 `List<VipPackageVo>`,无分页结构;
|
||||
- `POST /genealogy/pc/vip/orders`:创建当前业务用户的会员订单,body 为 `AppVipOrderBody`,成功响应 `VipOrderVo`;
|
||||
- `GET /genealogy/pc/vip/orders`:读取当前业务用户的订单,成功响应 `List<VipOrderVo>`,无分页结构;
|
||||
- 三条接口均要求 APP_USER 登录态;创建接口有后端防重复提交;
|
||||
- PC Controller 没有支付发起、支付回调、取消、关闭或退款接口,页面不得制造这些动作。
|
||||
|
||||
`AppVipOrderBody`:
|
||||
|
||||
| 字段 | 类型/约束 | 必填 | 来源 | 页面与提交时机 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `packageId` | int64,稳定字符串 ID | 是 | S/I | 用户点击套餐响应生成的卡片时选择;创建订单时发送,禁止手填 |
|
||||
| `genealogyId` | int64,稳定字符串 ID | 否 | S/I | 用户从“我的家谱”响应生成的下拉项中选择;非空时创建订单发送,禁止手填 |
|
||||
| `payType` | string;后端空值默认 `wechat` | 否 | A | PC 页面不提供支付方式选择且默认省略;等待后端开放真实支付能力后再确认枚举 |
|
||||
|
||||
`VipPackageVo`:
|
||||
|
||||
| 字段 | 类型/枚举 | 来源 | 页面用途 |
|
||||
| --- | --- | --- | --- |
|
||||
| `packageId` | int64/string | R/I | 套餐卡片选择和订单 body;不展示、不手填 |
|
||||
| `packageName`、`packageDesc` | string | R | 套餐名称和说明 |
|
||||
| `packageType` | string `vip/storage` | R | 显示为会员套餐/存储扩容;未知值整条过滤 |
|
||||
| `price`、`originalPrice` | decimal,非负 | R | 价格展示;不参与前端金额计算 |
|
||||
| `durationValue` | integer,非负,可空 | R | 与期限单位组合展示 |
|
||||
| `durationUnit` | string `permanent/day/month/year` | R | 永久/天/月/年 |
|
||||
| `genealogyLimit`、`memberLimit`、`storageLimitMb` | integer,非负,可空 | R | 套餐额度只读展示 |
|
||||
| `featureJson` | string/JSON | I | 契约未说明用户侧展示结构,当前不解析、不展示 |
|
||||
| `sortOrder` | integer | I | 后端列表顺序 owner,前端不重新排序 |
|
||||
| `status` | string `0/1` | I | 只接受正常 `0` 套餐;不提供修改入口 |
|
||||
| `remark` | string | R | 非空时作为套餐补充说明展示 |
|
||||
|
||||
`VipOrderVo`:
|
||||
|
||||
| 字段 | 类型/枚举 | 来源 | 页面用途 |
|
||||
| --- | --- | --- | --- |
|
||||
| `orderId` | int64/string | R/I | 创建后重读精确匹配;不展示、不手填 |
|
||||
| `orderNo` | string | R | 订单列表展示 |
|
||||
| `packageId` | int64/string | R/I | 响应有效性校验,不展示 |
|
||||
| `packageName` | string | R | 订单套餐名称 |
|
||||
| `genealogyId` | int64/string,可空 | R/I | 响应有效性校验,不展示 |
|
||||
| `genealogyNo`、`genealogyName` | string,可空 | R | 关联家谱信息展示 |
|
||||
| `orderAmount`、`payAmount` | decimal,非负 | R | 订单金额和应付金额展示;以后端值为准 |
|
||||
| `payType` | string,可空 | R | 后端返回时只读展示,不作为支付入口 |
|
||||
| `payStatus` | string `0/1/2/3` | R | 待支付/已支付/已关闭/已退款 |
|
||||
| `payTime`、`expireTime` | datetime,可空 | R | 支付和有效期信息展示 |
|
||||
| `status` | string `0/1` | I | 响应有效性校验,不提供修改入口 |
|
||||
| `remark` | string | R | 非空时展示 |
|
||||
| `appUserId`、`appUserNickName`、`appUserPhone` | int64/string | I | 当前账号及 enrichment 字段,不在页面展示 |
|
||||
|
||||
实现边界:
|
||||
|
||||
- `ApiClient` 是三条 path 与三字段请求白名单唯一 owner;
|
||||
- 页面启动时并行读取套餐、我的家谱和订单;所有业务 ID 仅来自响应;
|
||||
- 创建使用前端防重复锁;创建响应必须是完整有效 `VipOrderVo`,随后重读订单列表并精确找到同一 `orderId` 才宣称成功;
|
||||
- 页面省略 `payType`,由后端使用已确认的默认值;不展示后端尚未提供的支付、取消或退款按钮;
|
||||
- 401 清理登录态;403 保留登录态并展示后端错误;非法套餐、订单、金额、枚举或不安全数字 ID 整条过滤。
|
||||
|
||||
### 7.23 家谱主页只读概览契约
|
||||
|
||||
页面:`profile-family-home.html`。
|
||||
|
||||
只使用两条现有 PC 读取接口:
|
||||
|
||||
- `GET /genealogy/pc/genealogies/{genealogyId}/overview`:返回当前家谱 `AppGenealogyVo`;
|
||||
- `GET /genealogy/pc/genealogies/{genealogyId}/lineage/tree`:返回 `List<LineagePersonTreeView>`;
|
||||
- `genealogyId` 只从 `profile-common.js` 的当前家谱上下文取得,两条请求使用同一个稳定字符串 ID;
|
||||
- 后端 `overview` 当前实际复用家谱详情服务,不包含文章、相册、视频、祭祀或动态聚合统计。
|
||||
|
||||
`AppGenealogyVo` 主页字段:
|
||||
|
||||
| 字段 | 类型/约束 | 来源 | 页面用途 |
|
||||
| --- | --- | --- | --- |
|
||||
| `genealogyId` | int64/string | I/A/R | 当前上下文、两条请求 path、响应一致性校验;不展示、不手填 |
|
||||
| `genealogyNo` | string | R | 家谱编号只读展示 |
|
||||
| `genealogyName` | string,非空 | R | 页面标题和概览名称 |
|
||||
| `surname` | string | R | 姓氏只读展示 |
|
||||
| `ancestralHall` | string,可空 | R | 堂号非空时展示 |
|
||||
| `originPlace` | string,可空 | R | 祖籍非空时展示 |
|
||||
| `regionFullName` | string,可空 | R | 完整地区非空时展示 |
|
||||
| `memberCount`、`personCount` | integer,非负 | R | 家谱成员数和世系人物数;不在前端计算 |
|
||||
| `status` | string `0/1` | I | 只接受正常 `0` 响应 |
|
||||
| `canManage` | boolean | I | 严格为 `true` 时显示家谱管理入口 |
|
||||
| `canEditContent` | boolean | I | 保留为内容权限上下文,主页不据此制造写操作 |
|
||||
| `ownerUserId`、`coverOssId` | int64/string | I | 当前主页不展示、不手填 |
|
||||
| 其余地址、角色、成员状态字段 | 对应 DTO 类型 | I | 当前主页不消费,不输出原始 JSON |
|
||||
|
||||
`LineagePersonTreeView`:
|
||||
|
||||
| 字段 | 类型/结构 | 来源 | 页面用途 |
|
||||
| --- | --- | --- | --- |
|
||||
| `personId`、`genealogyId` | int64/string | R/I | 树节点稳定标识和响应校验;不直接展示 |
|
||||
| `name`、`generationName`、人物摘要字段 | string | R | 复用世系页的安全节点渲染 |
|
||||
| `relationType` | string `father/mother/spouse/child/adoptive` | R | 关系数据;主页不提供关系写操作 |
|
||||
| `relationName` | string | R | 非空时作为关系说明 |
|
||||
| `spouses`、`children` | `LineagePersonTreeView[]` | R | 递归世系预览 |
|
||||
|
||||
实现边界:
|
||||
|
||||
- `lineage-pages.js` 是世系节点规范化和树 HTML 的唯一 owner;主页不得复制一套树字段解释;
|
||||
- 页面并行读取概览和世系树,只展示基础信息、后端已有计数和真实树;
|
||||
- `/genealogy/dashboard/overview` 是后台权限接口,不属于 PC 家谱主页契约,前端不调用;
|
||||
- PC 没有成员邀请创建或分享接口,“邀请家人”只显示阻断说明,不提供可点击操作;
|
||||
- 主页没有写请求;401 清理登录态,403 保留登录态并显示读取错误。
|
||||
|
||||
### 7.24 帮助中心文章契约
|
||||
|
||||
页面:`help.html`。契约 owner:`utils/ApiClient.js` 的 `helpArticles` / `helpArticleDetail` 和 `public/js/help-pages.js`。
|
||||
|
||||
接口:
|
||||
|
||||
| method | 完整 path | 目录 | 鉴权 | 页面时机 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| GET | `/genealogy/pc/help-articles` | 内容文章 / PC 帮助文章列表 | APP_USER 登录态 | 帮助中心初始化或用户点击刷新 |
|
||||
| GET | `/genealogy/pc/help-articles/{helpId}` | 内容文章 / PC 帮助文章详情 | APP_USER 登录态 | 用户从列表点击“查看完整解答” |
|
||||
|
||||
请求字段:
|
||||
|
||||
| 位置 | 字段 | 必填 | 类型/约束 | 分类 | 页面来源与提交时机 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| Query | `helpCategory` | 否 | string;后端按完全相等过滤 | S | 当前页面不提供猜测分类选项,因此初始化时省略;后端提供正式分类字典后才能开放选择 |
|
||||
| Path | `helpId` | 是 | int64;前端始终按非零十进制字符串处理 | I | 只能来自列表响应;用户点击该文章详情时进入 path |
|
||||
| Header | `clientid` | 基础请求自动 | string | A | `AxiosRequestUtil` 统一填写;页面不输入 |
|
||||
| Header | `Authorization` | 是 | `Bearer <access_token>` | A | `AxiosRequestUtil` 从登录态自动填写;页面不读取、不展示 token |
|
||||
| Body | — | — | 两个接口均无 Body | — | — |
|
||||
|
||||
`HelpArticleVo` 响应字段:
|
||||
|
||||
| 字段 | 类型 | 分类 | 页面使用 |
|
||||
| --- | --- | --- | --- |
|
||||
| `helpId` | int64/string | I | 校验稳定 ID、绑定列表项并精确重读详情;不向用户显示 |
|
||||
| `helpCategory` | string | R | 列表和详情分类文字,可空 |
|
||||
| `helpTitle` | string | R | 列表标题,空值使整条响应失败 |
|
||||
| `helpContent` | string | R | 详情正文;转义并保留换行,不执行响应 HTML |
|
||||
| `coverOssId` | int64/string | I | 当前响应没有文件 URL,页面不显示、不拼接、不允许手填 |
|
||||
| `sortOrder` | int64 | I | 仅后端排序,不展示 |
|
||||
| `viewCount` | int64/string | R | 非负十进制计数;详情 GET 会由后端累计浏览量 |
|
||||
| `status` | string | I | 只接受正常值 `0`;停用文章整条拒绝 |
|
||||
| `remark` | string | I | 后台备注,不展示 |
|
||||
|
||||
响应与错误边界:
|
||||
|
||||
- 列表响应必须是直接数组,不接受分页 `rows` 猜测;任一元素缺少稳定 ID、标题、正文或正常状态时整批失败;
|
||||
- 详情必须返回与 path 中相同的 `helpId`,不回退到列表第一条;
|
||||
- YAML 把两条接口标为 `security: []`,但 `PcHelpArticleController` 没有 `@SaIgnore`,部署环境无 token 实测返回“认证失败”;按用户指定的后端项目为准,当前客户端必须携带登录 token,此项作为 Apifox/后端冲突保留;
|
||||
- 后端只声明统一成功包装,未提供帮助文章专属失败码;详情不存在或停用时按业务错误展示,不制造 404 枚举;
|
||||
- 无登录态或 401 清理登录状态并进入登录页;403 保留登录态并展示读取错误;
|
||||
- 页面只有读取动作,不提供编辑、发布、分类管理、封面 OSS ID 或成员邀请操作。
|
||||
|
||||
### 7.25 应用推广契约
|
||||
|
||||
页面:`app.html` 的“应用推广”区域。契约 owner:`utils/ApiClient.js` 的 `promotions` 和 `public/js/app-promotion-pages.js`。
|
||||
|
||||
接口:
|
||||
|
||||
| method | 完整 path | 目录 | 鉴权 | 页面时机 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| GET | `/genealogy/pc/promotions` | 应用推广 / PC 应用推广列表 | APP_USER 登录态 | 应用下载页初始化;仅在本地已有登录 token 时请求一次 |
|
||||
|
||||
请求字段:
|
||||
|
||||
| 位置 | 字段 | 必填 | 类型/约束 | 分类 | 页面来源与提交时机 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| Query | `platform` | 否 | string;SQL 字典 `gen_promotion_platform`:`all` 全部(默认)、`app` APP、`pc` PC、`wechat` 小程序 | A | 应用下载页初始化时固定发送 `pc`;服务端返回 `platform=pc` 或 `platform=all` 的正常记录,不让用户选择或手填 |
|
||||
| Header | `clientid` | 基础请求自动 | string | A | `AxiosRequestUtil` 统一填写;页面不输入 |
|
||||
| Header | `Authorization` | 是 | `Bearer <access_token>` | A | `AxiosRequestUtil` 从登录态自动填写;页面不读取、不展示 token |
|
||||
| Path | — | — | 无 Path 参数 | — | — |
|
||||
| Body | — | — | 无 Body | — | — |
|
||||
|
||||
`AppPromotionVo` 列表元素字段:
|
||||
|
||||
| 字段 | 类型 | 必填/约束 | 分类 | 页面使用 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `promotionId` | int64/string | SQL `BIGINT NOT NULL` 主键;前端按非零十进制字符串处理 | I | 稳定绑定卡片,不展示、不允许手填 |
|
||||
| `promotionKey` | string | SQL `varchar(80) NOT NULL DEFAULT 'banner'` | I | 后台识别字段,页面不展示、不提交 |
|
||||
| `promotionTitle` | string | SQL `varchar(200) NOT NULL`;页面要求非空 | R | 推广卡片标题,HTML 转义后展示 |
|
||||
| `promotionDesc` | string | SQL `varchar(500) DEFAULT ''`,可空 | R | 推广卡片说明,HTML 转义后展示 |
|
||||
| `coverOssId` | int64/string | 可空;响应没有可访问文件 URL | I | 不显示、不拼接 URL、不允许用户手填 OSS ID |
|
||||
| `targetUrl` | string | SQL `varchar(500) DEFAULT ''`,可空;页面只接受绝对 `http`/`https` URL | R | 合法时作为新窗口外链;危险、相对或无效地址降级为不可点击卡片 |
|
||||
| `platform` | string | SQL `varchar(30) NOT NULL DEFAULT 'all'`;字典枚举 `all/app/pc/wechat` | I | 仅后端筛选,当前页面不展示 |
|
||||
| `sortOrder` | int64 | SQL `BIGINT NOT NULL DEFAULT 0`;后端按升序排序,同序按 `promotionId` 降序 | I | 仅后端排序,页面不二次解释或展示 |
|
||||
| `status` | string | SQL `char(1) NOT NULL DEFAULT '0'`;`0` 正常、`1` 停用,页面只接受 `0` | I | 停用或缺失状态的记录使整批响应失败 |
|
||||
| `remark` | string | SQL `varchar(500) DEFAULT NULL`,可空 | I | 后台备注,不展示 |
|
||||
|
||||
响应、权限与错误边界:
|
||||
|
||||
- 成功响应经统一请求层解包后必须是直接 `AppPromotionVo[]`,不是分页结构;任一元素缺少稳定 ID、标题或正常状态时整批失败;
|
||||
- 空数组显示“当前暂无应用推广”;未登录时页面本身仍可浏览,但推广区域只显示登录入口且不发请求;
|
||||
- YAML 把该接口标为 `security: []`,并且未导出真实 Query `platform`;后端 `PcPromotionApiController` 没有 `@SaIgnore`,全局拦截器要求 APP_USER 登录态。SQL 字典已确认 `all/app/pc/wechat`,服务层按“请求平台或 `all`”过滤,因此当前实现携带登录 token 并固定发送 `platform=pc`;
|
||||
- SQL `gen_app_promotion` 另有 `(tenant_id, platform, status, del_flag)` 和 `(tenant_id, sort_order)` 索引;`del_flag`、租户及审计字段不属于 `AppPromotionVo`,页面不接收、不展示;
|
||||
- 401 清理失效 token 并把推广区域切回登录提示;403、网络失败或畸形响应只在推广区域显示读取失败,不跳转、不清理有效登录态;
|
||||
- 后端只声明统一成功包装,未提供推广专属失败响应和错误码;字段长度、默认值及平台枚举已由后端 SQL 冻结,不再作为阻断项;
|
||||
- 页面没有推广新增、编辑、上下架、排序、封面上传能力,不调用 APP 或后台管理接口,也不向用户暴露推广 ID、推广键或 OSS ID。
|
||||
|
||||
### 7.26 官网资讯契约
|
||||
|
||||
页面:`news.html`、`article-detail.html`。契约 owner:`utils/ApiClient.js` 的 `siteArticles`、`utils/AxiosRequestUtil.js` 的公开请求鉴权语义和 `public/js/site-news-pages.js`。
|
||||
|
||||
接口:
|
||||
|
||||
| method | 完整 path | 目录 | 鉴权 | 页面时机 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| GET | `/genealogy/pc/site/articles` | 站点内容 / PC 站点文章列表 | 公开,`auth: false` | 资讯列表初始化、分类切换后的页面初始化,以及详情页按列表响应 ID 精确重读 |
|
||||
|
||||
请求字段:
|
||||
|
||||
| 位置 | 字段 | 必填 | 类型/约束 | 分类 | 页面来源与提交时机 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| Query | `articleType` | 否 | string;SQL 字典 `gen_site_article_type`:`news` 新闻(默认)、`notice` 公告、`download` 下载 | S | 用户点击固定分类链接后从 URL Query 取得;“全部”省略该字段,非法值不发请求 |
|
||||
| Query | `limit` | 否 | integer,客户端只允许 1–100;服务端正数最大取 100 | A | 列表和详情重读均由页面固定提交 `100` |
|
||||
| Header | `clientid` | 基础请求自动 | string | A | `AxiosRequestUtil` 统一填写;页面不输入 |
|
||||
| Header | `Authorization` | 不发送 | — | — | 方法设置 `auth: false`;即使本地已有 token 也不发送,公开请求 401/403 不清理既有登录态 |
|
||||
| Path | — | — | 无 Path 参数 | — | — |
|
||||
| Body | — | — | 无 Body | — | — |
|
||||
|
||||
`SiteArticleVo` 列表元素字段:
|
||||
|
||||
| 字段 | 类型 | 必填/约束 | 分类 | 页面使用 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `articleId` | int64/string | SQL `BIGINT NOT NULL` 主键;前端按非零十进制字符串处理 | I | 只用于生成详情链接和精确匹配,不展示、不允许手填 |
|
||||
| `articleType` | string | SQL `varchar(50) NOT NULL DEFAULT 'news'`;枚举 `news/notice/download` | R | 转换为新闻、公告、下载标签;枚举外记录使整批失败 |
|
||||
| `articleTitle` | string | SQL `varchar(200) NOT NULL`;页面要求非空 | R | 列表和详情标题,HTML 转义 |
|
||||
| `articleSummary` | string | SQL `varchar(500) DEFAULT ''`,可空 | R | 列表摘要和详情导语,HTML 转义 |
|
||||
| `articleContent` | string | SQL `text`,可空 | R | 详情正文;HTML 转义并只保留换行,不执行响应 HTML |
|
||||
| `coverOssId` | int64/string | SQL `BIGINT`,可空;没有文件 URL 契约 | I | 不显示、不拼接 URL、不允许手填 |
|
||||
| `externalUrl` | string | SQL `varchar(500) DEFAULT ''`,可空 | R | 只接受绝对 HTTP/HTTPS;合法时显示安全新窗口外链 |
|
||||
| `publishTime` | string/date | SQL `datetime`,可空 | R | 列表和详情发布时间文本,HTML 转义 |
|
||||
| `sortOrder` | int64 | SQL `BIGINT NOT NULL DEFAULT 0` | I | 仅后端排序,不展示 |
|
||||
| `status` | string | SQL `char(1) NOT NULL DEFAULT '0'`;`0` 正常、`1` 停用 | I | 页面只接受 `0`,其他值使整批失败 |
|
||||
| `remark` | string | SQL `varchar(500) DEFAULT NULL`,可空 | I | 后台备注,不展示 |
|
||||
|
||||
响应、详情与错误边界:
|
||||
|
||||
- 成功响应经统一请求层解包后必须是直接 `SiteArticleVo[]`;不接受分页 `rows`、旧字段别名或任一非法元素;
|
||||
- 服务端只查正常状态,按 `sortOrder ASC`、`publishTime DESC`、`articleId DESC` 排序;页面保持服务端顺序;
|
||||
- 后端没有站点文章单条详情接口;详情 ID 只能来自列表链接,详情页重读 `{ limit: 100 }` 后精确匹配同一字符串 ID,不存在时不得回退第一条;
|
||||
- 标题、摘要、正文、类型和时间全部转义;正文只保留换行,危险或相对外链不渲染;
|
||||
- 空数组显示“当前暂无资讯”;非法分类/详情 ID 不发请求;网络、业务或畸形响应只更新内容区域,不跳转登录;
|
||||
- YAML 已导出 method/path、`articleType`、`limit` 和 `security: []`,但响应只引用通用列表结果,未导出 `SiteArticleVo` 11 个字段;完整字段、长度、默认值和枚举由后端 VO、SQL 与字典补齐;
|
||||
- 2026-07-30 使用本地页面对当前部署环境做匿名实测时,接口返回“认证失败,无法访问系统资源”;页面保持在资讯页且不清理已有 token。该行为与 YAML 的 `security: []` 冲突,后端需确认部署环境是否遗漏公开放行;前端不通过附带登录 token 绕过公开契约;
|
||||
- `coverOssId` 的文件 URL 和站点文章单条详情接口继续作为后端契约阻断;页面不猜 URL、不制造详情路径;
|
||||
- 本批不修改 `about/culture/privacy/terms/surname`,也不复用登录后的家谱谱文 CRUD 脚本。
|
||||
|
||||
## 8. 分阶段执行顺序
|
||||
|
||||
@@ -1247,7 +1561,7 @@ PC `GenealogyMemberVo` 响应字段及页面用途:
|
||||
|
||||
### 阶段 7:剩余 PC 页面分批开放
|
||||
|
||||
首批已完成家谱生命周期 ApiClient 契约和创建家谱页面:额度、三级地区、封面上传派生、严格 DTO 校验、防重复与创建后重读均已接入。加入申请/审核、反馈/工单、VIP 和家谱主页按独立后续批次实施;成员邀请、分享和资料完善提醒没有 PC Controller,继续阻断。
|
||||
首批已完成家谱生命周期 ApiClient 契约和创建家谱页面:额度、三级地区、封面上传派生、严格 DTO 校验、防重复与创建后重读均已接入。第二批已完成申请加入、我的申请、撤销和管理员审核页面;家谱/申请 ID 均来自响应或当前上下文,写后重读确认。第三批已完成意见反馈和工单展示:四页共用 PC 反馈集合、提交后三字段白名单重读、详情精确匹配和隐私过滤。第四批已完成 VIP 套餐、我的家谱选项和会员订单:页面不手填业务 ID,不伪造支付能力,创建后重读同一订单确认。第五批已完成家谱主页只读概览:复用 PC overview 与世系树,权限控制管理入口,不制造聚合统计或邀请能力。第六批已完成帮助中心:登录后读取 PC 帮助文章列表,列表响应产生详情 ID,详情精确重读并安全展示正文;Apifox 的匿名标记与后端实际鉴权冲突已记录。第七批已完成应用下载页的 PC 推广列表:未登录不发请求,登录后固定发送 SQL 字典确认的 `platform=pc`,只读取服务端返回的 `pc + all` 正常记录,外链仅允许绝对 HTTP/HTTPS;推广 ID、后台键和 OSS ID 均不向用户开放。第八批已完成官网资讯列表与详情:公开请求不发送或清理 token,分类使用 SQL 字典 `news/notice/download`,详情 ID 只来自列表响应并通过重读列表精确匹配;YAML 通用响应缺失完整 `SiteArticleVo`,且当前部署环境匿名访问仍返回认证失败,两项差异均已记录。成员邀请、分享和资料完善提醒没有 PC Controller,继续阻断。
|
||||
|
||||
## 9. 文件级施工建议
|
||||
|
||||
|
||||
@@ -0,0 +1,391 @@
|
||||
# 应用下载页 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: 官网内容或公开家谱接口的下一最小批次。
|
||||
```
|
||||
@@ -0,0 +1,376 @@
|
||||
# 家谱主页 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. 不创建或修改任何真实数据。
|
||||
@@ -0,0 +1,310 @@
|
||||
# 反馈与工单 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: '19181970173',
|
||||
feedbackTitle: 'must-drop',
|
||||
appUserId: 'must-drop'
|
||||
});
|
||||
await client.myFeedback();
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
{
|
||||
method: 'POST',
|
||||
url: '/genealogy/pc/feedback',
|
||||
body: {
|
||||
feedbackType: 'bug',
|
||||
feedbackContent: '页面按钮无响应',
|
||||
contactInfo: '19181970173'
|
||||
}
|
||||
},
|
||||
{ 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: ' 19181970173 ',
|
||||
feedbackTitle: 'must-drop',
|
||||
appUserId: 'must-drop'
|
||||
}), {
|
||||
feedbackType: 'bug',
|
||||
feedbackContent: '页面按钮无响应',
|
||||
contactInfo: '19181970173'
|
||||
});
|
||||
```
|
||||
|
||||
同时断言:
|
||||
|
||||
- `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: 浏览器只读验证**
|
||||
|
||||
使用真实登录态验证四个页面、我的反馈列表、无记录详情、隐私和控制台错误。未经用户本轮明确授权,不提交真实反馈。
|
||||
@@ -0,0 +1,266 @@
|
||||
# 家谱加入申请与审核 Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 开放家谱申请加入、我的申请、撤销待审核申请,以及家谱管理员审核申请的完整 PC 页面闭环。
|
||||
|
||||
**Architecture:** `utils/ApiClient.js` 已拥有本批全部 PC path/body。新增 `join-pages.js` 负责可申请家谱选择、申请表单、我的申请和撤销;新增 `join-review-pages.js` 负责当前家谱待审核列表和通过/拒绝。两个 UMD 模块只使用响应中的字符串 ID,写入后重读对应列表。
|
||||
|
||||
**Tech Stack:** 静态 HTML、原生 JavaScript UMD、Axios、Node `node:test`。
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- 后端 `D:\WorkSpace\Java\Genealogy` 全程只读。
|
||||
- 只调用 `/genealogy/pc/**`。
|
||||
- 家谱 ID 和申请 ID 必须来自 PC 响应或当前家谱上下文,不能手填。
|
||||
- `inviterUserId` 没有安全 PC 来源,不展示、不发送。
|
||||
- 所有写操作防重复,401 清登录态,403 保留登录态。
|
||||
- 每项生产行为先运行失败测试,再最小实现。
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 申请数据边界
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/join-pages.test.js`
|
||||
- Create: `public/js/join-pages.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `genealogyOptions(query)`、`applyToGenealogy(genealogyId, body)`、`myGenealogyJoinApplies()`、`cancelGenealogyJoinApply(applyId)`
|
||||
- Produces:
|
||||
- `normalizeJoinGenealogy(item)`
|
||||
- `buildJoinApplyBody(values)`
|
||||
- `validateJoinApplyBody(body)`
|
||||
- `normalizeJoinApply(item)`
|
||||
- `normalizeJoinApplies(data)`
|
||||
- `renderJoinGenealogyOptions(data)`
|
||||
- `renderMyJoinApplies(data)`
|
||||
|
||||
- [ ] **Step 1: 写失败的纯行为测试**
|
||||
|
||||
用完整 `AppGenealogyVo` 和 `GenealogyJoinApplyVo` 字面量验证:
|
||||
|
||||
```js
|
||||
assert.deepEqual(JoinPages.buildJoinApplyBody({
|
||||
applicantName: ' 叶子 ',
|
||||
phone: ' 19181970173 ',
|
||||
relationDesc: ' 族亲 ',
|
||||
applyReason: ' 申请加入 ',
|
||||
inviterUserId: 'must-drop'
|
||||
}), {
|
||||
applicantName: '叶子',
|
||||
phone: '19181970173',
|
||||
relationDesc: '族亲',
|
||||
applyReason: '申请加入'
|
||||
});
|
||||
```
|
||||
|
||||
断言:
|
||||
|
||||
- `applicantName <= 50`、`phone <= 30`、`relationDesc <= 100`、`applyReason <= 500`。
|
||||
- 可申请家谱必须有稳定 `genealogyId/genealogyName/surname`,停用家谱拒绝。
|
||||
- 申请必须有稳定 `applyId/genealogyId` 和 `status`;状态只允许 `0/1/2/3`。
|
||||
- 安全数字长 ID 只接受字符串;不安全 number 拒绝。
|
||||
- 普通用户列表不展示内部用户 ID、邀请人 ID、审核人 ID或原始 JSON。
|
||||
- 只有 `status=0` 渲染撤销按钮。
|
||||
|
||||
- [ ] **Step 2: 运行 RED**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/join-pages.test.js
|
||||
```
|
||||
|
||||
Expected: FAIL,模块不存在。
|
||||
|
||||
- [ ] **Step 3: 最小实现纯函数**
|
||||
|
||||
所有可见文本使用 `escapeHtml`;响应任一必需字段无效时拒绝该条,数组包含非法元素时整批返回空数组。
|
||||
|
||||
- [ ] **Step 4: 运行 GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/join-pages.test.js
|
||||
node --check public/js/join-pages.js
|
||||
```
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 申请页面与我的申请
|
||||
|
||||
**Files:**
|
||||
- Modify: `join-genealogy.html`
|
||||
- Modify: `profile-join-family.html`
|
||||
- Modify: `public/js/join-pages.js`
|
||||
- Modify: `tests/join-pages.test.js`
|
||||
- Modify: `tests/pending-pages.test.js`
|
||||
- Modify: `tests/pc-scope.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `initJoinApplyPage()`、`initMyJoinAppliesPage()`
|
||||
|
||||
- [ ] **Step 1: 写失败的页面行为测试**
|
||||
|
||||
断言:
|
||||
|
||||
- 两页均不再 pending,并加载 `join-pages.js`。
|
||||
- `join-genealogy.html` 有关键词搜索、后端家谱选项、申请资料表单;不存在 `genealogyId/inviterUserId/applyId` 文本输入。
|
||||
- `profile-join-family.html` 有我的申请列表和刷新入口;申请 ID 只存在于响应渲染按钮属性。
|
||||
- 页面没有邀请码输入或“分享码”伪语义。
|
||||
|
||||
- [ ] **Step 2: 运行 RED**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/join-pages.test.js tests/pending-pages.test.js tests/pc-scope.test.js
|
||||
```
|
||||
|
||||
Expected: FAIL,页面仍 pending/缺少真实行为。
|
||||
|
||||
- [ ] **Step 3: 实现申请和撤销**
|
||||
|
||||
`join-genealogy.html`:
|
||||
|
||||
1. 读取 `genealogyOptions({keyword})`。
|
||||
2. 用户只能点击响应卡片选择家谱,内部保存字符串 `genealogyId`。
|
||||
3. 构造并校验申请 body。
|
||||
4. 提交后重读 `myGenealogyJoinApplies()`,必须找到同一响应 `applyId`。
|
||||
5. 跳转 `profile-join-family.html`。
|
||||
|
||||
`profile-join-family.html`:
|
||||
|
||||
1. 读取并渲染我的申请。
|
||||
2. 只有待审核申请显示撤销。
|
||||
3. 二次确认后调用撤销接口并重读列表,确保该申请不再为待审核。
|
||||
|
||||
- [ ] **Step 4: 运行 GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/join-pages.test.js tests/pending-pages.test.js tests/pc-scope.test.js
|
||||
node --check public/js/join-pages.js
|
||||
```
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 管理员审核
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/join-review-pages.test.js`
|
||||
- Create: `public/js/join-review-pages.js`
|
||||
- Modify: `profile-join-review.html`
|
||||
- Modify: `tests/pending-pages.test.js`
|
||||
- Modify: `tests/pc-scope.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `genealogyDetail(genealogyId)`、`pendingGenealogyJoinApplies(genealogyId)`、`auditGenealogyJoinApply(genealogyId, applyId, body)`
|
||||
- Produces:
|
||||
- `getCurrentGenealogyId(search)`
|
||||
- `buildJoinAuditBody(values)`
|
||||
- `validateJoinAuditBody(body)`
|
||||
- `renderPendingJoinApplies(data, canManage)`
|
||||
- `initJoinReviewPage()`
|
||||
|
||||
- [ ] **Step 1: 写失败的审核行为测试**
|
||||
|
||||
```js
|
||||
assert.deepEqual(JoinReviewPages.buildJoinAuditBody({
|
||||
status: '2',
|
||||
auditRemark: ' 资料不一致 ',
|
||||
applyId: 'must-drop'
|
||||
}), {
|
||||
status: '2',
|
||||
auditRemark: '资料不一致'
|
||||
});
|
||||
```
|
||||
|
||||
断言:
|
||||
|
||||
- `status` 只允许 `1/2`,`auditRemark <= 500`。
|
||||
- 页面只从 URL/`ProfileUI` 读取家谱 ID。
|
||||
- 只有 `genealogyDetail.canManage=true` 显示审核动作。
|
||||
- 待审核列表显示申请人名称、申请手机号、关系和原因,但隐藏内部用户 ID、邀请人/审核人 ID。
|
||||
- 通过和拒绝按钮使用响应中的稳定 `applyId`。
|
||||
|
||||
- [ ] **Step 2: 运行 RED**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/join-review-pages.test.js
|
||||
```
|
||||
|
||||
Expected: FAIL,模块不存在且页面仍 pending。
|
||||
|
||||
- [ ] **Step 3: 实现审核页面**
|
||||
|
||||
加载当前家谱详情和待审核列表;没有上下文时阻止请求并返回家谱选择入口。点击通过使用 `{status:'1'}`;拒绝要求输入可选审核说明并使用 `{status:'2', auditRemark}`。审核成功后重读待审核列表,确保同一 `applyId` 不再存在。
|
||||
|
||||
- [ ] **Step 4: 运行 GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/join-review-pages.test.js tests/pending-pages.test.js tests/pc-scope.test.js
|
||||
node --check public/js/join-review-pages.js
|
||||
```
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 导航、规划与验证
|
||||
|
||||
**Files:**
|
||||
- Modify: `profile-families.html`
|
||||
- Modify: `profile-family-admin.html`
|
||||
- Modify: `profile.html`
|
||||
- Modify: `tests/stage6-navigation.test.js`
|
||||
- Modify: `docs/PC接口对接规划.md`
|
||||
- Modify: `docs/superpowers/plans/2026-07-29-genealogy-join-review.md`
|
||||
|
||||
- [ ] **Step 1: 写失败的导航测试**
|
||||
|
||||
断言我的家谱入口可达申请页,家谱管理入口携带上下文进入审核页;不存在指向手填家谱 ID 或邀请码页面的入口。
|
||||
|
||||
- [ ] **Step 2: 运行 RED**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/stage6-navigation.test.js tests/pending-pages.test.js
|
||||
```
|
||||
|
||||
Expected: FAIL,审核导航尚未稳定携带家谱上下文。
|
||||
|
||||
- [ ] **Step 3: 开放导航并更新规划**
|
||||
|
||||
记录申请/审核 DTO、VO、权限、状态、字段来源、写后重读和 `inviterUserId` 阻断。
|
||||
|
||||
- [ ] **Step 4: 聚焦和全量验证**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/join-pages.test.js tests/join-review-pages.test.js tests/api-client-contract.test.js tests/pending-pages.test.js tests/pc-scope.test.js tests/stage6-navigation.test.js
|
||||
npm test
|
||||
node --check public/js/join-pages.js
|
||||
node --check public/js/join-review-pages.js
|
||||
git -c safe.directory=D:/WorkSpace/Web/jiapu diff --check
|
||||
```
|
||||
|
||||
Expected: 全部 PASS。
|
||||
|
||||
- [ ] **Step 5: 浏览器验证并报告**
|
||||
|
||||
真实账号验证可申请家谱列表、我的申请空状态、无家谱上下文审核分支、隐私和控制台错误。未经用户明确授权,不提交或审核真实申请。
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# 帮助中心 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`。
|
||||
- 浏览器验证真实列表/空状态和控制台。
|
||||
@@ -0,0 +1,249 @@
|
||||
# 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: 浏览器只读验证**
|
||||
|
||||
真实登录态验证套餐、家谱下拉、订单空/有数据态、无伪支付动作和控制台错误。未经用户本轮明确授权,不创建真实订单。
|
||||
@@ -0,0 +1,424 @@
|
||||
# 官网资讯 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 页面。
|
||||
```
|
||||
@@ -0,0 +1,71 @@
|
||||
# 应用下载页 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. 浏览器使用测试账号验证真实列表或空状态;没有推广数据时,不制造详情或示例推广。
|
||||
@@ -0,0 +1,170 @@
|
||||
# 官网资讯 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` 和首页文章卡片;它们作为后续独立批次。
|
||||
- 不修改法律页“待法务确认”状态。
|
||||
Reference in New Issue
Block a user