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
+174
View File
@@ -32,6 +32,10 @@ test('API client exposes latest document-defined PC operations', () => {
'genealogiesMine', 'genealogyOptions', 'genealogyDetail', 'genealogyOverview', 'genealogyQuota',
'createGenealogy', 'applyToGenealogy', 'myGenealogyJoinApplies',
'pendingGenealogyJoinApplies', 'auditGenealogyJoinApply', 'cancelGenealogyJoinApply',
'submitFeedback', 'myFeedback',
'helpArticles', 'helpArticleDetail', 'siteArticles',
'promotions',
'vipPackages', 'createVipOrder', 'vipOrders',
'notifications', 'notificationDetail', 'unreadNotificationCount', 'markNotificationRead', 'markAllNotificationsRead',
'feeds', 'feedsPage', 'feedDetail', 'createFeed', 'updateFeed', 'deleteFeed',
'likeFeed', 'unlikeFeed', 'feedComments', 'feedCommentsPage', 'createFeedComment',
@@ -506,6 +510,110 @@ test('genealogy quota uses the current PC path without manufacturing a genealogy
await client.genealogyQuota();
});
test('help article methods use authenticated PC paths and whitelist the optional category', async () => {
const calls = [];
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: { getItem() { return 'access-token'; }, setItem() {}, removeItem() {} },
axiosInstance: {
request(config) {
calls.push(config);
return Promise.resolve({ data: { code: 200, data: [] } });
}
}
});
await client.helpArticles({ helpCategory: ' account ', keyword: 'must-drop' });
await client.helpArticleDetail('2062179707935264769');
assert.deepEqual(calls.map((config) => [
config.method,
config.url,
config.params,
config.headers.Authorization
]), [
['get', '/genealogy/pc/help-articles', { helpCategory: ' account ' }, 'Bearer access-token'],
['get', '/genealogy/pc/help-articles/2062179707935264769', undefined, 'Bearer access-token']
]);
});
test('promotion method uses the authenticated PC path and whitelists platform query', async () => {
const calls = [];
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: { getItem() { return 'access-token'; }, setItem() {}, removeItem() {} },
axiosInstance: {
request(config) {
calls.push(config);
return Promise.resolve({ data: { code: 200, data: [] } });
}
}
});
await client.promotions({
platform: 'pc',
keyword: 'must-drop'
});
assert.deepEqual(calls.map((config) => [
config.method,
config.url,
config.params,
config.headers.Authorization
]), [
['get', '/genealogy/pc/promotions', { platform: 'pc' }, 'Bearer access-token']
]);
assert.throws(
() => client.promotions({ platform: 'desktop' }),
/不支持的推广平台/
);
});
test('site article method uses the public PC path and validates SQL query values', async () => {
const calls = [];
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: {
getItem() { return 'access-token'; },
setItem() {},
removeItem() {}
},
axiosInstance: {
request(config) {
calls.push(config);
return Promise.resolve({ data: { code: 200, data: [] } });
}
}
});
await client.siteArticles({
articleType: 'notice',
limit: 100,
keyword: 'must-drop'
});
await client.siteArticles({ limit: 100 });
assert.deepEqual(calls.map((config) => [
config.method,
config.url,
config.params,
config.headers.Authorization
]), [
['get', '/genealogy/pc/site/articles', { articleType: 'notice', limit: 100 }, undefined],
['get', '/genealogy/pc/site/articles', { limit: 100 }, undefined]
]);
assert.throws(
() => client.siteArticles({ articleType: 'culture', limit: 100 }),
/不支持的资讯类型/
);
[0, -1, 1.5, 101, '100'].forEach((limit) => {
assert.throws(
() => client.siteArticles({ limit }),
/资讯数量限制/
);
});
});
test('genealogy context methods obtain IDs from the real PC list and detail paths', async () => {
const calls = [];
const client = GenealogyApi.createClient({
@@ -613,6 +721,72 @@ test('genealogy lifecycle methods use PC paths and strict request bodies', async
);
});
test('feedback methods use the PC collection path and AppFeedbackBody whitelist', async () => {
const calls = [];
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: { getItem() { return 'access-token'; }, setItem() {}, removeItem() {} },
axiosInstance: {
request(config) {
calls.push(config);
return Promise.resolve({ data: { code: 200, data: [] } });
}
}
});
await client.submitFeedback({
feedbackType: 'bug',
feedbackContent: '页面按钮无响应',
contactInfo: '19181970173',
feedbackTitle: 'must-drop',
appUserId: 'must-drop'
});
await client.myFeedback();
assert.deepEqual(calls.map((config) => [config.method, config.url, config.data]), [
['post', '/genealogy/pc/feedback', {
feedbackType: 'bug',
feedbackContent: '页面按钮无响应',
contactInfo: '19181970173'
}],
['get', '/genealogy/pc/feedback', undefined]
]);
});
test('VIP methods use PC package and order paths with AppVipOrderBody whitelist', async () => {
const calls = [];
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: { getItem() { return 'access-token'; }, setItem() {}, removeItem() {} },
axiosInstance: {
request(config) {
calls.push(config);
return Promise.resolve({ data: { code: 200, data: [] } });
}
}
});
await client.vipPackages();
await client.createVipOrder({
packageId: '2062179707935264769',
genealogyId: '2062179707935264770',
payType: 'wechat',
appUserId: 'must-drop',
payStatus: 'must-drop'
});
await client.vipOrders();
assert.deepEqual(calls.map((config) => [config.method, config.url, config.data]), [
['get', '/genealogy/pc/vip/packages', undefined],
['post', '/genealogy/pc/vip/orders', {
packageId: '2062179707935264769',
genealogyId: '2062179707935264770',
payType: 'wechat'
}],
['get', '/genealogy/pc/vip/orders', undefined]
]);
});
test('notification methods use the current PC list, unread and read paths', async () => {
const calls = [];
const client = GenealogyApi.createClient({