- 在ApiClient中新增submitFeedback、myFeedback、helpArticles、helpArticleDetail、 siteArticles、promotions、vipPackages、createVipOrder、vipOrders等方法 - 添加帮助文章和站点资讯的参数验证逻辑 - 更新测试文件添加新的API方法测试用例 - 在HTML页面中添加反馈、帮助和VIP服务相关页面的脚本引用 - 更新加入家谱页面为完整的申请流程界面 - 修改资讯详情页面为站点资讯展示页面 - 更新AxiosRequestUtil中认证处理逻辑 - 添加世系树渲染的HTML生成函数用于页面复用 - 更新文档中的API契约说明和页面规划
12 KiB
应用下载页 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;客户端即使被其它调用方使用,也只允许可选 Queryplatform。 - 页面不展示或拼接
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 的真实客户端边界,传入:
await client.promotions({
platform: 'pc',
keyword: 'must-drop'
});
手工断言请求为:
[
'get',
'/genealogy/pc/promotions',
{ platform: 'pc' },
'Bearer access-token'
]
该测试捕获错误 path、错误 method、契约外 Query 被透传或误设 auth: false。
- Step 2: 运行客户端测试观察 RED
Run:
node --test tests/api-client-contract.test.js
Expected: FAIL,提示 promotions 未导出或不是函数。
- Step 3: 添加最小客户端实现
在 helpArticleDetail 和 VIP 方法附近添加:
promotions: function (query) {
return request('GET', '/genealogy/pc/promotions', {
query: pickDefined(query, ['platform'])
});
},
不得增加 auth: false、默认 platform 或旧路径 fallback。
- Step 4: 运行客户端测试观察 GREEN
Run:
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): stringnormalizePromotion(item): PromotionView | nullnormalizePromotionList(data): PromotionView[]renderPromotionList(data): stringrenderPromotionLoginRequired(): stringloadPromotions(api): Promise<PromotionView[]>isUnauthorized(error): booleaninit(): Promise<void>
PromotionView 的唯一形状:
{
promotionId: '2062179707935264769',
promotionTitle: '下载移动端',
promotionDesc: '随时查看家谱内容',
targetUrl: 'https://example.com/download'
}
- Step 1: 写完整真实 DTO fixture 和失败测试
Fixture 必须包含后端全部 10 个字段:
{
promotionId: '2062179707935264769',
promotionKey: 'app-download',
promotionTitle: '下载移动端',
promotionDesc: '随时查看家谱内容',
coverOssId: '2062179707935264701',
targetUrl: 'https://example.com/download',
platform: 'all',
sortOrder: 10,
status: '0',
remark: 'internal-only'
}
分别覆盖:
- 规范化结果只保留
PromotionView四字段; - 不安全数字长 ID、空标题和
status=1返回null; - 直接数组正常,
{ rows: [...] }和混入非法元素返回空数组; https://、http://保留,javascript:、data:、相对路径和空值返回空字符串;- 渲染转义标题与说明,不出现
coverOssId、promotionKey、sortOrder、status、remark; - 合法 URL 使用
target="_blank"和rel="noopener noreferrer",非法 URL 不生成<a>; loadPromotions(api)只调用一次api.promotions(),不传platform;isUnauthorized仅把 HTTP/业务 401 视为登录失效,403 为普通错误。
- Step 2: 运行模块测试观察 RED
Run:
node --test tests/app-promotion-pages.test.js
Expected: FAIL,提示 public/js/app-promotion-pages.js 不存在。
- Step 3: 实现 UMD 模块的纯函数
实现稳定 ID:
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:
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: 实现渲染和读取函数
有链接时结构:
<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。空列表固定返回:
<div class="api-empty">当前暂无应用推广</div>
未登录固定返回:
<div class="api-empty">登录后可查看应用推广。<a href="login.html">去登录</a></div>
loadPromotions(api) 必须执行 await api.promotions(),并通过“原数组长度等于规范化后数组长度”确认响应完整。
- Step 5: 实现区域级初始化
init() 只在 [data-promotion-page] 和 [data-promotion-list] 同时存在时运行:
if (!api || !api.getToken || !api.getToken()) {
list.innerHTML = renderPromotionLoginRequired();
return;
}
有 token 时加载一次。401 调用 api.clearToken() 并渲染登录入口;403、网络失败或非法响应渲染“应用推广读取失败,请稍后重试”。不得修改 root.location。
- Step 6: 运行模块测试观察 GREEN
Run:
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,断言:
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:
node --test tests/app-promotion-pages.test.js tests/pc-scope.test.js
Expected: FAIL,提示 app.html 尚未加载新脚本。
- Step 3: 修改页面加载脚本
在 page-effects.js 后加载:
<script src="public/js/app-promotion-pages.js"></script>
保留现有 data-promotion-page、data-promotion-list 和初始加载文案。不得添加筛选器、刷新按钮、示例推广或隐藏 ID 输入。
- Step 4: 仅在需要时补充无图片卡片样式
如果 <a class="promotion-card"> 不能继承卡片文字颜色与块级点击区域,只增加:
.promotion-card {
display: block;
color: inherit;
text-decoration: none;
}
不得调整首页广告样式或重做应用下载页布局。
- Step 5: 运行页面与模块组合测试观察 GREEN
Run:
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:
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:
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:
npm test
Expected: 0 failures。
- Step 5: 浏览器验证真实分支
只读验证:
- 未登录打开
app.html,确认下载内容保留、推广区域显示登录入口、未发生推广请求; - 使用已授权测试账号登录后打开
app.html; - 后端返回空数组时显示“当前暂无应用推广”;有数据时检查标题、说明、安全外链和内部字段隐藏;
- 检查页面控制台;
- 不点击外部推广链接,不创建或修改后端数据;
- 浏览器控制不稳定时停止自动关闭标签页,只报告已取得的验证结果并清理本地临时服务。
- Step 6: 标记计划状态并按阶段格式汇报
报告必须包含:
Changed: ApiClient、推广模块、app.html 和规划新增内容。
Verified: RED/GREEN、专项、全量和浏览器覆盖数量。
Conflicts: YAML 匿名/无 Query 与后端真实鉴权/platform 的差异。
Blocked: 缺少封面 URL、platform 枚举或真实推广数据时的剩余联调项。
Next: 官网内容或公开家谱接口的下一最小批次。