feat(api): 添加帮助中心反馈系统和VIP服务功能
- 在ApiClient中新增submitFeedback、myFeedback、helpArticles、helpArticleDetail、 siteArticles、promotions、vipPackages、createVipOrder、vipOrders等方法 - 添加帮助文章和站点资讯的参数验证逻辑 - 更新测试文件添加新的API方法测试用例 - 在HTML页面中添加反馈、帮助和VIP服务相关页面的脚本引用 - 更新加入家谱页面为完整的申请流程界面 - 修改资讯详情页面为站点资讯展示页面 - 更新AxiosRequestUtil中认证处理逻辑 - 添加世系树渲染的HTML生成函数用于页面复用 - 更新文档中的API契约说明和页面规划
This commit is contained in:
@@ -0,0 +1,376 @@
|
||||
# 家谱主页 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:** 将 `profile-family-home.html` 从静态预览开放为真实 PC 家谱主页,展示当前家谱概览和世系树预览,并保留已接入业务页入口。
|
||||
|
||||
**Architecture:** `profile-common.js` 继续唯一拥有当前 `genealogyId`;页面只调用已有 `genealogyOverview(genealogyId)` 和 `lineageTree(genealogyId)`。`lineage-pages.js` 继续唯一拥有世系树节点规范化与 HTML 生成,新建 `family-home-pages.js` 只负责概览 DTO、权限显隐、并行读取和页面状态。
|
||||
|
||||
**Tech Stack:** 静态 HTML、原生 JavaScript UMD、Axios、Node `node:test`。
|
||||
|
||||
**Status:** 2026-07-29 已按 Task 1–4 实施;自动化与无家谱真实账号分支已验证,有家谱数据态等待具备真实家谱的账号复核。
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- 后端 `D:\WorkSpace\Java\Genealogy` 全程只读。
|
||||
- 只使用 PC 接口;不得调用 `/genealogy/dashboard/overview`、APP 或管理后台接口。
|
||||
- `genealogyId` 只能来自 `ProfileUI.getGenealogyId()`,始终按字符串处理。
|
||||
- `GET /genealogy/pc/genealogies/{genealogyId}/overview` 返回 `AppGenealogyVo`,它不是内容统计接口;不得制造文章、相册、视频或活动数量。
|
||||
- 世系树只读调用 `GET /genealogy/pc/genealogies/{genealogyId}/lineage/tree`。
|
||||
- 家谱管理入口仅在响应 `canManage=true` 时显示;内容页自身继续负责更细权限。
|
||||
- PC 没有成员邀请创建/分享接口;主页不得开放“邀请家人”操作。
|
||||
- 本批次没有写接口,不创建、修改或删除任何真实业务数据。
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 家谱主页 DTO 与页面契约
|
||||
|
||||
**Files:**
|
||||
- Create: `public/js/family-home-pages.js`
|
||||
- Create: `tests/family-home-pages.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `ProfileUI.getGenealogyId()`、`GenealogyApi.defaultClient`
|
||||
- Produces:
|
||||
- `normalizeFamilyOverview(item, expectedGenealogyId)`
|
||||
- `renderFamilyOverview(overview)`
|
||||
- `shouldShowFamilyManagement(overview)`
|
||||
- `shouldRedirectToLogin(api, error)`
|
||||
|
||||
- [ ] **Step 1: 写失败数据边界测试**
|
||||
|
||||
```js
|
||||
const overview = FamilyHomePages.normalizeFamilyOverview({
|
||||
genealogyId: '2062179707935264769',
|
||||
genealogyNo: 'G20260729001',
|
||||
genealogyName: '叶氏家谱',
|
||||
surname: '叶',
|
||||
ancestralHall: '南阳堂',
|
||||
originPlace: '四川成都',
|
||||
regionFullName: '四川省 成都市',
|
||||
memberCount: 12,
|
||||
personCount: 36,
|
||||
status: '0',
|
||||
canManage: true,
|
||||
canEditContent: true,
|
||||
ownerUserId: 'must-not-render',
|
||||
coverOssId: 'must-not-render'
|
||||
}, '2062179707935264769');
|
||||
|
||||
assert.deepEqual(overview, {
|
||||
genealogyId: '2062179707935264769',
|
||||
genealogyNo: 'G20260729001',
|
||||
genealogyName: '叶氏家谱',
|
||||
surname: '叶',
|
||||
ancestralHall: '南阳堂',
|
||||
originPlace: '四川成都',
|
||||
regionFullName: '四川省 成都市',
|
||||
memberCount: 12,
|
||||
personCount: 36,
|
||||
canManage: true,
|
||||
canEditContent: true
|
||||
});
|
||||
assert.equal(
|
||||
FamilyHomePages.normalizeFamilyOverview(
|
||||
{ genealogyId: Number.MAX_SAFE_INTEGER + 1, genealogyName: '非法', status: '0' },
|
||||
'2062179707935264769'
|
||||
),
|
||||
null
|
||||
);
|
||||
assert.equal(
|
||||
FamilyHomePages.normalizeFamilyOverview(
|
||||
{ genealogyId: '2', genealogyName: '串谱', status: '0' },
|
||||
'2062179707935264769'
|
||||
),
|
||||
null
|
||||
);
|
||||
```
|
||||
|
||||
同时断言:
|
||||
|
||||
- `genealogyName` 为空、`status!='0'`、负数/非安全计数均拒绝;
|
||||
- 渲染转义全部文本,不出现 `ownerUserId`、`coverOssId` 或原始 JSON;
|
||||
- 只有布尔值 `canManage===true` 才开放管理入口;
|
||||
- 401 清理登录态,403 不清理登录态。
|
||||
|
||||
- [ ] **Step 2: 运行 RED**
|
||||
|
||||
Run: `node --test tests/family-home-pages.test.js`
|
||||
|
||||
Expected: FAIL,`family-home-pages.js` 不存在。
|
||||
|
||||
- [ ] **Step 3: 最小实现概览规范化**
|
||||
|
||||
```js
|
||||
function normalizeFamilyOverview(item, expectedGenealogyId) {
|
||||
var source = item || {};
|
||||
var genealogyId = normalizeId(source.genealogyId);
|
||||
var expectedId = normalizeId(expectedGenealogyId);
|
||||
var memberCount = normalizeCount(source.memberCount);
|
||||
var personCount = normalizeCount(source.personCount);
|
||||
|
||||
if (!genealogyId || genealogyId !== expectedId ||
|
||||
!text(source.genealogyName) || String(source.status) !== '0' ||
|
||||
memberCount === null || personCount === null) return null;
|
||||
return {
|
||||
genealogyId: genealogyId,
|
||||
genealogyNo: text(source.genealogyNo),
|
||||
genealogyName: text(source.genealogyName),
|
||||
surname: text(source.surname),
|
||||
ancestralHall: text(source.ancestralHall),
|
||||
originPlace: text(source.originPlace),
|
||||
regionFullName: text(source.regionFullName),
|
||||
memberCount: memberCount,
|
||||
personCount: personCount,
|
||||
canManage: source.canManage === true,
|
||||
canEditContent: source.canEditContent === true
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 运行 GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/family-home-pages.test.js
|
||||
node --check public/js/family-home-pages.js
|
||||
```
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 复用世系树唯一渲染 owner
|
||||
|
||||
**Files:**
|
||||
- Modify: `public/js/lineage-pages.js`
|
||||
- Modify: `tests/lineage-pages.test.js`
|
||||
- Modify: `tests/family-home-pages.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `LineagePages.renderLineageTreeHtml(data)`
|
||||
- Consumes: 现有 `normalizeLineagePerson(item)`、`renderTreeNode(item, ancestry)`
|
||||
|
||||
- [ ] **Step 1: 写失败共享渲染测试**
|
||||
|
||||
```js
|
||||
const tree = [{
|
||||
personId: '2062179707935264770',
|
||||
genealogyId: '2062179707935264769',
|
||||
name: '<始祖>',
|
||||
status: '0',
|
||||
spouses: [],
|
||||
children: []
|
||||
}];
|
||||
|
||||
const html = LineagePages.renderLineageTreeHtml(tree);
|
||||
assert.match(html, /<始祖>/);
|
||||
assert.match(html, /data-lineage-person="2062179707935264770"/);
|
||||
assert.doesNotMatch(html, /2062179707935264769/);
|
||||
assert.match(LineagePages.renderLineageTreeHtml([]), /暂无世系树/);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 运行 RED**
|
||||
|
||||
Run: `node --test tests/lineage-pages.test.js tests/family-home-pages.test.js`
|
||||
|
||||
Expected: FAIL,`renderLineageTreeHtml` 未导出。
|
||||
|
||||
- [ ] **Step 3: 从现有 `renderTree` 提取纯 HTML owner**
|
||||
|
||||
```js
|
||||
function renderLineageTreeHtml(data) {
|
||||
var nodes = normalizeList(data)
|
||||
.map(function (item) { return renderTreeNode(item, {}); })
|
||||
.filter(Boolean);
|
||||
|
||||
return nodes.length
|
||||
? '<ul class="lineage-tree">' + nodes.join('') + '</ul>'
|
||||
: '<div class="api-empty">暂无世系树</div>';
|
||||
}
|
||||
|
||||
function renderTree(data) {
|
||||
var container = query('[data-lineage-tree]');
|
||||
if (container) container.innerHTML = renderLineageTreeHtml(data);
|
||||
}
|
||||
```
|
||||
|
||||
在 UMD 导出对象加入:
|
||||
|
||||
```js
|
||||
renderLineageTreeHtml: renderLineageTreeHtml
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 运行 GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/lineage-pages.test.js tests/family-home-pages.test.js
|
||||
node --check public/js/lineage-pages.js
|
||||
```
|
||||
|
||||
Expected: PASS,现有世系管理页输出不变。
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 开放真实家谱主页
|
||||
|
||||
**Files:**
|
||||
- Modify: `profile-family-home.html`
|
||||
- Modify: `public/js/family-home-pages.js`
|
||||
- Modify: `tests/family-home-pages.test.js`
|
||||
- Modify: `tests/pending-pages.test.js`
|
||||
- Modify: `tests/stage6-navigation.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes:
|
||||
- `api.genealogyOverview(genealogyId)`
|
||||
- `api.lineageTree(genealogyId)`
|
||||
- `LineagePages.renderLineageTreeHtml(data)`
|
||||
- Produces: `initFamilyHomePage()`、`init()`
|
||||
|
||||
- [ ] **Step 1: 写失败页面测试**
|
||||
|
||||
断言:
|
||||
|
||||
- `profile-family-home.html` 不再包含 `data-feature-status="pending"` 或 `pending-pages.js`;
|
||||
- 加载顺序为 `profile-common.js`、`lineage-pages.js`、`family-home-pages.js`;
|
||||
- 标题、摘要、计数、管理入口、世系预览均有稳定 `data-*` hook;
|
||||
- 硬编码“四川武胜汤氏族”被删除;
|
||||
- “邀请家人”不再是可点击业务入口,并明确提示“PC 暂未开放邀请”;
|
||||
- 谱文、相册、视频、功德、祭祀、世系、动态入口继续携带 `data-genealogy-context-link`;
|
||||
- 初始化只并行调用:
|
||||
|
||||
```js
|
||||
Promise.all([
|
||||
api.genealogyOverview(genealogyId),
|
||||
api.lineageTree(genealogyId)
|
||||
])
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 运行 RED**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/family-home-pages.test.js tests/pending-pages.test.js tests/stage6-navigation.test.js
|
||||
```
|
||||
|
||||
Expected: FAIL,主页仍为 pending 且包含硬编码家谱。
|
||||
|
||||
- [ ] **Step 3: 实现只读初始化流程**
|
||||
|
||||
```js
|
||||
async function initFamilyHomePage() {
|
||||
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
|
||||
var genealogyId = root.ProfileUI && root.ProfileUI.getGenealogyId();
|
||||
var results;
|
||||
var overview;
|
||||
|
||||
if (!genealogyId) {
|
||||
root.location.replace('profile-families.html?next=profile-family-home.html');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
results = await Promise.all([
|
||||
api.genealogyOverview(genealogyId),
|
||||
api.lineageTree(genealogyId)
|
||||
]);
|
||||
overview = normalizeFamilyOverview(results[0], genealogyId);
|
||||
if (!overview) throw new Error('家谱概览响应无效');
|
||||
renderFamilyOverview(overview);
|
||||
renderManagementAccess(overview.canManage);
|
||||
query('[data-lineage-home-tree]').innerHTML =
|
||||
root.LineagePages.renderLineageTreeHtml(results[1]);
|
||||
} catch (error) {
|
||||
if (shouldRedirectToLogin(api, error)) return redirectToLogin(api);
|
||||
renderFamilyHomeError(error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
页面只展示:
|
||||
|
||||
- 家谱名称、编号、姓氏、堂号、祖籍/地区;
|
||||
- `memberCount`、`personCount`;
|
||||
- 真实世系树及进入完整世系页的链接;
|
||||
- 后端已经接入的内容模块入口。
|
||||
|
||||
- [ ] **Step 4: 运行 GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/family-home-pages.test.js tests/pending-pages.test.js tests/stage6-navigation.test.js
|
||||
node --check public/js/family-home-pages.js
|
||||
```
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 规划记录与完整验收
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/PC接口对接规划.md`
|
||||
- Modify: `docs/superpowers/plans/2026-07-29-family-home.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1–3 的只读家谱主页闭环。
|
||||
|
||||
- [ ] **Step 1: 更新规划字段表**
|
||||
|
||||
新增家谱主页小节,逐字段记录:
|
||||
|
||||
| 字段 | 分类 | 页面用途 |
|
||||
| --- | --- | --- |
|
||||
| `genealogyId` | I/A | 当前上下文、两条请求 path、响应一致性校验 |
|
||||
| `genealogyNo`、`genealogyName`、`surname` | R | 标题和基础信息 |
|
||||
| `ancestralHall`、`originPlace`、`regionFullName` | R | 非空时展示 |
|
||||
| `memberCount`、`personCount` | R | 非负只读计数 |
|
||||
| `canManage`、`canEditContent` | I | 权限显隐,不作为用户输入 |
|
||||
| `ownerUserId`、`coverOssId` | I | 当前主页不展示、不手填 |
|
||||
| `LineagePersonTreeView.spouses/children` | R | 递归世系预览 |
|
||||
|
||||
同时记录:
|
||||
|
||||
- `/overview` 实际是详情别名,不包含内容聚合统计;
|
||||
- `/genealogy/dashboard/overview` 是后台权限接口,不进入 PC 前端;
|
||||
- 成员邀请没有 PC 接口,继续阻断。
|
||||
|
||||
- [ ] **Step 2: 聚焦验证**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/family-home-pages.test.js tests/lineage-pages.test.js tests/pending-pages.test.js tests/stage6-navigation.test.js tests/api-client-contract.test.js
|
||||
node --check public/js/family-home-pages.js
|
||||
node --check public/js/lineage-pages.js
|
||||
```
|
||||
|
||||
Expected: 全部 PASS。
|
||||
|
||||
- [ ] **Step 3: 全量和差异验证**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
npm test
|
||||
git -c safe.directory=D:/WorkSpace/Web/jiapu diff --check
|
||||
```
|
||||
|
||||
Expected: 0 failed,差异检查无错误。
|
||||
|
||||
- [ ] **Step 4: 浏览器真实只读验证**
|
||||
|
||||
使用真实登录态验证:
|
||||
|
||||
1. 无家谱上下文时只跳转选择页,不发家谱业务请求;
|
||||
2. 有真实家谱时标题、概览计数和世系树来自 PC 响应;
|
||||
3. 普通成员看不到管理按钮,管理者可见;
|
||||
4. 所有入口透传同一 `genealogyId`;
|
||||
5. 空世系、403、404、网络错误都有明确页面状态;
|
||||
6. 控制台无错误;
|
||||
7. 不创建或修改任何真实数据。
|
||||
Reference in New Issue
Block a user