feat(api): 完善家谱系统API客户端契约
- 实现家谱管理相关方法,包括创建、详情、概览、我的家谱和选项查询 - 添加家谱加入申请功能,支持申请、审核、取消和待审核列表操作 - 集成通知详情获取方法和通知ID安全验证机制 - 完善功德记录、谱文、相册、视频、祭祀活动的完整CRUD操作契约 - 实现家谱成员管理功能,包含成员列表、更新、移除和转让所有者操作 - 优化路径ID验证逻辑,拒绝不安全的数值ID并提供明确错误提示 - 更新测试用例以验证所有新增API方法的路径和请求体白名单机制
This commit is contained in:
@@ -33,7 +33,7 @@
|
||||
- `auditGenealogyJoinApply(genealogyId, applyId, body)`
|
||||
- `cancelGenealogyJoinApply(applyId)`
|
||||
|
||||
- [ ] **Step 1: 写失败的路径和 body 白名单测试**
|
||||
- [x] **Step 1: 写失败的路径和 body 白名单测试**
|
||||
|
||||
测试固定以下请求:
|
||||
|
||||
@@ -69,7 +69,7 @@ await client.auditGenealogyJoinApply(genealogyId, applyId, {
|
||||
- 审核 body 只保留 `status/auditRemark`。
|
||||
- 所有 path ID 拒绝不安全数字。
|
||||
|
||||
- [ ] **Step 2: 运行 RED**
|
||||
- [x] **Step 2: 运行 RED**
|
||||
|
||||
Run:
|
||||
|
||||
@@ -79,11 +79,11 @@ node --test --test-name-pattern "genealogy lifecycle" tests/api-client-contract.
|
||||
|
||||
Expected: FAIL,新方法尚不存在。
|
||||
|
||||
- [ ] **Step 3: 最小实现方法和白名单**
|
||||
- [x] **Step 3: 最小实现方法和白名单**
|
||||
|
||||
使用现有 `request()`、`pickDefined()` 和 `toRequiredPathId()`;不增加兼容别名或 APP fallback。
|
||||
|
||||
- [ ] **Step 4: 运行 GREEN**
|
||||
- [x] **Step 4: 运行 GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
@@ -110,7 +110,7 @@ Expected: PASS。
|
||||
- `normalizeCreatedGenealogy(item)`
|
||||
- `buildCreatedGenealogyUrl(item)`
|
||||
|
||||
- [ ] **Step 1: 写失败的创建行为测试**
|
||||
- [x] **Step 1: 写失败的创建行为测试**
|
||||
|
||||
```js
|
||||
assert.deepEqual(GenealogyEntryPages.buildGenealogyCreateBody({
|
||||
@@ -141,7 +141,7 @@ assert.deepEqual(GenealogyEntryPages.buildGenealogyCreateBody({
|
||||
- 创建响应必须有稳定 `genealogyId`、谱名和姓氏。
|
||||
- 跳转地址只能由响应 ID 产生:`profile-family-home.html?genealogyId=...`。
|
||||
|
||||
- [ ] **Step 2: 运行 RED**
|
||||
- [x] **Step 2: 运行 RED**
|
||||
|
||||
Run:
|
||||
|
||||
@@ -151,11 +151,11 @@ node --test tests/genealogy-entry-pages.test.js
|
||||
|
||||
Expected: FAIL,纯函数不存在。
|
||||
|
||||
- [ ] **Step 3: 最小实现纯函数**
|
||||
- [x] **Step 3: 最小实现纯函数**
|
||||
|
||||
严格构造 `AppGenealogyCreateBody`,可选空值省略,不读取页面外字段。
|
||||
|
||||
- [ ] **Step 4: 运行 GREEN**
|
||||
- [x] **Step 4: 运行 GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
@@ -182,7 +182,7 @@ Expected: PASS。
|
||||
- Consumes: Task 1 和 Task 2、`RegionPages`、`UploadPages`
|
||||
- Produces: `initGenealogyCreatePage()`
|
||||
|
||||
- [ ] **Step 1: 写失败的页面测试**
|
||||
- [x] **Step 1: 写失败的页面测试**
|
||||
|
||||
断言页面:
|
||||
|
||||
@@ -193,7 +193,7 @@ Expected: PASS。
|
||||
- 表单只包含后端创建 DTO 字段和地区选择辅助字段。
|
||||
- 创建按钮、状态区域和额度区域可由脚本控制。
|
||||
|
||||
- [ ] **Step 2: 运行 RED**
|
||||
- [x] **Step 2: 运行 RED**
|
||||
|
||||
Run:
|
||||
|
||||
@@ -203,7 +203,7 @@ node --test tests/genealogy-entry-pages.test.js tests/pending-pages.test.js test
|
||||
|
||||
Expected: FAIL,页面仍 pending 且缺上传/初始化行为。
|
||||
|
||||
- [ ] **Step 3: 实现页面初始化和提交**
|
||||
- [x] **Step 3: 实现页面初始化和提交**
|
||||
|
||||
初始化顺序:
|
||||
|
||||
@@ -218,7 +218,7 @@ Expected: FAIL,页面仍 pending 且缺上传/初始化行为。
|
||||
|
||||
401 清登录态;403 保留登录态;业务错误显示在表单状态区域。
|
||||
|
||||
- [ ] **Step 4: 运行 GREEN**
|
||||
- [x] **Step 4: 运行 GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
@@ -237,11 +237,11 @@ Expected: PASS。
|
||||
- Modify: `docs/PC接口对接规划.md`
|
||||
- Modify: `docs/superpowers/plans/2026-07-29-genealogy-create-first-batch.md`
|
||||
|
||||
- [ ] **Step 1: 更新契约和阶段状态**
|
||||
- [x] **Step 1: 更新契约和阶段状态**
|
||||
|
||||
记录 `AppGenealogyCreateBody`、`AppGenealogyVo`、额度、权限、上传派生字段、加入申请 ApiClient 契约及仍未开放的加入 UI。
|
||||
|
||||
- [ ] **Step 2: 聚焦和全量验证**
|
||||
- [x] **Step 2: 聚焦和全量验证**
|
||||
|
||||
Run:
|
||||
|
||||
@@ -253,6 +253,6 @@ git -c safe.directory=D:/WorkSpace/Web/jiapu diff --check
|
||||
|
||||
Expected: 全部 PASS。
|
||||
|
||||
- [ ] **Step 3: 浏览器验证并报告**
|
||||
- [x] **Step 3: 浏览器验证并报告**
|
||||
|
||||
使用真实账号验证创建页登录态、额度、地区加载、无手填 ID、控制台错误和提交前校验。不得为测试消耗真实创建额度;除非用户明确授权,不执行最终创建写操作。
|
||||
|
||||
@@ -0,0 +1,509 @@
|
||||
# 阶段 6 PC 功能页面 Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. Project rules reserve code edits for the primary agent; subagents may only perform read-only exploration and review.
|
||||
|
||||
**Goal:** 实现所有当前 PC 接口已具备闭环但前端缺失或未开放的谱文、相册/照片、祭祀/祭品、管理员邀约、指定账号绑定和家谱成员管理页面。
|
||||
|
||||
**Architecture:** `utils/ApiClient.js` 唯一拥有 PC method/path/query/body;每个业务使用独立 UMD 页面脚本,页面只消费脚本导出的规范化、校验、渲染和初始化接口。`profile-common.js` 提供家谱上下文,`upload-pages.js` 产生 OSS ID,所有写操作使用稳定字符串 ID、防重复提交并在成功后重读。
|
||||
|
||||
**Tech Stack:** 静态 HTML、原生 JavaScript UMD、Axios 请求封装、Node `node:test`、现有 profile CSS、wangEditor v5、统一分片上传。
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- 后端目录 `D:/WorkSpace/Java/Genealogy` 严格只读。
|
||||
- 只使用 `/genealogy/pc/**`,不得使用 APP 接口或旧路径 fallback。
|
||||
- 不允许手填业务 ID、用户 ID 或 OSS ID;int64 在浏览器边界保存为十进制字符串。
|
||||
- 普通列表无法重读停用记录的模块固定提交 `status=0`。
|
||||
- 每个模块严格执行 RED → GREEN → 聚焦测试 → 真实账号非破坏性验证 → 规划更新。
|
||||
- 当前工作树已有前几阶段改动;不得重置、覆盖或提交无关文件。除非用户另行要求,本计划不创建 Git commit。
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 扩展 PC ApiClient 契约
|
||||
|
||||
**Files:**
|
||||
- Modify: `utils/ApiClient.js`
|
||||
- Modify: `tests/api-client-contract.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
- `articles(genealogyId)`, `articleDetail(genealogyId, articleId)`, `createArticle(genealogyId, body)`, `updateArticle(genealogyId, articleId, body)`
|
||||
- `albums(genealogyId)`, `createAlbum(genealogyId, body)`, `updateAlbum(genealogyId, albumId, body)`, `albumPhotos(genealogyId, albumId)`, `createAlbumPhoto(genealogyId, albumId, body)`
|
||||
- `ceremonies(genealogyId)`, `ceremonyDetail(genealogyId, ceremonyId)`, `createCeremony`, `updateCeremony`, `ceremonyGifts`, `createCeremonyGift`
|
||||
- `replaceCeremonyInvitees(genealogyId, ceremonyId, inviteeUserIds)`
|
||||
- `genealogyMembers(genealogyId)`, `genealogyMemberOptions(genealogyId)`, `updateGenealogyMember`, `removeGenealogyMember`, `leaveGenealogy`, `transferGenealogyOwner`
|
||||
|
||||
- [x] **Step 1: 写失败的路径和 body 白名单测试**
|
||||
|
||||
```js
|
||||
await client.createArticle('9007199254740993001', {
|
||||
articleTitle: '族史',
|
||||
articleContent: '<p>正文</p>',
|
||||
coverOssId: '9007199254740993002',
|
||||
appUserId: 'must-drop',
|
||||
status: '0'
|
||||
});
|
||||
assert.deepEqual(calls[0].data, {
|
||||
articleTitle: '族史',
|
||||
articleContent: '<p>正文</p>',
|
||||
coverOssId: '9007199254740993002',
|
||||
status: '0'
|
||||
});
|
||||
|
||||
await client.replaceCeremonyInvitees(
|
||||
'9007199254740993001',
|
||||
'9007199254740993003',
|
||||
['9007199254740993004']
|
||||
);
|
||||
assert.deepEqual(calls.at(-1).data, {
|
||||
inviteeUserIds: ['9007199254740993004']
|
||||
});
|
||||
```
|
||||
|
||||
- [x] **Step 2: 运行 RED**
|
||||
|
||||
Run: `node --test --test-name-pattern "article|album|ceremony admin|genealogy member" tests/api-client-contract.test.js`
|
||||
|
||||
Expected: FAIL,原因是新方法不存在或仍只有删除方法。
|
||||
|
||||
- [x] **Step 3: 最小实现所有方法和白名单**
|
||||
|
||||
```js
|
||||
var ARTICLE_FIELDS = [
|
||||
'categoryId', 'articleTitle', 'articleSummary', 'coverOssId',
|
||||
'articleContent', 'authorName', 'sortOrder', 'status'
|
||||
];
|
||||
var ALBUM_FIELDS = ['albumName', 'albumDesc', 'coverOssId', 'sortOrder', 'status'];
|
||||
var ALBUM_PHOTO_FIELDS = [
|
||||
'ossId', 'photoTitle', 'photoDesc', 'photographer',
|
||||
'shootTime', 'sortOrder', 'status'
|
||||
];
|
||||
var CEREMONY_FIELDS = [
|
||||
'ceremonyType', 'ceremonyTitle', 'ceremonyDesc', 'ceremonyTime',
|
||||
'location', 'locationAddress', 'longitude', 'latitude',
|
||||
'coverOssId', 'sortOrder', 'status'
|
||||
];
|
||||
var CEREMONY_GIFT_FIELDS = ['giverName', 'giftAmount', 'giftMessage'];
|
||||
var MEMBER_UPDATE_FIELDS = ['memberName', 'relationName', 'roleType', 'lineagePersonId'];
|
||||
```
|
||||
|
||||
每个方法只通过既有 `request()`、`pickDefined()`、`toRequiredPathId()` 和路径 builder 发请求;成员 options 按 YAML 不发送 Query。
|
||||
|
||||
- [x] **Step 4: 运行 GREEN 和语法检查**
|
||||
|
||||
Run: `node --test tests/api-client-contract.test.js`
|
||||
|
||||
Run: `node --check utils/ApiClient.js`
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 谱文列表、详情、创建、修改和删除
|
||||
|
||||
**Files:**
|
||||
- Create: `public/js/article-pages.js`
|
||||
- Create: `tests/article-pages.test.js`
|
||||
- Modify: `profile-article.html`
|
||||
- Modify: `profile-article-edit.html`
|
||||
- Modify: `tests/pending-pages.test.js`
|
||||
- Modify: `tests/pc-scope.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1 article ApiClient methods、`ProfileUI.requireGenealogyContext()`、`ProfileUpload.bindUploadField()`、`RichEditorPages`
|
||||
- Produces: `buildArticleBody`, `normalizeArticle`, `normalizeArticles`, `matchesArticle`, `renderArticleList`, `renderArticleDetail`, `initArticleListPage`, `initArticleEditPage`
|
||||
|
||||
- [x] **Step 1: 写失败的谱文行为测试**
|
||||
|
||||
```js
|
||||
assert.deepEqual(ArticlePages.buildArticleBody({
|
||||
articleTitle: '族史',
|
||||
articleContent: '<p>正文</p>',
|
||||
coverOssId: '9007199254740993002',
|
||||
categoryId: '',
|
||||
status: '1'
|
||||
}), {
|
||||
articleTitle: '族史',
|
||||
articleContent: '<p>正文</p>',
|
||||
coverOssId: '9007199254740993002',
|
||||
status: '0'
|
||||
});
|
||||
assert.equal(
|
||||
ArticlePages.normalizeArticle({ articleId: Number.MAX_SAFE_INTEGER + 1 }),
|
||||
null
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
ArticlePages.renderArticleDetail({
|
||||
articleId: '9007199254740993003',
|
||||
articleTitle: '<img src=x onerror=alert(1)>',
|
||||
articleContent: '<script>alert(1)</script>',
|
||||
status: '0'
|
||||
}),
|
||||
/<script|onerror/
|
||||
);
|
||||
```
|
||||
|
||||
页面测试必须断言两个 HTML 不再 pending、加载 `article-pages.js`、没有手填 `articleId/categoryId/coverOssId/status`,列表页存在详情区域,编辑页存在标题/摘要/正文/作者/封面上传。
|
||||
|
||||
- [x] **Step 2: 运行 RED**
|
||||
|
||||
Run: `node --test tests/article-pages.test.js tests/api-client-contract.test.js tests/pending-pages.test.js tests/pc-scope.test.js`
|
||||
|
||||
Expected: FAIL,原因是模块不存在且页面仍为 pending。
|
||||
|
||||
- [x] **Step 3: 实现谱文脚本和页面**
|
||||
|
||||
```js
|
||||
function buildArticleBody(values) {
|
||||
return compact({
|
||||
articleTitle: requiredText(values.articleTitle, '请输入谱文标题'),
|
||||
articleSummary: optionalText(values.articleSummary),
|
||||
coverOssId: normalizeNullableId(values.coverOssId),
|
||||
articleContent: requiredText(values.articleContent, '请输入谱文正文'),
|
||||
authorName: optionalText(values.authorName),
|
||||
sortOrder: normalizeOptionalSafeInteger(values.sortOrder),
|
||||
status: '0'
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
列表从直接数组读取;详情按 `articleId` 匹配;列表没有独立分类选项源时不渲染 `categoryId` 输入。新增/修改成功后用返回 ID 或 URL ID 重读详情,删除后重读列表。所有可见文本转义,正文使用现有安全富文本展示规则。
|
||||
|
||||
- [x] **Step 4: 运行 GREEN**
|
||||
|
||||
Run: `node --test tests/article-pages.test.js tests/api-client-contract.test.js tests/pending-pages.test.js tests/pc-scope.test.js`
|
||||
|
||||
Run: `node --check public/js/article-pages.js`
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
- [x] **Step 5: 更新规划并报告谱文阶段**
|
||||
|
||||
在 `docs/PC接口对接规划.md` 记录 ArticleBody/ArticleVo 字段、C20 谱文状态、`categoryId` query 差异、封面 URL 和停用记录阻断。
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 相册列表、相册编辑和照片管理
|
||||
|
||||
**Files:**
|
||||
- Create: `profile-album-edit.html`
|
||||
- Create: `profile-album-detail.html`
|
||||
- Create: `public/js/album-pages.js`
|
||||
- Create: `tests/album-pages.test.js`
|
||||
- Modify: `profile-album.html`
|
||||
- Modify: `tests/pending-pages.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1 album ApiClient methods、ProfileUI、ProfileUpload
|
||||
- Produces: `buildAlbumBody`, `buildAlbumPhotoBody`, `normalizeAlbum`, `normalizeAlbumPhoto`, `findAlbumById`, `renderAlbumList`, `renderPhotoList`, three page initializers
|
||||
|
||||
- [x] **Step 1: 写失败的相册/照片测试**
|
||||
|
||||
```js
|
||||
assert.deepEqual(AlbumPages.buildAlbumPhotoBody({
|
||||
ossId: '9007199254740993010',
|
||||
photoTitle: '祠堂合影',
|
||||
shootTime: '2026-07-29T10:30',
|
||||
status: '1'
|
||||
}), {
|
||||
ossId: '9007199254740993010',
|
||||
photoTitle: '祠堂合影',
|
||||
shootTime: '2026-07-29 10:30:00',
|
||||
status: '0'
|
||||
});
|
||||
assert.equal(AlbumPages.findAlbumById(
|
||||
[{ albumId: '9007199254740993011', albumName: '旧影', status: '0' }],
|
||||
'9007199254740993011'
|
||||
).albumName, '旧影');
|
||||
```
|
||||
|
||||
页面测试断言 OSS ID 只能是 hidden/upload target,列表链接传播 `genealogyId + albumId`,详情页具有照片上传和真实删除入口。
|
||||
|
||||
- [x] **Step 2: 运行 RED**
|
||||
|
||||
Run: `node --test tests/album-pages.test.js tests/api-client-contract.test.js tests/pending-pages.test.js`
|
||||
|
||||
Expected: FAIL,原因是脚本和新增页面不存在。
|
||||
|
||||
- [x] **Step 3: 实现三个页面和脚本**
|
||||
|
||||
```js
|
||||
function buildAlbumBody(values) {
|
||||
return compact({
|
||||
albumName: requiredText(values.albumName, '请输入相册名称'),
|
||||
albumDesc: optionalText(values.albumDesc),
|
||||
coverOssId: normalizeNullableId(values.coverOssId),
|
||||
sortOrder: normalizeOptionalSafeInteger(values.sortOrder),
|
||||
status: '0'
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
相册编辑/详情通过重新读取相册列表并按稳定 `albumId` 匹配;照片列表调用 `/photos`。照片新增后重读照片列表;删除照片后重读并更新相册计数;删除相册后返回并重读相册列表。
|
||||
|
||||
- [x] **Step 4: 运行 GREEN**
|
||||
|
||||
Run: `node --test tests/album-pages.test.js tests/api-client-contract.test.js tests/pending-pages.test.js`
|
||||
|
||||
Run: `node --check public/js/album-pages.js`
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
- [x] **Step 5: 更新规划并报告相册阶段**
|
||||
|
||||
记录 AlbumBody、AlbumPhotoBody、AlbumVo、AlbumPhotoVo、文件 URL 缺口和停用记录阻断。
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 祭祀活动、祭品和管理员邀约
|
||||
|
||||
**Files:**
|
||||
- Create: `profile-ceremony.html`
|
||||
- Create: `profile-ceremony-detail.html`
|
||||
- Create: `public/js/ceremony-admin-pages.js`
|
||||
- Create: `tests/ceremony-admin-pages.test.js`
|
||||
- Modify: `profile-gift-edit.html`
|
||||
- Modify: `profile-gift.html`
|
||||
- Modify: `tests/pending-pages.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1 ceremony/member ApiClient methods、ProfileUI、ProfileUpload
|
||||
- Produces: `buildCeremonyBody`, `buildCeremonyGiftBody`, `normalizeCeremony`, `normalizeCeremonyGift`, `normalizeInviteeOption`, `buildInviteeBody`, list/detail/edit initializers
|
||||
|
||||
- [x] **Step 1: 写失败的祭祀测试**
|
||||
|
||||
```js
|
||||
assert.deepEqual(CeremonyAdminPages.buildCeremonyGiftBody({
|
||||
giverName: '宗亲',
|
||||
giftAmount: '88.50',
|
||||
giftMessage: '敬献'
|
||||
}), {
|
||||
giverName: '宗亲',
|
||||
giftAmount: 88.5,
|
||||
giftMessage: '敬献'
|
||||
});
|
||||
assert.throws(() => CeremonyAdminPages.buildCeremonyGiftBody({
|
||||
giftAmount: '-1'
|
||||
}));
|
||||
assert.deepEqual(CeremonyAdminPages.buildInviteeBody([
|
||||
'9007199254740993020',
|
||||
'9007199254740993020',
|
||||
'9007199254740993021'
|
||||
]), {
|
||||
inviteeUserIds: ['9007199254740993020', '9007199254740993021']
|
||||
});
|
||||
```
|
||||
|
||||
页面测试断言个人邀请脚本和管理员脚本职责分离;活动编辑没有手填封面 ID;详情页有祭品和受邀成员选择器。
|
||||
|
||||
- [x] **Step 2: 运行 RED**
|
||||
|
||||
Run: `node --test tests/ceremony-admin-pages.test.js tests/ceremony-pages.test.js tests/api-client-contract.test.js tests/pending-pages.test.js`
|
||||
|
||||
Expected: FAIL,原因是管理模块和页面不存在。
|
||||
|
||||
- [x] **Step 3: 实现活动、祭品和邀约管理**
|
||||
|
||||
```js
|
||||
function buildInviteeBody(ids) {
|
||||
var values = Array.from(new Set(ids.map(normalizeId).filter(Boolean)));
|
||||
return { inviteeUserIds: values };
|
||||
}
|
||||
```
|
||||
|
||||
活动固定 `status=0`;经纬度必须成对且范围合法;祭品金额非负并通过精确 JSON 数值检查。管理员候选过滤空 `appUserId`,提交空数组时显示取消全部待响应邀请确认。各写操作完成后重读活动详情、祭品或邀请名单。
|
||||
|
||||
- [x] **Step 4: 运行 GREEN**
|
||||
|
||||
Run: `node --test tests/ceremony-admin-pages.test.js tests/ceremony-pages.test.js tests/api-client-contract.test.js tests/pending-pages.test.js`
|
||||
|
||||
Run: `node --check public/js/ceremony-admin-pages.js`
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
- [x] **Step 5: 更新规划并报告祭祀阶段**
|
||||
|
||||
记录 CeremonyBody、CeremonyGiftBody、CeremonyInviteesBody、权限与金额/文件/停用阻断。
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 启用 SPECIFIED 世系账号绑定
|
||||
|
||||
**Files:**
|
||||
- Modify: `public/js/lineage-pages.js`
|
||||
- Modify: `profile-tree.html`
|
||||
- Modify: `tests/lineage-pages.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1 `genealogyMemberOptions(genealogyId)`
|
||||
- Produces: `normalizeBindingMemberOption`, `renderBindingMemberOptions`;扩展现有表单初始化和 `buildLineagePersonBody`
|
||||
|
||||
- [x] **Step 1: 写失败的绑定测试**
|
||||
|
||||
```js
|
||||
assert.deepEqual(LineagePages.normalizeBindingMemberOption({
|
||||
memberId: '9007199254740993030',
|
||||
appUserId: '9007199254740993031',
|
||||
memberName: '族员甲',
|
||||
appUserNickName: '账号甲',
|
||||
status: '0'
|
||||
}), {
|
||||
value: '9007199254740993031',
|
||||
label: '族员甲(账号甲)'
|
||||
});
|
||||
assert.equal(LineagePages.normalizeBindingMemberOption({
|
||||
memberId: '9007199254740993032',
|
||||
appUserId: null,
|
||||
status: '0'
|
||||
}), null);
|
||||
```
|
||||
|
||||
页面测试断言 `SPECIFIED` 不再 disabled、只显示成员选择器且不存在 appUserId 文本输入。
|
||||
|
||||
- [x] **Step 2: 运行 RED**
|
||||
|
||||
Run: `node --test tests/lineage-pages.test.js tests/api-client-contract.test.js`
|
||||
|
||||
Expected: FAIL,原因是选项仍禁用且规范化函数不存在。
|
||||
|
||||
- [x] **Step 3: 最小启用成员选项绑定**
|
||||
|
||||
只在 `canManage` 时显示 `SPECIFIED`;选择后内部保存字符串 `appUserId`。`NONE/SELF` 必须删除 body 中的 `appUserId`,`SPECIFIED` 必须选择有效 option;403 作为权限错误处理,不清登录态。
|
||||
|
||||
- [x] **Step 4: 运行 GREEN**
|
||||
|
||||
Run: `node --test tests/lineage-pages.test.js tests/api-client-contract.test.js`
|
||||
|
||||
Run: `node --check public/js/lineage-pages.js`
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
- [x] **Step 5: 更新规划并报告绑定阶段**
|
||||
|
||||
把 C15 更新为“由同家谱正常成员 options 解决;未绑定账号的成员不可选”,保留没有任意 AppUser 搜索能力的边界。
|
||||
|
||||
---
|
||||
|
||||
### Task 6: 家谱成员管理、退出和所有权转移
|
||||
|
||||
**Files:**
|
||||
- Create: `public/js/member-admin-pages.js`
|
||||
- Create: `tests/member-admin-pages.test.js`
|
||||
- Modify: `profile-family-admin.html`
|
||||
- Modify: `tests/pending-pages.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1 member ApiClient methods、`lineagePersonOptions`、ProfileUI
|
||||
- Produces: `normalizeMember`, `normalizeMemberOptions`, `buildMemberUpdateBody`, `canEditMember`, `canRemoveMember`, `canTransferOwner`, `renderMemberList`, `initMemberAdminPage`
|
||||
|
||||
- [x] **Step 1: 写失败的成员管理测试**
|
||||
|
||||
```js
|
||||
assert.deepEqual(MemberAdminPages.buildMemberUpdateBody({
|
||||
memberName: '族员甲',
|
||||
relationName: '侄',
|
||||
roleType: 'editor',
|
||||
lineagePersonId: '9007199254740993040',
|
||||
appUserId: 'must-drop'
|
||||
}), {
|
||||
memberName: '族员甲',
|
||||
relationName: '侄',
|
||||
roleType: 'editor',
|
||||
lineagePersonId: '9007199254740993040'
|
||||
});
|
||||
assert.throws(() => MemberAdminPages.buildMemberUpdateBody({
|
||||
roleType: 'owner'
|
||||
}));
|
||||
```
|
||||
|
||||
渲染测试断言手机号和内部 ID 不可见;owner 不显示移除按钮;当前 owner 不显示退出按钮而显示所有权转移。
|
||||
|
||||
- [x] **Step 2: 运行 RED**
|
||||
|
||||
Run: `node --test tests/member-admin-pages.test.js tests/api-client-contract.test.js tests/pending-pages.test.js`
|
||||
|
||||
Expected: FAIL,原因是脚本不存在且页面 pending。
|
||||
|
||||
- [x] **Step 3: 实现成员管理页面**
|
||||
|
||||
成员列表支持关键词刷新;编辑只提交 `memberName/relationName/roleType/lineagePersonId`。角色选择只有 `admin/editor/member`。移除、退出、转让都二次确认并使用响应中的 memberId;成功后重读成员列表,退出成功后清除当前家谱上下文并返回家谱选择页。
|
||||
|
||||
- [x] **Step 4: 运行 GREEN**
|
||||
|
||||
Run: `node --test tests/member-admin-pages.test.js tests/api-client-contract.test.js tests/pending-pages.test.js`
|
||||
|
||||
Run: `node --check public/js/member-admin-pages.js`
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
- [x] **Step 5: 更新规划并报告成员阶段**
|
||||
|
||||
记录成员更新字段、角色差异、退出/移除/转让权限与世系关系解除继续阻断。
|
||||
|
||||
---
|
||||
|
||||
### Task 7: 导航开放、真实验证和阶段收尾
|
||||
|
||||
**Files:**
|
||||
- Modify: `profile.html`
|
||||
- Modify: `profile-content.html`
|
||||
- Modify: `profile-families.html`
|
||||
- Modify: `docs/PC接口对接规划.md`
|
||||
- Modify: `tests/pending-pages.test.js`
|
||||
- Modify: `tests/pc-scope.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Tasks 1–6 completed pages
|
||||
- Produces: 所有功能的稳定导航入口和阶段 6 完成记录
|
||||
|
||||
- [x] **Step 1: 写失败的入口行为测试**
|
||||
|
||||
```js
|
||||
for (const page of [
|
||||
'profile-article.html',
|
||||
'profile-album.html',
|
||||
'profile-ceremony.html',
|
||||
'profile-family-admin.html'
|
||||
]) {
|
||||
assert.match(navigationHtml, new RegExp(page.replace('.', '\\.')));
|
||||
}
|
||||
```
|
||||
|
||||
测试同时断言新增/开放页面不加载 `pending-pages.js`,不存在 APP path、手填 ID 或原始 JSON。
|
||||
|
||||
- [x] **Step 2: 运行 RED**
|
||||
|
||||
Run: `node --test tests/pending-pages.test.js tests/pc-scope.test.js`
|
||||
|
||||
Expected: FAIL,原因是导航尚未开放。
|
||||
|
||||
- [x] **Step 3: 开放入口并更新规划**
|
||||
|
||||
个人中心和家谱内容入口链接到谱文、相册和祭祀;家谱管理入口链接到成员管理。规划逐模块记录字段表、验证结果、契约差异和唯一未实现的世系关系解除。
|
||||
|
||||
- [x] **Step 4: 聚焦和全量自动验证**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
node --test tests/article-pages.test.js tests/album-pages.test.js
|
||||
node --test tests/ceremony-admin-pages.test.js tests/ceremony-pages.test.js
|
||||
node --test tests/lineage-pages.test.js tests/member-admin-pages.test.js
|
||||
node --test tests/api-client-contract.test.js tests/pending-pages.test.js tests/pc-scope.test.js
|
||||
npm test
|
||||
node --check public/js/article-pages.js
|
||||
node --check public/js/album-pages.js
|
||||
node --check public/js/ceremony-admin-pages.js
|
||||
node --check public/js/member-admin-pages.js
|
||||
git -c safe.directory=D:/WorkSpace/Web/jiapu diff --check
|
||||
```
|
||||
|
||||
Expected: 全部 PASS。
|
||||
|
||||
- [x] **Step 5: 真实账号浏览器验证**
|
||||
|
||||
逐页验证登录态、家谱上下文、无数据空状态、筛选、隐私隐藏和控制台错误。真实账号没有家谱时,确认业务请求被阻止且所有入口回到 `profile-families.html?next=...`;不得制造家谱或业务记录。
|
||||
|
||||
- [x] **Step 6: 独立只读复核和最终报告**
|
||||
|
||||
安排默认子代理只读检查所有阶段 6 文件,主代理按 file:line 抽查。报告使用 `Changed / Verified / Blocked / Next`,明确世系关系解除、文件 URL、停用记录和缺真实数据写入验证的剩余风险。
|
||||
@@ -0,0 +1,119 @@
|
||||
# 阶段 6 PC 功能页面设计
|
||||
|
||||
## 目标
|
||||
|
||||
把当前 PC YAML 与只读后端已经具备完整业务闭环、但前端页面缺失或仍处于待开发状态的功能全部落到现有个人中心中。不得使用 APP 接口、静态业务 ID、手填用户 ID/OSS ID、猜测响应字段或不可重读的停用流程。
|
||||
|
||||
## 实施范围
|
||||
|
||||
### 1. 谱文
|
||||
|
||||
- 开放 `profile-article.html`,提供正常谱文列表、分类筛选、详情和权限化操作入口。
|
||||
- 开放 `profile-article-edit.html`,提供新增与修改。
|
||||
- 新增成功后使用响应中的稳定 `articleId` 重读详情;修改后重读同一详情;删除后重读列表。
|
||||
- `categoryId` 只能来自真实文章分类响应;如果 PC 没有分类列表/选项源,则分类保持省略,不能输入 ID。
|
||||
- 封面只能通过文件选择和上传产生 `coverOssId`。
|
||||
- 普通列表和详情只读取正常状态,因此创建、修改固定提交 `status=0`,不开放停用。
|
||||
|
||||
### 2. 相册与照片
|
||||
|
||||
- 开放 `profile-album.html` 作为相册列表入口。
|
||||
- 新增 `profile-album-edit.html` 负责相册新增、修改。
|
||||
- 新增 `profile-album-detail.html` 负责相册详情、照片列表、照片上传和照片删除。
|
||||
- 相册封面与照片文件都必须通过统一分片上传产生 OSS ID。
|
||||
- 相册或照片写入后重新读取相册/照片列表;删除后重新读取并校正数量。
|
||||
- 普通接口只返回正常记录,因此不开放相册或照片停用状态。
|
||||
- 在没有 PC 文件访问 URL 时,只显示上传状态、标题和业务元数据,不拼接 OSS 地址。
|
||||
|
||||
### 3. 祭祀活动与祭品
|
||||
|
||||
- 保留 `profile-gift.html` 的“我的邀请”能力,并增加进入祭祀活动管理的入口。
|
||||
- 开放 `profile-gift-edit.html`,负责祭祀活动新增、修改。
|
||||
- 新增 `profile-ceremony.html`,负责活动列表和详情。
|
||||
- 新增 `profile-ceremony-detail.html`,负责活动详情、祭品列表、新增祭品、删除祭品和管理员邀约名单。
|
||||
- 活动封面通过统一上传产生 `coverOssId`。
|
||||
- 祭品金额按 YAML `number` 和后端 `BigDecimal` 边界处理;拒绝负数以及 JSON 数值序列化会改变输入值的金额。
|
||||
- 活动与祭品写操作成功后重新读取对应详情/列表。
|
||||
- 普通活动列表只返回正常记录,因此不开放活动停用。
|
||||
|
||||
### 4. 管理员邀约名单
|
||||
|
||||
- 邀约管理放在 `profile-ceremony-detail.html`,不与当前用户“我的邀请”响应流程混合。
|
||||
- 候选人来自 `/genealogy/pc/genealogies/{genealogyId}/members/options`。
|
||||
- 只允许选择拥有稳定 `appUserId` 的同家谱正常成员;当前登录人由后端拒绝,前端不猜测替代账号。
|
||||
- 保存时一次提交完整 `inviteeUserIds`;空数组表示取消所有仍未响应邀请,提交前必须二次确认。
|
||||
- 保存成功后重新读取活动邀请名单。
|
||||
|
||||
### 5. 指定账号绑定世系人物
|
||||
|
||||
- 在 `profile-tree.html` 启用 `SPECIFIED`,仅对具有家谱管理权限的用户开放。
|
||||
- 账号候选来自家谱成员选项;过滤没有 `appUserId` 的成员。
|
||||
- 用户看到成员昵称/成员名称,不看到或手填 `appUserId`。
|
||||
- `NONE`、`SELF`、`SPECIFIED` 继续遵守互斥请求规则;后端负责最终租户、账号状态和重复绑定校验。
|
||||
|
||||
### 6. 家谱成员管理
|
||||
|
||||
- 开放 `profile-family-admin.html`。
|
||||
- 提供成员列表、关键词搜索、成员详情摘要、成员资料/角色修改、成员移除、当前用户退出家谱和所有权转移。
|
||||
- 角色编辑只发送后端实际接受的 `admin/editor/member`,不允许把 `owner/visitor` 作为更新值。
|
||||
- 世系人物关联只能来自真实世系人物选项。
|
||||
- 所有权转移只能从当前正常成员记录中选择稳定 `memberId`。
|
||||
- 移除、退出和所有权转移必须有明确影响说明与二次确认;成功后重新读取成员列表和家谱上下文。
|
||||
|
||||
## 明确保留的阻断
|
||||
|
||||
- PC 没有世系关系解除接口,不创建“解除父母/配偶/兄弟关系”按钮。
|
||||
- PC 没有统一 OSS ID 访问地址解析能力;谱文封面、相册照片、祭祀封面只显示后端已直接返回的 URL,否则不猜 URL。
|
||||
- 普通谱文、相册、照片和祭祀列表过滤停用记录,详情也不能稳定读取停用项;前端固定提交正常状态。
|
||||
- 谱文 YAML 遗漏后端可选 `categoryId` 列表 query,且文章分类选项来源需要单独核实;未确认前不发送该 query。
|
||||
- YAML 中部分 OSS ID 为 string、Java DTO 为 Long;浏览器始终保存十进制字符串,上传响应是唯一来源。
|
||||
- YAML 的文章、相册和祭祀响应仍为泛型包装;页面只消费对应 PC 控制器明确返回的 `ArticleVo`、`AlbumVo`、`AlbumPhotoVo`、`CeremonyVo`、`CeremonyGiftVo`,不读取 APP 路径。
|
||||
|
||||
## 前端结构
|
||||
|
||||
- `utils/ApiClient.js` 是所有 method/path/query/body 的唯一 owner,并为每个 Body 使用字段白名单。
|
||||
- 每个业务模块使用独立脚本:
|
||||
- `public/js/article-pages.js`
|
||||
- `public/js/album-pages.js`
|
||||
- `public/js/ceremony-admin-pages.js`
|
||||
- `public/js/member-admin-pages.js`
|
||||
- `public/js/ceremony-pages.js` 继续只负责当前账号的邀请读取与接受/拒绝,避免管理端与个人端状态混在一起。
|
||||
- `public/js/profile-common.js` 继续作为 `genealogyId` 的唯一上下文 owner。
|
||||
- `public/js/upload-pages.js` 继续作为 OSS ID 的唯一前端产生来源。
|
||||
|
||||
## 数据流与状态
|
||||
|
||||
1. 页面从 URL 和 `profile-common.js` 取得真实家谱上下文。
|
||||
2. 页面先读取家谱详情,按 `canView/canEditContent/canManage` 控制可见入口;后端继续执行最终权限校验。
|
||||
3. 列表记录返回稳定字符串 ID,用户点击后进入详情或编辑。
|
||||
4. 表单只提交 YAML/后端 PC DTO 声明的字段;可选空值默认省略。
|
||||
5. 写操作使用模块级提交锁,完成后重新读取服务端状态。
|
||||
6. 页面分别展示加载、空、校验失败、403、404、业务失败和网络失败状态。
|
||||
7. 所有响应文本经过转义;手机号、内部 ID、OSS ID 和原始 JSON不进入可见页面。
|
||||
|
||||
## 页面入口
|
||||
|
||||
- 个人中心和家谱管理导航开放谱文、相册、祭祀活动、成员管理入口。
|
||||
- 页面缺少 `genealogyId` 时返回家谱选择页,并通过 `next` 保留目标页面。
|
||||
- 已有待开发页面在功能完成并通过测试后移除 `data-feature-status="pending"` 和 `pending-pages.js`。
|
||||
- 新增页面沿用现有个人中心头部、侧边栏、按钮、卡片和移动端断点,不引入新的前端框架。
|
||||
|
||||
## 测试与验收
|
||||
|
||||
每个模块单独执行:
|
||||
|
||||
1. 先新增 `ApiClient` 路径、query、body 白名单和长 ID 失败测试并观察 RED。
|
||||
2. 新增规范化、权限、渲染转义、写后重读、防重复和页面字段测试并观察 RED。
|
||||
3. 最小实现后运行模块聚焦测试直到 GREEN。
|
||||
4. 使用真实登录账号验证读取、空状态和非破坏性筛选。
|
||||
5. 有真实业务数据时验证详情和写操作;没有数据时明确记录未覆盖项,不制造示例数据。
|
||||
6. 每个模块完成后运行相关测试并更新 `docs/PC接口对接规划.md`。
|
||||
7. 阶段完成后运行 `npm test`、JavaScript 语法检查、`git diff --check` 和凭据扫描。
|
||||
|
||||
## 完成标准
|
||||
|
||||
- 所有当前 PC 可闭环功能都有可访问页面和导航入口。
|
||||
- 所有请求字段有真实页面来源,所有 ID 均由响应、选择器或上传产生。
|
||||
- 不存在 APP 路径、旧路径 fallback、手填业务 ID、原始 JSON或静态业务数据。
|
||||
- 所有写操作防重复并在成功后重新读取。
|
||||
- 仍缺后端能力的功能保持明确禁用并写入规划阻断项。
|
||||
Reference in New Issue
Block a user