# 应用下载页 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>`。 - [ ] **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` - `isUnauthorized(error): boolean` - `init(): Promise` `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 不生成 ``; 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

...

...

了解详情
``` 无链接时使用 `
`,不输出 `href`、`target` 或 `rel`。空列表固定返回: ```html
当前暂无应用推广
``` 未登录固定返回: ```html
登录后可查看应用推广。去登录
``` `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 ``` 保留现有 `data-promotion-page`、`data-promotion-list` 和初始加载文案。不得添加筛选器、刷新按钮、示例推广或隐藏 ID 输入。 - [ ] **Step 4: 仅在需要时补充无图片卡片样式** 如果 `` 不能继承卡片文字颜色与块级点击区域,只增加: ```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: 官网内容或公开家谱接口的下一最小批次。 ```