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:
384
backend-go/internal/service/conduct_service.go
Normal file
384
backend-go/internal/service/conduct_service.go
Normal file
@@ -0,0 +1,384 @@
|
||||
// ===========================================
|
||||
// 多班级版班级管理系统 - 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
|
||||
}
|
||||
Reference in New Issue
Block a user