221 lines
6.9 KiB
TypeScript
221 lines
6.9 KiB
TypeScript
import "reflect-metadata";
|
|
import {
|
|
BadRequestException,
|
|
Body,
|
|
Controller,
|
|
Get,
|
|
Module,
|
|
Param,
|
|
Post,
|
|
Req,
|
|
ValidationPipe,
|
|
} from "@nestjs/common";
|
|
import { NestFactory } from "@nestjs/core";
|
|
import { Prisma } from "@prisma/client";
|
|
import { FastifyAdapter } from "@nestjs/platform-fastify";
|
|
import type { NestFastifyApplication } from "@nestjs/platform-fastify";
|
|
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 { AdminAuthService } from "./admin-auth.service.js";
|
|
import { AdminLoginDto } from "./admin-auth.dto.js";
|
|
import type { FastifyRequest } from "fastify";
|
|
import { readIdentity, signIdentity } from "./auth.js";
|
|
|
|
@Controller("health")
|
|
export class HealthController {
|
|
@Get()
|
|
check(): { status: string; service: string } {
|
|
return { status: "ok", service: "personal-brand-api" };
|
|
}
|
|
}
|
|
|
|
@Controller("public")
|
|
export class PublicController {
|
|
constructor(private readonly prisma: PrismaService) {}
|
|
|
|
@Get("brand")
|
|
async brand(): Promise<unknown> {
|
|
return this.prisma.brandProfile.findFirst({
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
tagline: true,
|
|
description: true,
|
|
avatarUrl: true,
|
|
coverUrl: true,
|
|
hours: true,
|
|
address: true,
|
|
deliveryNote: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
@Get("services")
|
|
async services(): Promise<unknown> {
|
|
return this.prisma.service.findMany({
|
|
where: { isEnabled: true },
|
|
orderBy: { sortOrder: "asc" },
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
description: true,
|
|
coverUrl: true,
|
|
priceText: true,
|
|
priceAmount: true,
|
|
bookingEnabled: true,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
@Controller("customer/session")
|
|
export class CustomerSessionController {
|
|
constructor(
|
|
private readonly wechatAuth: WechatAuthService,
|
|
private readonly prisma: PrismaService,
|
|
) {}
|
|
|
|
@Post("wechat")
|
|
async wechatLogin(@Body() body: { code?: string }): Promise<{ token: string }> {
|
|
const session = await this.wechatAuth.exchangeCode(body.code ?? "");
|
|
const brand = await this.prisma.brandProfile.findFirst({ select: { id: true } });
|
|
if (!brand) throw new BadRequestException("品牌尚未初始化");
|
|
await this.prisma.customerUser.upsert({
|
|
where: { brandId_openid: { brandId: brand.id, openid: session.openid } },
|
|
update: { lastLoginAt: new Date() },
|
|
create: { brandId: brand.id, openid: session.openid },
|
|
});
|
|
return {
|
|
token: signIdentity({ subject: session.openid, brandId: brand.id, role: "customer" }),
|
|
};
|
|
}
|
|
}
|
|
|
|
@Controller("admin/session")
|
|
export class AdminSessionController {
|
|
constructor(private readonly auth: AdminAuthService) {}
|
|
@Post("login")
|
|
login(@Body() body: AdminLoginDto): Promise<{ token: string; displayName: string }> {
|
|
return this.auth.login(body.username, body.password);
|
|
}
|
|
}
|
|
|
|
@Controller("customer/bookings")
|
|
export class CustomerBookingController {
|
|
constructor(
|
|
private readonly prisma: PrismaService,
|
|
private readonly bookings: BookingService,
|
|
) {}
|
|
|
|
@Post()
|
|
async create(@Req() request: FastifyRequest, @Body() body: CreateBookingDto): Promise<unknown> {
|
|
const identity = this.identity(request);
|
|
const customer = await this.prisma.customerUser.findUnique({
|
|
where: { brandId_openid: identity },
|
|
});
|
|
if (!customer) throw new BadRequestException("请先完成微信登录");
|
|
return this.bookings.create(identity.brandId, customer.id, {
|
|
...body,
|
|
extraData: body.extraData as Prisma.InputJsonObject,
|
|
});
|
|
}
|
|
|
|
@Get()
|
|
async list(@Req() request: FastifyRequest): Promise<unknown> {
|
|
const identity = this.identity(request);
|
|
const customer = await this.prisma.customerUser.findUnique({
|
|
where: { brandId_openid: identity },
|
|
});
|
|
if (!customer) throw new BadRequestException("请先完成微信登录");
|
|
return this.bookings.listForCustomer(identity.brandId, customer.id);
|
|
}
|
|
|
|
@Post(":id/cancel")
|
|
async cancel(@Req() request: FastifyRequest, @Param("id") id: string): Promise<unknown> {
|
|
const identity = this.identity(request);
|
|
const customer = await this.prisma.customerUser.findUnique({
|
|
where: { brandId_openid: identity },
|
|
});
|
|
if (!customer) throw new BadRequestException("请先完成微信登录");
|
|
return this.bookings.cancelForCustomer(identity.brandId, customer.id, id);
|
|
}
|
|
|
|
private identity(request: FastifyRequest): { brandId: string; openid: string } {
|
|
const token = request.headers.authorization?.replace(/^Bearer\s+/i, "");
|
|
if (!token) throw new BadRequestException("缺少客户登录态");
|
|
try {
|
|
const identity = readIdentity(token);
|
|
if (identity.role !== "customer") throw new BadRequestException("客户登录态无效");
|
|
return { brandId: identity.brandId, openid: identity.subject };
|
|
} catch (error) {
|
|
if (error instanceof BadRequestException) throw error;
|
|
throw new BadRequestException("客户登录态无效");
|
|
}
|
|
}
|
|
}
|
|
|
|
@Controller("admin/bookings")
|
|
export class AdminBookingController {
|
|
constructor(private readonly bookings: BookingService) {}
|
|
@Get()
|
|
list(@Req() request: FastifyRequest): Promise<unknown> {
|
|
this.requireAdmin(request);
|
|
return this.bookings.listForAdmin(
|
|
process.env.DEFAULT_BRAND_ID ?? "00000000-0000-0000-0000-000000000001",
|
|
);
|
|
}
|
|
@Post(":id/status")
|
|
update(
|
|
@Req() request: FastifyRequest,
|
|
@Param("id") id: string,
|
|
@Body()
|
|
body: UpdateBookingStatusDto,
|
|
): Promise<unknown> {
|
|
this.requireAdmin(request);
|
|
return this.bookings.updateStatus(
|
|
process.env.DEFAULT_BRAND_ID ?? "00000000-0000-0000-0000-000000000001",
|
|
id,
|
|
body.status,
|
|
body.adminNote,
|
|
);
|
|
}
|
|
|
|
private requireAdmin(request: FastifyRequest): void {
|
|
const token = request.headers.authorization?.replace(/^Bearer\s+/i, "");
|
|
if (!token) throw new BadRequestException("缺少管理员登录态");
|
|
const identity = readIdentity(token);
|
|
if (identity.role !== "admin") throw new BadRequestException("无管理员权限");
|
|
}
|
|
}
|
|
|
|
@Module({
|
|
controllers: [
|
|
HealthController,
|
|
PublicController,
|
|
CustomerSessionController,
|
|
AdminSessionController,
|
|
CustomerBookingController,
|
|
AdminBookingController,
|
|
],
|
|
providers: [PrismaService, WechatAuthService, BookingService, AdminAuthService],
|
|
exports: [PrismaService, WechatAuthService],
|
|
})
|
|
export class AppModule {}
|
|
|
|
export async function createApp(): Promise<NestFastifyApplication> {
|
|
const app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter());
|
|
app.useGlobalPipes(
|
|
new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }),
|
|
);
|
|
app.enableCors();
|
|
return app;
|
|
}
|
|
|
|
if (process.env.NODE_ENV !== "test") {
|
|
const app = await createApp();
|
|
await app.listen({ port: Number(process.env.API_PORT ?? 3001), host: "0.0.0.0" });
|
|
}
|