Files
verify/backend/internal/router/developer/dynamic.go
T
2026-04-27 17:22:56 +08:00

806 lines
22 KiB
Go

package developer
import (
"log"
"strings"
"time"
"unicode"
"verification-platform-backend/internal/database"
"verification-platform-backend/internal/model"
"verification-platform-backend/pkg/response"
"github.com/dop251/goja"
"github.com/gin-gonic/gin"
)
func normalizeKey(name string) string {
key := strings.ToLower(strings.TrimSpace(name))
key = strings.Map(func(r rune) rune {
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '-' {
return r
}
return '_'
}, key)
return key
}
func validateDynamicCode(code string) error {
vm := goja.New()
vm.Set("params", vm.NewObject())
vm.Set("user", vm.NewObject())
vm.Set("app", vm.NewObject())
wrappedCode := "(function() { " + code + " })()"
_, err := vm.RunString(wrappedCode)
if err != nil {
errStr := err.Error()
if strings.Contains(errStr, "ReferenceError") || strings.Contains(errStr, "is not defined") {
return nil
}
return err
}
return nil
}
func SetupDynamicRoutes(r *gin.RouterGroup) {
dynamicCodes := r.Group("/dynamic-codes")
{
dynamicCodes.GET("", handleListDynamicCodes)
dynamicCodes.GET("/:id", handleGetDynamicCode)
dynamicCodes.POST("", handleCreateDynamicCode)
dynamicCodes.PUT("/:id", handleUpdateDynamicCode)
dynamicCodes.DELETE("/:id", handleDeleteDynamicCode)
dynamicCodes.PUT("/:id/status", handleUpdateDynamicCodeStatus)
dynamicCodes.DELETE("/batch", handleBatchDeleteDynamicCodes)
dynamicCodes.PUT("/batch/status", handleBatchUpdateDynamicCodeStatus)
}
riskControl := r.Group("/risk-control")
{
riskControl.GET("/rules", handleGetRiskControlRules)
riskControl.POST("/rules", handleCreateRiskControlRule)
riskControl.PUT("/rules/:id", handleUpdateRiskControlRule)
riskControl.DELETE("/rules/:id", handleDeleteRiskControlRule)
riskControl.PUT("/rules/:id/status", handleUpdateRiskControlRuleStatus)
riskControl.DELETE("/rules/batch", handleBatchDeleteRiskControlRules)
riskControl.PUT("/rules/batch/status", handleBatchUpdateRiskControlRuleStatus)
}
}
func handleListDynamicCodes(c *gin.Context) {
userID := c.GetUint("user_id")
var applications []model.Application
if err := database.DB.Where("user_id = ?", userID).Find(&applications).Error; err != nil {
response.Error(c, 500, "获取应用列表失败")
return
}
var applicationIDs []uint
for _, app := range applications {
applicationIDs = append(applicationIDs, app.ID)
}
var dynamicCodes []model.DynamicCode
if err := database.DB.Preload("Application").Preload("Creator").Where("application_id IN ?", applicationIDs).Find(&dynamicCodes).Error; err != nil {
response.Error(c, 500, "获取动态代码列表失败")
return
}
type DynamicCodeResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Code string `json:"code"`
Description string `json:"description"`
ApplicationID uint `json:"application_id"`
ApplicationName string `json:"application_name"`
Enabled bool `json:"enabled"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Creator *struct {
ID uint `json:"id"`
Username string `json:"username"`
} `json:"creator,omitempty"`
}
var result []DynamicCodeResponse
for _, dc := range dynamicCodes {
var creator *struct {
ID uint `json:"id"`
Username string `json:"username"`
}
if dc.UserID != nil && dc.Creator.ID != 0 {
creator = &struct {
ID uint `json:"id"`
Username string `json:"username"`
}{
ID: dc.Creator.ID,
Username: dc.Creator.Username,
}
}
result = append(result, DynamicCodeResponse{
ID: dc.ID,
Name: dc.Name,
Code: dc.Code,
Description: dc.Description,
ApplicationID: dc.ApplicationID,
ApplicationName: dc.Application.Name,
Enabled: dc.Status == "active",
CreatedAt: dc.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: dc.UpdatedAt.Format("2006-01-02 15:04:05"),
Creator: creator,
})
}
response.Success(c, result)
}
func handleGetDynamicCode(c *gin.Context) {
userID := c.GetUint("user_id")
id := c.Param("id")
var dynamicCode model.DynamicCode
if err := database.DB.Preload("Application").Preload("Creator").Where("id = ?", id).First(&dynamicCode).Error; err != nil {
response.Error(c, 404, "动态代码不存在")
return
}
var app model.Application
if err := database.DB.Where("id = ? AND user_id = ?", dynamicCode.ApplicationID, userID).First(&app).Error; err != nil {
response.Error(c, 403, "无权限访问此动态代码")
return
}
type DynamicCodeResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Code string `json:"code"`
Description string `json:"description"`
ApplicationID uint `json:"application_id"`
ApplicationName string `json:"application_name"`
Enabled bool `json:"enabled"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Creator *struct {
ID uint `json:"id"`
Username string `json:"username"`
} `json:"creator,omitempty"`
}
var creator *struct {
ID uint `json:"id"`
Username string `json:"username"`
}
if dynamicCode.UserID != nil && dynamicCode.Creator.ID != 0 {
creator = &struct {
ID uint `json:"id"`
Username string `json:"username"`
}{
ID: dynamicCode.Creator.ID,
Username: dynamicCode.Creator.Username,
}
}
result := DynamicCodeResponse{
ID: dynamicCode.ID,
Name: dynamicCode.Name,
Code: dynamicCode.Code,
Description: dynamicCode.Description,
ApplicationID: dynamicCode.ApplicationID,
ApplicationName: dynamicCode.Application.Name,
Enabled: dynamicCode.Status == "active",
CreatedAt: dynamicCode.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: dynamicCode.UpdatedAt.Format("2006-01-02 15:04:05"),
Creator: creator,
}
response.Success(c, result)
}
func handleCreateDynamicCode(c *gin.Context) {
userID := c.GetUint("user_id")
var req struct {
Name string `json:"name" binding:"required"`
Code string `json:"code" binding:"required"`
Description string `json:"description"`
Enabled bool `json:"enabled"`
ApplicationID uint `json:"application_id" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, 400, "参数错误: "+err.Error())
return
}
if err := validateDynamicCode(req.Code); err != nil {
response.Error(c, 400, "代码语法错误: "+err.Error())
return
}
var app model.Application
if err := database.DB.Where("id = ? AND user_id = ?", req.ApplicationID, userID).First(&app).Error; err != nil {
response.Error(c, 404, "应用不存在")
return
}
status := "inactive"
if req.Enabled {
status = "active"
}
key := normalizeKey(req.Name)
var existingCode model.DynamicCode
err := database.DB.Unscoped().Where("key = ?", key).First(&existingCode).Error
if err == nil {
if existingCode.DeletedAt.Valid {
database.DB.Unscoped().Delete(&existingCode)
} else {
response.Error(c, 400, "该名称的动态代码已存在")
return
}
}
dynamicCode := model.DynamicCode{
UserID: &userID,
ApplicationID: req.ApplicationID,
Name: req.Name,
Key: key,
Code: req.Code,
Description: req.Description,
Status: status,
}
if err := database.DB.Create(&dynamicCode).Error; err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint failed") || strings.Contains(err.Error(), "constraint failed") {
response.Error(c, 400, "该名称的动态代码已存在")
return
}
response.Error(c, 500, "创建失败: "+err.Error())
return
}
response.Success(c, dynamicCode)
}
func handleUpdateDynamicCode(c *gin.Context) {
userID := c.GetUint("user_id")
id := c.Param("id")
var dynamicCode model.DynamicCode
if err := database.DB.Where("id = ?", id).First(&dynamicCode).Error; err != nil {
response.Error(c, 404, "动态代码不存在")
return
}
var app model.Application
if err := database.DB.Where("id = ? AND user_id = ?", dynamicCode.ApplicationID, userID).First(&app).Error; err != nil {
response.Error(c, 403, "无权限操作此动态代码")
return
}
var req struct {
Name string `json:"name" binding:"required"`
Code string `json:"code" binding:"required"`
Description string `json:"description"`
Enabled bool `json:"enabled"`
ApplicationID uint `json:"application_id" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, 400, "参数错误")
return
}
if err := validateDynamicCode(req.Code); err != nil {
response.Error(c, 400, "代码语法错误: "+err.Error())
return
}
if req.ApplicationID != dynamicCode.ApplicationID {
var newApp model.Application
if err := database.DB.Where("id = ? AND user_id = ?", req.ApplicationID, userID).First(&newApp).Error; err != nil {
response.Error(c, 403, "无权限操作此应用")
return
}
dynamicCode.ApplicationID = req.ApplicationID
}
status := "inactive"
if req.Enabled {
status = "active"
}
dynamicCode.Name = req.Name
dynamicCode.Key = normalizeKey(req.Name)
dynamicCode.Code = req.Code
dynamicCode.Description = req.Description
dynamicCode.Status = status
if err := database.DB.Save(&dynamicCode).Error; err != nil {
response.Error(c, 500, "更新失败")
return
}
response.Success(c, dynamicCode)
}
func handleDeleteDynamicCode(c *gin.Context) {
userID := c.GetUint("user_id")
id := c.Param("id")
var dynamicCode model.DynamicCode
if err := database.DB.Where("id = ?", id).First(&dynamicCode).Error; err != nil {
response.Error(c, 404, "动态代码不存在")
return
}
var app model.Application
if err := database.DB.Where("id = ? AND user_id = ?", dynamicCode.ApplicationID, userID).First(&app).Error; err != nil {
response.Error(c, 403, "无权限操作此动态代码")
return
}
if err := database.DB.Delete(&dynamicCode).Error; err != nil {
response.Error(c, 500, "删除失败")
return
}
response.Success(c, gin.H{"message": "删除成功"})
}
func handleUpdateDynamicCodeStatus(c *gin.Context) {
userID := c.GetUint("user_id")
id := c.Param("id")
var dynamicCode model.DynamicCode
if err := database.DB.Where("id = ?", id).First(&dynamicCode).Error; err != nil {
response.Error(c, 404, "动态代码不存在")
return
}
var app model.Application
if err := database.DB.Where("id = ? AND user_id = ?", dynamicCode.ApplicationID, userID).First(&app).Error; err != nil {
response.Error(c, 403, "无权限操作此动态代码")
return
}
var req struct {
Enabled bool `json:"enabled"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, 400, "参数错误")
return
}
status := "inactive"
if req.Enabled {
status = "active"
}
dynamicCode.Status = status
if err := database.DB.Save(&dynamicCode).Error; err != nil {
response.Error(c, 500, "更新状态失败")
return
}
response.Success(c, dynamicCode)
}
func handleBatchDeleteDynamicCodes(c *gin.Context) {
userID := c.GetUint("user_id")
var req struct {
IDs []uint `json:"ids" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, 400, "参数错误")
return
}
var applications []model.Application
if err := database.DB.Where("user_id = ?", userID).Find(&applications).Error; err != nil {
response.Error(c, 500, "获取应用列表失败")
return
}
var applicationIDs []uint
for _, app := range applications {
applicationIDs = append(applicationIDs, app.ID)
}
if err := database.DB.Where("id IN ? AND application_id IN ?", req.IDs, applicationIDs).Delete(&model.DynamicCode{}).Error; err != nil {
response.Error(c, 500, "批量删除失败")
return
}
response.Success(c, gin.H{"message": "批量删除成功"})
}
func handleBatchUpdateDynamicCodeStatus(c *gin.Context) {
userID := c.GetUint("user_id")
var req struct {
IDs []uint `json:"ids" binding:"required"`
Enabled bool `json:"enabled"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, 400, "参数错误")
return
}
var applications []model.Application
if err := database.DB.Where("user_id = ?", userID).Find(&applications).Error; err != nil {
response.Error(c, 500, "获取应用列表失败")
return
}
var applicationIDs []uint
for _, app := range applications {
applicationIDs = append(applicationIDs, app.ID)
}
status := "inactive"
if req.Enabled {
status = "active"
}
if err := database.DB.Model(&model.DynamicCode{}).Where("id IN ? AND application_id IN ?", req.IDs, applicationIDs).Update("status", status).Error; err != nil {
response.Error(c, 500, "批量更新状态失败")
return
}
response.Success(c, gin.H{"message": "批量更新状态成功"})
}
type RiskControlRule struct {
ID uint `json:"id"`
Type string `json:"type"`
Value string `json:"value"`
Reason string `json:"reason"`
Status string `json:"status"`
ExpiresAt *string `json:"expires_at"`
CreatedAt string `json:"created_at"`
}
func handleGetRiskControlRules(c *gin.Context) {
userID := c.GetUint("user_id")
var applications []model.Application
if err := database.DB.Where("user_id = ?", userID).Find(&applications).Error; err != nil {
response.Error(c, 500, "获取应用列表失败")
return
}
var applicationIDs []uint
for _, app := range applications {
applicationIDs = append(applicationIDs, app.ID)
}
var rules []model.RiskControlRule
if err := database.DB.Where("user_id = ? OR application_id IN ?", userID, applicationIDs).Order("created_at DESC").Find(&rules).Error; err != nil {
response.Error(c, 500, "获取风控规则失败")
return
}
var result []gin.H
for _, rule := range rules {
var expiresAt *string
if rule.ExpiresAt != nil {
t := rule.ExpiresAt.Format("2006-01-02 15:04:05")
expiresAt = &t
}
var appID *uint
if rule.ApplicationID != nil {
appID = rule.ApplicationID
}
result = append(result, gin.H{
"id": rule.ID,
"type": rule.Type,
"value": rule.Value,
"reason": rule.Reason,
"status": rule.Status,
"expires_at": expiresAt,
"application_id": appID,
"is_global": rule.ApplicationID == nil,
"created_at": rule.CreatedAt.Format("2006-01-02 15:04:05"),
})
}
response.Success(c, result)
}
func handleCreateRiskControlRule(c *gin.Context) {
userID := c.GetUint("user_id")
var req struct {
Type string `json:"type" binding:"required"`
Value string `json:"value" binding:"required"`
Reason string `json:"reason"`
ExpiresAt *string `json:"expires_at"`
ApplicationID *uint `json:"application_id"`
IsGlobal bool `json:"is_global"`
}
if err := c.ShouldBindJSON(&req); err != nil {
log.Printf("[ERROR] Failed to bind JSON: %v", err)
response.Error(c, 400, "参数错误")
return
}
log.Printf("[DEBUG] Create risk control rule: type=%s, value=%s, is_global=%v, application_id=%v", req.Type, req.Value, req.IsGlobal, req.ApplicationID)
var appID *uint
switch req.Type {
case "user":
if req.ApplicationID == nil {
response.Error(c, 400, "用户封禁规则必须指定应用")
return
}
var application model.Application
if err := database.DB.Where("id = ? AND user_id = ?", *req.ApplicationID, userID).First(&application).Error; err != nil {
response.Error(c, 404, "应用不存在或无权限")
return
}
appID = req.ApplicationID
case "ip", "device", "region":
if !req.IsGlobal && req.ApplicationID != nil {
var application model.Application
if err := database.DB.Where("id = ? AND user_id = ?", *req.ApplicationID, userID).First(&application).Error; err != nil {
response.Error(c, 404, "应用不存在或无权限")
return
}
appID = req.ApplicationID
} else {
appID = nil
}
default:
response.Error(c, 400, "不支持的规则类型")
return
}
rule := model.RiskControlRule{
UserID: userID,
ApplicationID: appID,
Type: req.Type,
Value: req.Value,
Reason: req.Reason,
Status: "active",
}
if req.ExpiresAt != nil && *req.ExpiresAt != "" {
t, err := time.Parse("2006-01-02T15:04", *req.ExpiresAt)
if err == nil {
rule.ExpiresAt = &t
} else {
log.Printf("[WARN] Failed to parse expires_at: %v", err)
}
}
log.Printf("[DEBUG] Creating rule: %+v", rule)
if err := database.DB.Create(&rule).Error; err != nil {
log.Printf("[ERROR] Failed to create risk control rule: %v", err)
response.Error(c, 500, "创建风控规则失败")
return
}
var expiresAt *string
if rule.ExpiresAt != nil {
t := rule.ExpiresAt.Format("2006-01-02 15:04:05")
expiresAt = &t
}
var appIDResp *uint
if rule.ApplicationID != nil {
appIDResp = rule.ApplicationID
}
response.Success(c, gin.H{
"id": rule.ID,
"type": rule.Type,
"value": rule.Value,
"reason": rule.Reason,
"status": rule.Status,
"expires_at": expiresAt,
"application_id": appIDResp,
"is_global": rule.ApplicationID == nil,
"created_at": rule.CreatedAt.Format("2006-01-02 15:04:05"),
})
}
func handleUpdateRiskControlRule(c *gin.Context) {
userID := c.GetUint("user_id")
ruleID := c.Param("id")
var req struct {
Type string `json:"type" binding:"required"`
Value string `json:"value" binding:"required"`
Reason string `json:"reason"`
ExpiresAt *string `json:"expires_at"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, 400, "参数错误")
return
}
var applications []model.Application
if err := database.DB.Where("user_id = ?", userID).Find(&applications).Error; err != nil {
response.Error(c, 500, "获取应用列表失败")
return
}
var applicationIDs []uint
for _, app := range applications {
applicationIDs = append(applicationIDs, app.ID)
}
var rule model.RiskControlRule
if err := database.DB.Where("id = ? AND application_id IN ?", ruleID, applicationIDs).First(&rule).Error; err != nil {
response.Error(c, 404, "风控规则不存在")
return
}
rule.Type = req.Type
rule.Value = req.Value
rule.Reason = req.Reason
if req.ExpiresAt != nil && *req.ExpiresAt != "" {
t, err := time.Parse("2006-01-02T15:04", *req.ExpiresAt)
if err == nil {
rule.ExpiresAt = &t
}
} else {
rule.ExpiresAt = nil
}
if err := database.DB.Save(&rule).Error; err != nil {
response.Error(c, 500, "更新风控规则失败")
return
}
var expiresAt *string
if rule.ExpiresAt != nil {
t := rule.ExpiresAt.Format("2006-01-02 15:04:05")
expiresAt = &t
}
response.Success(c, RiskControlRule{
ID: rule.ID,
Type: rule.Type,
Value: rule.Value,
Reason: rule.Reason,
Status: rule.Status,
ExpiresAt: expiresAt,
CreatedAt: rule.CreatedAt.Format("2006-01-02 15:04:05"),
})
}
func handleDeleteRiskControlRule(c *gin.Context) {
userID := c.GetUint("user_id")
ruleID := c.Param("id")
var applications []model.Application
if err := database.DB.Where("user_id = ?", userID).Find(&applications).Error; err != nil {
response.Error(c, 500, "获取应用列表失败")
return
}
var applicationIDs []uint
for _, app := range applications {
applicationIDs = append(applicationIDs, app.ID)
}
if err := database.DB.Where("id = ? AND application_id IN ?", ruleID, applicationIDs).Delete(&model.RiskControlRule{}).Error; err != nil {
response.Error(c, 500, "删除风控规则失败")
return
}
response.Success(c, gin.H{"message": "删除成功"})
}
func handleUpdateRiskControlRuleStatus(c *gin.Context) {
userID := c.GetUint("user_id")
ruleID := c.Param("id")
var req struct {
Status string `json:"status" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, 400, "参数错误")
return
}
var applications []model.Application
if err := database.DB.Where("user_id = ?", userID).Find(&applications).Error; err != nil {
response.Error(c, 500, "获取应用列表失败")
return
}
var applicationIDs []uint
for _, app := range applications {
applicationIDs = append(applicationIDs, app.ID)
}
var rule model.RiskControlRule
if err := database.DB.Where("id = ? AND application_id IN ?", ruleID, applicationIDs).First(&rule).Error; err != nil {
response.Error(c, 404, "风控规则不存在")
return
}
rule.Status = req.Status
if err := database.DB.Save(&rule).Error; err != nil {
response.Error(c, 500, "更新状态失败")
return
}
response.Success(c, gin.H{"message": "状态更新成功"})
}
func handleBatchDeleteRiskControlRules(c *gin.Context) {
userID := c.GetUint("user_id")
var req struct {
IDs []uint `json:"ids" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, 400, "参数错误")
return
}
var applications []model.Application
if err := database.DB.Where("user_id = ?", userID).Find(&applications).Error; err != nil {
response.Error(c, 500, "获取应用列表失败")
return
}
var applicationIDs []uint
for _, app := range applications {
applicationIDs = append(applicationIDs, app.ID)
}
if err := database.DB.Where("id IN ? AND application_id IN ?", req.IDs, applicationIDs).Delete(&model.RiskControlRule{}).Error; err != nil {
response.Error(c, 500, "批量删除失败")
return
}
response.Success(c, gin.H{"message": "批量删除成功"})
}
func handleBatchUpdateRiskControlRuleStatus(c *gin.Context) {
userID := c.GetUint("user_id")
var req struct {
IDs []uint `json:"ids" binding:"required"`
Status string `json:"status" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, 400, "参数错误")
return
}
var applications []model.Application
if err := database.DB.Where("user_id = ?", userID).Find(&applications).Error; err != nil {
response.Error(c, 500, "获取应用列表失败")
return
}
var applicationIDs []uint
for _, app := range applications {
applicationIDs = append(applicationIDs, app.ID)
}
if err := database.DB.Model(&model.RiskControlRule{}).Where("id IN ? AND application_id IN ?", req.IDs, applicationIDs).Update("status", req.Status).Error; err != nil {
response.Error(c, 500, "批量更新状态失败")
return
}
response.Success(c, gin.H{"message": "批量更新状态成功"})
}