// =========================================== // 多班级版班级管理系统 - 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" ) // AttendanceService 考勤服务 type AttendanceService struct { attendanceRepo *repository.AttendanceRepo studentRepo *repository.StudentRepo userRepo *repository.UserRepo conductRepo *repository.ConductRepo semesterRepo *repository.SemesterRepo settingRepo *repository.SystemSettingRepo classRepo *repository.ClassRepo } // NewAttendanceService 创建考勤服务 func NewAttendanceService( attendanceRepo *repository.AttendanceRepo, studentRepo *repository.StudentRepo, userRepo *repository.UserRepo, conductRepo *repository.ConductRepo, semesterRepo *repository.SemesterRepo, settingRepo *repository.SystemSettingRepo, classRepo *repository.ClassRepo, ) *AttendanceService { return &AttendanceService{ attendanceRepo: attendanceRepo, studentRepo: studentRepo, userRepo: userRepo, conductRepo: conductRepo, semesterRepo: semesterRepo, settingRepo: settingRepo, classRepo: classRepo, } } // CreateRecord 创建考勤记录 func (s *AttendanceService) CreateRecord(studentID int, dateStr, slot, status string, reason *string, applyDeduction bool, customDeduction *int, recorderID int, classID int) (map[string]interface{}, error) { // 校验学生是否属于当前班级(#7) student, err := s.studentRepo.GetByID(studentID) if err != nil || student == nil || student.ClassID != classID { return map[string]interface{}{"success": false, "message": "学生不属于当前班级"}, nil } // 解析日期 parsedDate, err := time.Parse("2006-01-02", dateStr) if err != nil { return map[string]interface{}{"success": false, "message": "日期格式错误"}, nil } // 获取活跃学期 var semesterID *int activeSemester, _ := s.semesterRepo.GetActive() if activeSemester != nil { semesterID = &activeSemester.SemesterID } record := &model.AttendanceRecord{ StudentID: studentID, Date: parsedDate, Slot: slot, Status: status, Reason: reason, RecorderID: recorderID, SemesterID: semesterID, } createResult, err := s.attendanceRepo.CreateRecord(record) if err != nil { return map[string]interface{}{"success": false, "message": "添加考勤记录失败"}, nil } attendanceID := createResult.AttendanceID // 更新已有记录时,先撤销旧扣分再应用新扣分 if createResult.IsUpdate && createResult.OldDeductionApplied == 1 && createResult.OldDeductionRecordID != nil { if err := s.conductRepo.RevokeRecord(*createResult.OldDeductionRecordID, recorderID); err != nil { logger.Sugared.Errorf("撤销旧考勤扣分失败: attendance_id=%d, old_record_id=%d, err=%v", attendanceID, *createResult.OldDeductionRecordID, err) return nil, fmt.Errorf("撤销旧扣分失败,操作已中止以避免双重扣分: %w", err) } } // 应用扣分(事务保护,避免数据不一致) if applyDeduction && (status == "absent" || status == "late" || status == "leave") { // 校验自定义扣分值必须为非负数 if customDeduction != nil && *customDeduction < 0 { return map[string]interface{}{"success": false, "message": "自定义扣分值不能为负数"}, nil } var pointsChange int if customDeduction != nil { pointsChange = -*customDeduction } else { pointsChange = s.getDeductionPoints(classID, status) } if pointsChange == 0 { return map[string]interface{}{"success": true, "message": "考勤记录添加成功(不扣分)"}, nil } // 获取操作人姓名 recorderName := "班主任" user, err := s.userRepo.GetByUserID(recorderID) if err == nil && user != nil { recorderName = user.RealName } statusText := map[string]string{ "absent": "缺勤", "late": "迟到", "leave": "请假", }[status] // 使用事务确保操行分记录创建、总分更新、考勤标记的原子性 db := s.semesterRepo.GetDB() txErr := db.Transaction(func(tx *gorm.DB) error { conductRecord := &model.ConductRecord{ StudentID: studentID, PointsChange: pointsChange, Reason: fmt.Sprintf("考勤:%s", statusText), RecorderID: recorderID, RecorderName: &recorderName, RelatedType: "attendance", RelatedID: &attendanceID, SemesterID: semesterID, } if err := tx.Create(conductRecord).Error; err != nil { return err } if err := tx.Model(&model.Student{}). Where("student_id = ?", studentID). Update("total_points", gorm.Expr("GREATEST(total_points + ?, 0)", pointsChange)).Error; err != nil { return err } if err := tx.Model(&model.AttendanceRecord{}). Where("attendance_id = ?", attendanceID). Updates(map[string]interface{}{ "deduction_applied": 1, "deduction_record_id": conductRecord.RecordID, }).Error; err != nil { return err } return nil }) if txErr != nil { logger.Sugared.Errorf("考勤扣分事务失败: attendance_id=%d, student_id=%d, err=%v", attendanceID, studentID, txErr) return map[string]interface{}{ "success": false, "message": "考勤记录添加成功,但扣分失败,请手动处理", "attendance_id": attendanceID, "deduction_failed": true, }, nil } logger.Sugared.Infof("用户[%d] 添加考勤记录[%d] -> %s (扣%d分)", recorderID, attendanceID, status, -pointsChange) } return map[string]interface{}{"success": true, "message": "考勤记录添加成功"}, nil } // GetRecords 获取考勤记录 func (s *AttendanceService) GetRecords(classID int, date string, studentID *int, slot string) (map[string]interface{}, error) { records, err := s.attendanceRepo.GetClassRecords(classID, date, derefInt(studentID), slot) if err != nil { return nil, err } return map[string]interface{}{"records": records}, nil } // getClassSettingValue 从 class_settings 读取设置值,若无则返回默认值 func (s *AttendanceService) getClassSettingValue(classID int, key, defaultVal string) string { if classID > 0 && s.classRepo != nil { setting, err := s.classRepo.GetSetting(classID, key) if err == nil && setting != nil && setting.SettingValue != "" { return setting.SettingValue } } return defaultVal } // getDeductionPoints 获取考勤扣分数值(优先从 class_settings 读取班级级配置) func (s *AttendanceService) getDeductionPoints(classID int, status string) int { switch status { case "absent": val := s.getClassSettingValue(classID, "deduction_attendance_absent", "3") if v, err := strconv.Atoi(val); err == nil { return -v } return -3 case "late": val := s.getClassSettingValue(classID, "deduction_attendance_late", "1") if v, err := strconv.Atoi(val); err == nil { return -v } return -1 case "leave": val := s.getClassSettingValue(classID, "deduction_attendance_leave", "0") if v, err := strconv.Atoi(val); err == nil { return -v } return 0 default: return 0 } }