// 已读取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).toBeGreaterThanOrEqual(28); 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('getMyCoupons'); expect(content).toContain('userCouponId'); }); 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('活动品类与合规商圈不得保留本地兜底数据,必须完全由后台配置驱动', () => { const actCreatePath = path.join(rootDir, 'pwuniapp', 'pages', 'activity', 'create.vue'); const content = fs.readFileSync(actCreatePath, 'utf-8'); expect(content).toContain('const games = ref([]);'); expect(content).toContain('const recommendPublicZones = ref([]);'); expect(content).toContain('game: \'\''); expect(content).not.toContain('北京市朝阳区三里屯太古里'); expect(content).not.toContain('北京市海淀区五道口购物中心'); expect(content).not.toContain("['美食陪吃', '同城陪唱'"); const actListPath = path.join(rootDir, 'pwuniapp', 'pages', 'activity', 'list.vue'); const listContent = fs.readFileSync(actListPath, 'utf-8'); expect(listContent).not.toContain('默认兜底品类'); }); 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 promotePath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'promote.vue'); const content = fs.readFileSync(userIndexPath, 'utf-8'); const promoteContent = fs.readFileSync(promotePath, 'utf-8'); expect(content).toContain('showInvitePoster'); expect(promoteContent).toContain('一键复制'); expect(promoteContent).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).not.toContain('getDemoAccounts'); expect(content).not.toContain('demoLogin'); expect(content).not.toContain('多角色体验账号直登'); 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('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); } }); }); describe('16. 独立专属提现审批明细与记录页面验证 (pages/user/withdraw-records.vue)', () => { it('pages.json 必须成功注册 pages/user/withdraw-records 且物理文件必须存在', () => { const recordsPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'withdraw-records.vue'); expect(fs.existsSync(recordsPath)).toBe(true); const pagesJsonPath = path.join(rootDir, 'pwuniapp', 'pages.json'); const pagesJson = fs.readFileSync(pagesJsonPath, 'utf-8'); expect(pagesJson).toContain('pages/user/withdraw-records'); expect(pagesJson).toContain('提现审批明细'); }); it('独立提现记录页面必须支持状态选项卡筛选、总览统计与数据拉取', () => { const recordsPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'withdraw-records.vue'); const content = fs.readFileSync(recordsPath, 'utf-8'); expect(content).toContain('tabList'); expect(content).toContain('filteredRecords'); expect(content).toContain('getUserWithdrawals'); expect(content).toContain('goToWithdraw'); }); it('提现页面必须实现对提现记录独立页面的原生导航跳转', () => { const wdPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'withdraw.vue'); const content = fs.readFileSync(wdPath, 'utf-8'); expect(content).toContain('/pages/user/withdraw-records'); }); it('提现记录页面代码严禁包含任何系统 Emoji 与 showModal 4 字门禁违规', () => { const recordsPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'withdraw-records.vue'); const content = fs.readFileSync(recordsPath, '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); } }); }); describe('17. 独立专属收支账单明细页面验证 (pages/user/bill-records.vue)', () => { it('pages.json 必须成功注册 pages/user/bill-records 且物理文件必须存在', () => { const billPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'bill-records.vue'); expect(fs.existsSync(billPath)).toBe(true); const pagesJsonPath = path.join(rootDir, 'pwuniapp', 'pages.json'); const pagesJson = fs.readFileSync(pagesJsonPath, 'utf-8'); expect(pagesJson).toContain('pages/user/bill-records'); expect(pagesJson).toContain('账单明细'); }); it('个人中心页面 (pages/user/index.vue) 账单明细入口必须原生跳转至 /pages/user/bill-records 且杜绝弹窗', () => { const userVuePath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'index.vue'); const content = fs.readFileSync(userVuePath, 'utf-8'); expect(content).toContain('/pages/user/bill-records'); expect(content).not.toContain("title: '最近收支明细'"); }); it('独立账单明细页面必须支持分类选项卡筛选、总览统计、一键复制流水号与双主题适配', () => { const billPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'bill-records.vue'); const content = fs.readFileSync(billPath, 'utf-8'); expect(content).toContain('tabList'); expect(content).toContain('filteredRecords'); expect(content).toContain('getUserFinanceRecords'); expect(content).toContain('handleCopyOrderNo'); expect(content).toContain('theme-warm'); expect(content).toContain('theme-cool'); expect(content).toContain('userBalance'); }); it('仓储层 getUserFinanceRecords 必须包含 user_id 字段选择与查询', () => { const repoPath = path.join(rootDir, 'server', 'src', 'db', 'repository.ts'); const content = fs.readFileSync(repoPath, 'utf-8'); expect(content).toContain('user_id as userId'); expect(content).toContain('user_id = ?'); }); it('账单明细页面代码严禁包含任何系统 Emoji 与 showModal 4 字门禁违规', () => { const billPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'bill-records.vue'); const content = fs.readFileSync(billPath, '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); } }); }); describe('18. 移动端用户资产与基本信息接口健全性验证 (GET /api/users/:id)', () => { it('仓储层 getUserById 必须包含合法的 SQL 查询语句且带有 FROM users WHERE id = ?', () => { const repoPath = path.join(rootDir, 'server', 'src', 'db', 'repository.ts'); const content = fs.readFileSync(repoPath, 'utf-8'); expect(content).toContain('FROM users'); expect(content).toContain('WHERE id = ?'); }); it('严禁在服务端内置演示用户兜底(测试数据必须落库)', () => { const repoPath = path.join(rootDir, 'server', 'src', 'db', 'repository.ts'); const repoContent = fs.readFileSync(repoPath, 'utf-8'); 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).not.toContain('userId === 2006'); expect(serverContent).not.toContain('userId === 101'); }); it('相关文件严禁包含任何系统 Emoji 违规字符', () => { const repoPath = path.join(rootDir, 'server', 'src', 'db', 'repository.ts'); const serverIndexPath = path.join(rootDir, 'server', 'src', 'index.ts'); const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u; expect(emojiRegex.test(fs.readFileSync(repoPath, 'utf-8'))).toBe(false); expect(emojiRegex.test(fs.readFileSync(serverIndexPath, 'utf-8'))).toBe(false); }); }); describe('19. 订单列表页面沉浸式导航栏与返回按钮规范验证 (pages/order/list.vue)', () => { const orderListPath = path.join(rootDir, 'pwuniapp', 'pages', 'order', 'list.vue'); it('订单列表页面必须包含 custom-nav-bar 沉浸式导航栏与返回上一级按钮', () => { expect(fs.existsSync(orderListPath)).toBe(true); const content = fs.readFileSync(orderListPath, 'utf-8'); expect(content).toContain('custom-nav-bar'); expect(content).toContain('btn-nav-back'); expect(content).toContain('handleBack'); expect(content).toContain('nav-title'); expect(content).toContain('我的订单'); expect(content).toContain('nav-capsule-placeholder'); }); it('必须包含智能返回上一级与针对个人中心等来源的返回兜底控制逻辑', () => { const content = fs.readFileSync(orderListPath, 'utf-8'); expect(content).toContain('getCurrentPages()'); expect(content).toContain('uni.navigateBack()'); expect(content).toContain('uni.switchTab'); expect(content).toContain('/pages/user/index'); const userIndexPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'index.vue'); const userContent = fs.readFileSync(userIndexPath, 'utf-8'); expect(userContent).toContain("uni.setStorageSync('ORDER_PAGE_FROM', '/pages/user/index')"); }); it('必须适配微信小程序胶囊安全避让变量与冷暖双主题', () => { const content = fs.readFileSync(orderListPath, 'utf-8'); expect(content).toContain('--safe-header-gap'); expect(content).toContain('--capsule-right-space'); expect(content).toContain('--capsule-height'); expect(content).toContain('.order-list-container.theme-warm'); }); it('订单列表页面源码与测试严禁包含任何系统 Emoji 违规字符', () => { const content = fs.readFileSync(orderListPath, 'utf-8'); const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u; expect(emojiRegex.test(content)).toBe(false); }); }); describe('20. 独立专属推广赚钱页面规格验证 (pages/user/promote.vue)', () => { const promotePath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'promote.vue'); const pagesJsonPath = path.join(rootDir, 'pwuniapp', 'pages.json'); it('pages.json 必须成功注册 pages/user/promote 且物理文件必须存在', () => { expect(fs.existsSync(promotePath)).toBe(true); const pagesJson = JSON.parse(fs.readFileSync(pagesJsonPath, 'utf-8')); const hasPage = pagesJson.pages.some((p: any) => p.path === 'pages/user/promote'); expect(hasPage).toBe(true); }); it('推广赚钱页面必须包含沉浸式导航栏、分销总览、海报/团队/订单/提现四大板块', () => { const content = fs.readFileSync(promotePath, 'utf-8'); expect(content).toContain('custom-nav-bar'); expect(content).toContain('推广赚钱'); expect(content).toContain('dist-summary-banner'); expect(content).toContain('tab-pane-poster'); expect(content).toContain('tab-pane-team'); expect(content).toContain('tab-pane-records'); expect(content).toContain('tab-pane-withdraw'); expect(content).toContain('handleBack'); }); it('个人中心页面 (pages/user/index.vue) 推广赚钱入口必须原生跳转至 /pages/user/promote 且杜绝弹窗', () => { const userIndexPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'index.vue'); const content = fs.readFileSync(userIndexPath, 'utf-8'); expect(content).toContain("navTo('/pages/user/promote')"); expect(content).toContain("url: '/pages/user/promote'"); expect(content).not.toContain('class="profile-edit-modal dist-modal-box"'); }); it('推广赚钱页面代码与测试严禁包含任何系统 Emoji 违规字符', () => { const content = fs.readFileSync(promotePath, 'utf-8'); const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u; expect(emojiRegex.test(content)).toBe(false); }); }); describe('21. 友圈广场点赞图标与矢量图标库完整性验证 (pwuniapp/common/icons.js & pages/community/index.vue)', () => { const iconsPath = path.join(rootDir, 'pwuniapp', 'common', 'icons.js'); const communityPath = path.join(rootDir, 'pwuniapp', 'pages', 'community', 'index.vue'); it('icons.js 必须完整导出 heart (未点赞) 与 heartFilled (已点赞高亮) 矢量图标', () => { expect(fs.existsSync(iconsPath)).toBe(true); const content = fs.readFileSync(iconsPath, 'utf-8'); expect(content).toContain('heart: svgToUri'); expect(content).toContain('heartFilled: svgToUri'); expect(content).toContain('#94A3B8'); expect(content).toContain('#EF4444'); }); it('友圈广场页面点赞按钮必须正确绑定 heartFilled 与 heart 图标', () => { expect(fs.existsSync(communityPath)).toBe(true); const content = fs.readFileSync(communityPath, 'utf-8'); expect(content).toContain(':src="post.liked ? ICONS.heartFilled : ICONS.heart"'); }); it('icons.js 与友圈广场页面严禁包含任何系统 Emoji 违规字符', () => { const iconsContent = fs.readFileSync(iconsPath, 'utf-8'); const communityContent = fs.readFileSync(communityPath, 'utf-8'); const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u; expect(emojiRegex.test(iconsContent)).toBe(false); expect(emojiRegex.test(communityContent)).toBe(false); }); }); describe('22. 友圈广场点赞状态跨页面持久化与防重闭环验证 (MySQL post_likes & UniApp 同步)', () => { it('MySQL 结构中必须包含 post_likes 用户点赞明细表及唯一防重键', () => { const mysqlPath = path.join(rootDir, 'server', 'src', 'db', 'mysql.ts'); const content = fs.readFileSync(mysqlPath, 'utf-8'); expect(content).toContain('CREATE TABLE IF NOT EXISTS post_likes'); expect(content).toContain('post_id'); expect(content).toContain('user_id'); expect(content).toContain('uq_post_user'); }); it('Repository getPosts 与 likePost 必须支持用户点赞状态关联查询与双向落盘', () => { const repoPath = path.join(rootDir, 'server', 'src', 'db', 'repository.ts'); const content = fs.readFileSync(repoPath, 'utf-8'); expect(content).toContain('SELECT post_id FROM post_likes WHERE user_id = ?'); expect(content).toContain('INSERT IGNORE INTO post_likes'); expect(content).toContain('DELETE FROM post_likes WHERE post_id = ? AND user_id = ?'); }); it('服务端路由 /api/posts 与 /api/posts/:id/like 必须支持透传 userId 与 action', () => { const indexPath = path.join(rootDir, 'server', 'src', 'index.ts'); const content = fs.readFileSync(indexPath, 'utf-8'); expect(content).toContain("app.get('/api/posts'"); expect(content).toContain("app.post('/api/posts/:id/like'"); expect(content).toContain("'like' | 'unlike'"); }); it('UniApp API 层与社区页面必须通过本地存储双重保障切页不丢点赞状态', () => { const apiPath = path.join(rootDir, 'pwuniapp', 'common', 'api.js'); const apiContent = fs.readFileSync(apiPath, 'utf-8'); expect(apiContent).toContain('COMMUNITY_LIKED_POST_IDS'); const communityPath = path.join(rootDir, 'pwuniapp', 'pages', 'community', 'index.vue'); const communityContent = fs.readFileSync(communityPath, 'utf-8'); expect(communityContent).toContain('COMMUNITY_LIKED_POST_IDS'); expect(communityContent).toContain('syncInitialLikedState'); expect(communityContent).toContain('uni.setStorageSync(\'COMMUNITY_LIKED_POST_IDS\''); }); it('所有新增的点赞持久化代码与测试严禁包含任何系统 Emoji 违规字符', () => { const mysqlPath = path.join(rootDir, 'server', 'src', 'db', 'mysql.ts'); const repoPath = path.join(rootDir, 'server', 'src', 'db', 'repository.ts'); const indexPath = path.join(rootDir, 'server', 'src', 'index.ts'); const apiPath = path.join(rootDir, 'pwuniapp', 'common', 'api.js'); const communityPath = path.join(rootDir, 'pwuniapp', 'pages', 'community', 'index.vue'); const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u; expect(emojiRegex.test(fs.readFileSync(mysqlPath, 'utf-8'))).toBe(false); expect(emojiRegex.test(fs.readFileSync(repoPath, 'utf-8'))).toBe(false); expect(emojiRegex.test(fs.readFileSync(indexPath, 'utf-8'))).toBe(false); expect(emojiRegex.test(fs.readFileSync(apiPath, 'utf-8'))).toBe(false); expect(emojiRegex.test(fs.readFileSync(communityPath, 'utf-8'))).toBe(false); }); }); describe('23. 全模块状态持久化与跨页面状态同步健壮性规范验证 (组局/门票/工作台/收藏/搭子)', () => { it('API 层 normalizeUserId 与收藏/组局方法必须优先支持 Storage 用户ID', () => { const apiPath = path.join(rootDir, 'pwuniapp', 'common', 'api.js'); const content = fs.readFileSync(apiPath, 'utf-8'); expect(content).toContain("uni.getStorageSync('userId')"); expect(content).toContain('function normalizeUserId'); expect(content).toContain('export async function getFavorites(userId)'); expect(content).toContain('export async function checkFavorite(expertId, userId)'); }); it('组局详情页必须动态响应当前用户并在 onShow 中拉取最新数据', () => { const actDetailPath = path.join(rootDir, 'pwuniapp', 'pages', 'activity', 'detail.vue'); const content = fs.readFileSync(actDetailPath, 'utf-8'); expect(content).toContain('currentUserId'); expect(content).toContain('currentActId'); expect(content).toContain('loadLiveDetail(currentActId.value)'); expect(content).not.toContain('const CURRENT_USER_ID = 1001;'); }); it('我的门票页面必须注册 onShow 并在退票与核销时绑定动态用户', () => { const ticketPath = path.join(rootDir, 'pwuniapp', 'pages', 'activity', 'my-ticket.vue'); const content = fs.readFileSync(ticketPath, 'utf-8'); expect(content).toContain('onShow'); expect(content).toContain('currentUserId'); expect(content).toContain('loadTicketData()'); expect(content).toContain('leaveActivity(actId, currentUserId.value)'); }); it('大神工作台接单状态必须持久化 EXPERT_RECEIVING_STATUS 并在 onShow 恢复', () => { const wsPath = path.join(rootDir, 'pwuniapp', 'pages', 'expert', 'workspace.vue'); const content = fs.readFileSync(wsPath, 'utf-8'); expect(content).toContain('EXPERT_RECEIVING_STATUS'); expect(content).toContain("uni.setStorageSync('EXPERT_RECEIVING_STATUS'"); }); it('大神主页必须在 onShow 中刷新当前用户的收藏状态', () => { const expertDetailPath = path.join(rootDir, 'pwuniapp', 'pages', 'expert', 'detail.vue'); const content = fs.readFileSync(expertDetailPath, 'utf-8'); expect(content).toContain('onShow'); expect(content).toContain('refreshFavoriteState'); expect(content).toContain('currentExpertTargetId'); }); it('友圈页面必须通过 COMMUNITY_APPLIED_POST_IDS 持久化申请上车状态并防重', () => { const communityPath = path.join(rootDir, 'pwuniapp', 'pages', 'community', 'index.vue'); const content = fs.readFileSync(communityPath, 'utf-8'); expect(content).toContain('COMMUNITY_APPLIED_POST_IDS'); expect(content).toContain('btn-applied'); expect(content).toContain('syncInitialPostStates'); }); it('大神主页必须接入全局双主题联动并在暖阳晨曦模式下适配 theme-warm', () => { const expertDetailPath = path.join(rootDir, 'pwuniapp', 'pages', 'expert', 'detail.vue'); const content = fs.readFileSync(expertDetailPath, 'utf-8'); expect(content).toContain("theme-warm': currentThemeKey === 'warm'"); expect(content).toContain('getCurrentThemeKey'); expect(content).toContain('applyThemeToNativeUI'); expect(content).toContain('theme-change'); expect(content).toContain('.expert-detail-container.theme-warm'); }); it('友圈置顶头条公告必须支持跑马灯平滑走字微动与点击呼出社区公约详情弹窗', () => { const communityPath = path.join(rootDir, 'pwuniapp', 'pages', 'community', 'index.vue'); const content = fs.readFileSync(communityPath, 'utf-8'); expect(content).toContain('notice-marquee-wrap'); expect(content).toContain('marqueeScroll'); expect(content).toContain('openNoticeDetailModal'); expect(content).toContain('noticeModalVisible'); expect(content).toContain('notice-modal-mask'); expect(content).toContain('notice-modal-content'); expect(content).toContain('closeNoticeDetailModal'); expect(content).toContain('ICONS.chevronRight'); }); it('全端弹窗遮罩层严禁使用微信小程序不兼容的 @click.self 且内部卡片必须使用 stopPropagation 阻断冒泡', () => { const orderListPath = path.join(rootDir, 'pwuniapp', 'pages', 'order', 'list.vue'); const orderCreatePath = path.join(rootDir, 'pwuniapp', 'pages', 'order', 'create.vue'); const expertDetailPath = path.join(rootDir, 'pwuniapp', 'pages', 'expert', 'detail.vue'); const expertWorkspacePath = path.join(rootDir, 'pwuniapp', 'pages', 'expert', 'workspace.vue'); const communityPath = path.join(rootDir, 'pwuniapp', 'pages', 'community', 'index.vue'); const orderListContent = fs.readFileSync(orderListPath, 'utf-8'); expect(orderListContent).not.toContain('@click.self'); expect(orderListContent).toContain('@click.stop="stopPropagation"'); const orderCreateContent = fs.readFileSync(orderCreatePath, 'utf-8'); expect(orderCreateContent).not.toContain('@click.self'); expect(orderCreateContent).toContain('@click.stop="stopPropagation"'); const expertDetailContent = fs.readFileSync(expertDetailPath, 'utf-8'); expect(expertDetailContent).not.toContain('@click.self'); expect(expertDetailContent).toContain('@click.stop="stopPropagation"'); const expertWorkspaceContent = fs.readFileSync(expertWorkspacePath, 'utf-8'); expect(expertWorkspaceContent).not.toContain('@click.self'); expect(expertWorkspaceContent).toContain('@click.stop="stopPropagation"'); const communityContent = fs.readFileSync(communityPath, 'utf-8'); expect(communityContent).not.toContain('@click.self'); expect(communityContent).toContain('@click.stop="stopPropagation"'); }); it('所有状态持久化修复代码与测试严禁包含任何系统 Emoji 违规字符', () => { const actDetailPath = path.join(rootDir, 'pwuniapp', 'pages', 'activity', 'detail.vue'); const ticketPath = path.join(rootDir, 'pwuniapp', 'pages', 'activity', 'my-ticket.vue'); const wsPath = path.join(rootDir, 'pwuniapp', 'pages', 'expert', 'workspace.vue'); const expertDetailPath = path.join(rootDir, 'pwuniapp', 'pages', 'expert', 'detail.vue'); const communityPath = path.join(rootDir, 'pwuniapp', 'pages', 'community', 'index.vue'); const apiPath = path.join(rootDir, 'pwuniapp', 'common', 'api.js'); const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u; expect(emojiRegex.test(fs.readFileSync(actDetailPath, 'utf-8'))).toBe(false); expect(emojiRegex.test(fs.readFileSync(ticketPath, 'utf-8'))).toBe(false); expect(emojiRegex.test(fs.readFileSync(wsPath, 'utf-8'))).toBe(false); expect(emojiRegex.test(fs.readFileSync(expertDetailPath, 'utf-8'))).toBe(false); expect(emojiRegex.test(fs.readFileSync(communityPath, 'utf-8'))).toBe(false); expect(emojiRegex.test(fs.readFileSync(apiPath, 'utf-8'))).toBe(false); }); }); describe('24. 独立专属钱包充值页面规格验证 (pages/user/recharge.vue)', () => { const rechargePath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'recharge.vue'); const pagesJsonPath = path.join(rootDir, 'pwuniapp', 'pages.json'); const userIndexPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'index.vue'); it('pages.json 必须成功注册 pages/user/recharge 且物理文件必须存在', () => { expect(fs.existsSync(rechargePath)).toBe(true); const pagesJson = JSON.parse(fs.readFileSync(pagesJsonPath, 'utf-8')); const hasPage = pagesJson.pages.some((p: any) => p.path === 'pages/user/recharge'); expect(hasPage).toBe(true); }); it('充值页面必须包含沉浸式导航栏、当前余额看板、档位网格、自定义金额、微信支付通道与提交栏', () => { const content = fs.readFileSync(rechargePath, 'utf-8'); expect(content).toContain('custom-nav-bar'); expect(content).toContain('钱包充值'); expect(content).toContain('handleBack'); expect(content).toContain('balance-card'); expect(content).toContain('tier-grid'); expect(content).toContain('custom-amount'); expect(content).toContain('channel-card'); expect(content).toContain('微信支付'); expect(content).toContain('fixed-submit-bar'); expect(content).toContain('rechargeBalance'); }); it('个人中心页面 (pages/user/index.vue) 充值入口必须原生跳转至 /pages/user/recharge 且杜绝弹窗', () => { const content = fs.readFileSync(userIndexPath, 'utf-8'); expect(content).toContain("url: '/pages/user/recharge'"); const rechargeFuncMatch = content.match(/const handleRecharge\s*=\s*(?:async\s*)?\(\)\s*=>\s*\{[\s\S]*?\n\};/); expect(rechargeFuncMatch).not.toBeNull(); expect(rechargeFuncMatch![0]).toContain("url: '/pages/user/recharge'"); expect(rechargeFuncMatch![0]).not.toContain('showActionSheet'); }); it('充值页面与测试代码严禁包含任何系统 Emoji 与 showModal 4 字门禁违规', () => { const rechargeContent = fs.readFileSync(rechargePath, 'utf-8'); const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u; expect(emojiRegex.test(rechargeContent)).toBe(false); const modalMatches = rechargeContent.match(/(?:confirmText|cancelText)\s*:\s*['"]([^'"]+)['"]/g) || []; for (const m of modalMatches) { const text = m.split(':')[1].replace(/['"\s]/g, ''); expect(text.length).toBeLessThanOrEqual(4); } }); }); describe('25. 接单大厅大神本人信息与状态动态联动验证 (pages/expert/hall.vue)', () => { const hallPath = path.join(rootDir, 'pwuniapp', 'pages', 'expert', 'hall.vue'); it('hall.vue 必须物理存在且支持从 Storage 动态加载当前登录大神的真实信息', () => { expect(fs.existsSync(hallPath)).toBe(true); const content = fs.readFileSync(hallPath, 'utf-8'); expect(content).toContain("uni.getStorageSync('userId')"); expect(content).toContain("getUserProfile"); expect(content).toContain("getExpertStats"); }); it('hall.vue 必须在 onShow 中刷新当前登录大神的资料与接单在线状态', () => { const content = fs.readFileSync(hallPath, 'utf-8'); expect(content).toContain("EXPERT_RECEIVING_STATUS"); expect(content).toContain("onShow"); expect(content).toContain("loadHallData"); }); it('立即接单 handleGrabOrder 必须动态绑定当前大神的真实 ID、昵称与头像', () => { const content = fs.readFileSync(hallPath, 'utf-8'); expect(content).toContain("expert.value.id"); expect(content).toContain("expert.value.nickname"); expect(content).toContain("expert.value.avatar"); expect(content).not.toContain("companionName: '我 (接单大神)'"); }); it('hall.vue 页面代码与测试严禁包含任何系统 Emoji 与 showModal 4 字门禁违规', () => { const content = fs.readFileSync(hallPath, 'utf-8'); const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u; expect(emojiRegex.test(content)).toBe(false); const modalMatches = content.match(/(?:confirmText|cancelText)\s*:\s*['"]([^'"]+)['"]/g) || []; for (const m of modalMatches) { const text = m.split(':')[1].replace(/['"\s]/g, ''); expect(text.length).toBeLessThanOrEqual(4); } }); it('派单卡片必须支持点击查看详情面板,且详情面板严禁使用 @click.self 必须 stopPropagation', () => { const content = fs.readFileSync(hallPath, 'utf-8'); expect(content).toContain('openOrderDetailModal'); expect(content).toContain('showDetailModal'); expect(content).toContain('dispatch-detail-mask'); expect(content).toContain('dispatch-detail-modal'); expect(content).toContain('closeOrderDetailModal'); expect(content).toContain('stopPropagation'); expect(content).not.toContain('@click.self'); }); it('派单详情弹窗酬金展示必须使用徽章卡片结构并实现货币符号与数字基线横排对齐', () => { const content = fs.readFileSync(hallPath, 'utf-8'); expect(content).toContain('reward-badge-card'); expect(content).toContain('badge-label'); expect(content).toContain('amount-row'); expect(content).toContain('curr-mark'); expect(content).toContain('reward-num'); }); it('派单详情弹窗单号必须支持脱敏简化展示并支持点击一键复制完整流水号', () => { const content = fs.readFileSync(hallPath, 'utf-8'); expect(content).toContain('formatShortOrderNo'); expect(content).toContain('copyOrderNo'); expect(content).toContain('uni.setClipboardData'); }); }); });