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

425 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 官网资讯 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`,新增两个真实请求层用例:
```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:
```powershell
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`
```js
if (req.auth !== false) handleUnauthorized(status);
```
不得改变默认鉴权请求的现有行为。
- [ ] **Step 4: 运行 GREEN**
Run:
```powershell
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`,并新增契约用例:
```js
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:
```powershell
node --test tests/api-client-contract.test.js
```
Expected: `siteArticles` 不存在。
- [ ] **Step 3: 写最小实现**
`utils/ApiClient.js` 增加 `siteArticles`
```js
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:
```powershell
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
```js
{
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:
```powershell
node --test tests/site-news-pages.test.js
```
Expected: 模块不存在。
- [ ] **Step 3: 写 UMD 模块最小实现**
创建 UMD 模块:Node 环境导出 `module.exports = factory(root)`;浏览器环境挂到 `root.SiteNewsPages`,并在 `DOMContentLoaded` 调用 `root.SiteNewsPages.init()`。关键行为:
```js
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:
```powershell
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 测试**
断言:
```js
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:
```powershell
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 范围。
- `SiteArticleVo` 11 个字段的 R/I/S/A 分类、SQL 长度和默认值。
- 列表直接数组、服务端排序、详情重读匹配。
- YAML 只给通用响应且没有导出 `SiteArticleVo` Schema 的差异。
- `coverOssId` 文件 URL 和站点文章单条详情接口仍阻断。
在阶段 7 增加第八批完成说明。
- [ ] **Step 5: 运行自动化验收**
Run:
```powershell
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: 阶段报告**
按以下格式报告并停下:
```text
Changed: 官网资讯列表、详情、公开请求语义和规划补充。
Verified: 专项/全量测试数量及真实浏览器覆盖。
Conflicts: YAML 通用响应与后端完整 VO/SQL 的差异。
Blocked: 单条详情接口、封面文件 URL、仍无 PC Controller 的 pending 页面。
```