Add all files

This commit is contained in:
taocong45644
2026-05-12 17:33:50 +08:00
parent 92b115da13
commit 853790560e
156 changed files with 40382 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true,
"exclude": ["node_modules", "dist", ".git", "../dist", "../src"]
},
"webpack": true
}
+40
View File
@@ -0,0 +1,40 @@
{
"name": "server",
"version": "1.0.0",
"private": true,
"description": "NestJS server application",
"scripts": {
"build": "nest build",
"dev": "nest start --watch",
"start": "nest start",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.958.0",
"@aws-sdk/lib-storage": "^3.958.0",
"@nestjs/common": "^10.4.15",
"@nestjs/core": "^10.4.15",
"@nestjs/platform-express": "^10.4.15",
"@supabase/supabase-js": "2.95.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.15.1",
"coze-coding-dev-sdk": "^0.7.16",
"dotenv": "^17.2.3",
"drizzle-kit": "^0.31.8",
"drizzle-orm": "^0.45.1",
"drizzle-zod": "^0.8.3",
"express": "5.2.1",
"pg": "^8.16.3",
"rxjs": "^7.8.1",
"zod": "^4.3.5"
},
"devDependencies": {
"@nestjs/cli": "^10.4.9",
"@nestjs/schematics": "^10.2.3",
"@types/express": "5.0.6",
"@types/node": "^22.10.2",
"drizzle-kit": "^0.31.8",
"typescript": "^5.7.2"
}
}
+23
View File
@@ -0,0 +1,23 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from '@/app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('hello')
getHello(): { status: string; data: string } {
return {
status: 'success',
data: this.appService.getHello()
};
}
@Get('health')
getHealth(): { status: string; data: string } {
return {
status: 'success',
data: new Date().toISOString(),
};
}
}
+21
View File
@@ -0,0 +1,21 @@
import { Module } from '@nestjs/common'
import { AuthController, StudentController, RecordController } from '@/controllers'
import { AuthService, StudentService, RecordService } from '@/services'
@Module({
controllers: [AuthController, StudentController, RecordController],
providers: [
AuthService,
RecordService,
{
provide: StudentService,
useFactory: (recordService: RecordService) => {
const studentService = new StudentService()
studentService.setGetRecordsFunc(() => recordService.getAllRecords())
return studentService
},
inject: [RecordService]
}
],
})
export class AppModule {}
+8
View File
@@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello, welcome to coze coding mini-program server!';
}
}
+26
View File
@@ -0,0 +1,26 @@
import { Controller, Post, Body } from '@nestjs/common'
import { AuthService } from '@/services/auth.service'
import { LoginDto } from '@/dto/auth.dto'
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('login')
async login(@Body() dto: LoginDto) {
try {
const userInfo = await this.authService.login(dto.code)
return {
code: 200,
msg: '登录成功',
data: userInfo
}
} catch (error) {
return {
code: 500,
msg: error.message || '登录失败',
data: null
}
}
}
}
+3
View File
@@ -0,0 +1,3 @@
export * from './auth.controller'
export * from './student.controller'
export * from './record.controller'
@@ -0,0 +1,80 @@
import { Controller, Get, Post, Put, Body, Query, Delete, Param } from '@nestjs/common'
import { RecordService } from '@/services/record.service'
import { CreateRecordDto, QueryRecordDto } from '@/dto/record.dto'
@Controller('records')
export class RecordController {
constructor(private readonly recordService: RecordService) {}
@Get()
async findAll(@Query() query: QueryRecordDto) {
try {
const result = await this.recordService.findAll(query)
return {
code: 200,
msg: '获取成功',
data: result
}
} catch (error) {
return {
code: 500,
msg: error.message || '获取失败',
data: { records: [], statistics: {} }
}
}
}
@Post()
async create(@Body() dto: CreateRecordDto) {
try {
const record = await this.recordService.create(dto)
return {
code: 200,
msg: '添加成功',
data: record
}
} catch (error) {
return {
code: 400,
msg: error.message || '添加失败',
data: null
}
}
}
@Put(':id')
async update(@Param('id') id: string, @Body() dto: CreateRecordDto, @Query('openid') openid: string) {
try {
const record = await this.recordService.update(parseInt(id), dto, openid)
return {
code: record ? 200 : 400,
msg: record ? '更新成功' : '更新失败',
data: record
}
} catch (error) {
return {
code: 500,
msg: error.message || '更新失败',
data: null
}
}
}
@Delete(':id')
async delete(@Param('id') id: string, @Query('openid') openid: string) {
try {
const success = await this.recordService.delete(parseInt(id), openid)
return {
code: success ? 200 : 400,
msg: success ? '删除成功' : '删除失败',
data: null
}
} catch (error) {
return {
code: 500,
msg: error.message || '删除失败',
data: null
}
}
}
}
@@ -0,0 +1,87 @@
import { Controller, Get, Post, Put, Delete, Body, Query, Param } from '@nestjs/common'
import { StudentService } from '@/services/student.service'
import { CreateStudentDto, UpdateStudentDto, QueryStudentDto } from '@/dto/student.dto'
@Controller('students')
export class StudentController {
constructor(private readonly studentService: StudentService) {}
@Get()
async findAll(@Query() query: QueryStudentDto) {
try {
const students = await this.studentService.findAll(query)
return {
code: 200,
msg: '获取成功',
data: students
}
} catch (error) {
return {
code: 500,
msg: error.message || '获取失败',
data: []
}
}
}
@Post()
async create(@Body() dto: CreateStudentDto) {
try {
const student = await this.studentService.create(dto)
return {
code: 200,
msg: '添加成功',
data: student
}
} catch (error) {
return {
code: 400,
msg: error.message || '添加失败',
data: null
}
}
}
@Put(':id')
async update(@Param('id') id: string, @Body() dto: UpdateStudentDto) {
try {
const student = await this.studentService.update(parseInt(id), dto)
if (!student) {
return {
code: 404,
msg: '学员不存在',
data: null
}
}
return {
code: 200,
msg: '更新成功',
data: student
}
} catch (error) {
return {
code: 500,
msg: error.message || '更新失败',
data: null
}
}
}
@Delete(':id')
async delete(@Param('id') id: string, @Query('openid') openid: string) {
try {
const success = await this.studentService.delete(parseInt(id), openid)
return {
code: success ? 200 : 400,
msg: success ? '删除成功' : '删除失败',
data: null
}
} catch (error) {
return {
code: 500,
msg: error.message || '删除失败',
data: null
}
}
}
}
+7
View File
@@ -0,0 +1,7 @@
import { IsString, IsNotEmpty } from 'class-validator'
export class LoginDto {
@IsString()
@IsNotEmpty()
code: string
}
+3
View File
@@ -0,0 +1,3 @@
export * from './auth.dto'
export * from './student.dto'
export * from './record.dto'
+45
View File
@@ -0,0 +1,45 @@
import { IsString, IsNotEmpty, IsNumber, IsOptional, Min } from 'class-validator'
export class CreateRecordDto {
@IsString()
@IsNotEmpty()
date: string
@IsString()
@IsNotEmpty()
name: string
@IsNumber()
@Min(0)
duration: number
@IsNumber()
@Min(0)
fee: number
@IsString()
@IsNotEmpty()
openid: string
}
export class QueryRecordDto {
@IsString()
@IsOptional()
openid?: string
@IsString()
@IsOptional()
name?: string
@IsNumber()
@IsOptional()
weekday?: number
@IsString()
@IsOptional()
month?: string
@IsNumber()
@IsOptional()
year?: number
}
+64
View File
@@ -0,0 +1,64 @@
import { IsString, IsNotEmpty, IsOptional, IsNumber, Min, Max } from 'class-validator'
export class CreateStudentDto {
@IsString()
@IsNotEmpty()
studentName: string
@IsString()
@IsNotEmpty()
openid: string
// 0: 正在上课, 1: 已结束上课,默认0
@IsNumber()
@IsOptional()
status?: number
// 总课时
@IsNumber()
@IsOptional()
totalHours?: number
// 备注
@IsString()
@IsOptional()
notes?: string
}
export class UpdateStudentDto {
@IsString()
@IsOptional()
studentName?: string
@IsNumber()
@Min(0)
@Max(1)
@IsOptional()
status?: number
// 总课时
@IsNumber()
@IsOptional()
totalHours?: number
// 备注
@IsString()
@IsOptional()
notes?: string
}
export class QueryStudentDto {
@IsString()
@IsOptional()
openid?: string
// 是否只返回正在上课的学员,0: 返回所有, 1: 只返回正在上课
@IsNumber()
@IsOptional()
activeOnly?: number
// 是否只返回已结束的学员
@IsNumber()
@IsOptional()
graduatedOnly?: number
}
+3
View File
@@ -0,0 +1,3 @@
export * from './user.entity'
export * from './student.entity'
export * from './record.entity'
+9
View File
@@ -0,0 +1,9 @@
export interface ClassRecord {
id: number
date: string
name: string
duration: number
fee: number
createTime: string
openid: string
}
+12
View File
@@ -0,0 +1,12 @@
export interface Student {
id: number
studentName: string
addTime: string
openid: string
// 0: 正在上课, 1: 已结束上课
status: number
// 新增字段
totalHours: number // 总课时
notes: string // 备注
usedHours?: number // 已用课时(查询时计算,不存储)
}
+6
View File
@@ -0,0 +1,6 @@
export interface UserInfo {
openid: string
nickName: string
avatarUrl: string
createTime: string
}
@@ -0,0 +1,23 @@
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class HttpStatusInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
// 如果是 POST 请求且状态码为 201,改为 200
if (request.method === 'POST' && response.statusCode === 201) {
response.status(200);
}
return next.handle();
}
}
+49
View File
@@ -0,0 +1,49 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from '@/app.module';
import * as express from 'express';
import { HttpStatusInterceptor } from '@/interceptors/http-status.interceptor';
function parsePort(): number {
const args = process.argv.slice(2);
const portIndex = args.indexOf('-p');
if (portIndex !== -1 && args[portIndex + 1]) {
const port = parseInt(args[portIndex + 1], 10);
if (!isNaN(port) && port > 0 && port < 65536) {
return port;
}
}
return 3000;
}
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors({
origin: true,
credentials: true,
});
app.setGlobalPrefix('api');
app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ limit: '50mb', extended: true }));
// 全局拦截器:统一将 POST 请求的 201 状态码改为 200
app.useGlobalInterceptors(new HttpStatusInterceptor());
// 1. 开启优雅关闭 Hooks (关键!)
app.enableShutdownHooks();
// 2. 解析端口
const port = parsePort();
try {
await app.listen(port);
console.log(`Server running on http://localhost:${port}`);
} catch (err) {
if (err.code === 'EADDRINUSE') {
console.error(`❌ 端口 \({port} 被占用! 请运行 'npx kill-port \){port}' 然后重试。`);
process.exit(1);
} else {
throw err;
}
}
console.log(`Application is running on: http://localhost:3000`);
}
bootstrap();
+38
View File
@@ -0,0 +1,38 @@
import { Injectable } from '@nestjs/common'
import { UserInfo } from '@/entities'
@Injectable()
export class AuthService {
private users: Map<string, UserInfo> = new Map()
async login(code: string): Promise<UserInfo> {
// 模拟微信登录:通过 code 生成 openid
const openid = `user_${code.substring(0, 8)}_${Date.now()}`
const now = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })
const userInfo: UserInfo = {
openid,
nickName: `用户${openid.substring(5, 9)}`,
avatarUrl: '',
createTime: now
}
this.users.set(openid, userInfo)
console.log('用户登录:', userInfo)
return userInfo
}
async getUserInfo(openid: string): Promise<UserInfo | undefined> {
return this.users.get(openid)
}
async updateUserInfo(openid: string, nickName: string, avatarUrl: string): Promise<UserInfo | undefined> {
const user = this.users.get(openid)
if (user) {
user.nickName = nickName
user.avatarUrl = avatarUrl
return user
}
return undefined
}
}
+4
View File
@@ -0,0 +1,4 @@
export * from './auth.service'
export * from './student.service'
export * from './record.service'
export { RecordQueryResult, RecordStatistics } from './record.service'
+137
View File
@@ -0,0 +1,137 @@
import { Injectable } from '@nestjs/common'
import { ClassRecord } from '@/entities'
import { CreateRecordDto, QueryRecordDto } from '@/dto'
export interface RecordStatistics {
totalDuration: number
totalFee: number
recordCount: number
studentDuration: number
studentFee: number
}
export interface RecordQueryResult {
records: ClassRecord[]
statistics: RecordStatistics
}
@Injectable()
export class RecordService {
private records: ClassRecord[] = []
private idCounter = 1
async create(dto: CreateRecordDto): Promise<ClassRecord> {
const now = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })
const record: ClassRecord = {
id: this.idCounter++,
date: dto.date,
name: dto.name,
duration: dto.duration,
fee: dto.fee,
createTime: now,
openid: dto.openid
}
this.records.push(record)
console.log('添加记录:', record)
return record
}
async findAll(query: QueryRecordDto): Promise<RecordQueryResult> {
let filtered = this.records
// 按用户筛选
if (query.openid) {
filtered = filtered.filter((r) => r.openid === query.openid)
}
// 按学员姓名完全匹配筛选
if (query.name) {
filtered = filtered.filter((r) => r.name === query.name)
}
// 按月份筛选(需要同时匹配年份和月份)
if (query.month) {
filtered = filtered.filter((r) => {
const date = new Date(r.date)
// month 参数格式为 "YYYY-MM"
const [year, month] = query.month!.split('-').map(Number)
return date.getFullYear() === year && date.getMonth() + 1 === month
})
}
// 按年份筛选
if (query.year) {
filtered = filtered.filter((r) => {
const date = new Date(r.date)
return date.getFullYear() === query.year
})
}
// 按日期降序排列
filtered.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
// 计算统计数据
const statistics = this.calculateStatistics(filtered, query.name)
return {
records: filtered,
statistics
}
}
private calculateStatistics(records: ClassRecord[], studentName?: string): RecordStatistics {
const totalDuration = records.reduce((sum, r) => sum + r.duration, 0)
const totalFee = records.reduce((sum, r) => sum + r.fee, 0)
const recordCount = records.length
// 个人统计(如果按姓名筛选)
let studentDuration = 0
let studentFee = 0
if (studentName) {
const studentRecords = records.filter((r) => r.name === studentName)
studentDuration = studentRecords.reduce((sum, r) => sum + r.duration, 0)
studentFee = studentRecords.reduce((sum, r) => sum + r.fee, 0)
}
return {
totalDuration,
totalFee,
recordCount,
studentDuration,
studentFee
}
}
async findOne(id: number): Promise<ClassRecord | undefined> {
return this.records.find((r) => r.id === id)
}
async delete(id: number, openid: string): Promise<boolean> {
const index = this.records.findIndex((r) => r.id === id && r.openid === openid)
if (index !== -1) {
this.records.splice(index, 1)
return true
}
return false
}
async update(id: number, dto: Partial<ClassRecord>, openid: string): Promise<ClassRecord | null> {
const record = this.records.find((r) => r.id === id && r.openid === openid)
if (!record) {
return null
}
if (dto.date !== undefined) record.date = dto.date
if (dto.name !== undefined) record.name = dto.name
if (dto.duration !== undefined) record.duration = dto.duration
if (dto.fee !== undefined) record.fee = dto.fee
return record
}
// 获取所有记录(供其他服务使用)
getAllRecords(): ClassRecord[] {
return this.records
}
}
+124
View File
@@ -0,0 +1,124 @@
import { Injectable } from '@nestjs/common'
import { Student } from '@/entities'
import { CreateStudentDto, UpdateStudentDto, QueryStudentDto } from '@/dto'
// 课时记录接口
interface ClassRecord {
id: number
date: string
name: string
duration: number
fee: number
createTime: string
openid: string
}
// 获取课时记录的函数类型
type GetRecordsFunc = () => ClassRecord[]
@Injectable()
export class StudentService {
private students: Student[] = []
private idCounter = 1
private getRecordsFunc: GetRecordsFunc | null = null
// 设置获取课时记录的函数
setGetRecordsFunc(func: GetRecordsFunc) {
this.getRecordsFunc = func
}
async create(dto: CreateStudentDto): Promise<Student> {
// 检查是否已存在同名学员
const exists = this.students.find(
(s) => s.openid === dto.openid && s.studentName === dto.studentName
)
if (exists) {
throw new Error('该学员已存在')
}
const now = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })
const student: Student = {
id: this.idCounter++,
studentName: dto.studentName,
addTime: now,
openid: dto.openid,
// 默认 status 为 0 (正在上课)
status: dto.status ?? 0,
// 新增字段
totalHours: dto.totalHours ?? 0,
notes: dto.notes ?? ''
}
this.students.push(student)
console.log('添加学员:', student)
return student
}
async findAll(dto: QueryStudentDto): Promise<any[]> {
let result = this.students
// 按 openid 筛选
if (dto.openid) {
result = result.filter((s) => s.openid === dto.openid)
}
// 只返回正在上课的学员
if (dto.activeOnly === 1) {
result = result.filter((s) => s.status === 0)
}
// 只返回已结束的学员
if (dto.graduatedOnly === 1) {
result = result.filter((s) => s.status === 1)
}
// 计算每个学员的已用课时
const records = this.getRecordsFunc ? this.getRecordsFunc() : []
return result.map(student => {
const usedHours = records
.filter(r => r.name === student.studentName && r.openid === student.openid)
.reduce((sum, r) => sum + r.duration, 0)
return {
...student,
usedHours: Math.round(usedHours * 10) / 10 // 保留1位小数
}
})
}
async findOne(id: number): Promise<Student | undefined> {
return this.students.find((s) => s.id === id)
}
async update(id: number, dto: UpdateStudentDto): Promise<Student | null> {
const student = this.students.find((s) => s.id === id)
if (!student) {
return null
}
if (dto.studentName !== undefined) {
student.studentName = dto.studentName
}
if (dto.status !== undefined) {
student.status = dto.status
}
if (dto.totalHours !== undefined) {
student.totalHours = dto.totalHours
}
if (dto.notes !== undefined) {
student.notes = dto.notes
}
return student
}
async delete(id: number, openid: string): Promise<boolean> {
const index = this.students.findIndex((s) => s.id === id && s.openid === openid)
if (index !== -1) {
this.students.splice(index, 1)
return true
}
return false
}
}
+24
View File
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": true,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false,
"paths": {
"@/*": ["./src/*"]
}
}
}