Files
jiapu/docs/superpowers/plans/2026-07-30-site-news.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

14 KiB
Raw Blame History

官网资讯 PC 接口对接实施计划

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:news.htmlarticle-detail.html 接入公开 PC 站点文章列表接口,交付可筛选列表与精确详情闭环。

Architecture: utils/ApiClient.js 独占 PC path、公开鉴权标记和 Query 校验;public/js/site-news-pages.js 独占 SiteArticleVo 规范化、渲染和页面初始化。后端没有单条详情接口,详情页使用 URL 中由列表响应生成的字符串 ID 重读列表并精确匹配。

Tech Stack: 原生 HTML/CSS/JavaScript、现有 Axios 请求层、Node node:test

Global Constraints

  • 后端 D:\WorkSpace\Java\Genealogy 只读,不得修改。
  • 只使用 /genealogy/pc/site/articles,不得调用 APP、后台管理或家谱谱文 CRUD 接口。
  • 本批只修改 news.htmlarticle-detail.html,不混改其他静态内容页。
  • 业务 ID 始终保持字符串,不提供手填 ID 或 OSS ID。
  • articleType 只允许 news/notice/downloadlimit 固定为 100 且客户端只允许 1–100 的整数。
  • 公开请求设置 auth: false;其 401/403 不清理既有 token、不跳转登录。
  • 响应只接受直接 SiteArticleVo[];旧字段、分页对象和任一非法元素均使整批失败。
  • 所有响应文本 HTML 转义;正文仅保留换行;外链只允许绝对 HTTP/HTTPS。
  • 当前工作区由用户明确授权直接施工;不得 stage、commit、push 或创建 PR。
  • 子代理仅用于探索和只读复核;业务代码修改、取舍与最终验证由主代理完成。

Task 1: 修正公开请求的登录态隔离

Files:

  • Modify: tests/request-auth-state.test.js
  • Modify: utils/AxiosRequestUtil.js

Interfaces:

  • Consumes: createRequester({ getToken, onUnauthorized, axiosInstance })

  • Produces: request(method, path, { auth: false }) 不发送 Authorization,且 HTTP/业务 401 均不调用 onUnauthorized

  • Step 1: 写失败测试

tests/request-auth-state.test.js 引入 ../utils/AxiosRequestUtil.js,新增两个真实请求层用例:

test('public HTTP 401 neither sends nor clears the stored login token', async () => {
  let storedToken = 'access-token';
  let seenRequest;
  const requester = AxiosRequestUtil.createRequester({
    baseUrl: 'https://api.example.test',
    clientId: 'web-pc',
    getToken() { return storedToken; },
    onUnauthorized() { storedToken = ''; },
    axiosInstance: {
      request(config) {
        seenRequest = config;
        return Promise.reject({
          message: 'Request failed',
          response: { status: 401, data: { code: 401, msg: '认证失败' } }
        });
      }
    }
  });

  await assert.rejects(requester('GET', '/public', { auth: false }));
  assert.equal(seenRequest.headers.Authorization, undefined);
  assert.equal(storedToken, 'access-token');
});

test('public business 401 preserves the stored login token', async () => {
  let storedToken = 'access-token';
  const requester = AxiosRequestUtil.createRequester({
    baseUrl: 'https://api.example.test',
    clientId: 'web-pc',
    getToken() { return storedToken; },
    onUnauthorized() { storedToken = ''; },
    axiosInstance: {
      request() {
        return Promise.resolve({
          data: { code: 401, msg: '认证失败' }
        });
      }
    }
  });

  await assert.rejects(requester('GET', '/public', { auth: false }));
  assert.equal(storedToken, 'access-token');
});
  • Step 2: 运行 RED

Run:

node --test tests/request-auth-state.test.js

Expected: 新增用例因 handleUnauthorized 未区分 auth: false 而失败。

  • Step 3: 写最小实现

utils/AxiosRequestUtil.js 的单次 request 闭包内,使 HTTP catch 与业务解包 catch 只在 req.auth !== false 时调用 handleUnauthorized

if (req.auth !== false) handleUnauthorized(status);

不得改变默认鉴权请求的现有行为。

  • Step 4: 运行 GREEN

Run:

node --test tests/request-auth-state.test.js

Expected: 现有默认 401 清 token、403 保留 token,以及新增公开请求用例全部通过。


Task 2: 增加 PC 站点文章 ApiClient 契约

Files:

  • Modify: tests/api-client-contract.test.js
  • Modify: utils/ApiClient.js

Interfaces:

  • Produces: siteArticles(query?: { articleType?: 'news'|'notice'|'download', limit?: number }): Promise<Array>

  • Request: GET /genealogy/pc/site/articles, { auth: false, query }

  • Step 1: 写失败测试

在公开方法清单中增加 siteArticles,并新增契约用例:

