feat(M03-C): 完成地图选店与距离排序
This commit is contained in:
@@ -33,6 +33,9 @@ const storeRoomVerifySql = read('database/migrations/2026061807_m03a_store_room_
|
||||
const contentUpSql = read('database/migrations/2026061808_m03b_decoration_ads_media.up.sql');
|
||||
const contentDownSql = read('database/migrations/2026061808_m03b_decoration_ads_media.down.sql');
|
||||
const contentVerifySql = read('database/migrations/2026061808_m03b_decoration_ads_media.verify.sql');
|
||||
const discoveryUpSql = read('database/migrations/2026061809_m03c_store_discovery.up.sql');
|
||||
const discoveryDownSql = read('database/migrations/2026061809_m03c_store_discovery.down.sql');
|
||||
const discoveryVerifySql = read('database/migrations/2026061809_m03c_store_discovery.verify.sql');
|
||||
|
||||
const coreTables = [
|
||||
'qipai_schema_migrations',
|
||||
@@ -160,5 +163,12 @@ for (const table of [
|
||||
assert.match(contentUpSql, /schema_version INT UNSIGNED/);
|
||||
assert.match(contentUpSql, /scope_type VARCHAR/);
|
||||
assert.match(contentUpSql, /checksum_sha256 CHAR\(64\)/);
|
||||
for (const column of ['city', 'district']) {
|
||||
assert.match(discoveryUpSql, new RegExp(`ADD COLUMN ${column}`));
|
||||
assert.match(discoveryDownSql, new RegExp(`DROP COLUMN ${column}`));
|
||||
assert.match(discoveryVerifySql, new RegExp(`'${column}'`));
|
||||
}
|
||||
assert.match(discoveryUpSql, /idx_qipai_stores_tenant_city_status/);
|
||||
assert.match(discoveryUpSql, /idx_qipai_stores_tenant_coordinates/);
|
||||
|
||||
console.log('PASS: M01-B through M03-B migration contracts are present.');
|
||||
console.log('PASS: M01-B through M03-C migration contracts are present.');
|
||||
|
||||
@@ -19,7 +19,8 @@ assert.match(plan.file, /2026061804_m02b_wechat_auth\.up\.sql/);
|
||||
assert.match(plan.file, /2026061805_m02c_rbac\.up\.sql/);
|
||||
assert.match(plan.file, /2026061806_m02d_user_management\.up\.sql/);
|
||||
assert.match(plan.file, /2026061807_m03a_store_room_domain\.up\.sql/);
|
||||
assert.match(plan.file, /2026061808_m03b_decoration_ads_media\.up\.sql$/);
|
||||
assert.match(plan.file, /2026061808_m03b_decoration_ads_media\.up\.sql/);
|
||||
assert.match(plan.file, /2026061809_m03c_store_discovery\.up\.sql$/);
|
||||
assert.match(plan.checksum, /^[a-f0-9]{64}$/);
|
||||
assert.ok(plan.statements.length >= 11);
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import { RbacRepository } from '../dist/auth/rbac-repository.js';
|
||||
import { UserManagementRepository } from '../dist/auth/user-management-repository.js';
|
||||
import { StoreRoomRepository, StoreRoomError } from '../dist/stores/store-room-repository.js';
|
||||
import { ContentRepository, ContentError } from '../dist/content/content-repository.js';
|
||||
import { StoreDiscoveryRepository } from '../dist/stores/store-discovery-repository.js';
|
||||
import {
|
||||
executeMigrationPlan,
|
||||
loadMigrationPlan,
|
||||
@@ -72,10 +73,10 @@ async function readMigrationVersions(pool) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT version, name
|
||||
FROM qipai_schema_migrations
|
||||
WHERE version IN (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
WHERE version IN (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ORDER BY version`,
|
||||
['2026061601', '2026061802', '2026061803', '2026061804',
|
||||
'2026061805', '2026061806', '2026061807', '2026061808']
|
||||
'2026061805', '2026061806', '2026061807', '2026061808', '2026061809']
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
@@ -341,7 +342,7 @@ async function assertStoreRoomDomain(pool, context) {
|
||||
traceId: 'm03a-live-test', ip: '127.0.0.1', userAgent: 'M03-A live test'
|
||||
};
|
||||
const store = await repository.createStore(actor, {
|
||||
name: 'M03A Store', address: 'Sanitized address',
|
||||
name: 'M03A Store', address: 'Sanitized address', city: '上海市', district: '黄浦区',
|
||||
longitude: 121.4737, latitude: 31.2304, contactPhone: '13800000003',
|
||||
timezone: 'Asia/Shanghai', businessStatus: 'OPEN',
|
||||
wifiSsid: 'M03A-WIFI', wifiPassword: 'sanitized-password',
|
||||
@@ -393,6 +394,37 @@ async function assertStoreRoomDomain(pool, context) {
|
||||
]);
|
||||
}
|
||||
|
||||
async function assertStoreDiscovery(pool, context) {
|
||||
const [secondStore] = await pool.query(
|
||||
`INSERT INTO qipai_stores
|
||||
(tenant_id, name, address, city, district, longitude, latitude, business_status, sort_order)
|
||||
VALUES (?, 'M03C Far Store', 'Sanitized address B', '上海市', '浦东新区',
|
||||
121.6000000, 31.3000000, 'OPEN', 2)`,
|
||||
[context.tenantId]
|
||||
);
|
||||
await pool.query(
|
||||
`INSERT INTO qipai_store_business_hours
|
||||
(tenant_id, store_id, weekday, open_minute, close_minute, is_closed)
|
||||
VALUES (?, ?, 4, 0, 0, 0)`,
|
||||
[context.tenantId, secondStore.insertId]
|
||||
);
|
||||
const repository = new StoreDiscoveryRepository(pool);
|
||||
const stores = await repository.findStores({
|
||||
tenantId: context.tenantId,
|
||||
city: '上海市',
|
||||
latitude: 31.2304,
|
||||
longitude: 121.4737,
|
||||
now: new Date('2026-06-18T04:00:00.000Z')
|
||||
});
|
||||
assert.equal(stores[0].name, 'M03A Store');
|
||||
assert.equal(stores[0].distanceMeters, 0);
|
||||
assert.ok(stores[1].distanceMeters > stores[0].distanceMeters);
|
||||
assert.equal(stores.every((store) => store.city === '上海市'), true);
|
||||
assert.equal((await repository.findStores({
|
||||
tenantId: context.tenantId, city: '不存在的城市'
|
||||
})).length, 0);
|
||||
}
|
||||
|
||||
async function assertContentManagement(pool, context) {
|
||||
const [adminRows] = await pool.query(
|
||||
`SELECT u.id FROM qipai_users u
|
||||
@@ -488,7 +520,8 @@ try {
|
||||
{ version: '2026061805', name: 'm02c_rbac' },
|
||||
{ version: '2026061806', name: 'm02d_user_management' },
|
||||
{ version: '2026061807', name: 'm03a_store_room_domain' },
|
||||
{ version: '2026061808', name: 'm03b_decoration_ads_media' }
|
||||
{ version: '2026061808', name: 'm03b_decoration_ads_media' },
|
||||
{ version: '2026061809', name: 'm03c_store_discovery' }
|
||||
]);
|
||||
await assertTaskDurability(pool);
|
||||
const loginContext = await assertPlatformTenantIsolation(pool);
|
||||
@@ -496,6 +529,7 @@ try {
|
||||
await assertUserManagement(pool, loginContext);
|
||||
await assertStoreRoomDomain(pool, loginContext);
|
||||
await assertContentManagement(pool, loginContext);
|
||||
await assertStoreDiscovery(pool, loginContext);
|
||||
await assertLegacyCompatibility(pool);
|
||||
console.log('PASS: first up, verify, tenant isolation and revocable auth checks completed.');
|
||||
|
||||
@@ -515,7 +549,8 @@ try {
|
||||
{ version: '2026061805', name: 'm02c_rbac' },
|
||||
{ version: '2026061806', name: 'm02d_user_management' },
|
||||
{ version: '2026061807', name: 'm03a_store_room_domain' },
|
||||
{ version: '2026061808', name: 'm03b_decoration_ads_media' }
|
||||
{ version: '2026061808', name: 'm03b_decoration_ads_media' },
|
||||
{ version: '2026061809', name: 'm03c_store_discovery' }
|
||||
]);
|
||||
await assertLegacyCompatibility(pool);
|
||||
console.log('PASS: second up and verify restored the schema.');
|
||||
@@ -560,7 +595,10 @@ try {
|
||||
'tenant-isolated media asset',
|
||||
'versioned decoration publish and archive',
|
||||
'store advertisement delivery scope',
|
||||
'platform advertisement rejection'
|
||||
'platform advertisement rejection',
|
||||
'city fallback store filtering',
|
||||
'server-side distance sorting',
|
||||
'empty manual city result'
|
||||
]
|
||||
}, null, 2));
|
||||
} finally {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { buildApp } from '../dist/app.js';
|
||||
import {
|
||||
haversineMeters,
|
||||
StoreDiscoveryRepository
|
||||
} from '../dist/stores/store-discovery-repository.js';
|
||||
|
||||
assert.ok(haversineMeters(31.2304, 121.4737, 31.2304, 121.4737) < 1);
|
||||
assert.ok(haversineMeters(31.2304, 121.4737, 31.2200, 121.4800) > 1000);
|
||||
|
||||
const rows = [
|
||||
{
|
||||
id: 11, name: '近店', address: 'A', city: '上海市', district: '黄浦区',
|
||||
longitude: '121.4737', latitude: '31.2304', contactPhone: '',
|
||||
timezone: 'Asia/Shanghai', businessStatus: 'OPEN',
|
||||
weekday: 4, openMinute: 0, closeMinute: 0, isClosed: 0, sortOrder: 1
|
||||
},
|
||||
{
|
||||
id: 12, name: '远店', address: 'B', city: '上海市', district: '浦东新区',
|
||||
longitude: '121.6000', latitude: '31.3000', contactPhone: '',
|
||||
timezone: 'Asia/Shanghai', businessStatus: 'OPEN',
|
||||
weekday: 4, openMinute: 0, closeMinute: 0, isClosed: 0, sortOrder: 2
|
||||
}
|
||||
];
|
||||
const repository = new StoreDiscoveryRepository({
|
||||
async execute(sql, params) {
|
||||
assert.match(sql, /s\.tenant_id = \?/);
|
||||
assert.deepEqual(params, ['7', '上海市']);
|
||||
return [rows, []];
|
||||
}
|
||||
});
|
||||
const stores = await repository.findStores({
|
||||
tenantId: '7', city: '上海市', latitude: 31.2304, longitude: 121.4737,
|
||||
now: new Date('2026-06-18T04:00:00.000Z')
|
||||
});
|
||||
assert.deepEqual(stores.map((store) => store.id), ['11', '12']);
|
||||
assert.equal(stores[0].distanceMeters, 0);
|
||||
assert.equal(stores[0].openNow, true);
|
||||
|
||||
const app = await buildApp({
|
||||
storeDiscovery: {
|
||||
tenancy: {
|
||||
async resolveLoginContext(appId, tenantId) {
|
||||
assert.equal(appId, 'wx-test-app');
|
||||
assert.equal(tenantId, '7');
|
||||
return { tenantId: '7', platformAppId: '9', appId };
|
||||
}
|
||||
},
|
||||
repository: {
|
||||
async findStores(query) {
|
||||
assert.equal(query.city, '上海市');
|
||||
return stores;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/app-api/stores?city=%E4%B8%8A%E6%B5%B7%E5%B8%82',
|
||||
headers: { 'x-wechat-appid': 'wx-test-app', 'tenant-id': '7' }
|
||||
});
|
||||
assert.equal(response.statusCode, 200);
|
||||
assert.equal(response.json().data[0].id, '11');
|
||||
const invalid = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/app-api/stores?latitude=31.2',
|
||||
headers: { 'x-wechat-appid': 'wx-test-app', 'tenant-id': '7' }
|
||||
});
|
||||
assert.equal(invalid.statusCode, 400);
|
||||
await app.close();
|
||||
|
||||
console.log('PASS: M03-C trusted distance sorting, city fallback and public store discovery are present.');
|
||||
Reference in New Issue
Block a user