Files
cpdaily_xiaozhushou/backed/cmd/server/main.go
T
becomer 4a80382b60
ci/woodpecker/tag/build Pipeline was successful
fix: 全局时间统一北京时间(Asia/Shanghai), 修复记录与展示偏移8小时
根因: 运行容器默认 UTC 且后端未设时区, time.Now()/gorm 时间戳/DSN
loc=Local/手动 Format 全部按 UTC, 导致展示为"美国时间"、月度与每日
边界错位。修复:
- main.go 在 database.Init 前固定 time.Local=Asia/Shanghai
  (DSN loc=Local 随之一致, 一处生效覆盖全部业务点)
- pending_school.go 今日判断改北京时间当日0点(原 Truncate 按UTC午夜偏移8h)
- Dockerfile/build.yml/compose 加 TZ=Asia/Shanghai 双保险
配套: 存量 datetime 列已 +8h 迁移(备份 /root/cpdaily-backup-pre-tz-*.sql)
2026-09-03 21:06:59 +08:00

205 lines
8.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"fmt"
"log"
"net/http"
"time"
"cpdaily-new/internal/config"
"cpdaily-new/internal/database"
"cpdaily-new/internal/handler"
"cpdaily-new/internal/middleware"
"cpdaily-new/internal/ratelimit"
"cpdaily-new/internal/service"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/filesystem"
)
func main() {
// 加载配置
if err := config.Load(""); err != nil {
log.Fatalf("加载配置失败: %v", err)
}
// 全局时区固定为中国标准时间(必须在 database.Init 之前设置:
// DSN 的 loc=Local 跟随 time.Local,影响数据库 datetime 读写序列化)。
// 运行镜像若不设 TZ 默认 UTC,会让所有记录/查询/统计/输出偏移 8 小时
// (前端展示为"美国时间"),每月 1 日与每日 0-8 点的边界计算也会错位。
loc, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
log.Fatalf("加载时区 Asia/Shanghai 失败: %v", err)
}
time.Local = loc
// 连接数据库
database.Init()
// 连接 Redis(验证码存储/限流)
service.InitRedis()
// 启动限流记录定时清理(每5分钟清理一次)
ratelimit.Cleanup(5 * time.Minute)
// 创建 Fiber 应用
app := fiber.New(fiber.Config{
AppName: "Cpdaily New",
// 信任 openresty 反代(host 网络,经 docker NAT 后源 IP 为 172.22.0.1),
// 从 X-Forwarded-For 取真实客户端 IP(如待适配学校提交记录)
TrustedProxies: []string{"172.22.0.0/16"},
EnableTrustedProxyCheck: true,
ProxyHeader: fiber.HeaderXForwardedFor,
})
// 中间件
app.Use(middleware.CORS())
// 路由
api := app.Group("/api")
// 认证相关
auth := api.Group("/auth")
auth.Get("/captcha", handler.GetCaptcha)
auth.Get("/schools", handler.SearchSchools)
auth.Post("/login", handler.Login)
auth.Post("/refresh", handler.RefreshToken)
auth.Get("/userinfo", middleware.AuthRequired(), handler.GetUserInfo)
auth.Post("/generate-invite", middleware.AuthRequired(), handler.GenerateMyInviteCode)
auth.Get("/my-invite", middleware.AuthRequired(), handler.GetMyInviteInfo)
auth.Post("/redeem-invite", middleware.AuthRequired(), handler.RedeemInviteReward)
auth.Post("/guide/complete", middleware.AuthRequired(), handler.CompleteGuide)
// 签到任务相关
task := api.Group("/task", middleware.AuthRequired())
task.Get("/titles", handler.GetTaskTitles)
task.Post("/create", handler.CreateTask)
task.Get("/list", handler.ListTasks)
task.Get("/form/:id", handler.GetTaskForm)
task.Post("/form/:id", handler.SaveTaskForm)
task.Put("/form/:id", handler.SaveTaskForm)
task.Put("/update/:id", handler.UpdateTask)
task.Delete("/delete/:id", handler.DeleteTask)
task.Get("/execute/:id", handler.ExecuteTask)
task.Get("/logs", handler.ListTaskLogs)
task.Delete("/logs/:id", handler.DeleteTaskLog)
task.Post("/upload-policy", handler.GetUploadPolicy)
task.Post("/preview-url", handler.GetPreviewUrl)
// CDK 兑换
cdk := api.Group("/cdk", middleware.AuthRequired())
cdk.Post("/redeem", handler.UserRedeemCDK)
// 设备绑定
device := api.Group("/device")
device.Post("/generate", middleware.AuthRequired(), handler.GenerateDeviceQr)
device.Post("/submit", handler.SubmitDeviceCode)
device.Get("/status", middleware.AuthRequired(), handler.GetDeviceStatus)
// 管理后台 API
admin := api.Group("/admin")
admin.Get("/setup/status", handler.AdminSetupStatus)
admin.Post("/setup/init", handler.AdminSetupInit)
admin.Post("/login", handler.AdminLogin)
admin.Get("/me", middleware.AdminAuthRequired(), handler.AdminMe)
// 管理后台 - 代理/管理员
adminAuth := api.Group("/admin", middleware.AdminAuthRequired())
adminAuth.Get("/cdks", handler.ListCDKs)
adminAuth.Post("/cdks", handler.GenerateCDK)
adminAuth.Put("/cdks/:id/status", handler.UpdateCDKStatus)
// 全量数据仅超管可见(代理只能看自己生成的)
adminAuth.Get("/all-cdks", middleware.AdminOnly(), handler.ListAllCDKs)
adminAuth.Get("/agents", handler.ListSubAgents)
adminAuth.Post("/agents", handler.CreateSubAgent)
adminAuth.Put("/agents/:id", handler.UpdateSubAgent)
adminAuth.Get("/agents/:id", handler.GetAgentDetail)
adminAuth.Put("/agents/:id/password", handler.UpdateAgentPassword)
adminAuth.Put("/agents/:id/status", handler.UpdateAgentStatus)
adminAuth.Get("/all-agents", middleware.AdminOnly(), handler.ListAllAgents)
adminAuth.Get("/agent/list", middleware.AdminOnly(), handler.ListAllAgents)
// 管理后台 - 用户管理(仅admin)
adminAuth.Get("/users", middleware.AdminOnly(), handler.ListUsers)
adminAuth.Get("/users/:id", middleware.AdminOnly(), handler.GetUserDetail)
adminAuth.Put("/users/:id/status", middleware.AdminOnly(), handler.UpdateUserStatus)
// 管理后台 - 公告(公告面向全站代理/用户推送,管理操作仅超管;代理/管理员通过 /my-announcements 收取公告)
adminAuth.Post("/announcements", middleware.AdminOnly(), handler.CreateAnnouncement)
adminAuth.Get("/announcements", middleware.AdminOnly(), handler.ListAnnouncements)
adminAuth.Get("/announcements/:id", middleware.AdminOnly(), handler.GetAnnouncement)
adminAuth.Put("/announcements/:id", middleware.AdminOnly(), handler.UpdateAnnouncement)
adminAuth.Delete("/announcements/:id", middleware.AdminOnly(), handler.DeleteAnnouncement)
// 管理后台 - 系统设置(全站计费/奖励/SMTP 配置,仅超管)
adminAuth.Get("/settings", middleware.AdminOnly(), handler.GetSettings)
adminAuth.Post("/settings", middleware.AdminOnly(), handler.UpdateSetting)
adminAuth.Put("/settings", middleware.AdminOnly(), handler.UpdateSetting)
adminAuth.Post("/settings/test-mail", middleware.AdminOnly(), handler.TestMail)
// 管理后台 - 我的公告(代理/管理员视角,共用普通用户公告处理器,但 target_type=agent)
adminAuth.Get("/my-announcements", handler.ListUserAnnouncements)
adminAuth.Get("/my-announcements/unread-count", handler.GetUnreadCount)
adminAuth.Get("/my-announcements/my-info", handler.GetMyAnnouncementInfo)
adminAuth.Post("/my-announcements/:id/read", handler.MarkAnnouncementRead)
// 用户公告
api.Get("/announcements", middleware.AuthRequired(), handler.ListUserAnnouncements)
api.Get("/announcements/unread-count", middleware.AuthRequired(), handler.GetUnreadCount)
api.Get("/announcements/my-info", middleware.AuthRequired(), handler.GetMyAnnouncementInfo)
api.Post("/announcements/:id/read", middleware.AuthRequired(), handler.MarkAnnouncementRead)
// 设备授权页面(纯 HTML)
api.Get("/device/authorize/:token", handler.AuthorizePage)
api.Get("/device/close", handler.ClosePage)
// 工单系统
ticket := api.Group("/ticket", middleware.AuthRequired())
ticket.Post("/create", handler.CreateTicket)
ticket.Get("/list", handler.ListMyTickets)
ticket.Get("/:id", handler.GetTicketDetail)
ticket.Post("/:id/message", handler.AddTicketMessage)
ticket.Put("/:id/close", handler.CloseTicket)
ticket.Delete("/:id", handler.DeleteTicket)
ticket.Get("/notifications/list", handler.ListTicketNotifications)
ticket.Get("/notifications/unread-count", handler.GetTicketUnreadCount)
ticket.Post("/notifications/:id/read", handler.MarkTicketNotificationRead)
// 用户报告登录失败(无需认证,有独立速率限制 + 验证码校验)
// 使用独立路径避免与 /ticket 组的 AuthRequired 中间件冲突
api.Post("/report-failed-login", handler.ReportFailedLogin)
// 管理后台 - 工单管理
adminAuth.Post("/ticket/reply/:id", handler.AdminReplyTicket)
adminAuth.Put("/ticket/status/:id", handler.AdminUpdateTicketStatus)
adminAuth.Get("/ticket/list", handler.AdminListTickets)
adminAuth.Get("/ticket/:id", handler.AdminGetTicket)
// 管理后台 - 待适配学校
adminAuth.Get("/pending-schools", handler.AdminListPendingSchools)
adminAuth.Post("/pending-schools/:id/mark-done", handler.AdminMarkSchoolDone)
// 健康检查(用于蓝绿部署)
app.Get("/health", func(c *fiber.Ctx) error {
return c.SendString("ok")
})
// 静态文件服务(前端 SPA)
app.Static("/assets", "./dist/assets", fiber.Static{
MaxAge: 86400,
})
app.Use(filesystem.New(filesystem.Config{
Root: http.Dir("./dist"),
Index: "index.html",
NotFoundFile: "index.html",
MaxAge: 3600,
}))
// 启动
addr := fmt.Sprintf(":%d", config.Global.Server.Port)
log.Printf("服务器启动在 %s", addr)
if err := app.Listen(addr); err != nil {
log.Fatalf("服务器启动失败: %v", err)
}
}