feat(M08-D): 补多小程序与租户配置

This commit is contained in:
Codex
2026-08-10 12:37:53 +08:00
parent b3fba4b815
commit c7f27bc724
12 changed files with 517 additions and 1 deletions
+91
View File
@@ -0,0 +1,91 @@
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 { PlatformAdminError, type PlatformAdminRepository } from '../tenancy/platform-admin-repository.js';
const id = z.string().regex(/^[1-9]\d{0,19}$/);
const configSchema = z.object({
brandName: z.string().trim().min(1).max(128),
logoUrl: z.string().trim().max(512).default(''),
themeColor: z.string().regex(/^#[0-9A-Fa-f]{6}$/),
servicePhone: z.string().trim().max(32).default(''),
franchisePhone: z.string().trim().max(32).default(''),
shareTitle: z.string().trim().max(128).default(''),
shareImageUrl: z.string().trim().max(512).default(''),
defaultStoreId: id.nullable().default(null),
extraConfig: z.record(z.unknown()).default({}),
bindingStatus: z.enum(['ACTIVE', 'DISABLED']),
isDefault: z.boolean()
});
const bindSchema = z.object({
appId: z.string().regex(/^[A-Za-z0-9_-]{6,64}$/),
appName: z.string().trim().min(1).max(128),
appStatus: z.enum(['ACTIVE', 'DISABLED']),
config: configSchema
});
export interface PlatformManagementRouteOptions {
repository: Pick<PlatformAdminRepository, 'listTenantApps' | 'updateTenantConfig' | 'bindApplication'>;
authRepository: Pick<AuthRepository, 'validateSession'>;
accessControl: { getAccessProfile(tenantId: string, userId: string): Promise<AccessProfile> };
jwtSecret: string;
}
export async function registerPlatformManagementRoutes(app: FastifyInstance, options: PlatformManagementRouteOptions) {
app.get('/admin-api/platform-apps', async (request, reply) => {
const actor = await requireActor(request, reply, options, false);
if (!actor) return;
return handle(reply, request.traceId, async () => ({
code: 0, data: await options.repository.listTenantApps(actor.tenantId), traceId: request.traceId
}));
});
app.put('/admin-api/platform-apps/:id/config', async (request, reply) => {
const actor = await requireActor(request, reply, options, false);
const params = z.object({ id }).safeParse(request.params);
const body = configSchema.safeParse(request.body);
if (!actor) return;
if (!params.success || !body.success) return invalid(reply, request.traceId);
return handle(reply, request.traceId, async () => ({
code: 0,
data: await options.repository.updateTenantConfig(actor, params.data.id, body.data),
traceId: request.traceId
}));
});
app.post('/admin-api/platform-apps/bind', async (request, reply) => {
const actor = await requireActor(request, reply, options, true);
const body = bindSchema.safeParse(request.body);
if (!actor) return;
if (!body.success) return invalid(reply, request.traceId);
return handle(reply, request.traceId, async () => reply.status(201).send({
code: 0, data: await options.repository.bindApplication(actor, body.data), traceId: request.traceId
}));
});
}
async function requireActor(request: FastifyRequest, reply: FastifyReply, options: PlatformManagementRouteOptions, platformOnly: boolean): 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);
const platform = access.roles.includes('PLATFORM_ADMIN') || access.capabilities.includes('platform.manage');
if ((platformOnly && !platform) || (!platformOnly && !platform && !access.capabilities.includes('tenant.manage'))) {
reply.status(403).send({ code: platformOnly ? 'PLATFORM_MANAGEMENT_FORBIDDEN' : 'TENANT_MANAGEMENT_FORBIDDEN', message: 'Management permission is required.', traceId: request.traceId });
return null;
}
return { tenantId: auth.session.tenantId, userId: auth.session.user.id, access, traceId: request.traceId, ip: request.ip, userAgent: request.headers['user-agent'] ?? '' };
}
async function handle(reply: FastifyReply, traceId: string, work: () => Promise<unknown>) {
try { return await work(); }
catch (error) {
if (!(error instanceof PlatformAdminError)) throw error;
const missing = error.code.endsWith('_NOT_FOUND');
return reply.status(missing ? 404 : 400).send({ code: error.code, message: 'The platform application operation is invalid.', traceId });
}
}
function invalid(reply: FastifyReply, traceId: string) {
return reply.status(400).send({ code: 'INVALID_PLATFORM_APP_REQUEST', message: 'The platform application request is invalid.', traceId });
}