feat: 多班级版班级管理系统 v2.0

技术栈:Go (Gin + GORM) + PHP + MySQL 5.7 + Redis

主要功能:
- 多班级完全隔离(class_id 贯穿全系统)
- 后端从 Python FastAPI 重写为 Go Gin(端口 56789)
- 超级管理员独立登录(env 配置路径,默认账密 admin/Admin123)
- 科任老师/课代表新角色
- 课代表作业管理页面
- 排行榜分项排行(操行分/考勤/作业)
- 角色加减分上下限由班主任配置
- 家长改密功能(可开关)
- 班级角色按需开关
- 宿舍号格式:南0-000
- 周度/月度重置功能
- MySQL 5.7 兼容
- Nginx 反向代理部署

开发者: Canglan
版权归属: Sea Network Technology Studio
许可证: Apache License 2.0
This commit is contained in:
2026-06-22 10:21:52 +08:00
commit 124d7f645e
140 changed files with 21103 additions and 0 deletions

View File

@@ -0,0 +1,291 @@
// ===========================================
// 多班级版班级管理系统 - Go 后端
//
// 开发者: Canglan
// 联系方式: admin@sea-studio.top
// 版权归属: Sea Network Technology Studio
// 许可证: Apache License 2.0
//
// 版权所有 © Sea Network Technology Studio
// ===========================================
package repository
import (
"fmt"
"time"
"gorm.io/gorm"
"hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/model"
)
// SemesterRepo 学期数据访问层
type SemesterRepo struct {
db *gorm.DB
}
// NewSemesterRepo 创建学期 Repository
func NewSemesterRepo(db *gorm.DB) *SemesterRepo {
return &SemesterRepo{db: db}
}
// GetDB 获取底层数据库连接(用于事务操作)
func (r *SemesterRepo) GetDB() *gorm.DB {
return r.db
}
// Create 创建学期
func (r *SemesterRepo) Create(semester *model.Semester) (int, error) {
if err := r.db.Create(semester).Error; err != nil {
return 0, err
}
return semester.SemesterID, nil
}
// GetByID 根据ID获取学期信息
func (r *SemesterRepo) GetByID(semesterID int) (*model.Semester, error) {
var semester model.Semester
if err := r.db.Where("semester_id = ?", semesterID).First(&semester).Error; err != nil {
return nil, err
}
return &semester, nil
}
// GetAll 获取所有学期列表
func (r *SemesterRepo) GetAll() ([]model.Semester, error) {
var semesters []model.Semester
if err := r.db.Order("created_at DESC").Find(&semesters).Error; err != nil {
return nil, err
}
return semesters, nil
}
// GetActive 获取当前活跃学期(优先 is_active 标记,降级为日期范围匹配)
func (r *SemesterRepo) GetActive() (*model.Semester, error) {
var semester model.Semester
// 第一优先级is_active 标记
if err := r.db.Where("is_active = 1 AND is_archived = 0").
Limit(1).First(&semester).Error; err == nil {
return &semester, nil
}
// 第二优先级:日期范围匹配
today := time.Now().Format("2006-01-02")
if err := r.db.Where("is_archived = 0 AND start_date <= ? AND (end_date IS NULL OR end_date >= ?)", today, today).
Limit(1).First(&semester).Error; err != nil {
return nil, err
}
return &semester, nil
}
// DeactivateAll 将所有学期设为非活跃
func (r *SemesterRepo) DeactivateAll() error {
return r.db.Model(&model.Semester{}).
Where("is_active = 1").
Update("is_active", 0).Error
}
// Activate 设为当前活跃学期
func (r *SemesterRepo) Activate(semesterID int) error {
return r.db.Model(&model.Semester{}).
Where("semester_id = ? AND is_archived = 0", semesterID).
Update("is_active", 1).Error
}
// Archive 归档学期
func (r *SemesterRepo) Archive(semesterID int) error {
return r.db.Model(&model.Semester{}).
Where("semester_id = ? AND is_archived = 0", semesterID).
Updates(map[string]interface{}{
"is_archived": 1,
"is_active": 0,
}).Error
}
// Update 编辑学期信息(仅未归档)
func (r *SemesterRepo) Update(semesterID int, updates map[string]interface{}) error {
if len(updates) == 0 {
return nil
}
return r.db.Model(&model.Semester{}).
Where("semester_id = ? AND is_archived = 0", semesterID).
Updates(updates).Error
}
// Delete 删除学期
func (r *SemesterRepo) Delete(semesterID int) error {
return r.db.Where("semester_id = ?", semesterID).Delete(&model.Semester{}).Error
}
// CountArchives 统计学期归档数据数量
func (r *SemesterRepo) CountArchives(semesterID int) (int64, error) {
var count int64
if err := r.db.Model(&model.SemesterArchive{}).
Where("semester_id = ?", semesterID).
Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
// CountRecordsBySemester 统计学期关联的记录数
func (r *SemesterRepo) CountRecordsBySemester(semesterID int) (conductCount, attendanceCount int64, err error) {
if err = r.db.Model(&model.ConductRecord{}).
Where("semester_id = ?", semesterID).
Count(&conductCount).Error; err != nil {
return 0, 0, err
}
if err = r.db.Model(&model.AttendanceRecord{}).
Where("semester_id = ?", semesterID).
Count(&attendanceCount).Error; err != nil {
return 0, 0, err
}
return conductCount, attendanceCount, nil
}
// AssociateRecordsByDateRange 按日期范围关联记录到学期
func (r *SemesterRepo) AssociateRecordsByDateRange(semesterID int, startDate, endDate string) (conductCount, attendanceCount int64, err error) {
if startDate == "" || endDate == "" {
return 0, 0, fmt.Errorf("日期范围不能为空")
}
// 关联操行分记录
result := r.db.Model(&model.ConductRecord{}).
Where("semester_id IS NULL AND created_at BETWEEN ? AND CONCAT(?, ' 23:59:59')", startDate, endDate).
Update("semester_id", semesterID)
if result.Error != nil {
return 0, 0, result.Error
}
conductCount = result.RowsAffected
// 关联考勤记录
result = r.db.Model(&model.AttendanceRecord{}).
Where("semester_id IS NULL AND `date` BETWEEN ? AND ?", startDate, endDate).
Update("semester_id", semesterID)
if result.Error != nil {
return conductCount, 0, result.Error
}
attendanceCount = result.RowsAffected
return conductCount, attendanceCount, nil
}
// GetConductRecordSemesterID 获取操行分记录所属的学期ID
func (r *SemesterRepo) GetConductRecordSemesterID(recordID int64) (*int, error) {
var record model.ConductRecord
if err := r.db.Where("record_id = ?", recordID).First(&record).Error; err != nil {
return nil, err
}
return record.SemesterID, nil
}
// ========== 学期归档操作 ==========
// BatchCreateArchives 批量创建归档快照
func (r *SemesterRepo) BatchCreateArchives(archives []model.SemesterArchive) error {
if len(archives) == 0 {
return nil
}
return r.db.Create(&archives).Error
}
// DeleteArchivesBySemester 删除指定学期的所有归档数据
func (r *SemesterRepo) DeleteArchivesBySemester(semesterID int) error {
return r.db.Where("semester_id = ?", semesterID).Delete(&model.SemesterArchive{}).Error
}
// GetArchivesBySemester 获取学期的归档数据
func (r *SemesterRepo) GetArchivesBySemester(semesterID int, classID int, page, pageSize int) ([]model.SemesterArchive, int64, error) {
var archives []model.SemesterArchive
var total int64
query := r.db.Model(&model.SemesterArchive{}).Where("semester_id = ?", semesterID)
if classID > 0 {
query = query.Where("class_id = ?", classID)
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
offset := (page - 1) * pageSize
if err := query.Order("rank_position ASC").
Limit(pageSize).
Offset(offset).
Find(&archives).Error; err != nil {
return nil, 0, err
}
return archives, total, nil
}
// GetArchivesByStudent 获取学生在所有已归档学期的数据
func (r *SemesterRepo) GetArchivesByStudent(studentID int) ([]model.SemesterArchive, error) {
var archives []model.SemesterArchive
if err := r.db.Table("semester_archives sa").
Select("sa.archive_id, sa.semester_id, sa.student_id, sa.student_no, "+
"sa.student_name, sa.final_points, sa.rank_position, "+
"sa.total_students, sa.attendance_present, sa.attendance_absent, "+
"sa.attendance_late, sa.attendance_leave, "+
"sa.homework_submitted, sa.homework_not_submitted, sa.homework_late, "+
"sa.archived_at, s.semester_name, s.start_date, s.end_date").
Joins("JOIN semesters s ON sa.semester_id = s.semester_id").
Where("sa.student_id = ?", studentID).
Order("sa.archived_at DESC").
Find(&archives).Error; err != nil {
return nil, err
}
return archives, nil
}
// ========== 周期归档操作 ==========
// GetPeriodArchives 获取周期归档列表
func (r *SemesterRepo) GetPeriodArchives(classID int, periodType string, page, pageSize int) ([]model.PeriodArchive, int64, error) {
var archives []model.PeriodArchive
var total int64
query := r.db.Model(&model.PeriodArchive{}).
Where("class_id = ? AND period_type = ?", classID, periodType)
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
offset := (page - 1) * pageSize
if err := query.Order("archived_at DESC, period_label DESC, rank_position ASC").
Limit(pageSize).
Offset(offset).
Find(&archives).Error; err != nil {
return nil, 0, err
}
return archives, total, nil
}
// GetPeriodArchiveLabels 获取班级的所有周期归档标签(按时间倒序去重)
func (r *SemesterRepo) GetPeriodArchiveLabels(classID int, periodType string) ([]string, error) {
var labels []string
if err := r.db.Model(&model.PeriodArchive{}).
Where("class_id = ? AND period_type = ?", classID, periodType).
Distinct("period_label").
Order("period_label DESC").
Pluck("period_label", &labels).Error; err != nil {
return nil, err
}
return labels, nil
}
// GetLatestPeriodArchiveLabel 获取指定班级最近一次周期归档的标签
func (r *SemesterRepo) GetLatestPeriodArchiveLabel(classID int, periodType string) (string, error) {
var archive model.PeriodArchive
if err := r.db.Where("class_id = ? AND period_type = ?", classID, periodType).
Order("archived_at DESC").
Limit(1).
First(&archive).Error; err != nil {
return "", err
}
return archive.PeriodLabel, nil
}