feat(server): add auth, user, and studio modules

Auth: WeChat login, JWT, roles guard (24 tests passing)
User: profile CRUD, training stats with month/total calculations
Studio: config management with auto-default creation
This commit is contained in:
richarjiang
2026-04-02 12:12:18 +08:00
parent e653580155
commit a1a91f96d8
23 changed files with 1284 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
import { Injectable, NotFoundException } from '@nestjs/common'
import { MembershipStatus, BookingStatus, UserRole } from '@mp-pilates/shared'
import { PrismaService } from '../prisma/prisma.service'
import type { UserProfileResponse, UserStatsResponse } from '@mp-pilates/shared'
@Injectable()
export class UserService {
constructor(private readonly prisma: PrismaService) {}
async getProfile(userId: string): Promise<UserProfileResponse> {
const user = await this.prisma.user.findUnique({
where: { id: userId },
include: {
_count: {
select: {
memberships: {
where: { status: MembershipStatus.ACTIVE },
},
},
},
},
})
if (!user) {
throw new NotFoundException('User not found')
}
return {
id: user.id,
phone: user.phone,
nickname: user.nickname,
avatarUrl: user.avatarUrl,
role: user.role as UserRole,
activeMembershipCount: user._count.memberships,
createdAt: user.createdAt.toISOString(),
}
}
async updateProfile(
userId: string,
dto: { nickname?: string; avatarUrl?: string },
): Promise<UserProfileResponse> {
const updated = await this.prisma.user.update({
where: { id: userId },
data: {
...(dto.nickname !== undefined && { nickname: dto.nickname }),
...(dto.avatarUrl !== undefined && { avatarUrl: dto.avatarUrl }),
},
include: {
_count: {
select: {
memberships: {
where: { status: MembershipStatus.ACTIVE },
},
},
},
},
})
return {
id: updated.id,
phone: updated.phone,
nickname: updated.nickname,
avatarUrl: updated.avatarUrl,
role: updated.role as UserRole,
activeMembershipCount: updated._count.memberships,
createdAt: updated.createdAt.toISOString(),
}
}
async getStats(userId: string): Promise<UserStatsResponse> {
const completedBookings = await this.prisma.booking.findMany({
where: {
userId,
status: BookingStatus.COMPLETED,
},
include: {
timeSlot: {
select: {
date: true,
startTime: true,
endTime: true,
},
},
},
})
const now = new Date()
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1)
const monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999)
const monthBookings = completedBookings.filter((b) => {
const slotDate = new Date(b.timeSlot.date)
return slotDate >= monthStart && slotDate <= monthEnd
})
const totalDays = new Set(
completedBookings.map((b) => b.timeSlot.date.toISOString().split('T')[0]),
).size
const monthDays = new Set(
monthBookings.map((b) => b.timeSlot.date.toISOString().split('T')[0]),
).size
const monthHours = monthBookings.reduce((sum, b) => {
const [startH, startM] = b.timeSlot.startTime.split(':').map(Number)
const [endH, endM] = b.timeSlot.endTime.split(':').map(Number)
const durationMinutes = endH * 60 + endM - (startH * 60 + startM)
return sum + durationMinutes / 60
}, 0)
return {
totalBookings: completedBookings.length,
totalDays,
monthBookings: monthBookings.length,
monthDays,
monthHours,
}
}
}