feat: 添加云端函数HTTP请求、SMTP邮件、数据库操作能力

- 添加HTTP客户端工具支持GET/POST请求
- 添加SMTP邮件发送功能
- 添加数据库操作(db.getRecords, db.deleteRecord, db.deleteRecords)
- 添加订单管理云端函数(读取、接收、筛选、成功、失败)
- 添加测试推送和邮件通知云端函数
- 修复云端函数编辑页面预览代码换行显示
- 云端变量数据模型重构(合并数据类型)
This commit is contained in:
2026-04-30 20:54:02 +08:00
parent a4df9e91a0
commit 10044b474f
46 changed files with 15249 additions and 250 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ import (
"github.com/gin-gonic/gin"
)
//go:embed embedded/dist
//go:embed embedded/dist/*
var embeddedFiles embed.FS
// @title 网络验证平台API
+2 -3
View File
@@ -457,9 +457,8 @@ type CloudVariable struct {
AppID *uint `json:"app_id"`
Key string `gorm:"size:100;index" json:"key"`
DefaultValue string `gorm:"type:text" json:"default_value"`
VarType string `gorm:"size:20;default:string" json:"var_type"` // string, integer, decimal, binary
DataType string `gorm:"size:20;default:single" json:"data_type"` // single, stream
MaxRecords int `gorm:"default:0" json:"max_records"` // 流水类型最大记录数,0=不限制
VarType string `gorm:"size:20;default:string" json:"var_type"` // string, integer, decimal, binary, stream
MaxRecords int `gorm:"default:0" json:"max_records"`
FilePath string `gorm:"size:500" json:"file_path"`
FileSize int64 `json:"file_size"`
MimeType string `gorm:"size:100" json:"mime_type"`
+6 -6
View File
@@ -730,8 +730,8 @@ func handleAppCreateVariableRecord(c *gin.Context) {
return
}
if variable.DataType != "stream" {
response.Error(c, 400, "该变量不是流水类型")
if variable.VarType != "stream" {
response.Error(c, 400, "该变量不是记录类型")
return
}
@@ -817,8 +817,8 @@ func handleAppGetVariableRecords(c *gin.Context) {
return
}
if variable.DataType != "stream" {
response.Error(c, 400, "该变量不是流水类型")
if variable.VarType != "stream" {
response.Error(c, 400, "该变量不是记录类型")
return
}
@@ -894,8 +894,8 @@ func handleAppDeleteVariableRecord(c *gin.Context) {
return
}
if variable.DataType != "stream" {
response.Error(c, 400, "该变量不是流水类型")
if variable.VarType != "stream" {
response.Error(c, 400, "该变量不是记录类型")
return
}
+281
View File
@@ -1,6 +1,8 @@
package app
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
@@ -8,6 +10,8 @@ import (
"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"
@@ -244,6 +248,68 @@ func handleExecuteDynamicCode(c *gin.Context) {
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())
@@ -278,6 +344,11 @@ func handleExecuteDynamicCode(c *gin.Context) {
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
}
}
}
}
@@ -425,3 +496,213 @@ func toString(v interface{}) string {
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
}
+4 -14
View File
@@ -398,7 +398,6 @@ func handleCreateCloudVariable(c *gin.Context) {
Key string `json:"key"`
DefaultValue string `json:"default_value"`
VarType string `json:"var_type"`
DataType string `json:"data_type"`
MaxRecords int `json:"max_records"`
Scope string `json:"scope"`
WritePermission string `json:"write_permission"`
@@ -431,17 +430,12 @@ func handleCreateCloudVariable(c *gin.Context) {
req.VarType = "string"
}
if req.DataType != "single" && req.DataType != "stream" {
req.DataType = "single"
}
variable := model.CloudVariable{
UserID: userID,
AppID: &req.AppID,
Key: req.Key,
DefaultValue: req.DefaultValue,
VarType: req.VarType,
DataType: req.DataType,
MaxRecords: req.MaxRecords,
Scope: req.Scope,
WritePermission: req.WritePermission,
@@ -464,7 +458,6 @@ func handleUpdateCloudVariable(c *gin.Context) {
Key string `json:"key"`
DefaultValue string `json:"default_value"`
VarType string `json:"var_type"`
DataType string `json:"data_type"`
MaxRecords int `json:"max_records"`
WritePermission string `json:"write_permission"`
Description string `json:"description"`
@@ -485,9 +478,6 @@ func handleUpdateCloudVariable(c *gin.Context) {
variable.Key = req.Key
variable.DefaultValue = req.DefaultValue
variable.VarType = req.VarType
if req.DataType == "single" || req.DataType == "stream" {
variable.DataType = req.DataType
}
variable.MaxRecords = req.MaxRecords
if req.WritePermission == "admin" || req.WritePermission == "user" || req.WritePermission == "app_user" {
variable.WritePermission = req.WritePermission
@@ -715,8 +705,8 @@ func handleGetCloudVariableRecords(c *gin.Context) {
return
}
if variable.DataType != "stream" {
response.Error(c, 400, "该变量不是流水类型")
if variable.VarType != "stream" {
response.Error(c, 400, "该变量不是记录类型")
return
}
@@ -794,8 +784,8 @@ func handleDeleteCloudVariableRecords(c *gin.Context) {
return
}
if variable.DataType != "stream" {
response.Error(c, 400, "该变量不是流水类型")
if variable.VarType != "stream" {
response.Error(c, 400, "该变量不是记录类型")
return
}
+199
View File
@@ -0,0 +1,199 @@
package httputil
import (
"bytes"
"encoding/json"
"fmt"
"net"
"net/http"
"net/url"
"strings"
"time"
)
const DefaultTimeout = 10 * time.Second
type HTTPResponse struct {
StatusCode int `json:"statusCode"`
Status string `json:"status"`
Headers map[string]string `json:"headers"`
Body string `json:"body"`
JSON map[string]interface{} `json:"json,omitempty"`
Error string `json:"error,omitempty"`
}
type HTTPClient struct {
client *http.Client
timeout time.Duration
}
func NewHTTPClient(timeout time.Duration) *HTTPClient {
if timeout <= 0 {
timeout = DefaultTimeout
}
return &HTTPClient{
client: &http.Client{
Timeout: timeout,
},
timeout: timeout,
}
}
func (c *HTTPClient) Get(urlStr string, headers map[string]string) *HTTPResponse {
return c.doRequest("GET", urlStr, headers, nil)
}
func (c *HTTPClient) Post(urlStr string, headers map[string]string, body interface{}) *HTTPResponse {
return c.doRequest("POST", urlStr, headers, body)
}
func (c *HTTPClient) doRequest(method, urlStr string, headers map[string]string, body interface{}) *HTTPResponse {
resp := &HTTPResponse{
Headers: make(map[string]string),
}
if err := validateURL(urlStr); err != nil {
resp.Error = err.Error()
return resp
}
var reqBody []byte
var err error
contentType := "application/json"
if body != nil {
switch v := body.(type) {
case string:
reqBody = []byte(v)
contentType = "text/plain"
case map[string]interface{}:
reqBody, err = json.Marshal(v)
if err != nil {
resp.Error = fmt.Sprintf("failed to marshal body: %v", err)
return resp
}
default:
reqBody, err = json.Marshal(v)
if err != nil {
resp.Error = fmt.Sprintf("failed to marshal body: %v", err)
return resp
}
}
}
var req *http.Request
if reqBody != nil {
req, err = http.NewRequest(method, urlStr, bytes.NewReader(reqBody))
} else {
req, err = http.NewRequest(method, urlStr, nil)
}
if err != nil {
resp.Error = fmt.Sprintf("failed to create request: %v", err)
return resp
}
if headers != nil {
for k, v := range headers {
req.Header.Set(k, v)
}
}
if body != nil && req.Header.Get("Content-Type") == "" {
req.Header.Set("Content-Type", contentType)
}
httpResp, err := c.client.Do(req)
if err != nil {
resp.Error = fmt.Sprintf("request failed: %v", err)
return resp
}
defer httpResp.Body.Close()
resp.StatusCode = httpResp.StatusCode
resp.Status = httpResp.Status
for k, v := range httpResp.Header {
if len(v) > 0 {
resp.Headers[k] = v[0]
}
}
buf := new(bytes.Buffer)
if _, err := buf.ReadFrom(httpResp.Body); err != nil {
resp.Error = fmt.Sprintf("failed to read response body: %v", err)
return resp
}
resp.Body = buf.String()
if strings.Contains(httpResp.Header.Get("Content-Type"), "application/json") {
var jsonData map[string]interface{}
if err := json.Unmarshal(buf.Bytes(), &jsonData); err == nil {
resp.JSON = jsonData
}
}
return resp
}
func validateURL(urlStr string) error {
parsedURL, err := url.Parse(urlStr)
if err != nil {
return fmt.Errorf("invalid URL: %v", err)
}
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
return fmt.Errorf("only http and https protocols are allowed")
}
host := parsedURL.Hostname()
if host == "" {
return fmt.Errorf("empty host")
}
if isPrivateIP(host) {
return fmt.Errorf("access to private IP addresses is not allowed")
}
if isLocalhost(host) {
return fmt.Errorf("access to localhost is not allowed")
}
return nil
}
func isLocalhost(host string) bool {
lowerHost := strings.ToLower(host)
return lowerHost == "localhost" || lowerHost == "127.0.0.1" || lowerHost == "::1"
}
func isPrivateIP(host string) bool {
ip := net.ParseIP(host)
if ip == nil {
return false
}
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
return true
}
privateBlocks := []string{
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"169.254.0.0/16",
"127.0.0.0/8",
"::1/128",
"fc00::/7",
"fe80::/10",
}
for _, block := range privateBlocks {
_, cidr, _ := net.ParseCIDR(block)
if cidr != nil && cidr.Contains(ip) {
return true
}
}
return false
}
+162
View File
@@ -0,0 +1,162 @@
package smtputil
import (
"crypto/tls"
"fmt"
"net/smtp"
"strings"
"time"
)
type SMTPConfig struct {
Host string
Port int
Username string
Password string
From string
UseTLS bool
}
type EmailMessage struct {
To []string
Subject string
Body string
HTML bool
}
type SMTPResult struct {
Success bool `json:"success"`
Error string `json:"error,omitempty"`
}
type SMTPClient struct {
config SMTPConfig
timeout time.Duration
}
func NewSMTPClient(config SMTPConfig, timeout time.Duration) *SMTPClient {
if timeout <= 0 {
timeout = 30 * time.Second
}
return &SMTPClient{
config: config,
timeout: timeout,
}
}
func (c *SMTPClient) Send(msg EmailMessage) *SMTPResult {
result := &SMTPResult{Success: false}
if len(msg.To) == 0 {
result.Error = "收件人不能为空"
return result
}
if c.config.Host == "" {
result.Error = "SMTP服务器地址不能为空"
return result
}
addr := fmt.Sprintf("%s:%d", c.config.Host, c.config.Port)
var auth smtp.Auth
if c.config.Username != "" && c.config.Password != "" {
auth = smtp.PlainAuth("", c.config.Username, c.config.Password, c.config.Host)
}
from := c.config.From
if from == "" {
from = c.config.Username
}
contentType := "text/plain"
if msg.HTML {
contentType = "text/html"
}
headers := make(map[string]string)
headers["From"] = from
headers["To"] = strings.Join(msg.To, ", ")
headers["Subject"] = msg.Subject
headers["MIME-Version"] = "1.0"
headers["Content-Type"] = fmt.Sprintf("%s; charset=UTF-8", contentType)
headers["Date"] = time.Now().Format(time.RFC1123Z)
var message strings.Builder
for k, v := range headers {
message.WriteString(fmt.Sprintf("%s: %s\r\n", k, v))
}
message.WriteString("\r\n")
message.WriteString(msg.Body)
if c.config.UseTLS {
tlsConfig := &tls.Config{
InsecureSkipVerify: false,
ServerName: c.config.Host,
}
conn, err := tls.Dial("tcp", addr, tlsConfig)
if err != nil {
result.Error = fmt.Sprintf("TLS连接失败: %v", err)
return result
}
defer conn.Close()
client, err := smtp.NewClient(conn, c.config.Host)
if err != nil {
result.Error = fmt.Sprintf("创建SMTP客户端失败: %v", err)
return result
}
defer client.Close()
if auth != nil {
if err := client.Auth(auth); err != nil {
result.Error = fmt.Sprintf("SMTP认证失败: %v", err)
return result
}
}
if err := client.Mail(from); err != nil {
result.Error = fmt.Sprintf("设置发件人失败: %v", err)
return result
}
for _, to := range msg.To {
if err := client.Rcpt(to); err != nil {
result.Error = fmt.Sprintf("设置收件人失败: %v", err)
return result
}
}
w, err := client.Data()
if err != nil {
result.Error = fmt.Sprintf("准备邮件数据失败: %v", err)
return result
}
_, err = w.Write([]byte(message.String()))
if err != nil {
result.Error = fmt.Sprintf("写入邮件内容失败: %v", err)
return result
}
if err := w.Close(); err != nil {
result.Error = fmt.Sprintf("关闭邮件写入失败: %v", err)
return result
}
if err := client.Quit(); err != nil {
result.Error = fmt.Sprintf("关闭SMTP连接失败: %v", err)
return result
}
} else {
err := smtp.SendMail(addr, auth, from, msg.To, []byte(message.String()))
if err != nil {
result.Error = fmt.Sprintf("发送邮件失败: %v", err)
return result
}
}
result.Success = true
return result
}
+31
View File
@@ -0,0 +1,31 @@
package main
import (
"log"
"verification-platform-backend/internal/database"
"verification-platform-backend/internal/model"
)
func main() {
database.Init()
var codes []model.DynamicCode
if err := database.DB.Limit(2).Find(&codes).Error; err != nil {
log.Fatal("查询失败:", err)
}
for _, code := range codes {
log.Printf("=== %s (key: %s) ===", code.Name, code.Key)
log.Printf("代码长度: %d", len(code.Code))
log.Printf("代码前200字符:\n%s", code.Code[:min(200, len(code.Code))])
log.Println()
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
+43
View File
@@ -0,0 +1,43 @@
package main
import (
"fmt"
"log"
"strings"
"verification-platform-backend/internal/database"
"verification-platform-backend/internal/model"
)
func main() {
database.Init()
var code model.DynamicCode
if err := database.DB.Where("key = ?", "test_push").First(&code).Error; err != nil {
log.Fatal("查询失败:", err)
}
fmt.Printf("ID: %d\n", code.ID)
fmt.Printf("Name: %s\n", code.Name)
fmt.Printf("Code length: %d\n", len(code.Code))
fmt.Printf("Contains newline: %v\n", strings.Contains(code.Code, "\n"))
fmt.Printf("Contains \\n literal: %v\n", strings.Contains(code.Code, "\\n"))
fmt.Printf("\n=== Raw Code ===\n")
fmt.Printf("%s\n", code.Code)
fmt.Printf("\n=== Code bytes (first 100) ===\n")
for i, b := range []byte(code.Code) {
if i >= 100 {
break
}
if b == '\n' {
fmt.Printf("[\\n]")
} else if b == '\r' {
fmt.Printf("[\\r]")
} else if b >= 32 && b < 127 {
fmt.Printf("%c", b)
} else {
fmt.Printf("[%d]", b)
}
}
fmt.Println()
}
+506
View File
@@ -0,0 +1,506 @@
package main
import (
"log"
"verification-platform-backend/internal/database"
"verification-platform-backend/internal/model"
"gorm.io/gorm"
)
func main() {
database.Init()
var app model.Application
if err := database.DB.First(&app).Error; err != nil {
if err == gorm.ErrRecordNotFound {
log.Fatal("没有找到任何应用,请先创建应用")
}
log.Fatal("查询应用失败:", err)
}
log.Printf("使用应用: %s (ID: %d)", app.Name, app.ID)
var existingVar model.CloudVariable
err := database.DB.Where("app_id = ? AND key = ?", app.ID, "orders").First(&existingVar).Error
if err == gorm.ErrRecordNotFound {
ordersVar := model.CloudVariable{
UserID: app.UserID,
AppID: &app.ID,
Key: "orders",
DefaultValue: "",
VarType: "stream",
MaxRecords: 10000,
Scope: "app",
WritePermission: "app_user",
Description: "订单记录存储",
Status: "active",
}
if err := database.DB.Create(&ordersVar).Error; err != nil {
log.Fatal("创建订单变量失败:", err)
}
log.Println("创建订单变量成功: orders")
} else {
log.Println("订单变量已存在: orders")
}
functions := []struct {
Name string
Key string
Description string
Code string
}{
{
Name: "测试推送",
Key: "test_push",
Description: "测试Telegram推送是否正常",
Code: `var telegramToken = params.telegram_token || "";
var chatId = params.chat_id || "";
if (!telegramToken || !chatId) {
return { error: "缺少telegram_token或chat_id参数" };
}
var msg = "用户: " + (params.username || "未知") + "\n";
msg += "程序: " + (params.app || "未知") + "\n";
msg += "内容: 信息接收测试, 接收到本消息则表明测试正常.";
var url = "https://api.telegram.org/bot" + telegramToken + "/sendMessage";
var result = http.post(url, {
"Content-Type": "application/json"
}, {
chat_id: chatId,
text: msg
});
if (result.error) {
return { success: false, error: result.error };
}
return { success: true, data: result.json };`,
},
{
Name: "读取订单",
Key: "read_order",
Description: "读取并存储订单记录,推送到Telegram",
Code: `var telegramToken = params.telegram_token || "";
var chatId = params.chat_id || "";
var order = params.order || {};
if (!order.TID) {
return { error: "缺少订单号TID" };
}
var msg = "用户: " + (params.username || "未知") + "\n";
msg += "程序: " + (params.app || "未知") + "\n";
msg += "类型: 读取注单\n";
msg += "单号: " + (order.TID || "") + "\n";
msg += "时间: " + (order.DATETIME || "") + "\n";
msg += "会员: " + (order.NAME0 || "") + "\n";
msg += "球种: " + (order.GT || "") + "\n";
msg += "赛段: " + (order.SESSION || "") + "\n";
msg += "玩法: " + (order.WAGERS_TYPE || "") + "\n";
if (order.SRV_IP === "手机") {
msg += "手机\n";
}
msg += "联盟: " + (order.LEAGUE || "") + "\n";
msg += "队伍: " + (order.TEAM_H || "") + " : " + (order.TEAM_C || "") + "\n";
msg += "比分: " + (order.SCORE || "") + "\n";
msg += "盘口: " + (order.ORDER_CON || "") + "\n";
msg += "下注: " + (order.ORDER_TYPE || "") + "\n";
msg += "赔率: " + (order.IORATIO || "") + "\n";
msg += "金额: " + (order.GOLD || "") + "\n";
msg += "开赛时间: " + (order.G_TIME || "");
var telegramResult = null;
if (telegramToken && chatId) {
var url = "https://api.telegram.org/bot" + telegramToken + "/sendMessage";
telegramResult = http.post(url, {
"Content-Type": "application/json"
}, {
chat_id: chatId,
text: msg
});
}
return {
action: "add_record",
variable_key: "orders",
record_data: order,
message: msg,
telegram_result: telegramResult
};`,
},
{
Name: "接收订单",
Key: "accept_order",
Description: "接收订单通知",
Code: `var telegramToken = params.telegram_token || "";
var chatId = params.chat_id || "";
var order = params.order || {};
var msg = "用户: " + (params.username || "未知") + "\n";
msg += "程序: " + (params.app || "未知") + "\n";
msg += "类型: 接收注单\n";
msg += "单号: " + (order.TID || "") + "\n";
msg += "时间: " + (order.DATETIME || "") + "\n";
msg += "会员: " + (order.NAME0 || "") + "\n";
msg += "球种: " + (order.GT || "") + "\n";
msg += "赛段: " + (order.SESSION || "") + "\n";
msg += "玩法: " + (order.WAGERS_TYPE || "") + "\n";
if (order.SRV_IP === "手机") {
msg += "手机\n";
}
msg += "联盟: " + (order.LEAGUE || "") + "\n";
msg += "队伍: " + (order.TEAM_H || "") + " : " + (order.TEAM_C || "") + "\n";
msg += "比分: " + (order.SCORE || "") + "\n";
msg += "盘口: " + (order.ORDER_CON || "") + "\n";
msg += "下注: " + (order.ORDER_TYPE || "") + "\n";
msg += "赔率: " + (order.IORATIO || "") + "\n";
msg += "金额: " + (order.GOLD || "") + "\n";
msg += "开赛时间: " + (order.G_TIME || "");
var telegramResult = null;
if (telegramToken && chatId) {
var url = "https://api.telegram.org/bot" + telegramToken + "/sendMessage";
telegramResult = http.post(url, {
"Content-Type": "application/json"
}, {
chat_id: chatId,
text: msg
});
}
return {
action: "add_record",
variable_key: "orders",
record_data: order,
message: msg,
telegram_result: telegramResult
};`,
},
{
Name: "筛选订单",
Key: "screen_order",
Description: "筛选订单通知",
Code: `var telegramToken = params.telegram_token || "";
var chatId = params.chat_id || "";
var order = params.order || {};
var msg = "用户: " + (params.username || "未知") + "\n";
msg += "程序: " + (params.app || "未知") + "\n";
msg += "类型: 筛选注单\n";
msg += "单号: " + (order.TID || "") + "\n";
msg += "时间: " + (order.DATETIME || "") + "\n";
msg += "会员: " + (order.NAME0 || "") + "\n";
msg += "球种: " + (order.GT || "") + "\n";
msg += "赛段: " + (order.SESSION || "") + "\n";
msg += "玩法: " + (order.WAGERS_TYPE || "") + "\n";
if (order.SRV_IP === "手机") {
msg += "手机\n";
}
msg += "联盟: " + (order.LEAGUE || "") + "\n";
msg += "队伍: " + (order.TEAM_H || "") + " : " + (order.TEAM_C || "") + "\n";
msg += "比分: " + (order.SCORE || "") + "\n";
msg += "最新比分: " + (order.SCORE_NEW || "") + "\n";
msg += "盘口: " + (order.ORDER_CON || "") + "\n";
msg += "最新盘口: " + (order.ORDER_CON_NEW || "") + "\n";
msg += "下注: " + (order.ORDER_TYPE || "") + "\n";
msg += "赔率: " + (order.IORATIO || "") + "\n";
msg += "最新赔率: " + (order.IORATIO_NEW || "") + "\n";
msg += "金额: " + (order.GOLD || "") + "\n";
msg += "开赛时间: " + (order.G_TIME || "") + "\n";
msg += "过滤原因: " + (order.REASON || "");
var telegramResult = null;
if (telegramToken && chatId) {
var url = "https://api.telegram.org/bot" + telegramToken + "/sendMessage";
telegramResult = http.post(url, {
"Content-Type": "application/json"
}, {
chat_id: chatId,
text: msg
});
}
return {
action: "add_record",
variable_key: "orders",
record_data: order,
message: msg,
telegram_result: telegramResult
};`,
},
{
Name: "跟单成功",
Key: "bet_order_success",
Description: "跟单成功通知",
Code: `var telegramToken = params.telegram_token || "";
var chatId = params.chat_id || "";
var order = params.order || {};
var msg = "用户: " + (params.username || "未知") + "\n";
msg += "程序: " + (params.app || "未知") + "\n";
msg += "类型: 跟单成功\n";
msg += "单号: " + (order.TID || "") + "\n";
msg += "时间: " + (order.DATETIME || "") + "\n";
msg += "会员: " + (order.NAME0 || "") + "\n";
msg += "球种: " + (order.GT || "") + "\n";
msg += "赛段: " + (order.SESSION || "") + "\n";
msg += "玩法: " + (order.WAGERS_TYPE || "") + "\n";
if (order.SRV_IP === "手机") {
msg += "手机\n";
}
msg += "联盟: " + (order.LEAGUE || "") + "\n";
msg += "队伍: " + (order.TEAM_H || "") + " : " + (order.TEAM_C || "") + "\n";
msg += "比分: " + (order.SCORE || "") + "\n";
msg += "最新比分: " + (order.SCORE_NEW || "") + "\n";
msg += "盘口: " + (order.ORDER_CON || "") + "\n";
msg += "最新盘口: " + (order.ORDER_CON_NEW || "") + "\n";
msg += "下注: " + (order.ORDER_TYPE || "") + "\n";
msg += "赔率: " + (order.IORATIO || "") + "\n";
msg += "最新赔率: " + (order.IORATIO_NEW || "") + "\n";
msg += "金额: " + (order.GOLD || "") + "\n";
msg += "开赛时间: " + (order.G_TIME || "");
var telegramResult = null;
if (telegramToken && chatId) {
var url = "https://api.telegram.org/bot" + telegramToken + "/sendMessage";
telegramResult = http.post(url, {
"Content-Type": "application/json"
}, {
chat_id: chatId,
text: msg
});
}
return {
action: "add_record",
variable_key: "orders",
record_data: order,
message: msg,
telegram_result: telegramResult
};`,
},
{
Name: "跟单失败",
Key: "bet_order_failed",
Description: "跟单失败通知",
Code: `var telegramToken = params.telegram_token || "";
var chatId = params.chat_id || "";
var order = params.order || {};
var msg = "用户: " + (params.username || "未知") + "\n";
msg += "程序: " + (params.app || "未知") + "\n";
msg += "类型: 跟单失败\n";
msg += "单号: " + (order.TID || "") + "\n";
msg += "时间: " + (order.DATETIME || "") + "\n";
msg += "会员: " + (order.NAME0 || "") + "\n";
msg += "球种: " + (order.GT || "") + "\n";
msg += "赛段: " + (order.SESSION || "") + "\n";
msg += "玩法: " + (order.WAGERS_TYPE || "") + "\n";
if (order.SRV_IP === "手机") {
msg += "手机\n";
}
msg += "联盟: " + (order.LEAGUE || "") + "\n";
msg += "队伍: " + (order.TEAM_H || "") + " : " + (order.TEAM_C || "") + "\n";
msg += "比分: " + (order.SCORE || "") + "\n";
msg += "最新比分: " + (order.SCORE_NEW || "") + "\n";
msg += "盘口: " + (order.ORDER_CON || "") + "\n";
msg += "最新盘口: " + (order.ORDER_CON_NEW || "") + "\n";
msg += "下注: " + (order.ORDER_TYPE || "") + "\n";
msg += "赔率: " + (order.IORATIO || "") + "\n";
msg += "最新赔率: " + (order.IORATIO_NEW || "") + "\n";
msg += "金额: " + (order.GOLD || "") + "\n";
msg += "开赛时间: " + (order.G_TIME || "") + "\n";
msg += "失败原因: " + (order.REASON || "");
var telegramResult = null;
if (telegramToken && chatId) {
var url = "https://api.telegram.org/bot" + telegramToken + "/sendMessage";
telegramResult = http.post(url, {
"Content-Type": "application/json"
}, {
chat_id: chatId,
text: msg
});
}
return {
action: "add_record",
variable_key: "orders",
record_data: order,
message: msg,
telegram_result: telegramResult
};`,
},
{
Name: "账户掉线",
Key: "account_disconnected",
Description: "账户掉线通知",
Code: `var telegramToken = params.telegram_token || "";
var chatId = params.chat_id || "";
var msg = "用户: " + (params.username || "未知") + "\n";
msg += "程序: " + (params.app || "未知") + "\n";
msg += "类型: 账户掉线";
var telegramResult = null;
if (telegramToken && chatId) {
var url = "https://api.telegram.org/bot" + telegramToken + "/sendMessage";
telegramResult = http.post(url, {
"Content-Type": "application/json"
}, {
chat_id: chatId,
text: msg
});
}
return { message: msg, telegram_result: telegramResult };`,
},
{
Name: "账户重连",
Key: "account_reconnected",
Description: "账户重连通知",
Code: `var telegramToken = params.telegram_token || "";
var chatId = params.chat_id || "";
var msg = "用户: " + (params.username || "未知") + "\n";
msg += "程序: " + (params.app || "未知") + "\n";
msg += "类型: 账户重连";
var telegramResult = null;
if (telegramToken && chatId) {
var url = "https://api.telegram.org/bot" + telegramToken + "/sendMessage";
telegramResult = http.post(url, {
"Content-Type": "application/json"
}, {
chat_id: chatId,
text: msg
});
}
return { message: msg, telegram_result: telegramResult };`,
},
{
Name: "测试邮件",
Key: "test_email",
Description: "测试SMTP邮件发送",
Code: `var smtpConfig = params.smtp_config || {};
var to = params.to || [];
if (!smtpConfig.host || !smtpConfig.username || !smtpConfig.password) {
return { error: "缺少SMTP配置参数" };
}
if (to.length === 0) {
return { error: "缺少收件人地址" };
}
var subject = params.subject || "测试邮件";
var body = params.body || "这是一封测试邮件";
var html = params.html || false;
var result = smtp.send(smtpConfig, to, subject, body, html);
if (result.success) {
return { success: true, message: "邮件发送成功" };
} else {
return { success: false, error: result.error };
}`,
},
{
Name: "订单邮件通知",
Key: "order_email_notify",
Description: "订单邮件通知,同时发送Telegram和邮件",
Code: `var telegramToken = params.telegram_token || "";
var chatId = params.chat_id || "";
var smtpConfig = params.smtp_config || {};
var to = params.to || [];
var order = params.order || {};
if (!order.TID) {
return { error: "缺少订单号TID" };
}
var msg = "用户: " + (params.username || "未知") + "\n";
msg += "程序: " + (params.app || "未知") + "\n";
msg += "类型: 订单通知\n";
msg += "单号: " + (order.TID || "") + "\n";
msg += "时间: " + (order.DATETIME || "") + "\n";
msg += "会员: " + (order.NAME0 || "") + "\n";
msg += "金额: " + (order.GOLD || "") + "\n";
var telegramResult = null;
if (telegramToken && chatId) {
var url = "https://api.telegram.org/bot" + telegramToken + "/sendMessage";
telegramResult = http.post(url, {
"Content-Type": "application/json"
}, {
chat_id: chatId,
text: msg
});
}
var emailResult = null;
if (smtpConfig.host && to.length > 0) {
var htmlBody = "<h2>订单通知</h2>";
htmlBody += "<p><strong>用户:</strong> " + (params.username || "未知") + "</p>";
htmlBody += "<p><strong>程序:</strong> " + (params.app || "未知") + "</p>";
htmlBody += "<p><strong>单号:</strong> " + (order.TID || "") + "</p>";
htmlBody += "<p><strong>时间:</strong> " + (order.DATETIME || "") + "</p>";
htmlBody += "<p><strong>会员:</strong> " + (order.NAME0 || "") + "</p>";
htmlBody += "<p><strong>金额:</strong> " + (order.GOLD || "") + "</p>";
emailResult = smtp.send(smtpConfig, to, "订单通知 - " + (order.TID || ""), htmlBody, true);
}
return {
action: "add_record",
variable_key: "orders",
record_data: order,
message: msg,
telegram_result: telegramResult,
email_result: emailResult
};`,
},
}
for _, fn := range functions {
var existingFn model.DynamicCode
err := database.DB.Unscoped().Where("application_id = ? AND key = ?", app.ID, fn.Key).First(&existingFn).Error
if err == nil {
database.DB.Unscoped().Delete(&existingFn)
log.Printf("删除旧的云端函数: %s (key: %s)", fn.Name, fn.Key)
}
log.Printf("创建云端函数: %s (key: %s)", fn.Name, fn.Key)
dynamicCode := model.DynamicCode{
ApplicationID: app.ID,
UserID: &app.UserID,
Name: fn.Name,
Key: fn.Key,
Code: fn.Code,
Description: fn.Description,
Status: "active",
}
if err := database.DB.Create(&dynamicCode).Error; err != nil {
log.Printf("创建云端函数失败 %s: %v", fn.Name, err)
}
}
log.Println("\n=== 创建完成 ===")
log.Println("云端变量: orders (记录类型)")
log.Println("云端函数:")
log.Println(" - test_push: 测试推送")
log.Println(" - read_order: 读取订单")
log.Println(" - accept_order: 接收订单")
log.Println(" - screen_order: 筛选订单")
log.Println(" - bet_order_success: 跟单成功")
log.Println(" - bet_order_failed: 跟单失败")
log.Println(" - account_disconnected: 账户掉线")
log.Println(" - account_reconnected: 账户重连")
log.Println(" - test_email: 测试邮件")
log.Println(" - order_email_notify: 订单邮件通知")
}