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,112 @@
// ===========================================
// 多班级版班级管理系统 - Go 后端
//
// 开发者: Canglan
// 联系方式: admin@sea-studio.top
// 版权归属: Sea Network Technology Studio
// 许可证: Apache License 2.0
//
// 版权所有 © Sea Network Technology Studio
// ===========================================
package repository
import (
"gorm.io/gorm"
"hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/model"
)
// AdminRoleRepo 管理员角色数据访问层
type AdminRoleRepo struct {
db *gorm.DB
}
// NewAdminRoleRepo 创建管理员角色 Repository
func NewAdminRoleRepo(db *gorm.DB) *AdminRoleRepo {
return &AdminRoleRepo{db: db}
}
// GetByUserID 获取用户的管理员角色(取第一个,含科目名称)
func (r *AdminRoleRepo) GetByUserID(userID int) (*model.AdminRole, error) {
var role model.AdminRole
if err := r.db.Table("admin_roles ar").
Select("ar.*, s.subject_name").
Joins("LEFT JOIN subjects s ON ar.subject_id = s.subject_id").
Where("ar.user_id = ?", userID).
Order("ar.admin_role_id ASC").
Limit(1).
First(&role).Error; err != nil {
return nil, err
}
return &role, nil
}
// GetByUserIDAndClass 获取用户在指定班级的管理员角色
func (r *AdminRoleRepo) GetByUserIDAndClass(userID int, classID int) (*model.AdminRole, error) {
var role model.AdminRole
if err := r.db.Table("admin_roles ar").
Select("ar.*, s.subject_name").
Joins("LEFT JOIN subjects s ON ar.subject_id = s.subject_id").
Where("ar.user_id = ? AND ar.class_id = ?", userID, classID).
Limit(1).
First(&role).Error; err != nil {
return nil, err
}
return &role, nil
}
// GetAllByClass 获取指定班级的所有管理员列表(含用户和科目信息)
func (r *AdminRoleRepo) GetAllByClass(classID int) ([]model.AdminRole, error) {
var roles []model.AdminRole
if err := r.db.Table("admin_roles ar").
Select("ar.*, u.real_name, u.username, u.status as user_status, s.subject_name").
Joins("JOIN users u ON ar.user_id = u.user_id AND u.status = 1").
Joins("LEFT JOIN subjects s ON ar.subject_id = s.subject_id").
Where("ar.class_id = ?", classID).
Order("ar.role_type").
Find(&roles).Error; err != nil {
return nil, err
}
return roles, nil
}
// Create 创建管理员角色
func (r *AdminRoleRepo) Create(role *model.AdminRole) (int, error) {
if err := r.db.Create(role).Error; err != nil {
return 0, err
}
return role.AdminRoleID, nil
}
// Delete 删除管理员角色(可指定班级)
func (r *AdminRoleRepo) Delete(userID int, classID int) error {
query := r.db.Where("user_id = ?", userID)
if classID > 0 {
query = query.Where("class_id = ?", classID)
}
return query.Delete(&model.AdminRole{}).Error
}
// UpdateRole 更新管理员角色类型和关联科目
func (r *AdminRoleRepo) UpdateRole(userID int, roleType string, classID int, subjectID *int) error {
query := r.db.Model(&model.AdminRole{}).Where("user_id = ?", userID)
if classID > 0 {
query = query.Where("class_id = ?", classID)
}
return query.Updates(map[string]interface{}{
"role_type": roleType,
"subject_id": subjectID,
}).Error
}
// GetUserRoleAndClassID 获取用户的角色类型和所属班级ID
func (r *AdminRoleRepo) GetUserRoleAndClassID(userID int) (string, int, error) {
var role model.AdminRole
if err := r.db.Where("user_id = ?", userID).
Limit(1).
First(&role).Error; err != nil {
return "", 0, err
}
return role.RoleType, role.ClassID, nil
}

View File

