Files
jiapu/docs/superpowers/plans/2026-07-29-app-promotions.md
T
fizzleaf 309598bfa6 feat(api): 添加帮助中心反馈系统和VIP服务功能
- 在ApiClient中新增submitFeedback、myFeedback、helpArticles、helpArticleDetail、
  siteArticles、promotions、vipPackages、createVipOrder、vipOrders等方法
- 添加帮助文章和站点资讯的参数验证逻辑
- 更新测试文件添加新的API方法测试用例
- 在HTML页面中添加反馈、帮助和VIP服务相关页面的脚本引用
- 更新加入家谱页面为完整的申请流程界面
- 修改资讯详情页面为站点资讯展示页面
- 更新AxiosRequestUtil中认证处理逻辑
- 添加世系树渲染的HTML生成函数用于页面复用
- 更新文档中的API契约说明和页面规划
2026-07-30 16:04:48 +08:00

392 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 应用下载页 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: 官网内容或公开家谱接口的下一最小批次。
```