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,231 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
|
||||
const VipPages = require('../public/js/vip-pages.js');
|
||||
const root = path.resolve(__dirname, '..');
|
||||
|
||||
const packageId = '2062179707935264769';
|
||||
const genealogyId = '2062179707935264770';
|
||||
const orderId = '2062179707935264771';
|
||||
|
||||
function packageFixture(overrides) {
|
||||
return Object.assign({
|
||||
packageId,
|
||||
packageName: '家谱年度会员',
|
||||
packageType: 'vip',
|
||||
packageDesc: '适合持续维护家谱',
|
||||
price: '99.00',
|
||||
originalPrice: '129.00',
|
||||
durationValue: 1,
|
||||
durationUnit: 'year',
|
||||
genealogyLimit: 3,
|
||||
memberLimit: 500,
|
||||
storageLimitMb: 10240,
|
||||
featureJson: '{"export":true}',
|
||||
sortOrder: 1,
|
||||
status: '0',
|
||||
remark: '年度套餐'
|
||||
}, overrides);
|
||||
}
|
||||
|
||||
function orderFixture(overrides) {
|
||||
return Object.assign({
|
||||
orderId,
|
||||
orderNo: 'VIP202607291200001234',
|
||||
packageId,
|
||||
packageName: '家谱年度会员',
|
||||
appUserId: '2062179707935264701',
|
||||
appUserNickName: '叶子',
|
||||
appUserPhone: '19100000000',
|
||||
genealogyId,
|
||||
genealogyNo: 'G20260729001',
|
||||
genealogyName: '叶氏家谱',
|
||||
orderAmount: '99.00',
|
||||
payAmount: '99.00',
|
||||
payType: 'wechat',
|
||||
payStatus: '0',
|
||||
payTime: null,
|
||||
expireTime: null,
|
||||
status: '0',
|
||||
remark: ''
|
||||
}, overrides);
|
||||
}
|
||||
|
||||
test('VIP 套餐只保留可展示字段并保持金额和长 ID 字符串', () => {
|
||||
assert.deepEqual(VipPages.normalizeVipPackage(packageFixture()), {
|
||||
packageId,
|
||||
packageName: '家谱年度会员',
|
||||
packageType: 'vip',
|
||||
packageDesc: '适合持续维护家谱',
|
||||
price: '99.00',
|
||||
originalPrice: '129.00',
|
||||
durationValue: 1,
|
||||
durationUnit: 'year',
|
||||
genealogyLimit: 3,
|
||||
memberLimit: 500,
|
||||
storageLimitMb: 10240,
|
||||
remark: '年度套餐'
|
||||
});
|
||||
assert.equal(VipPages.normalizeVipPackage(packageFixture({
|
||||
packageId: Number.MAX_SAFE_INTEGER + 1
|
||||
})), null);
|
||||
assert.equal(VipPages.normalizeVipPackage(packageFixture({
|
||||
packageType: 'unknown'
|
||||
})), null);
|
||||
assert.equal(VipPages.normalizeVipPackage(packageFixture({
|
||||
durationUnit: 'week'
|
||||
})), null);
|
||||
assert.equal(VipPages.normalizeVipPackage(packageFixture({
|
||||
price: '-1.00'
|
||||
})), null);
|
||||
assert.equal(VipPages.normalizeVipPackage(packageFixture({
|
||||
status: '1'
|
||||
})), null);
|
||||
});
|
||||
|
||||
test('套餐列表过滤停用或非法记录', () => {
|
||||
assert.equal(VipPages.normalizeVipPackages([
|
||||
packageFixture(),
|
||||
packageFixture({ packageId: '2062179707935264772', status: '1' })
|
||||
]).length, 1);
|
||||
assert.deepEqual(VipPages.normalizeVipPackages({ rows: [packageFixture()] }), []);
|
||||
});
|
||||
|
||||
test('家谱选择项只接受响应中的稳定 ID 和名称', () => {
|
||||
assert.deepEqual(VipPages.normalizeGenealogyOption({
|
||||
genealogyId,
|
||||
genealogyName: '叶氏家谱',
|
||||
surname: '叶',
|
||||
ownerUserId: 'must-drop'
|
||||
}), {
|
||||
genealogyId,
|
||||
genealogyName: '叶氏家谱',
|
||||
surname: '叶'
|
||||
});
|
||||
assert.equal(VipPages.normalizeGenealogyOption({
|
||||
genealogyId: Number.MAX_SAFE_INTEGER + 1,
|
||||
genealogyName: '叶氏家谱'
|
||||
}), null);
|
||||
assert.equal(VipPages.normalizeGenealogyOption({
|
||||
genealogyId,
|
||||
genealogyName: ''
|
||||
}), null);
|
||||
});
|
||||
|
||||
test('订单请求只构造 AppVipOrderBody 且页面默认省略支付方式', () => {
|
||||
assert.deepEqual(VipPages.buildVipOrderBody({
|
||||
packageId: ' ' + packageId + ' ',
|
||||
genealogyId: ' ' + genealogyId + ' ',
|
||||
payType: '',
|
||||
orderId: 'must-drop',
|
||||
appUserId: 'must-drop',
|
||||
payStatus: 'must-drop'
|
||||
}), {
|
||||
packageId,
|
||||
genealogyId
|
||||
});
|
||||
assert.deepEqual(VipPages.buildVipOrderBody({
|
||||
packageId,
|
||||
payType: ' wechat '
|
||||
}), {
|
||||
packageId,
|
||||
payType: 'wechat'
|
||||
});
|
||||
});
|
||||
|
||||
test('订单请求要求安全套餐 ID,并只接受已确认的可选值', () => {
|
||||
assert.equal(VipPages.validateVipOrderBody({ packageId }), '');
|
||||
assert.equal(VipPages.validateVipOrderBody({ packageId, genealogyId }), '');
|
||||
assert.match(VipPages.validateVipOrderBody({ packageId: '' }), /套餐/);
|
||||
assert.match(VipPages.validateVipOrderBody({
|
||||
packageId,
|
||||
genealogyId: 'unsafe'
|
||||
}), /家谱/);
|
||||
assert.match(VipPages.validateVipOrderBody({
|
||||
packageId,
|
||||
payType: 'alipay'
|
||||
}), /支付方式/);
|
||||
});
|
||||
|
||||
test('VIP 订单隐藏账号字段并校验金额和状态', () => {
|
||||
assert.deepEqual(VipPages.normalizeVipOrder(orderFixture()), {
|
||||
orderId,
|
||||
orderNo: 'VIP202607291200001234',
|
||||
packageId,
|
||||
packageName: '家谱年度会员',
|
||||
genealogyId,
|
||||
genealogyNo: 'G20260729001',
|
||||
genealogyName: '叶氏家谱',
|
||||
orderAmount: '99.00',
|
||||
payAmount: '99.00',
|
||||
payType: 'wechat',
|
||||
payStatus: '0',
|
||||
payTime: '',
|
||||
expireTime: '',
|
||||
status: '0',
|
||||
remark: ''
|
||||
});
|
||||
assert.equal(VipPages.normalizeVipOrder(orderFixture({
|
||||
orderId: Number.MAX_SAFE_INTEGER + 1
|
||||
})), null);
|
||||
assert.equal(VipPages.normalizeVipOrder(orderFixture({
|
||||
payAmount: '-1'
|
||||
})), null);
|
||||
assert.equal(VipPages.normalizeVipOrder(orderFixture({
|
||||
payStatus: '9'
|
||||
})), null);
|
||||
assert.equal(VipPages.normalizeVipOrder(orderFixture({
|
||||
status: '9'
|
||||
})), null);
|
||||
});
|
||||
|
||||
test('套餐和订单渲染不出现内部账号信息或伪支付操作', () => {
|
||||
const packagesHtml = VipPages.renderVipPackages([packageFixture({
|
||||
packageName: '<年度会员>'
|
||||
})], packageId);
|
||||
const ordersHtml = VipPages.renderVipOrders([orderFixture({
|
||||
appUserId: 'secret-user',
|
||||
appUserPhone: 'secret-phone'
|
||||
})]);
|
||||
const combined = packagesHtml + ordersHtml;
|
||||
|
||||
assert.match(packagesHtml, /<年度会员>/);
|
||||
assert.match(packagesHtml, new RegExp('data-vip-package-id="' + packageId + '"'));
|
||||
assert.match(ordersHtml, /待支付/);
|
||||
assert.doesNotMatch(combined, /secret-user|secret-phone|appUserId|appUserPhone|立即支付|模拟|取消订单|退款/);
|
||||
});
|
||||
|
||||
test('套餐渲染接受初始化流程已经规范化的数据', () => {
|
||||
const normalized = VipPages.normalizeVipPackages([packageFixture()]);
|
||||
const html = VipPages.renderVipPackages(normalized, packageId);
|
||||
|
||||
assert.match(html, /家谱年度会员/);
|
||||
assert.match(html, /data-vip-package-id=/);
|
||||
assert.doesNotMatch(html, /当前没有可用会员套餐/);
|
||||
});
|
||||
|
||||
test('会员服务页只从接口结果选择套餐和家谱,不暴露业务 ID 或未开放支付操作', () => {
|
||||
const source = fs.readFileSync(path.join(root, 'profile-services.html'), 'utf8');
|
||||
|
||||
assert.doesNotMatch(source, /data-feature-status="pending"/);
|
||||
assert.match(source, /src="public\/js\/vip-pages\.js"/);
|
||||
assert.match(source, /data-vip-package-list/);
|
||||
assert.match(source, /data-vip-selected-package/);
|
||||
assert.match(source, /<select[^>]+data-vip-genealogy-options/);
|
||||
assert.match(source, /data-vip-order-refresh/);
|
||||
assert.match(source, /data-vip-order-list/);
|
||||
assert.doesNotMatch(source, /<input[^>]+name="(?:packageId|genealogyId|orderId)"/);
|
||||
assert.doesNotMatch(source, /name="payType"/);
|
||||
assert.doesNotMatch(source, /立即支付|模拟支付|取消订单|退款/);
|
||||
});
|
||||
|
||||
test('会员服务运行时并行读取套餐、我的家谱和订单,并在创建后重新读取核验', () => {
|
||||
const source = fs.readFileSync(path.join(root, 'public/js/vip-pages.js'), 'utf8');
|
||||
|
||||
assert.match(source, /Promise\.all\(\[api\.vipPackages\(\), api\.genealogiesMine\(\), api\.vipOrders\(\)\]\)/);
|
||||
assert.match(source, /created = normalizeVipOrder\(await api\.createVipOrder\(body\)\)/);
|
||||
assert.match(source, /refreshed = normalizeVipOrders\(await api\.vipOrders\(\)\)/);
|
||||
assert.match(source, /item\.orderId === created\.orderId/);
|
||||
});
|
||||
Reference in New Issue
Block a user