refactor(data): 全面移除小程序写死的演示/兜底数据,测试数据统一落库
- 删除无引用的 common/mock.js;登录页与个人中心的演示账号改为 DB 配置 demo_accounts + /api/auth/demo-accounts、/api/auth/demo-login(签发真实 token) - 移除订单/活动/大神/动态/推广/工作台/门票/充值/举报等页面的演示列表与假兜底;新增真实空态 - 修复伪造成功:活动调价、评价回复、核销、海报保存、图片上传失败、提现/核销固定 1001、随机开黑房号、随机即时匹配、假语音播放等 - 服务端移除 101/2006 合成用户与合成大神兜底、移除默认服务包虚构价格;演示账号清单可在后台关闭 - 111 套件/733 用例全绿;已部署 20261021_162041,demo-accounts/demo-login 实测通过
这个提交包含在:
@@ -1,119 +1,37 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
describe('ActivityListContent (TDD 测试套件: 活动大厅陪吃·陪唱·陪喝内容与零Emoji校验)', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const mockFilePath = path.join(rootDir, 'pwuniapp', 'common', 'mock.js');
|
||||
const listVuePath = path.join(rootDir, 'pwuniapp', 'pages', 'activity', 'list.vue');
|
||||
const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u;
|
||||
/**
|
||||
* 活动大厅内容规范(第九十六批改写)
|
||||
* 背景:演示用 mock.js 已删除,活动数据统一来自数据库(GET /api/activities)。
|
||||
* 本套件改为校验:不再内置 mock、页面真实取数、无 Emoji、关键字段展示完整。
|
||||
*/
|
||||
describe('ActivityListContent(活动大厅真实数据与内容规范)', () => {
|
||||
const root = path.resolve(__dirname, '../../');
|
||||
const pagePath = path.join(root, 'pwuniapp', 'pages', 'activity', 'list.vue');
|
||||
const page = fs.readFileSync(pagePath, 'utf-8');
|
||||
const mockPath = path.join(root, 'pwuniapp', 'common', 'mock.js');
|
||||
|
||||
describe('1. mock.js 中 ACTIVITIES_LIST 内容规范校验', () => {
|
||||
it('活动列表中陪吃·陪唱·陪喝三大核心品类应占主导地位 (>= 80%)', () => {
|
||||
const mockContent = fs.readFileSync(mockFilePath, 'utf-8');
|
||||
|
||||
// 提取 ACTIVITIES_LIST 数组对象文本
|
||||
const listMatch = mockContent.match(/export const ACTIVITIES_LIST\s*=\s*(\[[\s\S]*?\n\];)/);
|
||||
expect(listMatch).not.toBeNull();
|
||||
|
||||
// 解析执行提取对象
|
||||
const evalList = new Function(`return ${listMatch![1].replace(/;\s*$/, '')}`)();
|
||||
expect(Array.isArray(evalList)).toBe(true);
|
||||
expect(evalList.length).toBeGreaterThanOrEqual(4);
|
||||
|
||||
const mainCategories = ['美食陪吃', '同城陪唱', '下午茶陪喝', '同城微醺'];
|
||||
const mainCategoryCount = evalList.filter((item: any) =>
|
||||
mainCategories.includes(item.game || item.category)
|
||||
).length;
|
||||
|
||||
const mainRatio = mainCategoryCount / evalList.length;
|
||||
expect(mainRatio).toBeGreaterThanOrEqual(0.8);
|
||||
});
|
||||
|
||||
it('所有活动必须包含合法公开场所、时间与发起人信息', () => {
|
||||
const mockContent = fs.readFileSync(mockFilePath, 'utf-8');
|
||||
const listMatch = mockContent.match(/export const ACTIVITIES_LIST\s*=\s*(\[[\s\S]*?\n\];)/);
|
||||
const evalList = new Function(`return ${listMatch![1].replace(/;\s*$/, '')}`)();
|
||||
|
||||
for (const item of evalList) {
|
||||
expect(item.id).toBeDefined();
|
||||
expect(item.title).toBeDefined();
|
||||
expect(item.location).toBeDefined();
|
||||
expect(item.startTime).toBeDefined();
|
||||
expect(item.creatorNickname).toBeDefined();
|
||||
expect(item.creatorAvatar).toBeDefined();
|
||||
expect(['AA', 'PROJECT']).toContain(item.payType);
|
||||
|
||||
// 验证地点均为公开合法商圈/门店,不能有私人住宅等违规词
|
||||
expect(item.location).not.toMatch(/(酒店客房|私人住宅|私密公寓)/);
|
||||
}
|
||||
});
|
||||
|
||||
it('ACTIVITIES_LIST 数据中严禁包含任何系统 Emoji 字符', () => {
|
||||
const mockContent = fs.readFileSync(mockFilePath, 'utf-8');
|
||||
const listMatch = mockContent.match(/export const ACTIVITIES_LIST\s*=\s*(\[[\s\S]*?\n\];)/);
|
||||
expect(listMatch).not.toBeNull();
|
||||
|
||||
const listContent = listMatch![1];
|
||||
const lines = listContent.split('\n');
|
||||
const violations: string[] = [];
|
||||
|
||||
lines.forEach((line, idx) => {
|
||||
if (emojiRegex.test(line)) {
|
||||
violations.push(`Line ${idx + 1}: ${line.trim()}`);
|
||||
}
|
||||
});
|
||||
|
||||
expect(violations).toEqual([]);
|
||||
});
|
||||
it('不得再内置活动 mock 数据', () => {
|
||||
expect(fs.existsSync(mockPath)).toBe(false);
|
||||
expect(page).not.toContain('ACTIVITIES_LIST');
|
||||
expect(page).not.toContain("from '@/common/mock");
|
||||
});
|
||||
|
||||
describe('2. pages/activity/list.vue 页面规范校验', () => {
|
||||
it('活动列表页面模板与样式中严禁包含任何 Emoji 字符', () => {
|
||||
const content = fs.readFileSync(listVuePath, 'utf-8');
|
||||
const lines = content.split('\n');
|
||||
const violations: string[] = [];
|
||||
|
||||
lines.forEach((line, idx) => {
|
||||
if (emojiRegex.test(line)) {
|
||||
violations.push(`Line ${idx + 1}: ${line.trim()}`);
|
||||
}
|
||||
});
|
||||
|
||||
expect(violations).toEqual([]);
|
||||
});
|
||||
|
||||
it('页面应包含陪吃·陪唱·陪喝相关的分类快捷切换或展示逻辑', () => {
|
||||
const content = fs.readFileSync(listVuePath, 'utf-8');
|
||||
// 验证包含美食陪吃、同城陪唱等核心品类关键词
|
||||
expect(content).toMatch(/(美食陪吃|陪吃)/);
|
||||
expect(content).toMatch(/(同城陪唱|陪唱)/);
|
||||
expect(content).toMatch(/(下午茶陪喝|陪喝)/);
|
||||
});
|
||||
it('活动列表必须从服务端取数', () => {
|
||||
expect(page).toContain('getActivities');
|
||||
expect(page).toContain('activities.value = list');
|
||||
});
|
||||
|
||||
describe('3. 活动大厅综合搜索框功能与双主题规范门禁 (TDD)', () => {
|
||||
it('页面必须包含顶部综合搜索框组件并支持关键词双向绑定', () => {
|
||||
const content = fs.readFileSync(listVuePath, 'utf-8');
|
||||
expect(content).toContain('header-search');
|
||||
expect(content).toContain('search-input');
|
||||
expect(content).toContain('searchKeyword');
|
||||
});
|
||||
it('活动卡片展示地点、开始时间与发起人字段', () => {
|
||||
expect(page).toContain('location');
|
||||
expect(page).toContain('startTime');
|
||||
expect(page).toContain('creatorNickname');
|
||||
});
|
||||
|
||||
it('搜索过滤逻辑必须同时支持标题、商圈地点、品类与局长多维度匹配', () => {
|
||||
const content = fs.readFileSync(listVuePath, 'utf-8');
|
||||
// 必须包含基于 searchKeyword 的模糊匹配逻辑
|
||||
expect(content).toContain('matchKeyword');
|
||||
expect(content).toMatch(/a\.title.*includes/);
|
||||
expect(content).toMatch(/a\.location.*includes/);
|
||||
expect(content).toMatch(/a\.creatorNickname.*includes/);
|
||||
});
|
||||
|
||||
it('搜索框在 theme-warm 浅色暖阳晨曦模式下必须适配纯白背景与深色文本', () => {
|
||||
const content = fs.readFileSync(listVuePath, 'utf-8');
|
||||
const warmSection = content.split('.activity-container.theme-warm')[1] || '';
|
||||
expect(warmSection).toContain('.header-search');
|
||||
expect(warmSection).toContain('.search-box');
|
||||
expect(warmSection).toContain('#FFFFFF');
|
||||
});
|
||||
it('页面严禁包含任何系统 Emoji 字符', () => {
|
||||
const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u;
|
||||
expect(emojiRegex.test(page)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -517,7 +517,8 @@ describe('ContentSecurityService & Compliance Architecture (TDD 微信内容安
|
||||
expect(content).toContain('open-type="getPhoneNumber"');
|
||||
expect(content).toContain('@getphonenumber="handleGetPhoneNumber"');
|
||||
expect(content).toContain('一键授权快捷登录');
|
||||
expect(content).toContain('DEMO_ACCOUNTS');
|
||||
expect(content).toContain('getDemoAccounts');
|
||||
expect(content).toContain('demoLogin');
|
||||
expect(content).toContain('用户服务协议');
|
||||
expect(content).toContain('隐私保护指引');
|
||||
// 验证已彻底移除登录大门前强制填写的头像昵称卡片,保障成熟商业极速登录体验
|
||||
@@ -819,15 +820,16 @@ describe('ContentSecurityService & Compliance Architecture (TDD 微信内容安
|
||||
expect(content).toContain('WHERE id = ?');
|
||||
});
|
||||
|
||||
it('仓储层与路由必须针对预设用户 2006 与 101 提供兜底配置防止出现 500/404', () => {
|
||||
it('严禁在服务端内置演示用户兜底(测试数据必须落库)', () => {
|
||||
const repoPath = path.join(rootDir, 'server', 'src', 'db', 'repository.ts');
|
||||
const repoContent = fs.readFileSync(repoPath, 'utf-8');
|
||||
expect(repoContent).toContain('if (id === 2006)');
|
||||
expect(repoContent).toContain('苏苏学姐');
|
||||
expect(repoContent).not.toContain('if (id === 2006)');
|
||||
expect(repoContent).not.toContain('糖心小糯米');
|
||||
|
||||
const serverIndexPath = path.join(rootDir, 'server', 'src', 'index.ts');
|
||||
const serverContent = fs.readFileSync(serverIndexPath, 'utf-8');
|
||||
expect(serverContent).toContain('userId === 2006');
|
||||
expect(serverContent).not.toContain('userId === 2006');
|
||||
expect(serverContent).not.toContain('userId === 101');
|
||||
});
|
||||
|
||||
it('相关文件严禁包含任何系统 Emoji 违规字符', () => {
|
||||
|
||||
@@ -102,14 +102,11 @@ describe('UI/UX 规范门禁 (TDD 测试套件: 全面杜绝系统 Emoji)', () =
|
||||
expect(textStyle).toContain('text-overflow: ellipsis');
|
||||
});
|
||||
|
||||
it('mock.js 中试听标题长度必须精简且不得超过12个字,防止在小屏过早被截断', () => {
|
||||
it('演示数据必须来自数据库,禁止在小程序内置 mock.js', () => {
|
||||
const mockJsPath = path.join(rootDir, 'pwuniapp', 'common', 'mock.js');
|
||||
const mockContent = fs.readFileSync(mockJsPath, 'utf-8');
|
||||
const voiceTitleMatches = mockContent.matchAll(/voiceTitle:\s*['"]([^'"]+)['"]/g);
|
||||
for (const match of voiceTitleMatches) {
|
||||
const title = match[1];
|
||||
expect(title.length).toBeLessThanOrEqual(12);
|
||||
}
|
||||
expect(fs.existsSync(mockJsPath)).toBe(false);
|
||||
const apiContent = fs.readFileSync(path.join(rootDir, 'pwuniapp', 'common', 'api.js'), 'utf-8');
|
||||
expect(apiContent).not.toContain("common/mock");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -22,9 +22,9 @@ describe('第八十二批服务包空值防护闸门', () => {
|
||||
expect(createPage).not.toContain('selectedService.unit');
|
||||
});
|
||||
|
||||
it('下单页在无服务包时按大神基础时薪兜底,保证仍可下单', () => {
|
||||
expect(createPage).toContain("id: 'base'");
|
||||
expect(createPage).toContain('基础陪伴服务');
|
||||
it('下单页在无服务包时必须阻止下单,不得伪造基础服务', () => {
|
||||
expect(createPage).not.toContain("id: 'base'");
|
||||
expect(createPage).toContain('该大神暂未配置服务包,无法下单');
|
||||
expect(createPage).toContain('Array.isArray(liveExpert.services) ? liveExpert.services : []');
|
||||
});
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ describe('订单与活动资金一致性护栏 (报名超卖 / 抢单 CAS / 评
|
||||
|
||||
it('小程序评价必须提交真实登录用户ID,严禁硬编码 1001 绕过买家校验', () => {
|
||||
const vue = fs.readFileSync(pwuniappOrderListPath, 'utf-8');
|
||||
expect(vue).toContain("userId: Number(uni.getStorageSync('userId') || 1001),");
|
||||
expect(vue).toContain('userId: normalizeUserId(),');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ describe('第七十三批公开配置隐私闸门', () => {
|
||||
const repo = fs.readFileSync(path.join(root, 'server/src/db/repository.ts'), 'utf-8');
|
||||
|
||||
it('公开配置排除风控词库与地图密钥配置', () => {
|
||||
expect(repo).toContain("const PUBLIC_EXCLUDED_KEYS = new Set(['security_words', 'map_location_config'])");
|
||||
expect(repo).toContain("const PUBLIC_EXCLUDED_KEYS = new Set(['security_words', 'map_location_config', 'demo_accounts'])");
|
||||
expect(repo).toContain('if (PUBLIC_EXCLUDED_KEYS.has(r.configKey)) continue;');
|
||||
});
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户