Files
SharedClassManager/backend-go/internal/service/conduct_service.go
canglan 124d7f645e feat: 多班级版班级管理系统 v2.0
技术栈: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
2026-06-22 10:21:52 +08:00

385 lines
12 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"
"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
}