feat(api): 添加帮助中心反馈系统和VIP服务功能

- 在ApiClient中新增submitFeedback、myFeedback、helpArticles、helpArticleDetail、
  siteArticles、promotions、vipPackages、createVipOrder、vipOrders等方法
- 添加帮助文章和站点资讯的参数验证逻辑
- 更新测试文件添加新的API方法测试用例
- 在HTML页面中添加反馈、帮助和VIP服务相关页面的脚本引用
- 更新加入家谱页面为完整的申请流程界面
- 修改资讯详情页面为站点资讯展示页面
- 更新AxiosRequestUtil中认证处理逻辑
- 添加世系树渲染的HTML生成函数用于页面复用
- 更新文档中的API契约说明和页面规划
This commit is contained in:
fizzleaf
2026-07-30 16:04:48 +08:00
parent fb1743aa2a
commit 309598bfa6
53 changed files with 6740 additions and 282 deletions
+189
View File
@@ -0,0 +1,189 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const JoinPages = require('../public/js/join-pages.js');
const genealogyId = '2062179707935264769';
const applyId = '2062179707935264770';
function read(relativePath) {
return fs.readFileSync(path.resolve(__dirname, '..', relativePath), 'utf8');
}
test('加入申请只构造 AppGenealogyJoinApplyBody 并裁剪可选文本', () => {
assert.deepEqual(JoinPages.buildJoinApplyBody({
applicantName: ' 叶子 ',
phone: ' 19181970173 ',
relationDesc: ' 族亲 ',
applyReason: ' 申请加入 ',
inviterUserId: 'must-drop',
genealogyId: 'must-drop'
}), {
applicantName: '叶子',
phone: '19181970173',
relationDesc: '族亲',
applyReason: '申请加入'
});
assert.deepEqual(JoinPages.buildJoinApplyBody({
applicantName: ' ',
phone: '',
relationDesc: null
}), {});
});
test('加入申请按后端长度约束校验四个可选字段', () => {
assert.equal(JoinPages.validateJoinApplyBody({ applicantName: '叶'.repeat(50) }), '');
assert.match(JoinPages.validateJoinApplyBody({ applicantName: '叶'.repeat(51) }), /50/);
assert.equal(JoinPages.validateJoinApplyBody({ phone: '1'.repeat(30) }), '');
assert.match(JoinPages.validateJoinApplyBody({ phone: '1'.repeat(31) }), /30/);
assert.equal(JoinPages.validateJoinApplyBody({ relationDesc: '亲'.repeat(100) }), '');
assert.match(JoinPages.validateJoinApplyBody({ relationDesc: '亲'.repeat(101) }), /100/);
assert.equal(JoinPages.validateJoinApplyBody({ applyReason: '因'.repeat(500) }), '');
assert.match(JoinPages.validateJoinApplyBody({ applyReason: '因'.repeat(501) }), /500/);
});
test('可申请家谱必须提供稳定字符串 ID、谱名和姓氏,并拒绝停用家谱', () => {
assert.deepEqual(JoinPages.normalizeJoinGenealogy({
genealogyId,
genealogyName: '叶氏家谱',
surname: '叶',
regionFullName: '四川省',
memberCount: 12,
joinMode: '0',
status: '0',
ownerUserId: 'must-drop'
}), {
genealogyId,
genealogyName: '叶氏家谱',
surname: '叶',
regionFullName: '四川省',
memberCount: 12,
joinMode: '0'
});
assert.equal(JoinPages.normalizeJoinGenealogy({
genealogyId: Number.MAX_SAFE_INTEGER + 1,
genealogyName: '叶氏家谱',
surname: '叶',
status: '0'
}), null);
assert.equal(JoinPages.normalizeJoinGenealogy({
genealogyId,
genealogyName: '叶氏家谱',
surname: '叶',
status: '1'
}), null);
});
test('我的申请只接收稳定 ID 和 0/1/2/3 状态', () => {
assert.deepEqual(JoinPages.normalizeJoinApply({
applyId,
genealogyId,
genealogyName: '叶氏家谱',
surname: '叶',
relationDesc: '族亲',
applyReason: '申请加入',
auditRemark: '已核实',
auditTime: '2026-07-29 12:00:00',
status: '1',
appUserId: 'must-drop',
inviterUserId: 'must-drop',
auditUserId: 'must-drop',
appUserPhone: 'must-drop'
}), {
applyId,
genealogyId,
genealogyName: '叶氏家谱',
surname: '叶',
relationDesc: '族亲',
applyReason: '申请加入',
auditRemark: '已核实',
auditTime: '2026-07-29 12:00:00',
status: '1'
});
assert.equal(JoinPages.normalizeJoinApply({
applyId: Number.MAX_SAFE_INTEGER + 1,
genealogyId,
genealogyName: '叶氏家谱',
status: '0'
}), null);
assert.equal(JoinPages.normalizeJoinApply({
applyId,
genealogyId,
genealogyName: '叶氏家谱',
status: '9'
}), null);
});
test('申请数组只要包含非法元素就整批拒绝', () => {
const valid = { applyId, genealogyId, genealogyName: '叶氏家谱', status: '0' };
assert.equal(JoinPages.normalizeJoinApplies([valid]).length, 1);
assert.deepEqual(JoinPages.normalizeJoinApplies([valid, { status: '0' }]), []);
assert.deepEqual(JoinPages.normalizeJoinApplies({ rows: [valid] }), []);
});
test('普通用户列表隐藏内部字段且只有待审核申请可以撤销', () => {
const html = JoinPages.renderMyJoinApplies([{
applyId,
genealogyId,
genealogyName: '<叶氏家谱>',
surname: '叶',
relationDesc: '族亲',
applyReason: '申请加入',
status: '0',
appUserId: 'secret-user',
inviterUserId: 'secret-inviter',
auditUserId: 'secret-auditor',
appUserPhone: 'secret-phone'
}]);
const approvedHtml = JoinPages.renderMyJoinApplies([{
applyId,
genealogyId,
genealogyName: '叶氏家谱',
status: '1'
}]);
assert.match(html, /&lt;叶氏家谱&gt;/);
assert.match(html, new RegExp('data-join-cancel-id="' + applyId + '"'));
assert.doesNotMatch(approvedHtml, /data-join-cancel-id/);
assert.doesNotMatch(html, /secret-user|secret-inviter|secret-auditor|secret-phone|must-drop/);
});
test('申请加入页由后端家谱选项驱动且不允许手填业务 ID 或邀请码', () => {
const source = read('join-genealogy.html');
assert.match(source, /data-join-apply-page/);
assert.match(source, /data-join-genealogy-search/);
assert.match(source, /data-join-genealogy-options/);
assert.match(source, /data-join-apply-form/);
assert.match(source, /name="applicantName"/);
assert.match(source, /name="phone"/);
assert.match(source, /name="relationDesc"/);
assert.match(source, /name="applyReason"/);
assert.match(source, /public\/js\/join-pages\.js/);
assert.doesNotMatch(source, /name="(?:genealogyId|applyId|inviterUserId|inviteCode)"/);
assert.doesNotMatch(source, /邀请码|分享码|手工.*ID|输入.*ID/);
});
test('我的加入申请页加载真实列表和刷新入口', () => {
const source = read('profile-join-family.html');
assert.doesNotMatch(source, /data-feature-status="pending"|pending-pages\.js/);
assert.match(source, /data-my-join-applies-page/);
assert.match(source, /data-join-apply-list="mine"/);
assert.match(source, /data-join-apply-refresh/);
assert.match(source, /public\/js\/join-pages\.js/);
assert.doesNotMatch(source, /邀请码|分享码|输入.*ID/);
});
test('加入申请运行时提交与撤销后都重新读取我的申请', () => {
const source = read('public/js/join-pages.js');
assert.match(source, /await api\.applyToGenealogy\(/);
assert.match(source, /await api\.myGenealogyJoinApplies\(\)/);
assert.match(source, /await api\.cancelGenealogyJoinApply\(/);
assert.match(source, /提交后无法读取同一申请/);
assert.match(source, /撤销后申请仍处于待审核状态/);
});