feat: add public and customer api foundations
This commit is contained in:
@@ -1,10 +1,41 @@
|
||||
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 { FastifyAdapter } from "@nestjs/platform-fastify";
|
||||
import type { NestFastifyApplication } from "@nestjs/platform-fastify";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
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()
|
||||
export class PrismaService extends PrismaClient {
|
||||
@@ -23,30 +54,108 @@ export class HealthController {
|
||||
|
||||
@Controller("public")
|
||||
export class PublicController {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
@Get("brand")
|
||||
brand(): { message: string } {
|
||||
return { message: "public brand endpoint is ready" };
|
||||
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")
|
||||
services(): { message: string } {
|
||||
return { message: "public services endpoint is ready" };
|
||||
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) {}
|
||||
constructor(
|
||||
private readonly wechatAuth: WechatAuthService,
|
||||
private readonly prisma: PrismaService,
|
||||
) {}
|
||||
|
||||
@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 ?? "");
|
||||
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({
|
||||
controllers: [HealthController, PublicController, CustomerSessionController],
|
||||
controllers: [
|
||||
HealthController,
|
||||
PublicController,
|
||||
CustomerSessionController,
|
||||
CustomerBookingController,
|
||||
],
|
||||
providers: [PrismaService, WechatAuthService],
|
||||
exports: [PrismaService, WechatAuthService],
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user