309598bfa6
- 在ApiClient中新增submitFeedback、myFeedback、helpArticles、helpArticleDetail、 siteArticles、promotions、vipPackages、createVipOrder、vipOrders等方法 - 添加帮助文章和站点资讯的参数验证逻辑 - 更新测试文件添加新的API方法测试用例 - 在HTML页面中添加反馈、帮助和VIP服务相关页面的脚本引用 - 更新加入家谱页面为完整的申请流程界面 - 修改资讯详情页面为站点资讯展示页面 - 更新AxiosRequestUtil中认证处理逻辑 - 添加世系树渲染的HTML生成函数用于页面复用 - 更新文档中的API契约说明和页面规划
392 lines
12 KiB
Markdown
392 lines
12 KiB
Markdown
# 应用下载页 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: 官网内容或公开家谱接口的下一最小批次。
|
||
```
|