// =========================================== // 多班级版班级管理系统 - Go 后端 // // 开发者: Canglan // 联系方式: admin@sea-studio.top // 版权归属: Sea Network Technology Studio // 许可证: Apache License 2.0 // // 版权所有 © Sea Network Technology Studio // =========================================== package service import ( "fmt" "strconv" "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" ) // ConductService 操行分服务 type ConductService struct { conductRepo *repository.ConductRepo studentRepo *repository.StudentRepo adminRoleRepo *repository.AdminRoleRepo semesterRepo *repository.SemesterRepo classRepo *repository.ClassRepo } // NewConductService 创建操行分服务 func NewConductService( conductRepo *repository.ConductRepo, studentRepo *repository.StudentRepo, adminRoleRepo *repository.AdminRoleRepo, semesterRepo *repository.SemesterRepo, classRepo *repository.ClassRepo, ) *ConductService { return &ConductService{ conductRepo: conductRepo, studentRepo: studentRepo, adminRoleRepo: adminRoleRepo, semesterRepo: semesterRepo, classRepo: classRepo, } } // AddPoints 批量加减分 func (s *ConductService) AddPoints(studentIDs []int, pointsChange int, reason string, recorderID int, recorderName string, classID int, relatedType string) (map[string]interface{}, error) { // 输入校验 if len(studentIDs) == 0 || len(studentIDs) > 200 { return map[string]interface{}{"success": false, "message": "学生数量需在1-200之间"}, nil } if reason == "" || len(reason) > 255 { return map[string]interface{}{"success": false, "message": "原因不能为空且不超过255字符"}, nil } if pointsChange == 0 || absInt(pointsChange) > 100 { return map[string]interface{}{"success": false, "message": "分值无效"}, nil } // 获取操作人角色 role, _, err := s.adminRoleRepo.GetUserRoleAndClassID(recorderID) if err != nil { return map[string]interface{}{"success": false, "message": "获取操作人角色失败"}, nil } // 权限验证(从 class_settings 读取限制,这里使用默认值) if err := s.validatePointsPermission(role, pointsChange, classID); err != nil { return map[string]interface{}{"success": false, "message": err.Error()}, nil } return s.addPointsInternal(studentIDs, pointsChange, reason, recorderID, recorderName, classID, relatedType) } // CadreAddPoints 课代表专用加减分(跳过角色权限验证,仅限作业相关扣分) func (s *ConductService) CadreAddPoints(studentIDs []int, pointsChange int, reason string, recorderID int, recorderName string, classID int, relatedType string) (map[string]interface{}, error) { // 输入校验 if len(studentIDs) == 0 || len(studentIDs) > 200 { return map[string]interface{}{"success": false, "message": "学生数量需在1-200之间"}, nil } if reason == "" || len(reason) > 255 { return map[string]interface{}{"success": false, "message": "原因不能为空且不超过255字符"}, nil } if pointsChange >= 0 || absInt(pointsChange) > 100 { return map[string]interface{}{"success": false, "message": "课代表只能进行扣分操作"}, nil } // 强制设置为作业类型 relatedType = "homework" return s.addPointsInternal(studentIDs, pointsChange, reason, recorderID, recorderName, classID, relatedType) } // addPointsInternal 批量加减分内部实现 func (s *ConductService) addPointsInternal(studentIDs []int, pointsChange int, reason string, recorderID int, recorderName string, classID int, relatedType string) (map[string]interface{}, error) { // 自动获取当前活跃学期 activeSemester, semErr := s.semesterRepo.GetActive() if semErr != nil { logger.Sugared.Warnf("获取活跃学期失败,操行分将不关联学期: %v", semErr) } var semesterID *int if activeSemester != nil { semesterID = &activeSemester.SemesterID } if relatedType == "" { relatedType = "manual" } successCount := 0 failCount := 0 var details []map[string]interface{} db := s.semesterRepo.GetDB() for _, studentID := range studentIDs { // 检查学生是否存在 student, err := s.studentRepo.GetByID(studentID) if err != nil || student == nil { failCount++ details = append(details, map[string]interface{}{"student_id": studentID, "error": "学生不存在"}) continue } // 校验学生是否属于当前班级 if student.ClassID != classID { failCount++ details = append(details, map[string]interface{}{"student_id": studentID, "error": "学生不属于当前班级"}) continue } // 使用事务确保记录创建和总分更新的原子性(#3) recordID, txErr := func() (int64, error) { var rid int64 txErr := db.Transaction(func(tx *gorm.DB) error { record := &model.ConductRecord{ StudentID: studentID, PointsChange: pointsChange, Reason: reason, RecorderID: recorderID, RecorderName: &recorderName, RelatedType: relatedType, SemesterID: semesterID, } if err := tx.Create(record).Error; err != nil { return err } rid = record.RecordID 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 } return nil }) return rid, txErr }() if txErr != nil { failCount++ details = append(details, map[string]interface{}{"student_id": studentID, "error": txErr.Error()}) continue } successCount++ details = append(details, map[string]interface{}{"student_id": studentID, "success": true, "record_id": recordID}) logger.Sugared.Infof("用户[%d] 对学生[%d] 进行 %d 分操作", recorderID, studentID, pointsChange) } return map[string]interface{}{ "success": failCount == 0, "success_count": successCount, "fail_count": failCount, "details": details, }, nil } // RevokeRecord 撤销记录(事务保护,避免并发重复撤销) func (s *ConductService) RevokeRecord(recordID int64, revokerID int, classID int) (map[string]interface{}, error) { record, err := s.conductRepo.GetRecordByID(recordID) if err != nil || record == nil { return map[string]interface{}{"success": false, "message": "记录不存在"}, nil } // 校验记录所属学生是否在当前操作者的班级中 student, _ := s.studentRepo.GetByID(record.StudentID) if student == nil || student.ClassID != classID { return map[string]interface{}{"success": false, "message": "无权操作该记录"}, nil } if record.IsRevoked == 1 { return map[string]interface{}{"success": false, "message": "该记录已被撤销"}, nil } db := s.semesterRepo.GetDB() txErr := db.Transaction(func(tx *gorm.DB) error { // 撤销记录 if err := tx.Model(&model.ConductRecord{}). Where("record_id = ? AND is_revoked = 0", recordID). Updates(map[string]interface{}{ "is_revoked": 1, "revoked_by": revokerID, }).Error; err != nil { return err } // 反向恢复学生总分(下限保护) return tx.Model(&model.Student{}). Where("student_id = ?", record.StudentID). Update("total_points", gorm.Expr("GREATEST(total_points + ?, 0)", -record.PointsChange)).Error }) if txErr != nil { return map[string]interface{}{"success": false, "message": "撤销失败"}, nil } return map[string]interface{}{ "success": true, "message": "撤销成功", "record": map[string]interface{}{ "student_id": record.StudentID, "recorder_name": derefStr(record.RecorderName), "points_change": record.PointsChange, }, }, nil } // RestoreRecord 反撤销记录(事务保护,避免并发重复恢复) func (s *ConductService) RestoreRecord(recordID int64, restorerID int, classID int) (map[string]interface{}, error) { record, err := s.conductRepo.GetRecordByID(recordID) if err != nil || record == nil { return map[string]interface{}{"success": false, "message": "记录不存在"}, nil } // 校验记录所属学生是否在当前操作者的班级中 student, _ := s.studentRepo.GetByID(record.StudentID) if student == nil || student.ClassID != classID { return map[string]interface{}{"success": false, "message": "无权操作该记录"}, nil } if record.IsRevoked == 0 { return map[string]interface{}{"success": false, "message": "该记录未被撤销,无需恢复"}, nil } db := s.semesterRepo.GetDB() txErr := db.Transaction(func(tx *gorm.DB) error { // 反撤销 if err := tx.Model(&model.ConductRecord{}). Where("record_id = ? AND is_revoked = 1", recordID). Updates(map[string]interface{}{ "is_revoked": 0, "revoked_by": nil, "revoked_at": nil, }).Error; err != nil { return err } // 恢复学生总分(下限保护) return tx.Model(&model.Student{}). Where("student_id = ?", record.StudentID). Update("total_points", gorm.Expr("GREATEST(total_points + ?, 0)", record.PointsChange)).Error }) if txErr != nil { return map[string]interface{}{"success": false, "message": "反撤销失败"}, nil } return map[string]interface{}{ "success": true, "message": "反撤销成功", }, nil } // GetHistory 获取操行分历史记录 func (s *ConductService) GetHistory(classID int, studentID *int, page, pageSize int, startDate, endDate, relatedType, reasonPrefix string, isRevoked *int, reasonSearch string) (map[string]interface{}, error) { includeRevoked := false if isRevoked != nil && *isRevoked == 1 { includeRevoked = true } offset := (page - 1) * pageSize records, err := s.conductRepo.GetAllRecords(classID, pageSize, offset, startDate, endDate, derefInt(studentID), includeRevoked, relatedType, reasonPrefix, isRevoked, reasonSearch) if err != nil { return nil, err } total, err := s.conductRepo.CountAllRecords(classID, startDate, endDate, derefInt(studentID), includeRevoked, relatedType, reasonPrefix, isRevoked, reasonSearch) if err != nil { return nil, err } totalPages := int(total) / pageSize if int(total)%pageSize > 0 { totalPages++ } return map[string]interface{}{ "records": records, "total": total, "page": page, "page_size": pageSize, "total_pages": totalPages, }, nil } // validatePointsPermission 验证角色加减分权限 func (s *ConductService) validatePointsPermission(role string, pointsChange, classID int) error { // 从 class_settings 读取配置,若无则使用默认值 maxPoints := func(key string, defaultVal int) int { if classID > 0 { setting, err := s.classRepo.GetSetting(classID, key) if err == nil && setting != nil { if v, e := strconv.Atoi(setting.SettingValue); e == nil { return v } } } return defaultVal } switch role { case "班主任": return nil // 无限制 case "班长": maxAdd := maxPoints("point_limit_班长_max", 5) maxSub := maxPoints("point_limit_班长_min", -5) if pointsChange > maxAdd || pointsChange < maxSub { return fmt.Errorf("班长单次只能加%d至减%d分以内", maxAdd, absInt(maxSub)) } case "学习委员": limit := maxPoints("point_limit_学习委员_max", 5) if absInt(pointsChange) > limit { return fmt.Errorf("学习委员单次只能加减%d分以内", limit) } case "科任老师": limit := maxPoints("point_limit_科任老师_max", 5) if absInt(pointsChange) > limit { return fmt.Errorf("科任老师单次只能加减%d分以内", limit) } case "考勤委员": if pointsChange > 0 { return fmt.Errorf("考勤委员只能进行扣分操作") } limit := maxPoints("point_limit_考勤委员_max", 8) if absInt(pointsChange) > limit { return fmt.Errorf("考勤委员单次最多扣%d分", limit) } case "劳动委员": limit := maxPoints("point_limit_劳动委员_max", 1) if absInt(pointsChange) > limit { return fmt.Errorf("劳动委员单次只能加减%d分以内", limit) } case "志愿委员": if pointsChange < 0 { return fmt.Errorf("志愿委员只能加分") } limit := maxPoints("point_limit_志愿委员_max", 5) if pointsChange > limit { return fmt.Errorf("志愿委员单次最多加%d分", limit) } case "课代表": return fmt.Errorf("课代表无权进行此操作") default: return fmt.Errorf("无权进行此操作") } return nil } // absInt 取绝对值 func absInt(x int) int { if x < 0 { return -x } return x }