perf: 完善订单管理
This commit is contained in:
37
packages/server/src/admin/admin.controller.ts
Normal file
37
packages/server/src/admin/admin.controller.ts
Normal 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 }
|
||||
}
|
||||
}
|
||||
7
packages/server/src/admin/admin.module.ts
Normal file
7
packages/server/src/admin/admin.module.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Module } from '@nestjs/common'
|
||||
import { AdminController } from './admin.controller'
|
||||
|
||||
@Module({
|
||||
controllers: [AdminController],
|
||||
})
|
||||
export class AdminModule {}
|
||||
@@ -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],
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user