// 已读取AGENTS.md import { ContentSecurityService } from '../src/core/security/content-security.service'; import * as fs from 'fs'; import * as path from 'path'; describe('ContentSecurityService & Compliance Architecture (TDD 微信内容安全与合规体验套件)', () => { let secService: ContentSecurityService; const rootDir = path.resolve(__dirname, '../../'); beforeEach(() => { secService = new ContentSecurityService(); }); describe('1. 文本内容安全过滤规则 (security.msgSecCheck)', () => { it('正常合规文本应安全放行', () => { const texts = [ '今晚三里屯吃铜锅涮肉,有一起的搭子吗?人均AA', '工体纯K量贩KTV麦霸欢唱局,准时到场', '王者荣耀荣耀王者五排车队,辅助位缺一' ]; for (const t of texts) { const res = secService.checkText(t); expect(res.pass).toBe(true); expect(res.code).toBe(0); expect(res.suggest).toBe('pass'); } }); it('涉黄低俗敏感词应被严格拦截 (code 87014)', () => { const badTexts = [ '同城美女上门特殊服务,随时可约', '招嫖包夜加微联系', '同城一夜情约炮' ]; for (const t of badTexts) { const res = secService.checkText(t); expect(res.pass).toBe(false); expect(res.code).toBe(87014); expect(res.suggest).toBe('block'); expect(res.message).toContain('涉嫌违规'); } }); it('黑产欺诈与引流私下交易词汇应被阻断', () => { const scamTexts = [ '加微信私下转账免手续费直接打款', '刷单兼职日赚千元' ]; for (const t of scamTexts) { const res = secService.checkText(t); expect(res.pass).toBe(false); expect(res.code).toBe(87014); } }); it('maskText 能够正确对违规敏感词执行脱敏掩码', () => { const masked = secService.maskText('严禁上门特殊服务违规行为'); expect(masked).toContain('******'); expect(masked).not.toContain('上门特殊服务'); }); }); describe('2. 多媒体图片内容安全校验 (security.mediaCheckAsync)', () => { it('合规格式与尺寸的图片应顺利通过校验', () => { const check = secService.checkImage('avatar_user_1.png', 'image/png', 500 * 1024); expect(check.pass).toBe(true); expect(check.code).toBe(0); }); it('非图片或伪装可执行脚本应被即时拦截', () => { const badCases = [ { file: 'virus.exe', mime: 'application/octet-stream', size: 1024 }, { file: 'shell.php', mime: 'text/plain', size: 1024 }, { file: 'pic.jpg.sh', mime: 'image/jpeg', size: 1024 } ]; for (const c of badCases) { const res = secService.checkImage(c.file, c.mime, c.size); expect(res.pass).toBe(false); expect(res.code).toBe(87014); } }); it('超大图片体积(超过10MB)应被安全拒绝', () => { const res = secService.checkImage('huge.png', 'image/png', 12 * 1024 * 1024); expect(res.pass).toBe(false); expect(res.message).toContain('10MB'); }); }); describe('3. 前端微信隐私保护指引弹窗组件规范性验证 (PrivacyModal.vue)', () => { const modalPath = path.join(rootDir, 'pwuniapp', 'components', 'PrivacyModal.vue'); it('PrivacyModal.vue 文件必须存在且格式有效', () => { expect(fs.existsSync(modalPath)).toBe(true); const content = fs.readFileSync(modalPath, 'utf-8'); expect(content).toContain('privacy-dialog'); expect(content).toContain('用户隐私保护指引'); }); it('必须支持微信小程序官方 agreePrivacyAuthorization 原生授权按钮', () => { const content = fs.readFileSync(modalPath, 'utf-8'); expect(content).toContain('open-type="agreePrivacyAuthorization"'); expect(content).toContain('wx.onNeedPrivacyAuthorization'); }); it('必须支持用户服务协议与隐私指引原生调用或弹窗', () => { const content = fs.readFileSync(modalPath, 'utf-8'); expect(content).toContain('openPrivacyContract'); }); it('必须适配冷暖双主题与无 Emoji 门禁', () => { const content = fs.readFileSync(modalPath, 'utf-8'); expect(content).toContain('theme-warm'); const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u; expect(emojiRegex.test(content)).toBe(false); }); it('首页与个人中心必须集成并暴露用户隐私指引入口', () => { const indexContent = fs.readFileSync(path.join(rootDir, 'pwuniapp', 'pages', 'index', 'index.vue'), 'utf-8'); const userContent = fs.readFileSync(path.join(rootDir, 'pwuniapp', 'pages', 'user', 'index.vue'), 'utf-8'); expect(indexContent).toContain('privacy-modal'); expect(userContent).toContain('privacy-modal'); }); }); describe('4. 小程序页面架构配置与文件完整性门禁验证 (pages.json)', () => { const pagesJsonPath = path.join(rootDir, 'pwuniapp', 'pages.json'); it('pages.json 注册的所有页面必须在源码目录物理存在,杜绝模拟器启动丢失 wxml 崩溃', () => { const raw = fs.readFileSync(pagesJsonPath, 'utf-8'); const config = JSON.parse(raw); expect(Array.isArray(config.pages)).toBe(true); expect(config.pages.length).toBe(17); for (const pageItem of config.pages) { const vueFilePath = path.join(rootDir, 'pwuniapp', `${pageItem.path}.vue`); expect({ path: pageItem.path, exists: fs.existsSync(vueFilePath) }).toEqual({ path: pageItem.path, exists: true }); } }); it('主包核心 TabBar 页面保持精炼且四大主页全部就绪', () => { const raw = fs.readFileSync(pagesJsonPath, 'utf-8'); const config = JSON.parse(raw); const tabPaths = config.tabBar.list.map((t: any) => t.pagePath); expect(tabPaths).toEqual([ 'pages/index/index', 'pages/activity/list', 'pages/order/list', 'pages/user/index' ]); }); }); describe('5. 动态自定义引流违规词库与首页运营全局弹窗组件规范验证', () => { it('支持动态加载自定义违规引流词并即时热拦截', () => { secService.setCustomKeywords(['私下走微信', '线下给现金', '联系方式v']); expect(secService.getCustomKeywords()).toEqual(['私下走微信', '线下给现金', '联系方式v']); const blocked1 = secService.checkText('加个好友,我们私下走微信吧'); expect(blocked1.pass).toBe(false); expect(blocked1.code).toBe(87014); expect(blocked1.message).toContain('私下走微信'); const blocked2 = secService.checkText('活动到了现场线下给现金可以吗'); expect(blocked2.pass).toBe(false); expect(blocked2.code).toBe(87014); expect(blocked2.message).toContain('线下给现金'); const passText = secService.checkText('今晚一起去望京合生麒麟社玩桌游'); expect(passText.pass).toBe(true); }); it('首页必须挂载 HomePopupModal 全局运营浮层弹窗组件', () => { const homePopupPath = path.join(rootDir, 'pwuniapp', 'components', 'HomePopupModal.vue'); expect(fs.existsSync(homePopupPath)).toBe(true); const indexContent = fs.readFileSync(path.join(rootDir, 'pwuniapp', 'pages', 'index', 'index.vue'), 'utf-8'); expect(indexContent).toContain('home-popup-modal'); expect(indexContent).toContain('HomePopupModal'); }); }); describe('6. 大神技能标签库与定价区间规范配置校验 (expert_rules)', () => { it('MySQL 数据库与种子数据必须注册 expert_rules 配置项且字段完整', () => { const mysqlPath = path.join(rootDir, 'server', 'src', 'db', 'mysql.ts'); const content = fs.readFileSync(mysqlPath, 'utf-8'); expect(content).toContain('expert_rules'); expect(content).toContain('minHourlyRate'); expect(content).toContain('maxHourlyRate'); expect(content).toContain('candidateTags'); }); it('管理后台 platform-config 必须包含 Tab 8 大神资质与定价规则可视化面板', () => { const adminConfigPath = path.join(rootDir, 'admin', 'arco-design-pro-vite', 'src', 'views', 'safety', 'platform-config', 'index.vue'); const content = fs.readFileSync(adminConfigPath, 'utf-8'); expect(content).toContain('expert_rules_config'); expect(content).toContain('大神资质与定价规则'); expect(content).toContain('expertRulesForm'); expect(content).toContain('expertCandidateTags'); }); it('服务端路由必须提供 PUT /api/companions/:id 接口并强校验价格上下限与敏感标签', () => { const serverIndexPath = path.join(rootDir, 'server', 'src', 'index.ts'); const content = fs.readFileSync(serverIndexPath, 'utf-8'); expect(content).toContain("app.put('/api/companions/:id'"); expect(content).toContain('minHourlyRate'); expect(content).toContain('maxHourlyRate'); expect(content).toContain('contentSecurityService.checkText'); }); it('测试套件与新增代码严禁任何 Emoji 违规', () => { const serverIndexPath = path.join(rootDir, 'server', 'src', 'index.ts'); const content = fs.readFileSync(serverIndexPath, 'utf-8'); const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u; expect(emojiRegex.test(content)).toBe(false); }); }); describe('7. 财务分销规则、组局预算规范与大神入驻申请闭环验证 (finance_rules & activity_rules)', () => { it('MySQL 数据库与种子数据必须注册 finance_rules 与 activity_rules 配置项', () => { const mysqlPath = path.join(rootDir, 'server', 'src', 'db', 'mysql.ts'); const content = fs.readFileSync(mysqlPath, 'utf-8'); expect(content).toContain('finance_rules'); expect(content).toContain('platformRatePercent'); expect(content).toContain('firstReferralPercent'); expect(content).toContain('activity_rules'); expect(content).toContain('minParticipants'); expect(content).toContain('presetPerPersonFees'); }); it('管理后台 platform-config 必须包含 Tab 9 财务规则与 Tab 10 组局预算面板', () => { const adminConfigPath = path.join(rootDir, 'admin', 'arco-design-pro-vite', 'src', 'views', 'safety', 'platform-config', 'index.vue'); const content = fs.readFileSync(adminConfigPath, 'utf-8'); expect(content).toContain('finance_rules_config'); expect(content).toContain('财务抽成与分销规则'); expect(content).toContain('activity_rules_config'); expect(content).toContain('组局发单与预算规范'); expect(content).toContain('financeRulesForm'); expect(content).toContain('activityRulesForm'); }); it('服务端路由必须提供 POST /api/companions/apply 真实申请接口且校验姓名/身份证/安全', () => { const serverIndexPath = path.join(rootDir, 'server', 'src', 'index.ts'); const content = fs.readFileSync(serverIndexPath, 'utf-8'); expect(content).toContain("app.post('/api/companions/apply'"); expect(content).toContain('真实姓名不能为空'); expect(content).toContain('身份证号不能为空'); }); it('小程序个人中心与发起组局页面必须动态绑定配置并实现门槛保护', () => { const userIndexContent = fs.readFileSync(path.join(rootDir, 'pwuniapp', 'pages', 'user', 'index.vue'), 'utf-8'); const createActContent = fs.readFileSync(path.join(rootDir, 'pwuniapp', 'pages', 'activity', 'create.vue'), 'utf-8'); expect(userIndexContent).toContain('applyExpertCert'); expect(userIndexContent).toContain('showExpertCertModal'); expect(userIndexContent).toContain('finance_rules'); expect(createActContent).toContain('presetPerPersonFees'); expect(createActContent).toContain('activity_rules'); }); }); describe('8. 订单结算体系与前端交互人性化专项验证 (支付方式二选一、动态优惠券、组局快捷时段与商圈选点)', () => { it('MySQL Schema 必须包含 pay_type 与 coupon_discount 字段升级补丁', () => { const mysqlPath = path.join(rootDir, 'server', 'src', 'db', 'mysql.ts'); const content = fs.readFileSync(mysqlPath, 'utf-8'); expect(content).toContain('ALTER TABLE orders ADD COLUMN pay_type'); expect(content).toContain('ALTER TABLE orders ADD COLUMN coupon_discount'); }); it('Repository createOrder 必须支持余额支付原子事务校验、扣减余额及财务流水记录', () => { const repoPath = path.join(rootDir, 'server', 'src', 'db', 'repository.ts'); const content = fs.readFileSync(repoPath, 'utf-8'); expect(content).toContain("data.payType === 'BALANCE'"); expect(content).toContain('SELECT balance FROM users WHERE id = ? FOR UPDATE'); expect(content).toContain('const newBalance = userBalance - amount'); expect(content).toContain('UPDATE users SET balance = ? WHERE id = ?'); expect(content).toContain('钱包余额支付订单'); expect(content).toContain("INSERT INTO finance_records (id, record_no, type, amount, direction, counterparty, related_id)"); }); it('小程序订单创建页面必须支持钱包余额与微信支付二选一并动态拉取营销优惠券', () => { const orderCreatePath = path.join(rootDir, 'pwuniapp', 'pages', 'order', 'create.vue'); const content = fs.readFileSync(orderCreatePath, 'utf-8'); expect(content).toContain('payType'); expect(content).toContain("'BALANCE'"); expect(content).toContain("'WECHAT'"); expect(content).toContain('getMarketingCoupons'); expect(content).toContain('couponDiscount'); }); it('小程序活动发起页面必须包含快捷时段胶囊与示范商圈及地图选点交互', () => { const actCreatePath = path.join(rootDir, 'pwuniapp', 'pages', 'activity', 'create.vue'); const content = fs.readFileSync(actCreatePath, 'utf-8'); expect(content).toContain('presetTimeOptions'); expect(content).toContain('recommendPublicZones'); expect(content).toContain('handleChooseLocation'); expect(content).toContain('uni.chooseLocation'); }); it('所有新增代码与测试套件严禁包含任何 Emoji 字符', () => { const pathsToCheck = [ path.join(rootDir, 'server', 'src', 'db', 'mysql.ts'), path.join(rootDir, 'server', 'src', 'db', 'repository.ts'), path.join(rootDir, 'pwuniapp', 'pages', 'order', 'create.vue'), path.join(rootDir, 'pwuniapp', 'pages', 'activity', 'create.vue'), path.join(rootDir, 'pwuniapp', 'common', 'icons.js'), path.join(rootDir, 'pwuniapp', 'common', 'api.js') ]; const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u; for (const p of pathsToCheck) { const text = fs.readFileSync(p, 'utf-8'); expect(emojiRegex.test(text)).toBe(false); } }); }); describe('9. 组局满员前「搭子自主退席与全额退款」与席位释放闭环验证 (P4 体验优化专项目标)', () => { it('MySQL 数据库必须支持 finance_records 表的 user_id 字段补丁', () => { const mysqlPath = path.join(rootDir, 'server', 'src', 'db', 'mysql.ts'); const content = fs.readFileSync(mysqlPath, 'utf-8'); expect(content).toContain('ALTER TABLE finance_records ADD COLUMN user_id'); }); it('Repository leaveActivity 必须支持原路退款至用户钱包并记入资金流水', () => { const repoPath = path.join(rootDir, 'server', 'src', 'db', 'repository.ts'); const content = fs.readFileSync(repoPath, 'utf-8'); expect(content).toContain('async leaveActivity(id: string, userId: number)'); expect(content).toContain('DELETE FROM participants WHERE id = ?'); expect(content).toContain('UPDATE users SET balance = balance + ? WHERE id = ?'); expect(content).toContain('成员退席原路退款'); }); it('服务端路由必须提供 POST /api/activities/:id/leave 接口', () => { const serverIndexPath = path.join(rootDir, 'server', 'src', 'index.ts'); const content = fs.readFileSync(serverIndexPath, 'utf-8'); expect(content).toContain("app.post('/api/activities/:id/leave'"); expect(content).toContain('leaveActivity'); }); it('小程序活动详情页必须支持普通搭子申请退席全额退款与本人席位标识', () => { const actDetailPath = path.join(rootDir, 'pwuniapp', 'pages', 'activity', 'detail.vue'); const content = fs.readFileSync(actDetailPath, 'utf-8'); expect(content).toContain('leaveActivity'); expect(content).toContain('handleLeave'); expect(content).toContain('申请退席 (全额退款)'); expect(content).toContain('my-badge'); expect(content).toContain('我的席位'); }); it('小程序门票页必须支持未核销门票自助申请退票退款', () => { const myTicketPath = path.join(rootDir, 'pwuniapp', 'pages', 'activity', 'my-ticket.vue'); const content = fs.readFileSync(myTicketPath, 'utf-8'); expect(content).toContain('leaveActivity'); expect(content).toContain('handleCancelTicket'); expect(content).toContain('申请退票退款 (释放席位)'); }); it('所有 P4 新增代码严禁包含任何 Emoji 违规字符', () => { const p4Files = [ path.join(rootDir, 'server', 'src', 'db', 'mysql.ts'), path.join(rootDir, 'server', 'src', 'db', 'repository.ts'), path.join(rootDir, 'server', 'src', 'index.ts'), path.join(rootDir, 'pwuniapp', 'pages', 'activity', 'detail.vue'), path.join(rootDir, 'pwuniapp', 'pages', 'activity', 'my-ticket.vue') ]; const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u; for (const f of p4Files) { const text = fs.readFileSync(f, 'utf-8'); expect(emojiRegex.test(text)).toBe(false); } }); }); describe('10. 个人中心未登录游客态、退出登录及切换账号门禁验证 (用户体验闭环与安全防范)', () => { it('个人中心页面必须包含退出登录、切换账号与独立登录页跳转入口', () => { const userIndexPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'index.vue'); const content = fs.readFileSync(userIndexPath, 'utf-8'); expect(content).toContain('handleLogout'); expect(content).toContain('openLoginModal'); expect(content).toContain('/pages/auth/login'); expect(content).toContain('isLoggedIn'); expect(content).toContain('account-actions-card'); expect(content).toContain('退出当前登录'); expect(content).toContain('切换账号'); expect(content).toContain('立即登录 / 注册'); }); it('个人中心核心交互必须包含未登录拦截守护 ensureLoggedIn', () => { const userIndexPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'index.vue'); const content = fs.readFileSync(userIndexPath, 'utf-8'); expect(content).toContain('const ensureLoggedIn = () =>'); expect(content).toContain('if (!ensureLoggedIn()) return;'); }); it('个人中心支持轻触头像、名称与UID呼出个人信息设置弹窗,并支持微信官方选头像与快速填入昵称', () => { const userIndexPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'index.vue'); const content = fs.readFileSync(userIndexPath, 'utf-8'); expect(content).toContain('handleAvatarOrNameClick'); expect(content).toContain('showEditModal'); expect(content).toContain('editProfile'); expect(content).toContain('open-type="chooseAvatar"'); expect(content).toContain('type="nickname"'); expect(content).toContain('onWechatChooseAvatar'); }); it('全端 uni.showModal 按钮文字长度严禁超过 4 个汉字 (符合微信官方标准,防止弹窗崩溃)', () => { const walk = (dir: string) => { const files = fs.readdirSync(dir); for (const f of files) { const full = path.join(dir, f); if (f === 'node_modules' || f === 'dist' || f === 'unpackage' || f === '.git') continue; if (fs.statSync(full).isDirectory()) { walk(full); } else if (f.endsWith('.vue') || f.endsWith('.js') || f.endsWith('.ts')) { const content = fs.readFileSync(full, 'utf-8'); const modalRegex = /showModal\s*\(\s*\{([\s\S]*?)\}\s*\)/g; let match; while ((match = modalRegex.exec(content)) !== null) { const modalBody = match[1]; const confirmMatch = modalBody.match(/confirmText\s*:\s*['"`]([^'"`]+)['"`]/); if (confirmMatch && confirmMatch[1].length > 4) { throw new Error(`Oversized confirmText in ${path.relative(rootDir, full)}: "${confirmMatch[1]}"`); } const cancelMatch = modalBody.match(/cancelText\s*:\s*['"`]([^'"`]+)['"`]/); if (cancelMatch && cancelMatch[1].length > 4) { throw new Error(`Oversized cancelText in ${path.relative(rootDir, full)}: "${cancelMatch[1]}"`); } } } } }; walk(path.join(rootDir, 'pwuniapp')); }); it('推广赚钱二级分销弹窗必须符合微信4字规范并支持一键复制专属邀请码', () => { const userIndexPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'index.vue'); const content = fs.readFileSync(userIndexPath, 'utf-8'); expect(content).toContain('showInvitePoster'); expect(content).toContain('一键复制'); expect(content).toContain('setClipboardData'); }); it('首页 Banner 轮播必须具备防 null 链式安全访问与数据容错过滤', () => { const indexVuePath = path.join(rootDir, 'pwuniapp', 'pages', 'index', 'index.vue'); const content = fs.readFileSync(indexVuePath, 'utf-8'); expect(content).toContain('banner?.bg'); expect(content).toContain('banner?.image'); expect(content).toContain('banners.value = list.filter(b => b && typeof b === \'object\');'); }); it('所有个人中心改造代码严禁包含任何系统 Emoji', () => { const userIndexPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'index.vue'); const text = fs.readFileSync(userIndexPath, 'utf-8'); const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u; expect(emojiRegex.test(text)).toBe(false); }); }); describe('11. 大神实名认证专属独立页面与多态流程验证 (pages/expert/apply.vue)', () => { it('pages.json 必须成功注册 pages/expert/apply 且物理文件必须存在', () => { const applyPath = path.join(rootDir, 'pwuniapp', 'pages', 'expert', 'apply.vue'); expect(fs.existsSync(applyPath)).toBe(true); const pagesJsonPath = path.join(rootDir, 'pwuniapp', 'pages.json'); const pagesJson = fs.readFileSync(pagesJsonPath, 'utf-8'); expect(pagesJson).toContain('pages/expert/apply'); expect(pagesJson).toContain('大神入驻认证'); }); it('大神入驻申请独立页面必须包含完整表单、协议声明与提交接口', () => { const applyPath = path.join(rootDir, 'pwuniapp', 'pages', 'expert', 'apply.vue'); const content = fs.readFileSync(applyPath, 'utf-8'); expect(content).toContain('applyForm'); expect(content).toContain('applyExpertCert'); expect(content).toContain('TRACK_OPTIONS'); expect(content).toContain('expertPriceMin'); expect(content).toContain('expertPriceMax'); expect(content).toContain('candidateTags'); expect(content).toContain('submitApply'); expect(content).toContain('实名认证审核中'); expect(content).toContain('已通过大神实名认证'); }); it('个人中心的大神入驻入口必须路由跳转至 /pages/expert/apply', () => { const userIndexPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'index.vue'); const content = fs.readFileSync(userIndexPath, 'utf-8'); expect(content).toContain('/pages/expert/apply'); }); it('新页面与改造代码严禁包含任何系统 Emoji', () => { const applyPath = path.join(rootDir, 'pwuniapp', 'pages', 'expert', 'apply.vue'); const content = fs.readFileSync(applyPath, 'utf-8'); const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u; expect(emojiRegex.test(content)).toBe(false); }); }); describe('12. 微信小程序官方标准登录/注册专属独立页面验证 (pages/auth/login.vue)', () => { it('pages.json 必须成功注册 pages/auth/login 且物理文件必须存在', () => { const loginPath = path.join(rootDir, 'pwuniapp', 'pages', 'auth', 'login.vue'); expect(fs.existsSync(loginPath)).toBe(true); const pagesJsonPath = path.join(rootDir, 'pwuniapp', 'pages.json'); const pagesJson = fs.readFileSync(pagesJsonPath, 'utf-8'); expect(pagesJson).toContain('pages/auth/login'); expect(pagesJson).toContain('账号登录与注册'); }); it('登录页面必须符合微信官方手机号快捷授权与极简合规标准', () => { const loginPath = path.join(rootDir, 'pwuniapp', 'pages', 'auth', 'login.vue'); const content = fs.readFileSync(loginPath, 'utf-8'); expect(content).toContain('open-type="getPhoneNumber"'); expect(content).toContain('@getphonenumber="handleGetPhoneNumber"'); expect(content).toContain('一键授权快捷登录'); expect(content).toContain('DEMO_ACCOUNTS'); expect(content).toContain('用户服务协议'); expect(content).toContain('隐私保护指引'); // 验证已彻底移除登录大门前强制填写的头像昵称卡片,保障成熟商业极速登录体验 expect(content).not.toContain('open-type="chooseAvatar"'); expect(content).not.toContain('type="nickname"'); }); it('登录注册页面代码严禁包含任何系统 Emoji', () => { const loginPath = path.join(rootDir, 'pwuniapp', 'pages', 'auth', 'login.vue'); const content = fs.readFileSync(loginPath, 'utf-8'); const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u; expect(emojiRegex.test(content)).toBe(false); }); it('登录页面底部协议必须支持后台灵活配置与单行紧凑展示规范', () => { const loginPath = path.join(rootDir, 'pwuniapp', 'pages', 'auth', 'login.vue'); const content = fs.readFileSync(loginPath, 'utf-8'); expect(content).toContain('getPlatformConfigs'); expect(content).toContain('platformProtocol'); expect(content).toContain('loadProtocolConfig'); expect(content).toContain('white-space: nowrap'); }); }); describe('13. 专属独立法律协议与隐私政策长文阅读页面验证 (pages/common/protocol.vue)', () => { it('pages.json 必须成功注册 pages/common/protocol 且物理文件必须存在', () => { const protoPath = path.join(rootDir, 'pwuniapp', 'pages', 'common', 'protocol.vue'); expect(fs.existsSync(protoPath)).toBe(true); const pagesJsonPath = path.join(rootDir, 'pwuniapp', 'pages.json'); const pagesJson = fs.readFileSync(pagesJsonPath, 'utf-8'); expect(pagesJson).toContain('pages/common/protocol'); expect(pagesJson).toContain('服务协议与隐私政策'); }); it('协议页面必须支持动态章节结构化拆分、长文上下滚动与双主题适配', () => { const protoPath = path.join(rootDir, 'pwuniapp', 'pages', 'common', 'protocol.vue'); const content = fs.readFileSync(protoPath, 'utf-8'); expect(content).toContain('scroll-view'); expect(content).toContain('formattedSections'); expect(content).toContain('getPlatformConfigs'); expect(content).toContain('applyThemeToNativeUI'); expect(content).toContain('theme-warm'); }); it('登录页与个人中心必须实现对专属协议页面的原生导航跳转', () => { const loginPath = path.join(rootDir, 'pwuniapp', 'pages', 'auth', 'login.vue'); const loginContent = fs.readFileSync(loginPath, 'utf-8'); expect(loginContent).toContain('/pages/common/protocol?type=service'); expect(loginContent).toContain('/pages/common/protocol?type=privacy'); const userPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'index.vue'); const userContent = fs.readFileSync(userPath, 'utf-8'); expect(userContent).toContain('/pages/common/protocol?type='); }); it('协议阅读页面代码严禁包含任何系统 Emoji', () => { const protoPath = path.join(rootDir, 'pwuniapp', 'pages', 'common', 'protocol.vue'); const content = fs.readFileSync(protoPath, 'utf-8'); const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u; expect(emojiRegex.test(content)).toBe(false); }); }); describe('14. 大神端经营与财务提现闭环规格验证 (2.2 陪玩端规格全套落地)', () => { it('服务端路由必须提供收益多维走势统计接口且按日/周/月返回指标数据', () => { const serverIndexPath = path.join(rootDir, 'server', 'src', 'index.ts'); const content = fs.readFileSync(serverIndexPath, 'utf-8'); expect(content).toContain("app.get('/api/expert/stats/:id/trends'"); expect(content).toContain('getExpertRevenueTrends'); const repoPath = path.join(rootDir, 'server', 'src', 'db', 'repository.ts'); const repoContent = fs.readFileSync(repoPath, 'utf-8'); expect(repoContent).toContain('getExpertRevenueTrends'); expect(repoContent).toContain('daily'); expect(repoContent).toContain('weekly'); expect(repoContent).toContain('monthly'); }); it('服务端必须支持微信零钱、支付宝、银行卡多渠道提现并记录完整审批流', () => { const repoPath = path.join(rootDir, 'server', 'src', 'db', 'repository.ts'); const repoContent = fs.readFileSync(repoPath, 'utf-8'); expect(repoContent).toContain('withdrawUserCommission'); expect(repoContent).toContain('getUserWithdrawals'); expect(repoContent).toContain('WECHAT'); expect(repoContent).toContain('ALIPAY'); expect(repoContent).toContain('BANK'); const serverIndexPath = path.join(rootDir, 'server', 'src', 'index.ts'); const serverContent = fs.readFileSync(serverIndexPath, 'utf-8'); expect(serverContent).toContain("app.get('/api/users/:id/withdrawals'"); }); it('服务端必须提供评价感谢回复、动态删除、服务状态切换接口', () => { const serverIndexPath = path.join(rootDir, 'server', 'src', 'index.ts'); const serverContent = fs.readFileSync(serverIndexPath, 'utf-8'); expect(serverContent).toContain("app.post('/api/reviews/:id/reply'"); expect(serverContent).toContain("app.delete('/api/posts/:id'"); expect(serverContent).toContain("app.put('/api/companions/:id/services/:serviceId/status'"); }); it('前端工作台 (workspace.vue) 必须集成收益走势图表、服务管理与提现审批看板', () => { const wsPath = path.join(rootDir, 'pwuniapp', 'pages', 'expert', 'workspace.vue'); const wsContent = fs.readFileSync(wsPath, 'utf-8'); expect(wsContent).toContain('收益走势统计分析'); expect(wsContent).toContain('getExpertRevenueTrends'); expect(wsContent).toContain('trendPeriod'); expect(wsContent).toContain('trendMetric'); expect(wsContent).toContain('expertServices'); expect(wsContent).toContain('updateExpertServiceStatus'); expect(wsContent).toContain('showWithdrawRecordsModal'); }); it('前端用户中心 (user/index.vue) 必须支持多渠道提现路由跳转与提现记录看板', () => { const userPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'index.vue'); const userContent = fs.readFileSync(userPath, 'utf-8'); expect(userContent).toContain('openWithdrawModal'); expect(userContent).toContain('openWithdrawRecordsModal'); expect(userContent).toContain('/pages/user/withdraw'); expect(userContent).toContain('showWithdrawRecordsModal'); expect(userContent).toContain('userWithdrawalsList'); }); it('前端动态广场与大神主页必须分别提供本人动态删除与评价回复感谢老板', () => { const commPath = path.join(rootDir, 'pwuniapp', 'pages', 'community', 'index.vue'); const commContent = fs.readFileSync(commPath, 'utf-8'); expect(commContent).toContain('deletePost'); expect(commContent).toContain('isPostOwner'); expect(commContent).toContain('handleDeletePost'); const detailPath = path.join(rootDir, 'pwuniapp', 'pages', 'expert', 'detail.vue'); const detailContent = fs.readFileSync(detailPath, 'utf-8'); expect(detailContent).toContain('replyOrderReview'); expect(detailContent).toContain('expert-reply-bubble'); expect(detailContent).toContain('handleReplyReview'); }); it('全量新增业务代码严禁出现 Emoji 与微信 showModal 超过 4 汉字违规', () => { const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u; const targetFiles = [ path.join(rootDir, 'pwuniapp', 'pages', 'expert', 'workspace.vue'), path.join(rootDir, 'pwuniapp', 'pages', 'user', 'index.vue'), path.join(rootDir, 'pwuniapp', 'pages', 'community', 'index.vue'), path.join(rootDir, 'pwuniapp', 'pages', 'expert', 'detail.vue') ]; targetFiles.forEach((file) => { const text = fs.readFileSync(file, 'utf-8'); expect(emojiRegex.test(text)).toBe(false); // 检查 confirmText 与 cancelText 不得超过 4 个字符 const modalMatches = text.matchAll(/(?:confirmText|cancelText):\s*['"]([^'"]+)['"]/g); for (const match of modalMatches) { const btnText = match[1]; expect(btnText.length).toBeLessThanOrEqual(4); } }); }); }); describe('15. 独立专属收益提现页面验证 (pages/user/withdraw.vue)', () => { it('pages.json 必须成功注册 pages/user/withdraw 且物理文件必须存在', () => { const wdPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'withdraw.vue'); expect(fs.existsSync(wdPath)).toBe(true); const pagesJsonPath = path.join(rootDir, 'pwuniapp', 'pages.json'); const pagesJson = fs.readFileSync(pagesJsonPath, 'utf-8'); expect(pagesJson).toContain('pages/user/withdraw'); expect(pagesJson).toContain('收益提现'); }); it('独立提现页面必须支持微信零钱、支付宝、银行卡三渠道与金额输入校验', () => { const wdPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'withdraw.vue'); const content = fs.readFileSync(wdPath, 'utf-8'); expect(content).toContain('currentChannel'); expect(content).toContain('WECHAT'); expect(content).toContain('ALIPAY'); expect(content).toContain('BANK'); expect(content).toContain('withdrawAmount'); expect(content).toContain('handleFillAllCommission'); expect(content).toContain('withdrawCommission'); expect(content).toContain('showRecordsDrawer'); }); it('个人中心与工作台必须实现对专属提现页面的原生导航跳转', () => { const userPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'index.vue'); const userContent = fs.readFileSync(userPath, 'utf-8'); expect(userContent).toContain('/pages/user/withdraw'); const wsPath = path.join(rootDir, 'pwuniapp', 'pages', 'expert', 'workspace.vue'); const wsContent = fs.readFileSync(wsPath, 'utf-8'); expect(wsContent).toContain('/pages/user/withdraw'); }); it('提现页面代码严禁包含任何系统 Emoji 与 showModal 4 字门禁违规', () => { const wdPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'withdraw.vue'); const content = fs.readFileSync(wdPath, 'utf-8'); const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u; expect(emojiRegex.test(content)).toBe(false); const modalMatches = content.matchAll(/(?:confirmText|cancelText):\s*['"]([^'"]+)['"]/g); for (const match of modalMatches) { const btnText = match[1]; expect(btnText.length).toBeLessThanOrEqual(4); } }); }); });