feat(M08-C): 补管理员验券与经营统计

This commit is contained in:
Codex
2026-08-10 11:08:55 +08:00
parent 3bfccb8513
commit ce52f07588
18 changed files with 1141 additions and 22 deletions
+94
View File
@@ -0,0 +1,94 @@
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import { z } from 'zod';
import type { AuthRepository } from '../auth/auth-repository.js';
import { authenticateAccessToken } from '../auth/authenticate.js';
import type { AccessProfile } from '../auth/rbac-repository.js';
import type { ManagementActor } from '../auth/user-management-repository.js';
import {
BusinessStatisticsError,
type BusinessStatisticsRepository
} from '../operations/business-statistics-repository.js';
const querySchema = z.object({
storeId: z.string().regex(/^[1-9]\d{0,19}$/),
from: z.coerce.date().optional(),
to: z.coerce.date().optional()
}).strict();
export interface BusinessStatisticsRouteOptions {
repository: Pick<BusinessStatisticsRepository, 'overview'>;
authRepository: Pick<AuthRepository, 'validateSession'>;
accessControl: { getAccessProfile(tenantId: string, userId: string): Promise<AccessProfile> };
jwtSecret: string;
}
export async function registerBusinessStatisticsRoutes(
app: FastifyInstance,
options: BusinessStatisticsRouteOptions
) {
app.get('/app-api/management/statistics', async (request, reply) => {
const actor = await requireActor(request, reply, options);
if (!actor) return;
const query = querySchema.safeParse(request.query);
if (!query.success) return invalid(reply, request.traceId);
const to = query.data.to ?? new Date();
const from = query.data.from ?? new Date(to.getTime() - 30 * 86400000);
const duration = to.getTime() - from.getTime();
if (duration <= 0 || duration > 93 * 86400000) return invalid(reply, request.traceId);
try {
return {
code: 0,
data: await options.repository.overview(actor, {
storeId: query.data.storeId,
from,
to
}),
traceId: request.traceId
};
} catch (error) {
if (!(error instanceof BusinessStatisticsError)) throw error;
return reply.status(403).send({
code: error.code,
message: 'Business statistics permission is required.',
traceId: request.traceId
});
}
});
}
async function requireActor(
request: FastifyRequest,
reply: FastifyReply,
options: BusinessStatisticsRouteOptions
): Promise<ManagementActor | null> {
const auth = await authenticateAccessToken(
request.headers.authorization, options.authRepository, options.jwtSecret
);
if (!auth) {
reply.status(401).send({
code: 'AUTH_SESSION_INVALID',
message: 'Authentication required.',
traceId: request.traceId
});
return null;
}
const access = await options.accessControl.getAccessProfile(
auth.session.tenantId, auth.session.user.id
);
return {
tenantId: auth.session.tenantId,
userId: auth.session.user.id,
access,
traceId: request.traceId,
ip: request.ip,
userAgent: request.headers['user-agent'] ?? ''
};
}
function invalid(reply: FastifyReply, traceId: string) {
return reply.status(400).send({
code: 'INVALID_BUSINESS_STATISTICS_REQUEST',
message: 'The statistics request is invalid.',
traceId
});
}