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
+179
View File
@@ -0,0 +1,179 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const SiteNewsPages = require('../public/js/site-news-pages.js');
const newsPage = fs.readFileSync(path.resolve(__dirname, '../news.html'), 'utf8');
const detailPage = fs.readFileSync(path.resolve(__dirname, '../article-detail.html'), 'utf8');
const articleId = '2062179707935264769';
function articleFixture(overrides) {
return Object.assign({
articleId,
articleType: 'notice',
articleTitle: '平台公告',
articleSummary: '公告摘要',
articleContent: '第一行\n第二行',
coverOssId: '2062179707935264701',
externalUrl: 'https://example.com/notice',
publishTime: '2026-07-30 09:00:00',
sortOrder: 1,
status: '0',
remark: 'internal-only'
}, overrides);
}
test('站点文章只保留完整 SiteArticleVo 的安全展示字段和字符串长 ID', () => {
assert.deepEqual(SiteNewsPages.normalizeArticle(articleFixture()), {
articleId,
articleType: 'notice',
articleTitle: '平台公告',
articleSummary: '公告摘要',
articleContent: '第一行\n第二行',
externalUrl: 'https://example.com/notice',
publishTime: '2026-07-30 09:00:00'
});
assert.equal(SiteNewsPages.normalizeArticle(articleFixture({
articleId: Number.MAX_SAFE_INTEGER + 1
})), null);
assert.equal(SiteNewsPages.normalizeArticle(articleFixture({ articleId: '0' })), null);
});
test('站点文章拒绝 SQL 字典外类型、停用记录和空标题', () => {
assert.equal(SiteNewsPages.normalizeArticle(articleFixture({ articleType: 'culture' })), null);
assert.equal(SiteNewsPages.normalizeArticle(articleFixture({ status: '1' })), null);
assert.equal(SiteNewsPages.normalizeArticle(articleFixture({ articleTitle: ' ' })), null);
assert.equal(SiteNewsPages.normalizeArticleType('news'), 'news');
assert.equal(SiteNewsPages.normalizeArticleType('notice'), 'notice');
assert.equal(SiteNewsPages.normalizeArticleType('download'), 'download');
assert.equal(SiteNewsPages.normalizeArticleType('culture'), '');
});
test('站点文章列表只接受直接数组且任一非法元素使整批失败', () => {
assert.equal(SiteNewsPages.normalizeArticleList([articleFixture()]).length, 1);
assert.deepEqual(SiteNewsPages.normalizeArticleList([]), []);
assert.deepEqual(SiteNewsPages.normalizeArticleList({ rows: [articleFixture()] }), []);
assert.deepEqual(SiteNewsPages.normalizeArticleList([
articleFixture(),
articleFixture({ articleId: '' })
]), []);
});
test('站点资讯列表使用响应 ID 生成详情链接并隐藏内部字段', () => {
const html = SiteNewsPages.renderArticleList([articleFixture({
articleTitle: '<script>alert(1)</script>',
articleSummary: '<img src=x>',
coverOssId: 'secret-oss',
remark: 'secret-remark'
})]);
assert.match(html, /article-detail\.html\?articleId=2062179707935264769/);
assert.match(html, /公告/);
assert.match(html, /&lt;script&gt;/);
assert.match(html, /&lt;img src=x&gt;/);
assert.doesNotMatch(html, /<script|<img|secret-oss|secret-remark|coverOssId|sortOrder|status|remark/);
assert.match(SiteNewsPages.renderArticleList([]), /当前暂无资讯/);
});
test('站点资讯详情转义正文、保留换行并只开放安全外链', () => {
const html = SiteNewsPages.renderArticleDetail(articleFixture({
articleContent: '<script>alert(1)</script>\n第二行'
}));
assert.match(html, /&lt;script&gt;alert\(1\)&lt;\/script&gt;<br \/>第二行/);
assert.match(html, /href="https:\/\/example\.com\/notice"/);
assert.match(html, /target="_blank"/);
assert.match(html, /rel="noopener noreferrer"/);
assert.doesNotMatch(html, /<script|coverOssId|secret-remark/);
['javascript:alert(1)', 'data:text/html,test', '/notice', 'notice.html', ''].forEach((value) => {
const unsafeHtml = SiteNewsPages.renderArticleDetail(articleFixture({ externalUrl: value }));
assert.doesNotMatch(unsafeHtml, /data-external-link/);
});
});
test('站点文章外链只接受绝对 HTTP 或 HTTPS', () => {
assert.equal(
SiteNewsPages.normalizeExternalUrl(' https://example.com/notice '),
'https://example.com/notice'
);
assert.equal(SiteNewsPages.normalizeExternalUrl('http://example.com'), 'http://example.com/');
assert.equal(SiteNewsPages.normalizeExternalUrl('javascript:alert(1)'), '');
assert.equal(SiteNewsPages.normalizeExternalUrl('/notice'), '');
});
test('资讯列表只提交已确认分类和系统 limit', async () => {
const calls = [];
const api = {
async siteArticles(query) {
calls.push(query);
return [articleFixture()];
}
};
assert.equal((await SiteNewsPages.loadArticles(api, 'notice')).length, 1);
assert.equal((await SiteNewsPages.loadArticles(api, '')).length, 1);
assert.deepEqual(calls, [
{ articleType: 'notice', limit: 100 },
{ limit: 100 }
]);
await assert.rejects(SiteNewsPages.loadArticles(api, 'culture'), /资讯分类无效/);
await assert.rejects(
SiteNewsPages.loadArticles({
async siteArticles() {
return { rows: [articleFixture()] };
}
}, ''),
/资讯列表响应无效/
);
});
test('资讯详情只精确匹配 URL 中由列表产生的文章 ID', async () => {
const api = {
async siteArticles(query) {
assert.deepEqual(query, { limit: 100 });
return [
articleFixture(),
articleFixture({ articleId: '2062179707935264770', articleTitle: '另一条' })
];
}
};
assert.equal((await SiteNewsPages.loadArticleDetail(api, articleId)).articleTitle, '平台公告');
await assert.rejects(
SiteNewsPages.loadArticleDetail(api, '2062179707935264771'),
/资讯不存在或已下线/
);
await assert.rejects(
SiteNewsPages.loadArticleDetail(api, Number.MAX_SAFE_INTEGER + 1),
/资讯编号无效/
);
});
test('详情 URL 只读取安全字符串文章 ID', () => {
assert.equal(SiteNewsPages.readArticleId('?articleId=' + articleId), articleId);
assert.equal(SiteNewsPages.readArticleId('?articleId=0'), '');
assert.equal(SiteNewsPages.readArticleId('?articleId=unsafe'), '');
assert.equal(SiteNewsPages.readArticleId(''), '');
});
test('官网资讯列表与详情页加载同一个公开内容 owner 且不暴露业务 ID', () => {
assert.match(newsPage, /<body[^>]*data-site-news-page/);
assert.match(newsPage, /data-site-news-list/);
assert.match(newsPage, /data-site-news-status/);
assert.match(newsPage, /news\.html\?articleType=news/);
assert.match(newsPage, /news\.html\?articleType=notice/);
assert.match(newsPage, /news\.html\?articleType=download/);
assert.match(newsPage, /src="public\/js\/site-news-pages\.js"/);
assert.match(detailPage, /<body[^>]*data-site-article-page/);
assert.match(detailPage, /<title>资讯详情 - 代代相传<\/title>/);
assert.match(detailPage, /data-site-article-detail/);
assert.match(detailPage, /data-site-article-status/);
assert.match(detailPage, /src="public\/js\/site-news-pages\.js"/);
assert.doesNotMatch(detailPage, /src="public\/js\/article-pages\.js"/);
assert.doesNotMatch(detailPage, /href="article-detail\.html"/);
assert.doesNotMatch(newsPage + detailPage, /name="(?:articleId|coverOssId)"/);
});