mirror of
https://github.com/skyle1995/NetworkAuth.git
synced 2026-05-25 02:24:05 +08:00
调整日志和鉴权接管方案
This commit is contained in:
@@ -521,24 +521,42 @@ func AppDeleteHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除相关的变量记录
|
// 检查是否有关联的变量
|
||||||
if err := tx.Where("app_uuid = ?", app.UUID).Delete(&models.Variable{}).Error; err != nil {
|
var varCount int64
|
||||||
|
if err := tx.Model(&models.Variable{}).Where("app_uuid = ?", app.UUID).Count(&varCount).Error; err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
logrus.WithError(err).Error("Failed to delete related variables")
|
logrus.WithError(err).Error("Failed to count related variables")
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
"code": 1,
|
"code": 1,
|
||||||
"msg": "删除相关变量失败",
|
"msg": "检查关联变量失败",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if varCount > 0 {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"code": 1,
|
||||||
|
"msg": "该应用下存在关联变量,禁止删除",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除相关的函数记录
|
// 检查是否有关联的函数
|
||||||
if err := tx.Where("app_uuid = ?", app.UUID).Delete(&models.Function{}).Error; err != nil {
|
var funcCount int64
|
||||||
|
if err := tx.Model(&models.Function{}).Where("app_uuid = ?", app.UUID).Count(&funcCount).Error; err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
logrus.WithError(err).Error("Failed to delete related functions")
|
logrus.WithError(err).Error("Failed to count related functions")
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
"code": 1,
|
"code": 1,
|
||||||
"msg": "删除相关函数失败",
|
"msg": "检查关联函数失败",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if funcCount > 0 {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"code": 1,
|
||||||
|
"msg": "该应用下存在关联函数,禁止删除",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -581,7 +599,7 @@ func AppDeleteHandler(c *gin.Context) {
|
|||||||
logrus.WithFields(logrus.Fields{
|
logrus.WithFields(logrus.Fields{
|
||||||
"app_id": app.ID,
|
"app_id": app.ID,
|
||||||
"app_uuid": app.UUID,
|
"app_uuid": app.UUID,
|
||||||
}).Debug("Successfully deleted app and related APIs, Variables and Functions")
|
}).Debug("Successfully deleted app and related APIs")
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"code": 0,
|
"code": 0,
|
||||||
@@ -1254,6 +1272,46 @@ func AppsBatchDeleteHandler(c *gin.Context) {
|
|||||||
|
|
||||||
// 删除这些应用的所有相关接口
|
// 删除这些应用的所有相关接口
|
||||||
if len(appUUIDs) > 0 {
|
if len(appUUIDs) > 0 {
|
||||||
|
// 检查是否有关联的变量
|
||||||
|
var varCount int64
|
||||||
|
if err := tx.Model(&models.Variable{}).Where("app_uuid IN ?", appUUIDs).Count(&varCount).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
logrus.WithError(err).Error("Failed to count related variables")
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"code": 1,
|
||||||
|
"msg": "检查关联变量失败",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if varCount > 0 {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"code": 1,
|
||||||
|
"msg": "所选应用中存在关联变量,禁止删除",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有关联的函数
|
||||||
|
var funcCount int64
|
||||||
|
if err := tx.Model(&models.Function{}).Where("app_uuid IN ?", appUUIDs).Count(&funcCount).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
logrus.WithError(err).Error("Failed to count related functions")
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"code": 1,
|
||||||
|
"msg": "检查关联函数失败",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if funcCount > 0 {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"code": 1,
|
||||||
|
"msg": "所选应用中存在关联函数,禁止删除",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := tx.Where("app_uuid IN ?", appUUIDs).Delete(&models.API{}).Error; err != nil {
|
if err := tx.Where("app_uuid IN ?", appUUIDs).Delete(&models.API{}).Error; err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
logrus.WithError(err).Error("Failed to delete related APIs")
|
logrus.WithError(err).Error("Failed to delete related APIs")
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -83,37 +84,51 @@ func LoginHandler(c *gin.Context) {
|
|||||||
|
|
||||||
// 验证验证码
|
// 验证验证码
|
||||||
if !VerifyCaptcha(c, body.Captcha) {
|
if !VerifyCaptcha(c, body.Captcha) {
|
||||||
recordLoginLog(c, body.Username, 0, "验证码错误")
|
recordLoginLog(c, "", body.Username, 0, "验证码错误或已过期")
|
||||||
authBaseController.HandleValidationError(c, "验证码错误")
|
authBaseController.HandleValidationError(c, "验证码错误或已过期")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从数据库中查找对应的用户
|
// 从数据库中查找对应的用户
|
||||||
db, err := database.GetDB()
|
db, err := database.GetDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordLoginLog(c, body.Username, 0, "数据库连接失败")
|
recordLoginLog(c, "", body.Username, 0, "数据库连接失败")
|
||||||
authBaseController.HandleInternalError(c, "数据库连接失败", err)
|
authBaseController.HandleInternalError(c, "数据库连接失败", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var user models.User
|
var user models.User
|
||||||
if err := db.Where("username = ? AND role = ?", body.Username, 0).First(&user).Error; err != nil {
|
if err := db.Where("username = ?", body.Username).First(&user).Error; err != nil {
|
||||||
recordLoginLog(c, body.Username, 0, "用户不存在或非管理员")
|
recordLoginLog(c, user.UUID, body.Username, 0, "用户不存在")
|
||||||
authBaseController.HandleValidationError(c, "用户不存在或密码错误")
|
authBaseController.HandleValidationError(c, "用户不存在或密码错误")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查账号状态 (Status=1 表示启用,否则禁止登录)
|
||||||
|
if user.Status != 1 {
|
||||||
|
recordLoginLog(c, user.UUID, body.Username, 0, "账号已被禁用")
|
||||||
|
authBaseController.HandleValidationError(c, "该账号已被禁用,请联系超级管理员")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否允许登录 (role=0 或 role=1 允许登录,role=2 不允许)
|
||||||
|
if user.Role > 1 {
|
||||||
|
recordLoginLog(c, user.UUID, body.Username, 0, "权限不足")
|
||||||
|
authBaseController.HandleValidationError(c, "权限不足,禁止登录")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 验证密码(使用盐值校验)
|
// 验证密码(使用盐值校验)
|
||||||
if !utils.VerifyPasswordWithSalt(body.Password, user.PasswordSalt, user.Password) {
|
if !utils.VerifyPasswordWithSalt(body.Password, user.PasswordSalt, user.Password) {
|
||||||
recordLoginLog(c, body.Username, 0, "密码错误")
|
recordLoginLog(c, user.UUID, body.Username, 0, "密码错误")
|
||||||
authBaseController.HandleValidationError(c, "用户不存在或密码错误")
|
authBaseController.HandleValidationError(c, "用户不存在或密码错误")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成JWT令牌
|
// 生成JWT令牌
|
||||||
token, err := generateJWTTokenForAdmin(user.Username, user.Password, user.UUID)
|
token, err := generateJWTTokenForAdmin(user.Username, user.Password, user.UUID, user.Role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordLoginLog(c, body.Username, 0, "生成令牌失败")
|
recordLoginLog(c, user.UUID, body.Username, 0, "生成令牌失败")
|
||||||
authBaseController.HandleInternalError(c, "生成令牌失败", err)
|
authBaseController.HandleInternalError(c, "生成令牌失败", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -125,19 +140,20 @@ func LoginHandler(c *gin.Context) {
|
|||||||
cookie := utils.CreateSecureCookie("admin_session", token, maxAge, domain, secure, sameSite)
|
cookie := utils.CreateSecureCookie("admin_session", token, maxAge, domain, secure, sameSite)
|
||||||
c.SetCookie(cookie.Name, cookie.Value, cookie.MaxAge, cookie.Path, cookie.Domain, cookie.Secure, cookie.HttpOnly)
|
c.SetCookie(cookie.Name, cookie.Value, cookie.MaxAge, cookie.Path, cookie.Domain, cookie.Secure, cookie.HttpOnly)
|
||||||
|
|
||||||
recordLoginLog(c, body.Username, 1, "登录成功")
|
recordLoginLog(c, user.UUID, body.Username, 1, "登录成功")
|
||||||
authBaseController.HandleSuccess(c, "登录成功", gin.H{
|
authBaseController.HandleSuccess(c, "登录成功", gin.H{
|
||||||
"redirect": "/admin",
|
"redirect": "/admin",
|
||||||
"avatar": user.Avatar,
|
"avatar": user.Avatar,
|
||||||
"nickname": user.Nickname,
|
"nickname": user.Nickname,
|
||||||
"username": user.Username,
|
"username": user.Username,
|
||||||
|
"role": user.Role,
|
||||||
"token": token,
|
"token": token,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// recordLoginLog 记录登录日志
|
// recordLoginLog 记录登录日志
|
||||||
// status: 1-成功, 0-失败
|
// status: 1-成功, 0-失败
|
||||||
func recordLoginLog(c *gin.Context, username string, status int, message string) {
|
func recordLoginLog(c *gin.Context, uuid string, username string, status int, message string) {
|
||||||
db, err := database.GetDB()
|
db, err := database.GetDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 记录日志失败不应影响主流程,但可以记录到系统日志
|
// 记录日志失败不应影响主流程,但可以记录到系统日志
|
||||||
@@ -147,10 +163,11 @@ func recordLoginLog(c *gin.Context, username string, status int, message string)
|
|||||||
|
|
||||||
log := models.LoginLog{
|
log := models.LoginLog{
|
||||||
Type: "admin",
|
Type: "admin",
|
||||||
|
UUID: uuid,
|
||||||
Username: username,
|
Username: username,
|
||||||
IP: c.ClientIP(),
|
IP: c.ClientIP(),
|
||||||
Status: status,
|
Status: status,
|
||||||
Message: message,
|
Message: "登录管理 - " + message,
|
||||||
UserAgent: c.Request.UserAgent(),
|
UserAgent: c.Request.UserAgent(),
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
@@ -237,8 +254,9 @@ func getJWTSecret() []byte {
|
|||||||
return []byte(secret)
|
return []byte(secret)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 使用默认不安全密钥(仅开发环境)
|
// 3. 如果仍未获取到,则记录严重错误并抛出 panic,拒绝使用硬编码的不安全密钥
|
||||||
return []byte("default-insecure-jwt-secret")
|
logrus.Fatal("致命安全错误: 无法获取有效的 JWT 密钥,请检查数据库设置或重新安装系统。系统拒绝以不安全模式运行。")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -248,8 +266,8 @@ func getJWTSecret() []byte {
|
|||||||
// JWTClaims JWT载荷结构体
|
// JWTClaims JWT载荷结构体
|
||||||
type JWTClaims struct {
|
type JWTClaims struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
UUID string `json:"uuid"` // 添加虚拟角色UUID
|
UUID string `json:"uuid"` // 用户UUID
|
||||||
Role int `json:"role"` // 添加虚拟角色
|
Role int `json:"role"` // 用户角色
|
||||||
PasswordHash string `json:"password_hash"` // 密码哈希摘要,用于验证密码是否被修改
|
PasswordHash string `json:"password_hash"` // 密码哈希摘要,用于验证密码是否被修改
|
||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
}
|
}
|
||||||
@@ -258,7 +276,7 @@ type JWTClaims struct {
|
|||||||
// - 包含管理员用户名信息和密码哈希
|
// - 包含管理员用户名信息和密码哈希
|
||||||
// - 设置过期时间
|
// - 设置过期时间
|
||||||
// - 使用HMAC-SHA256签名
|
// - 使用HMAC-SHA256签名
|
||||||
func generateJWTTokenForAdmin(username, passwordHash string, adminUUID string) (string, error) {
|
func generateJWTTokenForAdmin(username, passwordHash string, adminUUID string, role int) (string, error) {
|
||||||
// 生成密码哈希摘要(使用SHA256)
|
// 生成密码哈希摘要(使用SHA256)
|
||||||
// 注意:传入的 passwordHash 已经是数据库存的 Hash,这里我们再次 Hash 还是直接用?
|
// 注意:传入的 passwordHash 已经是数据库存的 Hash,这里我们再次 Hash 还是直接用?
|
||||||
// atomicLibrary 的实现是: utils.GenerateSHA256Hash(adminUser.Password)
|
// atomicLibrary 的实现是: utils.GenerateSHA256Hash(adminUser.Password)
|
||||||
@@ -271,7 +289,7 @@ func generateJWTTokenForAdmin(username, passwordHash string, adminUUID string) (
|
|||||||
claims := JWTClaims{
|
claims := JWTClaims{
|
||||||
Username: username,
|
Username: username,
|
||||||
UUID: adminUUID,
|
UUID: adminUUID,
|
||||||
Role: 0, // 0表示超级管理员
|
Role: role, // 用户真实角色
|
||||||
PasswordHash: passwordHashDigest, // 包含密码哈希摘要
|
PasswordHash: passwordHashDigest, // 包含密码哈希摘要
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(services.GetSettingsService().GetJWTExpire()) * time.Hour)),
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(services.GetSettingsService().GetJWTExpire()) * time.Hour)),
|
||||||
@@ -339,9 +357,23 @@ func validateAdminPasswordHash(claims *JWTClaims, c *gin.Context) bool {
|
|||||||
|
|
||||||
// 获取当前数据库中的管理员用户
|
// 获取当前数据库中的管理员用户
|
||||||
var adminUser models.User
|
var adminUser models.User
|
||||||
if err := db.Where("username = ? AND role = ?", claims.Username, 0).First(&adminUser).Error; err != nil {
|
if err := db.Where("uuid = ?", claims.UUID).First(&adminUser).Error; err != nil {
|
||||||
fmt.Printf("[SECURITY WARNING] Admin user not found in database - Username=%s, IP=%s\n",
|
fmt.Printf("[SECURITY WARNING] Admin user not found in database - UUID=%s, IP=%s\n",
|
||||||
claims.Username, c.ClientIP())
|
claims.UUID, c.ClientIP())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查账号状态 (Status=1 表示启用,否则强制下线)
|
||||||
|
if adminUser.Status != 1 {
|
||||||
|
fmt.Printf("[SECURITY WARNING] Admin user is disabled - UUID=%s, IP=%s\n",
|
||||||
|
claims.UUID, c.ClientIP())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否允许登录 (role=0 或 role=1 允许,role=2不允许访问admin后台)
|
||||||
|
if adminUser.Role > 1 {
|
||||||
|
fmt.Printf("[SECURITY WARNING] Admin user role > 1 - UUID=%s, IP=%s\n",
|
||||||
|
claims.UUID, c.ClientIP())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,7 +445,17 @@ func IsAdminAuthenticatedHttp(r *http.Request) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var adminUser models.User
|
var adminUser models.User
|
||||||
if err := db.Where("username = ? AND role = ?", claims.Username, 0).First(&adminUser).Error; err != nil {
|
if err := db.Where("uuid = ?", claims.UUID).First(&adminUser).Error; err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查账号状态 (Status=1 表示启用,否则强制下线)
|
||||||
|
if adminUser.Status != 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否允许登录 (role=0 或 role=1 允许,role=2不允许访问admin后台)
|
||||||
|
if adminUser.Role > 1 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -525,10 +567,10 @@ func GetCurrentAdminUserWithRefresh(c *gin.Context) (*JWTClaims, bool, error) {
|
|||||||
// 获取当前的 PasswordHash
|
// 获取当前的 PasswordHash
|
||||||
db, _ := database.GetDB()
|
db, _ := database.GetDB()
|
||||||
var adminUser models.User
|
var adminUser models.User
|
||||||
db.Where("username = ? AND role = ?", claims.Username, 0).First(&adminUser)
|
db.Where("uuid = ? AND role = ?", claims.UUID, claims.Role).First(&adminUser)
|
||||||
|
|
||||||
// 使用新的有效期生成令牌
|
// 使用新的有效期生成令牌
|
||||||
newToken, err := generateJWTTokenForAdmin(claims.Username, adminUser.Password, claims.UUID)
|
newToken, err := generateJWTTokenForAdmin(claims.Username, adminUser.Password, claims.UUID, adminUser.Role)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
tokenToSet = newToken
|
tokenToSet = newToken
|
||||||
refreshed = true
|
refreshed = true
|
||||||
|
|||||||
@@ -1,67 +1,33 @@
|
|||||||
package admin
|
package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"math/big"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"NetworkAuth/middleware"
|
"NetworkAuth/middleware"
|
||||||
|
"NetworkAuth/utils"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/mojocn/base64Captcha"
|
"github.com/mojocn/base64Captcha"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 全局变量
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
// 全局验证码存储器
|
|
||||||
var store = base64Captcha.DefaultMemStore
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 辅助函数
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
// secureRandomInt 生成安全的随机整数,范围 [0, max)
|
|
||||||
func secureRandomInt(max int) (int, error) {
|
|
||||||
n, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return int(n.Int64()), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// API处理器
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
// CaptchaHandler 生成验证码图片
|
// CaptchaHandler 生成验证码图片
|
||||||
// GET /admin/captcha - 返回验证码图片
|
// GET /admin/captcha - 返回验证码图片
|
||||||
func CaptchaHandler(c *gin.Context) {
|
func CaptchaHandler(c *gin.Context) {
|
||||||
// 随机生成4-6位长度
|
// 配置与 User 端一致,采用较弱的验证码强度以提升正常用户体验
|
||||||
// 使用crypto/rand生成安全的随机数
|
|
||||||
randomNum, err := secureRandomInt(3)
|
|
||||||
if err != nil {
|
|
||||||
c.String(http.StatusInternalServerError, "生成随机数失败")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
captchaLength := 4 + randomNum // 4-6位随机长度
|
|
||||||
|
|
||||||
// 配置验证码参数 - 使用字母数字混合
|
|
||||||
driver := base64Captcha.DriverString{
|
driver := base64Captcha.DriverString{
|
||||||
Height: 60,
|
Height: 60,
|
||||||
Width: 200,
|
Width: 200,
|
||||||
NoiseCount: 0,
|
Length: 4,
|
||||||
ShowLineOptions: 2 | 4,
|
NoiseCount: 20, // 加点背景噪点干扰
|
||||||
Length: captchaLength,
|
ShowLineOptions: 2 | 4, // 加点干扰线
|
||||||
Source: "ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789", // 混合大小写字母和数字,去除易混淆字符
|
Source: "ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789",
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成验证码
|
// 生成验证码,使用共享的 CaptchaStore
|
||||||
captcha := base64Captcha.NewCaptcha(&driver, store)
|
captcha := base64Captcha.NewCaptcha(&driver, utils.CaptchaStore)
|
||||||
id, b64s, _, err := captcha.Generate()
|
id, b64s, _, err := captcha.Generate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.String(http.StatusInternalServerError, "生成验证码失败")
|
c.String(http.StatusInternalServerError, "生成验证码失败")
|
||||||
@@ -105,24 +71,6 @@ func VerifyCaptcha(c *gin.Context, captchaValue string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 先尝试原始值验证
|
// 调用共享的 VerifyCaptcha
|
||||||
if store.Verify(captchaId, captchaValue, false) {
|
return utils.VerifyCaptcha(captchaId, captchaValue)
|
||||||
// 验证成功后删除验证码
|
|
||||||
store.Verify(captchaId, captchaValue, true)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果原始值验证失败,尝试小写验证
|
|
||||||
if store.Verify(captchaId, strings.ToLower(captchaValue), false) {
|
|
||||||
// 验证成功后删除验证码
|
|
||||||
store.Verify(captchaId, strings.ToLower(captchaValue), true)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 最后尝试大写验证
|
|
||||||
if store.Verify(captchaId, strings.ToUpper(captchaValue), true) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"NetworkAuth/controllers"
|
"NetworkAuth/controllers"
|
||||||
"NetworkAuth/middleware"
|
"NetworkAuth/middleware"
|
||||||
"NetworkAuth/models"
|
"NetworkAuth/models"
|
||||||
"NetworkAuth/services"
|
|
||||||
"NetworkAuth/utils/timeutil"
|
"NetworkAuth/utils/timeutil"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -43,8 +42,6 @@ func formatDBType(dbType string) string {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
// API处理器
|
// API处理器
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// SystemInfoHandler 系统信息API接口
|
|
||||||
// 返回系统运行状态的JSON数据,用于前端定时刷新
|
// 返回系统运行状态的JSON数据,用于前端定时刷新
|
||||||
func SystemInfoHandler(c *gin.Context) {
|
func SystemInfoHandler(c *gin.Context) {
|
||||||
version := constants.AppVersion
|
version := constants.AppVersion
|
||||||
@@ -107,41 +104,3 @@ func DashboardStatsHandler(c *gin.Context) {
|
|||||||
|
|
||||||
handlersBaseController.HandleSuccess(c, "ok", data)
|
handlersBaseController.HandleSuccess(c, "ok", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DashboardLoginLogsHandler 获取管理员最近登录日志
|
|
||||||
func DashboardLoginLogsHandler(c *gin.Context) {
|
|
||||||
db, ok := handlersBaseController.GetDB(c)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取分页参数
|
|
||||||
page, limit := handlersBaseController.GetPaginationParams(c)
|
|
||||||
|
|
||||||
// 获取当前管理员信息(可能是 username 或 admin_username,具体取决于认证中间件设置的 key)
|
|
||||||
username := c.GetString("admin_username")
|
|
||||||
if username == "" {
|
|
||||||
// 尝试获取其他可能的键名
|
|
||||||
username = c.GetString("username")
|
|
||||||
}
|
|
||||||
|
|
||||||
var total int64
|
|
||||||
query := db.Model(&models.LoginLog{}).Where("type = ?", "admin")
|
|
||||||
|
|
||||||
// 如果有用户名,则仅过滤该用户的日志
|
|
||||||
if username != "" {
|
|
||||||
query = query.Where("username = ?", username)
|
|
||||||
}
|
|
||||||
|
|
||||||
logs, total, err := services.Paginate[models.LoginLog](query, page, limit, "created_at desc")
|
|
||||||
if err != nil {
|
|
||||||
handlersBaseController.HandleInternalError(c, "获取登录日志失败", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
data := gin.H{
|
|
||||||
"total": total,
|
|
||||||
"list": logs,
|
|
||||||
}
|
|
||||||
handlersBaseController.HandleSuccess(c, "获取登录日志成功", data)
|
|
||||||
}
|
|
||||||
|
|||||||
287
controllers/admin/log.go
Normal file
287
controllers/admin/log.go
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"NetworkAuth/controllers"
|
||||||
|
"NetworkAuth/models"
|
||||||
|
"NetworkAuth/services"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 全局变量
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
var logBaseController = controllers.NewBaseController()
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 登录日志 API处理器
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// DashboardLoginLogsHandler 获取管理员最近登录日志
|
||||||
|
func DashboardLoginLogsHandler(c *gin.Context) {
|
||||||
|
db, ok := logBaseController.GetDB(c)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取分页参数
|
||||||
|
page, limit := logBaseController.GetPaginationParams(c)
|
||||||
|
|
||||||
|
// 获取当前管理员信息
|
||||||
|
uuid := c.GetString("admin_uuid")
|
||||||
|
username := c.GetString("admin_username")
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
query := db.Model(&models.LoginLog{})
|
||||||
|
|
||||||
|
// 如果有用户名,则仅过滤该用户的日志
|
||||||
|
if uuid != "" {
|
||||||
|
query = query.Where("uuid = ? OR (uuid = '' AND username = ?)", uuid, username)
|
||||||
|
}
|
||||||
|
|
||||||
|
logs, total, err := services.Paginate[models.LoginLog](query, page, limit, "created_at desc")
|
||||||
|
if err != nil {
|
||||||
|
logBaseController.HandleInternalError(c, "获取登录日志失败", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data := gin.H{
|
||||||
|
"total": total,
|
||||||
|
"list": logs,
|
||||||
|
}
|
||||||
|
logBaseController.HandleSuccess(c, "获取登录日志成功", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginLogsListHandler 登录日志列表API处理器
|
||||||
|
func LoginLogsListHandler(c *gin.Context) {
|
||||||
|
// 获取分页参数
|
||||||
|
page, limit := logBaseController.GetPaginationParams(c)
|
||||||
|
|
||||||
|
// 获取数据库连接
|
||||||
|
db, ok := logBaseController.GetDB(c)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
query := db.Model(&models.LoginLog{})
|
||||||
|
|
||||||
|
// 筛选条件:账号或UUID合并搜索
|
||||||
|
if username := strings.TrimSpace(c.Query("username")); username != "" {
|
||||||
|
query = query.Where("username = ? OR uuid = ?", username, username)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 筛选条件:IP
|
||||||
|
if ip := strings.TrimSpace(c.Query("ip")); ip != "" {
|
||||||
|
query = query.Where("ip = ?", ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 筛选条件:状态
|
||||||
|
if statusStr := strings.TrimSpace(c.Query("status")); statusStr != "" {
|
||||||
|
if status, err := strconv.Atoi(statusStr); err == nil {
|
||||||
|
query = query.Where("status = ?", status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 筛选条件:时间范围
|
||||||
|
query = logBaseController.ApplyTimeRangeQuery(c, query, "created_at")
|
||||||
|
|
||||||
|
// 泛型分页查询
|
||||||
|
logs, total, err := services.Paginate[models.LoginLog](query, page, limit, "created_at DESC")
|
||||||
|
if err != nil {
|
||||||
|
logBaseController.HandleInternalError(c, "获取日志列表失败", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换数据格式
|
||||||
|
var list []map[string]interface{}
|
||||||
|
for _, log := range logs {
|
||||||
|
list = append(list, map[string]interface{}{
|
||||||
|
"id": log.ID,
|
||||||
|
"uuid": log.UUID,
|
||||||
|
"username": log.Username,
|
||||||
|
"ip": log.IP,
|
||||||
|
"status": log.Status,
|
||||||
|
"message": log.Message,
|
||||||
|
"user_agent": log.UserAgent,
|
||||||
|
"created_at": log.CreatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
logBaseController.HandleSuccess(c, "ok", gin.H{
|
||||||
|
"list": list,
|
||||||
|
"total": total,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginLogsClearHandler 清空登录日志API处理器
|
||||||
|
func LoginLogsClearHandler(c *gin.Context) {
|
||||||
|
// 鉴权拦截:仅超级管理员 (role=0) 允许清空日志
|
||||||
|
if role, exists := c.Get("admin_role"); !exists || role.(int) != 0 {
|
||||||
|
logBaseController.HandleValidationError(c, "权限不足,仅超级管理员可清空日志")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
db, ok := logBaseController.GetDB(c)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查数据库类型
|
||||||
|
dbType := db.Dialector.Name()
|
||||||
|
|
||||||
|
if dbType == "sqlite" {
|
||||||
|
// SQLite 不支持 TRUNCATE,直接使用 DELETE 和重置自增序列
|
||||||
|
if err := db.Session(&gorm.Session{AllowGlobalUpdate: true}).Unscoped().Where("1 = 1").Delete(&models.LoginLog{}).Error; err != nil {
|
||||||
|
logrus.WithError(err).Error("Failed to clear login logs")
|
||||||
|
logBaseController.HandleInternalError(c, "清空登录日志失败", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 重置 sqlite 的自增序列
|
||||||
|
db.Exec("UPDATE sqlite_sequence SET seq = 0 WHERE name = 'login_logs'")
|
||||||
|
// 释放空间
|
||||||
|
db.Exec("VACUUM")
|
||||||
|
} else {
|
||||||
|
// 其他数据库(如 MySQL/PostgreSQL)尝试使用 TRUNCATE
|
||||||
|
if err := db.Exec("TRUNCATE TABLE login_logs").Error; err != nil {
|
||||||
|
// 如果 TRUNCATE 失败,回退到 DELETE
|
||||||
|
if err := db.Session(&gorm.Session{AllowGlobalUpdate: true}).Unscoped().Where("1 = 1").Delete(&models.LoginLog{}).Error; err != nil {
|
||||||
|
logrus.WithError(err).Error("Failed to clear login logs")
|
||||||
|
logBaseController.HandleInternalError(c, "清空登录日志失败", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录操作日志
|
||||||
|
var operator, operatorUUID string
|
||||||
|
operator = c.GetString("admin_username")
|
||||||
|
operatorUUID = c.GetString("admin_uuid")
|
||||||
|
if operator == "" {
|
||||||
|
operator = "system"
|
||||||
|
operatorUUID = "system"
|
||||||
|
}
|
||||||
|
|
||||||
|
log := models.OperationLog{
|
||||||
|
OperationType: "清空登录日志",
|
||||||
|
Operator: operator,
|
||||||
|
OperatorUUID: operatorUUID,
|
||||||
|
Details: "管理员清空了所有登录日志",
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
db.Create(&log)
|
||||||
|
|
||||||
|
logBaseController.HandleSuccess(c, "登录日志已清空", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 操作日志 API处理器
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// LogsListHandler 日志列表API处理器
|
||||||
|
func LogsListHandler(c *gin.Context) {
|
||||||
|
// 获取分页参数
|
||||||
|
page, limit := logBaseController.GetPaginationParams(c)
|
||||||
|
|
||||||
|
// 获取搜索参数
|
||||||
|
operationType := strings.TrimSpace(c.Query("operation_type"))
|
||||||
|
operator := strings.TrimSpace(c.Query("operator"))
|
||||||
|
|
||||||
|
// 获取数据库连接
|
||||||
|
db, ok := logBaseController.GetDB(c)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
query := db.Model(&models.OperationLog{})
|
||||||
|
|
||||||
|
// 筛选条件
|
||||||
|
if operationType != "" {
|
||||||
|
query = query.Where("operation_type = ?", operationType)
|
||||||
|
}
|
||||||
|
if operator != "" {
|
||||||
|
// 支持按 UUID 或 用户名 筛选
|
||||||
|
query = query.Where("operator_uuid = ? OR operator = ?", operator, operator)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 筛选条件:时间范围
|
||||||
|
query = logBaseController.ApplyTimeRangeQuery(c, query, "created_at")
|
||||||
|
|
||||||
|
// 泛型分页查询
|
||||||
|
logs, total, err := services.Paginate[models.OperationLog](query, page, limit, "created_at DESC")
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Error("查询日志列表失败")
|
||||||
|
logBaseController.HandleInternalError(c, "查询日志列表失败", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logBaseController.HandleSuccess(c, "获取日志列表成功", gin.H{
|
||||||
|
"list": logs,
|
||||||
|
"total": total,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogsClearHandler 清空日志API处理器
|
||||||
|
func LogsClearHandler(c *gin.Context) {
|
||||||
|
// 鉴权拦截:仅超级管理员 (role=0) 允许清空日志
|
||||||
|
if role, exists := c.Get("admin_role"); !exists || role.(int) != 0 {
|
||||||
|
logBaseController.HandleValidationError(c, "权限不足,仅超级管理员可清空日志")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
db, ok := logBaseController.GetDB(c)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查数据库类型
|
||||||
|
dbType := db.Dialector.Name()
|
||||||
|
|
||||||
|
if dbType == "sqlite" {
|
||||||
|
// SQLite 不支持 TRUNCATE,直接使用 DELETE 和重置自增序列
|
||||||
|
if err := db.Session(&gorm.Session{AllowGlobalUpdate: true}).Unscoped().Where("1 = 1").Delete(&models.OperationLog{}).Error; err != nil {
|
||||||
|
logrus.WithError(err).Error("清空操作日志失败")
|
||||||
|
logBaseController.HandleInternalError(c, "清空操作日志失败", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 重置 sqlite 的自增序列
|
||||||
|
db.Exec("UPDATE sqlite_sequence SET seq = 0 WHERE name = 'operation_logs'")
|
||||||
|
// 释放空间
|
||||||
|
db.Exec("VACUUM")
|
||||||
|
} else {
|
||||||
|
// 其他数据库(如 MySQL/PostgreSQL)尝试使用 TRUNCATE
|
||||||
|
if err := db.Exec("TRUNCATE TABLE operation_logs").Error; err != nil {
|
||||||
|
// 如果 TRUNCATE 失败,回退到 DELETE
|
||||||
|
if err := db.Session(&gorm.Session{AllowGlobalUpdate: true}).Unscoped().Where("1 = 1").Delete(&models.OperationLog{}).Error; err != nil {
|
||||||
|
logrus.WithError(err).Error("清空操作日志失败")
|
||||||
|
logBaseController.HandleInternalError(c, "清空操作日志失败", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录操作日志 (因为刚刚清空了,这条将是第一条)
|
||||||
|
var operator, operatorUUID string
|
||||||
|
operator = c.GetString("admin_username")
|
||||||
|
operatorUUID = c.GetString("admin_uuid")
|
||||||
|
if operator == "" {
|
||||||
|
operator = "system"
|
||||||
|
operatorUUID = "system"
|
||||||
|
}
|
||||||
|
|
||||||
|
log := models.OperationLog{
|
||||||
|
OperationType: "清空日志",
|
||||||
|
Operator: operator,
|
||||||
|
OperatorUUID: operatorUUID,
|
||||||
|
Details: "管理员清空了所有操作日志",
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
db.Create(&log)
|
||||||
|
|
||||||
|
logBaseController.HandleSuccess(c, "日志已清空", nil)
|
||||||
|
}
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
package admin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"NetworkAuth/controllers"
|
|
||||||
"NetworkAuth/models"
|
|
||||||
"NetworkAuth/services"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 全局变量
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
var loginLogBaseController = controllers.NewBaseController()
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 辅助函数
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
// RecordLoginLog 记录登录日志
|
|
||||||
func RecordLoginLog(c *gin.Context, username string, status int, message string) {
|
|
||||||
db, ok := loginLogBaseController.GetDB(c)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log := models.LoginLog{
|
|
||||||
Type: "admin",
|
|
||||||
Username: username,
|
|
||||||
IP: c.ClientIP(),
|
|
||||||
Status: status,
|
|
||||||
Message: message,
|
|
||||||
UserAgent: c.Request.UserAgent(),
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := db.Create(&log).Error; err != nil {
|
|
||||||
logrus.WithError(err).Error("Failed to create login log")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// API处理器
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
// LoginLogsListHandler 登录日志列表API处理器
|
|
||||||
func LoginLogsListHandler(c *gin.Context) {
|
|
||||||
// 获取分页参数
|
|
||||||
page, limit := loginLogBaseController.GetPaginationParams(c)
|
|
||||||
|
|
||||||
// 获取数据库连接
|
|
||||||
db, ok := loginLogBaseController.GetDB(c)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 兼容旧数据(Type为空)和新数据(Type=admin)
|
|
||||||
query := db.Model(&models.LoginLog{}).Where("type = ? OR type = ? OR type IS NULL", "admin", "")
|
|
||||||
|
|
||||||
// 筛选条件:用户名
|
|
||||||
if username := strings.TrimSpace(c.Query("username")); username != "" {
|
|
||||||
query = query.Where("username = ?", username)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 筛选条件:IP
|
|
||||||
if ip := strings.TrimSpace(c.Query("ip")); ip != "" {
|
|
||||||
query = query.Where("ip = ?", ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 筛选条件:状态
|
|
||||||
if statusStr := strings.TrimSpace(c.Query("status")); statusStr != "" {
|
|
||||||
if status, err := strconv.Atoi(statusStr); err == nil {
|
|
||||||
query = query.Where("status = ?", status)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 筛选条件:时间范围
|
|
||||||
query = loginLogBaseController.ApplyTimeRangeQuery(c, query, "created_at")
|
|
||||||
|
|
||||||
// 泛型分页查询
|
|
||||||
logs, total, err := services.Paginate[models.LoginLog](query, page, limit, "created_at DESC")
|
|
||||||
if err != nil {
|
|
||||||
loginLogBaseController.HandleInternalError(c, "获取日志列表失败", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转换数据格式
|
|
||||||
var list []map[string]interface{}
|
|
||||||
for _, log := range logs {
|
|
||||||
list = append(list, map[string]interface{}{
|
|
||||||
"id": log.ID,
|
|
||||||
"username": log.Username,
|
|
||||||
"ip": log.IP,
|
|
||||||
"status": log.Status,
|
|
||||||
"message": log.Message,
|
|
||||||
"user_agent": log.UserAgent,
|
|
||||||
"created_at": log.CreatedAt,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
loginLogBaseController.HandleSuccess(c, "ok", gin.H{
|
|
||||||
"list": list,
|
|
||||||
"total": total,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoginLogsClearHandler 清空登录日志API处理器
|
|
||||||
func LoginLogsClearHandler(c *gin.Context) {
|
|
||||||
db, ok := loginLogBaseController.GetDB(c)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 物理删除所有登录日志
|
|
||||||
if err := db.Session(&gorm.Session{AllowGlobalUpdate: true}).Unscoped().Where("type = ? OR type = ? OR type IS NULL", "admin", "").Delete(&models.LoginLog{}).Error; err != nil {
|
|
||||||
logrus.WithError(err).Error("Failed to clear login logs")
|
|
||||||
loginLogBaseController.HandleInternalError(c, "清空登录日志失败", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 记录操作日志
|
|
||||||
var operator, operatorUUID string
|
|
||||||
if claims, _, err := GetCurrentAdminUserWithRefresh(c); err == nil && claims != nil {
|
|
||||||
operator = claims.Username
|
|
||||||
operatorUUID = claims.UUID
|
|
||||||
} else {
|
|
||||||
operator = "admin"
|
|
||||||
operatorUUID = "00000000-0000-0000-0000-000000000000"
|
|
||||||
}
|
|
||||||
|
|
||||||
log := models.OperationLog{
|
|
||||||
OperationType: "清空登录日志",
|
|
||||||
Operator: operator,
|
|
||||||
OperatorUUID: operatorUUID,
|
|
||||||
Details: "管理员清空了所有登录日志",
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
}
|
|
||||||
db.Create(&log)
|
|
||||||
|
|
||||||
loginLogBaseController.HandleSuccess(c, "登录日志已清空", nil)
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
package admin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"NetworkAuth/controllers"
|
|
||||||
"NetworkAuth/models"
|
|
||||||
"NetworkAuth/services"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 全局变量
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
var logBaseController = controllers.NewBaseController()
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// API处理器
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
// LogsListHandler 日志列表API处理器
|
|
||||||
func LogsListHandler(c *gin.Context) {
|
|
||||||
// 获取分页参数
|
|
||||||
page, limit := logBaseController.GetPaginationParams(c)
|
|
||||||
|
|
||||||
// 获取搜索参数
|
|
||||||
operationType := strings.TrimSpace(c.Query("operation_type"))
|
|
||||||
operator := strings.TrimSpace(c.Query("operator"))
|
|
||||||
|
|
||||||
// 获取数据库连接
|
|
||||||
db, ok := logBaseController.GetDB(c)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
query := db.Model(&models.OperationLog{})
|
|
||||||
|
|
||||||
// 筛选条件
|
|
||||||
if operationType != "" {
|
|
||||||
query = query.Where("operation_type = ?", operationType)
|
|
||||||
}
|
|
||||||
if operator != "" {
|
|
||||||
// 支持按 UUID 或 用户名 筛选
|
|
||||||
query = query.Where("operator_uuid = ? OR operator = ?", operator, operator)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 筛选条件:时间范围
|
|
||||||
query = logBaseController.ApplyTimeRangeQuery(c, query, "created_at")
|
|
||||||
|
|
||||||
// 泛型分页查询
|
|
||||||
logs, total, err := services.Paginate[models.OperationLog](query, page, limit, "created_at DESC")
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Error("查询日志列表失败")
|
|
||||||
logBaseController.HandleInternalError(c, "查询日志列表失败", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logBaseController.HandleSuccess(c, "获取日志列表成功", gin.H{
|
|
||||||
"list": logs,
|
|
||||||
"total": total,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogsClearHandler 清空日志API处理器
|
|
||||||
func LogsClearHandler(c *gin.Context) {
|
|
||||||
db, ok := logBaseController.GetDB(c)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 开启事务进行清空
|
|
||||||
if err := db.Session(&gorm.Session{AllowGlobalUpdate: true}).Unscoped().Delete(&models.OperationLog{}).Error; err != nil {
|
|
||||||
logrus.WithError(err).Error("清空操作日志失败")
|
|
||||||
logBaseController.HandleInternalError(c, "清空操作日志失败", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 记录操作日志 (因为刚刚清空了,这条将是第一条)
|
|
||||||
var operator, operatorUUID string
|
|
||||||
if claims, _, err := GetCurrentAdminUserWithRefresh(c); err == nil && claims != nil {
|
|
||||||
operator = claims.Username
|
|
||||||
operatorUUID = claims.UUID
|
|
||||||
} else {
|
|
||||||
operator = "admin"
|
|
||||||
operatorUUID = "00000000-0000-0000-0000-000000000000"
|
|
||||||
}
|
|
||||||
|
|
||||||
log := models.OperationLog{
|
|
||||||
OperationType: "清空日志",
|
|
||||||
Operator: operator,
|
|
||||||
OperatorUUID: operatorUUID,
|
|
||||||
Details: "管理员清空了所有操作日志",
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
}
|
|
||||||
db.Create(&log)
|
|
||||||
|
|
||||||
logBaseController.HandleSuccess(c, "日志已清空", nil)
|
|
||||||
}
|
|
||||||
@@ -244,7 +244,7 @@ func SettingsPublicHandler(c *gin.Context) {
|
|||||||
|
|
||||||
var list []models.Settings
|
var list []models.Settings
|
||||||
// 查询公开的基本信息、维护模式和所有前端平台配置
|
// 查询公开的基本信息、维护模式和所有前端平台配置
|
||||||
if err := db.Where("name IN ? OR name LIKE ?", []string{"site_title", "site_description", "site_keywords", "site_logo", "contact_email", "maintenance_mode"}, "platform_%").Find(&list).Error; err != nil {
|
if err := db.Where("name IN ? OR name LIKE ?", []string{"site_title", "site_description", "site_keywords", "site_logo", "contact_email", "maintenance_mode", "hide_login_entrance"}, "platform_%").Find(&list).Error; err != nil {
|
||||||
authBaseController.HandleInternalError(c, "查询失败", err)
|
authBaseController.HandleInternalError(c, "查询失败", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,13 @@ import (
|
|||||||
|
|
||||||
// InstallSubmitHandler 处理安装表单提交
|
// InstallSubmitHandler 处理安装表单提交
|
||||||
func InstallSubmitHandler(c *gin.Context) {
|
func InstallSubmitHandler(c *gin.Context) {
|
||||||
|
// 二次安全校验:检查系统是否已经安装
|
||||||
|
isInstalledStr := services.GetSettingsService().GetString("is_installed", "0")
|
||||||
|
if isInstalledStr == "1" {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{"code": 403, "msg": "系统已安装,禁止重复初始化"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var req struct {
|
var req struct {
|
||||||
// 数据库配置
|
// 数据库配置
|
||||||
DbType string `json:"db_type" binding:"required,oneof=sqlite mysql"`
|
DbType string `json:"db_type" binding:"required,oneof=sqlite mysql"`
|
||||||
@@ -109,14 +116,13 @@ func InstallSubmitHandler(c *gin.Context) {
|
|||||||
|
|
||||||
// 更新或创建超级管理员账号
|
// 更新或创建超级管理员账号
|
||||||
var adminUser models.User
|
var adminUser models.User
|
||||||
if err := tx.Where("uuid = ?", "00000000-0000-0000-0000-000000000000").First(&adminUser).Error; err != nil {
|
if err := tx.Where("role = ?", 0).First(&adminUser).Error; err != nil {
|
||||||
// 如果不存在则创建
|
// 如果不存在则创建
|
||||||
adminUser = models.User{
|
adminUser = models.User{
|
||||||
UUID: "00000000-0000-0000-0000-000000000000",
|
|
||||||
Username: strings.TrimSpace(req.AdminUsername),
|
Username: strings.TrimSpace(req.AdminUsername),
|
||||||
Password: adminPasswordHash,
|
Password: adminPasswordHash,
|
||||||
PasswordSalt: adminSalt,
|
PasswordSalt: adminSalt,
|
||||||
Nickname: "管理员",
|
Nickname: "超级管理员",
|
||||||
Avatar: "",
|
Avatar: "",
|
||||||
Role: 0,
|
Role: 0,
|
||||||
Status: 1,
|
Status: 1,
|
||||||
@@ -133,7 +139,7 @@ func InstallSubmitHandler(c *gin.Context) {
|
|||||||
adminUser.Username = strings.TrimSpace(req.AdminUsername)
|
adminUser.Username = strings.TrimSpace(req.AdminUsername)
|
||||||
adminUser.Password = adminPasswordHash
|
adminUser.Password = adminPasswordHash
|
||||||
adminUser.PasswordSalt = adminSalt
|
adminUser.PasswordSalt = adminSalt
|
||||||
adminUser.Nickname = "管理员"
|
adminUser.Nickname = "超级管理员"
|
||||||
adminUser.Role = 0
|
adminUser.Role = 0
|
||||||
if err := tx.Save(&adminUser).Error; err != nil {
|
if err := tx.Save(&adminUser).Error; err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ func SeedDefaultSettings() error {
|
|||||||
Value: "0",
|
Value: "0",
|
||||||
Description: "维护模式,0=关闭维护模式,1=开启维护模式",
|
Description: "维护模式,0=关闭维护模式,1=开启维护模式",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "hide_login_entrance",
|
||||||
|
Value: "0",
|
||||||
|
Description: "隐藏登录入口,0=显示,1=隐藏(门户中不显示管理员或子账号登录入口)",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "encryption_key",
|
Name: "encryption_key",
|
||||||
Value: encryptionKey,
|
Value: encryptionKey,
|
||||||
|
|||||||
16
go.mod
16
go.mod
@@ -3,14 +3,17 @@ module NetworkAuth
|
|||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/andybalholm/brotli v1.2.1
|
||||||
github.com/gin-contrib/cors v1.7.6
|
github.com/gin-contrib/cors v1.7.6
|
||||||
github.com/gin-gonic/gin v1.12.0
|
github.com/gin-gonic/gin v1.12.0
|
||||||
github.com/glebarez/sqlite v1.11.0
|
github.com/glebarez/sqlite v1.11.0
|
||||||
|
github.com/go-resty/resty/v2 v2.17.2
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/mojocn/base64Captcha v1.3.8
|
github.com/mojocn/base64Captcha v1.3.8
|
||||||
github.com/redis/go-redis/v9 v9.18.0
|
github.com/redis/go-redis/v9 v9.18.0
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
|
github.com/skycheung803/go-bypasser v0.0.0-20250704092818-4b1591564510
|
||||||
github.com/spf13/cobra v1.9.1
|
github.com/spf13/cobra v1.9.1
|
||||||
github.com/spf13/viper v1.20.1
|
github.com/spf13/viper v1.20.1
|
||||||
github.com/xuri/excelize/v2 v2.10.1
|
github.com/xuri/excelize/v2 v2.10.1
|
||||||
@@ -26,6 +29,7 @@ require (
|
|||||||
github.com/bytedance/sonic v1.15.0 // indirect
|
github.com/bytedance/sonic v1.15.0 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/cloudflare/circl v1.5.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
@@ -36,15 +40,21 @@ require (
|
|||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||||
|
github.com/go-rod/rod v0.116.2 // indirect
|
||||||
|
github.com/go-rod/stealth v0.4.9 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||||
github.com/goccy/go-json v0.10.6 // indirect
|
github.com/goccy/go-json v0.10.6 // indirect
|
||||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
|
github.com/imroc/req/v3 v3.49.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/compress v1.17.11 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
@@ -53,6 +63,7 @@ require (
|
|||||||
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
|
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
|
||||||
github.com/quic-go/qpack v0.6.0 // indirect
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||||
|
github.com/refraction-networking/utls v1.6.7 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/richardlehane/mscfb v1.0.6 // indirect
|
github.com/richardlehane/mscfb v1.0.6 // indirect
|
||||||
github.com/richardlehane/msoleps v1.0.6 // indirect
|
github.com/richardlehane/msoleps v1.0.6 // indirect
|
||||||
@@ -67,6 +78,11 @@ require (
|
|||||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
github.com/xuri/efp v0.0.1 // indirect
|
github.com/xuri/efp v0.0.1 // indirect
|
||||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
|
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
|
||||||
|
github.com/ysmood/fetchup v0.2.3 // indirect
|
||||||
|
github.com/ysmood/goob v0.4.0 // indirect
|
||||||
|
github.com/ysmood/got v0.40.0 // indirect
|
||||||
|
github.com/ysmood/gson v0.7.3 // indirect
|
||||||
|
github.com/ysmood/leakless v0.9.0 // indirect
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||||
go.uber.org/atomic v1.11.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
go.uber.org/multierr v1.9.0 // indirect
|
go.uber.org/multierr v1.9.0 // indirect
|
||||||
|
|||||||
49
go.sum
49
go.sum
@@ -1,5 +1,7 @@
|
|||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
|
||||||
|
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
@@ -12,6 +14,8 @@ github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiD
|
|||||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys=
|
||||||
|
github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
@@ -46,6 +50,13 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
|||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||||
|
github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk=
|
||||||
|
github.com/go-resty/resty/v2 v2.17.2/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
|
||||||
|
github.com/go-rod/rod v0.113.0/go.mod h1:aiedSEFg5DwG/fnNbUOTPMTTWX3MRj6vIs/a684Mthw=
|
||||||
|
github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA=
|
||||||
|
github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg=
|
||||||
|
github.com/go-rod/stealth v0.4.9 h1:X2PmQk4DUF2wzw6GOsWjW/glb8K5ebnftbEvLh7MlZ4=
|
||||||
|
github.com/go-rod/stealth v0.4.9/go.mod h1:eAzyvw8c0iAd5nJJsSWeh0fQ5z94vCIfdi1hUmYDimc=
|
||||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||||
@@ -62,10 +73,17 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
|
|||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
|
||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||||
|
github.com/imroc/req/v3 v3.49.0 h1:5Rac2qvz7Dq0E3PeBo/c2szV3hagPQIGLoHtfBmYhu4=
|
||||||
|
github.com/imroc/req/v3 v3.49.0/go.mod h1:XZf4t94DNJzcA0UOBlA68hmSrWsAyvN407ADdH4mzCA=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
@@ -74,6 +92,8 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
|||||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||||
|
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
@@ -101,6 +121,8 @@ github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SA
|
|||||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||||
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
|
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
|
||||||
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
|
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
|
||||||
|
github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM=
|
||||||
|
github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
@@ -115,6 +137,8 @@ github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsF
|
|||||||
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/skycheung803/go-bypasser v0.0.0-20250704092818-4b1591564510 h1:4/6v3lBnoJOjc878II458DVLurtYS3H4S09TBT6PDf8=
|
||||||
|
github.com/skycheung803/go-bypasser v0.0.0-20250704092818-4b1591564510/go.mod h1:7moDdG5Uh/9NTDph5drq217W+drrX9MIa4obMx9o91A=
|
||||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||||
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
|
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
|
||||||
@@ -153,6 +177,25 @@ github.com/xuri/excelize/v2 v2.10.1 h1:V62UlqopMqha3kOpnlHy2CcRVw1V8E63jFoWUmMzx
|
|||||||
github.com/xuri/excelize/v2 v2.10.1/go.mod h1:iG5tARpgaEeIhTqt3/fgXCGoBRt4hNXgCp3tfXKoOIc=
|
github.com/xuri/excelize/v2 v2.10.1/go.mod h1:iG5tARpgaEeIhTqt3/fgXCGoBRt4hNXgCp3tfXKoOIc=
|
||||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
|
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
|
||||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||||
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
|
github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ=
|
||||||
|
github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns=
|
||||||
|
github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ=
|
||||||
|
github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18=
|
||||||
|
github.com/ysmood/gop v0.0.2/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk=
|
||||||
|
github.com/ysmood/gop v0.2.0 h1:+tFrG0TWPxT6p9ZaZs+VY+opCvHU8/3Fk6BaNv6kqKg=
|
||||||
|
github.com/ysmood/gop v0.2.0/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk=
|
||||||
|
github.com/ysmood/got v0.34.1/go.mod h1:yddyjq/PmAf08RMLSwDjPyCvHvYed+WjHnQxpH851LM=
|
||||||
|
github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q=
|
||||||
|
github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg=
|
||||||
|
github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY=
|
||||||
|
github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM=
|
||||||
|
github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE=
|
||||||
|
github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg=
|
||||||
|
github.com/ysmood/leakless v0.8.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ=
|
||||||
|
github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU=
|
||||||
|
github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||||
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||||
@@ -231,6 +274,8 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
|||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||||
|
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||||
|
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
|||||||
58
middleware/ratelimit.go
Normal file
58
middleware/ratelimit.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"NetworkAuth/utils"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RateLimit 基于 Redis 的简单固定窗口限流中间件
|
||||||
|
// limit: 时间窗口内允许的最大请求数
|
||||||
|
// window: 时间窗口大小
|
||||||
|
func RateLimit(limit int, window time.Duration) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
client := utils.GetRedis()
|
||||||
|
if client == nil {
|
||||||
|
// 如果 Redis 未配置或不可用,则放行(降级处理)
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ip := c.ClientIP()
|
||||||
|
path := c.FullPath()
|
||||||
|
if path == "" {
|
||||||
|
path = c.Request.URL.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建 Redis Key,按 IP 和接口路径限制
|
||||||
|
key := fmt.Sprintf("ratelimit:%s:%s", path, ip)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// 使用 INCR 增加计数
|
||||||
|
count, err := client.Incr(ctx, key).Result()
|
||||||
|
if err != nil {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是第一次访问,设置过期时间
|
||||||
|
if count == 1 {
|
||||||
|
client.Expire(ctx, key, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
if count > int64(limit) {
|
||||||
|
c.JSON(http.StatusTooManyRequests, gin.H{
|
||||||
|
"code": 429,
|
||||||
|
"msg": "请求过于频繁,请稍后再试",
|
||||||
|
})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,8 +24,8 @@ const (
|
|||||||
APITypeSingleLogin = 10 // 卡密登录
|
APITypeSingleLogin = 10 // 卡密登录
|
||||||
|
|
||||||
// 账号管理
|
// 账号管理
|
||||||
APITypeUserLogin = 20 // 用户登录
|
APITypeUserLogin = 20 // 字系统登录
|
||||||
APITypeUserRegin = 21 // 用户注册
|
APITypeUserRegin = 21 // 字系统注册
|
||||||
APITypeUserRecharge = 22 // 用户充值
|
APITypeUserRecharge = 22 // 用户充值
|
||||||
|
|
||||||
// 登出操作
|
// 登出操作
|
||||||
@@ -188,8 +188,8 @@ func GetAPITypes(categorized bool) interface{} {
|
|||||||
{
|
{
|
||||||
Name: "账号管理",
|
Name: "账号管理",
|
||||||
Types: []APITypeInfo{
|
Types: []APITypeInfo{
|
||||||
{Type: APITypeUserLogin, Name: "用户登录"},
|
{Type: APITypeUserLogin, Name: "子系统登录"},
|
||||||
{Type: APITypeUserRegin, Name: "用户注册"},
|
{Type: APITypeUserRegin, Name: "子系统注册"},
|
||||||
{Type: APITypeUserRecharge, Name: "用户充值"},
|
{Type: APITypeUserRecharge, Name: "用户充值"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
type LoginLog struct {
|
type LoginLog struct {
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
Type string `gorm:"type:varchar(20);index;comment:日志类型(admin/user)" json:"type"`
|
Type string `gorm:"type:varchar(20);index;comment:日志类型(admin/user)" json:"type"`
|
||||||
|
UUID string `gorm:"type:char(36);index;comment:用户UUID" json:"uuid"`
|
||||||
Username string `gorm:"type:varchar(100);index;comment:登录用户名" json:"username"`
|
Username string `gorm:"type:varchar(100);index;comment:登录用户名" json:"username"`
|
||||||
IP string `gorm:"type:varchar(50);comment:登录IP" json:"ip"`
|
IP string `gorm:"type:varchar(50);comment:登录IP" json:"ip"`
|
||||||
Status int `gorm:"type:tinyint;comment:登录状态 1:成功 0:失败" json:"status"`
|
Status int `gorm:"type:tinyint;comment:登录状态 1:成功 0:失败" json:"status"`
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ type User struct {
|
|||||||
Password string `gorm:"size:255;not null;comment:密码哈希值"`
|
Password string `gorm:"size:255;not null;comment:密码哈希值"`
|
||||||
PasswordSalt string `gorm:"size:64;not null;comment:密码加密盐值"`
|
PasswordSalt string `gorm:"size:64;not null;comment:密码加密盐值"`
|
||||||
Status int `gorm:"not null;default:1;comment:状态:0禁用,1启用" json:"status"`
|
Status int `gorm:"not null;default:1;comment:状态:0禁用,1启用" json:"status"`
|
||||||
Role int `gorm:"not null;default:2;comment:角色类型:0超级管理员,1代理成员,2普通成员" json:"role"`
|
Role int `gorm:"not null;default:2;comment:角色类型:0超级管理员,1管理员,2子账号" json:"role"`
|
||||||
Permissions string `gorm:"size:255;comment:权限列表,逗号分隔" json:"permissions"`
|
Permissions string `gorm:"size:255;comment:权限列表,逗号分隔" json:"permissions"`
|
||||||
Nickname string `gorm:"size:64;comment:用户昵称" json:"nickname"`
|
Nickname string `gorm:"size:64;comment:用户昵称" json:"nickname"`
|
||||||
Remark string `gorm:"size:255;comment:备注信息" json:"remark"`
|
Remark string `gorm:"size:255;comment:备注信息" json:"remark"`
|
||||||
|
|||||||
@@ -41,12 +41,12 @@ func RegisterAdminRoutes(rg *gin.RouterGroup) {
|
|||||||
authorized.POST("/settings/generate-key", adminctl.SettingsGenerateKeyHandler)
|
authorized.POST("/settings/generate-key", adminctl.SettingsGenerateKeyHandler)
|
||||||
|
|
||||||
// 操作日志API
|
// 操作日志API
|
||||||
authorized.GET("/logs", adminctl.LogsListHandler)
|
authorized.GET("/logs", adminctl.LogsListHandler) // 获取操作日志列表
|
||||||
authorized.POST("/logs/clear", adminctl.LogsClearHandler)
|
authorized.POST("/logs/clear", adminctl.LogsClearHandler) // 清空操作日志
|
||||||
|
|
||||||
// 登录日志API
|
// 登录日志API
|
||||||
authorized.GET("/login_logs", adminctl.LoginLogsListHandler)
|
authorized.GET("/login_logs", adminctl.LoginLogsListHandler) // 获取登录日志列表
|
||||||
authorized.POST("/login_logs/clear", adminctl.LoginLogsClearHandler)
|
authorized.POST("/login_logs/clear", adminctl.LoginLogsClearHandler) // 清空登录日志
|
||||||
|
|
||||||
// 子账号相关API (Mock)
|
// 子账号相关API (Mock)
|
||||||
authorized.GET("/subaccounts/simple", adminctl.SubAccountSimpleListHandler)
|
authorized.GET("/subaccounts/simple", adminctl.SubAccountSimpleListHandler)
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
defaultctrl "NetworkAuth/controllers/default"
|
defaultctrl "NetworkAuth/controllers/default"
|
||||||
|
"NetworkAuth/middleware"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -11,6 +13,6 @@ import (
|
|||||||
func RegisterDefaultRoutes(rg *gin.RouterGroup) {
|
func RegisterDefaultRoutes(rg *gin.RouterGroup) {
|
||||||
homeGroup := rg.Group("/home")
|
homeGroup := rg.Group("/home")
|
||||||
|
|
||||||
// 根路径
|
// 根路径 (限制:每分钟最多 60 次请求,防止 CC)
|
||||||
homeGroup.GET("", defaultctrl.RootHandler)
|
homeGroup.GET("", middleware.RateLimit(60, time.Minute), defaultctrl.RootHandler)
|
||||||
}
|
}
|
||||||
|
|||||||
326
services/request/resty_client.go
Normal file
326
services/request/resty_client.go
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
package request
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/flate"
|
||||||
|
"compress/gzip"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/cookiejar"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/andybalholm/brotli"
|
||||||
|
"github.com/go-resty/resty/v2"
|
||||||
|
"github.com/skycheung803/go-bypasser"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RestyClient struct {
|
||||||
|
client *resty.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (request *RestyClient) Resty() *resty.Client {
|
||||||
|
return request.client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient 创建一个基于 uTLS 指纹与 HTTP/2 指纹的 Resty 客户端
|
||||||
|
// baseURL 不为空则设置默认 BaseURL;proxyStr 不为空则启用 HTTP 代理(仅 HTTP/1.1)
|
||||||
|
// persistCookies 启用持久化 Cookie;followRedirect 启用重定向跟随;timeout 设置超时时间(秒,0 或负数则默认 60 秒)
|
||||||
|
func NewClient(baseURL string, proxyStr string, persistCookies bool, timeout int) *RestyClient {
|
||||||
|
rc := resty.New()
|
||||||
|
|
||||||
|
if baseURL != "" {
|
||||||
|
rc.SetBaseURL(baseURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
if persistCookies {
|
||||||
|
jar, _ := cookiejar.New(nil)
|
||||||
|
rc.SetCookieJar(jar)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置请求超时时间,如果传入 0 或负数则默认 60 秒
|
||||||
|
if timeout <= 0 {
|
||||||
|
timeout = 60
|
||||||
|
}
|
||||||
|
rc.SetTimeout(time.Duration(timeout) * time.Second)
|
||||||
|
|
||||||
|
// 统一设置客户端默认请求头(调用级 headers 可覆盖),字段按字母顺序排列
|
||||||
|
rc.SetHeader("accept", "*/*")
|
||||||
|
rc.SetHeader("accept-language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6")
|
||||||
|
rc.SetHeader("connection", "keep-alive")
|
||||||
|
rc.SetHeader("pragma", "no-cache")
|
||||||
|
rc.SetHeader("priority", "u=1,i")
|
||||||
|
rc.SetHeader("sec-ch-ua", "\"Chromium\";v=\"146\", \"Not-A.Brand\";v=\"24\", \"Google Chrome\";v=\"146\"")
|
||||||
|
rc.SetHeader("sec-ch-ua-mobile", "?0")
|
||||||
|
rc.SetHeader("sec-ch-ua-platform", "\"macOS\"")
|
||||||
|
rc.SetHeader("sec-fetch-dest", "empty")
|
||||||
|
rc.SetHeader("sec-fetch-mode", "cors")
|
||||||
|
rc.SetHeader("sec-fetch-site", "same-origin")
|
||||||
|
rc.SetHeader("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36")
|
||||||
|
|
||||||
|
// 初始化 go-bypasser 替代原有的 spoofed-round-tripper
|
||||||
|
opts := []bypasser.BypasserOption{
|
||||||
|
bypasser.WithInsecureSkipVerify(true),
|
||||||
|
}
|
||||||
|
if proxyStr != "" {
|
||||||
|
opts = append(opts, bypasser.WithProxy(proxyStr))
|
||||||
|
}
|
||||||
|
|
||||||
|
bypass, err := bypasser.NewBypasser(opts...)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rc.SetTransport(bypass.Transport)
|
||||||
|
|
||||||
|
return &RestyClient{client: rc}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fillResponseBody 使用反射强制填充响应体
|
||||||
|
// 当 Resty 因为重定向策略错误而提前返回时,它可能不会读取 Body
|
||||||
|
// 此方法手动读取 RawResponse.Body 并回填到 resty.Response 的私有 body 字段中
|
||||||
|
func (request *RestyClient) fillResponseBody(resp *resty.Response) {
|
||||||
|
if resp == nil || resp.RawResponse == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 如果已经有 body 内容,则不处理
|
||||||
|
if len(resp.Body()) > 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取底层 Body
|
||||||
|
bodyBytes, err := io.ReadAll(resp.RawResponse.Body)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.RawResponse.Body.Close()
|
||||||
|
// 重置 Body 以便后续可能得读取
|
||||||
|
resp.RawResponse.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||||
|
|
||||||
|
// 使用反射设置私有字段 body
|
||||||
|
v := reflect.ValueOf(resp).Elem()
|
||||||
|
f := v.FieldByName("body")
|
||||||
|
if f.IsValid() {
|
||||||
|
// 必须使用 UnsafeAddr 获取未导出字段的地址
|
||||||
|
rf := reflect.NewAt(f.Type(), unsafe.Pointer(f.UnsafeAddr())).Elem()
|
||||||
|
rf.SetBytes(bodyBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置 size 字段
|
||||||
|
s := v.FieldByName("size")
|
||||||
|
if s.IsValid() {
|
||||||
|
rs := reflect.NewAt(s.Type(), unsafe.Pointer(s.UnsafeAddr())).Elem()
|
||||||
|
rs.SetInt(int64(len(bodyBytes)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeReq 构造带可选请求头的 resty.Request
|
||||||
|
// 功能:基于客户端创建请求对象,并在传入 headers 时进行设置
|
||||||
|
// 返回:带有请求头的请求对象
|
||||||
|
func (request *RestyClient) makeReq(headers map[string]string, cookies []*http.Cookie) *resty.Request {
|
||||||
|
req := request.client.R()
|
||||||
|
if len(headers) > 0 {
|
||||||
|
req = req.SetHeaders(headers)
|
||||||
|
}
|
||||||
|
if len(cookies) > 0 {
|
||||||
|
req = req.SetCookies(cookies)
|
||||||
|
}
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
// doWithEncodingFallback 封装请求发送并在出现压缩相关错误时进行一次降级重试
|
||||||
|
// 逻辑:首次请求失败且错误包含 gzip/zstd/brotli/magic number mismatch 时,设置 accept-encoding 为 identity 重试一次
|
||||||
|
func (request *RestyClient) doWithEncodingFallback(headers map[string]string, cookies []*http.Cookie, allowRedirect bool, do func(*resty.Request) (*resty.Response, error)) (*resty.Response, error) {
|
||||||
|
req := request.makeReq(headers, cookies)
|
||||||
|
if allowRedirect {
|
||||||
|
request.client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(10))
|
||||||
|
} else {
|
||||||
|
// 使用 http.ErrUseLastResponse 确保 302 响应被返回且 Body 可读,而不是报错
|
||||||
|
request.client.SetRedirectPolicy(resty.RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
resp, err := do(req)
|
||||||
|
|
||||||
|
// 尝试补救响应体(特别是当重定向被禁用导致报错时)
|
||||||
|
request.fillResponseBody(resp)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
s := err.Error()
|
||||||
|
if strings.Contains(s, "gzip: invalid header") || strings.Contains(s, "magic number mismatch") || strings.Contains(s, "zstd") || strings.Contains(s, "brotli") {
|
||||||
|
h2 := map[string]string{}
|
||||||
|
for k, v := range headers {
|
||||||
|
if strings.ToLower(k) != "accept-encoding" {
|
||||||
|
h2[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h2["Accept-Encoding"] = "identity"
|
||||||
|
req2 := request.makeReq(h2, cookies)
|
||||||
|
resp2, err2 := do(req2)
|
||||||
|
request.fillResponseBody(resp2)
|
||||||
|
if err2 == nil {
|
||||||
|
return resp2, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeResponse 处理响应解压与 JSON 解析
|
||||||
|
// 功能:自动识别 gzip 压缩并解压;在 result 非空时按 JSON 解析到 result
|
||||||
|
// 返回:解析错误(成功时为 nil)
|
||||||
|
func (request *RestyClient) decodeResponse(resp *resty.Response, result interface{}) error {
|
||||||
|
if resp == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ct := strings.ToLower(resp.Header().Get("Content-Type"))
|
||||||
|
ce := strings.ToLower(resp.Header().Get("Content-Encoding"))
|
||||||
|
body := resp.Body()
|
||||||
|
if strings.Contains(ce, "gzip") && len(body) > 0 {
|
||||||
|
gr, gerr := gzip.NewReader(bytes.NewReader(body))
|
||||||
|
if gerr == nil {
|
||||||
|
defer gr.Close()
|
||||||
|
if dec, derr := io.ReadAll(gr); derr == nil {
|
||||||
|
body = dec
|
||||||
|
resp.SetBody(body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if strings.Contains(ce, "deflate") && len(body) > 0 {
|
||||||
|
// 处理 deflate 压缩
|
||||||
|
dr := flate.NewReader(bytes.NewReader(body))
|
||||||
|
defer dr.Close()
|
||||||
|
if dec, derr := io.ReadAll(dr); derr == nil {
|
||||||
|
body = dec
|
||||||
|
resp.SetBody(body)
|
||||||
|
}
|
||||||
|
} else if strings.Contains(ce, "br") && len(body) > 0 {
|
||||||
|
// 处理 brotli 压缩
|
||||||
|
br := brotli.NewReader(bytes.NewReader(body))
|
||||||
|
if dec, derr := io.ReadAll(br); derr == nil {
|
||||||
|
body = dec
|
||||||
|
resp.SetBody(body) // 将解压后的 body 写回 response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if result != nil && (strings.Contains(ct, "application/json") || json.Valid(body)) {
|
||||||
|
if err := json.Unmarshal(body, result); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestyGet 发送 GET 请求
|
||||||
|
func (request *RestyClient) RestyGet(path string, result interface{}, headers map[string]string, cookies []*http.Cookie, allowRedirect bool) (*resty.Response, error) {
|
||||||
|
resp, err := request.doWithEncodingFallback(headers, cookies, allowRedirect, func(r *resty.Request) (*resty.Response, error) {
|
||||||
|
return r.Get(path)
|
||||||
|
})
|
||||||
|
if resp == nil && err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := request.decodeResponse(resp, result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestyPost 发送 POST 请求
|
||||||
|
func (request *RestyClient) RestyPost(path string, data any, result interface{}, headers map[string]string, cookies []*http.Cookie, allowRedirect bool) (*resty.Response, error) {
|
||||||
|
resp, err := request.doWithEncodingFallback(headers, cookies, allowRedirect, func(r *resty.Request) (*resty.Response, error) {
|
||||||
|
return r.SetBody(data).Post(path)
|
||||||
|
})
|
||||||
|
if resp == nil && err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := request.decodeResponse(resp, result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestyPut 发送 PUT 请求
|
||||||
|
// 功能:发送 PUT,支持请求级 headers 覆盖客户端默认,自动识别 gzip 并解析 JSON
|
||||||
|
// 返回:响应对象与错误信息
|
||||||
|
func (request *RestyClient) RestyPut(path string, data any, result interface{}, headers map[string]string, cookies []*http.Cookie, allowRedirect bool) (*resty.Response, error) {
|
||||||
|
resp, err := request.doWithEncodingFallback(headers, cookies, allowRedirect, func(r *resty.Request) (*resty.Response, error) {
|
||||||
|
return r.SetBody(data).Put(path)
|
||||||
|
})
|
||||||
|
if resp == nil && err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := request.decodeResponse(resp, result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestyPatch 发送 PATCH 请求
|
||||||
|
// 功能:发送 PATCH,支持请求级 headers 覆盖客户端默认,自动识别 gzip 并解析 JSON
|
||||||
|
// 返回:响应对象与错误信息
|
||||||
|
func (request *RestyClient) RestyPatch(path string, data any, result interface{}, headers map[string]string, cookies []*http.Cookie, allowRedirect bool) (*resty.Response, error) {
|
||||||
|
resp, err := request.doWithEncodingFallback(headers, cookies, allowRedirect, func(r *resty.Request) (*resty.Response, error) {
|
||||||
|
return r.SetBody(data).Patch(path)
|
||||||
|
})
|
||||||
|
if resp == nil && err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := request.decodeResponse(resp, result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestyDelete 发送 DELETE 请求
|
||||||
|
// 功能:发送 DELETE,支持请求级 headers 覆盖客户端默认,自动识别 gzip 并解析 JSON
|
||||||
|
// 返回:响应对象与错误信息
|
||||||
|
func (request *RestyClient) RestyDelete(path string, result interface{}, headers map[string]string, cookies []*http.Cookie, allowRedirect bool) (*resty.Response, error) {
|
||||||
|
resp, err := request.doWithEncodingFallback(headers, cookies, allowRedirect, func(r *resty.Request) (*resty.Response, error) {
|
||||||
|
return r.Delete(path)
|
||||||
|
})
|
||||||
|
if resp == nil && err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := request.decodeResponse(resp, result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestyHead 发送 HEAD 请求
|
||||||
|
// 功能:发送 HEAD,支持请求级 headers 覆盖客户端默认;HEAD 通常无正文
|
||||||
|
// 返回:响应对象与错误信息
|
||||||
|
func (request *RestyClient) RestyHead(path string, headers map[string]string, cookies []*http.Cookie, allowRedirect bool) (*resty.Response, error) {
|
||||||
|
resp, err := request.doWithEncodingFallback(headers, cookies, allowRedirect, func(r *resty.Request) (*resty.Response, error) {
|
||||||
|
return r.Head(path)
|
||||||
|
})
|
||||||
|
if resp == nil && err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestyOptions 发送 OPTIONS 请求
|
||||||
|
// 功能:发送 OPTIONS,支持请求级 headers 覆盖客户端默认
|
||||||
|
// 返回:响应对象与错误信息
|
||||||
|
func (request *RestyClient) RestyOptions(path string, headers map[string]string, cookies []*http.Cookie, allowRedirect bool) (*resty.Response, error) {
|
||||||
|
resp, err := request.doWithEncodingFallback(headers, cookies, allowRedirect, func(r *resty.Request) (*resty.Response, error) {
|
||||||
|
return r.Options(path)
|
||||||
|
})
|
||||||
|
if resp == nil && err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
38
utils/captcha.go
Normal file
38
utils/captcha.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mojocn/base64Captcha"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CaptchaStore 全局验证码存储器
|
||||||
|
// 使用 base64Captcha 提供的默认内存存储,确保 admin 和 user 端可以共享验证码状态
|
||||||
|
var CaptchaStore = base64Captcha.DefaultMemStore
|
||||||
|
|
||||||
|
// VerifyCaptcha 验证验证码的有效性
|
||||||
|
// captchaId: 验证码的唯一标识符
|
||||||
|
// captchaValue: 用户输入的验证码内容
|
||||||
|
// 返回值: 验证是否通过
|
||||||
|
// 该函数提供函数级注释,支持大小写不敏感匹配,验证通过后会自动删除验证码
|
||||||
|
func VerifyCaptcha(captchaId, captchaValue string) bool {
|
||||||
|
if captchaId == "" || captchaValue == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 switch 进行连续逻辑判断,尝试不同的大小写组合
|
||||||
|
switch {
|
||||||
|
case CaptchaStore.Verify(captchaId, captchaValue, true):
|
||||||
|
// 原始值匹配成功
|
||||||
|
return true
|
||||||
|
case CaptchaStore.Verify(captchaId, strings.ToLower(captchaValue), true):
|
||||||
|
// 小写匹配成功
|
||||||
|
return true
|
||||||
|
case CaptchaStore.Verify(captchaId, strings.ToUpper(captchaValue), true):
|
||||||
|
// 大写匹配成功
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
// 匹配失败
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user