文件
shanghaipeiwan/server/tests/client-features.spec.ts
xcbyy 94cf6cdc69 perf: 清理无界缓存、合并收益走势查询、补齐组合索引并优化前端定时器/图片加载
- 移除只写不读的 verifyCodeCache(无界内存增长)

- 大神收益走势由 20 次逐日/逐周/逐月查询改为 3 次 GROUP BY 聚合

- 派单池地理编码改为并行,避免串行等待外部接口

- 补齐 orders/activities/participants/user_notifications/finance/withdrawals/posts 组合索引;修复 withdrawals 使用不存在 created_at 的失效索引

- 数据库连接池支持 MYSQL_CONNECTION_LIMIT/MYSQL_QUEUE_LIMIT

- 首页/弹窗定时器卸载清理,活动列表参与者按活动 id 查询,大厅组局按页取数,图片 lazy-load

- 121 套件/792 用例全绿;已部署并验证新索引与 9ms 走势查询
2026-10-23 15:31:37 +08:00

546 行
27 KiB
TypeScript

import * as fs from 'fs';
import * as path from 'path';
import { ContentSecurityService } from '../src/core/security/content-security.service';
describe('ClientFeatures (TDD 测试套件: 用户端功能闭环)', () => {
const rootDir = path.resolve(__dirname, '../../');
const uniPagesDir = path.join(rootDir, 'pwuniapp', 'pages');
const serverSrcDir = path.join(rootDir, 'server', 'src');
const contentSecurityService = new ContentSecurityService();
describe('1. 举报违规行为通道闭环验证', () => {
it('后端 repository 必须实现 createReport 与 getReports 并支持存证入库', () => {
const repoPath = path.join(serverSrcDir, 'db', 'repository.ts');
const content = fs.readFileSync(repoPath, 'utf-8');
expect(content).toContain('createReport');
expect(content).toContain('getReports');
expect(content).toContain('reports');
expect(content).toContain('reporter_id');
expect(content).toContain('target_type');
});
it('后端 index.ts 必须暴露 POST /api/reports 路由并集成文本安全检测', () => {
const indexPath = path.join(serverSrcDir, 'index.ts');
const content = fs.readFileSync(indexPath, 'utf-8');
expect(content).toContain("app.post('/api/reports'");
expect(content).toContain('contentSecurityService.checkText');
expect(content).toContain('repo.createReport');
});
it('违规线索说明命中涉黄/欺诈敏感词时,内容安全风控应拦截', () => {
const checkResult = contentSecurityService.checkText('加微信私下转账免手续费直接打款');
expect(checkResult.pass).toBe(false);
expect(checkResult.code).toBe(87014);
expect(checkResult.suggest).toBe('block');
});
it('后端 repository 必须实现 handleReport 处置流转并支持处分与驳回', () => {
const repoPath = path.join(serverSrcDir, 'db', 'repository.ts');
const content = fs.readFileSync(repoPath, 'utf-8');
expect(content).toContain('handleReport');
expect(content).toContain('handle_result');
expect(content).toContain('handle_note');
expect(content).toContain('handled_at');
});
it('后端 index.ts 必须暴露 POST /api/reports/:id/handle 路由', () => {
const indexPath = path.join(serverSrcDir, 'index.ts');
const content = fs.readFileSync(indexPath, 'utf-8');
expect(content).toContain("app.post('/api/reports/:id/handle'");
expect(content).toContain('repo.handleReport');
});
it('管理后台必须挂载 SafetyReports 路由且存在对应处理页面视图', () => {
const routePath = path.join(rootDir, 'admin', 'arco-design-pro-vite', 'src', 'router', 'routes', 'modules', 'safety.ts');
const routeContent = fs.readFileSync(routePath, 'utf-8');
expect(routeContent).toContain("path: 'reports'");
expect(routeContent).toContain("name: 'SafetyReports'");
const viewPath = path.join(rootDir, 'admin', 'arco-design-pro-vite', 'src', 'views', 'safety', 'reports', 'index.vue');
expect(fs.existsSync(viewPath)).toBe(true);
const viewContent = fs.readFileSync(viewPath, 'utf-8');
expect(viewContent).toContain('/api/reports');
expect(viewContent).toContain('handleAction');
});
it('小程序端 Toast 提示必须在 7 汉字以内以防被微信小程序方块图标截断', () => {
const orderListPath = path.join(uniPagesDir, 'order', 'list.vue');
const orderContent = fs.readFileSync(orderListPath, 'utf-8');
expect(orderContent).toContain("title: '举报提交成功'");
const detailPath = path.join(uniPagesDir, 'expert', 'detail.vue');
const detailContent = fs.readFileSync(detailPath, 'utf-8');
expect(detailContent).toContain("title: '举报提交成功'");
});
});
describe('2. 订单待支付倒计时与退款仲裁工单闭环验证', () => {
it('15分钟待支付倒计时计算逻辑应准确', () => {
const createdAt = new Date().toISOString();
const createdTime = new Date(createdAt).getTime();
const expireTime = createdTime + 15 * 60 * 1000;
const now = createdTime + 5 * 60 * 1000; // 过去了5分钟
const remainingSeconds = Math.max(0, Math.floor((expireTime - now) / 1000));
expect(remainingSeconds).toBe(10 * 60); // 剩余 600 秒
const minutes = Math.floor(remainingSeconds / 60);
const seconds = remainingSeconds % 60;
expect(`${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`).toBe('10:00');
});
it('后端 repository 必须实现 applyOrderRefund 并原子写入 arbitrations 仲裁表', () => {
const repoPath = path.join(serverSrcDir, 'db', 'repository.ts');
const content = fs.readFileSync(repoPath, 'utf-8');
expect(content).toContain('applyOrderRefund');
expect(content).toContain('争议仲裁中');
expect(content).toContain('arbitrations');
});
it('仲裁全额退款用户时,必须原子将订单金额退还买家钱包余额并记录入账流水', () => {
const repoPath = path.join(serverSrcDir, 'db', 'repository.ts');
const content = fs.readFileSync(repoPath, 'utf-8');
expect(content).toContain('UPDATE users SET balance = balance + ?');
expect(content).toContain('仲裁退款');
expect(content).toContain('finance_records');
});
it('提现申请被管理员驳回时,必须原子将预扣佣金原路退还用户佣金账户并记录冲正流水', () => {
const repoPath = path.join(serverSrcDir, 'db', 'repository.ts');
const content = fs.readFileSync(repoPath, 'utf-8');
expect(content).toContain('UPDATE users SET commission = commission + ?');
expect(content).toContain('提现驳回退还');
});
it('后端 repository 与管理后台必须支持平台管理员强制违规下架组局并原路退款搭子', () => {
const repoPath = path.join(serverSrcDir, 'db', 'repository.ts');
const content = fs.readFileSync(repoPath, 'utf-8');
expect(content).toContain('cancelActivity');
expect(content).toContain('isAdmin');
const adminActListPath = path.join(rootDir, 'admin', 'arco-design-pro-vite', 'src', 'views', 'activity', 'list', 'index.vue');
const adminActContent = fs.readFileSync(adminActListPath, 'utf-8');
expect(adminActContent).toContain('handleCancelActivity');
expect(adminActContent).toContain('违规下架');
});
it('后端 index.ts POST /api/orders/:id/refund 必须接收理由与说明并执行风控拦截', () => {
const indexPath = path.join(serverSrcDir, 'index.ts');
const content = fs.readFileSync(indexPath, 'utf-8');
expect(content).toContain("app.post('/api/orders/:id/refund'");
expect(content).toContain('reason');
expect(content).toContain('contentSecurityService.checkText');
expect(content).toContain('repo.applyOrderRefund');
});
});
describe('3. 个人资料性别与签名更新闭环验证', () => {
it('后端 repository 必须在 updateUserProfile 中支持 gender 与 signature 字段处理', () => {
const repoPath = path.join(serverSrcDir, 'db', 'repository.ts');
const content = fs.readFileSync(repoPath, 'utf-8');
expect(content).toContain('gender');
expect(content).toContain('signature');
expect(content).toContain('slice(0, 64)');
});
it('后端 index.ts POST /api/users/:id/profile 必须支持性别白名单与个性签名风控', () => {
const indexPath = path.join(serverSrcDir, 'index.ts');
const content = fs.readFileSync(indexPath, 'utf-8');
expect(content).toContain("app.post('/api/users/:id/profile'");
expect(content).toContain('signature');
expect(content).toContain('contentSecurityService.checkText');
});
it('后端 index.ts POST /api/users/:id/profile 必须校验手机号与微信号格式并拦截非法输入', () => {
const indexPath = path.join(serverSrcDir, 'index.ts');
const content = fs.readFileSync(indexPath, 'utf-8');
expect(content).toContain('1[3-9]');
expect(content).toContain('a-zA-Z');
expect(content).toContain('联系方式格式不正确');
});
it('前端 user/index.vue 必须集成联系方式格式校验与非法标点过滤', () => {
const userVuePath = path.join(uniPagesDir, 'user', 'index.vue');
const content = fs.readFileSync(userVuePath, 'utf-8');
expect(content).toContain('validateContact');
expect(content).toContain('onPhoneInput');
expect(content).toContain('field-label-row');
});
});
describe('4. 前端核心页面规范与结构完整性校验', () => {
it('个人中心页面 (user/index.vue) 必须包含个性签名展示与性别修改入口', () => {
const userVuePath = path.join(uniPagesDir, 'user', 'index.vue');
const content = fs.readFileSync(userVuePath, 'utf-8');
expect(content).toContain('user.signature');
expect(content).toContain('editForm.gender');
expect(content).toContain('editForm.signature');
expect(content).toContain('个性签名');
});
it('订单中心页面 (order/list.vue) 必须包含倒计时徽章、退款仲裁工单与举报入口', () => {
const orderVuePath = path.join(uniPagesDir, 'order', 'list.vue');
const content = fs.readFileSync(orderVuePath, 'utf-8');
expect(content).toContain('countdown-badge');
expect(content).toContain('getOrderCountdown');
expect(content).toContain('openRefundModal');
expect(content).toContain('openReportModal');
expect(content).toContain('refundForm');
expect(content).toContain('reportForm');
});
it('陪玩详情页面 (expert/detail.vue) 必须包含违规举报操作入口与弹窗', () => {
const detailVuePath = path.join(uniPagesDir, 'expert', 'detail.vue');
const content = fs.readFileSync(detailVuePath, 'utf-8');
expect(content).toContain('openReportModal');
expect(content).toContain('submitReportTicket');
expect(content).toContain('presetReportReasons');
expect(content).toContain('review-modal-mask');
});
});
describe('5. 我的收藏陪玩闭环验证', () => {
it('后端 repository 必须实现收藏陪玩管理方法 (getFavorites, addFavorite, removeFavorite, isFavorite)', () => {
const repoPath = path.join(serverSrcDir, 'db', 'repository.ts');
const content = fs.readFileSync(repoPath, 'utf-8');
expect(content).toContain('getFavorites');
expect(content).toContain('addFavorite');
expect(content).toContain('removeFavorite');
expect(content).toContain('isFavorite');
expect(content).toContain('favorites');
});
it('后端 index.ts 必须暴露收藏相关 API 路由', () => {
const indexPath = path.join(serverSrcDir, 'index.ts');
const content = fs.readFileSync(indexPath, 'utf-8');
expect(content).toContain("app.get('/api/favorites'");
expect(content).toContain("app.post('/api/favorites'");
expect(content).toContain("app.delete('/api/favorites/:expertId'");
expect(content).toContain("app.get('/api/favorites/check'");
});
it('陪玩详情页、个人中心与独立收藏页面必须实现收藏状态联动与列表渲染', () => {
const detailVuePath = path.join(uniPagesDir, 'expert', 'detail.vue');
const detailContent = fs.readFileSync(detailVuePath, 'utf-8');
expect(detailContent).toContain('toggleFavorite');
expect(detailContent).toContain('isFav');
// 个人中心点击我的收藏直接跳转独立页面
const userVuePath = path.join(uniPagesDir, 'user', 'index.vue');
const userContent = fs.readFileSync(userVuePath, 'utf-8');
expect(userContent).toContain('openFavoritesModal');
expect(userContent).toContain('favoriteList');
expect(userContent).toContain('/pages/user/favorites');
// 独立收藏页面完整闭环与能力
const favVuePath = path.join(uniPagesDir, 'user', 'favorites.vue');
expect(fs.existsSync(favVuePath)).toBe(true);
const favContent = fs.readFileSync(favVuePath, 'utf-8');
expect(favContent).toContain('removeFavorite');
expect(favContent).toContain('handleCancelFavorite');
expect(favContent).toContain('filterOnlyOnline');
expect(favContent).toContain('isOnline');
});
});
describe('6. 系统与隐私设置闭环验证', () => {
it('个人中心与独立设置页面必须实现手机号脱敏计算、服务通知、清理缓存与版本合规', () => {
// 个人中心验证:包含脱敏计算与跳转独立设置页
const userVuePath = path.join(uniPagesDir, 'user', 'index.vue');
const userContent = fs.readFileSync(userVuePath, 'utf-8');
expect(userContent).toContain('openSettingsModal');
expect(userContent).toContain('privacyHidePhone');
expect(userContent).toContain('displayPhone');
expect(userContent).toContain('clearLocalCache');
expect(userContent).toContain('cacheSizeText');
expect(userContent).toContain('/pages/user/settings');
// 独立系统与隐私设置页面规格验证
const settingsVuePath = path.join(uniPagesDir, 'user', 'settings.vue');
expect(fs.existsSync(settingsVuePath)).toBe(true);
const settingsContent = fs.readFileSync(settingsVuePath, 'utf-8');
expect(settingsContent).toContain('privacyHidePhone');
expect(settingsContent).toContain('onToggleHidePhone');
expect(settingsContent).toContain('noticeSubscribe');
expect(settingsContent).toContain('onToggleNotice');
expect(settingsContent).toContain('clearLocalCache');
expect(settingsContent).toContain('cacheSizeText');
expect(settingsContent).toContain('v2.6.0 正式版');
expect(settingsContent).toContain('goToProtocol');
});
});
describe('7. 分销推广管理闭环验证', () => {
it('后端 repository 必须实现下级团队与返佣订单流水方法', () => {
const repoPath = path.join(serverSrcDir, 'db', 'repository.ts');
const content = fs.readFileSync(repoPath, 'utf-8');
expect(content).toContain('getDistributionTeam');
expect(content).toContain('getDistributionRecords');
expect(content).toContain('distribution_records');
});
it('后端 index.ts 必须暴露分销团队与返佣订单路由', () => {
const indexPath = path.join(serverSrcDir, 'index.ts');
const content = fs.readFileSync(indexPath, 'utf-8');
expect(content).toContain("app.get('/api/distribution/team'");
expect(content).toContain("app.get('/api/distribution/records'");
});
it('个人中心页面必须实现专属海报、团队列表、返佣订单与佣金提现看板', () => {
const userVuePath = path.join(uniPagesDir, 'user', 'index.vue');
const content = fs.readFileSync(userVuePath, 'utf-8');
expect(content).toContain('openDistributionModal');
expect(content).toContain('distActiveTab');
expect(content).toContain('distributionTeam');
expect(content).toContain('distributionRecords');
expect(content).toContain('submitDistWithdraw');
expect(content).toContain('suncode-box');
});
});
describe('8. 首页搜索、多维复合筛选与雷达即时速配闭环验证', () => {
it('首页页面 (index/index.vue) 必须包含搜索框、热搜推荐词与多维复合筛选工具栏', () => {
const indexVuePath = path.join(uniPagesDir, 'index', 'index.vue');
const content = fs.readFileSync(indexVuePath, 'utf-8');
expect(content).toContain('searchKeyword');
expect(content).toContain('hotSearchWords');
expect(content).toContain('filterOnlyOnline');
expect(content).toContain('filterGender');
expect(content).toContain('filterSort');
expect(content).toContain('filterPriceRange');
expect(content).toContain('priceOptions');
});
it('首页页面必须实现雷达即时派单速配与搜索无结果重置能力', () => {
const indexVuePath = path.join(uniPagesDir, 'index', 'index.vue');
const content = fs.readFileSync(indexVuePath, 'utf-8');
expect(content).toContain('startInstantMatch');
expect(content).toContain('matchedExpert');
expect(content).toContain('radar-scan-box');
expect(content).toContain('empty-search-box');
expect(content).toContain('resetAllFilters');
});
it('即时匹配必须实时拉取大神、只匹配真实注册且接单中的大神,并支持换一位', () => {
const indexVuePath = path.join(uniPagesDir, 'index', 'index.vue');
const content = fs.readFileSync(indexVuePath, 'utf-8');
const apiContent = fs.readFileSync(path.join(rootDir, 'pwuniapp', 'common', 'api.js'), 'utf-8');
expect(content).toContain('matchCompanions({ ...(pos || {}), radius: 5, limit: 20 })');
expect(content).toContain('lastMatchedId');
expect(content).toContain('matchedMeta');
expect(content).toContain('换一位');
expect(content).toContain('在线大神即时匹配');
expect(content).not.toContain('首位接单大神命中');
expect(apiContent).toContain('userId: Number(c.userId) || 0');
expect(apiContent).toContain('export async function matchCompanions');
});
});
describe('9. 大神认证审核双向联动、服务评价监管与友圈搭子上车业务完整闭环', () => {
it('审核大神入驻通过时,必须联动更新 users 表 role 为 "认证大神" 与 is_expert = 1', () => {
const repoPath = path.join(serverSrcDir, 'db', 'repository.ts');
const content = fs.readFileSync(repoPath, 'utf-8');
expect(content).toContain('auditCompanion');
expect(content).toContain('UPDATE users SET role = "认证大神", is_expert = 1');
expect(content).toContain('UPDATE users SET role = "普通用户", is_expert = 0');
});
it('后端必须提供评价与回复监管接口 (GET /api/reviews 与 DELETE /api/reviews/:id)', () => {
const indexPath = path.join(serverSrcDir, 'index.ts');
const content = fs.readFileSync(indexPath, 'utf-8');
expect(content).toContain("app.get('/api/reviews'");
expect(content).toContain("app.delete('/api/reviews/:id'");
});
it('管理后台路由与菜单中必须挂载服务评价与回复监管模块', () => {
const routePath = path.join(rootDir, 'admin', 'arco-design-pro-vite', 'src', 'router', 'routes', 'modules', 'community.ts');
const content = fs.readFileSync(routePath, 'utf-8');
expect(content).toContain('review-manage');
expect(content).toContain('menu.community.reviewManage');
});
it('后端必须提供搭子上车申请查询与审核处置接口', () => {
const indexPath = path.join(serverSrcDir, 'index.ts');
const content = fs.readFileSync(indexPath, 'utf-8');
expect(content).toContain("app.get('/api/posts/:id/applies'");
expect(content).toContain("app.get('/api/post-applies'");
expect(content).toContain("app.post('/api/post-applies/:id/handle'");
});
it('管理后台动态管理必须支持查看搭子上车申请并执行同意/婉拒处置', () => {
const postManagePath = path.join(rootDir, 'admin', 'arco-design-pro-vite', 'src', 'views', 'community', 'post-manage', 'index.vue');
const content = fs.readFileSync(postManagePath, 'utf-8');
expect(content).toContain('openAppliesDrawer');
expect(content).toContain('appliesDrawerVisible');
expect(content).toContain('handleApplyAction');
expect(content).toContain('/api/post-applies/');
});
});
describe('10. 全链路跨端业务闭环深度校验 (实名审核回流/完单结算+二级返佣/争议结案/搭子审批/举报反馈/大神ID双向解析)', () => {
it('大神认证申请与个人中心必须实时同步后端 certStatus 与 rejectReason,支持驳回后查看原因并重新提审', () => {
const applyVuePath = path.join(uniPagesDir, 'expert', 'apply.vue');
const applyContent = fs.readFileSync(applyVuePath, 'utf-8');
expect(applyContent).toContain('syncRemoteCertStatus');
expect(applyContent).toContain('rejectReason');
const userIndexPath = path.join(uniPagesDir, 'user', 'index.vue');
const userContent = fs.readFileSync(userIndexPath, 'utf-8');
expect(userContent).toContain('setExpertCertStatus');
expect(userContent).toContain('rejectReason');
});
it('订单确认完成或评价时,后端必须触发 completeOrderWithSettlement 自动结算大神佣金与一二级分销返佣', () => {
const repoPath = path.join(serverSrcDir, 'db', 'repository.ts');
const content = fs.readFileSync(repoPath, 'utf-8');
expect(content).toContain('completeOrderWithSettlement');
expect(content).toContain('大神完单收益结算');
expect(content).toContain('INSERT INTO distribution_records');
});
it('争议仲裁驳回退款或扣信用分时,必须同步将关联订单从争议仲裁中流转为已完成并触发结算', () => {
const repoPath = path.join(serverSrcDir, 'db', 'repository.ts');
const content = fs.readFileSync(repoPath, 'utf-8');
expect(content).toContain('handleArbitration');
expect(content).toContain("SET status = '已完成'");
expect(content).toContain('this.completeOrderWithSettlement(shouldSettleOrderId)');
});
it('友圈动态必须返回搭子上车申请状态 myApplyStatus,且小程序端支持房主直接审批申请与申请人查看状态', () => {
const communityVuePath = path.join(uniPagesDir, 'community', 'index.vue');
const content = fs.readFileSync(communityVuePath, 'utf-8');
expect(content).toContain('handleManageApplies');
expect(content).toContain('getPostApplies');
expect(content).toContain('handlePostApply');
expect(content).toContain('myApplyStatus');
});
it('用户提交举报投诉后,个人中心必须提供“我的举报与维权反馈”查询入口,且管理后台工作台联动待办提醒', () => {
const userIndexPath = path.join(uniPagesDir, 'user', 'index.vue');
const userContent = fs.readFileSync(userIndexPath, 'utf-8');
expect(userContent).toContain('openMyReportsModal');
expect(userContent).toContain('getMyReports');
expect(userContent).toContain('我的举报与维权反馈');
const todoListPath = path.join(rootDir, 'admin', 'arco-design-pro-vite', 'src', 'views', 'dashboard', 'workplace', 'components', 'quick-todo-list.vue');
const todoContent = fs.readFileSync(todoListPath, 'utf-8');
expect(todoContent).toContain('pendingReports');
expect(todoContent).toContain('/safety/reports');
});
it('分销推广中心必须支持绑定上级邀请码 (POST /api/distribution/bind) 并实时汇总下级贡献佣金', () => {
const indexPath = path.join(serverSrcDir, 'index.ts');
const indexContent = fs.readFileSync(indexPath, 'utf-8');
expect(indexContent).toContain("app.post('/api/distribution/bind'");
const promoteVuePath = path.join(uniPagesDir, 'user', 'promote.vue');
const promoteContent = fs.readFileSync(promoteVuePath, 'utf-8');
expect(promoteContent).toContain('handleBindInviteCode');
expect(promoteContent).toContain('bindInviteCode');
});
it('大神 ID 必须支持 1..6 与 101..106 双向解析 (resolveCompanionRow),确保收藏列表、评价、资料修改与在线状态切换全链路一致', () => {
const repoPath = path.join(serverSrcDir, 'db', 'repository.ts');
const content = fs.readFileSync(repoPath, 'utf-8');
expect(content).toContain('resolveCompanionRow');
const hallVuePath = path.join(uniPagesDir, 'expert', 'hall.vue');
const hallContent = fs.readFileSync(hallVuePath, 'utf-8');
expect(hallContent).toContain('updateCompanionProfile');
const workspaceVuePath = path.join(uniPagesDir, 'expert', 'workspace.vue');
const wsContent = fs.readFileSync(workspaceVuePath, 'utf-8');
expect(wsContent).toContain('updateCompanionProfile');
expect(wsContent).toContain('completeOrderApi');
});
it('3.1 活动 4 位核销码必须执行 CAS 原子防重核销,并在核销后触发发起人费用清算与付费活动二级分润', () => {
const repoPath = path.join(serverSrcDir, 'db', 'repository.ts');
const content = fs.readFileSync(repoPath, 'utf-8');
expect(content).toContain('WHERE id = ? AND checkin_status = 0');
expect(content).toContain('settleActivityCheckinAndReferral');
expect(content).toContain('活动核销费用清算');
});
it('3.2 双身份架构必须维护 role_flags (1:玩家, 2:大神, 3:双身份) 并在抢单接口拦截未通过实名认证的大神档案', () => {
const mysqlPath = path.join(serverSrcDir, 'db', 'mysql.ts');
const mysqlContent = fs.readFileSync(mysqlPath, 'utf-8');
expect(mysqlContent).toContain('role_flags');
const repoPath = path.join(serverSrcDir, 'db', 'repository.ts');
const repoContent = fs.readFileSync(repoPath, 'utf-8');
expect(repoContent).toContain('roleFlags');
expect(repoContent).toContain('大神档案未通过实名认证审核,无法接单');
});
it('3.3 分销裂变必须建立 distribution_relations 父子终身关联表,并支持小程序扫码/带参进入自动锁粉绑定', () => {
const mysqlPath = path.join(serverSrcDir, 'db', 'mysql.ts');
const mysqlContent = fs.readFileSync(mysqlPath, 'utf-8');
expect(mysqlContent).toContain('CREATE TABLE IF NOT EXISTS distribution_relations');
const repoPath = path.join(serverSrcDir, 'db', 'repository.ts');
const repoContent = fs.readFileSync(repoPath, 'utf-8');
expect(repoContent).toContain('INSERT INTO distribution_relations');
expect(repoContent).toContain('当前账号已终身绑定上级推荐人,不可重复修改绑定');
const indexVuePath = path.join(uniPagesDir, 'index', 'index.vue');
const indexContent = fs.readFileSync(indexVuePath, 'utf-8');
expect(indexContent).toContain('PENDING_INVITER_CODE');
});
it('3.4 已封禁用户核心业务防线:后端仓储与路由必须全面拦截发单、报名、下单与动态评论', () => {
const serverIndexPath = path.join(serverSrcDir, 'index.ts');
const indexContent = fs.readFileSync(serverIndexPath, 'utf-8');
expect(indexContent).toContain('assertUserNotBanned');
expect(indexContent).toContain('您的账号已被管理员封禁');
const repoPath = path.join(serverSrcDir, 'db', 'repository.ts');
const repoContent = fs.readFileSync(repoPath, 'utf-8');
expect(repoContent).toContain("(u.status IS NULL OR u.status != '已封禁')");
expect(repoContent).toContain("UPDATE activities SET status = 'CANCELLED' WHERE creator_id = ? AND status = 'RECRUITING'");
const rolePath = path.join(rootDir, 'pwuniapp', 'common', 'role.js');
const roleContent = fs.readFileSync(rolePath, 'utf-8');
expect(roleContent).toContain('isUserBanned');
expect(roleContent).toContain("uInfo.status === '已封禁'");
});
});
});