feat(lbs): 真实距离与按距离派单落地
- 数据库:orders/activities 新增 latitude/longitude(含存量表 ALTER 迁移) - 服务端:Haversine 计算 distanceKm/distanceText,派单池支持 lat/lng/radius 过滤并按距离升序;历史无坐标订单用腾讯地理编码兜底并回写缓存 - 新增 /api/lbs/reverse 逆地址解析(Key 仅服务端持有,登录 + 限流) - 小程序:雷达/接单大厅改用真实定位与真实距离,删除全部写死演示距离;下单页新增地图选点,组局页保存选点经纬度 - 修复接单大厅点接单会新建订单的问题,改为真正 claimOrder 认领 - 新增 lbs-distance 护栏(110 套件/729 用例全绿)
这个提交包含在:
@@ -0,0 +1,81 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第九十四批:真实距离(LBS)落地闸门。
|
||||
* 背景:雷达页/接单大厅的「650m / 1.4km / 1.2km内」全部是写死演示值,
|
||||
* orders/activities 也没有经纬度字段,导致「按距离派单」实际不生效。
|
||||
*/
|
||||
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 apiCode = read('pwuniapp/common/api.js');
|
||||
const radar = read('pwuniapp/pages/expert/radar.vue');
|
||||
const hall = read('pwuniapp/pages/expert/hall.vue');
|
||||
const orderCreate = read('pwuniapp/pages/order/create.vue');
|
||||
const activityCreate = read('pwuniapp/pages/activity/create.vue');
|
||||
|
||||
it('订单与组局表必须存储经纬度', () => {
|
||||
expect(mysql).toContain('latitude DECIMAL(10,6)');
|
||||
expect(mysql).toContain('longitude DECIMAL(10,6)');
|
||||
expect(mysql).toContain('ALTER TABLE activities ADD COLUMN latitude');
|
||||
expect(mysql).toContain('ALTER TABLE orders ADD COLUMN latitude');
|
||||
});
|
||||
|
||||
it('服务端按 Haversine 计算距离并返回距离文案', () => {
|
||||
expect(repo).toContain('export function haversineKm');
|
||||
expect(repo).toContain('export function formatDistanceText');
|
||||
expect(repo).toContain('distanceText');
|
||||
expect(repo).toContain('radiusKm');
|
||||
expect(repo).toContain('async updateOrderGeo');
|
||||
});
|
||||
|
||||
it('派单池接口接收定位并按半径过滤,历史地址走腾讯地理编码兜底', () => {
|
||||
const start = indexCode.indexOf("app.get('/api/orders/pool'");
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
const block = indexCode.slice(start, start + 2200);
|
||||
expect(block).toContain('req.query.lat');
|
||||
expect(block).toContain('req.query.radius');
|
||||
expect(block).toContain('geocodeAddress');
|
||||
expect(block).toContain('repo.updateOrderGeo');
|
||||
});
|
||||
|
||||
it('逆地址解析接口仅服务端持有 Key,并要求登录', () => {
|
||||
expect(indexCode).toContain("app.get('/api/lbs/reverse'");
|
||||
expect(indexCode).toContain('resolveTencentMapKey');
|
||||
expect(indexCode).toContain("resolveSecureUserId(req, Number(req.query.userId || 0), '解析当前位置')");
|
||||
// Key 不允许出现在小程序端
|
||||
expect(apiCode).not.toContain('7I4BZ');
|
||||
});
|
||||
|
||||
it('小程序雷达与接单大厅改用真实距离,删除写死演示距离', () => {
|
||||
expect(radar).toContain('getUserLocation');
|
||||
expect(radar).toContain('p.distanceText');
|
||||
expect(radar).not.toContain("distance: '650m'");
|
||||
expect(radar).not.toContain("dist: '1.4km'");
|
||||
|
||||
expect(hall).toContain('getUserLocation');
|
||||
expect(hall).toContain('p.distanceText');
|
||||
expect(hall).not.toContain("'1.2km内'");
|
||||
expect(hall).not.toContain("'1.1km'");
|
||||
});
|
||||
|
||||
it('下单/组局在选点后提交经纬度', () => {
|
||||
expect(orderCreate).toContain('pickVenue');
|
||||
expect(orderCreate).toContain('uni.chooseLocation');
|
||||
expect(orderCreate).toContain('latitude: isOffline.value ? meetCoords.value.latitude : null');
|
||||
expect(activityCreate).toContain('form.value.latitude');
|
||||
expect(apiCode).toContain('latitude: payload.latitude');
|
||||
});
|
||||
|
||||
it('小程序的派单池调用支持携带定位与半径', () => {
|
||||
expect(apiCode).toContain('export async function getOrdersPool(options = {})');
|
||||
expect(apiCode).toContain('qs.push(`lat=${lat}`, `lng=${lng}`)');
|
||||
expect(apiCode).toContain('radius=${radius}');
|
||||
});
|
||||
});
|
||||
@@ -12,9 +12,11 @@ describe('第二十九批抢单池过滤闸门', () => {
|
||||
it('抢单池必须排除终态/已核销订单并过滤已认领订单', () => {
|
||||
const start = repoCode.indexOf('async getOrdersPool(');
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
const block = repoCode.slice(start, start + 1400);
|
||||
const block = repoCode.slice(start, start + 5200);
|
||||
expect(block).toContain("WHERE status = '待接单'");
|
||||
expect(block).toContain('claimed_by IS NULL');
|
||||
expect(block).toContain('LIMIT 20');
|
||||
// SQL 多取候选,最终按距离排序后截断 20 条
|
||||
expect(block).toContain('LIMIT 50');
|
||||
expect(block).toContain('slice(0, 20)');
|
||||
});
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户