feat(api): 完善家谱系统API客户端契约
- 实现家谱管理相关方法,包括创建、详情、概览、我的家谱和选项查询 - 添加家谱加入申请功能,支持申请、审核、取消和待审核列表操作 - 集成通知详情获取方法和通知ID安全验证机制 - 完善功德记录、谱文、相册、视频、祭祀活动的完整CRUD操作契约 - 实现家谱成员管理功能,包含成员列表、更新、移除和转让所有者操作 - 优化路径ID验证逻辑,拒绝不安全的数值ID并提供明确错误提示 - 更新测试用例以验证所有新增API方法的路径和请求体白名单机制
This commit is contained in:
+243
-6
@@ -13,33 +13,67 @@ test('世系页只从 URL 读取真实家谱编号', () => {
|
||||
test('世系人物表单只构造 Apifox LineagePersonBody 字段', () => {
|
||||
assert.deepEqual(
|
||||
LineagePages.buildLineagePersonBody({
|
||||
bindingMode: 'SPECIFIED',
|
||||
appUserId: '2061000000000000001',
|
||||
avatarOssId: '2061000000000000002',
|
||||
name: '李明',
|
||||
sex: '0',
|
||||
generation: '3',
|
||||
generationName: '德',
|
||||
fatherId: '2061000000000000003',
|
||||
motherId: '2061000000000000004',
|
||||
personNo: 'P202607090001',
|
||||
aliasName: '明远',
|
||||
birthDate: '1990-01-01',
|
||||
birthDate: '1990-01-01T08:30',
|
||||
birthLunar: '0',
|
||||
birthPlace: '成都',
|
||||
deathDate: '2050-02-03T10:45',
|
||||
deathLunar: '1',
|
||||
deathPlace: '重庆',
|
||||
burialPlace: '青城山',
|
||||
personStatus: '1',
|
||||
sortOrder: '1',
|
||||
relationName: '妻',
|
||||
biography: '人物简介',
|
||||
remark: '家族备注',
|
||||
personName: '旧字段',
|
||||
generationNo: 4,
|
||||
introduction: '旧字段'
|
||||
}),
|
||||
{
|
||||
bindingMode: 'SPECIFIED',
|
||||
appUserId: '2061000000000000001',
|
||||
avatarOssId: '2061000000000000002',
|
||||
name: '李明',
|
||||
sex: '0',
|
||||
generation: 3,
|
||||
generationName: '德',
|
||||
fatherId: '2061000000000000003',
|
||||
motherId: '2061000000000000004',
|
||||
personNo: 'P202607090001',
|
||||
aliasName: '明远',
|
||||
birthDate: '1990-01-01',
|
||||
birthDate: '1990-01-01 08:30:00',
|
||||
birthLunar: '0',
|
||||
birthPlace: '成都',
|
||||
deathDate: '2050-02-03 10:45:00',
|
||||
deathLunar: '1',
|
||||
deathPlace: '重庆',
|
||||
burialPlace: '青城山',
|
||||
personStatus: '1',
|
||||
sortOrder: 1,
|
||||
relationName: '妻',
|
||||
biography: '人物简介'
|
||||
biography: '人物简介',
|
||||
remark: '家族备注'
|
||||
}
|
||||
);
|
||||
assert.deepEqual(LineagePages.buildLineagePersonBody({
|
||||
bindingMode: 'SELF',
|
||||
appUserId: '2061000000000000001',
|
||||
name: '李明'
|
||||
}), {
|
||||
bindingMode: 'SELF',
|
||||
name: '李明'
|
||||
});
|
||||
});
|
||||
|
||||
test('世系人物要求安全的 ID、姓名和有效世代值', () => {
|
||||
@@ -60,9 +94,168 @@ test('世系人物要求安全的 ID、姓名和有效世代值', () => {
|
||||
}
|
||||
);
|
||||
assert.equal(LineagePages.normalizeLineagePerson({ personId: Number('2060000000000000000'), name: '李明' }), null);
|
||||
assert.equal(LineagePages.validateLineagePersonBody({ name: '' }), '请填写成员姓名');
|
||||
assert.equal(LineagePages.validateLineagePersonBody(LineagePages.buildLineagePersonBody({ name: '李明', generation: '1.5' })), '世代序号必须是整数');
|
||||
assert.equal(LineagePages.validateLineagePersonBody(LineagePages.buildLineagePersonBody({ name: '李明', sortOrder: '1.5' })), '排序值必须是整数');
|
||||
assert.equal(LineagePages.normalizeLineagePerson({
|
||||
personId: '2060000000000000000',
|
||||
fatherId: Number('2061000000000000000'),
|
||||
name: '李明'
|
||||
}), null);
|
||||
assert.equal(LineagePages.validateLineagePersonBody({ bindingMode: 'NONE', name: '' }), '请填写成员姓名');
|
||||
assert.equal(LineagePages.validateLineagePersonBody({ name: '李明' }), '请选择账号绑定方式');
|
||||
assert.equal(LineagePages.validateLineagePersonBody({ bindingMode: 'OTHER', name: '李明' }), '账号绑定方式无效');
|
||||
assert.equal(LineagePages.validateLineagePersonBody({ bindingMode: 'SPECIFIED', name: '李明' }), '指定账号绑定缺少用户选项');
|
||||
assert.equal(LineagePages.validateLineagePersonBody(LineagePages.buildLineagePersonBody({ bindingMode: 'NONE', name: '李明', generation: '1.5' })), '世代序号必须是整数');
|
||||
assert.equal(LineagePages.validateLineagePersonBody(LineagePages.buildLineagePersonBody({ bindingMode: 'NONE', name: '李明', sortOrder: '1.5' })), '排序值必须是整数');
|
||||
assert.equal(LineagePages.validateLineagePersonBody({ bindingMode: 'NONE', name: '李明', sex: '3' }), '性别选项无效');
|
||||
assert.equal(LineagePages.validateLineagePersonBody({ bindingMode: 'NONE', name: '李明', personStatus: '3' }), '人物状态选项无效');
|
||||
assert.equal(LineagePages.validateLineagePersonBody({ bindingMode: 'NONE', name: '李明', birthLunar: '2' }), '出生历法选项无效');
|
||||
});
|
||||
|
||||
test('世系人物响应保留完整详情字段并拒绝不安全的嵌套 ID', () => {
|
||||
assert.deepEqual(LineagePages.normalizeLineagePerson({
|
||||
personId: '2060000000000000000',
|
||||
genealogyId: '2060000000000000001',
|
||||
appUserId: '2060000000000000002',
|
||||
avatarOssId: '2060000000000000003',
|
||||
fatherId: '2060000000000000004',
|
||||
motherId: '2060000000000000005',
|
||||
genealogyName: '李氏家谱',
|
||||
genealogyNo: 'G20260728001',
|
||||
appUserNickName: '明远',
|
||||
personNo: 'P202607090001',
|
||||
name: '李明',
|
||||
aliasName: '明远',
|
||||
sex: '0',
|
||||
generation: 3,
|
||||
generationName: '德',
|
||||
fatherName: '李父',
|
||||
motherName: '王母',
|
||||
spouseNames: '张氏',
|
||||
birthDate: '1990-01-01 08:30:00',
|
||||
birthLunar: '0',
|
||||
birthPlace: '成都',
|
||||
deathDate: '2050-02-03 10:45:00',
|
||||
deathLunar: '1',
|
||||
deathPlace: '重庆',
|
||||
burialPlace: '青城山',
|
||||
personStatus: '1',
|
||||
biography: '人物简介',
|
||||
sortOrder: 1,
|
||||
status: '0',
|
||||
remark: '家族备注'
|
||||
}), {
|
||||
personId: '2060000000000000000',
|
||||
name: '李明',
|
||||
genealogyName: '李氏家谱',
|
||||
genealogyNo: 'G20260728001',
|
||||
appUserNickName: '明远',
|
||||
personNo: 'P202607090001',
|
||||
aliasName: '明远',
|
||||
sex: '0',
|
||||
generationName: '德',
|
||||
fatherName: '李父',
|
||||
motherName: '王母',
|
||||
spouseNames: '张氏',
|
||||
birthDate: '1990-01-01 08:30:00',
|
||||
birthLunar: '0',
|
||||
birthPlace: '成都',
|
||||
deathDate: '2050-02-03 10:45:00',
|
||||
deathLunar: '1',
|
||||
deathPlace: '重庆',
|
||||
burialPlace: '青城山',
|
||||
personStatus: '1',
|
||||
biography: '人物简介',
|
||||
status: '0',
|
||||
remark: '家族备注',
|
||||
appUserId: '2060000000000000002',
|
||||
genealogyId: '2060000000000000001',
|
||||
fatherId: '2060000000000000004',
|
||||
motherId: '2060000000000000005',
|
||||
avatarOssId: '2060000000000000003',
|
||||
generation: 3,
|
||||
sortOrder: 1
|
||||
});
|
||||
});
|
||||
|
||||
test('家谱内容权限只控制世系写操作,读取和分页始终可用', () => {
|
||||
assert.deepEqual(LineagePages.normalizeLineageAccess({ canEditContent: false }), {
|
||||
canEditContent: false
|
||||
});
|
||||
assert.deepEqual(LineagePages.normalizeLineageAccess({ canEditContent: true }), {
|
||||
canEditContent: true
|
||||
});
|
||||
assert.equal(LineagePages.canUseLineagePagination(false), true);
|
||||
assert.equal(LineagePages.canUseLineagePagination(true), false);
|
||||
});
|
||||
|
||||
test('世代选项来自正常字辈列表并自动映射序号和字辈', () => {
|
||||
assert.deepEqual(LineagePages.normalizeGenerationOption({
|
||||
poemId: '2060000000000000000',
|
||||
generationNo: 3,
|
||||
generationText: '德',
|
||||
status: '0'
|
||||
}), {
|
||||
generation: 3,
|
||||
generationName: '德'
|
||||
});
|
||||
assert.equal(LineagePages.normalizeGenerationOption({ generationNo: 0, generationText: '德' }), null);
|
||||
assert.equal(LineagePages.normalizeGenerationOption({ generationNo: 3, generationText: '' }), null);
|
||||
});
|
||||
|
||||
test('已有账号绑定按当前登录用户区分 SELF 与只读保留的 SPECIFIED', () => {
|
||||
assert.deepEqual(LineagePages.normalizeLineageBinding({}, '2060000000000000001'), {
|
||||
bindingMode: 'NONE',
|
||||
appUserId: ''
|
||||
});
|
||||
assert.deepEqual(LineagePages.normalizeLineageBinding({
|
||||
appUserId: '2060000000000000001'
|
||||
}, '2060000000000000001'), {
|
||||
bindingMode: 'SELF',
|
||||
appUserId: ''
|
||||
});
|
||||
assert.deepEqual(LineagePages.normalizeLineageBinding({
|
||||
appUserId: '2060000000000000002'
|
||||
}, '2060000000000000001'), {
|
||||
bindingMode: 'SPECIFIED',
|
||||
appUserId: '2060000000000000002'
|
||||
});
|
||||
});
|
||||
|
||||
test('指定账号绑定只使用当前家谱正常且已绑定账号的成员选项', () => {
|
||||
assert.deepEqual(LineagePages.normalizeGenealogyMemberOption({
|
||||
memberId: '2060000000000000001',
|
||||
genealogyId: '2060000000000000002',
|
||||
appUserId: '2060000000000000003',
|
||||
appUserNickName: '明远',
|
||||
memberName: '李明',
|
||||
roleType: 'member',
|
||||
status: '0'
|
||||
}), {
|
||||
memberId: '2060000000000000001',
|
||||
genealogyId: '2060000000000000002',
|
||||
appUserId: '2060000000000000003',
|
||||
appUserNickName: '明远',
|
||||
memberName: '李明',
|
||||
roleType: 'member',
|
||||
status: '0'
|
||||
});
|
||||
assert.equal(LineagePages.normalizeGenealogyMemberOption({
|
||||
memberId: '1',
|
||||
genealogyId: '2',
|
||||
appUserId: null,
|
||||
memberName: '未绑定成员',
|
||||
status: '0'
|
||||
}), null);
|
||||
assert.equal(LineagePages.normalizeGenealogyMemberOption({
|
||||
memberId: '1',
|
||||
genealogyId: '2',
|
||||
appUserId: '3',
|
||||
memberName: '停用成员',
|
||||
status: '1'
|
||||
}), null);
|
||||
assert.deepEqual(LineagePages.normalizeGenealogyMemberOptions([
|
||||
{ memberId: '1', genealogyId: '2', appUserId: '3', memberName: '正常成员', status: '0' },
|
||||
{ memberId: '4', genealogyId: '2', appUserId: null, memberName: '未绑定成员', status: '0' }
|
||||
]).map((option) => option.appUserId), ['3']);
|
||||
});
|
||||
|
||||
test('世系关系模式只使用 PC 已定义的四种新增关系', () => {
|
||||
@@ -76,12 +269,56 @@ test('世系关系模式只使用 PC 已定义的四种新增关系', () => {
|
||||
test('世系页按 Apifox 关键词范围搜索并提供分页入口', () => {
|
||||
const page = fs.readFileSync(path.join(__dirname, '..', 'profile-tree.html'), 'utf8');
|
||||
|
||||
assert.deepEqual(LineagePages.buildLineagePageQuery({
|
||||
keyword: ' 李明 ',
|
||||
generation: '3',
|
||||
personStatus: '1'
|
||||
}, 2, 20), {
|
||||
pageNum: 2,
|
||||
pageSize: 20,
|
||||
keyword: '李明',
|
||||
generation: 3,
|
||||
personStatus: '1'
|
||||
});
|
||||
assert.deepEqual(LineagePages.buildLineagePageQuery({}, 1, 20), {
|
||||
pageNum: 1,
|
||||
pageSize: 20
|
||||
});
|
||||
assert.match(page, /placeholder="输入姓名、别名或人物编号搜索"/);
|
||||
assert.match(page, /data-lineage-generation-filter/);
|
||||
assert.match(page, /data-lineage-status-filter/);
|
||||
assert.match(page, /data-lineage-pagination/);
|
||||
assert.match(page, /data-lineage-page-action="previous"/);
|
||||
assert.match(page, /data-lineage-page-action="next"/);
|
||||
});
|
||||
|
||||
test('世系表单为全部 LineagePersonBody 字段提供真实来源且不允许手填业务 ID', () => {
|
||||
const page = fs.readFileSync(path.join(__dirname, '..', 'profile-tree.html'), 'utf8');
|
||||
const styles = fs.readFileSync(path.join(__dirname, '..', 'public', 'css', 'profile-module.css'), 'utf8');
|
||||
|
||||
[
|
||||
'bindingMode', 'appUserId', 'avatarOssId', 'personNo', 'name', 'aliasName', 'sex',
|
||||
'generation', 'generationName', 'fatherId', 'motherId', 'birthDate', 'birthLunar',
|
||||
'birthPlace', 'deathDate', 'deathLunar', 'deathPlace', 'burialPlace', 'personStatus',
|
||||
'biography', 'sortOrder', 'remark', 'relationName'
|
||||
].forEach((name) => assert.match(page, new RegExp('name="' + name + '"')));
|
||||
assert.match(page, /value="SPECIFIED"(?![^>]+disabled)/);
|
||||
assert.match(page, /select[^>]+name="appUserId"[^>]+data-lineage-member-options|select[^>]+data-lineage-member-options[^>]+name="appUserId"/);
|
||||
assert.doesNotMatch(page, /name="appUserId"[^>]+type="(?:text|number)"|type="(?:text|number)"[^>]+name="appUserId"/);
|
||||
assert.doesNotMatch(page, /name="appUserId"[^>]+type="hidden"|type="hidden"[^>]+name="appUserId"/);
|
||||
assert.match(page, /name="avatarOssId"[^>]+type="hidden"|type="hidden"[^>]+name="avatarOssId"/);
|
||||
assert.match(page, /data-upload-target="#lineage-avatar-oss-id"/);
|
||||
assert.match(page, /name="generation"[^>]*data-lineage-generation/);
|
||||
assert.match(page, /name="generationName"[^>]+readonly|readonly[^>]+name="generationName"/);
|
||||
assert.match(page, /name="fatherId"[^>]*data-lineage-parent-option/);
|
||||
assert.match(page, /name="motherId"[^>]*data-lineage-parent-option/);
|
||||
assert.match(page, /data-lineage-management[^>]+hidden/);
|
||||
assert.match(page, /public\/js\/upload-pages\.js/);
|
||||
assert.match(styles, /\[data-lineage-management\]\[hidden\]\s*\{[^}]*display:\s*none\s*!important/s);
|
||||
assert.match(styles, /\[data-lineage-spouse-field\]\[hidden\][^{]*\{[^}]*display:\s*none\s*!important/s);
|
||||
assert.match(styles, /\[data-lineage-pagination\]\[hidden\][^{]*\{[^}]*display:\s*none\s*!important/s);
|
||||
});
|
||||
|
||||
test('世系管理的 403 不会被当作登录失效', () => {
|
||||
assert.equal(LineagePages.isForbidden({ status: 403 }), true);
|
||||
assert.equal(LineagePages.shouldRedirectToLogin({ getToken() { return 'token'; } }, { status: 403 }), false);
|
||||
|
||||
Reference in New Issue
Block a user