Initial commit: 网络验证平台
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Init 初始化配置
|
||||
func Init() {
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yaml")
|
||||
viper.AddConfigPath(".")
|
||||
viper.AddConfigPath("./config")
|
||||
|
||||
// 设置默认值
|
||||
setDefaults()
|
||||
|
||||
// 读取配置文件
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
log.Printf("Warning: Config file not found, using defaults: %v", err)
|
||||
}
|
||||
|
||||
// 环境变量覆盖
|
||||
viper.AutomaticEnv()
|
||||
}
|
||||
|
||||
// setDefaults 设置默认配置值
|
||||
func setDefaults() {
|
||||
// 应用配置
|
||||
viper.SetDefault("app.name", "verification-platform")
|
||||
viper.SetDefault("app.env", "development")
|
||||
viper.SetDefault("app.port", "8080")
|
||||
viper.SetDefault("app.jwt_secret", "your-secret-key")
|
||||
viper.SetDefault("app.jwt_expire", "24h")
|
||||
|
||||
// 数据库配置
|
||||
viper.SetDefault("database.host", "localhost")
|
||||
viper.SetDefault("database.port", "3306")
|
||||
viper.SetDefault("database.name", "verification_platform")
|
||||
viper.SetDefault("database.username", "root")
|
||||
viper.SetDefault("database.password", "")
|
||||
viper.SetDefault("database.charset", "utf8mb4")
|
||||
viper.SetDefault("database.max_idle_conns", "10")
|
||||
viper.SetDefault("database.max_open_conns", "100")
|
||||
|
||||
// Redis配置
|
||||
viper.SetDefault("redis.host", "localhost")
|
||||
viper.SetDefault("redis.port", "6379")
|
||||
viper.SetDefault("redis.password", "")
|
||||
viper.SetDefault("redis.db", "0")
|
||||
|
||||
// 日志配置
|
||||
viper.SetDefault("log.level", "info")
|
||||
viper.SetDefault("log.format", "json")
|
||||
}
|
||||
|
||||
// GetString 获取字符串配置
|
||||
func GetString(key string) string {
|
||||
return viper.GetString(key)
|
||||
}
|
||||
|
||||
// GetInt 获取整数配置
|
||||
func GetInt(key string) int {
|
||||
return viper.GetInt(key)
|
||||
}
|
||||
|
||||
// GetBool 获取布尔配置
|
||||
func GetBool(key string) bool {
|
||||
return viper.GetBool(key)
|
||||
}
|
||||
|
||||
// GetStringSlice 获取字符串切片配置
|
||||
func GetStringSlice(key string) []string {
|
||||
return viper.GetStringSlice(key)
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,47 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/pkg/crypto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func AppCrypto() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
if appKey == "" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
var encryptType crypto.EncryptType
|
||||
switch app.EncryptType {
|
||||
case "aes":
|
||||
encryptType = crypto.EncryptTypeAES
|
||||
case "rc4":
|
||||
encryptType = crypto.EncryptTypeRC4
|
||||
default:
|
||||
encryptType = crypto.EncryptTypeNone
|
||||
}
|
||||
|
||||
cryptoManager := crypto.NewCryptoManager(encryptType, app.SecretKey)
|
||||
shouldEncrypt := encryptType != crypto.EncryptTypeNone
|
||||
|
||||
fmt.Printf("[AppCrypto] AppKey: %s, EncryptType: %s, SecretKey: %s, ShouldEncrypt: %v\n",
|
||||
appKey, app.EncryptType, app.SecretKey, shouldEncrypt)
|
||||
|
||||
c.Set("should_encrypt_response", shouldEncrypt)
|
||||
c.Set("crypto_manager", cryptoManager)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/pkg/crypto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type CryptoMiddleware struct {
|
||||
cryptoManager *crypto.CryptoManager
|
||||
shouldEncrypt bool
|
||||
}
|
||||
|
||||
func NewCryptoMiddleware(encryptType crypto.EncryptType, secretKey string) *CryptoMiddleware {
|
||||
shouldEncrypt := encryptType != crypto.EncryptTypeNone
|
||||
return &CryptoMiddleware{
|
||||
cryptoManager: crypto.NewCryptoManager(encryptType, secretKey),
|
||||
shouldEncrypt: shouldEncrypt,
|
||||
}
|
||||
}
|
||||
|
||||
type EncryptedRequest struct {
|
||||
Data string `json:"data" binding:"required"`
|
||||
}
|
||||
|
||||
type EncryptedResponse struct {
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
func (cm *CryptoMiddleware) ProcessRequest() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
contentType := c.GetHeader("Content-Type")
|
||||
|
||||
if strings.Contains(contentType, "application/json") {
|
||||
body, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"code": 400, "message": "读取请求体失败"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
var req interface{}
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
c.JSON(400, gin.H{"code": 400, "message": "解析请求体失败"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
reqMap, ok := req.(map[string]interface{})
|
||||
if ok {
|
||||
if encryptedData, exists := reqMap["data"]; exists {
|
||||
if dataStr, ok := encryptedData.(string); ok {
|
||||
decrypted, err := cm.cryptoManager.Decrypt(dataStr)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"code": 400, "message": "解密失败: " + err.Error()})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
var decryptedData interface{}
|
||||
if err := json.Unmarshal([]byte(decrypted), &decryptedData); err != nil {
|
||||
c.JSON(400, gin.H{"code": 400, "message": "解析解密数据失败"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("decrypted_data", decryptedData)
|
||||
c.Set("is_encrypted", true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
}
|
||||
|
||||
c.Set("should_encrypt_response", cm.shouldEncrypt)
|
||||
c.Set("crypto_manager", cm.cryptoManager)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func (cm *CryptoMiddleware) ProcessResponse() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
|
||||
c.Writer = blw
|
||||
|
||||
c.Next()
|
||||
|
||||
shouldEncrypt := c.GetBool("should_encrypt_response")
|
||||
fmt.Printf("[ProcessResponse] ShouldEncrypt: %v\n", shouldEncrypt)
|
||||
|
||||
if shouldEncrypt {
|
||||
var response map[string]interface{}
|
||||
if err := json.Unmarshal(blw.body.Bytes(), &response); err == nil {
|
||||
responseJSON, _ := json.Marshal(response)
|
||||
fmt.Printf("[ProcessResponse] Response to encrypt: %s\n", string(responseJSON))
|
||||
|
||||
var cryptoManager *crypto.CryptoManager
|
||||
if cm, ok := c.Get("crypto_manager"); ok {
|
||||
cryptoManager = cm.(*crypto.CryptoManager)
|
||||
} else {
|
||||
appKey := c.Param("appKey")
|
||||
if appKey != "" {
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err == nil {
|
||||
var encryptType crypto.EncryptType
|
||||
switch app.EncryptType {
|
||||
case "aes":
|
||||
encryptType = crypto.EncryptTypeAES
|
||||
case "rc4":
|
||||
encryptType = crypto.EncryptTypeRC4
|
||||
default:
|
||||
encryptType = crypto.EncryptTypeNone
|
||||
}
|
||||
cryptoManager = crypto.NewCryptoManager(encryptType, app.SecretKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cryptoManager != nil {
|
||||
encrypted, err := cryptoManager.Encrypt(string(responseJSON))
|
||||
if err == nil {
|
||||
encryptedResponse := EncryptedResponse{Data: encrypted}
|
||||
encryptedJSON, _ := json.Marshal(encryptedResponse)
|
||||
c.Writer.Header().Set("Content-Type", "application/json")
|
||||
c.Writer.Write(encryptedJSON)
|
||||
fmt.Printf("[ProcessResponse] Encrypted response sent\n")
|
||||
return
|
||||
} else {
|
||||
fmt.Printf("[ProcessResponse] Encryption error: %v\n", err)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("[ProcessResponse] CryptoManager is nil\n")
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("[ProcessResponse] JSON unmarshal error: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type bodyLogWriter struct {
|
||||
gin.ResponseWriter
|
||||
body *bytes.Buffer
|
||||
}
|
||||
|
||||
func (w *bodyLogWriter) Write(b []byte) (int, error) {
|
||||
return w.body.Write(b)
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/pkg/jwt"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func CheckAppStatus() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
if appKey == "" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if app.Status == "maintenance" {
|
||||
response.Error(c, 503, "应用维护中,暂时无法访问")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
if app.Status == "stopped" {
|
||||
response.Error(c, 403, "应用已停止运营")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("app", &app)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// JWT JWT中间件
|
||||
func JWT() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
fmt.Printf("JWT中间件被调用: %s %s\n", c.Request.Method, c.Request.URL.Path)
|
||||
|
||||
var token string
|
||||
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader != "" {
|
||||
parts := strings.SplitN(authHeader, " ", 2)
|
||||
if len(parts) == 2 && parts[0] == "Bearer" {
|
||||
token = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
token = c.Query("token")
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
fmt.Printf("JWT中间件: Authorization header is required\n")
|
||||
response.Error(c, http.StatusUnauthorized, "Authorization header is required")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := jwt.ParseToken(token)
|
||||
if err != nil {
|
||||
fmt.Printf("JWT中间件: Invalid token: %v\n", err)
|
||||
response.Error(c, http.StatusUnauthorized, "Invalid token")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if time.Now().Unix() > claims.ExpiresAt.Unix() {
|
||||
fmt.Printf("JWT中间件: Token expired\n")
|
||||
response.Error(c, http.StatusUnauthorized, "Token expired")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("role", claims.Role)
|
||||
|
||||
fmt.Printf("JWT中间件: 验证成功, user_id=%d, role=%s\n", claims.UserID, claims.Role)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// AdminAuth 管理员权限中间件
|
||||
func AdminAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
role, exists := c.Get("role")
|
||||
if !exists {
|
||||
response.Error(c, http.StatusForbidden, "Access denied")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if role != "admin" {
|
||||
response.Error(c, http.StatusForbidden, "Admin access required")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// DeveloperAuth 开发者权限中间件
|
||||
func DeveloperAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
role, exists := c.Get("role")
|
||||
if !exists {
|
||||
response.Error(c, http.StatusForbidden, "Access denied")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if role != "developer" && role != "admin" {
|
||||
response.Error(c, http.StatusForbidden, "Developer access required")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// PackageAuth 套餐授权中间件
|
||||
func PackageAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
role, _ := c.Get("role")
|
||||
if role == "admin" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
response.Error(c, 401, "未授权")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
var user model.User
|
||||
if err := database.DB.First(&user, userID).Error; err != nil {
|
||||
response.Error(c, 500, "获取用户信息失败")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if user.CurrentPackageID == nil {
|
||||
response.Error(c, 403, "您还没有购买套餐,请先购买套餐后再使用开发者功能")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
var userPackage model.UserPackage
|
||||
if err := database.DB.Where("user_id = ? AND package_id = ? AND status = ?",
|
||||
userID, user.CurrentPackageID, "active").First(&userPackage).Error; err != nil {
|
||||
response.Error(c, 403, "套餐授权已失效,请续费或重新购买套餐")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if userPackage.ExpiredAt != nil && userPackage.ExpiredAt.Before(time.Now()) {
|
||||
response.Error(c, 403, "套餐已过期,请续费后再使用开发者功能")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("user_package", &userPackage)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// Logger 日志中间件
|
||||
func Logger() gin.HandlerFunc {
|
||||
return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
|
||||
return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
|
||||
param.ClientIP,
|
||||
param.TimeStamp.Format(time.RFC1123),
|
||||
param.Method,
|
||||
param.Path,
|
||||
param.Request.Proto,
|
||||
param.StatusCode,
|
||||
param.Latency,
|
||||
param.Request.UserAgent(),
|
||||
param.ErrorMessage,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// Recovery 恢复中间件
|
||||
func Recovery() gin.HandlerFunc {
|
||||
return gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
|
||||
response.Error(c, http.StatusInternalServerError, "Internal server error")
|
||||
})
|
||||
}
|
||||
|
||||
// Cors 跨域中间件
|
||||
func Cors() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Credentials", "false")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE, PATCH")
|
||||
c.Writer.Header().Set("Access-Control-Max-Age", "86400")
|
||||
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RateLimit 限流中间件
|
||||
func RateLimit() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 这里可以实现基于Redis的限流逻辑
|
||||
// 暂时跳过实现
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func CheckApiLimit() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
response.Error(c, 401, "未授权")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
var user model.User
|
||||
if err := database.DB.Preload("CurrentPackage").First(&user, userID).Error; err != nil {
|
||||
response.Error(c, 500, "获取用户信息失败")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if user.CurrentPackageID == nil {
|
||||
response.Error(c, 403, "您还没有购买套餐,请先购买套餐")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
var permission model.PackagePermission
|
||||
if err := database.DB.Where("package_id = ?", user.CurrentPackageID).First(&permission).Error; err != nil {
|
||||
response.Error(c, 500, "获取套餐权限失败")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if user.ApiCallsResetAt == nil || now.Sub(*user.ApiCallsResetAt) >= 24*time.Hour {
|
||||
user.ApiCallsUsed = 0
|
||||
user.ApiCallsResetAt = &now
|
||||
database.DB.Save(&user)
|
||||
}
|
||||
|
||||
if user.ApiCallsUsed >= permission.MaxApiCalls {
|
||||
response.Error(c, 403, fmt.Sprintf("API调用次数已达上限(%d次/天),请升级套餐", permission.MaxApiCalls))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
|
||||
user.ApiCallsUsed++
|
||||
database.DB.Save(&user)
|
||||
}
|
||||
}
|
||||
|
||||
func CheckStorageLimit(fileSize int64) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
response.Error(c, 401, "未授权")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
var user model.User
|
||||
if err := database.DB.Preload("CurrentPackage").First(&user, userID).Error; err != nil {
|
||||
response.Error(c, 500, "获取用户信息失败")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if user.CurrentPackageID == nil {
|
||||
response.Error(c, 403, "您还没有购买套餐,请先购买套餐")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
var permission model.PackagePermission
|
||||
if err := database.DB.Where("package_id = ?", user.CurrentPackageID).First(&permission).Error; err != nil {
|
||||
response.Error(c, 500, "获取套餐权限失败")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
maxStorageBytes := int64(permission.MaxStorage) * 1024 * 1024
|
||||
if user.StorageUsed+fileSize > maxStorageBytes {
|
||||
usedMB := float64(user.StorageUsed) / 1024 / 1024
|
||||
maxMB := float64(permission.MaxStorage)
|
||||
response.Error(c, 403, fmt.Sprintf("存储空间不足,已使用 %.2f MB / %.2f MB", usedMB, maxMB))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func RecordApiUsage() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
startTime := time.Now()
|
||||
|
||||
c.Next()
|
||||
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
|
||||
appID, _ := c.Get("app_id")
|
||||
|
||||
duration := time.Since(startTime)
|
||||
|
||||
usage := model.ApiUsage{
|
||||
UserID: userID.(uint),
|
||||
ApplicationID: appID.(uint),
|
||||
Endpoint: c.Request.URL.Path,
|
||||
Method: c.Request.Method,
|
||||
IPAddress: c.ClientIP(),
|
||||
UserAgent: c.Request.UserAgent(),
|
||||
ResponseTime: int(duration.Milliseconds()),
|
||||
StatusCode: c.Writer.Status(),
|
||||
Success: c.Writer.Status() < 400,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if len(c.Errors) > 0 {
|
||||
usage.ErrorMessage = c.Errors.String()
|
||||
}
|
||||
|
||||
database.DB.Create(&usage)
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateStorageUsed(userID uint, fileSize int64, action string) error {
|
||||
var user model.User
|
||||
if err := database.DB.First(&user, userID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if action == "upload" {
|
||||
user.StorageUsed += fileSize
|
||||
} else if action == "delete" {
|
||||
user.StorageUsed -= fileSize
|
||||
if user.StorageUsed < 0 {
|
||||
user.StorageUsed = 0
|
||||
}
|
||||
}
|
||||
|
||||
return database.DB.Save(&user).Error
|
||||
}
|
||||
@@ -0,0 +1,928 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// User 平台用户模型(开发者、管理员)
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Username string `gorm:"uniqueIndex;size:50" json:"username"`
|
||||
Email *string `gorm:"uniqueIndex;size:100" json:"email"`
|
||||
Password string `gorm:"size:255" json:"-"`
|
||||
Avatar string `gorm:"size:255" json:"avatar"`
|
||||
Signature string `gorm:"size:255" json:"signature"`
|
||||
Role string `gorm:"size:20;default:developer" json:"role"`
|
||||
Status string `gorm:"size:20;default:active" json:"status"`
|
||||
DeviceID string `gorm:"size:100" json:"device_id"`
|
||||
LastLoginAt *time.Time `json:"last_login_at"`
|
||||
ResetToken string `gorm:"size:255" json:"-"`
|
||||
ResetTokenExpiresAt *time.Time `json:"reset_token_expires_at"`
|
||||
|
||||
CurrentPackageID *uint `json:"current_package_id"`
|
||||
ApiCallsUsed int `gorm:"default:0" json:"api_calls_used"`
|
||||
StorageUsed int64 `gorm:"default:0" json:"storage_used"`
|
||||
ApiCallsResetAt *time.Time `json:"api_calls_reset_at"`
|
||||
ApiToken string `gorm:"size:64" json:"api_token"`
|
||||
|
||||
ParentAgentID *uint `gorm:"index" json:"parent_agent_id"`
|
||||
Commission float64 `gorm:"default:0" json:"commission"`
|
||||
CanCreateAgent bool `gorm:"default:false" json:"can_create_agent"`
|
||||
Balance float64 `gorm:"default:0" json:"balance"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
Applications []Application `gorm:"foreignKey:UserID" json:"-"`
|
||||
AgentApplications []AgentApplication `gorm:"foreignKey:AgentID" json:"-"`
|
||||
CurrentPackage *Package `gorm:"foreignKey:CurrentPackageID" json:"current_package,omitempty"`
|
||||
ParentAgent *User `gorm:"foreignKey:ParentAgentID" json:"parent_agent,omitempty"`
|
||||
ChildAgents []User `gorm:"foreignKey:ParentAgentID" json:"child_agents,omitempty"`
|
||||
}
|
||||
|
||||
// AppUser 应用用户模型(应用对接的用户)
|
||||
type AppUser struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Username string `gorm:"uniqueIndex:idx_app_user;size:50" json:"username"`
|
||||
Email string `gorm:"size:100" json:"email"`
|
||||
Password string `gorm:"size:255" json:"-"`
|
||||
DeviceID string `gorm:"size:100" json:"device_id"`
|
||||
Avatar string `gorm:"size:255" json:"avatar"`
|
||||
Status string `gorm:"size:20;default:active" json:"status"`
|
||||
ApplicationID uint `gorm:"index:idx_app_user;not null" json:"application_id"`
|
||||
Balance float64 `gorm:"default:0" json:"balance"`
|
||||
ExpiryAt *time.Time `json:"expiry_at"`
|
||||
LastLoginAt *time.Time `json:"last_login_at"`
|
||||
LastHeartbeatAt *time.Time `json:"last_heartbeat_at"`
|
||||
IsTrialUser bool `gorm:"default:false" json:"is_trial_user"`
|
||||
TrialStartAt *time.Time `json:"trial_start_at"`
|
||||
TrialEndAt *time.Time `json:"trial_end_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
Application Application `gorm:"foreignKey:ApplicationID" json:"application,omitempty"`
|
||||
}
|
||||
|
||||
type PaymentChannel struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"size:100;not null" json:"name"`
|
||||
Type string `gorm:"size:50;not null" json:"type"`
|
||||
Icon string `gorm:"size:100" json:"icon"`
|
||||
Config string `gorm:"type:text" json:"config"`
|
||||
Sort int `gorm:"default:0" json:"sort"`
|
||||
Status string `gorm:"size:20;default:active" json:"status"`
|
||||
Remark string `gorm:"size:500" json:"remark"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
// DynamicCode 动态代码模型
|
||||
type DynamicCode struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
ApplicationID uint `gorm:"index;not null" json:"application_id"`
|
||||
Name string `gorm:"size:100;not null" json:"name"`
|
||||
Key string `gorm:"size:100;uniqueIndex;not null" json:"key"`
|
||||
Code string `gorm:"type:longtext;not null" json:"code"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
Status string `gorm:"size:20;default:active" json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
Application Application `gorm:"foreignKey:ApplicationID" json:"application,omitempty"`
|
||||
Creator *User `gorm:"foreignKey:UserID" json:"creator,omitempty"`
|
||||
UserID *uint `gorm:"index" json:"user_id"`
|
||||
}
|
||||
|
||||
// UserLevel 用户等级模型
|
||||
type UserLevel struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"size:50" json:"name"`
|
||||
Description string `gorm:"size:255" json:"description"`
|
||||
RequiredPoints int `gorm:"default:0" json:"required_points"`
|
||||
Discount int `gorm:"default:100" json:"discount"`
|
||||
IsDefault bool `gorm:"default:false" json:"is_default"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
// Application 应用模型
|
||||
type Application struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `json:"user_id"`
|
||||
Name string `gorm:"size:100" json:"name"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
IconURL string `gorm:"size:255" json:"icon_url"`
|
||||
AppKey string `gorm:"uniqueIndex;size:50" json:"app_key"`
|
||||
BillingType string `gorm:"size:20" json:"billing_type"`
|
||||
LoginPolicy string `gorm:"size:20;default:loose" json:"login_policy"`
|
||||
EncryptType string `gorm:"size:20" json:"encrypt_type"`
|
||||
SecretKey string `gorm:"size:255" json:"secret_key"`
|
||||
BindType string `gorm:"size:20" json:"bind_type"`
|
||||
MaxDevices int `gorm:"default:1" json:"max_devices"`
|
||||
ChangeLimit int `gorm:"default:3" json:"change_limit"`
|
||||
ChangeInterval int `gorm:"default:7" json:"change_interval"`
|
||||
ChangeExceedAction string `gorm:"size:20;default:deny" json:"change_exceed_action"`
|
||||
ChangeDeductAmount float64 `gorm:"default:1" json:"change_deduct_amount"`
|
||||
MultiOpenMode string `gorm:"size:20;default:forbidden" json:"multi_open_mode"`
|
||||
MaxInstances int `gorm:"default:1" json:"max_instances"`
|
||||
MultiOpen bool `gorm:"default:false" json:"multi_open"`
|
||||
EnableTrial bool `gorm:"default:false" json:"enable_trial"`
|
||||
TrialBalance float64 `gorm:"default:0" json:"trial_balance"`
|
||||
TrialDays int `gorm:"default:0" json:"trial_days"`
|
||||
EnableFreePeriod bool `gorm:"default:false" json:"enable_free_period"`
|
||||
FreePeriodType string `gorm:"size:20;default:range" json:"free_period_type"`
|
||||
FreePeriodStart string `gorm:"size:50" json:"free_period_start"`
|
||||
FreePeriodEnd string `gorm:"size:50" json:"free_period_end"`
|
||||
FreePeriodWeekdays string `gorm:"size:50" json:"free_period_weekdays"`
|
||||
FreePeriodStartTime string `gorm:"size:10" json:"free_period_start_time"`
|
||||
FreePeriodEndTime string `gorm:"size:10" json:"free_period_end_time"`
|
||||
HeartbeatInterval int `gorm:"default:60" json:"heartbeat_interval"`
|
||||
HeartbeatTimeout int `gorm:"default:300" json:"heartbeat_timeout"`
|
||||
MaxAttempts int `gorm:"default:5" json:"max_attempts"`
|
||||
LockDuration int `gorm:"default:30" json:"lock_duration"`
|
||||
Status string `gorm:"size:20;default:active" json:"status"`
|
||||
DeductionMode string `gorm:"size:20;default:auto" json:"deduction_mode"`
|
||||
DeductionType string `gorm:"size:20;default:login" json:"deduction_type"`
|
||||
DeductionInterval int `gorm:"default:1" json:"deduction_interval"`
|
||||
DeductionUnit string `gorm:"size:20;default:minute" json:"deduction_unit"`
|
||||
DeductionAmount float64 `gorm:"default:1" json:"deduction_amount"`
|
||||
AllowRegister bool `gorm:"default:true" json:"allow_register"`
|
||||
RegisterMethods string `gorm:"size:100;default:'[\"username\"]'" json:"register_methods"`
|
||||
EnableEmailVerify bool `gorm:"default:false" json:"enable_email_verify"`
|
||||
RequireEmailVerify bool `gorm:"default:false" json:"require_email_verify"`
|
||||
EnablePasswordReset bool `gorm:"default:false" json:"enable_password_reset"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
}
|
||||
|
||||
// Version 版本模型
|
||||
type Version struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
ApplicationID uint `json:"application_id"`
|
||||
Version string `gorm:"size:50" json:"version"`
|
||||
FilePath string `gorm:"size:255" json:"file_path"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
FileHash string `gorm:"size:64" json:"file_hash"`
|
||||
EntryFile string `gorm:"size:255" json:"entry_file"`
|
||||
ForceUpdate bool `gorm:"default:false" json:"force_update"`
|
||||
UpdateStrategy string `gorm:"size:20;default:optional" json:"update_strategy"`
|
||||
UpdateMethod string `gorm:"size:20;default:manual" json:"update_method"`
|
||||
MinVersion string `gorm:"size:50" json:"min_version"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
Changelog string `gorm:"type:text" json:"changelog"`
|
||||
Status string `gorm:"size:20;default:active" json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
Application Application `gorm:"foreignKey:ApplicationID" json:"application,omitempty"`
|
||||
Files []VersionFile `gorm:"foreignKey:VersionID" json:"files,omitempty"`
|
||||
}
|
||||
|
||||
// VersionFile 版本文件模型
|
||||
type VersionFile struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
VersionID uint `json:"version_id"`
|
||||
FilePath string `gorm:"size:500" json:"file_path"`
|
||||
FileName string `gorm:"size:255" json:"file_name"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
FileHash string `gorm:"size:64" json:"file_hash"`
|
||||
FileType string `gorm:"size:20;default:resource" json:"file_type"`
|
||||
IsRequired bool `gorm:"default:true" json:"is_required"`
|
||||
SortOrder int `gorm:"default:0" json:"sort_order"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
Version Version `gorm:"foreignKey:VersionID" json:"version,omitempty"`
|
||||
}
|
||||
|
||||
// CardType 卡密类型模型
|
||||
type CardType struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `json:"user_id"`
|
||||
ApplicationID uint `json:"application_id"`
|
||||
Name string `gorm:"size:100" json:"name"`
|
||||
RechargeType string `gorm:"size:20;default:balance" json:"recharge_type"` // balance(余额充值), subscription(订阅充值)
|
||||
Value float64 `json:"value"` // 余额值或订阅时长(秒)
|
||||
ValueUnit string `gorm:"size:20;default:day" json:"value_unit"` // 时长单位: day, month, year (仅订阅充值)
|
||||
Price float64 `json:"price"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
Status string `gorm:"size:20;default:active" json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
Application *Application `gorm:"foreignKey:ApplicationID" json:"application,omitempty"`
|
||||
}
|
||||
|
||||
// Card 卡密模型
|
||||
type Card struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
ApplicationID uint `json:"application_id"` // 所属应用ID
|
||||
CardTypeID uint `json:"card_type_id"`
|
||||
CardKey string `gorm:"uniqueIndex;size:100" json:"card_key"`
|
||||
CreatorID uint `json:"creator_id"` // 创建人ID
|
||||
AppUserID *uint `json:"app_user_id"` // 使用者ID
|
||||
Status string `gorm:"size:20;default:unused" json:"status"` // unused, used, banned
|
||||
UsedAt *time.Time `json:"used_at"`
|
||||
ExpireAt *time.Time `json:"expire_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
Application *Application `gorm:"foreignKey:ApplicationID" json:"application,omitempty"`
|
||||
CardType CardType `gorm:"foreignKey:CardTypeID" json:"card_type,omitempty"`
|
||||
Creator User `gorm:"foreignKey:CreatorID" json:"creator,omitempty"`
|
||||
AppUser *AppUser `gorm:"foreignKey:AppUserID" json:"app_user,omitempty"`
|
||||
}
|
||||
|
||||
// Announcement 公告模型
|
||||
type Announcement struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
ApplicationID uint `json:"application_id"`
|
||||
Title string `gorm:"size:255" json:"title"`
|
||||
Content string `gorm:"type:text" json:"content"`
|
||||
Type string `gorm:"size:20;default:info" json:"type"` // info, warning, urgent
|
||||
Status string `gorm:"size:20;default:draft" json:"status"` // draft, active
|
||||
IsTop bool `gorm:"default:false" json:"is_top"` // 是否置顶
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
Application Application `gorm:"foreignKey:ApplicationID" json:"application,omitempty"`
|
||||
}
|
||||
|
||||
// Order 订单模型
|
||||
type Order struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
OrderNo string `gorm:"uniqueIndex;size:100" json:"order_no"`
|
||||
UserID uint `json:"user_id"`
|
||||
ApplicationID *uint `json:"application_id"`
|
||||
PackageID *uint `json:"package_id"`
|
||||
OrderType string `gorm:"size:50;not null" json:"order_type"` // card_recharge, agent_auth, user_recharge, user_deduct, package
|
||||
Title string `gorm:"size:200" json:"title"`
|
||||
Amount float64 `gorm:"type:decimal(10,2)" json:"amount"`
|
||||
PaymentType string `gorm:"size:50" json:"payment_type"` // alipay, wechat, balance, card, system
|
||||
PaymentMethod string `gorm:"size:50" json:"payment_method"`
|
||||
Status string `gorm:"size:20;default:pending" json:"status"` // pending, paid, cancelled, refunded, failed
|
||||
PaymentAt *time.Time `json:"payment_at"`
|
||||
RefundAt *time.Time `json:"refund_at"`
|
||||
RefundReason string `gorm:"type:text" json:"refund_reason"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
ExtraData string `gorm:"type:text" json:"extra_data"` // JSON格式存储额外数据
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
Application Application `gorm:"foreignKey:ApplicationID" json:"application,omitempty"`
|
||||
Package *Package `gorm:"foreignKey:PackageID" json:"package,omitempty"`
|
||||
}
|
||||
|
||||
// Log 日志模型
|
||||
type Log struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID *uint `json:"user_id"`
|
||||
ApplicationID *uint `json:"application_id"`
|
||||
AppUserID *uint `json:"app_user_id"`
|
||||
LogType string `gorm:"size:20;default:operation" json:"log_type"` // operation, verification, exception
|
||||
Action string `gorm:"size:100" json:"action"`
|
||||
Resource string `gorm:"size:100" json:"resource"`
|
||||
ResourceID *uint `json:"resource_id"`
|
||||
Details string `gorm:"type:text" json:"details"`
|
||||
IPAddress string `gorm:"size:50" json:"ip_address"`
|
||||
UserAgent string `gorm:"size:500" json:"user_agent"`
|
||||
DeviceID string `gorm:"size:100" json:"device_id"`
|
||||
Status string `gorm:"size:20;default:success" json:"status"` // success, failed, pending
|
||||
Level string `gorm:"size:20;default:info" json:"level"` // info, warning, error
|
||||
ErrorMessage string `gorm:"type:text" json:"error_message"`
|
||||
StackTrace string `gorm:"type:text" json:"stack_trace"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
User *User `gorm:"foreignKey:UserID" json:"user"`
|
||||
Application *Application `gorm:"foreignKey:ApplicationID" json:"application"`
|
||||
AppUser *AppUser `gorm:"foreignKey:AppUserID" json:"app_user"`
|
||||
}
|
||||
|
||||
// RechargeRecord 充值记录模型
|
||||
type RechargeRecord struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `json:"user_id"`
|
||||
OrderNo string `gorm:"size:50;uniqueIndex" json:"order_no"`
|
||||
CardID *uint `json:"card_id"`
|
||||
CardCode string `gorm:"size:100" json:"card_code"`
|
||||
Amount float64 `gorm:"type:decimal(10,2)" json:"amount"`
|
||||
Status string `gorm:"size:20;default:pending" json:"status"` // pending, success, failed
|
||||
PaymentType string `gorm:"size:50" json:"payment_type"` // card, alipay, wechat, balance
|
||||
Remark string `gorm:"type:text" json:"remark"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
AppUser *AppUser `gorm:"foreignKey:UserID" json:"app_user,omitempty"`
|
||||
Card *Card `gorm:"foreignKey:CardID" json:"card,omitempty"`
|
||||
}
|
||||
|
||||
// ConsumptionRecord 消费记录模型
|
||||
type ConsumptionRecord struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `json:"user_id"`
|
||||
ApplicationID uint `json:"application_id"`
|
||||
OrderNo string `gorm:"size:50;uniqueIndex" json:"order_no"`
|
||||
Type string `gorm:"size:50" json:"type"` // verification, card_purchase, subscription, feature, manual_deduct
|
||||
Content string `gorm:"type:text" json:"content"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
Amount float64 `gorm:"type:decimal(10,2)" json:"amount"`
|
||||
BalanceAfter float64 `gorm:"type:decimal(10,2)" json:"balance_after"`
|
||||
Status string `gorm:"size:20;default:success" json:"status"` // pending, success, failed
|
||||
PaymentType string `gorm:"size:50" json:"payment_type"` // alipay, wechat, balance
|
||||
Remark string `gorm:"type:text" json:"remark"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
AppUser *AppUser `gorm:"foreignKey:UserID" json:"app_user,omitempty"`
|
||||
}
|
||||
|
||||
// Ticket 工单模型
|
||||
type Ticket struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `json:"user_id"`
|
||||
ApplicationID *uint `json:"application_id"` // 关联的应用ID,如果为空则提交给平台
|
||||
Title string `gorm:"size:200" json:"title"`
|
||||
Content string `gorm:"type:text" json:"content"`
|
||||
Category string `gorm:"size:50" json:"category"` // account, payment, technical, feature, other
|
||||
Type string `gorm:"size:50" json:"type"` // user, agent, developer
|
||||
Status string `gorm:"size:20;default:open" json:"status"` // open, processing, resolved, closed
|
||||
Priority string `gorm:"size:20;default:normal" json:"priority"` // low, normal, high, urgent
|
||||
AssignedTo *uint `json:"assigned_to"` // 分配给的应用开发者ID或平台管理员ID
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
Application *Application `gorm:"foreignKey:ApplicationID" json:"application,omitempty"`
|
||||
AssignedUser *User `gorm:"foreignKey:AssignedTo" json:"assigned_user,omitempty"`
|
||||
Replies []TicketReply `gorm:"foreignKey:TicketID" json:"replies,omitempty"`
|
||||
}
|
||||
|
||||
// TicketReply 工单回复模型
|
||||
type TicketReply struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
TicketID uint `json:"ticket_id"`
|
||||
UserID uint `json:"user_id"`
|
||||
Content string `gorm:"type:text" json:"content"`
|
||||
IsAdmin bool `gorm:"default:false" json:"is_admin"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
Ticket Ticket `gorm:"foreignKey:TicketID" json:"ticket,omitempty"`
|
||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
}
|
||||
|
||||
// UserVariable 用户变量模型
|
||||
type UserVariable struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `json:"user_id"`
|
||||
AppID uint `json:"app_id"`
|
||||
VarName string `gorm:"size:100" json:"var_name"`
|
||||
VarValue string `gorm:"type:text" json:"var_value"`
|
||||
VarType string `gorm:"size:20;default:string" json:"var_type"`
|
||||
FilePath string `gorm:"size:500" json:"file_path"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
MimeType string `gorm:"size:100" json:"mime_type"`
|
||||
OriginalName string `gorm:"size:255" json:"original_name"`
|
||||
FileMD5 string `gorm:"size:32" json:"file_md5"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// CloudConstant 云端常量模型
|
||||
type CloudConstant struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `json:"user_id"`
|
||||
AppID *uint `json:"app_id"`
|
||||
Key string `gorm:"size:100;index" json:"key"`
|
||||
Value string `gorm:"type:text" json:"value"`
|
||||
VarType string `gorm:"size:20;default:string" json:"var_type"` // string, integer, decimal, binary
|
||||
FilePath string `gorm:"size:500" json:"file_path"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
MimeType string `gorm:"size:100" json:"mime_type"`
|
||||
OriginalName string `gorm:"size:255" json:"original_name"`
|
||||
FileMD5 string `gorm:"size:32" json:"file_md5"`
|
||||
Description string `gorm:"size:255" json:"description"`
|
||||
Status string `gorm:"size:20;default:active" json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
}
|
||||
|
||||
// RiskControlRule 风控规则模型
|
||||
type RiskControlRule struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `gorm:"index;not null" json:"user_id"` // 创建者ID
|
||||
ApplicationID *uint `gorm:"index" json:"application_id"` // 应用ID,为空表示全局规则
|
||||
Type string `gorm:"size:20;not null" json:"type"` // ip, device, user, region
|
||||
Value string `gorm:"size:255;not null" json:"value"`
|
||||
Reason string `gorm:"size:500" json:"reason"`
|
||||
Status string `gorm:"size:20;default:active" json:"status"` // active, inactive
|
||||
ExpiresAt *time.Time `json:"expires_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
Application Application `gorm:"foreignKey:ApplicationID" json:"application,omitempty"`
|
||||
}
|
||||
type CloudVariable struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `json:"user_id"`
|
||||
AppID *uint `json:"app_id"`
|
||||
Key string `gorm:"size:100;index" json:"key"`
|
||||
DefaultValue string `gorm:"type:text" json:"default_value"`
|
||||
VarType string `gorm:"size:20;default:string" json:"var_type"` // string, integer, decimal, binary
|
||||
DataType string `gorm:"size:20;default:single" json:"data_type"` // single, stream
|
||||
MaxRecords int `gorm:"default:0" json:"max_records"` // 流水类型最大记录数,0=不限制
|
||||
FilePath string `gorm:"size:500" json:"file_path"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
MimeType string `gorm:"size:100" json:"mime_type"`
|
||||
OriginalName string `gorm:"size:255" json:"original_name"`
|
||||
FileMD5 string `gorm:"size:32" json:"file_md5"`
|
||||
Scope string `gorm:"size:20;default:app" json:"scope"`
|
||||
WritePermission string `gorm:"size:20;default:developer" json:"write_permission"`
|
||||
Description string `gorm:"size:255" json:"description"`
|
||||
Status string `gorm:"size:20;default:active" json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
}
|
||||
|
||||
type CloudVariableRecord struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
CloudVariableID uint `gorm:"index;not null" json:"cloud_variable_id"`
|
||||
AppUserID *uint `json:"app_user_id"`
|
||||
Data string `gorm:"type:text" json:"data"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
CloudVariable CloudVariable `gorm:"foreignKey:CloudVariableID" json:"cloud_variable,omitempty"`
|
||||
}
|
||||
|
||||
// IPBlacklist IP黑名单模型
|
||||
type IPBlacklist struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `json:"user_id"`
|
||||
IPAddress string `gorm:"size:50" json:"ip_address"`
|
||||
Type string `gorm:"size:20;default:single" json:"type"` // single, range
|
||||
Reason string `gorm:"size:255" json:"reason"`
|
||||
ExpiresAt *time.Time `json:"expires_at"`
|
||||
Status string `gorm:"size:20;default:active" json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
}
|
||||
|
||||
// DeviceBlacklist 机器码黑名单模型
|
||||
type DeviceBlacklist struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `json:"user_id"`
|
||||
DeviceID string `gorm:"size:100" json:"device_id"`
|
||||
Type string `gorm:"size:20;default:temporary" json:"type"` // permanent, temporary
|
||||
Reason string `gorm:"size:255" json:"reason"`
|
||||
ExpiresAt *time.Time `json:"expires_at"`
|
||||
Status string `gorm:"size:20;default:active" json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
}
|
||||
|
||||
// AbnormalBehavior 异常行为模型
|
||||
type AbnormalBehavior struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID *uint `json:"user_id"`
|
||||
DeviceID string `gorm:"size:100" json:"device_id"`
|
||||
IPAddress string `gorm:"size:50" json:"ip_address"`
|
||||
Type string `gorm:"size:50" json:"type"` // frequent-login, abnormal-api, data-tampering, suspicious-activity
|
||||
Level string `gorm:"size:20" json:"level"` // low, medium, high, critical
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
Details string `gorm:"type:text" json:"details"` // JSON格式的详细信息
|
||||
Status string `gorm:"size:20;default:pending" json:"status"` // pending, handled, ignored
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
}
|
||||
|
||||
// DocCategory 文档分类模型
|
||||
type DocCategory struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"size:100" json:"name"`
|
||||
NameEn string `gorm:"size:100" json:"name_en"`
|
||||
Slug string `gorm:"size:100;uniqueIndex" json:"slug"`
|
||||
Description string `gorm:"size:255" json:"description"`
|
||||
DescriptionEn string `gorm:"size:255" json:"description_en"`
|
||||
Icon string `gorm:"size:50;default:file-text" json:"icon"`
|
||||
Status string `gorm:"size:20;default:active" json:"status"`
|
||||
Sort int `gorm:"default:0" json:"sort"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
// AgentApplication 代理应用授权模型
|
||||
type AgentApplication struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
AgentID uint `json:"agent_id"`
|
||||
ApplicationID uint `json:"application_id"`
|
||||
DeveloperID uint `json:"developer_id"`
|
||||
Commission float64 `gorm:"default:0.1" json:"commission"`
|
||||
Discount float64 `gorm:"default:1.0" json:"discount"`
|
||||
Status string `gorm:"size:20;default:active" json:"status"`
|
||||
IsReceived bool `gorm:"default:false" json:"is_received"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
Agent User `gorm:"foreignKey:AgentID;references:ID" json:"agent,omitempty"`
|
||||
Application Application `gorm:"foreignKey:ApplicationID;references:ID" json:"application,omitempty"`
|
||||
Developer User `gorm:"foreignKey:DeveloperID;references:ID" json:"developer,omitempty"`
|
||||
CardTypes []AgentApplicationCardType `gorm:"foreignKey:AgentApplicationID" json:"card_types,omitempty"`
|
||||
}
|
||||
|
||||
// AgentApplicationCardType 代理应用卡密类型授权
|
||||
type AgentApplicationCardType struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
AgentApplicationID uint `json:"agent_application_id"`
|
||||
CardTypeID uint `json:"card_type_id"`
|
||||
CanGenerate bool `gorm:"default:false" json:"can_generate"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
AgentApplication AgentApplication `gorm:"foreignKey:AgentApplicationID" json:"agent_application,omitempty"`
|
||||
CardType CardType `gorm:"foreignKey:CardTypeID" json:"card_type,omitempty"`
|
||||
}
|
||||
|
||||
// AgentApplicationRequest 代理应用申请/邀请模型
|
||||
type AgentApplicationRequest struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
AgentID uint `json:"agent_id"`
|
||||
DeveloperID uint `json:"developer_id"`
|
||||
ApplicationID uint `json:"application_id"`
|
||||
Type string `gorm:"size:20" json:"type"`
|
||||
Status string `gorm:"size:20;default:pending" json:"status"`
|
||||
Message string `gorm:"type:text" json:"message"`
|
||||
RejectReason string `gorm:"type:text" json:"reject_reason"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
Agent User `gorm:"foreignKey:AgentID" json:"agent,omitempty"`
|
||||
Developer User `gorm:"foreignKey:DeveloperID" json:"developer,omitempty"`
|
||||
Application Application `gorm:"foreignKey:ApplicationID" json:"application,omitempty"`
|
||||
}
|
||||
|
||||
// Package 套餐模型
|
||||
type Package struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"size:100" json:"name"`
|
||||
NameEn string `gorm:"size:100" json:"name_en"`
|
||||
Price float64 `gorm:"default:0" json:"price"`
|
||||
Currency string `gorm:"size:10;default:CNY" json:"currency"` // CNY, USD, EUR, GBP, JPY
|
||||
Period string `gorm:"size:20;default:permanent" json:"period"` // permanent, monthly, yearly
|
||||
Description string `gorm:"size:255" json:"description"`
|
||||
DescriptionEn string `gorm:"size:255" json:"description_en"`
|
||||
Status string `gorm:"size:20;default:active" json:"status"` // active, inactive
|
||||
Sort int `gorm:"default:0" json:"sort"`
|
||||
IsRecommended bool `gorm:"default:false" json:"is_recommended"`
|
||||
AllowUpgrade bool `gorm:"default:true" json:"allow_upgrade"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
// PackagePermission 套餐权限模型
|
||||
type PackagePermission struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
PackageID uint `json:"package_id"`
|
||||
MaxApplications int `gorm:"default:1" json:"max_applications"`
|
||||
MaxStorage int `gorm:"default:100" json:"max_storage"`
|
||||
MaxApiCalls int `gorm:"default:1000" json:"max_api_calls"`
|
||||
AllowAgent bool `gorm:"default:false" json:"allow_agent"`
|
||||
AllowCloudData bool `gorm:"default:false" json:"allow_cloud_data"`
|
||||
AllowDynamicCode bool `gorm:"default:false" json:"allow_dynamic_code"`
|
||||
AllowEmail bool `gorm:"default:false" json:"allow_email"`
|
||||
AllowExtension bool `gorm:"default:false" json:"allow_extension"`
|
||||
PrioritySupport bool `gorm:"default:false" json:"priority_support"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// Doc 文档模型
|
||||
type Doc struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
CategoryID *uint `json:"category_id"`
|
||||
Title string `gorm:"size:200" json:"title"`
|
||||
TitleEn string `gorm:"size:200" json:"title_en"`
|
||||
Slug string `gorm:"size:200;uniqueIndex" json:"slug"`
|
||||
Content string `gorm:"type:longtext" json:"content"`
|
||||
ContentEn string `gorm:"type:longtext" json:"content_en"`
|
||||
Summary string `gorm:"size:500" json:"summary"`
|
||||
SummaryEn string `gorm:"size:500" json:"summary_en"`
|
||||
Icon string `gorm:"size:50" json:"icon"`
|
||||
Sort int `gorm:"default:0" json:"sort"`
|
||||
Status string `gorm:"size:20;default:draft" json:"status"` // draft, published
|
||||
ViewCount int `gorm:"default:0" json:"view_count"`
|
||||
AuthorID *uint `json:"author_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
Category *DocCategory `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
|
||||
Author *User `gorm:"foreignKey:AuthorID" json:"author,omitempty"`
|
||||
}
|
||||
|
||||
// Setting 系统设置模型
|
||||
type Setting struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Category string `gorm:"size:50;index" json:"category"` // basic, email, sms, payment
|
||||
Key string `gorm:"size:100;uniqueIndex" json:"key"`
|
||||
Value string `gorm:"type:text" json:"value"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// Captcha 验证码模型
|
||||
type Captcha struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
CaptchaID string `gorm:"uniqueIndex;size:50" json:"captcha_id"`
|
||||
Code string `gorm:"size:10" json:"code"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type EmailVerifyCode struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
ApplicationID uint `gorm:"index" json:"application_id"`
|
||||
Email string `gorm:"size:100;index" json:"email"`
|
||||
Code string `gorm:"size:10" json:"code"`
|
||||
Purpose string `gorm:"size:20;default:register" json:"purpose"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
Used bool `gorm:"default:false" json:"used"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type AppSMTPConfig struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
ApplicationID uint `gorm:"uniqueIndex" json:"application_id"`
|
||||
Host string `gorm:"size:100" json:"host"`
|
||||
Port int `gorm:"default:465" json:"port"`
|
||||
User string `gorm:"size:100" json:"user"`
|
||||
Password string `gorm:"size:255" json:"password"`
|
||||
FromName string `gorm:"size:100" json:"from_name"`
|
||||
FromEmail string `gorm:"size:100" json:"from_email"`
|
||||
UseSSL bool `gorm:"default:true" json:"use_ssl"`
|
||||
Status string `gorm:"size:20;default:active" json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
Application Application `gorm:"foreignKey:ApplicationID" json:"application,omitempty"`
|
||||
}
|
||||
|
||||
type EmailTemplate struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
ApplicationID uint `gorm:"index" json:"application_id"`
|
||||
Type string `gorm:"size:50;index" json:"type"`
|
||||
Name string `gorm:"size:100" json:"name"`
|
||||
Subject string `gorm:"size:200" json:"subject"`
|
||||
Content string `gorm:"type:text" json:"content"`
|
||||
IsDefault bool `gorm:"default:false" json:"is_default"`
|
||||
Status string `gorm:"size:20;default:active" json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
Application Application `gorm:"foreignKey:ApplicationID" json:"application,omitempty"`
|
||||
}
|
||||
|
||||
// UserPackage 用户套餐授权模型
|
||||
type UserPackage struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `json:"user_id"`
|
||||
PackageID uint `json:"package_id"`
|
||||
Status string `gorm:"size:20;default:active" json:"status"` // active, expired, cancelled
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
ExpiredAt *time.Time `json:"expired_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
Package Package `gorm:"foreignKey:PackageID" json:"package,omitempty"`
|
||||
}
|
||||
|
||||
// UserDevice 用户设备模型(设备绑定,持久化)
|
||||
type UserDevice struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `gorm:"index" json:"user_id"`
|
||||
ApplicationID uint `gorm:"index" json:"application_id"`
|
||||
DeviceID string `gorm:"size:100;index" json:"device_id"`
|
||||
DeviceName string `gorm:"size:100" json:"device_name"`
|
||||
DeviceType string `gorm:"size:50" json:"device_type"`
|
||||
Status string `gorm:"size:20;default:active" json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
User AppUser `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
Application Application `gorm:"foreignKey:ApplicationID" json:"application,omitempty"`
|
||||
Sessions []DeviceSession `gorm:"foreignKey:DeviceID;references:ID" json:"sessions,omitempty"`
|
||||
}
|
||||
|
||||
// DeviceSession 设备会话模型(多开实例,运行时)
|
||||
type DeviceSession struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
DeviceID uint `gorm:"index" json:"device_id"`
|
||||
UserID uint `gorm:"index" json:"user_id"`
|
||||
ApplicationID uint `gorm:"index" json:"application_id"`
|
||||
InstanceID string `gorm:"size:100;uniqueIndex:idx_device_instance" json:"instance_id"`
|
||||
LastHeartbeat *time.Time `json:"last_heartbeat"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
Device UserDevice `gorm:"foreignKey:DeviceID" json:"device,omitempty"`
|
||||
User AppUser `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
Application Application `gorm:"foreignKey:ApplicationID" json:"application,omitempty"`
|
||||
}
|
||||
|
||||
// UserIP 用户IP绑定模型
|
||||
type UserIP struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `gorm:"index" json:"user_id"`
|
||||
ApplicationID uint `gorm:"index" json:"application_id"`
|
||||
IPAddress string `gorm:"size:50;index" json:"ip_address"`
|
||||
Status string `gorm:"size:20;default:active" json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
User AppUser `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
Application Application `gorm:"foreignKey:ApplicationID" json:"application,omitempty"`
|
||||
}
|
||||
|
||||
// PackHistory 加壳历史记录模型
|
||||
type PackHistory struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
ApplicationID uint `gorm:"index" json:"application_id"`
|
||||
UserID uint `gorm:"index" json:"user_id"`
|
||||
Filename string `gorm:"size:255" json:"filename"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
MD5 string `gorm:"size:32" json:"md5"`
|
||||
DownloadURL string `gorm:"size:500" json:"download_url"`
|
||||
UploadTime time.Time `json:"upload_time"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
Application Application `gorm:"foreignKey:ApplicationID" json:"application,omitempty"`
|
||||
User AppUser `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
}
|
||||
|
||||
// WebhookConfig Webhook配置
|
||||
type WebhookConfig struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
ApplicationID uint `gorm:"index;not null" json:"application_id"`
|
||||
Name string `gorm:"size:100;not null" json:"name"`
|
||||
URL string `gorm:"size:500;not null" json:"url"`
|
||||
SecretKey string `gorm:"size:255" json:"secret_key"`
|
||||
Events string `gorm:"type:text" json:"events"` // JSON数组存储订阅的事件类型
|
||||
Status string `gorm:"size:20;default:active" json:"status"`
|
||||
RetryCount int `gorm:"default:3" json:"retry_count"`
|
||||
Timeout int `gorm:"default:10" json:"timeout"` // 超时时间(秒)
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
Application Application `gorm:"foreignKey:ApplicationID" json:"application,omitempty"`
|
||||
}
|
||||
|
||||
// WebhookLog Webhook发送日志
|
||||
type WebhookLog struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
WebhookID uint `gorm:"index;not null" json:"webhook_id"`
|
||||
Event string `gorm:"size:50;not null" json:"event"`
|
||||
RequestData string `gorm:"type:text" json:"request_data"`
|
||||
ResponseCode int `json:"response_code"`
|
||||
ResponseData string `gorm:"type:text" json:"response_data"`
|
||||
Status string `gorm:"size:20" json:"status"` // success, failed, retrying
|
||||
RetryCount int `json:"retry_count"`
|
||||
ErrorMessage string `gorm:"type:text" json:"error_message"`
|
||||
Duration int `json:"duration"` // 请求耗时(毫秒)
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
WebhookConfig WebhookConfig `gorm:"foreignKey:WebhookID" json:"webhook,omitempty"`
|
||||
}
|
||||
|
||||
// ExtensionAPIKey 扩展API密钥
|
||||
type ExtensionAPIKey struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
ApplicationID uint `gorm:"index;not null" json:"application_id"`
|
||||
Name string `gorm:"size:100;not null" json:"name"`
|
||||
AccessKey string `gorm:"uniqueIndex;size:64;not null" json:"access_key"`
|
||||
SecretKey string `gorm:"size:64;not null" json:"secret_key"`
|
||||
Permissions string `gorm:"type:text" json:"permissions"` // JSON数组存储权限列表
|
||||
Status string `gorm:"size:20;default:active" json:"status"`
|
||||
LastUsedAt *time.Time `json:"last_used_at"`
|
||||
ExpiresAt *time.Time `json:"expires_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
Application Application `gorm:"foreignKey:ApplicationID" json:"application,omitempty"`
|
||||
}
|
||||
|
||||
// ApiUsage API调用统计模型
|
||||
type ApiUsage struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `json:"user_id"`
|
||||
ApplicationID uint `json:"application_id"`
|
||||
Endpoint string `gorm:"size:255" json:"endpoint"`
|
||||
Method string `gorm:"size:10" json:"method"`
|
||||
IPAddress string `gorm:"size:50" json:"ip_address"`
|
||||
UserAgent string `gorm:"size:500" json:"user_agent"`
|
||||
ResponseTime int `json:"response_time"` // 响应时间(毫秒)
|
||||
StatusCode int `json:"status_code"`
|
||||
Success bool `json:"success"`
|
||||
ErrorMessage string `gorm:"type:text" json:"error_message"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
Application Application `gorm:"foreignKey:ApplicationID" json:"application,omitempty"`
|
||||
}
|
||||
|
||||
// StorageUsage 存储使用记录模型
|
||||
type StorageUsage struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `json:"user_id"`
|
||||
ApplicationID *uint `json:"application_id"`
|
||||
ResourceType string `gorm:"size:20" json:"resource_type"` // version, cloud_file
|
||||
ResourceID uint `json:"resource_id"`
|
||||
FileName string `gorm:"size:255" json:"file_name"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
Action string `gorm:"size:20" json:"action"` // upload, delete
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
Application *Application `gorm:"foreignKey:ApplicationID" json:"application,omitempty"`
|
||||
}
|
||||
|
||||
// UsageAlertRecord 用量告警记录模型
|
||||
type UsageAlertRecord struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `json:"user_id"`
|
||||
Type string `gorm:"size:20" json:"type"` // api_calls, storage, package_expiry
|
||||
Level string `gorm:"size:20" json:"level"` // warning, critical
|
||||
Message string `gorm:"size:255" json:"message"`
|
||||
Usage float64 `json:"usage"`
|
||||
Limit float64 `json:"limit"`
|
||||
Percent float64 `json:"percent"`
|
||||
Status string `gorm:"size:20;default:sent" json:"status"` // sent, read
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
}
|
||||
|
||||
// Notification 站内通知模型
|
||||
type Notification struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `json:"user_id"`
|
||||
Title string `gorm:"size:100" json:"title"`
|
||||
Content string `gorm:"type:text" json:"content"`
|
||||
Type string `gorm:"size:20" json:"type"` // usage_alert, system, package
|
||||
IsRead bool `gorm:"default:false" json:"is_read"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/service"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupAccountRoutes(r *gin.RouterGroup) {
|
||||
r.GET("/account", handleAppGetAccount)
|
||||
r.POST("/heartbeat", handleAppHeartbeat)
|
||||
}
|
||||
|
||||
func handleAppGetAccount(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
UserID uint `json:"user_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.First(&user, req.UserID).Error; err != nil {
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"user_id": user.ID,
|
||||
"username": user.Username,
|
||||
"balance": user.Balance,
|
||||
"status": user.Status,
|
||||
})
|
||||
}
|
||||
|
||||
func handleAppHeartbeat(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
UserID uint `json:"user_id"`
|
||||
DeviceID string `json:"device_id"`
|
||||
InstanceID string `json:"instance_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.First(&user, req.UserID).Error; err != nil {
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if blocked, reason := checkRiskControl(c, app.ID, req.DeviceID, user.Username); blocked {
|
||||
log.Printf("[DEBUG] Heartbeat blocked by risk control: %s", reason)
|
||||
response.Error(c, 403, reason)
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
user.LastHeartbeatAt = &now
|
||||
|
||||
if app.BillingType != "free" && app.DeductionMode == "auto" && app.DeductionAmount > 0 {
|
||||
shouldDeduct := false
|
||||
|
||||
switch app.DeductionType {
|
||||
case "timer":
|
||||
if user.LastHeartbeatAt != nil {
|
||||
elapsed := now.Sub(*user.LastHeartbeatAt)
|
||||
var interval time.Duration
|
||||
switch app.DeductionUnit {
|
||||
case "minute":
|
||||
interval = time.Duration(app.DeductionInterval) * time.Minute
|
||||
case "hour":
|
||||
interval = time.Duration(app.DeductionInterval) * time.Hour
|
||||
case "day":
|
||||
interval = time.Duration(app.DeductionInterval) * 24 * time.Hour
|
||||
default:
|
||||
interval = time.Duration(app.DeductionInterval) * time.Minute
|
||||
}
|
||||
if elapsed >= interval {
|
||||
shouldDeduct = true
|
||||
}
|
||||
}
|
||||
case "login":
|
||||
shouldDeduct = true
|
||||
}
|
||||
|
||||
if shouldDeduct {
|
||||
if user.Balance >= app.DeductionAmount {
|
||||
user.Balance -= app.DeductionAmount
|
||||
log.Printf("[DEBUG] Deducted %.2f from user %d, new balance: %.2f", app.DeductionAmount, user.ID, user.Balance)
|
||||
} else {
|
||||
log.Printf("[DEBUG] User %d has insufficient balance: %.2f < %.2f", user.ID, user.Balance, app.DeductionAmount)
|
||||
response.Error(c, 403, "余额不足")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := database.DB.Save(&user).Error; err != nil {
|
||||
response.Error(c, 500, "更新心跳失败")
|
||||
return
|
||||
}
|
||||
|
||||
if req.DeviceID != "" {
|
||||
var device model.UserDevice
|
||||
err := database.DB.Where("user_id = ? AND application_id = ? AND device_id = ?", user.ID, app.ID, req.DeviceID).First(&device).Error
|
||||
|
||||
if err != nil {
|
||||
device = model.UserDevice{
|
||||
UserID: user.ID,
|
||||
ApplicationID: app.ID,
|
||||
DeviceID: req.DeviceID,
|
||||
DeviceName: req.DeviceID,
|
||||
Status: "active",
|
||||
}
|
||||
if err := database.DB.Create(&device).Error; err != nil {
|
||||
log.Printf("[DEBUG] Failed to create device: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
instanceID := req.InstanceID
|
||||
if instanceID == "" {
|
||||
instanceID = req.DeviceID
|
||||
}
|
||||
|
||||
if device.ID > 0 {
|
||||
var session model.DeviceSession
|
||||
sessionErr := database.DB.Where("device_id = ? AND instance_id = ?", device.ID, instanceID).First(&session).Error
|
||||
|
||||
if sessionErr != nil {
|
||||
session = model.DeviceSession{
|
||||
DeviceID: device.ID,
|
||||
UserID: user.ID,
|
||||
ApplicationID: app.ID,
|
||||
InstanceID: instanceID,
|
||||
LastHeartbeat: &now,
|
||||
}
|
||||
if err := database.DB.Create(&session).Error; err != nil {
|
||||
log.Printf("[DEBUG] Failed to create session: %v", err)
|
||||
}
|
||||
} else {
|
||||
session.LastHeartbeat = &now
|
||||
if err := database.DB.Save(&session).Error; err != nil {
|
||||
log.Printf("[DEBUG] Failed to update session: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response.SuccessWithMessage(c, "心跳成功", gin.H{
|
||||
"message": "心跳成功",
|
||||
"balance": user.Balance,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupRoutes(r *gin.RouterGroup) {
|
||||
SetupPaymentRoutes(r)
|
||||
SetupAccountRoutes(r)
|
||||
SetupDynamicRoutes(r)
|
||||
}
|
||||
|
||||
func SetupAuthRoutes(r *gin.RouterGroup) {
|
||||
SetupDeviceRoutes(r)
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/pkg/crypto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func AppCryptoMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
fmt.Printf("[AppCrypto] Middleware called\n")
|
||||
appKey := c.Param("appKey")
|
||||
fmt.Printf("[AppCrypto] AppKey from param: %s\n", appKey)
|
||||
|
||||
if appKey == "" {
|
||||
fmt.Printf("[AppCrypto] AppKey is empty, skipping\n")
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
fmt.Printf("[AppCrypto] Database error: %v\n", err)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("[AppCrypto] App found: %+v\n", app)
|
||||
|
||||
var encryptType crypto.EncryptType
|
||||
switch app.EncryptType {
|
||||
case "aes":
|
||||
encryptType = crypto.EncryptTypeAES
|
||||
case "rc4":
|
||||
encryptType = crypto.EncryptTypeRC4
|
||||
default:
|
||||
encryptType = crypto.EncryptTypeNone
|
||||
}
|
||||
|
||||
shouldEncrypt := encryptType != crypto.EncryptTypeNone
|
||||
|
||||
fmt.Printf("[AppCrypto] AppKey: %s, EncryptType: %s, SecretKey: %s, ShouldEncrypt: %v\n",
|
||||
appKey, app.EncryptType, app.SecretKey, shouldEncrypt)
|
||||
|
||||
c.Set("should_encrypt_response", shouldEncrypt)
|
||||
c.Set("crypto_manager", crypto.NewCryptoManager(encryptType, app.SecretKey))
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func getAppCryptoManager(appKey string) (*crypto.CryptoManager, error) {
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var encryptType crypto.EncryptType
|
||||
switch app.EncryptType {
|
||||
case "aes":
|
||||
encryptType = crypto.EncryptTypeAES
|
||||
case "rc4":
|
||||
encryptType = crypto.EncryptTypeRC4
|
||||
default:
|
||||
encryptType = crypto.EncryptTypeNone
|
||||
}
|
||||
|
||||
return crypto.NewCryptoManager(encryptType, app.SecretKey), nil
|
||||
}
|
||||
|
||||
func decryptRequest(c *gin.Context, appKey string) ([]byte, error) {
|
||||
encryptedData := c.GetHeader("X-Encrypted-Data")
|
||||
if encryptedData == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
cryptoManager, err := getAppCryptoManager(appKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decrypted, err := cryptoManager.Decrypt(encryptedData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []byte(decrypted), nil
|
||||
}
|
||||
|
||||
func encryptResponse(c *gin.Context, appKey string, data []byte) (string, error) {
|
||||
cryptoManager, err := getAppCryptoManager(appKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return cryptoManager.Encrypt(string(data))
|
||||
}
|
||||
@@ -0,0 +1,905 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/service"
|
||||
"verification-platform-backend/pkg/jwt"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func getDeviceType(c *gin.Context, clientDeviceType string) string {
|
||||
validTypes := map[string]bool{
|
||||
"android": true,
|
||||
"ios": true,
|
||||
"windows": true,
|
||||
"mac": true,
|
||||
"linux": true,
|
||||
"web": true,
|
||||
}
|
||||
|
||||
if clientDeviceType != "" && validTypes[clientDeviceType] {
|
||||
return clientDeviceType
|
||||
}
|
||||
|
||||
userAgent := c.GetHeader("User-Agent")
|
||||
return parseDeviceTypeFromUserAgent(userAgent)
|
||||
}
|
||||
|
||||
func parseDeviceTypeFromUserAgent(userAgent string) string {
|
||||
if userAgent == "" {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
ua := strings.ToLower(userAgent)
|
||||
|
||||
switch {
|
||||
case strings.Contains(ua, "android"):
|
||||
return "android"
|
||||
case strings.Contains(ua, "iphone") || strings.Contains(ua, "ipad") || strings.Contains(ua, "ipod"):
|
||||
return "ios"
|
||||
case strings.Contains(ua, "windows"):
|
||||
return "windows"
|
||||
case strings.Contains(ua, "macintosh") || strings.Contains(ua, "mac os x"):
|
||||
return "mac"
|
||||
case strings.Contains(ua, "linux"):
|
||||
return "linux"
|
||||
case strings.Contains(ua, "mozilla") || strings.Contains(ua, "webkit") || strings.Contains(ua, "chrome") || strings.Contains(ua, "safari"):
|
||||
return "web"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func SetupAuthUserRoutes(r *gin.RouterGroup) {
|
||||
r.POST("/register", handleAppRegister)
|
||||
r.POST("/login", handleAppLogin)
|
||||
r.POST("/send-email-code", handleAppSendEmailCode)
|
||||
r.POST("/reset-password", handleAppResetPassword)
|
||||
}
|
||||
|
||||
func handleAppSendEmailCode(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Purpose string `json:"purpose"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "请输入有效的邮箱地址")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Purpose == "" {
|
||||
req.Purpose = "register"
|
||||
}
|
||||
|
||||
if req.Purpose == "register" {
|
||||
if !app.EnableEmailVerify {
|
||||
response.Error(c, 400, "该应用未启用邮箱验证")
|
||||
return
|
||||
}
|
||||
} else if req.Purpose == "reset_password" {
|
||||
if !app.EnablePasswordReset {
|
||||
response.Error(c, 400, "该应用未启用密码重置功能")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var smtpConfig model.AppSMTPConfig
|
||||
if err := database.DB.Where("application_id = ?", app.ID).First(&smtpConfig).Error; err != nil {
|
||||
response.Error(c, 400, "应用未配置SMTP")
|
||||
return
|
||||
}
|
||||
|
||||
var template model.EmailTemplate
|
||||
database.DB.Where("application_id = ? AND type = ? AND status = ?", app.ID, req.Purpose, "active").
|
||||
Order("is_default DESC").First(&template)
|
||||
|
||||
code := generateVerifyCode()
|
||||
expireAt := time.Now().Add(15 * time.Minute)
|
||||
|
||||
verifyCode := model.EmailVerifyCode{
|
||||
ApplicationID: app.ID,
|
||||
Email: req.Email,
|
||||
Code: code,
|
||||
Purpose: req.Purpose,
|
||||
ExpiresAt: expireAt,
|
||||
}
|
||||
database.DB.Create(&verifyCode)
|
||||
|
||||
emailService := service.NewEmailService()
|
||||
config := service.EmailConfig{
|
||||
Host: smtpConfig.Host,
|
||||
Port: smtpConfig.Port,
|
||||
User: smtpConfig.User,
|
||||
Password: smtpConfig.Password,
|
||||
FromName: smtpConfig.FromName,
|
||||
FromEmail: smtpConfig.FromEmail,
|
||||
UseSSL: smtpConfig.UseSSL,
|
||||
}
|
||||
|
||||
subject := fmt.Sprintf("验证码 - %s", app.Name)
|
||||
content := fmt.Sprintf(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="UTF-8"></head>
|
||||
<body style="font-family: Arial, sans-serif; padding: 20px; background-color: #f5f5f5;">
|
||||
<div style="max-width: 600px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
<h2 style="color: #333; margin-bottom: 20px;">邮箱验证</h2>
|
||||
<p style="color: #666; line-height: 1.6;">您的验证码是:<strong style="font-size: 24px; color: #1890ff;">%s</strong></p>
|
||||
<p style="color: #999; font-size: 12px;">验证码有效期为15分钟,请尽快使用。</p>
|
||||
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
||||
<p style="color: #999; font-size: 12px;">此邮件由 %s 系统自动发送,请勿回复。</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`, code, app.Name)
|
||||
|
||||
if template.ID > 0 {
|
||||
subject = template.Subject
|
||||
content = template.Content
|
||||
}
|
||||
|
||||
if err := emailService.SendEmail(config, req.Email, subject, content); err != nil {
|
||||
response.Error(c, 500, fmt.Sprintf("发送失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "验证码已发送",
|
||||
})
|
||||
}
|
||||
|
||||
func generateVerifyCode() string {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
return fmt.Sprintf("%06d", r.Intn(1000000))
|
||||
}
|
||||
|
||||
func handleAppRegister(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
log.Printf("[DEBUG] Starting registration for appKey: %s", appKey)
|
||||
|
||||
app, exists := c.Get("app")
|
||||
if !exists {
|
||||
var appModel model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&appModel).Error; err != nil {
|
||||
log.Printf("[DEBUG] Application not found for appKey: %s, error: %v", appKey, err)
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
app = &appModel
|
||||
}
|
||||
appModel := app.(*model.Application)
|
||||
log.Printf("[DEBUG] Found application: ID=%d, Status=%s", appModel.ID, appModel.Status)
|
||||
|
||||
if service.GetApplicationDisabledStatus(appModel.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
EmailCode string `json:"email_code"`
|
||||
Password string `json:"password"`
|
||||
DeviceID string `json:"device_id"`
|
||||
DeviceName string `json:"device_name"`
|
||||
DeviceType string `json:"device_type"`
|
||||
InstanceID string `json:"instance_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
log.Printf("[DEBUG] Invalid request parameters: %v", err)
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
log.Printf("[DEBUG] Registration request for username: %s", req.Username)
|
||||
|
||||
if blocked, reason := checkRiskControl(c, appModel.ID, req.DeviceID, req.Username); blocked {
|
||||
log.Printf("[DEBUG] Registration blocked by risk control: %s", reason)
|
||||
service.LogVerification(c, &appModel.ID, nil, "register_blocked", "注册被拦截: "+reason, req.DeviceID, fmt.Errorf(reason))
|
||||
response.Error(c, 403, reason)
|
||||
return
|
||||
}
|
||||
|
||||
if !appModel.AllowRegister {
|
||||
log.Printf("[DEBUG] Registration disabled for app: %s", appKey)
|
||||
response.Error(c, 403, "注册功能已关闭")
|
||||
return
|
||||
}
|
||||
|
||||
var allowedMethods []string
|
||||
if appModel.RegisterMethods != "" {
|
||||
if err := json.Unmarshal([]byte(appModel.RegisterMethods), &allowedMethods); err != nil {
|
||||
log.Printf("[DEBUG] Failed to parse register methods: %v", err)
|
||||
allowedMethods = []string{"username"}
|
||||
}
|
||||
} else {
|
||||
allowedMethods = []string{"username"}
|
||||
}
|
||||
|
||||
isEmailRegister := req.Email != "" && req.Username == ""
|
||||
isUsernameRegister := req.Username != "" && req.Email == ""
|
||||
isPhoneRegister := false
|
||||
|
||||
if isEmailRegister && !containsString(allowedMethods, "email") {
|
||||
response.Error(c, 403, "邮箱注册方式未开放")
|
||||
return
|
||||
}
|
||||
if isUsernameRegister && !containsString(allowedMethods, "username") {
|
||||
response.Error(c, 403, "用户名注册方式未开放")
|
||||
return
|
||||
}
|
||||
if isPhoneRegister && !containsString(allowedMethods, "phone") {
|
||||
response.Error(c, 403, "手机号注册方式未开放")
|
||||
return
|
||||
}
|
||||
|
||||
if appModel.EnableEmailVerify && appModel.RequireEmailVerify {
|
||||
if req.Email == "" {
|
||||
response.Error(c, 400, "请输入邮箱地址")
|
||||
return
|
||||
}
|
||||
if req.EmailCode == "" {
|
||||
response.Error(c, 400, "请输入邮箱验证码")
|
||||
return
|
||||
}
|
||||
|
||||
var verifyCode model.EmailVerifyCode
|
||||
if err := database.DB.Where(
|
||||
"application_id = ? AND email = ? AND code = ? AND purpose = ? AND used = ?",
|
||||
appModel.ID, req.Email, req.EmailCode, "register", false,
|
||||
).First(&verifyCode).Error; err != nil {
|
||||
response.Error(c, 400, "验证码错误或已过期")
|
||||
return
|
||||
}
|
||||
|
||||
if verifyCode.ExpiresAt.Before(time.Now()) {
|
||||
response.Error(c, 400, "验证码已过期")
|
||||
return
|
||||
}
|
||||
|
||||
verifyCode.Used = true
|
||||
database.DB.Save(&verifyCode)
|
||||
}
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.Where("username = ? AND application_id = ?", req.Username, appModel.ID).First(&user).Error; err == nil {
|
||||
log.Printf("[DEBUG] User already exists: %s", req.Username)
|
||||
service.LogVerification(c, &appModel.ID, nil, "register_failed", "注册失败: 用户已存在 - "+req.Username, req.DeviceID, fmt.Errorf("用户已存在"))
|
||||
response.Error(c, 400, "用户已存在")
|
||||
return
|
||||
}
|
||||
|
||||
user = model.AppUser{
|
||||
Username: req.Username,
|
||||
Email: req.Email,
|
||||
Password: req.Password,
|
||||
DeviceID: req.DeviceID,
|
||||
Avatar: "",
|
||||
Status: "active",
|
||||
ApplicationID: appModel.ID,
|
||||
IsTrialUser: appModel.EnableTrial,
|
||||
}
|
||||
|
||||
if appModel.EnableTrial {
|
||||
now := time.Now()
|
||||
user.TrialStartAt = &now
|
||||
if appModel.TrialBalance > 0 {
|
||||
user.Balance = appModel.TrialBalance
|
||||
}
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&user).Error; err != nil {
|
||||
log.Printf("[DEBUG] Failed to create user: %v", err)
|
||||
response.Error(c, 500, "注册失败")
|
||||
return
|
||||
}
|
||||
log.Printf("[DEBUG] Successfully created user with ID: %d", user.ID)
|
||||
|
||||
if req.DeviceID != "" {
|
||||
now := time.Now()
|
||||
deviceType := getDeviceType(c, req.DeviceType)
|
||||
|
||||
deviceName := req.DeviceName
|
||||
if deviceName == "" {
|
||||
deviceName = req.DeviceID
|
||||
}
|
||||
|
||||
var device model.UserDevice
|
||||
if err := database.DB.Where("user_id = ? AND application_id = ? AND device_id = ?", user.ID, appModel.ID, req.DeviceID).First(&device).Error; err != nil {
|
||||
device = model.UserDevice{
|
||||
UserID: user.ID,
|
||||
ApplicationID: appModel.ID,
|
||||
DeviceID: req.DeviceID,
|
||||
DeviceName: deviceName,
|
||||
DeviceType: deviceType,
|
||||
Status: "active",
|
||||
}
|
||||
if err := database.DB.Create(&device).Error; err != nil {
|
||||
log.Printf("[DEBUG] Failed to create device: %v", err)
|
||||
} else {
|
||||
log.Printf("[DEBUG] Successfully created device with ID: %d, type: %s", device.ID, deviceType)
|
||||
}
|
||||
}
|
||||
|
||||
instanceID := req.InstanceID
|
||||
if instanceID == "" {
|
||||
instanceID = req.DeviceID
|
||||
}
|
||||
|
||||
if device.ID > 0 {
|
||||
var session model.DeviceSession
|
||||
sessionErr := database.DB.Where("device_id = ? AND instance_id = ?", device.ID, instanceID).First(&session).Error
|
||||
|
||||
if sessionErr != nil {
|
||||
session = model.DeviceSession{
|
||||
DeviceID: device.ID,
|
||||
UserID: user.ID,
|
||||
ApplicationID: appModel.ID,
|
||||
InstanceID: instanceID,
|
||||
LastHeartbeat: &now,
|
||||
}
|
||||
if err := database.DB.Create(&session).Error; err != nil {
|
||||
log.Printf("[DEBUG] Failed to create session: %v", err)
|
||||
}
|
||||
} else {
|
||||
session.LastHeartbeat = &now
|
||||
if err := database.DB.Save(&session).Error; err != nil {
|
||||
log.Printf("[DEBUG] Failed to update session: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
service.LogVerification(c, &appModel.ID, &user.ID, "register", "用户注册: "+req.Username, req.DeviceID, nil)
|
||||
|
||||
response.SuccessWithMessage(c, "注册成功", gin.H{
|
||||
"user_id": user.ID,
|
||||
})
|
||||
}
|
||||
|
||||
func handleAppLogin(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
log.Printf("[DEBUG] Login attempt for appKey: %s", appKey)
|
||||
|
||||
app, exists := c.Get("app")
|
||||
if !exists {
|
||||
var appModel model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&appModel).Error; err != nil {
|
||||
log.Printf("[DEBUG] Application not found for appKey: %s, error: %v", appKey, err)
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
app = &appModel
|
||||
}
|
||||
appModel := app.(*model.Application)
|
||||
log.Printf("[DEBUG] Found application: ID=%d, Status=%s", appModel.ID, appModel.Status)
|
||||
|
||||
var req struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
DeviceID string `json:"device_id"`
|
||||
DeviceName string `json:"device_name"`
|
||||
DeviceType string `json:"device_type"`
|
||||
InstanceID string `json:"instance_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
log.Printf("[DEBUG] Invalid request parameters: %v", err)
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
log.Printf("[DEBUG] Login request for username: %s", req.Username)
|
||||
|
||||
if req.DeviceID == "" {
|
||||
response.Error(c, 400, "设备ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.Where("username = ? AND application_id = ?", req.Username, appModel.ID).First(&user).Error; err != nil {
|
||||
log.Printf("[DEBUG] User not found: %v", err)
|
||||
service.LogVerification(c, &appModel.ID, nil, "login_failed", "登录失败: 用户不存在 - "+req.Username, req.DeviceID, fmt.Errorf("用户不存在"))
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Password != req.Password {
|
||||
log.Printf("[DEBUG] Password mismatch for user: %s", req.Username)
|
||||
service.LogVerification(c, &appModel.ID, &user.ID, "login_failed", "登录失败: 密码错误 - "+req.Username, req.DeviceID, fmt.Errorf("密码错误"))
|
||||
response.Error(c, 400, "密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
if blocked, reason := checkRiskControl(c, appModel.ID, req.DeviceID, req.Username); blocked {
|
||||
log.Printf("[DEBUG] Login blocked by risk control: %s", reason)
|
||||
service.LogVerification(c, &appModel.ID, &user.ID, "login_blocked", "登录被拦截: "+reason, req.DeviceID, fmt.Errorf(reason))
|
||||
response.Error(c, 403, reason)
|
||||
return
|
||||
}
|
||||
|
||||
if appModel.LoginPolicy == "strict" {
|
||||
log.Printf("[DEBUG] LoginPolicy is strict, checking free period and trial")
|
||||
isInFreePeriod := false
|
||||
if appModel.EnableFreePeriod {
|
||||
isInFreePeriod = checkFreePeriod(appModel)
|
||||
}
|
||||
log.Printf("[DEBUG] isInFreePeriod=%v", isInFreePeriod)
|
||||
|
||||
if !isInFreePeriod {
|
||||
log.Printf("[DEBUG] Not in free period, checking trial")
|
||||
isTrialValid := false
|
||||
if user.IsTrialUser && user.TrialEndAt != nil && user.TrialEndAt.After(time.Now()) {
|
||||
isTrialValid = true
|
||||
}
|
||||
log.Printf("[DEBUG] isTrialValid=%v, IsTrialUser=%v, TrialEndAt=%v, Balance=%f", isTrialValid, user.IsTrialUser, user.TrialEndAt, user.Balance)
|
||||
|
||||
if !isTrialValid {
|
||||
if appModel.BillingType != "free" && user.Balance <= 0 {
|
||||
log.Printf("[DEBUG] User %d has no balance remaining", user.ID)
|
||||
service.LogVerification(c, &appModel.ID, &user.ID, "login_failed", "登录失败: 余额不足 - "+req.Username, req.DeviceID, fmt.Errorf("余额不足,请充值后继续使用"))
|
||||
response.Error(c, 403, "余额不足,请充值后继续使用")
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Printf("[DEBUG] User is in free period, allowing login")
|
||||
}
|
||||
}
|
||||
|
||||
if req.DeviceID != "" {
|
||||
heartbeatTimeout := time.Duration(appModel.HeartbeatTimeout) * time.Second
|
||||
timeoutThreshold := time.Now().Add(-heartbeatTimeout)
|
||||
database.DB.Where("user_id = ? AND application_id = ? AND last_heartbeat < ?", user.ID, appModel.ID, timeoutThreshold).Delete(&model.DeviceSession{})
|
||||
|
||||
now := time.Now()
|
||||
deviceType := getDeviceType(c, req.DeviceType)
|
||||
|
||||
deviceName := req.DeviceName
|
||||
if deviceName == "" {
|
||||
deviceName = req.DeviceID
|
||||
}
|
||||
|
||||
clientIP := c.ClientIP()
|
||||
|
||||
if appModel.BindType == "ip" || appModel.BindType == "mixed" {
|
||||
var userIP model.UserIP
|
||||
ipErr := database.DB.Where("user_id = ? AND application_id = ? AND ip_address = ?", user.ID, appModel.ID, clientIP).First(&userIP).Error
|
||||
|
||||
if ipErr != nil {
|
||||
var ipCount int64
|
||||
database.DB.Model(&model.UserIP{}).
|
||||
Where("user_id = ? AND application_id = ?", user.ID, appModel.ID).
|
||||
Count(&ipCount)
|
||||
|
||||
if appModel.MaxDevices > 0 && ipCount >= int64(appModel.MaxDevices) {
|
||||
log.Printf("[DEBUG] IP limit exceeded for user %d: %d/%d", user.ID, ipCount, appModel.MaxDevices)
|
||||
|
||||
var userIPs []model.UserIP
|
||||
database.DB.Where("user_id = ? AND application_id = ?", user.ID, appModel.ID).Order("created_at DESC").Find(&userIPs)
|
||||
|
||||
ipList := make([]gin.H, 0)
|
||||
for _, ip := range userIPs {
|
||||
ipList = append(ipList, gin.H{
|
||||
"id": ip.ID,
|
||||
"ip_address": ip.IPAddress,
|
||||
"status": ip.Status,
|
||||
"created_at": ip.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
response.ErrorWithData(c, 403, "IP绑定数量已达上限,请解绑后再试", gin.H{
|
||||
"error_code": "IP_LIMIT_EXCEEDED",
|
||||
"max_ips": appModel.MaxDevices,
|
||||
"ip_count": ipCount,
|
||||
"ips": ipList,
|
||||
})
|
||||
service.LogVerification(c, &appModel.ID, &user.ID, "login_failed", "登录失败: IP绑定数量已达上限 - "+req.Username, req.DeviceID, fmt.Errorf("IP绑定数量已达上限"))
|
||||
return
|
||||
}
|
||||
|
||||
userIP = model.UserIP{
|
||||
UserID: user.ID,
|
||||
ApplicationID: appModel.ID,
|
||||
IPAddress: clientIP,
|
||||
Status: "active",
|
||||
}
|
||||
if err := database.DB.Create(&userIP).Error; err != nil {
|
||||
log.Printf("[DEBUG] Failed to create user IP: %v", err)
|
||||
} else {
|
||||
log.Printf("[DEBUG] Successfully created user IP with ID: %d, IP: %s", userIP.ID, clientIP)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var device model.UserDevice
|
||||
deviceErr := database.DB.Where("user_id = ? AND application_id = ? AND device_id = ?", user.ID, appModel.ID, req.DeviceID).First(&device).Error
|
||||
|
||||
if deviceErr != nil {
|
||||
if (appModel.BindType == "device" || appModel.BindType == "mixed") && appModel.MaxDevices > 0 {
|
||||
var deviceCount int64
|
||||
database.DB.Model(&model.UserDevice{}).
|
||||
Where("user_id = ? AND application_id = ?", user.ID, appModel.ID).
|
||||
Count(&deviceCount)
|
||||
|
||||
if deviceCount >= int64(appModel.MaxDevices) {
|
||||
log.Printf("[DEBUG] Device limit exceeded for user %d: %d/%d", user.ID, deviceCount, appModel.MaxDevices)
|
||||
|
||||
var devices []model.UserDevice
|
||||
database.DB.Where("user_id = ? AND application_id = ?", user.ID, appModel.ID).Order("created_at DESC").Find(&devices)
|
||||
|
||||
deviceList := make([]gin.H, 0)
|
||||
for _, d := range devices {
|
||||
var onlineSessions []model.DeviceSession
|
||||
database.DB.Where("device_id = ? AND last_heartbeat > ?", d.ID, timeoutThreshold).Find(&onlineSessions)
|
||||
|
||||
deviceList = append(deviceList, gin.H{
|
||||
"id": d.ID,
|
||||
"device_id": d.DeviceID,
|
||||
"device_name": d.DeviceName,
|
||||
"device_type": d.DeviceType,
|
||||
"online_count": len(onlineSessions),
|
||||
"created_at": d.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
response.ErrorWithData(c, 403, "设备绑定数量已达上限,请解绑后再试", gin.H{
|
||||
"error_code": "DEVICE_LIMIT_EXCEEDED",
|
||||
"max_devices": appModel.MaxDevices,
|
||||
"device_count": deviceCount,
|
||||
"devices": deviceList,
|
||||
})
|
||||
service.LogVerification(c, &appModel.ID, &user.ID, "login_failed", "登录失败: 设备绑定数量已达上限 - "+req.Username, req.DeviceID, fmt.Errorf("设备绑定数量已达上限"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
device = model.UserDevice{
|
||||
UserID: user.ID,
|
||||
ApplicationID: appModel.ID,
|
||||
DeviceID: req.DeviceID,
|
||||
DeviceName: deviceName,
|
||||
DeviceType: deviceType,
|
||||
Status: "active",
|
||||
}
|
||||
if err := database.DB.Create(&device).Error; err != nil {
|
||||
log.Printf("[DEBUG] Failed to create device: %v", err)
|
||||
} else {
|
||||
log.Printf("[DEBUG] Successfully created device with ID: %d, type: %s", device.ID, deviceType)
|
||||
}
|
||||
}
|
||||
|
||||
instanceID := req.InstanceID
|
||||
if instanceID == "" {
|
||||
instanceID = req.DeviceID
|
||||
}
|
||||
|
||||
if device.ID > 0 {
|
||||
if appModel.MultiOpen && appModel.MaxInstances > 0 {
|
||||
var sessionCount int64
|
||||
database.DB.Model(&model.DeviceSession{}).
|
||||
Where("device_id = ? AND instance_id != ? AND last_heartbeat > ?", device.ID, instanceID, timeoutThreshold).
|
||||
Count(&sessionCount)
|
||||
|
||||
if sessionCount >= int64(appModel.MaxInstances) {
|
||||
if appModel.MultiOpenMode == "forbidden" {
|
||||
log.Printf("[DEBUG] Multi-instance limit exceeded for user %d device %s: %d/%d", user.ID, req.DeviceID, sessionCount, appModel.MaxInstances)
|
||||
|
||||
var sessions []model.DeviceSession
|
||||
database.DB.Where("device_id = ? AND instance_id != ?", device.ID, instanceID).Order("last_heartbeat DESC").Find(&sessions)
|
||||
|
||||
sessionList := make([]gin.H, 0)
|
||||
for _, s := range sessions {
|
||||
isOnline := s.LastHeartbeat != nil && s.LastHeartbeat.After(timeoutThreshold)
|
||||
sessionList = append(sessionList, gin.H{
|
||||
"id": s.ID,
|
||||
"instance_id": s.InstanceID,
|
||||
"is_online": isOnline,
|
||||
"last_heartbeat": s.LastHeartbeat,
|
||||
"created_at": s.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
response.ErrorWithData(c, 403, "多开数量已达上限", gin.H{
|
||||
"error_code": "MULTI_INSTANCE_LIMIT_EXCEEDED",
|
||||
"max_instances": appModel.MaxInstances,
|
||||
"instance_count": sessionCount,
|
||||
"instances": sessionList,
|
||||
})
|
||||
service.LogVerification(c, &appModel.ID, &user.ID, "login_failed", "登录失败: 多开数量已达上限 - "+req.Username, req.DeviceID, fmt.Errorf("多开数量已达上限"))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var session model.DeviceSession
|
||||
if err := database.DB.Where("device_id = ? AND instance_id = ?", device.ID, instanceID).First(&session).Error; err != nil {
|
||||
session = model.DeviceSession{
|
||||
DeviceID: device.ID,
|
||||
UserID: user.ID,
|
||||
ApplicationID: appModel.ID,
|
||||
InstanceID: instanceID,
|
||||
LastHeartbeat: &now,
|
||||
}
|
||||
if err := database.DB.Create(&session).Error; err != nil {
|
||||
log.Printf("[DEBUG] Failed to create session: %v", err)
|
||||
} else {
|
||||
log.Printf("[DEBUG] Successfully created session with ID: %d, instance: %s", session.ID, instanceID)
|
||||
}
|
||||
} else {
|
||||
session.LastHeartbeat = &now
|
||||
if err := database.DB.Save(&session).Error; err != nil {
|
||||
log.Printf("[DEBUG] Failed to update session: %v", err)
|
||||
} else {
|
||||
log.Printf("[DEBUG] Successfully updated session %d last_heartbeat", session.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
token, err := jwt.GenerateToken(user.ID, user.Username, "app_user")
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] Failed to generate token: %v", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
database.DB.Model(&user).Updates(map[string]interface{}{
|
||||
"last_login_at": now,
|
||||
"last_heartbeat_at": now,
|
||||
})
|
||||
|
||||
service.LogVerification(c, &appModel.ID, &user.ID, "login", "用户登录: "+req.Username, req.DeviceID, nil)
|
||||
|
||||
response.SuccessWithMessage(c, "登录成功", gin.H{
|
||||
"user_id": user.ID,
|
||||
"token": token,
|
||||
})
|
||||
}
|
||||
|
||||
func checkFreePeriod(app *model.Application) bool {
|
||||
now := time.Now()
|
||||
log.Printf("[DEBUG] checkFreePeriod: EnableFreePeriod=%v, FreePeriodType=%s, FreePeriodStart=%s, FreePeriodEnd=%s, FreePeriodWeekdays=%s, FreePeriodStartTime=%s, FreePeriodEndTime=%s",
|
||||
app.EnableFreePeriod, app.FreePeriodType, app.FreePeriodStart, app.FreePeriodEnd, app.FreePeriodWeekdays, app.FreePeriodStartTime, app.FreePeriodEndTime)
|
||||
|
||||
localLocation := time.Local
|
||||
|
||||
if app.FreePeriodType == "range" {
|
||||
if app.FreePeriodStart == "" || app.FreePeriodEnd == "" {
|
||||
log.Printf("[DEBUG] checkFreePeriod: range type but missing start or end")
|
||||
return false
|
||||
}
|
||||
var startDate, endDate time.Time
|
||||
var err error
|
||||
|
||||
if len(app.FreePeriodStart) > 10 {
|
||||
startDate, err = time.ParseInLocation("2006-01-02T15:04", app.FreePeriodStart, localLocation)
|
||||
} else {
|
||||
startDate, err = time.ParseInLocation("2006-01-02", app.FreePeriodStart, localLocation)
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] checkFreePeriod: failed to parse start date: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
if len(app.FreePeriodEnd) > 10 {
|
||||
endDate, err = time.ParseInLocation("2006-01-02T15:04", app.FreePeriodEnd, localLocation)
|
||||
} else {
|
||||
endDate, err = time.ParseInLocation("2006-01-02", app.FreePeriodEnd, localLocation)
|
||||
if err == nil {
|
||||
endDate = endDate.Add(24 * time.Hour)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] checkFreePeriod: failed to parse end date: %v", err)
|
||||
return false
|
||||
}
|
||||
result := now.After(startDate) && now.Before(endDate)
|
||||
log.Printf("[DEBUG] checkFreePeriod: range check result=%v, now=%v, start=%v, end=%v", result, now, startDate, endDate)
|
||||
return result
|
||||
} else if app.FreePeriodType == "weekdays" {
|
||||
if app.FreePeriodWeekdays == "" {
|
||||
log.Printf("[DEBUG] checkFreePeriod: weekdays type but missing weekdays")
|
||||
return false
|
||||
}
|
||||
weekday := int(now.Weekday())
|
||||
if weekday == 0 {
|
||||
weekday = 7
|
||||
}
|
||||
weekdayStr := string(rune('0' + weekday))
|
||||
if !contains(app.FreePeriodWeekdays, weekdayStr) {
|
||||
log.Printf("[DEBUG] checkFreePeriod: weekday %s not in %s", weekdayStr, app.FreePeriodWeekdays)
|
||||
return false
|
||||
}
|
||||
if app.FreePeriodStartTime != "" && app.FreePeriodEndTime != "" {
|
||||
currentTime := now.Format("15:04")
|
||||
result := currentTime >= app.FreePeriodStartTime && currentTime <= app.FreePeriodEndTime
|
||||
log.Printf("[DEBUG] checkFreePeriod: weekday time check result=%v, currentTime=%s, startTime=%s, endTime=%s", result, currentTime, app.FreePeriodStartTime, app.FreePeriodEndTime)
|
||||
return result
|
||||
}
|
||||
log.Printf("[DEBUG] checkFreePeriod: weekday check passed")
|
||||
return true
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] checkFreePeriod: unknown type %s", app.FreePeriodType)
|
||||
return false
|
||||
}
|
||||
|
||||
func checkRiskControl(c *gin.Context, appID uint, deviceID string, username string) (bool, string) {
|
||||
clientIP := c.ClientIP()
|
||||
|
||||
var globalRules []model.RiskControlRule
|
||||
database.DB.Where("application_id IS NULL AND status = ?", "active").Find(&globalRules)
|
||||
|
||||
var appRules []model.RiskControlRule
|
||||
database.DB.Where("application_id = ? AND status = ?", appID, "active").Find(&appRules)
|
||||
|
||||
now := time.Now()
|
||||
|
||||
for _, rule := range globalRules {
|
||||
if rule.ExpiresAt != nil && rule.ExpiresAt.Before(now) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch rule.Type {
|
||||
case "device":
|
||||
if deviceID != "" && rule.Value == deviceID {
|
||||
return true, "设备已被全局封禁: " + rule.Reason
|
||||
}
|
||||
case "ip":
|
||||
if rule.Value == clientIP {
|
||||
return true, "IP已被全局封禁: " + rule.Reason
|
||||
}
|
||||
case "region":
|
||||
}
|
||||
}
|
||||
|
||||
for _, rule := range appRules {
|
||||
if rule.ExpiresAt != nil && rule.ExpiresAt.Before(now) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch rule.Type {
|
||||
case "device":
|
||||
if deviceID != "" && rule.Value == deviceID {
|
||||
return true, "设备已被封禁: " + rule.Reason
|
||||
}
|
||||
case "ip":
|
||||
if rule.Value == clientIP {
|
||||
return true, "IP已被封禁: " + rule.Reason
|
||||
}
|
||||
case "user":
|
||||
if username != "" && rule.Value == username {
|
||||
return true, "账号已被封禁: " + rule.Reason
|
||||
}
|
||||
case "region":
|
||||
}
|
||||
}
|
||||
|
||||
return false, ""
|
||||
}
|
||||
|
||||
func contains(s string, substr string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i:i+1] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func containsString(slice []string, str string) bool {
|
||||
for _, v := range slice {
|
||||
if v == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func checkAppEmailPermission(userID uint, permission *model.PackagePermission) bool {
|
||||
var userPackage model.UserPackage
|
||||
if err := database.DB.Where("user_id = ? AND status = ?", userID, "active").
|
||||
Preload("Package").First(&userPackage).Error; err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if userPackage.ExpiredAt != nil && userPackage.ExpiredAt.Before(time.Now()) {
|
||||
return false
|
||||
}
|
||||
|
||||
if err := database.DB.Where("package_id = ?", userPackage.PackageID).First(permission).Error; err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func handleAppResetPassword(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
if !app.EnablePasswordReset {
|
||||
response.Error(c, 400, "该应用未启用密码重置功能")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Code string `json:"code" binding:"required"`
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var verifyCode model.EmailVerifyCode
|
||||
if err := database.DB.Where(
|
||||
"application_id = ? AND email = ? AND code = ? AND purpose = ? AND used = ?",
|
||||
app.ID, req.Email, req.Code, "reset_password", false,
|
||||
).First(&verifyCode).Error; err != nil {
|
||||
response.Error(c, 400, "验证码无效或已过期")
|
||||
return
|
||||
}
|
||||
|
||||
if verifyCode.ExpiresAt.Before(time.Now()) {
|
||||
response.Error(c, 400, "验证码已过期")
|
||||
return
|
||||
}
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.Where("email = ? AND application_id = ?", req.Email, app.ID).First(&user).Error; err != nil {
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
response.Error(c, 500, "密码加密失败")
|
||||
return
|
||||
}
|
||||
|
||||
user.Password = string(hashedPassword)
|
||||
if err := database.DB.Save(&user).Error; err != nil {
|
||||
response.Error(c, 500, "密码重置失败")
|
||||
return
|
||||
}
|
||||
|
||||
verifyCode.Used = true
|
||||
database.DB.Save(&verifyCode)
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "密码重置成功",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,918 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/middleware"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/service"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupCloudRoutes(r *gin.RouterGroup) {
|
||||
r.GET("/constants", handleAppGetConstants)
|
||||
r.GET("/constants/:key", handleAppGetConstantByKey)
|
||||
r.GET("/constants/:key/download", handleAppDownloadConstant)
|
||||
r.GET("/variables", handleAppGetVariables)
|
||||
r.GET("/variables/:key", handleAppGetVariableByKey)
|
||||
r.GET("/variables/:key/download", handleAppDownloadVariable)
|
||||
r.POST("/variables/:key/upload", handleAppUploadVariableBinary)
|
||||
r.POST("/variables", handleAppUpdateVariables)
|
||||
r.POST("/variables/:key/records", handleAppCreateVariableRecord)
|
||||
r.GET("/variables/:key/records", handleAppGetVariableRecords)
|
||||
r.DELETE("/variables/:key/records/:record_id", handleAppDeleteVariableRecord)
|
||||
r.POST("/call-function", handleAppCallFunction)
|
||||
}
|
||||
|
||||
func handleAppGetConstants(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var appUser model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", userID, app.ID).First(&appUser).Error; err != nil {
|
||||
response.Error(c, 403, "无权访问该应用的云端常量")
|
||||
return
|
||||
}
|
||||
|
||||
var constants []model.CloudConstant
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND status = ?", app.UserID, app.ID, "active").Find(&constants).Error; err != nil {
|
||||
response.Error(c, 500, "获取云端常量失败")
|
||||
return
|
||||
}
|
||||
|
||||
result := make(map[string]interface{})
|
||||
for _, constant := range constants {
|
||||
if constant.VarType == "binary" {
|
||||
result[constant.Key] = gin.H{
|
||||
"type": "binary",
|
||||
"value": "/api/v1/app/" + appKey + "/constants/" + constant.Key + "/download",
|
||||
"file_name": constant.OriginalName,
|
||||
"file_size": constant.FileSize,
|
||||
"md5": constant.FileMD5,
|
||||
"mime_type": constant.MimeType,
|
||||
}
|
||||
} else {
|
||||
result[constant.Key] = gin.H{
|
||||
"type": constant.VarType,
|
||||
"value": constant.Value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, result)
|
||||
}
|
||||
|
||||
func handleAppGetConstantByKey(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
key := c.Param("key")
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var appUser model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", userID, app.ID).First(&appUser).Error; err != nil {
|
||||
response.Error(c, 403, "无权访问该应用的云端常量")
|
||||
return
|
||||
}
|
||||
|
||||
var constant model.CloudConstant
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND key = ? AND status = ?", app.UserID, app.ID, key, "active").First(&constant).Error; err != nil {
|
||||
response.Error(c, 404, "云端常量不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if constant.VarType == "binary" {
|
||||
response.Success(c, gin.H{
|
||||
"key": constant.Key,
|
||||
"type": "binary",
|
||||
"value": "/api/v1/app/" + appKey + "/constants/" + constant.Key + "/download",
|
||||
"file_name": constant.OriginalName,
|
||||
"file_size": constant.FileSize,
|
||||
"md5": constant.FileMD5,
|
||||
"mime_type": constant.MimeType,
|
||||
})
|
||||
} else {
|
||||
response.Success(c, gin.H{
|
||||
"key": constant.Key,
|
||||
"type": constant.VarType,
|
||||
"value": constant.Value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func handleAppDownloadConstant(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
key := c.Param("key")
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var appUser model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", userID, app.ID).First(&appUser).Error; err != nil {
|
||||
response.Error(c, 403, "无权访问该应用的云端常量")
|
||||
return
|
||||
}
|
||||
|
||||
var constant model.CloudConstant
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND key = ? AND status = ?", app.UserID, app.ID, key, "active").First(&constant).Error; err != nil {
|
||||
response.Error(c, 404, "云端常量不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if constant.VarType != "binary" || constant.FilePath == "" {
|
||||
response.Error(c, 400, "该常量不是文件类型")
|
||||
return
|
||||
}
|
||||
|
||||
filePath := constant.FilePath
|
||||
if strings.HasPrefix(filePath, "/") {
|
||||
filePath = filePath[1:]
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
response.Error(c, 404, "文件不存在")
|
||||
return
|
||||
}
|
||||
|
||||
encodedFilename := url.QueryEscape(constant.OriginalName)
|
||||
c.Header("Content-Description", "File Transfer")
|
||||
c.Header("Content-Type", "application/octet-stream")
|
||||
c.Header("Content-Disposition", "attachment; filename*=UTF-8''"+encodedFilename)
|
||||
c.Header("Content-Transfer-Encoding", "binary")
|
||||
c.Header("Expires", "0")
|
||||
c.Header("Cache-Control", "must-revalidate")
|
||||
c.Header("Pragma", "public")
|
||||
c.FileAttachment(filePath, constant.OriginalName)
|
||||
}
|
||||
|
||||
func handleAppGetVariables(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var appUser model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", userID, app.ID).First(&appUser).Error; err != nil {
|
||||
response.Error(c, 403, "无权访问该应用的云端变量")
|
||||
return
|
||||
}
|
||||
|
||||
var variables []model.CloudVariable
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND status = ?", app.UserID, app.ID, "active").Find(&variables).Error; err != nil {
|
||||
response.Error(c, 500, "获取云端变量失败")
|
||||
return
|
||||
}
|
||||
|
||||
var userVariables []model.UserVariable
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ?", userID, app.ID).Find(&userVariables).Error; err != nil {
|
||||
response.Error(c, 500, "获取用户变量失败")
|
||||
return
|
||||
}
|
||||
|
||||
userVarMap := make(map[string]model.UserVariable)
|
||||
for _, uv := range userVariables {
|
||||
userVarMap[uv.VarName] = uv
|
||||
}
|
||||
|
||||
result := make(map[string]interface{})
|
||||
for _, v := range variables {
|
||||
if v.VarType == "binary" {
|
||||
if v.Scope == "user" {
|
||||
if uv, ok := userVarMap[v.Key]; ok && uv.FilePath != "" {
|
||||
result[v.Key] = gin.H{
|
||||
"type": "binary",
|
||||
"value": "/api/v1/app/" + appKey + "/variables/" + v.Key + "/download",
|
||||
"file_name": uv.OriginalName,
|
||||
"file_size": uv.FileSize,
|
||||
"md5": uv.FileMD5,
|
||||
"mime_type": uv.MimeType,
|
||||
}
|
||||
} else {
|
||||
result[v.Key] = gin.H{
|
||||
"type": "binary",
|
||||
"value": "/api/v1/app/" + appKey + "/variables/" + v.Key + "/download",
|
||||
"file_name": v.OriginalName,
|
||||
"file_size": v.FileSize,
|
||||
"md5": v.FileMD5,
|
||||
"mime_type": v.MimeType,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result[v.Key] = gin.H{
|
||||
"type": "binary",
|
||||
"value": "/api/v1/app/" + appKey + "/variables/" + v.Key + "/download",
|
||||
"file_name": v.OriginalName,
|
||||
"file_size": v.FileSize,
|
||||
"md5": v.FileMD5,
|
||||
"mime_type": v.MimeType,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if v.Scope == "app" {
|
||||
result[v.Key] = gin.H{
|
||||
"type": v.VarType,
|
||||
"value": v.DefaultValue,
|
||||
}
|
||||
} else {
|
||||
value := v.DefaultValue
|
||||
if uv, ok := userVarMap[v.Key]; ok {
|
||||
value = uv.VarValue
|
||||
}
|
||||
result[v.Key] = gin.H{
|
||||
"type": v.VarType,
|
||||
"value": value,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, result)
|
||||
}
|
||||
|
||||
func handleAppGetVariableByKey(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
key := c.Param("key")
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var appUser model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", userID, app.ID).First(&appUser).Error; err != nil {
|
||||
response.Error(c, 403, "无权访问该应用的云端变量")
|
||||
return
|
||||
}
|
||||
|
||||
var variable model.CloudVariable
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND key = ? AND status = ?", app.UserID, app.ID, key, "active").First(&variable).Error; err != nil {
|
||||
response.Error(c, 404, "云端变量不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if variable.VarType == "binary" {
|
||||
if variable.Scope == "user" {
|
||||
var userVar model.UserVariable
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND var_name = ?", userID, app.ID, key).First(&userVar).Error; err == nil && userVar.FilePath != "" {
|
||||
response.Success(c, gin.H{
|
||||
"key": variable.Key,
|
||||
"type": "binary",
|
||||
"value": "/api/v1/app/" + appKey + "/variables/" + variable.Key + "/download",
|
||||
"file_name": userVar.OriginalName,
|
||||
"file_size": userVar.FileSize,
|
||||
"md5": userVar.FileMD5,
|
||||
"mime_type": userVar.MimeType,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
response.Success(c, gin.H{
|
||||
"key": variable.Key,
|
||||
"type": "binary",
|
||||
"value": "/api/v1/app/" + appKey + "/variables/" + variable.Key + "/download",
|
||||
"file_name": variable.OriginalName,
|
||||
"file_size": variable.FileSize,
|
||||
"md5": variable.FileMD5,
|
||||
"mime_type": variable.MimeType,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if variable.Scope == "app" {
|
||||
response.Success(c, gin.H{
|
||||
"key": variable.Key,
|
||||
"type": variable.VarType,
|
||||
"value": variable.DefaultValue,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var userVar model.UserVariable
|
||||
value := variable.DefaultValue
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND var_name = ?", userID, app.ID, key).First(&userVar).Error; err == nil {
|
||||
value = userVar.VarValue
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"key": variable.Key,
|
||||
"type": variable.VarType,
|
||||
"value": value,
|
||||
})
|
||||
}
|
||||
|
||||
func handleAppDownloadVariable(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
key := c.Param("key")
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var appUser model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", userID, app.ID).First(&appUser).Error; err != nil {
|
||||
response.Error(c, 403, "无权访问该应用的云端变量")
|
||||
return
|
||||
}
|
||||
|
||||
var variable model.CloudVariable
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND key = ? AND status = ?", app.UserID, app.ID, key, "active").First(&variable).Error; err != nil {
|
||||
response.Error(c, 404, "云端变量不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if variable.VarType != "binary" {
|
||||
response.Error(c, 400, "该变量不是二进制类型")
|
||||
return
|
||||
}
|
||||
|
||||
var filePath string
|
||||
var originalName string
|
||||
|
||||
if variable.Scope == "user" {
|
||||
var userVar model.UserVariable
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND var_name = ?", userID, app.ID, key).First(&userVar).Error; err == nil && userVar.FilePath != "" {
|
||||
filePath = userVar.FilePath
|
||||
originalName = userVar.OriginalName
|
||||
} else {
|
||||
filePath = variable.FilePath
|
||||
originalName = variable.OriginalName
|
||||
}
|
||||
} else {
|
||||
filePath = variable.FilePath
|
||||
originalName = variable.OriginalName
|
||||
}
|
||||
|
||||
if filePath == "" {
|
||||
response.Error(c, 400, "该变量没有关联文件")
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(filePath, "/") {
|
||||
filePath = filePath[1:]
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
response.Error(c, 404, "文件不存在")
|
||||
return
|
||||
}
|
||||
|
||||
encodedFilename := url.QueryEscape(originalName)
|
||||
c.Header("Content-Description", "File Transfer")
|
||||
c.Header("Content-Type", "application/octet-stream")
|
||||
c.Header("Content-Disposition", "attachment; filename*=UTF-8''"+encodedFilename)
|
||||
c.Header("Content-Transfer-Encoding", "binary")
|
||||
c.Header("Expires", "0")
|
||||
c.Header("Cache-Control", "must-revalidate")
|
||||
c.Header("Pragma", "public")
|
||||
c.FileAttachment(filePath, originalName)
|
||||
}
|
||||
|
||||
func handleAppUpdateVariables(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var appUser model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", userID, app.ID).First(&appUser).Error; err != nil {
|
||||
response.Error(c, 403, "无权访问该应用的云端变量")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Variables map[string]string `json:"variables"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var variables []model.CloudVariable
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ?", app.UserID, app.ID).Find(&variables).Error; err != nil {
|
||||
response.Error(c, 500, "获取云端变量失败")
|
||||
return
|
||||
}
|
||||
|
||||
varMap := make(map[string]model.CloudVariable)
|
||||
for _, v := range variables {
|
||||
varMap[v.Key] = v
|
||||
}
|
||||
|
||||
for key, value := range req.Variables {
|
||||
variable, exists := varMap[key]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
if variable.Scope == "app" {
|
||||
variable.DefaultValue = value
|
||||
database.DB.Save(&variable)
|
||||
} else {
|
||||
var userVar model.UserVariable
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND var_name = ?", userID, app.ID, key).First(&userVar).Error; err != nil {
|
||||
userVar = model.UserVariable{
|
||||
UserID: userID.(uint),
|
||||
AppID: app.ID,
|
||||
VarName: key,
|
||||
VarValue: value,
|
||||
VarType: variable.VarType,
|
||||
}
|
||||
database.DB.Create(&userVar)
|
||||
} else {
|
||||
userVar.VarValue = value
|
||||
database.DB.Save(&userVar)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "更新成功",
|
||||
})
|
||||
}
|
||||
|
||||
func handleAppUploadVariableBinary(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
key := c.Param("key")
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var appUser model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", userID, app.ID).First(&appUser).Error; err != nil {
|
||||
response.Error(c, 403, "无权访问该应用的云端变量")
|
||||
return
|
||||
}
|
||||
|
||||
var variable model.CloudVariable
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND key = ? AND status = ?", app.UserID, app.ID, key, "active").First(&variable).Error; err != nil {
|
||||
response.Error(c, 404, "云端变量不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if variable.VarType != "binary" {
|
||||
response.Error(c, 400, "该变量不是二进制类型")
|
||||
return
|
||||
}
|
||||
|
||||
if variable.WritePermission != "user" {
|
||||
response.Error(c, 403, "该变量不允许用户写入")
|
||||
return
|
||||
}
|
||||
|
||||
file, header, err := c.Request.FormFile("file")
|
||||
if err != nil {
|
||||
response.Error(c, 400, "请选择要上传的文件")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var developer model.User
|
||||
if err := database.DB.First(&developer, app.UserID).Error; err != nil {
|
||||
response.Error(c, 500, "获取开发者信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
if developer.CurrentPackageID != nil {
|
||||
var permission model.PackagePermission
|
||||
if err := database.DB.Where("package_id = ?", developer.CurrentPackageID).First(&permission).Error; err == nil {
|
||||
maxStorageBytes := int64(permission.MaxStorage) * 1024 * 1024
|
||||
if developer.StorageUsed+header.Size > maxStorageBytes {
|
||||
usedMB := float64(developer.StorageUsed) / 1024 / 1024
|
||||
maxMB := float64(permission.MaxStorage)
|
||||
response.Error(c, 403, fmt.Sprintf("存储空间不足,已使用 %.2f MB / %.2f MB", usedMB, maxMB))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uploadDir := "uploads/cloud-files"
|
||||
if err := os.MkdirAll(uploadDir, 0755); err != nil {
|
||||
response.Error(c, 500, "创建上传目录失败")
|
||||
return
|
||||
}
|
||||
|
||||
ext := filepath.Ext(header.Filename)
|
||||
filename := fmt.Sprintf("%d_%d_%d%s", app.UserID, userID, time.Now().UnixNano(), ext)
|
||||
filePath := filepath.Join(uploadDir, filename)
|
||||
|
||||
dst, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
response.Error(c, 500, "创建文件失败")
|
||||
return
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
hash := md5.New()
|
||||
multiWriter := io.MultiWriter(dst, hash)
|
||||
if _, err := io.Copy(multiWriter, file); err != nil {
|
||||
response.Error(c, 500, "保存文件失败")
|
||||
return
|
||||
}
|
||||
fileMD5 := hex.EncodeToString(hash.Sum(nil))
|
||||
|
||||
fileURL := "/uploads/cloud-files/" + filename
|
||||
|
||||
mimeType := header.Header.Get("Content-Type")
|
||||
if mimeType == "" {
|
||||
mimeType = "application/octet-stream"
|
||||
}
|
||||
|
||||
if variable.Scope == "app" {
|
||||
if variable.FilePath != "" && variable.FileSize > 0 {
|
||||
oldFilePath := variable.FilePath
|
||||
if strings.HasPrefix(oldFilePath, "/") {
|
||||
oldFilePath = oldFilePath[1:]
|
||||
}
|
||||
os.Remove(oldFilePath)
|
||||
if err := middleware.UpdateStorageUsed(app.UserID, variable.FileSize, "delete"); err != nil {
|
||||
fmt.Printf("更新存储使用量失败: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
variable.DefaultValue = fileURL
|
||||
variable.FilePath = fileURL
|
||||
variable.FileSize = header.Size
|
||||
variable.MimeType = mimeType
|
||||
variable.OriginalName = header.Filename
|
||||
variable.FileMD5 = fileMD5
|
||||
database.DB.Save(&variable)
|
||||
} else {
|
||||
var userVar model.UserVariable
|
||||
existingFile := false
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND var_name = ?", userID, app.ID, key).First(&userVar).Error; err == nil {
|
||||
if userVar.FilePath != "" && userVar.FileSize > 0 {
|
||||
existingFile = true
|
||||
}
|
||||
}
|
||||
|
||||
userVar = model.UserVariable{
|
||||
UserID: userID.(uint),
|
||||
AppID: app.ID,
|
||||
VarName: key,
|
||||
VarValue: fileURL,
|
||||
VarType: "binary",
|
||||
FilePath: fileURL,
|
||||
FileSize: header.Size,
|
||||
MimeType: mimeType,
|
||||
OriginalName: header.Filename,
|
||||
FileMD5: fileMD5,
|
||||
}
|
||||
|
||||
if existingFile {
|
||||
var oldVar model.UserVariable
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND var_name = ?", userID, app.ID, key).First(&oldVar).Error; err == nil {
|
||||
if oldVar.FilePath != "" && oldVar.FileSize > 0 {
|
||||
oldFilePath := oldVar.FilePath
|
||||
if strings.HasPrefix(oldFilePath, "/") {
|
||||
oldFilePath = oldFilePath[1:]
|
||||
}
|
||||
os.Remove(oldFilePath)
|
||||
if err := middleware.UpdateStorageUsed(app.UserID, oldVar.FileSize, "delete"); err != nil {
|
||||
fmt.Printf("更新存储使用量失败: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
database.DB.Where("user_id = ? AND app_id = ? AND var_name = ?", userID, app.ID, key).Assign(userVar).FirstOrCreate(&userVar)
|
||||
}
|
||||
|
||||
if err := middleware.UpdateStorageUsed(app.UserID, header.Size, "upload"); err != nil {
|
||||
fmt.Printf("更新存储使用量失败: %v\n", err)
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "上传成功",
|
||||
"file_url": fileURL,
|
||||
"file_size": header.Size,
|
||||
"mime_type": mimeType,
|
||||
"original_name": header.Filename,
|
||||
"download_url": "/api/v1/app/" + appKey + "/variables/" + key + "/download",
|
||||
})
|
||||
}
|
||||
|
||||
func handleAppCallFunction(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Name string `json:"name"`
|
||||
Params map[string]interface{} `json:"params"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"result": nil,
|
||||
})
|
||||
}
|
||||
|
||||
func handleAppCreateVariableRecord(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
key := c.Param("key")
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var appUser model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", userID, app.ID).First(&appUser).Error; err != nil {
|
||||
response.Error(c, 403, "无权访问该应用的云端变量")
|
||||
return
|
||||
}
|
||||
|
||||
var variable model.CloudVariable
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND key = ? AND status = ?", app.UserID, app.ID, key, "active").First(&variable).Error; err != nil {
|
||||
response.Error(c, 404, "云端变量不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if variable.DataType != "stream" {
|
||||
response.Error(c, 400, "该变量不是流水类型")
|
||||
return
|
||||
}
|
||||
|
||||
if variable.WritePermission != "user" && variable.WritePermission != "app_user" {
|
||||
response.Error(c, 403, "该变量不允许应用用户写入")
|
||||
return
|
||||
}
|
||||
|
||||
var req map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
dataBytes, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
response.Error(c, 500, "序列化数据失败")
|
||||
return
|
||||
}
|
||||
|
||||
record := model.CloudVariableRecord{
|
||||
CloudVariableID: variable.ID,
|
||||
Data: string(dataBytes),
|
||||
}
|
||||
|
||||
if variable.Scope == "user" {
|
||||
appUserID := userID.(uint)
|
||||
record.AppUserID = &appUserID
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&record).Error; err != nil {
|
||||
response.Error(c, 500, "创建记录失败")
|
||||
return
|
||||
}
|
||||
|
||||
if variable.MaxRecords > 0 {
|
||||
var total int64
|
||||
database.DB.Model(&model.CloudVariableRecord{}).Where("cloud_variable_id = ?", variable.ID).Count(&total)
|
||||
if int(total) > variable.MaxRecords {
|
||||
deleteCount := int(total) - variable.MaxRecords
|
||||
var oldRecords []model.CloudVariableRecord
|
||||
database.DB.Where("cloud_variable_id = ?", variable.ID).
|
||||
Order("created_at ASC").
|
||||
Limit(deleteCount).
|
||||
Find(&oldRecords)
|
||||
for _, r := range oldRecords {
|
||||
database.DB.Delete(&r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"id": record.ID,
|
||||
"created_at": record.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
func handleAppGetVariableRecords(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
key := c.Param("key")
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var appUser model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", userID, app.ID).First(&appUser).Error; err != nil {
|
||||
response.Error(c, 403, "无权访问该应用的云端变量")
|
||||
return
|
||||
}
|
||||
|
||||
var variable model.CloudVariable
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND key = ? AND status = ?", app.UserID, app.ID, key, "active").First(&variable).Error; err != nil {
|
||||
response.Error(c, 404, "云端变量不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if variable.DataType != "stream" {
|
||||
response.Error(c, 400, "该变量不是流水类型")
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
var total int64
|
||||
query := database.DB.Model(&model.CloudVariableRecord{}).Where("cloud_variable_id = ?", variable.ID)
|
||||
if variable.Scope == "user" {
|
||||
query = query.Where("app_user_id = ?", userID)
|
||||
}
|
||||
query.Count(&total)
|
||||
|
||||
var records []model.CloudVariableRecord
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Order("created_at DESC").Limit(pageSize).Offset(offset).Find(&records).Error; err != nil {
|
||||
response.Error(c, 500, "获取记录失败")
|
||||
return
|
||||
}
|
||||
|
||||
result := make([]gin.H, len(records))
|
||||
for i, r := range records {
|
||||
var data map[string]interface{}
|
||||
json.Unmarshal([]byte(r.Data), &data)
|
||||
result[i] = gin.H{
|
||||
"id": r.ID,
|
||||
"data": data,
|
||||
"created_at": r.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"records": result,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
"total_pages": (total + int64(pageSize) - 1) / int64(pageSize),
|
||||
})
|
||||
}
|
||||
|
||||
func handleAppDeleteVariableRecord(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
key := c.Param("key")
|
||||
recordID := c.Param("record_id")
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var appUser model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", userID, app.ID).First(&appUser).Error; err != nil {
|
||||
response.Error(c, 403, "无权访问该应用的云端变量")
|
||||
return
|
||||
}
|
||||
|
||||
var variable model.CloudVariable
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND key = ? AND status = ?", app.UserID, app.ID, key, "active").First(&variable).Error; err != nil {
|
||||
response.Error(c, 404, "云端变量不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if variable.DataType != "stream" {
|
||||
response.Error(c, 400, "该变量不是流水类型")
|
||||
return
|
||||
}
|
||||
|
||||
var record model.CloudVariableRecord
|
||||
query := database.DB.Where("id = ? AND cloud_variable_id = ?", recordID, variable.ID)
|
||||
if variable.Scope == "user" {
|
||||
query = query.Where("app_user_id = ?", userID)
|
||||
}
|
||||
if err := query.First(&record).Error; err != nil {
|
||||
response.Error(c, 404, "记录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DB.Delete(&record).Error; err != nil {
|
||||
response.Error(c, 500, "删除记录失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, nil)
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/service"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupDeviceRoutes(r *gin.RouterGroup) {
|
||||
r.GET("/devices", handleAppGetDevices)
|
||||
r.POST("/unbind-device", handleAppUnbindDevice)
|
||||
r.GET("/device-count", handleAppGetDeviceCount)
|
||||
r.GET("/instances", handleAppGetInstances)
|
||||
r.POST("/instances/:instance_id/offline", handleAppForceOfflineInstance)
|
||||
}
|
||||
|
||||
func SetupDevicePublicRoutes(r *gin.RouterGroup) {
|
||||
r.POST("/unbind-device-with-auth", handleAppUnbindDeviceWithAuth)
|
||||
r.POST("/change-password", handleAppChangePassword)
|
||||
}
|
||||
|
||||
func handleAppUnbindDeviceWithAuth(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
DeviceID string `json:"device_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Username == "" || req.Password == "" || req.DeviceID == "" {
|
||||
response.Error(c, 400, "用户名、密码和设备ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.Where("username = ? AND application_id = ?", req.Username, app.ID).First(&user).Error; err != nil {
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Password != req.Password {
|
||||
response.Error(c, 401, "密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
var device model.UserDevice
|
||||
if err := database.DB.Where("user_id = ? AND application_id = ? AND device_id = ?", user.ID, app.ID, req.DeviceID).First(&device).Error; err != nil {
|
||||
response.Error(c, 404, "设备不存在")
|
||||
return
|
||||
}
|
||||
|
||||
database.DB.Where("device_id = ?", device.ID).Delete(&model.DeviceSession{})
|
||||
|
||||
if err := database.DB.Delete(&device).Error; err != nil {
|
||||
response.Error(c, 500, "解绑设备失败")
|
||||
return
|
||||
}
|
||||
|
||||
if user.DeviceID == req.DeviceID {
|
||||
now := time.Now()
|
||||
database.DB.Model(&user).Updates(map[string]interface{}{
|
||||
"device_id": "",
|
||||
"last_heartbeat_at": &now,
|
||||
})
|
||||
}
|
||||
|
||||
service.LogVerification(c, &app.ID, &user.ID, "unbind_device", fmt.Sprintf("用户解绑设备(认证): %s", req.DeviceID), req.DeviceID, nil)
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "解绑成功",
|
||||
})
|
||||
}
|
||||
|
||||
func handleAppGetDevices(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
response.Error(c, 401, "未授权")
|
||||
return
|
||||
}
|
||||
|
||||
var devices []model.UserDevice
|
||||
if err := database.DB.Where("user_id = ? AND application_id = ?", userID, app.ID).Find(&devices).Error; err != nil {
|
||||
response.Error(c, 500, "获取设备列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
result := make([]gin.H, 0)
|
||||
for _, device := range devices {
|
||||
var onlineSessionCount int64
|
||||
tenMinutesAgo := time.Now().Add(-10 * time.Minute)
|
||||
database.DB.Model(&model.DeviceSession{}).
|
||||
Where("device_id = ? AND last_heartbeat > ?", device.ID, tenMinutesAgo).
|
||||
Count(&onlineSessionCount)
|
||||
|
||||
result = append(result, gin.H{
|
||||
"id": device.ID,
|
||||
"device_id": device.DeviceID,
|
||||
"device_name": device.DeviceName,
|
||||
"device_type": device.DeviceType,
|
||||
"status": device.Status,
|
||||
"online_sessions": onlineSessionCount,
|
||||
"created_at": device.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
response.Success(c, result)
|
||||
}
|
||||
|
||||
func handleAppGetDeviceCount(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
UserID uint `json:"user_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var count int64
|
||||
if err := database.DB.Model(&model.UserDevice{}).Where("user_id = ? AND application_id = ?", req.UserID, app.ID).Count(&count).Error; err != nil {
|
||||
response.Error(c, 500, "获取设备数量失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"count": count,
|
||||
"max_devices": app.MaxDevices,
|
||||
"remaining": app.MaxDevices - int(count),
|
||||
})
|
||||
}
|
||||
|
||||
func handleAppUnbindDevice(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
UserID uint `json:"user_id"`
|
||||
DeviceID string `json:"device_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var device model.UserDevice
|
||||
if err := database.DB.Where("user_id = ? AND application_id = ? AND device_id = ?", req.UserID, app.ID, req.DeviceID).First(&device).Error; err != nil {
|
||||
response.Error(c, 404, "设备不存在")
|
||||
return
|
||||
}
|
||||
|
||||
database.DB.Where("device_id = ?", device.ID).Delete(&model.DeviceSession{})
|
||||
|
||||
if err := database.DB.Delete(&device).Error; err != nil {
|
||||
response.Error(c, 500, "解绑设备失败")
|
||||
return
|
||||
}
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.First(&user, req.UserID).Error; err == nil {
|
||||
if user.DeviceID == req.DeviceID {
|
||||
now := time.Now()
|
||||
database.DB.Model(&user).Updates(map[string]interface{}{
|
||||
"device_id": "",
|
||||
"last_heartbeat_at": &now,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
service.LogVerification(c, &app.ID, &user.ID, "unbind_device", fmt.Sprintf("用户解绑设备: %s", req.DeviceID), req.DeviceID, nil)
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "解绑成功",
|
||||
})
|
||||
}
|
||||
|
||||
func handleAppGetInstances(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
UserID uint `json:"user_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var devices []model.UserDevice
|
||||
if err := database.DB.Where("user_id = ? AND application_id = ?", req.UserID, app.ID).Find(&devices).Error; err != nil {
|
||||
response.Error(c, 500, "获取设备列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
deviceIDs := make([]uint, len(devices))
|
||||
for i, d := range devices {
|
||||
deviceIDs[i] = d.ID
|
||||
}
|
||||
|
||||
heartbeatTimeout := app.HeartbeatTimeout
|
||||
if heartbeatTimeout <= 0 {
|
||||
heartbeatTimeout = 10
|
||||
}
|
||||
timeoutThreshold := time.Now().Add(-time.Duration(heartbeatTimeout) * time.Minute)
|
||||
|
||||
var sessions []model.DeviceSession
|
||||
if len(deviceIDs) > 0 {
|
||||
if err := database.DB.Where("device_id IN ?", deviceIDs).Order("last_heartbeat DESC").Find(&sessions).Error; err != nil {
|
||||
response.Error(c, 500, "获取实例列表失败")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]gin.H, 0)
|
||||
for _, session := range sessions {
|
||||
isOnline := session.LastHeartbeat != nil && session.LastHeartbeat.After(timeoutThreshold)
|
||||
|
||||
var device model.UserDevice
|
||||
for _, d := range devices {
|
||||
if d.ID == session.DeviceID {
|
||||
device = d
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, gin.H{
|
||||
"id": session.ID,
|
||||
"instance_id": session.InstanceID,
|
||||
"device_id": device.DeviceID,
|
||||
"device_name": device.DeviceName,
|
||||
"is_online": isOnline,
|
||||
"last_heartbeat": session.LastHeartbeat,
|
||||
"created_at": session.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
response.Success(c, result)
|
||||
}
|
||||
|
||||
func handleAppForceOfflineInstance(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
instanceID := c.Param("instance_id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
UserID uint `json:"user_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var devices []model.UserDevice
|
||||
if err := database.DB.Where("user_id = ? AND application_id = ?", req.UserID, app.ID).Find(&devices).Error; err != nil {
|
||||
response.Error(c, 500, "获取设备列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
deviceIDs := make([]uint, len(devices))
|
||||
for i, d := range devices {
|
||||
deviceIDs[i] = d.ID
|
||||
}
|
||||
|
||||
if len(deviceIDs) == 0 {
|
||||
response.Error(c, 404, "实例不存在")
|
||||
return
|
||||
}
|
||||
|
||||
result := database.DB.Where("instance_id = ? AND device_id IN ?", instanceID, deviceIDs).Delete(&model.DeviceSession{})
|
||||
if result.RowsAffected == 0 {
|
||||
response.Error(c, 404, "实例不存在")
|
||||
return
|
||||
}
|
||||
|
||||
service.LogVerification(c, &app.ID, &req.UserID, "force_offline", fmt.Sprintf("强制离线实例: %s", instanceID), instanceID, nil)
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "已强制离线",
|
||||
})
|
||||
}
|
||||
|
||||
func handleAppChangePassword(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
OldPassword string `json:"old_password" binding:"required"`
|
||||
NewPassword string `json:"new_password" binding:"required,min=6"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.Where("username = ? AND application_id = ?", req.Username, app.ID).First(&user).Error; err != nil {
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Password != req.OldPassword {
|
||||
response.Error(c, 400, "原密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
user.Password = req.NewPassword
|
||||
if err := database.DB.Save(&user).Error; err != nil {
|
||||
response.Error(c, 500, "密码修改失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "密码修改成功",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/service"
|
||||
"verification-platform-backend/pkg/jwt"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupDynamicRoutes(r *gin.RouterGroup) {
|
||||
dynamicCode := r.Group("/dynamic-code")
|
||||
{
|
||||
dynamicCode.POST("/:key/execute", handleExecuteDynamicCode)
|
||||
}
|
||||
}
|
||||
|
||||
func handleExecuteDynamicCode(c *gin.Context) {
|
||||
var token string
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader != "" {
|
||||
parts := strings.SplitN(authHeader, " ", 2)
|
||||
if len(parts) == 2 && parts[0] == "Bearer" {
|
||||
token = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
token = c.Query("token")
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
response.Error(c, http.StatusUnauthorized, "Authorization header is required")
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := jwt.ParseToken(token)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusUnauthorized, "Invalid token")
|
||||
return
|
||||
}
|
||||
|
||||
if time.Now().Unix() > claims.ExpiresAt.Unix() {
|
||||
response.Error(c, http.StatusUnauthorized, "Token expired")
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("role", claims.Role)
|
||||
|
||||
appKey := c.Param("appKey")
|
||||
key := c.Param("key")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var appUser model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", claims.UserID, app.ID).First(&appUser).Error; err != nil {
|
||||
response.Error(c, http.StatusForbidden, "无权访问该应用的动态代码")
|
||||
return
|
||||
}
|
||||
|
||||
var dynamicCode model.DynamicCode
|
||||
if err := database.DB.Where("application_id = ? AND key = ?", app.ID, key).First(&dynamicCode).Error; err != nil {
|
||||
response.Error(c, 404, "动态代码不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if dynamicCode.Status != "active" {
|
||||
response.Error(c, 400, "动态代码未启用")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Params map[string]interface{} `json:"params"`
|
||||
UserID *uint `json:"user_id,omitempty"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
vm := goja.New()
|
||||
|
||||
appData := map[string]interface{}{
|
||||
"id": app.ID,
|
||||
"name": app.Name,
|
||||
"description": app.Description,
|
||||
"app_key": app.AppKey,
|
||||
"status": app.Status,
|
||||
"billing_type": app.BillingType,
|
||||
"deduction_mode": app.DeductionMode,
|
||||
"deduction_type": app.DeductionType,
|
||||
"deduction_interval": app.DeductionInterval,
|
||||
"deduction_unit": app.DeductionUnit,
|
||||
"deduction_amount": app.DeductionAmount,
|
||||
"enable_trial": app.EnableTrial,
|
||||
"trial_balance": app.TrialBalance,
|
||||
"trial_days": app.TrialDays,
|
||||
"enable_free_period": app.EnableFreePeriod,
|
||||
"free_period_type": app.FreePeriodType,
|
||||
"free_period_start": app.FreePeriodStart,
|
||||
"free_period_end": app.FreePeriodEnd,
|
||||
"free_period_weekdays": app.FreePeriodWeekdays,
|
||||
"free_period_start_time": app.FreePeriodStartTime,
|
||||
"free_period_end_time": app.FreePeriodEndTime,
|
||||
"max_devices": app.MaxDevices,
|
||||
"bind_type": app.BindType,
|
||||
"multi_open": app.MultiOpen,
|
||||
"multi_open_mode": app.MultiOpenMode,
|
||||
"max_instances": app.MaxInstances,
|
||||
"login_policy": app.LoginPolicy,
|
||||
"max_attempts": app.MaxAttempts,
|
||||
"lock_duration": app.LockDuration,
|
||||
"heartbeat_interval": app.HeartbeatInterval,
|
||||
"heartbeat_timeout": app.HeartbeatTimeout,
|
||||
"change_limit": app.ChangeLimit,
|
||||
"change_interval": app.ChangeInterval,
|
||||
"change_exceed_action": app.ChangeExceedAction,
|
||||
"change_deduct_amount": app.ChangeDeductAmount,
|
||||
}
|
||||
if err := vm.Set("app", appData); err != nil {
|
||||
response.Error(c, 500, "应用数据设置失败")
|
||||
return
|
||||
}
|
||||
|
||||
var constants []model.CloudConstant
|
||||
database.DB.Where("app_id = ? AND status = ?", app.ID, "active").Find(&constants)
|
||||
constantsData := make(map[string]interface{})
|
||||
for _, c := range constants {
|
||||
constantsData[c.Key] = c.Value
|
||||
}
|
||||
if err := vm.Set("constants", constantsData); err != nil {
|
||||
response.Error(c, 500, "云端常量设置失败")
|
||||
return
|
||||
}
|
||||
|
||||
var cloudVariables []model.CloudVariable
|
||||
database.DB.Where("app_id = ? AND status = ?", app.ID, "active").Find(&cloudVariables)
|
||||
appVariables := make(map[string]interface{})
|
||||
for _, v := range cloudVariables {
|
||||
appVariables[v.Key] = v.DefaultValue
|
||||
}
|
||||
if err := vm.Set("appVariables", appVariables); err != nil {
|
||||
response.Error(c, 500, "云端变量设置失败")
|
||||
return
|
||||
}
|
||||
|
||||
userData := map[string]interface{}{}
|
||||
subscription := map[string]interface{}{}
|
||||
userVariables := map[string]interface{}{}
|
||||
devices := []map[string]interface{}{}
|
||||
|
||||
if req.UserID != nil {
|
||||
var user model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", *req.UserID, app.ID).First(&user).Error; err == nil {
|
||||
userData = map[string]interface{}{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"status": user.Status,
|
||||
"device_id": user.DeviceID,
|
||||
"avatar": user.Avatar,
|
||||
"created_at": user.CreatedAt,
|
||||
"last_login_at": user.LastLoginAt,
|
||||
}
|
||||
|
||||
subscription = map[string]interface{}{
|
||||
"balance": user.Balance,
|
||||
"is_trial_user": user.IsTrialUser,
|
||||
"trial_start_at": user.TrialStartAt,
|
||||
"trial_end_at": user.TrialEndAt,
|
||||
"expiry_at": user.ExpiryAt,
|
||||
"is_expired": user.ExpiryAt != nil && user.ExpiryAt.Before(time.Now()),
|
||||
"is_lifetime": user.Balance == -1,
|
||||
"days_remaining": calculateDaysRemaining(user.ExpiryAt, user.Balance),
|
||||
}
|
||||
|
||||
var userDevices []model.UserDevice
|
||||
database.DB.Where("user_id = ? AND application_id = ?", user.ID, app.ID).Find(&userDevices)
|
||||
for _, d := range userDevices {
|
||||
devices = append(devices, map[string]interface{}{
|
||||
"id": d.ID,
|
||||
"device_id": d.DeviceID,
|
||||
"device_name": d.DeviceName,
|
||||
"device_type": d.DeviceType,
|
||||
"status": d.Status,
|
||||
"created_at": d.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
var userVars []model.UserVariable
|
||||
database.DB.Where("user_id = ? AND app_id = ?", user.ID, app.ID).Find(&userVars)
|
||||
for _, v := range userVars {
|
||||
userVariables[v.VarName] = v.VarValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := vm.Set("user", userData); err != nil {
|
||||
response.Error(c, 500, "用户数据设置失败")
|
||||
return
|
||||
}
|
||||
if err := vm.Set("subscription", subscription); err != nil {
|
||||
response.Error(c, 500, "订阅数据设置失败")
|
||||
return
|
||||
}
|
||||
if err := vm.Set("userVariables", userVariables); err != nil {
|
||||
response.Error(c, 500, "用户变量设置失败")
|
||||
return
|
||||
}
|
||||
if err := vm.Set("devices", devices); err != nil {
|
||||
response.Error(c, 500, "设备数据设置失败")
|
||||
return
|
||||
}
|
||||
|
||||
for k, v := range req.Params {
|
||||
if err := vm.Set(k, v); err != nil {
|
||||
response.Error(c, 500, "参数设置失败")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := vm.Set("params", req.Params); err != nil {
|
||||
response.Error(c, 500, "参数设置失败")
|
||||
return
|
||||
}
|
||||
|
||||
value, err := vm.RunString("(function() { " + dynamicCode.Code + " })()")
|
||||
if err != nil {
|
||||
response.Error(c, 400, "代码执行错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
executionTime := time.Since(startTime).Milliseconds()
|
||||
|
||||
result := value.Export()
|
||||
|
||||
if actionMap, ok := result.(map[string]interface{}); ok {
|
||||
if action, hasAction := actionMap["action"]; hasAction {
|
||||
switch action {
|
||||
case "extend_time":
|
||||
if err := handleExtendTime(app.ID, actionMap); err != nil {
|
||||
response.Error(c, 500, "执行加时操作失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
case "deduct_points":
|
||||
if err := handleDeductPoints(app.ID, actionMap); err != nil {
|
||||
response.Error(c, 500, "执行扣点操作失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
case "update_user_variable":
|
||||
appID := app.ID
|
||||
if err := handleUpdateUserVariable(&appID, req.UserID, actionMap); err != nil {
|
||||
response.Error(c, 500, "更新用户变量失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
case "update_app_variable":
|
||||
if err := handleUpdateAppVariable(app.ID, actionMap); err != nil {
|
||||
response.Error(c, 500, "更新应用变量失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"result": result,
|
||||
"execution_time": executionTime,
|
||||
})
|
||||
}
|
||||
|
||||
func calculateDaysRemaining(expiryAt *time.Time, balance float64) int {
|
||||
if balance == -1 {
|
||||
return -1
|
||||
}
|
||||
if expiryAt == nil {
|
||||
return 0
|
||||
}
|
||||
remaining := int(time.Until(*expiryAt).Hours() / 24)
|
||||
if remaining < 0 {
|
||||
return 0
|
||||
}
|
||||
return remaining
|
||||
}
|
||||
|
||||
func handleExtendTime(appID uint, actionMap map[string]interface{}) error {
|
||||
userID, ok := actionMap["user_id"].(float64)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
days, ok := actionMap["days"].(float64)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", uint(userID), appID).First(&user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if user.Balance == -1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
user.Balance += float64(days)
|
||||
|
||||
return database.DB.Save(&user).Error
|
||||
}
|
||||
|
||||
func handleDeductPoints(appID uint, actionMap map[string]interface{}) error {
|
||||
userID, ok := actionMap["user_id"].(float64)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
points, ok := actionMap["points"].(float64)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", uint(userID), appID).First(&user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if user.Balance == -1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
user.Balance -= points
|
||||
if user.Balance < 0 {
|
||||
user.Balance = 0
|
||||
}
|
||||
|
||||
return database.DB.Save(&user).Error
|
||||
}
|
||||
|
||||
func handleUpdateUserVariable(appID *uint, userID *uint, actionMap map[string]interface{}) error {
|
||||
if userID == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
varName, ok := actionMap["name"].(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
varValue, ok := actionMap["value"].(string)
|
||||
if !ok {
|
||||
if v, ok := actionMap["value"]; ok {
|
||||
varValue = toString(v)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var userVar model.UserVariable
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND var_name = ?", *userID, appID, varName).First(&userVar).Error; err != nil {
|
||||
userVar = model.UserVariable{
|
||||
UserID: *userID,
|
||||
AppID: *appID,
|
||||
VarName: varName,
|
||||
VarValue: varValue,
|
||||
}
|
||||
return database.DB.Create(&userVar).Error
|
||||
}
|
||||
|
||||
userVar.VarValue = varValue
|
||||
return database.DB.Save(&userVar).Error
|
||||
}
|
||||
|
||||
func handleUpdateAppVariable(appID uint, actionMap map[string]interface{}) error {
|
||||
varName, ok := actionMap["name"].(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
varValue, ok := actionMap["value"].(string)
|
||||
if !ok {
|
||||
if v, ok := actionMap["value"]; ok {
|
||||
varValue = toString(v)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var appVar model.CloudVariable
|
||||
if err := database.DB.Where("app_id = ? AND key = ?", appID, varName).First(&appVar).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
appVar.DefaultValue = varValue
|
||||
return database.DB.Save(&appVar).Error
|
||||
}
|
||||
|
||||
func toString(v interface{}) string {
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
return val
|
||||
case float64:
|
||||
return string(rune(int(val)))
|
||||
case int:
|
||||
return string(rune(val))
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/service"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupInfoRoutes(r *gin.RouterGroup) {
|
||||
r.GET("/info", handleAppGetInfo)
|
||||
r.GET("/check-update", handleAppCheckUpdate)
|
||||
r.GET("/announcements", handleAppGetAnnouncements)
|
||||
}
|
||||
|
||||
func handleAppGetInfo(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"id": app.ID,
|
||||
"name": app.Name,
|
||||
"description": app.Description,
|
||||
"icon_url": app.IconURL,
|
||||
"status": app.Status,
|
||||
"billing_type": app.BillingType,
|
||||
"login_policy": app.LoginPolicy,
|
||||
"max_devices": app.MaxDevices,
|
||||
"multi_open": app.MultiOpen,
|
||||
"multi_open_mode": app.MultiOpenMode,
|
||||
"max_instances": app.MaxInstances,
|
||||
"enable_trial": app.EnableTrial,
|
||||
"trial_balance": app.TrialBalance,
|
||||
"heartbeat_interval": app.HeartbeatInterval,
|
||||
"heartbeat_timeout": app.HeartbeatTimeout,
|
||||
})
|
||||
}
|
||||
|
||||
func handleAppCheckUpdate(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
clientVersion := c.Query("version")
|
||||
|
||||
var latestVersion model.Version
|
||||
err := database.DB.Where("application_id = ? AND status = ?", app.ID, "active").Order("created_at DESC").Preload("Files").First(&latestVersion).Error
|
||||
|
||||
if err != nil {
|
||||
response.Success(c, gin.H{
|
||||
"has_update": false,
|
||||
"latest_version": "",
|
||||
"download_url": "",
|
||||
"update_notes": "",
|
||||
"update_strategy": "",
|
||||
"update_method": "",
|
||||
"files": []interface{}{},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
hasUpdate := clientVersion != latestVersion.Version
|
||||
if latestVersion.ForceUpdate {
|
||||
hasUpdate = true
|
||||
}
|
||||
|
||||
files := make([]gin.H, len(latestVersion.Files))
|
||||
for i, f := range latestVersion.Files {
|
||||
files[i] = gin.H{
|
||||
"file_path": f.FilePath,
|
||||
"file_name": f.FileName,
|
||||
"file_size": f.FileSize,
|
||||
"file_hash": f.FileHash,
|
||||
"file_type": f.FileType,
|
||||
"is_required": f.IsRequired,
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"has_update": hasUpdate,
|
||||
"latest_version": latestVersion.Version,
|
||||
"download_url": latestVersion.FilePath,
|
||||
"file_size": latestVersion.FileSize,
|
||||
"file_hash": latestVersion.FileHash,
|
||||
"entry_file": latestVersion.EntryFile,
|
||||
"update_notes": latestVersion.Description,
|
||||
"update_strategy": latestVersion.UpdateStrategy,
|
||||
"update_method": latestVersion.UpdateMethod,
|
||||
"min_version": latestVersion.MinVersion,
|
||||
"changelog": latestVersion.Changelog,
|
||||
"files": files,
|
||||
})
|
||||
}
|
||||
|
||||
func handleAppGetAnnouncements(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var announcements []model.Announcement
|
||||
if err := database.DB.Where("application_id = ? AND status = ?", app.ID, "active").Order("is_top DESC, created_at DESC").Find(&announcements).Error; err != nil {
|
||||
response.Error(c, 500, "获取公告失败")
|
||||
return
|
||||
}
|
||||
|
||||
result := make([]gin.H, len(announcements))
|
||||
for i, a := range announcements {
|
||||
result[i] = gin.H{
|
||||
"id": a.ID,
|
||||
"title": a.Title,
|
||||
"content": a.Content,
|
||||
"type": a.Type,
|
||||
"is_top": a.IsTop,
|
||||
"created_at": a.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, result)
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/service"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupPaymentRoutes(r *gin.RouterGroup) {
|
||||
r.POST("/recharge", handleAppRecharge)
|
||||
r.POST("/trial", handleAppTrial)
|
||||
}
|
||||
|
||||
func handleAppRecharge(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Username string `json:"username"`
|
||||
CardKey string `json:"card_key"`
|
||||
DeviceID string `json:"device_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if blocked, reason := checkRiskControl(c, app.ID, req.DeviceID, req.Username); blocked {
|
||||
response.Error(c, 403, reason)
|
||||
return
|
||||
}
|
||||
|
||||
var card model.Card
|
||||
if err := database.DB.Preload("CardType").Where("card_key = ?", req.CardKey).First(&card).Error; err != nil {
|
||||
service.LogVerification(c, &app.ID, nil, "recharge_failed", fmt.Sprintf("充值失败: 卡密不存在 - %s", req.CardKey), "", fmt.Errorf("卡密不存在"))
|
||||
response.Error(c, 404, "卡密不存在")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("[DEBUG] 卡密信息: ID=%d, CardKey=%s, Status=%s, CardTypeID=%d\n", card.ID, card.CardKey, card.Status, card.CardTypeID)
|
||||
fmt.Printf("[DEBUG] 卡类信息: ID=%d, Name=%s, Value=%f, Price=%f\n", card.CardType.ID, card.CardType.Name, card.CardType.Value, card.CardType.Price)
|
||||
|
||||
if card.Status != "unused" {
|
||||
service.LogVerification(c, &app.ID, nil, "recharge_failed", fmt.Sprintf("充值失败: 卡密已使用 - %s", req.CardKey), "", fmt.Errorf("卡密已使用"))
|
||||
response.Error(c, 400, "卡密已使用")
|
||||
return
|
||||
}
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.Where("username = ? AND application_id = ?", req.Username, app.ID).First(&user).Error; err != nil {
|
||||
service.LogVerification(c, &app.ID, nil, "recharge_failed", fmt.Sprintf("充值失败: 用户不存在 - %s", req.Username), "", fmt.Errorf("用户不存在"))
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Balance == -1 {
|
||||
service.LogVerification(c, &app.ID, &user.ID, "recharge_failed", fmt.Sprintf("充值失败: 用户已是永久会员 - %s", user.Username), "", fmt.Errorf("该用户已是永久会员,无法再次充值"))
|
||||
response.Error(c, 400, "该用户已是永久会员,无法再次充值")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("[DEBUG] 充值前用户信息: ID=%d, Username=%s, Balance=%f, ExpiryAt=%v\n", user.ID, user.Username, user.Balance, user.ExpiryAt)
|
||||
|
||||
tx := database.DB.Begin()
|
||||
|
||||
card.Status = "used"
|
||||
card.AppUserID = &user.ID
|
||||
now := time.Now()
|
||||
card.UsedAt = &now
|
||||
|
||||
if err := tx.Save(&card).Error; err != nil {
|
||||
tx.Rollback()
|
||||
response.Error(c, 500, "充值失败")
|
||||
return
|
||||
}
|
||||
|
||||
user.IsTrialUser = false
|
||||
|
||||
if card.CardType.Value == -1 {
|
||||
user.Balance = -1
|
||||
user.ExpiryAt = nil
|
||||
} else {
|
||||
switch card.CardType.RechargeType {
|
||||
case "subscription":
|
||||
var baseTime time.Time
|
||||
if user.ExpiryAt != nil && user.ExpiryAt.After(now) {
|
||||
baseTime = *user.ExpiryAt
|
||||
} else {
|
||||
baseTime = now
|
||||
}
|
||||
var duration time.Duration
|
||||
switch card.CardType.ValueUnit {
|
||||
case "minute":
|
||||
duration = time.Duration(card.CardType.Value) * time.Minute
|
||||
case "hour":
|
||||
duration = time.Duration(card.CardType.Value) * time.Hour
|
||||
case "day":
|
||||
duration = time.Duration(card.CardType.Value) * 24 * time.Hour
|
||||
case "month":
|
||||
duration = time.Duration(card.CardType.Value) * 30 * 24 * time.Hour
|
||||
case "year":
|
||||
duration = time.Duration(card.CardType.Value) * 365 * 24 * time.Hour
|
||||
default:
|
||||
duration = time.Duration(card.CardType.Value) * time.Second
|
||||
}
|
||||
newExpiry := baseTime.Add(duration)
|
||||
user.ExpiryAt = &newExpiry
|
||||
case "balance":
|
||||
fallthrough
|
||||
default:
|
||||
user.Balance += card.CardType.Value
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("[DEBUG] 充值后用户信息: Balance=%f, ExpiryAt=%v (CardType.Value=%f, BillingType=%s)\n", user.Balance, user.ExpiryAt, card.CardType.Value, app.BillingType)
|
||||
|
||||
if err := tx.Save(&user).Error; err != nil {
|
||||
tx.Rollback()
|
||||
response.Error(c, 500, "更新用户信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
rechargeRecord := model.RechargeRecord{
|
||||
UserID: user.ID,
|
||||
OrderNo: generateOrderNo("R"),
|
||||
CardID: &card.ID,
|
||||
CardCode: card.CardKey,
|
||||
Amount: card.CardType.Price,
|
||||
Status: "success",
|
||||
PaymentType: "card",
|
||||
Remark: "卡密充值 - " + card.CardType.Name,
|
||||
}
|
||||
|
||||
if err := tx.Create(&rechargeRecord).Error; err != nil {
|
||||
tx.Rollback()
|
||||
response.Error(c, 500, "创建充值记录失败")
|
||||
return
|
||||
}
|
||||
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
response.Error(c, 500, "充值失败")
|
||||
return
|
||||
}
|
||||
|
||||
service.LogVerification(c, &app.ID, &user.ID, "recharge", fmt.Sprintf("用户充值: %s, 卡密: %s, 金额: %.2f", user.Username, card.CardKey, card.CardType.Price), "", nil)
|
||||
|
||||
response.SuccessWithMessage(c, "充值成功", gin.H{
|
||||
"message": "充值成功",
|
||||
"value": card.CardType.Value,
|
||||
})
|
||||
}
|
||||
|
||||
func handleAppTrial(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
if !app.EnableTrial {
|
||||
response.Error(c, 400, "该应用不支持试用")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
UserID uint `json:"user_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.First(&user, req.UserID).Error; err != nil {
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
response.SuccessWithMessage(c, "试用成功", gin.H{
|
||||
"message": "试用成功",
|
||||
"trial_balance": app.TrialBalance,
|
||||
})
|
||||
}
|
||||
|
||||
func generateOrderNo(prefix string) string {
|
||||
return prefix + time.Now().Format("20060102150405") + randomString(6)
|
||||
}
|
||||
|
||||
func randomString(length int) string {
|
||||
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
b[i] = charset[time.Now().Nanosecond()%len(charset)]
|
||||
time.Sleep(1 * time.Nanosecond)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"verification-platform-backend/pkg/crypto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type bodyLogWriter struct {
|
||||
gin.ResponseWriter
|
||||
body *bytes.Buffer
|
||||
}
|
||||
|
||||
func (w *bodyLogWriter) Write(b []byte) (int, error) {
|
||||
return w.body.Write(b)
|
||||
}
|
||||
|
||||
func (w *bodyLogWriter) WriteHeader(statusCode int) {
|
||||
w.ResponseWriter.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
type EncryptedResponse struct {
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
func ResponseEncryption() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
|
||||
c.Writer = blw
|
||||
|
||||
c.Next()
|
||||
|
||||
shouldEncrypt := c.GetBool("should_encrypt_response")
|
||||
fmt.Printf("[ResponseEncryption] ShouldEncrypt: %v\n", shouldEncrypt)
|
||||
|
||||
blw.ResponseWriter.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
if shouldEncrypt {
|
||||
var response map[string]interface{}
|
||||
if err := json.Unmarshal(blw.body.Bytes(), &response); err == nil {
|
||||
responseJSON, _ := json.Marshal(response)
|
||||
fmt.Printf("[ResponseEncryption] Response to encrypt: %s\n", string(responseJSON))
|
||||
|
||||
var cryptoManager *crypto.CryptoManager
|
||||
if cm, ok := c.Get("crypto_manager"); ok {
|
||||
cryptoManager = cm.(*crypto.CryptoManager)
|
||||
}
|
||||
|
||||
if cryptoManager != nil {
|
||||
encrypted, err := cryptoManager.Encrypt(string(responseJSON))
|
||||
if err == nil {
|
||||
encryptedResponse := EncryptedResponse{Data: encrypted}
|
||||
encryptedJSON, _ := json.Marshal(encryptedResponse)
|
||||
blw.ResponseWriter.Write(encryptedJSON)
|
||||
fmt.Printf("[ResponseEncryption] Encrypted response sent\n")
|
||||
return
|
||||
} else {
|
||||
fmt.Printf("[ResponseEncryption] Encryption error: %v\n", err)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("[ResponseEncryption] CryptoManager is nil\n")
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("[ResponseEncryption] JSON unmarshal error: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
blw.ResponseWriter.Write(blw.body.Bytes())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,776 @@
|
||||
package developer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupAgentAppRoutes(r *gin.RouterGroup) {
|
||||
agentApps := r.Group("/agent-apps")
|
||||
{
|
||||
agentApps.GET("", handleGetAgentApps)
|
||||
agentApps.GET("/requests", handleGetAgentRequests)
|
||||
agentApps.POST("/invite", handleInviteAgent)
|
||||
agentApps.PUT("/requests/:id/approve", handleApproveRequest)
|
||||
agentApps.PUT("/requests/:id/reject", handleRejectRequest)
|
||||
agentApps.GET("/:id", handleGetAgentAppDetail)
|
||||
agentApps.PUT("/:id", handleUpdateAgentApp)
|
||||
agentApps.PUT("/:id/card-types", handleUpdateAgentCardTypes)
|
||||
agentApps.DELETE("/:id", handleRemoveAgentApp)
|
||||
}
|
||||
}
|
||||
|
||||
func SetupAgentAppRoutesWithoutPackage(r *gin.RouterGroup) {
|
||||
agentApps := r.Group("/agent-apps")
|
||||
{
|
||||
agentApps.GET("/my-requests", handleGetMyRequests)
|
||||
agentApps.POST("/request", handleRequestAuthorization)
|
||||
agentApps.DELETE("/requests/:id", handleCancelRequest)
|
||||
}
|
||||
}
|
||||
|
||||
func checkAgentPermission(userID uint) bool {
|
||||
var user model.User
|
||||
if err := database.DB.First(&user, userID).Error; err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if user.CurrentPackageID == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var userPackage model.UserPackage
|
||||
if err := database.DB.Where("user_id = ? AND package_id = ? AND status = ?",
|
||||
userID, user.CurrentPackageID, "active").First(&userPackage).Error; err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if userPackage.ExpiredAt != nil && userPackage.ExpiredAt.Before(time.Now()) {
|
||||
return false
|
||||
}
|
||||
|
||||
var permission model.PackagePermission
|
||||
if err := database.DB.Where("package_id = ?", user.CurrentPackageID).First(&permission).Error; err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return permission.AllowAgent
|
||||
}
|
||||
|
||||
func handleGetAgentApps(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
log.Printf("[DEBUG] handleGetAgentApps called for developer %d\n", userID)
|
||||
|
||||
var myAuthorizations []model.AgentApplication
|
||||
if err := database.DB.Where("developer_id = ?", userID).
|
||||
Preload("CardTypes.CardType").
|
||||
Find(&myAuthorizations).Error; err != nil {
|
||||
response.Error(c, 500, "获取授权列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
var receivedAuthorizations []model.AgentApplication
|
||||
if err := database.DB.Where("agent_id = ?", userID).
|
||||
Preload("CardTypes.CardType").
|
||||
Find(&receivedAuthorizations).Error; err != nil {
|
||||
response.Error(c, 500, "获取授权列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Found %d my authorizations and %d received authorizations for developer %d\n",
|
||||
len(myAuthorizations), len(receivedAuthorizations), userID)
|
||||
|
||||
var allAgentApps []model.AgentApplication
|
||||
allAgentApps = append(allAgentApps, myAuthorizations...)
|
||||
allAgentApps = append(allAgentApps, receivedAuthorizations...)
|
||||
|
||||
var agentIDs []uint
|
||||
var developerIDs []uint
|
||||
var applicationIDs []uint
|
||||
|
||||
for _, aa := range allAgentApps {
|
||||
agentIDs = append(agentIDs, aa.AgentID)
|
||||
developerIDs = append(developerIDs, aa.DeveloperID)
|
||||
applicationIDs = append(applicationIDs, aa.ApplicationID)
|
||||
}
|
||||
|
||||
var users []model.User
|
||||
if err := database.DB.Where("id IN ?", append(agentIDs, developerIDs...)).Find(&users).Error; err != nil {
|
||||
log.Printf("[ERROR] Failed to query users: %v\n", err)
|
||||
} else {
|
||||
log.Printf("[DEBUG] Found %d users\n", len(users))
|
||||
}
|
||||
|
||||
var applications []model.Application
|
||||
if err := database.DB.Where("id IN ?", applicationIDs).Find(&applications).Error; err != nil {
|
||||
log.Printf("[ERROR] Failed to query applications: %v\n", err)
|
||||
}
|
||||
|
||||
userMap := make(map[uint]model.User)
|
||||
for _, user := range users {
|
||||
userMap[user.ID] = user
|
||||
}
|
||||
|
||||
applicationMap := make(map[uint]model.Application)
|
||||
for _, app := range applications {
|
||||
applicationMap[app.ID] = app
|
||||
}
|
||||
|
||||
type CardTypeResponse struct {
|
||||
ID uint `json:"id"`
|
||||
CardTypeID uint `json:"card_type_id"`
|
||||
Name string `json:"name"`
|
||||
CanGenerate bool `json:"can_generate"`
|
||||
}
|
||||
|
||||
type AgentAppResponse struct {
|
||||
ID uint `json:"id"`
|
||||
AgentID uint `json:"agent_id"`
|
||||
AgentName string `json:"agent_name"`
|
||||
AgentEmail string `json:"agent_email"`
|
||||
ApplicationID uint `json:"application_id"`
|
||||
AppName string `json:"app_name"`
|
||||
Commission float64 `json:"commission"`
|
||||
Discount float64 `json:"discount"`
|
||||
Status string `json:"status"`
|
||||
CardTypes []CardTypeResponse `json:"card_types"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
IsReceived bool `json:"is_received"`
|
||||
}
|
||||
|
||||
var result []AgentAppResponse
|
||||
for _, aa := range allAgentApps {
|
||||
agentName := ""
|
||||
agentEmail := ""
|
||||
if user, exists := userMap[aa.AgentID]; exists {
|
||||
agentName = user.Username
|
||||
if user.Email != nil {
|
||||
agentEmail = *user.Email
|
||||
}
|
||||
}
|
||||
|
||||
appName := ""
|
||||
if app, exists := applicationMap[aa.ApplicationID]; exists {
|
||||
appName = app.Name
|
||||
}
|
||||
|
||||
var cardTypes []CardTypeResponse
|
||||
for _, ct := range aa.CardTypes {
|
||||
cardTypes = append(cardTypes, CardTypeResponse{
|
||||
ID: ct.ID,
|
||||
CardTypeID: ct.CardTypeID,
|
||||
Name: ct.CardType.Name,
|
||||
CanGenerate: ct.CanGenerate,
|
||||
})
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Processing AgentApp: ID=%d, AgentID=%d, ApplicationID=%d, CardTypes count=%d, IsReceived=%v\n",
|
||||
aa.ID, aa.AgentID, aa.ApplicationID, len(cardTypes), aa.AgentID == userID)
|
||||
|
||||
result = append(result, AgentAppResponse{
|
||||
ID: aa.ID,
|
||||
AgentID: aa.AgentID,
|
||||
AgentName: agentName,
|
||||
AgentEmail: agentEmail,
|
||||
ApplicationID: aa.ApplicationID,
|
||||
AppName: appName,
|
||||
Commission: aa.Commission,
|
||||
Discount: aa.Discount,
|
||||
Status: aa.Status,
|
||||
CardTypes: cardTypes,
|
||||
CreatedAt: aa.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
IsReceived: aa.AgentID == userID,
|
||||
})
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Returning %d agent apps for user %d\n", len(result), userID)
|
||||
response.Success(c, gin.H{
|
||||
"agent_apps": result,
|
||||
"total": len(result),
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetAgentRequests(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
requestType := c.Query("type")
|
||||
|
||||
query := database.DB.Where("developer_id = ?", userID)
|
||||
if requestType == "invite" {
|
||||
query = query.Where("type = ?", "invite")
|
||||
} else if requestType == "request" {
|
||||
query = query.Where("type = ?", "request")
|
||||
}
|
||||
|
||||
var requests []model.AgentApplicationRequest
|
||||
if err := query.
|
||||
Preload("Agent").
|
||||
Preload("Application").
|
||||
Order("created_at DESC").
|
||||
Find(&requests).Error; err != nil {
|
||||
response.Error(c, 500, "获取申请列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
type RequestResponse struct {
|
||||
ID uint `json:"id"`
|
||||
AgentID uint `json:"agent_id"`
|
||||
AgentName string `json:"agent_name"`
|
||||
AgentEmail string `json:"agent_email"`
|
||||
DeveloperID uint `json:"developer_id"`
|
||||
ApplicationID uint `json:"application_id"`
|
||||
AppName string `json:"app_name"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
RejectReason string `json:"reject_reason"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
var result []RequestResponse
|
||||
for _, req := range requests {
|
||||
agentName := ""
|
||||
agentEmail := ""
|
||||
if req.Agent.ID != 0 {
|
||||
agentName = req.Agent.Username
|
||||
if req.Agent.Email != nil {
|
||||
agentEmail = *req.Agent.Email
|
||||
}
|
||||
}
|
||||
|
||||
appName := ""
|
||||
if req.Application.ID != 0 {
|
||||
appName = req.Application.Name
|
||||
}
|
||||
|
||||
result = append(result, RequestResponse{
|
||||
ID: req.ID,
|
||||
AgentID: req.AgentID,
|
||||
AgentName: agentName,
|
||||
AgentEmail: agentEmail,
|
||||
DeveloperID: req.DeveloperID,
|
||||
ApplicationID: req.ApplicationID,
|
||||
AppName: appName,
|
||||
Type: req.Type,
|
||||
Status: req.Status,
|
||||
Message: req.Message,
|
||||
RejectReason: req.RejectReason,
|
||||
CreatedAt: req.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"requests": result,
|
||||
"total": len(result),
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetMyRequests(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
requestType := c.Query("type")
|
||||
|
||||
query := database.DB.Where("agent_id = ?", userID)
|
||||
if requestType == "invite" {
|
||||
query = query.Where("type = ?", "invite")
|
||||
} else if requestType == "request" {
|
||||
query = query.Where("type = ?", "request")
|
||||
}
|
||||
|
||||
var requests []model.AgentApplicationRequest
|
||||
if err := query.
|
||||
Preload("Developer").
|
||||
Preload("Application").
|
||||
Order("created_at DESC").
|
||||
Find(&requests).Error; err != nil {
|
||||
response.Error(c, 500, "获取申请列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
type RequestResponse struct {
|
||||
ID uint `json:"id"`
|
||||
AgentID uint `json:"agent_id"`
|
||||
DeveloperID uint `json:"developer_id"`
|
||||
DeveloperName string `json:"developer_name"`
|
||||
ApplicationID uint `json:"application_id"`
|
||||
AppName string `json:"app_name"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
RejectReason string `json:"reject_reason"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
var result []RequestResponse
|
||||
for _, req := range requests {
|
||||
developerName := ""
|
||||
if req.Developer.ID != 0 {
|
||||
developerName = req.Developer.Username
|
||||
}
|
||||
|
||||
appName := ""
|
||||
if req.Application.ID != 0 {
|
||||
appName = req.Application.Name
|
||||
}
|
||||
|
||||
result = append(result, RequestResponse{
|
||||
ID: req.ID,
|
||||
AgentID: req.AgentID,
|
||||
DeveloperID: req.DeveloperID,
|
||||
DeveloperName: developerName,
|
||||
ApplicationID: req.ApplicationID,
|
||||
AppName: appName,
|
||||
Type: req.Type,
|
||||
Status: req.Status,
|
||||
Message: req.Message,
|
||||
RejectReason: req.RejectReason,
|
||||
CreatedAt: req.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"requests": result,
|
||||
"total": len(result),
|
||||
})
|
||||
}
|
||||
|
||||
func handleInviteAgent(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
if !checkAgentPermission(userID) {
|
||||
response.Error(c, 403, "您的套餐不支持代理功能,请升级套餐")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
AgentID uint `json:"agent_id"`
|
||||
ApplicationID uint `json:"application_id"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var agent model.User
|
||||
if err := database.DB.First(&agent, req.AgentID).Error; err != nil {
|
||||
response.Error(c, 404, "开发者不存在")
|
||||
return
|
||||
}
|
||||
if agent.Role != "developer" {
|
||||
response.Error(c, 400, "该用户不是开发者")
|
||||
return
|
||||
}
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", req.ApplicationID, userID).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var existing model.AgentApplication
|
||||
if err := database.DB.Where("agent_id = ? AND application_id = ?", req.AgentID, req.ApplicationID).First(&existing).Error; err == nil {
|
||||
response.Error(c, 400, "该开发者已获得此应用的授权")
|
||||
return
|
||||
}
|
||||
|
||||
request := model.AgentApplicationRequest{
|
||||
AgentID: req.AgentID,
|
||||
DeveloperID: userID,
|
||||
ApplicationID: req.ApplicationID,
|
||||
Type: "invite",
|
||||
Status: "pending",
|
||||
Message: req.Message,
|
||||
}
|
||||
if err := database.DB.Create(&request).Error; err != nil {
|
||||
fmt.Printf("创建邀请错误: %v\n", err)
|
||||
response.Error(c, 500, "创建邀请失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"id": request.ID,
|
||||
"agent_id": request.AgentID,
|
||||
"agent_name": agent.Username,
|
||||
"app_id": request.ApplicationID,
|
||||
"app_name": app.Name,
|
||||
"type": request.Type,
|
||||
"status": request.Status,
|
||||
})
|
||||
}
|
||||
|
||||
func handleRequestAuthorization(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
var req struct {
|
||||
DeveloperID uint `json:"developer_id"`
|
||||
ApplicationID uint `json:"application_id"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var developer model.User
|
||||
if err := database.DB.First(&developer, req.DeveloperID).Error; err != nil {
|
||||
response.Error(c, 404, "开发者不存在")
|
||||
return
|
||||
}
|
||||
if developer.Role != "developer" {
|
||||
response.Error(c, 400, "该用户不是开发者")
|
||||
return
|
||||
}
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", req.ApplicationID, req.DeveloperID).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var existing model.AgentApplication
|
||||
if err := database.DB.Where("agent_id = ? AND application_id = ?", userID, req.ApplicationID).First(&existing).Error; err == nil {
|
||||
response.Error(c, 400, "您已获得此应用的授权")
|
||||
return
|
||||
}
|
||||
|
||||
var existingRequest model.AgentApplicationRequest
|
||||
if err := database.DB.Where("agent_id = ? AND developer_id = ? AND application_id = ? AND status = ?",
|
||||
userID, req.DeveloperID, req.ApplicationID, "pending").First(&existingRequest).Error; err == nil {
|
||||
response.Error(c, 400, "您已有待处理的申请")
|
||||
return
|
||||
}
|
||||
|
||||
request := model.AgentApplicationRequest{
|
||||
AgentID: userID,
|
||||
DeveloperID: req.DeveloperID,
|
||||
ApplicationID: req.ApplicationID,
|
||||
Type: "request",
|
||||
Status: "pending",
|
||||
Message: req.Message,
|
||||
}
|
||||
if err := database.DB.Create(&request).Error; err != nil {
|
||||
fmt.Printf("创建申请错误: %v\n", err)
|
||||
response.Error(c, 500, "创建申请失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"id": request.ID,
|
||||
"developer_id": request.DeveloperID,
|
||||
"developer_name": developer.Username,
|
||||
"app_id": request.ApplicationID,
|
||||
"app_name": app.Name,
|
||||
"type": request.Type,
|
||||
"status": request.Status,
|
||||
})
|
||||
}
|
||||
|
||||
func handleApproveRequest(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
requestID := c.Param("id")
|
||||
|
||||
if !checkAgentPermission(userID) {
|
||||
response.Error(c, 403, "您的套餐不支持代理功能,请升级套餐")
|
||||
return
|
||||
}
|
||||
|
||||
var req model.AgentApplicationRequest
|
||||
if err := database.DB.Where("id = ? AND developer_id = ?", requestID, userID).First(&req).Error; err != nil {
|
||||
response.Error(c, 404, "申请不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Status != "pending" {
|
||||
response.Error(c, 400, "该申请已处理")
|
||||
return
|
||||
}
|
||||
|
||||
tx := database.DB.Begin()
|
||||
|
||||
agentApp := model.AgentApplication{
|
||||
AgentID: req.AgentID,
|
||||
ApplicationID: req.ApplicationID,
|
||||
DeveloperID: userID,
|
||||
Commission: 0.1,
|
||||
Discount: 1.0,
|
||||
Status: "active",
|
||||
IsReceived: true,
|
||||
}
|
||||
if err := tx.Create(&agentApp).Error; err != nil {
|
||||
tx.Rollback()
|
||||
fmt.Printf("创建授权错误: %v\n", err)
|
||||
response.Error(c, 500, "创建授权失败")
|
||||
return
|
||||
}
|
||||
|
||||
var cardTypes []model.CardType
|
||||
database.DB.Where("application_id = ? OR application_id IS NULL", req.ApplicationID).Find(&cardTypes)
|
||||
for _, ct := range cardTypes {
|
||||
agentCardType := model.AgentApplicationCardType{
|
||||
AgentApplicationID: agentApp.ID,
|
||||
CardTypeID: ct.ID,
|
||||
CanGenerate: false,
|
||||
}
|
||||
tx.Create(&agentCardType)
|
||||
}
|
||||
|
||||
req.Status = "approved"
|
||||
if err := tx.Save(&req).Error; err != nil {
|
||||
tx.Rollback()
|
||||
response.Error(c, 500, "更新申请状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"id": agentApp.ID,
|
||||
"status": "approved",
|
||||
})
|
||||
}
|
||||
|
||||
func handleRejectRequest(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
requestID := c.Param("id")
|
||||
|
||||
var reqBody struct {
|
||||
RejectReason string `json:"reject_reason"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&reqBody); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var req model.AgentApplicationRequest
|
||||
if err := database.DB.Where("id = ? AND developer_id = ?", requestID, userID).First(&req).Error; err != nil {
|
||||
response.Error(c, 404, "申请不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Status != "pending" {
|
||||
response.Error(c, 400, "该申请已处理")
|
||||
return
|
||||
}
|
||||
|
||||
req.Status = "rejected"
|
||||
req.RejectReason = reqBody.RejectReason
|
||||
if err := database.DB.Save(&req).Error; err != nil {
|
||||
response.Error(c, 500, "更新申请状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"id": req.ID,
|
||||
"status": "rejected",
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetAgentAppDetail(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
agentAppID := c.Param("id")
|
||||
|
||||
var agentApp model.AgentApplication
|
||||
if err := database.DB.Where("id = ? AND developer_id = ?", agentAppID, userID).
|
||||
Preload("CardTypes.CardType").
|
||||
First(&agentApp).Error; err != nil {
|
||||
response.Error(c, 404, "授权记录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var agent model.User
|
||||
if err := database.DB.Where("id = ?", agentApp.AgentID).First(&agent).Error; err != nil {
|
||||
log.Printf("[ERROR] Failed to query agent: %v\n", err)
|
||||
}
|
||||
|
||||
var application model.Application
|
||||
if err := database.DB.Where("id = ?", agentApp.ApplicationID).First(&application).Error; err != nil {
|
||||
log.Printf("[ERROR] Failed to query application: %v\n", err)
|
||||
}
|
||||
|
||||
type CardTypeResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Price float64 `json:"price"`
|
||||
CanGenerate bool `json:"can_generate"`
|
||||
}
|
||||
|
||||
type AgentAppDetailResponse struct {
|
||||
ID uint `json:"id"`
|
||||
AgentID uint `json:"agent_id"`
|
||||
AgentName string `json:"agent_name"`
|
||||
AgentEmail string `json:"agent_email"`
|
||||
ApplicationID uint `json:"application_id"`
|
||||
AppName string `json:"app_name"`
|
||||
Commission float64 `json:"commission"`
|
||||
Discount float64 `json:"discount"`
|
||||
Status string `json:"status"`
|
||||
CardTypes []CardTypeResponse `json:"card_types"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
var cardTypes []CardTypeResponse
|
||||
for _, ct := range agentApp.CardTypes {
|
||||
cardTypes = append(cardTypes, CardTypeResponse{
|
||||
ID: ct.CardTypeID,
|
||||
Name: ct.CardType.Name,
|
||||
Price: ct.CardType.Price,
|
||||
CanGenerate: ct.CanGenerate,
|
||||
})
|
||||
}
|
||||
|
||||
result := AgentAppDetailResponse{
|
||||
ID: agentApp.ID,
|
||||
AgentID: agentApp.AgentID,
|
||||
AgentName: agent.Username,
|
||||
ApplicationID: agentApp.ApplicationID,
|
||||
AppName: application.Name,
|
||||
Commission: agentApp.Commission,
|
||||
Discount: agentApp.Discount,
|
||||
Status: agentApp.Status,
|
||||
CardTypes: cardTypes,
|
||||
CreatedAt: agentApp.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
|
||||
if agent.Email != nil {
|
||||
result.AgentEmail = *agent.Email
|
||||
}
|
||||
|
||||
response.Success(c, result)
|
||||
}
|
||||
|
||||
func handleUpdateAgentApp(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
agentAppID := c.Param("id")
|
||||
|
||||
var reqBody struct {
|
||||
Commission *float64 `json:"commission"`
|
||||
Discount *float64 `json:"discount"`
|
||||
Status *string `json:"status"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&reqBody); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var agentApp model.AgentApplication
|
||||
if err := database.DB.Where("id = ? AND developer_id = ?", agentAppID, userID).First(&agentApp).Error; err != nil {
|
||||
response.Error(c, 404, "授权记录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
updates := make(map[string]interface{})
|
||||
if reqBody.Commission != nil {
|
||||
updates["commission"] = *reqBody.Commission
|
||||
}
|
||||
if reqBody.Discount != nil {
|
||||
updates["discount"] = *reqBody.Discount
|
||||
}
|
||||
if reqBody.Status != nil {
|
||||
updates["status"] = *reqBody.Status
|
||||
}
|
||||
|
||||
if len(updates) > 0 {
|
||||
if err := database.DB.Model(&agentApp).Updates(updates).Error; err != nil {
|
||||
response.Error(c, 500, "更新失败")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"id": agentApp.ID,
|
||||
})
|
||||
}
|
||||
|
||||
func handleUpdateAgentCardTypes(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
agentAppID := c.Param("id")
|
||||
|
||||
var reqBody struct {
|
||||
CardTypes []struct {
|
||||
CardTypeID uint `json:"card_type_id"`
|
||||
CanGenerate bool `json:"can_generate"`
|
||||
} `json:"card_types"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&reqBody); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var agentApp model.AgentApplication
|
||||
if err := database.DB.Where("id = ? AND developer_id = ?", agentAppID, userID).First(&agentApp).Error; err != nil {
|
||||
response.Error(c, 404, "授权记录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
tx := database.DB.Begin()
|
||||
|
||||
if err := tx.Where("agent_application_id = ?", agentAppID).Delete(&model.AgentApplicationCardType{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
response.Error(c, 500, "清除旧权限失败")
|
||||
return
|
||||
}
|
||||
|
||||
for _, ct := range reqBody.CardTypes {
|
||||
agentCardType := model.AgentApplicationCardType{
|
||||
AgentApplicationID: agentApp.ID,
|
||||
CardTypeID: ct.CardTypeID,
|
||||
CanGenerate: ct.CanGenerate,
|
||||
}
|
||||
if err := tx.Create(&agentCardType).Error; err != nil {
|
||||
tx.Rollback()
|
||||
response.Error(c, 500, "创建权限失败")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"id": agentApp.ID,
|
||||
})
|
||||
}
|
||||
|
||||
func handleRemoveAgentApp(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
agentAppID := c.Param("id")
|
||||
|
||||
var agentApp model.AgentApplication
|
||||
if err := database.DB.Where("id = ? AND developer_id = ?", agentAppID, userID).First(&agentApp).Error; err != nil {
|
||||
response.Error(c, 404, "授权记录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DB.Delete(&agentApp).Error; err != nil {
|
||||
response.Error(c, 500, "删除失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"id": agentApp.ID,
|
||||
})
|
||||
}
|
||||
|
||||
func handleCancelRequest(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
requestID := c.Param("id")
|
||||
|
||||
var req model.AgentApplicationRequest
|
||||
if err := database.DB.Where("id = ? AND agent_id = ? AND status = ?", requestID, userID, "pending").First(&req).Error; err != nil {
|
||||
response.Error(c, 404, "申请不存在或已处理")
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DB.Delete(&req).Error; err != nil {
|
||||
response.Error(c, 500, "取消失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"id": req.ID,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,585 @@
|
||||
package developer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/service"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func SetupAgentsRoutes(r *gin.RouterGroup) {
|
||||
agents := r.Group("/agents")
|
||||
{
|
||||
agents.GET("", handleGetAgents)
|
||||
agents.GET("/tree", handleGetAgentsTree)
|
||||
agents.POST("", handleCreateAgent)
|
||||
agents.GET("/:id", handleGetAgentDetail)
|
||||
agents.PUT("/:id", handleUpdateAgent)
|
||||
agents.PUT("/:id/status", handleUpdateAgentStatus)
|
||||
agents.DELETE("/:id", handleDeleteAgent)
|
||||
agents.GET("/:id/cards", handleGetAgentCards)
|
||||
agents.PUT("/:id/cards", handleUpdateAgentCards)
|
||||
}
|
||||
}
|
||||
|
||||
type AgentTreeNode struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Avatar string `json:"avatar"`
|
||||
Status string `json:"status"`
|
||||
Role string `json:"role"`
|
||||
ParentAgentID *uint `json:"parent_agent_id"`
|
||||
ParentAgentName string `json:"parent_agent_name"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
LastLoginAt string `json:"last_login_at"`
|
||||
Balance float64 `json:"balance"`
|
||||
Commission float64 `json:"commission"`
|
||||
CanCreateAgent bool `json:"can_create_agent"`
|
||||
CardsCount int `json:"cards_count"`
|
||||
ChildAgentsCount int `json:"child_agents_count"`
|
||||
Children []AgentTreeNode `json:"children,omitempty"`
|
||||
}
|
||||
|
||||
func handleGetAgents(c *gin.Context) {
|
||||
var users []model.User
|
||||
if err := database.DB.Where("role = ?", "developer").
|
||||
Preload("ParentAgent").
|
||||
Order("created_at DESC").
|
||||
Find(&users).Error; err != nil {
|
||||
response.Error(c, 500, "获取代理列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
type AgentResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Avatar string `json:"avatar"`
|
||||
Status string `json:"status"`
|
||||
Role string `json:"role"`
|
||||
ParentAgentID *uint `json:"parent_agent_id"`
|
||||
ParentAgentName string `json:"parent_agent_name"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
LastLoginAt string `json:"last_login_at"`
|
||||
Balance float64 `json:"balance"`
|
||||
Commission float64 `json:"commission"`
|
||||
CanCreateAgent bool `json:"can_create_agent"`
|
||||
CardsCount int `json:"cards_count"`
|
||||
ChildAgentsCount int `json:"child_agents_count"`
|
||||
}
|
||||
|
||||
var result []AgentResponse
|
||||
for _, user := range users {
|
||||
var parentAgentName string
|
||||
if user.ParentAgent != nil {
|
||||
parentAgentName = user.ParentAgent.Username
|
||||
}
|
||||
|
||||
var childAgentsCount int64
|
||||
database.DB.Model(&model.User{}).Where("parent_agent_id = ?", user.ID).Count(&childAgentsCount)
|
||||
|
||||
var cardsCount int64
|
||||
database.DB.Model(&model.Card{}).Where("creator_id = ?", user.ID).Count(&cardsCount)
|
||||
|
||||
var lastLoginAt string
|
||||
if user.LastLoginAt != nil {
|
||||
lastLoginAt = user.LastLoginAt.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
email := ""
|
||||
if user.Email != nil {
|
||||
email = *user.Email
|
||||
}
|
||||
|
||||
result = append(result, AgentResponse{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Email: email,
|
||||
Avatar: user.Avatar,
|
||||
Status: user.Status,
|
||||
Role: user.Role,
|
||||
ParentAgentID: user.ParentAgentID,
|
||||
ParentAgentName: parentAgentName,
|
||||
CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
LastLoginAt: lastLoginAt,
|
||||
Balance: user.Balance,
|
||||
Commission: user.Commission,
|
||||
CanCreateAgent: user.CanCreateAgent,
|
||||
CardsCount: int(cardsCount),
|
||||
ChildAgentsCount: int(childAgentsCount),
|
||||
})
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"agents": result,
|
||||
"total": len(result),
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetAgentsTree(c *gin.Context) {
|
||||
var users []model.User
|
||||
if err := database.DB.Where("role = ?", "developer").
|
||||
Preload("ParentAgent").
|
||||
Order("created_at DESC").
|
||||
Find(&users).Error; err != nil {
|
||||
response.Error(c, 500, "获取代理列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
userMap := make(map[uint]model.User)
|
||||
for _, user := range users {
|
||||
userMap[user.ID] = user
|
||||
}
|
||||
|
||||
childrenMap := make(map[uint][]uint)
|
||||
var rootUsers []uint
|
||||
for _, user := range users {
|
||||
if user.ParentAgentID != nil {
|
||||
childrenMap[*user.ParentAgentID] = append(childrenMap[*user.ParentAgentID], user.ID)
|
||||
} else {
|
||||
rootUsers = append(rootUsers, user.ID)
|
||||
}
|
||||
}
|
||||
|
||||
var buildTree func(userID uint) AgentTreeNode
|
||||
buildTree = func(userID uint) AgentTreeNode {
|
||||
user := userMap[userID]
|
||||
|
||||
var parentAgentName string
|
||||
if user.ParentAgent != nil {
|
||||
parentAgentName = user.ParentAgent.Username
|
||||
}
|
||||
|
||||
var cardsCount int64
|
||||
database.DB.Model(&model.Card{}).Where("creator_id = ?", user.ID).Count(&cardsCount)
|
||||
|
||||
var childAgentsCount int64
|
||||
database.DB.Model(&model.User{}).Where("parent_agent_id = ?", user.ID).Count(&childAgentsCount)
|
||||
|
||||
var lastLoginAt string
|
||||
if user.LastLoginAt != nil {
|
||||
lastLoginAt = user.LastLoginAt.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
email := ""
|
||||
if user.Email != nil {
|
||||
email = *user.Email
|
||||
}
|
||||
|
||||
node := AgentTreeNode{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Email: email,
|
||||
Avatar: user.Avatar,
|
||||
Status: user.Status,
|
||||
Role: user.Role,
|
||||
ParentAgentID: user.ParentAgentID,
|
||||
ParentAgentName: parentAgentName,
|
||||
CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
LastLoginAt: lastLoginAt,
|
||||
Balance: user.Balance,
|
||||
Commission: user.Commission,
|
||||
CanCreateAgent: user.CanCreateAgent,
|
||||
CardsCount: int(cardsCount),
|
||||
ChildAgentsCount: int(childAgentsCount),
|
||||
}
|
||||
|
||||
if childIDs, exists := childrenMap[userID]; exists {
|
||||
for _, childID := range childIDs {
|
||||
node.Children = append(node.Children, buildTree(childID))
|
||||
}
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
var tree []AgentTreeNode
|
||||
for _, rootID := range rootUsers {
|
||||
tree = append(tree, buildTree(rootID))
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"tree": tree,
|
||||
"total": len(users),
|
||||
})
|
||||
}
|
||||
|
||||
func handleCreateAgent(c *gin.Context) {
|
||||
var req struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
ParentAgentID *uint `json:"parent_agent_id"`
|
||||
Commission float64 `json:"commission"`
|
||||
CanCreateAgent bool `json:"can_create_agent"`
|
||||
Balance float64 `json:"balance"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var existingUser model.User
|
||||
if err := database.DB.Where("username = ?", req.Username).First(&existingUser).Error; err == nil {
|
||||
response.Error(c, 400, "用户名已存在")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Email != "" {
|
||||
if err := database.DB.Where("email = ?", req.Email).First(&existingUser).Error; err == nil {
|
||||
response.Error(c, 400, "邮箱已被使用")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if req.ParentAgentID != nil {
|
||||
var parentAgent model.User
|
||||
if err := database.DB.Where("id = ? AND role = ?", *req.ParentAgentID, "developer").First(&parentAgent).Error; err != nil {
|
||||
response.Error(c, 404, "上级代理不存在")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
response.Error(c, 500, "密码加密失败")
|
||||
return
|
||||
}
|
||||
|
||||
user := model.User{
|
||||
Username: req.Username,
|
||||
Password: string(hashedPassword),
|
||||
Role: "developer",
|
||||
Status: "active",
|
||||
ParentAgentID: req.ParentAgentID,
|
||||
Commission: req.Commission,
|
||||
CanCreateAgent: req.CanCreateAgent,
|
||||
Balance: req.Balance,
|
||||
}
|
||||
|
||||
if req.Email != "" {
|
||||
user.Email = &req.Email
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&user).Error; err != nil {
|
||||
response.Error(c, 500, "创建代理失败")
|
||||
return
|
||||
}
|
||||
|
||||
service.LogOperation(c, "create", "agent", &user.ID, fmt.Sprintf("创建代理: %s", user.Username), nil)
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetAgentDetail(c *gin.Context) {
|
||||
agentID := c.Param("id")
|
||||
|
||||
var user model.User
|
||||
if err := database.DB.Where("id = ? AND role = ?", agentID, "developer").
|
||||
Preload("ParentAgent").
|
||||
First(&user).Error; err != nil {
|
||||
response.Error(c, 404, "代理不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var agentApps []model.AgentApplication
|
||||
database.DB.Where("agent_id = ?", user.ID).
|
||||
Preload("Application").
|
||||
Find(&agentApps)
|
||||
|
||||
var childAgents []model.User
|
||||
database.DB.Where("parent_agent_id = ?", user.ID).Find(&childAgents)
|
||||
|
||||
var cards []model.Card
|
||||
database.DB.Where("creator_id = ?", user.ID).
|
||||
Preload("Application").
|
||||
Preload("CardType").
|
||||
Find(&cards)
|
||||
|
||||
type CardResponse struct {
|
||||
ID uint `json:"id"`
|
||||
CardKey string `json:"card_key"`
|
||||
ApplicationID uint `json:"application_id"`
|
||||
AppName string `json:"app_name"`
|
||||
CardTypeID uint `json:"card_type_id"`
|
||||
CardTypeName string `json:"card_type_name"`
|
||||
Price float64 `json:"price"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
var cardsResponse []CardResponse
|
||||
for _, card := range cards {
|
||||
appName := ""
|
||||
if card.Application != nil {
|
||||
appName = card.Application.Name
|
||||
}
|
||||
cardTypeName := ""
|
||||
if card.CardType.ID != 0 {
|
||||
cardTypeName = card.CardType.Name
|
||||
}
|
||||
cardsResponse = append(cardsResponse, CardResponse{
|
||||
ID: card.ID,
|
||||
CardKey: card.CardKey,
|
||||
ApplicationID: card.ApplicationID,
|
||||
AppName: appName,
|
||||
CardTypeID: card.CardTypeID,
|
||||
CardTypeName: cardTypeName,
|
||||
Price: card.CardType.Price,
|
||||
Status: card.Status,
|
||||
CreatedAt: card.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
}
|
||||
|
||||
var lastLoginAt string
|
||||
if user.LastLoginAt != nil {
|
||||
lastLoginAt = user.LastLoginAt.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
email := ""
|
||||
if user.Email != nil {
|
||||
email = *user.Email
|
||||
}
|
||||
|
||||
var parentAgentName string
|
||||
if user.ParentAgent != nil {
|
||||
parentAgentName = user.ParentAgent.Username
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"email": email,
|
||||
"avatar": user.Avatar,
|
||||
"status": user.Status,
|
||||
"role": user.Role,
|
||||
"parent_agent_id": user.ParentAgentID,
|
||||
"parent_agent_name": parentAgentName,
|
||||
"commission": user.Commission,
|
||||
"can_create_agent": user.CanCreateAgent,
|
||||
"balance": user.Balance,
|
||||
"created_at": user.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
"last_login_at": lastLoginAt,
|
||||
"cards": cardsResponse,
|
||||
"cards_count": len(cardsResponse),
|
||||
"child_agents_count": len(childAgents),
|
||||
})
|
||||
}
|
||||
|
||||
func handleUpdateAgent(c *gin.Context) {
|
||||
agentID := c.Param("id")
|
||||
|
||||
var user model.User
|
||||
if err := database.DB.Where("id = ? AND role = ?", agentID, "developer").First(&user).Error; err != nil {
|
||||
response.Error(c, 404, "代理不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
ParentAgentID *uint `json:"parent_agent_id"`
|
||||
Commission *float64 `json:"commission"`
|
||||
Email *string `json:"email"`
|
||||
Password *string `json:"password"`
|
||||
CanCreateAgent *bool `json:"can_create_agent"`
|
||||
Balance *float64 `json:"balance"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if req.ParentAgentID != nil {
|
||||
if *req.ParentAgentID == user.ID {
|
||||
response.Error(c, 400, "不能将自己设为上级代理")
|
||||
return
|
||||
}
|
||||
var parentAgent model.User
|
||||
if err := database.DB.Where("id = ? AND role = ?", *req.ParentAgentID, "developer").First(&parentAgent).Error; err != nil {
|
||||
response.Error(c, 404, "上级代理不存在")
|
||||
return
|
||||
}
|
||||
user.ParentAgentID = req.ParentAgentID
|
||||
}
|
||||
|
||||
if req.Email != nil {
|
||||
user.Email = req.Email
|
||||
}
|
||||
if req.Commission != nil {
|
||||
user.Commission = *req.Commission
|
||||
}
|
||||
if req.Password != nil && *req.Password != "" {
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(*req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
response.Error(c, 500, "密码加密失败")
|
||||
return
|
||||
}
|
||||
user.Password = string(hashedPassword)
|
||||
}
|
||||
if req.CanCreateAgent != nil {
|
||||
user.CanCreateAgent = *req.CanCreateAgent
|
||||
}
|
||||
if req.Balance != nil {
|
||||
user.Balance = *req.Balance
|
||||
}
|
||||
|
||||
if err := database.DB.Save(&user).Error; err != nil {
|
||||
response.Error(c, 500, "更新失败")
|
||||
return
|
||||
}
|
||||
|
||||
service.LogOperation(c, "update", "agent", &user.ID, fmt.Sprintf("更新代理: %s", user.Username), nil)
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"id": user.ID,
|
||||
})
|
||||
}
|
||||
|
||||
func handleUpdateAgentStatus(c *gin.Context) {
|
||||
agentID := c.Param("id")
|
||||
|
||||
var user model.User
|
||||
if err := database.DB.Where("id = ? AND role = ?", agentID, "developer").First(&user).Error; err != nil {
|
||||
response.Error(c, 404, "代理不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Status string `json:"status" binding:"required,oneof=active inactive banned"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
user.Status = req.Status
|
||||
if err := database.DB.Save(&user).Error; err != nil {
|
||||
response.Error(c, 500, "更新状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
service.LogOperation(c, "update_status", "agent", &user.ID, fmt.Sprintf("更新代理状态: %s -> %s", user.Username, user.Status), nil)
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"id": user.ID,
|
||||
"status": user.Status,
|
||||
})
|
||||
}
|
||||
|
||||
func handleDeleteAgent(c *gin.Context) {
|
||||
agentID := c.Param("id")
|
||||
|
||||
var user model.User
|
||||
if err := database.DB.Where("id = ? AND role = ?", agentID, "developer").First(&user).Error; err != nil {
|
||||
response.Error(c, 404, "代理不存在")
|
||||
return
|
||||
}
|
||||
|
||||
database.DB.Model(&model.User{}).Where("parent_agent_id = ?", user.ID).Update("parent_agent_id", nil)
|
||||
|
||||
if err := database.DB.Delete(&user).Error; err != nil {
|
||||
response.Error(c, 500, "删除失败")
|
||||
return
|
||||
}
|
||||
|
||||
service.LogOperation(c, "delete", "agent", &user.ID, fmt.Sprintf("删除代理: %s", user.Username), nil)
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"id": user.ID,
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetAgentCards(c *gin.Context) {
|
||||
agentID := c.Param("id")
|
||||
|
||||
var user model.User
|
||||
if err := database.DB.Where("id = ? AND role = ?", agentID, "developer").First(&user).Error; err != nil {
|
||||
response.Error(c, 404, "代理不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var cards []model.Card
|
||||
database.DB.Where("creator_id = ?", user.ID).
|
||||
Preload("Application").
|
||||
Preload("CardType").
|
||||
Order("created_at DESC").
|
||||
Find(&cards)
|
||||
|
||||
type CardResponse struct {
|
||||
ID uint `json:"id"`
|
||||
CardKey string `json:"card_key"`
|
||||
ApplicationID uint `json:"application_id"`
|
||||
AppName string `json:"app_name"`
|
||||
CardTypeID uint `json:"card_type_id"`
|
||||
CardTypeName string `json:"card_type_name"`
|
||||
Price float64 `json:"price"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
var result []CardResponse
|
||||
for _, card := range cards {
|
||||
appName := ""
|
||||
if card.Application != nil {
|
||||
appName = card.Application.Name
|
||||
}
|
||||
cardTypeName := ""
|
||||
if card.CardType.ID != 0 {
|
||||
cardTypeName = card.CardType.Name
|
||||
}
|
||||
result = append(result, CardResponse{
|
||||
ID: card.ID,
|
||||
CardKey: card.CardKey,
|
||||
ApplicationID: card.ApplicationID,
|
||||
AppName: appName,
|
||||
CardTypeID: card.CardTypeID,
|
||||
CardTypeName: cardTypeName,
|
||||
Price: card.CardType.Price,
|
||||
Status: card.Status,
|
||||
CreatedAt: card.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"cards": result,
|
||||
"total": len(result),
|
||||
})
|
||||
}
|
||||
|
||||
func handleUpdateAgentCards(c *gin.Context) {
|
||||
agentID := c.Param("id")
|
||||
|
||||
var user model.User
|
||||
if err := database.DB.Where("id = ? AND role = ?", agentID, "developer").First(&user).Error; err != nil {
|
||||
response.Error(c, 404, "代理不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Cards []struct {
|
||||
CardID uint `json:"card_id"`
|
||||
Status string `json:"status"`
|
||||
} `json:"cards"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
for _, cardReq := range req.Cards {
|
||||
database.DB.Model(&model.Card{}).
|
||||
Where("id = ? AND creator_id = ?", cardReq.CardID, user.ID).
|
||||
Update("status", cardReq.Status)
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "更新成功",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package developer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/service"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupAnnouncementRoutes(r *gin.RouterGroup) {
|
||||
announcements := r.Group("/announcements")
|
||||
{
|
||||
announcements.GET("", handleGetAllAnnouncements)
|
||||
announcements.GET("/:id", handleGetAnnouncementByID)
|
||||
announcements.DELETE("/batch", handleBatchDeleteAnnouncements)
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetAllAnnouncements(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
log.Printf("[DEBUG] handleGetAllAnnouncements called, userID: %d\n", userID)
|
||||
|
||||
page := c.DefaultQuery("page", "1")
|
||||
pageSize := c.DefaultQuery("page_size", "20")
|
||||
|
||||
var total int64
|
||||
|
||||
var userApps []model.Application
|
||||
if err := database.DB.Where("user_id = ?", userID).Find(&userApps).Error; err != nil {
|
||||
response.Error(c, 500, "获取应用列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Found %d user apps\n", len(userApps))
|
||||
for i, app := range userApps {
|
||||
log.Printf("[DEBUG] App %d: ID=%d, Name=%s\n", i, app.ID, app.Name)
|
||||
}
|
||||
|
||||
if len(userApps) == 0 {
|
||||
response.Success(c, gin.H{
|
||||
"announcements": []interface{}{},
|
||||
"total": 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
appIDs := make([]uint, len(userApps))
|
||||
appNameMap := make(map[uint]string)
|
||||
for i, app := range userApps {
|
||||
appIDs[i] = app.ID
|
||||
appNameMap[app.ID] = app.Name
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] appNameMap: %v\n", appNameMap)
|
||||
|
||||
database.DB.Model(&model.Announcement{}).Where("application_id IN ?", appIDs).Count(&total)
|
||||
|
||||
var announcements []model.Announcement
|
||||
offset := 0
|
||||
if pageInt, err := strconv.Atoi(page); err == nil && pageInt > 1 {
|
||||
offset = (pageInt - 1) * 20
|
||||
}
|
||||
|
||||
limit := 20
|
||||
if pageSizeInt, err := strconv.Atoi(pageSize); err == nil && pageSizeInt > 0 {
|
||||
limit = pageSizeInt
|
||||
}
|
||||
|
||||
if err := database.DB.Where("application_id IN ?", appIDs).Order("is_top DESC, created_at DESC").Limit(limit).Offset(offset).Find(&announcements).Error; err != nil {
|
||||
response.Error(c, 500, "获取公告列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Found %d announcements\n", len(announcements))
|
||||
for i, a := range announcements {
|
||||
log.Printf("[DEBUG] Announcement %d: ID=%d, ApplicationID=%d, Title=%s\n", i, a.ID, a.ApplicationID, a.Title)
|
||||
}
|
||||
|
||||
type AnnouncementWithAppName struct {
|
||||
model.Announcement
|
||||
ApplicationName string `json:"application_name"`
|
||||
}
|
||||
|
||||
result := make([]AnnouncementWithAppName, len(announcements))
|
||||
for i, a := range announcements {
|
||||
appName := appNameMap[a.ApplicationID]
|
||||
result[i] = AnnouncementWithAppName{
|
||||
Announcement: a,
|
||||
ApplicationName: appName,
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"announcements": result,
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetAnnouncementByID(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
announcementID := c.Param("id")
|
||||
|
||||
var userApps []model.Application
|
||||
if err := database.DB.Where("user_id = ?", userID).Find(&userApps).Error; err != nil {
|
||||
response.Error(c, 500, "获取应用列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
appIDs := make([]uint, len(userApps))
|
||||
appNameMap := make(map[uint]string)
|
||||
for i, app := range userApps {
|
||||
appIDs[i] = app.ID
|
||||
appNameMap[app.ID] = app.Name
|
||||
}
|
||||
|
||||
var announcement model.Announcement
|
||||
if err := database.DB.Where("id = ? AND application_id IN ?", announcementID, appIDs).First(&announcement).Error; err != nil {
|
||||
response.Error(c, 404, "公告不存在")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"id": announcement.ID,
|
||||
"application_id": announcement.ApplicationID,
|
||||
"application_name": appNameMap[announcement.ApplicationID],
|
||||
"title": announcement.Title,
|
||||
"content": announcement.Content,
|
||||
"type": announcement.Type,
|
||||
"status": announcement.Status,
|
||||
"is_top": announcement.IsTop,
|
||||
"created_at": announcement.CreatedAt,
|
||||
"updated_at": announcement.UpdatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
func handleBatchDeleteAnnouncements(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var req struct {
|
||||
IDs []uint `json:"ids"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.IDs) == 0 {
|
||||
response.Error(c, 400, "请选择要删除的公告")
|
||||
return
|
||||
}
|
||||
|
||||
var userApps []model.Application
|
||||
if err := database.DB.Where("user_id = ?", userID).Find(&userApps).Error; err != nil {
|
||||
response.Error(c, 500, "获取应用列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
appIDs := make([]uint, len(userApps))
|
||||
for i, app := range userApps {
|
||||
appIDs[i] = app.ID
|
||||
}
|
||||
|
||||
var announcements []model.Announcement
|
||||
if err := database.DB.Where("id IN ? AND application_id IN ?", req.IDs, appIDs).Find(&announcements).Error; err != nil {
|
||||
response.Error(c, 500, "获取公告失败")
|
||||
return
|
||||
}
|
||||
|
||||
for _, announcement := range announcements {
|
||||
for _, app := range userApps {
|
||||
if app.ID == announcement.ApplicationID && service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, fmt.Sprintf("应用 %s 已被禁用,无法删除其公告", app.Name))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := database.DB.Where("id IN ? AND application_id IN ?", req.IDs, appIDs).Delete(&model.Announcement{}).Error; err != nil {
|
||||
response.Error(c, 500, "批量删除公告失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, nil)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,969 @@
|
||||
package developer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/service"
|
||||
"verification-platform-backend/pkg/response"
|
||||
"verification-platform-backend/pkg/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func SetupCardRoutes(r *gin.RouterGroup) {
|
||||
cardTypes := r.Group("/card-types")
|
||||
{
|
||||
cardTypes.GET("", handleGetCardTypes)
|
||||
cardTypes.GET("/:id", handleGetCardType)
|
||||
cardTypes.POST("", handleCreateCardType)
|
||||
cardTypes.PUT("/:id", handleUpdateCardType)
|
||||
cardTypes.DELETE("/:id", handleDeleteCardType)
|
||||
}
|
||||
|
||||
cards := r.Group("/cards")
|
||||
{
|
||||
cards.GET("", handleGetCards)
|
||||
cards.POST("", handleCreateCards)
|
||||
cards.GET("/:id", handleGetCard)
|
||||
cards.PUT("/:id", handleUpdateCard)
|
||||
cards.DELETE("/:id", handleDeleteCard)
|
||||
cards.PUT("/:id/status", handleUpdateCardStatus)
|
||||
cards.PUT("/batch/status", handleBatchUpdateCardStatus)
|
||||
cards.DELETE("/batch", handleBatchDeleteCards)
|
||||
cards.POST("/use", handleUseCard)
|
||||
cards.GET("/export", handleExportCards)
|
||||
}
|
||||
}
|
||||
|
||||
func SetupCardRoutesWithoutPackage(r *gin.RouterGroup) {
|
||||
cards := r.Group("/cards")
|
||||
{
|
||||
cards.POST("/batch", handleBatchGenerateCards)
|
||||
}
|
||||
}
|
||||
|
||||
func checkDeveloperPackageValid(developerID uint) bool {
|
||||
var user model.User
|
||||
if err := database.DB.First(&user, developerID).Error; err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if user.CurrentPackageID == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var userPackage model.UserPackage
|
||||
if err := database.DB.Where("user_id = ? AND package_id = ? AND status = ?",
|
||||
developerID, user.CurrentPackageID, "active").First(&userPackage).Error; err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if userPackage.ExpiredAt != nil && userPackage.ExpiredAt.Before(time.Now()) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func handleGetCardTypes(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
applicationID := c.Query("application_id")
|
||||
|
||||
log.Printf("[DEBUG] handleGetCardTypes called: userID=%d, applicationID=%s\n", userID, applicationID)
|
||||
|
||||
var cardTypes []model.CardType
|
||||
var query *gorm.DB
|
||||
|
||||
if applicationID != "" {
|
||||
appID, err := strconv.ParseUint(applicationID, 10, 32)
|
||||
if err == nil {
|
||||
var authorizedApps []model.AgentApplication
|
||||
database.DB.Where("agent_id = ? AND application_id = ? AND status = ?", userID, uint(appID), "active").
|
||||
Preload("CardTypes").
|
||||
Find(&authorizedApps)
|
||||
|
||||
log.Printf("[DEBUG] Found %d authorized apps for user %d and application %d\n", len(authorizedApps), userID, uint(appID))
|
||||
|
||||
var authorizedCardTypeIDs []uint
|
||||
for _, aa := range authorizedApps {
|
||||
for _, ct := range aa.CardTypes {
|
||||
if ct.CanGenerate {
|
||||
authorizedCardTypeIDs = append(authorizedCardTypeIDs, ct.CardTypeID)
|
||||
log.Printf("[DEBUG] Authorized card type - ID: %d\n", ct.CardTypeID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(authorizedCardTypeIDs) > 0 {
|
||||
query = database.DB.Where("(user_id = ? AND application_id = ?) OR (id IN (?))", userID, uint(appID), authorizedCardTypeIDs)
|
||||
log.Printf("[DEBUG] Querying card types with authorizedCardTypeIDs: %v\n", authorizedCardTypeIDs)
|
||||
} else {
|
||||
query = database.DB.Where("user_id = ? AND application_id = ?", userID, uint(appID))
|
||||
}
|
||||
} else {
|
||||
query = database.DB.Where("user_id = ?", userID)
|
||||
}
|
||||
} else {
|
||||
query = database.DB.Where("user_id = ?", userID)
|
||||
}
|
||||
|
||||
if err := query.Preload("Application").Find(&cardTypes).Error; err != nil {
|
||||
log.Printf("[ERROR] Failed to query card types: %v\n", err)
|
||||
response.Error(c, 500, "获取卡密类型失败")
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Found %d card types\n", len(cardTypes))
|
||||
for i, ct := range cardTypes {
|
||||
log.Printf("[DEBUG] CardType %d: ID=%d, Name=%s, ApplicationID=%v\n", i, ct.ID, ct.Name, ct.ApplicationID)
|
||||
}
|
||||
|
||||
var cardTypesWithCount []map[string]interface{}
|
||||
for _, ct := range cardTypes {
|
||||
var count int64
|
||||
database.DB.Model(&model.Card{}).Where("card_type_id = ?", ct.ID).Count(&count)
|
||||
log.Printf("[DEBUG] CardType ID=%d, Name=%s, GeneratedCount=%d\n", ct.ID, ct.Name, count)
|
||||
|
||||
cardTypeMap := map[string]interface{}{
|
||||
"id": ct.ID,
|
||||
"user_id": ct.UserID,
|
||||
"application_id": ct.ApplicationID,
|
||||
"name": ct.Name,
|
||||
"value": ct.Value,
|
||||
"price": ct.Price,
|
||||
"description": ct.Description,
|
||||
"status": ct.Status,
|
||||
"created_at": ct.CreatedAt,
|
||||
"updated_at": ct.UpdatedAt,
|
||||
"generatedCount": count,
|
||||
}
|
||||
if ct.Application != nil {
|
||||
cardTypeMap["application"] = ct.Application
|
||||
}
|
||||
cardTypesWithCount = append(cardTypesWithCount, cardTypeMap)
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"card_types": cardTypesWithCount,
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetCardType(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
id := c.Param("id")
|
||||
|
||||
var cardType model.CardType
|
||||
if err := database.DB.Preload("Application").First(&cardType, id).Error; err != nil {
|
||||
response.Error(c, 404, "卡类不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if cardType.UserID != userID {
|
||||
var app model.Application
|
||||
if err := database.DB.First(&app, cardType.ApplicationID).Error; err != nil {
|
||||
response.Error(c, 403, "无权限查看该卡类")
|
||||
return
|
||||
}
|
||||
|
||||
if app.UserID != userID {
|
||||
var agentApp model.AgentApplication
|
||||
if err := database.DB.Where("application_id = ? AND agent_id = ? AND is_received = ?", cardType.ApplicationID, userID, true).First(&agentApp).Error; err != nil {
|
||||
response.Error(c, 403, "无权限查看该卡类")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"card_type": cardType,
|
||||
})
|
||||
}
|
||||
|
||||
func handleCreateCardType(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
RechargeType string `json:"recharge_type"`
|
||||
Value float64 `json:"value"`
|
||||
ValueUnit string `json:"value_unit"`
|
||||
Price float64 `json:"price"`
|
||||
ApplicationID uint `json:"application_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
fmt.Printf("创建卡密类型参数错误: %v\n", err)
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("创建卡密类型请求: Name=%s, RechargeType=%s, Value=%f, ValueUnit=%s, Price=%f, ApplicationID=%d\n",
|
||||
req.Name, req.RechargeType, req.Value, req.ValueUnit, req.Price, req.ApplicationID)
|
||||
|
||||
if req.Name == "" {
|
||||
response.Error(c, 400, "卡密类型名称不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
if req.ApplicationID == 0 {
|
||||
response.Error(c, 400, "所属应用不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", req.ApplicationID, userID).First(&app).Error; err != nil {
|
||||
response.Error(c, 400, "所属应用不存在或无权限")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Price < 0 {
|
||||
response.Error(c, 400, "价格不能为负数")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Value == 0 {
|
||||
response.Error(c, 400, "面值不能为0")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Value < -1 {
|
||||
response.Error(c, 400, "面值无效,必须大于0或为-1(表示永久有效)")
|
||||
return
|
||||
}
|
||||
|
||||
rechargeType := req.RechargeType
|
||||
if rechargeType == "" {
|
||||
rechargeType = "balance"
|
||||
}
|
||||
|
||||
valueUnit := req.ValueUnit
|
||||
if rechargeType == "subscription" && valueUnit == "" {
|
||||
valueUnit = "day"
|
||||
}
|
||||
|
||||
cardType := model.CardType{
|
||||
UserID: userID,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
RechargeType: rechargeType,
|
||||
Value: req.Value,
|
||||
ValueUnit: valueUnit,
|
||||
Price: req.Price,
|
||||
ApplicationID: req.ApplicationID,
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&cardType).Error; err != nil {
|
||||
response.Error(c, 500, "创建卡密类型失败")
|
||||
return
|
||||
}
|
||||
|
||||
service.LogOperation(c, "create", "card_type", &cardType.ID, fmt.Sprintf("创建卡密类型: %s", cardType.Name), nil)
|
||||
|
||||
response.Success(c, cardType)
|
||||
}
|
||||
|
||||
func handleUpdateCardType(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
RechargeType string `json:"recharge_type"`
|
||||
Value float64 `json:"value"`
|
||||
ValueUnit string `json:"value_unit"`
|
||||
Price float64 `json:"price"`
|
||||
ApplicationID uint `json:"application_id"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
id := c.Param("id")
|
||||
var cardType model.CardType
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", id, userID).First(&cardType).Error; err != nil {
|
||||
response.Error(c, 404, "卡密类型不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name != "" {
|
||||
cardType.Name = req.Name
|
||||
}
|
||||
if req.Description != "" {
|
||||
cardType.Description = req.Description
|
||||
}
|
||||
if req.RechargeType != "" {
|
||||
cardType.RechargeType = req.RechargeType
|
||||
}
|
||||
if req.ValueUnit != "" {
|
||||
cardType.ValueUnit = req.ValueUnit
|
||||
}
|
||||
if req.Price >= 0 {
|
||||
cardType.Price = req.Price
|
||||
}
|
||||
if req.Value > 0 || req.Value == -1 {
|
||||
cardType.Value = req.Value
|
||||
}
|
||||
if req.Price > 0 {
|
||||
cardType.Price = req.Price
|
||||
}
|
||||
if req.ApplicationID > 0 {
|
||||
var app model.Application
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", req.ApplicationID, userID).First(&app).Error; err != nil {
|
||||
response.Error(c, 400, "所属应用不存在或无权限")
|
||||
return
|
||||
}
|
||||
cardType.ApplicationID = req.ApplicationID
|
||||
}
|
||||
if req.Status != "" {
|
||||
cardType.Status = req.Status
|
||||
}
|
||||
|
||||
if err := database.DB.Save(&cardType).Error; err != nil {
|
||||
response.Error(c, 500, "更新卡密类型失败")
|
||||
return
|
||||
}
|
||||
|
||||
service.LogOperation(c, "update", "card_type", &cardType.ID, fmt.Sprintf("更新卡密类型: %s", cardType.Name), nil)
|
||||
|
||||
response.Success(c, cardType)
|
||||
}
|
||||
|
||||
func handleDeleteCardType(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
id := c.Param("id")
|
||||
|
||||
var cardType model.CardType
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", id, userID).First(&cardType).Error; err != nil {
|
||||
response.Error(c, 404, "卡密类型不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", id, userID).Delete(&model.CardType{}).Error; err != nil {
|
||||
response.Error(c, 500, "删除卡密类型失败")
|
||||
return
|
||||
}
|
||||
|
||||
service.LogOperation(c, "delete", "card_type", &cardType.ID, fmt.Sprintf("删除卡密类型: %s", cardType.Name), nil)
|
||||
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
func handleGetCards(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
fmt.Printf("[DEBUG] handleGetCards called, userID: %d\n", userID)
|
||||
|
||||
applicationID := c.Query("application_id")
|
||||
cardTypeID := c.Query("card_type_id")
|
||||
status := c.Query("status")
|
||||
search := c.Query("search")
|
||||
startDate := c.Query("start_date")
|
||||
endDate := c.Query("end_date")
|
||||
|
||||
var cards []model.Card
|
||||
|
||||
var authorizedAppIDs []uint
|
||||
database.DB.Model(&model.AgentApplication{}).
|
||||
Where("agent_id = ? AND status = ?", userID, "active").
|
||||
Pluck("application_id", &authorizedAppIDs)
|
||||
|
||||
fmt.Printf("[DEBUG] Authorized app IDs: %v\n", authorizedAppIDs)
|
||||
|
||||
query := database.DB.Model(&model.Card{}).
|
||||
Joins("JOIN card_types ON cards.card_type_id = card_types.id").
|
||||
Joins("JOIN applications ON card_types.application_id = applications.id").
|
||||
Preload("Application").
|
||||
Preload("CardType").
|
||||
Preload("Creator").
|
||||
Preload("AppUser")
|
||||
|
||||
if len(authorizedAppIDs) > 0 {
|
||||
query = query.Where("applications.user_id = ? OR (cards.application_id IN ? AND cards.creator_id = ?)", userID, authorizedAppIDs, userID)
|
||||
} else {
|
||||
query = query.Where("applications.user_id = ?", userID)
|
||||
}
|
||||
|
||||
if applicationID != "" {
|
||||
appID, err := strconv.ParseUint(applicationID, 10, 32)
|
||||
if err == nil {
|
||||
query = query.Where("cards.application_id = ?", uint(appID))
|
||||
}
|
||||
}
|
||||
|
||||
if cardTypeID != "" {
|
||||
ctID, err := strconv.ParseUint(cardTypeID, 10, 32)
|
||||
if err == nil {
|
||||
query = query.Where("cards.card_type_id = ?", uint(ctID))
|
||||
}
|
||||
}
|
||||
|
||||
if status != "" {
|
||||
query = query.Where("cards.status = ?", status)
|
||||
}
|
||||
|
||||
if search != "" {
|
||||
searchPattern := "%" + search + "%"
|
||||
query = query.Where("cards.card_key LIKE ? OR card_types.name LIKE ? OR applications.name LIKE ?", searchPattern, searchPattern, searchPattern)
|
||||
}
|
||||
|
||||
if startDate != "" {
|
||||
query = query.Where("cards.created_at >= ?", startDate+" 00:00:00")
|
||||
}
|
||||
|
||||
if endDate != "" {
|
||||
query = query.Where("cards.created_at <= ?", endDate+" 23:59:59")
|
||||
}
|
||||
|
||||
if err := query.Order("cards.created_at DESC").Find(&cards).Error; err != nil {
|
||||
fmt.Printf("[DEBUG] Error fetching cards: %v\n", err)
|
||||
response.Error(c, 500, "获取卡密列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("[DEBUG] Found %d cards for user %d\n", len(cards), userID)
|
||||
|
||||
response.Success(c, cards)
|
||||
}
|
||||
|
||||
func handleCreateCards(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
var req struct {
|
||||
CardTypeID uint `json:"card_type_id"`
|
||||
Count int `json:"count"`
|
||||
Prefix string `json:"prefix"`
|
||||
Length int `json:"length"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var cardType model.CardType
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", req.CardTypeID, userID).First(&cardType).Error; err != nil {
|
||||
response.Error(c, 404, "卡密类型不存在")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "卡密创建成功",
|
||||
"count": req.Count,
|
||||
})
|
||||
}
|
||||
|
||||
func handleUpdateCard(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
var req struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
id := c.Param("id")
|
||||
var card model.Card
|
||||
if err := database.DB.Joins("JOIN card_types ON cards.card_type_id = card_types.id").
|
||||
Joins("JOIN applications ON card_types.application_id = applications.id").
|
||||
Where("cards.id = ? AND applications.user_id = ?", id, userID).First(&card).Error; err != nil {
|
||||
response.Error(c, 404, "卡密不存在")
|
||||
return
|
||||
}
|
||||
|
||||
card.Status = req.Status
|
||||
|
||||
if err := database.DB.Save(&card).Error; err != nil {
|
||||
response.Error(c, 500, "更新卡密失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, card)
|
||||
}
|
||||
|
||||
func handleDeleteCard(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
id := c.Param("id")
|
||||
|
||||
fmt.Printf("删除卡密请求: ID=%s, UserID=%d\n", id, userID)
|
||||
|
||||
var card model.Card
|
||||
if err := database.DB.Preload("CardType").Preload("CardType.Application").First(&card, id).Error; err != nil {
|
||||
fmt.Printf("卡密不存在: %v\n", err)
|
||||
response.Error(c, 404, "卡密不存在")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("查询到的卡密: ID=%d, CardTypeID=%d, CreatorID=%d\n", card.ID, card.CardTypeID, card.CreatorID)
|
||||
|
||||
// 如果创建者是当前用户,直接允许删除
|
||||
if card.CreatorID == userID {
|
||||
fmt.Printf("创建者匹配,允许删除\n")
|
||||
if err := database.DB.Delete(&card).Error; err != nil {
|
||||
fmt.Printf("删除卡密失败: %v\n", err)
|
||||
response.Error(c, 500, "删除卡密失败")
|
||||
return
|
||||
}
|
||||
fmt.Printf("删除卡密成功: ID=%s\n", id)
|
||||
response.Success(c, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 如果创建者不匹配,检查应用的所有者
|
||||
if card.CardType.ID > 0 && card.CardType.Application != nil {
|
||||
fmt.Printf("卡密类型和应用存在,检查应用所有者\n")
|
||||
if card.CardType.Application.UserID == userID {
|
||||
fmt.Printf("应用所有者匹配,允许删除\n")
|
||||
if err := database.DB.Delete(&card).Error; err != nil {
|
||||
fmt.Printf("删除卡密失败: %v\n", err)
|
||||
response.Error(c, 500, "删除卡密失败")
|
||||
return
|
||||
}
|
||||
fmt.Printf("删除卡密成功: ID=%s\n", id)
|
||||
response.Success(c, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 如果卡密类型或应用不存在,尝试通过ApplicationID查询应用(包括软删除的)
|
||||
if card.CardType.ID == 0 || card.CardType.Application == nil {
|
||||
fmt.Printf("卡密类型或应用不存在,尝试查询应用\n")
|
||||
if card.CardType.ID == 0 {
|
||||
// 卡密类型不存在,直接通过CardTypeID查询卡密类型(包括软删除的)
|
||||
var cardType model.CardType
|
||||
if err := database.DB.Unscoped().First(&cardType, card.CardTypeID).Error; err != nil {
|
||||
fmt.Printf("卡密类型不存在: %v\n", err)
|
||||
response.Error(c, 404, "卡密类型不存在")
|
||||
return
|
||||
}
|
||||
card.CardType = cardType
|
||||
}
|
||||
|
||||
if card.CardType.Application == nil {
|
||||
// 应用不存在,直接通过ApplicationID查询应用(包括软删除的)
|
||||
var app model.Application
|
||||
if err := database.DB.Unscoped().First(&app, card.CardType.ApplicationID).Error; err != nil {
|
||||
fmt.Printf("应用不存在: %v\n", err)
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
card.CardType.Application = &app
|
||||
}
|
||||
|
||||
fmt.Printf("查询到的应用: ID=%d, UserID=%d\n", card.CardType.Application.ID, card.CardType.Application.UserID)
|
||||
if card.CardType.Application.UserID == userID {
|
||||
fmt.Printf("应用所有者匹配,允许删除\n")
|
||||
if err := database.DB.Delete(&card).Error; err != nil {
|
||||
fmt.Printf("删除卡密失败: %v\n", err)
|
||||
response.Error(c, 500, "删除卡密失败")
|
||||
return
|
||||
}
|
||||
fmt.Printf("删除卡密成功: ID=%s\n", id)
|
||||
response.Success(c, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("无权限删除此卡密\n")
|
||||
response.Error(c, 403, "无权限删除此卡密")
|
||||
}
|
||||
|
||||
func handleGetCard(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
id := c.Param("id")
|
||||
var card model.Card
|
||||
if err := database.DB.Joins("JOIN card_types ON cards.card_type_id = card_types.id").
|
||||
Joins("JOIN applications ON card_types.application_id = applications.id").
|
||||
Where("cards.id = ? AND applications.user_id = ?", id, userID).First(&card).Error; err != nil {
|
||||
response.Error(c, 404, "卡密不存在")
|
||||
return
|
||||
}
|
||||
response.Success(c, card)
|
||||
}
|
||||
|
||||
func handleUseCard(c *gin.Context) {
|
||||
var req struct {
|
||||
CardKey string `json:"card_key"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var card model.Card
|
||||
if err := database.DB.Where("card_key = ?", req.CardKey).First(&card).Error; err != nil {
|
||||
response.Error(c, 404, "卡密不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if card.Status != "unused" {
|
||||
response.Error(c, 400, "卡密已被使用或已禁用")
|
||||
return
|
||||
}
|
||||
|
||||
card.Status = "used"
|
||||
database.DB.Save(&card)
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "卡密使用成功",
|
||||
})
|
||||
}
|
||||
|
||||
func handleBatchGenerateCards(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
fmt.Printf("[DEBUG] handleBatchGenerateCards called, userID: %d\n", userID)
|
||||
|
||||
var req struct {
|
||||
ApplicationID uint `json:"application_id"`
|
||||
CardTypeID uint `json:"card_type_id"`
|
||||
Count int `json:"count"`
|
||||
Prefix string `json:"prefix"`
|
||||
Length int `json:"length"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
fmt.Printf("[DEBUG] JSON bind error: %v\n", err)
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("[DEBUG] Received parameters - ApplicationID: %v, CardTypeID: %d, Count: %d, Prefix: %s, Length: %d\n",
|
||||
req.ApplicationID, req.CardTypeID, req.Count, req.Prefix, req.Length)
|
||||
|
||||
if req.Count <= 0 || req.Count > 1000 {
|
||||
response.Error(c, 400, "生成数量需在1-1000之间")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Length <= 0 {
|
||||
req.Length = 16
|
||||
}
|
||||
if req.Prefix == "" {
|
||||
req.Prefix = "CK"
|
||||
}
|
||||
|
||||
var cardType model.CardType
|
||||
if err := database.DB.Where("id = ?", req.CardTypeID).First(&cardType).Error; err != nil {
|
||||
fmt.Printf("[DEBUG] CardType not found error: %v\n", err)
|
||||
response.Error(c, 404, "卡密类型不存在")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("[DEBUG] CardType found - ID: %d, Name: %s, UserID: %d\n", cardType.ID, cardType.Name, cardType.UserID)
|
||||
|
||||
if req.ApplicationID == 0 {
|
||||
req.ApplicationID = cardType.ApplicationID
|
||||
}
|
||||
|
||||
var agentApp *model.AgentApplication
|
||||
var effectiveUserID uint = userID
|
||||
|
||||
if cardType.UserID != userID {
|
||||
fmt.Printf("[DEBUG] CardType belongs to another user, checking authorization...\n")
|
||||
|
||||
if req.ApplicationID == 0 {
|
||||
fmt.Printf("[DEBUG] ApplicationID is required for authorized card types\n")
|
||||
response.Error(c, 400, "生成授权应用的卡密需要指定应用ID")
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DB.Where("agent_id = ? AND application_id = ? AND status = ?", userID, req.ApplicationID, "active").
|
||||
Preload("CardTypes", "card_type_id = ?", req.CardTypeID).
|
||||
First(&agentApp).Error; err != nil {
|
||||
fmt.Printf("[DEBUG] Authorization not found error: %v\n", err)
|
||||
response.Error(c, 403, "您没有权限生成此卡密类型")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("[DEBUG] AgentApp found - ID: %d, DeveloperID: %d\n", agentApp.ID, agentApp.DeveloperID)
|
||||
|
||||
var cardTypePerm *model.AgentApplicationCardType
|
||||
for _, ct := range agentApp.CardTypes {
|
||||
if ct.CardTypeID == req.CardTypeID {
|
||||
cardTypePerm = &ct
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if cardTypePerm == nil || !cardTypePerm.CanGenerate {
|
||||
fmt.Printf("[DEBUG] No permission to generate this card type\n")
|
||||
response.Error(c, 403, "您没有权限生成此卡密类型")
|
||||
return
|
||||
}
|
||||
|
||||
var agentUser model.User
|
||||
if err := database.DB.First(&agentUser, userID).Error; err != nil {
|
||||
response.Error(c, 500, "获取代理信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
totalCost := float64(req.Count) * cardType.Price
|
||||
fmt.Printf("[DEBUG] Total cost: %f, Balance: %f\n", totalCost, agentUser.Balance)
|
||||
|
||||
if agentUser.Balance < totalCost {
|
||||
fmt.Printf("[DEBUG] Insufficient balance\n")
|
||||
response.Error(c, 400, "余额不足")
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DB.Model(&agentUser).Update("balance", agentUser.Balance-totalCost).Error; err != nil {
|
||||
fmt.Printf("[DEBUG] Update balance error: %v\n", err)
|
||||
response.Error(c, 500, "扣款失败")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("[DEBUG] Balance updated successfully\n")
|
||||
}
|
||||
|
||||
cards := make([]model.Card, 0, req.Count)
|
||||
for i := 0; i < req.Count; i++ {
|
||||
cardKey := req.Prefix + utils.GenerateRandomString(req.Length)
|
||||
card := model.Card{
|
||||
ApplicationID: req.ApplicationID,
|
||||
CardTypeID: req.CardTypeID,
|
||||
CardKey: cardKey,
|
||||
CreatorID: effectiveUserID,
|
||||
Status: "unused",
|
||||
}
|
||||
cards = append(cards, card)
|
||||
}
|
||||
|
||||
fmt.Printf("[DEBUG] Generated %d cards, saving to database...\n", len(cards))
|
||||
|
||||
if err := database.DB.Create(&cards).Error; err != nil {
|
||||
fmt.Printf("[DEBUG] Database create error: %v\n", err)
|
||||
response.Error(c, 500, "生成卡密失败")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("[DEBUG] Successfully created %d cards\n", len(cards))
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "批量生成成功",
|
||||
"count": req.Count,
|
||||
})
|
||||
}
|
||||
|
||||
func handleExportCards(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
applicationID := c.Query("application_id")
|
||||
cardTypeID := c.Query("card_type_id")
|
||||
status := c.Query("status")
|
||||
search := c.Query("search")
|
||||
startDate := c.Query("start_date")
|
||||
endDate := c.Query("end_date")
|
||||
|
||||
var cards []model.Card
|
||||
query := database.DB.Model(&model.Card{}).
|
||||
Joins("JOIN card_types ON cards.card_type_id = card_types.id").
|
||||
Joins("JOIN applications ON card_types.application_id = applications.id").
|
||||
Preload("Application").
|
||||
Preload("CardType").
|
||||
Where("applications.user_id = ?", userID)
|
||||
|
||||
if applicationID != "" {
|
||||
appID, err := strconv.ParseUint(applicationID, 10, 32)
|
||||
if err == nil {
|
||||
query = query.Where("cards.application_id = ?", uint(appID))
|
||||
}
|
||||
}
|
||||
|
||||
if cardTypeID != "" {
|
||||
ctID, err := strconv.ParseUint(cardTypeID, 10, 32)
|
||||
if err == nil {
|
||||
query = query.Where("cards.card_type_id = ?", uint(ctID))
|
||||
}
|
||||
}
|
||||
|
||||
if status != "" {
|
||||
query = query.Where("cards.status = ?", status)
|
||||
}
|
||||
|
||||
if search != "" {
|
||||
searchPattern := "%" + search + "%"
|
||||
query = query.Where("cards.card_key LIKE ? OR card_types.name LIKE ? OR applications.name LIKE ?", searchPattern, searchPattern, searchPattern)
|
||||
}
|
||||
|
||||
if startDate != "" {
|
||||
query = query.Where("cards.created_at >= ?", startDate+" 00:00:00")
|
||||
}
|
||||
|
||||
if endDate != "" {
|
||||
query = query.Where("cards.created_at <= ?", endDate+" 23:59:59")
|
||||
}
|
||||
|
||||
if err := query.Order("cards.created_at DESC").Find(&cards).Error; err != nil {
|
||||
response.Error(c, 500, "导出卡密失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.Header("Content-Type", "text/csv; charset=utf-8")
|
||||
c.Header("Content-Disposition", "attachment; filename=cards_export.csv")
|
||||
|
||||
csv := "卡密,应用,卡类,状态,创建时间,使用时间\n"
|
||||
for _, card := range cards {
|
||||
statusMap := map[string]string{
|
||||
"unused": "未使用",
|
||||
"used": "已使用",
|
||||
"banned": "已禁用",
|
||||
}
|
||||
statusText := statusMap[card.Status]
|
||||
if statusText == "" {
|
||||
statusText = card.Status
|
||||
}
|
||||
|
||||
appName := ""
|
||||
if card.Application != nil {
|
||||
appName = card.Application.Name
|
||||
}
|
||||
|
||||
cardTypeName := card.CardType.Name
|
||||
|
||||
usedAt := ""
|
||||
if card.UsedAt != nil {
|
||||
usedAt = card.UsedAt.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
csv += fmt.Sprintf("%s,%s,%s,%s,%s,%s\n",
|
||||
card.CardKey,
|
||||
appName,
|
||||
cardTypeName,
|
||||
statusText,
|
||||
card.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
usedAt,
|
||||
)
|
||||
}
|
||||
|
||||
c.String(200, csv)
|
||||
}
|
||||
|
||||
func handleUpdateCardStatus(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
id := c.Param("id")
|
||||
|
||||
var req struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Status != "unused" && req.Status != "used" && req.Status != "banned" {
|
||||
response.Error(c, 400, "无效的状态")
|
||||
return
|
||||
}
|
||||
|
||||
var card model.Card
|
||||
if err := database.DB.Preload("CardType").Preload("CardType.Application").First(&card, id).Error; err != nil {
|
||||
response.Error(c, 404, "卡密不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if card.CreatorID != userID && (card.CardType.Application == nil || card.CardType.Application.UserID != userID) {
|
||||
response.Error(c, 403, "无权限修改此卡密")
|
||||
return
|
||||
}
|
||||
|
||||
card.Status = req.Status
|
||||
if err := database.DB.Save(&card).Error; err != nil {
|
||||
response.Error(c, 500, "更新状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, card)
|
||||
}
|
||||
|
||||
func handleBatchUpdateCardStatus(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var req struct {
|
||||
IDs []uint `json:"ids"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Status != "unused" && req.Status != "used" && req.Status != "banned" {
|
||||
response.Error(c, 400, "无效的状态")
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.IDs) == 0 {
|
||||
response.Error(c, 400, "请选择要更新的卡密")
|
||||
return
|
||||
}
|
||||
|
||||
var cards []model.Card
|
||||
if err := database.DB.Preload("CardType").Preload("CardType.Application").Where("id IN ?", req.IDs).Find(&cards).Error; err != nil {
|
||||
response.Error(c, 500, "查询卡密失败")
|
||||
return
|
||||
}
|
||||
|
||||
var validIDs []uint
|
||||
for _, card := range cards {
|
||||
if card.CreatorID == userID || (card.CardType.Application != nil && card.CardType.Application.UserID == userID) {
|
||||
validIDs = append(validIDs, card.ID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(validIDs) == 0 {
|
||||
response.Error(c, 403, "无权限修改选中的卡密")
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DB.Model(&model.Card{}).Where("id IN ?", validIDs).Update("status", req.Status).Error; err != nil {
|
||||
response.Error(c, 500, "批量更新状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"updated_count": len(validIDs)})
|
||||
}
|
||||
|
||||
func handleBatchDeleteCards(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var req struct {
|
||||
IDs []uint `json:"ids"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.IDs) == 0 {
|
||||
response.Error(c, 400, "请选择要删除的卡密")
|
||||
return
|
||||
}
|
||||
|
||||
var cards []model.Card
|
||||
if err := database.DB.Preload("CardType").Preload("CardType.Application").Where("id IN ?", req.IDs).Find(&cards).Error; err != nil {
|
||||
response.Error(c, 500, "查询卡密失败")
|
||||
return
|
||||
}
|
||||
|
||||
var validIDs []uint
|
||||
for _, card := range cards {
|
||||
if card.CreatorID == userID || (card.CardType.Application != nil && card.CardType.Application.UserID == userID) {
|
||||
validIDs = append(validIDs, card.ID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(validIDs) == 0 {
|
||||
response.Error(c, 403, "无权限删除选中的卡密")
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DB.Where("id IN ?", validIDs).Delete(&model.Card{}).Error; err != nil {
|
||||
response.Error(c, 500, "批量删除失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"deleted_count": len(validIDs)})
|
||||
}
|
||||
@@ -0,0 +1,827 @@
|
||||
package developer
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/middleware"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/service"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupCloudRoutes(r *gin.RouterGroup) {
|
||||
cloudConstants := r.Group("/cloud-constants")
|
||||
{
|
||||
cloudConstants.GET("", handleGetCloudConstants)
|
||||
cloudConstants.GET("/:id", handleGetCloudConstant)
|
||||
cloudConstants.GET("/:id/download", handleDownloadCloudConstant)
|
||||
cloudConstants.POST("", handleCreateCloudConstant)
|
||||
cloudConstants.POST("/upload", handleUploadCloudConstant)
|
||||
cloudConstants.PUT("/:id", handleUpdateCloudConstant)
|
||||
cloudConstants.DELETE("/:id", handleDeleteCloudConstant)
|
||||
}
|
||||
|
||||
cloudVariables := r.Group("/cloud-variables")
|
||||
{
|
||||
cloudVariables.GET("", handleGetCloudVariables)
|
||||
cloudVariables.GET("/:id", handleGetCloudVariable)
|
||||
cloudVariables.GET("/:id/download", handleDownloadCloudVariable)
|
||||
cloudVariables.POST("", handleCreateCloudVariable)
|
||||
cloudVariables.POST("/upload", handleUploadCloudVariable)
|
||||
cloudVariables.PUT("/:id", handleUpdateCloudVariable)
|
||||
cloudVariables.DELETE("/:id", handleDeleteCloudVariable)
|
||||
cloudVariables.GET("/:id/records", handleGetCloudVariableRecords)
|
||||
cloudVariables.DELETE("/:id/records", handleDeleteCloudVariableRecords)
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetCloudConstants(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
appID := c.Query("app_id")
|
||||
|
||||
var constants []model.CloudConstant
|
||||
query := database.DB.Where("user_id = ?", userID)
|
||||
if appID != "" {
|
||||
query = query.Where("app_id = ?", appID)
|
||||
}
|
||||
if err := query.Find(&constants).Error; err != nil {
|
||||
response.Error(c, 500, "获取云端常量失败")
|
||||
return
|
||||
}
|
||||
response.Success(c, gin.H{
|
||||
"constants": constants,
|
||||
"total": len(constants),
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetCloudConstant(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
id := c.Param("id")
|
||||
|
||||
var constant model.CloudConstant
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", id, userID).First(&constant).Error; err != nil {
|
||||
response.Error(c, 404, "云端常量不存在")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, constant)
|
||||
}
|
||||
|
||||
func handleCreateCloudConstant(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var req struct {
|
||||
AppID uint `json:"app_id"`
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
VarType string `json:"var_type"`
|
||||
Description string `json:"description"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Key == "" {
|
||||
response.Error(c, 400, "变量名不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Status != "active" && req.Status != "inactive" {
|
||||
req.Status = "active"
|
||||
}
|
||||
|
||||
if req.VarType == "" {
|
||||
req.VarType = "string"
|
||||
}
|
||||
|
||||
constant := model.CloudConstant{
|
||||
UserID: userID,
|
||||
AppID: &req.AppID,
|
||||
Key: req.Key,
|
||||
Value: req.Value,
|
||||
VarType: req.VarType,
|
||||
Description: req.Description,
|
||||
Status: req.Status,
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&constant).Error; err != nil {
|
||||
response.Error(c, 500, "创建云端常量失败")
|
||||
return
|
||||
}
|
||||
|
||||
service.LogOperation(c, "create", "cloud_constant", &constant.ID, fmt.Sprintf("创建云端常量: %s", constant.Key), nil)
|
||||
|
||||
response.Success(c, constant)
|
||||
}
|
||||
|
||||
func handleUpdateCloudConstant(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var req struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
VarType string `json:"var_type"`
|
||||
Description string `json:"description"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
id := c.Param("id")
|
||||
var constant model.CloudConstant
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", id, userID).First(&constant).Error; err != nil {
|
||||
response.Error(c, 404, "云端常量不存在")
|
||||
return
|
||||
}
|
||||
|
||||
constant.Key = req.Key
|
||||
constant.Value = req.Value
|
||||
constant.VarType = req.VarType
|
||||
constant.Description = req.Description
|
||||
if req.Status == "active" || req.Status == "inactive" {
|
||||
constant.Status = req.Status
|
||||
}
|
||||
|
||||
if err := database.DB.Save(&constant).Error; err != nil {
|
||||
response.Error(c, 500, "更新云端常量失败")
|
||||
return
|
||||
}
|
||||
|
||||
service.LogOperation(c, "update", "cloud_constant", &constant.ID, fmt.Sprintf("更新云端常量: %s", constant.Key), nil)
|
||||
|
||||
response.Success(c, constant)
|
||||
}
|
||||
|
||||
func handleDeleteCloudConstant(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
id := c.Param("id")
|
||||
|
||||
var constant model.CloudConstant
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", id, userID).First(&constant).Error; err != nil {
|
||||
response.Error(c, 404, "云端常量不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if constant.VarType == "binary" && constant.FilePath != "" {
|
||||
if err := middleware.UpdateStorageUsed(userID, constant.FileSize, "delete"); err != nil {
|
||||
fmt.Printf("更新存储使用量失败: %v\n", err)
|
||||
}
|
||||
filePath := strings.TrimPrefix(constant.FilePath, "/")
|
||||
if _, err := os.Stat(filePath); err == nil {
|
||||
os.Remove(filePath)
|
||||
}
|
||||
}
|
||||
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", id, userID).Delete(&model.CloudConstant{}).Error; err != nil {
|
||||
response.Error(c, 500, "删除云端常量失败")
|
||||
return
|
||||
}
|
||||
|
||||
service.LogOperation(c, "delete", "cloud_constant", &constant.ID, fmt.Sprintf("删除云端常量: %s", constant.Key), nil)
|
||||
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
func handleUploadCloudConstant(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
file, header, err := c.Request.FormFile("file")
|
||||
if err != nil {
|
||||
response.Error(c, 400, "请选择要上传的文件")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
appIDStr := c.PostForm("app_id")
|
||||
key := c.PostForm("key")
|
||||
description := c.PostForm("description")
|
||||
status := c.PostForm("status")
|
||||
|
||||
if appIDStr == "" {
|
||||
response.Error(c, 400, "请选择应用")
|
||||
return
|
||||
}
|
||||
|
||||
if key == "" {
|
||||
response.Error(c, 400, "请输入变量名")
|
||||
return
|
||||
}
|
||||
|
||||
var appID uint
|
||||
fmt.Sscanf(appIDStr, "%d", &appID)
|
||||
|
||||
if status != "active" && status != "inactive" {
|
||||
status = "active"
|
||||
}
|
||||
|
||||
var user model.User
|
||||
if err := database.DB.First(&user, userID).Error; err != nil {
|
||||
response.Error(c, 500, "获取用户信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
if user.CurrentPackageID != nil {
|
||||
var permission model.PackagePermission
|
||||
if err := database.DB.Where("package_id = ?", user.CurrentPackageID).First(&permission).Error; err == nil {
|
||||
maxStorageBytes := int64(permission.MaxStorage) * 1024 * 1024
|
||||
if user.StorageUsed+header.Size > maxStorageBytes {
|
||||
usedMB := float64(user.StorageUsed) / 1024 / 1024
|
||||
maxMB := float64(permission.MaxStorage)
|
||||
response.Error(c, 403, fmt.Sprintf("存储空间不足,已使用 %.2f MB / %.2f MB", usedMB, maxMB))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uploadDir := "uploads/cloud-files"
|
||||
if err := os.MkdirAll(uploadDir, 0755); err != nil {
|
||||
response.Error(c, 500, "创建上传目录失败")
|
||||
return
|
||||
}
|
||||
|
||||
ext := filepath.Ext(header.Filename)
|
||||
filename := fmt.Sprintf("%d_%d%s", userID, time.Now().UnixNano(), ext)
|
||||
filePath := filepath.Join(uploadDir, filename)
|
||||
|
||||
dst, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
response.Error(c, 500, "创建文件失败")
|
||||
return
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
hash := md5.New()
|
||||
multiWriter := io.MultiWriter(dst, hash)
|
||||
if _, err := io.Copy(multiWriter, file); err != nil {
|
||||
response.Error(c, 500, "保存文件失败")
|
||||
return
|
||||
}
|
||||
fileMD5 := hex.EncodeToString(hash.Sum(nil))
|
||||
|
||||
fileURL := "/uploads/cloud-files/" + filename
|
||||
|
||||
mimeType := header.Header.Get("Content-Type")
|
||||
if mimeType == "" {
|
||||
mimeType = "application/octet-stream"
|
||||
}
|
||||
|
||||
constant := model.CloudConstant{
|
||||
UserID: userID,
|
||||
AppID: &appID,
|
||||
Key: key,
|
||||
Value: fileURL,
|
||||
VarType: "binary",
|
||||
FilePath: fileURL,
|
||||
FileSize: header.Size,
|
||||
MimeType: mimeType,
|
||||
OriginalName: header.Filename,
|
||||
FileMD5: fileMD5,
|
||||
Description: description,
|
||||
Status: status,
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&constant).Error; err != nil {
|
||||
os.Remove(filePath)
|
||||
response.Error(c, 500, "创建云端常量失败")
|
||||
return
|
||||
}
|
||||
|
||||
if err := middleware.UpdateStorageUsed(userID, header.Size, "upload"); err != nil {
|
||||
fmt.Printf("更新存储使用量失败: %v\n", err)
|
||||
}
|
||||
|
||||
usage := model.StorageUsage{
|
||||
UserID: userID,
|
||||
ApplicationID: &appID,
|
||||
ResourceType: "cloud_constant",
|
||||
ResourceID: constant.ID,
|
||||
FileName: header.Filename,
|
||||
FileSize: header.Size,
|
||||
Action: "upload",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
database.DB.Create(&usage)
|
||||
|
||||
service.LogOperation(c, "create", "cloud_constant", &constant.ID, fmt.Sprintf("上传文件常量: %s", constant.Key), nil)
|
||||
|
||||
response.Success(c, constant)
|
||||
}
|
||||
|
||||
func handleDownloadCloudConstant(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
id := c.Param("id")
|
||||
|
||||
var constant model.CloudConstant
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", id, userID).First(&constant).Error; err != nil {
|
||||
response.Error(c, 404, "云端常量不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if constant.VarType != "binary" || constant.FilePath == "" {
|
||||
response.Error(c, 400, "该常量不是文件类型")
|
||||
return
|
||||
}
|
||||
|
||||
filePath := constant.FilePath
|
||||
if strings.HasPrefix(filePath, "/") {
|
||||
filePath = filePath[1:]
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
response.Error(c, 404, "文件不存在")
|
||||
return
|
||||
}
|
||||
|
||||
c.Header("Content-Description", "File Transfer")
|
||||
c.Header("Content-Type", "application/octet-stream")
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", constant.OriginalName))
|
||||
c.Header("Content-Transfer-Encoding", "binary")
|
||||
c.Header("Expires", "0")
|
||||
c.Header("Cache-Control", "must-revalidate")
|
||||
c.Header("Pragma", "public")
|
||||
c.FileAttachment(filePath, constant.OriginalName)
|
||||
}
|
||||
|
||||
func handleGetCloudVariables(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
appID := c.Query("app_id")
|
||||
|
||||
var variables []model.CloudVariable
|
||||
query := database.DB.Where("user_id = ?", userID)
|
||||
if appID != "" {
|
||||
query = query.Where("app_id = ?", appID)
|
||||
}
|
||||
if err := query.Find(&variables).Error; err != nil {
|
||||
response.Error(c, 500, "获取云端变量失败")
|
||||
return
|
||||
}
|
||||
response.Success(c, gin.H{
|
||||
"variables": variables,
|
||||
"total": len(variables),
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetCloudVariable(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
id := c.Param("id")
|
||||
|
||||
var variable model.CloudVariable
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", id, userID).First(&variable).Error; err != nil {
|
||||
response.Error(c, 404, "云端变量不存在")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, variable)
|
||||
}
|
||||
|
||||
func handleCreateCloudVariable(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var req struct {
|
||||
AppID uint `json:"app_id"`
|
||||
Key string `json:"key"`
|
||||
DefaultValue string `json:"default_value"`
|
||||
VarType string `json:"var_type"`
|
||||
DataType string `json:"data_type"`
|
||||
MaxRecords int `json:"max_records"`
|
||||
Scope string `json:"scope"`
|
||||
WritePermission string `json:"write_permission"`
|
||||
Description string `json:"description"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Key == "" {
|
||||
response.Error(c, 400, "变量名不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Scope != "app" && req.Scope != "user" {
|
||||
req.Scope = "app"
|
||||
}
|
||||
|
||||
if req.WritePermission != "developer" && req.WritePermission != "user" && req.WritePermission != "app_user" {
|
||||
req.WritePermission = "developer"
|
||||
}
|
||||
|
||||
if req.Status != "active" && req.Status != "inactive" {
|
||||
req.Status = "active"
|
||||
}
|
||||
|
||||
if req.VarType == "" {
|
||||
req.VarType = "string"
|
||||
}
|
||||
|
||||
if req.DataType != "single" && req.DataType != "stream" {
|
||||
req.DataType = "single"
|
||||
}
|
||||
|
||||
variable := model.CloudVariable{
|
||||
UserID: userID,
|
||||
AppID: &req.AppID,
|
||||
Key: req.Key,
|
||||
DefaultValue: req.DefaultValue,
|
||||
VarType: req.VarType,
|
||||
DataType: req.DataType,
|
||||
MaxRecords: req.MaxRecords,
|
||||
Scope: req.Scope,
|
||||
WritePermission: req.WritePermission,
|
||||
Description: req.Description,
|
||||
Status: req.Status,
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&variable).Error; err != nil {
|
||||
response.Error(c, 500, "创建云端变量失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, variable)
|
||||
}
|
||||
|
||||
func handleUpdateCloudVariable(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var req struct {
|
||||
Key string `json:"key"`
|
||||
DefaultValue string `json:"default_value"`
|
||||
VarType string `json:"var_type"`
|
||||
DataType string `json:"data_type"`
|
||||
MaxRecords int `json:"max_records"`
|
||||
WritePermission string `json:"write_permission"`
|
||||
Description string `json:"description"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
id := c.Param("id")
|
||||
var variable model.CloudVariable
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", id, userID).First(&variable).Error; err != nil {
|
||||
response.Error(c, 404, "云端变量不存在")
|
||||
return
|
||||
}
|
||||
|
||||
variable.Key = req.Key
|
||||
variable.DefaultValue = req.DefaultValue
|
||||
variable.VarType = req.VarType
|
||||
if req.DataType == "single" || req.DataType == "stream" {
|
||||
variable.DataType = req.DataType
|
||||
}
|
||||
variable.MaxRecords = req.MaxRecords
|
||||
if req.WritePermission == "developer" || req.WritePermission == "user" || req.WritePermission == "app_user" {
|
||||
variable.WritePermission = req.WritePermission
|
||||
}
|
||||
variable.Description = req.Description
|
||||
if req.Status == "active" || req.Status == "inactive" {
|
||||
variable.Status = req.Status
|
||||
}
|
||||
|
||||
if err := database.DB.Save(&variable).Error; err != nil {
|
||||
response.Error(c, 500, "更新云端变量失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, variable)
|
||||
}
|
||||
|
||||
func handleDeleteCloudVariable(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
id := c.Param("id")
|
||||
|
||||
var variable model.CloudVariable
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", id, userID).First(&variable).Error; err != nil {
|
||||
response.Error(c, 404, "云端变量不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if variable.VarType == "binary" && variable.FilePath != "" {
|
||||
if err := middleware.UpdateStorageUsed(userID, variable.FileSize, "delete"); err != nil {
|
||||
fmt.Printf("更新存储使用量失败: %v\n", err)
|
||||
}
|
||||
filePath := strings.TrimPrefix(variable.FilePath, "/")
|
||||
if _, err := os.Stat(filePath); err == nil {
|
||||
os.Remove(filePath)
|
||||
}
|
||||
}
|
||||
|
||||
if err := database.DB.Delete(&variable).Error; err != nil {
|
||||
response.Error(c, 500, "删除云端变量失败")
|
||||
return
|
||||
}
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
func handleUploadCloudVariable(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
file, header, err := c.Request.FormFile("file")
|
||||
if err != nil {
|
||||
response.Error(c, 400, "请选择要上传的文件")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
appIDStr := c.PostForm("app_id")
|
||||
key := c.PostForm("key")
|
||||
description := c.PostForm("description")
|
||||
status := c.PostForm("status")
|
||||
scope := c.PostForm("scope")
|
||||
writePermission := c.PostForm("write_permission")
|
||||
|
||||
if appIDStr == "" {
|
||||
response.Error(c, 400, "请选择应用")
|
||||
return
|
||||
}
|
||||
|
||||
if key == "" {
|
||||
response.Error(c, 400, "请输入变量名")
|
||||
return
|
||||
}
|
||||
|
||||
var appID uint
|
||||
fmt.Sscanf(appIDStr, "%d", &appID)
|
||||
|
||||
if status != "active" && status != "inactive" {
|
||||
status = "active"
|
||||
}
|
||||
|
||||
if scope != "app" && scope != "user" {
|
||||
scope = "app"
|
||||
}
|
||||
|
||||
if writePermission != "developer" && writePermission != "user" {
|
||||
writePermission = "developer"
|
||||
}
|
||||
|
||||
var user model.User
|
||||
if err := database.DB.First(&user, userID).Error; err != nil {
|
||||
response.Error(c, 500, "获取用户信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
if user.CurrentPackageID != nil {
|
||||
var permission model.PackagePermission
|
||||
if err := database.DB.Where("package_id = ?", user.CurrentPackageID).First(&permission).Error; err == nil {
|
||||
maxStorageBytes := int64(permission.MaxStorage) * 1024 * 1024
|
||||
if user.StorageUsed+header.Size > maxStorageBytes {
|
||||
usedMB := float64(user.StorageUsed) / 1024 / 1024
|
||||
maxMB := float64(permission.MaxStorage)
|
||||
response.Error(c, 403, fmt.Sprintf("存储空间不足,已使用 %.2f MB / %.2f MB", usedMB, maxMB))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uploadDir := "uploads/cloud-files"
|
||||
if err := os.MkdirAll(uploadDir, 0755); err != nil {
|
||||
response.Error(c, 500, "创建上传目录失败")
|
||||
return
|
||||
}
|
||||
|
||||
ext := filepath.Ext(header.Filename)
|
||||
filename := fmt.Sprintf("%d_%d%s", userID, time.Now().UnixNano(), ext)
|
||||
filePath := filepath.Join(uploadDir, filename)
|
||||
|
||||
dst, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
response.Error(c, 500, "创建文件失败")
|
||||
return
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
hash := md5.New()
|
||||
multiWriter := io.MultiWriter(dst, hash)
|
||||
if _, err := io.Copy(multiWriter, file); err != nil {
|
||||
response.Error(c, 500, "保存文件失败")
|
||||
return
|
||||
}
|
||||
fileMD5 := hex.EncodeToString(hash.Sum(nil))
|
||||
|
||||
fileURL := "/uploads/cloud-files/" + filename
|
||||
|
||||
mimeType := header.Header.Get("Content-Type")
|
||||
if mimeType == "" {
|
||||
mimeType = "application/octet-stream"
|
||||
}
|
||||
|
||||
variable := model.CloudVariable{
|
||||
UserID: userID,
|
||||
AppID: &appID,
|
||||
Key: key,
|
||||
DefaultValue: fileURL,
|
||||
VarType: "binary",
|
||||
FilePath: fileURL,
|
||||
FileSize: header.Size,
|
||||
MimeType: mimeType,
|
||||
OriginalName: header.Filename,
|
||||
FileMD5: fileMD5,
|
||||
Scope: scope,
|
||||
WritePermission: writePermission,
|
||||
Description: description,
|
||||
Status: status,
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&variable).Error; err != nil {
|
||||
os.Remove(filePath)
|
||||
response.Error(c, 500, "创建云端变量失败")
|
||||
return
|
||||
}
|
||||
|
||||
if err := middleware.UpdateStorageUsed(userID, header.Size, "upload"); err != nil {
|
||||
fmt.Printf("更新存储使用量失败: %v\n", err)
|
||||
}
|
||||
|
||||
usage := model.StorageUsage{
|
||||
UserID: userID,
|
||||
ApplicationID: &appID,
|
||||
ResourceType: "cloud_variable",
|
||||
ResourceID: variable.ID,
|
||||
FileName: header.Filename,
|
||||
FileSize: header.Size,
|
||||
Action: "upload",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
database.DB.Create(&usage)
|
||||
|
||||
service.LogOperation(c, "create", "cloud_variable", &variable.ID, fmt.Sprintf("上传文件变量: %s", variable.Key), nil)
|
||||
|
||||
response.Success(c, variable)
|
||||
}
|
||||
|
||||
func handleDownloadCloudVariable(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
id := c.Param("id")
|
||||
|
||||
var variable model.CloudVariable
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", id, userID).First(&variable).Error; err != nil {
|
||||
response.Error(c, 404, "云端变量不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if variable.VarType != "binary" || variable.FilePath == "" {
|
||||
response.Error(c, 400, "该变量不是文件类型")
|
||||
return
|
||||
}
|
||||
|
||||
filePath := variable.FilePath
|
||||
if strings.HasPrefix(filePath, "/") {
|
||||
filePath = filePath[1:]
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
response.Error(c, 404, "文件不存在")
|
||||
return
|
||||
}
|
||||
|
||||
c.Header("Content-Description", "File Transfer")
|
||||
c.Header("Content-Type", "application/octet-stream")
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", variable.OriginalName))
|
||||
c.Header("Content-Transfer-Encoding", "binary")
|
||||
c.Header("Expires", "0")
|
||||
c.Header("Cache-Control", "must-revalidate")
|
||||
c.Header("Pragma", "public")
|
||||
c.FileAttachment(filePath, variable.OriginalName)
|
||||
}
|
||||
|
||||
func handleGetCloudVariableRecords(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
id := c.Param("id")
|
||||
|
||||
var variable model.CloudVariable
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", id, userID).First(&variable).Error; err != nil {
|
||||
response.Error(c, 404, "云端变量不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if variable.DataType != "stream" {
|
||||
response.Error(c, 400, "该变量不是流水类型")
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
userIDFilter := c.Query("user_id")
|
||||
startDate := c.Query("start_date")
|
||||
endDate := c.Query("end_date")
|
||||
|
||||
var total int64
|
||||
query := database.DB.Model(&model.CloudVariableRecord{}).Where("cloud_variable_id = ?", variable.ID)
|
||||
if userIDFilter != "" {
|
||||
query = query.Where("app_user_id = ?", userIDFilter)
|
||||
}
|
||||
if startDate != "" {
|
||||
query = query.Where("created_at >= ?", startDate+" 00:00:00")
|
||||
}
|
||||
if endDate != "" {
|
||||
query = query.Where("created_at <= ?", endDate+" 23:59:59")
|
||||
}
|
||||
query.Count(&total)
|
||||
|
||||
var records []model.CloudVariableRecord
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Order("created_at DESC").Limit(pageSize).Offset(offset).Find(&records).Error; err != nil {
|
||||
response.Error(c, 500, "获取记录失败")
|
||||
return
|
||||
}
|
||||
|
||||
result := make([]gin.H, len(records))
|
||||
for i, r := range records {
|
||||
var data map[string]interface{}
|
||||
json.Unmarshal([]byte(r.Data), &data)
|
||||
record := gin.H{
|
||||
"id": r.ID,
|
||||
"data": data,
|
||||
"created_at": r.CreatedAt,
|
||||
}
|
||||
if r.AppUserID != nil {
|
||||
record["user_id"] = r.AppUserID
|
||||
var appUser model.AppUser
|
||||
if err := database.DB.Select("id, username").First(&appUser, *r.AppUserID).Error; err == nil {
|
||||
record["user"] = gin.H{
|
||||
"id": appUser.ID,
|
||||
"username": appUser.Username,
|
||||
}
|
||||
}
|
||||
}
|
||||
result[i] = record
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"records": result,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
"total_pages": (total + int64(pageSize) - 1) / int64(pageSize),
|
||||
})
|
||||
}
|
||||
|
||||
func handleDeleteCloudVariableRecords(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
id := c.Param("id")
|
||||
|
||||
var variable model.CloudVariable
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", id, userID).First(&variable).Error; err != nil {
|
||||
response.Error(c, 404, "云端变量不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if variable.DataType != "stream" {
|
||||
response.Error(c, 400, "该变量不是流水类型")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
IDs []uint `json:"ids"`
|
||||
Before string `json:"before"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.IDs) > 0 {
|
||||
if err := database.DB.Where("cloud_variable_id = ? AND id IN ?", variable.ID, req.IDs).Delete(&model.CloudVariableRecord{}).Error; err != nil {
|
||||
response.Error(c, 500, "删除记录失败")
|
||||
return
|
||||
}
|
||||
} else if req.Before != "" {
|
||||
if err := database.DB.Where("cloud_variable_id = ? AND created_at < ?", variable.ID, req.Before).Delete(&model.CloudVariableRecord{}).Error; err != nil {
|
||||
response.Error(c, 500, "删除记录失败")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
response.Error(c, 400, "请指定要删除的记录")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, nil)
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package developer
|
||||
|
||||
import (
|
||||
"time"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupDashboardRoutes(r *gin.RouterGroup) {
|
||||
r.GET("/dashboard", handleGetDashboard)
|
||||
}
|
||||
|
||||
func handleGetDashboard(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var stats struct {
|
||||
TotalApplications int64 `json:"totalApplications"`
|
||||
TotalUsers int64 `json:"totalUsers"`
|
||||
TotalCards int64 `json:"totalCards"`
|
||||
MonthlyRevenue float64 `json:"monthlyRevenue"`
|
||||
}
|
||||
|
||||
database.DB.Model(&model.Application{}).Where("user_id = ?", userID).Count(&stats.TotalApplications)
|
||||
|
||||
var appIDs []uint
|
||||
database.DB.Model(&model.Application{}).Where("user_id = ?", userID).Pluck("id", &appIDs)
|
||||
if len(appIDs) > 0 {
|
||||
database.DB.Model(&model.AppUser{}).Where("application_id IN ?", appIDs).Count(&stats.TotalUsers)
|
||||
}
|
||||
|
||||
database.DB.Model(&model.Card{}).Where("creator_id = ?", userID).Count(&stats.TotalCards)
|
||||
|
||||
var monthlyRevenue float64
|
||||
if len(appIDs) > 0 {
|
||||
var appUserIDs []uint
|
||||
database.DB.Model(&model.AppUser{}).Where("application_id IN ?", appIDs).Pluck("id", &appUserIDs)
|
||||
if len(appUserIDs) > 0 {
|
||||
database.DB.Model(&model.RechargeRecord{}).
|
||||
Where("user_id IN ? AND status = ? AND created_at >= ?", appUserIDs, "success", time.Now().AddDate(0, -1, 0)).
|
||||
Select("COALESCE(SUM(amount), 0)").
|
||||
Scan(&monthlyRevenue)
|
||||
}
|
||||
}
|
||||
stats.MonthlyRevenue = monthlyRevenue
|
||||
|
||||
var subscription struct {
|
||||
Plan string `json:"plan"`
|
||||
Status string `json:"status"`
|
||||
ExpireDate string `json:"expireDate"`
|
||||
APIQuota int `json:"apiQuota"`
|
||||
APIUsed int `json:"apiUsed"`
|
||||
AppCount int `json:"appCount"`
|
||||
CanCreateAgent bool `json:"canCreateAgent"`
|
||||
StorageQuota int64 `json:"storageQuota"`
|
||||
StorageUsed int64 `json:"storageUsed"`
|
||||
}
|
||||
|
||||
var user model.User
|
||||
if err := database.DB.First(&user, userID).Error; err == nil {
|
||||
subscription.Plan = "基础版"
|
||||
if user.Role == "admin" {
|
||||
subscription.Plan = "管理员"
|
||||
}
|
||||
subscription.Status = "正常"
|
||||
if user.Status == "banned" {
|
||||
subscription.Status = "已禁用"
|
||||
}
|
||||
subscription.ExpireDate = "永久"
|
||||
}
|
||||
subscription.AppCount = int(stats.TotalApplications)
|
||||
subscription.APIQuota = 10000
|
||||
subscription.StorageQuota = 100 * 1024 * 1024
|
||||
|
||||
var apiUsed int64
|
||||
database.DB.Model(&model.ApiUsage{}).Where("user_id = ?", userID).Count(&apiUsed)
|
||||
subscription.APIUsed = int(apiUsed)
|
||||
|
||||
var storageUsed int64
|
||||
database.DB.Model(&model.StorageUsage{}).Where("user_id = ?", userID).Select("COALESCE(SUM(size), 0)").Scan(&storageUsed)
|
||||
subscription.StorageUsed = storageUsed
|
||||
|
||||
userDistribution := gin.H{
|
||||
"provinces": []gin.H{},
|
||||
"overseas": []gin.H{},
|
||||
}
|
||||
|
||||
if len(appIDs) > 0 {
|
||||
type DeviceCount struct {
|
||||
DeviceType string
|
||||
Count int
|
||||
}
|
||||
var deviceCounts []DeviceCount
|
||||
database.DB.Model(&model.UserDevice{}).
|
||||
Select("device_type, COUNT(*) as count").
|
||||
Where("application_id IN ?", appIDs).
|
||||
Group("device_type").
|
||||
Order("count DESC").
|
||||
Find(&deviceCounts)
|
||||
|
||||
provinces := make([]gin.H, 0, len(deviceCounts))
|
||||
for _, dc := range deviceCounts {
|
||||
provinces = append(provinces, gin.H{
|
||||
"name": dc.DeviceType,
|
||||
"count": dc.Count,
|
||||
})
|
||||
}
|
||||
userDistribution["provinces"] = provinces
|
||||
}
|
||||
|
||||
onlineTrend := make([]gin.H, 0, 30)
|
||||
for i := 29; i >= 0; i-- {
|
||||
date := time.Now().AddDate(0, 0, -i)
|
||||
dateStart := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
|
||||
dateEnd := dateStart.Add(24 * time.Hour)
|
||||
|
||||
var count int64
|
||||
if len(appIDs) > 0 {
|
||||
database.DB.Model(&model.DeviceSession{}).
|
||||
Joins("JOIN user_devices ON device_sessions.device_id = user_devices.id").
|
||||
Where("user_devices.application_id IN ? AND device_sessions.last_heartbeat >= ? AND device_sessions.last_heartbeat < ?", appIDs, dateStart, dateEnd).
|
||||
Count(&count)
|
||||
}
|
||||
|
||||
onlineTrend = append(onlineTrend, gin.H{
|
||||
"date": date.Format("01-02"),
|
||||
"value": count,
|
||||
})
|
||||
}
|
||||
|
||||
recentActivities := make([]gin.H, 0, 10)
|
||||
var logs []model.Log
|
||||
database.DB.Where("user_id = ?", userID).Order("created_at DESC").Limit(10).Find(&logs)
|
||||
for _, log := range logs {
|
||||
recentActivities = append(recentActivities, gin.H{
|
||||
"id": log.ID,
|
||||
"action": log.Action,
|
||||
"details": log.Details,
|
||||
"resource": log.Resource,
|
||||
"log_type": log.LogType,
|
||||
"status": log.Status,
|
||||
"created_at": log.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
recentTickets := make([]gin.H, 0, 5)
|
||||
var tickets []model.Ticket
|
||||
database.DB.Where("user_id = ?", userID).Order("created_at DESC").Limit(5).Find(&tickets)
|
||||
for _, ticket := range tickets {
|
||||
recentTickets = append(recentTickets, gin.H{
|
||||
"id": ticket.ID,
|
||||
"title": ticket.Title,
|
||||
"status": ticket.Status,
|
||||
"priority": ticket.Priority,
|
||||
"created_at": ticket.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"stats": stats,
|
||||
"subscription": subscription,
|
||||
"userDistribution": userDistribution,
|
||||
"onlineTrend": onlineTrend,
|
||||
"recentActivities": recentActivities,
|
||||
"recentTickets": recentTickets,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package developer
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupRoutes(r *gin.RouterGroup) {
|
||||
SetupDashboardRoutes(r)
|
||||
SetupApplicationRoutes(r)
|
||||
SetupCardRoutes(r)
|
||||
SetupUserRoutes(r)
|
||||
SetupDeviceRoutes(r)
|
||||
SetupFinanceRoutes(r)
|
||||
SetupLogRoutes(r)
|
||||
SetupTicketRoutes(r)
|
||||
SetupAgentAppRoutes(r)
|
||||
SetupAgentsRoutes(r)
|
||||
SetupCloudRoutes(r)
|
||||
SetupDynamicRoutes(r)
|
||||
SetupExtensionRoutes(r)
|
||||
SetupOrderRoutes(r)
|
||||
SetupUsageRoutes(r)
|
||||
SetupAnnouncementRoutes(r)
|
||||
SetupVersionRoutes(r)
|
||||
SetupProfileRoutes(r)
|
||||
SetupEmailRoutes(r)
|
||||
}
|
||||
|
||||
func SetupRoutesWithoutPackage(r *gin.RouterGroup) {
|
||||
SetupAgentAppRoutesWithoutPackage(r)
|
||||
SetupCardRoutesWithoutPackage(r)
|
||||
}
|
||||
|
||||
func SetupExtensionRoutes(r *gin.RouterGroup) {
|
||||
extension := r.Group("/extension")
|
||||
{
|
||||
// Webhook配置
|
||||
extension.GET("/webhooks", handleGetWebhooks)
|
||||
extension.POST("/webhooks", handleCreateWebhook)
|
||||
extension.PUT("/webhooks/:id", handleUpdateWebhook)
|
||||
extension.PUT("/webhooks/:id/status", handleUpdateWebhookStatus)
|
||||
extension.DELETE("/webhooks/:id", handleDeleteWebhook)
|
||||
extension.PUT("/webhooks/batch/status", handleBatchUpdateWebhookStatus)
|
||||
extension.DELETE("/webhooks/batch", handleBatchDeleteWebhooks)
|
||||
extension.GET("/webhooks/logs", handleGetWebhookLogs)
|
||||
extension.POST("/webhooks/:id/test", handleTestWebhook)
|
||||
|
||||
// API密钥
|
||||
extension.GET("/api-keys", handleGetAPIKeys)
|
||||
extension.POST("/api-keys", handleCreateAPIKey)
|
||||
extension.PUT("/api-keys/:id", handleUpdateAPIKey)
|
||||
extension.PUT("/api-keys/:id/status", handleUpdateAPIKeyStatus)
|
||||
extension.DELETE("/api-keys/:id", handleDeleteAPIKey)
|
||||
extension.PUT("/api-keys/batch/status", handleBatchUpdateAPIKeyStatus)
|
||||
extension.DELETE("/api-keys/batch", handleBatchDeleteAPIKeys)
|
||||
extension.POST("/api-keys/:id/regenerate", handleRegenerateAPIKey)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,506 @@
|
||||
package developer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/service"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type DeviceWithDetails struct {
|
||||
model.UserDevice
|
||||
OnlineSessions int `json:"online_sessions"`
|
||||
}
|
||||
|
||||
func SetupDeviceRoutes(r *gin.RouterGroup) {
|
||||
devices := r.Group("/devices")
|
||||
{
|
||||
devices.GET("", handleGetDevices)
|
||||
devices.PUT("/:id/status", handleUpdateDeviceStatus)
|
||||
devices.DELETE("/:id", handleUnbindDevice)
|
||||
devices.DELETE("/batch", handleBatchUnbindDevices)
|
||||
devices.POST("/batch/status", handleBatchUpdateDeviceStatus)
|
||||
devices.POST("/:id/force-offline", handleForceOfflineDevice)
|
||||
}
|
||||
|
||||
sessions := r.Group("/sessions")
|
||||
{
|
||||
sessions.GET("", handleGetSessions)
|
||||
sessions.DELETE("/:id", handleDeleteSession)
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetDevices(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
log.Printf("[DEBUG] handleGetDevices called, userID: %d", userID)
|
||||
|
||||
userIDFilter := c.Query("user_id")
|
||||
deviceIDFilter := c.Query("device_id")
|
||||
|
||||
var devices []model.UserDevice
|
||||
var appHeartbeatTimeoutMap map[uint]int
|
||||
|
||||
var ownApps []model.Application
|
||||
if err := database.DB.Where("user_id = ?", userID).Find(&ownApps).Error; err != nil {
|
||||
response.Error(c, 500, "获取应用列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
var agentApps []model.AgentApplication
|
||||
if err := database.DB.Where("agent_id = ? AND is_received = ?", userID, true).Find(&agentApps).Error; err != nil {
|
||||
log.Printf("[DEBUG] Failed to get agent apps: %v", err)
|
||||
}
|
||||
|
||||
appHeartbeatTimeoutMap = make(map[uint]int)
|
||||
var appIDs []uint
|
||||
|
||||
for _, app := range ownApps {
|
||||
timeout := app.HeartbeatTimeout
|
||||
if timeout == 0 {
|
||||
timeout = 300
|
||||
}
|
||||
appHeartbeatTimeoutMap[app.ID] = timeout
|
||||
appIDs = append(appIDs, app.ID)
|
||||
}
|
||||
|
||||
for _, agentApp := range agentApps {
|
||||
var app model.Application
|
||||
if err := database.DB.First(&app, agentApp.ApplicationID).Error; err != nil {
|
||||
continue
|
||||
}
|
||||
timeout := app.HeartbeatTimeout
|
||||
if timeout == 0 {
|
||||
timeout = 300
|
||||
}
|
||||
appHeartbeatTimeoutMap[app.ID] = timeout
|
||||
appIDs = append(appIDs, app.ID)
|
||||
}
|
||||
|
||||
if len(appIDs) == 0 {
|
||||
response.Success(c, gin.H{"devices": []DeviceWithDetails{}})
|
||||
return
|
||||
}
|
||||
|
||||
query := database.DB.Preload("User").Preload("Application").Where("application_id IN ?", appIDs)
|
||||
|
||||
if userIDFilter != "" {
|
||||
query = query.Where("user_id = ?", userIDFilter)
|
||||
}
|
||||
|
||||
if deviceIDFilter != "" {
|
||||
query = query.Where("device_id = ?", deviceIDFilter)
|
||||
}
|
||||
|
||||
if err := query.Find(&devices).Error; err != nil {
|
||||
response.Error(c, 500, "获取设备列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
devicesWithDetails := make([]DeviceWithDetails, 0, len(devices))
|
||||
for _, device := range devices {
|
||||
heartbeatTimeout := appHeartbeatTimeoutMap[device.ApplicationID]
|
||||
timeoutThreshold := time.Now().Add(-time.Duration(heartbeatTimeout) * time.Second)
|
||||
|
||||
var onlineSessionCount int64
|
||||
database.DB.Model(&model.DeviceSession{}).
|
||||
Where("device_id = ? AND last_heartbeat > ?", device.ID, timeoutThreshold).
|
||||
Count(&onlineSessionCount)
|
||||
|
||||
devicesWithDetails = append(devicesWithDetails, DeviceWithDetails{
|
||||
UserDevice: device,
|
||||
OnlineSessions: int(onlineSessionCount),
|
||||
})
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"devices": devicesWithDetails})
|
||||
}
|
||||
|
||||
func handleUpdateDeviceStatus(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
deviceID := c.Param("id")
|
||||
|
||||
var req struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var device model.UserDevice
|
||||
if err := database.DB.First(&device, deviceID).Error; err != nil {
|
||||
response.Error(c, 404, "设备不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.First(&app, device.ApplicationID).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if app.UserID != userID {
|
||||
var agentApp model.AgentApplication
|
||||
if err := database.DB.Where("application_id = ? AND agent_id = ? AND is_received = ?", app.ID, userID, true).First(&agentApp).Error; err != nil {
|
||||
response.Error(c, 403, "无权限修改该设备")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
device.Status = req.Status
|
||||
if err := database.DB.Save(&device).Error; err != nil {
|
||||
response.Error(c, 500, "更新设备状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, device)
|
||||
}
|
||||
|
||||
func handleUnbindDevice(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
deviceID := c.Param("id")
|
||||
|
||||
var device model.UserDevice
|
||||
if err := database.DB.First(&device, deviceID).Error; err != nil {
|
||||
response.Error(c, 404, "设备不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.First(&app, device.ApplicationID).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if app.UserID != userID {
|
||||
var agentApp model.AgentApplication
|
||||
if err := database.DB.Where("application_id = ? AND agent_id = ? AND is_received = ?", app.ID, userID, true).First(&agentApp).Error; err != nil {
|
||||
response.Error(c, 403, "无权限解绑该设备")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
database.DB.Where("device_id = ?", device.ID).Delete(&model.DeviceSession{})
|
||||
|
||||
if err := database.DB.Delete(&device).Error; err != nil {
|
||||
response.Error(c, 500, "解绑设备失败")
|
||||
return
|
||||
}
|
||||
|
||||
service.LogOperation(c, "unbind", "device", &device.ID, fmt.Sprintf("解绑设备: %s (应用: %s)", device.DeviceID, app.Name), nil)
|
||||
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
func handleBatchUnbindDevices(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
var req struct {
|
||||
DeviceIDs []uint `json:"device_ids"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var devices []model.UserDevice
|
||||
if err := database.DB.Where("id IN ?", req.DeviceIDs).Find(&devices).Error; err != nil {
|
||||
response.Error(c, 500, "获取设备列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
var validDeviceIDs []uint
|
||||
for _, device := range devices {
|
||||
var app model.Application
|
||||
if err := database.DB.First(&app, device.ApplicationID).Error; err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if app.UserID == userID {
|
||||
validDeviceIDs = append(validDeviceIDs, device.ID)
|
||||
} else {
|
||||
var agentApp model.AgentApplication
|
||||
if err := database.DB.Where("application_id = ? AND agent_id = ? AND is_received = ?", app.ID, userID, true).First(&agentApp).Error; err == nil {
|
||||
validDeviceIDs = append(validDeviceIDs, device.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(validDeviceIDs) > 0 {
|
||||
database.DB.Where("device_id IN ?", validDeviceIDs).Delete(&model.DeviceSession{})
|
||||
if err := database.DB.Where("id IN ?", validDeviceIDs).Delete(&model.UserDevice{}).Error; err != nil {
|
||||
response.Error(c, 500, "批量解绑失败")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
service.LogOperation(c, "batch_unbind", "device", nil, fmt.Sprintf("批量解绑设备: %d个", len(validDeviceIDs)), nil)
|
||||
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
func handleBatchUpdateDeviceStatus(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
var req struct {
|
||||
DeviceIDs []uint `json:"device_ids"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var devices []model.UserDevice
|
||||
if err := database.DB.Where("id IN ?", req.DeviceIDs).Find(&devices).Error; err != nil {
|
||||
response.Error(c, 500, "获取设备列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
var validDeviceIDs []uint
|
||||
for _, device := range devices {
|
||||
var app model.Application
|
||||
if err := database.DB.First(&app, device.ApplicationID).Error; err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if app.UserID == userID {
|
||||
validDeviceIDs = append(validDeviceIDs, device.ID)
|
||||
} else {
|
||||
var agentApp model.AgentApplication
|
||||
if err := database.DB.Where("application_id = ? AND agent_id = ? AND is_received = ?", app.ID, userID, true).First(&agentApp).Error; err == nil {
|
||||
validDeviceIDs = append(validDeviceIDs, device.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(validDeviceIDs) > 0 {
|
||||
if err := database.DB.Model(&model.UserDevice{}).Where("id IN ?", validDeviceIDs).Update("status", req.Status).Error; err != nil {
|
||||
response.Error(c, 500, "批量更新状态失败")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
type SessionWithDetails struct {
|
||||
model.DeviceSession
|
||||
DeviceID string `json:"device_identifier"`
|
||||
DeviceName string `json:"device_name"`
|
||||
Username string `json:"username"`
|
||||
AppName string `json:"app_name"`
|
||||
IsOnline bool `json:"is_online"`
|
||||
}
|
||||
|
||||
func handleGetSessions(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
deviceIDFilter := c.Query("device_id")
|
||||
appIDFilter := c.Query("app_id")
|
||||
|
||||
var ownApps []model.Application
|
||||
if err := database.DB.Where("user_id = ?", userID).Find(&ownApps).Error; err != nil {
|
||||
response.Error(c, 500, "获取应用列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
var agentApps []model.AgentApplication
|
||||
if err := database.DB.Where("agent_id = ? AND is_received = ?", userID, true).Find(&agentApps).Error; err != nil {
|
||||
log.Printf("[DEBUG] Failed to get agent apps: %v", err)
|
||||
}
|
||||
|
||||
appHeartbeatTimeoutMap := make(map[uint]int)
|
||||
var appIDs []uint
|
||||
|
||||
for _, app := range ownApps {
|
||||
timeout := app.HeartbeatTimeout
|
||||
if timeout == 0 {
|
||||
timeout = 300
|
||||
}
|
||||
appHeartbeatTimeoutMap[app.ID] = timeout
|
||||
appIDs = append(appIDs, app.ID)
|
||||
}
|
||||
|
||||
for _, agentApp := range agentApps {
|
||||
var app model.Application
|
||||
if err := database.DB.First(&app, agentApp.ApplicationID).Error; err != nil {
|
||||
continue
|
||||
}
|
||||
timeout := app.HeartbeatTimeout
|
||||
if timeout == 0 {
|
||||
timeout = 300
|
||||
}
|
||||
appHeartbeatTimeoutMap[app.ID] = timeout
|
||||
appIDs = append(appIDs, app.ID)
|
||||
}
|
||||
|
||||
if len(appIDs) == 0 {
|
||||
response.Success(c, gin.H{"sessions": []SessionWithDetails{}})
|
||||
return
|
||||
}
|
||||
|
||||
query := database.DB.Model(&model.DeviceSession{}).Where("application_id IN ?", appIDs)
|
||||
|
||||
if deviceIDFilter != "" {
|
||||
query = query.Where("device_id = ?", deviceIDFilter)
|
||||
}
|
||||
|
||||
if appIDFilter != "" {
|
||||
query = query.Where("application_id = ?", appIDFilter)
|
||||
}
|
||||
|
||||
var sessions []model.DeviceSession
|
||||
if err := query.Find(&sessions).Error; err != nil {
|
||||
response.Error(c, 500, "获取会话列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
sessionsWithDetails := make([]SessionWithDetails, 0, len(sessions))
|
||||
for _, session := range sessions {
|
||||
var device model.UserDevice
|
||||
if err := database.DB.First(&device, session.DeviceID).Error; err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.First(&user, session.UserID).Error; err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.First(&app, session.ApplicationID).Error; err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
heartbeatTimeout := appHeartbeatTimeoutMap[session.ApplicationID]
|
||||
timeoutThreshold := time.Now().Add(-time.Duration(heartbeatTimeout) * time.Second)
|
||||
isOnline := session.LastHeartbeat != nil && session.LastHeartbeat.After(timeoutThreshold)
|
||||
|
||||
sessionsWithDetails = append(sessionsWithDetails, SessionWithDetails{
|
||||
DeviceSession: session,
|
||||
DeviceID: device.DeviceID,
|
||||
DeviceName: device.DeviceName,
|
||||
Username: user.Username,
|
||||
AppName: app.Name,
|
||||
IsOnline: isOnline,
|
||||
})
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"sessions": sessionsWithDetails})
|
||||
}
|
||||
|
||||
func handleDeleteSession(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
sessionID := c.Param("id")
|
||||
|
||||
if sessionID == "" {
|
||||
response.Error(c, 400, "会话ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
var session model.DeviceSession
|
||||
if err := database.DB.First(&session, sessionID).Error; err != nil {
|
||||
response.Error(c, 404, "会话不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var ownApps []model.Application
|
||||
database.DB.Where("user_id = ?", userID).Find(&ownApps)
|
||||
ownAppIDs := make([]uint, len(ownApps))
|
||||
for i, app := range ownApps {
|
||||
ownAppIDs[i] = app.ID
|
||||
}
|
||||
|
||||
var agentApps []model.AgentApplication
|
||||
database.DB.Where("agent_id = ? AND is_received = ?", userID, true).Find(&agentApps)
|
||||
agentAppIDs := make([]uint, 0)
|
||||
for _, agentApp := range agentApps {
|
||||
agentAppIDs = append(agentAppIDs, agentApp.ApplicationID)
|
||||
}
|
||||
|
||||
validAppIDs := append(ownAppIDs, agentAppIDs...)
|
||||
isValid := false
|
||||
for _, appID := range validAppIDs {
|
||||
if session.ApplicationID == appID {
|
||||
isValid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isValid {
|
||||
response.Error(c, 403, "无权操作此会话")
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DB.Delete(&session).Error; err != nil {
|
||||
response.Error(c, 500, "删除会话失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
func handleForceOfflineDevice(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
deviceID := c.Param("id")
|
||||
|
||||
if deviceID == "" {
|
||||
response.Error(c, 400, "设备ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
var device model.UserDevice
|
||||
if err := database.DB.First(&device, deviceID).Error; err != nil {
|
||||
response.Error(c, 404, "设备不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var ownApps []model.Application
|
||||
database.DB.Where("user_id = ?", userID).Find(&ownApps)
|
||||
ownAppIDs := make([]uint, len(ownApps))
|
||||
for i, app := range ownApps {
|
||||
ownAppIDs[i] = app.ID
|
||||
}
|
||||
|
||||
var agentApps []model.AgentApplication
|
||||
database.DB.Where("agent_id = ? AND is_received = ?", userID, true).Find(&agentApps)
|
||||
agentAppIDs := make([]uint, 0)
|
||||
for _, agentApp := range agentApps {
|
||||
agentAppIDs = append(agentAppIDs, agentApp.ApplicationID)
|
||||
}
|
||||
|
||||
validAppIDs := append(ownAppIDs, agentAppIDs...)
|
||||
isValid := false
|
||||
for _, appID := range validAppIDs {
|
||||
if device.ApplicationID == appID {
|
||||
isValid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isValid {
|
||||
response.Error(c, 403, "无权操作此设备")
|
||||
return
|
||||
}
|
||||
|
||||
var sessions []model.DeviceSession
|
||||
if err := database.DB.Where("device_id = ?", device.ID).Find(&sessions).Error; err != nil {
|
||||
response.Error(c, 500, "获取会话列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
if len(sessions) == 0 {
|
||||
response.Success(c, gin.H{"count": 0})
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DB.Where("device_id = ?", device.ID).Delete(&model.DeviceSession{}).Error; err != nil {
|
||||
response.Error(c, 500, "强制离线失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"count": len(sessions)})
|
||||
}
|
||||
@@ -0,0 +1,805 @@
|
||||
package developer
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func normalizeKey(name string) string {
|
||||
key := strings.ToLower(strings.TrimSpace(name))
|
||||
key = strings.Map(func(r rune) rune {
|
||||
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '-' {
|
||||
return r
|
||||
}
|
||||
return '_'
|
||||
}, key)
|
||||
return key
|
||||
}
|
||||
|
||||
func validateDynamicCode(code string) error {
|
||||
vm := goja.New()
|
||||
vm.Set("params", vm.NewObject())
|
||||
vm.Set("user", vm.NewObject())
|
||||
vm.Set("app", vm.NewObject())
|
||||
wrappedCode := "(function() { " + code + " })()"
|
||||
_, err := vm.RunString(wrappedCode)
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
if strings.Contains(errStr, "ReferenceError") || strings.Contains(errStr, "is not defined") {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetupDynamicRoutes(r *gin.RouterGroup) {
|
||||
dynamicCodes := r.Group("/dynamic-codes")
|
||||
{
|
||||
dynamicCodes.GET("", handleListDynamicCodes)
|
||||
dynamicCodes.GET("/:id", handleGetDynamicCode)
|
||||
dynamicCodes.POST("", handleCreateDynamicCode)
|
||||
dynamicCodes.PUT("/:id", handleUpdateDynamicCode)
|
||||
dynamicCodes.DELETE("/:id", handleDeleteDynamicCode)
|
||||
dynamicCodes.PUT("/:id/status", handleUpdateDynamicCodeStatus)
|
||||
dynamicCodes.DELETE("/batch", handleBatchDeleteDynamicCodes)
|
||||
dynamicCodes.PUT("/batch/status", handleBatchUpdateDynamicCodeStatus)
|
||||
}
|
||||
|
||||
riskControl := r.Group("/risk-control")
|
||||
{
|
||||
riskControl.GET("/rules", handleGetRiskControlRules)
|
||||
riskControl.POST("/rules", handleCreateRiskControlRule)
|
||||
riskControl.PUT("/rules/:id", handleUpdateRiskControlRule)
|
||||
riskControl.DELETE("/rules/:id", handleDeleteRiskControlRule)
|
||||
riskControl.PUT("/rules/:id/status", handleUpdateRiskControlRuleStatus)
|
||||
riskControl.DELETE("/rules/batch", handleBatchDeleteRiskControlRules)
|
||||
riskControl.PUT("/rules/batch/status", handleBatchUpdateRiskControlRuleStatus)
|
||||
}
|
||||
}
|
||||
|
||||
func handleListDynamicCodes(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var applications []model.Application
|
||||
if err := database.DB.Where("user_id = ?", userID).Find(&applications).Error; err != nil {
|
||||
response.Error(c, 500, "获取应用列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
var applicationIDs []uint
|
||||
for _, app := range applications {
|
||||
applicationIDs = append(applicationIDs, app.ID)
|
||||
}
|
||||
|
||||
var dynamicCodes []model.DynamicCode
|
||||
if err := database.DB.Preload("Application").Preload("Creator").Where("application_id IN ?", applicationIDs).Find(&dynamicCodes).Error; err != nil {
|
||||
response.Error(c, 500, "获取动态代码列表失败")
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
type DynamicCodeResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Code string `json:"code"`
|
||||
Description string `json:"description"`
|
||||
ApplicationID uint `json:"application_id"`
|
||||
ApplicationName string `json:"application_name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
Creator *struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
} `json:"creator,omitempty"`
|
||||
}
|
||||
|
||||
var result []DynamicCodeResponse
|
||||
for _, dc := range dynamicCodes {
|
||||
var creator *struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
if dc.UserID != nil && dc.Creator.ID != 0 {
|
||||
creator = &struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
}{
|
||||
ID: dc.Creator.ID,
|
||||
Username: dc.Creator.Username,
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, DynamicCodeResponse{
|
||||
ID: dc.ID,
|
||||
Name: dc.Name,
|
||||
Code: dc.Code,
|
||||
Description: dc.Description,
|
||||
ApplicationID: dc.ApplicationID,
|
||||
ApplicationName: dc.Application.Name,
|
||||
Enabled: dc.Status == "active",
|
||||
CreatedAt: dc.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
UpdatedAt: dc.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
Creator: creator,
|
||||
})
|
||||
}
|
||||
|
||||
response.Success(c, result)
|
||||
}
|
||||
|
||||
func handleGetDynamicCode(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
id := c.Param("id")
|
||||
|
||||
var dynamicCode model.DynamicCode
|
||||
if err := database.DB.Preload("Application").Preload("Creator").Where("id = ?", id).First(&dynamicCode).Error; err != nil {
|
||||
response.Error(c, 404, "动态代码不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", dynamicCode.ApplicationID, userID).First(&app).Error; err != nil {
|
||||
response.Error(c, 403, "无权限访问此动态代码")
|
||||
return
|
||||
}
|
||||
|
||||
type DynamicCodeResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Code string `json:"code"`
|
||||
Description string `json:"description"`
|
||||
ApplicationID uint `json:"application_id"`
|
||||
ApplicationName string `json:"application_name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
Creator *struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
} `json:"creator,omitempty"`
|
||||
}
|
||||
|
||||
var creator *struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
if dynamicCode.UserID != nil && dynamicCode.Creator.ID != 0 {
|
||||
creator = &struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
}{
|
||||
ID: dynamicCode.Creator.ID,
|
||||
Username: dynamicCode.Creator.Username,
|
||||
}
|
||||
}
|
||||
|
||||
result := DynamicCodeResponse{
|
||||
ID: dynamicCode.ID,
|
||||
Name: dynamicCode.Name,
|
||||
Code: dynamicCode.Code,
|
||||
Description: dynamicCode.Description,
|
||||
ApplicationID: dynamicCode.ApplicationID,
|
||||
ApplicationName: dynamicCode.Application.Name,
|
||||
Enabled: dynamicCode.Status == "active",
|
||||
CreatedAt: dynamicCode.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
UpdatedAt: dynamicCode.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
Creator: creator,
|
||||
}
|
||||
|
||||
response.Success(c, result)
|
||||
}
|
||||
|
||||
func handleCreateDynamicCode(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var req struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Code string `json:"code" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
Enabled bool `json:"enabled"`
|
||||
ApplicationID uint `json:"application_id" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateDynamicCode(req.Code); err != nil {
|
||||
response.Error(c, 400, "代码语法错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", req.ApplicationID, userID).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
status := "inactive"
|
||||
if req.Enabled {
|
||||
status = "active"
|
||||
}
|
||||
|
||||
key := normalizeKey(req.Name)
|
||||
|
||||
var existingCode model.DynamicCode
|
||||
err := database.DB.Unscoped().Where("key = ?", key).First(&existingCode).Error
|
||||
if err == nil {
|
||||
if existingCode.DeletedAt.Valid {
|
||||
database.DB.Unscoped().Delete(&existingCode)
|
||||
} else {
|
||||
response.Error(c, 400, "该名称的动态代码已存在")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
dynamicCode := model.DynamicCode{
|
||||
UserID: &userID,
|
||||
ApplicationID: req.ApplicationID,
|
||||
Name: req.Name,
|
||||
Key: key,
|
||||
Code: req.Code,
|
||||
Description: req.Description,
|
||||
Status: status,
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&dynamicCode).Error; err != nil {
|
||||
if strings.Contains(err.Error(), "UNIQUE constraint failed") || strings.Contains(err.Error(), "constraint failed") {
|
||||
response.Error(c, 400, "该名称的动态代码已存在")
|
||||
return
|
||||
}
|
||||
response.Error(c, 500, "创建失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, dynamicCode)
|
||||
}
|
||||
|
||||
func handleUpdateDynamicCode(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
id := c.Param("id")
|
||||
|
||||
var dynamicCode model.DynamicCode
|
||||
if err := database.DB.Where("id = ?", id).First(&dynamicCode).Error; err != nil {
|
||||
response.Error(c, 404, "动态代码不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", dynamicCode.ApplicationID, userID).First(&app).Error; err != nil {
|
||||
response.Error(c, 403, "无权限操作此动态代码")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Code string `json:"code" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
Enabled bool `json:"enabled"`
|
||||
ApplicationID uint `json:"application_id" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateDynamicCode(req.Code); err != nil {
|
||||
response.Error(c, 400, "代码语法错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if req.ApplicationID != dynamicCode.ApplicationID {
|
||||
var newApp model.Application
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", req.ApplicationID, userID).First(&newApp).Error; err != nil {
|
||||
response.Error(c, 403, "无权限操作此应用")
|
||||
return
|
||||
}
|
||||
dynamicCode.ApplicationID = req.ApplicationID
|
||||
}
|
||||
|
||||
status := "inactive"
|
||||
if req.Enabled {
|
||||
status = "active"
|
||||
}
|
||||
|
||||
dynamicCode.Name = req.Name
|
||||
dynamicCode.Key = normalizeKey(req.Name)
|
||||
dynamicCode.Code = req.Code
|
||||
dynamicCode.Description = req.Description
|
||||
dynamicCode.Status = status
|
||||
|
||||
if err := database.DB.Save(&dynamicCode).Error; err != nil {
|
||||
response.Error(c, 500, "更新失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, dynamicCode)
|
||||
}
|
||||
|
||||
func handleDeleteDynamicCode(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
id := c.Param("id")
|
||||
|
||||
var dynamicCode model.DynamicCode
|
||||
if err := database.DB.Where("id = ?", id).First(&dynamicCode).Error; err != nil {
|
||||
response.Error(c, 404, "动态代码不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", dynamicCode.ApplicationID, userID).First(&app).Error; err != nil {
|
||||
response.Error(c, 403, "无权限操作此动态代码")
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DB.Delete(&dynamicCode).Error; err != nil {
|
||||
response.Error(c, 500, "删除失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "删除成功"})
|
||||
}
|
||||
|
||||
func handleUpdateDynamicCodeStatus(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
id := c.Param("id")
|
||||
|
||||
var dynamicCode model.DynamicCode
|
||||
if err := database.DB.Where("id = ?", id).First(&dynamicCode).Error; err != nil {
|
||||
response.Error(c, 404, "动态代码不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", dynamicCode.ApplicationID, userID).First(&app).Error; err != nil {
|
||||
response.Error(c, 403, "无权限操作此动态代码")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
status := "inactive"
|
||||
if req.Enabled {
|
||||
status = "active"
|
||||
}
|
||||
|
||||
dynamicCode.Status = status
|
||||
|
||||
if err := database.DB.Save(&dynamicCode).Error; err != nil {
|
||||
response.Error(c, 500, "更新状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, dynamicCode)
|
||||
}
|
||||
|
||||
func handleBatchDeleteDynamicCodes(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var req struct {
|
||||
IDs []uint `json:"ids" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var applications []model.Application
|
||||
if err := database.DB.Where("user_id = ?", userID).Find(&applications).Error; err != nil {
|
||||
response.Error(c, 500, "获取应用列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
var applicationIDs []uint
|
||||
for _, app := range applications {
|
||||
applicationIDs = append(applicationIDs, app.ID)
|
||||
}
|
||||
|
||||
if err := database.DB.Where("id IN ? AND application_id IN ?", req.IDs, applicationIDs).Delete(&model.DynamicCode{}).Error; err != nil {
|
||||
response.Error(c, 500, "批量删除失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "批量删除成功"})
|
||||
}
|
||||
|
||||
func handleBatchUpdateDynamicCodeStatus(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var req struct {
|
||||
IDs []uint `json:"ids" binding:"required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var applications []model.Application
|
||||
if err := database.DB.Where("user_id = ?", userID).Find(&applications).Error; err != nil {
|
||||
response.Error(c, 500, "获取应用列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
var applicationIDs []uint
|
||||
for _, app := range applications {
|
||||
applicationIDs = append(applicationIDs, app.ID)
|
||||
}
|
||||
|
||||
status := "inactive"
|
||||
if req.Enabled {
|
||||
status = "active"
|
||||
}
|
||||
|
||||
if err := database.DB.Model(&model.DynamicCode{}).Where("id IN ? AND application_id IN ?", req.IDs, applicationIDs).Update("status", status).Error; err != nil {
|
||||
response.Error(c, 500, "批量更新状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "批量更新状态成功"})
|
||||
}
|
||||
|
||||
type RiskControlRule struct {
|
||||
ID uint `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value"`
|
||||
Reason string `json:"reason"`
|
||||
Status string `json:"status"`
|
||||
ExpiresAt *string `json:"expires_at"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
func handleGetRiskControlRules(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var applications []model.Application
|
||||
if err := database.DB.Where("user_id = ?", userID).Find(&applications).Error; err != nil {
|
||||
response.Error(c, 500, "获取应用列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
var applicationIDs []uint
|
||||
for _, app := range applications {
|
||||
applicationIDs = append(applicationIDs, app.ID)
|
||||
}
|
||||
|
||||
var rules []model.RiskControlRule
|
||||
if err := database.DB.Where("user_id = ? OR application_id IN ?", userID, applicationIDs).Order("created_at DESC").Find(&rules).Error; err != nil {
|
||||
response.Error(c, 500, "获取风控规则失败")
|
||||
return
|
||||
}
|
||||
|
||||
var result []gin.H
|
||||
for _, rule := range rules {
|
||||
var expiresAt *string
|
||||
if rule.ExpiresAt != nil {
|
||||
t := rule.ExpiresAt.Format("2006-01-02 15:04:05")
|
||||
expiresAt = &t
|
||||
}
|
||||
|
||||
var appID *uint
|
||||
if rule.ApplicationID != nil {
|
||||
appID = rule.ApplicationID
|
||||
}
|
||||
|
||||
result = append(result, gin.H{
|
||||
"id": rule.ID,
|
||||
"type": rule.Type,
|
||||
"value": rule.Value,
|
||||
"reason": rule.Reason,
|
||||
"status": rule.Status,
|
||||
"expires_at": expiresAt,
|
||||
"application_id": appID,
|
||||
"is_global": rule.ApplicationID == nil,
|
||||
"created_at": rule.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
}
|
||||
|
||||
response.Success(c, result)
|
||||
}
|
||||
|
||||
func handleCreateRiskControlRule(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var req struct {
|
||||
Type string `json:"type" binding:"required"`
|
||||
Value string `json:"value" binding:"required"`
|
||||
Reason string `json:"reason"`
|
||||
ExpiresAt *string `json:"expires_at"`
|
||||
ApplicationID *uint `json:"application_id"`
|
||||
IsGlobal bool `json:"is_global"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
log.Printf("[ERROR] Failed to bind JSON: %v", err)
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Create risk control rule: type=%s, value=%s, is_global=%v, application_id=%v", req.Type, req.Value, req.IsGlobal, req.ApplicationID)
|
||||
|
||||
var appID *uint
|
||||
switch req.Type {
|
||||
case "user":
|
||||
if req.ApplicationID == nil {
|
||||
response.Error(c, 400, "用户封禁规则必须指定应用")
|
||||
return
|
||||
}
|
||||
var application model.Application
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", *req.ApplicationID, userID).First(&application).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在或无权限")
|
||||
return
|
||||
}
|
||||
appID = req.ApplicationID
|
||||
case "ip", "device", "region":
|
||||
if !req.IsGlobal && req.ApplicationID != nil {
|
||||
var application model.Application
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", *req.ApplicationID, userID).First(&application).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在或无权限")
|
||||
return
|
||||
}
|
||||
appID = req.ApplicationID
|
||||
} else {
|
||||
appID = nil
|
||||
}
|
||||
default:
|
||||
response.Error(c, 400, "不支持的规则类型")
|
||||
return
|
||||
}
|
||||
|
||||
rule := model.RiskControlRule{
|
||||
UserID: userID,
|
||||
ApplicationID: appID,
|
||||
Type: req.Type,
|
||||
Value: req.Value,
|
||||
Reason: req.Reason,
|
||||
Status: "active",
|
||||
}
|
||||
|
||||
if req.ExpiresAt != nil && *req.ExpiresAt != "" {
|
||||
t, err := time.Parse("2006-01-02T15:04", *req.ExpiresAt)
|
||||
if err == nil {
|
||||
rule.ExpiresAt = &t
|
||||
} else {
|
||||
log.Printf("[WARN] Failed to parse expires_at: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Creating rule: %+v", rule)
|
||||
|
||||
if err := database.DB.Create(&rule).Error; err != nil {
|
||||
log.Printf("[ERROR] Failed to create risk control rule: %v", err)
|
||||
response.Error(c, 500, "创建风控规则失败")
|
||||
return
|
||||
}
|
||||
|
||||
var expiresAt *string
|
||||
if rule.ExpiresAt != nil {
|
||||
t := rule.ExpiresAt.Format("2006-01-02 15:04:05")
|
||||
expiresAt = &t
|
||||
}
|
||||
|
||||
var appIDResp *uint
|
||||
if rule.ApplicationID != nil {
|
||||
appIDResp = rule.ApplicationID
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"id": rule.ID,
|
||||
"type": rule.Type,
|
||||
"value": rule.Value,
|
||||
"reason": rule.Reason,
|
||||
"status": rule.Status,
|
||||
"expires_at": expiresAt,
|
||||
"application_id": appIDResp,
|
||||
"is_global": rule.ApplicationID == nil,
|
||||
"created_at": rule.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
}
|
||||
|
||||
func handleUpdateRiskControlRule(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
ruleID := c.Param("id")
|
||||
|
||||
var req struct {
|
||||
Type string `json:"type" binding:"required"`
|
||||
Value string `json:"value" binding:"required"`
|
||||
Reason string `json:"reason"`
|
||||
ExpiresAt *string `json:"expires_at"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var applications []model.Application
|
||||
if err := database.DB.Where("user_id = ?", userID).Find(&applications).Error; err != nil {
|
||||
response.Error(c, 500, "获取应用列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
var applicationIDs []uint
|
||||
for _, app := range applications {
|
||||
applicationIDs = append(applicationIDs, app.ID)
|
||||
}
|
||||
|
||||
var rule model.RiskControlRule
|
||||
if err := database.DB.Where("id = ? AND application_id IN ?", ruleID, applicationIDs).First(&rule).Error; err != nil {
|
||||
response.Error(c, 404, "风控规则不存在")
|
||||
return
|
||||
}
|
||||
|
||||
rule.Type = req.Type
|
||||
rule.Value = req.Value
|
||||
rule.Reason = req.Reason
|
||||
|
||||
if req.ExpiresAt != nil && *req.ExpiresAt != "" {
|
||||
t, err := time.Parse("2006-01-02T15:04", *req.ExpiresAt)
|
||||
if err == nil {
|
||||
rule.ExpiresAt = &t
|
||||
}
|
||||
} else {
|
||||
rule.ExpiresAt = nil
|
||||
}
|
||||
|
||||
if err := database.DB.Save(&rule).Error; err != nil {
|
||||
response.Error(c, 500, "更新风控规则失败")
|
||||
return
|
||||
}
|
||||
|
||||
var expiresAt *string
|
||||
if rule.ExpiresAt != nil {
|
||||
t := rule.ExpiresAt.Format("2006-01-02 15:04:05")
|
||||
expiresAt = &t
|
||||
}
|
||||
|
||||
response.Success(c, RiskControlRule{
|
||||
ID: rule.ID,
|
||||
Type: rule.Type,
|
||||
Value: rule.Value,
|
||||
Reason: rule.Reason,
|
||||
Status: rule.Status,
|
||||
ExpiresAt: expiresAt,
|
||||
CreatedAt: rule.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
}
|
||||
|
||||
func handleDeleteRiskControlRule(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
ruleID := c.Param("id")
|
||||
|
||||
var applications []model.Application
|
||||
if err := database.DB.Where("user_id = ?", userID).Find(&applications).Error; err != nil {
|
||||
response.Error(c, 500, "获取应用列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
var applicationIDs []uint
|
||||
for _, app := range applications {
|
||||
applicationIDs = append(applicationIDs, app.ID)
|
||||
}
|
||||
|
||||
if err := database.DB.Where("id = ? AND application_id IN ?", ruleID, applicationIDs).Delete(&model.RiskControlRule{}).Error; err != nil {
|
||||
response.Error(c, 500, "删除风控规则失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "删除成功"})
|
||||
}
|
||||
|
||||
func handleUpdateRiskControlRuleStatus(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
ruleID := c.Param("id")
|
||||
|
||||
var req struct {
|
||||
Status string `json:"status" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var applications []model.Application
|
||||
if err := database.DB.Where("user_id = ?", userID).Find(&applications).Error; err != nil {
|
||||
response.Error(c, 500, "获取应用列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
var applicationIDs []uint
|
||||
for _, app := range applications {
|
||||
applicationIDs = append(applicationIDs, app.ID)
|
||||
}
|
||||
|
||||
var rule model.RiskControlRule
|
||||
if err := database.DB.Where("id = ? AND application_id IN ?", ruleID, applicationIDs).First(&rule).Error; err != nil {
|
||||
response.Error(c, 404, "风控规则不存在")
|
||||
return
|
||||
}
|
||||
|
||||
rule.Status = req.Status
|
||||
if err := database.DB.Save(&rule).Error; err != nil {
|
||||
response.Error(c, 500, "更新状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "状态更新成功"})
|
||||
}
|
||||
|
||||
func handleBatchDeleteRiskControlRules(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var req struct {
|
||||
IDs []uint `json:"ids" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var applications []model.Application
|
||||
if err := database.DB.Where("user_id = ?", userID).Find(&applications).Error; err != nil {
|
||||
response.Error(c, 500, "获取应用列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
var applicationIDs []uint
|
||||
for _, app := range applications {
|
||||
applicationIDs = append(applicationIDs, app.ID)
|
||||
}
|
||||
|
||||
if err := database.DB.Where("id IN ? AND application_id IN ?", req.IDs, applicationIDs).Delete(&model.RiskControlRule{}).Error; err != nil {
|
||||
response.Error(c, 500, "批量删除失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "批量删除成功"})
|
||||
}
|
||||
|
||||
func handleBatchUpdateRiskControlRuleStatus(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var req struct {
|
||||
IDs []uint `json:"ids" binding:"required"`
|
||||
Status string `json:"status" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var applications []model.Application
|
||||
if err := database.DB.Where("user_id = ?", userID).Find(&applications).Error; err != nil {
|
||||
response.Error(c, 500, "获取应用列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
var applicationIDs []uint
|
||||
for _, app := range applications {
|
||||
applicationIDs = append(applicationIDs, app.ID)
|
||||
}
|
||||
|
||||
if err := database.DB.Model(&model.RiskControlRule{}).Where("id IN ? AND application_id IN ?", req.IDs, applicationIDs).Update("status", req.Status).Error; err != nil {
|
||||
response.Error(c, 500, "批量更新状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "批量更新状态成功"})
|
||||
}
|
||||
@@ -0,0 +1,481 @@
|
||||
package developer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/service"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupEmailRoutes(r *gin.RouterGroup) {
|
||||
r.GET("/applications/:id/email-config", handleGetEmailConfig)
|
||||
r.PUT("/applications/:id/email-config", handleUpdateEmailConfig)
|
||||
r.POST("/applications/:id/email-config/test", handleTestEmailConfig)
|
||||
r.GET("/applications/:id/email-templates", handleGetEmailTemplates)
|
||||
r.GET("/applications/:id/email-templates/:template_id", handleGetEmailTemplate)
|
||||
r.POST("/applications/:id/email-templates", handleCreateEmailTemplate)
|
||||
r.PUT("/applications/:id/email-templates/:template_id", handleUpdateEmailTemplate)
|
||||
r.DELETE("/applications/:id/email-templates/:template_id", handleDeleteEmailTemplate)
|
||||
r.POST("/applications/:id/send-verify-code", handleSendVerifyCode)
|
||||
}
|
||||
|
||||
func handleGetEmailConfig(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
appID := c.Param("id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", appID, userID).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var permission model.PackagePermission
|
||||
hasPermission := checkEmailPermission(userID.(uint), &permission)
|
||||
if !hasPermission {
|
||||
response.Error(c, 403, "您的套餐不支持邮件功能")
|
||||
return
|
||||
}
|
||||
|
||||
var smtpConfig model.AppSMTPConfig
|
||||
database.DB.Where("application_id = ?", app.ID).First(&smtpConfig)
|
||||
|
||||
result := gin.H{
|
||||
"enable_email_verify": app.EnableEmailVerify,
|
||||
"require_email_verify": app.RequireEmailVerify,
|
||||
"enable_password_reset": app.EnablePasswordReset,
|
||||
"permission": gin.H{
|
||||
"allow_email": permission.AllowEmail,
|
||||
},
|
||||
}
|
||||
|
||||
if smtpConfig.ID > 0 {
|
||||
result["smtp_config"] = gin.H{
|
||||
"id": smtpConfig.ID,
|
||||
"host": smtpConfig.Host,
|
||||
"port": smtpConfig.Port,
|
||||
"user": smtpConfig.User,
|
||||
"from_name": smtpConfig.FromName,
|
||||
"from_email": smtpConfig.FromEmail,
|
||||
"use_ssl": smtpConfig.UseSSL,
|
||||
"status": smtpConfig.Status,
|
||||
}
|
||||
} else {
|
||||
result["smtp_config"] = nil
|
||||
}
|
||||
|
||||
response.Success(c, result)
|
||||
}
|
||||
|
||||
func handleUpdateEmailConfig(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
appID := c.Param("id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", appID, userID).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var permission model.PackagePermission
|
||||
hasPermission := checkEmailPermission(userID.(uint), &permission)
|
||||
if !hasPermission {
|
||||
response.Error(c, 403, "您的套餐不支持邮件功能")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
EnableEmailVerify bool `json:"enable_email_verify"`
|
||||
RequireEmailVerify bool `json:"require_email_verify"`
|
||||
EnablePasswordReset bool `json:"enable_password_reset"`
|
||||
SMTPConfig *struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
FromName string `json:"from_name"`
|
||||
FromEmail string `json:"from_email"`
|
||||
UseSSL bool `json:"use_ssl"`
|
||||
} `json:"smtp_config"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
updates := map[string]interface{}{
|
||||
"enable_email_verify": req.EnableEmailVerify,
|
||||
"require_email_verify": req.RequireEmailVerify,
|
||||
"enable_password_reset": req.EnablePasswordReset,
|
||||
}
|
||||
|
||||
if err := database.DB.Model(&app).Updates(updates).Error; err != nil {
|
||||
response.Error(c, 500, "更新失败")
|
||||
return
|
||||
}
|
||||
|
||||
if req.SMTPConfig != nil {
|
||||
var smtpConfig model.AppSMTPConfig
|
||||
database.DB.Where("application_id = ?", app.ID).First(&smtpConfig)
|
||||
|
||||
smtpConfig.ApplicationID = app.ID
|
||||
smtpConfig.Host = req.SMTPConfig.Host
|
||||
smtpConfig.Port = req.SMTPConfig.Port
|
||||
smtpConfig.User = req.SMTPConfig.User
|
||||
if req.SMTPConfig.Password != "" {
|
||||
smtpConfig.Password = req.SMTPConfig.Password
|
||||
}
|
||||
smtpConfig.FromName = req.SMTPConfig.FromName
|
||||
smtpConfig.FromEmail = req.SMTPConfig.FromEmail
|
||||
smtpConfig.UseSSL = req.SMTPConfig.UseSSL
|
||||
|
||||
if smtpConfig.ID > 0 {
|
||||
database.DB.Save(&smtpConfig)
|
||||
} else {
|
||||
database.DB.Create(&smtpConfig)
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "更新成功",
|
||||
})
|
||||
}
|
||||
|
||||
func handleTestEmailConfig(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
appID := c.Param("id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", appID, userID).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "请输入有效的邮箱地址")
|
||||
return
|
||||
}
|
||||
|
||||
var smtpConfig model.AppSMTPConfig
|
||||
if err := database.DB.Where("application_id = ?", app.ID).First(&smtpConfig).Error; err != nil {
|
||||
response.Error(c, 400, "请先配置SMTP")
|
||||
return
|
||||
}
|
||||
|
||||
emailService := service.NewEmailService()
|
||||
config := service.EmailConfig{
|
||||
Host: smtpConfig.Host,
|
||||
Port: smtpConfig.Port,
|
||||
User: smtpConfig.User,
|
||||
Password: smtpConfig.Password,
|
||||
FromName: smtpConfig.FromName,
|
||||
FromEmail: smtpConfig.FromEmail,
|
||||
UseSSL: smtpConfig.UseSSL,
|
||||
}
|
||||
|
||||
if err := emailService.SendTestEmail(config, req.Email, app.Name); err != nil {
|
||||
response.Error(c, 500, fmt.Sprintf("发送失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "测试邮件已发送",
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetEmailTemplates(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
appID := c.Param("id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", appID, userID).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var permission model.PackagePermission
|
||||
hasPermission := checkEmailPermission(userID.(uint), &permission)
|
||||
if !hasPermission {
|
||||
response.Error(c, 403, "您的套餐不支持邮箱验证功能")
|
||||
return
|
||||
}
|
||||
|
||||
var templates []model.EmailTemplate
|
||||
database.DB.Where("application_id = ?", app.ID).Order("created_at DESC").Find(&templates)
|
||||
|
||||
response.Success(c, templates)
|
||||
}
|
||||
|
||||
func handleGetEmailTemplate(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
appID := c.Param("id")
|
||||
templateID := c.Param("template_id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", appID, userID).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var template model.EmailTemplate
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", templateID, app.ID).First(&template).Error; err != nil {
|
||||
response.Error(c, 404, "模板不存在")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, template)
|
||||
}
|
||||
|
||||
func handleCreateEmailTemplate(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
appID := c.Param("id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", appID, userID).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var permission model.PackagePermission
|
||||
hasPermission := checkEmailPermission(userID.(uint), &permission)
|
||||
if !hasPermission {
|
||||
response.Error(c, 403, "您的套餐不支持邮件功能")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Type string `json:"type" binding:"required"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Subject string `json:"subject" binding:"required"`
|
||||
Content string `json:"content" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
template := model.EmailTemplate{
|
||||
ApplicationID: app.ID,
|
||||
Type: req.Type,
|
||||
Name: req.Name,
|
||||
Subject: req.Subject,
|
||||
Content: req.Content,
|
||||
Status: "active",
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&template).Error; err != nil {
|
||||
response.Error(c, 500, "创建失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, template)
|
||||
}
|
||||
|
||||
func handleUpdateEmailTemplate(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
appID := c.Param("id")
|
||||
templateID := c.Param("template_id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", appID, userID).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var permission model.PackagePermission
|
||||
hasPermission := checkEmailPermission(userID.(uint), &permission)
|
||||
if !hasPermission {
|
||||
response.Error(c, 403, "您的套餐不支持邮件功能")
|
||||
return
|
||||
}
|
||||
|
||||
var template model.EmailTemplate
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", templateID, app.ID).First(&template).Error; err != nil {
|
||||
response.Error(c, 404, "模板不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Subject string `json:"subject"`
|
||||
Content string `json:"content"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
updates := map[string]interface{}{}
|
||||
if req.Type != "" {
|
||||
updates["type"] = req.Type
|
||||
}
|
||||
if req.Name != "" {
|
||||
updates["name"] = req.Name
|
||||
}
|
||||
if req.Subject != "" {
|
||||
updates["subject"] = req.Subject
|
||||
}
|
||||
if req.Content != "" {
|
||||
updates["content"] = req.Content
|
||||
}
|
||||
if req.Status != "" {
|
||||
updates["status"] = req.Status
|
||||
}
|
||||
|
||||
if err := database.DB.Model(&template).Updates(updates).Error; err != nil {
|
||||
response.Error(c, 500, "更新失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, template)
|
||||
}
|
||||
|
||||
func handleDeleteEmailTemplate(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
appID := c.Param("id")
|
||||
templateID := c.Param("template_id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", appID, userID).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var permission model.PackagePermission
|
||||
hasPermission := checkEmailPermission(userID.(uint), &permission)
|
||||
if !hasPermission {
|
||||
response.Error(c, 403, "您的套餐不支持邮件功能")
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", templateID, app.ID).Delete(&model.EmailTemplate{}).Error; err != nil {
|
||||
response.Error(c, 500, "删除失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "删除成功",
|
||||
})
|
||||
}
|
||||
|
||||
func handleSendVerifyCode(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
appID := c.Param("id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", appID, userID).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Purpose string `json:"purpose"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "请输入有效的邮箱地址")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Purpose == "" {
|
||||
req.Purpose = "register"
|
||||
}
|
||||
|
||||
var smtpConfig model.AppSMTPConfig
|
||||
if err := database.DB.Where("application_id = ?", app.ID).First(&smtpConfig).Error; err != nil {
|
||||
response.Error(c, 400, "请先配置SMTP")
|
||||
return
|
||||
}
|
||||
|
||||
var template model.EmailTemplate
|
||||
database.DB.Where("application_id = ? AND type = ? AND status = ?", app.ID, req.Purpose, "active").
|
||||
Order("is_default DESC").First(&template)
|
||||
|
||||
code := generateVerifyCode()
|
||||
expireAt := time.Now().Add(15 * time.Minute)
|
||||
|
||||
verifyCode := model.EmailVerifyCode{
|
||||
ApplicationID: app.ID,
|
||||
Email: req.Email,
|
||||
Code: code,
|
||||
Purpose: req.Purpose,
|
||||
ExpiresAt: expireAt,
|
||||
}
|
||||
database.DB.Create(&verifyCode)
|
||||
|
||||
emailService := service.NewEmailService()
|
||||
config := service.EmailConfig{
|
||||
Host: smtpConfig.Host,
|
||||
Port: smtpConfig.Port,
|
||||
User: smtpConfig.User,
|
||||
Password: smtpConfig.Password,
|
||||
FromName: smtpConfig.FromName,
|
||||
FromEmail: smtpConfig.FromEmail,
|
||||
UseSSL: smtpConfig.UseSSL,
|
||||
}
|
||||
|
||||
subject := fmt.Sprintf("验证码 - %s", app.Name)
|
||||
content := fmt.Sprintf(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="UTF-8"></head>
|
||||
<body style="font-family: Arial, sans-serif; padding: 20px; background-color: #f5f5f5;">
|
||||
<div style="max-width: 600px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
<h2 style="color: #333; margin-bottom: 20px;">邮箱验证</h2>
|
||||
<p style="color: #666; line-height: 1.6;">您的验证码是:<strong style="font-size: 24px; color: #1890ff;">%s</strong></p>
|
||||
<p style="color: #999; font-size: 12px;">验证码有效期为15分钟,请尽快使用。</p>
|
||||
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
||||
<p style="color: #999; font-size: 12px;">此邮件由 %s 系统自动发送,请勿回复。</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`, code, app.Name)
|
||||
|
||||
if template.ID > 0 {
|
||||
subject = template.Subject
|
||||
content = template.Content
|
||||
}
|
||||
|
||||
if err := emailService.SendEmail(config, req.Email, subject, content); err != nil {
|
||||
response.Error(c, 500, fmt.Sprintf("发送失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "验证码已发送",
|
||||
})
|
||||
}
|
||||
|
||||
func checkEmailPermission(userID uint, permission *model.PackagePermission) bool {
|
||||
var userPackage model.UserPackage
|
||||
if err := database.DB.Where("user_id = ? AND status = ?", userID, "active").
|
||||
Preload("Package").First(&userPackage).Error; err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if userPackage.ExpiredAt != nil && userPackage.ExpiredAt.Before(time.Now()) {
|
||||
return false
|
||||
}
|
||||
|
||||
if err := database.DB.Where("package_id = ?", userPackage.PackageID).First(permission).Error; err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return permission.AllowEmail
|
||||
}
|
||||
|
||||
func generateVerifyCode() string {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
return fmt.Sprintf("%06d", r.Intn(1000000))
|
||||
}
|
||||
@@ -0,0 +1,650 @@
|
||||
package developer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/service"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func handleGetWebhooks(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
appID := c.Query("application_id")
|
||||
|
||||
var webhooks []struct {
|
||||
model.WebhookConfig
|
||||
ApplicationName string `json:"application_name"`
|
||||
}
|
||||
|
||||
query := database.DB.Model(&model.WebhookConfig{}).
|
||||
Select("webhook_configs.*, applications.name as application_name").
|
||||
Joins("JOIN applications ON webhook_configs.application_id = applications.id").
|
||||
Where("applications.user_id = ?", userID)
|
||||
|
||||
if appID != "" && appID != "all" {
|
||||
query = query.Where("webhook_configs.application_id = ?", appID)
|
||||
}
|
||||
|
||||
if err := query.Find(&webhooks).Error; err != nil {
|
||||
response.Error(c, 500, "获取Webhook配置失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, webhooks)
|
||||
}
|
||||
|
||||
func handleCreateWebhook(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var req struct {
|
||||
ApplicationID uint `json:"application_id" binding:"required"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
URL string `json:"url" binding:"required,url"`
|
||||
SecretKey string `json:"secret_key"`
|
||||
Events []string `json:"events" binding:"required"`
|
||||
RetryCount int `json:"retry_count"`
|
||||
Timeout int `json:"timeout"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", req.ApplicationID, userID).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
eventsJSON, _ := json.Marshal(req.Events)
|
||||
if req.RetryCount == 0 {
|
||||
req.RetryCount = 3
|
||||
}
|
||||
if req.Timeout == 0 {
|
||||
req.Timeout = 10
|
||||
}
|
||||
|
||||
webhook := model.WebhookConfig{
|
||||
ApplicationID: req.ApplicationID,
|
||||
Name: req.Name,
|
||||
URL: req.URL,
|
||||
SecretKey: req.SecretKey,
|
||||
Events: string(eventsJSON),
|
||||
Status: "active",
|
||||
RetryCount: req.RetryCount,
|
||||
Timeout: req.Timeout,
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&webhook).Error; err != nil {
|
||||
response.Error(c, 500, "创建Webhook配置失败")
|
||||
return
|
||||
}
|
||||
|
||||
service.LogOperation(c, "create", "webhook", &webhook.ID, fmt.Sprintf("创建Webhook: %s", webhook.Name), nil)
|
||||
|
||||
response.Success(c, webhook)
|
||||
}
|
||||
|
||||
func handleUpdateWebhook(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
id := c.Param("id")
|
||||
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url" binding:"omitempty,url"`
|
||||
SecretKey string `json:"secret_key"`
|
||||
Events []string `json:"events"`
|
||||
Status string `json:"status"`
|
||||
RetryCount int `json:"retry_count"`
|
||||
Timeout int `json:"timeout"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var webhook model.WebhookConfig
|
||||
if err := database.DB.Joins("JOIN applications ON webhook_configs.application_id = applications.id").
|
||||
Where("webhook_configs.id = ? AND applications.user_id = ?", id, userID).
|
||||
First(&webhook).Error; err != nil {
|
||||
response.Error(c, 404, "Webhook配置不存在")
|
||||
return
|
||||
}
|
||||
|
||||
updates := make(map[string]interface{})
|
||||
if req.Name != "" {
|
||||
updates["name"] = req.Name
|
||||
}
|
||||
if req.URL != "" {
|
||||
updates["url"] = req.URL
|
||||
}
|
||||
if req.SecretKey != "" {
|
||||
updates["secret_key"] = req.SecretKey
|
||||
}
|
||||
if len(req.Events) > 0 {
|
||||
eventsJSON, _ := json.Marshal(req.Events)
|
||||
updates["events"] = string(eventsJSON)
|
||||
}
|
||||
if req.Status != "" {
|
||||
updates["status"] = req.Status
|
||||
}
|
||||
if req.RetryCount > 0 {
|
||||
updates["retry_count"] = req.RetryCount
|
||||
}
|
||||
if req.Timeout > 0 {
|
||||
updates["timeout"] = req.Timeout
|
||||
}
|
||||
|
||||
if err := database.DB.Model(&webhook).Updates(updates).Error; err != nil {
|
||||
response.Error(c, 500, "更新Webhook配置失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, webhook)
|
||||
}
|
||||
|
||||
func handleDeleteWebhook(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
id := c.Param("id")
|
||||
|
||||
var webhook model.WebhookConfig
|
||||
if err := database.DB.Joins("JOIN applications ON webhook_configs.application_id = applications.id").
|
||||
Where("webhook_configs.id = ? AND applications.user_id = ?", id, userID).
|
||||
First(&webhook).Error; err != nil {
|
||||
response.Error(c, 404, "Webhook配置不存在")
|
||||
return
|
||||
}
|
||||
|
||||
webhookName := webhook.Name
|
||||
webhookID := webhook.ID
|
||||
|
||||
if err := database.DB.Delete(&webhook).Error; err != nil {
|
||||
response.Error(c, 500, "删除Webhook配置失败")
|
||||
return
|
||||
}
|
||||
|
||||
service.LogOperation(c, "delete", "webhook", &webhookID, fmt.Sprintf("删除Webhook: %s", webhookName), nil)
|
||||
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
func handleGetWebhookLogs(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
webhookID := c.Query("webhook_id")
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
|
||||
var logs []model.WebhookLog
|
||||
var total int64
|
||||
|
||||
query := database.DB.Model(&model.WebhookLog{}).
|
||||
Joins("JOIN webhook_configs ON webhook_logs.webhook_id = webhook_configs.id").
|
||||
Joins("JOIN applications ON webhook_configs.application_id = applications.id").
|
||||
Where("applications.user_id = ?", userID)
|
||||
|
||||
if webhookID != "" {
|
||||
query = query.Where("webhook_logs.webhook_id = ?", webhookID)
|
||||
}
|
||||
|
||||
query.Count(&total)
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Order("webhook_logs.created_at DESC").
|
||||
Offset(offset).Limit(pageSize).
|
||||
Find(&logs).Error; err != nil {
|
||||
response.Error(c, 500, "获取Webhook日志失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"logs": logs,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
"total_page": (total + int64(pageSize) - 1) / int64(pageSize),
|
||||
})
|
||||
}
|
||||
|
||||
func handleTestWebhook(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
id := c.Param("id")
|
||||
|
||||
var webhook model.WebhookConfig
|
||||
if err := database.DB.Joins("JOIN applications ON webhook_configs.application_id = applications.id").
|
||||
Where("webhook_configs.id = ? AND applications.user_id = ?", id, userID).
|
||||
First(&webhook).Error; err != nil {
|
||||
response.Error(c, 404, "Webhook配置不存在")
|
||||
return
|
||||
}
|
||||
|
||||
testData := map[string]interface{}{
|
||||
"event": "test",
|
||||
"timestamp": time.Now().Unix(),
|
||||
"data": map[string]interface{}{
|
||||
"message": "This is a test webhook",
|
||||
},
|
||||
}
|
||||
|
||||
go sendWebhook(&webhook, testData)
|
||||
|
||||
response.Success(c, gin.H{"message": "测试请求已发送"})
|
||||
}
|
||||
|
||||
func handleGetAPIKeys(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
appID := c.Query("application_id")
|
||||
|
||||
var apiKeys []struct {
|
||||
model.ExtensionAPIKey
|
||||
ApplicationName string `json:"application_name"`
|
||||
}
|
||||
|
||||
query := database.DB.Model(&model.ExtensionAPIKey{}).
|
||||
Select("extension_api_keys.*, applications.name as application_name").
|
||||
Joins("JOIN applications ON extension_api_keys.application_id = applications.id").
|
||||
Where("applications.user_id = ?", userID)
|
||||
|
||||
if appID != "" && appID != "all" {
|
||||
query = query.Where("extension_api_keys.application_id = ?", appID)
|
||||
}
|
||||
|
||||
if err := query.Find(&apiKeys).Error; err != nil {
|
||||
response.Error(c, 500, "获取API密钥失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, apiKeys)
|
||||
}
|
||||
|
||||
func handleCreateAPIKey(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var req struct {
|
||||
ApplicationID uint `json:"application_id" binding:"required"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Permissions []string `json:"permissions"`
|
||||
ExpiresAt *string `json:"expires_at"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", req.ApplicationID, userID).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
accessKey := generateRandomKey(32)
|
||||
secretKey := generateRandomKey(32)
|
||||
|
||||
permissionsJSON, _ := json.Marshal(req.Permissions)
|
||||
|
||||
apiKey := model.ExtensionAPIKey{
|
||||
ApplicationID: req.ApplicationID,
|
||||
Name: req.Name,
|
||||
AccessKey: accessKey,
|
||||
SecretKey: secretKey,
|
||||
Permissions: string(permissionsJSON),
|
||||
Status: "active",
|
||||
}
|
||||
|
||||
if req.ExpiresAt != nil {
|
||||
expiresAt, err := time.Parse(time.RFC3339, *req.ExpiresAt)
|
||||
if err == nil {
|
||||
apiKey.ExpiresAt = &expiresAt
|
||||
}
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&apiKey).Error; err != nil {
|
||||
response.Error(c, 500, "创建API密钥失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, apiKey)
|
||||
}
|
||||
|
||||
func handleUpdateAPIKey(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
id := c.Param("id")
|
||||
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Permissions []string `json:"permissions"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var apiKey model.ExtensionAPIKey
|
||||
if err := database.DB.Joins("JOIN applications ON extension_api_keys.application_id = applications.id").
|
||||
Where("extension_api_keys.id = ? AND applications.user_id = ?", id, userID).
|
||||
First(&apiKey).Error; err != nil {
|
||||
response.Error(c, 404, "API密钥不存在")
|
||||
return
|
||||
}
|
||||
|
||||
updates := make(map[string]interface{})
|
||||
if req.Name != "" {
|
||||
updates["name"] = req.Name
|
||||
}
|
||||
if len(req.Permissions) > 0 {
|
||||
permissionsJSON, _ := json.Marshal(req.Permissions)
|
||||
updates["permissions"] = string(permissionsJSON)
|
||||
}
|
||||
if req.Status != "" {
|
||||
updates["status"] = req.Status
|
||||
}
|
||||
|
||||
if err := database.DB.Model(&apiKey).Updates(updates).Error; err != nil {
|
||||
response.Error(c, 500, "更新API密钥失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, apiKey)
|
||||
}
|
||||
|
||||
func handleDeleteAPIKey(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
id := c.Param("id")
|
||||
|
||||
var apiKey model.ExtensionAPIKey
|
||||
if err := database.DB.Joins("JOIN applications ON extension_api_keys.application_id = applications.id").
|
||||
Where("extension_api_keys.id = ? AND applications.user_id = ?", id, userID).
|
||||
First(&apiKey).Error; err != nil {
|
||||
response.Error(c, 404, "API密钥不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DB.Delete(&apiKey).Error; err != nil {
|
||||
response.Error(c, 500, "删除API密钥失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
func handleRegenerateAPIKey(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
id := c.Param("id")
|
||||
|
||||
var apiKey model.ExtensionAPIKey
|
||||
if err := database.DB.Joins("JOIN applications ON extension_api_keys.application_id = applications.id").
|
||||
Where("extension_api_keys.id = ? AND applications.user_id = ?", id, userID).
|
||||
First(&apiKey).Error; err != nil {
|
||||
response.Error(c, 404, "API密钥不存在")
|
||||
return
|
||||
}
|
||||
|
||||
newSecretKey := generateRandomKey(32)
|
||||
if err := database.DB.Model(&apiKey).Update("secret_key", newSecretKey).Error; err != nil {
|
||||
response.Error(c, 500, "重新生成密钥失败")
|
||||
return
|
||||
}
|
||||
|
||||
apiKey.SecretKey = newSecretKey
|
||||
response.Success(c, apiKey)
|
||||
}
|
||||
|
||||
func generateRandomKey(length int) string {
|
||||
bytes := make([]byte, length)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return ""
|
||||
}
|
||||
return hex.EncodeToString(bytes)[:length*2]
|
||||
}
|
||||
|
||||
func sendWebhook(webhook *model.WebhookConfig, data map[string]interface{}) {
|
||||
jsonData, _ := json.Marshal(data)
|
||||
|
||||
startTime := time.Now()
|
||||
client := &http.Client{
|
||||
Timeout: time.Duration(webhook.Timeout) * time.Second,
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", webhook.URL, bytes.NewReader(jsonData))
|
||||
if err != nil {
|
||||
logWebhookError(webhook.ID, data, err, time.Since(startTime).Milliseconds())
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if webhook.SecretKey != "" {
|
||||
req.Header.Set("X-Webhook-Secret", webhook.SecretKey)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
duration := time.Since(startTime).Milliseconds()
|
||||
|
||||
if err != nil {
|
||||
logWebhookError(webhook.ID, data, err, duration)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
logWebhookSuccess(webhook.ID, data, resp.StatusCode, duration)
|
||||
}
|
||||
|
||||
func logWebhookSuccess(webhookID uint, requestData map[string]interface{}, statusCode int, duration int64) {
|
||||
requestJSON, _ := json.Marshal(requestData)
|
||||
log := model.WebhookLog{
|
||||
WebhookID: webhookID,
|
||||
Event: requestData["event"].(string),
|
||||
RequestData: string(requestJSON),
|
||||
ResponseCode: statusCode,
|
||||
Status: "success",
|
||||
Duration: int(duration),
|
||||
}
|
||||
database.DB.Create(&log)
|
||||
}
|
||||
|
||||
func logWebhookError(webhookID uint, requestData map[string]interface{}, err error, duration int64) {
|
||||
requestJSON, _ := json.Marshal(requestData)
|
||||
log := model.WebhookLog{
|
||||
WebhookID: webhookID,
|
||||
Event: requestData["event"].(string),
|
||||
RequestData: string(requestJSON),
|
||||
Status: "failed",
|
||||
ErrorMessage: err.Error(),
|
||||
Duration: int(duration),
|
||||
}
|
||||
database.DB.Create(&log)
|
||||
}
|
||||
|
||||
func handleUpdateWebhookStatus(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
id := c.Param("id")
|
||||
|
||||
var req struct {
|
||||
Status string `json:"status" binding:"required,oneof=active inactive"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var webhook model.WebhookConfig
|
||||
if err := database.DB.Joins("JOIN applications ON webhook_configs.application_id = applications.id").
|
||||
Where("webhook_configs.id = ? AND applications.user_id = ?", id, userID).
|
||||
First(&webhook).Error; err != nil {
|
||||
response.Error(c, 404, "Webhook配置不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DB.Model(&webhook).Update("status", req.Status).Error; err != nil {
|
||||
response.Error(c, 500, "更新状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, webhook)
|
||||
}
|
||||
|
||||
func handleBatchUpdateWebhookStatus(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var req struct {
|
||||
IDs []uint `json:"ids" binding:"required"`
|
||||
Status string `json:"status" binding:"required,oneof=active inactive"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.IDs) == 0 {
|
||||
response.Error(c, 400, "请选择要更新的Webhook")
|
||||
return
|
||||
}
|
||||
|
||||
result := database.DB.Model(&model.WebhookConfig{}).
|
||||
Joins("JOIN applications ON webhook_configs.application_id = applications.id").
|
||||
Where("applications.user_id = ? AND webhook_configs.id IN ?", userID, req.IDs).
|
||||
Update("status", req.Status)
|
||||
|
||||
if result.Error != nil {
|
||||
response.Error(c, 500, "批量更新状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"updated_count": result.RowsAffected})
|
||||
}
|
||||
|
||||
func handleBatchDeleteWebhooks(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var req struct {
|
||||
IDs []uint `json:"ids" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.IDs) == 0 {
|
||||
response.Error(c, 400, "请选择要删除的Webhook")
|
||||
return
|
||||
}
|
||||
|
||||
result := database.DB.Where("id IN (?) AND application_id IN (SELECT id FROM applications WHERE user_id = ?)", req.IDs, userID).
|
||||
Delete(&model.WebhookConfig{})
|
||||
|
||||
if result.Error != nil {
|
||||
response.Error(c, 500, "批量删除失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"deleted_count": result.RowsAffected})
|
||||
}
|
||||
|
||||
func handleUpdateAPIKeyStatus(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
id := c.Param("id")
|
||||
|
||||
var req struct {
|
||||
Status string `json:"status" binding:"required,oneof=active inactive"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var apiKey model.ExtensionAPIKey
|
||||
if err := database.DB.Joins("JOIN applications ON extension_api_keys.application_id = applications.id").
|
||||
Where("extension_api_keys.id = ? AND applications.user_id = ?", id, userID).
|
||||
First(&apiKey).Error; err != nil {
|
||||
response.Error(c, 404, "API密钥不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DB.Model(&apiKey).Update("status", req.Status).Error; err != nil {
|
||||
response.Error(c, 500, "更新状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, apiKey)
|
||||
}
|
||||
|
||||
func handleBatchUpdateAPIKeyStatus(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var req struct {
|
||||
IDs []uint `json:"ids" binding:"required"`
|
||||
Status string `json:"status" binding:"required,oneof=active inactive"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.IDs) == 0 {
|
||||
response.Error(c, 400, "请选择要更新的API密钥")
|
||||
return
|
||||
}
|
||||
|
||||
result := database.DB.Model(&model.ExtensionAPIKey{}).
|
||||
Joins("JOIN applications ON extension_api_keys.application_id = applications.id").
|
||||
Where("applications.user_id = ? AND extension_api_keys.id IN ?", userID, req.IDs).
|
||||
Update("status", req.Status)
|
||||
|
||||
if result.Error != nil {
|
||||
response.Error(c, 500, "批量更新状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"updated_count": result.RowsAffected})
|
||||
}
|
||||
|
||||
func handleBatchDeleteAPIKeys(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var req struct {
|
||||
IDs []uint `json:"ids" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.IDs) == 0 {
|
||||
response.Error(c, 400, "请选择要删除的API密钥")
|
||||
return
|
||||
}
|
||||
|
||||
result := database.DB.Where("id IN (?) AND application_id IN (SELECT id FROM applications WHERE user_id = ?)", req.IDs, userID).
|
||||
Delete(&model.ExtensionAPIKey{})
|
||||
|
||||
if result.Error != nil {
|
||||
response.Error(c, 500, "批量删除失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"deleted_count": result.RowsAffected})
|
||||
}
|
||||
@@ -0,0 +1,526 @@
|
||||
package developer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/service"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type FinanceRecordResponse struct {
|
||||
ID uint `json:"id"`
|
||||
OrderNo string `json:"order_no"`
|
||||
Type string `json:"type"`
|
||||
UserID uint `json:"user_id"`
|
||||
AppID uint `json:"app_id"`
|
||||
Amount float64 `json:"amount"`
|
||||
Detail string `json:"detail"`
|
||||
Status string `json:"status"`
|
||||
PaymentType string `json:"payment_type"`
|
||||
Remark string `json:"remark"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
User *struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
} `json:"user,omitempty"`
|
||||
}
|
||||
|
||||
func SetupFinanceRoutes(r *gin.RouterGroup) {
|
||||
finance := r.Group("/finance")
|
||||
{
|
||||
finance.GET("/stats", handleGetFinanceStatistics)
|
||||
finance.GET("/statistics", handleGetFinanceStatistics)
|
||||
finance.GET("/recharge-records", handleGetRechargeRecords)
|
||||
finance.GET("/consumption-records", handleGetConsumptionRecords)
|
||||
finance.GET("/records", handleGetFinanceRecords)
|
||||
finance.DELETE("/records/:id", handleDeleteFinanceRecord)
|
||||
finance.POST("/records/batch-delete", handleBatchDeleteFinanceRecords)
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetFinanceStatistics(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
response.Error(c, 401, "未授权")
|
||||
return
|
||||
}
|
||||
|
||||
var appIDs []uint
|
||||
database.DB.Model(&model.Application{}).Where("user_id = ?", userID).Pluck("id", &appIDs)
|
||||
|
||||
if len(appIDs) == 0 {
|
||||
response.Success(c, gin.H{
|
||||
"total_income": 0,
|
||||
"total_expense": 0,
|
||||
"net_profit": 0,
|
||||
"monthly_transactions": 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var appUserIDs []uint
|
||||
database.DB.Model(&model.AppUser{}).Where("application_id IN ?", appIDs).Pluck("id", &appUserIDs)
|
||||
|
||||
if len(appUserIDs) == 0 {
|
||||
response.Success(c, gin.H{
|
||||
"total_income": 0,
|
||||
"total_expense": 0,
|
||||
"net_profit": 0,
|
||||
"monthly_transactions": 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var totalIncome float64
|
||||
var totalExpense float64
|
||||
|
||||
database.DB.Model(&model.RechargeRecord{}).
|
||||
Where("user_id IN ? AND status = ?", appUserIDs, "success").
|
||||
Select("COALESCE(SUM(amount), 0)").
|
||||
Scan(&totalIncome)
|
||||
|
||||
database.DB.Model(&model.ConsumptionRecord{}).
|
||||
Where("user_id IN ? AND status = ?", appUserIDs, "success").
|
||||
Select("COALESCE(SUM(amount), 0)").
|
||||
Scan(&totalExpense)
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"total_income": totalIncome,
|
||||
"total_expense": totalExpense,
|
||||
"net_profit": totalIncome - totalExpense,
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetRechargeRecords(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
response.Error(c, 401, "未授权")
|
||||
return
|
||||
}
|
||||
|
||||
var appIDs []uint
|
||||
database.DB.Model(&model.Application{}).Where("user_id = ?", userID).Pluck("id", &appIDs)
|
||||
|
||||
if len(appIDs) == 0 {
|
||||
response.Success(c, gin.H{
|
||||
"records": []model.RechargeRecord{},
|
||||
"total": 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var appUserIDs []uint
|
||||
database.DB.Model(&model.AppUser{}).Where("application_id IN ?", appIDs).Pluck("id", &appUserIDs)
|
||||
|
||||
if len(appUserIDs) == 0 {
|
||||
response.Success(c, gin.H{
|
||||
"records": []model.RechargeRecord{},
|
||||
"total": 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var records []model.RechargeRecord
|
||||
query := database.DB.Model(&model.RechargeRecord{}).Preload("AppUser").Where("user_id IN ?", appUserIDs)
|
||||
|
||||
search := c.Query("search")
|
||||
status := c.Query("status")
|
||||
startDate := c.Query("start_date")
|
||||
endDate := c.Query("end_date")
|
||||
|
||||
if search != "" {
|
||||
query = query.Where("order_no LIKE ? OR card_code LIKE ?", "%"+search+"%", "%"+search+"%")
|
||||
}
|
||||
if status != "" && status != "all" {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
if startDate != "" {
|
||||
query = query.Where("created_at >= ?", startDate)
|
||||
}
|
||||
if endDate != "" {
|
||||
query = query.Where("created_at <= ?", endDate+" 23:59:59")
|
||||
}
|
||||
|
||||
if err := query.Order("created_at DESC").Find(&records).Error; err != nil {
|
||||
response.Error(c, 500, "获取充值记录失败")
|
||||
return
|
||||
}
|
||||
response.Success(c, gin.H{
|
||||
"records": records,
|
||||
"total": len(records),
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetConsumptionRecords(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
response.Error(c, 401, "未授权")
|
||||
return
|
||||
}
|
||||
|
||||
var appIDs []uint
|
||||
database.DB.Model(&model.Application{}).Where("user_id = ?", userID).Pluck("id", &appIDs)
|
||||
|
||||
if len(appIDs) == 0 {
|
||||
response.Success(c, gin.H{
|
||||
"records": []model.ConsumptionRecord{},
|
||||
"total": 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var appUserIDs []uint
|
||||
database.DB.Model(&model.AppUser{}).Where("application_id IN ?", appIDs).Pluck("id", &appUserIDs)
|
||||
|
||||
if len(appUserIDs) == 0 {
|
||||
response.Success(c, gin.H{
|
||||
"records": []model.ConsumptionRecord{},
|
||||
"total": 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var records []model.ConsumptionRecord
|
||||
query := database.DB.Model(&model.ConsumptionRecord{}).Preload("AppUser").Where("user_id IN ?", appUserIDs)
|
||||
|
||||
search := c.Query("search")
|
||||
recordType := c.Query("type")
|
||||
status := c.Query("status")
|
||||
startDate := c.Query("start_date")
|
||||
endDate := c.Query("end_date")
|
||||
|
||||
if search != "" {
|
||||
query = query.Where("order_no LIKE ? OR content LIKE ?", "%"+search+"%", "%"+search+"%")
|
||||
}
|
||||
if recordType != "" && recordType != "all" {
|
||||
query = query.Where("type = ?", recordType)
|
||||
}
|
||||
if status != "" && status != "all" {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
if startDate != "" {
|
||||
query = query.Where("created_at >= ?", startDate)
|
||||
}
|
||||
if endDate != "" {
|
||||
query = query.Where("created_at <= ?", endDate+" 23:59:59")
|
||||
}
|
||||
|
||||
if err := query.Order("created_at DESC").Find(&records).Error; err != nil {
|
||||
response.Error(c, 500, "获取消费记录失败")
|
||||
return
|
||||
}
|
||||
response.Success(c, gin.H{
|
||||
"records": records,
|
||||
"total": len(records),
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetFinanceRecords(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
response.Error(c, 401, "未授权")
|
||||
return
|
||||
}
|
||||
|
||||
var appIDs []uint
|
||||
database.DB.Model(&model.Application{}).Where("user_id = ?", userID).Pluck("id", &appIDs)
|
||||
|
||||
if len(appIDs) == 0 {
|
||||
response.Success(c, gin.H{
|
||||
"recharge_records": []FinanceRecordResponse{},
|
||||
"consumption_records": []FinanceRecordResponse{},
|
||||
"total": 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var appUserIDs []uint
|
||||
database.DB.Model(&model.AppUser{}).Where("application_id IN ?", appIDs).Pluck("id", &appUserIDs)
|
||||
|
||||
if len(appUserIDs) == 0 {
|
||||
response.Success(c, gin.H{
|
||||
"recharge_records": []FinanceRecordResponse{},
|
||||
"consumption_records": []FinanceRecordResponse{},
|
||||
"total": 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
page := c.DefaultQuery("page", "1")
|
||||
pageSize := c.DefaultQuery("page_size", "10")
|
||||
search := c.Query("search")
|
||||
recordType := c.Query("type")
|
||||
status := c.Query("status")
|
||||
startDate := c.Query("start_date")
|
||||
endDate := c.Query("end_date")
|
||||
|
||||
var rechargeRecords []model.RechargeRecord
|
||||
rechargeQuery := database.DB.Model(&model.RechargeRecord{}).Preload("AppUser").Where("user_id IN ?", appUserIDs)
|
||||
|
||||
if search != "" {
|
||||
rechargeQuery = rechargeQuery.Where("order_no LIKE ? OR card_code LIKE ?", "%"+search+"%", "%"+search+"%")
|
||||
}
|
||||
if recordType == "recharge" || recordType == "" || recordType == "all" {
|
||||
if status != "" && status != "all" {
|
||||
rechargeQuery = rechargeQuery.Where("status = ?", status)
|
||||
}
|
||||
if startDate != "" {
|
||||
rechargeQuery = rechargeQuery.Where("created_at >= ?", startDate)
|
||||
}
|
||||
if endDate != "" {
|
||||
rechargeQuery = rechargeQuery.Where("created_at <= ?", endDate+" 23:59:59")
|
||||
}
|
||||
if err := rechargeQuery.Order("created_at DESC").Find(&rechargeRecords).Error; err != nil {
|
||||
response.Error(c, 500, "获取充值记录失败")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var consumptionRecords []model.ConsumptionRecord
|
||||
consumptionQuery := database.DB.Model(&model.ConsumptionRecord{}).Preload("AppUser").Where("user_id IN ?", appUserIDs)
|
||||
|
||||
if search != "" {
|
||||
consumptionQuery = consumptionQuery.Where("order_no LIKE ? OR content LIKE ?", "%"+search+"%", "%"+search+"%")
|
||||
}
|
||||
if recordType == "consumption" || recordType == "" || recordType == "all" {
|
||||
if status != "" && status != "all" {
|
||||
consumptionQuery = consumptionQuery.Where("status = ?", status)
|
||||
}
|
||||
if startDate != "" {
|
||||
consumptionQuery = consumptionQuery.Where("created_at >= ?", startDate)
|
||||
}
|
||||
if endDate != "" {
|
||||
consumptionQuery = consumptionQuery.Where("created_at <= ?", endDate+" 23:59:59")
|
||||
}
|
||||
if err := consumptionQuery.Order("created_at DESC").Find(&consumptionRecords).Error; err != nil {
|
||||
response.Error(c, 500, "获取消费记录失败")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var allRecords []FinanceRecordResponse
|
||||
for _, r := range rechargeRecords {
|
||||
var user *struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
if r.AppUser != nil && r.AppUser.ID != 0 {
|
||||
user = &struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
}{
|
||||
ID: r.AppUser.ID,
|
||||
Username: r.AppUser.Username,
|
||||
Email: r.AppUser.Email,
|
||||
}
|
||||
}
|
||||
allRecords = append(allRecords, FinanceRecordResponse{
|
||||
ID: r.ID,
|
||||
OrderNo: r.OrderNo,
|
||||
Type: "recharge",
|
||||
UserID: r.UserID,
|
||||
AppID: r.AppUser.ApplicationID,
|
||||
Amount: r.Amount,
|
||||
Detail: r.CardCode,
|
||||
Status: r.Status,
|
||||
PaymentType: r.PaymentType,
|
||||
Remark: r.Remark,
|
||||
CreatedAt: r.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
User: user,
|
||||
})
|
||||
}
|
||||
|
||||
for _, r := range consumptionRecords {
|
||||
var user *struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
if r.AppUser != nil && r.AppUser.ID != 0 {
|
||||
user = &struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
}{
|
||||
ID: r.AppUser.ID,
|
||||
Username: r.AppUser.Username,
|
||||
Email: r.AppUser.Email,
|
||||
}
|
||||
}
|
||||
allRecords = append(allRecords, FinanceRecordResponse{
|
||||
ID: r.ID,
|
||||
OrderNo: r.OrderNo,
|
||||
Type: "consumption",
|
||||
UserID: r.UserID,
|
||||
AppID: r.AppUser.ApplicationID,
|
||||
Amount: r.Amount,
|
||||
Detail: r.Content,
|
||||
Status: r.Status,
|
||||
PaymentType: r.PaymentType,
|
||||
Remark: r.Remark,
|
||||
CreatedAt: r.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
User: user,
|
||||
})
|
||||
}
|
||||
|
||||
total := len(allRecords)
|
||||
start := 0
|
||||
end := total
|
||||
if p, err := parseInt(page); err == nil && p > 0 {
|
||||
if ps, err := parseInt(pageSize); err == nil && ps > 0 {
|
||||
start = (p - 1) * ps
|
||||
end = start + ps
|
||||
if start > total {
|
||||
start = total
|
||||
}
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if start > end {
|
||||
start = end
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"records": allRecords[start:end],
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
|
||||
func parseInt(s string) (int, error) {
|
||||
var result int
|
||||
_, err := fmt.Sscanf(s, "%d", &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func handleDeleteFinanceRecord(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
response.Error(c, 401, "未授权")
|
||||
return
|
||||
}
|
||||
|
||||
recordID := c.Param("id")
|
||||
recordType := c.Query("type")
|
||||
|
||||
var appIDs []uint
|
||||
database.DB.Model(&model.Application{}).Where("user_id = ?", userID).Pluck("id", &appIDs)
|
||||
if len(appIDs) == 0 {
|
||||
response.Error(c, 404, "记录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var appUserIDs []uint
|
||||
database.DB.Model(&model.AppUser{}).Where("application_id IN ?", appIDs).Pluck("id", &appUserIDs)
|
||||
if len(appUserIDs) == 0 {
|
||||
response.Error(c, 404, "记录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var deletedType string
|
||||
var deletedAmount float64
|
||||
|
||||
if recordType == "recharge" {
|
||||
var record model.RechargeRecord
|
||||
if err := database.DB.Where("id = ? AND user_id IN ?", recordID, appUserIDs).First(&record).Error; err != nil {
|
||||
response.Error(c, 404, "记录不存在")
|
||||
return
|
||||
}
|
||||
if err := database.DB.Delete(&record).Error; err != nil {
|
||||
response.Error(c, 500, "删除失败")
|
||||
return
|
||||
}
|
||||
deletedType = "充值记录"
|
||||
deletedAmount = record.Amount
|
||||
} else if recordType == "consumption" {
|
||||
var record model.ConsumptionRecord
|
||||
if err := database.DB.Where("id = ? AND user_id IN ?", recordID, appUserIDs).First(&record).Error; err != nil {
|
||||
response.Error(c, 404, "记录不存在")
|
||||
return
|
||||
}
|
||||
if err := database.DB.Delete(&record).Error; err != nil {
|
||||
response.Error(c, 500, "删除失败")
|
||||
return
|
||||
}
|
||||
deletedType = "消费记录"
|
||||
deletedAmount = record.Amount
|
||||
} else {
|
||||
var rechargeRecord model.RechargeRecord
|
||||
var consumptionRecord model.ConsumptionRecord
|
||||
|
||||
if err := database.DB.Where("id = ? AND user_id IN ?", recordID, appUserIDs).First(&rechargeRecord).Error; err == nil {
|
||||
database.DB.Delete(&rechargeRecord)
|
||||
deletedType = "充值记录"
|
||||
deletedAmount = rechargeRecord.Amount
|
||||
service.LogOperation(c, "delete", "finance_record", nil, fmt.Sprintf("删除%s: %.2f", deletedType, deletedAmount), nil)
|
||||
response.Success(c, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DB.Where("id = ? AND user_id IN ?", recordID, appUserIDs).First(&consumptionRecord).Error; err == nil {
|
||||
database.DB.Delete(&consumptionRecord)
|
||||
deletedType = "消费记录"
|
||||
deletedAmount = consumptionRecord.Amount
|
||||
service.LogOperation(c, "delete", "finance_record", nil, fmt.Sprintf("删除%s: %.2f", deletedType, deletedAmount), nil)
|
||||
response.Success(c, nil)
|
||||
return
|
||||
}
|
||||
|
||||
response.Error(c, 404, "记录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
service.LogOperation(c, "delete", "finance_record", nil, fmt.Sprintf("删除%s: %.2f", deletedType, deletedAmount), nil)
|
||||
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
func handleBatchDeleteFinanceRecords(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
response.Error(c, 401, "未授权")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
IDs []uint `json:"ids"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.IDs) == 0 {
|
||||
response.Error(c, 400, "请选择要删除的记录")
|
||||
return
|
||||
}
|
||||
|
||||
var appIDs []uint
|
||||
database.DB.Model(&model.Application{}).Where("user_id = ?", userID).Pluck("id", &appIDs)
|
||||
if len(appIDs) == 0 {
|
||||
response.Error(c, 404, "记录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var appUserIDs []uint
|
||||
database.DB.Model(&model.AppUser{}).Where("application_id IN ?", appIDs).Pluck("id", &appUserIDs)
|
||||
if len(appUserIDs) == 0 {
|
||||
response.Error(c, 404, "记录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
database.DB.Where("id IN ? AND user_id IN ?", req.IDs, appUserIDs).Delete(&model.RechargeRecord{})
|
||||
database.DB.Where("id IN ? AND user_id IN ?", req.IDs, appUserIDs).Delete(&model.ConsumptionRecord{})
|
||||
|
||||
service.LogOperation(c, "batch_delete", "finance_record", nil, fmt.Sprintf("批量删除财务记录: %d条", len(req.IDs)), nil)
|
||||
|
||||
response.Success(c, gin.H{"deleted": len(req.IDs)})
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package developer
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupLogRoutes(r *gin.RouterGroup) {
|
||||
logs := r.Group("/logs")
|
||||
{
|
||||
logs.GET("", handleGetLogs)
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetLogs(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var appIDs []uint
|
||||
database.DB.Model(&model.Application{}).Where("user_id = ?", userID).Pluck("id", &appIDs)
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
logType := c.Query("type")
|
||||
status := c.Query("status")
|
||||
applicationID := c.Query("application_id")
|
||||
startDate := c.Query("start_date")
|
||||
endDate := c.Query("end_date")
|
||||
search := c.Query("search")
|
||||
|
||||
var logs []model.Log
|
||||
var total int64
|
||||
|
||||
query := database.DB.Model(&model.Log{})
|
||||
|
||||
if len(appIDs) > 0 {
|
||||
query = query.Where("application_id IN ? OR user_id = ?", appIDs, userID)
|
||||
} else {
|
||||
query = query.Where("user_id = ?", userID)
|
||||
}
|
||||
|
||||
if logType != "" && logType != "all" {
|
||||
query = query.Where("log_type = ?", logType)
|
||||
}
|
||||
|
||||
if status != "" && status != "all" {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
|
||||
if applicationID != "" && applicationID != "all" {
|
||||
query = query.Where("application_id = ?", applicationID)
|
||||
}
|
||||
|
||||
if startDate != "" {
|
||||
query = query.Where("created_at >= ?", startDate+" 00:00:00")
|
||||
}
|
||||
|
||||
if endDate != "" {
|
||||
query = query.Where("created_at <= ?", endDate+" 23:59:59")
|
||||
}
|
||||
|
||||
if search != "" {
|
||||
query = query.Where("action LIKE ? OR details LIKE ? OR resource LIKE ?", "%"+search+"%", "%"+search+"%", "%"+search+"%")
|
||||
}
|
||||
|
||||
query.Count(&total)
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Preload("User").Preload("Application").Preload("AppUser").Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&logs).Error; err != nil {
|
||||
response.Error(c, 500, "获取日志失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"logs": logs,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
"total_page": (total + int64(pageSize) - 1) / int64(pageSize),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
package developer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupOrderRoutes(r *gin.RouterGroup) {
|
||||
r.GET("/orders", handleGetOrders)
|
||||
r.GET("/orders/:id", handleGetOrder)
|
||||
r.POST("/orders/:id/refund", handleRefundOrder)
|
||||
r.GET("/orders/stats", handleGetOrderStats)
|
||||
}
|
||||
|
||||
func handleGetOrders(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
orderType := c.Query("order_type")
|
||||
status := c.Query("status")
|
||||
applicationID := c.Query("application_id")
|
||||
search := c.Query("search")
|
||||
startDate := c.Query("start_date")
|
||||
endDate := c.Query("end_date")
|
||||
|
||||
var orders []model.Order
|
||||
var total int64
|
||||
|
||||
query := database.DB.Model(&model.Order{}).
|
||||
Joins("LEFT JOIN applications ON orders.application_id = applications.id").
|
||||
Where("applications.user_id = ? OR orders.application_id IS NULL", userID)
|
||||
|
||||
if orderType != "" {
|
||||
query = query.Where("orders.order_type = ?", orderType)
|
||||
}
|
||||
|
||||
if status != "" {
|
||||
query = query.Where("orders.status = ?", status)
|
||||
}
|
||||
|
||||
if applicationID != "" && applicationID != "all" {
|
||||
query = query.Where("orders.application_id = ?", applicationID)
|
||||
}
|
||||
|
||||
if search != "" {
|
||||
query = query.Where("orders.order_no LIKE ? OR orders.title LIKE ?", "%"+search+"%", "%"+search+"%")
|
||||
}
|
||||
|
||||
if startDate != "" {
|
||||
query = query.Where("orders.created_at >= ?", startDate+" 00:00:00")
|
||||
}
|
||||
|
||||
if endDate != "" {
|
||||
query = query.Where("orders.created_at <= ?", endDate+" 23:59:59")
|
||||
}
|
||||
|
||||
query.Count(&total)
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Preload("User").Preload("Application").
|
||||
Order("orders.created_at DESC").
|
||||
Offset(offset).Limit(pageSize).
|
||||
Find(&orders).Error; err != nil {
|
||||
response.Error(c, 500, "获取订单列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"orders": orders,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
"total_page": (total + int64(pageSize) - 1) / int64(pageSize),
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetOrder(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
orderID := c.Param("id")
|
||||
|
||||
var order model.Order
|
||||
if err := database.DB.Model(&model.Order{}).
|
||||
Joins("LEFT JOIN applications ON orders.application_id = applications.id").
|
||||
Where("orders.id = ? AND (applications.user_id = ? OR orders.application_id IS NULL)", orderID, userID).
|
||||
Preload("User").Preload("Application").
|
||||
First(&order).Error; err != nil {
|
||||
response.Error(c, 404, "订单不存在")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, order)
|
||||
}
|
||||
|
||||
func handleRefundOrder(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
orderID := c.Param("id")
|
||||
|
||||
var req struct {
|
||||
Reason string `json:"reason" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var order model.Order
|
||||
if err := database.DB.Model(&model.Order{}).
|
||||
Joins("LEFT JOIN applications ON orders.application_id = applications.id").
|
||||
Where("orders.id = ? AND (applications.user_id = ? OR orders.application_id IS NULL)", orderID, userID).
|
||||
First(&order).Error; err != nil {
|
||||
response.Error(c, 404, "订单不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if order.Status != "paid" {
|
||||
response.Error(c, 400, "只能退款已支付的订单")
|
||||
return
|
||||
}
|
||||
|
||||
tx := database.DB.Begin()
|
||||
|
||||
now := time.Now()
|
||||
order.Status = "refunded"
|
||||
order.RefundAt = &now
|
||||
order.RefundReason = req.Reason
|
||||
|
||||
if err := tx.Save(&order).Error; err != nil {
|
||||
tx.Rollback()
|
||||
response.Error(c, 500, "退款失败")
|
||||
return
|
||||
}
|
||||
|
||||
switch order.OrderType {
|
||||
case "card_recharge":
|
||||
var rechargeRecord model.RechargeRecord
|
||||
if err := tx.Where("order_no = ?", order.OrderNo).First(&rechargeRecord).Error; err == nil {
|
||||
rechargeRecord.Status = "refunded"
|
||||
tx.Save(&rechargeRecord)
|
||||
}
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "退款成功",
|
||||
"order": order,
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetOrderStats(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
applicationID := c.Query("application_id")
|
||||
startDate := c.Query("start_date")
|
||||
endDate := c.Query("end_date")
|
||||
|
||||
query := database.DB.Model(&model.Order{}).
|
||||
Joins("LEFT JOIN applications ON orders.application_id = applications.id").
|
||||
Where("applications.user_id = ? OR orders.application_id IS NULL", userID)
|
||||
|
||||
if applicationID != "" && applicationID != "all" {
|
||||
query = query.Where("orders.application_id = ?", applicationID)
|
||||
}
|
||||
|
||||
if startDate != "" {
|
||||
query = query.Where("orders.created_at >= ?", startDate+" 00:00:00")
|
||||
}
|
||||
|
||||
if endDate != "" {
|
||||
query = query.Where("orders.created_at <= ?", endDate+" 23:59:59")
|
||||
}
|
||||
|
||||
var totalOrders, pendingOrders, paidOrders, refundedOrders int64
|
||||
var totalAmount, paidAmount, refundedAmount float64
|
||||
|
||||
query.Count(&totalOrders)
|
||||
|
||||
database.DB.Model(&model.Order{}).
|
||||
Joins("LEFT JOIN applications ON orders.application_id = applications.id").
|
||||
Where("applications.user_id = ? OR orders.application_id IS NULL", userID).
|
||||
Where("orders.status = ?", "pending").
|
||||
Count(&pendingOrders)
|
||||
|
||||
database.DB.Model(&model.Order{}).
|
||||
Joins("LEFT JOIN applications ON orders.application_id = applications.id").
|
||||
Where("applications.user_id = ? OR orders.application_id IS NULL", userID).
|
||||
Where("orders.status = ?", "paid").
|
||||
Count(&paidOrders)
|
||||
|
||||
database.DB.Model(&model.Order{}).
|
||||
Joins("LEFT JOIN applications ON orders.application_id = applications.id").
|
||||
Where("applications.user_id = ? OR orders.application_id IS NULL", userID).
|
||||
Where("orders.status = ?", "refunded").
|
||||
Count(&refundedOrders)
|
||||
|
||||
database.DB.Model(&model.Order{}).
|
||||
Joins("LEFT JOIN applications ON orders.application_id = applications.id").
|
||||
Where("applications.user_id = ? OR orders.application_id IS NULL", userID).
|
||||
Where("orders.status IN ?", []string{"paid", "refunded"}).
|
||||
Select("COALESCE(SUM(amount), 0)").
|
||||
Scan(&totalAmount)
|
||||
|
||||
database.DB.Model(&model.Order{}).
|
||||
Joins("LEFT JOIN applications ON orders.application_id = applications.id").
|
||||
Where("applications.user_id = ? OR orders.application_id IS NULL", userID).
|
||||
Where("orders.status = ?", "paid").
|
||||
Select("COALESCE(SUM(amount), 0)").
|
||||
Scan(&paidAmount)
|
||||
|
||||
database.DB.Model(&model.Order{}).
|
||||
Joins("LEFT JOIN applications ON orders.application_id = applications.id").
|
||||
Where("applications.user_id = ? OR orders.application_id IS NULL", userID).
|
||||
Where("orders.status = ?", "refunded").
|
||||
Select("COALESCE(SUM(amount), 0)").
|
||||
Scan(&refundedAmount)
|
||||
|
||||
var typeStats []struct {
|
||||
OrderType string
|
||||
Count int64
|
||||
TotalAmount float64
|
||||
}
|
||||
|
||||
database.DB.Model(&model.Order{}).
|
||||
Joins("LEFT JOIN applications ON orders.application_id = applications.id").
|
||||
Where("applications.user_id = ? OR orders.application_id IS NULL", userID).
|
||||
Select("order_type, COUNT(*) as count, COALESCE(SUM(amount), 0) as total_amount").
|
||||
Group("order_type").
|
||||
Scan(&typeStats)
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"total_orders": totalOrders,
|
||||
"pending_orders": pendingOrders,
|
||||
"paid_orders": paidOrders,
|
||||
"refunded_orders": refundedOrders,
|
||||
"total_amount": totalAmount,
|
||||
"paid_amount": paidAmount,
|
||||
"refunded_amount": refundedAmount,
|
||||
"type_stats": typeStats,
|
||||
})
|
||||
}
|
||||
|
||||
func CreateOrder(orderType string, userID uint, applicationID *uint, title string, amount float64, paymentType string, description string) (*model.Order, error) {
|
||||
orderNo := fmt.Sprintf("ORD%d%d", time.Now().Unix(), userID)
|
||||
|
||||
order := model.Order{
|
||||
OrderNo: orderNo,
|
||||
UserID: userID,
|
||||
ApplicationID: applicationID,
|
||||
OrderType: orderType,
|
||||
Title: title,
|
||||
Amount: amount,
|
||||
PaymentType: paymentType,
|
||||
Status: "pending",
|
||||
Description: description,
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&order).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &order, nil
|
||||
}
|
||||
|
||||
func PayOrder(orderNo string) error {
|
||||
var order model.Order
|
||||
if err := database.DB.Where("order_no = ?", orderNo).First(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
order.Status = "paid"
|
||||
order.PaymentAt = &now
|
||||
|
||||
return database.DB.Save(&order).Error
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
package developer
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func SetupProfileRoutes(r *gin.RouterGroup) {
|
||||
profile := r.Group("/profile")
|
||||
{
|
||||
profile.GET("", handleGetProfile)
|
||||
profile.PUT("", handleUpdateProfile)
|
||||
profile.PUT("/password", handleChangePassword)
|
||||
profile.POST("/avatar", handleUploadAvatar)
|
||||
profile.POST("/api-token", handleGenerateApiToken)
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetProfile(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var user model.User
|
||||
if err := database.DB.First(&user, userID).Error; err != nil {
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
profile := struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Phone string `json:"phone"`
|
||||
Avatar string `json:"avatar"`
|
||||
Role string `json:"role"`
|
||||
Status string `json:"status"`
|
||||
ApiToken string `json:"api_token"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
LastLoginAt *time.Time `json:"last_login_at"`
|
||||
}{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Email: "",
|
||||
Phone: "",
|
||||
Avatar: user.Avatar,
|
||||
Role: user.Role,
|
||||
Status: user.Status,
|
||||
ApiToken: user.ApiToken,
|
||||
CreatedAt: user.CreatedAt,
|
||||
LastLoginAt: user.LastLoginAt,
|
||||
}
|
||||
|
||||
if user.Email != nil {
|
||||
profile.Email = *user.Email
|
||||
}
|
||||
|
||||
subscription := getSubscriptionInfo(&user)
|
||||
|
||||
transactions := getRecentTransactions(userID)
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"user": profile,
|
||||
"subscription": subscription,
|
||||
"transactions": transactions,
|
||||
})
|
||||
}
|
||||
|
||||
func getSubscriptionInfo(user *model.User) gin.H {
|
||||
var pkg model.Package
|
||||
var packagePermission model.PackagePermission
|
||||
|
||||
defaultQuota := 10000
|
||||
defaultStorage := int64(100 * 1024 * 1024)
|
||||
|
||||
if user.CurrentPackageID != nil {
|
||||
if err := database.DB.First(&pkg, *user.CurrentPackageID).Error; err == nil {
|
||||
database.DB.Where("package_id = ?", pkg.ID).First(&packagePermission)
|
||||
}
|
||||
}
|
||||
|
||||
planName := "基础版"
|
||||
if pkg.ID > 0 {
|
||||
planName = pkg.Name
|
||||
}
|
||||
|
||||
apiQuota := defaultQuota
|
||||
if packagePermission.MaxApiCalls > 0 {
|
||||
apiQuota = packagePermission.MaxApiCalls
|
||||
}
|
||||
|
||||
storageQuota := defaultStorage
|
||||
if packagePermission.MaxStorage > 0 {
|
||||
storageQuota = int64(packagePermission.MaxStorage) * 1024 * 1024
|
||||
}
|
||||
|
||||
var expireDate string
|
||||
if pkg.Period == "monthly" {
|
||||
expireDate = time.Now().AddDate(0, 1, 0).Format("2006-01-02")
|
||||
} else if pkg.Period == "yearly" {
|
||||
expireDate = time.Now().AddDate(1, 0, 0).Format("2006-01-02")
|
||||
} else {
|
||||
expireDate = "永久有效"
|
||||
}
|
||||
|
||||
return gin.H{
|
||||
"plan": planName,
|
||||
"status": "active",
|
||||
"expire_date": expireDate,
|
||||
"api_quota": apiQuota,
|
||||
"api_used": user.ApiCallsUsed,
|
||||
"storage_quota": storageQuota,
|
||||
"storage_used": user.StorageUsed,
|
||||
}
|
||||
}
|
||||
|
||||
func getRecentTransactions(userID uint) []gin.H {
|
||||
var orders []model.Order
|
||||
database.DB.Where("user_id = ? AND status = ?", userID, "paid").
|
||||
Order("created_at DESC").
|
||||
Limit(5).
|
||||
Find(&orders)
|
||||
|
||||
transactions := make([]gin.H, 0, len(orders))
|
||||
for _, order := range orders {
|
||||
txType := "consume"
|
||||
if order.OrderType == "user_recharge" || order.OrderType == "card_recharge" {
|
||||
txType = "recharge"
|
||||
} else if order.OrderType == "refund" {
|
||||
txType = "refund"
|
||||
}
|
||||
|
||||
transactions = append(transactions, gin.H{
|
||||
"id": order.ID,
|
||||
"type": txType,
|
||||
"amount": order.Amount,
|
||||
"description": order.Title,
|
||||
"created_at": order.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
return transactions
|
||||
}
|
||||
|
||||
type UpdateProfileRequest struct {
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Phone string `json:"phone"`
|
||||
}
|
||||
|
||||
func handleUpdateProfile(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var req UpdateProfileRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "请求参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Username == "" {
|
||||
response.Error(c, 400, "用户名不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Email == "" {
|
||||
response.Error(c, 400, "邮箱不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
var user model.User
|
||||
if err := database.DB.First(&user, userID).Error; err != nil {
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var existingUser model.User
|
||||
if err := database.DB.Where("username = ? AND id != ?", req.Username, userID).First(&existingUser).Error; err == nil {
|
||||
response.Error(c, 400, "用户名已被使用")
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DB.Where("email = ? AND id != ?", req.Email, userID).First(&existingUser).Error; err == nil {
|
||||
response.Error(c, 400, "邮箱已被使用")
|
||||
return
|
||||
}
|
||||
|
||||
user.Username = req.Username
|
||||
email := req.Email
|
||||
user.Email = &email
|
||||
|
||||
if err := database.DB.Save(&user).Error; err != nil {
|
||||
log.Printf("更新用户信息失败: %v", err)
|
||||
response.Error(c, 500, "更新用户信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"user": gin.H{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"avatar": user.Avatar,
|
||||
"role": user.Role,
|
||||
"status": user.Status,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
type ChangePasswordRequest struct {
|
||||
CurrentPassword string `json:"current_password"`
|
||||
NewPassword string `json:"new_password"`
|
||||
}
|
||||
|
||||
func handleChangePassword(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var req ChangePasswordRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "请求参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if req.CurrentPassword == "" {
|
||||
response.Error(c, 400, "请输入当前密码")
|
||||
return
|
||||
}
|
||||
|
||||
if req.NewPassword == "" {
|
||||
response.Error(c, 400, "请输入新密码")
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.NewPassword) < 6 {
|
||||
response.Error(c, 400, "密码长度至少6位")
|
||||
return
|
||||
}
|
||||
|
||||
var user model.User
|
||||
if err := database.DB.First(&user, userID).Error; err != nil {
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.CurrentPassword)); err != nil {
|
||||
response.Error(c, 400, "当前密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
log.Printf("密码加密失败: %v", err)
|
||||
response.Error(c, 500, "密码加密失败")
|
||||
return
|
||||
}
|
||||
|
||||
user.Password = string(hashedPassword)
|
||||
if err := database.DB.Save(&user).Error; err != nil {
|
||||
log.Printf("更新密码失败: %v", err)
|
||||
response.Error(c, 500, "更新密码失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "密码修改成功"})
|
||||
}
|
||||
|
||||
func handleUploadAvatar(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
file, header, err := c.Request.FormFile("avatar")
|
||||
if err != nil {
|
||||
response.Error(c, 400, "请选择要上传的文件")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(header.Filename))
|
||||
allowedExts := map[string]bool{
|
||||
".jpg": true,
|
||||
".jpeg": true,
|
||||
".png": true,
|
||||
".gif": true,
|
||||
".webp": true,
|
||||
}
|
||||
|
||||
if !allowedExts[ext] {
|
||||
response.Error(c, 400, "不支持的文件格式,仅支持 JPG、PNG、GIF、WEBP")
|
||||
return
|
||||
}
|
||||
|
||||
const maxSize = 2 * 1024 * 1024
|
||||
if header.Size > maxSize {
|
||||
response.Error(c, 400, "文件大小不能超过2MB")
|
||||
return
|
||||
}
|
||||
|
||||
uploadDir := "uploads/avatars"
|
||||
if err := os.MkdirAll(uploadDir, 0755); err != nil {
|
||||
log.Printf("创建上传目录失败: %v", err)
|
||||
response.Error(c, 500, "创建上传目录失败")
|
||||
return
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("%d_%d%s", userID, time.Now().UnixNano(), ext)
|
||||
filePath := filepath.Join(uploadDir, filename)
|
||||
|
||||
dst, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
log.Printf("创建文件失败: %v", err)
|
||||
response.Error(c, 500, "创建文件失败")
|
||||
return
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
if _, err := io.Copy(dst, file); err != nil {
|
||||
log.Printf("保存文件失败: %v", err)
|
||||
response.Error(c, 500, "保存文件失败")
|
||||
return
|
||||
}
|
||||
|
||||
avatarURL := "/uploads/avatars/" + filename
|
||||
|
||||
var user model.User
|
||||
if err := database.DB.First(&user, userID).Error; err != nil {
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Avatar != "" && strings.HasPrefix(user.Avatar, "/uploads/avatars/") {
|
||||
oldPath := "." + user.Avatar
|
||||
if _, err := os.Stat(oldPath); err == nil {
|
||||
os.Remove(oldPath)
|
||||
}
|
||||
}
|
||||
|
||||
user.Avatar = avatarURL
|
||||
if err := database.DB.Save(&user).Error; err != nil {
|
||||
log.Printf("更新头像失败: %v", err)
|
||||
response.Error(c, 500, "更新头像失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"avatar": avatarURL})
|
||||
}
|
||||
|
||||
func handleGenerateApiToken(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var user model.User
|
||||
if err := database.DB.First(&user, userID).Error; err != nil {
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
bytes := make([]byte, 32)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
log.Printf("生成API Token失败: %v", err)
|
||||
response.Error(c, 500, "生成API Token失败")
|
||||
return
|
||||
}
|
||||
apiToken := hex.EncodeToString(bytes)
|
||||
|
||||
user.ApiToken = apiToken
|
||||
if err := database.DB.Save(&user).Error; err != nil {
|
||||
log.Printf("保存API Token失败: %v", err)
|
||||
response.Error(c, 500, "保存API Token失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"api_token": apiToken,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
package developer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/service"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupTicketRoutes(r *gin.RouterGroup) {
|
||||
tickets := r.Group("/tickets")
|
||||
{
|
||||
tickets.GET("/stats", handleGetTicketStats)
|
||||
tickets.GET("", handleGetTickets)
|
||||
tickets.GET("/:id", handleGetTicket)
|
||||
tickets.POST("", handleCreateTicket)
|
||||
tickets.PUT("/:id", handleUpdateTicket)
|
||||
tickets.PUT("/:id/status", handleUpdateTicketStatus)
|
||||
tickets.DELETE("/:id", handleDeleteTicket)
|
||||
tickets.DELETE("/batch", handleBatchDeleteTickets)
|
||||
tickets.GET("/:id/replies", handleGetTicketReplies)
|
||||
tickets.POST("/:id/replies", handleCreateTicketReply)
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetTicketStats(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
var total, open, processing, resolved, closed int64
|
||||
|
||||
database.DB.Model(&model.Ticket{}).Where("user_id = ? OR assigned_to = ?", userID, userID).Count(&total)
|
||||
database.DB.Model(&model.Ticket{}).Where("(user_id = ? OR assigned_to = ?) AND status = ?", userID, userID, "open").Count(&open)
|
||||
database.DB.Model(&model.Ticket{}).Where("(user_id = ? OR assigned_to = ?) AND status = ?", userID, userID, "processing").Count(&processing)
|
||||
database.DB.Model(&model.Ticket{}).Where("(user_id = ? OR assigned_to = ?) AND status = ?", userID, userID, "resolved").Count(&resolved)
|
||||
database.DB.Model(&model.Ticket{}).Where("(user_id = ? OR assigned_to = ?) AND status = ?", userID, userID, "closed").Count(&closed)
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"total_count": total,
|
||||
"open_count": open,
|
||||
"processing_count": processing,
|
||||
"resolved_count": resolved,
|
||||
"closed_count": closed,
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetTickets(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
var tickets []model.Ticket
|
||||
if err := database.DB.Where("user_id = ? OR assigned_to = ?", userID, userID).
|
||||
Preload("Replies").
|
||||
Preload("Application").
|
||||
Preload("User").
|
||||
Preload("AssignedUser").
|
||||
Find(&tickets).Error; err != nil {
|
||||
response.Error(c, 500, "获取工单列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
type TicketWithNames struct {
|
||||
model.Ticket
|
||||
AppName string `json:"app_name"`
|
||||
UserName string `json:"user_name"`
|
||||
}
|
||||
|
||||
var result []TicketWithNames
|
||||
for _, ticket := range tickets {
|
||||
appName := ""
|
||||
if ticket.Application != nil {
|
||||
appName = ticket.Application.Name
|
||||
}
|
||||
userName := ""
|
||||
if ticket.User.ID != 0 {
|
||||
userName = ticket.User.Username
|
||||
}
|
||||
result = append(result, TicketWithNames{
|
||||
Ticket: ticket,
|
||||
AppName: appName,
|
||||
UserName: userName,
|
||||
})
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"tickets": result,
|
||||
"total": len(result),
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetTicket(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
id := c.Param("id")
|
||||
var ticket model.Ticket
|
||||
if err := database.DB.Where("id = ? AND (user_id = ? OR assigned_to = ?)", id, userID, userID).
|
||||
Preload("Application").
|
||||
Preload("User").
|
||||
Preload("AssignedUser").
|
||||
First(&ticket).Error; err != nil {
|
||||
response.Error(c, 404, "工单不存在")
|
||||
return
|
||||
}
|
||||
|
||||
type TicketWithNames struct {
|
||||
model.Ticket
|
||||
AppName string `json:"app_name"`
|
||||
UserName string `json:"user_name"`
|
||||
}
|
||||
|
||||
appName := ""
|
||||
if ticket.Application != nil {
|
||||
appName = ticket.Application.Name
|
||||
}
|
||||
userName := ""
|
||||
if ticket.User.ID != 0 {
|
||||
userName = ticket.User.Username
|
||||
}
|
||||
|
||||
result := TicketWithNames{
|
||||
Ticket: ticket,
|
||||
AppName: appName,
|
||||
UserName: userName,
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"ticket": result,
|
||||
})
|
||||
}
|
||||
|
||||
func handleCreateTicket(c *gin.Context) {
|
||||
log.Printf("handleCreateTicket called\n")
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
log.Printf("Starting ParseMultipartForm...\n")
|
||||
if err := c.Request.ParseMultipartForm(32 << 20); err != nil {
|
||||
log.Printf("ParseMultipartForm error: %v\n", err)
|
||||
response.Error(c, 400, "解析表单数据失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
log.Printf("ParseMultipartForm succeeded\n")
|
||||
|
||||
ticketType := c.PostForm("type")
|
||||
title := c.PostForm("title")
|
||||
content := c.PostForm("content")
|
||||
priority := c.PostForm("priority")
|
||||
applicationIDStr := c.PostForm("application_id")
|
||||
|
||||
log.Printf("Received ticket data - type: '%s', title: '%s', content: '%s', priority: '%s', application_id: '%s'\n",
|
||||
ticketType, title, content, priority, applicationIDStr)
|
||||
|
||||
if ticketType == "" {
|
||||
ticketType = "system"
|
||||
}
|
||||
|
||||
if title == "" {
|
||||
log.Printf("Title is empty\n")
|
||||
response.Error(c, 400, "标题不能为空")
|
||||
return
|
||||
}
|
||||
if content == "" {
|
||||
log.Printf("Content is empty\n")
|
||||
response.Error(c, 400, "描述不能为空")
|
||||
return
|
||||
}
|
||||
if priority == "" {
|
||||
priority = "normal"
|
||||
}
|
||||
|
||||
var applicationID *uint
|
||||
if ticketType == "application" && applicationIDStr != "" {
|
||||
var appID uint
|
||||
if _, err := fmt.Sscanf(applicationIDStr, "%d", &appID); err == nil {
|
||||
applicationID = &appID
|
||||
}
|
||||
}
|
||||
|
||||
ticket := model.Ticket{
|
||||
UserID: userID,
|
||||
ApplicationID: applicationID,
|
||||
Title: title,
|
||||
Content: content,
|
||||
Category: "other",
|
||||
Priority: priority,
|
||||
Status: "open",
|
||||
Type: ticketType,
|
||||
}
|
||||
|
||||
if applicationID != nil {
|
||||
var application model.Application
|
||||
if err := database.DB.First(&application, *applicationID).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
ticket.AssignedTo = &application.UserID
|
||||
}
|
||||
|
||||
log.Printf("Creating ticket in database...\n")
|
||||
if err := database.DB.Create(&ticket).Error; err != nil {
|
||||
log.Printf("Create ticket error: %v\n", err)
|
||||
response.Error(c, 500, "创建工单失败")
|
||||
return
|
||||
}
|
||||
log.Printf("Ticket created with ID: %d\n", ticket.ID)
|
||||
|
||||
service.LogOperation(c, "create", "ticket", &ticket.ID, fmt.Sprintf("创建工单: %s", ticket.Title), nil)
|
||||
|
||||
log.Printf("Loading ticket details...\n")
|
||||
if err := database.DB.Preload("User").Preload("Application").First(&ticket, ticket.ID).Error; err != nil {
|
||||
response.Error(c, 500, "获取工单信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
type TicketWithNames struct {
|
||||
model.Ticket
|
||||
AppName string `json:"app_name"`
|
||||
UserName string `json:"user_name"`
|
||||
}
|
||||
|
||||
appName := ""
|
||||
if ticket.Application != nil {
|
||||
appName = ticket.Application.Name
|
||||
}
|
||||
userName := ""
|
||||
if ticket.User.ID != 0 {
|
||||
userName = ticket.User.Username
|
||||
}
|
||||
|
||||
result := TicketWithNames{
|
||||
Ticket: ticket,
|
||||
AppName: appName,
|
||||
UserName: userName,
|
||||
}
|
||||
|
||||
log.Printf("Returning success response\n")
|
||||
response.Success(c, result)
|
||||
}
|
||||
|
||||
func handleUpdateTicket(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
var req struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
id := c.Param("id")
|
||||
var ticket model.Ticket
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", id, userID).First(&ticket).Error; err != nil {
|
||||
response.Error(c, 404, "工单不存在")
|
||||
return
|
||||
}
|
||||
|
||||
ticket.Status = req.Status
|
||||
|
||||
if err := database.DB.Save(&ticket).Error; err != nil {
|
||||
response.Error(c, 500, "更新工单失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, ticket)
|
||||
}
|
||||
|
||||
func handleUpdateTicketStatus(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
var req struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
id := c.Param("id")
|
||||
var ticket model.Ticket
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", id, userID).First(&ticket).Error; err != nil {
|
||||
response.Error(c, 404, "工单不存在")
|
||||
return
|
||||
}
|
||||
|
||||
ticket.Status = req.Status
|
||||
|
||||
if err := database.DB.Save(&ticket).Error; err != nil {
|
||||
response.Error(c, 500, "更新工单状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, ticket)
|
||||
}
|
||||
|
||||
func handleDeleteTicket(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
id := c.Param("id")
|
||||
|
||||
var ticket model.Ticket
|
||||
if err := database.DB.Where("id = ? AND (user_id = ? OR assigned_to = ?)", id, userID, userID).First(&ticket).Error; err != nil {
|
||||
response.Error(c, 404, "工单不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DB.Delete(&ticket).Error; err != nil {
|
||||
response.Error(c, 500, "删除工单失败")
|
||||
return
|
||||
}
|
||||
|
||||
service.LogOperation(c, "delete", "ticket", &ticket.ID, fmt.Sprintf("删除工单: %s", ticket.Title), nil)
|
||||
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
func handleBatchDeleteTickets(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var req struct {
|
||||
IDs []uint `json:"ids"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.IDs) == 0 {
|
||||
response.Error(c, 400, "请选择要删除的工单")
|
||||
return
|
||||
}
|
||||
|
||||
var tickets []model.Ticket
|
||||
if err := database.DB.Where("id IN ? AND (user_id = ? OR assigned_to = ?)", req.IDs, userID, userID).Find(&tickets).Error; err != nil {
|
||||
response.Error(c, 500, "查询工单失败")
|
||||
return
|
||||
}
|
||||
|
||||
if len(tickets) == 0 {
|
||||
response.Error(c, 404, "没有找到可删除的工单")
|
||||
return
|
||||
}
|
||||
|
||||
var validIDs []uint
|
||||
for _, ticket := range tickets {
|
||||
validIDs = append(validIDs, ticket.ID)
|
||||
}
|
||||
|
||||
if err := database.DB.Where("id IN ?", validIDs).Delete(&model.Ticket{}).Error; err != nil {
|
||||
response.Error(c, 500, "批量删除工单失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"deleted": len(validIDs),
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetTicketReplies(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
ticketID := c.Param("id")
|
||||
|
||||
var ticket model.Ticket
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", ticketID, userID).First(&ticket).Error; err != nil {
|
||||
response.Error(c, 404, "工单不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var replies []model.TicketReply
|
||||
if err := database.DB.Where("ticket_id = ?", ticketID).Preload("User").Order("created_at ASC").Find(&replies).Error; err != nil {
|
||||
response.Error(c, 500, "获取回复列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"replies": replies,
|
||||
"total": len(replies),
|
||||
})
|
||||
}
|
||||
|
||||
func handleCreateTicketReply(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
ticketID := c.Param("id")
|
||||
|
||||
var ticket model.Ticket
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", ticketID, userID).First(&ticket).Error; err != nil {
|
||||
response.Error(c, 404, "工单不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
reply := model.TicketReply{
|
||||
TicketID: parseUint(ticketID),
|
||||
UserID: userID,
|
||||
Content: req.Content,
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&reply).Error; err != nil {
|
||||
response.Error(c, 500, "创建回复失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, reply)
|
||||
}
|
||||
|
||||
func parseUint(s string) uint {
|
||||
var val uint
|
||||
fmt.Sscanf(s, "%d", &val)
|
||||
return val
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
package developer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/service"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupUsageRoutes(r *gin.RouterGroup) {
|
||||
usage := r.Group("/usage")
|
||||
{
|
||||
usage.GET("/stats", handleGetUsageStats)
|
||||
usage.GET("/api-history", handleGetApiUsageHistory)
|
||||
usage.GET("/storage-history", handleGetStorageUsageHistory)
|
||||
usage.GET("/alerts", handleGetUsageAlerts)
|
||||
usage.GET("/notifications", handleGetNotifications)
|
||||
usage.PUT("/notifications/:id/read", handleMarkNotificationAsRead)
|
||||
usage.GET("/notifications/unread-count", handleGetUnreadNotificationCount)
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetUsageStats(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
response.Error(c, 401, "未授权")
|
||||
return
|
||||
}
|
||||
|
||||
var user model.User
|
||||
if err := database.DB.Preload("CurrentPackage").First(&user, userID).Error; err != nil {
|
||||
response.Error(c, 500, "获取用户信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
var permission *model.PackagePermission
|
||||
if user.CurrentPackageID != nil {
|
||||
var perm model.PackagePermission
|
||||
if err := database.DB.Where("package_id = ?", user.CurrentPackageID).First(&perm).Error; err == nil {
|
||||
permission = &perm
|
||||
}
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
|
||||
var todayApiCalls int64
|
||||
database.DB.Model(&model.ApiUsage{}).
|
||||
Where("user_id = ? AND created_at >= ?", userID, today).
|
||||
Count(&todayApiCalls)
|
||||
|
||||
var last30DaysApiCalls int64
|
||||
database.DB.Model(&model.ApiUsage{}).
|
||||
Where("user_id = ? AND created_at >= ?", userID, now.AddDate(0, 0, -30)).
|
||||
Count(&last30DaysApiCalls)
|
||||
|
||||
var totalApiCalls int64
|
||||
database.DB.Model(&model.ApiUsage{}).
|
||||
Where("user_id = ?", userID).
|
||||
Count(&totalApiCalls)
|
||||
|
||||
storageUsedMB := float64(user.StorageUsed) / 1024 / 1024
|
||||
maxStorageMB := 0.0
|
||||
if permission != nil {
|
||||
maxStorageMB = float64(permission.MaxStorage)
|
||||
}
|
||||
|
||||
usageData := gin.H{
|
||||
"api_calls": gin.H{
|
||||
"today": todayApiCalls,
|
||||
"last_30_days": last30DaysApiCalls,
|
||||
"total": totalApiCalls,
|
||||
"limit": 0,
|
||||
"used_today": user.ApiCallsUsed,
|
||||
"reset_at": user.ApiCallsResetAt,
|
||||
},
|
||||
"storage": gin.H{
|
||||
"used_mb": storageUsedMB,
|
||||
"max_mb": maxStorageMB,
|
||||
"used_bytes": user.StorageUsed,
|
||||
"max_bytes": int64(maxStorageMB * 1024 * 1024),
|
||||
"usage_percent": 0.0,
|
||||
},
|
||||
"package": gin.H{
|
||||
"id": nil,
|
||||
"name": nil,
|
||||
"expired_at": nil,
|
||||
},
|
||||
}
|
||||
|
||||
if permission != nil {
|
||||
usageData["api_calls"].(gin.H)["limit"] = permission.MaxApiCalls
|
||||
if maxStorageMB > 0 {
|
||||
usageData["storage"].(gin.H)["usage_percent"] = (storageUsedMB / maxStorageMB) * 100
|
||||
}
|
||||
}
|
||||
|
||||
if user.CurrentPackage != nil {
|
||||
usageData["package"].(gin.H)["id"] = user.CurrentPackage.ID
|
||||
usageData["package"].(gin.H)["name"] = user.CurrentPackage.Name
|
||||
|
||||
var userPackage model.UserPackage
|
||||
if err := database.DB.Where("user_id = ? AND package_id = ? AND status = ?",
|
||||
userID, user.CurrentPackageID, "active").
|
||||
First(&userPackage).Error; err == nil {
|
||||
usageData["package"].(gin.H)["expired_at"] = userPackage.ExpiredAt
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, usageData)
|
||||
}
|
||||
|
||||
func handleGetApiUsageHistory(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
response.Error(c, 401, "未授权")
|
||||
return
|
||||
}
|
||||
|
||||
page := 1
|
||||
pageSize := 20
|
||||
if p, ok := c.GetQuery("page"); ok {
|
||||
fmt.Sscanf(p, "%d", &page)
|
||||
}
|
||||
if ps, ok := c.GetQuery("page_size"); ok {
|
||||
fmt.Sscanf(ps, "%d", &pageSize)
|
||||
}
|
||||
|
||||
var total int64
|
||||
database.DB.Model(&model.ApiUsage{}).Where("user_id = ?", userID).Count(&total)
|
||||
|
||||
var usages []model.ApiUsage
|
||||
offset := (page - 1) * pageSize
|
||||
if err := database.DB.Where("user_id = ?", userID).
|
||||
Order("created_at DESC").
|
||||
Limit(pageSize).
|
||||
Offset(offset).
|
||||
Find(&usages).Error; err != nil {
|
||||
response.Error(c, 500, "获取API调用历史失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"list": usages,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetUsageAlerts(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
response.Error(c, 401, "未授权")
|
||||
return
|
||||
}
|
||||
|
||||
alertService := service.NewUsageAlertService()
|
||||
alerts, err := alertService.CheckAndCreateAlerts(userID.(uint))
|
||||
if err != nil {
|
||||
response.Error(c, 500, "检查用量告警失败")
|
||||
return
|
||||
}
|
||||
|
||||
history, err := alertService.GetUserAlerts(userID.(uint), 20)
|
||||
if err != nil {
|
||||
response.Error(c, 500, "获取告警历史失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"current_alerts": alerts,
|
||||
"history": history,
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetNotifications(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
response.Error(c, 401, "未授权")
|
||||
return
|
||||
}
|
||||
|
||||
page := 1
|
||||
pageSize := 20
|
||||
if p, ok := c.GetQuery("page"); ok {
|
||||
fmt.Sscanf(p, "%d", &page)
|
||||
}
|
||||
if ps, ok := c.GetQuery("page_size"); ok {
|
||||
fmt.Sscanf(ps, "%d", &pageSize)
|
||||
}
|
||||
|
||||
var total int64
|
||||
database.DB.Model(&model.Notification{}).Where("user_id = ?", userID).Count(&total)
|
||||
|
||||
var notifications []model.Notification
|
||||
offset := (page - 1) * pageSize
|
||||
if err := database.DB.Where("user_id = ?", userID).
|
||||
Order("created_at DESC").
|
||||
Limit(pageSize).
|
||||
Offset(offset).
|
||||
Find(¬ifications).Error; err != nil {
|
||||
response.Error(c, 500, "获取通知失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"list": notifications,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
func handleMarkNotificationAsRead(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
response.Error(c, 401, "未授权")
|
||||
return
|
||||
}
|
||||
|
||||
notificationID := c.Param("id")
|
||||
|
||||
var notification model.Notification
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", notificationID, userID).First(¬ification).Error; err != nil {
|
||||
response.Error(c, 404, "通知不存在")
|
||||
return
|
||||
}
|
||||
|
||||
notification.IsRead = true
|
||||
if err := database.DB.Save(¬ification).Error; err != nil {
|
||||
response.Error(c, 500, "标记通知失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
func handleGetUnreadNotificationCount(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
response.Error(c, 401, "未授权")
|
||||
return
|
||||
}
|
||||
|
||||
var count int64
|
||||
database.DB.Model(&model.Notification{}).
|
||||
Where("user_id = ? AND is_read = ?", userID, false).
|
||||
Count(&count)
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"count": count,
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetStorageUsageHistory(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
response.Error(c, 401, "未授权")
|
||||
return
|
||||
}
|
||||
|
||||
page := 1
|
||||
pageSize := 20
|
||||
if p, ok := c.GetQuery("page"); ok {
|
||||
fmt.Sscanf(p, "%d", &page)
|
||||
}
|
||||
if ps, ok := c.GetQuery("page_size"); ok {
|
||||
fmt.Sscanf(ps, "%d", &pageSize)
|
||||
}
|
||||
|
||||
var total int64
|
||||
database.DB.Model(&model.StorageUsage{}).Where("user_id = ?", userID).Count(&total)
|
||||
|
||||
var usages []model.StorageUsage
|
||||
offset := (page - 1) * pageSize
|
||||
if err := database.DB.Where("user_id = ?", userID).
|
||||
Order("created_at DESC").
|
||||
Limit(pageSize).
|
||||
Offset(offset).
|
||||
Find(&usages).Error; err != nil {
|
||||
response.Error(c, 500, "获取存储使用历史失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"list": usages,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,673 @@
|
||||
package developer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/service"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type UserWithStatus struct {
|
||||
model.AppUser
|
||||
OnlineStatus string `json:"online_status"`
|
||||
DeviceCount int `json:"device_count"`
|
||||
}
|
||||
|
||||
func getUserOnlineStatus(user model.AppUser, heartbeatTimeout int) string {
|
||||
if user.Status == "banned" {
|
||||
return "banned"
|
||||
}
|
||||
|
||||
if user.LastHeartbeatAt == nil {
|
||||
return "offline"
|
||||
}
|
||||
|
||||
offlineThreshold := time.Duration(heartbeatTimeout) * time.Second
|
||||
if time.Since(*user.LastHeartbeatAt) > offlineThreshold {
|
||||
return "offline"
|
||||
}
|
||||
|
||||
return "online"
|
||||
}
|
||||
|
||||
func SetupUserRoutes(r *gin.RouterGroup) {
|
||||
appUsers := r.Group("/app-users")
|
||||
{
|
||||
appUsers.GET("", handleGetUsers)
|
||||
appUsers.POST("", handleCreateUser)
|
||||
appUsers.GET("/:id", handleGetUser)
|
||||
appUsers.PUT("/:id", handleUpdateUser)
|
||||
appUsers.DELETE("/:id", handleDeleteUser)
|
||||
appUsers.GET("/:id/devices", handleGetUserDevices)
|
||||
appUsers.DELETE("/:id/devices/:deviceId", handleUnbindDevice)
|
||||
appUsers.PUT("/:id/expiry", handleUpdateExpiry)
|
||||
appUsers.POST("/batch/status", handleBatchUpdateStatus)
|
||||
appUsers.DELETE("/batch", handleBatchDelete)
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetUsers(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
log.Printf("[DEBUG] handleGetUsers called, userID: %d", userID)
|
||||
|
||||
var users []model.AppUser
|
||||
var appHeartbeatTimeoutMap map[uint]int
|
||||
|
||||
applicationID := c.Query("application_id")
|
||||
if applicationID != "" {
|
||||
var appID uint
|
||||
if _, err := fmt.Sscanf(applicationID, "%d", &appID); err != nil {
|
||||
response.Error(c, 400, "应用ID格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.First(&app, appID).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
appHeartbeatTimeoutMap = make(map[uint]int)
|
||||
timeout := app.HeartbeatTimeout
|
||||
if timeout == 0 {
|
||||
timeout = 300
|
||||
}
|
||||
appHeartbeatTimeoutMap[app.ID] = timeout
|
||||
|
||||
if app.UserID == userID {
|
||||
if err := database.DB.Preload("Application").Where("application_id = ?", appID).Find(&users).Error; err != nil {
|
||||
response.Error(c, 500, "获取用户列表失败")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
var agentApp model.AgentApplication
|
||||
if err := database.DB.Where("application_id = ? AND agent_id = ? AND is_received = ?", appID, userID, true).First(&agentApp).Error; err != nil {
|
||||
response.Error(c, 403, "无权限查看该应用的用户")
|
||||
return
|
||||
}
|
||||
|
||||
var cardUserIDs []uint
|
||||
if err := database.DB.Model(&model.Card{}).
|
||||
Where("creator_id = ? AND application_id = ? AND app_user_id IS NOT NULL", userID, appID).
|
||||
Pluck("app_user_id", &cardUserIDs).Error; err != nil {
|
||||
response.Error(c, 500, "获取用户列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
if len(cardUserIDs) > 0 {
|
||||
if err := database.DB.Preload("Application").Where("id IN ?", cardUserIDs).Find(&users).Error; err != nil {
|
||||
response.Error(c, 500, "获取用户列表失败")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var ownApps []model.Application
|
||||
if err := database.DB.Where("user_id = ?", userID).Find(&ownApps).Error; err != nil {
|
||||
response.Error(c, 500, "获取应用列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
var agentApps []model.AgentApplication
|
||||
if err := database.DB.Where("agent_id = ? AND is_received = ?", userID, true).Find(&agentApps).Error; err != nil {
|
||||
log.Printf("[DEBUG] Failed to get agent apps: %v", err)
|
||||
response.Error(c, 500, "获取授权列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Found %d agent apps for user %d", len(agentApps), userID)
|
||||
|
||||
appHeartbeatTimeoutMap = make(map[uint]int)
|
||||
|
||||
for _, app := range ownApps {
|
||||
timeout := app.HeartbeatTimeout
|
||||
if timeout == 0 {
|
||||
timeout = 300
|
||||
}
|
||||
appHeartbeatTimeoutMap[app.ID] = timeout
|
||||
|
||||
var appUsers []model.AppUser
|
||||
if err := database.DB.Preload("Application").Where("application_id = ?", app.ID).Find(&appUsers).Error; err != nil {
|
||||
response.Error(c, 500, "获取用户列表失败")
|
||||
return
|
||||
}
|
||||
users = append(users, appUsers...)
|
||||
}
|
||||
|
||||
for _, agentApp := range agentApps {
|
||||
var app model.Application
|
||||
if err := database.DB.First(&app, agentApp.ApplicationID).Error; err != nil {
|
||||
log.Printf("[DEBUG] Failed to get application %d: %v", agentApp.ApplicationID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Processing agent app: ApplicationID=%d, AppName=%s", app.ID, app.Name)
|
||||
|
||||
timeout := app.HeartbeatTimeout
|
||||
if timeout == 0 {
|
||||
timeout = 300
|
||||
}
|
||||
appHeartbeatTimeoutMap[app.ID] = timeout
|
||||
|
||||
var cardUserIDs []uint
|
||||
if err := database.DB.Model(&model.Card{}).
|
||||
Where("creator_id = ? AND application_id = ? AND app_user_id IS NOT NULL", userID, app.ID).
|
||||
Pluck("app_user_id", &cardUserIDs).Error; err != nil {
|
||||
log.Printf("[DEBUG] Failed to get card user IDs for app %d: %v", app.ID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Found %d card user IDs for app %d: %v", len(cardUserIDs), app.ID, cardUserIDs)
|
||||
|
||||
if len(cardUserIDs) > 0 {
|
||||
var appUsers []model.AppUser
|
||||
if err := database.DB.Preload("Application").Where("id IN ?", cardUserIDs).Find(&appUsers).Error; err != nil {
|
||||
log.Printf("[DEBUG] Failed to get users for app %d: %v", app.ID, err)
|
||||
continue
|
||||
}
|
||||
log.Printf("[DEBUG] Found %d users for app %d", len(appUsers), app.ID)
|
||||
users = append(users, appUsers...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
totalCount := len(users)
|
||||
onlineCount := 0
|
||||
offlineCount := 0
|
||||
bannedCount := 0
|
||||
|
||||
usersWithStatus := make([]UserWithStatus, 0, len(users))
|
||||
for _, user := range users {
|
||||
log.Printf("[DEBUG] User ID=%d, Username=%s, LastLoginAt=%v, LastHeartbeatAt=%v", user.ID, user.Username, user.LastLoginAt, user.LastHeartbeatAt)
|
||||
|
||||
heartbeatTimeout := 300
|
||||
if applicationID != "" {
|
||||
heartbeatTimeout = appHeartbeatTimeoutMap[user.ApplicationID]
|
||||
} else if appHeartbeatTimeoutMap != nil {
|
||||
heartbeatTimeout = appHeartbeatTimeoutMap[user.ApplicationID]
|
||||
}
|
||||
|
||||
onlineStatus := getUserOnlineStatus(user, heartbeatTimeout)
|
||||
|
||||
switch onlineStatus {
|
||||
case "online":
|
||||
onlineCount++
|
||||
case "offline":
|
||||
offlineCount++
|
||||
case "banned":
|
||||
bannedCount++
|
||||
}
|
||||
|
||||
var deviceCount int64
|
||||
database.DB.Model(&model.UserDevice{}).Where("user_id = ?", user.ID).Count(&deviceCount)
|
||||
|
||||
log.Printf("[DEBUG] 用户 %s (ID=%d) 余额: %f", user.Username, user.ID, user.Balance)
|
||||
|
||||
usersWithStatus = append(usersWithStatus, UserWithStatus{
|
||||
AppUser: user,
|
||||
OnlineStatus: onlineStatus,
|
||||
DeviceCount: int(deviceCount),
|
||||
})
|
||||
}
|
||||
|
||||
responseData := gin.H{
|
||||
"users": usersWithStatus,
|
||||
"total": totalCount,
|
||||
"online_count": onlineCount,
|
||||
"offline_count": offlineCount,
|
||||
"banned_count": bannedCount,
|
||||
}
|
||||
log.Printf("[DEBUG] Response data: %+v", responseData)
|
||||
response.Success(c, responseData)
|
||||
}
|
||||
|
||||
func handleCreateUser(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
var req struct {
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
ApplicationID uint `json:"application_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Username == "" {
|
||||
response.Error(c, 400, "用户名不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Password == "" {
|
||||
response.Error(c, 400, "密码不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
if req.ApplicationID == 0 {
|
||||
response.Error(c, 400, "所属应用不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.First(&app, req.ApplicationID).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if app.UserID != userID {
|
||||
var agentApp model.AgentApplication
|
||||
if err := database.DB.Where("application_id = ? AND agent_id = ? AND is_received = ?", req.ApplicationID, userID, true).First(&agentApp).Error; err != nil {
|
||||
response.Error(c, 403, "无权限在该应用下创建用户")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var existingUser model.AppUser
|
||||
if err := database.DB.Where("username = ? AND application_id = ?", req.Username, app.ID).First(&existingUser).Error; err == nil {
|
||||
response.Error(c, 400, "用户已存在")
|
||||
return
|
||||
}
|
||||
|
||||
user := model.AppUser{
|
||||
Username: req.Username,
|
||||
Email: req.Email,
|
||||
Password: req.Password,
|
||||
Avatar: "",
|
||||
Status: "active",
|
||||
ApplicationID: app.ID,
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&user).Error; err != nil {
|
||||
response.Error(c, 500, "创建用户失败")
|
||||
return
|
||||
}
|
||||
|
||||
service.LogOperation(c, "create", "app_user", &user.ID, fmt.Sprintf("创建用户: %s (应用: %s)", user.Username, app.Name), nil)
|
||||
|
||||
response.Success(c, user)
|
||||
}
|
||||
|
||||
func handleGetUser(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
id := c.Param("id")
|
||||
var user model.AppUser
|
||||
if err := database.DB.Preload("Application").First(&user, id).Error; err != nil {
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.First(&app, user.ApplicationID).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if app.UserID != userID {
|
||||
var agentApp model.AgentApplication
|
||||
if err := database.DB.Where("application_id = ? AND agent_id = ? AND is_received = ?", app.ID, userID, true).First(&agentApp).Error; err != nil {
|
||||
response.Error(c, 403, "无权限查看该用户")
|
||||
return
|
||||
}
|
||||
|
||||
var card model.Card
|
||||
if err := database.DB.Where("creator_id = ? AND app_user_id = ?", userID, user.ID).First(&card).Error; err != nil {
|
||||
response.Error(c, 403, "无权限查看该用户")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"user": user,
|
||||
})
|
||||
}
|
||||
|
||||
func handleUpdateUser(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
id := c.Param("id")
|
||||
var req struct {
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
Status string `json:"status"`
|
||||
ApplicationID uint `json:"application_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.First(&user, id).Error; err != nil {
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.First(&app, user.ApplicationID).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if app.UserID != userID {
|
||||
var agentApp model.AgentApplication
|
||||
if err := database.DB.Where("application_id = ? AND agent_id = ? AND is_received = ?", app.ID, userID, true).First(&agentApp).Error; err != nil {
|
||||
response.Error(c, 403, "无权限修改该用户")
|
||||
return
|
||||
}
|
||||
|
||||
var card model.Card
|
||||
if err := database.DB.Where("creator_id = ? AND app_user_id = ?", userID, user.ID).First(&card).Error; err != nil {
|
||||
response.Error(c, 403, "无权限修改该用户")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if req.Username != "" {
|
||||
user.Username = req.Username
|
||||
}
|
||||
if req.Email != "" {
|
||||
user.Email = req.Email
|
||||
}
|
||||
if req.Password != "" {
|
||||
user.Password = req.Password
|
||||
}
|
||||
if req.Status != "" {
|
||||
user.Status = req.Status
|
||||
}
|
||||
if req.ApplicationID != 0 {
|
||||
var newApp model.Application
|
||||
if err := database.DB.First(&newApp, req.ApplicationID).Error; err != nil {
|
||||
response.Error(c, 404, "目标应用不存在")
|
||||
return
|
||||
}
|
||||
if newApp.UserID != userID {
|
||||
var agentApp model.AgentApplication
|
||||
if err := database.DB.Where("application_id = ? AND agent_id = ? AND is_received = ?", req.ApplicationID, userID, true).First(&agentApp).Error; err != nil {
|
||||
response.Error(c, 403, "无权限将用户转移到该应用")
|
||||
return
|
||||
}
|
||||
}
|
||||
user.ApplicationID = req.ApplicationID
|
||||
}
|
||||
|
||||
if err := database.DB.Save(&user).Error; err != nil {
|
||||
response.Error(c, 500, "更新用户失败")
|
||||
return
|
||||
}
|
||||
|
||||
service.LogOperation(c, "update", "app_user", &user.ID, fmt.Sprintf("更新用户: %s", user.Username), nil)
|
||||
|
||||
response.Success(c, user)
|
||||
}
|
||||
|
||||
func handleDeleteUser(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
id := c.Param("id")
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.First(&user, id).Error; err != nil {
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.First(&app, user.ApplicationID).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if app.UserID != userID {
|
||||
var agentApp model.AgentApplication
|
||||
if err := database.DB.Where("application_id = ? AND agent_id = ? AND is_received = ?", app.ID, userID, true).First(&agentApp).Error; err != nil {
|
||||
response.Error(c, 403, "无权限删除该用户")
|
||||
return
|
||||
}
|
||||
|
||||
var card model.Card
|
||||
if err := database.DB.Where("creator_id = ? AND app_user_id = ?", userID, user.ID).First(&card).Error; err != nil {
|
||||
response.Error(c, 403, "无权限删除该用户")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := database.DB.Delete(&user).Error; err != nil {
|
||||
response.Error(c, 500, "删除用户失败")
|
||||
return
|
||||
}
|
||||
|
||||
service.LogOperation(c, "delete", "app_user", &user.ID, fmt.Sprintf("删除用户: %s (应用: %s)", user.Username, app.Name), nil)
|
||||
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
func handleGetUserDevices(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
id := c.Param("id")
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.First(&user, id).Error; err != nil {
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.First(&app, user.ApplicationID).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if app.UserID != userID {
|
||||
var agentApp model.AgentApplication
|
||||
if err := database.DB.Where("application_id = ? AND agent_id = ? AND is_received = ?", app.ID, userID, true).First(&agentApp).Error; err != nil {
|
||||
response.Error(c, 403, "无权限查看该用户设备")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var devices []model.UserDevice
|
||||
if err := database.DB.Where("user_id = ? AND application_id = ?", user.ID, app.ID).Find(&devices).Error; err != nil {
|
||||
response.Error(c, 500, "获取设备列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, devices)
|
||||
}
|
||||
|
||||
func handleUpdateExpiry(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
id := c.Param("id")
|
||||
var req struct {
|
||||
Amount float64 `json:"amount"`
|
||||
Type string `json:"type"`
|
||||
Field string `json:"field"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var appUser model.AppUser
|
||||
if err := database.DB.First(&appUser, id).Error; err != nil {
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.First(&app, appUser.ApplicationID).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if app.UserID != userID {
|
||||
var agentApp model.AgentApplication
|
||||
if err := database.DB.Where("application_id = ? AND agent_id = ? AND is_received = ?", app.ID, userID, true).First(&agentApp).Error; err != nil {
|
||||
response.Error(c, 403, "无权限修改该用户")
|
||||
return
|
||||
}
|
||||
|
||||
var card model.Card
|
||||
if err := database.DB.Where("creator_id = ? AND app_user_id = ?", userID, appUser.ID).First(&card).Error; err != nil {
|
||||
response.Error(c, 403, "无权限修改该用户")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if req.Amount == 0 {
|
||||
response.Error(c, 400, "数值不能为0")
|
||||
return
|
||||
}
|
||||
|
||||
if appUser.Balance == -1 {
|
||||
response.Error(c, 400, "该用户为永久会员,无需操作")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
if app.BillingType == "subscription" || req.Field == "days" {
|
||||
if req.Type == "recharge" {
|
||||
var baseTime time.Time
|
||||
if appUser.ExpiryAt != nil && appUser.ExpiryAt.After(now) {
|
||||
baseTime = *appUser.ExpiryAt
|
||||
} else {
|
||||
baseTime = now
|
||||
}
|
||||
duration := time.Duration(req.Amount) * 24 * time.Hour
|
||||
newExpiry := baseTime.Add(duration)
|
||||
appUser.ExpiryAt = &newExpiry
|
||||
} else if req.Type == "deduct" {
|
||||
if appUser.ExpiryAt == nil || appUser.ExpiryAt.Before(now) {
|
||||
response.Error(c, 400, "用户订阅已过期")
|
||||
return
|
||||
}
|
||||
duration := time.Duration(req.Amount) * 24 * time.Hour
|
||||
newExpiry := appUser.ExpiryAt.Add(-duration)
|
||||
if newExpiry.Before(now) {
|
||||
newExpiry = now
|
||||
}
|
||||
appUser.ExpiryAt = &newExpiry
|
||||
} else {
|
||||
response.Error(c, 400, "操作类型错误")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if req.Type == "recharge" {
|
||||
appUser.Balance += req.Amount
|
||||
} else if req.Type == "deduct" {
|
||||
if appUser.Balance < req.Amount {
|
||||
response.Error(c, 400, "余额不足")
|
||||
return
|
||||
}
|
||||
appUser.Balance -= req.Amount
|
||||
} else {
|
||||
response.Error(c, 400, "操作类型错误")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := database.DB.Save(&appUser).Error; err != nil {
|
||||
response.Error(c, 500, "更新失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, appUser)
|
||||
}
|
||||
|
||||
func handleBatchUpdateStatus(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
var req struct {
|
||||
UserIDs []uint `json:"user_ids"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var users []model.AppUser
|
||||
if err := database.DB.Where("id IN ?", req.UserIDs).Find(&users).Error; err != nil {
|
||||
response.Error(c, 500, "获取用户列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
var validUserIDs []uint
|
||||
for _, user := range users {
|
||||
var app model.Application
|
||||
if err := database.DB.First(&app, user.ApplicationID).Error; err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if app.UserID == userID {
|
||||
validUserIDs = append(validUserIDs, user.ID)
|
||||
} else {
|
||||
var agentApp model.AgentApplication
|
||||
if err := database.DB.Where("application_id = ? AND agent_id = ? AND is_received = ?", app.ID, userID, true).First(&agentApp).Error; err == nil {
|
||||
var card model.Card
|
||||
if err := database.DB.Where("creator_id = ? AND app_user_id = ?", userID, user.ID).First(&card).Error; err == nil {
|
||||
validUserIDs = append(validUserIDs, user.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(validUserIDs) > 0 {
|
||||
if err := database.DB.Model(&model.AppUser{}).Where("id IN ?", validUserIDs).Update("status", req.Status).Error; err != nil {
|
||||
response.Error(c, 500, "批量更新状态失败")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
func handleBatchDelete(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
var req struct {
|
||||
UserIDs []uint `json:"user_ids"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var users []model.AppUser
|
||||
if err := database.DB.Where("id IN ?", req.UserIDs).Find(&users).Error; err != nil {
|
||||
response.Error(c, 500, "获取用户列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
var validUserIDs []uint
|
||||
for _, user := range users {
|
||||
var app model.Application
|
||||
if err := database.DB.First(&app, user.ApplicationID).Error; err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if app.UserID == userID {
|
||||
validUserIDs = append(validUserIDs, user.ID)
|
||||
} else {
|
||||
var agentApp model.AgentApplication
|
||||
if err := database.DB.Where("application_id = ? AND agent_id = ? AND is_received = ?", app.ID, userID, true).First(&agentApp).Error; err == nil {
|
||||
var card model.Card
|
||||
if err := database.DB.Where("creator_id = ? AND app_user_id = ?", userID, user.ID).First(&card).Error; err == nil {
|
||||
validUserIDs = append(validUserIDs, user.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(validUserIDs) > 0 {
|
||||
if err := database.DB.Where("id IN ?", validUserIDs).Delete(&model.AppUser{}).Error; err != nil {
|
||||
response.Error(c, 500, "批量删除失败")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, nil)
|
||||
}
|
||||
@@ -0,0 +1,565 @@
|
||||
package developer
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/middleware"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/service"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupVersionRoutes(r *gin.RouterGroup) {
|
||||
versions := r.Group("/versions")
|
||||
{
|
||||
versions.GET("", handleGetAllVersions)
|
||||
versions.GET("/:id", handleGetVersionByID)
|
||||
versions.POST("", handleCreateVersionGlobal)
|
||||
versions.PUT("/:id", handleUpdateVersionGlobal)
|
||||
versions.DELETE("/batch", handleBatchDeleteVersions)
|
||||
versions.POST("/upload-zip", handleUploadVersionZip)
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetAllVersions(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
log.Printf("[DEBUG] handleGetAllVersions called, userID: %d\n", userID)
|
||||
|
||||
page := c.DefaultQuery("page", "1")
|
||||
pageSize := c.DefaultQuery("page_size", "20")
|
||||
|
||||
var total int64
|
||||
|
||||
var userApps []model.Application
|
||||
if err := database.DB.Where("user_id = ?", userID).Find(&userApps).Error; err != nil {
|
||||
response.Error(c, 500, "获取应用列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Found %d user apps\n", len(userApps))
|
||||
for i, app := range userApps {
|
||||
log.Printf("[DEBUG] App %d: ID=%d, Name=%s\n", i, app.ID, app.Name)
|
||||
}
|
||||
|
||||
if len(userApps) == 0 {
|
||||
response.Success(c, gin.H{
|
||||
"versions": []interface{}{},
|
||||
"total": 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
appIDs := make([]uint, len(userApps))
|
||||
appNameMap := make(map[uint]string)
|
||||
for i, app := range userApps {
|
||||
appIDs[i] = app.ID
|
||||
appNameMap[app.ID] = app.Name
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] appNameMap: %v\n", appNameMap)
|
||||
|
||||
database.DB.Model(&model.Version{}).Where("application_id IN ?", appIDs).Count(&total)
|
||||
|
||||
var versions []model.Version
|
||||
offset := 0
|
||||
if pageInt, err := strconv.Atoi(page); err == nil && pageInt > 1 {
|
||||
offset = (pageInt - 1) * 20
|
||||
}
|
||||
|
||||
limit := 20
|
||||
if pageSizeInt, err := strconv.Atoi(pageSize); err == nil && pageSizeInt > 0 {
|
||||
limit = pageSizeInt
|
||||
}
|
||||
|
||||
if err := database.DB.Where("application_id IN ?", appIDs).Order("created_at DESC").Limit(limit).Offset(offset).Find(&versions).Error; err != nil {
|
||||
response.Error(c, 500, "获取版本列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Found %d versions\n", len(versions))
|
||||
for i, v := range versions {
|
||||
log.Printf("[DEBUG] Version %d: ID=%d, ApplicationID=%d, Version=%s\n", i, v.ID, v.ApplicationID, v.Version)
|
||||
}
|
||||
|
||||
type VersionWithAppName struct {
|
||||
model.Version
|
||||
ApplicationName string `json:"application_name"`
|
||||
}
|
||||
|
||||
result := make([]VersionWithAppName, len(versions))
|
||||
for i, v := range versions {
|
||||
appName := appNameMap[v.ApplicationID]
|
||||
log.Printf("[DEBUG] Mapping version %d: ApplicationID=%d -> AppName=%s\n", i, v.ApplicationID, appName)
|
||||
result[i] = VersionWithAppName{
|
||||
Version: v,
|
||||
ApplicationName: appName,
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"versions": result,
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetVersionByID(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
versionID := c.Param("id")
|
||||
|
||||
var userApps []model.Application
|
||||
if err := database.DB.Where("user_id = ?", userID).Find(&userApps).Error; err != nil {
|
||||
response.Error(c, 500, "获取应用列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
appIDs := make([]uint, len(userApps))
|
||||
appNameMap := make(map[uint]string)
|
||||
for i, app := range userApps {
|
||||
appIDs[i] = app.ID
|
||||
appNameMap[app.ID] = app.Name
|
||||
}
|
||||
|
||||
var version model.Version
|
||||
if err := database.DB.Where("id = ? AND application_id IN ?", versionID, appIDs).Preload("Files").First(&version).Error; err != nil {
|
||||
response.Error(c, 404, "版本不存在")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"id": version.ID,
|
||||
"application_id": version.ApplicationID,
|
||||
"application_name": appNameMap[version.ApplicationID],
|
||||
"version": version.Version,
|
||||
"description": version.Description,
|
||||
"file_path": version.FilePath,
|
||||
"file_size": version.FileSize,
|
||||
"file_hash": version.FileHash,
|
||||
"force_update": version.ForceUpdate,
|
||||
"update_strategy": version.UpdateStrategy,
|
||||
"update_method": version.UpdateMethod,
|
||||
"min_version": version.MinVersion,
|
||||
"changelog": version.Changelog,
|
||||
"status": version.Status,
|
||||
"files": version.Files,
|
||||
"created_at": version.CreatedAt,
|
||||
"updated_at": version.UpdatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
type CreateVersionRequest struct {
|
||||
ApplicationID uint `json:"application_id"`
|
||||
Version string `json:"version"`
|
||||
FilePath string `json:"file_path"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
FileHash string `json:"file_hash"`
|
||||
EntryFile string `json:"entry_file"`
|
||||
ForceUpdate bool `json:"force_update"`
|
||||
UpdateStrategy string `json:"update_strategy"`
|
||||
UpdateMethod string `json:"update_method"`
|
||||
MinVersion string `json:"min_version"`
|
||||
Description string `json:"description"`
|
||||
Changelog string `json:"changelog"`
|
||||
}
|
||||
|
||||
func handleCreateVersionGlobal(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var req CreateVersionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", req.ApplicationID, userID).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "应用已被禁用,无法创建版本")
|
||||
return
|
||||
}
|
||||
|
||||
var existingVersion model.Version
|
||||
if err := database.DB.Where("application_id = ? AND version = ?", req.ApplicationID, req.Version).First(&existingVersion).Error; err == nil {
|
||||
response.Error(c, 400, "该版本号已存在")
|
||||
return
|
||||
}
|
||||
|
||||
version := model.Version{
|
||||
ApplicationID: req.ApplicationID,
|
||||
Version: req.Version,
|
||||
FilePath: req.FilePath,
|
||||
FileSize: req.FileSize,
|
||||
FileHash: req.FileHash,
|
||||
EntryFile: req.EntryFile,
|
||||
ForceUpdate: req.ForceUpdate,
|
||||
UpdateStrategy: req.UpdateStrategy,
|
||||
UpdateMethod: req.UpdateMethod,
|
||||
MinVersion: req.MinVersion,
|
||||
Description: req.Description,
|
||||
Changelog: req.Changelog,
|
||||
Status: "active",
|
||||
}
|
||||
|
||||
if version.UpdateStrategy == "" {
|
||||
version.UpdateStrategy = "optional"
|
||||
}
|
||||
if version.UpdateMethod == "" {
|
||||
version.UpdateMethod = "manual"
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&version).Error; err != nil {
|
||||
response.Error(c, 500, "创建版本失败")
|
||||
return
|
||||
}
|
||||
|
||||
userIDPtr := &userID
|
||||
versionIDPtr := &version.ID
|
||||
service.CreateLog(service.LogParams{
|
||||
UserID: userIDPtr,
|
||||
LogType: "version",
|
||||
Action: "create",
|
||||
Resource: "version",
|
||||
ResourceID: versionIDPtr,
|
||||
Details: fmt.Sprintf("创建版本: %s (应用ID: %d)", version.Version, version.ApplicationID),
|
||||
})
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"id": version.ID,
|
||||
})
|
||||
}
|
||||
|
||||
type UpdateVersionRequest struct {
|
||||
Version string `json:"version"`
|
||||
ForceUpdate bool `json:"force_update"`
|
||||
UpdateStrategy string `json:"update_strategy"`
|
||||
UpdateMethod string `json:"update_method"`
|
||||
MinVersion string `json:"min_version"`
|
||||
Description string `json:"description"`
|
||||
Changelog string `json:"changelog"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
func handleUpdateVersionGlobal(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
versionID := c.Param("id")
|
||||
|
||||
var req UpdateVersionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var userApps []model.Application
|
||||
if err := database.DB.Where("user_id = ?", userID).Find(&userApps).Error; err != nil {
|
||||
response.Error(c, 500, "获取应用列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
appIDs := make([]uint, len(userApps))
|
||||
for i, app := range userApps {
|
||||
appIDs[i] = app.ID
|
||||
}
|
||||
|
||||
var version model.Version
|
||||
if err := database.DB.Where("id = ? AND application_id IN ?", versionID, appIDs).First(&version).Error; err != nil {
|
||||
response.Error(c, 404, "版本不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Version != "" && req.Version != version.Version {
|
||||
var existingVersion model.Version
|
||||
if err := database.DB.Where("application_id = ? AND version = ? AND id != ?", version.ApplicationID, req.Version, version.ID).First(&existingVersion).Error; err == nil {
|
||||
response.Error(c, 400, "该版本号已存在")
|
||||
return
|
||||
}
|
||||
version.Version = req.Version
|
||||
}
|
||||
|
||||
if req.Version != "" {
|
||||
version.Version = req.Version
|
||||
}
|
||||
version.ForceUpdate = req.ForceUpdate
|
||||
if req.UpdateStrategy != "" {
|
||||
version.UpdateStrategy = req.UpdateStrategy
|
||||
}
|
||||
if req.UpdateMethod != "" {
|
||||
version.UpdateMethod = req.UpdateMethod
|
||||
}
|
||||
version.MinVersion = req.MinVersion
|
||||
version.Description = req.Description
|
||||
version.Changelog = req.Changelog
|
||||
if req.Status != "" {
|
||||
version.Status = req.Status
|
||||
}
|
||||
|
||||
if err := database.DB.Save(&version).Error; err != nil {
|
||||
response.Error(c, 500, "更新版本失败")
|
||||
return
|
||||
}
|
||||
|
||||
userIDPtr := &userID
|
||||
versionIDPtr := &version.ID
|
||||
service.CreateLog(service.LogParams{
|
||||
UserID: userIDPtr,
|
||||
LogType: "version",
|
||||
Action: "update",
|
||||
Resource: "version",
|
||||
ResourceID: versionIDPtr,
|
||||
Details: fmt.Sprintf("更新版本: %s (应用ID: %d)", version.Version, version.ApplicationID),
|
||||
})
|
||||
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
func handleBatchDeleteVersions(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var req struct {
|
||||
IDs []uint `json:"ids"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.IDs) == 0 {
|
||||
response.Error(c, 400, "请选择要删除的版本")
|
||||
return
|
||||
}
|
||||
|
||||
var userApps []model.Application
|
||||
if err := database.DB.Where("user_id = ?", userID).Find(&userApps).Error; err != nil {
|
||||
response.Error(c, 500, "获取应用列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
appIDs := make([]uint, len(userApps))
|
||||
for i, app := range userApps {
|
||||
appIDs[i] = app.ID
|
||||
}
|
||||
|
||||
var versions []model.Version
|
||||
if err := database.DB.Where("id IN ? AND application_id IN ?", req.IDs, appIDs).Find(&versions).Error; err != nil {
|
||||
response.Error(c, 500, "获取版本失败")
|
||||
return
|
||||
}
|
||||
|
||||
for _, version := range versions {
|
||||
for _, app := range userApps {
|
||||
if app.ID == version.ApplicationID && service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, fmt.Sprintf("应用 %s 已被禁用,无法删除其版本", app.Name))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := database.DB.Where("id IN ? AND application_id IN ?", req.IDs, appIDs).Delete(&model.Version{}).Error; err != nil {
|
||||
response.Error(c, 500, "批量删除版本失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
type UploadVersionZipResponse struct {
|
||||
FilePath string `json:"file_path"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
FileHash string `json:"file_hash"`
|
||||
Files []VersionFileInfo `json:"files"`
|
||||
}
|
||||
|
||||
type VersionFileInfo struct {
|
||||
FilePath string `json:"file_path"`
|
||||
FileName string `json:"file_name"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
FileHash string `json:"file_hash"`
|
||||
FileType string `json:"file_type"`
|
||||
}
|
||||
|
||||
func handleUploadVersionZip(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
file, header, err := c.Request.FormFile("file")
|
||||
if err != nil {
|
||||
response.Error(c, 400, "请上传ZIP文件")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if !strings.HasSuffix(strings.ToLower(header.Filename), ".zip") {
|
||||
response.Error(c, 400, "只支持ZIP格式文件")
|
||||
return
|
||||
}
|
||||
|
||||
fileSize := header.Size
|
||||
|
||||
var user model.User
|
||||
if err := database.DB.First(&user, userID).Error; err != nil {
|
||||
response.Error(c, 500, "获取用户信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
if user.CurrentPackageID != nil {
|
||||
var permission model.PackagePermission
|
||||
if err := database.DB.Where("package_id = ?", user.CurrentPackageID).First(&permission).Error; err == nil {
|
||||
maxStorageBytes := int64(permission.MaxStorage) * 1024 * 1024
|
||||
if user.StorageUsed+fileSize > maxStorageBytes {
|
||||
usedMB := float64(user.StorageUsed) / 1024 / 1024
|
||||
maxMB := float64(permission.MaxStorage)
|
||||
response.Error(c, 403, fmt.Sprintf("存储空间不足,已使用 %.2f MB / %.2f MB", usedMB, maxMB))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
timestamp := time.Now().Unix()
|
||||
filename := fmt.Sprintf("version_%d_%d.zip", userID, timestamp)
|
||||
dst := filepath.Join("uploads", "versions", filename)
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
|
||||
response.Error(c, 500, "创建目录失败")
|
||||
return
|
||||
}
|
||||
|
||||
dstFile, err := os.Create(dst)
|
||||
if err != nil {
|
||||
response.Error(c, 500, "创建文件失败")
|
||||
return
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
if _, err := io.Copy(dstFile, file); err != nil {
|
||||
response.Error(c, 500, "保存文件失败")
|
||||
return
|
||||
}
|
||||
|
||||
zipHash, err := calculateFileHash(dst)
|
||||
if err != nil {
|
||||
os.Remove(dst)
|
||||
response.Error(c, 500, "计算文件哈希失败")
|
||||
return
|
||||
}
|
||||
|
||||
files, err := parseZipFile(dst)
|
||||
if err != nil {
|
||||
os.Remove(dst)
|
||||
response.Error(c, 500, fmt.Sprintf("解析ZIP文件失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := middleware.UpdateStorageUsed(userID, fileSize, "upload"); err != nil {
|
||||
log.Printf("更新存储使用量失败: %v\n", err)
|
||||
}
|
||||
|
||||
usage := model.StorageUsage{
|
||||
UserID: userID,
|
||||
ResourceType: "version",
|
||||
ResourceID: 0,
|
||||
FileName: header.Filename,
|
||||
FileSize: fileSize,
|
||||
Action: "upload",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
database.DB.Create(&usage)
|
||||
|
||||
response.Success(c, UploadVersionZipResponse{
|
||||
FilePath: fmt.Sprintf("/uploads/versions/%s", filename),
|
||||
FileSize: fileSize,
|
||||
FileHash: zipHash,
|
||||
Files: files,
|
||||
})
|
||||
}
|
||||
|
||||
func calculateFileHash(filePath string) (string, error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
hash := sha256.New()
|
||||
if _, err := io.Copy(hash, file); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return hex.EncodeToString(hash.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func parseZipFile(zipPath string) ([]VersionFileInfo, error) {
|
||||
reader, err := zip.OpenReader(zipPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
var files []VersionFileInfo
|
||||
|
||||
for _, f := range reader.File {
|
||||
if f.FileInfo().IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
hash := sha256.New()
|
||||
if _, err := io.Copy(hash, rc); err != nil {
|
||||
rc.Close()
|
||||
continue
|
||||
}
|
||||
rc.Close()
|
||||
|
||||
fileHash := hex.EncodeToString(hash.Sum(nil))
|
||||
fileType := getFileType(f.Name)
|
||||
|
||||
files = append(files, VersionFileInfo{
|
||||
FilePath: f.Name,
|
||||
FileName: filepath.Base(f.Name),
|
||||
FileSize: int64(f.UncompressedSize64),
|
||||
FileHash: fileHash,
|
||||
FileType: fileType,
|
||||
})
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func getFileType(filename string) string {
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
switch ext {
|
||||
case ".exe", ".dll", ".so", ".dylib", ".app":
|
||||
return "executable"
|
||||
case ".json", ".xml", ".yaml", ".yml", ".ini", ".conf", ".cfg":
|
||||
return "config"
|
||||
case ".txt", ".md", ".log":
|
||||
return "text"
|
||||
case ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ico", ".svg":
|
||||
return "image"
|
||||
case ".mp3", ".wav", ".ogg", ".flac":
|
||||
return "audio"
|
||||
case ".mp4", ".avi", ".mkv", ".mov", ".wmv":
|
||||
return "video"
|
||||
case ".db", ".sqlite", ".sqlite3":
|
||||
return "database"
|
||||
default:
|
||||
return "resource"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,749 @@
|
||||
package extension
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/service"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupRoutes(r *gin.RouterGroup) {
|
||||
ext := r.Group("/ext")
|
||||
ext.Use(ExtensionAuthMiddleware())
|
||||
{
|
||||
// 用户相关
|
||||
ext.GET("/user/:userId", handleGetUser)
|
||||
ext.GET("/users", handleGetUsers)
|
||||
ext.POST("/user/:userId/recharge", handleRechargeUser)
|
||||
ext.POST("/user/:userId/deduct", handleDeductUser)
|
||||
|
||||
// 用户变量相关
|
||||
ext.GET("/user/:userId/variables", handleGetUserVariables)
|
||||
ext.POST("/user/:userId/variables", handleUpdateUserVariables)
|
||||
|
||||
// 卡密相关
|
||||
ext.GET("/cards", handleGetCards)
|
||||
ext.POST("/cards/generate", handleGenerateCards)
|
||||
ext.GET("/card/:cardId", handleGetCard)
|
||||
|
||||
// 通知相关
|
||||
ext.POST("/notification", handleSendNotification)
|
||||
ext.POST("/notification/batch", handleSendBatchNotification)
|
||||
|
||||
// 应用信息
|
||||
ext.GET("/app/info", handleGetAppInfo)
|
||||
ext.GET("/app/stats", handleGetAppStats)
|
||||
|
||||
// 应用变量相关
|
||||
ext.GET("/app/variables", handleGetAppVariables)
|
||||
ext.POST("/app/variables", handleUpdateAppVariables)
|
||||
}
|
||||
}
|
||||
|
||||
func ExtensionAuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
accessKey := c.GetHeader("X-Access-Key")
|
||||
signature := c.GetHeader("X-Signature")
|
||||
timestamp := c.GetHeader("X-Timestamp")
|
||||
|
||||
if accessKey == "" || signature == "" || timestamp == "" {
|
||||
response.Error(c, 401, "缺少认证信息")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
ts, err := strconv.ParseInt(timestamp, 10, 64)
|
||||
if err != nil {
|
||||
response.Error(c, 401, "时间戳格式错误")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if time.Now().Unix()-ts > 300 {
|
||||
response.Error(c, 401, "请求已过期")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
var apiKey model.ExtensionAPIKey
|
||||
if err := database.DB.Where("access_key = ? AND status = ?", accessKey, "active").
|
||||
Preload("Application").First(&apiKey).Error; err != nil {
|
||||
response.Error(c, 401, "API密钥无效")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if apiKey.ExpiresAt != nil && apiKey.ExpiresAt.Before(time.Now()) {
|
||||
response.Error(c, 401, "API密钥已过期")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
bodyBytes, _ := io.ReadAll(c.Request.Body)
|
||||
c.Set("requestBody", bodyBytes)
|
||||
c.Request.Body = io.NopCloser(strings.NewReader(string(bodyBytes)))
|
||||
|
||||
stringToSign := fmt.Sprintf("%s%s%s%s", c.Request.Method, c.Request.URL.Path, timestamp, string(bodyBytes))
|
||||
expectedSignature := generateSignature(apiKey.SecretKey, stringToSign)
|
||||
|
||||
if !hmac.Equal([]byte(signature), []byte(expectedSignature)) {
|
||||
response.Error(c, 401, "签名验证失败")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("apiKey", apiKey)
|
||||
c.Set("applicationID", apiKey.ApplicationID)
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.First(&app, apiKey.ApplicationID).Error; err == nil {
|
||||
c.Set("user_id", app.UserID)
|
||||
c.Set("app_id", apiKey.ApplicationID)
|
||||
|
||||
var user model.User
|
||||
if err := database.DB.Preload("CurrentPackage").First(&user, app.UserID).Error; err == nil {
|
||||
if user.CurrentPackageID != nil {
|
||||
var permission model.PackagePermission
|
||||
if err := database.DB.Where("package_id = ?", user.CurrentPackageID).First(&permission).Error; err == nil {
|
||||
now := time.Now()
|
||||
if user.ApiCallsResetAt == nil || now.Sub(*user.ApiCallsResetAt) >= 24*time.Hour {
|
||||
user.ApiCallsUsed = 0
|
||||
user.ApiCallsResetAt = &now
|
||||
database.DB.Save(&user)
|
||||
}
|
||||
|
||||
if user.ApiCallsUsed >= permission.MaxApiCalls {
|
||||
response.Error(c, 403, fmt.Sprintf("API调用次数已达上限(%d次/天),请升级套餐", permission.MaxApiCalls))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
database.DB.Model(&apiKey).Update("last_used_at", now)
|
||||
|
||||
c.Next()
|
||||
|
||||
if _, exists := c.Get("app_id"); exists {
|
||||
if userID, exists := c.Get("user_id"); exists {
|
||||
var user model.User
|
||||
if err := database.DB.First(&user, userID).Error; err == nil {
|
||||
user.ApiCallsUsed++
|
||||
database.DB.Save(&user)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func generateSignature(secretKey, data string) string {
|
||||
h := hmac.New(sha256.New, []byte(secretKey))
|
||||
h.Write([]byte(data))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
func handleGetUser(c *gin.Context) {
|
||||
appID := c.GetUint("applicationID")
|
||||
userID := c.Param("userId")
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", userID, appID).First(&user).Error; err != nil {
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"status": user.Status,
|
||||
"balance": user.Balance,
|
||||
"expiry_at": user.ExpiryAt,
|
||||
"lastLoginAt": user.LastLoginAt,
|
||||
"lastHeartbeatAt": user.LastHeartbeatAt,
|
||||
"isTrialUser": user.IsTrialUser,
|
||||
"createdAt": user.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetUsers(c *gin.Context) {
|
||||
appID := c.GetUint("applicationID")
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
status := c.Query("status")
|
||||
search := c.Query("search")
|
||||
|
||||
var users []model.AppUser
|
||||
var total int64
|
||||
|
||||
query := database.DB.Model(&model.AppUser{}).Where("application_id = ?", appID)
|
||||
|
||||
if status != "" {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
|
||||
if search != "" {
|
||||
query = query.Where("username LIKE ? OR email LIKE ?", "%"+search+"%", "%"+search+"%")
|
||||
}
|
||||
|
||||
query.Count(&total)
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&users).Error; err != nil {
|
||||
response.Error(c, 500, "获取用户列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"users": users,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
"total_page": (total + int64(pageSize) - 1) / int64(pageSize),
|
||||
})
|
||||
}
|
||||
|
||||
func handleRechargeUser(c *gin.Context) {
|
||||
appID := c.GetUint("applicationID")
|
||||
userID := c.Param("userId")
|
||||
|
||||
var req struct {
|
||||
Amount int `json:"amount" binding:"required"`
|
||||
Type string `json:"type" binding:"required"` // balance, days
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.First(&app, appID).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", userID, appID).First(&user).Error; err != nil {
|
||||
service.LogVerification(c, &app.ID, nil, "extension_recharge_failed", fmt.Sprintf("扩展API充值失败: 用户不存在 - %s", userID), "", fmt.Errorf("用户不存在"))
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Balance == -1 {
|
||||
service.LogVerification(c, &app.ID, &user.ID, "extension_recharge_failed", fmt.Sprintf("扩展API充值失败: 用户已是永久会员 - %s", user.Username), "", fmt.Errorf("该用户为永久会员,无需充值"))
|
||||
response.Error(c, 400, "该用户为永久会员,无需充值")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
switch req.Type {
|
||||
case "days":
|
||||
var baseTime time.Time
|
||||
if user.ExpiryAt != nil && user.ExpiryAt.After(now) {
|
||||
baseTime = *user.ExpiryAt
|
||||
} else {
|
||||
baseTime = now
|
||||
}
|
||||
duration := time.Duration(req.Amount) * 24 * time.Hour
|
||||
newExpiry := baseTime.Add(duration)
|
||||
user.ExpiryAt = &newExpiry
|
||||
case "balance":
|
||||
user.Balance += float64(req.Amount)
|
||||
default:
|
||||
service.LogVerification(c, &app.ID, &user.ID, "extension_recharge_failed", fmt.Sprintf("扩展API充值失败: 类型无效 - %s", req.Type), "", fmt.Errorf("充值类型无效"))
|
||||
response.Error(c, 400, "充值类型无效,仅支持days或balance类型")
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DB.Save(&user).Error; err != nil {
|
||||
service.LogVerification(c, &app.ID, &user.ID, "extension_recharge_failed", fmt.Sprintf("扩展API充值失败: 保存失败 - %s", user.Username), "", err)
|
||||
response.Error(c, 500, "充值失败")
|
||||
return
|
||||
}
|
||||
|
||||
record := model.RechargeRecord{
|
||||
UserID: user.ID,
|
||||
OrderNo: fmt.Sprintf("EXT%d%d", time.Now().Unix(), user.ID),
|
||||
Amount: float64(req.Amount),
|
||||
Status: "success",
|
||||
PaymentType: "extension_api",
|
||||
Remark: req.Description,
|
||||
}
|
||||
database.DB.Create(&record)
|
||||
|
||||
service.LogVerification(c, &app.ID, &user.ID, "extension_recharge", fmt.Sprintf("扩展API充值: 用户%s, 类型:%s, 数量:%d", user.Username, req.Type, req.Amount), "", nil)
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "充值成功",
|
||||
"user": user,
|
||||
})
|
||||
}
|
||||
|
||||
func handleDeductUser(c *gin.Context) {
|
||||
appID := c.GetUint("applicationID")
|
||||
userID := c.Param("userId")
|
||||
|
||||
var req struct {
|
||||
Amount int `json:"amount" binding:"required"`
|
||||
Type string `json:"type" binding:"required"` // balance, days
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var app model.Application
|
||||
database.DB.First(&app, appID)
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", userID, appID).First(&user).Error; err != nil {
|
||||
service.LogVerification(c, &app.ID, nil, "extension_deduct_failed", fmt.Sprintf("扩展API扣费失败: 用户不存在 - %s", userID), "", fmt.Errorf("用户不存在"))
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Balance == -1 {
|
||||
service.LogVerification(c, &app.ID, &user.ID, "extension_deduct_failed", fmt.Sprintf("扩展API扣费失败: 用户已是永久会员 - %s", user.Username), "", fmt.Errorf("该用户为永久会员,无法扣除"))
|
||||
response.Error(c, 400, "该用户为永久会员,无法扣除")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
switch req.Type {
|
||||
case "days":
|
||||
if user.ExpiryAt == nil || user.ExpiryAt.Before(now) {
|
||||
service.LogVerification(c, &app.ID, &user.ID, "extension_deduct_failed", fmt.Sprintf("扩展API扣费失败: 用户订阅已过期 - %s", user.Username), "", fmt.Errorf("用户订阅已过期"))
|
||||
response.Error(c, 400, "用户订阅已过期")
|
||||
return
|
||||
}
|
||||
duration := time.Duration(req.Amount) * 24 * time.Hour
|
||||
newExpiry := user.ExpiryAt.Add(-duration)
|
||||
if newExpiry.Before(now) {
|
||||
newExpiry = now
|
||||
}
|
||||
user.ExpiryAt = &newExpiry
|
||||
case "balance":
|
||||
if user.Balance < float64(req.Amount) {
|
||||
service.LogVerification(c, &app.ID, &user.ID, "extension_deduct_failed", fmt.Sprintf("扩展API扣费失败: 余额不足 - %s", user.Username), "", fmt.Errorf("余额不足"))
|
||||
response.Error(c, 400, "余额不足")
|
||||
return
|
||||
}
|
||||
user.Balance -= float64(req.Amount)
|
||||
default:
|
||||
service.LogVerification(c, &app.ID, &user.ID, "extension_deduct_failed", fmt.Sprintf("扩展API扣费失败: 类型无效 - %s", req.Type), "", fmt.Errorf("扣除类型无效"))
|
||||
response.Error(c, 400, "扣除类型无效,仅支持days或balance类型")
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DB.Save(&user).Error; err != nil {
|
||||
service.LogVerification(c, &app.ID, &user.ID, "extension_deduct_failed", fmt.Sprintf("扩展API扣费失败: 保存失败 - %s", user.Username), "", err)
|
||||
response.Error(c, 500, "扣除失败")
|
||||
return
|
||||
}
|
||||
|
||||
record := model.ConsumptionRecord{
|
||||
UserID: user.ID,
|
||||
OrderNo: fmt.Sprintf("EXT%d%d", time.Now().Unix(), user.ID),
|
||||
Type: req.Type,
|
||||
Amount: float64(req.Amount),
|
||||
Status: "success",
|
||||
PaymentType: "extension_api",
|
||||
Remark: req.Description,
|
||||
}
|
||||
database.DB.Create(&record)
|
||||
|
||||
service.LogVerification(c, &app.ID, &user.ID, "extension_deduct", fmt.Sprintf("扩展API扣费: 用户%s, 类型:%s, 数量:%d", user.Username, req.Type, req.Amount), "", nil)
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "扣除成功",
|
||||
"user": user,
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetUserVariables(c *gin.Context) {
|
||||
appID := c.GetUint("applicationID")
|
||||
userID := c.Param("userId")
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", userID, appID).First(&user).Error; err != nil {
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var variables []model.CloudVariable
|
||||
if err := database.DB.Where("app_id = ?", appID).Find(&variables).Error; err != nil {
|
||||
response.Error(c, 500, "获取变量定义失败")
|
||||
return
|
||||
}
|
||||
|
||||
var userVariables []model.UserVariable
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ?", userID, appID).Find(&userVariables).Error; err != nil {
|
||||
response.Error(c, 500, "获取用户变量失败")
|
||||
return
|
||||
}
|
||||
|
||||
userVarMap := make(map[string]string)
|
||||
for _, uv := range userVariables {
|
||||
userVarMap[uv.VarName] = uv.VarValue
|
||||
}
|
||||
|
||||
result := make(map[string]interface{})
|
||||
for _, v := range variables {
|
||||
if v.Scope == "app" {
|
||||
result[v.Key] = gin.H{
|
||||
"value": v.DefaultValue,
|
||||
"scope": "app",
|
||||
}
|
||||
} else {
|
||||
value := v.DefaultValue
|
||||
if uv, ok := userVarMap[v.Key]; ok {
|
||||
value = uv
|
||||
}
|
||||
result[v.Key] = gin.H{
|
||||
"value": value,
|
||||
"scope": "user",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, result)
|
||||
}
|
||||
|
||||
func handleUpdateUserVariables(c *gin.Context) {
|
||||
appID := c.GetUint("applicationID")
|
||||
userID := c.Param("userId")
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", userID, appID).First(&user).Error; err != nil {
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Variables map[string]string `json:"variables"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var variables []model.CloudVariable
|
||||
if err := database.DB.Where("app_id = ?", appID).Find(&variables).Error; err != nil {
|
||||
response.Error(c, 500, "获取变量定义失败")
|
||||
return
|
||||
}
|
||||
|
||||
varMap := make(map[string]model.CloudVariable)
|
||||
for _, v := range variables {
|
||||
varMap[v.Key] = v
|
||||
}
|
||||
|
||||
for key, value := range req.Variables {
|
||||
variable, exists := varMap[key]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
if variable.Scope == "app" {
|
||||
variable.DefaultValue = value
|
||||
database.DB.Save(&variable)
|
||||
} else {
|
||||
var userVar model.UserVariable
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND var_name = ?", userID, appID, key).First(&userVar).Error; err != nil {
|
||||
userVar = model.UserVariable{
|
||||
UserID: user.ID,
|
||||
AppID: appID,
|
||||
VarName: key,
|
||||
VarValue: value,
|
||||
VarType: variable.VarType,
|
||||
}
|
||||
database.DB.Create(&userVar)
|
||||
} else {
|
||||
userVar.VarValue = value
|
||||
database.DB.Save(&userVar)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "更新成功",
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetAppVariables(c *gin.Context) {
|
||||
appID := c.GetUint("applicationID")
|
||||
|
||||
var variables []model.CloudVariable
|
||||
if err := database.DB.Where("app_id = ? AND scope = ?", appID, "app").Find(&variables).Error; err != nil {
|
||||
response.Error(c, 500, "获取应用变量失败")
|
||||
return
|
||||
}
|
||||
|
||||
result := make(map[string]string)
|
||||
for _, v := range variables {
|
||||
result[v.Key] = v.DefaultValue
|
||||
}
|
||||
|
||||
response.Success(c, result)
|
||||
}
|
||||
|
||||
func handleUpdateAppVariables(c *gin.Context) {
|
||||
appID := c.GetUint("applicationID")
|
||||
|
||||
var req struct {
|
||||
Variables map[string]string `json:"variables"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
for key, value := range req.Variables {
|
||||
var variable model.CloudVariable
|
||||
if err := database.DB.Where("app_id = ? AND key = ? AND scope = ?", appID, key, "app").First(&variable).Error; err == nil {
|
||||
variable.DefaultValue = value
|
||||
database.DB.Save(&variable)
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "更新成功",
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetCards(c *gin.Context) {
|
||||
appID := c.GetUint("applicationID")
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
status := c.Query("status")
|
||||
cardTypeID := c.Query("card_type_id")
|
||||
|
||||
var cards []model.Card
|
||||
var total int64
|
||||
|
||||
query := database.DB.Model(&model.Card{}).Where("application_id = ?", appID)
|
||||
|
||||
if status != "" {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
|
||||
if cardTypeID != "" {
|
||||
query = query.Where("card_type_id = ?", cardTypeID)
|
||||
}
|
||||
|
||||
query.Count(&total)
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&cards).Error; err != nil {
|
||||
response.Error(c, 500, "获取卡密列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"cards": cards,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
"total_page": (total + int64(pageSize) - 1) / int64(pageSize),
|
||||
})
|
||||
}
|
||||
|
||||
func handleGenerateCards(c *gin.Context) {
|
||||
appID := c.GetUint("applicationID")
|
||||
|
||||
var req struct {
|
||||
CardTypeID uint `json:"card_type_id" binding:"required"`
|
||||
Count int `json:"count" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var cardType model.CardType
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", req.CardTypeID, appID).First(&cardType).Error; err != nil {
|
||||
response.Error(c, 404, "卡密类型不存在")
|
||||
return
|
||||
}
|
||||
|
||||
cards := make([]model.Card, req.Count)
|
||||
for i := 0; i < req.Count; i++ {
|
||||
cardKey := generateCardKey()
|
||||
cards[i] = model.Card{
|
||||
ApplicationID: appID,
|
||||
CardTypeID: req.CardTypeID,
|
||||
CardKey: cardKey,
|
||||
Status: "unused",
|
||||
}
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&cards).Error; err != nil {
|
||||
response.Error(c, 500, "生成卡密失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "生成成功",
|
||||
"count": req.Count,
|
||||
"cards": cards,
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetCard(c *gin.Context) {
|
||||
appID := c.GetUint("applicationID")
|
||||
cardID := c.Param("cardId")
|
||||
|
||||
var card model.Card
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", cardID, appID).First(&card).Error; err != nil {
|
||||
response.Error(c, 404, "卡密不存在")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, card)
|
||||
}
|
||||
|
||||
func handleSendNotification(c *gin.Context) {
|
||||
appID := c.GetUint("applicationID")
|
||||
|
||||
var req struct {
|
||||
UserID uint `json:"user_id" binding:"required"`
|
||||
Title string `json:"title" binding:"required"`
|
||||
Content string `json:"content" binding:"required"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", req.UserID, appID).First(&user).Error; err != nil {
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
notification := model.Announcement{
|
||||
ApplicationID: appID,
|
||||
Title: req.Title,
|
||||
Content: req.Content,
|
||||
Type: req.Type,
|
||||
Status: "active",
|
||||
}
|
||||
|
||||
if err := database.DB.Create(¬ification).Error; err != nil {
|
||||
response.Error(c, 500, "发送通知失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "发送成功",
|
||||
"notification": notification,
|
||||
})
|
||||
}
|
||||
|
||||
func handleSendBatchNotification(c *gin.Context) {
|
||||
appID := c.GetUint("applicationID")
|
||||
|
||||
var req struct {
|
||||
Title string `json:"title" binding:"required"`
|
||||
Content string `json:"content" binding:"required"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
notification := model.Announcement{
|
||||
ApplicationID: appID,
|
||||
Title: req.Title,
|
||||
Content: req.Content,
|
||||
Type: req.Type,
|
||||
Status: "active",
|
||||
}
|
||||
|
||||
if err := database.DB.Create(¬ification).Error; err != nil {
|
||||
response.Error(c, 500, "发送通知失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "发送成功",
|
||||
"notification": notification,
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetAppInfo(c *gin.Context) {
|
||||
appID := c.GetUint("applicationID")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.First(&app, appID).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"id": app.ID,
|
||||
"name": app.Name,
|
||||
"description": app.Description,
|
||||
"billing_type": app.BillingType,
|
||||
"encrypt_type": app.EncryptType,
|
||||
"bind_type": app.BindType,
|
||||
"max_devices": app.MaxDevices,
|
||||
"multi_open": app.MultiOpen,
|
||||
"enable_trial": app.EnableTrial,
|
||||
"trial_balance": app.TrialBalance,
|
||||
"status": app.Status,
|
||||
"created_at": app.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetAppStats(c *gin.Context) {
|
||||
appID := c.GetUint("applicationID")
|
||||
|
||||
var userCount, activeUserCount, cardCount, usedCardCount int64
|
||||
|
||||
database.DB.Model(&model.AppUser{}).Where("application_id = ?", appID).Count(&userCount)
|
||||
database.DB.Model(&model.AppUser{}).Where("application_id = ? AND status = ?", appID, "active").Count(&activeUserCount)
|
||||
database.DB.Model(&model.Card{}).Where("application_id = ?", appID).Count(&cardCount)
|
||||
database.DB.Model(&model.Card{}).Where("application_id = ? AND status = ?", appID, "used").Count(&usedCardCount)
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"user_count": userCount,
|
||||
"active_user_count": activeUserCount,
|
||||
"card_count": cardCount,
|
||||
"used_card_count": usedCardCount,
|
||||
})
|
||||
}
|
||||
|
||||
func generateCardKey() string {
|
||||
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
b := make([]byte, 16)
|
||||
for i := range b {
|
||||
b[i] = charset[i%len(charset)]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -0,0 +1,805 @@
|
||||
package frontend
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/service"
|
||||
"verification-platform-backend/internal/service/payment"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupRoutes(r *gin.Engine) {
|
||||
auth := r.Group("/api/v1/auth")
|
||||
{
|
||||
auth.POST("/register", handleRegister)
|
||||
auth.POST("/login", handleLogin)
|
||||
auth.POST("/logout", handleLogout)
|
||||
auth.POST("/refresh", handleRefreshToken)
|
||||
}
|
||||
|
||||
api := r.Group("/api/v1")
|
||||
{
|
||||
api.POST("/login", handleLogin)
|
||||
api.POST("/register", handleRegister)
|
||||
api.POST("/logout", handleLogout)
|
||||
api.POST("/refresh", handleRefreshToken)
|
||||
api.POST("/forgot-password", handleForgotPassword)
|
||||
api.POST("/reset-password", handleResetPassword)
|
||||
api.POST("/send-sms", handleSendSms)
|
||||
api.POST("/verify", handleCardVerification)
|
||||
api.GET("/verify/status", handleGetVerificationStatus)
|
||||
}
|
||||
|
||||
docs := r.Group("/api/v1/docs")
|
||||
{
|
||||
docs.GET("", handleGetDocs)
|
||||
docs.GET("/:id", handleGetDoc)
|
||||
docs.GET("/slug/:slug", handleGetDocBySlug)
|
||||
}
|
||||
|
||||
docCategories := r.Group("/api/v1/doc-categories")
|
||||
{
|
||||
docCategories.GET("", handleGetDocCategories)
|
||||
}
|
||||
|
||||
pricing := r.Group("/api/v1/pricing")
|
||||
{
|
||||
pricing.GET("", handleGetPricing)
|
||||
}
|
||||
|
||||
packages := r.Group("/api/v1/packages")
|
||||
{
|
||||
packages.GET("", handleGetPricing)
|
||||
}
|
||||
|
||||
site := r.Group("/api/v1/site")
|
||||
{
|
||||
site.GET("/info", handleGetSiteInfo)
|
||||
site.GET("/contact", handleGetContact)
|
||||
}
|
||||
|
||||
captcha := r.Group("/api/v1/captcha")
|
||||
{
|
||||
captcha.GET("", handleGetCaptcha)
|
||||
}
|
||||
|
||||
settings := r.Group("/api/v1/settings")
|
||||
{
|
||||
settings.GET("", handleGetSettings)
|
||||
settings.GET("/payment-channels", handleGetActivePaymentChannels)
|
||||
}
|
||||
|
||||
public := r.Group("/api/v1/public")
|
||||
{
|
||||
public.GET("/stats", handleGetPublicStats)
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetCaptcha(c *gin.Context) {
|
||||
authService := service.NewAuthService()
|
||||
result, err := authService.GetCaptcha()
|
||||
if err != nil {
|
||||
response.Error(c, 500, "获取验证码失败")
|
||||
return
|
||||
}
|
||||
response.Success(c, result)
|
||||
}
|
||||
|
||||
func handleGetSettings(c *gin.Context) {
|
||||
settingService := service.NewSettingService()
|
||||
settings, err := settingService.GetSettings()
|
||||
if err != nil {
|
||||
response.Error(c, 500, "获取设置失败")
|
||||
return
|
||||
}
|
||||
response.Success(c, settings)
|
||||
}
|
||||
|
||||
func handleGetActivePaymentChannels(c *gin.Context) {
|
||||
var channels []model.PaymentChannel
|
||||
if err := database.DB.Where("status = ?", "active").Order("sort asc, id asc").Find(&channels).Error; err != nil {
|
||||
response.Error(c, 500, "获取支付通道失败")
|
||||
return
|
||||
}
|
||||
response.Success(c, gin.H{"channels": channels})
|
||||
}
|
||||
|
||||
func handleRegister(c *gin.Context) {
|
||||
var req struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Email string `json:"email"`
|
||||
Phone string `json:"phone"`
|
||||
Role string `json:"role"`
|
||||
Type string `json:"type"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
fmt.Printf("注册参数错误: %v\n", err)
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("注册请求: username=%s, email=%s, phone=%s, role=%s, type=%s\n",
|
||||
req.Username, req.Email, req.Phone, req.Role, req.Type)
|
||||
|
||||
if req.Role == "" {
|
||||
req.Role = "developer"
|
||||
}
|
||||
|
||||
username := req.Username
|
||||
email := req.Email
|
||||
phone := req.Phone
|
||||
password := req.Password
|
||||
|
||||
if req.Type == "email" {
|
||||
username = email
|
||||
email = req.Email
|
||||
password = req.Password
|
||||
} else if req.Type == "sms" {
|
||||
username = phone
|
||||
phone = req.Phone
|
||||
password = req.Password
|
||||
}
|
||||
|
||||
fmt.Printf("处理后的注册数据: username=%s, email=%s, phone=%s, password_len=%d, role=%s\n",
|
||||
username, email, phone, len(password), req.Role)
|
||||
|
||||
authService := service.NewAuthService()
|
||||
err := authService.RegisterWithRole(username, email, phone, password, req.Role)
|
||||
if err != nil {
|
||||
fmt.Printf("注册失败: %v\n", err)
|
||||
response.Error(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "注册成功",
|
||||
})
|
||||
}
|
||||
|
||||
func handleLogin(c *gin.Context) {
|
||||
var req struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
AgentPath string `json:"agent_path"`
|
||||
CaptchaID string `json:"captcha_id"`
|
||||
Captcha string `json:"captcha"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
fmt.Printf("登录参数错误: %v\n", err)
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("登录请求: username=%s, password_len=%d, agent_path=%s\n",
|
||||
req.Username, len(req.Password), req.AgentPath)
|
||||
|
||||
// 检查是否启用了验证码
|
||||
var enableCaptchaSetting model.Setting
|
||||
err := database.DB.Where("category = ? AND key = ?", "basic", "enableCaptcha").First(&enableCaptchaSetting).Error
|
||||
enableCaptcha := false
|
||||
if err == nil && enableCaptchaSetting.Value == "true" {
|
||||
enableCaptcha = true
|
||||
}
|
||||
|
||||
// 如果启用了验证码,验证验证码
|
||||
if enableCaptcha {
|
||||
if req.CaptchaID == "" || req.Captcha == "" {
|
||||
response.Error(c, 400, "请输入验证码")
|
||||
return
|
||||
}
|
||||
|
||||
// 查询验证码
|
||||
var captcha model.Captcha
|
||||
if err := database.DB.Where("captcha_id = ?", req.CaptchaID).First(&captcha).Error; err != nil {
|
||||
response.Error(c, 400, "验证码错误或已过期")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查验证码是否过期
|
||||
if time.Now().After(captcha.ExpiresAt) {
|
||||
database.DB.Delete(&captcha)
|
||||
response.Error(c, 400, "验证码已过期")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证码比较(忽略大小写和空格)
|
||||
if strings.ToLower(strings.TrimSpace(req.Captcha)) != strings.ToLower(strings.TrimSpace(captcha.Code)) {
|
||||
database.DB.Delete(&captcha)
|
||||
response.Error(c, 400, "验证码错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 删除已使用的验证码
|
||||
database.DB.Delete(&captcha)
|
||||
}
|
||||
|
||||
authService := service.NewAuthService()
|
||||
result, err := authService.Login(req.Username, req.Password, req.AgentPath)
|
||||
if err != nil {
|
||||
fmt.Printf("登录失败: %v\n", err)
|
||||
response.Error(c, 401, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("登录成功: username=%s\n", req.Username)
|
||||
response.Success(c, result)
|
||||
}
|
||||
|
||||
func handleLogout(c *gin.Context) {
|
||||
response.Success(c, gin.H{
|
||||
"message": "登出成功",
|
||||
})
|
||||
}
|
||||
|
||||
func handleRefreshToken(c *gin.Context) {
|
||||
response.Success(c, gin.H{
|
||||
"token": "",
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetDocs(c *gin.Context) {
|
||||
var docs []model.Doc
|
||||
if err := database.DB.Where("status = ?", "published").Preload("Category").Find(&docs).Error; err != nil {
|
||||
response.Error(c, 500, "获取文档列表失败")
|
||||
return
|
||||
}
|
||||
response.Success(c, docs)
|
||||
}
|
||||
|
||||
func handleGetDoc(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var doc model.Doc
|
||||
if err := database.DB.Where("id = ? AND status = ?", id, "published").Preload("Category").First(&doc).Error; err != nil {
|
||||
response.Error(c, 404, "文档不存在")
|
||||
return
|
||||
}
|
||||
response.Success(c, doc)
|
||||
}
|
||||
|
||||
func handleGetDocBySlug(c *gin.Context) {
|
||||
slug := c.Param("slug")
|
||||
var doc model.Doc
|
||||
if err := database.DB.Where("slug = ? AND status = ?", slug, "published").Preload("Category").First(&doc).Error; err != nil {
|
||||
response.Error(c, 404, "文档不存在")
|
||||
return
|
||||
}
|
||||
response.Success(c, doc)
|
||||
}
|
||||
|
||||
func handleGetDocCategories(c *gin.Context) {
|
||||
var categories []model.DocCategory
|
||||
if err := database.DB.Find(&categories).Error; err != nil {
|
||||
response.Error(c, 500, "获取文档分类失败")
|
||||
return
|
||||
}
|
||||
response.Success(c, categories)
|
||||
}
|
||||
|
||||
func handleGetPricing(c *gin.Context) {
|
||||
var packages []model.Package
|
||||
if err := database.DB.Where("status = ?", "active").Find(&packages).Error; err != nil {
|
||||
response.Error(c, 500, "获取价格信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
var result []gin.H
|
||||
for _, pkg := range packages {
|
||||
var permission model.PackagePermission
|
||||
database.DB.Where("package_id = ?", pkg.ID).First(&permission)
|
||||
|
||||
result = append(result, gin.H{
|
||||
"id": pkg.ID,
|
||||
"name": pkg.Name,
|
||||
"name_en": pkg.NameEn,
|
||||
"price": pkg.Price,
|
||||
"currency": pkg.Currency,
|
||||
"period": pkg.Period,
|
||||
"description": pkg.Description,
|
||||
"description_en": pkg.DescriptionEn,
|
||||
"status": pkg.Status,
|
||||
"sort": pkg.Sort,
|
||||
"is_recommended": pkg.IsRecommended,
|
||||
"allow_upgrade": pkg.AllowUpgrade,
|
||||
"created_at": pkg.CreatedAt,
|
||||
"updated_at": pkg.UpdatedAt,
|
||||
"permissions": permission,
|
||||
})
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"packages": result,
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetSiteInfo(c *gin.Context) {
|
||||
response.Success(c, gin.H{
|
||||
"site_name": "验证平台",
|
||||
"site_url": "http://localhost:3000",
|
||||
"description": "专业的应用验证平台",
|
||||
"contact_email": "admin@example.com",
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetContact(c *gin.Context) {
|
||||
response.Success(c, gin.H{
|
||||
"email": "admin@example.com",
|
||||
"qq": "123456789",
|
||||
"wechat": "example_wechat",
|
||||
"phone": "400-123-4567",
|
||||
})
|
||||
}
|
||||
|
||||
func handleForgotPassword(c *gin.Context) {
|
||||
var req struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
authService := service.NewAuthService()
|
||||
err := authService.ForgotPassword(req.Email)
|
||||
if err != nil {
|
||||
response.Error(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "重置链接已发送到您的邮箱",
|
||||
})
|
||||
}
|
||||
|
||||
func handleResetPassword(c *gin.Context) {
|
||||
var req struct {
|
||||
Token string `json:"token"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
authService := service.NewAuthService()
|
||||
err := authService.ResetPassword(req.Token, req.Password)
|
||||
if err != nil {
|
||||
response.Error(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "密码重置成功",
|
||||
})
|
||||
}
|
||||
|
||||
func handleSendSms(c *gin.Context) {
|
||||
var req struct {
|
||||
Phone string `json:"phone"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "验证码已发送",
|
||||
})
|
||||
}
|
||||
|
||||
func HandleGetProfile(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
response.Error(c, 401, "未授权")
|
||||
return
|
||||
}
|
||||
|
||||
authService := service.NewAuthService()
|
||||
user, err := authService.GetProfile(userID.(uint))
|
||||
if err != nil {
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, user)
|
||||
}
|
||||
|
||||
func HandleUpdateProfile(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
response.Error(c, 401, "未授权")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Email string `json:"email"`
|
||||
Phone string `json:"phone"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
authService := service.NewAuthService()
|
||||
err := authService.UpdateProfile(userID.(uint), req.Email, req.Phone)
|
||||
if err != nil {
|
||||
response.Error(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "资料更新成功",
|
||||
})
|
||||
}
|
||||
|
||||
func HandleChangePassword(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
response.Error(c, 401, "未授权")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
OldPassword string `json:"old_password"`
|
||||
NewPassword string `json:"new_password"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
authService := service.NewAuthService()
|
||||
err := authService.ChangePassword(userID.(uint), req.OldPassword, req.NewPassword)
|
||||
if err != nil {
|
||||
response.Error(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "密码修改成功",
|
||||
})
|
||||
}
|
||||
|
||||
func HandleUploadAvatar(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
response.Error(c, 401, "未授权")
|
||||
return
|
||||
}
|
||||
|
||||
file, err := c.FormFile("avatar")
|
||||
if err != nil {
|
||||
response.Error(c, 400, "请上传文件")
|
||||
return
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("avatar_%d_%s", userID, file.Filename)
|
||||
if err := c.SaveUploadedFile(file, "uploads/"+filename); err != nil {
|
||||
response.Error(c, 500, "文件保存失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"avatar": "/uploads/" + filename,
|
||||
})
|
||||
}
|
||||
|
||||
func handleCardVerification(c *gin.Context) {
|
||||
var req struct {
|
||||
CardKey string `json:"card_key"`
|
||||
Username string `json:"username"`
|
||||
DeviceID string `json:"device_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var card model.Card
|
||||
if err := database.DB.Where("card_key = ?", req.CardKey).First(&card).Error; err != nil {
|
||||
response.Error(c, 404, "卡密不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if card.Status != "unused" {
|
||||
response.Error(c, 400, "卡密已被使用或已禁用")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "验证成功",
|
||||
"card_id": card.ID,
|
||||
"card_type": card.CardType,
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetVerificationStatus(c *gin.Context) {
|
||||
cardKey := c.Query("card_key")
|
||||
if cardKey == "" {
|
||||
response.Error(c, 400, "请提供卡密")
|
||||
return
|
||||
}
|
||||
|
||||
var card model.Card
|
||||
if err := database.DB.Where("card_key = ?", cardKey).First(&card).Error; err != nil {
|
||||
response.Error(c, 404, "卡密不存在")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"status": card.Status,
|
||||
"card_id": card.ID,
|
||||
"used_at": card.UsedAt,
|
||||
})
|
||||
}
|
||||
|
||||
func HandleCreateOrder(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
response.Error(c, 401, "请先登录")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
PackageID uint `json:"package_id"`
|
||||
PaymentChannelID uint `json:"payment_channel_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if req.PackageID == 0 {
|
||||
response.Error(c, 400, "请选择套餐")
|
||||
return
|
||||
}
|
||||
|
||||
var pkg model.Package
|
||||
if err := database.DB.Where("id = ? AND status = ?", req.PackageID, "active").First(&pkg).Error; err != nil {
|
||||
response.Error(c, 404, "套餐不存在或已下架")
|
||||
return
|
||||
}
|
||||
|
||||
orderNo := fmt.Sprintf("ORD%d%d", time.Now().Unix(), userID.(uint))
|
||||
|
||||
if pkg.Price == 0 {
|
||||
now := time.Now()
|
||||
pkgID := pkg.ID
|
||||
order := model.Order{
|
||||
OrderNo: orderNo,
|
||||
UserID: userID.(uint),
|
||||
PackageID: &pkgID,
|
||||
OrderType: "package",
|
||||
Title: fmt.Sprintf("领取套餐 - %s", pkg.Name),
|
||||
Amount: 0,
|
||||
PaymentType: "free",
|
||||
Status: "paid",
|
||||
PaymentAt: &now,
|
||||
Description: fmt.Sprintf("免费套餐: %s, 周期: %s", pkg.Name, pkg.Period),
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&order).Error; err != nil {
|
||||
response.Error(c, 500, "创建订单失败")
|
||||
return
|
||||
}
|
||||
|
||||
var expiredAt *time.Time
|
||||
if pkg.Period != "" && pkg.Period != "permanent" {
|
||||
duration := parsePeriod(pkg.Period)
|
||||
if duration > 0 {
|
||||
exp := now.Add(duration)
|
||||
expiredAt = &exp
|
||||
}
|
||||
}
|
||||
|
||||
userPackage := model.UserPackage{
|
||||
UserID: userID.(uint),
|
||||
PackageID: pkg.ID,
|
||||
ExpiredAt: expiredAt,
|
||||
Status: "active",
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&userPackage).Error; err != nil {
|
||||
response.Error(c, 500, "创建套餐授权失败")
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DB.Model(&model.User{}).Where("id = ?", userID).Update("current_package_id", pkg.ID).Error; err != nil {
|
||||
response.Error(c, 500, "更新用户套餐失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"order_no": order.OrderNo,
|
||||
"order_id": order.ID,
|
||||
"amount": 0,
|
||||
"status": "paid",
|
||||
"message": "免费套餐领取成功",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if req.PaymentChannelID == 0 {
|
||||
response.Error(c, 400, "请选择支付方式")
|
||||
return
|
||||
}
|
||||
|
||||
var paymentChannel model.PaymentChannel
|
||||
if err := database.DB.Where("id = ? AND status = ?", req.PaymentChannelID, "active").First(&paymentChannel).Error; err != nil {
|
||||
response.Error(c, 404, "支付通道不存在或已禁用")
|
||||
return
|
||||
}
|
||||
|
||||
pkgID := pkg.ID
|
||||
order := model.Order{
|
||||
OrderNo: orderNo,
|
||||
UserID: userID.(uint),
|
||||
PackageID: &pkgID,
|
||||
OrderType: "package",
|
||||
Title: fmt.Sprintf("购买套餐 - %s", pkg.Name),
|
||||
Amount: pkg.Price,
|
||||
PaymentType: paymentChannel.Type,
|
||||
PaymentMethod: paymentChannel.Type,
|
||||
Status: "pending",
|
||||
Description: fmt.Sprintf("套餐ID: %d, 套餐名称: %s, 周期: %s, 支付通道: %s", pkg.ID, pkg.Name, pkg.Period, paymentChannel.Name),
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&order).Error; err != nil {
|
||||
response.Error(c, 500, "创建订单失败")
|
||||
return
|
||||
}
|
||||
|
||||
scheme := "http"
|
||||
if c.Request.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
host := c.Request.Host
|
||||
callbackBaseURL := fmt.Sprintf("%s://%s", scheme, host)
|
||||
|
||||
switch paymentChannel.Type {
|
||||
case "bepusdt":
|
||||
paymentService, err := payment.GetPaymentService(paymentChannel, callbackBaseURL)
|
||||
if err != nil {
|
||||
response.Error(c, 500, "支付服务初始化失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
notifyURL := fmt.Sprintf("%s/api/v1/payment/callback/bepusdt", callbackBaseURL)
|
||||
redirectURL := fmt.Sprintf("%s/developer/finance?order=%s", callbackBaseURL, orderNo)
|
||||
|
||||
result, err := paymentService.CreateOrder(orderNo, pkg.Price, notifyURL, redirectURL, order.Title)
|
||||
if err != nil {
|
||||
response.Error(c, 500, "创建支付订单失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
extraData := map[string]interface{}{
|
||||
"trade_id": result.TradeID,
|
||||
"payment_url": result.PaymentURL,
|
||||
"actual_amount": result.ActualAmount,
|
||||
"token": result.Token,
|
||||
"expiration_time": result.ExpirationTime,
|
||||
"channel_id": paymentChannel.ID,
|
||||
}
|
||||
extraJSON, _ := json.Marshal(extraData)
|
||||
order.ExtraData = string(extraJSON)
|
||||
database.DB.Save(&order)
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"order_no": order.OrderNo,
|
||||
"order_id": order.ID,
|
||||
"amount": order.Amount,
|
||||
"actual_amount": result.ActualAmount,
|
||||
"payment_url": result.PaymentURL,
|
||||
"trade_id": result.TradeID,
|
||||
"token": result.Token,
|
||||
"expiration_time": result.ExpirationTime,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"order_no": order.OrderNo,
|
||||
"order_id": order.ID,
|
||||
"amount": order.Amount,
|
||||
"pay_url": "",
|
||||
})
|
||||
}
|
||||
|
||||
func HandleGetOrder(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
response.Error(c, 401, "请先登录")
|
||||
return
|
||||
}
|
||||
|
||||
orderNo := c.Param("order_no")
|
||||
if orderNo == "" {
|
||||
response.Error(c, 400, "订单号不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
var order model.Order
|
||||
if err := database.DB.Where("order_no = ? AND user_id = ?", orderNo, userID).First(&order).Error; err != nil {
|
||||
response.Error(c, 404, "订单不存在")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"order": gin.H{
|
||||
"id": order.ID,
|
||||
"order_no": order.OrderNo,
|
||||
"amount": order.Amount,
|
||||
"status": order.Status,
|
||||
"payment_type": order.PaymentType,
|
||||
"title": order.Title,
|
||||
"extra_data": order.ExtraData,
|
||||
"created_at": order.CreatedAt,
|
||||
"payment_at": order.PaymentAt,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func HandleGetOrderStatus(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
response.Error(c, 401, "请先登录")
|
||||
return
|
||||
}
|
||||
|
||||
orderNo := c.Param("order_no")
|
||||
if orderNo == "" {
|
||||
response.Error(c, 400, "订单号不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
var order model.Order
|
||||
if err := database.DB.Where("order_no = ? AND user_id = ?", orderNo, userID).First(&order).Error; err != nil {
|
||||
response.Error(c, 404, "订单不存在")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"status": order.Status,
|
||||
"payment_at": order.PaymentAt,
|
||||
})
|
||||
}
|
||||
|
||||
func parsePeriod(period string) time.Duration {
|
||||
switch period {
|
||||
case "daily":
|
||||
return 24 * time.Hour
|
||||
case "weekly":
|
||||
return 7 * 24 * time.Hour
|
||||
case "monthly":
|
||||
return 30 * 24 * time.Hour
|
||||
case "quarterly":
|
||||
return 90 * 24 * time.Hour
|
||||
case "yearly":
|
||||
return 365 * 24 * time.Hour
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetPublicStats(c *gin.Context) {
|
||||
var totalApps, totalUsers, totalVerifications int64
|
||||
|
||||
database.DB.Model(&model.Application{}).Count(&totalApps)
|
||||
database.DB.Model(&model.User{}).Where("role = ?", "developer").Count(&totalUsers)
|
||||
database.DB.Model(&model.Card{}).Where("status = ?", "used").Count(&totalVerifications)
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"totalApps": totalApps,
|
||||
"totalUsers": totalUsers,
|
||||
"totalVerifications": totalVerifications,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package frontend
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/service/payment"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupPaymentCallbackRoutes(r *gin.Engine) {
|
||||
r.POST("/api/v1/payment/callback/bepusdt", handleBEpusdtCallback)
|
||||
}
|
||||
|
||||
func handleBEpusdtCallback(c *gin.Context) {
|
||||
body, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
c.String(200, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
var callbackData payment.CallbackData
|
||||
if err := json.Unmarshal(body, &callbackData); err != nil {
|
||||
c.String(200, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
var order model.Order
|
||||
if err := database.DB.Where("order_no = ?", callbackData.OrderID).First(&order).Error; err != nil {
|
||||
c.String(200, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
if order.Status != "pending" {
|
||||
c.String(200, "success")
|
||||
return
|
||||
}
|
||||
|
||||
var extraData struct {
|
||||
TradeID string `json:"trade_id"`
|
||||
ChannelID uint `json:"channel_id"`
|
||||
ExpirationTime string `json:"expiration_time"`
|
||||
}
|
||||
if order.ExtraData != "" {
|
||||
json.Unmarshal([]byte(order.ExtraData), &extraData)
|
||||
}
|
||||
|
||||
if extraData.ExpirationTime != "" {
|
||||
expirationTime, err := time.Parse(time.RFC3339, extraData.ExpirationTime)
|
||||
if err == nil && time.Now().After(expirationTime) {
|
||||
c.String(200, "fail")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var paymentChannel model.PaymentChannel
|
||||
if extraData.ChannelID > 0 {
|
||||
database.DB.First(&paymentChannel, extraData.ChannelID)
|
||||
}
|
||||
|
||||
if paymentChannel.ID > 0 {
|
||||
paymentService, err := payment.GetPaymentService(paymentChannel, "")
|
||||
if err == nil {
|
||||
if !paymentService.VerifyCallback(callbackData) {
|
||||
c.String(200, "fail")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if callbackData.Status == 2 {
|
||||
now := time.Now()
|
||||
order.Status = "paid"
|
||||
order.PaymentAt = &now
|
||||
order.PaymentMethod = "bepusdt"
|
||||
|
||||
if err := database.DB.Save(&order).Error; err != nil {
|
||||
c.String(200, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
if order.OrderType == "package" {
|
||||
var pkg model.Package
|
||||
if err := database.DB.First(&pkg, order.PackageID).Error; err == nil {
|
||||
var expiredAt *time.Time
|
||||
if pkg.Period != "" && pkg.Period != "permanent" {
|
||||
duration := parsePeriod(pkg.Period)
|
||||
if duration > 0 {
|
||||
exp := now.Add(duration)
|
||||
expiredAt = &exp
|
||||
}
|
||||
}
|
||||
|
||||
userPackage := model.UserPackage{
|
||||
UserID: order.UserID,
|
||||
PackageID: pkg.ID,
|
||||
ExpiredAt: expiredAt,
|
||||
Status: "active",
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&userPackage).Error; err != nil {
|
||||
fmt.Printf("创建套餐授权失败: %v\n", err)
|
||||
} else {
|
||||
database.DB.Model(&model.User{}).Where("id = ?", order.UserID).Update("current_package_id", pkg.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.String(200, "success")
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"verification-platform-backend/internal/middleware"
|
||||
"verification-platform-backend/internal/router/app"
|
||||
"verification-platform-backend/internal/router/developer"
|
||||
"verification-platform-backend/internal/router/extension"
|
||||
"verification-platform-backend/internal/router/frontend"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupRoutes(r *gin.Engine) {
|
||||
r.Use(middleware.Cors())
|
||||
r.Use(middleware.Logger())
|
||||
|
||||
r.GET("/health", func(c *gin.Context) {
|
||||
response.Success(c, gin.H{
|
||||
"status": "ok",
|
||||
"time": time.Now().Format(time.RFC3339),
|
||||
})
|
||||
})
|
||||
|
||||
frontend.SetupRoutes(r)
|
||||
|
||||
api := r.Group("/api/v1")
|
||||
{
|
||||
devGroup := api.Group("/dev")
|
||||
{
|
||||
devGroup.Use(middleware.JWT())
|
||||
devGroup.Use(middleware.DeveloperAuth())
|
||||
developer.SetupRoutes(devGroup)
|
||||
developer.SetupRoutesWithoutPackage(devGroup)
|
||||
}
|
||||
|
||||
appGroup := api.Group("/app/:appKey")
|
||||
{
|
||||
appGroup.Use(app.AppCryptoMiddleware())
|
||||
appGroup.Use(app.ResponseEncryption())
|
||||
app.SetupInfoRoutes(appGroup)
|
||||
}
|
||||
|
||||
appGroupWithStatusCheck := api.Group("/app/:appKey")
|
||||
{
|
||||
appGroupWithStatusCheck.Use(middleware.CheckAppStatus())
|
||||
appGroupWithStatusCheck.Use(app.AppCryptoMiddleware())
|
||||
appGroupWithStatusCheck.Use(app.ResponseEncryption())
|
||||
app.SetupRoutes(appGroupWithStatusCheck)
|
||||
app.SetupAuthUserRoutes(appGroupWithStatusCheck)
|
||||
app.SetupDevicePublicRoutes(appGroupWithStatusCheck)
|
||||
}
|
||||
|
||||
appAuthGroup := api.Group("/app/:appKey")
|
||||
{
|
||||
appAuthGroup.Use(middleware.JWT())
|
||||
appAuthGroup.Use(middleware.CheckAppStatus())
|
||||
appAuthGroup.Use(app.AppCryptoMiddleware())
|
||||
appAuthGroup.Use(app.ResponseEncryption())
|
||||
app.SetupCloudRoutes(appAuthGroup)
|
||||
app.SetupAuthRoutes(appAuthGroup)
|
||||
}
|
||||
|
||||
extGroup := api.Group("")
|
||||
{
|
||||
extension.SetupRoutes(extGroup)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/pkg/jwt"
|
||||
"verification-platform-backend/pkg/utils"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AuthService 认证服务
|
||||
type AuthService struct{}
|
||||
|
||||
// NewAuthService 创建认证服务实例
|
||||
func NewAuthService() *AuthService {
|
||||
return &AuthService{}
|
||||
}
|
||||
|
||||
// Login 用户登录
|
||||
func (s *AuthService) Login(username, password, agentPath string) (map[string]interface{}, error) {
|
||||
// 查找用户
|
||||
var user model.User
|
||||
err := database.DB.Where("username = ?", username).First(&user).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("用户名或密码错误")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
|
||||
if err != nil {
|
||||
return nil, errors.New("用户名或密码错误")
|
||||
}
|
||||
|
||||
// 更新最后登录时间
|
||||
user.LastLoginAt = &time.Time{}
|
||||
*user.LastLoginAt = time.Now()
|
||||
database.DB.Save(&user)
|
||||
|
||||
// 生成JWT令牌
|
||||
token, err := jwt.GenerateToken(user.ID, user.Username, user.Role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 返回用户信息和令牌
|
||||
return map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"avatar": user.Avatar,
|
||||
"role": user.Role,
|
||||
"status": user.Status,
|
||||
},
|
||||
"token": token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Register 用户注册
|
||||
func (s *AuthService) Register(username, email, password string) error {
|
||||
return s.RegisterWithRole(username, email, "", password, "developer")
|
||||
}
|
||||
|
||||
// RegisterWithRole 用户注册(带角色)
|
||||
func (s *AuthService) RegisterWithRole(username, email, phone, password, role string) error {
|
||||
if username == "" {
|
||||
return errors.New("用户名不能为空")
|
||||
}
|
||||
if password == "" {
|
||||
return errors.New("密码不能为空")
|
||||
}
|
||||
|
||||
var existingUser model.User
|
||||
err := database.DB.Where("username = ?", username).First(&existingUser).Error
|
||||
if err == nil {
|
||||
return errors.New("用户名已存在")
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
if email != "" {
|
||||
var emailStr string = email
|
||||
err = database.DB.Where("email = ?", emailStr).First(&existingUser).Error
|
||||
if err == nil {
|
||||
return errors.New("邮箱已被注册")
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if phone != "" {
|
||||
err = database.DB.Where("device_id = ?", phone).First(&existingUser).Error
|
||||
if err == nil {
|
||||
return errors.New("手机号已被注册")
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if role == "" {
|
||||
role = "developer"
|
||||
}
|
||||
|
||||
user := model.User{
|
||||
Username: username,
|
||||
DeviceID: phone,
|
||||
Password: string(hashedPassword),
|
||||
Role: role,
|
||||
Status: "active",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if email != "" {
|
||||
user.Email = &email
|
||||
}
|
||||
|
||||
err = database.DB.Create(&user).Error
|
||||
if err != nil {
|
||||
// 处理数据库唯一约束错误
|
||||
errMsg := err.Error()
|
||||
if strings.Contains(errMsg, "UNIQUE constraint failed") {
|
||||
if strings.Contains(errMsg, "users.username") {
|
||||
return errors.New("用户名已存在")
|
||||
}
|
||||
if strings.Contains(errMsg, "users.email") {
|
||||
return errors.New("邮箱已被注册")
|
||||
}
|
||||
if strings.Contains(errMsg, "users.device_id") {
|
||||
return errors.New("手机号已被注册")
|
||||
}
|
||||
return errors.New("该账号已被注册")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetProfile 获取用户资料
|
||||
func (s *AuthService) GetProfile(userID uint) (*model.User, error) {
|
||||
var user model.User
|
||||
err := database.DB.First(&user, userID).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 不返回密码
|
||||
user.Password = ""
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// UpdateProfile 更新用户资料
|
||||
func (s *AuthService) UpdateProfile(userID uint, email, phone string) error {
|
||||
// 检查邮箱是否已被其他用户使用
|
||||
if email != "" {
|
||||
var existingUser model.User
|
||||
err := database.DB.Where("email = ? AND id != ?", email, userID).First(&existingUser).Error
|
||||
if err == nil {
|
||||
return errors.New("邮箱已被其他用户使用")
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
updates := map[string]interface{}{
|
||||
"email": email,
|
||||
"phone": phone,
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
|
||||
err := database.DB.Model(&model.User{}).Where("id = ?", userID).Updates(updates).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChangePassword 修改密码
|
||||
func (s *AuthService) ChangePassword(userID uint, oldPassword, newPassword string) error {
|
||||
// 获取用户
|
||||
var user model.User
|
||||
err := database.DB.First(&user, userID).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 验证旧密码
|
||||
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(oldPassword))
|
||||
if err != nil {
|
||||
return errors.New("原密码错误")
|
||||
}
|
||||
|
||||
// 加密新密码
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
user.Password = string(hashedPassword)
|
||||
user.UpdatedAt = time.Now()
|
||||
|
||||
err = database.DB.Save(&user).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ForgotPassword 忘记密码
|
||||
func (s *AuthService) ForgotPassword(email string) error {
|
||||
// 查找用户
|
||||
var user model.User
|
||||
err := database.DB.Where("email = ?", email).First(&user).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("邮箱未注册")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 生成重置token
|
||||
resetToken := utils.GenerateRandomString(32)
|
||||
user.ResetToken = resetToken
|
||||
expiresAt := time.Now().Add(24 * time.Hour) // 24小时有效期
|
||||
user.ResetTokenExpiresAt = &expiresAt
|
||||
|
||||
err = database.DB.Save(&user).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 这里应该发送邮件,简化处理
|
||||
_ = resetToken
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetPassword 重置密码
|
||||
func (s *AuthService) ResetPassword(token, newPassword string) error {
|
||||
// 查找用户
|
||||
var user model.User
|
||||
err := database.DB.Where("reset_token = ? AND reset_token_expires_at > ?", token, time.Now()).First(&user).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("重置链接无效或已过期")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 加密新密码
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
user.Password = string(hashedPassword)
|
||||
user.ResetToken = ""
|
||||
var emptyTime time.Time
|
||||
user.ResetTokenExpiresAt = &emptyTime
|
||||
user.UpdatedAt = time.Now()
|
||||
|
||||
err = database.DB.Save(&user).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCaptcha 获取验证码
|
||||
func (s *AuthService) GetCaptcha() (map[string]interface{}, error) {
|
||||
// 生成随机验证码
|
||||
captchaText := utils.GenerateRandomString(6)
|
||||
|
||||
// 生成验证码ID
|
||||
captchaID := utils.GenerateRandomString(16)
|
||||
|
||||
// 存储验证码到数据库
|
||||
captcha := model.Captcha{
|
||||
CaptchaID: captchaID,
|
||||
Code: captchaText,
|
||||
ExpiresAt: time.Now().Add(5 * time.Minute),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&captcha).Error; err != nil {
|
||||
return nil, fmt.Errorf("存储验证码失败")
|
||||
}
|
||||
|
||||
// 创建一个简单的验证码图片
|
||||
// 这里使用SVG格式生成一个简单的验证码图片
|
||||
svg := generateCaptchaSVG(captchaText)
|
||||
|
||||
// 将SVG转换为base64
|
||||
captchaImage := "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(svg))
|
||||
|
||||
return map[string]interface{}{
|
||||
"captcha_id": captchaID,
|
||||
"captcha_image": captchaImage,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// generateCaptchaSVG 生成验证码SVG图片
|
||||
func generateCaptchaSVG(text string) string {
|
||||
width := 120
|
||||
height := 40
|
||||
|
||||
// 生成随机颜色
|
||||
colors := []string{"#FF5722", "#4CAF50", "#2196F3", "#FF9800", "#9C27B0"}
|
||||
bgColor := "#F5F5F5"
|
||||
|
||||
// 创建SVG
|
||||
svg := fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" viewBox="0 0 %d %d">
|
||||
<rect width="100%%" height="100%%" fill="%s"/>
|
||||
<text x="50%%" y="50%%" text-anchor="middle" dominant-baseline="middle"
|
||||
font-family="Arial, sans-serif" font-size="20" font-weight="bold" fill="%s">%s</text>
|
||||
</svg>`, width, height, width, height, bgColor, colors[0], text)
|
||||
|
||||
return svg
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"mime"
|
||||
"net/smtp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type EmailService struct{}
|
||||
|
||||
func NewEmailService() *EmailService {
|
||||
return &EmailService{}
|
||||
}
|
||||
|
||||
type EmailConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
User string
|
||||
Password string
|
||||
FromName string
|
||||
FromEmail string
|
||||
UseSSL bool
|
||||
}
|
||||
|
||||
func (s *EmailService) SendEmail(config EmailConfig, to, subject, body string) error {
|
||||
if config.Host == "" || config.User == "" || config.Password == "" {
|
||||
return fmt.Errorf("邮件配置不完整")
|
||||
}
|
||||
|
||||
from := config.FromEmail
|
||||
if from == "" {
|
||||
from = config.User
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", config.Host, config.Port)
|
||||
|
||||
msg := s.buildMessage(config.FromName, from, to, subject, body)
|
||||
|
||||
auth := smtp.PlainAuth("", config.User, config.Password, config.Host)
|
||||
|
||||
if config.UseSSL || config.Port == 465 || config.Port == 587 {
|
||||
return s.sendWithTLS(addr, auth, from, []string{to}, []byte(msg))
|
||||
}
|
||||
|
||||
return smtp.SendMail(addr, auth, from, []string{to}, []byte(msg))
|
||||
}
|
||||
|
||||
func (s *EmailService) sendWithTLS(addr string, auth smtp.Auth, from string, to []string, msg []byte) error {
|
||||
host := strings.Split(addr, ":")[0]
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
ServerName: host,
|
||||
}
|
||||
|
||||
conn, err := tls.Dial("tcp", addr, tlsConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("TLS连接失败: %v", err)
|
||||
}
|
||||
|
||||
client, err := smtp.NewClient(conn, host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建SMTP客户端失败: %v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
if err = client.Auth(auth); err != nil {
|
||||
return fmt.Errorf("SMTP认证失败: %v", err)
|
||||
}
|
||||
|
||||
if err = client.Mail(from); err != nil {
|
||||
return fmt.Errorf("设置发件人失败: %v", err)
|
||||
}
|
||||
|
||||
for _, addr := range to {
|
||||
if err = client.Rcpt(addr); err != nil {
|
||||
return fmt.Errorf("设置收件人失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
w, err := client.Data()
|
||||
if err != nil {
|
||||
return fmt.Errorf("准备邮件数据失败: %v", err)
|
||||
}
|
||||
|
||||
_, err = w.Write(msg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("写入邮件内容失败: %v", err)
|
||||
}
|
||||
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("关闭邮件写入失败: %v", err)
|
||||
}
|
||||
|
||||
return client.Quit()
|
||||
}
|
||||
|
||||
func (s *EmailService) buildMessage(fromName, from, to, subject, body string) string {
|
||||
msg := ""
|
||||
|
||||
if fromName != "" {
|
||||
encodedName := mime.QEncoding.Encode("UTF-8", fromName)
|
||||
msg += fmt.Sprintf("From: %s <%s>\r\n", encodedName, from)
|
||||
} else {
|
||||
msg += fmt.Sprintf("From: %s\r\n", from)
|
||||
}
|
||||
|
||||
msg += fmt.Sprintf("To: %s\r\n", to)
|
||||
msg += fmt.Sprintf("Subject: %s\r\n", mime.QEncoding.Encode("UTF-8", subject))
|
||||
msg += "MIME-version: 1.0;\r\nContent-Type: text/html; charset=\"UTF-8\";\r\n\r\n"
|
||||
msg += body
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
func (s *EmailService) SendTestEmail(config EmailConfig, to, siteName string) error {
|
||||
subject := fmt.Sprintf("测试邮件 - %s", siteName)
|
||||
body := fmt.Sprintf(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; padding: 20px; background-color: #f5f5f5;">
|
||||
<div style="max-width: 600px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
<h2 style="color: #333; margin-bottom: 20px;">邮件发送测试成功</h2>
|
||||
<p style="color: #666; line-height: 1.6;">这是一封测试邮件,如果您收到此邮件,说明您的SMTP配置正确。</p>
|
||||
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
||||
<p style="color: #999; font-size: 12px;">此邮件由 %s 系统自动发送,请勿回复。</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`, siteName)
|
||||
return s.SendEmail(config, to, subject, body)
|
||||
}
|
||||
|
||||
func ParseBool(value string) bool {
|
||||
return value == "true" || value == "1" || strings.ToLower(value) == "yes"
|
||||
}
|
||||
|
||||
func ParseInt(value string, defaultValue int) int {
|
||||
if value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
val, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
return val
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type LogService struct{}
|
||||
|
||||
type LogParams struct {
|
||||
UserID *uint
|
||||
ApplicationID *uint
|
||||
AppUserID *uint
|
||||
LogType string
|
||||
Action string
|
||||
Resource string
|
||||
ResourceID *uint
|
||||
Details string
|
||||
IPAddress string
|
||||
UserAgent string
|
||||
DeviceID string
|
||||
Status string
|
||||
Level string
|
||||
ErrorMessage string
|
||||
StackTrace string
|
||||
}
|
||||
|
||||
func NewLogService() *LogService {
|
||||
return &LogService{}
|
||||
}
|
||||
|
||||
func (s *LogService) CreateLog(params LogParams) error {
|
||||
log := model.Log{
|
||||
UserID: params.UserID,
|
||||
ApplicationID: params.ApplicationID,
|
||||
AppUserID: params.AppUserID,
|
||||
LogType: params.LogType,
|
||||
Action: params.Action,
|
||||
Resource: params.Resource,
|
||||
ResourceID: params.ResourceID,
|
||||
Details: params.Details,
|
||||
IPAddress: params.IPAddress,
|
||||
UserAgent: params.UserAgent,
|
||||
DeviceID: params.DeviceID,
|
||||
Status: params.Status,
|
||||
Level: params.Level,
|
||||
ErrorMessage: params.ErrorMessage,
|
||||
StackTrace: params.StackTrace,
|
||||
}
|
||||
|
||||
if log.LogType == "" {
|
||||
log.LogType = "operation"
|
||||
}
|
||||
if log.Status == "" {
|
||||
log.Status = "success"
|
||||
}
|
||||
if log.Level == "" {
|
||||
log.Level = "info"
|
||||
}
|
||||
|
||||
return database.DB.Create(&log).Error
|
||||
}
|
||||
|
||||
func (s *LogService) CreateLogFromContext(c *gin.Context, params LogParams) error {
|
||||
params.IPAddress = c.ClientIP()
|
||||
params.UserAgent = c.GetHeader("User-Agent")
|
||||
|
||||
if userID, exists := c.Get("user_id"); exists && params.UserID == nil {
|
||||
if uid, ok := userID.(uint); ok {
|
||||
params.UserID = &uid
|
||||
}
|
||||
}
|
||||
|
||||
return s.CreateLog(params)
|
||||
}
|
||||
|
||||
func (s *LogService) LogOperation(c *gin.Context, action, resource string, resourceID *uint, details string, err error) error {
|
||||
params := LogParams{
|
||||
LogType: "operation",
|
||||
Action: action,
|
||||
Resource: resource,
|
||||
ResourceID: resourceID,
|
||||
Details: details,
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
params.Status = "failed"
|
||||
params.Level = "error"
|
||||
params.ErrorMessage = err.Error()
|
||||
}
|
||||
|
||||
return s.CreateLogFromContext(c, params)
|
||||
}
|
||||
|
||||
func (s *LogService) LogVerification(c *gin.Context, applicationID *uint, appUserID *uint, action, details string, deviceID string, err error) error {
|
||||
params := LogParams{
|
||||
LogType: "verification",
|
||||
ApplicationID: applicationID,
|
||||
AppUserID: appUserID,
|
||||
Action: action,
|
||||
Resource: "verification",
|
||||
Details: details,
|
||||
DeviceID: deviceID,
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
params.Status = "failed"
|
||||
params.Level = "warning"
|
||||
params.ErrorMessage = err.Error()
|
||||
}
|
||||
|
||||
return s.CreateLogFromContext(c, params)
|
||||
}
|
||||
|
||||
func (s *LogService) LogException(c *gin.Context, action, errorMessage, stackTrace string) error {
|
||||
params := LogParams{
|
||||
LogType: "exception",
|
||||
Action: action,
|
||||
Resource: "system",
|
||||
Status: "failed",
|
||||
Level: "error",
|
||||
ErrorMessage: errorMessage,
|
||||
StackTrace: stackTrace,
|
||||
}
|
||||
|
||||
return s.CreateLogFromContext(c, params)
|
||||
}
|
||||
|
||||
var logService = NewLogService()
|
||||
|
||||
func CreateLog(params LogParams) error {
|
||||
return logService.CreateLog(params)
|
||||
}
|
||||
|
||||
func CreateLogFromContext(c *gin.Context, params LogParams) error {
|
||||
return logService.CreateLogFromContext(c, params)
|
||||
}
|
||||
|
||||
func LogOperation(c *gin.Context, action, resource string, resourceID *uint, details string, err error) error {
|
||||
return logService.LogOperation(c, action, resource, resourceID, details, err)
|
||||
}
|
||||
|
||||
func LogVerification(c *gin.Context, applicationID *uint, appUserID *uint, action, details string, DeviceFingerprint string, err error) error {
|
||||
return logService.LogVerification(c, applicationID, appUserID, action, details, DeviceFingerprint, err)
|
||||
}
|
||||
|
||||
func LogException(c *gin.Context, action, errorMessage, stackTrace string) error {
|
||||
return logService.LogException(c, action, errorMessage, stackTrace)
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
)
|
||||
|
||||
type PackageFeature string
|
||||
|
||||
const (
|
||||
FeatureCloudData PackageFeature = "cloud_data"
|
||||
FeatureDynamicCode PackageFeature = "dynamic_code"
|
||||
FeatureEmail PackageFeature = "email"
|
||||
FeatureExtension PackageFeature = "extension"
|
||||
FeatureAgent PackageFeature = "agent"
|
||||
)
|
||||
|
||||
func GetUserPackagePermission(userID uint) (*model.PackagePermission, error) {
|
||||
var user model.User
|
||||
if err := database.DB.First(&user, userID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user.CurrentPackageID == nil {
|
||||
return nil, fmt.Errorf("用户没有当前套餐")
|
||||
}
|
||||
|
||||
var userPackage model.UserPackage
|
||||
if err := database.DB.Where("user_id = ? AND package_id = ? AND status = ?", userID, *user.CurrentPackageID, "active").First(&userPackage).Error; err != nil {
|
||||
return nil, fmt.Errorf("用户套餐已过期或无效")
|
||||
}
|
||||
|
||||
var permission model.PackagePermission
|
||||
if err := database.DB.Where("package_id = ?", *user.CurrentPackageID).First(&permission).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &permission, nil
|
||||
}
|
||||
|
||||
func CheckPackageFeature(userID uint, feature PackageFeature) (bool, error) {
|
||||
permission, err := GetUserPackagePermission(userID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
switch feature {
|
||||
case FeatureCloudData:
|
||||
return permission.AllowCloudData, nil
|
||||
case FeatureDynamicCode:
|
||||
return permission.AllowDynamicCode, nil
|
||||
case FeatureEmail:
|
||||
return permission.AllowEmail, nil
|
||||
case FeatureExtension:
|
||||
return permission.AllowExtension, nil
|
||||
case FeatureAgent:
|
||||
return permission.AllowAgent, nil
|
||||
default:
|
||||
return false, fmt.Errorf("未知的功能: %s", feature)
|
||||
}
|
||||
}
|
||||
|
||||
func GetApplicationDisabledStatus(appID uint) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type ApplicationWithStatus struct {
|
||||
ID uint `json:"id"`
|
||||
UserID uint `json:"user_id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
AppKey string `json:"app_key"`
|
||||
IconURL string `json:"icon_url"`
|
||||
Status string `json:"status"`
|
||||
BillingType string `json:"billing_type"`
|
||||
LoginPolicy string `json:"login_policy"`
|
||||
MaxDevices int `json:"max_devices"`
|
||||
MultiOpen bool `json:"multi_open"`
|
||||
MultiOpenMode string `json:"multi_open_mode"`
|
||||
EnableTrial bool `json:"enable_trial"`
|
||||
TrialDays int `json:"trial_days"`
|
||||
EnableFreePeriod bool `json:"enable_free_period"`
|
||||
FreePeriodType string `json:"free_period_type"`
|
||||
FreePeriodStart string `json:"free_period_start"`
|
||||
FreePeriodEnd string `json:"free_period_end"`
|
||||
FreePeriodWeekdays string `json:"free_period_weekdays"`
|
||||
FreePeriodStartTime string `json:"free_period_start_time"`
|
||||
FreePeriodEndTime string `json:"free_period_end_time"`
|
||||
HeartbeatInterval int `json:"heartbeat_interval"`
|
||||
HeartbeatTimeout int `json:"heartbeat_timeout"`
|
||||
MaxAttempts int `json:"max_attempts"`
|
||||
LockDuration int `json:"lock_duration"`
|
||||
DeductionMode string `json:"deduction_mode"`
|
||||
DeductionType string `json:"deduction_type"`
|
||||
DeductionInterval int `json:"deduction_interval"`
|
||||
DeductionUnit string `json:"deduction_unit"`
|
||||
DeductionAmount float64 `json:"deduction_amount"`
|
||||
AllowRegister bool `json:"allow_register"`
|
||||
RegisterMethods string `json:"register_methods"`
|
||||
EnableEmailVerify bool `json:"enable_email_verify"`
|
||||
RequireEmailVerify bool `json:"require_email_verify"`
|
||||
EnablePasswordReset bool `json:"enable_password_reset"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
DisabledByPackage bool `json:"disabled_by_package"`
|
||||
}
|
||||
|
||||
func GetApplicationsWithDisabledStatus(userID uint) ([]ApplicationWithStatus, error) {
|
||||
var user model.User
|
||||
if err := database.DB.First(&user, userID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var apps []model.Application
|
||||
if err := database.DB.Where("user_id = ?", userID).Order("created_at ASC").Find(&apps).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]ApplicationWithStatus, len(apps))
|
||||
for i, app := range apps {
|
||||
result[i] = ApplicationWithStatus{
|
||||
ID: app.ID,
|
||||
UserID: app.UserID,
|
||||
Name: app.Name,
|
||||
Description: app.Description,
|
||||
AppKey: app.AppKey,
|
||||
IconURL: app.IconURL,
|
||||
Status: app.Status,
|
||||
BillingType: app.BillingType,
|
||||
LoginPolicy: app.LoginPolicy,
|
||||
MaxDevices: app.MaxDevices,
|
||||
MultiOpen: app.MultiOpen,
|
||||
MultiOpenMode: app.MultiOpenMode,
|
||||
EnableTrial: app.EnableTrial,
|
||||
TrialDays: app.TrialDays,
|
||||
EnableFreePeriod: app.EnableFreePeriod,
|
||||
FreePeriodType: app.FreePeriodType,
|
||||
FreePeriodStart: app.FreePeriodStart,
|
||||
FreePeriodEnd: app.FreePeriodEnd,
|
||||
FreePeriodWeekdays: app.FreePeriodWeekdays,
|
||||
FreePeriodStartTime: app.FreePeriodStartTime,
|
||||
FreePeriodEndTime: app.FreePeriodEndTime,
|
||||
HeartbeatInterval: app.HeartbeatInterval,
|
||||
HeartbeatTimeout: app.HeartbeatTimeout,
|
||||
MaxAttempts: app.MaxAttempts,
|
||||
LockDuration: app.LockDuration,
|
||||
DeductionMode: app.DeductionMode,
|
||||
DeductionType: app.DeductionType,
|
||||
DeductionInterval: app.DeductionInterval,
|
||||
DeductionUnit: app.DeductionUnit,
|
||||
DeductionAmount: app.DeductionAmount,
|
||||
AllowRegister: app.AllowRegister,
|
||||
RegisterMethods: app.RegisterMethods,
|
||||
EnableEmailVerify: app.EnableEmailVerify,
|
||||
RequireEmailVerify: app.RequireEmailVerify,
|
||||
EnablePasswordReset: app.EnablePasswordReset,
|
||||
CreatedAt: app.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
UpdatedAt: app.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
}
|
||||
}
|
||||
|
||||
if user.CurrentPackageID == nil {
|
||||
for i := range result {
|
||||
result[i].DisabledByPackage = true
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
var userPackage model.UserPackage
|
||||
if err := database.DB.Where("user_id = ? AND package_id = ? AND status = ?", userID, *user.CurrentPackageID, "active").First(&userPackage).Error; err != nil {
|
||||
for i := range result {
|
||||
result[i].DisabledByPackage = true
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
var permission model.PackagePermission
|
||||
if err := database.DB.Where("package_id = ?", *user.CurrentPackageID).First(&permission).Error; err != nil {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
if permission.MaxApplications <= 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
for i := range result {
|
||||
result[i].DisabledByPackage = i >= permission.MaxApplications
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type BEpusdtConfig struct {
|
||||
ApiURL string
|
||||
ApiToken string
|
||||
TradeType string
|
||||
Fiat string
|
||||
Timeout int
|
||||
Rate string
|
||||
}
|
||||
|
||||
type BEpusdtClient struct {
|
||||
config BEpusdtConfig
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
type CreateTransactionRequest struct {
|
||||
OrderID string `json:"order_id"`
|
||||
Amount float64 `json:"amount"`
|
||||
NotifyURL string `json:"notify_url"`
|
||||
RedirectURL string `json:"redirect_url"`
|
||||
Signature string `json:"signature"`
|
||||
TradeType string `json:"trade_type,omitempty"`
|
||||
Fiat string `json:"fiat,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Timeout int `json:"timeout,omitempty"`
|
||||
Rate string `json:"rate,omitempty"`
|
||||
Address string `json:"address,omitempty"`
|
||||
}
|
||||
|
||||
type CreateTransactionResponse struct {
|
||||
StatusCode int `json:"status_code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
Fiat string `json:"fiat"`
|
||||
TradeID string `json:"trade_id"`
|
||||
OrderID string `json:"order_id"`
|
||||
Amount interface{} `json:"amount"`
|
||||
ActualAmount interface{} `json:"actual_amount"`
|
||||
Status int `json:"status"`
|
||||
Token string `json:"token"`
|
||||
ExpirationTime int `json:"expiration_time"`
|
||||
PaymentURL string `json:"payment_url"`
|
||||
} `json:"data"`
|
||||
RequestID string `json:"request_id"`
|
||||
}
|
||||
|
||||
type CallbackData struct {
|
||||
TradeID string `json:"trade_id"`
|
||||
OrderID string `json:"order_id"`
|
||||
Amount float64 `json:"amount"`
|
||||
ActualAmount float64 `json:"actual_amount"`
|
||||
Token string `json:"token"`
|
||||
BlockTransactionID string `json:"block_transaction_id"`
|
||||
Signature string `json:"signature"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
func NewBEpusdtClient(config BEpusdtConfig) *BEpusdtClient {
|
||||
if config.Fiat == "" {
|
||||
config.Fiat = "CNY"
|
||||
}
|
||||
if config.Timeout == 0 {
|
||||
config.Timeout = 600
|
||||
}
|
||||
|
||||
return &BEpusdtClient{
|
||||
config: config,
|
||||
client: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *BEpusdtClient) GenerateSignature(params map[string]string) string {
|
||||
keys := make([]string, 0, len(params))
|
||||
for k := range params {
|
||||
if params[k] != "" && k != "signature" && k != "sign_type" {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
var parts []string
|
||||
for _, k := range keys {
|
||||
parts = append(parts, fmt.Sprintf("%s=%s", k, params[k]))
|
||||
}
|
||||
signStr := strings.Join(parts, "&") + c.config.ApiToken
|
||||
|
||||
hash := md5.New()
|
||||
hash.Write([]byte(signStr))
|
||||
return hex.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
func (c *BEpusdtClient) CreateTransaction(orderID string, amount float64, notifyURL, redirectURL, name string) (*CreateTransactionResponse, error) {
|
||||
params := map[string]string{
|
||||
"order_id": orderID,
|
||||
"amount": fmt.Sprintf("%.2f", amount),
|
||||
"notify_url": notifyURL,
|
||||
"redirect_url": redirectURL,
|
||||
}
|
||||
|
||||
if c.config.TradeType != "" {
|
||||
params["trade_type"] = c.config.TradeType
|
||||
}
|
||||
if c.config.Fiat != "" {
|
||||
params["fiat"] = c.config.Fiat
|
||||
}
|
||||
if name != "" {
|
||||
params["name"] = name
|
||||
}
|
||||
if c.config.Timeout > 0 {
|
||||
params["timeout"] = fmt.Sprintf("%d", c.config.Timeout)
|
||||
}
|
||||
if c.config.Rate != "" {
|
||||
params["rate"] = c.config.Rate
|
||||
}
|
||||
|
||||
params["signature"] = c.GenerateSignature(params)
|
||||
|
||||
jsonData, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
apiURL := strings.TrimRight(c.config.ApiURL, "/")
|
||||
reqURL := apiURL + "/api/v1/order/create-transaction"
|
||||
|
||||
req, err := http.NewRequest("POST", reqURL, strings.NewReader(string(jsonData)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to send request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
var result CreateTransactionResponse
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
if result.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("API error: %s (code: %d)", result.Message, result.StatusCode)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (c *BEpusdtClient) VerifyCallback(data CallbackData) bool {
|
||||
params := map[string]string{
|
||||
"trade_id": data.TradeID,
|
||||
"order_id": data.OrderID,
|
||||
"amount": fmt.Sprintf("%.2f", data.Amount),
|
||||
"actual_amount": fmt.Sprintf("%.2f", data.ActualAmount),
|
||||
"token": data.Token,
|
||||
"block_transaction_id": data.BlockTransactionID,
|
||||
"status": fmt.Sprintf("%d", data.Status),
|
||||
}
|
||||
|
||||
expectedSign := c.GenerateSignature(params)
|
||||
return strings.EqualFold(expectedSign, data.Signature)
|
||||
}
|
||||
|
||||
func ParseCallbackFromQuery(query url.Values) CallbackData {
|
||||
var data CallbackData
|
||||
data.TradeID = query.Get("trade_id")
|
||||
data.OrderID = query.Get("order_id")
|
||||
data.Token = query.Get("token")
|
||||
data.BlockTransactionID = query.Get("block_transaction_id")
|
||||
data.Signature = query.Get("signature")
|
||||
|
||||
if amount := query.Get("amount"); amount != "" {
|
||||
fmt.Sscanf(amount, "%f", &data.Amount)
|
||||
}
|
||||
if actualAmount := query.Get("actual_amount"); actualAmount != "" {
|
||||
fmt.Sscanf(actualAmount, "%f", &data.ActualAmount)
|
||||
}
|
||||
if status := query.Get("status"); status != "" {
|
||||
fmt.Sscanf(status, "%d", &data.Status)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func ParseCallbackFromJSON(body []byte) (CallbackData, error) {
|
||||
var data CallbackData
|
||||
err := json.Unmarshal(body, &data)
|
||||
return data, err
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"verification-platform-backend/internal/model"
|
||||
)
|
||||
|
||||
type PaymentResult struct {
|
||||
OrderID string
|
||||
TradeID string
|
||||
PaymentURL string
|
||||
ActualAmount string
|
||||
Token string
|
||||
ExpirationTime int
|
||||
}
|
||||
|
||||
type PaymentService interface {
|
||||
CreateOrder(orderID string, amount float64, notifyURL, redirectURL, name string) (*PaymentResult, error)
|
||||
VerifyCallback(data interface{}) bool
|
||||
}
|
||||
|
||||
func GetPaymentService(channel model.PaymentChannel, callbackBaseURL string) (PaymentService, error) {
|
||||
switch channel.Type {
|
||||
case "bepusdt":
|
||||
config, err := parseBEpusdtConfig(channel.Config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse bepusdt config: %w", err)
|
||||
}
|
||||
return NewBEpusdtAdapter(config, callbackBaseURL), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported payment type: %s", channel.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func parseBEpusdtConfig(configStr string) (BEpusdtConfig, error) {
|
||||
var config BEpusdtConfig
|
||||
if err := json.Unmarshal([]byte(configStr), &config); err != nil {
|
||||
return config, fmt.Errorf("invalid config format: %w", err)
|
||||
}
|
||||
|
||||
if config.ApiURL == "" {
|
||||
return config, fmt.Errorf("api_url is required")
|
||||
}
|
||||
if config.ApiToken == "" {
|
||||
return config, fmt.Errorf("api_token is required")
|
||||
}
|
||||
if config.TradeType == "" {
|
||||
config.TradeType = "usdt.trc20"
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func interfaceToString(v interface{}) string {
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
return val
|
||||
case float64:
|
||||
return strconv.FormatFloat(val, 'f', -1, 64)
|
||||
case int:
|
||||
return strconv.Itoa(val)
|
||||
default:
|
||||
return fmt.Sprintf("%v", val)
|
||||
}
|
||||
}
|
||||
|
||||
type BEpusdtAdapter struct {
|
||||
client *BEpusdtClient
|
||||
callbackBaseURL string
|
||||
}
|
||||
|
||||
func NewBEpusdtAdapter(config BEpusdtConfig, callbackBaseURL string) *BEpusdtAdapter {
|
||||
return &BEpusdtAdapter{
|
||||
client: NewBEpusdtClient(config),
|
||||
callbackBaseURL: callbackBaseURL,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *BEpusdtAdapter) CreateOrder(orderID string, amount float64, notifyURL, redirectURL, name string) (*PaymentResult, error) {
|
||||
resp, err := a.client.CreateTransaction(orderID, amount, notifyURL, redirectURL, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &PaymentResult{
|
||||
OrderID: resp.Data.OrderID,
|
||||
TradeID: resp.Data.TradeID,
|
||||
PaymentURL: resp.Data.PaymentURL,
|
||||
ActualAmount: interfaceToString(resp.Data.ActualAmount),
|
||||
Token: resp.Data.Token,
|
||||
ExpirationTime: resp.Data.ExpirationTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *BEpusdtAdapter) VerifyCallback(data interface{}) bool {
|
||||
switch v := data.(type) {
|
||||
case CallbackData:
|
||||
return a.client.VerifyCallback(v)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type SettingService struct{}
|
||||
|
||||
func NewSettingService() *SettingService {
|
||||
return &SettingService{}
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSettings() (map[string]interface{}, error) {
|
||||
var settings []model.Setting
|
||||
if err := database.DB.Find(&settings).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[string]interface{})
|
||||
for _, setting := range settings {
|
||||
if _, ok := result[setting.Category]; !ok {
|
||||
result[setting.Category] = make(map[string]interface{})
|
||||
}
|
||||
result[setting.Category].(map[string]interface{})[setting.Key] = setting.Value
|
||||
}
|
||||
|
||||
fmt.Println("========================================")
|
||||
fmt.Println("GetSettings 返回的数据:")
|
||||
for cat, data := range result {
|
||||
fmt.Printf(" 分类: %s\n", cat)
|
||||
if dataMap, ok := data.(map[string]interface{}); ok {
|
||||
for k, v := range dataMap {
|
||||
fmt.Printf(" %s: %v\n", k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Println("========================================")
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSettingsByCategory(category string) (map[string]interface{}, error) {
|
||||
var settings []model.Setting
|
||||
if err := database.DB.Where("category = ?", category).Find(&settings).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[string]interface{})
|
||||
for _, setting := range settings {
|
||||
result[setting.Key] = setting.Value
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *SettingService) UpdateSettings(category string, data map[string]interface{}) error {
|
||||
fmt.Println("========================================")
|
||||
fmt.Printf("UpdateSettings 被调用: category=%s\n", category)
|
||||
for key, value := range data {
|
||||
var strValue string
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
strValue = v
|
||||
case bool:
|
||||
if v {
|
||||
strValue = "true"
|
||||
} else {
|
||||
strValue = "false"
|
||||
}
|
||||
case int:
|
||||
strValue = fmt.Sprintf("%d", v)
|
||||
case int64:
|
||||
strValue = fmt.Sprintf("%d", v)
|
||||
case float64:
|
||||
strValue = fmt.Sprintf("%.0f", v)
|
||||
case float32:
|
||||
strValue = fmt.Sprintf("%.0f", v)
|
||||
default:
|
||||
fmt.Printf(" 跳过: %s (类型: %T, 值: %v)\n", key, value, value)
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf(" 保存: %s = %s\n", key, strValue)
|
||||
|
||||
setting := model.Setting{
|
||||
Category: category,
|
||||
Key: key,
|
||||
Value: strValue,
|
||||
}
|
||||
|
||||
if err := database.DB.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "key"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"value", "category", "updated_at"}),
|
||||
}).Create(&setting).Error; err != nil {
|
||||
fmt.Printf(" 保存失败: %v\n", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
fmt.Println("========================================")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSetting(category, key string) (string, error) {
|
||||
var setting model.Setting
|
||||
if err := database.DB.Where("category = ? AND key = ?", category, key).First(&setting).Error; err != nil {
|
||||
return "", err
|
||||
}
|
||||
return setting.Value, nil
|
||||
}
|
||||
|
||||
func (s *SettingService) SetSetting(category, key, value string) error {
|
||||
setting := model.Setting{
|
||||
Category: category,
|
||||
Key: key,
|
||||
Value: value,
|
||||
}
|
||||
|
||||
return database.DB.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "key"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"value", "category", "updated_at"}),
|
||||
}).Create(&setting).Error
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SmsService struct{}
|
||||
|
||||
func NewSmsService() *SmsService {
|
||||
return &SmsService{}
|
||||
}
|
||||
|
||||
type SmsConfig struct {
|
||||
Provider string
|
||||
AccessKey string
|
||||
SecretKey string
|
||||
SignName string
|
||||
TemplateCode string
|
||||
}
|
||||
|
||||
func (s *SmsService) SendSms(config SmsConfig, phone, code string) error {
|
||||
switch config.Provider {
|
||||
case "aliyun":
|
||||
return s.sendAliyunSms(config, phone, code)
|
||||
case "tencent":
|
||||
return s.sendTencentSms(config, phone, code)
|
||||
case "huawei":
|
||||
return s.sendHuaweiSms(config, phone, code)
|
||||
default:
|
||||
return fmt.Errorf("不支持的短信服务商: %s", config.Provider)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SmsService) sendAliyunSms(config SmsConfig, phone, code string) error {
|
||||
params := url.Values{}
|
||||
params.Set("AccessKeyId", config.AccessKey)
|
||||
params.Set("Action", "SendSms")
|
||||
params.Set("Format", "JSON")
|
||||
params.Set("PhoneNumbers", phone)
|
||||
params.Set("RegionId", "cn-hangzhou")
|
||||
params.Set("SignName", config.SignName)
|
||||
params.Set("SignatureMethod", "HMAC-SHA1")
|
||||
params.Set("SignatureNonce", fmt.Sprintf("%d", getTimeStamp()))
|
||||
params.Set("SignatureVersion", "1.0")
|
||||
params.Set("TemplateCode", config.TemplateCode)
|
||||
params.Set("TemplateParam", fmt.Sprintf(`{"code":"%s"}`, code))
|
||||
params.Set("Timestamp", getTimeStampStr())
|
||||
params.Set("Version", "2017-05-25")
|
||||
|
||||
signature := s.generateAliyunSignature(params, config.SecretKey)
|
||||
params.Set("Signature", signature)
|
||||
|
||||
resp, err := http.Get(fmt.Sprintf("https://dysmsapi.aliyuncs.com/?%s", params.Encode()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("请求阿里云短信API失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取响应失败: %v", err)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Code string `json:"Code"`
|
||||
Message string `json:"Message"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return fmt.Errorf("解析响应失败: %v", err)
|
||||
}
|
||||
|
||||
if result.Code != "OK" {
|
||||
return fmt.Errorf("发送失败: %s", result.Message)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SmsService) generateAliyunSignature(params url.Values, secretKey string) string {
|
||||
keys := make([]string, 0, len(params))
|
||||
for k := range params {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
encodedParams := make([]string, 0, len(params))
|
||||
for _, k := range keys {
|
||||
encodedParams = append(encodedParams, fmt.Sprintf("%s=%s", specialUrlEncode(k), specialUrlEncode(params.Get(k))))
|
||||
}
|
||||
sortParams := strings.Join(encodedParams, "&")
|
||||
|
||||
stringToSign := fmt.Sprintf("GET&%s&%s", specialUrlEncode("/"), specialUrlEncode(sortParams))
|
||||
|
||||
signature := hmacSHA1(secretKey+"&", stringToSign)
|
||||
return signature
|
||||
}
|
||||
|
||||
func (s *SmsService) sendTencentSms(config SmsConfig, phone, code string) error {
|
||||
return fmt.Errorf("腾讯云短信服务暂未实现")
|
||||
}
|
||||
|
||||
func (s *SmsService) sendHuaweiSms(config SmsConfig, phone, code string) error {
|
||||
return fmt.Errorf("华为云短信服务暂未实现")
|
||||
}
|
||||
|
||||
func (s *SmsService) SendTestSms(config SmsConfig, phone string) error {
|
||||
code := "123456"
|
||||
return s.SendSms(config, phone, code)
|
||||
}
|
||||
|
||||
func specialUrlEncode(s string) string {
|
||||
encoded := url.QueryEscape(s)
|
||||
encoded = strings.ReplaceAll(encoded, "+", "%20")
|
||||
encoded = strings.ReplaceAll(encoded, "*", "%2A")
|
||||
encoded = strings.ReplaceAll(encoded, "%7E", "~")
|
||||
return encoded
|
||||
}
|
||||
|
||||
func hmacSHA1(key, data string) string {
|
||||
return data
|
||||
}
|
||||
|
||||
func getTimeStamp() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func getTimeStampStr() string {
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
)
|
||||
|
||||
type UsageAlertService struct{}
|
||||
|
||||
func NewUsageAlertService() *UsageAlertService {
|
||||
return &UsageAlertService{}
|
||||
}
|
||||
|
||||
type AlertType string
|
||||
|
||||
const (
|
||||
AlertTypeAPICalls AlertType = "api_calls"
|
||||
AlertTypeStorage AlertType = "storage"
|
||||
AlertTypePackageExp AlertType = "package_expiry"
|
||||
)
|
||||
|
||||
type AlertLevel string
|
||||
|
||||
const (
|
||||
AlertLevelWarning AlertLevel = "warning"
|
||||
AlertLevelCritical AlertLevel = "critical"
|
||||
)
|
||||
|
||||
type UsageAlert struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Type AlertType `json:"type"`
|
||||
Level AlertLevel `json:"level"`
|
||||
Message string `json:"message"`
|
||||
Usage float64 `json:"usage"`
|
||||
Limit float64 `json:"limit"`
|
||||
Percent float64 `json:"percent"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func (s *UsageAlertService) CheckAndCreateAlerts(userID uint) ([]UsageAlert, error) {
|
||||
var alerts []UsageAlert
|
||||
|
||||
var user model.User
|
||||
if err := database.DB.Preload("CurrentPackage").First(&user, userID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user.CurrentPackageID == nil {
|
||||
return alerts, nil
|
||||
}
|
||||
|
||||
var permission model.PackagePermission
|
||||
if err := database.DB.Where("package_id = ?", user.CurrentPackageID).First(&permission).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if alert := s.checkAPICallsUsage(&user, &permission); alert != nil {
|
||||
alerts = append(alerts, *alert)
|
||||
}
|
||||
|
||||
if alert := s.checkStorageUsage(&user, &permission); alert != nil {
|
||||
alerts = append(alerts, *alert)
|
||||
}
|
||||
|
||||
if alert := s.checkPackageExpiry(&user); alert != nil {
|
||||
alerts = append(alerts, *alert)
|
||||
}
|
||||
|
||||
for _, alert := range alerts {
|
||||
s.sendNotification(&alert)
|
||||
}
|
||||
|
||||
return alerts, nil
|
||||
}
|
||||
|
||||
func (s *UsageAlertService) checkAPICallsUsage(user *model.User, permission *model.PackagePermission) *UsageAlert {
|
||||
if permission.MaxApiCalls == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
usagePercent := float64(user.ApiCallsUsed) / float64(permission.MaxApiCalls) * 100
|
||||
|
||||
var lastAlert model.UsageAlertRecord
|
||||
hasRecentAlert := database.DB.Where(
|
||||
"user_id = ? AND type = ? AND created_at > ?",
|
||||
user.ID,
|
||||
AlertTypeAPICalls,
|
||||
time.Now().Add(-24*time.Hour),
|
||||
).First(&lastAlert).Error == nil
|
||||
|
||||
if hasRecentAlert {
|
||||
return nil
|
||||
}
|
||||
|
||||
if usagePercent >= 90 {
|
||||
return &UsageAlert{
|
||||
UserID: user.ID,
|
||||
Type: AlertTypeAPICalls,
|
||||
Level: AlertLevelCritical,
|
||||
Message: fmt.Sprintf("API调用次数已使用 %.1f%%,即将达到上限", usagePercent),
|
||||
Usage: float64(user.ApiCallsUsed),
|
||||
Limit: float64(permission.MaxApiCalls),
|
||||
Percent: usagePercent,
|
||||
}
|
||||
}
|
||||
|
||||
if usagePercent >= 80 {
|
||||
return &UsageAlert{
|
||||
UserID: user.ID,
|
||||
Type: AlertTypeAPICalls,
|
||||
Level: AlertLevelWarning,
|
||||
Message: fmt.Sprintf("API调用次数已使用 %.1f%%,请注意用量", usagePercent),
|
||||
Usage: float64(user.ApiCallsUsed),
|
||||
Limit: float64(permission.MaxApiCalls),
|
||||
Percent: usagePercent,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UsageAlertService) checkStorageUsage(user *model.User, permission *model.PackagePermission) *UsageAlert {
|
||||
if permission.MaxStorage == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
usagePercent := float64(user.StorageUsed) / float64(int64(permission.MaxStorage)*1024*1024) * 100
|
||||
|
||||
var lastAlert model.UsageAlertRecord
|
||||
hasRecentAlert := database.DB.Where(
|
||||
"user_id = ? AND type = ? AND created_at > ?",
|
||||
user.ID,
|
||||
AlertTypeStorage,
|
||||
time.Now().Add(-24*time.Hour),
|
||||
).First(&lastAlert).Error == nil
|
||||
|
||||
if hasRecentAlert {
|
||||
return nil
|
||||
}
|
||||
|
||||
if usagePercent >= 90 {
|
||||
return &UsageAlert{
|
||||
UserID: user.ID,
|
||||
Type: AlertTypeStorage,
|
||||
Level: AlertLevelCritical,
|
||||
Message: fmt.Sprintf("存储空间已使用 %.1f%%,即将达到上限", usagePercent),
|
||||
Usage: float64(user.StorageUsed) / 1024 / 1024,
|
||||
Limit: float64(permission.MaxStorage),
|
||||
Percent: usagePercent,
|
||||
}
|
||||
}
|
||||
|
||||
if usagePercent >= 80 {
|
||||
return &UsageAlert{
|
||||
UserID: user.ID,
|
||||
Type: AlertTypeStorage,
|
||||
Level: AlertLevelWarning,
|
||||
Message: fmt.Sprintf("存储空间已使用 %.1f%%,请注意用量", usagePercent),
|
||||
Usage: float64(user.StorageUsed) / 1024 / 1024,
|
||||
Limit: float64(permission.MaxStorage),
|
||||
Percent: usagePercent,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UsageAlertService) checkPackageExpiry(user *model.User) *UsageAlert {
|
||||
var userPackage model.UserPackage
|
||||
if err := database.DB.Where(
|
||||
"user_id = ? AND package_id = ? AND status = ?",
|
||||
user.ID,
|
||||
user.CurrentPackageID,
|
||||
"active",
|
||||
).First(&userPackage).Error; err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if userPackage.ExpiredAt == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
daysUntilExpiry := time.Until(*userPackage.ExpiredAt).Hours() / 24
|
||||
|
||||
var lastAlert model.UsageAlertRecord
|
||||
hasRecentAlert := database.DB.Where(
|
||||
"user_id = ? AND type = ? AND created_at > ?",
|
||||
user.ID,
|
||||
AlertTypePackageExp,
|
||||
time.Now().Add(-24*time.Hour),
|
||||
).First(&lastAlert).Error == nil
|
||||
|
||||
if hasRecentAlert {
|
||||
return nil
|
||||
}
|
||||
|
||||
if daysUntilExpiry <= 3 {
|
||||
return &UsageAlert{
|
||||
UserID: user.ID,
|
||||
Type: AlertTypePackageExp,
|
||||
Level: AlertLevelCritical,
|
||||
Message: fmt.Sprintf("套餐将在 %.0f 天后过期,请及时续费", daysUntilExpiry),
|
||||
Usage: daysUntilExpiry,
|
||||
Limit: 0,
|
||||
Percent: 0,
|
||||
}
|
||||
}
|
||||
|
||||
if daysUntilExpiry <= 7 {
|
||||
return &UsageAlert{
|
||||
UserID: user.ID,
|
||||
Type: AlertTypePackageExp,
|
||||
Level: AlertLevelWarning,
|
||||
Message: fmt.Sprintf("套餐将在 %.0f 天后过期,请及时续费", daysUntilExpiry),
|
||||
Usage: daysUntilExpiry,
|
||||
Limit: 0,
|
||||
Percent: 0,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UsageAlertService) sendNotification(alert *UsageAlert) {
|
||||
alertRecord := model.UsageAlertRecord{
|
||||
UserID: alert.UserID,
|
||||
Type: string(alert.Type),
|
||||
Level: string(alert.Level),
|
||||
Message: alert.Message,
|
||||
Usage: alert.Usage,
|
||||
Limit: alert.Limit,
|
||||
Percent: alert.Percent,
|
||||
Status: "sent",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
database.DB.Create(&alertRecord)
|
||||
|
||||
notification := model.Notification{
|
||||
UserID: alert.UserID,
|
||||
Title: "用量告警",
|
||||
Content: alert.Message,
|
||||
Type: "usage_alert",
|
||||
IsRead: false,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
database.DB.Create(¬ification)
|
||||
}
|
||||
|
||||
func (s *UsageAlertService) CheckAllUsers() error {
|
||||
var users []model.User
|
||||
if err := database.DB.Where("current_package_id IS NOT NULL").Find(&users).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
_, err := s.CheckAndCreateAlerts(user.ID)
|
||||
if err != nil {
|
||||
fmt.Printf("检查用户 %d 用量告警失败: %v\n", user.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UsageAlertService) GetUserAlerts(userID uint, limit int) ([]model.UsageAlertRecord, error) {
|
||||
var alerts []model.UsageAlertRecord
|
||||
err := database.DB.Where("user_id = ?", userID).
|
||||
Order("created_at DESC").
|
||||
Limit(limit).
|
||||
Find(&alerts).Error
|
||||
return alerts, err
|
||||
}
|
||||
|
||||
func (s *UsageAlertService) MarkAlertAsRead(alertID uint) error {
|
||||
return database.DB.Model(&model.UsageAlertRecord{}).
|
||||
Where("id = ?", alertID).
|
||||
Update("status", "read").Error
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
)
|
||||
|
||||
type EventType string
|
||||
|
||||
const (
|
||||
EventUserRegistered EventType = "user.registered"
|
||||
EventUserLogin EventType = "user.login"
|
||||
EventUserRecharged EventType = "user.recharged"
|
||||
EventUserExpired EventType = "user.expired"
|
||||
EventCardUsed EventType = "card.used"
|
||||
EventCardExpired EventType = "card.expired"
|
||||
EventAbnormalDetected EventType = "abnormal.detected"
|
||||
)
|
||||
|
||||
type WebhookPayload struct {
|
||||
Event EventType `json:"event"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
}
|
||||
|
||||
func TriggerEvent(appID uint, event EventType, data map[string]interface{}) {
|
||||
var webhooks []model.WebhookConfig
|
||||
if err := database.DB.Where("application_id = ? AND status = ?", appID, "active").Find(&webhooks).Error; err != nil {
|
||||
log.Printf("Failed to fetch webhooks: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, webhook := range webhooks {
|
||||
var events []string
|
||||
if err := json.Unmarshal([]byte(webhook.Events), &events); err != nil {
|
||||
log.Printf("Failed to parse webhook events: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if !containsEvent(events, string(event)) {
|
||||
continue
|
||||
}
|
||||
|
||||
payload := WebhookPayload{
|
||||
Event: event,
|
||||
Timestamp: time.Now().Unix(),
|
||||
Data: data,
|
||||
}
|
||||
|
||||
go sendWebhookNotification(&webhook, payload)
|
||||
}
|
||||
}
|
||||
|
||||
func containsEvent(events []string, event string) bool {
|
||||
for _, e := range events {
|
||||
if e == event || e == "*" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func sendWebhookNotification(webhook *model.WebhookConfig, payload WebhookPayload) {
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
log.Printf("Failed to marshal webhook payload: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Sending webhook to %s: %s", webhook.URL, string(payloadBytes))
|
||||
fmt.Printf("[Webhook] Sending to %s: Event=%s, AppID=%d\n", webhook.URL, payload.Event, webhook.ApplicationID)
|
||||
}
|
||||
Reference in New Issue
Block a user