await client.siteArticles({
  articleType: 'notice',
  limit: 100,
  keyword: 'must-drop'
});

assert.deepEqual(seen, {
  method: 'get',
  url: '/genealogy/pc/site/articles',
  params: { articleType: 'notice', limit: 100 },
  authorization: undefined
});

assert.throws(
  () => client.siteArticles({ articleType: 'culture', limit: 100 }),
  /不支持的资讯类型/
);
assert.throws(
  () => client.siteArticles({ limit: 101 }),
  /资讯数量限制/
);

同时覆盖 limit0、小数、字符串和负数,确保只接受 1–100 的 Number 整数;省略 articleType 时只发送 { limit: 100 }

  • Step 2: 运行 RED

Run:

node --test tests/api-client-contract.test.js

Expected: siteArticles 不存在。

  • Step 3: 写最小实现

utils/ApiClient.js 增加 siteArticles

siteArticles: function (query) {
  var source = query || {};
  var articleType = source.articleType === undefined || source.articleType === null
    ? ''
    : String(source.articleType).trim();
  var limit = source.limit;
  var params = {};

  if (articleType && ['news', 'notice', 'download'].indexOf(articleType) === -1) {
    throw new Error('不支持的资讯类型:' + articleType);
  }
  if (limit !== undefined &&
      (typeof limit !== 'number' || !Number.isInteger(limit) || limit < 1 || limit > 100)) {
    throw new Error('资讯数量限制必须是 1 到 100 的整数');
  }
  if (articleType) params.articleType = articleType;
  if (limit !== undefined) params.limit = limit;
  return request('GET', '/genealogy/pc/site/articles', {
    auth: false,
    query: params
  });
},
  • Step 4: 运行 GREEN

Run:

node --test tests/api-client-contract.test.js tests/request-auth-state.test.js

Expected: 全部通过,且公开请求没有 Authorization Header。


Task 3: 实现站点资讯页面 owner

Files:

  • Create: tests/site-news-pages.test.js
  • Create: public/js/site-news-pages.js

Interfaces:

  • Consumes: GenealogyApi.defaultClient.siteArticles({ articleType?, limit: 100 })

  • Produces:

    • normalizeArticle(item)
    • normalizeArticleList(data)
    • normalizeArticleType(value)
    • normalizeExternalUrl(value)
    • readArticleId(search)
    • renderArticleList(data)
    • renderArticleDetail(item)
    • loadArticles(api, articleType?)
    • loadArticleDetail(api, articleId)
    • init()
  • Step 1: 写完整 RED 测试

创建 tests/site-news-pages.test.js,使用完整 SiteArticleVo fixture

{
  articleId: '2062179707935264769',
  articleType: 'notice',
  articleTitle: '平台公告',
  articleSummary: '公告摘要',
  articleContent: '第一行\n第二行',
  coverOssId: '2062179707935264701',
  externalUrl: 'https://example.com/notice',
  publishTime: '2026-07-30 09:00:00',
  sortOrder: 1,
  status: '0',
  remark: 'internal-only'
}

必须覆盖:

  • 安全长 ID 字符串保留;不安全 Number、零、空 ID 拒绝。

  • articleType 只接受 news/notice/download

  • 标题必填、状态必须为 0

  • 直接数组全有或全无;{ rows: [...] } 拒绝。

  • 规范化结果不包含 coverOssId/sortOrder/status/remark

  • 列表 HTML 指向 article-detail.html?articleId=2062179707935264769

  • 标题、摘要、正文转义;正文换行变为 <br />

  • 危险、相对和 data: 外链不渲染;合法 HTTP/HTTPS 使用 noopener noreferrer

  • loadArticles(api, 'notice') 调用 { articleType: 'notice', limit: 100 }

  • loadArticles(api, '') 调用 { limit: 100 }

  • loadArticleDetail 只返回同 ID;未匹配抛出“资讯不存在或已下线”。

  • 非法详情 ID 在调用 API 前抛出“资讯编号无效”。

  • readArticleId('?articleId=...') 只返回安全字符串。

  • Step 2: 运行 RED

Run:

node --test tests/site-news-pages.test.js

Expected: 模块不存在。

  • Step 3: 写 UMD 模块最小实现

创建 UMD 模块:Node 环境导出 module.exports = factory(root);浏览器环境挂到 root.SiteNewsPages,并在 DOMContentLoaded 调用 root.SiteNewsPages.init()。关键行为:

async function loadArticles(api, articleType) {
  var type = normalizeArticleType(articleType);
  var query = { limit: 100 };
  var data;
  var items;

  if (articleType && !type) throw new Error('资讯分类无效');
  if (type) query.articleType = type;
  data = await api.siteArticles(query);
  items = normalizeArticleList(data);
  if (!Array.isArray(data) || items.length !== data.length) {
    throw new Error('资讯列表响应无效');
  }
  return items;
}

