feat(M08-A): 接入顾客端选店下单入口

This commit is contained in:
Codex
2026-06-24 21:26:32 +08:00
parent c90f6e34a1
commit 4ac2c960e4
20 changed files with 734 additions and 32 deletions
+87 -2
View File
@@ -1,6 +1,6 @@
import type { FastifyInstance } from 'fastify';
import { z } from 'zod';
import type { AuthRepository } from '../auth/auth-repository.js';
import type { AuthRepository, LoginContext } from '../auth/auth-repository.js';
import type { StoreDiscoveryRepository } from '../stores/store-discovery-repository.js';
const headersSchema = z.object({
@@ -21,7 +21,7 @@ const querySchema = z.object({
});
export interface StoreDiscoveryRouteOptions {
repository: Pick<StoreDiscoveryRepository, 'findStores'>;
repository: Pick<StoreDiscoveryRepository, 'findStores' | 'getStore' | 'listRooms'>;
tenancy: Pick<AuthRepository, 'resolveLoginContext'>;
}
@@ -64,4 +64,89 @@ export async function registerStoreDiscoveryRoutes(
throw error;
}
});
app.get('/app-api/stores/:storeId', async (request, reply) => {
const context = await resolveContext(request, reply, options);
const params = storeIdSchema.safeParse(request.params);
if (!context) return;
if (!params.success) return invalid(reply, request.traceId);
const store = await options.repository.getStore({
tenantId: context.tenantId,
storeId: params.data.storeId
});
if (!store) {
return reply.status(404).send({
code: 'STORE_NOT_FOUND',
message: 'Store was not found.',
traceId: request.traceId
});
}
return { code: 0, data: store, traceId: request.traceId };
});
app.get('/app-api/stores/:storeId/rooms', async (request, reply) => {
const context = await resolveContext(request, reply, options);
const params = storeIdSchema.safeParse(request.params);
if (!context) return;
if (!params.success) return invalid(reply, request.traceId);
return {
code: 0,
data: await options.repository.listRooms({
tenantId: context.tenantId,
storeId: params.data.storeId
}),
traceId: request.traceId
};
});
}
const storeIdSchema = z.object({
storeId: z.string().regex(/^[1-9]\d{0,19}$/)
});
async function resolveContext(
request: { headers: unknown; traceId: string },
reply: { status(code: number): { send(payload: unknown): unknown } },
options: StoreDiscoveryRouteOptions
): Promise<LoginContext | null> {
const headers = headersSchema.safeParse(request.headers);
if (!headers.success) {
invalid(reply, request.traceId);
return null;
}
try {
const context = await options.tenancy.resolveLoginContext(
headers.data['x-wechat-appid'], headers.data['tenant-id']
);
if (!context) {
reply.status(404).send({
code: 'APP_TENANT_NOT_FOUND',
message: 'Application tenant binding was not found.',
traceId: request.traceId
});
return null;
}
return context;
} catch (error) {
if (error instanceof Error && error.message === 'TENANT_SELECTION_REQUIRED') {
reply.status(409).send({
code: 'TENANT_SELECTION_REQUIRED',
message: 'tenant-id is required for an application bound to multiple tenants.',
traceId: request.traceId
});
return null;
}
throw error;
}
}
function invalid(
reply: { status(code: number): { send(payload: unknown): unknown } },
traceId: string
) {
return reply.status(400).send({
code: 'INVALID_STORE_DISCOVERY_REQUEST',
message: 'AppID and valid store filters are required.',
traceId
});
}