@@ -0,0 +1,168 @@
// ===========================================
// 多班级版班级管理系统 - Go 后端
//
// 开发者: Canglan
// 联系方式: admin@sea-studio.top
// 版权归属: Sea Network Technology Studio
// 许可证: Apache License 2.0
//
// 版权所有 © Sea Network Technology Studio
// ===========================================
package repository
import (
"time"
"gorm.io/gorm"
"hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/model"
)
// AssignmentRepo 作业数据访问层
type AssignmentRepo struct {
db *gorm.DB
}
// NewAssignmentRepo 创建作业 Repository
func NewAssignmentRepo(db *gorm.DB) *AssignmentRepo {
return &AssignmentRepo{db: db}
}
// ========== Assignment 操作 ==========
// CreateAssignment 创建作业
func (r *AssignmentRepo) CreateAssignment(assignment *model.Assignment) (int, error) {
if err := r.db.Create(assignment).Error; err != nil {
return 0, err
}
return assignment.AssignmentID, nil
}
// GetAssignmentByID 根据ID获取作业
func (r *AssignmentRepo) GetAssignmentByID(assignmentID int) (*model.Assignment, error) {
var assignment model.Assignment
if err := r.db.Where("assignment_id = ?", assignmentID).First(&assignment).Error; err != nil {
return nil, err
}
return &assignment, nil
}
// GetAssignmentsByClass 获取班级作业列表
func (r *AssignmentRepo) GetAssignmentsByClass(classID int, subjectID int, page, pageSize int) ([]model.Assignment, int64, error) {
var assignments []model.Assignment
var total int64
query := r.db.Model(&model.Assignment{}).Where("class_id = ?", classID)
if subjectID > 0 {
query = query.Where("subject_id = ?", subjectID)
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
offset := (page - 1) * pageSize
if err := query.Order("created_at DESC").
Limit(pageSize).
Offset(offset).
Find(&assignments).Error; err != nil {
return nil, 0, err
}
return assignments, total, nil
}
// GetAssignmentsBySubject 获取科目关联的作业列表
func (r *AssignmentRepo) GetAssignmentsBySubject(subjectID int) ([]model.Assignment, error) {
var assignments []model.Assignment
if err := r.db.Where("subject_id = ?", subjectID).
Order("created_at DESC").
Find(&assignments).Error; err != nil {
return nil, err
}
return assignments, nil
}
// DeleteAssignment 删除作业
func (r *AssignmentRepo) DeleteAssignment(assignmentID int) error {
return r.db.Where("assignment_id = ?", assignmentID).Delete(&model.Assignment{}).Error
}
// GetHomeworkStatsByDateRange 通过作业截止日期范围查询学生作业提交统计
func (r *AssignmentRepo) GetHomeworkStatsByDateRange(startDate, endDate time.Time) ([]struct {
StudentID int
Status string
Count int64
}, error) {
var stats []struct {
StudentID int
Status string
Count int64
}
err := r.db.Table("homework_submissions hs").
Select("hs.student_id, hs.status, COUNT(*) as count").
Joins("JOIN assignments a ON hs.assignment_id = a.assignment_id").
Where("a.deadline BETWEEN ? AND ?", startDate, endDate).
Group("hs.student_id, hs.status").
Find(&stats).Error
if err != nil {
return nil, err
}
return stats, nil
}
// ========== AssignmentSubmission 操作 ==========
// CreateSubmission 创建作业提交记录
func (r *AssignmentRepo) CreateSubmission(submission *model.AssignmentSubmission) (int, error) {
if err := r.db.Create(submission).Error; err != nil {
return 0, err
}
return submission.SubmissionID, nil
}
// GetSubmissionByAssignmentAndStudent 获取指定作业和学生的提交记录
func (r *AssignmentRepo) GetSubmissionByAssignmentAndStudent(assignmentID, studentID int) (*model.AssignmentSubmission, error) {
var submission model.AssignmentSubmission
if err := r.db.Where("assignment_id = ? AND student_id = ?", assignmentID, studentID).
First(&submission).Error; err != nil {
return nil, err
}
return &submission, nil
}
// GetSubmissionsByAssignment 获取作业的所有提交记录
func (r *AssignmentRepo) GetSubmissionsByAssignment(assignmentID int) ([]model.AssignmentSubmission, error) {
var submissions []model.AssignmentSubmission
if err := r.db.Where("assignment_id = ?", assignmentID).
Find(&submissions).Error; err != nil {
return nil, err
}
return submissions, nil
}
// GetSubmissionsByStudent 获取学生的所有提交记录
func (r *AssignmentRepo) GetSubmissionsByStudent(studentID int) ([]model.AssignmentSubmission, error) {
var submissions []model.AssignmentSubmission
if err := r.db.Where("student_id = ?", studentID).
Find(&submissions).Error; err != nil {
return nil, err
}
return submissions, nil
}
// UpdateSubmission 更新提交记录
func (r *AssignmentRepo) UpdateSubmission(submissionID int, updates map[string]interface{}) error {
return r.db.Model(&model.AssignmentSubmission{}).
Where("submission_id = ?", submissionID).
Updates(updates).Error
}
// BatchCreateSubmissions 批量创建提交记录
func (r *AssignmentRepo) BatchCreateSubmissions(submissions []model.AssignmentSubmission) error {
if len(submissions) == 0 {
return nil
}
return r.db.Create(&submissions).Error
}

View File

@@ -0,0 +1,184 @@
// ===========================================
// 多班级版班级管理系统 - Go 后端
//
// 开发者: Canglan
// 联系方式: admin@sea-studio.top
// 版权归属: Sea Network Technology Studio
// 许可证: Apache License 2.0
//
// 版权所有 © Sea Network Technology Studio
// ===========================================
package repository
import (
"time"
"gorm.io/gorm"
"hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/model"
)
// AttendanceRepo 考勤数据访问层
type AttendanceRepo struct {
db *gorm.DB
}
// NewAttendanceRepo 创建考勤 Repository
func NewAttendanceRepo(db *gorm.DB) *AttendanceRepo {
return &AttendanceRepo{db: db}
}
// GetStudentRecords 获取学生考勤记录
func (r *AttendanceRepo) GetStudentRecords(studentID int, month string) ([]model.AttendanceRecord, error) {
var records []model.AttendanceRecord
query := r.db.Where("student_id = ?", studentID)
if month != "" {
query = query.Where("DATE_FORMAT(date, '%Y-%m') = ?", month)
}
if err := query.Order("date DESC").Find(&records).Error; err != nil {
return nil, err
}
return records, nil
}
// GetClassRecords 获取班级考勤记录(支持多种过滤条件)
func (r *AttendanceRepo) GetClassRecords(classID int, date string, studentID int, slot string) ([]model.AttendanceRecord, error) {
var records []model.AttendanceRecord
query := r.db.Table("attendance_records ar").
Select("ar.*, s.name as student_name, s.student_no").
Joins("JOIN students s ON ar.student_id = s.student_id").
Where("1 = 1")
if classID > 0 {
query = query.Where("s.class_id = ?", classID)
}
if date != "" {
query = query.Where("ar.date = ?", date)
}
if studentID > 0 {
query = query.Where("ar.student_id = ?", studentID)
}
if slot != "" {
query = query.Where("ar.slot = ?", slot)
}
if err := query.Order("ar.date DESC, s.student_no").Find(&records).Error; err != nil {
return nil, err
}
return records, nil
}
// CreateRecordResult 创建或更新考勤记录的结果
type CreateRecordResult struct {
AttendanceID int
IsUpdate bool
OldDeductionApplied int8
OldDeductionRecordID *int64
}
// CreateRecord 创建或更新考勤记录(存在则更新),使用事务+行锁防止并发竞态
func (r *AttendanceRepo) CreateRecord(record *model.AttendanceRecord) (*CreateRecordResult, error) {
var result CreateRecordResult
err := r.db.Transaction(func(tx *gorm.DB) error {
// 使用 SELECT ... FOR UPDATE 锁定记录,防止并发请求同时判定为"不存在"
var existing model.AttendanceRecord
findErr := tx.Set("gorm:query_option", "FOR UPDATE").
Where("student_id = ? AND date = ? AND slot = ?",
record.StudentID, record.Date, record.Slot).
First(&existing).Error
if findErr == nil {
// 更新已有记录
if updateErr := tx.Model(&existing).Updates(map[string]interface{}{
"status": record.Status,
"reason": record.Reason,
"recorder_id": record.RecorderID,
}).Error; updateErr != nil {
return updateErr
}
result = CreateRecordResult{
AttendanceID: existing.AttendanceID,
IsUpdate: true,
OldDeductionApplied: existing.DeductionApplied,
OldDeductionRecordID: existing.DeductionRecordID,
}
return nil
}
if findErr != gorm.ErrRecordNotFound {
return findErr
}
// 插入新记录
if createErr := tx.Create(record).Error; createErr != nil {
return createErr
}
result = CreateRecordResult{
AttendanceID: record.AttendanceID,
IsUpdate: false,
}
return nil
})
if err != nil {
return nil, err
}
return &result, nil
}
// GetAttendanceStatsBySemester 批量查询学期内所有学生的考勤统计
func (r *AttendanceRepo) GetAttendanceStatsBySemester(semesterID int, startDate, endDate string) ([]struct {
StudentID int
Status string
Count int64
}, error) {
var stats []struct {
StudentID int
Status string
Count int64
}
err := r.db.Model(&model.AttendanceRecord{}).
Select("student_id, status, COUNT(*) as count").
Where("semester_id = ? OR (semester_id IS NULL AND `date` BETWEEN ? AND ?)", semesterID, startDate, endDate).
Group("student_id, status").
Find(&stats).Error
if err != nil {
return nil, err
}
return stats, nil
}
// GetAttendanceStatsByDateRange 通过日期范围查询学生考勤统计
func (r *AttendanceRepo) GetAttendanceStatsByDateRange(startDate, endDate time.Time, classID int) ([]struct {
StudentID int
Status string
Count int64
}, error) {
var stats []struct {
StudentID int
Status string
Count int64
}
query := r.db.Model(&model.AttendanceRecord{}).
Select("student_id, status, COUNT(*) as count").
Where("date BETWEEN ? AND ?", startDate, endDate)
if classID > 0 {
query = query.Where("student_id IN (SELECT student_id FROM students WHERE class_id = ?)", classID)
}
err := query.Group("student_id, status").Find(&stats).Error
if err != nil {
return nil, err
}
return stats, nil
}
// AssociateSemester 将考勤记录关联到学期
func (r *AttendanceRepo) AssociateSemester(attendanceID int, semesterID int) error {
return r.db.Model(&model.AttendanceRecord{}).
Where("attendance_id = ? AND semester_id IS NULL", attendanceID).
Update("semester_id", semesterID).Error
}

View File

@@ -0,0 +1,184 @@
// ===========================================
// 多班级版班级管理系统 - Go 后端
//
// 开发者: Canglan
// 联系方式: admin@sea-studio.top
// 版权归属: Sea Network Technology Studio
// 许可证: Apache License 2.0
//
// 版权所有 © Sea Network Technology Studio
// ===========================================
package repository
import (
"gorm.io/gorm"
"gorm.io/gorm/clause"
"hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/model"
)
// ClassRepo 班级数据访问层
type ClassRepo struct {
db *gorm.DB
}
// NewClassRepo 创建班级 Repository
func NewClassRepo(db *gorm.DB) *ClassRepo {
return &ClassRepo{db: db}
}
// GetDB 获取底层数据库连接
func (r *ClassRepo) GetDB() *gorm.DB {
return r.db
}
// GetByID 根据ID获取班级信息
func (r *ClassRepo) GetByID(classID int) (*model.Class, error) {
var class model.Class
if err := r.db.Where("class_id = ?", classID).First(&class).Error; err != nil {
return nil, err
}
return &class, nil
}
// GetAll 获取所有班级列表
func (r *ClassRepo) GetAll(includeDisabled bool) ([]model.Class, error) {
var classes []model.Class
query := r.db.Where("1 = 1")
if !includeDisabled {
query = query.Where("status = 1")
}
if err := query.Order("class_id").Find(&classes).Error; err != nil {
return nil, err
}
return classes, nil
}
// GetByName 根据班级名称获取班级
func (r *ClassRepo) GetByName(className string) (*model.Class, error) {
var class model.Class
if err := r.db.Where("class_name = ?", className).First(&class).Error; err != nil {
return nil, err
}
return &class, nil
}
// Create 创建班级
func (r *ClassRepo) Create(class *model.Class) (int, error) {
if err := r.db.Create(class).Error; err != nil {
return 0, err
}
return class.ClassID, nil
}
// Update 更新班级信息(仅更新非零值字段)
func (r *ClassRepo) Update(classID int, updates map[string]interface{}) error {
if len(updates) == 0 {
return nil
}
return r.db.Model(&model.Class{}).
Where("class_id = ?", classID).
Updates(updates).Error
}
// Delete 删除班级(硬删除,需先确认无学生)
func (r *ClassRepo) Delete(classID int) error {
return r.db.Where("class_id = ?", classID).Delete(&model.Class{}).Error
}
// GetStudentCount 获取班级活跃学生数量
func (r *ClassRepo) GetStudentCount(classID int) (int64, error) {
var count int64
if err := r.db.Model(&model.Student{}).
Where("class_id = ? AND status = 1", classID).
Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
// HasActiveStudents 检查班级是否有活跃学生
func (r *ClassRepo) HasActiveStudents(classID int) (bool, error) {
count, err := r.GetStudentCount(classID)
if err != nil {
return false, err
}
return count > 0, nil
}
// ========== 班级设置操作 ==========
// GetSettings 获取班级的所有设置
func (r *ClassRepo) GetSettings(classID int) ([]model.ClassSetting, error) {
var settings []model.ClassSetting
if err := r.db.Where("class_id = ?", classID).Find(&settings).Error; err != nil {
return nil, err
}
return settings, nil
}
// GetSetting 获取班级单个设置项
func (r *ClassRepo) GetSetting(classID int, key string) (*model.ClassSetting, error) {
var setting model.ClassSetting
if err := r.db.Where("class_id = ? AND setting_key = ?", classID, key).First(&setting).Error; err != nil {
return nil, err
}
return &setting, nil
}
// SaveSetting 保存班级设置项upsert
func (r *ClassRepo) SaveSetting(classID int, key, value string) error {
setting := model.ClassSetting{
ClassID: classID,
SettingKey: key,
SettingValue: value,
}
return r.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "class_id"}, {Name: "setting_key"}},
DoUpdates: clause.AssignmentColumns([]string{"setting_value"}),
}).Create(&setting).Error
}
// BatchSaveSettings 批量保存班级设置项
func (r *ClassRepo) BatchSaveSettings(classID int, settings map[string]string) error {
for key, value := range settings {
if err := r.SaveSetting(classID, key, value); err != nil {
return err
}
}
return nil
}
// ========== 班级功能开关操作 ==========
// GetFeatures 获取班级的所有功能开关
func (r *ClassRepo) GetFeatures(classID int) ([]model.ClassFeature, error) {
var features []model.ClassFeature
if err := r.db.Where("class_id = ?", classID).Find(&features).Error; err != nil {
return nil, err
}
return features, nil
}
// GetFeature 获取班级单个功能开关
func (r *ClassRepo) GetFeature(classID int, featureKey string) (*model.ClassFeature, error) {
var feature model.ClassFeature
if err := r.db.Where("class_id = ? AND feature_key = ?", classID, featureKey).First(&feature).Error; err != nil {
return nil, err
}
return &feature, nil
}
// SaveFeature 保存班级功能开关upsert
func (r *ClassRepo) SaveFeature(classID int, featureKey string, enabled int8) error {
feature := model.ClassFeature{
ClassID: classID,
FeatureKey: featureKey,
Enabled: enabled,
}
return r.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "class_id"}, {Name: "feature_key"}},
DoUpdates: clause.AssignmentColumns([]string{"enabled"}),
}).Create(&feature).Error
}

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
}