init()

  • [data-site-news-page]:从 location.search 读取 articleType,加载并渲染 [data-site-news-list]

  • [data-site-article-page]:读取 articleId;非法时不发请求;合法时精确重读并渲染 [data-site-article-detail]

  • 所有失败只更新对应区域和 [data-site-*-status],不跳转、不清 token。

  • Step 4: 运行 GREEN

Run:

node --test tests/site-news-pages.test.js tests/api-client-contract.test.js tests/request-auth-state.test.js

Expected: 全部通过。


Task 4: 接入 HTML、规划并完成验收

Files:

  • Modify: news.html
  • Modify: article-detail.html
  • Modify: tests/site-news-pages.test.js
  • Modify: tests/public-static-pages.test.js
  • Modify: tests/pc-scope.test.js
  • Modify: docs/PC接口对接规划.md

Interfaces:

  • news.html: [data-site-news-page], [data-site-news-list], [data-site-news-status]

  • article-detail.html: [data-site-article-page], [data-site-article-detail], [data-site-article-status]

  • Step 1: 写 HTML 接入 RED 测试

断言:

assert.match(newsPage, /data-site-news-page/);
assert.match(newsPage, /data-site-news-list/);
assert.match(newsPage, /news\.html\?articleType=news/);
assert.match(newsPage, /news\.html\?articleType=notice/);
assert.match(newsPage, /news\.html\?articleType=download/);
assert.match(newsPage, /src="public\/js\/site-news-pages\.js"/);

assert.match(detailPage, /data-site-article-page/);
assert.match(detailPage, /data-site-article-detail/);
assert.match(detailPage, /src="public\/js\/site-news-pages\.js"/);
assert.doesNotMatch(detailPage, /src="public\/js\/article-pages\.js"/);
assert.doesNotMatch(newsPage + detailPage, /name="(?:articleId|coverOssId)"/);

更新 public-static-pages.test.js,移除旧的固定 #platform/#culture 链接断言,改为真实分类入口和详情目标文件断言。

  • Step 2: 运行 RED

Run:

node --test tests/site-news-pages.test.js tests/public-static-pages.test.js tests/pc-scope.test.js

Expected: HTML 尚未加载新模块、旧固定链接断言需迁移。

  • Step 3: 修改 HTML

news.html

  • <body data-site-news-page>
  • 删除“当前为静态展示”文案和两条硬编码文章。
  • 列表容器初始化为“资讯内容加载中”。
  • 分类链接使用规格中的四个固定 URL。
  • 按顺序加载 config.jsStorageUtil.jsaxios.jsAxiosRequestUtil.jsApiClient.jspage-effects.jssite-news-pages.js

article-detail.html

  • body 标记改为 data-site-article-page,避免与家谱谱文语义混用。

  • 将 hero 和正文合并到 data-site-article-detail 可替换容器。

  • 增加 data-site-article-status

  • 加载 site-news-pages.js,不加载 article-pages.js

  • Step 4: 更新规划

docs/PC接口对接规划.md 第 7 节新增“官网资讯契约”,记录:

  • method/path、公开鉴权、Query 枚举和 limit 范围。
  • SiteArticleVo 11 个字段的 R/I/S/A 分类、SQL 长度和默认值。
  • 列表直接数组、服务端排序、详情重读匹配。
  • YAML 只给通用响应且没有导出 SiteArticleVo Schema 的差异。
  • coverOssId 文件 URL 和站点文章单条详情接口仍阻断。

在阶段 7 增加第八批完成说明。

  • Step 5: 运行自动化验收

Run:

node --check public/js/site-news-pages.js
node --check utils/ApiClient.js
node --check utils/AxiosRequestUtil.js
node --test tests/site-news-pages.test.js tests/api-client-contract.test.js tests/request-auth-state.test.js tests/public-static-pages.test.js tests/pc-scope.test.js
node --test tests/*.test.js
git -c safe.directory=D:/WorkSpace/Web/jiapu diff --check

Expected: 所有命令退出码 0。

  • Step 6: 浏览器验收

启动临时本地静态服务器,验证:

  1. 未登录打开 news.html,页面不跳登录。
  2. 真实列表、分类或空状态正确。
  3. 有数据时点击一个由响应生成的详情链接,详情 ID 精确匹配;无数据时记录真实空状态。
  4. 控制台没有业务脚本错误。
  5. 不点击外部链接。
  6. 停止服务器并删除临时辅助文件。
  • Step 7: 阶段报告

按以下格式报告并停下:

Changed: 官网资讯列表、详情、公开请求语义和规划补充。
Verified: 专项/全量测试数量及真实浏览器覆盖。
Conflicts: YAML 通用响应与后端完整 VO/SQL 的差异。
Blocked: 单条详情接口、封面文件 URL、仍无 PC Controller 的 pending 页面。