- 后端从 Python FastAPI 重写为 Go Gin(端口 56789) - 多班级完全隔离 - 超级管理员独立登录 - 课代表作业管理、排行榜分项排行 - 角色加减分上下限可配置 - 家长改密功能(可开关) - 周度/月度重置功能 - MySQL 5.7 兼容 - 43轮代码审查+全部修复 - Apache 2.0 许可证
211 lines
6.7 KiB
Go
211 lines
6.7 KiB
Go
// ===========================================
|
||
// 多班级版班级管理系统 - Go 后端
|
||
//
|
||
// 开发者: Canglan
|
||
// 联系方式: admin@sea-studio.top
|
||
// 版权归属: Sea Network Technology Studio
|
||
// 许可证: Apache License 2.0
|
||
//
|
||
// 版权所有 © Sea Network Technology Studio
|
||
// ===========================================
|
||
|
||
package main
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"net/http"
|
||
"os"
|
||
"os/signal"
|
||
"syscall"
|
||
"time"
|
||
|
||
"hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/config"
|
||
"hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/handler"
|
||
"hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/repository"
|
||
"hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/router"
|
||
"hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/service"
|
||
"hz-gitea.sea-studio.top/canglan/SharedClassManager/pkg/database"
|
||
"hz-gitea.sea-studio.top/canglan/SharedClassManager/pkg/logger"
|
||
)
|
||
|
||
func main() {
|
||
// ========== 1. 加载配置 ==========
|
||
cfg, err := config.Load()
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "加载配置失败: %v\n", err)
|
||
os.Exit(1)
|
||
}
|
||
|
||
// ========== 2. 初始化日志 ==========
|
||
logger.Init(cfg.LogLevel, cfg.IsProduction())
|
||
defer logger.Sync()
|
||
|
||
logger.Sugared.Infof("应用启动: %s (env=%s, port=%s)", cfg.AppName, cfg.AppEnv, cfg.AppPort)
|
||
|
||
// ========== 3. 初始化 MySQL ==========
|
||
mysqlDB, err := database.InitMySQL(cfg)
|
||
if err != nil {
|
||
logger.Sugared.Fatalf("初始化 MySQL 失败: %v", err)
|
||
}
|
||
logger.Sugared.Info("MySQL 连接成功")
|
||
|
||
sqlDB, err := mysqlDB.DB()
|
||
if err != nil {
|
||
logger.Sugared.Fatalf("获取 sql.DB 失败: %v", err)
|
||
}
|
||
defer sqlDB.Close()
|
||
|
||
// ========== 4. 初始化 Redis ==========
|
||
redisClient, err := database.InitRedis(cfg)
|
||
if err != nil {
|
||
logger.Sugared.Fatalf("初始化 Redis 失败: %v", err)
|
||
}
|
||
logger.Sugared.Info("Redis 连接成功")
|
||
defer redisClient.Close()
|
||
|
||
// ========== 5. 初始化 Repository 层 ==========
|
||
userRepo := repository.NewUserRepo(mysqlDB)
|
||
studentRepo := repository.NewStudentRepo(mysqlDB)
|
||
adminRoleRepo := repository.NewAdminRoleRepo(mysqlDB)
|
||
classRepo := repository.NewClassRepo(mysqlDB)
|
||
conductRepo := repository.NewConductRepo(mysqlDB)
|
||
attendanceRepo := repository.NewAttendanceRepo(mysqlDB)
|
||
semesterRepo := repository.NewSemesterRepo(mysqlDB)
|
||
subjectRepo := repository.NewSubjectRepo(mysqlDB)
|
||
assignmentRepo := repository.NewAssignmentRepo(mysqlDB)
|
||
logRepo := repository.NewLogRepo(mysqlDB)
|
||
superAdminRepo := repository.NewSuperAdminRepo(mysqlDB)
|
||
settingRepo := repository.NewSystemSettingRepo(mysqlDB)
|
||
|
||
// ========== 6. 初始化 Service 层 ==========
|
||
logService := service.NewLogService(logRepo)
|
||
|
||
authService := service.NewAuthService(
|
||
userRepo, studentRepo, adminRoleRepo, classRepo, logService,
|
||
)
|
||
adminService := service.NewAdminService(
|
||
userRepo, studentRepo, adminRoleRepo, classRepo,
|
||
)
|
||
conductService := service.NewConductService(
|
||
conductRepo, studentRepo, adminRoleRepo, semesterRepo, classRepo,
|
||
)
|
||
attendanceService := service.NewAttendanceService(
|
||
attendanceRepo, studentRepo, userRepo, conductRepo, semesterRepo, settingRepo, classRepo,
|
||
)
|
||
semesterService := service.NewSemesterService(
|
||
semesterRepo, studentRepo, classRepo, attendanceRepo, assignmentRepo, logService,
|
||
)
|
||
classService := service.NewClassService(
|
||
classRepo, userRepo, adminRoleRepo,
|
||
)
|
||
subjectService := service.NewSubjectService(subjectRepo)
|
||
studentService := service.NewStudentService(
|
||
studentRepo, conductRepo, attendanceRepo, semesterRepo,
|
||
)
|
||
parentService := service.NewParentService(
|
||
userRepo, studentRepo, conductRepo, attendanceRepo,
|
||
)
|
||
rankingService := service.NewRankingService(
|
||
studentRepo, conductRepo,
|
||
)
|
||
superAdminService := service.NewSuperAdminService(superAdminRepo, logService)
|
||
configService := service.NewConfigService(classRepo)
|
||
|
||
// 确保默认超级管理员存在
|
||
if err := superAdminService.EnsureDefaultAdmin(); err != nil {
|
||
logger.Sugared.Errorf("初始化默认超级管理员失败: %v", err)
|
||
}
|
||
|
||
// ========== 7. 初始化 Handler 层 ==========
|
||
handlers := &router.Handlers{
|
||
Auth: handler.NewAuthHandler(authService, superAdminService),
|
||
Admin: handler.NewAdminHandler(adminService, conductService, attendanceService, rankingService, logService),
|
||
Student: handler.NewStudentHandler(studentService, classRepo),
|
||
Parent: handler.NewParentHandler(parentService, authService, classService),
|
||
Subject: handler.NewSubjectHandler(subjectService),
|
||
Semester: handler.NewSemesterHandler(semesterService),
|
||
Class: handler.NewClassHandler(classService),
|
||
Config: handler.NewConfigHandler(configService),
|
||
SuperAdmin: handler.NewSuperAdminHandler(superAdminService),
|
||
Cadre: handler.NewCadreHandler(assignmentRepo, conductService, adminRoleRepo),
|
||
}
|
||
|
||
// ========== 8. 初始化路由 ==========
|
||
r := router.SetupRouter(cfg, handlers)
|
||
|
||
// ========== 9. 启动 HTTP 服务 ==========
|
||
addr := fmt.Sprintf(":%s", cfg.AppPort)
|
||
srv := &http.Server{
|
||
Addr: addr,
|
||
Handler: r,
|
||
ReadTimeout: 30 * time.Second,
|
||
WriteTimeout: 60 * time.Second,
|
||
IdleTimeout: 120 * time.Second,
|
||
}
|
||
|
||
// 优雅关闭
|
||
go func() {
|
||
logger.Sugared.Infof("HTTP 服务启动: http://0.0.0.0%s", addr)
|
||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||
logger.Sugared.Fatalf("HTTP 服务异常: %v", err)
|
||
}
|
||
}()
|
||
|
||
// ========== 10. 等待中断信号 ==========
|
||
quit := make(chan os.Signal, 1)
|
||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||
|
||
// ========== 自动周期重置定时任务 ==========
|
||
// 每天凌晨 1:00 检查是否有班级需要执行周/月重置
|
||
// 使用独立 done 通道避免与 quit 通道的竞态条件
|
||
timerDone := make(chan struct{})
|
||
go func() {
|
||
runAutoPeriodReset := func() {
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
logger.Sugared.Errorf("自动周期重置 panic: %v", r)
|
||
}
|
||
}()
|
||
semesterService.AutoPeriodReset()
|
||
}
|
||
|
||
// 计算距离下一个凌晨 1:00 的等待时间
|
||
waitUntilNext1AM := func() time.Duration {
|
||
now := time.Now()
|
||
next := time.Date(now.Year(), now.Month(), now.Day(), 1, 0, 0, 0, now.Location())
|
||
if now.After(next) {
|
||
next = next.Add(24 * time.Hour)
|
||
}
|
||
return next.Sub(now)
|
||
}
|
||
|
||
timer := time.NewTimer(waitUntilNext1AM())
|
||
defer timer.Stop()
|
||
|
||
for {
|
||
select {
|
||
case <-timerDone:
|
||
logger.Sugared.Info("定时任务收到退出信号,停止")
|
||
return
|
||
case <-timer.C:
|
||
runAutoPeriodReset()
|
||
timer.Reset(24 * time.Hour)
|
||
}
|
||
}
|
||
}()
|
||
|
||
sig := <-quit
|
||
close(timerDone)
|
||
logger.Sugared.Infof("收到信号 %v,正在关闭服务...", sig)
|
||
|
||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||
defer cancel()
|
||
|
||
if err := srv.Shutdown(ctx); err != nil {
|
||
logger.Sugared.Errorf("服务关闭异常: %v", err)
|
||
}
|
||
|
||
logger.Sugared.Info("服务已安全停止")
|
||
}
|