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 c6db68a9f4
135 changed files with 19933 additions and 0 deletions

View File

@@ -0,0 +1,131 @@
// ===========================================
// 多班级版班级管理系统 - Go 后端
//
// 开发者: Canglan
// 联系方式: admin@sea-studio.top
// 版权归属: Sea Network Technology Studio
// 许可证: Apache License 2.0
//
// 版权所有 © Sea Network Technology Studio
// ===========================================
package middleware
import (
"bytes"
"encoding/json"
"io"
"net/url"
"strings"
"github.com/gin-gonic/gin"
)
// Sanitize 输入清理中间件(路径遍历防护 + 长度限制)
func Sanitize() gin.HandlerFunc {
return func(c *gin.Context) {
// 处理 POST、PUT、PATCH 请求体
if c.Request.Method == "POST" || c.Request.Method == "PUT" || c.Request.Method == "PATCH" {
body, err := io.ReadAll(c.Request.Body)
if err == nil && len(body) > 0 {
var data interface{}
if json.Unmarshal(body, &data) == nil {
cleaned := sanitizeData(data)
newBody, _ := json.Marshal(cleaned)
c.Request.Body = io.NopCloser(bytes.NewBuffer(newBody))
c.Request.ContentLength = int64(len(newBody))
} else {
// 非 JSON 请求体,恢复原始 body
c.Request.Body = io.NopCloser(bytes.NewBuffer(body))
}
}
}
// 清理查询参数GET 等请求的 URL query string
if c.Request.URL.RawQuery != "" {
params := c.Request.URL.Query()
dirty := false
for key, values := range params {
for i, v := range values {
cleaned := sanitizeString(v)
if cleaned != v {
values[i] = cleaned
dirty = true
}
}
params[key] = values
}
if dirty {
c.Request.URL.RawQuery = params.Encode()
}
}
c.Next()
}
}
// sanitizeData 递归清理数据
func sanitizeData(data interface{}) interface{} {
switch v := data.(type) {
case map[string]interface{}:
result := make(map[string]interface{}, len(v))
for key, val := range v {
result[key] = sanitizeData(val)
}
return result
case []interface{}:
result := make([]interface{}, len(v))
for i, val := range v {
result[i] = sanitizeData(val)
}
return result
case string:
return sanitizeString(v)
default:
return v
}
}
// sanitizeString 清理字符串
func sanitizeString(value string) string {
if value == "" {
return ""
}
value = strings.TrimSpace(value)
// 路径遍历防护(循环解码直到稳定,防止多层编码绕过)
for {
decoded, err := url.PathUnescape(value)
if err != nil || decoded == value {
break
}
value = decoded
}
// 大小写无关的路径遍历模式清理(循环移除直到无匹配)
lower := strings.ToLower(value)
for strings.Contains(lower, "../") || strings.Contains(lower, "..\\") {
replaced := false
for _, pattern := range []string{"../", "..\\"} {
if idx := strings.Index(lower, pattern); idx >= 0 {
value = value[:idx] + value[idx+len(pattern):]
lower = lower[:idx] + lower[idx+len(pattern):]
replaced = true
break
}
}
if !replaced {
break
}
}
// 限制长度(按 rune 截断,避免切断多字节 UTF-8 字符)
runes := []rune(value)
if len(runes) > 1000 {
value = string(runes[:1000])
}
// SQL 注入由 GORM 参数化查询防护,无需正则替换(避免破坏合法输入)
return value
}