feat: add public and customer api foundations

This commit is contained in:
2026-09-18 15:36:50 +08:00
parent de8f989083
commit bd45625d36

View File

@@ -1,10 +1,41 @@
import "reflect-metadata"; import "reflect-metadata";
import { Body, Controller, Get, Injectable, Module, Post } from "@nestjs/common"; import {
BadRequestException,
Body,
Controller,
Get,
Injectable,
Module,
Post,
Req,
} from "@nestjs/common";
import { NestFactory } from "@nestjs/core"; import { NestFactory } from "@nestjs/core";
import { FastifyAdapter } from "@nestjs/platform-fastify"; import { FastifyAdapter } from "@nestjs/platform-fastify";
import type { NestFastifyApplication } from "@nestjs/platform-fastify"; import type { NestFastifyApplication } from "@nestjs/platform-fastify";
import { PrismaClient } from "@prisma/client"; import { PrismaClient } from "@prisma/client";
import { WechatAuthService } from "./wechat-auth.js"; import { WechatAuthService } from "./wechat-auth.js";
import type { FastifyRequest } from "fastify";
import { createHmac } from "node:crypto";
function signIdentity(identity: { brandId: string; openid: string }): string {
const payload = Buffer.from(JSON.stringify(identity)).toString("base64url");
const signature = createHmac("sha256", process.env.SESSION_SECRET ?? "development-only-secret")
.update(payload)
.digest("base64url");
return `${payload}.${signature}`;
}
function readIdentity(token: string): { brandId: string; openid: string } {
const [payload, signature] = token.split(".");
const expected = createHmac("sha256", process.env.SESSION_SECRET ?? "development-only-secret")
.update(payload ?? "")
.digest("base64url");
if (!payload || signature !== expected) throw new BadRequestException("客户登录态无效");
return JSON.parse(Buffer.from(payload, "base64url").toString("utf8")) as {
brandId: string;
openid: string;
};
}
@Injectable() @Injectable()
export class PrismaService extends PrismaClient { export class PrismaService extends PrismaClient {
@@ -23,30 +54,108 @@ export class HealthController {
@Controller("public") @Controller("public")
export class PublicController { export class PublicController {
constructor(private readonly prisma: PrismaService) {}
@Get("brand") @Get("brand")
brand(): { message: string } { async brand(): Promise<unknown> {
return { message: "public brand endpoint is ready" }; 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") @Get("services")
services(): { message: string } { async services(): Promise<unknown> {
return { message: "public services endpoint is ready" }; 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") @Controller("customer/session")
export class CustomerSessionController { export class CustomerSessionController {
constructor(private readonly wechatAuth: WechatAuthService) {} constructor(
private readonly wechatAuth: WechatAuthService,
private readonly prisma: PrismaService,
) {}
@Post("wechat") @Post("wechat")
async wechatLogin(@Body() body: { code?: string }): Promise<{ openid: string }> { async wechatLogin(@Body() body: { code?: string }): Promise<{ token: string }> {
const session = await this.wechatAuth.exchangeCode(body.code ?? ""); const session = await this.wechatAuth.exchangeCode(body.code ?? "");
return { openid: session.openid }; 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({ brandId: brand.id, openid: session.openid }) };
}
}
@Controller("customer/bookings")
export class CustomerBookingController {
constructor(private readonly prisma: PrismaService) {}
@Get()
async list(@Req() request: FastifyRequest): Promise<unknown> {
const identity = this.identity(request);
return this.prisma.booking.findMany({
where: { customerUser: { brandId: identity.brandId, openid: identity.openid } },
orderBy: { createdAt: "desc" },
select: {
bookingNo: true,
requestType: true,
customerName: true,
status: true,
estimatedAmount: true,
requestedDate: true,
requestedTime: true,
fulfillmentType: true,
createdAt: true,
items: { select: { serviceNameSnapshot: true, quantity: true, subtotalSnapshot: true } },
},
});
}
private identity(request: FastifyRequest): { brandId: string; openid: string } {
const token = request.headers.authorization?.replace(/^Bearer\s+/i, "");
if (!token) throw new BadRequestException("缺少客户登录态");
try {
return readIdentity(token);
} catch (error) {
if (error instanceof BadRequestException) throw error;
throw new BadRequestException("客户登录态无效");
}
} }
} }
@Module({ @Module({
controllers: [HealthController, PublicController, CustomerSessionController], controllers: [
HealthController,
PublicController,
CustomerSessionController,
CustomerBookingController,
],
providers: [PrismaService, WechatAuthService], providers: [PrismaService, WechatAuthService],
exports: [PrismaService, WechatAuthService], exports: [PrismaService, WechatAuthService],
}) })