diff --git a/apps/api/src/admin.dto.ts b/apps/api/src/admin.dto.ts index 64b7b77..f16eee1 100644 --- a/apps/api/src/admin.dto.ts +++ b/apps/api/src/admin.dto.ts @@ -1,4 +1,4 @@ -import { IsIn, IsOptional, IsString } from "class-validator"; +import { IsArray, IsIn, IsOptional, IsString } from "class-validator"; export class UpdateBookingStatusDto { @IsIn(["received", "contacting", "confirmed", "completed", "cancelled"]) @@ -8,3 +8,11 @@ export class UpdateBookingStatusDto { @IsString() adminNote?: string; } + +export class CreateTagDto { + @IsString() name!: string; +} + +export class UpdateBookingTagsDto { + @IsArray() @IsString({ each: true }) tagIds!: string[]; +} diff --git a/apps/api/src/booking.service.ts b/apps/api/src/booking.service.ts index 35fbdb0..2fbbc8e 100644 --- a/apps/api/src/booking.service.ts +++ b/apps/api/src/booking.service.ts @@ -137,4 +137,43 @@ export class BookingService { data: { status, ...(adminNote !== undefined ? { adminNote } : {}), isRead: true }, }); } + + async markRead(brandId: string, id: string) { + const booking = await this.prisma.booking.findFirst({ where: { id, brandId } }); + if (!booking) throw new NotFoundException("订单不存在"); + return this.prisma.booking.update({ where: { id }, data: { isRead: true } }); + } + + async listTags(brandId: string) { + return this.prisma.tag.findMany({ + where: { brandId, isEnabled: true }, + orderBy: [{ isPreset: "desc" }, { name: "asc" }], + }); + } + + async createTag(brandId: string, name: string) { + const normalized = name.trim(); + if (!normalized || normalized.length > 24) throw new BadRequestException("标签名称长度无效"); + return this.prisma.tag.create({ data: { brandId, name: normalized } }); + } + + async updateTags(brandId: string, bookingId: string, tagIds: string[]) { + const booking = await this.prisma.booking.findFirst({ where: { id: bookingId, brandId } }); + if (!booking) throw new NotFoundException("订单不存在"); + const tags = await this.prisma.tag.findMany({ + where: { id: { in: tagIds }, brandId, isEnabled: true }, + select: { id: true }, + }); + if (tags.length !== new Set(tagIds).size) throw new BadRequestException("存在无效标签"); + await this.prisma.$transaction([ + this.prisma.bookingTag.deleteMany({ where: { bookingId } }), + this.prisma.bookingTag.createMany({ + data: tags.map((tag) => ({ bookingId, tagId: tag.id })), + }), + ]); + return this.prisma.booking.findUnique({ + where: { id: bookingId }, + include: { tags: { include: { tag: true } } }, + }); + } } diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 7892a9a..45ef8d0 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -18,7 +18,7 @@ import { PrismaService } from "./prisma.service.js"; import { WechatAuthService } from "./wechat-auth.js"; import { BookingService } from "./booking.service.js"; import { CreateBookingDto } from "./booking.dto.js"; -import { UpdateBookingStatusDto } from "./admin.dto.js"; +import { CreateTagDto, UpdateBookingStatusDto, UpdateBookingTagsDto } from "./admin.dto.js"; import { AdminAuthService } from "./admin-auth.service.js"; import { AdminLoginDto } from "./admin-auth.dto.js"; import type { FastifyRequest } from "fastify"; @@ -183,6 +183,46 @@ export class AdminBookingController { ); } + @Post(":id/read") + markRead(@Req() request: FastifyRequest, @Param("id") id: string): Promise { + this.requireAdmin(request); + return this.bookings.markRead( + process.env.DEFAULT_BRAND_ID ?? "00000000-0000-0000-0000-000000000001", + id, + ); + } + + @Post(":id/tags") + updateTags( + @Req() request: FastifyRequest, + @Param("id") id: string, + @Body() body: UpdateBookingTagsDto, + ): Promise { + this.requireAdmin(request); + return this.bookings.updateTags( + process.env.DEFAULT_BRAND_ID ?? "00000000-0000-0000-0000-000000000001", + id, + body.tagIds, + ); + } + + @Get("/tags") + listTags(@Req() request: FastifyRequest): Promise { + this.requireAdmin(request); + return this.bookings.listTags( + process.env.DEFAULT_BRAND_ID ?? "00000000-0000-0000-0000-000000000001", + ); + } + + @Post("/tags") + createTag(@Req() request: FastifyRequest, @Body() body: CreateTagDto): Promise { + this.requireAdmin(request); + return this.bookings.createTag( + process.env.DEFAULT_BRAND_ID ?? "00000000-0000-0000-0000-000000000001", + body.name, + ); + } + private requireAdmin(request: FastifyRequest): void { const token = request.headers.authorization?.replace(/^Bearer\s+/i, ""); if (!token) throw new BadRequestException("缺少管理员登录态");