- 在ApiClient中新增submitFeedback、myFeedback、helpArticles、helpArticleDetail、 siteArticles、promotions、vipPackages、createVipOrder、vipOrders等方法 - 添加帮助文章和站点资讯的参数验证逻辑 - 更新测试文件添加新的API方法测试用例 - 在HTML页面中添加反馈、帮助和VIP服务相关页面的脚本引用 - 更新加入家谱页面为完整的申请流程界面 - 修改资讯详情页面为站点资讯展示页面 - 更新AxiosRequestUtil中认证处理逻辑 - 添加世系树渲染的HTML生成函数用于页面复用 - 更新文档中的API契约说明和页面规划
14 KiB
官网资讯 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.html 与 article-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.html与article-detail.html,不混改其他静态内容页。 - 业务 ID 始终保持字符串,不提供手填 ID 或 OSS ID。
articleType只允许news/notice/download;limit固定为 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 }),
/资讯数量限制/
);
同时覆盖 limit 的 0、小数、字符串和负数,确保只接受 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.js、StorageUtil.js、axios.js、AxiosRequestUtil.js、ApiClient.js、page-effects.js、site-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 范围。
SiteArticleVo11 个字段的 R/I/S/A 分类、SQL 长度和默认值。- 列表直接数组、服务端排序、详情重读匹配。
- YAML 只给通用响应且没有导出
SiteArticleVoSchema 的差异。 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: 浏览器验收
启动临时本地静态服务器,验证:
- 未登录打开
news.html,页面不跳登录。 - 真实列表、分类或空状态正确。
- 有数据时点击一个由响应生成的详情链接,详情 ID 精确匹配;无数据时记录真实空状态。
- 控制台没有业务脚本错误。
- 不点击外部链接。
- 停止服务器并删除临时辅助文件。
- Step 7: 阶段报告
按以下格式报告并停下:
Changed: 官网资讯列表、详情、公开请求语义和规划补充。
Verified: 专项/全量测试数量及真实浏览器覆盖。
Conflicts: YAML 通用响应与后端完整 VO/SQL 的差异。
Blocked: 单条详情接口、封面文件 URL、仍无 PC Controller 的 pending 页面。