Files
cpdaily_xiaozhushou/backed/internal/handler/pending_school.go
T
becomer 2e2c161770 feat: init cpdaily-xiaozhushou with blue-green deploy
- 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
2026-07-03 15:37:55 +08:00

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})
}