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,227 @@
// ===========================================
// 多班级版班级管理系统 - Go 后端
//
// 开发者: Canglan
// 联系方式: admin@sea-studio.top
// 版权归属: Sea Network Technology Studio
// 许可证: Apache License 2.0
//
// 版权所有 © Sea Network Technology Studio
// ===========================================
package middleware
import (
"context"
"strings"
"github.com/gin-gonic/gin"
"hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/config"
"hz-gitea.sea-studio.top/canglan/SharedClassManager/pkg/database"
appJwt "hz-gitea.sea-studio.top/canglan/SharedClassManager/pkg/jwt"
"hz-gitea.sea-studio.top/canglan/SharedClassManager/pkg/logger"
"hz-gitea.sea-studio.top/canglan/SharedClassManager/pkg/response"
)
// 上下文 Key 常量
const (
CtxUserID = "user_id"
CtxUsername = "username"
CtxUserType = "user_type"
CtxStudentID = "student_id"
CtxRole = "role"
CtxRealName = "real_name"
CtxClassID = "class_id"
)
// 公开路径(不需要认证)
var publicPaths = map[string]bool{
"/": true,
"/health": true,
"/api/auth/login": true,
}
// RegisterPublicPath 注册额外的公开路径(需在路由初始化阶段调用)
func RegisterPublicPath(path string) {
publicPaths[path] = true
}
// AuthRequired JWT 认证中间件
func AuthRequired() gin.HandlerFunc {
return func(c *gin.Context) {
path := c.Request.URL.Path
// 公开路径跳过
if publicPaths[path] {
c.Next()
return
}
cfg := config.AppConfig
// 获取 Authorization header
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
response.Unauthorized(c, "缺少认证令牌")
c.Abort()
return
}
// 解析 Bearer Token
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
response.Unauthorized(c, "认证格式错误")
c.Abort()
return
}
tokenStr := parts[1]
// 验证 JWT
claims, err := appJwt.VerifyToken(tokenStr)
if err != nil {
logger.Sugared.Warnf("JWT 验证失败: path=%s, err=%v", path, err)
response.Unauthorized(c, "令牌无效或已过期")
c.Abort()
return
}
// 验证 Redis 中的 Token
ctx := context.Background()
storedToken, err := database.GetUserToken(ctx, claims.UserID)
if err != nil || storedToken != tokenStr {
logger.Sugared.Warnf("Redis Token 不匹配: path=%s, user_id=%d", path, claims.UserID)
// 主动清理 Redis 中的旧 Token避免残留
if err == nil && storedToken != "" && storedToken != tokenStr {
_ = database.DeleteUserToken(ctx, claims.UserID)
}
response.Unauthorized(c, "令牌已失效,请重新登录")
c.Abort()
return
}
// 刷新 Token 过期时间(空闲超时)
_ = database.ExpireToken(ctx, claims.UserID, cfg.JWTIdleTimeoutMinutes)
// 将用户信息写入 Gin 上下文
c.Set(CtxUserID, claims.UserID)
c.Set(CtxUsername, claims.Username)
c.Set(CtxUserType, claims.UserType)
c.Set(CtxRealName, claims.RealName)
if claims.StudentID != nil {
c.Set(CtxStudentID, *claims.StudentID)
}
c.Set(CtxRole, claims.Role)
if claims.ClassID != nil {
c.Set(CtxClassID, *claims.ClassID)
}
logger.Sugared.Debugf("认证成功: %s %s, user_id=%d, username=%s",
c.Request.Method, path, claims.UserID, claims.Username)
c.Next()
}
}
// RequireRole 角色权限中间件
func RequireRole(roles ...string) gin.HandlerFunc {
roleSet := make(map[string]bool, len(roles))
for _, r := range roles {
roleSet[r] = true
}
return func(c *gin.Context) {
userType, _ := c.Get(CtxUserType)
role, _ := c.Get(CtxRole)
// 超级管理员直接通过
if userType == "super_admin" {
c.Next()
return
}
// 检查 user_type
if ut, ok := userType.(string); ok && roleSet[ut] {
c.Next()
return
}
// 检查 roleadmin_roles.role_type
if r, ok := role.(string); ok && roleSet[r] {
c.Next()
return
}
response.Forbidden(c, "权限不足")
c.Abort()
}
}
// GetUserID 从上下文获取用户 ID
func GetUserID(c *gin.Context) int {
if v, exists := c.Get(CtxUserID); exists {
if id, ok := v.(int); ok {
return id
}
}
return 0
}
// GetUsername 从上下文获取用户名
func GetUsername(c *gin.Context) string {
if v, exists := c.Get(CtxUsername); exists {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
// GetUserType 从上下文获取用户类型
func GetUserType(c *gin.Context) string {
if v, exists := c.Get(CtxUserType); exists {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
// GetRole 从上下文获取角色
func GetRole(c *gin.Context) string {
if v, exists := c.Get(CtxRole); exists {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
// GetClassID 从上下文获取班级 ID
func GetClassID(c *gin.Context) int {
if v, exists := c.Get(CtxClassID); exists {
if id, ok := v.(int); ok {
return id
}
}
return 0
}
// GetStudentID 从上下文获取学生 ID
func GetStudentID(c *gin.Context) int {
if v, exists := c.Get(CtxStudentID); exists {
if id, ok := v.(int); ok {
return id
}
}
return 0
}
// GetRealName 从上下文获取真实姓名
func GetRealName(c *gin.Context) string {
if v, exists := c.Get(CtxRealName); exists {
if s, ok := v.(string); ok {
return s
}
}
return ""
}