cb688be222
- 订阅模式返回到期时间 expiry_at - 余额模式返回余额 balance - 永久会员返回 is_permanent 标识 - 免费模式只返回成功消息 Co-Authored-By: Claude <noreply@anthropic.com>
227 lines
6.2 KiB
Go
227 lines
6.2 KiB
Go
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"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
// 从 JWT token 中获取用户信息
|
|
userIDInterface, exists := c.Get("user_id")
|
|
if !exists {
|
|
response.Error(c, 401, "未授权")
|
|
return
|
|
}
|
|
userID := userIDInterface.(uint)
|
|
|
|
usernameInterface, _ := c.Get("username")
|
|
username := usernameInterface.(string)
|
|
|
|
// 从 JWT token 中获取设备信息
|
|
deviceIDInterface, exists := c.Get("device_id")
|
|
deviceID := ""
|
|
if exists {
|
|
deviceID = deviceIDInterface.(string)
|
|
}
|
|
|
|
instanceIDInterface, exists := c.Get("instance_id")
|
|
instanceID := ""
|
|
if exists {
|
|
instanceID = instanceIDInterface.(string)
|
|
}
|
|
if instanceID == "" {
|
|
instanceID = deviceID
|
|
}
|
|
|
|
var user model.AppUser
|
|
if err := database.DB.First(&user, userID).Error; err != nil {
|
|
response.Error(c, 404, "用户不存在")
|
|
return
|
|
}
|
|
|
|
if blocked, reason := checkRiskControl(c, app.ID, deviceID, 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 == -1 {
|
|
log.Printf("[DEBUG] User %d is permanent member, skip deduction", user.ID)
|
|
} else if app.BillingType == "subscription" {
|
|
if user.ExpiryAt == nil || user.ExpiryAt.Before(now) {
|
|
log.Printf("[DEBUG] User %d subscription expired, ExpiryAt=%v", user.ID, user.ExpiryAt)
|
|
response.Error(c, 403, "订阅已过期")
|
|
return
|
|
}
|
|
log.Printf("[DEBUG] User %d subscription valid, skip balance deduction", user.ID)
|
|
} else {
|
|
result := database.DB.Model(&model.AppUser{}).Where("id = ? AND balance >= ?", user.ID, app.DeductionAmount).
|
|
Update("balance", gorm.Expr("balance - ?", app.DeductionAmount))
|
|
if result.Error != nil || result.RowsAffected == 0 {
|
|
log.Printf("[DEBUG] User %d has insufficient balance: %.2f < %.2f", user.ID, user.Balance, app.DeductionAmount)
|
|
response.Error(c, 403, "余额不足")
|
|
return
|
|
}
|
|
database.DB.Where("id = ?", user.ID).First(&user)
|
|
log.Printf("[DEBUG] Deducted %.2f from user %d, new balance: %.2f", app.DeductionAmount, user.ID, user.Balance)
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := database.DB.Save(&user).Error; err != nil {
|
|
response.Error(c, 500, "更新心跳失败")
|
|
return
|
|
}
|
|
|
|
if deviceID != "" {
|
|
var device model.UserDevice
|
|
err := database.DB.Where("user_id = ? AND application_id = ? AND device_id = ?", user.ID, app.ID, deviceID).First(&device).Error
|
|
|
|
if err != nil {
|
|
device = model.UserDevice{
|
|
UserID: user.ID,
|
|
ApplicationID: app.ID,
|
|
DeviceID: deviceID,
|
|
DeviceName: deviceID,
|
|
Status: "active",
|
|
}
|
|
if err := database.DB.Create(&device).Error; err != nil {
|
|
log.Printf("[DEBUG] Failed to create device: %v", err)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 根据运营模式返回不同参数
|
|
heartbeatData := gin.H{
|
|
"message": "心跳成功",
|
|
}
|
|
|
|
// 永久会员返回永久标识
|
|
if user.Balance == -1 {
|
|
heartbeatData["is_permanent"] = true
|
|
} else if app.BillingType == "subscription" {
|
|
// 订阅模式返回到期时间
|
|
if user.ExpiryAt != nil {
|
|
heartbeatData["expiry_at"] = user.ExpiryAt.Format("2006-01-02 15:04:05")
|
|
heartbeatData["expiry_timestamp"] = user.ExpiryAt.Unix()
|
|
}
|
|
} else if app.BillingType != "free" {
|
|
// 余额模式返回余额
|
|
heartbeatData["balance"] = user.Balance
|
|
}
|
|
|
|
response.SuccessWithMessage(c, "心跳成功", heartbeatData)
|
|
}
|