2e2c161770
- backed/: Go Fiber backend with cpdaily integration - frondend/: Vue 3 + Vite frontend - Dockerfile: multi-stage build (Node → Go → Alpine) - deploy/docker-compose.yml: production compose file - .woodpecker/build.yml: CI/CD with blue-green zero-downtime deploy - /health endpoint for deployment health checks - Port 8884/8885 blue-green swap via 1Panel OpenResty
217 lines
6.0 KiB
Go
217 lines
6.0 KiB
Go
package handler
|
|
|
|
import (
|
|
"cpdaily-new/internal/database"
|
|
"cpdaily-new/internal/model"
|
|
"cpdaily-new/internal/service"
|
|
"cpdaily-new/internal/utils"
|
|
"log/slog"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
)
|
|
|
|
// ---------- 速率限制 ----------
|
|
|
|
type rateEntry struct {
|
|
count int
|
|
resetTime time.Time
|
|
}
|
|
|
|
var (
|
|
rateMu sync.Mutex
|
|
rateMap = make(map[string]*rateEntry)
|
|
rateLimit = 5 // 每个 IP 最多提交次数
|
|
rateWindow = 24 * time.Hour // 时间窗口
|
|
)
|
|
|
|
func checkRateLimit(ip string) bool {
|
|
rateMu.Lock()
|
|
defer rateMu.Unlock()
|
|
|
|
now := time.Now()
|
|
entry, ok := rateMap[ip]
|
|
if !ok || now.After(entry.resetTime) {
|
|
rateMap[ip] = &rateEntry{count: 1, resetTime: now.Add(rateWindow)}
|
|
return true
|
|
}
|
|
if entry.count >= rateLimit {
|
|
return false
|
|
}
|
|
entry.count++
|
|
return true
|
|
}
|
|
|
|
// ---------- 请求结构体 ----------
|
|
|
|
type ReportFailedLoginRequest struct {
|
|
SchoolID string `json:"schoolId"`
|
|
SchoolName string `json:"schoolName"`
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
CaptchaID string `json:"captchaId"`
|
|
CaptchaCode string `json:"captchaCode"`
|
|
}
|
|
|
|
type PendingSchoolRes struct {
|
|
ID uint `json:"id"`
|
|
SchoolID string `json:"schoolId"`
|
|
SchoolName string `json:"schoolName"`
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
IP string `json:"ip"`
|
|
Status string `json:"status"`
|
|
CreatedAt string `json:"createdAt"`
|
|
}
|
|
|
|
// ---------- 用户端 API ----------
|
|
|
|
// ReportFailedLogin 用户报告登录失败(提交测试账号信息)
|
|
func ReportFailedLogin(c *fiber.Ctx) error {
|
|
// userId 可能不存在(未登录用户提交)
|
|
var userID uint
|
|
if uid, ok := c.Locals("userId").(uint); ok {
|
|
userID = uid
|
|
}
|
|
|
|
var req ReportFailedLoginRequest
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return utils.FailParam(c, "请求参数错误")
|
|
}
|
|
if req.SchoolID == "" || req.SchoolName == "" || req.Username == "" || req.Password == "" {
|
|
return utils.FailParam(c, "参数不完整")
|
|
}
|
|
if req.CaptchaID == "" || req.CaptchaCode == "" {
|
|
return utils.FailParam(c, "请输入验证码")
|
|
}
|
|
|
|
// 校验图片验证码
|
|
if !service.VerifyCaptcha(req.CaptchaID, req.CaptchaCode) {
|
|
return utils.FailParam(c, "验证码错误或已过期")
|
|
}
|
|
|
|
// 0. 速率限制:同 IP 每天最多 5 次
|
|
clientIP := c.IP()
|
|
slog.Info("[pending-school] 请求到达",
|
|
"schoolId", req.SchoolID,
|
|
"schoolName", req.SchoolName,
|
|
"username", req.Username,
|
|
"ip", clientIP,
|
|
"userId", userID,
|
|
)
|
|
|
|
if !checkRateLimit(clientIP) {
|
|
slog.Warn("[pending-school] 速率限制触发", "ip", clientIP, "schoolId", req.SchoolID, "userId", userID)
|
|
return utils.Success(c, fiber.Map{"accepted": true})
|
|
}
|
|
|
|
// 1. 检查该校是否有今天登录过的账号(说明学校可正常使用,忽略本次报告)
|
|
today := time.Now().Truncate(24 * time.Hour)
|
|
var todayLoginCount int64
|
|
database.DB.Model(&model.SysUser{}).
|
|
Where("school_id = ? AND last_login_at >= ?", req.SchoolID, today).
|
|
Count(&todayLoginCount)
|
|
if todayLoginCount > 0 {
|
|
slog.Info("[pending-school] 该校有今日登录的用户,提示用户检查账号",
|
|
"schoolId", req.SchoolID,
|
|
"schoolName", req.SchoolName,
|
|
"todayLoginCount", todayLoginCount,
|
|
)
|
|
return utils.Fail(c, 400, "当前学校已有用户正常登录,请仔细检查学工号密码信息")
|
|
}
|
|
|
|
// 2. 先查询待处理表中该校已存在多少条未处理记录
|
|
var pendingCount int64
|
|
database.DB.Model(&model.PendingSchool{}).
|
|
Where("school_id = ? AND status = ?", req.SchoolID, "pending").
|
|
Count(&pendingCount)
|
|
|
|
if pendingCount >= 5 {
|
|
slog.Info("[pending-school] 该校待处理记录已达上限,忽略提交",
|
|
"schoolId", req.SchoolID,
|
|
"schoolName", req.SchoolName,
|
|
"pendingCount", pendingCount,
|
|
)
|
|
return utils.Success(c, fiber.Map{"accepted": true})
|
|
}
|
|
|
|
// 2. 插入记录
|
|
record := model.PendingSchool{
|
|
SchoolID: req.SchoolID,
|
|
SchoolName: req.SchoolName,
|
|
Username: req.Username,
|
|
Password: req.Password,
|
|
IP: clientIP,
|
|
Status: "pending",
|
|
}
|
|
if err := database.DB.Create(&record).Error; err != nil {
|
|
slog.Error("[pending-school] 保存记录失败", "error", err)
|
|
return utils.FailServer(c, "提交失败")
|
|
}
|
|
|
|
slog.Info("[pending-school] 记录已保存",
|
|
"recordId", record.ID,
|
|
"schoolId", req.SchoolID,
|
|
"schoolName", req.SchoolName,
|
|
"username", req.Username,
|
|
"ip", clientIP,
|
|
)
|
|
|
|
return utils.Success(c, fiber.Map{"accepted": true})
|
|
}
|
|
|
|
// ---------- 管理端 API ----------
|
|
|
|
// AdminListPendingSchools 管理端查看待适配学校列表(按学校分组)
|
|
func AdminListPendingSchools(c *fiber.Ctx) error {
|
|
status := c.Query("status", "pending")
|
|
|
|
var records []model.PendingSchool
|
|
query := database.DB.Order("created_at DESC")
|
|
if status == "pending" {
|
|
query = query.Where("status = ?", "pending")
|
|
}
|
|
if err := query.Find(&records).Error; err != nil {
|
|
return utils.FailServer(c, "查询失败")
|
|
}
|
|
|
|
items := make([]PendingSchoolRes, 0, len(records))
|
|
for _, r := range records {
|
|
items = append(items, PendingSchoolRes{
|
|
ID: r.ID,
|
|
SchoolID: r.SchoolID,
|
|
SchoolName: r.SchoolName,
|
|
Username: r.Username,
|
|
Password: r.Password,
|
|
IP: r.IP,
|
|
Status: r.Status,
|
|
CreatedAt: r.CreatedAt.Format("2006-01-02 15:04"),
|
|
})
|
|
}
|
|
|
|
return utils.Success(c, items)
|
|
}
|
|
|
|
// AdminMarkSchoolDone 标记某条记录为已完成
|
|
func AdminMarkSchoolDone(c *fiber.Ctx) error {
|
|
recordID, err := c.ParamsInt("id")
|
|
if err != nil {
|
|
return utils.FailParam(c, "无效的记录ID")
|
|
}
|
|
|
|
var record model.PendingSchool
|
|
if err := database.DB.First(&record, recordID).Error; err != nil {
|
|
return utils.Fail(c, 404, "记录不存在")
|
|
}
|
|
|
|
// 将该学校所有待处理的记录标记为已完成
|
|
database.DB.Model(&model.PendingSchool{}).
|
|
Where("school_id = ? AND status = ?", record.SchoolID, "pending").
|
|
Update("status", "completed")
|
|
|
|
slog.Info("学校已标记为适配完成", "schoolId", record.SchoolID, "schoolName", record.SchoolName)
|
|
|
|
return utils.Success(c, fiber.Map{"completed": true})
|
|
}
|