技术栈: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
227 lines
7.3 KiB
Go
227 lines
7.3 KiB
Go
// ===========================================
|
||
// 多班级版班级管理系统 - 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
|
||
}
|
||
}
|