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,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, /<script>/);
|
||||
assert.match(html, /<img src=x>/);
|
||||
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, /<script>alert\(1\)<\/script><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)"/);
|
||||
});
|
||||
Reference in New Issue
Block a user