feat: 恭学教育基地管理系统初始提交
- 后端: NestJS 11 + TypeORM + JWT认证 + SQLite/MySQL - 前端: React 19 + Ant Design 6 + Vite 8 + ECharts - 功能模块: 数据面板、学生管理、宿舍管理、入住管理、费用录入、账单管理、教室管理、押金管理、操作日志、账号管理 - 支持Docker一键部署
This commit is contained in:
36
backend/src/entities/bill-item.entity.ts
Normal file
36
backend/src/entities/bill-item.entity.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { Bill } from './bill.entity';
|
||||
|
||||
@Entity('bill_items')
|
||||
export class BillItem {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ name: 'bill_id' })
|
||||
billId: number;
|
||||
|
||||
@Column({ name: 'room_id', nullable: true })
|
||||
roomId: number;
|
||||
|
||||
@Column({ name: 'expense_type', length: 20, nullable: true })
|
||||
expenseType: string;
|
||||
|
||||
@Column({ length: 200, nullable: true })
|
||||
description: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
days: number;
|
||||
|
||||
@Column({ name: 'total_room_days', nullable: true })
|
||||
totalRoomDays: number;
|
||||
|
||||
@Column({ name: 'room_total_amount', type: 'decimal', precision: 10, scale: 2, nullable: true })
|
||||
roomTotalAmount: number;
|
||||
|
||||
@Column({ name: 'student_amount', type: 'decimal', precision: 10, scale: 2, nullable: true })
|
||||
studentAmount: number;
|
||||
|
||||
@ManyToOne(() => Bill, (b) => b.items)
|
||||
@JoinColumn({ name: 'bill_id' })
|
||||
bill: Bill;
|
||||
}
|
||||
40
backend/src/entities/bill.entity.ts
Normal file
40
backend/src/entities/bill.entity.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, JoinColumn, OneToMany } from 'typeorm';
|
||||
import { Student } from './student.entity';
|
||||
import { BillItem } from './bill-item.entity';
|
||||
|
||||
@Entity('bills')
|
||||
export class Bill {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ name: 'student_id' })
|
||||
studentId: number;
|
||||
|
||||
@Column({ name: 'period_start', type: 'date' })
|
||||
periodStart: string;
|
||||
|
||||
@Column({ name: 'period_end', type: 'date' })
|
||||
periodEnd: string;
|
||||
|
||||
@Column({ name: 'shared_amount', type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||
sharedAmount: number;
|
||||
|
||||
@Column({ name: 'personal_amount', type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||
personalAmount: number;
|
||||
|
||||
@Column({ name: 'total_amount', type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||
totalAmount: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 20, default: 'draft' })
|
||||
status: string;
|
||||
|
||||
@CreateDateColumn({ name: 'generated_at' })
|
||||
generatedAt: Date;
|
||||
|
||||
@ManyToOne(() => Student, (s) => s.bills)
|
||||
@JoinColumn({ name: 'student_id' })
|
||||
student: Student;
|
||||
|
||||
@OneToMany(() => BillItem, (bi) => bi.bill)
|
||||
items: BillItem[];
|
||||
}
|
||||
58
backend/src/entities/classroom-rental.entity.ts
Normal file
58
backend/src/entities/classroom-rental.entity.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn, Index } from 'typeorm';
|
||||
import { Classroom } from './classroom.entity';
|
||||
import { Tenant } from './tenant.entity';
|
||||
|
||||
@Entity('classroom_rentals')
|
||||
@Index(['classroomId', 'startDate', 'endDate'])
|
||||
export class ClassroomRental {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ name: 'classroom_id' })
|
||||
classroomId: number;
|
||||
|
||||
@ManyToOne(() => Classroom)
|
||||
@JoinColumn({ name: 'classroom_id' })
|
||||
classroom: Classroom;
|
||||
|
||||
@Column({ name: 'tenant_id' })
|
||||
tenantId: number;
|
||||
|
||||
@ManyToOne(() => Tenant)
|
||||
@JoinColumn({ name: 'tenant_id' })
|
||||
tenant: Tenant;
|
||||
|
||||
@Column({ name: 'start_date', type: 'date' })
|
||||
startDate: string;
|
||||
|
||||
@Column({ name: 'end_date', type: 'date' })
|
||||
endDate: string;
|
||||
|
||||
// 合同 PDF 相对路径(相对 UPLOAD_DIR),仅存文件名
|
||||
@Column({ name: 'contract_path', length: 255, nullable: true })
|
||||
contractPath: string;
|
||||
|
||||
@Column({ name: 'contract_original_name', length: 255, nullable: true })
|
||||
contractOriginalName: string;
|
||||
|
||||
@Column({ name: 'daily_rate', type: 'decimal', precision: 10, scale: 2, nullable: true })
|
||||
dailyRate: number;
|
||||
|
||||
@Column({ name: 'total_amount', type: 'decimal', precision: 10, scale: 2, nullable: true })
|
||||
totalAmount: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 20, default: 'active' })
|
||||
status: string; // active / ended / cancelled
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
notes: string;
|
||||
|
||||
@Column({ name: 'created_by', nullable: true })
|
||||
createdBy: number;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
37
backend/src/entities/classroom.entity.ts
Normal file
37
backend/src/entities/classroom.entity.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity('classrooms')
|
||||
export class Classroom {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ length: 50 })
|
||||
name: string;
|
||||
|
||||
@Column({ length: 50, nullable: true })
|
||||
building: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
floor: number;
|
||||
|
||||
@Column({ default: 30 })
|
||||
capacity: number;
|
||||
|
||||
@Column({ name: 'room_type', type: 'varchar', length: 20, default: '大' })
|
||||
roomType: string; // 大 / 次大 / 小
|
||||
|
||||
@Column({ name: 'course_type', length: 50, nullable: true })
|
||||
courseType: string;
|
||||
|
||||
@Column({ length: 50, nullable: true })
|
||||
supervisor: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 20, default: 'available' })
|
||||
status: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
notes: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
}
|
||||
46
backend/src/entities/deposit.entity.ts
Normal file
46
backend/src/entities/deposit.entity.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { Student } from './student.entity';
|
||||
|
||||
@Entity('deposits')
|
||||
export class Deposit {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ name: 'student_id' })
|
||||
studentId: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2, default: 500 })
|
||||
amount: number;
|
||||
|
||||
// paid: 已缴 | refunded: 已退 | deducted: 已扣除(部分或全部)
|
||||
@Column({ type: 'varchar', length: 20, default: 'paid' })
|
||||
status: string;
|
||||
|
||||
@Column({ name: 'paid_date', type: 'date' })
|
||||
paidDate: string;
|
||||
|
||||
@Column({ name: 'refund_date', type: 'date', nullable: true })
|
||||
refundDate: string;
|
||||
|
||||
@Column({ name: 'refund_amount', type: 'decimal', precision: 10, scale: 2, nullable: true })
|
||||
refundAmount: number;
|
||||
|
||||
@Column({ name: 'deduction_amount', type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||
deductionAmount: number;
|
||||
|
||||
@Column({ name: 'deduction_reason', type: 'text', nullable: true })
|
||||
deductionReason: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
notes: string;
|
||||
|
||||
@Column({ name: 'recorded_by', nullable: true })
|
||||
recordedBy: number;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@ManyToOne(() => Student, { eager: true })
|
||||
@JoinColumn({ name: 'student_id' })
|
||||
student: Student;
|
||||
}
|
||||
13
backend/src/entities/index.ts
Normal file
13
backend/src/entities/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export { Student } from './student.entity';
|
||||
export { Room } from './room.entity';
|
||||
export { Occupancy } from './occupancy.entity';
|
||||
export { RoomExpense } from './room-expense.entity';
|
||||
export { PersonalExpense } from './personal-expense.entity';
|
||||
export { Bill } from './bill.entity';
|
||||
export { BillItem } from './bill-item.entity';
|
||||
export { User } from './user.entity';
|
||||
export { OperationLog } from './operation-log.entity';
|
||||
export { Deposit } from './deposit.entity';
|
||||
export { Classroom } from './classroom.entity';
|
||||
export { Tenant } from './tenant.entity';
|
||||
export { ClassroomRental } from './classroom-rental.entity';
|
||||
44
backend/src/entities/occupancy.entity.ts
Normal file
44
backend/src/entities/occupancy.entity.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { Student } from './student.entity';
|
||||
import { Room } from './room.entity';
|
||||
|
||||
@Entity('occupancies')
|
||||
export class Occupancy {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ name: 'student_id' })
|
||||
studentId: number;
|
||||
|
||||
@Column({ name: 'room_id' })
|
||||
roomId: number;
|
||||
|
||||
@Column({ name: 'check_in_date', type: 'date' })
|
||||
checkInDate: string;
|
||||
|
||||
@Column({ name: 'check_out_date', type: 'date', nullable: true })
|
||||
checkOutDate: string;
|
||||
|
||||
@Column({ name: 'billing_start_date', type: 'date' })
|
||||
billingStartDate: string;
|
||||
|
||||
@Column({ name: 'billing_end_date', type: 'date', nullable: true })
|
||||
billingEndDate: string;
|
||||
|
||||
@Column({ name: 'check_out_reason', length: 100, nullable: true })
|
||||
checkOutReason: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
notes: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@ManyToOne(() => Student, (s) => s.occupancies)
|
||||
@JoinColumn({ name: 'student_id' })
|
||||
student: Student;
|
||||
|
||||
@ManyToOne(() => Room, (r) => r.occupancies)
|
||||
@JoinColumn({ name: 'room_id' })
|
||||
room: Room;
|
||||
}
|
||||
40
backend/src/entities/operation-log.entity.ts
Normal file
40
backend/src/entities/operation-log.entity.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity('operation_logs')
|
||||
export class OperationLog {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ name: 'user_id', nullable: true })
|
||||
userId: number;
|
||||
|
||||
@Column({ length: 50, nullable: true })
|
||||
username: string;
|
||||
|
||||
@Column({ length: 50 })
|
||||
module: string;
|
||||
|
||||
@Column({ length: 50 })
|
||||
action: string;
|
||||
|
||||
@Column({ name: 'target_id', nullable: true })
|
||||
targetId: number;
|
||||
|
||||
@Column({ name: 'target_type', length: 50, nullable: true })
|
||||
targetType: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
detail: string;
|
||||
|
||||
@Column({ name: 'ip_address', length: 50, nullable: true })
|
||||
ipAddress: string;
|
||||
|
||||
@Column({ name: 'user_agent', length: 500, nullable: true })
|
||||
userAgent: string;
|
||||
|
||||
@Column({ name: 'status', length: 20, nullable: true, default: 'success' })
|
||||
status: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
}
|
||||
36
backend/src/entities/personal-expense.entity.ts
Normal file
36
backend/src/entities/personal-expense.entity.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { Student } from './student.entity';
|
||||
|
||||
@Entity('personal_expenses')
|
||||
export class PersonalExpense {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ name: 'student_id' })
|
||||
studentId: number;
|
||||
|
||||
@Column({ name: 'room_id', nullable: true })
|
||||
roomId: number;
|
||||
|
||||
@Column({ name: 'expense_type', type: 'varchar', length: 20 })
|
||||
expenseType: string;
|
||||
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2 })
|
||||
amount: number;
|
||||
|
||||
@Column({ name: 'expense_date', type: 'date' })
|
||||
expenseDate: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description: string;
|
||||
|
||||
@Column({ name: 'recorded_by', nullable: true })
|
||||
recordedBy: number;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@ManyToOne(() => Student, (s) => s.personalExpenses)
|
||||
@JoinColumn({ name: 'student_id' })
|
||||
student: Student;
|
||||
}
|
||||
36
backend/src/entities/room-expense.entity.ts
Normal file
36
backend/src/entities/room-expense.entity.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { Room } from './room.entity';
|
||||
|
||||
@Entity('room_expenses')
|
||||
export class RoomExpense {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ name: 'room_id' })
|
||||
roomId: number;
|
||||
|
||||
@Column({ name: 'expense_type', type: 'varchar', length: 20 })
|
||||
expenseType: string;
|
||||
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2 })
|
||||
amount: number;
|
||||
|
||||
@Column({ name: 'period_start', type: 'date' })
|
||||
periodStart: string;
|
||||
|
||||
@Column({ name: 'period_end', type: 'date' })
|
||||
periodEnd: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description: string;
|
||||
|
||||
@Column({ name: 'recorded_by', nullable: true })
|
||||
recordedBy: number;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@ManyToOne(() => Room, (r) => r.roomExpenses)
|
||||
@JoinColumn({ name: 'room_id' })
|
||||
room: Room;
|
||||
}
|
||||
39
backend/src/entities/room.entity.ts
Normal file
39
backend/src/entities/room.entity.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, OneToMany } from 'typeorm';
|
||||
import { Occupancy } from './occupancy.entity';
|
||||
import { RoomExpense } from './room-expense.entity';
|
||||
|
||||
@Entity('rooms')
|
||||
export class Room {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ name: 'room_number', length: 20, unique: true })
|
||||
roomNumber: string;
|
||||
|
||||
@Column({ length: 50, nullable: true })
|
||||
building: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
floor: number;
|
||||
|
||||
@Column()
|
||||
capacity: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 20, default: 'available' })
|
||||
status: string;
|
||||
|
||||
@Column({ name: 'room_type', length: 20, nullable: true })
|
||||
roomType: string;
|
||||
|
||||
@Column({ length: 10, nullable: true })
|
||||
gender: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@OneToMany(() => Occupancy, (o) => o.room)
|
||||
occupancies: Occupancy[];
|
||||
|
||||
@OneToMany(() => RoomExpense, (e) => e.room)
|
||||
roomExpenses: RoomExpense[];
|
||||
}
|
||||
55
backend/src/entities/student.entity.ts
Normal file
55
backend/src/entities/student.entity.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm';
|
||||
import { Occupancy } from './occupancy.entity';
|
||||
import { PersonalExpense } from './personal-expense.entity';
|
||||
import { Bill } from './bill.entity';
|
||||
|
||||
@Entity('students')
|
||||
export class Student {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ length: 50 })
|
||||
name: string;
|
||||
|
||||
@Column({ length: 20, nullable: true })
|
||||
phone: string;
|
||||
|
||||
@Column({ name: 'id_number', length: 30, nullable: true })
|
||||
idNumber: string;
|
||||
|
||||
@Column({ length: 10, nullable: true })
|
||||
gender: string;
|
||||
|
||||
@Column({ length: 20, nullable: true })
|
||||
ethnicity: string;
|
||||
|
||||
@Column({ name: 'emergency_contact', length: 50, nullable: true })
|
||||
emergencyContact: string;
|
||||
|
||||
@Column({ name: 'emergency_phone', length: 20, nullable: true })
|
||||
emergencyPhone: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 20, default: 'active' })
|
||||
status: string;
|
||||
|
||||
@Column({ length: 100, nullable: true })
|
||||
organization: string;
|
||||
|
||||
@Column({ length: 50, nullable: true })
|
||||
supervisor: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
|
||||
@OneToMany(() => Occupancy, (o) => o.student)
|
||||
occupancies: Occupancy[];
|
||||
|
||||
@OneToMany(() => PersonalExpense, (e) => e.student)
|
||||
personalExpenses: PersonalExpense[];
|
||||
|
||||
@OneToMany(() => Bill, (b) => b.student)
|
||||
bills: Bill[];
|
||||
}
|
||||
32
backend/src/entities/tenant.entity.ts
Normal file
32
backend/src/entities/tenant.entity.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity('tenants')
|
||||
export class Tenant {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ length: 100 })
|
||||
name: string;
|
||||
|
||||
@Column({ length: 50, nullable: true })
|
||||
contact: string;
|
||||
|
||||
@Column({ length: 30, nullable: true })
|
||||
phone: string;
|
||||
|
||||
// 可视化颜色(hex),为空时由后端自动分配
|
||||
@Column({ length: 20, nullable: true })
|
||||
color: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
notes: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 20, default: 'active' })
|
||||
status: string; // active / archived
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
34
backend/src/entities/user.entity.ts
Normal file
34
backend/src/entities/user.entity.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity('users')
|
||||
export class User {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ length: 50, unique: true })
|
||||
username: string;
|
||||
|
||||
@Column({ name: 'password_hash', length: 255 })
|
||||
passwordHash: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 20, default: 'operator' })
|
||||
role: string;
|
||||
|
||||
@Column({ length: 50, nullable: true })
|
||||
name: string;
|
||||
|
||||
@Column({ name: 'allowed_menus', type: 'text', nullable: true })
|
||||
allowedMenus: string;
|
||||
|
||||
@Column({ name: 'is_active', default: true })
|
||||
isActive: boolean;
|
||||
|
||||
@Column({ name: 'last_login_at', type: 'datetime', nullable: true })
|
||||
lastLoginAt: Date;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
Reference in New Issue
Block a user