Files
verify/backend/internal/router/app/dynamic.go
T
admin 10044b474f feat: 添加云端函数HTTP请求、SMTP邮件、数据库操作能力
- 添加HTTP客户端工具支持GET/POST请求
- 添加SMTP邮件发送功能
- 添加数据库操作(db.getRecords, db.deleteRecord, db.deleteRecords)
- 添加订单管理云端函数(读取、接收、筛选、成功、失败)
- 添加测试推送和邮件通知云端函数
- 修复云端函数编辑页面预览代码换行显示
- 云端变量数据模型重构(合并数据类型)
2026-04-30 20:54:02 +08:00

709 lines
19 KiB
Go

package app
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"verification-platform-backend/internal/database"
"verification-platform-backend/internal/model"
"verification-platform-backend/internal/service"
"verification-platform-backend/internal/utils/httputil"
"verification-platform-backend/internal/utils/smtputil"
"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
}
httpClient := httputil.NewHTTPClient(10 * time.Second)
httpObj := map[string]interface{}{
"get": func(url string, headers map[string]interface{}) *httputil.HTTPResponse {
convertedHeaders := make(map[string]string)
for k, v := range headers {
convertedHeaders[k] = fmt.Sprintf("%v", v)
}
return httpClient.Get(url, convertedHeaders)
},
"post": func(url string, headers map[string]interface{}, body interface{}) *httputil.HTTPResponse {
convertedHeaders := make(map[string]string)
for k, v := range headers {
convertedHeaders[k] = fmt.Sprintf("%v", v)
}
return httpClient.Post(url, convertedHeaders, body)
},
}
if err := vm.Set("http", httpObj); err != nil {
response.Error(c, 500, "HTTP对象设置失败")
return
}
dbObj := map[string]interface{}{
"getRecords": func(variableKey string, page, pageSize int) map[string]interface{} {
return getRecords(app.ID, req.UserID, variableKey, page, pageSize)
},
"deleteRecord": func(variableKey string, recordID uint) map[string]interface{} {
return deleteRecord(app.ID, req.UserID, variableKey, recordID)
},
"deleteRecords": func(variableKey string, recordIDs []uint) map[string]interface{} {
return deleteRecords(app.ID, req.UserID, variableKey, recordIDs)
},
}
if err := vm.Set("db", dbObj); err != nil {
response.Error(c, 500, "数据库对象设置失败")
return
}
smtpObj := map[string]interface{}{
"send": func(config map[string]interface{}, to []string, subject, body string, html bool) *smtputil.SMTPResult {
smtpConfig := smtputil.SMTPConfig{
Host: getString(config, "host"),
Port: getInt(config, "port", 587),
Username: getString(config, "username"),
Password: getString(config, "password"),
From: getString(config, "from"),
UseTLS: getBool(config, "use_tls", true),
}
client := smtputil.NewSMTPClient(smtpConfig, 30*time.Second)
return client.Send(smtputil.EmailMessage{
To: to,
Subject: subject,
Body: body,
HTML: html,
})
},
}
if err := vm.Set("smtp", smtpObj); err != nil {
response.Error(c, 500, "SMTP对象设置失败")
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
}
case "add_record":
if err := handleAddRecord(app.ID, req.UserID, 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 ""
}
}
func handleAddRecord(appID uint, userID *uint, actionMap map[string]interface{}) error {
variableKey, ok := actionMap["variable_key"].(string)
if !ok {
return fmt.Errorf("缺少variable_key参数")
}
recordData, ok := actionMap["record_data"]
if !ok {
return fmt.Errorf("缺少record_data参数")
}
var variable model.CloudVariable
if err := database.DB.Where("app_id = ? AND key = ?", appID, variableKey).First(&variable).Error; err != nil {
return fmt.Errorf("云端变量不存在: %s", variableKey)
}
if variable.VarType != "stream" {
return fmt.Errorf("该变量不是记录类型")
}
dataBytes, err := json.Marshal(recordData)
if err != nil {
return fmt.Errorf("序列化记录数据失败: %v", err)
}
record := model.CloudVariableRecord{
CloudVariableID: variable.ID,
Data: string(dataBytes),
}
if variable.Scope == "user" && userID != nil {
appUserID := *userID
record.AppUserID = &appUserID
}
if err := database.DB.Create(&record).Error; err != nil {
return fmt.Errorf("创建记录失败: %v", err)
}
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 _, oldRecord := range oldRecords {
database.DB.Delete(&oldRecord)
}
}
}
return nil
}
func getRecords(appID uint, userID *uint, variableKey string, page, pageSize int) map[string]interface{} {
result := map[string]interface{}{
"success": false,
"records": []interface{}{},
"total": 0,
"page": page,
"pageSize": pageSize,
}
var variable model.CloudVariable
if err := database.DB.Where("app_id = ? AND key = ?", appID, variableKey).First(&variable).Error; err != nil {
result["error"] = "云端变量不存在"
return result
}
if variable.VarType != "stream" {
result["error"] = "该变量不是记录类型"
return result
}
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" && userID != nil {
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").Offset(offset).Limit(pageSize).Find(&records).Error; err != nil {
result["error"] = "查询记录失败"
return result
}
var recordList []interface{}
for _, record := range records {
var data interface{}
if err := json.Unmarshal([]byte(record.Data), &data); err == nil {
recordList = append(recordList, map[string]interface{}{
"id": record.ID,
"data": data,
"created_at": record.CreatedAt,
})
}
}
result["success"] = true
result["records"] = recordList
result["total"] = total
return result
}
func deleteRecord(appID uint, userID *uint, variableKey string, recordID uint) map[string]interface{} {
result := map[string]interface{}{
"success": false,
}
var variable model.CloudVariable
if err := database.DB.Where("app_id = ? AND key = ?", appID, variableKey).First(&variable).Error; err != nil {
result["error"] = "云端变量不存在"
return result
}
if variable.VarType != "stream" {
result["error"] = "该变量不是记录类型"
return result
}
query := database.DB.Where("id = ? AND cloud_variable_id = ?", recordID, variable.ID)
if variable.Scope == "user" && userID != nil {
query = query.Where("app_user_id = ?", *userID)
}
if err := query.Delete(&model.CloudVariableRecord{}).Error; err != nil {
result["error"] = "删除记录失败"
return result
}
result["success"] = true
return result
}
func deleteRecords(appID uint, userID *uint, variableKey string, recordIDs []uint) map[string]interface{} {
result := map[string]interface{}{
"success": false,
"deleted": 0,
}
var variable model.CloudVariable
if err := database.DB.Where("app_id = ? AND key = ?", appID, variableKey).First(&variable).Error; err != nil {
result["error"] = "云端变量不存在"
return result
}
if variable.VarType != "stream" {
result["error"] = "该变量不是记录类型"
return result
}
query := database.DB.Where("id IN ? AND cloud_variable_id = ?", recordIDs, variable.ID)
if variable.Scope == "user" && userID != nil {
query = query.Where("app_user_id = ?", *userID)
}
deleteResult := query.Delete(&model.CloudVariableRecord{})
if deleteResult.Error != nil {
result["error"] = "删除记录失败"
return result
}
result["success"] = true
result["deleted"] = deleteResult.RowsAffected
return result
}
func getString(m map[string]interface{}, key string) string {
if v, ok := m[key]; ok {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
func getInt(m map[string]interface{}, key string, defaultValue int) int {
if v, ok := m[key]; ok {
switch val := v.(type) {
case int:
return val
case float64:
return int(val)
}
}
return defaultValue
}
func getBool(m map[string]interface{}, key string, defaultValue bool) bool {
if v, ok := m[key]; ok {
if b, ok := v.(bool); ok {
return b
}
}
return defaultValue
}