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

12 KiB
Raw Blame History

应用下载页 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 的真实客户端边界,传入:

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): string
    • normalizePromotion(item): PromotionView | null
    • normalizePromotionList(data): PromotionView[]
    • renderPromotionList(data): string
    • renderPromotionLoginRequired(): string
    • loadPromotions(api): Promise<PromotionView[]>
    • isUnauthorized(error): boolean
    • init(): 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'
}

分别覆盖:

  1. 规范化结果只保留 PromotionView 四字段;
  2. 不安全数字长 ID、空标题和 status=1 返回 null
  3. 直接数组正常,{ rows: [...] } 和混入非法元素返回空数组;
  4. https://http:// 保留,javascript:data:、相对路径和空值返回空字符串;
  5. 渲染转义标题与说明,不出现 coverOssIdpromotionKeysortOrderstatusremark
  6. 合法 URL 使用 target="_blank"rel="noopener noreferrer",非法 URL 不生成 <a>
  7. loadPromotions(api) 只调用一次 api.promotions(),不传 platform
  8. 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、非空 promotionTitlestatus === '0',然后只返回 PromotionViewnormalizePromotionList 必须拒绝非直接数组以及包含任一非法元素的数组。

  • 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">,不输出 hreftargetrel。空列表固定返回:

<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-pagedata-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 为空;

  • promotionIdpromotionKeycoverOssIdplatformsortOrderstatusremark 为 I

  • promotionTitlepromotionDesc、安全 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 0diff --check 无错误,CRLF warning 可记录但不算失败。

  • Step 4: 运行全量测试

Run:

npm test

Expected: 0 failures。

  • Step 5: 浏览器验证真实分支

只读验证:

  1. 未登录打开 app.html,确认下载内容保留、推广区域显示登录入口、未发生推广请求;
  2. 使用已授权测试账号登录后打开 app.html
  3. 后端返回空数组时显示“当前暂无应用推广”;有数据时检查标题、说明、安全外链和内部字段隐藏;
  4. 检查页面控制台;
  5. 不点击外部推广链接,不创建或修改后端数据;
  6. 浏览器控制不稳定时停止自动关闭标签页,只报告已取得的验证结果并清理本地临时服务。
  • Step 6: 标记计划状态并按阶段格式汇报

报告必须包含:

Changed: ApiClient、推广模块、app.html 和规划新增内容。
Verified: RED/GREEN、专项、全量和浏览器覆盖数量。
Conflicts: YAML 匿名/无 Query 与后端真实鉴权/platform 的差异。
Blocked: 缺少封面 URL、platform 枚举或真实推广数据时的剩余联调项。
Next: 官网内容或公开家谱接口的下一最小批次。