- 后端从 Python FastAPI 重写为 Go Gin(端口 56789) - 多班级完全隔离 - 超级管理员独立登录 - 课代表作业管理、排行榜分项排行 - 角色加减分上下限可配置 - 家长改密功能(可开关) - 周度/月度重置功能 - MySQL 5.7 兼容 - 43轮代码审查+全部修复 - Apache 2.0 许可证
159 lines
5.4 KiB
Go
159 lines
5.4 KiB
Go
// ===========================================
|
||
// 多班级版班级管理系统 - Go 后端
|
||
//
|
||
// 开发者: Canglan
|
||
// 联系方式: admin@sea-studio.top
|
||
// 版权归属: Sea Network Technology Studio
|
||
// 许可证: Apache License 2.0
|
||
//
|
||
// 版权所有 © Sea Network Technology Studio
|
||
// ===========================================
|
||
|
||
package service
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
|
||
"hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/config"
|
||
"hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/repository"
|
||
"hz-gitea.sea-studio.top/canglan/SharedClassManager/pkg/crypto"
|
||
"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"
|
||
)
|
||
|
||
// SuperAdminService 超级管理员服务
|
||
type SuperAdminService struct {
|
||
superAdminRepo *repository.SuperAdminRepo
|
||
logService *LogService
|
||
}
|
||
|
||
// NewSuperAdminService 创建超级管理员服务
|
||
func NewSuperAdminService(superAdminRepo *repository.SuperAdminRepo, logService *LogService) *SuperAdminService {
|
||
return &SuperAdminService{superAdminRepo: superAdminRepo, logService: logService}
|
||
}
|
||
|
||
// EnsureDefaultAdmin 确保默认超级管理员存在
|
||
func (s *SuperAdminService) EnsureDefaultAdmin() error {
|
||
cfg := config.AppConfig
|
||
|
||
logger.Sugared.Warnf("⚠️ 当前使用默认超级管理员密码,部署环境请务必修改 SUPER_ADMIN_DEFAULT_PASSWORD 并重启服务")
|
||
|
||
// 为超级管理员生成独立的随机 Salt
|
||
salt, err := crypto.GenerateRandomPassword(16)
|
||
if err != nil {
|
||
return fmt.Errorf("生成随机盐值失败: %w", err)
|
||
}
|
||
passwordHash := crypto.HashPassword(cfg.SuperAdminDefaultPass, salt)
|
||
if err := s.superAdminRepo.EnsureDefaultAdmin(
|
||
cfg.SuperAdminDefaultUser,
|
||
passwordHash,
|
||
salt,
|
||
"系统管理员",
|
||
); err != nil {
|
||
return fmt.Errorf("创建默认超级管理员失败: %w", err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// Login 超级管理员登录
|
||
func (s *SuperAdminService) Login(username, password, ip, userAgent string) (map[string]interface{}, error) {
|
||
ctx := context.Background()
|
||
cfg := config.AppConfig
|
||
|
||
// 检查登录失败次数(用户名级 + IP 级双重限流,使用原子 Incr 防止 TOCTOU 竞态)
|
||
attemptsKey := fmt.Sprintf("login_attempts:sa:%s", username)
|
||
ipAttemptsKey := fmt.Sprintf("login_attempts:ip:super_admin:%s", ip)
|
||
|
||
count, _ := incrWithExpireAtomic(ctx, attemptsKey, 300)
|
||
if count > 5 {
|
||
return map[string]interface{}{"success": false, "message": "登录失败次数过多,请5分钟后重试"}, nil
|
||
}
|
||
// IP 级限流
|
||
ipCount, _ := incrWithExpireAtomic(ctx, ipAttemptsKey, 300)
|
||
if ipCount > 20 {
|
||
return map[string]interface{}{"success": false, "message": "登录失败次数过多,请5分钟后重试"}, nil
|
||
}
|
||
|
||
admin, err := s.superAdminRepo.GetByUsername(username)
|
||
if err != nil {
|
||
s.logService.WriteLoginLog(username, 0, ip, userAgent, "用户名或密码错误")
|
||
return map[string]interface{}{"success": false, "message": "用户名或密码错误"}, nil
|
||
}
|
||
|
||
if !crypto.VerifyPassword(password, admin.PasswordHash, admin.Salt) {
|
||
s.logService.WriteLoginLog(username, 0, ip, userAgent, "用户名或密码错误")
|
||
return map[string]interface{}{"success": false, "message": "用户名或密码错误"}, nil
|
||
}
|
||
|
||
// 清除用户名级登录失败记录,IP 级计数由 TTL 自然过期(与普通用户策略一致,防止同 IP 其他用户限流被重置)
|
||
database.RDB.Del(ctx, attemptsKey)
|
||
s.logService.WriteLoginLog(username, 1, ip, userAgent, "")
|
||
|
||
// 生成 Token
|
||
token, err := appJwt.CreateToken(
|
||
admin.ID, admin.Username, "super_admin",
|
||
nil, "系统管理员", admin.RealName, nil, false,
|
||
)
|
||
if err != nil {
|
||
return map[string]interface{}{"success": false, "message": "生成令牌失败"}, nil
|
||
}
|
||
|
||
_ = database.SetUserToken(ctx, admin.ID, token, cfg.JWTIdleTimeoutMinutes)
|
||
|
||
needChangePassword := admin.NeedChangePassword == 1
|
||
redirect := "/admin/dashboard.php"
|
||
if needChangePassword {
|
||
redirect = "/admin/password.php"
|
||
}
|
||
|
||
return map[string]interface{}{
|
||
"success": true,
|
||
"token": token,
|
||
"user_id": admin.ID,
|
||
"username": admin.Username,
|
||
"real_name": admin.RealName,
|
||
"user_type": "super_admin",
|
||
"need_change_password": needChangePassword,
|
||
"redirect": redirect,
|
||
}, nil
|
||
}
|
||
|
||
// ChangePassword 超级管理员修改密码(操作 super_admins 表,使用独立 salt)
|
||
func (s *SuperAdminService) ChangePassword(adminID int, oldPassword, newPassword string, force bool) error {
|
||
admin, err := s.superAdminRepo.GetByID(adminID)
|
||
if err != nil {
|
||
return fmt.Errorf("超级管理员不存在")
|
||
}
|
||
|
||
// 验证原密码(强制改密时跳过)
|
||
if !force {
|
||
if !crypto.VerifyPassword(oldPassword, admin.PasswordHash, admin.Salt) {
|
||
return fmt.Errorf("原密码错误")
|
||
}
|
||
}
|
||
|
||
// 验证新密码强度
|
||
if valid, msg := crypto.ValidatePasswordStrength(newPassword); !valid {
|
||
return fmt.Errorf("%s", msg)
|
||
}
|
||
|
||
// 生成新的独立 salt
|
||
newSalt, err := crypto.GenerateRandomPassword(16)
|
||
if err != nil {
|
||
return fmt.Errorf("生成随机盐值失败: %w", err)
|
||
}
|
||
newHash := crypto.HashPassword(newPassword, newSalt)
|
||
|
||
if err := s.superAdminRepo.UpdatePasswordWithSalt(adminID, newHash, newSalt); err != nil {
|
||
return fmt.Errorf("密码修改失败")
|
||
}
|
||
|
||
// 清除旧 Token,强制重新登录
|
||
ctx := context.Background()
|
||
_ = database.DeleteUserToken(ctx, adminID)
|
||
|
||
return nil
|
||
}
|