Files
SharedClassManager/backend-go/internal/service/attendance_service.go
canglan d6dec878bd feat: 多班级版 v2.0 - Go后端重写 + 43轮代码审查
- 后端从 Python FastAPI 重写为 Go Gin(端口 56789)
- 多班级完全隔离
- 超级管理员独立登录
- 课代表作业管理、排行榜分项排行
- 角色加减分上下限可配置
- 家长改密功能(可开关)
- 周度/月度重置功能
- MySQL 5.7 兼容
- 43轮代码审查+全部修复
- Apache 2.0 许可证
2026-06-22 10:06:10 +08:00

227 lines
7.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ===========================================
// 多班级版班级管理系统 - 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
}
}