View File

@@ -0,0 +1,91 @@
// ===========================================
// 多班级版班级管理系统 - Go 后端
//
// 开发者: Canglan
// 联系方式: admin@sea-studio.top
// 版权归属: Sea Network Technology Studio
// 许可证: Apache License 2.0
//
// 版权所有 © Sea Network Technology Studio
// ===========================================
package repository
import (
"gorm.io/gorm"
"hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/model"
)
// LogRepo 日志数据访问层
type LogRepo struct {
db *gorm.DB
}
// NewLogRepo 创建日志 Repository
func NewLogRepo(db *gorm.DB) *LogRepo {
return &LogRepo{db: db}
}
// ========== 操作日志 ==========
// CreateOperationLog 写入操作日志
func (r *LogRepo) CreateOperationLog(log *model.OperationLog) (int64, error) {
if err := r.db.Create(log).Error; err != nil {
return 0, err
}
return log.LogID, nil
}
// GetOperationLogs 查询操作日志(支持按操作者和班级过滤)
func (r *LogRepo) GetOperationLogs(operatorID int, classID int, operationType string, page, pageSize int) ([]model.OperationLog, int64, error) {
var logs []model.OperationLog
var total int64
query := r.db.Model(&model.OperationLog{}).Where("1 = 1")
if operatorID > 0 {
query = query.Where("operator_id = ?", operatorID)
}
if classID > 0 {
query = query.Where("class_id = ?", classID)
}
if operationType != "" {
query = query.Where("operation_type = ?", operationType)
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
offset := (page - 1) * pageSize
if err := query.Order("created_at DESC").
Limit(pageSize).
Offset(offset).
Find(&logs).Error; err != nil {
return nil, 0, err
}
return logs, total, nil
}
// ========== 登录日志 ==========
// CreateLoginLog 写入登录日志
func (r *LogRepo) CreateLoginLog(log *model.LoginLog) (int64, error) {
if err := r.db.Create(log).Error; err != nil {
return 0, err
}
return log.LogID, nil
}
// GetRecentLoginFailCount 获取最近 5 分钟内的登录失败次数
func (r *LogRepo) GetRecentLoginFailCount(username string) (int64, error) {
var count int64
if err := r.db.Model(&model.LoginLog{}).
Where("username = ? AND login_result = 0 AND created_at > DATE_SUB(NOW(), INTERVAL 5 MINUTE)", username).
Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}

View File

@@ -0,0 +1,291 @@
// ===========================================
// 多班级版班级管理系统 - Go 后端
//
// 开发者: Canglan
// 联系方式: admin@sea-studio.top
// 版权归属: Sea Network Technology Studio
// 许可证: Apache License 2.0
//
// 版权所有 © Sea Network Technology Studio
// ===========================================
package repository
import (
"fmt"
"time"
"gorm.io/gorm"
"hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/model"
)
// SemesterRepo 学期数据访问层
type SemesterRepo struct {
db *gorm.DB
}
// NewSemesterRepo 创建学期 Repository
func NewSemesterRepo(db *gorm.DB) *SemesterRepo {
return &SemesterRepo{db: db}
}
// GetDB 获取底层数据库连接(用于事务操作)
func (r *SemesterRepo) GetDB() *gorm.DB {
return r.db
}
// Create 创建学期
func (r *SemesterRepo) Create(semester *model.Semester) (int, error) {
if err := r.db.Create(semester).Error; err != nil {
return 0, err
}
return semester.SemesterID, nil
}
// GetByID 根据ID获取学期信息
func (r *SemesterRepo) GetByID(semesterID int) (*model.Semester, error) {
var semester model.Semester
if err := r.db.Where("semester_id = ?", semesterID).First(&semester).Error; err != nil {
return nil, err
}
return &semester, nil
}
// GetAll 获取所有学期列表
func (r *SemesterRepo) GetAll() ([]model.Semester, error) {
var semesters []model.Semester
if err := r.db.Order("created_at DESC").Find(&semesters).Error; err != nil {
return nil, err
}
return semesters, nil
}
// GetActive 获取当前活跃学期(优先 is_active 标记,降级为日期范围匹配)
func (r *SemesterRepo) GetActive() (*model.Semester, error) {
var semester model.Semester
// 第一优先级is_active 标记
if err := r.db.Where("is_active = 1 AND is_archived = 0").
Limit(1).First(&semester).Error; err == nil {
return &semester, nil
}
// 第二优先级:日期范围匹配
today := time.Now().Format("2006-01-02")
if err := r.db.Where("is_archived = 0 AND start_date <= ? AND (end_date IS NULL OR end_date >= ?)", today, today).
Limit(1).First(&semester).Error; err != nil {
return nil, err
}
return &semester, nil
}
// DeactivateAll 将所有学期设为非活跃
func (r *SemesterRepo) DeactivateAll() error {
return r.db.Model(&model.Semester{}).
Where("is_active = 1").
Update("is_active", 0).Error
}
// Activate 设为当前活跃学期
func (r *SemesterRepo) Activate(semesterID int) error {
return r.db.Model(&model.Semester{}).
Where("semester_id = ? AND is_archived = 0", semesterID).
Update("is_active", 1).Error
}
// Archive 归档学期
func (r *SemesterRepo) Archive(semesterID int) error {
return r.db.Model(&model.Semester{}).
Where("semester_id = ? AND is_archived = 0", semesterID).
Updates(map[string]interface{}{
"is_archived": 1,
"is_active": 0,
}).Error
}
// Update 编辑学期信息(仅未归档)
func (r *SemesterRepo) Update(semesterID int, updates map[string]interface{}) error {
if len(updates) == 0 {
return nil
}
return r.db.Model(&model.Semester{}).
Where("semester_id = ? AND is_archived = 0", semesterID).
Updates(updates).Error
}
// Delete 删除学期
func (r *SemesterRepo) Delete(semesterID int) error {
return r.db.Where("semester_id = ?", semesterID).Delete(&model.Semester{}).Error
}
// CountArchives 统计学期归档数据数量
func (r *SemesterRepo) CountArchives(semesterID int) (int64, error) {
var count int64
if err := r.db.Model(&model.SemesterArchive{}).
Where("semester_id = ?", semesterID).
Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
// CountRecordsBySemester 统计学期关联的记录数
func (r *SemesterRepo) CountRecordsBySemester(semesterID int) (conductCount, attendanceCount int64, err error) {
if err = r.db.Model(&model.ConductRecord{}).
Where("semester_id = ?", semesterID).
Count(&conductCount).Error; err != nil {
return 0, 0, err
}
if err = r.db.Model(&model.AttendanceRecord{}).
Where("semester_id = ?", semesterID).
Count(&attendanceCount).Error; err != nil {
return 0, 0, err
}
return conductCount, attendanceCount, nil
}
// AssociateRecordsByDateRange 按日期范围关联记录到学期
func (r *SemesterRepo) AssociateRecordsByDateRange(semesterID int, startDate, endDate string) (conductCount, attendanceCount int64, err error) {
if startDate == "" || endDate == "" {
return 0, 0, fmt.Errorf("日期范围不能为空")
}
// 关联操行分记录
result := r.db.Model(&model.ConductRecord{}).
Where("semester_id IS NULL AND created_at BETWEEN ? AND CONCAT(?, ' 23:59:59')", startDate, endDate).
Update("semester_id", semesterID)
if result.Error != nil {
return 0, 0, result.Error
}
conductCount = result.RowsAffected
// 关联考勤记录
result = r.db.Model(&model.AttendanceRecord{}).
Where("semester_id IS NULL AND `date` BETWEEN ? AND ?", startDate, endDate).
Update("semester_id", semesterID)
if result.Error != nil {
return conductCount, 0, result.Error
}
attendanceCount = result.RowsAffected
return conductCount, attendanceCount, nil
}
// GetConductRecordSemesterID 获取操行分记录所属的学期ID
func (r *SemesterRepo) GetConductRecordSemesterID(recordID int64) (*int, error) {
var record model.ConductRecord
if err := r.db.Where("record_id = ?", recordID).First(&record).Error; err != nil {
return nil, err
}
return record.SemesterID, nil
}
// ========== 学期归档操作 ==========
// BatchCreateArchives 批量创建归档快照
func (r *SemesterRepo) BatchCreateArchives(archives []model.SemesterArchive) error {
if len(archives) == 0 {
return nil
}
return r.db.Create(&archives).Error
}
// DeleteArchivesBySemester 删除指定学期的所有归档数据
func (r *SemesterRepo) DeleteArchivesBySemester(semesterID int) error {
return r.db.Where("semester_id = ?", semesterID).Delete(&model.SemesterArchive{}).Error
}
// GetArchivesBySemester 获取学期的归档数据
func (r *SemesterRepo) GetArchivesBySemester(semesterID int, classID int, page, pageSize int) ([]model.SemesterArchive, int64, error) {
var archives []model.SemesterArchive
var total int64
query := r.db.Model(&model.SemesterArchive{}).Where("semester_id = ?", semesterID)
if classID > 0 {
query = query.Where("class_id = ?", classID)
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
offset := (page - 1) * pageSize
if err := query.Order("rank_position ASC").
Limit(pageSize).
Offset(offset).
Find(&archives).Error; err != nil {
return nil, 0, err
}
return archives, total, nil
}
// GetArchivesByStudent 获取学生在所有已归档学期的数据
func (r *SemesterRepo) GetArchivesByStudent(studentID int) ([]model.SemesterArchive, error) {
var archives []model.SemesterArchive
if err := r.db.Table("semester_archives sa").
Select("sa.archive_id, sa.semester_id, sa.student_id, sa.student_no, "+
"sa.student_name, sa.final_points, sa.rank_position, "+
"sa.total_students, sa.attendance_present, sa.attendance_absent, "+
"sa.attendance_late, sa.attendance_leave, "+
"sa.homework_submitted, sa.homework_not_submitted, sa.homework_late, "+
"sa.archived_at, s.semester_name, s.start_date, s.end_date").
Joins("JOIN semesters s ON sa.semester_id = s.semester_id").
Where("sa.student_id = ?", studentID).
Order("sa.archived_at DESC").
Find(&archives).Error; err != nil {
return nil, err
}
return archives, nil
}
// ========== 周期归档操作 ==========
// GetPeriodArchives 获取周期归档列表
func (r *SemesterRepo) GetPeriodArchives(classID int, periodType string, page, pageSize int) ([]model.PeriodArchive, int64, error) {
var archives []model.PeriodArchive
var total int64
query := r.db.Model(&model.PeriodArchive{}).
Where("class_id = ? AND period_type = ?", classID, periodType)
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
offset := (page - 1) * pageSize
if err := query.Order("archived_at DESC, period_label DESC, rank_position ASC").
Limit(pageSize).
Offset(offset).
Find(&archives).Error; err != nil {
return nil, 0, err
}
return archives, total, nil
}
// GetPeriodArchiveLabels 获取班级的所有周期归档标签(按时间倒序去重)
func (r *SemesterRepo) GetPeriodArchiveLabels(classID int, periodType string) ([]string, error) {
var labels []string
if err := r.db.Model(&model.PeriodArchive{}).
Where("class_id = ? AND period_type = ?", classID, periodType).
Distinct("period_label").
Order("period_label DESC").
Pluck("period_label", &labels).Error; err != nil {
return nil, err
}
return labels, nil
}
// GetLatestPeriodArchiveLabel 获取指定班级最近一次周期归档的标签
func (r *SemesterRepo) GetLatestPeriodArchiveLabel(classID int, periodType string) (string, error) {
var archive model.PeriodArchive
if err := r.db.Where("class_id = ? AND period_type = ?", classID, periodType).
Order("archived_at DESC").
Limit(1).
First(&archive).Error; err != nil {
return "", err
}
return archive.PeriodLabel, nil
}

View File

@@ -0,0 +1,230 @@
// ===========================================
// 多班级版班级管理系统 - Go 后端
//
// 开发者: Canglan
// 联系方式: admin@sea-studio.top
// 版权归属: Sea Network Technology Studio
// 许可证: Apache License 2.0
//
// 版权所有 © Sea Network Technology Studio
// ===========================================
package repository
import (
"fmt"
"strings"
"gorm.io/gorm"
"hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/model"
)
// StudentRepo 学生数据访问层
type StudentRepo struct {
db *gorm.DB
}
// NewStudentRepo 创建学生 Repository
func NewStudentRepo(db *gorm.DB) *StudentRepo {
return &StudentRepo{db: db}
}
// GetByID 根据ID获取学生信息含班级名称
func (r *StudentRepo) GetByID(studentID int) (*model.Student, error) {
var student model.Student
if err := r.db.Table("students s").
Select("s.*, c.class_name").
Joins("LEFT JOIN classes c ON s.class_id = c.class_id").
Where("s.student_id = ?", studentID).
First(&student).Error; err != nil {
return nil, err
}
return &student, nil
}
// GetByStudentNo 根据学号获取学生(可指定班级)
func (r *StudentRepo) GetByStudentNo(studentNo string, classID int) (*model.Student, error) {
var student model.Student
query := r.db.Where("student_no = ?", studentNo)
if classID > 0 {
query = query.Where("class_id = ?", classID)
}
if err := query.First(&student).Error; err != nil {
return nil, err
}
return &student, nil
}
// GetAll 获取指定班级的学生列表
func (r *StudentRepo) GetAll(classID int, includeDisabled bool) ([]model.Student, error) {
var students []model.Student
query := r.db.Where("class_id = ?", classID)
if !includeDisabled {
query = query.Where("status = 1")
}
if err := query.Order("student_no").Find(&students).Error; err != nil {
return nil, err
}
return students, nil
}
// GetDormitoryList 获取班级内所有不重复的宿舍号列表
func (r *StudentRepo) GetDormitoryList(classID int) ([]string, error) {
var dormitories []string
err := r.db.Model(&model.Student{}).
Where("class_id = ? AND status = 1 AND dormitory_number IS NOT NULL AND dormitory_number != ''", classID).
Distinct("dormitory_number").
Order("dormitory_number").
Pluck("dormitory_number", &dormitories).Error
if err != nil {
return nil, err
}
return dormitories, nil
}
// Create 创建学生记录
func (r *StudentRepo) Create(student *model.Student) (int, error) {
if err := r.db.Create(student).Error; err != nil {
return 0, err
}
return student.StudentID, nil
}
// Update 更新学生信息(仅更新非零值字段)
func (r *StudentRepo) Update(studentID int, updates map[string]interface{}) error {
if len(updates) == 0 {
return nil
}
return r.db.Model(&model.Student{}).
Where("student_id = ?", studentID).
Updates(updates).Error
}
// SoftDelete 软删除学生
func (r *StudentRepo) SoftDelete(studentID int) error {
return r.db.Model(&model.Student{}).
Where("student_id = ?", studentID).
Update("status", 0).Error
}
// UpdateTotalPoints 更新学生总分(增量更新,下限保护为 0
func (r *StudentRepo) UpdateTotalPoints(studentID int, pointsChange int) error {
return r.db.Model(&model.Student{}).
Where("student_id = ?", studentID).
Update("total_points", gorm.Expr("GREATEST(total_points + ?, 0)", pointsChange)).Error
}
// GetRanking 获取班级内学生排行
func (r *StudentRepo) GetRanking(classID int, limit int) ([]model.Student, error) {
var students []model.Student
if err := r.db.Where("status = 1 AND class_id = ?", classID).
Order("total_points DESC, student_id ASC").
Limit(limit).
Find(&students).Error; err != nil {
return nil, err
}
return students, nil
}
// GetTotalCount 获取班级内活跃学生总数
func (r *StudentRepo) GetTotalCount(classID int) (int64, error) {
var count int64
if err := r.db.Model(&model.Student{}).
Where("status = 1 AND class_id = ?", classID).
Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
// ListByClass 分页获取班级学生列表(支持搜索和宿舍号过滤)
func (r *StudentRepo) ListByClass(classID int, page, pageSize int, search, dormitoryNumber string) ([]model.Student, int64, error) {
var students []model.Student
var total int64
query := r.db.Model(&model.Student{}).Where("status = 1 AND class_id = ?", classID)
if search != "" {
escaped := strings.NewReplacer("\\", "\\\\", "%", "\\%", "_", "\\_").Replace(search)
searchPattern := fmt.Sprintf("%%%s%%", escaped)
query = query.Where("student_no LIKE ? OR name LIKE ?", searchPattern, searchPattern)
}
if dormitoryNumber != "" {
query = query.Where("dormitory_number = ?", dormitoryNumber)
}
// 获取总数
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// 分页查询
offset := (page - 1) * pageSize
if err := query.Order("student_no").
Limit(pageSize).
Offset(offset).
Find(&students).Error; err != nil {
return nil, 0, err
}
return students, total, nil
}
// BatchCreate 批量创建学生
func (r *StudentRepo) BatchCreate(students []model.Student) error {
return r.db.Create(&students).Error
}
// GetStudentNosByClass 获取指定班级所有学生学号(用于批量导入去重)
func (r *StudentRepo) GetStudentNosByClass(classID int) ([]string, error) {
var studentNos []string
if err := r.db.Model(&model.Student{}).
Where("class_id = ?", classID).
Pluck("student_no", &studentNos).Error; err != nil {
return nil, err
}
return studentNos, nil
}
// ResetPoints 重置班级内所有学生的操行分为初始值
func (r *StudentRepo) ResetPoints(classID int, initialPoints int) error {
return r.db.Model(&model.Student{}).
Where("class_id = ? AND status = 1", classID).
Update("total_points", initialPoints).Error
}
// GetByParentAccount 根据家长账号查找学生
func (r *StudentRepo) GetByParentAccount(parentAccount string) (*model.Student, error) {
var student model.Student
if err := r.db.Where("parent_account = ? AND status = 1", parentAccount).First(&student).Error; err != nil {
return nil, err
}
return &student, nil
}
// GetRankByStudentID 使用密集排名dense rank计算学生排名相同分数同名次后续名次不跳过
func (r *StudentRepo) GetRankByStudentID(classID, studentID int) (int, error) {
var student model.Student
if err := r.db.Select("total_points").Where("student_id = ?", studentID).First(&student).Error; err != nil {
return 0, err
}
var distinctHigherCount int64
if err := r.db.Raw("SELECT COUNT(DISTINCT total_points) FROM students WHERE status = 1 AND class_id = ? AND total_points > ?",
classID, student.TotalPoints).Scan(&distinctHigherCount).Error; err != nil {
return 0, err
}
return int(distinctHigherCount) + 1, nil
}
// GetStudentsByClassID 获取班级内所有活跃学生(用于归档等批量操作)
func (r *StudentRepo) GetStudentsByClassID(classID int) ([]model.Student, error) {
var students []model.Student
if err := r.db.Where("class_id = ? AND status = 1", classID).
Order("total_points DESC, student_id ASC").
Find(&students).Error; err != nil {
return nil, err
}
return students, nil
}

View File

@@ -0,0 +1,104 @@
// ===========================================
// 多班级版班级管理系统 - Go 后端
//
// 开发者: Canglan
// 联系方式: admin@sea-studio.top
// 版权归属: Sea Network Technology Studio
// 许可证: Apache License 2.0
//
// 版权所有 © Sea Network Technology Studio
// ===========================================
package repository
import (
"gorm.io/gorm"
"hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/model"
)
// SubjectRepo 科目数据访问层
type SubjectRepo struct {
db *gorm.DB
}
// NewSubjectRepo 创建科目 Repository
func NewSubjectRepo(db *gorm.DB) *SubjectRepo {
return &SubjectRepo{db: db}
}
// GetAll 获取所有科目列表
func (r *SubjectRepo) GetAll(isActive *bool) ([]model.Subject, error) {
var subjects []model.Subject
query := r.db.Where("1 = 1")
if isActive != nil {
if *isActive {
query = query.Where("is_active = 1")
} else {
query = query.Where("is_active = 0")
}
}
if err := query.Order("sort_order, subject_id").Find(&subjects).Error; err != nil {
return nil, err
}
return subjects, nil
}
// GetByID 根据ID获取科目
func (r *SubjectRepo) GetByID(subjectID int) (*model.Subject, error) {
var subject model.Subject
if err := r.db.Where("subject_id = ?", subjectID).First(&subject).Error; err != nil {
return nil, err
}
return &subject, nil
}
// GetByName 根据科目名称获取科目
func (r *SubjectRepo) GetByName(subjectName string) (*model.Subject, error) {
var subject model.Subject
if err := r.db.Where("subject_name = ?", subjectName).First(&subject).Error; err != nil {
return nil, err
}
return &subject, nil
}
// Create 创建科目
func (r *SubjectRepo) Create(subject *model.Subject) (int, error) {
if err := r.db.Create(subject).Error; err != nil {
return 0, err
}
return subject.SubjectID, nil
}
// Update 更新科目信息
func (r *SubjectRepo) Update(subjectID int, updates map[string]interface{}) error {
if len(updates) == 0 {
return nil
}
return r.db.Model(&model.Subject{}).
Where("subject_id = ?", subjectID).
Updates(updates).Error
}
// Delete 删除科目
func (r *SubjectRepo) Delete(subjectID int) error {
return r.db.Where("subject_id = ?", subjectID).Delete(&model.Subject{}).Error
}
// HasRelatedData 检查科目是否有关联的作业数据
func (r *SubjectRepo) HasRelatedData(subjectID int) (bool, error) {
var count int64
if err := r.db.Model(&model.Assignment{}).
Where("subject_id = ?", subjectID).
Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
// Activate 激活科目
func (r *SubjectRepo) Activate(subjectID int) error {
return r.db.Model(&model.Subject{}).
Where("subject_id = ?", subjectID).
Update("is_active", 1).Error
}

View File

@@ -0,0 +1,101 @@
// ===========================================
// 多班级版班级管理系统 - Go 后端
//
// 开发者: Canglan
// 联系方式: admin@sea-studio.top
// 版权归属: Sea Network Technology Studio
// 许可证: Apache License 2.0
//
// 版权所有 © Sea Network Technology Studio
// ===========================================
package repository
import (
"gorm.io/gorm"
"gorm.io/gorm/clause"
"hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/model"
)
// SuperAdminRepo 超级管理员数据访问层
type SuperAdminRepo struct {
db *gorm.DB
}
// NewSuperAdminRepo 创建超级管理员 Repository
func NewSuperAdminRepo(db *gorm.DB) *SuperAdminRepo {
return &SuperAdminRepo{db: db}
}
// GetByUsername 根据用户名获取超级管理员
func (r *SuperAdminRepo) GetByUsername(username string) (*model.SuperAdmin, error) {
var admin model.SuperAdmin
if err := r.db.Where("username = ? AND status = 1", username).First(&admin).Error; err != nil {
return nil, err
}
return &admin, nil
}
// GetByID 根据ID获取超级管理员
func (r *SuperAdminRepo) GetByID(id int) (*model.SuperAdmin, error) {
var admin model.SuperAdmin
if err := r.db.Where("id = ?", id).First(&admin).Error; err != nil {
return nil, err
}
return &admin, nil
}
// Create 创建超级管理员
func (r *SuperAdminRepo) Create(admin *model.SuperAdmin) (int, error) {
if err := r.db.Create(admin).Error; err != nil {
return 0, err
}
return admin.ID, nil
}
// UpdatePassword 更新超级管理员密码并清除强制改密标记
func (r *SuperAdminRepo) UpdatePassword(id int, passwordHash string) error {
return r.db.Model(&model.SuperAdmin{}).
Where("id = ?", id).
Updates(map[string]interface{}{
"password_hash": passwordHash,
"need_change_password": 0,
}).Error
}
// CheckUsernameExists 检查用户名是否存在
func (r *SuperAdminRepo) CheckUsernameExists(username string) (bool, error) {
var count int64
if err := r.db.Model(&model.SuperAdmin{}).Where("username = ?", username).Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
// List 获取所有超级管理员
func (r *SuperAdminRepo) List() ([]model.SuperAdmin, error) {
var admins []model.SuperAdmin
if err := r.db.Order("id").Find(&admins).Error; err != nil {
return nil, err
}
return admins, nil
}
// UpdateStatus 更新超级管理员状态
func (r *SuperAdminRepo) UpdateStatus(id int, status int8) error {
return r.db.Model(&model.SuperAdmin{}).
Where("id = ?", id).
Update("status", status).Error
}
// EnsureDefaultAdmin 确保默认超级管理员存在(使用 INSERT IGNORE 避免并发竞态)
func (r *SuperAdminRepo) EnsureDefaultAdmin(username, passwordHash, realName string) error {
admin := model.SuperAdmin{
Username: username,
PasswordHash: passwordHash,
RealName: realName,
Status: 1,
}
return r.db.Clauses(clause.OnConflict{DoNothing: true}).Create(&admin).Error
}

View File

@@ -0,0 +1,100 @@
// ===========================================
// 多班级版班级管理系统 - Go 后端
//
// 开发者: Canglan
// 联系方式: admin@sea-studio.top
// 版权归属: Sea Network Technology Studio
// 许可证: Apache License 2.0
//
// 版权所有 © Sea Network Technology Studio
// ===========================================
package repository
import (
"gorm.io/gorm"
"gorm.io/gorm/clause"
"hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/model"
)
// SystemSettingRepo 系统设置数据访问层
type SystemSettingRepo struct {
db *gorm.DB
}
// NewSystemSettingRepo 创建系统设置 Repository
func NewSystemSettingRepo(db *gorm.DB) *SystemSettingRepo {
return &SystemSettingRepo{db: db}
}
// GetByKey 根据键名获取系统设置
func (r *SystemSettingRepo) GetByKey(key string) (*model.SystemSetting, error) {
var setting model.SystemSetting
if err := r.db.Where("setting_key = ?", key).First(&setting).Error; err != nil {
return nil, err
}
return &setting, nil
}
// GetAll 获取所有系统设置
func (r *SystemSettingRepo) GetAll() ([]model.SystemSetting, error) {
var settings []model.SystemSetting
if err := r.db.Find(&settings).Error; err != nil {
return nil, err
}
return settings, nil
}
// GetByKeyMap 获取所有系统设置并转为 map
func (r *SystemSettingRepo) GetByKeyMap() (map[string]string, error) {
settings, err := r.GetAll()
if err != nil {
return nil, err
}
result := make(map[string]string, len(settings))
for _, s := range settings {
result[s.SettingKey] = s.SettingValue
}
return result, nil
}
// Save 保存系统设置upsert
func (r *SystemSettingRepo) Save(key, value string) error {
setting := model.SystemSetting{
SettingKey: key,
SettingValue: value,
}
return r.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "setting_key"}},
DoUpdates: clause.AssignmentColumns([]string{"setting_value"}),
}).Create(&setting).Error
}
// BatchSave 批量保存系统设置
func (r *SystemSettingRepo) BatchSave(settings map[string]string) error {
for key, value := range settings {
if err := r.Save(key, value); err != nil {
return err
}
}
return nil
}
// GetValue 根据键名获取设置值
func (r *SystemSettingRepo) GetValue(key string) (string, error) {
setting, err := r.GetByKey(key)
if err != nil {
return "", err
}
return setting.SettingValue, nil
}
// GetValueWithDefault 根据键名获取设置值,不存在则返回默认值
func (r *SystemSettingRepo) GetValueWithDefault(key, defaultValue string) string {
setting, err := r.GetByKey(key)
if err != nil {
return defaultValue
}
return setting.SettingValue
}

View File

@@ -0,0 +1,166 @@
// ===========================================
// 多班级版班级管理系统 - Go 后端
//
// 开发者: Canglan
// 联系方式: admin@sea-studio.top
// 版权归属: Sea Network Technology Studio
// 许可证: Apache License 2.0
//
// 版权所有 © Sea Network Technology Studio
// ===========================================
package repository
import (
"time"
"gorm.io/gorm"
"hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/model"
)
// UserRepo 用户数据访问层
type UserRepo struct {
db *gorm.DB
}
// NewUserRepo 创建用户 Repository
func NewUserRepo(db *gorm.DB) *UserRepo {
return &UserRepo{db: db}
}
// GetByUsername 根据用户名获取用户(含状态过滤)
func (r *UserRepo) GetByUsername(username string) (*model.User, error) {
var user model.User
if err := r.db.Where("username = ? AND status = 1", username).First(&user).Error; err != nil {
return nil, err
}
return &user, nil
}
// GetByUserID 根据用户ID获取用户
func (r *UserRepo) GetByUserID(userID int) (*model.User, error) {
var user model.User
if err := r.db.Where("user_id = ?", userID).First(&user).Error; err != nil {
return nil, err
}
return &user, nil
}
// CreateStudent 创建学生账号
func (r *UserRepo) CreateStudent(username, passwordHash, realName string, studentID int) (int, error) {
user := model.User{
Username: username,
PasswordHash: passwordHash,
RealName: realName,
UserType: "student",
StudentID: &studentID,
Status: 1,
NeedChangePassword: 1,
}
if err := r.db.Create(&user).Error; err != nil {
return 0, err
}
return user.UserID, nil
}
// CreateParent 创建家长账号
func (r *UserRepo) CreateParent(username, passwordHash, realName string, studentID int) (int, error) {
user := model.User{
Username: username,
PasswordHash: passwordHash,
RealName: realName,
UserType: "parent",
StudentID: &studentID,
Status: 1,
NeedChangePassword: 0,
}
if err := r.db.Create(&user).Error; err != nil {
return 0, err
}
return user.UserID, nil
}
// CreateAdmin 创建管理员账号
func (r *UserRepo) CreateAdmin(username, passwordHash, realName string) (int, error) {
user := model.User{
Username: username,
PasswordHash: passwordHash,
RealName: realName,
UserType: "admin",
Status: 1,
NeedChangePassword: 1,
}
if err := r.db.Create(&user).Error; err != nil {
return 0, err
}
return user.UserID, nil
}
// UpdatePassword 更新密码并清除强制改密标记
func (r *UserRepo) UpdatePassword(userID int, passwordHash string) error {
return r.db.Model(&model.User{}).
Where("user_id = ?", userID).
Updates(map[string]interface{}{
"password_hash": passwordHash,
"need_change_password": 0,
}).Error
}
// UpdateLastLogin 更新最后登录信息
func (r *UserRepo) UpdateLastLogin(userID int, ip string) error {
return r.db.Model(&model.User{}).
Where("user_id = ?", userID).
Updates(map[string]interface{}{
"last_login_time": time.Now(),
"last_login_ip": ip,
}).Error
}
// CheckUsernameExists 检查用户名是否存在
func (r *UserRepo) CheckUsernameExists(username string) (bool, error) {
var count int64
if err := r.db.Model(&model.User{}).Where("username = ?", username).Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
// UpdateStatus 更新用户状态
func (r *UserRepo) UpdateStatus(userID int, status int8) error {
return r.db.Model(&model.User{}).
Where("user_id = ?", userID).
Update("status", status).Error
}
// UpdateRealName 更新用户真实姓名
func (r *UserRepo) UpdateRealName(userID int, realName string) error {
return r.db.Model(&model.User{}).
Where("user_id = ?", userID).
Update("real_name", realName).Error
}
// GetByStudentID 根据学生ID获取关联的用户账号
func (r *UserRepo) GetByStudentID(studentID int) (*model.User, error) {
var user model.User
if err := r.db.Where("student_id = ? AND status = 1", studentID).First(&user).Error; err != nil {
return nil, err
}
return &user, nil
}
// DeleteUser 硬删除用户记录
func (r *UserRepo) DeleteUser(userID int) error {
return r.db.Unscoped().Where("user_id = ?", userID).Delete(&model.User{}).Error
}
// GetActiveUsernames 获取所有活跃用户的用户名列表(用于批量导入去重)
func (r *UserRepo) GetActiveUsernames() ([]string, error) {
var usernames []string
if err := r.db.Model(&model.User{}).
Where("status = 1").
Pluck("username", &usernames).Error; err != nil {
return nil, err
}
return usernames, nil
}