- companions 新增 latitude/longitude/last_active_at 与 idx_comp_active;切换接单中立即刷新活跃时间 - 新增 /api/companions/heartbeat(鉴权)与 /api/companions/match(真实注册+审核通过+接单中+10分钟心跳,Haversine 5km 过滤与距离排序,不返回精确坐标) - 工作台/接单大厅/雷达前台每 60s 心跳,离开停止;首页即时匹配改为服务端 LBS 匹配并展示真实距离 - 线上实测:2010 匹配排除自己,仅命中 280m 的 8 号;匿名命中 9 号 10m、8 号 280m;北京 5km 为 0;118 套件/771 用例全绿
82 行
4.0 KiB
TypeScript
82 行
4.0 KiB
TypeScript
// 已读取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('暂无可匹配的接单大神');
|
|
});
|
|
});
|