Initial commit: 网络验证平台
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/service"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupAccountRoutes(r *gin.RouterGroup) {
|
||||
r.GET("/account", handleAppGetAccount)
|
||||
r.POST("/heartbeat", handleAppHeartbeat)
|
||||
}
|
||||
|
||||
func handleAppGetAccount(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
UserID uint `json:"user_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.First(&user, req.UserID).Error; err != nil {
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"user_id": user.ID,
|
||||
"username": user.Username,
|
||||
"balance": user.Balance,
|
||||
"status": user.Status,
|
||||
})
|
||||
}
|
||||
|
||||
func handleAppHeartbeat(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
UserID uint `json:"user_id"`
|
||||
DeviceID string `json:"device_id"`
|
||||
InstanceID string `json:"instance_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.First(&user, req.UserID).Error; err != nil {
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if blocked, reason := checkRiskControl(c, app.ID, req.DeviceID, user.Username); blocked {
|
||||
log.Printf("[DEBUG] Heartbeat blocked by risk control: %s", reason)
|
||||
response.Error(c, 403, reason)
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
user.LastHeartbeatAt = &now
|
||||
|
||||
if app.BillingType != "free" && app.DeductionMode == "auto" && app.DeductionAmount > 0 {
|
||||
shouldDeduct := false
|
||||
|
||||
switch app.DeductionType {
|
||||
case "timer":
|
||||
if user.LastHeartbeatAt != nil {
|
||||
elapsed := now.Sub(*user.LastHeartbeatAt)
|
||||
var interval time.Duration
|
||||
switch app.DeductionUnit {
|
||||
case "minute":
|
||||
interval = time.Duration(app.DeductionInterval) * time.Minute
|
||||
case "hour":
|
||||
interval = time.Duration(app.DeductionInterval) * time.Hour
|
||||
case "day":
|
||||
interval = time.Duration(app.DeductionInterval) * 24 * time.Hour
|
||||
default:
|
||||
interval = time.Duration(app.DeductionInterval) * time.Minute
|
||||
}
|
||||
if elapsed >= interval {
|
||||
shouldDeduct = true
|
||||
}
|
||||
}
|
||||
case "login":
|
||||
shouldDeduct = true
|
||||
}
|
||||
|
||||
if shouldDeduct {
|
||||
if user.Balance >= app.DeductionAmount {
|
||||
user.Balance -= app.DeductionAmount
|
||||
log.Printf("[DEBUG] Deducted %.2f from user %d, new balance: %.2f", app.DeductionAmount, user.ID, user.Balance)
|
||||
} else {
|
||||
log.Printf("[DEBUG] User %d has insufficient balance: %.2f < %.2f", user.ID, user.Balance, app.DeductionAmount)
|
||||
response.Error(c, 403, "余额不足")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := database.DB.Save(&user).Error; err != nil {
|
||||
response.Error(c, 500, "更新心跳失败")
|
||||
return
|
||||
}
|
||||
|
||||
if req.DeviceID != "" {
|
||||
var device model.UserDevice
|
||||
err := database.DB.Where("user_id = ? AND application_id = ? AND device_id = ?", user.ID, app.ID, req.DeviceID).First(&device).Error
|
||||
|
||||
if err != nil {
|
||||
device = model.UserDevice{
|
||||
UserID: user.ID,
|
||||
ApplicationID: app.ID,
|
||||
DeviceID: req.DeviceID,
|
||||
DeviceName: req.DeviceID,
|
||||
Status: "active",
|
||||
}
|
||||
if err := database.DB.Create(&device).Error; err != nil {
|
||||
log.Printf("[DEBUG] Failed to create device: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
instanceID := req.InstanceID
|
||||
if instanceID == "" {
|
||||
instanceID = req.DeviceID
|
||||
}
|
||||
|
||||
if device.ID > 0 {
|
||||
var session model.DeviceSession
|
||||
sessionErr := database.DB.Where("device_id = ? AND instance_id = ?", device.ID, instanceID).First(&session).Error
|
||||
|
||||
if sessionErr != nil {
|
||||
session = model.DeviceSession{
|
||||
DeviceID: device.ID,
|
||||
UserID: user.ID,
|
||||
ApplicationID: app.ID,
|
||||
InstanceID: instanceID,
|
||||
LastHeartbeat: &now,
|
||||
}
|
||||
if err := database.DB.Create(&session).Error; err != nil {
|
||||
log.Printf("[DEBUG] Failed to create session: %v", err)
|
||||
}
|
||||
} else {
|
||||
session.LastHeartbeat = &now
|
||||
if err := database.DB.Save(&session).Error; err != nil {
|
||||
log.Printf("[DEBUG] Failed to update session: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response.SuccessWithMessage(c, "心跳成功", gin.H{
|
||||
"message": "心跳成功",
|
||||
"balance": user.Balance,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupRoutes(r *gin.RouterGroup) {
|
||||
SetupPaymentRoutes(r)
|
||||
SetupAccountRoutes(r)
|
||||
SetupDynamicRoutes(r)
|
||||
}
|
||||
|
||||
func SetupAuthRoutes(r *gin.RouterGroup) {
|
||||
SetupDeviceRoutes(r)
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/pkg/crypto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func AppCryptoMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
fmt.Printf("[AppCrypto] Middleware called\n")
|
||||
appKey := c.Param("appKey")
|
||||
fmt.Printf("[AppCrypto] AppKey from param: %s\n", appKey)
|
||||
|
||||
if appKey == "" {
|
||||
fmt.Printf("[AppCrypto] AppKey is empty, skipping\n")
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
fmt.Printf("[AppCrypto] Database error: %v\n", err)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("[AppCrypto] App found: %+v\n", app)
|
||||
|
||||
var encryptType crypto.EncryptType
|
||||
switch app.EncryptType {
|
||||
case "aes":
|
||||
encryptType = crypto.EncryptTypeAES
|
||||
case "rc4":
|
||||
encryptType = crypto.EncryptTypeRC4
|
||||
default:
|
||||
encryptType = crypto.EncryptTypeNone
|
||||
}
|
||||
|
||||
shouldEncrypt := encryptType != crypto.EncryptTypeNone
|
||||
|
||||
fmt.Printf("[AppCrypto] AppKey: %s, EncryptType: %s, SecretKey: %s, ShouldEncrypt: %v\n",
|
||||
appKey, app.EncryptType, app.SecretKey, shouldEncrypt)
|
||||
|
||||
c.Set("should_encrypt_response", shouldEncrypt)
|
||||
c.Set("crypto_manager", crypto.NewCryptoManager(encryptType, app.SecretKey))
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func getAppCryptoManager(appKey string) (*crypto.CryptoManager, error) {
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var encryptType crypto.EncryptType
|
||||
switch app.EncryptType {
|
||||
case "aes":
|
||||
encryptType = crypto.EncryptTypeAES
|
||||
case "rc4":
|
||||
encryptType = crypto.EncryptTypeRC4
|
||||
default:
|
||||
encryptType = crypto.EncryptTypeNone
|
||||
}
|
||||
|
||||
return crypto.NewCryptoManager(encryptType, app.SecretKey), nil
|
||||
}
|
||||
|
||||
func decryptRequest(c *gin.Context, appKey string) ([]byte, error) {
|
||||
encryptedData := c.GetHeader("X-Encrypted-Data")
|
||||
if encryptedData == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
cryptoManager, err := getAppCryptoManager(appKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decrypted, err := cryptoManager.Decrypt(encryptedData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []byte(decrypted), nil
|
||||
}
|
||||
|
||||
func encryptResponse(c *gin.Context, appKey string, data []byte) (string, error) {
|
||||
cryptoManager, err := getAppCryptoManager(appKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return cryptoManager.Encrypt(string(data))
|
||||
}
|
||||
@@ -0,0 +1,905 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/service"
|
||||
"verification-platform-backend/pkg/jwt"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func getDeviceType(c *gin.Context, clientDeviceType string) string {
|
||||
validTypes := map[string]bool{
|
||||
"android": true,
|
||||
"ios": true,
|
||||
"windows": true,
|
||||
"mac": true,
|
||||
"linux": true,
|
||||
"web": true,
|
||||
}
|
||||
|
||||
if clientDeviceType != "" && validTypes[clientDeviceType] {
|
||||
return clientDeviceType
|
||||
}
|
||||
|
||||
userAgent := c.GetHeader("User-Agent")
|
||||
return parseDeviceTypeFromUserAgent(userAgent)
|
||||
}
|
||||
|
||||
func parseDeviceTypeFromUserAgent(userAgent string) string {
|
||||
if userAgent == "" {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
ua := strings.ToLower(userAgent)
|
||||
|
||||
switch {
|
||||
case strings.Contains(ua, "android"):
|
||||
return "android"
|
||||
case strings.Contains(ua, "iphone") || strings.Contains(ua, "ipad") || strings.Contains(ua, "ipod"):
|
||||
return "ios"
|
||||
case strings.Contains(ua, "windows"):
|
||||
return "windows"
|
||||
case strings.Contains(ua, "macintosh") || strings.Contains(ua, "mac os x"):
|
||||
return "mac"
|
||||
case strings.Contains(ua, "linux"):
|
||||
return "linux"
|
||||
case strings.Contains(ua, "mozilla") || strings.Contains(ua, "webkit") || strings.Contains(ua, "chrome") || strings.Contains(ua, "safari"):
|
||||
return "web"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func SetupAuthUserRoutes(r *gin.RouterGroup) {
|
||||
r.POST("/register", handleAppRegister)
|
||||
r.POST("/login", handleAppLogin)
|
||||
r.POST("/send-email-code", handleAppSendEmailCode)
|
||||
r.POST("/reset-password", handleAppResetPassword)
|
||||
}
|
||||
|
||||
func handleAppSendEmailCode(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Purpose string `json:"purpose"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "请输入有效的邮箱地址")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Purpose == "" {
|
||||
req.Purpose = "register"
|
||||
}
|
||||
|
||||
if req.Purpose == "register" {
|
||||
if !app.EnableEmailVerify {
|
||||
response.Error(c, 400, "该应用未启用邮箱验证")
|
||||
return
|
||||
}
|
||||
} else if req.Purpose == "reset_password" {
|
||||
if !app.EnablePasswordReset {
|
||||
response.Error(c, 400, "该应用未启用密码重置功能")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var smtpConfig model.AppSMTPConfig
|
||||
if err := database.DB.Where("application_id = ?", app.ID).First(&smtpConfig).Error; err != nil {
|
||||
response.Error(c, 400, "应用未配置SMTP")
|
||||
return
|
||||
}
|
||||
|
||||
var template model.EmailTemplate
|
||||
database.DB.Where("application_id = ? AND type = ? AND status = ?", app.ID, req.Purpose, "active").
|
||||
Order("is_default DESC").First(&template)
|
||||
|
||||
code := generateVerifyCode()
|
||||
expireAt := time.Now().Add(15 * time.Minute)
|
||||
|
||||
verifyCode := model.EmailVerifyCode{
|
||||
ApplicationID: app.ID,
|
||||
Email: req.Email,
|
||||
Code: code,
|
||||
Purpose: req.Purpose,
|
||||
ExpiresAt: expireAt,
|
||||
}
|
||||
database.DB.Create(&verifyCode)
|
||||
|
||||
emailService := service.NewEmailService()
|
||||
config := service.EmailConfig{
|
||||
Host: smtpConfig.Host,
|
||||
Port: smtpConfig.Port,
|
||||
User: smtpConfig.User,
|
||||
Password: smtpConfig.Password,
|
||||
FromName: smtpConfig.FromName,
|
||||
FromEmail: smtpConfig.FromEmail,
|
||||
UseSSL: smtpConfig.UseSSL,
|
||||
}
|
||||
|
||||
subject := fmt.Sprintf("验证码 - %s", app.Name)
|
||||
content := fmt.Sprintf(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="UTF-8"></head>
|
||||
<body style="font-family: Arial, sans-serif; padding: 20px; background-color: #f5f5f5;">
|
||||
<div style="max-width: 600px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
<h2 style="color: #333; margin-bottom: 20px;">邮箱验证</h2>
|
||||
<p style="color: #666; line-height: 1.6;">您的验证码是:<strong style="font-size: 24px; color: #1890ff;">%s</strong></p>
|
||||
<p style="color: #999; font-size: 12px;">验证码有效期为15分钟,请尽快使用。</p>
|
||||
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
||||
<p style="color: #999; font-size: 12px;">此邮件由 %s 系统自动发送,请勿回复。</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`, code, app.Name)
|
||||
|
||||
if template.ID > 0 {
|
||||
subject = template.Subject
|
||||
content = template.Content
|
||||
}
|
||||
|
||||
if err := emailService.SendEmail(config, req.Email, subject, content); err != nil {
|
||||
response.Error(c, 500, fmt.Sprintf("发送失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "验证码已发送",
|
||||
})
|
||||
}
|
||||
|
||||
func generateVerifyCode() string {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
return fmt.Sprintf("%06d", r.Intn(1000000))
|
||||
}
|
||||
|
||||
func handleAppRegister(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
log.Printf("[DEBUG] Starting registration for appKey: %s", appKey)
|
||||
|
||||
app, exists := c.Get("app")
|
||||
if !exists {
|
||||
var appModel model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&appModel).Error; err != nil {
|
||||
log.Printf("[DEBUG] Application not found for appKey: %s, error: %v", appKey, err)
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
app = &appModel
|
||||
}
|
||||
appModel := app.(*model.Application)
|
||||
log.Printf("[DEBUG] Found application: ID=%d, Status=%s", appModel.ID, appModel.Status)
|
||||
|
||||
if service.GetApplicationDisabledStatus(appModel.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
EmailCode string `json:"email_code"`
|
||||
Password string `json:"password"`
|
||||
DeviceID string `json:"device_id"`
|
||||
DeviceName string `json:"device_name"`
|
||||
DeviceType string `json:"device_type"`
|
||||
InstanceID string `json:"instance_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
log.Printf("[DEBUG] Invalid request parameters: %v", err)
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
log.Printf("[DEBUG] Registration request for username: %s", req.Username)
|
||||
|
||||
if blocked, reason := checkRiskControl(c, appModel.ID, req.DeviceID, req.Username); blocked {
|
||||
log.Printf("[DEBUG] Registration blocked by risk control: %s", reason)
|
||||
service.LogVerification(c, &appModel.ID, nil, "register_blocked", "注册被拦截: "+reason, req.DeviceID, fmt.Errorf(reason))
|
||||
response.Error(c, 403, reason)
|
||||
return
|
||||
}
|
||||
|
||||
if !appModel.AllowRegister {
|
||||
log.Printf("[DEBUG] Registration disabled for app: %s", appKey)
|
||||
response.Error(c, 403, "注册功能已关闭")
|
||||
return
|
||||
}
|
||||
|
||||
var allowedMethods []string
|
||||
if appModel.RegisterMethods != "" {
|
||||
if err := json.Unmarshal([]byte(appModel.RegisterMethods), &allowedMethods); err != nil {
|
||||
log.Printf("[DEBUG] Failed to parse register methods: %v", err)
|
||||
allowedMethods = []string{"username"}
|
||||
}
|
||||
} else {
|
||||
allowedMethods = []string{"username"}
|
||||
}
|
||||
|
||||
isEmailRegister := req.Email != "" && req.Username == ""
|
||||
isUsernameRegister := req.Username != "" && req.Email == ""
|
||||
isPhoneRegister := false
|
||||
|
||||
if isEmailRegister && !containsString(allowedMethods, "email") {
|
||||
response.Error(c, 403, "邮箱注册方式未开放")
|
||||
return
|
||||
}
|
||||
if isUsernameRegister && !containsString(allowedMethods, "username") {
|
||||
response.Error(c, 403, "用户名注册方式未开放")
|
||||
return
|
||||
}
|
||||
if isPhoneRegister && !containsString(allowedMethods, "phone") {
|
||||
response.Error(c, 403, "手机号注册方式未开放")
|
||||
return
|
||||
}
|
||||
|
||||
if appModel.EnableEmailVerify && appModel.RequireEmailVerify {
|
||||
if req.Email == "" {
|
||||
response.Error(c, 400, "请输入邮箱地址")
|
||||
return
|
||||
}
|
||||
if req.EmailCode == "" {
|
||||
response.Error(c, 400, "请输入邮箱验证码")
|
||||
return
|
||||
}
|
||||
|
||||
var verifyCode model.EmailVerifyCode
|
||||
if err := database.DB.Where(
|
||||
"application_id = ? AND email = ? AND code = ? AND purpose = ? AND used = ?",
|
||||
appModel.ID, req.Email, req.EmailCode, "register", false,
|
||||
).First(&verifyCode).Error; err != nil {
|
||||
response.Error(c, 400, "验证码错误或已过期")
|
||||
return
|
||||
}
|
||||
|
||||
if verifyCode.ExpiresAt.Before(time.Now()) {
|
||||
response.Error(c, 400, "验证码已过期")
|
||||
return
|
||||
}
|
||||
|
||||
verifyCode.Used = true
|
||||
database.DB.Save(&verifyCode)
|
||||
}
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.Where("username = ? AND application_id = ?", req.Username, appModel.ID).First(&user).Error; err == nil {
|
||||
log.Printf("[DEBUG] User already exists: %s", req.Username)
|
||||
service.LogVerification(c, &appModel.ID, nil, "register_failed", "注册失败: 用户已存在 - "+req.Username, req.DeviceID, fmt.Errorf("用户已存在"))
|
||||
response.Error(c, 400, "用户已存在")
|
||||
return
|
||||
}
|
||||
|
||||
user = model.AppUser{
|
||||
Username: req.Username,
|
||||
Email: req.Email,
|
||||
Password: req.Password,
|
||||
DeviceID: req.DeviceID,
|
||||
Avatar: "",
|
||||
Status: "active",
|
||||
ApplicationID: appModel.ID,
|
||||
IsTrialUser: appModel.EnableTrial,
|
||||
}
|
||||
|
||||
if appModel.EnableTrial {
|
||||
now := time.Now()
|
||||
user.TrialStartAt = &now
|
||||
if appModel.TrialBalance > 0 {
|
||||
user.Balance = appModel.TrialBalance
|
||||
}
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&user).Error; err != nil {
|
||||
log.Printf("[DEBUG] Failed to create user: %v", err)
|
||||
response.Error(c, 500, "注册失败")
|
||||
return
|
||||
}
|
||||
log.Printf("[DEBUG] Successfully created user with ID: %d", user.ID)
|
||||
|
||||
if req.DeviceID != "" {
|
||||
now := time.Now()
|
||||
deviceType := getDeviceType(c, req.DeviceType)
|
||||
|
||||
deviceName := req.DeviceName
|
||||
if deviceName == "" {
|
||||
deviceName = req.DeviceID
|
||||
}
|
||||
|
||||
var device model.UserDevice
|
||||
if err := database.DB.Where("user_id = ? AND application_id = ? AND device_id = ?", user.ID, appModel.ID, req.DeviceID).First(&device).Error; err != nil {
|
||||
device = model.UserDevice{
|
||||
UserID: user.ID,
|
||||
ApplicationID: appModel.ID,
|
||||
DeviceID: req.DeviceID,
|
||||
DeviceName: deviceName,
|
||||
DeviceType: deviceType,
|
||||
Status: "active",
|
||||
}
|
||||
if err := database.DB.Create(&device).Error; err != nil {
|
||||
log.Printf("[DEBUG] Failed to create device: %v", err)
|
||||
} else {
|
||||
log.Printf("[DEBUG] Successfully created device with ID: %d, type: %s", device.ID, deviceType)
|
||||
}
|
||||
}
|
||||
|
||||
instanceID := req.InstanceID
|
||||
if instanceID == "" {
|
||||
instanceID = req.DeviceID
|
||||
}
|
||||
|
||||
if device.ID > 0 {
|
||||
var session model.DeviceSession
|
||||
sessionErr := database.DB.Where("device_id = ? AND instance_id = ?", device.ID, instanceID).First(&session).Error
|
||||
|
||||
if sessionErr != nil {
|
||||
session = model.DeviceSession{
|
||||
DeviceID: device.ID,
|
||||
UserID: user.ID,
|
||||
ApplicationID: appModel.ID,
|
||||
InstanceID: instanceID,
|
||||
LastHeartbeat: &now,
|
||||
}
|
||||
if err := database.DB.Create(&session).Error; err != nil {
|
||||
log.Printf("[DEBUG] Failed to create session: %v", err)
|
||||
}
|
||||
} else {
|
||||
session.LastHeartbeat = &now
|
||||
if err := database.DB.Save(&session).Error; err != nil {
|
||||
log.Printf("[DEBUG] Failed to update session: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
service.LogVerification(c, &appModel.ID, &user.ID, "register", "用户注册: "+req.Username, req.DeviceID, nil)
|
||||
|
||||
response.SuccessWithMessage(c, "注册成功", gin.H{
|
||||
"user_id": user.ID,
|
||||
})
|
||||
}
|
||||
|
||||
func handleAppLogin(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
log.Printf("[DEBUG] Login attempt for appKey: %s", appKey)
|
||||
|
||||
app, exists := c.Get("app")
|
||||
if !exists {
|
||||
var appModel model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&appModel).Error; err != nil {
|
||||
log.Printf("[DEBUG] Application not found for appKey: %s, error: %v", appKey, err)
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
app = &appModel
|
||||
}
|
||||
appModel := app.(*model.Application)
|
||||
log.Printf("[DEBUG] Found application: ID=%d, Status=%s", appModel.ID, appModel.Status)
|
||||
|
||||
var req struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
DeviceID string `json:"device_id"`
|
||||
DeviceName string `json:"device_name"`
|
||||
DeviceType string `json:"device_type"`
|
||||
InstanceID string `json:"instance_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
log.Printf("[DEBUG] Invalid request parameters: %v", err)
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
log.Printf("[DEBUG] Login request for username: %s", req.Username)
|
||||
|
||||
if req.DeviceID == "" {
|
||||
response.Error(c, 400, "设备ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.Where("username = ? AND application_id = ?", req.Username, appModel.ID).First(&user).Error; err != nil {
|
||||
log.Printf("[DEBUG] User not found: %v", err)
|
||||
service.LogVerification(c, &appModel.ID, nil, "login_failed", "登录失败: 用户不存在 - "+req.Username, req.DeviceID, fmt.Errorf("用户不存在"))
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Password != req.Password {
|
||||
log.Printf("[DEBUG] Password mismatch for user: %s", req.Username)
|
||||
service.LogVerification(c, &appModel.ID, &user.ID, "login_failed", "登录失败: 密码错误 - "+req.Username, req.DeviceID, fmt.Errorf("密码错误"))
|
||||
response.Error(c, 400, "密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
if blocked, reason := checkRiskControl(c, appModel.ID, req.DeviceID, req.Username); blocked {
|
||||
log.Printf("[DEBUG] Login blocked by risk control: %s", reason)
|
||||
service.LogVerification(c, &appModel.ID, &user.ID, "login_blocked", "登录被拦截: "+reason, req.DeviceID, fmt.Errorf(reason))
|
||||
response.Error(c, 403, reason)
|
||||
return
|
||||
}
|
||||
|
||||
if appModel.LoginPolicy == "strict" {
|
||||
log.Printf("[DEBUG] LoginPolicy is strict, checking free period and trial")
|
||||
isInFreePeriod := false
|
||||
if appModel.EnableFreePeriod {
|
||||
isInFreePeriod = checkFreePeriod(appModel)
|
||||
}
|
||||
log.Printf("[DEBUG] isInFreePeriod=%v", isInFreePeriod)
|
||||
|
||||
if !isInFreePeriod {
|
||||
log.Printf("[DEBUG] Not in free period, checking trial")
|
||||
isTrialValid := false
|
||||
if user.IsTrialUser && user.TrialEndAt != nil && user.TrialEndAt.After(time.Now()) {
|
||||
isTrialValid = true
|
||||
}
|
||||
log.Printf("[DEBUG] isTrialValid=%v, IsTrialUser=%v, TrialEndAt=%v, Balance=%f", isTrialValid, user.IsTrialUser, user.TrialEndAt, user.Balance)
|
||||
|
||||
if !isTrialValid {
|
||||
if appModel.BillingType != "free" && user.Balance <= 0 {
|
||||
log.Printf("[DEBUG] User %d has no balance remaining", user.ID)
|
||||
service.LogVerification(c, &appModel.ID, &user.ID, "login_failed", "登录失败: 余额不足 - "+req.Username, req.DeviceID, fmt.Errorf("余额不足,请充值后继续使用"))
|
||||
response.Error(c, 403, "余额不足,请充值后继续使用")
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Printf("[DEBUG] User is in free period, allowing login")
|
||||
}
|
||||
}
|
||||
|
||||
if req.DeviceID != "" {
|
||||
heartbeatTimeout := time.Duration(appModel.HeartbeatTimeout) * time.Second
|
||||
timeoutThreshold := time.Now().Add(-heartbeatTimeout)
|
||||
database.DB.Where("user_id = ? AND application_id = ? AND last_heartbeat < ?", user.ID, appModel.ID, timeoutThreshold).Delete(&model.DeviceSession{})
|
||||
|
||||
now := time.Now()
|
||||
deviceType := getDeviceType(c, req.DeviceType)
|
||||
|
||||
deviceName := req.DeviceName
|
||||
if deviceName == "" {
|
||||
deviceName = req.DeviceID
|
||||
}
|
||||
|
||||
clientIP := c.ClientIP()
|
||||
|
||||
if appModel.BindType == "ip" || appModel.BindType == "mixed" {
|
||||
var userIP model.UserIP
|
||||
ipErr := database.DB.Where("user_id = ? AND application_id = ? AND ip_address = ?", user.ID, appModel.ID, clientIP).First(&userIP).Error
|
||||
|
||||
if ipErr != nil {
|
||||
var ipCount int64
|
||||
database.DB.Model(&model.UserIP{}).
|
||||
Where("user_id = ? AND application_id = ?", user.ID, appModel.ID).
|
||||
Count(&ipCount)
|
||||
|
||||
if appModel.MaxDevices > 0 && ipCount >= int64(appModel.MaxDevices) {
|
||||
log.Printf("[DEBUG] IP limit exceeded for user %d: %d/%d", user.ID, ipCount, appModel.MaxDevices)
|
||||
|
||||
var userIPs []model.UserIP
|
||||
database.DB.Where("user_id = ? AND application_id = ?", user.ID, appModel.ID).Order("created_at DESC").Find(&userIPs)
|
||||
|
||||
ipList := make([]gin.H, 0)
|
||||
for _, ip := range userIPs {
|
||||
ipList = append(ipList, gin.H{
|
||||
"id": ip.ID,
|
||||
"ip_address": ip.IPAddress,
|
||||
"status": ip.Status,
|
||||
"created_at": ip.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
response.ErrorWithData(c, 403, "IP绑定数量已达上限,请解绑后再试", gin.H{
|
||||
"error_code": "IP_LIMIT_EXCEEDED",
|
||||
"max_ips": appModel.MaxDevices,
|
||||
"ip_count": ipCount,
|
||||
"ips": ipList,
|
||||
})
|
||||
service.LogVerification(c, &appModel.ID, &user.ID, "login_failed", "登录失败: IP绑定数量已达上限 - "+req.Username, req.DeviceID, fmt.Errorf("IP绑定数量已达上限"))
|
||||
return
|
||||
}
|
||||
|
||||
userIP = model.UserIP{
|
||||
UserID: user.ID,
|
||||
ApplicationID: appModel.ID,
|
||||
IPAddress: clientIP,
|
||||
Status: "active",
|
||||
}
|
||||
if err := database.DB.Create(&userIP).Error; err != nil {
|
||||
log.Printf("[DEBUG] Failed to create user IP: %v", err)
|
||||
} else {
|
||||
log.Printf("[DEBUG] Successfully created user IP with ID: %d, IP: %s", userIP.ID, clientIP)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var device model.UserDevice
|
||||
deviceErr := database.DB.Where("user_id = ? AND application_id = ? AND device_id = ?", user.ID, appModel.ID, req.DeviceID).First(&device).Error
|
||||
|
||||
if deviceErr != nil {
|
||||
if (appModel.BindType == "device" || appModel.BindType == "mixed") && appModel.MaxDevices > 0 {
|
||||
var deviceCount int64
|
||||
database.DB.Model(&model.UserDevice{}).
|
||||
Where("user_id = ? AND application_id = ?", user.ID, appModel.ID).
|
||||
Count(&deviceCount)
|
||||
|
||||
if deviceCount >= int64(appModel.MaxDevices) {
|
||||
log.Printf("[DEBUG] Device limit exceeded for user %d: %d/%d", user.ID, deviceCount, appModel.MaxDevices)
|
||||
|
||||
var devices []model.UserDevice
|
||||
database.DB.Where("user_id = ? AND application_id = ?", user.ID, appModel.ID).Order("created_at DESC").Find(&devices)
|
||||
|
||||
deviceList := make([]gin.H, 0)
|
||||
for _, d := range devices {
|
||||
var onlineSessions []model.DeviceSession
|
||||
database.DB.Where("device_id = ? AND last_heartbeat > ?", d.ID, timeoutThreshold).Find(&onlineSessions)
|
||||
|
||||
deviceList = append(deviceList, gin.H{
|
||||
"id": d.ID,
|
||||
"device_id": d.DeviceID,
|
||||
"device_name": d.DeviceName,
|
||||
"device_type": d.DeviceType,
|
||||
"online_count": len(onlineSessions),
|
||||
"created_at": d.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
response.ErrorWithData(c, 403, "设备绑定数量已达上限,请解绑后再试", gin.H{
|
||||
"error_code": "DEVICE_LIMIT_EXCEEDED",
|
||||
"max_devices": appModel.MaxDevices,
|
||||
"device_count": deviceCount,
|
||||
"devices": deviceList,
|
||||
})
|
||||
service.LogVerification(c, &appModel.ID, &user.ID, "login_failed", "登录失败: 设备绑定数量已达上限 - "+req.Username, req.DeviceID, fmt.Errorf("设备绑定数量已达上限"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
device = model.UserDevice{
|
||||
UserID: user.ID,
|
||||
ApplicationID: appModel.ID,
|
||||
DeviceID: req.DeviceID,
|
||||
DeviceName: deviceName,
|
||||
DeviceType: deviceType,
|
||||
Status: "active",
|
||||
}
|
||||
if err := database.DB.Create(&device).Error; err != nil {
|
||||
log.Printf("[DEBUG] Failed to create device: %v", err)
|
||||
} else {
|
||||
log.Printf("[DEBUG] Successfully created device with ID: %d, type: %s", device.ID, deviceType)
|
||||
}
|
||||
}
|
||||
|
||||
instanceID := req.InstanceID
|
||||
if instanceID == "" {
|
||||
instanceID = req.DeviceID
|
||||
}
|
||||
|
||||
if device.ID > 0 {
|
||||
if appModel.MultiOpen && appModel.MaxInstances > 0 {
|
||||
var sessionCount int64
|
||||
database.DB.Model(&model.DeviceSession{}).
|
||||
Where("device_id = ? AND instance_id != ? AND last_heartbeat > ?", device.ID, instanceID, timeoutThreshold).
|
||||
Count(&sessionCount)
|
||||
|
||||
if sessionCount >= int64(appModel.MaxInstances) {
|
||||
if appModel.MultiOpenMode == "forbidden" {
|
||||
log.Printf("[DEBUG] Multi-instance limit exceeded for user %d device %s: %d/%d", user.ID, req.DeviceID, sessionCount, appModel.MaxInstances)
|
||||
|
||||
var sessions []model.DeviceSession
|
||||
database.DB.Where("device_id = ? AND instance_id != ?", device.ID, instanceID).Order("last_heartbeat DESC").Find(&sessions)
|
||||
|
||||
sessionList := make([]gin.H, 0)
|
||||
for _, s := range sessions {
|
||||
isOnline := s.LastHeartbeat != nil && s.LastHeartbeat.After(timeoutThreshold)
|
||||
sessionList = append(sessionList, gin.H{
|
||||
"id": s.ID,
|
||||
"instance_id": s.InstanceID,
|
||||
"is_online": isOnline,
|
||||
"last_heartbeat": s.LastHeartbeat,
|
||||
"created_at": s.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
response.ErrorWithData(c, 403, "多开数量已达上限", gin.H{
|
||||
"error_code": "MULTI_INSTANCE_LIMIT_EXCEEDED",
|
||||
"max_instances": appModel.MaxInstances,
|
||||
"instance_count": sessionCount,
|
||||
"instances": sessionList,
|
||||
})
|
||||
service.LogVerification(c, &appModel.ID, &user.ID, "login_failed", "登录失败: 多开数量已达上限 - "+req.Username, req.DeviceID, fmt.Errorf("多开数量已达上限"))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var session model.DeviceSession
|
||||
if err := database.DB.Where("device_id = ? AND instance_id = ?", device.ID, instanceID).First(&session).Error; err != nil {
|
||||
session = model.DeviceSession{
|
||||
DeviceID: device.ID,
|
||||
UserID: user.ID,
|
||||
ApplicationID: appModel.ID,
|
||||
InstanceID: instanceID,
|
||||
LastHeartbeat: &now,
|
||||
}
|
||||
if err := database.DB.Create(&session).Error; err != nil {
|
||||
log.Printf("[DEBUG] Failed to create session: %v", err)
|
||||
} else {
|
||||
log.Printf("[DEBUG] Successfully created session with ID: %d, instance: %s", session.ID, instanceID)
|
||||
}
|
||||
} else {
|
||||
session.LastHeartbeat = &now
|
||||
if err := database.DB.Save(&session).Error; err != nil {
|
||||
log.Printf("[DEBUG] Failed to update session: %v", err)
|
||||
} else {
|
||||
log.Printf("[DEBUG] Successfully updated session %d last_heartbeat", session.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
token, err := jwt.GenerateToken(user.ID, user.Username, "app_user")
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] Failed to generate token: %v", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
database.DB.Model(&user).Updates(map[string]interface{}{
|
||||
"last_login_at": now,
|
||||
"last_heartbeat_at": now,
|
||||
})
|
||||
|
||||
service.LogVerification(c, &appModel.ID, &user.ID, "login", "用户登录: "+req.Username, req.DeviceID, nil)
|
||||
|
||||
response.SuccessWithMessage(c, "登录成功", gin.H{
|
||||
"user_id": user.ID,
|
||||
"token": token,
|
||||
})
|
||||
}
|
||||
|
||||
func checkFreePeriod(app *model.Application) bool {
|
||||
now := time.Now()
|
||||
log.Printf("[DEBUG] checkFreePeriod: EnableFreePeriod=%v, FreePeriodType=%s, FreePeriodStart=%s, FreePeriodEnd=%s, FreePeriodWeekdays=%s, FreePeriodStartTime=%s, FreePeriodEndTime=%s",
|
||||
app.EnableFreePeriod, app.FreePeriodType, app.FreePeriodStart, app.FreePeriodEnd, app.FreePeriodWeekdays, app.FreePeriodStartTime, app.FreePeriodEndTime)
|
||||
|
||||
localLocation := time.Local
|
||||
|
||||
if app.FreePeriodType == "range" {
|
||||
if app.FreePeriodStart == "" || app.FreePeriodEnd == "" {
|
||||
log.Printf("[DEBUG] checkFreePeriod: range type but missing start or end")
|
||||
return false
|
||||
}
|
||||
var startDate, endDate time.Time
|
||||
var err error
|
||||
|
||||
if len(app.FreePeriodStart) > 10 {
|
||||
startDate, err = time.ParseInLocation("2006-01-02T15:04", app.FreePeriodStart, localLocation)
|
||||
} else {
|
||||
startDate, err = time.ParseInLocation("2006-01-02", app.FreePeriodStart, localLocation)
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] checkFreePeriod: failed to parse start date: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
if len(app.FreePeriodEnd) > 10 {
|
||||
endDate, err = time.ParseInLocation("2006-01-02T15:04", app.FreePeriodEnd, localLocation)
|
||||
} else {
|
||||
endDate, err = time.ParseInLocation("2006-01-02", app.FreePeriodEnd, localLocation)
|
||||
if err == nil {
|
||||
endDate = endDate.Add(24 * time.Hour)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] checkFreePeriod: failed to parse end date: %v", err)
|
||||
return false
|
||||
}
|
||||
result := now.After(startDate) && now.Before(endDate)
|
||||
log.Printf("[DEBUG] checkFreePeriod: range check result=%v, now=%v, start=%v, end=%v", result, now, startDate, endDate)
|
||||
return result
|
||||
} else if app.FreePeriodType == "weekdays" {
|
||||
if app.FreePeriodWeekdays == "" {
|
||||
log.Printf("[DEBUG] checkFreePeriod: weekdays type but missing weekdays")
|
||||
return false
|
||||
}
|
||||
weekday := int(now.Weekday())
|
||||
if weekday == 0 {
|
||||
weekday = 7
|
||||
}
|
||||
weekdayStr := string(rune('0' + weekday))
|
||||
if !contains(app.FreePeriodWeekdays, weekdayStr) {
|
||||
log.Printf("[DEBUG] checkFreePeriod: weekday %s not in %s", weekdayStr, app.FreePeriodWeekdays)
|
||||
return false
|
||||
}
|
||||
if app.FreePeriodStartTime != "" && app.FreePeriodEndTime != "" {
|
||||
currentTime := now.Format("15:04")
|
||||
result := currentTime >= app.FreePeriodStartTime && currentTime <= app.FreePeriodEndTime
|
||||
log.Printf("[DEBUG] checkFreePeriod: weekday time check result=%v, currentTime=%s, startTime=%s, endTime=%s", result, currentTime, app.FreePeriodStartTime, app.FreePeriodEndTime)
|
||||
return result
|
||||
}
|
||||
log.Printf("[DEBUG] checkFreePeriod: weekday check passed")
|
||||
return true
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] checkFreePeriod: unknown type %s", app.FreePeriodType)
|
||||
return false
|
||||
}
|
||||
|
||||
func checkRiskControl(c *gin.Context, appID uint, deviceID string, username string) (bool, string) {
|
||||
clientIP := c.ClientIP()
|
||||
|
||||
var globalRules []model.RiskControlRule
|
||||
database.DB.Where("application_id IS NULL AND status = ?", "active").Find(&globalRules)
|
||||
|
||||
var appRules []model.RiskControlRule
|
||||
database.DB.Where("application_id = ? AND status = ?", appID, "active").Find(&appRules)
|
||||
|
||||
now := time.Now()
|
||||
|
||||
for _, rule := range globalRules {
|
||||
if rule.ExpiresAt != nil && rule.ExpiresAt.Before(now) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch rule.Type {
|
||||
case "device":
|
||||
if deviceID != "" && rule.Value == deviceID {
|
||||
return true, "设备已被全局封禁: " + rule.Reason
|
||||
}
|
||||
case "ip":
|
||||
if rule.Value == clientIP {
|
||||
return true, "IP已被全局封禁: " + rule.Reason
|
||||
}
|
||||
case "region":
|
||||
}
|
||||
}
|
||||
|
||||
for _, rule := range appRules {
|
||||
if rule.ExpiresAt != nil && rule.ExpiresAt.Before(now) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch rule.Type {
|
||||
case "device":
|
||||
if deviceID != "" && rule.Value == deviceID {
|
||||
return true, "设备已被封禁: " + rule.Reason
|
||||
}
|
||||
case "ip":
|
||||
if rule.Value == clientIP {
|
||||
return true, "IP已被封禁: " + rule.Reason
|
||||
}
|
||||
case "user":
|
||||
if username != "" && rule.Value == username {
|
||||
return true, "账号已被封禁: " + rule.Reason
|
||||
}
|
||||
case "region":
|
||||
}
|
||||
}
|
||||
|
||||
return false, ""
|
||||
}
|
||||
|
||||
func contains(s string, substr string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i:i+1] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func containsString(slice []string, str string) bool {
|
||||
for _, v := range slice {
|
||||
if v == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func checkAppEmailPermission(userID uint, permission *model.PackagePermission) bool {
|
||||
var userPackage model.UserPackage
|
||||
if err := database.DB.Where("user_id = ? AND status = ?", userID, "active").
|
||||
Preload("Package").First(&userPackage).Error; err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if userPackage.ExpiredAt != nil && userPackage.ExpiredAt.Before(time.Now()) {
|
||||
return false
|
||||
}
|
||||
|
||||
if err := database.DB.Where("package_id = ?", userPackage.PackageID).First(permission).Error; err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func handleAppResetPassword(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
if !app.EnablePasswordReset {
|
||||
response.Error(c, 400, "该应用未启用密码重置功能")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Code string `json:"code" binding:"required"`
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var verifyCode model.EmailVerifyCode
|
||||
if err := database.DB.Where(
|
||||
"application_id = ? AND email = ? AND code = ? AND purpose = ? AND used = ?",
|
||||
app.ID, req.Email, req.Code, "reset_password", false,
|
||||
).First(&verifyCode).Error; err != nil {
|
||||
response.Error(c, 400, "验证码无效或已过期")
|
||||
return
|
||||
}
|
||||
|
||||
if verifyCode.ExpiresAt.Before(time.Now()) {
|
||||
response.Error(c, 400, "验证码已过期")
|
||||
return
|
||||
}
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.Where("email = ? AND application_id = ?", req.Email, app.ID).First(&user).Error; err != nil {
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
response.Error(c, 500, "密码加密失败")
|
||||
return
|
||||
}
|
||||
|
||||
user.Password = string(hashedPassword)
|
||||
if err := database.DB.Save(&user).Error; err != nil {
|
||||
response.Error(c, 500, "密码重置失败")
|
||||
return
|
||||
}
|
||||
|
||||
verifyCode.Used = true
|
||||
database.DB.Save(&verifyCode)
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "密码重置成功",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,918 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/middleware"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/service"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupCloudRoutes(r *gin.RouterGroup) {
|
||||
r.GET("/constants", handleAppGetConstants)
|
||||
r.GET("/constants/:key", handleAppGetConstantByKey)
|
||||
r.GET("/constants/:key/download", handleAppDownloadConstant)
|
||||
r.GET("/variables", handleAppGetVariables)
|
||||
r.GET("/variables/:key", handleAppGetVariableByKey)
|
||||
r.GET("/variables/:key/download", handleAppDownloadVariable)
|
||||
r.POST("/variables/:key/upload", handleAppUploadVariableBinary)
|
||||
r.POST("/variables", handleAppUpdateVariables)
|
||||
r.POST("/variables/:key/records", handleAppCreateVariableRecord)
|
||||
r.GET("/variables/:key/records", handleAppGetVariableRecords)
|
||||
r.DELETE("/variables/:key/records/:record_id", handleAppDeleteVariableRecord)
|
||||
r.POST("/call-function", handleAppCallFunction)
|
||||
}
|
||||
|
||||
func handleAppGetConstants(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var appUser model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", userID, app.ID).First(&appUser).Error; err != nil {
|
||||
response.Error(c, 403, "无权访问该应用的云端常量")
|
||||
return
|
||||
}
|
||||
|
||||
var constants []model.CloudConstant
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND status = ?", app.UserID, app.ID, "active").Find(&constants).Error; err != nil {
|
||||
response.Error(c, 500, "获取云端常量失败")
|
||||
return
|
||||
}
|
||||
|
||||
result := make(map[string]interface{})
|
||||
for _, constant := range constants {
|
||||
if constant.VarType == "binary" {
|
||||
result[constant.Key] = gin.H{
|
||||
"type": "binary",
|
||||
"value": "/api/v1/app/" + appKey + "/constants/" + constant.Key + "/download",
|
||||
"file_name": constant.OriginalName,
|
||||
"file_size": constant.FileSize,
|
||||
"md5": constant.FileMD5,
|
||||
"mime_type": constant.MimeType,
|
||||
}
|
||||
} else {
|
||||
result[constant.Key] = gin.H{
|
||||
"type": constant.VarType,
|
||||
"value": constant.Value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, result)
|
||||
}
|
||||
|
||||
func handleAppGetConstantByKey(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
key := c.Param("key")
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var appUser model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", userID, app.ID).First(&appUser).Error; err != nil {
|
||||
response.Error(c, 403, "无权访问该应用的云端常量")
|
||||
return
|
||||
}
|
||||
|
||||
var constant model.CloudConstant
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND key = ? AND status = ?", app.UserID, app.ID, key, "active").First(&constant).Error; err != nil {
|
||||
response.Error(c, 404, "云端常量不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if constant.VarType == "binary" {
|
||||
response.Success(c, gin.H{
|
||||
"key": constant.Key,
|
||||
"type": "binary",
|
||||
"value": "/api/v1/app/" + appKey + "/constants/" + constant.Key + "/download",
|
||||
"file_name": constant.OriginalName,
|
||||
"file_size": constant.FileSize,
|
||||
"md5": constant.FileMD5,
|
||||
"mime_type": constant.MimeType,
|
||||
})
|
||||
} else {
|
||||
response.Success(c, gin.H{
|
||||
"key": constant.Key,
|
||||
"type": constant.VarType,
|
||||
"value": constant.Value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func handleAppDownloadConstant(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
key := c.Param("key")
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var appUser model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", userID, app.ID).First(&appUser).Error; err != nil {
|
||||
response.Error(c, 403, "无权访问该应用的云端常量")
|
||||
return
|
||||
}
|
||||
|
||||
var constant model.CloudConstant
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND key = ? AND status = ?", app.UserID, app.ID, key, "active").First(&constant).Error; err != nil {
|
||||
response.Error(c, 404, "云端常量不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if constant.VarType != "binary" || constant.FilePath == "" {
|
||||
response.Error(c, 400, "该常量不是文件类型")
|
||||
return
|
||||
}
|
||||
|
||||
filePath := constant.FilePath
|
||||
if strings.HasPrefix(filePath, "/") {
|
||||
filePath = filePath[1:]
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
response.Error(c, 404, "文件不存在")
|
||||
return
|
||||
}
|
||||
|
||||
encodedFilename := url.QueryEscape(constant.OriginalName)
|
||||
c.Header("Content-Description", "File Transfer")
|
||||
c.Header("Content-Type", "application/octet-stream")
|
||||
c.Header("Content-Disposition", "attachment; filename*=UTF-8''"+encodedFilename)
|
||||
c.Header("Content-Transfer-Encoding", "binary")
|
||||
c.Header("Expires", "0")
|
||||
c.Header("Cache-Control", "must-revalidate")
|
||||
c.Header("Pragma", "public")
|
||||
c.FileAttachment(filePath, constant.OriginalName)
|
||||
}
|
||||
|
||||
func handleAppGetVariables(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var appUser model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", userID, app.ID).First(&appUser).Error; err != nil {
|
||||
response.Error(c, 403, "无权访问该应用的云端变量")
|
||||
return
|
||||
}
|
||||
|
||||
var variables []model.CloudVariable
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND status = ?", app.UserID, app.ID, "active").Find(&variables).Error; err != nil {
|
||||
response.Error(c, 500, "获取云端变量失败")
|
||||
return
|
||||
}
|
||||
|
||||
var userVariables []model.UserVariable
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ?", userID, app.ID).Find(&userVariables).Error; err != nil {
|
||||
response.Error(c, 500, "获取用户变量失败")
|
||||
return
|
||||
}
|
||||
|
||||
userVarMap := make(map[string]model.UserVariable)
|
||||
for _, uv := range userVariables {
|
||||
userVarMap[uv.VarName] = uv
|
||||
}
|
||||
|
||||
result := make(map[string]interface{})
|
||||
for _, v := range variables {
|
||||
if v.VarType == "binary" {
|
||||
if v.Scope == "user" {
|
||||
if uv, ok := userVarMap[v.Key]; ok && uv.FilePath != "" {
|
||||
result[v.Key] = gin.H{
|
||||
"type": "binary",
|
||||
"value": "/api/v1/app/" + appKey + "/variables/" + v.Key + "/download",
|
||||
"file_name": uv.OriginalName,
|
||||
"file_size": uv.FileSize,
|
||||
"md5": uv.FileMD5,
|
||||
"mime_type": uv.MimeType,
|
||||
}
|
||||
} else {
|
||||
result[v.Key] = gin.H{
|
||||
"type": "binary",
|
||||
"value": "/api/v1/app/" + appKey + "/variables/" + v.Key + "/download",
|
||||
"file_name": v.OriginalName,
|
||||
"file_size": v.FileSize,
|
||||
"md5": v.FileMD5,
|
||||
"mime_type": v.MimeType,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result[v.Key] = gin.H{
|
||||
"type": "binary",
|
||||
"value": "/api/v1/app/" + appKey + "/variables/" + v.Key + "/download",
|
||||
"file_name": v.OriginalName,
|
||||
"file_size": v.FileSize,
|
||||
"md5": v.FileMD5,
|
||||
"mime_type": v.MimeType,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if v.Scope == "app" {
|
||||
result[v.Key] = gin.H{
|
||||
"type": v.VarType,
|
||||
"value": v.DefaultValue,
|
||||
}
|
||||
} else {
|
||||
value := v.DefaultValue
|
||||
if uv, ok := userVarMap[v.Key]; ok {
|
||||
value = uv.VarValue
|
||||
}
|
||||
result[v.Key] = gin.H{
|
||||
"type": v.VarType,
|
||||
"value": value,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, result)
|
||||
}
|
||||
|
||||
func handleAppGetVariableByKey(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
key := c.Param("key")
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var appUser model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", userID, app.ID).First(&appUser).Error; err != nil {
|
||||
response.Error(c, 403, "无权访问该应用的云端变量")
|
||||
return
|
||||
}
|
||||
|
||||
var variable model.CloudVariable
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND key = ? AND status = ?", app.UserID, app.ID, key, "active").First(&variable).Error; err != nil {
|
||||
response.Error(c, 404, "云端变量不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if variable.VarType == "binary" {
|
||||
if variable.Scope == "user" {
|
||||
var userVar model.UserVariable
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND var_name = ?", userID, app.ID, key).First(&userVar).Error; err == nil && userVar.FilePath != "" {
|
||||
response.Success(c, gin.H{
|
||||
"key": variable.Key,
|
||||
"type": "binary",
|
||||
"value": "/api/v1/app/" + appKey + "/variables/" + variable.Key + "/download",
|
||||
"file_name": userVar.OriginalName,
|
||||
"file_size": userVar.FileSize,
|
||||
"md5": userVar.FileMD5,
|
||||
"mime_type": userVar.MimeType,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
response.Success(c, gin.H{
|
||||
"key": variable.Key,
|
||||
"type": "binary",
|
||||
"value": "/api/v1/app/" + appKey + "/variables/" + variable.Key + "/download",
|
||||
"file_name": variable.OriginalName,
|
||||
"file_size": variable.FileSize,
|
||||
"md5": variable.FileMD5,
|
||||
"mime_type": variable.MimeType,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if variable.Scope == "app" {
|
||||
response.Success(c, gin.H{
|
||||
"key": variable.Key,
|
||||
"type": variable.VarType,
|
||||
"value": variable.DefaultValue,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var userVar model.UserVariable
|
||||
value := variable.DefaultValue
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND var_name = ?", userID, app.ID, key).First(&userVar).Error; err == nil {
|
||||
value = userVar.VarValue
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"key": variable.Key,
|
||||
"type": variable.VarType,
|
||||
"value": value,
|
||||
})
|
||||
}
|
||||
|
||||
func handleAppDownloadVariable(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
key := c.Param("key")
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var appUser model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", userID, app.ID).First(&appUser).Error; err != nil {
|
||||
response.Error(c, 403, "无权访问该应用的云端变量")
|
||||
return
|
||||
}
|
||||
|
||||
var variable model.CloudVariable
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND key = ? AND status = ?", app.UserID, app.ID, key, "active").First(&variable).Error; err != nil {
|
||||
response.Error(c, 404, "云端变量不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if variable.VarType != "binary" {
|
||||
response.Error(c, 400, "该变量不是二进制类型")
|
||||
return
|
||||
}
|
||||
|
||||
var filePath string
|
||||
var originalName string
|
||||
|
||||
if variable.Scope == "user" {
|
||||
var userVar model.UserVariable
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND var_name = ?", userID, app.ID, key).First(&userVar).Error; err == nil && userVar.FilePath != "" {
|
||||
filePath = userVar.FilePath
|
||||
originalName = userVar.OriginalName
|
||||
} else {
|
||||
filePath = variable.FilePath
|
||||
originalName = variable.OriginalName
|
||||
}
|
||||
} else {
|
||||
filePath = variable.FilePath
|
||||
originalName = variable.OriginalName
|
||||
}
|
||||
|
||||
if filePath == "" {
|
||||
response.Error(c, 400, "该变量没有关联文件")
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(filePath, "/") {
|
||||
filePath = filePath[1:]
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
response.Error(c, 404, "文件不存在")
|
||||
return
|
||||
}
|
||||
|
||||
encodedFilename := url.QueryEscape(originalName)
|
||||
c.Header("Content-Description", "File Transfer")
|
||||
c.Header("Content-Type", "application/octet-stream")
|
||||
c.Header("Content-Disposition", "attachment; filename*=UTF-8''"+encodedFilename)
|
||||
c.Header("Content-Transfer-Encoding", "binary")
|
||||
c.Header("Expires", "0")
|
||||
c.Header("Cache-Control", "must-revalidate")
|
||||
c.Header("Pragma", "public")
|
||||
c.FileAttachment(filePath, originalName)
|
||||
}
|
||||
|
||||
func handleAppUpdateVariables(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var appUser model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", userID, app.ID).First(&appUser).Error; err != nil {
|
||||
response.Error(c, 403, "无权访问该应用的云端变量")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Variables map[string]string `json:"variables"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var variables []model.CloudVariable
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ?", app.UserID, app.ID).Find(&variables).Error; err != nil {
|
||||
response.Error(c, 500, "获取云端变量失败")
|
||||
return
|
||||
}
|
||||
|
||||
varMap := make(map[string]model.CloudVariable)
|
||||
for _, v := range variables {
|
||||
varMap[v.Key] = v
|
||||
}
|
||||
|
||||
for key, value := range req.Variables {
|
||||
variable, exists := varMap[key]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
if variable.Scope == "app" {
|
||||
variable.DefaultValue = value
|
||||
database.DB.Save(&variable)
|
||||
} else {
|
||||
var userVar model.UserVariable
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND var_name = ?", userID, app.ID, key).First(&userVar).Error; err != nil {
|
||||
userVar = model.UserVariable{
|
||||
UserID: userID.(uint),
|
||||
AppID: app.ID,
|
||||
VarName: key,
|
||||
VarValue: value,
|
||||
VarType: variable.VarType,
|
||||
}
|
||||
database.DB.Create(&userVar)
|
||||
} else {
|
||||
userVar.VarValue = value
|
||||
database.DB.Save(&userVar)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "更新成功",
|
||||
})
|
||||
}
|
||||
|
||||
func handleAppUploadVariableBinary(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
key := c.Param("key")
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var appUser model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", userID, app.ID).First(&appUser).Error; err != nil {
|
||||
response.Error(c, 403, "无权访问该应用的云端变量")
|
||||
return
|
||||
}
|
||||
|
||||
var variable model.CloudVariable
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND key = ? AND status = ?", app.UserID, app.ID, key, "active").First(&variable).Error; err != nil {
|
||||
response.Error(c, 404, "云端变量不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if variable.VarType != "binary" {
|
||||
response.Error(c, 400, "该变量不是二进制类型")
|
||||
return
|
||||
}
|
||||
|
||||
if variable.WritePermission != "user" {
|
||||
response.Error(c, 403, "该变量不允许用户写入")
|
||||
return
|
||||
}
|
||||
|
||||
file, header, err := c.Request.FormFile("file")
|
||||
if err != nil {
|
||||
response.Error(c, 400, "请选择要上传的文件")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var developer model.User
|
||||
if err := database.DB.First(&developer, app.UserID).Error; err != nil {
|
||||
response.Error(c, 500, "获取开发者信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
if developer.CurrentPackageID != nil {
|
||||
var permission model.PackagePermission
|
||||
if err := database.DB.Where("package_id = ?", developer.CurrentPackageID).First(&permission).Error; err == nil {
|
||||
maxStorageBytes := int64(permission.MaxStorage) * 1024 * 1024
|
||||
if developer.StorageUsed+header.Size > maxStorageBytes {
|
||||
usedMB := float64(developer.StorageUsed) / 1024 / 1024
|
||||
maxMB := float64(permission.MaxStorage)
|
||||
response.Error(c, 403, fmt.Sprintf("存储空间不足,已使用 %.2f MB / %.2f MB", usedMB, maxMB))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uploadDir := "uploads/cloud-files"
|
||||
if err := os.MkdirAll(uploadDir, 0755); err != nil {
|
||||
response.Error(c, 500, "创建上传目录失败")
|
||||
return
|
||||
}
|
||||
|
||||
ext := filepath.Ext(header.Filename)
|
||||
filename := fmt.Sprintf("%d_%d_%d%s", app.UserID, userID, time.Now().UnixNano(), ext)
|
||||
filePath := filepath.Join(uploadDir, filename)
|
||||
|
||||
dst, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
response.Error(c, 500, "创建文件失败")
|
||||
return
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
hash := md5.New()
|
||||
multiWriter := io.MultiWriter(dst, hash)
|
||||
if _, err := io.Copy(multiWriter, file); err != nil {
|
||||
response.Error(c, 500, "保存文件失败")
|
||||
return
|
||||
}
|
||||
fileMD5 := hex.EncodeToString(hash.Sum(nil))
|
||||
|
||||
fileURL := "/uploads/cloud-files/" + filename
|
||||
|
||||
mimeType := header.Header.Get("Content-Type")
|
||||
if mimeType == "" {
|
||||
mimeType = "application/octet-stream"
|
||||
}
|
||||
|
||||
if variable.Scope == "app" {
|
||||
if variable.FilePath != "" && variable.FileSize > 0 {
|
||||
oldFilePath := variable.FilePath
|
||||
if strings.HasPrefix(oldFilePath, "/") {
|
||||
oldFilePath = oldFilePath[1:]
|
||||
}
|
||||
os.Remove(oldFilePath)
|
||||
if err := middleware.UpdateStorageUsed(app.UserID, variable.FileSize, "delete"); err != nil {
|
||||
fmt.Printf("更新存储使用量失败: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
variable.DefaultValue = fileURL
|
||||
variable.FilePath = fileURL
|
||||
variable.FileSize = header.Size
|
||||
variable.MimeType = mimeType
|
||||
variable.OriginalName = header.Filename
|
||||
variable.FileMD5 = fileMD5
|
||||
database.DB.Save(&variable)
|
||||
} else {
|
||||
var userVar model.UserVariable
|
||||
existingFile := false
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND var_name = ?", userID, app.ID, key).First(&userVar).Error; err == nil {
|
||||
if userVar.FilePath != "" && userVar.FileSize > 0 {
|
||||
existingFile = true
|
||||
}
|
||||
}
|
||||
|
||||
userVar = model.UserVariable{
|
||||
UserID: userID.(uint),
|
||||
AppID: app.ID,
|
||||
VarName: key,
|
||||
VarValue: fileURL,
|
||||
VarType: "binary",
|
||||
FilePath: fileURL,
|
||||
FileSize: header.Size,
|
||||
MimeType: mimeType,
|
||||
OriginalName: header.Filename,
|
||||
FileMD5: fileMD5,
|
||||
}
|
||||
|
||||
if existingFile {
|
||||
var oldVar model.UserVariable
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND var_name = ?", userID, app.ID, key).First(&oldVar).Error; err == nil {
|
||||
if oldVar.FilePath != "" && oldVar.FileSize > 0 {
|
||||
oldFilePath := oldVar.FilePath
|
||||
if strings.HasPrefix(oldFilePath, "/") {
|
||||
oldFilePath = oldFilePath[1:]
|
||||
}
|
||||
os.Remove(oldFilePath)
|
||||
if err := middleware.UpdateStorageUsed(app.UserID, oldVar.FileSize, "delete"); err != nil {
|
||||
fmt.Printf("更新存储使用量失败: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
database.DB.Where("user_id = ? AND app_id = ? AND var_name = ?", userID, app.ID, key).Assign(userVar).FirstOrCreate(&userVar)
|
||||
}
|
||||
|
||||
if err := middleware.UpdateStorageUsed(app.UserID, header.Size, "upload"); err != nil {
|
||||
fmt.Printf("更新存储使用量失败: %v\n", err)
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "上传成功",
|
||||
"file_url": fileURL,
|
||||
"file_size": header.Size,
|
||||
"mime_type": mimeType,
|
||||
"original_name": header.Filename,
|
||||
"download_url": "/api/v1/app/" + appKey + "/variables/" + key + "/download",
|
||||
})
|
||||
}
|
||||
|
||||
func handleAppCallFunction(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Name string `json:"name"`
|
||||
Params map[string]interface{} `json:"params"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"result": nil,
|
||||
})
|
||||
}
|
||||
|
||||
func handleAppCreateVariableRecord(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
key := c.Param("key")
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var appUser model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", userID, app.ID).First(&appUser).Error; err != nil {
|
||||
response.Error(c, 403, "无权访问该应用的云端变量")
|
||||
return
|
||||
}
|
||||
|
||||
var variable model.CloudVariable
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND key = ? AND status = ?", app.UserID, app.ID, key, "active").First(&variable).Error; err != nil {
|
||||
response.Error(c, 404, "云端变量不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if variable.DataType != "stream" {
|
||||
response.Error(c, 400, "该变量不是流水类型")
|
||||
return
|
||||
}
|
||||
|
||||
if variable.WritePermission != "user" && variable.WritePermission != "app_user" {
|
||||
response.Error(c, 403, "该变量不允许应用用户写入")
|
||||
return
|
||||
}
|
||||
|
||||
var req map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
dataBytes, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
response.Error(c, 500, "序列化数据失败")
|
||||
return
|
||||
}
|
||||
|
||||
record := model.CloudVariableRecord{
|
||||
CloudVariableID: variable.ID,
|
||||
Data: string(dataBytes),
|
||||
}
|
||||
|
||||
if variable.Scope == "user" {
|
||||
appUserID := userID.(uint)
|
||||
record.AppUserID = &appUserID
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&record).Error; err != nil {
|
||||
response.Error(c, 500, "创建记录失败")
|
||||
return
|
||||
}
|
||||
|
||||
if variable.MaxRecords > 0 {
|
||||
var total int64
|
||||
database.DB.Model(&model.CloudVariableRecord{}).Where("cloud_variable_id = ?", variable.ID).Count(&total)
|
||||
if int(total) > variable.MaxRecords {
|
||||
deleteCount := int(total) - variable.MaxRecords
|
||||
var oldRecords []model.CloudVariableRecord
|
||||
database.DB.Where("cloud_variable_id = ?", variable.ID).
|
||||
Order("created_at ASC").
|
||||
Limit(deleteCount).
|
||||
Find(&oldRecords)
|
||||
for _, r := range oldRecords {
|
||||
database.DB.Delete(&r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"id": record.ID,
|
||||
"created_at": record.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
func handleAppGetVariableRecords(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
key := c.Param("key")
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var appUser model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", userID, app.ID).First(&appUser).Error; err != nil {
|
||||
response.Error(c, 403, "无权访问该应用的云端变量")
|
||||
return
|
||||
}
|
||||
|
||||
var variable model.CloudVariable
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND key = ? AND status = ?", app.UserID, app.ID, key, "active").First(&variable).Error; err != nil {
|
||||
response.Error(c, 404, "云端变量不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if variable.DataType != "stream" {
|
||||
response.Error(c, 400, "该变量不是流水类型")
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
var total int64
|
||||
query := database.DB.Model(&model.CloudVariableRecord{}).Where("cloud_variable_id = ?", variable.ID)
|
||||
if variable.Scope == "user" {
|
||||
query = query.Where("app_user_id = ?", userID)
|
||||
}
|
||||
query.Count(&total)
|
||||
|
||||
var records []model.CloudVariableRecord
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Order("created_at DESC").Limit(pageSize).Offset(offset).Find(&records).Error; err != nil {
|
||||
response.Error(c, 500, "获取记录失败")
|
||||
return
|
||||
}
|
||||
|
||||
result := make([]gin.H, len(records))
|
||||
for i, r := range records {
|
||||
var data map[string]interface{}
|
||||
json.Unmarshal([]byte(r.Data), &data)
|
||||
result[i] = gin.H{
|
||||
"id": r.ID,
|
||||
"data": data,
|
||||
"created_at": r.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"records": result,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
"total_pages": (total + int64(pageSize) - 1) / int64(pageSize),
|
||||
})
|
||||
}
|
||||
|
||||
func handleAppDeleteVariableRecord(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
key := c.Param("key")
|
||||
recordID := c.Param("record_id")
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var appUser model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", userID, app.ID).First(&appUser).Error; err != nil {
|
||||
response.Error(c, 403, "无权访问该应用的云端变量")
|
||||
return
|
||||
}
|
||||
|
||||
var variable model.CloudVariable
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND key = ? AND status = ?", app.UserID, app.ID, key, "active").First(&variable).Error; err != nil {
|
||||
response.Error(c, 404, "云端变量不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if variable.DataType != "stream" {
|
||||
response.Error(c, 400, "该变量不是流水类型")
|
||||
return
|
||||
}
|
||||
|
||||
var record model.CloudVariableRecord
|
||||
query := database.DB.Where("id = ? AND cloud_variable_id = ?", recordID, variable.ID)
|
||||
if variable.Scope == "user" {
|
||||
query = query.Where("app_user_id = ?", userID)
|
||||
}
|
||||
if err := query.First(&record).Error; err != nil {
|
||||
response.Error(c, 404, "记录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DB.Delete(&record).Error; err != nil {
|
||||
response.Error(c, 500, "删除记录失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, nil)
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/service"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupDeviceRoutes(r *gin.RouterGroup) {
|
||||
r.GET("/devices", handleAppGetDevices)
|
||||
r.POST("/unbind-device", handleAppUnbindDevice)
|
||||
r.GET("/device-count", handleAppGetDeviceCount)
|
||||
r.GET("/instances", handleAppGetInstances)
|
||||
r.POST("/instances/:instance_id/offline", handleAppForceOfflineInstance)
|
||||
}
|
||||
|
||||
func SetupDevicePublicRoutes(r *gin.RouterGroup) {
|
||||
r.POST("/unbind-device-with-auth", handleAppUnbindDeviceWithAuth)
|
||||
r.POST("/change-password", handleAppChangePassword)
|
||||
}
|
||||
|
||||
func handleAppUnbindDeviceWithAuth(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
DeviceID string `json:"device_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Username == "" || req.Password == "" || req.DeviceID == "" {
|
||||
response.Error(c, 400, "用户名、密码和设备ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.Where("username = ? AND application_id = ?", req.Username, app.ID).First(&user).Error; err != nil {
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Password != req.Password {
|
||||
response.Error(c, 401, "密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
var device model.UserDevice
|
||||
if err := database.DB.Where("user_id = ? AND application_id = ? AND device_id = ?", user.ID, app.ID, req.DeviceID).First(&device).Error; err != nil {
|
||||
response.Error(c, 404, "设备不存在")
|
||||
return
|
||||
}
|
||||
|
||||
database.DB.Where("device_id = ?", device.ID).Delete(&model.DeviceSession{})
|
||||
|
||||
if err := database.DB.Delete(&device).Error; err != nil {
|
||||
response.Error(c, 500, "解绑设备失败")
|
||||
return
|
||||
}
|
||||
|
||||
if user.DeviceID == req.DeviceID {
|
||||
now := time.Now()
|
||||
database.DB.Model(&user).Updates(map[string]interface{}{
|
||||
"device_id": "",
|
||||
"last_heartbeat_at": &now,
|
||||
})
|
||||
}
|
||||
|
||||
service.LogVerification(c, &app.ID, &user.ID, "unbind_device", fmt.Sprintf("用户解绑设备(认证): %s", req.DeviceID), req.DeviceID, nil)
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "解绑成功",
|
||||
})
|
||||
}
|
||||
|
||||
func handleAppGetDevices(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
response.Error(c, 401, "未授权")
|
||||
return
|
||||
}
|
||||
|
||||
var devices []model.UserDevice
|
||||
if err := database.DB.Where("user_id = ? AND application_id = ?", userID, app.ID).Find(&devices).Error; err != nil {
|
||||
response.Error(c, 500, "获取设备列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
result := make([]gin.H, 0)
|
||||
for _, device := range devices {
|
||||
var onlineSessionCount int64
|
||||
tenMinutesAgo := time.Now().Add(-10 * time.Minute)
|
||||
database.DB.Model(&model.DeviceSession{}).
|
||||
Where("device_id = ? AND last_heartbeat > ?", device.ID, tenMinutesAgo).
|
||||
Count(&onlineSessionCount)
|
||||
|
||||
result = append(result, gin.H{
|
||||
"id": device.ID,
|
||||
"device_id": device.DeviceID,
|
||||
"device_name": device.DeviceName,
|
||||
"device_type": device.DeviceType,
|
||||
"status": device.Status,
|
||||
"online_sessions": onlineSessionCount,
|
||||
"created_at": device.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
response.Success(c, result)
|
||||
}
|
||||
|
||||
func handleAppGetDeviceCount(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
UserID uint `json:"user_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var count int64
|
||||
if err := database.DB.Model(&model.UserDevice{}).Where("user_id = ? AND application_id = ?", req.UserID, app.ID).Count(&count).Error; err != nil {
|
||||
response.Error(c, 500, "获取设备数量失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"count": count,
|
||||
"max_devices": app.MaxDevices,
|
||||
"remaining": app.MaxDevices - int(count),
|
||||
})
|
||||
}
|
||||
|
||||
func handleAppUnbindDevice(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
UserID uint `json:"user_id"`
|
||||
DeviceID string `json:"device_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var device model.UserDevice
|
||||
if err := database.DB.Where("user_id = ? AND application_id = ? AND device_id = ?", req.UserID, app.ID, req.DeviceID).First(&device).Error; err != nil {
|
||||
response.Error(c, 404, "设备不存在")
|
||||
return
|
||||
}
|
||||
|
||||
database.DB.Where("device_id = ?", device.ID).Delete(&model.DeviceSession{})
|
||||
|
||||
if err := database.DB.Delete(&device).Error; err != nil {
|
||||
response.Error(c, 500, "解绑设备失败")
|
||||
return
|
||||
}
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.First(&user, req.UserID).Error; err == nil {
|
||||
if user.DeviceID == req.DeviceID {
|
||||
now := time.Now()
|
||||
database.DB.Model(&user).Updates(map[string]interface{}{
|
||||
"device_id": "",
|
||||
"last_heartbeat_at": &now,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
service.LogVerification(c, &app.ID, &user.ID, "unbind_device", fmt.Sprintf("用户解绑设备: %s", req.DeviceID), req.DeviceID, nil)
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "解绑成功",
|
||||
})
|
||||
}
|
||||
|
||||
func handleAppGetInstances(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
UserID uint `json:"user_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var devices []model.UserDevice
|
||||
if err := database.DB.Where("user_id = ? AND application_id = ?", req.UserID, app.ID).Find(&devices).Error; err != nil {
|
||||
response.Error(c, 500, "获取设备列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
deviceIDs := make([]uint, len(devices))
|
||||
for i, d := range devices {
|
||||
deviceIDs[i] = d.ID
|
||||
}
|
||||
|
||||
heartbeatTimeout := app.HeartbeatTimeout
|
||||
if heartbeatTimeout <= 0 {
|
||||
heartbeatTimeout = 10
|
||||
}
|
||||
timeoutThreshold := time.Now().Add(-time.Duration(heartbeatTimeout) * time.Minute)
|
||||
|
||||
var sessions []model.DeviceSession
|
||||
if len(deviceIDs) > 0 {
|
||||
if err := database.DB.Where("device_id IN ?", deviceIDs).Order("last_heartbeat DESC").Find(&sessions).Error; err != nil {
|
||||
response.Error(c, 500, "获取实例列表失败")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]gin.H, 0)
|
||||
for _, session := range sessions {
|
||||
isOnline := session.LastHeartbeat != nil && session.LastHeartbeat.After(timeoutThreshold)
|
||||
|
||||
var device model.UserDevice
|
||||
for _, d := range devices {
|
||||
if d.ID == session.DeviceID {
|
||||
device = d
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, gin.H{
|
||||
"id": session.ID,
|
||||
"instance_id": session.InstanceID,
|
||||
"device_id": device.DeviceID,
|
||||
"device_name": device.DeviceName,
|
||||
"is_online": isOnline,
|
||||
"last_heartbeat": session.LastHeartbeat,
|
||||
"created_at": session.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
response.Success(c, result)
|
||||
}
|
||||
|
||||
func handleAppForceOfflineInstance(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
instanceID := c.Param("instance_id")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
UserID uint `json:"user_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var devices []model.UserDevice
|
||||
if err := database.DB.Where("user_id = ? AND application_id = ?", req.UserID, app.ID).Find(&devices).Error; err != nil {
|
||||
response.Error(c, 500, "获取设备列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
deviceIDs := make([]uint, len(devices))
|
||||
for i, d := range devices {
|
||||
deviceIDs[i] = d.ID
|
||||
}
|
||||
|
||||
if len(deviceIDs) == 0 {
|
||||
response.Error(c, 404, "实例不存在")
|
||||
return
|
||||
}
|
||||
|
||||
result := database.DB.Where("instance_id = ? AND device_id IN ?", instanceID, deviceIDs).Delete(&model.DeviceSession{})
|
||||
if result.RowsAffected == 0 {
|
||||
response.Error(c, 404, "实例不存在")
|
||||
return
|
||||
}
|
||||
|
||||
service.LogVerification(c, &app.ID, &req.UserID, "force_offline", fmt.Sprintf("强制离线实例: %s", instanceID), instanceID, nil)
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "已强制离线",
|
||||
})
|
||||
}
|
||||
|
||||
func handleAppChangePassword(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
OldPassword string `json:"old_password" binding:"required"`
|
||||
NewPassword string `json:"new_password" binding:"required,min=6"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.Where("username = ? AND application_id = ?", req.Username, app.ID).First(&user).Error; err != nil {
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Password != req.OldPassword {
|
||||
response.Error(c, 400, "原密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
user.Password = req.NewPassword
|
||||
if err := database.DB.Save(&user).Error; err != nil {
|
||||
response.Error(c, 500, "密码修改失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "密码修改成功",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/service"
|
||||
"verification-platform-backend/pkg/jwt"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupDynamicRoutes(r *gin.RouterGroup) {
|
||||
dynamicCode := r.Group("/dynamic-code")
|
||||
{
|
||||
dynamicCode.POST("/:key/execute", handleExecuteDynamicCode)
|
||||
}
|
||||
}
|
||||
|
||||
func handleExecuteDynamicCode(c *gin.Context) {
|
||||
var token string
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader != "" {
|
||||
parts := strings.SplitN(authHeader, " ", 2)
|
||||
if len(parts) == 2 && parts[0] == "Bearer" {
|
||||
token = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
token = c.Query("token")
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
response.Error(c, http.StatusUnauthorized, "Authorization header is required")
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := jwt.ParseToken(token)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusUnauthorized, "Invalid token")
|
||||
return
|
||||
}
|
||||
|
||||
if time.Now().Unix() > claims.ExpiresAt.Unix() {
|
||||
response.Error(c, http.StatusUnauthorized, "Token expired")
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("role", claims.Role)
|
||||
|
||||
appKey := c.Param("appKey")
|
||||
key := c.Param("key")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var appUser model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", claims.UserID, app.ID).First(&appUser).Error; err != nil {
|
||||
response.Error(c, http.StatusForbidden, "无权访问该应用的动态代码")
|
||||
return
|
||||
}
|
||||
|
||||
var dynamicCode model.DynamicCode
|
||||
if err := database.DB.Where("application_id = ? AND key = ?", app.ID, key).First(&dynamicCode).Error; err != nil {
|
||||
response.Error(c, 404, "动态代码不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if dynamicCode.Status != "active" {
|
||||
response.Error(c, 400, "动态代码未启用")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Params map[string]interface{} `json:"params"`
|
||||
UserID *uint `json:"user_id,omitempty"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
vm := goja.New()
|
||||
|
||||
appData := map[string]interface{}{
|
||||
"id": app.ID,
|
||||
"name": app.Name,
|
||||
"description": app.Description,
|
||||
"app_key": app.AppKey,
|
||||
"status": app.Status,
|
||||
"billing_type": app.BillingType,
|
||||
"deduction_mode": app.DeductionMode,
|
||||
"deduction_type": app.DeductionType,
|
||||
"deduction_interval": app.DeductionInterval,
|
||||
"deduction_unit": app.DeductionUnit,
|
||||
"deduction_amount": app.DeductionAmount,
|
||||
"enable_trial": app.EnableTrial,
|
||||
"trial_balance": app.TrialBalance,
|
||||
"trial_days": app.TrialDays,
|
||||
"enable_free_period": app.EnableFreePeriod,
|
||||
"free_period_type": app.FreePeriodType,
|
||||
"free_period_start": app.FreePeriodStart,
|
||||
"free_period_end": app.FreePeriodEnd,
|
||||
"free_period_weekdays": app.FreePeriodWeekdays,
|
||||
"free_period_start_time": app.FreePeriodStartTime,
|
||||
"free_period_end_time": app.FreePeriodEndTime,
|
||||
"max_devices": app.MaxDevices,
|
||||
"bind_type": app.BindType,
|
||||
"multi_open": app.MultiOpen,
|
||||
"multi_open_mode": app.MultiOpenMode,
|
||||
"max_instances": app.MaxInstances,
|
||||
"login_policy": app.LoginPolicy,
|
||||
"max_attempts": app.MaxAttempts,
|
||||
"lock_duration": app.LockDuration,
|
||||
"heartbeat_interval": app.HeartbeatInterval,
|
||||
"heartbeat_timeout": app.HeartbeatTimeout,
|
||||
"change_limit": app.ChangeLimit,
|
||||
"change_interval": app.ChangeInterval,
|
||||
"change_exceed_action": app.ChangeExceedAction,
|
||||
"change_deduct_amount": app.ChangeDeductAmount,
|
||||
}
|
||||
if err := vm.Set("app", appData); err != nil {
|
||||
response.Error(c, 500, "应用数据设置失败")
|
||||
return
|
||||
}
|
||||
|
||||
var constants []model.CloudConstant
|
||||
database.DB.Where("app_id = ? AND status = ?", app.ID, "active").Find(&constants)
|
||||
constantsData := make(map[string]interface{})
|
||||
for _, c := range constants {
|
||||
constantsData[c.Key] = c.Value
|
||||
}
|
||||
if err := vm.Set("constants", constantsData); err != nil {
|
||||
response.Error(c, 500, "云端常量设置失败")
|
||||
return
|
||||
}
|
||||
|
||||
var cloudVariables []model.CloudVariable
|
||||
database.DB.Where("app_id = ? AND status = ?", app.ID, "active").Find(&cloudVariables)
|
||||
appVariables := make(map[string]interface{})
|
||||
for _, v := range cloudVariables {
|
||||
appVariables[v.Key] = v.DefaultValue
|
||||
}
|
||||
if err := vm.Set("appVariables", appVariables); err != nil {
|
||||
response.Error(c, 500, "云端变量设置失败")
|
||||
return
|
||||
}
|
||||
|
||||
userData := map[string]interface{}{}
|
||||
subscription := map[string]interface{}{}
|
||||
userVariables := map[string]interface{}{}
|
||||
devices := []map[string]interface{}{}
|
||||
|
||||
if req.UserID != nil {
|
||||
var user model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", *req.UserID, app.ID).First(&user).Error; err == nil {
|
||||
userData = map[string]interface{}{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"status": user.Status,
|
||||
"device_id": user.DeviceID,
|
||||
"avatar": user.Avatar,
|
||||
"created_at": user.CreatedAt,
|
||||
"last_login_at": user.LastLoginAt,
|
||||
}
|
||||
|
||||
subscription = map[string]interface{}{
|
||||
"balance": user.Balance,
|
||||
"is_trial_user": user.IsTrialUser,
|
||||
"trial_start_at": user.TrialStartAt,
|
||||
"trial_end_at": user.TrialEndAt,
|
||||
"expiry_at": user.ExpiryAt,
|
||||
"is_expired": user.ExpiryAt != nil && user.ExpiryAt.Before(time.Now()),
|
||||
"is_lifetime": user.Balance == -1,
|
||||
"days_remaining": calculateDaysRemaining(user.ExpiryAt, user.Balance),
|
||||
}
|
||||
|
||||
var userDevices []model.UserDevice
|
||||
database.DB.Where("user_id = ? AND application_id = ?", user.ID, app.ID).Find(&userDevices)
|
||||
for _, d := range userDevices {
|
||||
devices = append(devices, map[string]interface{}{
|
||||
"id": d.ID,
|
||||
"device_id": d.DeviceID,
|
||||
"device_name": d.DeviceName,
|
||||
"device_type": d.DeviceType,
|
||||
"status": d.Status,
|
||||
"created_at": d.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
var userVars []model.UserVariable
|
||||
database.DB.Where("user_id = ? AND app_id = ?", user.ID, app.ID).Find(&userVars)
|
||||
for _, v := range userVars {
|
||||
userVariables[v.VarName] = v.VarValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := vm.Set("user", userData); err != nil {
|
||||
response.Error(c, 500, "用户数据设置失败")
|
||||
return
|
||||
}
|
||||
if err := vm.Set("subscription", subscription); err != nil {
|
||||
response.Error(c, 500, "订阅数据设置失败")
|
||||
return
|
||||
}
|
||||
if err := vm.Set("userVariables", userVariables); err != nil {
|
||||
response.Error(c, 500, "用户变量设置失败")
|
||||
return
|
||||
}
|
||||
if err := vm.Set("devices", devices); err != nil {
|
||||
response.Error(c, 500, "设备数据设置失败")
|
||||
return
|
||||
}
|
||||
|
||||
for k, v := range req.Params {
|
||||
if err := vm.Set(k, v); err != nil {
|
||||
response.Error(c, 500, "参数设置失败")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := vm.Set("params", req.Params); err != nil {
|
||||
response.Error(c, 500, "参数设置失败")
|
||||
return
|
||||
}
|
||||
|
||||
value, err := vm.RunString("(function() { " + dynamicCode.Code + " })()")
|
||||
if err != nil {
|
||||
response.Error(c, 400, "代码执行错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
executionTime := time.Since(startTime).Milliseconds()
|
||||
|
||||
result := value.Export()
|
||||
|
||||
if actionMap, ok := result.(map[string]interface{}); ok {
|
||||
if action, hasAction := actionMap["action"]; hasAction {
|
||||
switch action {
|
||||
case "extend_time":
|
||||
if err := handleExtendTime(app.ID, actionMap); err != nil {
|
||||
response.Error(c, 500, "执行加时操作失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
case "deduct_points":
|
||||
if err := handleDeductPoints(app.ID, actionMap); err != nil {
|
||||
response.Error(c, 500, "执行扣点操作失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
case "update_user_variable":
|
||||
appID := app.ID
|
||||
if err := handleUpdateUserVariable(&appID, req.UserID, actionMap); err != nil {
|
||||
response.Error(c, 500, "更新用户变量失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
case "update_app_variable":
|
||||
if err := handleUpdateAppVariable(app.ID, actionMap); err != nil {
|
||||
response.Error(c, 500, "更新应用变量失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"result": result,
|
||||
"execution_time": executionTime,
|
||||
})
|
||||
}
|
||||
|
||||
func calculateDaysRemaining(expiryAt *time.Time, balance float64) int {
|
||||
if balance == -1 {
|
||||
return -1
|
||||
}
|
||||
if expiryAt == nil {
|
||||
return 0
|
||||
}
|
||||
remaining := int(time.Until(*expiryAt).Hours() / 24)
|
||||
if remaining < 0 {
|
||||
return 0
|
||||
}
|
||||
return remaining
|
||||
}
|
||||
|
||||
func handleExtendTime(appID uint, actionMap map[string]interface{}) error {
|
||||
userID, ok := actionMap["user_id"].(float64)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
days, ok := actionMap["days"].(float64)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", uint(userID), appID).First(&user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if user.Balance == -1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
user.Balance += float64(days)
|
||||
|
||||
return database.DB.Save(&user).Error
|
||||
}
|
||||
|
||||
func handleDeductPoints(appID uint, actionMap map[string]interface{}) error {
|
||||
userID, ok := actionMap["user_id"].(float64)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
points, ok := actionMap["points"].(float64)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", uint(userID), appID).First(&user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if user.Balance == -1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
user.Balance -= points
|
||||
if user.Balance < 0 {
|
||||
user.Balance = 0
|
||||
}
|
||||
|
||||
return database.DB.Save(&user).Error
|
||||
}
|
||||
|
||||
func handleUpdateUserVariable(appID *uint, userID *uint, actionMap map[string]interface{}) error {
|
||||
if userID == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
varName, ok := actionMap["name"].(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
varValue, ok := actionMap["value"].(string)
|
||||
if !ok {
|
||||
if v, ok := actionMap["value"]; ok {
|
||||
varValue = toString(v)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var userVar model.UserVariable
|
||||
if err := database.DB.Where("user_id = ? AND app_id = ? AND var_name = ?", *userID, appID, varName).First(&userVar).Error; err != nil {
|
||||
userVar = model.UserVariable{
|
||||
UserID: *userID,
|
||||
AppID: *appID,
|
||||
VarName: varName,
|
||||
VarValue: varValue,
|
||||
}
|
||||
return database.DB.Create(&userVar).Error
|
||||
}
|
||||
|
||||
userVar.VarValue = varValue
|
||||
return database.DB.Save(&userVar).Error
|
||||
}
|
||||
|
||||
func handleUpdateAppVariable(appID uint, actionMap map[string]interface{}) error {
|
||||
varName, ok := actionMap["name"].(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
varValue, ok := actionMap["value"].(string)
|
||||
if !ok {
|
||||
if v, ok := actionMap["value"]; ok {
|
||||
varValue = toString(v)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var appVar model.CloudVariable
|
||||
if err := database.DB.Where("app_id = ? AND key = ?", appID, varName).First(&appVar).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
appVar.DefaultValue = varValue
|
||||
return database.DB.Save(&appVar).Error
|
||||
}
|
||||
|
||||
func toString(v interface{}) string {
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
return val
|
||||
case float64:
|
||||
return string(rune(int(val)))
|
||||
case int:
|
||||
return string(rune(val))
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/service"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupInfoRoutes(r *gin.RouterGroup) {
|
||||
r.GET("/info", handleAppGetInfo)
|
||||
r.GET("/check-update", handleAppCheckUpdate)
|
||||
r.GET("/announcements", handleAppGetAnnouncements)
|
||||
}
|
||||
|
||||
func handleAppGetInfo(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"id": app.ID,
|
||||
"name": app.Name,
|
||||
"description": app.Description,
|
||||
"icon_url": app.IconURL,
|
||||
"status": app.Status,
|
||||
"billing_type": app.BillingType,
|
||||
"login_policy": app.LoginPolicy,
|
||||
"max_devices": app.MaxDevices,
|
||||
"multi_open": app.MultiOpen,
|
||||
"multi_open_mode": app.MultiOpenMode,
|
||||
"max_instances": app.MaxInstances,
|
||||
"enable_trial": app.EnableTrial,
|
||||
"trial_balance": app.TrialBalance,
|
||||
"heartbeat_interval": app.HeartbeatInterval,
|
||||
"heartbeat_timeout": app.HeartbeatTimeout,
|
||||
})
|
||||
}
|
||||
|
||||
func handleAppCheckUpdate(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
clientVersion := c.Query("version")
|
||||
|
||||
var latestVersion model.Version
|
||||
err := database.DB.Where("application_id = ? AND status = ?", app.ID, "active").Order("created_at DESC").Preload("Files").First(&latestVersion).Error
|
||||
|
||||
if err != nil {
|
||||
response.Success(c, gin.H{
|
||||
"has_update": false,
|
||||
"latest_version": "",
|
||||
"download_url": "",
|
||||
"update_notes": "",
|
||||
"update_strategy": "",
|
||||
"update_method": "",
|
||||
"files": []interface{}{},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
hasUpdate := clientVersion != latestVersion.Version
|
||||
if latestVersion.ForceUpdate {
|
||||
hasUpdate = true
|
||||
}
|
||||
|
||||
files := make([]gin.H, len(latestVersion.Files))
|
||||
for i, f := range latestVersion.Files {
|
||||
files[i] = gin.H{
|
||||
"file_path": f.FilePath,
|
||||
"file_name": f.FileName,
|
||||
"file_size": f.FileSize,
|
||||
"file_hash": f.FileHash,
|
||||
"file_type": f.FileType,
|
||||
"is_required": f.IsRequired,
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"has_update": hasUpdate,
|
||||
"latest_version": latestVersion.Version,
|
||||
"download_url": latestVersion.FilePath,
|
||||
"file_size": latestVersion.FileSize,
|
||||
"file_hash": latestVersion.FileHash,
|
||||
"entry_file": latestVersion.EntryFile,
|
||||
"update_notes": latestVersion.Description,
|
||||
"update_strategy": latestVersion.UpdateStrategy,
|
||||
"update_method": latestVersion.UpdateMethod,
|
||||
"min_version": latestVersion.MinVersion,
|
||||
"changelog": latestVersion.Changelog,
|
||||
"files": files,
|
||||
})
|
||||
}
|
||||
|
||||
func handleAppGetAnnouncements(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var announcements []model.Announcement
|
||||
if err := database.DB.Where("application_id = ? AND status = ?", app.ID, "active").Order("is_top DESC, created_at DESC").Find(&announcements).Error; err != nil {
|
||||
response.Error(c, 500, "获取公告失败")
|
||||
return
|
||||
}
|
||||
|
||||
result := make([]gin.H, len(announcements))
|
||||
for i, a := range announcements {
|
||||
result[i] = gin.H{
|
||||
"id": a.ID,
|
||||
"title": a.Title,
|
||||
"content": a.Content,
|
||||
"type": a.Type,
|
||||
"is_top": a.IsTop,
|
||||
"created_at": a.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, result)
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/service"
|
||||
"verification-platform-backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupPaymentRoutes(r *gin.RouterGroup) {
|
||||
r.POST("/recharge", handleAppRecharge)
|
||||
r.POST("/trial", handleAppTrial)
|
||||
}
|
||||
|
||||
func handleAppRecharge(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Username string `json:"username"`
|
||||
CardKey string `json:"card_key"`
|
||||
DeviceID string `json:"device_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if blocked, reason := checkRiskControl(c, app.ID, req.DeviceID, req.Username); blocked {
|
||||
response.Error(c, 403, reason)
|
||||
return
|
||||
}
|
||||
|
||||
var card model.Card
|
||||
if err := database.DB.Preload("CardType").Where("card_key = ?", req.CardKey).First(&card).Error; err != nil {
|
||||
service.LogVerification(c, &app.ID, nil, "recharge_failed", fmt.Sprintf("充值失败: 卡密不存在 - %s", req.CardKey), "", fmt.Errorf("卡密不存在"))
|
||||
response.Error(c, 404, "卡密不存在")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("[DEBUG] 卡密信息: ID=%d, CardKey=%s, Status=%s, CardTypeID=%d\n", card.ID, card.CardKey, card.Status, card.CardTypeID)
|
||||
fmt.Printf("[DEBUG] 卡类信息: ID=%d, Name=%s, Value=%f, Price=%f\n", card.CardType.ID, card.CardType.Name, card.CardType.Value, card.CardType.Price)
|
||||
|
||||
if card.Status != "unused" {
|
||||
service.LogVerification(c, &app.ID, nil, "recharge_failed", fmt.Sprintf("充值失败: 卡密已使用 - %s", req.CardKey), "", fmt.Errorf("卡密已使用"))
|
||||
response.Error(c, 400, "卡密已使用")
|
||||
return
|
||||
}
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.Where("username = ? AND application_id = ?", req.Username, app.ID).First(&user).Error; err != nil {
|
||||
service.LogVerification(c, &app.ID, nil, "recharge_failed", fmt.Sprintf("充值失败: 用户不存在 - %s", req.Username), "", fmt.Errorf("用户不存在"))
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Balance == -1 {
|
||||
service.LogVerification(c, &app.ID, &user.ID, "recharge_failed", fmt.Sprintf("充值失败: 用户已是永久会员 - %s", user.Username), "", fmt.Errorf("该用户已是永久会员,无法再次充值"))
|
||||
response.Error(c, 400, "该用户已是永久会员,无法再次充值")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("[DEBUG] 充值前用户信息: ID=%d, Username=%s, Balance=%f, ExpiryAt=%v\n", user.ID, user.Username, user.Balance, user.ExpiryAt)
|
||||
|
||||
tx := database.DB.Begin()
|
||||
|
||||
card.Status = "used"
|
||||
card.AppUserID = &user.ID
|
||||
now := time.Now()
|
||||
card.UsedAt = &now
|
||||
|
||||
if err := tx.Save(&card).Error; err != nil {
|
||||
tx.Rollback()
|
||||
response.Error(c, 500, "充值失败")
|
||||
return
|
||||
}
|
||||
|
||||
user.IsTrialUser = false
|
||||
|
||||
if card.CardType.Value == -1 {
|
||||
user.Balance = -1
|
||||
user.ExpiryAt = nil
|
||||
} else {
|
||||
switch card.CardType.RechargeType {
|
||||
case "subscription":
|
||||
var baseTime time.Time
|
||||
if user.ExpiryAt != nil && user.ExpiryAt.After(now) {
|
||||
baseTime = *user.ExpiryAt
|
||||
} else {
|
||||
baseTime = now
|
||||
}
|
||||
var duration time.Duration
|
||||
switch card.CardType.ValueUnit {
|
||||
case "minute":
|
||||
duration = time.Duration(card.CardType.Value) * time.Minute
|
||||
case "hour":
|
||||
duration = time.Duration(card.CardType.Value) * time.Hour
|
||||
case "day":
|
||||
duration = time.Duration(card.CardType.Value) * 24 * time.Hour
|
||||
case "month":
|
||||
duration = time.Duration(card.CardType.Value) * 30 * 24 * time.Hour
|
||||
case "year":
|
||||
duration = time.Duration(card.CardType.Value) * 365 * 24 * time.Hour
|
||||
default:
|
||||
duration = time.Duration(card.CardType.Value) * time.Second
|
||||
}
|
||||
newExpiry := baseTime.Add(duration)
|
||||
user.ExpiryAt = &newExpiry
|
||||
case "balance":
|
||||
fallthrough
|
||||
default:
|
||||
user.Balance += card.CardType.Value
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("[DEBUG] 充值后用户信息: Balance=%f, ExpiryAt=%v (CardType.Value=%f, BillingType=%s)\n", user.Balance, user.ExpiryAt, card.CardType.Value, app.BillingType)
|
||||
|
||||
if err := tx.Save(&user).Error; err != nil {
|
||||
tx.Rollback()
|
||||
response.Error(c, 500, "更新用户信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
rechargeRecord := model.RechargeRecord{
|
||||
UserID: user.ID,
|
||||
OrderNo: generateOrderNo("R"),
|
||||
CardID: &card.ID,
|
||||
CardCode: card.CardKey,
|
||||
Amount: card.CardType.Price,
|
||||
Status: "success",
|
||||
PaymentType: "card",
|
||||
Remark: "卡密充值 - " + card.CardType.Name,
|
||||
}
|
||||
|
||||
if err := tx.Create(&rechargeRecord).Error; err != nil {
|
||||
tx.Rollback()
|
||||
response.Error(c, 500, "创建充值记录失败")
|
||||
return
|
||||
}
|
||||
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
response.Error(c, 500, "充值失败")
|
||||
return
|
||||
}
|
||||
|
||||
service.LogVerification(c, &app.ID, &user.ID, "recharge", fmt.Sprintf("用户充值: %s, 卡密: %s, 金额: %.2f", user.Username, card.CardKey, card.CardType.Price), "", nil)
|
||||
|
||||
response.SuccessWithMessage(c, "充值成功", gin.H{
|
||||
"message": "充值成功",
|
||||
"value": card.CardType.Value,
|
||||
})
|
||||
}
|
||||
|
||||
func handleAppTrial(c *gin.Context) {
|
||||
appKey := c.Param("appKey")
|
||||
var app model.Application
|
||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if service.GetApplicationDisabledStatus(app.ID) {
|
||||
response.Error(c, 403, "该应用已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
if !app.EnableTrial {
|
||||
response.Error(c, 400, "该应用不支持试用")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
UserID uint `json:"user_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.First(&user, req.UserID).Error; err != nil {
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
response.SuccessWithMessage(c, "试用成功", gin.H{
|
||||
"message": "试用成功",
|
||||
"trial_balance": app.TrialBalance,
|
||||
})
|
||||
}
|
||||
|
||||
func generateOrderNo(prefix string) string {
|
||||
return prefix + time.Now().Format("20060102150405") + randomString(6)
|
||||
}
|
||||
|
||||
func randomString(length int) string {
|
||||
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
b[i] = charset[time.Now().Nanosecond()%len(charset)]
|
||||
time.Sleep(1 * time.Nanosecond)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"verification-platform-backend/pkg/crypto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type bodyLogWriter struct {
|
||||
gin.ResponseWriter
|
||||
body *bytes.Buffer
|
||||
}
|
||||
|
||||
func (w *bodyLogWriter) Write(b []byte) (int, error) {
|
||||
return w.body.Write(b)
|
||||
}
|
||||
|
||||
func (w *bodyLogWriter) WriteHeader(statusCode int) {
|
||||
w.ResponseWriter.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
type EncryptedResponse struct {
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
func ResponseEncryption() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
|
||||
c.Writer = blw
|
||||
|
||||
c.Next()
|
||||
|
||||
shouldEncrypt := c.GetBool("should_encrypt_response")
|
||||
fmt.Printf("[ResponseEncryption] ShouldEncrypt: %v\n", shouldEncrypt)
|
||||
|
||||
blw.ResponseWriter.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
if shouldEncrypt {
|
||||
var response map[string]interface{}
|
||||
if err := json.Unmarshal(blw.body.Bytes(), &response); err == nil {
|
||||
responseJSON, _ := json.Marshal(response)
|
||||
fmt.Printf("[ResponseEncryption] Response to encrypt: %s\n", string(responseJSON))
|
||||
|
||||
var cryptoManager *crypto.CryptoManager
|
||||
if cm, ok := c.Get("crypto_manager"); ok {
|
||||
cryptoManager = cm.(*crypto.CryptoManager)
|
||||
}
|
||||
|
||||
if cryptoManager != nil {
|
||||
encrypted, err := cryptoManager.Encrypt(string(responseJSON))
|
||||
if err == nil {
|
||||
encryptedResponse := EncryptedResponse{Data: encrypted}
|
||||
encryptedJSON, _ := json.Marshal(encryptedResponse)
|
||||
blw.ResponseWriter.Write(encryptedJSON)
|
||||
fmt.Printf("[ResponseEncryption] Encrypted response sent\n")
|
||||
return
|
||||
} else {
|
||||
fmt.Printf("[ResponseEncryption] Encryption error: %v\n", err)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("[ResponseEncryption] CryptoManager is nil\n")
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("[ResponseEncryption] JSON unmarshal error: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
blw.ResponseWriter.Write(blw.body.Bytes())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user