feat(M08-B): 补保洁照片上传和自动建单
This commit is contained in:
@@ -3,6 +3,7 @@ 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 { MediaStorage, MediaValidationError } from '../content/media-storage.js';
|
||||
import {
|
||||
CleaningTaskError,
|
||||
cleaningTaskStatuses,
|
||||
@@ -22,7 +23,9 @@ const submitSchema = z.object({
|
||||
});
|
||||
|
||||
export interface CleaningRouteOptions {
|
||||
repository: Pick<CleaningTaskRepository, 'listHall' | 'listMine' | 'claim' | 'start' | 'submit' | 'stats'>;
|
||||
repository: Pick<CleaningTaskRepository,
|
||||
'listHall' | 'listMine' | 'claim' | 'start' | 'rework' | 'submit' | 'assertCanUploadPhoto' | 'stats'>;
|
||||
mediaStorage?: MediaStorage;
|
||||
authRepository: Pick<AuthRepository, 'validateSession'>;
|
||||
accessControl: { getAccessProfile(tenantId: string, userId: string): Promise<AccessProfile> };
|
||||
jwtSecret: string;
|
||||
@@ -32,6 +35,14 @@ export async function registerCleaningRoutes(
|
||||
app: FastifyInstance,
|
||||
options: CleaningRouteOptions
|
||||
): Promise<void> {
|
||||
if (options.mediaStorage && !app.hasContentTypeParser('application/octet-stream')) {
|
||||
app.addContentTypeParser(
|
||||
'application/octet-stream',
|
||||
{ parseAs: 'buffer', bodyLimit: 8 * 1024 * 1024 },
|
||||
(_request, body, done) => done(null, body)
|
||||
);
|
||||
}
|
||||
|
||||
app.get('/app-api/cleaning/tasks/hall', async (request, reply) => {
|
||||
const actor = await requireActor(request, reply, options, 'read');
|
||||
if (!actor) return;
|
||||
@@ -90,6 +101,43 @@ export async function registerCleaningRoutes(
|
||||
}));
|
||||
});
|
||||
|
||||
app.post('/app-api/cleaning/tasks/:taskId/rework', async (request, reply) => {
|
||||
const actor = await requireActor(request, reply, options, 'write');
|
||||
if (!actor) return;
|
||||
const params = paramsSchema.safeParse(request.params);
|
||||
if (!params.success) return invalid(reply, request.traceId);
|
||||
return handle(reply, request.traceId, async () => ({
|
||||
code: 0,
|
||||
data: await options.repository.rework({ ...actor, taskId: params.data.taskId }),
|
||||
traceId: request.traceId
|
||||
}));
|
||||
});
|
||||
|
||||
app.post('/app-api/cleaning/tasks/:taskId/photos', async (request, reply) => {
|
||||
if (!options.mediaStorage) return reply.status(501).send({
|
||||
code: 'CLEANING_PHOTO_UPLOAD_UNAVAILABLE',
|
||||
message: 'Cleaning photo upload is not configured.',
|
||||
traceId: request.traceId
|
||||
});
|
||||
const actor = await requireActor(request, reply, options, 'write');
|
||||
if (!actor) return;
|
||||
const params = paramsSchema.safeParse(request.params);
|
||||
const originalName = singleHeader(request.headers['x-file-name']);
|
||||
if (!params.success || !originalName || !Buffer.isBuffer(request.body)) {
|
||||
return invalid(reply, request.traceId);
|
||||
}
|
||||
return handle(reply, request.traceId, async () => {
|
||||
await options.repository.assertCanUploadPhoto({ ...actor, taskId: params.data.taskId });
|
||||
const image = await options.mediaStorage!.storeImage({
|
||||
tenantId: actor.tenantId,
|
||||
originalName,
|
||||
contentType: singleHeader(request.headers['x-image-content-type']) ?? '',
|
||||
body: request.body as Buffer
|
||||
});
|
||||
return reply.status(201).send({ code: 0, data: image, traceId: request.traceId });
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/app-api/cleaning/tasks/:taskId/submit', async (request, reply) => {
|
||||
const actor = await requireActor(request, reply, options, 'write');
|
||||
if (!actor) return;
|
||||
@@ -150,6 +198,13 @@ async function handle(reply: FastifyReply, traceId: string, work: () => Promise<
|
||||
try {
|
||||
return await work();
|
||||
} catch (error) {
|
||||
if (error instanceof MediaValidationError) {
|
||||
return reply.status(400).send({
|
||||
code: error.code,
|
||||
message: 'The cleaning task request cannot be completed.',
|
||||
traceId
|
||||
});
|
||||
}
|
||||
if (!(error instanceof CleaningTaskError)) throw error;
|
||||
const statusCode = error.code === 'CLEANING_TASK_FORBIDDEN' ? 403 : 409;
|
||||
return reply.status(statusCode).send({
|
||||
@@ -160,6 +215,10 @@ async function handle(reply: FastifyReply, traceId: string, work: () => Promise<
|
||||
}
|
||||
}
|
||||
|
||||
function singleHeader(value: string | string[] | undefined) {
|
||||
return Array.isArray(value) ? value[0] : value;
|
||||
}
|
||||
|
||||
function invalid(reply: FastifyReply, traceId: string) {
|
||||
return reply.status(400).send({
|
||||
code: 'INVALID_CLEANING_TASK_REQUEST',
|
||||
|
||||
Reference in New Issue
Block a user