Files
verify/backend/internal/router/app/account.go
T
admin 113c01e89f feat: 心跳接口改用JWT token获取用户设备信息
- JWT Claims 增加 device_id 和 instance_id 字段
- 登录时将设备信息编码到 token
- 心跳接口从 token 自动获取用户和设备信息
- 心跳路由移到需要 JWT 认证的路由组
- 更新 API 文档

优点:
- 更安全:无法伪造设备信息
- 请求更简洁:心跳无需传递参数
- 符合业界标准做法

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-16 02:25:53 +08:00

211 lines
5.7 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)
}
}
}
}
response.SuccessWithMessage(c, "心跳成功", gin.H{
"message": "心跳成功",
"balance": user.Balance,
})
}