feat: 同步管理端、用户端与后端业务,并接通线上 Web 接口

补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。

Co-authored-by: Cursor <cursoragent@cursor.com>
这个提交包含在:
wikiboss
2026-09-23 18:01:47 +08:00
父节点 8ea9c25386
当前提交 62e0ae4435
共有 227 个文件被更改,包括 21855 次插入 和 5264 次删除

查看文件

@@ -103,3 +103,97 @@ test('5. 页面 locale 必须覆盖筛选与列文案,且禁止 any', () => {
assert.ok(content.includes('radarLog.columns.viewedBlocks'), '必须有 viewedBlocks 列文案');
assert.ok(!/\bany\b/.test(content), 'locale 禁止使用 any');
});
test('6. 深度访问高亮:停留 ≥180000ms 或滚动深度 ≥0.6;缺字段不高亮', () => {
const content = fs.readFileSync(radarLogPagePath, 'utf-8');
assert.ok(
content.includes('180000'),
'必须包含停留时长阈值 180000 毫秒'
);
assert.ok(
content.includes('0.6'),
'必须包含滚动深度阈值 0.6'
);
assert.ok(
/isDeepVisit|deepVisit/.test(content),
'必须具备 isDeepVisit / deepVisit 判定'
);
assert.ok(
content.includes('rowClassName') || content.includes('onRow'),
'必须通过 rowClassName 或 onRow 对深度访问行高亮'
);
// 缺字段:须对 stayDurationMs / scrollDepth 做有限数值校验后再比阈值
assert.ok(
/Number\.isFinite|typeof[\s\S]{0,40}number/.test(content),
'字段缺失或非有限数时不得误判为深度访问'
);
assert.ok(
content.includes("t['radarLog.deepVisit']") ||
content.includes('radarLog.deepVisit'),
'深度访问文案必须走 locale'
);
});
test('7. locale 必须含深度访问文案;页面禁止写死国家或示例访客', () => {
const pageContent = fs.readFileSync(radarLogPagePath, 'utf-8');
const localeContent = fs.readFileSync(radarLogLocalePath, 'utf-8');
assert.ok(
localeContent.includes("'radarLog.deepVisit'"),
'locale 必须包含 radarLog.deepVisit'
);
assert.ok(localeContent.includes("'en-US'"), 'locale 必须含 en-US');
assert.ok(localeContent.includes("'zh-CN'"), 'locale 必须含 zh-CN');
assert.ok(localeContent.includes("'ar-SA'"), 'locale 必须含 ar-SA');
const bannedCountries = [
'United States',
'United Kingdom',
'Singapore',
'Germany',
'Japan',
'China',
'Dubai',
'Saudi Arabia',
];
for (const name of bannedCountries) {
assert.ok(!pageContent.includes(name), `禁止硬编码国家/地区名: ${name}`);
}
assert.ok(
!pageContent.includes('示例访客') &&
!pageContent.includes('sample-visitor') &&
!pageContent.includes('visitor_demo') &&
!pageContent.includes('Alex') &&
!/visitorId\s*:\s*['"][^'"]+['"]/.test(pageContent),
'禁止写死示例访客行'
);
assert.ok(
!/useState\s*<\s*RadarLogRow\s*\[\s*\]\s*>\s*\(\s*\[[^\]]+visitorId/.test(
pageContent
) &&
!/const\s+MOCK_|const\s+SAMPLE_|const\s+DEMO_/.test(pageContent),
'禁止用示例访客初始化表格数据'
);
});
test('8. 请求失败必须空表,不得残留示例访客', () => {
const content = fs.readFileSync(radarLogPagePath, 'utf-8');
assert.ok(content.includes('setData([])'), '失败必须 setData([])');
assert.ok(
/useState[<\s(][^)]*RadarLogRow[^)]*\)?\s*\(\s*\[\s*\]\s*\)/.test(content) ||
/useState\s*<\s*RadarLogRow\s*\[\s*\]\s*>\s*\(\s*\[\s*\]\s*\)/.test(content),
'初始 data 必须为空数组'
);
// 非 1000 与 catch 均应清空
assert.ok(
/code\s*===\s*1000[\s\S]*?setData\(\[\]\)/.test(content) ||
(content.includes('setData([])') && content.includes('catch')),
'code 非 1000 或 catch 必须清空表格'
);
assert.ok(
!content.includes('Mock.js') && !content.includes('mockList'),
'失败路径禁止留下 mock/示例列表'
);
});