feat: 多班级版班级管理系统 v2.0
技术栈:Go (Gin + GORM) + PHP + MySQL 5.7 + Redis 主要功能: - 多班级完全隔离(class_id 贯穿全系统) - 后端 Go Gin(端口 56789),Nginx 反代 - 超级管理员独立登录(env 配置,默认账密 admin/Admin123) - bcrypt 密码加密(无 PASSWORD_SALT) - 科任老师/课代表新角色 - 课代表作业管理页面 - 排行榜分项排行(操行分/考勤/作业) - 角色加减分上下限由班主任配置 - 家长改密功能(可开关) - 班级角色按需开关 - 宿舍号格式:南0-000 - 周度/月度重置功能 - MySQL 5.7 兼容 - 43 轮代码审查 + 全部修复 开发者: Canglan 版权归属: Sea Network Technology Studio 许可证: Apache License 2.0
This commit is contained in:
665
backend-go/internal/service/semester_service.go
Normal file
665
backend-go/internal/service/semester_service.go
Normal file
@@ -0,0 +1,665 @@
|
||||
// ===========================================
|
||||
// 多班级版班级管理系统 - Go 后端
|
||||
//
|
||||
// 开发者: Canglan
|
||||
// 联系方式: admin@sea-studio.top
|
||||
// 版权归属: Sea Network Technology Studio
|
||||
// 许可证: Apache License 2.0
|
||||
//
|
||||
// 版权所有 © Sea Network Technology Studio
|
||||
// ===========================================
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/model"
|
||||
"hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/repository"
|
||||
"hz-gitea.sea-studio.top/canglan/SharedClassManager/pkg/logger"
|
||||
)
|
||||
|
||||
// SemesterService 学期服务
|
||||
type SemesterService struct {
|
||||
semesterRepo *repository.SemesterRepo
|
||||
studentRepo *repository.StudentRepo
|
||||
classRepo *repository.ClassRepo
|
||||
attendanceRepo *repository.AttendanceRepo
|
||||
assignmentRepo *repository.AssignmentRepo
|
||||
logService *LogService
|
||||
}
|
||||
|
||||
// NewSemesterService 创建学期服务
|
||||
func NewSemesterService(
|
||||
semesterRepo *repository.SemesterRepo,
|
||||
studentRepo *repository.StudentRepo,
|
||||
classRepo *repository.ClassRepo,
|
||||
attendanceRepo *repository.AttendanceRepo,
|
||||
assignmentRepo *repository.AssignmentRepo,
|
||||
logService *LogService,
|
||||
) *SemesterService {
|
||||
return &SemesterService{
|
||||
semesterRepo: semesterRepo,
|
||||
studentRepo: studentRepo,
|
||||
classRepo: classRepo,
|
||||
attendanceRepo: attendanceRepo,
|
||||
assignmentRepo: assignmentRepo,
|
||||
logService: logService,
|
||||
}
|
||||
}
|
||||
|
||||
// ListSemesters 获取学期列表
|
||||
func (s *SemesterService) ListSemesters() (map[string]interface{}, error) {
|
||||
semesters, err := s.semesterRepo.GetAll()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
today := time.Now()
|
||||
for i := range semesters {
|
||||
conductCount, attendanceCount, _ := s.semesterRepo.CountRecordsBySemester(semesters[i].SemesterID)
|
||||
semesters[i].ConductCount = conductCount
|
||||
semesters[i].AttendanceCount = attendanceCount
|
||||
|
||||
// 计算当前周数
|
||||
if semesters[i].IsActive == 1 && semesters[i].StartDate != nil {
|
||||
delta := today.Sub(*semesters[i].StartDate).Hours() / (24 * 7)
|
||||
if delta >= 0 {
|
||||
week := int(delta) + 1
|
||||
semesters[i].CurrentWeek = &week
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"semesters": semesters,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetActiveSemester 获取当前活跃学期
|
||||
func (s *SemesterService) GetActiveSemester() (*model.Semester, error) {
|
||||
return s.semesterRepo.GetActive()
|
||||
}
|
||||
|
||||
// CreateSemester 创建学期
|
||||
func (s *SemesterService) CreateSemester(semesterName string, startDate, endDate *string) (map[string]interface{}, error) {
|
||||
semester := &model.Semester{
|
||||
SemesterName: semesterName,
|
||||
IsActive: 0,
|
||||
IsArchived: 0,
|
||||
}
|
||||
|
||||
if startDate != nil && *startDate != "" {
|
||||
t, err := time.Parse("2006-01-02", *startDate)
|
||||
if err == nil {
|
||||
semester.StartDate = &t
|
||||
}
|
||||
}
|
||||
if endDate != nil && *endDate != "" {
|
||||
t, err := time.Parse("2006-01-02", *endDate)
|
||||
if err == nil {
|
||||
semester.EndDate = &t
|
||||
}
|
||||
}
|
||||
|
||||
semesterID, err := s.semesterRepo.Create(semester)
|
||||
if err != nil {
|
||||
return map[string]interface{}{"success": false, "message": "创建学期失败"}, nil
|
||||
}
|
||||
|
||||
// 如果日期范围包含今天,自动激活
|
||||
if semester.StartDate != nil {
|
||||
today := time.Now()
|
||||
if semester.StartDate.Before(today) || sameDay(*semester.StartDate, today) {
|
||||
if semester.EndDate == nil || semester.EndDate.After(today) || sameDay(*semester.EndDate, today) {
|
||||
_ = s.semesterRepo.DeactivateAll()
|
||||
_ = s.semesterRepo.Activate(semesterID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "学期创建成功",
|
||||
"semester_id": semesterID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ActivateSemester 激活学期
|
||||
func (s *SemesterService) ActivateSemester(semesterID int) error {
|
||||
semester, err := s.semesterRepo.GetByID(semesterID)
|
||||
if err != nil || semester == nil {
|
||||
return fmt.Errorf("学期不存在")
|
||||
}
|
||||
if semester.IsArchived == 1 {
|
||||
return fmt.Errorf("已归档的学期不能设为当前学期")
|
||||
}
|
||||
|
||||
_ = s.semesterRepo.DeactivateAll()
|
||||
return s.semesterRepo.Activate(semesterID)
|
||||
}
|
||||
|
||||
// UpdateSemester 更新学期
|
||||
func (s *SemesterService) UpdateSemester(semesterID int, semesterName, startDate, endDate *string) error {
|
||||
semester, err := s.semesterRepo.GetByID(semesterID)
|
||||
if err != nil || semester == nil {
|
||||
return fmt.Errorf("学期不存在")
|
||||
}
|
||||
if semester.IsArchived == 1 {
|
||||
return fmt.Errorf("已归档的学期不能编辑")
|
||||
}
|
||||
|
||||
updates := make(map[string]interface{})
|
||||
if semesterName != nil {
|
||||
updates["semester_name"] = *semesterName
|
||||
}
|
||||
if startDate != nil {
|
||||
t, err := time.Parse("2006-01-02", *startDate)
|
||||
if err == nil {
|
||||
updates["start_date"] = t
|
||||
}
|
||||
}
|
||||
if endDate != nil {
|
||||
t, err := time.Parse("2006-01-02", *endDate)
|
||||
if err == nil {
|
||||
updates["end_date"] = t
|
||||
}
|
||||
}
|
||||
|
||||
return s.semesterRepo.Update(semesterID, updates)
|
||||
}
|
||||
|
||||
// DeleteSemester 删除学期
|
||||
func (s *SemesterService) DeleteSemester(semesterID int) error {
|
||||
archiveCount, err := s.semesterRepo.CountArchives(semesterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if archiveCount > 0 {
|
||||
return fmt.Errorf("该学期有 %d 条归档数据,无法删除", archiveCount)
|
||||
}
|
||||
return s.semesterRepo.Delete(semesterID)
|
||||
}
|
||||
|
||||
// AssociateRecords 关联记录到学期
|
||||
func (s *SemesterService) AssociateRecords(semesterID int) (map[string]interface{}, error) {
|
||||
semester, err := s.semesterRepo.GetByID(semesterID)
|
||||
if err != nil || semester == nil {
|
||||
return map[string]interface{}{"success": false, "message": "学期不存在"}, nil
|
||||
}
|
||||
if semester.IsArchived == 1 {
|
||||
return map[string]interface{}{"success": false, "message": "已归档的学期不能关联数据"}, nil
|
||||
}
|
||||
if semester.StartDate == nil {
|
||||
return map[string]interface{}{"success": false, "message": "学期未设置开始日期,无法关联数据"}, nil
|
||||
}
|
||||
|
||||
startDate := semester.StartDate.Format("2006-01-02")
|
||||
endDate := time.Now().Format("2006-01-02")
|
||||
if semester.EndDate != nil {
|
||||
endDate = semester.EndDate.Format("2006-01-02")
|
||||
}
|
||||
|
||||
conductCount, attendanceCount, err := s.semesterRepo.AssociateRecordsByDateRange(semesterID, startDate, endDate)
|
||||
if err != nil {
|
||||
return map[string]interface{}{"success": false, "message": "关联记录失败"}, nil
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"success": true,
|
||||
"message": fmt.Sprintf("关联完成:操行分 %d 条,考勤 %d 条", conductCount, attendanceCount),
|
||||
"data": map[string]interface{}{
|
||||
"conduct": conductCount,
|
||||
"attendance": attendanceCount,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ArchiveSemester 归档学期
|
||||
func (s *SemesterService) ArchiveSemester(semesterID, classID int, resetScores bool) (map[string]interface{}, error) {
|
||||
semester, err := s.semesterRepo.GetByID(semesterID)
|
||||
if err != nil || semester == nil {
|
||||
return map[string]interface{}{"success": false, "message": "学期不存在"}, nil
|
||||
}
|
||||
if semester.IsArchived == 1 {
|
||||
return map[string]interface{}{"success": false, "message": "该学期已归档"}, nil
|
||||
}
|
||||
if semester.StartDate == nil {
|
||||
return map[string]interface{}{"success": false, "message": "学期未设置开始日期,无法进行归档"}, nil
|
||||
}
|
||||
if classID == 0 {
|
||||
return map[string]interface{}{"success": false, "message": "未指定班级"}, nil
|
||||
}
|
||||
|
||||
// 获取班级活跃学生
|
||||
students, err := s.studentRepo.GetStudentsByClassID(classID)
|
||||
if err != nil || len(students) == 0 {
|
||||
return map[string]interface{}{"success": false, "message": "没有可归档的学生数据"}, nil
|
||||
}
|
||||
totalStudents := len(students)
|
||||
|
||||
// 查询考勤统计
|
||||
startDate := semester.StartDate.Format("2006-01-02")
|
||||
endDate := time.Now().Format("2006-01-02")
|
||||
if semester.EndDate != nil {
|
||||
endDate = semester.EndDate.Format("2006-01-02")
|
||||
}
|
||||
attendanceStats, _ := s.attendanceRepo.GetAttendanceStatsBySemester(semesterID, startDate, endDate)
|
||||
attendanceMap := make(map[int]map[string]int64)
|
||||
for _, stat := range attendanceStats {
|
||||
if attendanceMap[stat.StudentID] == nil {
|
||||
attendanceMap[stat.StudentID] = make(map[string]int64)
|
||||
}
|
||||
attendanceMap[stat.StudentID][stat.Status] = stat.Count
|
||||
}
|
||||
|
||||
// 查询作业统计
|
||||
homeworkStats, err := s.assignmentRepo.GetHomeworkStatsByDateRange(*semester.StartDate, time.Now())
|
||||
if err != nil {
|
||||
logger.Sugared.Warnf("查询作业统计失败,归档快照中作业数据可能不完整: %v", err)
|
||||
}
|
||||
homeworkMap := make(map[int]map[string]int64)
|
||||
for _, stat := range homeworkStats {
|
||||
if homeworkMap[stat.StudentID] == nil {
|
||||
homeworkMap[stat.StudentID] = make(map[string]int64)
|
||||
}
|
||||
homeworkMap[stat.StudentID][stat.Status] = stat.Count
|
||||
}
|
||||
|
||||
// 使用事务确保归档操作的原子性,并通过行锁防止并发归档
|
||||
db := s.semesterRepo.GetDB()
|
||||
txErr := db.Transaction(func(tx *gorm.DB) error {
|
||||
// 使用 SELECT ... FOR UPDATE 锁定学期记录,防止并发归档
|
||||
var lockedSemester model.Semester
|
||||
if err := tx.Set("gorm:query_option", "FOR UPDATE").
|
||||
Where("semester_id = ?", semesterID).First(&lockedSemester).Error; err != nil {
|
||||
return fmt.Errorf("锁定学期记录失败: %w", err)
|
||||
}
|
||||
if lockedSemester.IsArchived == 1 {
|
||||
return fmt.Errorf("该学期已被其他操作归档")
|
||||
}
|
||||
|
||||
// 删除旧的归档数据
|
||||
if err := tx.Where("semester_id = ?", semesterID).Delete(&model.SemesterArchive{}).Error; err != nil {
|
||||
return fmt.Errorf("删除旧归档数据失败: %w", err)
|
||||
}
|
||||
|
||||
// 创建归档快照(填充考勤和作业统计)
|
||||
var archives []model.SemesterArchive
|
||||
for rank, stu := range students {
|
||||
stuAttendance := attendanceMap[stu.StudentID]
|
||||
stuHomework := homeworkMap[stu.StudentID]
|
||||
archive := model.SemesterArchive{
|
||||
SemesterID: semesterID,
|
||||
ClassID: classID,
|
||||
StudentID: stu.StudentID,
|
||||
StudentNo: stu.StudentNo,
|
||||
StudentName: stu.Name,
|
||||
FinalPoints: stu.TotalPoints,
|
||||
RankPosition: intPtr(rank + 1),
|
||||
TotalStudents: &totalStudents,
|
||||
AttendancePresent: int(stuAttendance["present"]),
|
||||
AttendanceAbsent: int(stuAttendance["absent"]),
|
||||
AttendanceLate: int(stuAttendance["late"]),
|
||||
AttendanceLeave: int(stuAttendance["leave"]),
|
||||
HomeworkSubmitted: int(stuHomework["submitted"]),
|
||||
HomeworkNotSubmitted: int(stuHomework["not_submitted"]),
|
||||
HomeworkLate: int(stuHomework["late"]),
|
||||
}
|
||||
archives = append(archives, archive)
|
||||
}
|
||||
|
||||
if len(archives) > 0 {
|
||||
if err := tx.Create(&archives).Error; err != nil {
|
||||
return fmt.Errorf("创建归档快照失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 归档学期
|
||||
if err := tx.Model(&model.Semester{}).
|
||||
Where("semester_id = ? AND is_archived = 0", semesterID).
|
||||
Updates(map[string]interface{}{"is_archived": 1, "is_active": 0}).Error; err != nil {
|
||||
return fmt.Errorf("归档学期失败: %w", err)
|
||||
}
|
||||
|
||||
// 重置分数(从 class_settings 读取初始分,若无则默认 60)
|
||||
if resetScores {
|
||||
initialPoints := 60
|
||||
var setting model.ClassSetting
|
||||
if err := tx.Where("class_id = ? AND setting_key = ?", classID, "initial_points").First(&setting).Error; err == nil {
|
||||
if v, e := strconv.Atoi(setting.SettingValue); e == nil {
|
||||
initialPoints = v
|
||||
}
|
||||
}
|
||||
if err := tx.Model(&model.Student{}).
|
||||
Where("class_id = ? AND status = 1", classID).
|
||||
Update("total_points", initialPoints).Error; err != nil {
|
||||
return fmt.Errorf("重置分数失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if txErr != nil {
|
||||
logger.Sugared.Errorf("归档事务失败: %v", txErr)
|
||||
return map[string]interface{}{"success": false, "message": "归档失败: " + txErr.Error()}, nil
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "归档成功",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetArchiveRecords 获取归档数据
|
||||
func (s *SemesterService) GetArchiveRecords(semesterID, classID, page, pageSize int) (map[string]interface{}, error) {
|
||||
archives, total, err := s.semesterRepo.GetArchivesBySemester(semesterID, classID, page, pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
totalPages := int(total) / pageSize
|
||||
if int(total)%pageSize > 0 {
|
||||
totalPages++
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"items": archives,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
"total_pages": totalPages,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// sameDay 判断两个时间是否同一天
|
||||
func sameDay(a, b time.Time) bool {
|
||||
return a.Year() == b.Year() && a.YearDay() == b.YearDay()
|
||||
}
|
||||
|
||||
// ========== 周期重置功能 ==========
|
||||
|
||||
// PeriodReset 周度/月度重置
|
||||
// 1. 创建当前操行分快照
|
||||
// 2. 将所有学生操行分重置为 class_settings.initial_points
|
||||
// 3. 记录操作日志
|
||||
func (s *SemesterService) PeriodReset(classID int, period string, operatorID int, operatorName string, ip string) error {
|
||||
periodLabel := generatePeriodLabel(period, time.Now())
|
||||
|
||||
// 读取初始分
|
||||
initialPoints := 60
|
||||
var setting model.ClassSetting
|
||||
if err := s.classRepo.GetDB().Where("class_id = ? AND setting_key = ?", classID, "initial_points").First(&setting).Error; err == nil {
|
||||
if v, e := strconv.Atoi(setting.SettingValue); e == nil {
|
||||
initialPoints = v
|
||||
}
|
||||
}
|
||||
|
||||
// 获取班级活跃学生
|
||||
students, err := s.studentRepo.GetStudentsByClassID(classID)
|
||||
if err != nil || len(students) == 0 {
|
||||
return fmt.Errorf("没有可重置的学生数据")
|
||||
}
|
||||
|
||||
totalStudents := len(students)
|
||||
var archives []model.PeriodArchive
|
||||
for rank, stu := range students {
|
||||
archive := model.PeriodArchive{
|
||||
ClassID: classID,
|
||||
PeriodType: period,
|
||||
PeriodLabel: periodLabel,
|
||||
StudentID: stu.StudentID,
|
||||
StudentNo: stu.StudentNo,
|
||||
StudentName: stu.Name,
|
||||
FinalPoints: stu.TotalPoints,
|
||||
RankPosition: intPtr(rank + 1),
|
||||
TotalStudents: &totalStudents,
|
||||
ResetBy: "manual",
|
||||
OperatorID: &operatorID,
|
||||
}
|
||||
archives = append(archives, archive)
|
||||
}
|
||||
|
||||
// 使用事务确保原子性,并将存在检查移入事务内防止竞态条件
|
||||
db := s.semesterRepo.GetDB()
|
||||
txErr := db.Transaction(func(tx *gorm.DB) error {
|
||||
// 在事务内检查本期是否已有归档数据(防并发重复重置)
|
||||
var existCount int64
|
||||
if err := tx.Model(&model.PeriodArchive{}).
|
||||
Where("class_id = ? AND period_type = ? AND period_label = ?", classID, period, periodLabel).
|
||||
Count(&existCount).Error; err != nil {
|
||||
return fmt.Errorf("检查归档数据失败: %w", err)
|
||||
}
|
||||
if existCount > 0 {
|
||||
return fmt.Errorf("当前周期(%s)已有归档数据,请勿重复重置", periodLabel)
|
||||
}
|
||||
|
||||
// 创建归档快照
|
||||
if len(archives) > 0 {
|
||||
if err := tx.Create(&archives).Error; err != nil {
|
||||
return fmt.Errorf("创建周期归档快照失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 重置分数
|
||||
if err := tx.Model(&model.Student{}).
|
||||
Where("class_id = ? AND status = 1", classID).
|
||||
Update("total_points", initialPoints).Error; err != nil {
|
||||
return fmt.Errorf("重置分数失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if txErr != nil {
|
||||
logger.Sugared.Errorf("周期重置事务失败: %v", txErr)
|
||||
return txErr
|
||||
}
|
||||
|
||||
// 记录操作日志
|
||||
details := fmt.Sprintf("手动执行%s重置,周期标签: %s,影响学生数: %d", periodCN(period), periodLabel, totalStudents)
|
||||
s.logService.WriteOperationLog(
|
||||
operatorID, operatorName, "班主任", "period_reset",
|
||||
nil, nil, &details, &ip, &classID,
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AutoPeriodReset 自动周期重置检查(由定时任务调用)
|
||||
func (s *SemesterService) AutoPeriodReset() {
|
||||
logger.Sugared.Info("开始检查自动周期重置...")
|
||||
|
||||
// 获取所有启用的班级
|
||||
classes, err := s.classRepo.GetAll(false)
|
||||
if err != nil {
|
||||
logger.Sugared.Errorf("获取班级列表失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
for _, cls := range classes {
|
||||
// 读取 reset_frequency
|
||||
var freqSetting model.ClassSetting
|
||||
if err := s.classRepo.GetDB().Where("class_id = ? AND setting_key = ?", cls.ClassID, "reset_frequency").First(&freqSetting).Error; err != nil {
|
||||
continue // 无配置,跳过
|
||||
}
|
||||
freq := freqSetting.SettingValue
|
||||
if freq == "none" || freq == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
shouldReset := false
|
||||
switch freq {
|
||||
case "weekly":
|
||||
// 读取 reset_day_of_week(默认1=周一)
|
||||
resetDay := 1
|
||||
var daySetting model.ClassSetting
|
||||
if err := s.classRepo.GetDB().Where("class_id = ? AND setting_key = ?", cls.ClassID, "reset_day_of_week").First(&daySetting).Error; err == nil {
|
||||
if v, e := strconv.Atoi(daySetting.SettingValue); e == nil && v >= 1 && v <= 7 {
|
||||
resetDay = v
|
||||
}
|
||||
}
|
||||
// Go的Weekday: 0=Sunday, 1=Monday, ..., 6=Saturday
|
||||
// 映射: 1=周一(1) ... 6=周六(6), 7=周日(0)
|
||||
var targetWeekday time.Weekday
|
||||
if resetDay == 7 {
|
||||
targetWeekday = time.Sunday
|
||||
} else {
|
||||
targetWeekday = time.Weekday(resetDay)
|
||||
}
|
||||
if now.Weekday() == targetWeekday {
|
||||
shouldReset = true
|
||||
}
|
||||
case "monthly":
|
||||
// 读取 reset_day_of_month(默认1)
|
||||
resetDay := 1
|
||||
var daySetting model.ClassSetting
|
||||
if err := s.classRepo.GetDB().Where("class_id = ? AND setting_key = ?", cls.ClassID, "reset_day_of_month").First(&daySetting).Error; err == nil {
|
||||
if v, e := strconv.Atoi(daySetting.SettingValue); e == nil && v >= 1 && v <= 28 {
|
||||
resetDay = v
|
||||
}
|
||||
}
|
||||
if now.Day() == resetDay {
|
||||
shouldReset = true
|
||||
}
|
||||
}
|
||||
|
||||
if !shouldReset {
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查今天是否已经重置过
|
||||
periodLabel := generatePeriodLabel(freq, now)
|
||||
var existCount int64
|
||||
if err := s.classRepo.GetDB().Model(&model.PeriodArchive{}).
|
||||
Where("class_id = ? AND period_type = ? AND period_label = ? AND reset_by = ?",
|
||||
cls.ClassID, freq, periodLabel, "auto").
|
||||
Count(&existCount).Error; err != nil {
|
||||
logger.Sugared.Errorf("检查班级 %d 周期归档失败: %v", cls.ClassID, err)
|
||||
continue
|
||||
}
|
||||
if existCount > 0 {
|
||||
logger.Sugared.Infof("班级 %d 本期(%s)已自动重置,跳过", cls.ClassID, periodLabel)
|
||||
continue
|
||||
}
|
||||
|
||||
// 执行自动重置
|
||||
logger.Sugared.Infof("自动重置班级 %d (%s, %s)", cls.ClassID, cls.ClassName, periodLabel)
|
||||
if err := s.autoPeriodResetClass(cls.ClassID, freq, periodLabel); err != nil {
|
||||
logger.Sugared.Errorf("自动重置班级 %d 失败: %v", cls.ClassID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// autoPeriodResetClass 单个班级的自动周期重置
|
||||
func (s *SemesterService) autoPeriodResetClass(classID int, period, periodLabel string) error {
|
||||
initialPoints := 60
|
||||
var setting model.ClassSetting
|
||||
if err := s.classRepo.GetDB().Where("class_id = ? AND setting_key = ?", classID, "initial_points").First(&setting).Error; err == nil {
|
||||
if v, e := strconv.Atoi(setting.SettingValue); e == nil {
|
||||
initialPoints = v
|
||||
}
|
||||
}
|
||||
|
||||
students, err := s.studentRepo.GetStudentsByClassID(classID)
|
||||
if err != nil || len(students) == 0 {
|
||||
return fmt.Errorf("没有可重置的学生数据")
|
||||
}
|
||||
|
||||
totalStudents := len(students)
|
||||
var archives []model.PeriodArchive
|
||||
for rank, stu := range students {
|
||||
archive := model.PeriodArchive{
|
||||
ClassID: classID,
|
||||
PeriodType: period,
|
||||
PeriodLabel: periodLabel,
|
||||
StudentID: stu.StudentID,
|
||||
StudentNo: stu.StudentNo,
|
||||
StudentName: stu.Name,
|
||||
FinalPoints: stu.TotalPoints,
|
||||
RankPosition: intPtr(rank + 1),
|
||||
TotalStudents: &totalStudents,
|
||||
ResetBy: "auto",
|
||||
}
|
||||
archives = append(archives, archive)
|
||||
}
|
||||
|
||||
db := s.semesterRepo.GetDB()
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
if len(archives) > 0 {
|
||||
if err := tx.Create(&archives).Error; err != nil {
|
||||
return fmt.Errorf("创建周期归档快照失败: %w", err)
|
||||
}
|
||||
}
|
||||
if err := tx.Model(&model.Student{}).
|
||||
Where("class_id = ? AND status = 1", classID).
|
||||
Update("total_points", initialPoints).Error; err != nil {
|
||||
return fmt.Errorf("重置分数失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// GetPeriodArchives 获取周期归档列表
|
||||
func (s *SemesterService) GetPeriodArchives(classID int, period string, page, pageSize int) (map[string]interface{}, error) {
|
||||
archives, total, err := s.semesterRepo.GetPeriodArchives(classID, period, page, pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
totalPages := int(total) / pageSize
|
||||
if int(total)%pageSize > 0 {
|
||||
totalPages++
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"items": archives,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
"total_pages": totalPages,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// generatePeriodLabel 生成周期标签
|
||||
func generatePeriodLabel(period string, t time.Time) string {
|
||||
switch period {
|
||||
case "weekly":
|
||||
year, week := t.ISOWeek()
|
||||
return fmt.Sprintf("%d-W%02d", year, week)
|
||||
case "monthly":
|
||||
return t.Format("2006-01")
|
||||
default:
|
||||
return t.Format("2006-01-02")
|
||||
}
|
||||
}
|
||||
|
||||
// periodCN 周期类型的中文描述
|
||||
func periodCN(period string) string {
|
||||
switch period {
|
||||
case "weekly":
|
||||
return "每周"
|
||||
case "monthly":
|
||||
return "每月"
|
||||
default:
|
||||
return period
|
||||
}
|
||||
}
|
||||
|
||||
// PeriodLabelCN 周期类型的中文标签(当前周期)
|
||||
func PeriodLabelCN(period string) string {
|
||||
switch period {
|
||||
case "weekly":
|
||||
return "本周"
|
||||
case "monthly":
|
||||
return "本月"
|
||||
default:
|
||||
return period
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user