perf: 完善订单管理

This commit is contained in:
richarjiang
2026-04-05 21:03:18 +08:00
parent fdb13c32c2
commit 4633ceea8c
29 changed files with 1000 additions and 261 deletions

View File

@@ -0,0 +1,37 @@
import { Controller, Get, UseGuards } from '@nestjs/common'
import { JwtAuthGuard } from '../auth/jwt-auth.guard'
import { Roles } from '../auth/roles.decorator'
import { RolesGuard } from '../auth/roles.guard'
import { UserRole } from '@mp-pilates/shared'
import { PrismaService } from '../prisma/prisma.service'
interface AdminStats {
todayBookings: number
totalOrders: number
totalBookings: number
}
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.ADMIN)
export class AdminController {
constructor(private readonly prisma: PrismaService) {}
@Get('stats')
async getStats(): Promise<AdminStats> {
const today = new Date()
today.setUTCHours(0, 0, 0, 0)
const [todayBookings, totalOrders, totalBookings] = await Promise.all([
this.prisma.booking.count({
where: {
timeSlot: { date: today },
},
}),
this.prisma.order.count(),
this.prisma.booking.count(),
])
return { todayBookings, totalOrders, totalBookings }
}
}

View File

@@ -0,0 +1,7 @@
import { Module } from '@nestjs/common'
import { AdminController } from './admin.controller'
@Module({
controllers: [AdminController],
})
export class AdminModule {}

View File

@@ -10,6 +10,7 @@ import { MembershipModule } from './membership/membership.module'
import { BookingModule } from './booking/booking.module'
import { SchedulerModule } from './scheduler/scheduler.module'
import { PaymentModule } from './payment/payment.module'
import { AdminModule } from './admin/admin.module'
@Module({
imports: [
@@ -26,6 +27,7 @@ import { PaymentModule } from './payment/payment.module'
BookingModule,
SchedulerModule,
PaymentModule,
AdminModule,
],
controllers: [AppController],
})

View File

@@ -9,7 +9,7 @@ import {
UseGuards,
ValidationPipe,
} from '@nestjs/common'
import { UserRole } from '@mp-pilates/shared'
import { UserRole, OrderStatus } from '@mp-pilates/shared'
import { JwtAuthGuard } from '../auth/jwt-auth.guard'
import { RolesGuard } from '../auth/roles.guard'
import { Roles } from '../auth/roles.decorator'
@@ -85,7 +85,7 @@ export class PaymentController {
return this.paymentService.getAllOrders(
page ? parseInt(page, 10) : 1,
limit ? parseInt(limit, 10) : 10,
status as any,
status ? (status as OrderStatus) : undefined,
)
}
}

View File

@@ -3,24 +3,28 @@ import {
Get,
Put,
Body,
Query,
UseGuards,
} from '@nestjs/common'
import { UserRole } from '@mp-pilates/shared'
import { JwtAuthGuard } from '../auth/jwt-auth.guard'
import { RolesGuard } from '../auth/roles.guard'
import { Roles } from '../auth/roles.decorator'
import { CurrentUser } from '../common/decorators/current-user.decorator'
import { UserService } from './user.service'
import { UpdateProfileDto } from './dto/update-profile.dto'
@UseGuards(JwtAuthGuard)
@Controller('user')
@Controller()
export class UserController {
constructor(private readonly userService: UserService) {}
@Get('profile')
@Get('user/profile')
getProfile(@CurrentUser('sub') userId: string) {
return this.userService.getProfile(userId)
}
@Put('profile')
@Put('user/profile')
updateProfile(
@CurrentUser('sub') userId: string,
@Body() dto: UpdateProfileDto,
@@ -28,8 +32,25 @@ export class UserController {
return this.userService.updateProfile(userId, dto)
}
@Get('stats')
@Get('user/stats')
getStats(@CurrentUser('sub') userId: string) {
return this.userService.getStats(userId)
}
// ─── Admin: Member Management ─────────────────────────────────────────────
@Get('admin/members')
@UseGuards(RolesGuard)
@Roles(UserRole.ADMIN)
getMembers(
@Query('page') page?: string,
@Query('limit') limit?: string,
@Query('search') search?: string,
) {
return this.userService.getMembers(
page ? Number(page) : 1,
limit ? Number(limit) : 20,
search && search !== 'undefined' ? search : undefined,
)
}
}

View File

@@ -1,8 +1,10 @@
import { Module } from '@nestjs/common'
import { AuthModule } from '../auth/auth.module'
import { UserController } from './user.controller'
import { UserService } from './user.service'
@Module({
imports: [AuthModule],
controllers: [UserController],
providers: [UserService],
exports: [UserService],

View File

@@ -1,7 +1,7 @@
import { Injectable, NotFoundException } from '@nestjs/common'
import { MembershipStatus, BookingStatus, UserRole } from '@mp-pilates/shared'
import type { PaginatedData, UserProfileResponse, UserStatsResponse } from '@mp-pilates/shared'
import { PrismaService } from '../prisma/prisma.service'
import type { UserProfileResponse, UserStatsResponse } from '@mp-pilates/shared'
@Injectable()
export class UserService {
@@ -117,4 +117,89 @@ export class UserService {
monthHours,
}
}
// ─── Admin: paginated member list ─────────────────────────────────────────
async getMembers(
page: number,
limit: number,
search?: string,
): Promise<PaginatedData<{
userId: string
openid: string
nickname: string
phone: string | null
avatarUrl: string | null
totalBookings: number
completedBookings: number
cancelledBookings: number
}>> {
const where = search
? {
OR: [
{ nickname: { contains: search, mode: 'insensitive' as const } },
{ openid: { contains: search, mode: 'insensitive' as const } },
{ phone: { contains: search } },
],
}
: {}
const [users, total] = await Promise.all([
this.prisma.user.findMany({
where,
select: {
id: true,
openid: true,
nickname: true,
phone: true,
avatarUrl: true,
_count: {
select: {
bookings: true,
},
},
},
orderBy: { createdAt: 'desc' },
skip: (page - 1) * limit,
take: limit,
}),
this.prisma.user.count({ where }),
])
// Batch-fetch booking stats for the page of users
const userIds = users.map((u) => u.id)
const bookingStats = userIds.length
? await this.prisma.booking.groupBy({
by: ['userId', 'status'],
where: { userId: { in: userIds } },
_count: { id: true },
})
: []
const statsMap = new Map<string, { total: number; completed: number; cancelled: number }>()
for (const stat of bookingStats) {
const entry = statsMap.get(stat.userId) ?? { total: 0, completed: 0, cancelled: 0 }
entry.total += stat._count.id
if (stat.status === BookingStatus.COMPLETED) entry.completed += stat._count.id
if (stat.status === BookingStatus.CANCELLED) entry.cancelled += stat._count.id
statsMap.set(stat.userId, entry)
}
const items = users.map((u) => {
const s = statsMap.get(u.id) ?? { total: 0, completed: 0, cancelled: 0 }
return {
userId: u.id,
openid: u.openid,
nickname: u.nickname,
phone: u.phone,
avatarUrl: u.avatarUrl,
totalBookings: s.total,
completedBookings: s.completed,
cancelledBookings: s.cancelled,
}
})
return { items, total, page, limit }
}
}