feat(M03-C): 完成地图选店与距离排序
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import type { AuthRepository } from '../auth/auth-repository.js';
|
||||
import type { StoreDiscoveryRepository } from '../stores/store-discovery-repository.js';
|
||||
|
||||
const headersSchema = z.object({
|
||||
'x-wechat-appid': z.string().trim().min(6).max(64),
|
||||
'tenant-id': z.string().regex(/^[1-9]\d{0,19}$/).optional()
|
||||
});
|
||||
const querySchema = z.object({
|
||||
city: z.string().trim().min(1).max(64).optional(),
|
||||
businessStatus: z.enum(['OPEN', 'CLOSED', 'SUSPENDED']).optional(),
|
||||
openNow: z.enum(['true', 'false']).transform((value) => value === 'true').optional(),
|
||||
latitude: z.coerce.number().min(-90).max(90).optional(),
|
||||
longitude: z.coerce.number().min(-180).max(180).optional(),
|
||||
maxDistanceMeters: z.coerce.number().int().min(1).max(500000).optional()
|
||||
}).refine((value) => (value.latitude === undefined) === (value.longitude === undefined), {
|
||||
message: 'latitude and longitude must be supplied together'
|
||||
}).refine((value) => value.maxDistanceMeters === undefined || value.latitude !== undefined, {
|
||||
message: 'distance filter requires coordinates'
|
||||
});
|
||||
|
||||
export interface StoreDiscoveryRouteOptions {
|
||||
repository: Pick<StoreDiscoveryRepository, 'findStores'>;
|
||||
tenancy: Pick<AuthRepository, 'resolveLoginContext'>;
|
||||
}
|
||||
|
||||
export async function registerStoreDiscoveryRoutes(
|
||||
app: FastifyInstance, options: StoreDiscoveryRouteOptions
|
||||
) {
|
||||
app.get('/app-api/stores', async (request, reply) => {
|
||||
const headers = headersSchema.safeParse(request.headers);
|
||||
const query = querySchema.safeParse(request.query);
|
||||
if (!headers.success || !query.success) {
|
||||
return reply.status(400).send({
|
||||
code: 'INVALID_STORE_DISCOVERY_REQUEST',
|
||||
message: 'AppID and valid city or coordinate filters are required.',
|
||||
traceId: request.traceId
|
||||
});
|
||||
}
|
||||
try {
|
||||
const context = await options.tenancy.resolveLoginContext(
|
||||
headers.data['x-wechat-appid'], headers.data['tenant-id']
|
||||
);
|
||||
if (!context) {
|
||||
return reply.status(404).send({
|
||||
code: 'APP_TENANT_NOT_FOUND', message: 'Application tenant binding was not found.',
|
||||
traceId: request.traceId
|
||||
});
|
||||
}
|
||||
return {
|
||||
code: 0,
|
||||
data: await options.repository.findStores({ tenantId: context.tenantId, ...query.data }),
|
||||
traceId: request.traceId
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === 'TENANT_SELECTION_REQUIRED') {
|
||||
return reply.status(409).send({
|
||||
code: 'TENANT_SELECTION_REQUIRED',
|
||||
message: 'tenant-id is required for an application bound to multiple tenants.',
|
||||
traceId: request.traceId
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -21,6 +21,8 @@ const hoursSchema = z.array(z.object({
|
||||
const storeSchema = z.object({
|
||||
name: z.string().trim().min(1).max(128),
|
||||
address: z.string().trim().max(255).default(''),
|
||||
city: z.string().trim().max(64).default(''),
|
||||
district: z.string().trim().max(64).default(''),
|
||||
longitude: coordinate.min(-180).max(180).nullable().optional(),
|
||||
latitude: coordinate.min(-90).max(90).nullable().optional(),
|
||||
contactPhone: z.string().trim().max(32).default(''),
|
||||
|
||||
Reference in New Issue
Block a user