feat(M08-C): 补管理员验券与经营统计
This commit is contained in:
@@ -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
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user