feat(api): 添加帮助中心反馈系统和VIP服务功能

- 在ApiClient中新增submitFeedback、myFeedback、helpArticles、helpArticleDetail、
  siteArticles、promotions、vipPackages、createVipOrder、vipOrders等方法
- 添加帮助文章和站点资讯的参数验证逻辑
- 更新测试文件添加新的API方法测试用例
- 在HTML页面中添加反馈、帮助和VIP服务相关页面的脚本引用
- 更新加入家谱页面为完整的申请流程界面
- 修改资讯详情页面为站点资讯展示页面
- 更新AxiosRequestUtil中认证处理逻辑
- 添加世系树渲染的HTML生成函数用于页面复用
- 更新文档中的API契约说明和页面规划
This commit is contained in:
fizzleaf
2026-07-30 16:04:48 +08:00
parent fb1743aa2a
commit 309598bfa6
53 changed files with 6740 additions and 282 deletions
@@ -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: 官网内容或公开家谱接口的下一最小批次。
```