Initial commit: 网络验证平台

This commit is contained in:
Admin
2026-04-27 17:22:56 +08:00
commit afe67d704e
780 changed files with 88960 additions and 0 deletions
+76
View File
@@ -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
+47
View File
@@ -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()
}
}
+156
View File
@@ -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)
}
+234
View File
@@ -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()
}
}
+156
View File
@@ -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
}
+928
View File
@@ -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"`
}
+185
View File
@@ -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,
})
}
+15
View File
@@ -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))
}
+905
View File
@@ -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": "密码重置成功",
})
}
+918
View File
@@ -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)
}
+390
View File
@@ -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": "密码修改成功",
})
}
+427
View File
@@ -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 ""
}
}
+146
View File
@@ -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)
}
+217
View File
@@ -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,
})
}
+585
View File
@@ -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
+969
View File
@@ -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)})
}
+827
View File
@@ -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": "批量更新状态成功"})
}
+481
View File
@@ -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)})
}
+84
View File
@@ -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),
})
}
+282
View File
@@ -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
}
+296
View File
@@ -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(&notifications).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(&notification).Error; err != nil {
response.Error(c, 404, "通知不存在")
return
}
notification.IsRead = true
if err := database.DB.Save(&notification).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,
})
}
+673
View File
@@ -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(&notification).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(&notification).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")
}
+71
View File
@@ -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)
}
}
}
+342
View File
@@ -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
}
+154
View File
@@ -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
}
+151
View File
@@ -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)
}
+192
View File
@@ -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
}
+211
View File
@@ -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
}
+105
View File
@@ -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
}
}
+127
View File
@@ -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
}
+133
View File
@@ -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 ""
}
+280
View File
@@ -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(&notification)
}
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
}
+77
View File
@@ -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)
}