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:
2026-06-22 10:21:52 +08:00
commit 4a82eff3c6
135 changed files with 19963 additions and 0 deletions

View File

@@ -0,0 +1,294 @@
// ===========================================
// 多班级版班级管理系统 - Go 后端
//
// 开发者: Canglan
// 联系方式: admin@sea-studio.top
// 版权归属: Sea Network Technology Studio
// 许可证: Apache License 2.0
//
// 版权所有 © Sea Network Technology Studio
// ===========================================
package repository
import (
"fmt"
"strings"
"time"
"gorm.io/gorm"
"hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/model"
)
// ConductRepo 操行分记录数据访问层
type ConductRepo struct {
db *gorm.DB
}
// NewConductRepo 创建操行分 Repository
func NewConductRepo(db *gorm.DB) *ConductRepo {
return &ConductRepo{db: db}
}
// CreateRecord 创建操行分记录
func (r *ConductRepo) CreateRecord(record *model.ConductRecord) (int64, error) {
if err := r.db.Create(record).Error; err != nil {
return 0, err
}
return record.RecordID, nil
}
// GetRecordByID 根据ID获取记录含学生信息
func (r *ConductRepo) GetRecordByID(recordID int64) (*model.ConductRecord, error) {
var record model.ConductRecord
if err := r.db.Table("conduct_records cr").
Select("cr.*, s.name as student_name, s.total_points").
Joins("JOIN students s ON cr.student_id = s.student_id").
Where("cr.record_id = ?", recordID).
First(&record).Error; err != nil {
return nil, err
}
return &record, nil
}
// CountStudentRecords 统计学生操行分记录总数
func (r *ConductRepo) CountStudentRecords(studentID int, includeRevoked bool, startDate, endDate string, recorderID int) (int64, error) {
var count int64
query := r.db.Model(&model.ConductRecord{}).Where("student_id = ?", studentID)
if !includeRevoked {
query = query.Where("is_revoked = 0")
}
if startDate != "" {
query = query.Where("DATE(created_at) >= ?", startDate)
}
if endDate != "" {
query = query.Where("DATE(created_at) <= ?", endDate)
}
if recorderID > 0 {
query = query.Where("recorder_id = ?", recorderID)
}
if err := query.Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
// GetStudentRecords 获取学生操行分记录
func (r *ConductRepo) GetStudentRecords(studentID int, limit, offset int, includeRevoked bool, startDate, endDate string, recorderID int) ([]model.ConductRecord, error) {
var records []model.ConductRecord
query := r.db.Table("conduct_records cr").
Select("cr.*, u.real_name as recorder_real").
Joins("LEFT JOIN users u ON cr.recorder_id = u.user_id").
Where("cr.student_id = ?", studentID)
if !includeRevoked {
query = query.Where("cr.is_revoked = 0")
}
if startDate != "" {
query = query.Where("DATE(cr.created_at) >= ?", startDate)
}
if endDate != "" {
query = query.Where("DATE(cr.created_at) <= ?", endDate)
}
if recorderID > 0 {
query = query.Where("cr.recorder_id = ?", recorderID)
}
if err := query.Order("cr.created_at DESC").
Limit(limit).
Offset(offset).
Find(&records).Error; err != nil {
return nil, err
}
return records, nil
}
// GetAllRecords 获取所有记录(管理员用,支持多种过滤条件)
func (r *ConductRepo) GetAllRecords(classID int, limit, offset int, startDate, endDate string,
studentID int, includeRevoked bool, relatedType, reasonPrefix string,
isRevoked *int, reasonSearch string) ([]model.ConductRecord, error) {
var records []model.ConductRecord
query := r.db.Table("conduct_records cr").
Select("cr.*, s.name as student_name, s.student_no, s.class_id, u.real_name as recorder_real, ru.real_name as revoker_name").
Joins("JOIN students s ON cr.student_id = s.student_id").
Joins("JOIN users u ON cr.recorder_id = u.user_id").
Joins("LEFT JOIN users ru ON cr.revoked_by = ru.user_id").
Where("1 = 1")
if !includeRevoked {
query = query.Where("cr.is_revoked = 0")
}
if classID > 0 {
query = query.Where("s.class_id = ?", classID)
}
if studentID > 0 {
query = query.Where("cr.student_id = ?", studentID)
}
if startDate != "" {
query = query.Where("DATE(cr.created_at) >= ?", startDate)
}
if endDate != "" {
query = query.Where("DATE(cr.created_at) <= ?", endDate)
}
if relatedType != "" {
query = query.Where("cr.related_type = ?", relatedType)
}
if reasonPrefix != "" {
query = query.Where("cr.reason LIKE ?", fmt.Sprintf("%s%%", reasonPrefix))
}
if reasonSearch != "" {
escaped := strings.NewReplacer("\\", "\\\\", "%", "\\%", "_", "\\_").Replace(reasonSearch)
query = query.Where("cr.reason LIKE ?", fmt.Sprintf("%%%s%%", escaped))
}
if isRevoked != nil {
query = query.Where("cr.is_revoked = ?", *isRevoked)
}
if err := query.Order("cr.created_at DESC").
Limit(limit).
Offset(offset).
Find(&records).Error; err != nil {
return nil, err
}
return records, nil
}
// CountAllRecords 统计记录总数(与 GetAllRecords 使用相同过滤条件)
func (r *ConductRepo) CountAllRecords(classID int, startDate, endDate string,
studentID int, includeRevoked bool, relatedType, reasonPrefix string,
isRevoked *int, reasonSearch string) (int64, error) {
var count int64
query := r.db.Table("conduct_records cr").
Joins("JOIN students s ON cr.student_id = s.student_id").
Where("1 = 1")
if !includeRevoked {
query = query.Where("cr.is_revoked = 0")
}
if classID > 0 {
query = query.Where("s.class_id = ?", classID)
}
if studentID > 0 {
query = query.Where("cr.student_id = ?", studentID)
}
if startDate != "" {
query = query.Where("DATE(cr.created_at) >= ?", startDate)
}
if endDate != "" {
query = query.Where("DATE(cr.created_at) <= ?", endDate)
}
if relatedType != "" {
query = query.Where("cr.related_type = ?", relatedType)
}
if reasonPrefix != "" {
query = query.Where("cr.reason LIKE ?", fmt.Sprintf("%s%%", reasonPrefix))
}
if reasonSearch != "" {
escaped := strings.NewReplacer("\\", "\\\\", "%", "\\%", "_", "\\_").Replace(reasonSearch)
query = query.Where("cr.reason LIKE ?", fmt.Sprintf("%%%s%%", escaped))
}
if isRevoked != nil {
query = query.Where("cr.is_revoked = ?", *isRevoked)
}
if err := query.Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
// RevokeRecord 撤销单条操行分记录
func (r *ConductRepo) RevokeRecord(recordID int64, revokerID int) error {
return r.db.Model(&model.ConductRecord{}).
Where("record_id = ? AND is_revoked = 0", recordID).
Updates(map[string]interface{}{
"is_revoked": 1,
"revoked_by": revokerID,
}).Error
}
// BatchRevokeRecords 批量撤销记录
func (r *ConductRepo) BatchRevokeRecords(recordIDs []int64, revokerID int) (int64, error) {
result := r.db.Model(&model.ConductRecord{}).
Where("record_id IN ? AND is_revoked = 0", recordIDs).
Updates(map[string]interface{}{
"is_revoked": 1,
"revoked_by": revokerID,
"revoked_at": time.Now(),
})
if result.Error != nil {
return 0, result.Error
}
return result.RowsAffected, nil
}
// BatchRestoreRecords 批量反撤销记录
func (r *ConductRepo) BatchRestoreRecords(recordIDs []int64) (int64, error) {
result := r.db.Model(&model.ConductRecord{}).
Where("record_id IN ? AND is_revoked = 1", recordIDs).
Updates(map[string]interface{}{
"is_revoked": 0,
"revoked_by": nil,
"revoked_at": nil,
})
if result.Error != nil {
return 0, result.Error
}
return result.RowsAffected, nil
}
// AssociateSemester 将记录关联到学期
func (r *ConductRepo) AssociateSemester(recordID int64, semesterID int) error {
return r.db.Model(&model.ConductRecord{}).
Where("record_id = ? AND semester_id IS NULL", recordID).
Update("semester_id", semesterID).Error
}
// GetHomeworkRecords 获取学生作业相关的操行分记录
func (r *ConductRepo) GetHomeworkRecords(studentID int) ([]model.ConductRecord, error) {
var records []model.ConductRecord
if err := r.db.Where("student_id = ? AND related_type = 'homework' AND is_revoked = 0", studentID).
Order("created_at DESC").
Find(&records).Error; err != nil {
return nil, err
}
return records, nil
}
// GetStudentPointsByType 按 related_type 在 SQL 层聚合学生分数(避免全量加载),支持 limit 限制返回数量
func (r *ConductRepo) GetStudentPointsByType(classID int, relatedType string, limit int) ([]struct {
StudentID int
StudentNo string
Name string
TotalPoints int
}, error) {
var results []struct {
StudentID int
StudentNo string
Name string
TotalPoints int
}
err := r.db.Table("conduct_records cr").
Select("cr.student_id, s.student_no, s.name, SUM(cr.points_change) as total_points").
Joins("JOIN students s ON cr.student_id = s.student_id").
Where("s.class_id = ? AND s.status = 1 AND cr.related_type = ? AND cr.is_revoked = 0", classID, relatedType).
Group("cr.student_id, s.student_no, s.name").
Order("total_points DESC").
Limit(limit).
Find(&results).Error
return results, err
}
// GetStudentTotalPoints 获取学生当前总分
func (r *ConductRepo) GetStudentTotalPoints(studentID int) (int, error) {
var student model.Student
if err := r.db.Where("student_id = ?", studentID).First(&student).Error; err != nil {
return 0, err
}
return student.TotalPoints, nil
}