// 已读取AGENTS.md import * as fs from 'fs'; import * as path from 'path'; /** * 大神 LBS 即时匹配与接单心跳闸门: * - companions 必须持久化最近坐标与 last_active_at; * - 匹配只取真实注册、审核通过、接单中、10 分钟内有心跳的大神; * - 带用户坐标时按 Haversine 距离过滤 5km 并升序; * - 大神页前台必须持续心跳,首页即时匹配必须走服务端匹配接口。 */ describe('大神 LBS 即时匹配与接单心跳', () => { const root = path.resolve(__dirname, '../../'); const read = (rel: string) => fs.readFileSync(path.join(root, rel), 'utf-8'); const mysql = read('server/src/db/mysql.ts'); const repo = read('server/src/db/repository.ts'); const indexCode = read('server/src/index.ts'); const api = read('pwuniapp/common/api.js'); const heartbeat = read('pwuniapp/common/expert-heartbeat.js'); const home = read('pwuniapp/pages/index/index.vue'); const workspace = read('pwuniapp/pages/expert/workspace.vue'); const hall = read('pwuniapp/pages/expert/hall.vue'); const radar = read('pwuniapp/pages/expert/radar.vue'); it('companions 表必须有坐标、最后活跃时间与匹配索引', () => { expect(mysql).toContain('latitude DECIMAL(10,7) DEFAULT NULL'); expect(mysql).toContain('longitude DECIMAL(10,7) DEFAULT NULL'); expect(mysql).toContain('last_active_at DATETIME DEFAULT NULL'); expect(mysql).toContain("ALTER TABLE companions ADD COLUMN latitude"); expect(mysql).toContain("['companions', 'idx_comp_active', 'status, last_active_at']"); }); it('匹配仓储必须只取真实注册、审核通过、接单中且 10 分钟内有心跳的大神', () => { expect(repo).toContain('async matchCompanions('); expect(repo).toContain('user_id IS NOT NULL AND user_id > 0'); expect(repo).toContain("audit_status = '已通过'"); expect(repo).toContain("status = '接单中'"); expect(repo).toContain('last_active_at >= DATE_SUB(NOW(), INTERVAL 10 MINUTE)'); expect(repo).toContain('haversineKm(fromLat, fromLng'); expect(repo).toContain('distanceText'); expect(repo).toContain('async updateCompanionHeartbeat('); // 接单中切换时立即刷新活跃时间 expect(repo).toContain("fields.push('last_active_at = NOW()')"); }); it('匹配接口必须在详情路由之前注册,心跳接口必须鉴权', () => { const matchIdx = indexCode.indexOf("app.get('/api/companions/match'"); const detailIdx = indexCode.indexOf("app.get('/api/companions/:id'"); expect(matchIdx).toBeGreaterThan(-1); expect(detailIdx).toBeGreaterThan(matchIdx); expect(indexCode).toContain("app.post('/api/companions/heartbeat'"); expect(indexCode).toContain("'大神接单心跳'"); expect(indexCode).toContain('repo.matchCompanions({'); expect(indexCode).toContain('repo.updateCompanionHeartbeat('); }); it('客户端必须提供匹配与心跳 API,且不返回精确坐标', () => { expect(api).toContain('export async function matchCompanions'); expect(api).toContain('export async function sendCompanionHeartbeat'); expect(api).toContain('/companions/match?'); expect(api).toContain("http.post('/companions/heartbeat'"); expect(api).toContain('distanceText: c.distanceText'); }); it('大神工作台/大厅/雷达前台必须启动心跳,离开时停止', () => { expect(heartbeat).toContain('export function startHeartbeat'); expect(heartbeat).toContain('export function stopHeartbeat'); expect(heartbeat).toContain('sendCompanionHeartbeat'); for (const page of [workspace, hall, radar]) { expect(page).toContain('startHeartbeat()'); expect(page).toContain('stopHeartbeat()'); } }); it('首页即时匹配必须调用服务端匹配接口并展示真实距离', () => { expect(home).toContain('matchCompanions({ ...(pos || {}), radius: 5, limit: 20 })'); expect(home).toContain('getUserLocation'); expect(home).toContain('distanceText'); expect(home).toContain('暂无可匹配的接单大神'); }); });