Compare commits

..

28 Commits
main ... docs

Author SHA1 Message Date
longpeng
69b0a4cd83 更新Swagger配置,修改Host和BasePath 2025-06-27 14:39:32 +08:00
longpeng
e1beee6478 用于测试swagger的接口 2025-06-27 12:10:54 +08:00
longpeng
a2bf792823 test 2025-06-27 10:48:59 +08:00
longpeng
b280c4db97 swagger文档 2025-06-27 10:30:48 +08:00
longpeng
eba56bc756 上下文传递 2025-06-27 01:01:58 +08:00
longpeng
4dcbef8307 增强API测试逻辑,新增响应头处理,优化代码格式 2025-06-25 18:43:06 +08:00
longpeng
3e1de3d671 修复API测试用例执行函数中的请求体处理逻辑,将请求体类型从bytes更改为str,并在POST请求中解析JSON数据 2025-06-25 15:49:50 +08:00
longpeng
6231ef37bf 更新活动名称以遵循命名规范,增强API测试参数验证逻辑,调整任务队列名称以保持一致性 2025-06-25 15:27:45 +08:00
longpeng
a299cf384c 更新任务队列名称,将 Python 和 Go Worker 的任务队列分别改为 "python-task-queue" 和 "data-task-queue" 以保持一致性 2025-06-25 14:53:07 +08:00
longpeng
ae42336160 重构配置结构,新增 Temporal 配置支持,优化 OpenAI 配置命名,新增工作流相关路由和服务逻辑 2025-06-25 14:49:45 +08:00
longpeng
182bbbd215 重构复合案例相关逻辑,新增DAO层方法以简化数据库操作,优化活动加载步骤的实现 2025-06-25 11:03:00 +08:00
longpeng
6b4350b915 新增步骤类型与Activity函数名映射工具,更新复合案例模型字段以支持ActivityName,并调整服务层逻辑适配映射功能 2025-06-25 00:32:51 +08:00
longpeng
bc52b82b93 更新复合案例模型字段名称,将 StepConfig 重命名为 ParametersJson 以提高字段表达清晰度和一致性 2025-06-24 23:09:01 +08:00
longpeng
71eb131e58 更新 DynamicTestSuiteWorkflow,完善中文注释,新增步骤加载、排序及动态执行逻辑,支持条件跳转与错误处理 2025-06-24 22:49:05 +08:00
longpeng
1cf5e37f29 暂存 2025-06-24 22:10:31 +08:00
longpeng
950d73ade4 更新.gitignore文件,新增对.sql和.yaml文件的忽略规则 2025-06-24 12:14:49 +08:00
longpeng
8f073eb9d1 添加复合案例服务日志记录,改进事务处理和错误捕获逻辑,完善处理器与服务初始化 2025-06-24 12:14:06 +08:00
longpeng
e246f2fc06 新增复合案例管理,包括模型定义、服务层逻辑及相关API接口 2025-06-24 11:36:57 +08:00
longpeng
4a14eb3ef0 引入HTTP客户端封装库以简化请求处理并提升代码复用性 2025-06-20 20:00:29 +08:00
longpeng
61afb0a8db 完善TestRunWorkflow中文注释,增加详细参数说明及逻辑注解以提升代码可读性 2025-06-20 19:33:50 +08:00
longpeng
bf5c5602ed 重构代码生成目录结构以优化Go和Python的Protobuf文件管理 2025-06-20 19:28:34 +08:00
longpeng
2a792eb0bc 重构代码生成目录结构以优化Go和Python的Protobuf文件管理 2025-06-20 19:27:24 +08:00
longpeng
6bb795ab7f 将_heartbeat_task方法改为静态方法以优化其调用方式 2025-06-20 17:48:56 +08:00
longpeng
c4d6e553ed 将注释翻译为中文以提高代码可读性 2025-06-20 17:44:54 +08:00
longpeng
9aa99178ab 将注释翻译为中文以提高代码可读性 2025-06-20 17:42:42 +08:00
longpeng
c50d6f1771 为API和UI测试活动引入心跳机制,以增强Temporal任务的健壮性 2025-06-20 16:28:33 +08:00
longpeng
fa70f376f4 重构工作流活动选项初始化,以实现更清晰的上下文处理 2025-06-20 16:28:18 +08:00
longpeng
e9336e1af4 go与python的最佳实践 2025-06-20 15:44:01 +08:00
49 changed files with 6409 additions and 234 deletions

7
.gitignore vendored
View File

@ -180,5 +180,8 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
*/gen
*/*/gen
*pb2.py*
*pb.go
*.sql
*.yaml

View File

@ -12,22 +12,22 @@ py: gen_py
gen_go:
@echo "Generating Go Protobuf code..."
mkdir -p server/gen/pb
mkdir -p pkg/pb
protoc --proto_path=proto \
--go_out=server/gen/pb \
--go_out=pkg/pb \
--go_opt=paths=source_relative \
proto/*.proto
gen_py:
@echo "Generating Python Protobuf code..."
mkdir -p worker/gen
mkdir -p workers/python/pb
python3 -m grpc_tools.protoc \
--proto_path=proto \
--python_out=worker/gen \
--pyi_out=worker/gen \
--python_out=workers/python/pb \
--pyi_out=workers/python/pb \
proto/*.proto
clean:
@echo "Cleaning generated files..."
rm -rf server/gen
rm -rf worker/gen
rm -rf pkg/pb
rm -rf workers/python/pb

View File

@ -1,2 +0,0 @@
# Beacon

71
activities/activities.go Normal file
View File

@ -0,0 +1,71 @@
package activities
import (
"beacon/pkg/dao/mysql"
"beacon/pkg/pb"
"context"
"fmt"
"go.temporal.io/sdk/activity"
)
// LoadCompositeCaseStepsActivity 用于加载复合案例步骤的Activity结构
// LoadCompositeCaseSteps 加载复合案例步骤定义
func LoadCompositeCaseSteps(ctx context.Context, compositeCaseId string) ([]*pb.CompositeCaseStepDefinition, error) {
// 获取 Activity 日志记录器
logger := activity.GetLogger(ctx)
logger.Info("Loading composite case steps", "compositeCaseId", compositeCaseId)
// 参数验证
if compositeCaseId == "" {
return nil, fmt.Errorf("compositeCaseId cannot be empty")
}
// 发送心跳信号,表明 Activity 正在运行
activity.RecordHeartbeat(ctx)
// 通过DAO从数据库加载复合案例步骤数据
steps, err := dao.GetCompositeCaseSteps(compositeCaseId)
if err != nil {
logger.Error("Failed to load composite case steps from database", "error", err)
return nil, fmt.Errorf("failed to load composite case steps: %w", err)
}
// 如果没有找到步骤,返回空切片而不是错误
if len(steps) == 0 {
logger.Warn("No steps found for composite case", "compositeCaseId", compositeCaseId)
return []*pb.CompositeCaseStepDefinition{}, nil
}
// 转换数据库模型为 Protobuf 结构
var pbSteps []*pb.CompositeCaseStepDefinition
for _, step := range steps {
pbStep := &pb.CompositeCaseStepDefinition{
StepId: int64(step.ID),
StepOrder: int32(step.StepOrder),
StepType: step.StepType,
ActivityName: step.ActivityName,
ParametersJson: step.ParametersJson,
//SuccessNextStepOrder: convertToInt32Ptr(step.SuccessNextStepOrder),
//FailureNextStepOrder: convertToInt32Ptr(step.FailureNextStepOrder),
//IsParallel: step.IsParallel,
//DependsOnStepIds: step.DependsOnStepIds,
//ContinueOnFailure: step.ContinueOnFailure,
//TimeoutSeconds: int32(step.TimeoutSeconds),
//RetryCount: int32(step.RetryCount),
//Description: step.Description,
//CreatedAt: step.CreatedAt.Unix(),
//UpdatedAt: step.UpdatedAt.Unix(),
}
pbSteps = append(pbSteps, pbStep)
}
// 再次发送心跳信号
activity.RecordHeartbeat(ctx)
logger.Info("Successfully loaded composite case steps",
"compositeCaseId", compositeCaseId,
"stepCount", len(pbSteps))
return pbSteps, nil
}

46
activities/openai.go Normal file
View File

@ -0,0 +1,46 @@
package activities
import (
"beacon/pkg/openai"
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type OpenAI struct {
}
func (s OpenAI) Create(ctx *gin.Context) {
file, err := ctx.FormFile("file")
if err != nil {
zap.L().Info(fmt.Sprintf("获取文件失败:%s", err.Error()))
ctx.JSON(http.StatusBadRequest, gin.H{"message": err.Error()})
return
}
//err, data := openai.GetGeminiAICaptcha(file)
//if err != nil {
// ctx.JSON(http.StatusOK, gin.H{
// "message": "success",
// "data": err.Error(),
// })
// return
//}
err, data := openai.GetOllamaAICaptcha(file)
if err != nil {
zap.L().Info(fmt.Sprintf("GetOllamaAICaptcha %s", err.Error()))
ctx.JSON(http.StatusOK, gin.H{
"message": "success",
"data": err.Error(),
})
return
}
ctx.JSON(http.StatusOK, gin.H{
"message": "success",
"data": data,
})
}

198
config/default.go Executable file
View File

@ -0,0 +1,198 @@
package config
import (
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
"strings"
"sync"
)
// Conf 定义全局的变量
var Conf = new(SrvConfig)
// var ConfLock = new(sync.RWMutex) // 配置读写锁,高并发情况下保证获取的是最新的配置
// viper.GetXxx()读取的方式
// 注意:
// Viper使用的是 `mapstructure`
// SrvConfig 服务配置
type SrvConfig struct {
Name string `mapstructure:"name"`
Mode string `mapstructure:"mode"`
Version string `mapstructure:"version"`
IP string `mapstructure:"ip"`
Port int `mapstructure:"port"`
Pid string `mapstructure:"pid"`
*LogConfig `mapstructure:"log"`
*OCRConfig `mapstructure:"ocr"`
*MinIoConfig `mapstructure:"minio"`
*OpenCVConfig `mapstructure:"opencv"`
*MySQLConfig `mapstructure:"mysql"`
*SnowflakeConfig `mapstructure:"snowflake"`
*GrpcConfig `mapstructure:"grpc"`
*ApolloConfig `mapstructure:"apollo"`
*Registrar `mapstructure:"registrar"`
*RcloneConfig `mapstructure:"rclone"`
*OpenAIConfig `mapstructure:"openai"`
*TemporalConfig `mapstructure:"temporal"` // Temporal配置
}
type LogConfig struct {
Level string `mapstructure:"level"`
Filename string `mapstructure:"filename"`
FilenameErr string `mapstructure:"filename_err"`
MaxSize int `mapstructure:"max_size"`
MaxAge int `mapstructure:"max_age"`
MaxBackups int `mapstructure:"max_backups"`
}
type ApolloConfig struct {
AppID string `mapstructure:"app_id"`
Cluster string `mapstructure:"cluster"` // 环境
NameSpaceNames []string `mapstructure:"name_spaceNames"`
MetaAddr string `mapstructure:"meta_addr"` // 配置中心地址
AccesskeySecret string `mapstructure:"access_key_secret"`
}
type OCRConfig struct {
URL string `mapstructure:"url"`
Det bool `mapstructure:"det"`
Rec bool `mapstructure:"rec"`
Mode int `mapstructure:"mode"`
Addr string `mapstructure:"addr"`
}
type MinIoConfig struct {
Endpoint string `mapstructure:"endpoint"`
AccessKeyId string `mapstructure:"access_key_id"`
SecretAccessKey string `mapstructure:"secret_access_key"`
Secure bool `mapstructure:"secure"`
BucketName string `mapstructure:"bucket_name"`
}
type OpenCVConfig struct {
Threshold float32 `mapstructure:"threshold"`
}
type MySQLConfig struct {
Host string `mapstructure:"host"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
DB string `mapstructure:"dbname"`
Port int `mapstructure:"port"`
MaxOpenConns int `mapstructure:"max_open_conns"`
MaxIdleConns int `mapstructure:"max_idle_conns"`
}
type SnowflakeConfig struct {
StartTime string `mapstructure:"start_time"`
MachineID int64 `mapstructure:"machine_id"`
}
type GrpcConfig struct {
Port int `mapstructure:"port"`
MaxRecvMsgSize int `mapstructure:"max_recv_msg_size"`
MaxSendMsgSize int `mapstructure:"max_send_msg_size"`
}
type Registrar struct {
Enabled bool `mapstructure:"enabled"`
Address []string `mapstructure:"address"`
}
type RcloneConfig struct {
RemoteName string `mapstructure:"remote_name"`
BucketName string `mapstructure:"bucket_name"`
RclonePath string `mapstructure:"rclone_path"` // rclone命令所在位置在全局环境中则填入rclone
Expire int `mapstructure:"expire"` // 公共储存通有效时间s
RcloneConfig string `mapstructure:"rclone_config"` // Rclone配置文件所在目录
CustomDomains string `mapstructure:"custom_domains"` // 自定义域
}
type OpenAIConfig struct {
BaseURL string `mapstructure:"base_url"`
ApiKey string `mapstructure:"api_key"`
Model string `mapstructure:"model"`
Prompt string `mapstructure:"prompt"`
}
type TemporalConfig struct {
Host string `mapstructure:"host"` // Temporal服务地址
Port int `mapstructure:"port"` // Temporal服务端口
}
// Init 整个服务配置文件初始化的方法
func Init(yamlContent string) (err error) {
// 方式1直接指定配置文件路径相对路径或者绝对路径
// 相对路径:相对执行的可执行文件的相对路径
// viper.SetConfigFile("./conf/config.yaml")
viper.SetConfigType("yaml")
err = viper.ReadConfig(strings.NewReader(yamlContent))
if err != nil {
// 读取配置信息失败
fmt.Printf("viper.ReadInConfig failed, err:%v\n", err)
return
}
// 如果使用的是 viper.GetXxx()方式使用配置的话,就无须下面的操作
// 把读取到的配置信息反序列化到 Conf 变量中
if err := viper.Unmarshal(&Conf); err != nil {
fmt.Printf("viper.Unmarshal failed, err:%v\n", err)
}
/*viper.WatchConfig() // 配置文件监听
viper.OnConfigChange(func(in fsnotify.Event) {
ConfLock.Lock() // 获取锁
defer ConfLock.Unlock() // 释放锁
fmt.Println("配置文件修改了...")
if err := viper.Unmarshal(&Conf); err != nil {
fmt.Printf("viper.Unmarshal failed, err:%v\n", err)
}
})*/
/*err, ip := GetNetworkCard([]string{Conf.MetaAddr})
if err != nil {
return err
}
fmt.Println(ip)
Conf.IP = "10.10.1.254"*/
return
}
var ConfLock = new(sync.RWMutex) // 配置读写锁,高并发情况下保证获取的是最新的配置
func InitFromFile(filePath string) (err error) {
// 方式1直接指定配置文件路径相对路径或者绝对路径
// 相对路径:相对执行的可执行文件的相对路径
// viper.SetConfigFile("./conf/config.yaml")
viper.SetConfigFile(filePath)
err = viper.ReadInConfig() // 读取配置信息
if err != nil {
// 读取配置信息失败
fmt.Printf("viper.ReadInConfig failed, err:%v\n", err)
return
}
// 如果使用的是 viper.GetXxx()方式使用配置的话,就无须下面的操作
// 把读取到的配置信息反序列化到 Conf 变量中
if err := viper.Unmarshal(Conf); err != nil {
fmt.Printf("viper.Unmarshal failed, err:%v\n", err)
}
viper.WatchConfig() // 配置文件监听
viper.OnConfigChange(func(in fsnotify.Event) {
ConfLock.Lock() // 获取锁
defer ConfLock.Unlock() // 释放锁
fmt.Println("配置文件修改了...")
if err := viper.Unmarshal(&Conf); err != nil {
fmt.Printf("viper.Unmarshal failed, err:%v\n", err)
}
})
return
}

View File

@ -0,0 +1,255 @@
# Activity 之间参数传递指南
## 概述
在动态测试工作流中,多个 Activity 之间经常需要传递参数。例如,第二个 Activity 可能需要使用第一个 Activity 响应的某个字段作为入参。本文档详细说明了如何实现这种参数传递机制。
## 支持的变量格式
### 1. 全局变量
格式:`${global.key}`
- 用于访问工作流级别的全局变量
- 这些变量在工作流启动时通过 `GlobalParameters` 传入
### 2. 步骤结果变量
格式:`${step.stepId.field}`
- 用于访问之前步骤的执行结果
- `stepId` 是步骤的ID
- `field` 是结果中的具体字段
## 变量引用示例
### API 测试结果变量
```json
{
"test_case_id": "api_test_001",
"endpoint": "/api/login",
"http_method": "POST",
"headers": {
"Content-Type": "application/json"
},
"request_body": "{\"username\": \"testuser\", \"password\": \"testpass\"}",
"expected_status_code": 200
}
```
假设这个API测试的步骤ID是123执行后返回
```json
{
"base_result": {
"success": true,
"message": "API Test Passed"
},
"actual_status_code": 200,
"response_body": "{\"token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\", \"user_id\": 456}",
"headers": {
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"Content-Type": "application/json"
}
}
```
### 可用的变量引用
1. **响应体**`${step.123.response_body}`
2. **状态码**`${step.123.actual_status_code}`
3. **响应头**`${step.123.headers.Authorization}`
4. **JSON字段**`${step.123.json_token}` (自动提取的token字段)
5. **用户ID**`${step.123.json_user_id}` (自动提取的user_id字段)
## 实际使用场景
### 场景1登录后使用Token进行API调用
**步骤1登录API**
```json
{
"test_case_id": "login_test",
"endpoint": "/api/login",
"http_method": "POST",
"headers": {
"Content-Type": "application/json"
},
"request_body": "{\"username\": \"testuser\", \"password\": \"testpass\"}",
"expected_status_code": 200
}
```
**步骤2使用Token调用受保护的API**
```json
{
"test_case_id": "protected_api_test",
"endpoint": "/api/user/profile",
"http_method": "GET",
"headers": {
"Authorization": "${step.123.json_token}",
"Content-Type": "application/json"
},
"expected_status_code": 200
}
```
### 场景2使用响应头中的Token
```json
{
"test_case_id": "api_with_header_token",
"endpoint": "/api/data",
"http_method": "GET",
"headers": {
"Authorization": "${step.123.headers.Authorization}",
"Content-Type": "application/json"
},
"expected_status_code": 200
}
```
### 场景3使用响应体中的用户ID
```json
{
"test_case_id": "user_data_test",
"endpoint": "/api/users/${step.123.json_user_id}/data",
"http_method": "GET",
"headers": {
"Authorization": "${step.123.json_token}",
"Content-Type": "application/json"
},
"expected_status_code": 200
}
```
## UI 测试结果变量
### UI 测试结果示例
```json
{
"base_result": {
"success": true,
"message": "UI Test Passed"
},
"screenshot_url": "https://s3.amazonaws.com/screenshots/test_001.png",
"html_report_url": "https://s3.amazonaws.com/reports/test_001.html"
}
```
### 可用的变量引用
1. **截图URL**`${step.456.screenshot_url}`
2. **报告URL**`${step.456.html_report_url}`
## 全局变量使用
### 工作流启动时传入全局变量
```go
input := &pb.DynamicTestRunInput{
RunId: "test_run_001",
CompositeCaseId: "composite_case_001",
GlobalParameters: map[string]string{
"base_url": "https://api.example.com",
"environment": "staging",
"api_version": "v1",
},
}
```
### 在步骤参数中使用全局变量
```json
{
"test_case_id": "api_test",
"endpoint": "${global.base_url}/${global.api_version}/users",
"http_method": "GET",
"headers": {
"X-Environment": "${global.environment}",
"Content-Type": "application/json"
},
"expected_status_code": 200
}
```
## 复杂场景示例
### 多步骤参数传递链
**步骤1用户注册**
```json
{
"test_case_id": "user_registration",
"endpoint": "/api/register",
"http_method": "POST",
"headers": {
"Content-Type": "application/json"
},
"request_body": "{\"username\": \"newuser\", \"email\": \"newuser@example.com\", \"password\": \"password123\"}",
"expected_status_code": 201
}
```
**步骤2用户登录使用注册返回的用户ID**
```json
{
"test_case_id": "user_login",
"endpoint": "/api/login",
"http_method": "POST",
"headers": {
"Content-Type": "application/json"
},
"request_body": "{\"username\": \"newuser\", \"password\": \"password123\"}",
"expected_status_code": 200
}
```
**步骤3获取用户信息使用登录返回的token**
```json
{
"test_case_id": "get_user_info",
"endpoint": "/api/users/${step.123.json_user_id}",
"http_method": "GET",
"headers": {
"Authorization": "Bearer ${step.124.json_token}",
"Content-Type": "application/json"
},
"expected_status_code": 200
}
```
**步骤4更新用户信息使用步骤3的token和用户ID**
```json
{
"test_case_id": "update_user_info",
"endpoint": "/api/users/${step.123.json_user_id}",
"http_method": "PUT",
"headers": {
"Authorization": "Bearer ${step.124.json_token}",
"Content-Type": "application/json"
},
"request_body": "{\"display_name\": \"Updated User\", \"bio\": \"This is my updated bio\"}",
"expected_status_code": 200
}
```
## 注意事项
1. **变量解析顺序**:先解析全局变量,再解析步骤变量
2. **错误处理**:如果变量不存在,会保持原始占位符不变
3. **类型安全**:所有变量都会被转换为字符串
4. **性能考虑**:变量解析在每次步骤执行时进行,避免频繁的字符串操作
5. **调试支持**:工作流日志会记录变量解析过程
## 最佳实践
1. **使用有意义的变量名**:避免使用过于简单的变量名
2. **文档化变量**:在步骤描述中说明使用的变量
3. **测试变量解析**:在开发环境中测试变量解析是否正确
4. **错误处理**:在步骤参数中提供默认值或错误处理逻辑
5. **版本控制**:记录变量格式的变更,确保向后兼容
## 扩展功能
### 自定义变量提取器
可以通过扩展 `extractApiResultToGlobalVariables``extractUiResultToGlobalVariables` 函数来支持更多自定义字段的提取。
### 条件变量
可以基于步骤的成功/失败状态来设置不同的变量值。
### 变量验证
可以在变量解析时添加验证逻辑,确保必需的变量存在且格式正确。

986
docs/docs.go Normal file
View File

@ -0,0 +1,986 @@
// Package docs Code generated by swaggo/swag. DO NOT EDIT
package docs
import "github.com/swaggo/swag"
const docTemplate = `{
"schemes": {{ marshal .Schemes }},
"swagger": "2.0",
"info": {
"description": "{{escape .Description}}",
"title": "{{.Title}}",
"termsOfService": "http://swagger.io/terms/",
"contact": {
"name": "API Support",
"url": "http://www.swagger.io/support",
"email": "support@swagger.io"
},
"license": {
"name": "Apache 2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
},
"version": "{{.Version}}"
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/composite-cases": {
"get": {
"description": "List composite cases with pagination",
"produces": [
"application/json"
],
"tags": [
"composite"
],
"summary": "List composite cases",
"parameters": [
{
"type": "integer",
"description": "Page number",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "Page size",
"name": "page_size",
"in": "query"
},
{
"type": "string",
"description": "Status",
"name": "status",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
},
"post": {
"description": "Create a new composite case",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"composite"
],
"summary": "Create a composite case",
"parameters": [
{
"description": "Create Composite Case Request",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.CreateCompositeCaseRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/composite-cases/{id}": {
"get": {
"description": "Get a composite case by ID",
"produces": [
"application/json"
],
"tags": [
"composite"
],
"summary": "Get a composite case",
"parameters": [
{
"type": "integer",
"description": "Composite Case ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
},
"put": {
"description": "Update a composite case by ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"composite"
],
"summary": "Update a composite case",
"parameters": [
{
"type": "integer",
"description": "Composite Case ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Update Composite Case Request",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.UpdateCompositeCaseRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
},
"delete": {
"description": "Delete a composite case by ID",
"produces": [
"application/json"
],
"tags": [
"composite"
],
"summary": "Delete a composite case",
"parameters": [
{
"type": "integer",
"description": "Composite Case ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/test/core-transfer": {
"post": {
"description": "Core transfer endpoint for handling core banking transfer operations",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"test"
],
"summary": "Core transfer endpoint",
"parameters": [
{
"description": "Core transfer request body",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.CoreTransfer"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/test/test-endpoint": {
"post": {
"description": "Test endpoint for API functionality verification",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"test"
],
"summary": "Test endpoint",
"parameters": [
{
"description": "Test request body",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.Endpoint"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/workflows/start": {
"post": {
"description": "Start a new workflow",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"workflow"
],
"summary": "Start a workflow",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/workflows/{id}": {
"get": {
"description": "Get the status of a workflow by ID",
"produces": [
"application/json"
],
"tags": [
"workflow"
],
"summary": "Get workflow status",
"parameters": [
{
"type": "string",
"description": "Workflow ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/workflows/{id}/results": {
"get": {
"description": "Get the results of a workflow by ID",
"produces": [
"application/json"
],
"tags": [
"workflow"
],
"summary": "Get workflow results",
"parameters": [
{
"type": "string",
"description": "Workflow ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
}
},
"definitions": {
"handlers.CoreTransfer": {
"type": "object",
"properties": {
"service": {
"type": "object",
"properties": {
"APP_HEAD": {
"type": "object",
"properties": {
"BranchId": {
"type": "string"
},
"TlrNo": {
"type": "string"
},
"array": {
"type": "object",
"properties": {
"AuthTlrInf": {
"type": "object",
"properties": {
"ApprTellerNo": {
"type": "string"
}
}
}
}
}
}
},
"QryRcrdNo": {
"type": "string"
},
"SYS_HEAD": {
"type": "object",
"properties": {
"PrvdSysSeqNo": {
"type": "string"
},
"SvcCd": {
"type": "string"
},
"SvcScn": {
"type": "string"
},
"TranDt": {
"type": "string"
},
"TranRetSt": {
"type": "string"
},
"array": {
"type": "object",
"properties": {
"RetInf": {
"type": "object",
"properties": {
"RetCd": {
"type": "string"
},
"RetMsg": {
"type": "string"
}
}
}
}
}
}
},
"TxnStrtNo": {
"type": "string"
},
"array": {
"type": "array",
"items": {
"type": "object",
"properties": {
"TxnInfArray": {
"type": "object",
"properties": {
"AcctNm": {
"type": "string"
},
"CnclRvrsFlg": {
"type": "string"
},
"CnlNo": {
"type": "string"
},
"CntprAcctNoOrCardNo": {
"type": "string"
},
"ImprtBlkVchrNo": {
"type": "string"
},
"ImprtBlkVchrTp": {
"type": "string"
},
"OrigTxnSeqNo": {
"type": "string"
},
"OthrBnkBnkNo": {
"type": "string"
},
"RmkInf": {
"type": "string"
},
"SubBrId": {
"type": "string"
},
"TxnAfBal": {
"type": "string"
},
"TxnAmt": {
"type": "string"
},
"TxnCcy": {
"type": "string"
},
"TxnCd": {
"type": "string"
},
"TxnSmyDsc": {
"type": "string"
},
"TxnSysDt": {
"type": "string"
},
"TxnSysTm": {
"type": "string"
},
"TxnTlrNo": {
"type": "string"
}
}
}
}
}
}
}
}
}
},
"handlers.Endpoint": {
"type": "object",
"properties": {
"BODY": {
"type": "object",
"properties": {
"BASE_ACCT_NO": {
"type": "string"
},
"CARD_NO": {
"type": "array",
"items": {
"type": "object",
"properties": {
"DOCUMENT_ID": {
"type": "string"
},
"DOCUMENT_TYPE": {
"type": "string"
},
"LINKMAN_NAME": {
"type": "string"
},
"LINKMAN_TYPE": {
"type": "string"
},
"PHONE_NO1": {
"type": "string"
},
"PHONE_NO2": {
"type": "string"
}
}
}
},
"HANG_SEQ_NO": {
"type": "array",
"items": {
"type": "string"
}
},
"SERV_DETAIL": {
"type": "object",
"properties": {
"CHARGE_MODE": {
"type": "string"
},
"DISC_FEE_AMT": {
"type": "string"
},
"DISC_RATE": {
"type": "object",
"properties": {
"CHARGE_MODE": {
"type": "string"
},
"PHONE_NO2": {
"type": "object",
"properties": {
"DOCUMENT_TYPE": {
"type": "array",
"items": {
"type": "object",
"properties": {
"DOCUMENT_ID": {
"type": "object",
"properties": {
"LINKMAN_TYPE": {
"type": "string"
},
"PHONE_NO2": {
"type": "string"
}
}
},
"DOCUMENT_TYPE": {
"type": "string"
},
"LINKMAN_NAME": {
"type": "string"
},
"LINKMAN_TYPE": {
"type": "string"
}
}
}
},
"LINKMAN_NAME": {
"type": "string"
},
"LINKMAN_TYPE": {
"type": "string"
}
}
}
}
},
"DISC_TYPE": {
"type": "string"
},
"FEE_AMT": {
"type": "string"
},
"FEE_TYPE": {
"type": "string"
},
"ORIG_FEE_AMT": {
"type": "string"
}
}
},
"SETTLEMENT_DATE": {
"type": "string"
},
"TRAN_AMT": {
"type": "string"
},
"TRAN_CCY": {
"type": "string"
},
"TRAN_TYPE": {
"type": "string"
}
}
},
"PUB_DOMAIN": {
"type": "object",
"properties": {
"AUTH_FLAG": {
"type": "string"
},
"AUTH_INFO_NUM": {
"type": "string"
},
"AUTH_STATUS": {
"type": "string"
},
"AUTH_TELLER": {
"type": "string"
},
"BRANCH_ID": {
"type": "string"
},
"CHANNEL_CODE": {
"type": "string"
},
"CONFIRM_FLAG": {
"type": "string"
},
"CONFIRM_STATUS": {
"type": "string"
},
"CONSUM_TRAN_DATE": {
"type": "string"
},
"CONSUM_TRAN_TIME": {
"type": "string"
},
"CURR_PAGE_NUM": {
"type": "string"
},
"LEGAL_CODE": {
"type": "string"
},
"PAGE_UP_DOWN": {
"type": "string"
},
"PER_PAGE_NUM": {
"type": "string"
},
"PROVID_TRAN_DATE": {
"type": "string"
},
"PUB_EXTEND": {
"type": "string"
},
"TRAN_TELLER": {
"type": "string"
}
}
},
"SYS_HEAD": {
"type": "object",
"properties": {
"CHARACTER_SET": {
"type": "string"
},
"COMM_TYPE": {
"type": "string"
},
"CONSUM_REQ_DATE": {
"type": "string"
},
"CONSUM_REQ_TIME": {
"type": "string"
},
"CONSUM_SYS_CODE": {
"type": "string"
},
"FILE_FLAG": {
"type": "string"
},
"GLOBAL_SEQ": {
"type": "string"
},
"LOCAL_LANG": {
"type": "string"
},
"PROVID_SYS_CODE": {
"type": "string"
},
"SCENES_CODE": {
"type": "string"
},
"SCENES_VERSION": {
"type": "string"
},
"SERVICE_NAME": {
"type": "string"
},
"SERVICE_REQ_SEQ": {
"type": "string"
},
"SERVICE_VERSION": {
"type": "string"
},
"SRC_ENC_NODE": {
"type": "string"
}
}
}
}
},
"models.CreateCompositeCaseRequest": {
"type": "object",
"required": [
"name"
],
"properties": {
"description": {
"type": "string"
},
"name": {
"type": "string"
},
"status": {
"type": "string"
},
"steps": {
"type": "array",
"items": {
"$ref": "#/definitions/models.CreateCompositeCaseStepRequest"
}
}
}
},
"models.CreateCompositeCaseStepRequest": {
"type": "object",
"required": [
"step_name",
"step_order",
"step_type"
],
"properties": {
"activity_name": {
"type": "string"
},
"is_required": {
"type": "boolean"
},
"parameters_json": {
"type": "string"
},
"step_description": {
"type": "string"
},
"step_name": {
"type": "string"
},
"step_order": {
"type": "integer"
},
"step_type": {
"type": "string"
}
}
},
"models.UpdateCompositeCaseRequest": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"name": {
"type": "string"
},
"status": {
"type": "string"
},
"steps": {
"type": "array",
"items": {
"$ref": "#/definitions/models.UpdateCompositeCaseStepRequest"
}
}
}
},
"models.UpdateCompositeCaseStepRequest": {
"type": "object",
"properties": {
"activity_name": {
"type": "string"
},
"id": {
"type": "integer"
},
"is_required": {
"type": "boolean"
},
"parameters_json": {
"type": "string"
},
"step_description": {
"type": "string"
},
"step_name": {
"type": "string"
},
"step_order": {
"type": "integer"
},
"step_type": {
"type": "string"
}
}
}
}
}`
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
Version: "1.0",
Host: "100.118.61.45:18090",
BasePath: "/v1/api",
Schemes: []string{},
Title: "Beacon API",
Description: "This is a sample server for a beacon.",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
LeftDelim: "{{",
RightDelim: "}}",
}
func init() {
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
}

962
docs/swagger.json Normal file
View File

@ -0,0 +1,962 @@
{
"swagger": "2.0",
"info": {
"description": "This is a sample server for a beacon.",
"title": "Beacon API",
"termsOfService": "http://swagger.io/terms/",
"contact": {
"name": "API Support",
"url": "http://www.swagger.io/support",
"email": "support@swagger.io"
},
"license": {
"name": "Apache 2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
},
"version": "1.0"
},
"host": "localhost:8080",
"basePath": "/api/v1",
"paths": {
"/composite-cases": {
"get": {
"description": "List composite cases with pagination",
"produces": [
"application/json"
],
"tags": [
"composite"
],
"summary": "List composite cases",
"parameters": [
{
"type": "integer",
"description": "Page number",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "Page size",
"name": "page_size",
"in": "query"
},
{
"type": "string",
"description": "Status",
"name": "status",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
},
"post": {
"description": "Create a new composite case",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"composite"
],
"summary": "Create a composite case",
"parameters": [
{
"description": "Create Composite Case Request",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.CreateCompositeCaseRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/composite-cases/{id}": {
"get": {
"description": "Get a composite case by ID",
"produces": [
"application/json"
],
"tags": [
"composite"
],
"summary": "Get a composite case",
"parameters": [
{
"type": "integer",
"description": "Composite Case ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
},
"put": {
"description": "Update a composite case by ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"composite"
],
"summary": "Update a composite case",
"parameters": [
{
"type": "integer",
"description": "Composite Case ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Update Composite Case Request",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.UpdateCompositeCaseRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
},
"delete": {
"description": "Delete a composite case by ID",
"produces": [
"application/json"
],
"tags": [
"composite"
],
"summary": "Delete a composite case",
"parameters": [
{
"type": "integer",
"description": "Composite Case ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/test/core-transfer": {
"post": {
"description": "Core transfer endpoint for handling core banking transfer operations",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"test"
],
"summary": "Core transfer endpoint",
"parameters": [
{
"description": "Core transfer request body",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.CoreTransfer"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/test/test-endpoint": {
"post": {
"description": "Test endpoint for API functionality verification",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"test"
],
"summary": "Test endpoint",
"parameters": [
{
"description": "Test request body",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.Endpoint"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/workflows/start": {
"post": {
"description": "Start a new workflow",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"workflow"
],
"summary": "Start a workflow",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/workflows/{id}": {
"get": {
"description": "Get the status of a workflow by ID",
"produces": [
"application/json"
],
"tags": [
"workflow"
],
"summary": "Get workflow status",
"parameters": [
{
"type": "string",
"description": "Workflow ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/workflows/{id}/results": {
"get": {
"description": "Get the results of a workflow by ID",
"produces": [
"application/json"
],
"tags": [
"workflow"
],
"summary": "Get workflow results",
"parameters": [
{
"type": "string",
"description": "Workflow ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
}
},
"definitions": {
"handlers.CoreTransfer": {
"type": "object",
"properties": {
"service": {
"type": "object",
"properties": {
"APP_HEAD": {
"type": "object",
"properties": {
"BranchId": {
"type": "string"
},
"TlrNo": {
"type": "string"
},
"array": {
"type": "object",
"properties": {
"AuthTlrInf": {
"type": "object",
"properties": {
"ApprTellerNo": {
"type": "string"
}
}
}
}
}
}
},
"QryRcrdNo": {
"type": "string"
},
"SYS_HEAD": {
"type": "object",
"properties": {
"PrvdSysSeqNo": {
"type": "string"
},
"SvcCd": {
"type": "string"
},
"SvcScn": {
"type": "string"
},
"TranDt": {
"type": "string"
},
"TranRetSt": {
"type": "string"
},
"array": {
"type": "object",
"properties": {
"RetInf": {
"type": "object",
"properties": {
"RetCd": {
"type": "string"
},
"RetMsg": {
"type": "string"
}
}
}
}
}
}
},
"TxnStrtNo": {
"type": "string"
},
"array": {
"type": "array",
"items": {
"type": "object",
"properties": {
"TxnInfArray": {
"type": "object",
"properties": {
"AcctNm": {
"type": "string"
},
"CnclRvrsFlg": {
"type": "string"
},
"CnlNo": {
"type": "string"
},
"CntprAcctNoOrCardNo": {
"type": "string"
},
"ImprtBlkVchrNo": {
"type": "string"
},
"ImprtBlkVchrTp": {
"type": "string"
},
"OrigTxnSeqNo": {
"type": "string"
},
"OthrBnkBnkNo": {
"type": "string"
},
"RmkInf": {
"type": "string"
},
"SubBrId": {
"type": "string"
},
"TxnAfBal": {
"type": "string"
},
"TxnAmt": {
"type": "string"
},
"TxnCcy": {
"type": "string"
},
"TxnCd": {
"type": "string"
},
"TxnSmyDsc": {
"type": "string"
},
"TxnSysDt": {
"type": "string"
},
"TxnSysTm": {
"type": "string"
},
"TxnTlrNo": {
"type": "string"
}
}
}
}
}
}
}
}
}
},
"handlers.Endpoint": {
"type": "object",
"properties": {
"BODY": {
"type": "object",
"properties": {
"BASE_ACCT_NO": {
"type": "string"
},
"CARD_NO": {
"type": "array",
"items": {
"type": "object",
"properties": {
"DOCUMENT_ID": {
"type": "string"
},
"DOCUMENT_TYPE": {
"type": "string"
},
"LINKMAN_NAME": {
"type": "string"
},
"LINKMAN_TYPE": {
"type": "string"
},
"PHONE_NO1": {
"type": "string"
},
"PHONE_NO2": {
"type": "string"
}
}
}
},
"HANG_SEQ_NO": {
"type": "array",
"items": {
"type": "string"
}
},
"SERV_DETAIL": {
"type": "object",
"properties": {
"CHARGE_MODE": {
"type": "string"
},
"DISC_FEE_AMT": {
"type": "string"
},
"DISC_RATE": {
"type": "object",
"properties": {
"CHARGE_MODE": {
"type": "string"
},
"PHONE_NO2": {
"type": "object",
"properties": {
"DOCUMENT_TYPE": {
"type": "array",
"items": {
"type": "object",
"properties": {
"DOCUMENT_ID": {
"type": "object",
"properties": {
"LINKMAN_TYPE": {
"type": "string"
},
"PHONE_NO2": {
"type": "string"
}
}
},
"DOCUMENT_TYPE": {
"type": "string"
},
"LINKMAN_NAME": {
"type": "string"
},
"LINKMAN_TYPE": {
"type": "string"
}
}
}
},
"LINKMAN_NAME": {
"type": "string"
},
"LINKMAN_TYPE": {
"type": "string"
}
}
}
}
},
"DISC_TYPE": {
"type": "string"
},
"FEE_AMT": {
"type": "string"
},
"FEE_TYPE": {
"type": "string"
},
"ORIG_FEE_AMT": {
"type": "string"
}
}
},
"SETTLEMENT_DATE": {
"type": "string"
},
"TRAN_AMT": {
"type": "string"
},
"TRAN_CCY": {
"type": "string"
},
"TRAN_TYPE": {
"type": "string"
}
}
},
"PUB_DOMAIN": {
"type": "object",
"properties": {
"AUTH_FLAG": {
"type": "string"
},
"AUTH_INFO_NUM": {
"type": "string"
},
"AUTH_STATUS": {
"type": "string"
},
"AUTH_TELLER": {
"type": "string"
},
"BRANCH_ID": {
"type": "string"
},
"CHANNEL_CODE": {
"type": "string"
},
"CONFIRM_FLAG": {
"type": "string"
},
"CONFIRM_STATUS": {
"type": "string"
},
"CONSUM_TRAN_DATE": {
"type": "string"
},
"CONSUM_TRAN_TIME": {
"type": "string"
},
"CURR_PAGE_NUM": {
"type": "string"
},
"LEGAL_CODE": {
"type": "string"
},
"PAGE_UP_DOWN": {
"type": "string"
},
"PER_PAGE_NUM": {
"type": "string"
},
"PROVID_TRAN_DATE": {
"type": "string"
},
"PUB_EXTEND": {
"type": "string"
},
"TRAN_TELLER": {
"type": "string"
}
}
},
"SYS_HEAD": {
"type": "object",
"properties": {
"CHARACTER_SET": {
"type": "string"
},
"COMM_TYPE": {
"type": "string"
},
"CONSUM_REQ_DATE": {
"type": "string"
},
"CONSUM_REQ_TIME": {
"type": "string"
},
"CONSUM_SYS_CODE": {
"type": "string"
},
"FILE_FLAG": {
"type": "string"
},
"GLOBAL_SEQ": {
"type": "string"
},
"LOCAL_LANG": {
"type": "string"
},
"PROVID_SYS_CODE": {
"type": "string"
},
"SCENES_CODE": {
"type": "string"
},
"SCENES_VERSION": {
"type": "string"
},
"SERVICE_NAME": {
"type": "string"
},
"SERVICE_REQ_SEQ": {
"type": "string"
},
"SERVICE_VERSION": {
"type": "string"
},
"SRC_ENC_NODE": {
"type": "string"
}
}
}
}
},
"models.CreateCompositeCaseRequest": {
"type": "object",
"required": [
"name"
],
"properties": {
"description": {
"type": "string"
},
"name": {
"type": "string"
},
"status": {
"type": "string"
},
"steps": {
"type": "array",
"items": {
"$ref": "#/definitions/models.CreateCompositeCaseStepRequest"
}
}
}
},
"models.CreateCompositeCaseStepRequest": {
"type": "object",
"required": [
"step_name",
"step_order",
"step_type"
],
"properties": {
"activity_name": {
"type": "string"
},
"is_required": {
"type": "boolean"
},
"parameters_json": {
"type": "string"
},
"step_description": {
"type": "string"
},
"step_name": {
"type": "string"
},
"step_order": {
"type": "integer"
},
"step_type": {
"type": "string"
}
}
},
"models.UpdateCompositeCaseRequest": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"name": {
"type": "string"
},
"status": {
"type": "string"
},
"steps": {
"type": "array",
"items": {
"$ref": "#/definitions/models.UpdateCompositeCaseStepRequest"
}
}
}
},
"models.UpdateCompositeCaseStepRequest": {
"type": "object",
"properties": {
"activity_name": {
"type": "string"
},
"id": {
"type": "integer"
},
"is_required": {
"type": "boolean"
},
"parameters_json": {
"type": "string"
},
"step_description": {
"type": "string"
},
"step_name": {
"type": "string"
},
"step_order": {
"type": "integer"
},
"step_type": {
"type": "string"
}
}
}
}
}

76
go.mod
View File

@ -3,31 +3,89 @@ module beacon
go 1.24.4
require (
github.com/fsnotify/fsnotify v1.9.0
github.com/gin-contrib/cors v1.7.6
github.com/gin-gonic/gin v1.10.1
github.com/go-playground/locales v0.14.1
github.com/go-playground/universal-translator v0.18.1
github.com/go-playground/validator/v10 v10.26.0
github.com/go-sql-driver/mysql v1.9.3
github.com/google/uuid v1.6.0
github.com/natefinch/lumberjack v2.0.0+incompatible
github.com/openai/openai-go v1.6.0
github.com/spf13/viper v1.20.1
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.4
go.temporal.io/sdk v1.34.0
google.golang.org/protobuf v1.36.5
go.uber.org/zap v1.18.1
google.golang.org/protobuf v1.36.6
gorm.io/driver/mysql v1.6.0
gorm.io/gorm v1.30.0
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/bytedance/sonic v1.13.3 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-openapi/jsonpointer v0.21.1 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nexus-rpc/sdk-go v0.3.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/robfig/cron v1.2.0 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.12.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tidwall/gjson v1.14.4 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
go.temporal.io/api v1.46.0 // indirect
golang.org/x/net v0.36.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed // indirect
google.golang.org/grpc v1.66.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.18.0 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/time v0.8.0 // indirect
golang.org/x/tools v0.34.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
google.golang.org/grpc v1.67.3 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

216
go.sum
View File

@ -1,9 +1,26 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -13,9 +30,45 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw=
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@ -28,16 +81,29 @@ github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaW
github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@ -46,9 +112,27 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=
github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
github.com/nexus-rpc/sdk-go v0.3.0 h1:Y3B0kLYbMhd4C2u00kcYajvmOrfozEtTV/nHSnV57jA=
github.com/nexus-rpc/sdk-go v0.3.0/go.mod h1:TpfkM2Cw0Rlk9drGkoiSMpFqflKTiQLWUNyKJjF8mKQ=
github.com/openai/openai-go v1.6.0 h1:KGjDS5sDrO27vykzO50BYknuabzVxuFuwAB8DjrmexI=
github.com/openai/openai-go v1.6.0/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@ -57,39 +141,104 @@ github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.temporal.io/api v1.46.0 h1:O1efPDB6O2B8uIeCDIa+3VZC7tZMvYsMZYQapSbHvCg=
go.temporal.io/api v1.46.0/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM=
go.temporal.io/sdk v1.34.0 h1:VLg/h6ny7GvLFVoQPqz2NcC93V9yXboQwblkRvZ1cZE=
go.temporal.io/sdk v1.34.0/go.mod h1:iE4U5vFrH3asOhqpBBphpj9zNtw8btp8+MSaf5A0D3w=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
go.uber.org/zap v1.18.1 h1:CSUJ2mjFszzEWt4CdKISEuChVIXGBn3lAPwkRGyVrc4=
go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE=
go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI=
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -98,9 +247,12 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -108,8 +260,9 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -118,16 +271,25 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@ -138,6 +300,9 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -147,28 +312,39 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed h1:3RgNmBoI9MZhsj3QxC+AP/qQhNwpCLOvYDYYsFrhFt0=
google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed h1:J6izYgfBXAI3xTKLgxzTmUltdYaLsuBxFCgDHWJ/eXg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c=
google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/grpc v1.67.3 h1:OgPcDAFKHnH8X3O4WcO4XUc8GRDeKsKReqbQtiCj7N8=
google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ=
sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4=

97
main.go Normal file
View File

@ -0,0 +1,97 @@
// @title Beacon API
// @version 1.0
// @description This is a sample server for a beacon.
// @termsOfService http://swagger.io/terms/
// @contact.name API Support
// @contact.url http://www.swagger.io/support
// @contact.email support@swagger.io
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @host localhost:8080
// @BasePath /api/v1
package main
import (
"beacon/config"
dao "beacon/pkg/dao/mysql"
"beacon/pkg/logger"
"beacon/pkg/validator"
"beacon/routers"
workers "beacon/workers/go"
"flag"
"fmt"
"os"
"os/signal"
"syscall"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
func Run() {
// 从命令行获取配置
var cfn string
flag.StringVar(&cfn, "conf", "./config/config.yaml", "指定启动的配置文件")
flag.Parse()
// 1.加载配置文件
err := config.InitFromFile(cfn)
if err != nil {
panic(err) // 程序启动时加载配置文件失败直接退出
}
// 2.加载日志
err = logger.Init(config.Conf.LogConfig, config.Conf.Mode)
if err != nil {
panic(err) // 程序启动时初始化日志模块失败直接退出
}
// 添加校验规则
err = validator.InitTrans("zh")
if err != nil {
panic(err)
}
// gorm
err = dao.InitGorm(config.Conf.MySQLConfig)
if err != nil {
panic(err)
}
gin.SetMode(gin.DebugMode)
// 6.HTTP 初始化路由
r := routers.Init()
go func() {
// 启动服务
err := r.Run(fmt.Sprintf("%s:%d", config.Conf.IP, config.Conf.Port))
if err != nil {
panic(err)
}
}()
zap.L().Info(fmt.Sprintf("Serving Gin on %s:%d", config.Conf.IP, config.Conf.Port))
zap.L().Info("service start...")
go func() {
// 启动 Temporal Worker
err := workers.StartWorkflow(config.Conf.TemporalConfig)
if err != nil {
zap.L().Error("启动 Temporal Worker 失败", zap.Error(err))
os.Exit(1) // 如果启动 Temporal Worker 失败,退出程序
}
}()
// 接收操作系统发来的中断信号
QuitChan := make(chan os.Signal)
signal.Notify(QuitChan, syscall.SIGTERM, syscall.SIGINT, syscall.SIGKILL) // kill -15 CTRL+C kill -9
<-QuitChan
}
func main() {
Run()
}

26
middleware/cors.go Normal file
View File

@ -0,0 +1,26 @@
package middleware
import "time"
import (
"github.com/gin-contrib/cors"
)
var CSRFMiddleware = cors.New(cors.Config{
//准许跨域请求网站,多个使用,分开,限制使用*
AllowOrigins: []string{"*"},
//准许使用的请求方式
AllowMethods: []string{"OPTIONS", "PUT", "PATCH", "POST", "GET", "DELETE"},
//准许使用的请求表头
AllowHeaders: []string{"Origin", "AccessToken", "Content-Type"},
//显示的请求表头
ExposeHeaders: []string{"Content-Type"},
//凭证共享,确定共享
AllowCredentials: true,
//容许跨域的原点网站,可以直接return true就万事大吉了
AllowOriginFunc: func(origin string) bool {
return true
},
//超时时间设定
MaxAge: 24 * time.Hour,
})

33
middleware/crawler.go Normal file
View File

@ -0,0 +1,33 @@
package middleware
import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"net/http"
"strings"
)
// AntiCrawlingStrategy 反爬虫策略
func AntiCrawlingStrategy() gin.HandlerFunc {
return func(context *gin.Context) {
UserAgent := strings.Split(context.Request.UserAgent(), "/")
var requestClient string
if len(UserAgent) > 0 {
requestClient = UserAgent[0]
}
IllegalList := []string{"", "Apache-HttpClient", "python-requests", "PostmanRuntime", "Go-http-client"}
for _, illegal := range IllegalList {
if illegal == requestClient {
zap.L().Warn("非法用户请求", zap.String("UserAgent", requestClient))
context.JSON(http.StatusNotImplemented, gin.H{
"code": -1,
"msg": "系统错误,请稍后再试",
})
context.Abort()
return
}
}
context.Next()
}
}

82
middleware/default.go Normal file
View File

@ -0,0 +1,82 @@
package middleware
import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"net"
"net/http"
"net/http/httputil"
"os"
"runtime/debug"
"strings"
"time"
)
func GinLogger(logger *zap.Logger) gin.HandlerFunc {
return func(context *gin.Context) {
start := time.Now()
path := context.Request.URL.Path
query := context.Request.URL.RawQuery
context.Next()
cost := time.Since(start)
logger.Info(path,
zap.Int("status", context.Writer.Status()),
zap.String("method", context.Request.Method),
zap.String("path", path),
zap.String("query", query),
zap.String("ip", context.ClientIP()),
zap.String("user-agent", context.Request.UserAgent()),
zap.String("errors", context.Errors.ByType(gin.ErrorTypePrivate).String()),
zap.Duration("cost", cost),
)
}
}
func GinRecovery(logger *zap.Logger, stack bool) gin.HandlerFunc {
return func(context *gin.Context) {
defer func() {
if err := recover(); err != nil {
// Check for a broken connection, as it is not really a
// condition that warrants a panic stack trace.
var brokenPipe bool
if ne, ok := err.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
brokenPipe = true
}
}
}
httpRequest, _ := httputil.DumpRequest(context.Request, false)
if brokenPipe {
logger.Error(context.Request.URL.Path,
zap.Any("error", err),
zap.String("request", string(httpRequest)),
)
// If the connection is dead, we can't write a status to it.
context.Error(err.(error)) // nolint: err check
context.Abort()
return
}
if stack {
logger.Error("[Recovery from panic]",
zap.Any("error", err),
zap.String("request", string(httpRequest)),
zap.String("stack", string(debug.Stack())),
)
} else {
logger.Error("[Recovery from panic]",
zap.Any("error", err),
zap.String("request", string(httpRequest)),
)
}
// 500
context.AbortWithStatus(http.StatusInternalServerError)
}
}()
context.Next()
}
}

74
models/composite.go Normal file
View File

@ -0,0 +1,74 @@
package models
import (
"gorm.io/gorm"
"time"
)
// CompositeCase 复合案例定义
type CompositeCase struct {
ID uint `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"not null;size:255"`
Description string `json:"description" gorm:"type:text"`
Status string `json:"status" gorm:"default:'active';size:50"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
// 关联的步骤
Steps []CompositeCaseStep `json:"steps" gorm:"foreignKey:CompositeCaseID"`
}
// CompositeCaseStep 复合案例步骤
type CompositeCaseStep struct {
ID uint `json:"id" gorm:"primaryKey"`
CompositeCaseID uint `json:"composite_case_id" gorm:"not null"`
StepOrder int `json:"step_order" gorm:"not null"`
StepName string `json:"step_name" gorm:"not null;size:255"`
StepDescription string `json:"step_description" gorm:"type:text"`
StepType string `json:"step_type" gorm:"not null;size:100"`
ActivityName string `json:"activity_name" gorm:"not null;size:255"`
ParametersJson string `json:"parameters_json" gorm:"type:json"`
IsRequired bool `json:"is_required" gorm:"default:true"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// CreateCompositeCaseRequest 创建复合案例请求
type CreateCompositeCaseRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
Status string `json:"status"`
Steps []CreateCompositeCaseStepRequest `json:"steps"`
}
// CreateCompositeCaseStepRequest 创建复合案例步骤请求
type CreateCompositeCaseStepRequest struct {
StepOrder int `json:"step_order" binding:"required"`
StepName string `json:"step_name" binding:"required"`
StepDescription string `json:"step_description"`
StepType string `json:"step_type" binding:"required"`
ActivityName string `json:"activity_name"`
ParametersJson string `json:"parameters_json"`
IsRequired bool `json:"is_required"`
}
// UpdateCompositeCaseRequest 更新复合案例请求
type UpdateCompositeCaseRequest struct {
Name string `json:"name"`
Description string `json:"description"`
Status string `json:"status"`
Steps []UpdateCompositeCaseStepRequest `json:"steps"`
}
// UpdateCompositeCaseStepRequest 更新复合案例步骤请求
type UpdateCompositeCaseStepRequest struct {
ID uint `json:"id"`
StepOrder int `json:"step_order"`
StepName string `json:"step_name"`
StepDescription string `json:"step_description"`
StepType string `json:"step_type"`
ActivityName string `json:"activity_name"`
ParametersJson string `json:"parameters_json"`
IsRequired bool `json:"is_required"`
}

157
pkg/dao/mysql/composite.go Normal file
View File

@ -0,0 +1,157 @@
package dao
import (
"beacon/models"
"errors"
"fmt"
"gorm.io/gorm"
"strconv"
)
// CreateCompositeCase 创建复合案例
func CreateCompositeCase(tx *gorm.DB, compositeCase *models.CompositeCase) error {
if tx == nil {
tx = DB
}
if compositeCase.Status == "" {
compositeCase.Status = "active"
}
return tx.Create(compositeCase).Error
}
// GetCompositeCaseByID 根据ID获取复合案例
func GetCompositeCaseByID(id uint) (*models.CompositeCase, error) {
var compositeCase models.CompositeCase
err := DB.Preload("Steps", func(db *gorm.DB) *gorm.DB {
return db.Order("step_order ASC")
}).First(&compositeCase, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("复合案例不存在")
}
return nil, fmt.Errorf("获取复合案例失败: %w", err)
}
return &compositeCase, nil
}
// UpdateCompositeCase 更新复合案例基本信息
func UpdateCompositeCase(tx *gorm.DB, id uint, updates map[string]interface{}) error {
if tx == nil {
tx = DB
}
if len(updates) == 0 {
return nil
}
return tx.Model(&models.CompositeCase{}).Where("id = ?", id).Updates(updates).Error
}
// DeleteCompositeCase 删除复合案例
func DeleteCompositeCase(tx *gorm.DB, id uint) error {
if tx == nil {
tx = DB
}
return tx.Delete(&models.CompositeCase{}, id).Error
}
// ListCompositeCases 获取复合案例列表
func ListCompositeCases(page, pageSize int, status string) ([]models.CompositeCase, int64, error) {
var compositeCases []models.CompositeCase
var total int64
query := DB.Model(&models.CompositeCase{})
if status != "" {
query = query.Where("status = ?", status)
}
// 获取总数
if err := query.Count(&total).Error; err != nil {
return nil, 0, fmt.Errorf("获取复合案例总数失败: %w", err)
}
// 分页查询
offset := (page - 1) * pageSize
err := query.Preload("Steps", func(db *gorm.DB) *gorm.DB {
return db.Order("step_order ASC")
}).Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&compositeCases).Error
if err != nil {
return nil, 0, fmt.Errorf("获取复合案例列表失败: %w", err)
}
return compositeCases, total, nil
}
// ExistsCompositeCase 检查复合案例是否存在
func ExistsCompositeCase(tx *gorm.DB, id uint) (*models.CompositeCase, error) {
if tx == nil {
tx = DB
}
var compositeCase models.CompositeCase
err := tx.First(&compositeCase, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("复合案例不存在")
}
return nil, fmt.Errorf("查询复合案例失败: %w", err)
}
return &compositeCase, nil
}
// CreateCompositeCaseSteps 创建复合案例步骤
func CreateCompositeCaseSteps(tx *gorm.DB, steps []models.CompositeCaseStep) error {
if tx == nil {
tx = DB
}
if len(steps) == 0 {
return nil
}
return tx.Create(&steps).Error
}
// GetCompositeCaseSteps 根据复合案例ID获取步骤列表
func GetCompositeCaseSteps(compositeCaseId string) ([]models.CompositeCaseStep, error) {
var steps []models.CompositeCaseStep
// 将字符串ID转换为uint
id, err := strconv.ParseUint(compositeCaseId, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid composite case id: %w", err)
}
err = DB.Where("composite_case_id = ?", uint(id)).
Order("step_order ASC").
Find(&steps).Error
if err != nil {
return nil, fmt.Errorf("获取复合案例步骤失败: %w", err)
}
return steps, nil
}
// DeleteCompositeCaseStepsByCompositeCaseID 根据复合案例ID删除所有步骤
func DeleteCompositeCaseStepsByCompositeCaseID(tx *gorm.DB, compositeCaseId uint) error {
if tx == nil {
tx = DB
}
return tx.Where("composite_case_id = ?", compositeCaseId).Delete(&models.CompositeCaseStep{}).Error
}
// BeginTransaction 开启事务
func BeginTransaction() *gorm.DB {
return DB.Begin()
}

42
pkg/dao/mysql/default.go Normal file
View File

@ -0,0 +1,42 @@
package dao
import (
"beacon/config"
"fmt"
_ "github.com/go-sql-driver/mysql" // 匿名导入 init()
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"time"
)
var DB *gorm.DB
// InitGorm 初始化MySQL连接
func InitGorm(cfg *config.MySQLConfig) (err error) {
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.DB)
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
if err != nil {
return err
}
// 额外的连接配置
db, err := DB.DB() // database/sql.sqlxDB
if err != nil {
return
}
// 以下配置要配合 my.conf 进行配置
// SetMaxIdleConns 设置空闲连接池中连接的最大数量
db.SetMaxIdleConns(cfg.MaxIdleConns)
// SetMaxOpenConns 设置打开数据库连接的最大数量。
db.SetMaxOpenConns(cfg.MaxOpenConns)
// SetConnMaxLifetime 设置了连接可复用的最大时间。
db.SetConnMaxLifetime(time.Hour)
return
}

48
pkg/grequests/option.go Normal file
View File

@ -0,0 +1,48 @@
package grequests
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/url"
"strings"
)
// Option 定义请求选项
type Option func(*http.Request)
// WithParams 设置 URL 查询参数
func WithParams(params map[string]string) Option {
return func(req *http.Request) {
q := req.URL.Query()
for k, v := range params {
q.Add(k, v)
}
req.URL.RawQuery = q.Encode()
}
}
// WithData 设置表单数据
func WithData(data map[string]string) Option {
return func(req *http.Request) {
form := url.Values{}
for k, v := range data {
form.Add(k, v)
}
req.Body = io.NopCloser(strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}
}
// WithJSON 设置 JSON 数据
func WithJSON(jsonData interface{}) Option {
return func(req *http.Request) {
jsonBytes, err := json.Marshal(jsonData)
if err != nil {
return // 在实际应用中应处理错误
}
req.Body = io.NopCloser(bytes.NewReader(jsonBytes))
req.Header.Set("Content-Type", "application/json")
}
}

78
pkg/grequests/request.go Normal file
View File

@ -0,0 +1,78 @@
package grequests
import (
"io"
"net/http"
"time"
)
// Session 管理 HTTP 会话
type Session struct {
client *http.Client
}
// NewSession 创建一个新的会话
func NewSession() *Session {
return &Session{
client: &http.Client{
Timeout: 30 * time.Second, // 默认超时 30 秒
},
}
}
// 默认会话,用于包级别的快捷函数
var defaultSession = NewSession()
// Request 执行 HTTP 请求
func (s *Session) Request(method, url string, options ...Option) (*Response, error) {
req, err := http.NewRequest(method, url, nil)
if err != nil {
return nil, err
}
// 应用所有选项
for _, opt := range options {
opt(req)
}
resp, err := s.client.Do(req)
if err != nil {
return nil, err
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
return
}
}(resp.Body)
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return &Response{
StatusCode: resp.StatusCode,
Body: body,
Headers: resp.Header,
}, nil
}
// Get 执行 GET 请求(会话方法)
func (s *Session) Get(url string, options ...Option) (*Response, error) {
return s.Request("GET", url, options...)
}
// Post 执行 POST 请求(会话方法)
func (s *Session) Post(url string, options ...Option) (*Response, error) {
return s.Request("POST", url, options...)
}
// Get 包级别的快捷函数
func Get(url string, options ...Option) (*Response, error) {
return defaultSession.Request("GET", url, options...)
}
func Post(url string, options ...Option) (*Response, error) {
return defaultSession.Request("POST", url, options...)
}

23
pkg/grequests/response.go Normal file
View File

@ -0,0 +1,23 @@
package grequests
import (
"encoding/json"
"net/http"
)
// Response 封装 HTTP 响应
type Response struct {
StatusCode int
Body []byte
Headers http.Header
}
// Text 返回响应的文本内容
func (r *Response) Text() string {
return string(r.Body)
}
// JSON 解析响应为 JSON 并存储到指定变量
func (r *Response) JSON(v interface{}) error {
return json.Unmarshal(r.Body, v)
}

67
pkg/logger/logger.go Executable file
View File

@ -0,0 +1,67 @@
package logger
import (
"beacon/config"
"github.com/natefinch/lumberjack"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"os"
)
var Lg *zap.Logger
// zap日志库三要素
// 1.encoder编码 2.输出位置 3.日志级别
// Init 初始化lg
func Init(cfg *config.LogConfig, mode string) (err error) {
writeSyncer := getLogWriter(cfg.Filename, cfg.MaxSize, cfg.MaxBackups, cfg.MaxAge)
writeErrSyncer := getLogWriter(cfg.FilenameErr, cfg.MaxSize, cfg.MaxBackups, cfg.MaxAge)
encoder := getEncoder()
var l = new(zapcore.Level)
err = l.UnmarshalText([]byte(cfg.Level))
if err != nil {
return
}
var core zapcore.Core
if mode == "dev" {
// 进入开发模式,日志输出到终端
consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
core = zapcore.NewTee(
zapcore.NewCore(encoder, writeSyncer, l),
zapcore.NewCore(consoleEncoder, zapcore.Lock(os.Stdout), zapcore.DebugLevel),
)
} else {
confCore := zapcore.NewCore(encoder, writeSyncer, l)
errCore := zapcore.NewCore(encoder, writeErrSyncer, zapcore.ErrorLevel)
core = zapcore.NewTee(confCore, errCore)
}
// 复习回顾日志默认输出到app.log如何将err日志单独在 app.err.log 记录一份
Lg = zap.New(core, zap.AddCaller()) // zap.AddCaller() 添加调用栈信息
zap.ReplaceGlobals(Lg) // 替换zap包全局的logger
zap.L().Info("init logger success")
return
}
func getEncoder() zapcore.Encoder {
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
encoderConfig.TimeKey = "time"
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder
encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
return zapcore.NewJSONEncoder(encoderConfig)
}
func getLogWriter(filename string, maxSize, maxBackup, maxAge int) zapcore.WriteSyncer {
lumberJackLogger := &lumberjack.Logger{
Filename: filename,
MaxSize: maxSize,
MaxBackups: maxBackup,
MaxAge: maxAge,
}
return zapcore.AddSync(lumberJackLogger)
}

66
pkg/openai/openai.go Normal file
View File

@ -0,0 +1,66 @@
package openai
import (
"beacon/config"
"beacon/utils"
"context"
"encoding/base64"
"fmt"
"mime/multipart"
"net/http"
"github.com/openai/openai-go"
"github.com/openai/openai-go/option"
)
// encodeImageToBase64 是一个辅助函数,用于读取图像文件,
// 检测其 MIME 类型,并将其编码为 Base64 数据 URI。
func encodeImageToBase64(file *multipart.FileHeader) (string, error) {
// 1. 读取文件字节。
bytes := utils.FileStreamToBytes(file)
// 2. 检测文件的 MIME 类型。
// 这对于构建正确的数据 URI至关重要。
mimeType := http.DetectContentType(bytes)
// 3. 对文件字节进行 Base64 编码。
encodedStr := base64.StdEncoding.EncodeToString(bytes)
// 4. 构建并返回完整的数据 URI 字符串。
return fmt.Sprintf("data:%s;base64,%s", mimeType, encodedStr), nil
}
func GetOllamaAICaptcha(file *multipart.FileHeader) (error, string) {
client := openai.NewClient(
option.WithAPIKey(config.Conf.OpenAIConfig.ApiKey), // defaults to os.LookupEnv("OPENAI_API_KEY")
option.WithBaseURL(config.Conf.OpenAIConfig.BaseURL),
)
base64Image, _ := encodeImageToBase64(file)
var message []openai.ChatCompletionContentPartUnionParam
message = append(message,
openai.ChatCompletionContentPartUnionParam{
OfImageURL: &openai.ChatCompletionContentPartImageParam{
ImageURL: openai.ChatCompletionContentPartImageImageURLParam{
URL: base64Image,
},
},
},
openai.ChatCompletionContentPartUnionParam{
OfText: &openai.ChatCompletionContentPartTextParam{
Text: config.Conf.OpenAIConfig.Prompt,
},
},
)
chatCompletion, err := client.Chat.Completions.New(context.TODO(), openai.ChatCompletionNewParams{
Messages: []openai.ChatCompletionMessageParamUnion{
openai.UserMessage(message),
},
Model: config.Conf.OpenAIConfig.Model,
})
if err != nil {
return err, ""
}
return nil, chatCompletion.Choices[0].Message.Content
}

105
pkg/validator/validator.go Executable file
View File

@ -0,0 +1,105 @@
package validator
import (
"encoding/json"
"errors"
"fmt"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
enTranslations "github.com/go-playground/validator/v10/translations/en"
zhTranslations "github.com/go-playground/validator/v10/translations/zh"
"go.uber.org/zap"
"reflect"
"strings"
)
// Trans 定义一个全局翻译器T
var Trans ut.Translator
// InitTrans 初始化翻译器
func InitTrans(locale string) (err error) {
// 修改gin框架中的Validator引擎属性实现自定制
v, ok := binding.Validator.Engine().(*validator.Validate)
if ok {
// 注册一个获取json tag的自定义方法
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
zhT := zh.New() // 中文翻译器
enT := en.New() // 英文翻译器
// 第一个参数是备用fallback的语言环境
// 后面的参数是应该支持的语言环境(支持多个)
// uni := ut.New(zhT, zhT) 也是可以的
uni := ut.New(enT, zhT, enT)
// locale 通常取决于 http 请求头的 'Accept-Language'
var ok bool
// 也可以使用 uni.FindTranslator(...) 传入多个locale进行查找
Trans, ok = uni.GetTranslator(locale)
if !ok {
return fmt.Errorf("uni.GetTranslator(%s) failed", locale)
}
// 注册翻译器
switch locale {
case "en":
err = enTranslations.RegisterDefaultTranslations(v, Trans)
case "zh":
err = zhTranslations.RegisterDefaultTranslations(v, Trans)
default:
err = enTranslations.RegisterDefaultTranslations(v, Trans)
}
return
}
return
}
func RemoveTopStruct(fields map[string]string) map[string]string {
zap.L().Debug("RemoveTopStruct", zap.Any("fields", fields))
message := map[string]string{}
for field, err := range fields {
dotIndex := strings.Index(field, ".")
if dotIndex != -1 {
message[field[dotIndex+1:]] = err
} else {
message[field] = err
}
}
if len(message) == 0 {
message = map[string]string{"body": "请求的参数有误"}
}
return message
}
func TranslateErr(err error) map[string]string {
zap.L().Debug("TranslateErr", zap.Any("err", err))
var errs validator.ValidationErrors
if errors.As(err, &errs) {
return RemoveTopStruct(errs.Translate(Trans))
}
// 处理 JSON unmarshal 错误,提取字段名
var unmarshalErr *json.UnmarshalTypeError
if errors.As(err, &unmarshalErr) {
fieldName := unmarshalErr.Field
if fieldName != "" {
// 获取目标类型
targetType := unmarshalErr.Type.String()
if strings.Contains(fieldName, ".") {
fieldName = strings.Split(fieldName, ".")[len(strings.Split(fieldName, "."))-1]
}
return map[string]string{fieldName: fmt.Sprintf("类型错误,期望类型为 %s", targetType)}
}
}
return map[string]string{"body": err.Error()}
}

View File

@ -2,7 +2,7 @@ syntax = "proto3";
package test_pb;
option go_package = "Beacon/server/gen/pb"; // your_module_path
option go_package = "Beacon/pkg/pb"; // your_module_path
// Python proto_gen common_test_pb2.py
// Python import proto_gen.common_test_pb2
@ -22,7 +22,7 @@ message ApiTestRequest {
string endpoint = 2; // API路径
string http_method = 3; // "GET", "POST", etc.
map<string, string> headers = 4;
bytes request_body = 5; // JSON或其他二进制数据
string request_body = 5; // JSON或其他二进制数据
int32 expected_status_code = 6;
}
@ -51,6 +51,7 @@ message ApiTestResult {
BaseTestResult base_result = 1;
int32 actual_status_code = 2;
string response_body = 3;
map<string, string> headers = 4;
}
// UI测试结果

44
proto/dynamic_test.proto Normal file
View File

@ -0,0 +1,44 @@
syntax = "proto3";
package test_pb;
option go_package = "Beacon/pkg/pb"; // your_module_path
message DynamicTestRunInput {
string run_id = 1;
string composite_case_id = 2; // ID
map<string, string> global_parameters = 3; // environment_url
// steps Go Workflow
// Proto Workflow DB 使
// Workflow
repeated DynamicStep steps = 4; // DB ID Workflow
}
message DynamicStep {
string step_id = 1; // step_id
string activity_name = 2; // Activity
map<string, string> parameters_json = 3; // JSONB string bytes
// Workflow
// DAG Workflow DB
}
//
message CompositeCaseStepDefinition {
int64 step_id = 1; // step_id
int32 step_order = 2; //
string step_type = 3; // e.g., "API_TEST", "UI_TEST"
string activity_name = 4; // Temporal Activity
// JSON Workflow
// Activity oneof JSON
string parameters_json = 5; // JSON Activity
// (/ step_order)
optional int32 success_next_step_order = 6; //
optional int32 failure_next_step_order = 7; //
// {"prev_step_id": "xyz", "status": "success"}
// Workflow JSON
string run_condition_json = 8;
}

View File

@ -0,0 +1,244 @@
package handlers
import (
"beacon/models"
"beacon/services"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"net/http"
"strconv"
)
type CompositeCaseHandler struct {
service *services.CompositeCaseService
}
// NewCompositeCaseHandler 创建 CompositeCaseHandler 实例
func NewCompositeCaseHandler() *CompositeCaseHandler {
return &CompositeCaseHandler{
service: &services.CompositeCaseService{},
}
}
// CreateCompositeCase POST /api/composite-cases
// CreateCompositeCase godoc
// @Summary Create a composite case
// @Description Create a new composite case
// @Tags composite
// @Accept json
// @Produce json
// @Param body body models.CreateCompositeCaseRequest true "Create Composite Case Request"
// @Success 201 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
// @Failure 500 {object} map[string]interface{}
// @Router /composite-cases [post]
func (h *CompositeCaseHandler) CreateCompositeCase(c *gin.Context) {
var req models.CreateCompositeCaseRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "请求参数错误",
"details": err.Error(),
})
return
}
zap.L().Info("req", zap.Any("req", req))
compositeCase, err := h.service.CreateCompositeCase(&req)
zap.L().Info("compositeCase", zap.Any("compositeCase", compositeCase))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "创建复合案例失败",
"details": err.Error(),
})
return
}
c.JSON(http.StatusCreated, gin.H{
"message": "创建复合案例成功",
"data": "compositeCase",
})
}
// GetCompositeCase godoc
// @Summary Get a composite case
// @Description Get a composite case by ID
// @Tags composite
// @Produce json
// @Param id path int true "Composite Case ID"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
// @Failure 404 {object} map[string]interface{}
// @Failure 500 {object} map[string]interface{}
// @Router /composite-cases/{id} [get]
func (h *CompositeCaseHandler) GetCompositeCase(c *gin.Context) {
idParam := c.Param("id")
id, err := strconv.ParseUint(idParam, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "无效的ID参数",
})
return
}
compositeCase, err := h.service.GetCompositeCaseByID(uint(id))
if err != nil {
if err.Error() == "复合案例不存在" {
c.JSON(http.StatusNotFound, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"error": "获取复合案例失败",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "获取复合案例成功",
"data": compositeCase,
})
}
// UpdateCompositeCase godoc
// @Summary Update a composite case
// @Description Update a composite case by ID
// @Tags composite
// @Accept json
// @Produce json
// @Param id path int true "Composite Case ID"
// @Param body body models.UpdateCompositeCaseRequest true "Update Composite Case Request"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
// @Failure 404 {object} map[string]interface{}
// @Failure 500 {object} map[string]interface{}
// @Router /composite-cases/{id} [put]
func (h *CompositeCaseHandler) UpdateCompositeCase(c *gin.Context) {
idParam := c.Param("id")
id, err := strconv.ParseUint(idParam, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "无效的ID参数",
})
return
}
var req models.UpdateCompositeCaseRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "请求参数错误",
"details": err.Error(),
})
return
}
compositeCase, err := h.service.UpdateCompositeCase(uint(id), &req)
if err != nil {
if err.Error() == "复合案例不存在" {
c.JSON(http.StatusNotFound, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"error": "更新复合案例失败",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "更新复合案例成功",
"data": compositeCase,
})
}
// DeleteCompositeCase godoc
// @Summary Delete a composite case
// @Description Delete a composite case by ID
// @Tags composite
// @Produce json
// @Param id path int true "Composite Case ID"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
// @Failure 404 {object} map[string]interface{}
// @Failure 500 {object} map[string]interface{}
// @Router /composite-cases/{id} [delete]
func (h *CompositeCaseHandler) DeleteCompositeCase(c *gin.Context) {
idParam := c.Param("id")
id, err := strconv.ParseUint(idParam, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "无效的ID参数",
})
return
}
err = h.service.DeleteCompositeCase(uint(id))
if err != nil {
if err.Error() == "复合案例不存在" {
c.JSON(http.StatusNotFound, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"error": "删除复合案例失败",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "删除复合案例成功",
})
}
// ListCompositeCases godoc
// @Summary List composite cases
// @Description List composite cases with pagination
// @Tags composite
// @Produce json
// @Param page query int false "Page number"
// @Param page_size query int false "Page size"
// @Param status query string false "Status"
// @Success 200 {object} map[string]interface{}
// @Failure 500 {object} map[string]interface{}
// @Router /composite-cases [get]
func (h *CompositeCaseHandler) ListCompositeCases(c *gin.Context) {
// 获取查询参数
pageStr := c.DefaultQuery("page", "1")
pageSizeStr := c.DefaultQuery("page_size", "10")
status := c.Query("status")
page, err := strconv.Atoi(pageStr)
if err != nil || page < 1 {
page = 1
}
pageSize, err := strconv.Atoi(pageSizeStr)
if err != nil || pageSize < 1 || pageSize > 100 {
pageSize = 10
}
compositeCases, total, err := h.service.ListCompositeCases(page, pageSize, status)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "获取复合案例列表失败",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "获取复合案例列表成功",
"data": gin.H{
"items": compositeCases,
"total": total,
"page": page,
"page_size": pageSize,
"total_pages": (total + int64(pageSize) - 1) / int64(pageSize),
},
})
}

193
routers/handlers/test.go Normal file
View File

@ -0,0 +1,193 @@
package handlers
import (
"github.com/gin-gonic/gin"
"net/http"
)
type TestHandler struct {
}
func NewTestHandler() *TestHandler {
return &TestHandler{}
}
// TestEndpoint godoc
// @Summary Test endpoint
// @Description Test endpoint for API functionality verification
// @Tags test
// @Accept json
// @Produce json
// @Param body body Endpoint true "Test request body"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
// @Router /test/test-endpoint [post]
func (h *TestHandler) TestEndpoint(c *gin.Context) {
var req Endpoint
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "请求参数错误",
"details": err.Error(),
})
return
}
c.JSON(200, gin.H{
"message": "Test endpoint is working",
})
}
// CoreTransferEndpoint godoc
// @Summary Core transfer endpoint
// @Description Core transfer endpoint for handling core banking transfer operations
// @Tags test
// @Accept json
// @Produce json
// @Param body body CoreTransfer true "Core transfer request body"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
// @Router /test/core-transfer [post]
func (h *TestHandler) CoreTransferEndpoint(c *gin.Context) {
var req CoreTransfer
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "请求参数错误",
"details": err.Error(),
})
return
}
c.JSON(200, gin.H{
"message": "Test endpoint is working",
})
}
type Endpoint struct {
BODY struct {
BASEACCTNO string `json:"BASE_ACCT_NO"`
TRANTYPE string `json:"TRAN_TYPE"`
TRANCCY string `json:"TRAN_CCY"`
TRANAMT string `json:"TRAN_AMT"`
SETTLEMENTDATE string `json:"SETTLEMENT_DATE"`
SERVDETAIL struct {
FEETYPE string `json:"FEE_TYPE"`
FEEAMT string `json:"FEE_AMT"`
ORIGFEEAMT string `json:"ORIG_FEE_AMT"`
DISCFEEAMT string `json:"DISC_FEE_AMT"`
DISCTYPE string `json:"DISC_TYPE"`
DISCRATE struct {
PHONENO2 struct {
LINKMANTYPE string `json:"LINKMAN_TYPE"`
LINKMANNAME string `json:"LINKMAN_NAME"`
DOCUMENTTYPE []struct {
LINKMANTYPE string `json:"LINKMAN_TYPE"`
LINKMANNAME string `json:"LINKMAN_NAME"`
DOCUMENTTYPE string `json:"DOCUMENT_TYPE"`
DOCUMENTID struct {
LINKMANTYPE string `json:"LINKMAN_TYPE"`
PHONENO2 string `json:"PHONE_NO2"`
} `json:"DOCUMENT_ID"`
} `json:"DOCUMENT_TYPE"`
} `json:"PHONE_NO2"`
CHARGEMODE string `json:"CHARGE_MODE"`
} `json:"DISC_RATE"`
CHARGEMODE string `json:"CHARGE_MODE"`
} `json:"SERV_DETAIL"`
HANGSEQNO []string `json:"HANG_SEQ_NO"`
CARDNO []struct {
LINKMANTYPE string `json:"LINKMAN_TYPE"`
LINKMANNAME string `json:"LINKMAN_NAME"`
DOCUMENTTYPE string `json:"DOCUMENT_TYPE"`
DOCUMENTID string `json:"DOCUMENT_ID"`
PHONENO1 string `json:"PHONE_NO1"`
PHONENO2 string `json:"PHONE_NO2"`
} `json:"CARD_NO"`
} `json:"BODY"`
PUBDOMAIN struct {
AUTHFLAG string `json:"AUTH_FLAG"`
AUTHINFONUM string `json:"AUTH_INFO_NUM"`
AUTHSTATUS string `json:"AUTH_STATUS"`
AUTHTELLER string `json:"AUTH_TELLER"`
BRANCHID string `json:"BRANCH_ID"`
CHANNELCODE string `json:"CHANNEL_CODE"`
CONFIRMFLAG string `json:"CONFIRM_FLAG"`
CONFIRMSTATUS string `json:"CONFIRM_STATUS"`
CONSUMTRANDATE string `json:"CONSUM_TRAN_DATE"`
CONSUMTRANTIME string `json:"CONSUM_TRAN_TIME"`
CURRPAGENUM string `json:"CURR_PAGE_NUM"`
LEGALCODE string `json:"LEGAL_CODE"`
PAGEUPDOWN string `json:"PAGE_UP_DOWN"`
PERPAGENUM string `json:"PER_PAGE_NUM"`
PROVIDTRANDATE string `json:"PROVID_TRAN_DATE"`
PUBEXTEND string `json:"PUB_EXTEND"`
TRANTELLER string `json:"TRAN_TELLER"`
} `json:"PUB_DOMAIN"`
SYSHEAD struct {
CHARACTERSET string `json:"CHARACTER_SET"`
COMMTYPE string `json:"COMM_TYPE"`
CONSUMREQDATE string `json:"CONSUM_REQ_DATE"`
CONSUMREQTIME string `json:"CONSUM_REQ_TIME"`
CONSUMSYSCODE string `json:"CONSUM_SYS_CODE"`
FILEFLAG string `json:"FILE_FLAG"`
GLOBALSEQ string `json:"GLOBAL_SEQ"`
LOCALLANG string `json:"LOCAL_LANG"`
PROVIDSYSCODE string `json:"PROVID_SYS_CODE"`
SCENESCODE string `json:"SCENES_CODE"`
SCENESVERSION string `json:"SCENES_VERSION"`
SERVICENAME string `json:"SERVICE_NAME"`
SERVICEREQSEQ string `json:"SERVICE_REQ_SEQ"`
SERVICEVERSION string `json:"SERVICE_VERSION"`
SRCENCNODE string `json:"SRC_ENC_NODE"`
} `json:"SYS_HEAD"`
}
type CoreTransfer struct {
Service struct {
SYSHEAD struct {
SvcCd string `json:"SvcCd"`
SvcScn string `json:"SvcScn"`
PrvdSysSeqNo string `json:"PrvdSysSeqNo"`
TranDt string `json:"TranDt"`
TranRetSt string `json:"TranRetSt"`
Array struct {
RetInf struct {
RetCd string `json:"RetCd"`
RetMsg string `json:"RetMsg"`
} `json:"RetInf"`
} `json:"array"`
} `json:"SYS_HEAD"`
APPHEAD struct {
BranchId string `json:"BranchId"`
TlrNo string `json:"TlrNo"`
Array struct {
AuthTlrInf struct {
ApprTellerNo string `json:"ApprTellerNo"`
} `json:"AuthTlrInf"`
} `json:"array"`
} `json:"APP_HEAD"`
TxnStrtNo string `json:"TxnStrtNo"`
QryRcrdNo string `json:"QryRcrdNo"`
Array []struct {
TxnInfArray struct {
TxnSysDt string `json:"TxnSysDt,omitempty"`
TxnSysTm string `json:"TxnSysTm,omitempty"`
OrigTxnSeqNo string `json:"OrigTxnSeqNo,omitempty"`
CnlNo string `json:"CnlNo,omitempty"`
SubBrId string `json:"SubBrId,omitempty"`
TxnTlrNo string `json:"TxnTlrNo,omitempty"`
TxnCd string `json:"TxnCd"`
TxnSmyDsc string `json:"TxnSmyDsc"`
TxnAmt string `json:"TxnAmt"`
TxnAfBal string `json:"TxnAfBal"`
TxnCcy string `json:"TxnCcy"`
CntprAcctNoOrCardNo string `json:"CntprAcctNoOrCardNo"`
AcctNm string `json:"AcctNm"`
OthrBnkBnkNo string `json:"OthrBnkBnkNo"`
ImprtBlkVchrTp string `json:"ImprtBlkVchrTp"`
ImprtBlkVchrNo string `json:"ImprtBlkVchrNo"`
RmkInf string `json:"RmkInf,omitempty"`
CnclRvrsFlg string `json:"CnclRvrsFlg,omitempty"`
} `json:"TxnInfArray"`
} `json:"array"`
} `json:"service"`
}

View File

@ -0,0 +1,102 @@
package handlers
import (
"beacon/services"
"github.com/gin-gonic/gin"
)
type WorkflowHandler struct {
service *services.WorkflowService
}
func NewWorkflowHandler() *WorkflowHandler {
return &WorkflowHandler{
service: &services.WorkflowService{},
}
}
// StartWorkflow godoc
// @Summary Start a workflow
// @Description Start a new workflow
// @Tags workflow
// @Accept json
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Failure 500 {object} map[string]interface{}
// @Router /workflows/start [post]
func (h *WorkflowHandler) StartWorkflow(c *gin.Context) {
err := h.service.Start("13")
if err != nil {
c.JSON(500, gin.H{
"code": -1,
"message": "Failed to start workflow",
})
return
}
c.JSON(200, gin.H{
"message": "Workflow started successfully",
})
}
// StopWorkflow POST /api/workflow/stop
func (h *WorkflowHandler) StopWorkflow(c *gin.Context) {
// 停止工作流的逻辑
// 这里可以调用 Temporal 的 API 来停止指定的工作流
// 例如,使用 WorkflowID 或 RunID 来停止工作流
c.JSON(200, gin.H{
"message": "Workflow stopped successfully",
})
}
// GetWorkflowStatus godoc
// @Summary Get workflow status
// @Description Get the status of a workflow by ID
// @Tags workflow
// @Produce json
// @Param id path string true "Workflow ID"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
// @Failure 500 {object} map[string]interface{}
// @Router /workflows/{id} [get]
func (h *WorkflowHandler) GetWorkflowStatus(c *gin.Context) {
workflowID := c.Param("workflowID")
if workflowID == "" {
c.JSON(400, gin.H{"error": "Workflow ID is required"})
return
}
status, err := h.service.GetWorkflowStatus(workflowID)
if err != nil {
c.JSON(500, gin.H{"error": "Failed to get workflow status", "details": err.Error()})
return
}
c.JSON(200, gin.H{"status": status})
}
// GetWorkflowResults godoc
// @Summary Get workflow results
// @Description Get the results of a workflow by ID
// @Tags workflow
// @Produce json
// @Param id path string true "Workflow ID"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
// @Failure 500 {object} map[string]interface{}
// @Router /workflows/{id}/results [get]
func (h *WorkflowHandler) GetWorkflowResults(c *gin.Context) {
workflowID := c.Param("workflowID")
if workflowID == "" {
c.JSON(400, gin.H{"error": "Workflow ID is required"})
return
}
results, err := h.service.GetWorkflowResults(workflowID)
if err != nil {
c.JSON(500, gin.H{"error": "Failed to get workflow results", "details": err.Error()})
return
}
c.JSON(200, gin.H{"results": results})
}

138
routers/router.go Normal file
View File

@ -0,0 +1,138 @@
package routers
import (
"beacon/config"
_ "beacon/docs"
"beacon/middleware"
"beacon/pkg/logger"
"beacon/routers/handlers"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)
func Init() *gin.Engine {
// 基于获取引擎对象(路由对象)
r := gin.New()
r.Use(middleware.GinLogger(logger.Lg))
r.Use(middleware.GinRecovery(logger.Lg, false))
//r.Use(middleware.AntiCrawlingStrategy()) // 反爬虫策略
r.Use(middleware.CSRFMiddleware)
//r.Use(middleware.JwtAuthMiddleware()) // 用户认证
// 设置自定义 404 页面
r.NoRoute(func(c *gin.Context) {
// 您可以使用 HTML 模板文件
//c.HTML(404, "404.html", gin.H{
// "title": "Page Not Found",
//})
// 或者,您可以直接返回 HTML 代码
c.Data(404, "text/html", []byte(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 Not Found</title>
<style>
body {
font-family: sans-serif;
margin: 0;
padding: 0;
background-color: #f5f5f5;
}
.container {
display: flex;
justify-content: center;
align-items: center;
text-align: center;
height: 100vh;
}
.content {
max-width: 600px;
padding: 30px;
background-color: #fff;
}
h1 {
font-size: 36px;
margin-bottom: 20px;
}
p {
font-size: 18px;
line-height: 1.5em;
}
a {
color: #000;
text-decoration: none;
}
</style>
</head>
<body>
<div class="container">
<div class="content">
<h1>404</h1>
<h2>Page Not Found</h2>
<p>The page you are looking for does not exist or has been moved.</p>
<p>You can <a href="/">go back to the homepage</a> or <a href="mailto:support@example.com">contact us</a> if you need assistance.</p>
</div>
</div>
</body>
</html>
`))
})
// 路由分组
v1 := r.Group(config.Conf.Version)
SetupCompositeCaseRoutes(v1)
SetupWorkflowRoutes(v1)
SetupTestRoutes(v1)
// 添加 Swagger 中间件
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
return r
}
func SetupCompositeCaseRoutes(group *gin.RouterGroup) {
// 初始化服务和处理器
//var handler *handlers.CompositeCaseHandler
handler := *handlers.NewCompositeCaseHandler()
api := group.Group("/api")
{
// 复合案例相关路由
api.POST("/composite-cases", handler.CreateCompositeCase)
api.GET("/composite-cases", handler.ListCompositeCases)
api.GET("/composite-cases/:id", handler.GetCompositeCase)
api.PUT("/composite-cases/:id", handler.UpdateCompositeCase)
api.DELETE("/composite-cases/:id", handler.DeleteCompositeCase)
}
}
func SetupWorkflowRoutes(group *gin.RouterGroup) {
// 初始化服务和处理器
handler := *handlers.NewWorkflowHandler()
api := group.Group("/api")
{
// 工作流相关路由
api.POST("/workflows/start", handler.StartWorkflow)
api.GET("/workflows/:id", handler.GetWorkflowStatus)
api.GET("/workflows/:id/results", handler.GetWorkflowResults)
}
}
func SetupTestRoutes(group *gin.RouterGroup) {
// 测试相关路由
testHandler := handlers.NewTestHandler()
api := group.Group("/api")
{
api.POST("/test/test-endpoint", testHandler.TestEndpoint)
api.POST("/test/core-transfer", testHandler.CoreTransferEndpoint)
}
}

View File

@ -1,19 +0,0 @@
package activities
import (
"context"
"fmt"
"math/rand"
)
// AddSuffixActivity appends a fixed suffix to the input data.
func AddSuffixActivity(ctx context.Context, data string) (string, error) {
suffixes := []string{
"-one", "-two", "-three", "-four", "-five",
"-six", "-seven", "-eight", "-nine", "-ten",
}
suffix := suffixes[rand.Intn(len(suffixes))]
result := fmt.Sprintf("%s%s", data, suffix)
fmt.Println("Go Activity: Modified data to:", result)
return result, nil
}

View File

@ -1,49 +0,0 @@
package main
import (
"beacon/server/workflows"
"context"
"fmt"
"github.com/google/uuid"
"go.temporal.io/sdk/client"
"log"
"os"
)
func main() {
if len(os.Args) < 2 {
log.Fatalln("Usage: go run main.go <data>")
}
inputData := os.Args[1]
// Create a Temporal client using the new Dial method.
c, err := client.Dial(client.Options{HostPort: "temporal.newai.day:17233"})
if err != nil {
log.Fatalln("Unable to create Temporal client", err)
}
defer c.Close()
// Generate a unique Workflow ID.
workflowID := "data-processing-workflow-" + uuid.New().String()
// Set up Workflow start options.
workflowOptions := client.StartWorkflowOptions{
ID: workflowID,
TaskQueue: "data-processing-task-queue",
}
// Start the Workflow.
we, err := c.ExecuteWorkflow(context.Background(), workflowOptions, workflows.TestRunWorkflow, inputData)
if err != nil {
log.Fatalln("Unable to execute Workflow", err)
}
// Wait for the Workflow to complete and get the result.
var result string
err = we.Get(context.Background(), &result)
if err != nil {
log.Fatalln("Unable to get Workflow result", err)
}
fmt.Println("Processed Data:", result)
}

View File

@ -1,52 +0,0 @@
package workflows
// 定义 Temporal Workflow
import (
"beacon/server/activities"
"fmt"
"go.temporal.io/sdk/temporal"
"go.temporal.io/sdk/workflow"
"strings"
"time"
)
// TestRunWorkflow 定义了整个测试执行的工作流
func TestRunWorkflow(ctx workflow.Context, data string) (string, error) {
logger := workflow.GetLogger(ctx)
retryPolicy := &temporal.RetryPolicy{
InitialInterval: time.Second, // First retry after 1 second
BackoffCoefficient: 2.0, // Double the wait time on each retry (1s → 2s → 4s → 8s, etc.)
MaximumInterval: 100 * time.Second, // Cap wait time at 100 seconds
MaximumAttempts: 50, // Retry up to 5 times before giving up
}
// Step 1: Add a prefix(Python)
pythonCtx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
TaskQueue: "python-task-queue",
StartToCloseTimeout: time.Minute,
RetryPolicy: retryPolicy,
})
var prefixed string
if err := workflow.ExecuteActivity(pythonCtx, "PythonAddRandomPrefixActivity", data).Get(pythonCtx, &prefixed); err != nil {
return "", fmt.Errorf("failed to add prefix: %w", err)
}
goCtx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: time.Minute, //activity must complete within 1 minute
RetryPolicy: retryPolicy,
})
var suffixed string
err := workflow.ExecuteActivity(goCtx, activities.AddSuffixActivity, prefixed).Get(goCtx, &suffixed)
if err != nil {
return "", fmt.Errorf("failed to add suffix: %w", err)
}
var processed string
// Demo Activity 3: Simulate uppercasing the data.
logger.Info("Demo Activity 3: Pretending to uppercase the data:", data)
processed = strings.ToUpper(suffixed)
return processed, nil
}

316
services/composite.go Normal file
View File

@ -0,0 +1,316 @@
package services
import (
"beacon/models"
"beacon/pkg/dao/mysql"
"beacon/utils"
"encoding/json"
"fmt"
"go.uber.org/zap"
)
type CompositeCaseService struct {
}
// CreateCompositeCase 创建复合案例
func (s *CompositeCaseService) CreateCompositeCase(req *models.CreateCompositeCaseRequest) (*models.CompositeCase, error) {
zap.L().Info("开始创建复合案例",
zap.String("name", req.Name),
zap.String("description", req.Description),
zap.String("status", req.Status),
zap.Int("steps_count", len(req.Steps)))
// 开启事务
tx := dao.BeginTransaction()
if tx.Error != nil {
zap.L().Error("创建复合案例失败 - 事务开启失败", zap.Error(tx.Error))
return nil, tx.Error
}
defer func() {
if r := recover(); r != nil {
zap.L().Error("创建复合案例失败 - 发生panic", zap.Any("panic", r))
tx.Rollback()
}
}()
// 创建复合案例
compositeCase := &models.CompositeCase{
Name: req.Name,
Description: req.Description,
Status: req.Status,
}
if err := dao.CreateCompositeCase(tx, compositeCase); err != nil {
zap.L().Error("创建复合案例失败 - 数据库创建失败", zap.Error(err))
tx.Rollback()
return nil, fmt.Errorf("创建复合案例失败: %w", err)
}
zap.L().Info("复合案例创建成功",
zap.Uint("id", compositeCase.ID),
zap.String("name", compositeCase.Name))
// 创建步骤
if len(req.Steps) > 0 {
zap.L().Info("开始创建步骤", zap.Int("steps_count", len(req.Steps)))
var steps []models.CompositeCaseStep
for _, stepReq := range req.Steps {
activityName, _ := utils.GetActivityName(stepReq.StepType)
step := models.CompositeCaseStep{
CompositeCaseID: compositeCase.ID,
StepOrder: stepReq.StepOrder,
StepName: stepReq.StepName,
StepDescription: stepReq.StepDescription,
StepType: stepReq.StepType,
ActivityName: activityName,
ParametersJson: fixParametersJson(stepReq.ParametersJson),
IsRequired: stepReq.IsRequired,
}
steps = append(steps, step)
}
if err := dao.CreateCompositeCaseSteps(tx, steps); err != nil {
zap.L().Error("创建复合案例步骤失败",
zap.Uint("composite_case_id", compositeCase.ID),
zap.Error(err))
tx.Rollback()
return nil, fmt.Errorf("创建复合案例步骤失败: %w", err)
}
zap.L().Info("复合案例步骤创建成功",
zap.Uint("composite_case_id", compositeCase.ID),
zap.Int("created_steps_count", len(steps)))
compositeCase.Steps = steps
}
tx.Commit()
zap.L().Info("复合案例创建完成",
zap.Uint("id", compositeCase.ID),
zap.String("name", compositeCase.Name))
return compositeCase, nil
}
// GetCompositeCaseByID 根据ID获取复合案例
func (s *CompositeCaseService) GetCompositeCaseByID(id uint) (*models.CompositeCase, error) {
zap.L().Debug("开始查询复合案例", zap.Uint("id", id))
compositeCase, err := dao.GetCompositeCaseByID(id)
if err != nil {
zap.L().Error("获取复合案例失败", zap.Uint("id", id), zap.Error(err))
return nil, err
}
zap.L().Debug("复合案例查询成功",
zap.Uint("id", compositeCase.ID),
zap.String("name", compositeCase.Name),
zap.Int("steps_count", len(compositeCase.Steps)))
return compositeCase, nil
}
// UpdateCompositeCase 更新复合案例
func (s *CompositeCaseService) UpdateCompositeCase(id uint, req *models.UpdateCompositeCaseRequest) (*models.CompositeCase, error) {
zap.L().Info("开始更新复合案例", zap.Uint("id", id))
// 开启事务
tx := dao.BeginTransaction()
if tx.Error != nil {
zap.L().Error("更新复合案例失败 - 事务开启失败",
zap.Uint("id", id),
zap.Error(tx.Error))
return nil, tx.Error
}
defer func() {
if r := recover(); r != nil {
zap.L().Error("更新复合案例失败 - 发生panic",
zap.Uint("id", id),
zap.Any("panic", r))
tx.Rollback()
}
}()
// 检查复合案例是否存在
if _, err := dao.ExistsCompositeCase(tx, id); err != nil {
tx.Rollback()
zap.L().Warn("更新复合案例失败 - 复合案例不存在", zap.Uint("id", id))
return nil, err
}
// 更新基本信息
updates := make(map[string]interface{})
if req.Name != "" {
updates["name"] = req.Name
}
if req.Description != "" {
updates["description"] = req.Description
}
if req.Status != "" {
updates["status"] = req.Status
}
if len(updates) > 0 {
zap.L().Info("更新复合案例基本信息",
zap.Uint("id", id),
zap.Any("updates", updates))
if err := dao.UpdateCompositeCase(tx, id, updates); err != nil {
zap.L().Error("更新复合案例基本信息失败",
zap.Uint("id", id),
zap.Error(err))
tx.Rollback()
return nil, fmt.Errorf("更新复合案例失败: %w", err)
}
}
// 更新步骤
if req.Steps != nil {
zap.L().Info("开始更新复合案例步骤",
zap.Uint("id", id),
zap.Int("new_steps_count", len(req.Steps)))
// 删除现有步骤
if err := dao.DeleteCompositeCaseStepsByCompositeCaseID(tx, id); err != nil {
zap.L().Error("删除现有步骤失败",
zap.Uint("id", id),
zap.Error(err))
tx.Rollback()
return nil, fmt.Errorf("删除现有步骤失败: %w", err)
}
// 创建新步骤
if len(req.Steps) > 0 {
var steps []models.CompositeCaseStep
for _, stepReq := range req.Steps {
activityName, _ := utils.GetActivityName(stepReq.StepType)
step := models.CompositeCaseStep{
CompositeCaseID: id,
StepOrder: stepReq.StepOrder,
StepName: stepReq.StepName,
StepDescription: stepReq.StepDescription,
StepType: stepReq.StepType,
ActivityName: activityName,
ParametersJson: fixParametersJson(stepReq.ParametersJson),
IsRequired: stepReq.IsRequired,
}
steps = append(steps, step)
}
if err := dao.CreateCompositeCaseSteps(tx, steps); err != nil {
zap.L().Error("创建新步骤失败",
zap.Uint("id", id),
zap.Error(err))
tx.Rollback()
return nil, fmt.Errorf("创建新步骤失败: %w", err)
}
zap.L().Info("新步骤创建成功",
zap.Uint("id", id),
zap.Int("created_steps_count", len(steps)))
}
}
tx.Commit()
zap.L().Info("复合案例更新完成", zap.Uint("id", id))
// 重新查询并返回更新后的数据
return s.GetCompositeCaseByID(id)
}
func fixParametersJson(jsonStr string) string {
var params map[string]interface{}
if err := json.Unmarshal([]byte(jsonStr), &params); err != nil {
// Not a valid json string, return as is.
return jsonStr
}
if rb, ok := params["request_body"]; ok {
if rbs, ok := rb.(string); ok && rbs == "{" {
params["request_body"] = "{}"
}
}
fixedJSONBytes, err := json.Marshal(params)
if err != nil {
// Failed to marshal back, return original.
return jsonStr
}
return string(fixedJSONBytes)
}
// DeleteCompositeCase 删除复合案例
func (s *CompositeCaseService) DeleteCompositeCase(id uint) error {
zap.L().Info("开始删除复合案例", zap.Uint("id", id))
// 开启事务
tx := dao.BeginTransaction()
if tx.Error != nil {
zap.L().Error("删除复合案例失败 - 事务开启失败",
zap.Uint("id", id),
zap.Error(tx.Error))
return tx.Error
}
defer func() {
if r := recover(); r != nil {
zap.L().Error("删除复合案例失败 - 发生panic",
zap.Uint("id", id),
zap.Any("panic", r))
tx.Rollback()
}
}()
// 检查复合案例是否存在
compositeCase, err := dao.ExistsCompositeCase(tx, id)
if err != nil {
tx.Rollback()
zap.L().Warn("删除复合案例失败", zap.Uint("id", id), zap.Error(err))
return err
}
zap.L().Info("找到复合案例,开始删除",
zap.Uint("id", id),
zap.String("name", compositeCase.Name))
// 删除关联的步骤
if err := dao.DeleteCompositeCaseStepsByCompositeCaseID(tx, id); err != nil {
zap.L().Error("删除复合案例步骤失败",
zap.Uint("id", id),
zap.Error(err))
tx.Rollback()
return fmt.Errorf("删除复合案例步骤失败: %w", err)
}
zap.L().Debug("复合案例步骤删除成功", zap.Uint("id", id))
// 删除复合案例
if err := dao.DeleteCompositeCase(tx, id); err != nil {
zap.L().Error("删除复合案例失败",
zap.Uint("id", id),
zap.Error(err))
tx.Rollback()
return fmt.Errorf("删除复合案例失败: %w", err)
}
tx.Commit()
zap.L().Info("复合案例删除完成",
zap.Uint("id", id),
zap.String("name", compositeCase.Name))
return nil
}
// ListCompositeCases 获取复合案例列表
func (s *CompositeCaseService) ListCompositeCases(page, pageSize int, status string) ([]models.CompositeCase, int64, error) {
zap.L().Info("开始查询复合案例列表",
zap.Int("page", page),
zap.Int("page_size", pageSize),
zap.String("status", status))
compositeCases, total, err := dao.ListCompositeCases(page, pageSize, status)
if err != nil {
zap.L().Error("获取复合案例列表失败", zap.Error(err))
return nil, 0, err
}
zap.L().Info("复合案例列表查询成功",
zap.Int("returned_count", len(compositeCases)),
zap.Int64("total_count", total))
return compositeCases, total, nil
}

79
services/workflow.go Normal file
View File

@ -0,0 +1,79 @@
package services
// Go 服务端入口,触发 Workflow
import (
"beacon/pkg/pb"
"beacon/workflows"
"context"
"github.com/google/uuid"
"go.temporal.io/sdk/client"
"go.uber.org/zap"
)
type WorkflowService struct {
}
func (s *WorkflowService) Start(compositeCaseId string) error {
// 创建 Temporal 客户端
c, err := client.Dial(client.Options{
HostPort: "temporal.newai.day:17233", // 根据你的 Temporal Server 配置
Namespace: "default",
})
if err != nil {
zap.L().Error("Unable to create Temporal client", zap.Error(err))
return err
}
defer c.Close()
// 模拟一个触发测试的事件 (例如来自 Web UI 或 CI/CD)
runID := uuid.New().String()
testInput := &pb.DynamicTestRunInput{
RunId: runID,
CompositeCaseId: compositeCaseId,
}
workflowOptions := client.StartWorkflowOptions{
ID: "test_workflow_" + runID,
TaskQueue: "data-task-queue", // 保持与 Python Worker 一致
}
zap.L().Info("Starting TestRunWorkflow", zap.String("runID", runID))
we, err := c.ExecuteWorkflow(context.Background(), workflowOptions, workflows.DynamicTestSuiteWorkflow, testInput)
if err != nil {
zap.L().Error("Unable to execute workflows", zap.Error(err))
return err
}
zap.L().Info("Workflow started", zap.String("workflowID", we.GetID()), zap.String("runID", we.GetRunID()))
// 等待 Workflow 完成并获取结果
var result pb.TestRunOutput
err = we.Get(context.Background(), &result)
if err != nil {
zap.L().Error("Unable to get workflows result", zap.Error(err))
return err
}
zap.L().Info("Workflow finished", zap.Bool("OverallSuccess", result.OverallSuccess), zap.String("Message", result.CompletionMessage))
zap.L().Debug("API Test Results", zap.Any("ApiResults", result.ApiResults))
zap.L().Debug("UI Test Results", zap.Any("UiResults", result.UiResults))
// 后续可以根据 result 生成报告、发送通知等
return nil
}
func (s *WorkflowService) GetWorkflowResults(workflowID string) (status string, err error) {
// 这里可以实现获取工作流结果的逻辑
// 例如,查询 Temporal Server 获取指定工作流的结果
// 可以使用 WorkflowID 或 RunID 来查询
zap.L().Debug("GetWorkflowResults", zap.Any("workflowID", workflowID))
return
}
func (s *WorkflowService) GetWorkflowStatus(workflowID string) (status string, err error) {
// 这里可以实现获取工作流状态的逻辑
// 例如,查询 Temporal Server 获取指定工作流的状态
// 可以使用 WorkflowID 或 RunID 来查询
zap.L().Debug("GetWorkflowStatus", zap.Any("workflowID", workflowID))
return
}

31
utils/file.go Normal file
View File

@ -0,0 +1,31 @@
package utils
import (
"fmt"
"go.uber.org/zap"
"io"
"mime/multipart"
)
func FileStreamToBytes(file *multipart.FileHeader) []byte {
// 打开文件
f, err := file.Open()
if err != nil {
zap.L().Info(fmt.Sprintf("无法打开文件: %s", err.Error()))
return nil
}
defer func(file multipart.File) {
err := file.Close()
if err != nil {
zap.L().Warn(fmt.Sprintf("警告文件未正常关闭:%s", err.Error()))
}
}(f)
// 读取文件的所有内容
fileBytes, err := io.ReadAll(f)
if err != nil {
zap.L().Info(fmt.Sprintf("无法读取文件内容: %s", err.Error()))
return nil
}
return fileBytes
}

View File

@ -0,0 +1,484 @@
package utils
import (
"encoding/json"
"fmt"
"go.uber.org/zap"
"regexp"
"strconv"
"strings"
"beacon/pkg/pb"
)
// ParameterProcessor 参数处理器用于处理activity之间的参数传递
type ParameterProcessor struct {
globalVariables map[string]interface{}
activityResults map[int64]*ActivityResult
}
// ActivityResult 用于存储每个activity的执行结果
type ActivityResult struct {
StepID int64 `json:"step_id"`
StepName string `json:"step_name"`
Success bool `json:"success"`
Data interface{} `json:"data"`
}
// NewParameterProcessor 创建新的参数处理器
func NewParameterProcessor(globalVariables map[string]interface{}) *ParameterProcessor {
return &ParameterProcessor{
globalVariables: globalVariables,
activityResults: make(map[int64]*ActivityResult),
}
}
// AddActivityResult 添加activity执行结果
func (p *ParameterProcessor) AddActivityResult(stepID int64, result *ActivityResult) {
p.activityResults[stepID] = result
}
// ProcessTemplate 处理参数模板,替换其中的变量引用
func (p *ParameterProcessor) ProcessTemplate(template string) (string, error) {
if template == "" {
return template, nil
}
result := template
// 1. 替换全局变量引用 ${global.key}
result = p.replaceGlobalVariables(result)
// 2. 替换步骤结果变量引用 ${step.stepId.field}
result = p.replaceStepVariables(result)
// 3. 替换条件变量引用 ${if.condition:value1:value2}
result = p.replaceConditionalVariables(result)
// 4. 替换函数调用 ${func:arg1:arg2}
result = p.replaceFunctionCalls(result)
return result, nil
}
// replaceGlobalVariables 替换全局变量引用
func (p *ParameterProcessor) replaceGlobalVariables(template string) string {
// 使用正则表达式匹配 ${global.key} 格式
re := regexp.MustCompile(`\$\{global\.([^}]+)\}`)
return re.ReplaceAllStringFunc(template, func(match string) string {
// 提取变量名
matches := re.FindStringSubmatch(match)
if len(matches) < 2 {
return match // 保持原样
}
key := matches[1]
if value, exists := p.globalVariables[key]; exists {
return fmt.Sprintf("%v", value)
}
return match // 变量不存在,保持原样
})
}
// replaceStepVariables 替换步骤结果变量引用
func (p *ParameterProcessor) replaceStepVariables(template string) string {
// 匹配 ${step.stepId.field} 格式
re := regexp.MustCompile(`\$\{step\.(\d+)\.([^}]+)\}`)
return re.ReplaceAllStringFunc(template, func(match string) string {
matches := re.FindStringSubmatch(match)
if len(matches) < 3 {
return match
}
stepIDStr := matches[1]
field := matches[2]
stepID, err := strconv.ParseInt(stepIDStr, 10, 64)
if err != nil {
return match
}
result, exists := p.activityResults[stepID]
if !exists {
return match
}
return p.extractFieldValue(result, field)
})
}
// replaceConditionalVariables 替换条件变量引用
func (p *ParameterProcessor) replaceConditionalVariables(template string) string {
// 匹配 ${if.condition:value1:value2} 格式
re := regexp.MustCompile(`\$\{if\.([^:]+):([^:]*):([^}]*)\}`)
return re.ReplaceAllStringFunc(template, func(match string) string {
matches := re.FindStringSubmatch(match)
if len(matches) < 4 {
return match
}
condition := matches[1]
valueIfTrue := matches[2]
valueIfFalse := matches[3]
if p.evaluateCondition(condition) {
return valueIfTrue
}
return valueIfFalse
})
}
// replaceFunctionCalls 替换函数调用
func (p *ParameterProcessor) replaceFunctionCalls(template string) string {
// 匹配 ${func:arg1:arg2} 格式
re := regexp.MustCompile(`\$\{([^:]+):([^}]*)\}`)
return re.ReplaceAllStringFunc(template, func(match string) string {
matches := re.FindStringSubmatch(match)
if len(matches) < 3 {
return match
}
funcName := matches[1]
args := matches[2]
return p.executeFunction(funcName, args)
})
}
// extractFieldValue 从activity结果中提取指定字段的值
func (p *ParameterProcessor) extractFieldValue(result *ActivityResult, field string) string {
switch result.Data.(type) {
case *pb.ApiTestResult:
return p.extractApiResultField(result.Data.(*pb.ApiTestResult), field)
case *pb.UiTestResult:
return p.extractUiResultField(result.Data.(*pb.UiTestResult), field)
default:
return ""
}
}
// extractApiResultField 从API测试结果中提取字段值
func (p *ParameterProcessor) extractApiResultField(result *pb.ApiTestResult, field string) string {
switch field {
case "response_body":
// 保证输出是 JSON 字符串格式
b, err := json.Marshal(result.ResponseBody)
if err == nil {
return string(b[1 : len(b)-1]) // 去掉外层引号
}
return result.ResponseBody
case "actual_status_code":
return strconv.Itoa(int(result.ActualStatusCode))
case "success":
return strconv.FormatBool(result.BaseResult.Success)
case "message":
return result.BaseResult.Message
default:
zap.L().Debug("未知字段", zap.String("field", field), zap.String("ResponseBody", result.ResponseBody))
// 检查是否是响应头
if strings.HasPrefix(field, "headers.") {
headerKey := strings.TrimPrefix(field, "headers.")
if headerValue, exists := result.Headers[headerKey]; exists {
return headerValue
}
}
// 检查是否是JSON字段
if strings.HasPrefix(field, "json.") {
jsonField := strings.TrimPrefix(field, "json.")
return p.extractJsonField(result.ResponseBody, jsonField)
}
return ""
}
}
// extractUiResultField 从UI测试结果中提取字段值
func (p *ParameterProcessor) extractUiResultField(result *pb.UiTestResult, field string) string {
switch field {
case "screenshot_url":
return result.ScreenshotUrl
case "html_report_url":
return result.HtmlReportUrl
case "success":
return strconv.FormatBool(result.BaseResult.Success)
case "message":
return result.BaseResult.Message
default:
return ""
}
}
// extractJsonField 从JSON响应体中提取指定字段
func (p *ParameterProcessor) extractJsonField(responseBody, field string) string {
if responseBody == "" {
return ""
}
var data map[string]interface{}
if err := json.Unmarshal([]byte(responseBody), &data); err != nil {
return ""
}
// 支持嵌套字段,如 "user.id"
keys := strings.Split(field, ".")
current := data
for i, key := range keys {
if i == len(keys)-1 {
// 最后一个键
if value, exists := current[key]; exists {
return fmt.Sprintf("%v", value)
}
} else {
// 中间键,需要继续深入
if next, exists := current[key]; exists {
if nextMap, ok := next.(map[string]interface{}); ok {
current = nextMap
} else {
return ""
}
} else {
return ""
}
}
}
return ""
}
// evaluateCondition 评估条件表达式
func (p *ParameterProcessor) evaluateCondition(condition string) bool {
// 支持简单的条件表达式
// 例如step.123.success, global.environment == "production"
// 检查步骤成功状态
if strings.HasSuffix(condition, ".success") {
stepIDStr := strings.TrimSuffix(condition, ".success")
if strings.HasPrefix(stepIDStr, "step.") {
stepIDStr = strings.TrimPrefix(stepIDStr, "step.")
stepID, err := strconv.ParseInt(stepIDStr, 10, 64)
if err == nil {
if result, exists := p.activityResults[stepID]; exists {
return result.Success
}
}
}
}
// 检查全局变量
if strings.HasPrefix(condition, "global.") {
key := strings.TrimPrefix(condition, "global.")
if value, exists := p.globalVariables[key]; exists {
// 转换为布尔值
switch v := value.(type) {
case bool:
return v
case string:
return v != "" && v != "false" && v != "0"
case int, int64, float64:
return fmt.Sprintf("%v", v) != "0"
default:
return value != nil
}
}
}
// 检查相等性
if strings.Contains(condition, "==") {
parts := strings.Split(condition, "==")
if len(parts) == 2 {
left := strings.TrimSpace(parts[0])
right := strings.TrimSpace(strings.Trim(parts[1], `"'`))
// 检查全局变量
if strings.HasPrefix(left, "global.") {
key := strings.TrimPrefix(left, "global.")
if value, exists := p.globalVariables[key]; exists {
return fmt.Sprintf("%v", value) == right
}
}
}
}
return false
}
// executeFunction 执行函数调用
func (p *ParameterProcessor) executeFunction(funcName, args string) string {
argList := strings.Split(args, ":")
switch funcName {
case "concat":
// 连接字符串
return strings.Join(argList, "")
case "upper":
// 转换为大写
if len(argList) > 0 {
return strings.ToUpper(argList[0])
}
case "lower":
// 转换为小写
if len(argList) > 0 {
return strings.ToLower(argList[0])
}
case "substring":
// 子字符串
if len(argList) >= 3 {
str := argList[0]
start, _ := strconv.Atoi(argList[1])
end, _ := strconv.Atoi(argList[2])
if start < len(str) && end <= len(str) && start < end {
return str[start:end]
}
}
case "default":
// 默认值
if len(argList) >= 2 {
value := argList[0]
defaultValue := argList[1]
if value == "" {
return defaultValue
}
return value
}
}
return ""
}
// GetAvailableVariables 获取所有可用的变量列表
func (p *ParameterProcessor) GetAvailableVariables() map[string][]string {
variables := make(map[string][]string)
// 全局变量
var globalVars []string
for key := range p.globalVariables {
globalVars = append(globalVars, key)
}
variables["global"] = globalVars
// 步骤变量
for stepID, result := range p.activityResults {
stepKey := fmt.Sprintf("step.%d", stepID)
var stepVars []string
switch result.Data.(type) {
case *pb.ApiTestResult:
stepVars = []string{
"response_body",
"actual_status_code",
"success",
"message",
"headers.*",
"json.*",
}
case *pb.UiTestResult:
stepVars = []string{
"screenshot_url",
"html_report_url",
"success",
"message",
}
}
variables[stepKey] = stepVars
}
return variables
}
// ValidateTemplate 验证模板中的变量引用是否有效
func (p *ParameterProcessor) ValidateTemplate(template string) (bool, []string) {
var errors []string
// 检查全局变量引用
globalRe := regexp.MustCompile(`\$\{global\.([^}]+)\}`)
matches := globalRe.FindAllStringSubmatch(template, -1)
for _, match := range matches {
if len(match) >= 2 {
key := match[1]
if _, exists := p.globalVariables[key]; !exists {
errors = append(errors, fmt.Sprintf("全局变量 '%s' 不存在", key))
}
}
}
// 检查步骤变量引用
stepRe := regexp.MustCompile(`\$\{step\.(\d+)\.([^}]+)\}`)
matches = stepRe.FindAllStringSubmatch(template, -1)
for _, match := range matches {
if len(match) >= 3 {
stepIDStr := match[1]
field := match[2]
stepID, err := strconv.ParseInt(stepIDStr, 10, 64)
if err != nil {
errors = append(errors, fmt.Sprintf("无效的步骤ID: %s", stepIDStr))
continue
}
if _, exists := p.activityResults[stepID]; !exists {
errors = append(errors, fmt.Sprintf("步骤 %d 不存在", stepID))
continue
}
// 检查字段是否有效
if !p.isValidField(stepID, field) {
errors = append(errors, fmt.Sprintf("步骤 %d 不支持字段: %s", stepID, field))
}
}
}
return len(errors) == 0, errors
}
// isValidField 检查字段是否有效
func (p *ParameterProcessor) isValidField(stepID int64, field string) bool {
result, exists := p.activityResults[stepID]
if !exists {
return false
}
switch result.Data.(type) {
case *pb.ApiTestResult:
validFields := map[string]bool{
"response_body": true,
"actual_status_code": true,
"success": true,
"message": true,
}
if validFields[field] {
return true
}
// 检查是否是响应头
if strings.HasPrefix(field, "headers.") {
return true
}
// 检查是否是JSON字段
if strings.HasPrefix(field, "json.") {
return true
}
case *pb.UiTestResult:
validFields := map[string]bool{
"screenshot_url": true,
"html_report_url": true,
"success": true,
"message": true,
}
return validFields[field]
}
return false
}

View File

@ -0,0 +1,385 @@
package utils
import (
"beacon/pkg/pb"
"testing"
)
func TestParameterProcessor_ProcessTemplate(t *testing.T) {
// 创建测试用的全局变量
globalVariables := map[string]interface{}{
"base_url": "https://api.example.com",
"environment": "staging",
"api_version": "v1",
"timeout": "30",
"retry_count": "3",
}
// 创建参数处理器
processor := NewParameterProcessor(globalVariables)
// 模拟API测试结果
apiResult := &pb.ApiTestResult{
BaseResult: &pb.BaseTestResult{
Success: true,
Message: "API Test Passed",
},
ActualStatusCode: 200,
ResponseBody: `{"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "user_id": 456, "name": "John Doe"}`,
Headers: map[string]string{
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"Content-Type": "application/json",
},
}
// 添加activity结果
processor.AddActivityResult(123, &ActivityResult{
StepID: 123,
StepName: "RunApiTest",
Success: true,
Data: apiResult,
})
// 测试用例
tests := []struct {
name string
template string
expected string
}{
{
name: "全局变量替换",
template: `{"endpoint": "${global.base_url}/${global.api_version}/users"}`,
expected: `{"endpoint": "https://api.example.com/v1/users"}`,
},
{
name: "API响应体替换",
template: `{"token": "${step.123.response_body}"}`,
expected: `{"token": "{\"token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\", \"user_id\": 456, \"name\": \"John Doe\"}"}`,
},
{
name: "API状态码替换",
template: `{"status": "${step.123.actual_status_code}"}`,
expected: `{"status": "200"}`,
},
{
name: "API响应头替换",
template: `{"auth": "${step.123.headers.Authorization}"}`,
expected: `{"auth": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}`,
},
{
name: "JSON字段提取",
template: `{"user_id": "${step.123.json.user_id}", "name": "${step.123.json.name}"}`,
expected: `{"user_id": "456", "name": "John Doe"}`,
},
{
name: "混合变量替换",
template: `{"endpoint": "${global.base_url}/${global.api_version}/users/${step.123.json.user_id}", "headers": {"Authorization": "${step.123.json.token}"}}`,
expected: `{"endpoint": "https://api.example.com/v1/users/456", "headers": {"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}}`,
},
{
name: "条件变量替换",
template: `{"env": "${if.step.123.success:production:staging}"}`,
expected: `{"env": "production"}`,
},
{
name: "函数调用",
template: `{"upper_name": "${upper:${step.123.json.name}}"}`,
expected: `{"upper_name": "JOHN DOE"}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := processor.ProcessTemplate(tt.template)
if err != nil {
t.Errorf("ProcessTemplate() error = %v", err)
return
}
if result != tt.expected {
t.Errorf("ProcessTemplate() = %v, want %v", result, tt.expected)
}
})
}
}
func TestParameterProcessor_ValidateTemplate(t *testing.T) {
// 创建测试用的全局变量
globalVariables := map[string]interface{}{
"base_url": "https://api.example.com",
"environment": "staging",
}
// 创建参数处理器
processor := NewParameterProcessor(globalVariables)
// 添加一个activity结果
apiResult := &pb.ApiTestResult{
BaseResult: &pb.BaseTestResult{
Success: true,
Message: "API Test Passed",
},
ActualStatusCode: 200,
ResponseBody: `{"token": "test-token"}`,
Headers: map[string]string{
"Authorization": "Bearer test-token",
},
}
processor.AddActivityResult(123, &ActivityResult{
StepID: 123,
StepName: "RunApiTest",
Success: true,
Data: apiResult,
})
// 测试用例
tests := []struct {
name string
template string
expectValid bool
expectErrors int
}{
{
name: "有效模板",
template: `{"endpoint": "${global.base_url}/users", "token": "${step.123.json.token}"}`,
expectValid: true,
expectErrors: 0,
},
{
name: "无效的全局变量",
template: `{"endpoint": "${global.invalid_key}/users"}`,
expectValid: false,
expectErrors: 1,
},
{
name: "无效的步骤ID",
template: `{"token": "${step.999.json.token}"}`,
expectValid: false,
expectErrors: 1,
},
{
name: "无效的字段",
template: `{"invalid": "${step.123.invalid_field}"}`,
expectValid: false,
expectErrors: 1,
},
{
name: "多个错误",
template: `{"endpoint": "${global.invalid_key}/users", "token": "${step.999.json.token}"}`,
expectValid: false,
expectErrors: 2,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
isValid, errors := processor.ValidateTemplate(tt.template)
if isValid != tt.expectValid {
t.Errorf("ValidateTemplate() isValid = %v, want %v", isValid, tt.expectValid)
}
if len(errors) != tt.expectErrors {
t.Errorf("ValidateTemplate() errors count = %d, want %d", len(errors), tt.expectErrors)
}
})
}
}
func TestParameterProcessor_GetAvailableVariables(t *testing.T) {
// 创建测试用的全局变量
globalVariables := map[string]interface{}{
"base_url": "https://api.example.com",
"environment": "staging",
"api_version": "v1",
}
// 创建参数处理器
processor := NewParameterProcessor(globalVariables)
// 添加API测试结果
apiResult := &pb.ApiTestResult{
BaseResult: &pb.BaseTestResult{
Success: true,
Message: "API Test Passed",
},
ActualStatusCode: 200,
ResponseBody: `{"token": "test-token"}`,
Headers: map[string]string{
"Authorization": "Bearer test-token",
},
}
processor.AddActivityResult(123, &ActivityResult{
StepID: 123,
StepName: "RunApiTest",
Success: true,
Data: apiResult,
})
// 添加UI测试结果
uiResult := &pb.UiTestResult{
BaseResult: &pb.BaseTestResult{
Success: true,
Message: "UI Test Passed",
},
ScreenshotUrl: "https://s3.amazonaws.com/screenshots/test.png",
HtmlReportUrl: "https://s3.amazonaws.com/reports/test.html",
}
processor.AddActivityResult(456, &ActivityResult{
StepID: 456,
StepName: "RunUiTest",
Success: true,
Data: uiResult,
})
// 获取可用变量
variables := processor.GetAvailableVariables()
// 验证全局变量
if globalVars, exists := variables["global"]; !exists {
t.Error("Global variables not found")
} else {
expectedGlobalVars := []string{"base_url", "environment", "api_version"}
for _, expected := range expectedGlobalVars {
found := false
for _, actual := range globalVars {
if actual == expected {
found = true
break
}
}
if !found {
t.Errorf("Global variable '%s' not found", expected)
}
}
}
// 验证步骤变量
if step123Vars, exists := variables["step.123"]; !exists {
t.Error("Step 123 variables not found")
} else {
expectedStepVars := []string{"response_body", "actual_status_code", "success", "message", "headers.*", "json.*"}
for _, expected := range expectedStepVars {
found := false
for _, actual := range step123Vars {
if actual == expected {
found = true
break
}
}
if !found {
t.Errorf("Step 123 variable '%s' not found", expected)
}
}
}
if step456Vars, exists := variables["step.456"]; !exists {
t.Error("Step 456 variables not found")
} else {
expectedStepVars := []string{"screenshot_url", "html_report_url", "success", "message"}
for _, expected := range expectedStepVars {
found := false
for _, actual := range step456Vars {
if actual == expected {
found = true
break
}
}
if !found {
t.Errorf("Step 456 variable '%s' not found", expected)
}
}
}
}
func TestParameterProcessor_ComplexScenarios(t *testing.T) {
// 创建测试用的全局变量
globalVariables := map[string]interface{}{
"base_url": "https://api.example.com",
"environment": "production",
"api_version": "v2",
"timeout": "60",
}
// 创建参数处理器
processor := NewParameterProcessor(globalVariables)
// 模拟用户注册结果
registerResult := &pb.ApiTestResult{
BaseResult: &pb.BaseTestResult{
Success: true,
Message: "User registered successfully",
},
ActualStatusCode: 201,
ResponseBody: `{"user_id": 789, "username": "newuser", "email": "newuser@example.com"}`,
Headers: map[string]string{
"Content-Type": "application/json",
},
}
// 模拟用户登录结果
loginResult := &pb.ApiTestResult{
BaseResult: &pb.BaseTestResult{
Success: true,
Message: "User logged in successfully",
},
ActualStatusCode: 200,
ResponseBody: `{"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refresh_token": "refresh123", "expires_in": 3600}`,
Headers: map[string]string{
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"Content-Type": "application/json",
},
}
// 添加activity结果
processor.AddActivityResult(201, &ActivityResult{
StepID: 201,
StepName: "RunApiTest",
Success: true,
Data: registerResult,
})
processor.AddActivityResult(202, &ActivityResult{
StepID: 202,
StepName: "RunApiTest",
Success: true,
Data: loginResult,
})
// 测试复杂的参数传递场景
complexTemplate := `{
"test_case_id": "get_user_profile",
"endpoint": "${global.base_url}/${global.api_version}/users/${step.201.json.user_id}/profile",
"http_method": "GET",
"headers": {
"Authorization": "Bearer ${step.202.json.token}",
"X-Environment": "${global.environment}",
"X-Timeout": "${global.timeout}",
"Content-Type": "application/json"
},
"expected_status_code": 200
}`
expected := `{
"test_case_id": "get_user_profile",
"endpoint": "https://api.example.com/v2/users/789/profile",
"http_method": "GET",
"headers": {
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"X-Environment": "production",
"X-Timeout": "60",
"Content-Type": "application/json"
},
"expected_status_code": 200
}`
result, err := processor.ProcessTemplate(complexTemplate)
if err != nil {
t.Errorf("ProcessTemplate() error = %v", err)
return
}
if result != expected {
t.Errorf("ProcessTemplate() = %v, want %v", result, expected)
}
}

13
utils/router.go Executable file
View File

@ -0,0 +1,13 @@
package utils
import "github.com/gin-gonic/gin"
func RouterRegister(group *gin.RouterGroup, requestMethods []string, relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
/* 路由注册:一次性给同一个视图方法绑定多种请求方式 */
/* 可通switch来分发不同的请求 */
var routers gin.IRoutes
for _, requestMethod := range requestMethods {
routers = group.Handle(requestMethod, relativePath, handlers...)
}
return routers
}

View File

@ -0,0 +1,42 @@
package utils
import (
"fmt"
"strings"
)
// GetActivityName 根据步骤类型获取对应的 Activity 函数名
// 参数:
// - stepType: 数据库中的步骤类型,如 "api", "ui" 等
//
// 返回值:
// - activityName: 对应的 Temporal Activity 函数名
// - error: 如果步骤类型不支持则返回错误
func GetActivityName(stepType string) (string, error) {
// 标准化输入:转为大写并去除空格
normalizedStepType := strings.ToUpper(strings.TrimSpace(stepType))
// 步骤类型到 Activity 名称的映射表
stepTypeToActivity := map[string]string{
// ================== 测试类型 Activity ==================
"API": "RunApiTest", // API 接口测试
"UI": "RunUiTest", // UI 自动化测试
"SQL": "RunSqlTest", // SQL 执行sql语句
"PYTHON": "RunPythonTest", // PYTHON 执行python脚本处理数据
}
// 查找对应的 Activity 名称
activityName, exists := stepTypeToActivity[normalizedStepType]
if !exists {
// 返回详细的错误信息,包含支持的类型列表
supportedTypes := make([]string, 0, len(stepTypeToActivity))
for stepType := range stepTypeToActivity {
supportedTypes = append(supportedTypes, stepType)
}
return "", fmt.Errorf("unsupported step type '%s'. Supported types: %v",
stepType, supportedTypes)
}
return activityName, nil
}

View File

@ -1,34 +0,0 @@
package main
import (
"beacon/server/activities"
"beacon/server/workflows"
"go.temporal.io/sdk/client"
"go.temporal.io/sdk/worker"
"log"
)
func main() {
// Create a Temporal client with the default options.
c, err := client.Dial(client.Options{HostPort: "temporal.newai.day:17233"})
if err != nil {
log.Fatalln("Unable to create Temporal client", err)
}
defer c.Close()
taskQueue := "data-processing-task-queue"
// Create a Worker that listens on the specified Task Queue.
w := worker.New(c, taskQueue, worker.Options{})
// Register the Workflow and the real Go suffix Activity with the Worker.
w.RegisterWorkflow(workflows.TestRunWorkflow)
w.RegisterActivity(activities.AddSuffixActivity)
//(for later) w.RegisterActivity(activities.AddSuffixActivity)
// Start the Worker. This call blocks until the Worker is interrupted.
err = w.Run(worker.InterruptCh())
if err != nil {
log.Fatalln("Unable to start Worker", err)
}
}

70
workers/go/workers.go Normal file
View File

@ -0,0 +1,70 @@
package workers
import (
"beacon/activities"
"beacon/config"
"beacon/workflows"
"fmt"
"go.temporal.io/sdk/client"
"go.temporal.io/sdk/worker"
"go.uber.org/zap"
"log"
)
func StartWorkflow(cnf *config.TemporalConfig) error {
// 创建使用默认选项的 Temporal 客户端。
c, err := client.Dial(client.Options{HostPort: fmt.Sprintf("%s:%d", cnf.Host, cnf.Port)})
if err != nil {
log.Fatalln("Unable to create Temporal client", err)
}
defer c.Close()
taskQueue := "data-task-queue"
// 创建一个监听指定任务队列的 Worker。
w := worker.New(c, taskQueue, worker.Options{})
// 将工作流和带有真实 Go 后缀的活动注册到 Worker。
w.RegisterWorkflow(workflows.TestRunWorkflow)
w.RegisterWorkflow(workflows.DynamicTestSuiteWorkflow)
w.RegisterActivity(activities.LoadCompositeCaseSteps)
//(for later) w.RegisterActivity(activities.AddSuffixActivity)
// 注意Python 和 ts 活动将由 Python/ts 进程处理,此处未进行注册。
// 启动 Worker。此调用会阻塞直到 Worker 被中断。
err = w.Run(worker.InterruptCh())
if err != nil {
zap.L().Error(fmt.Sprintf("Unable to start worker: %v", err))
}
return nil
}
/*func main() {
// 创建使用默认选项的 Temporal 客户端。
c, err := client.Dial(client.Options{HostPort: "temporal.newai.day:17233"})
if err != nil {
log.Fatalln("Unable to create Temporal client", err)
}
defer c.Close()
taskQueue := "test-task-queue"
// 创建一个监听指定任务队列的 Worker。
w := worker.New(c, taskQueue, worker.Options{})
// 将工作流和带有真实 Go 后缀的活动注册到 Worker。
w.RegisterWorkflow(workflows.TestRunWorkflow)
w.RegisterWorkflow(workflows.DynamicTestSuiteWorkflow)
w.RegisterActivity(activities.LoadCompositeCaseSteps)
//(for later) w.RegisterActivity(activities.AddSuffixActivity)
// 注意Python 和 ts 活动将由 Python/ts 进程处理,此处未进行注册。
// 启动 Worker。此调用会阻塞直到 Worker 被中断。
err = w.Run(worker.InterruptCh())
if err != nil {
log.Fatalln("Unable to start Worker", err)
}
}
*/

View File

@ -1,92 +1,180 @@
# 实现 Temporal Activity 逻辑
import asyncio
import os
import sys
import time
import random
from temporalio import activity
# 确保能导入 gen 模块
# 确保能导入 gen 模块将gen目录添加到Python路径
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), 'gen')))
# 全局变量来存储 protobuf 模块
from gen import common_test_pb2 as pb
# 导入protobuf生成的模块和其他依赖
from pb import common_test_pb2 as pb
from api_tests import execute_api_test_case
from ui_tests import execute_ui_test_case
from utils import upload_file_to_s3, scalar_map_to_dict
class TestActivities:
@activity.defn(name="run_api_test")
async def run_api_test(self,req: pb.ApiTestRequest) -> pb.ApiTestResult:
"""执行API测试的Temporal Activity实现"""
"""
测试活动类包含API测试和UI测试的Temporal Activity实现
"""
@staticmethod
async def _heartbeat_task(interval_seconds=30):
"""
心跳任务定期发送心跳信号防止长时间运行的Activity被Temporal服务器认为已死亡
Args:
interval_seconds (int): 心跳发送间隔默认30秒
"""
while True:
try:
# 等待指定间隔时间
await asyncio.sleep(interval_seconds)
# 发送心跳信号告知Temporal服务器Activity仍在运行
activity.heartbeat()
activity.logger.debug("Activity heartbeat sent")
except asyncio.CancelledError:
# 心跳任务被取消,正常退出
activity.logger.debug("Heartbeat task cancelled")
break
except Exception as e:
# 心跳发送失败,记录警告但继续尝试
activity.logger.warning(f"Failed to send heartbeat: {e}")
@activity.defn(name="RunApiTest")
async def run_api_test(self, req: pb.ApiTestRequest) -> pb.ApiTestResult:
"""
执行API测试的Temporal Activity实现
Args:
req (pb.ApiTestRequest): API测试请求对象包含测试用例ID端点HTTP方法等信息
Returns:
pb.ApiTestResult: API测试结果对象包含测试状态响应数据等信息
"""
activity.logger.info(f"Received API Test Request: {req.test_case_id}")
# 记录测试开始时间,用于计算执行时长
start_time = time.time()
# 初始化测试结果对象
result = pb.ApiTestResult()
result.base_result.test_case_id = req.test_case_id
# 启动后台心跳任务,确保长时间运行的测试不会超时
heartbeat_task = asyncio.create_task(self._heartbeat_task())
try:
# 调用实际的API测试逻辑
api_test_success, actual_status, response_body, log_output = execute_api_test_case(
# 发送初始心跳信号
activity.heartbeat()
# 调用实际的API测试逻辑执行HTTP请求并验证响应
api_test_success, actual_status, response_headers, response_body, log_output = execute_api_test_case(
req.test_case_id, req.endpoint, req.http_method, scalar_map_to_dict(req.headers), req.request_body,
req.expected_status_code
)
# 填充测试结果
result.base_result.success = api_test_success
result.actual_status_code = actual_status
result.response_body = response_body.decode('utf-8') if isinstance(response_body, bytes) else str(response_body)
# 处理响应体,确保为字符串格式
result.response_body = response_body.decode('utf-8') if isinstance(response_body, bytes) else str(
response_body)
result.base_result.log_output = log_output
result.base_result.message = "API Test Passed" if api_test_success else "API Test Failed"
# 处理响应头信息
if response_headers:
for key, value in response_headers.items():
# 确保键和值都是字符串类型
result.headers[str(key)] = str(value)
except Exception as e:
# 捕获测试执行过程中的异常
activity.logger.error(f"API Test Failed for {req.test_case_id}: {e}")
result.base_result.success = False
result.base_result.message = f"API Test Error: {e}"
result.base_result.error_details = str(e)
finally:
# 清理工作:取消心跳任务
heartbeat_task.cancel()
try:
# 等待心跳任务完全结束
await heartbeat_task
except asyncio.CancelledError:
pass
# 计算并记录测试执行时长
result.base_result.duration_seconds = time.time() - start_time
return result
@activity.defn(name="run_ui_test")
async def run_ui_test(self,req: pb.UiTestRequest) -> pb.UiTestResult:
"""执行UI测试的Temporal Activity实现"""
@activity.defn(name="RunUiTest")
async def run_ui_test(self, req: pb.UiTestRequest) -> pb.UiTestResult:
"""
执行UI测试的Temporal Activity实现
Args:
req (pb.UiTestRequest): UI测试请求对象包含测试用例IDURL路径浏览器类型等信息
Returns:
pb.UiTestResult: UI测试结果对象包含测试状态截图URL报告URL等信息
"""
activity.logger.info(f"Received UI Test Request: {req.test_case_id}")
# 记录测试开始时间
start_time = time.time()
# 初始化测试结果对象
result = pb.UiTestResult()
result.base_result.test_case_id = req.test_case_id
# 启动后台心跳任务
heartbeat_task = asyncio.create_task(self._heartbeat_task())
try:
# 调用实际的UI测试逻辑返回本地文件路径
# 发送初始心跳信号
activity.heartbeat()
# 调用实际的UI测试逻辑执行浏览器自动化测试并返回本地文件路径
ui_test_success, log_output, screenshot_path, html_report_path = await execute_ui_test_case(
req.test_case_id, req.url_path, req.browser_type, req.headless, scalar_map_to_dict(req.user_data)
)
# 填充基本测试结果
result.base_result.success = ui_test_success
result.base_result.log_output = log_output
result.base_result.message = "UI Test Passed" if ui_test_success else "UI Test Failed"
# 上传截图和报告到对象存储并返回URL
# 处理测试生成的文件:上传截图和报告到对象存储并返回URL
if screenshot_path and os.path.exists(screenshot_path):
# 在长时间操作前发送心跳,防止超时
activity.heartbeat()
# 上传截图到S3并获取访问URL
result.screenshot_url = await upload_file_to_s3(screenshot_path, f"screenshots/{req.test_case_id}.png")
os.remove(screenshot_path) # 清理本地文件
# 清理本地临时文件
os.remove(screenshot_path)
if html_report_path and os.path.exists(html_report_path):
# 在长时间操作前发送心跳
activity.heartbeat()
# 上传HTML报告到S3并获取访问URL
result.html_report_url = await upload_file_to_s3(html_report_path, f"reports/{req.test_case_id}.html")
os.remove(html_report_path) # 清理本地文件
# 清理本地临时文件
os.remove(html_report_path)
except Exception as e:
# 捕获UI测试执行过程中的异常
activity.logger.error(f"UI Test Failed for {req.test_case_id}: {e}")
result.base_result.success = False
result.base_result.message = f"UI Test Error: {e}"
result.base_result.error_details = str(e)
finally:
# 清理工作:取消心跳任务
heartbeat_task.cancel()
try:
# 等待心跳任务完全结束
await heartbeat_task
except asyncio.CancelledError:
pass
# 计算并记录测试执行时长
result.base_result.duration_seconds = time.time() - start_time
return result
@activity.defn(name="PythonAddRandomPrefixActivity")
async def python_add_random_prefix_activity(self,data: str) -> str:
prefixes = [
"alpha-", "beta-", "gamma-", "delta-", "epsilon-",
"zeta-", "eta-", "theta-", "iota-", "kappa-",
]
prefix = random.choice(prefixes)
return f"{prefix}{data}"

View File

@ -1,18 +1,23 @@
# 接口测试具体实现
import json
import requests
def execute_api_test_case(test_case_id: str, endpoint: str, http_method: str, headers: dict, request_body: bytes, expected_status_code: int):
def execute_api_test_case(test_case_id: str, endpoint: str, http_method: str, headers: dict, request_body: str,
expected_status_code: int):
"""
实际执行API测试的函数
可以集成 pytest, requests 等库
"""
base_url = "http://localhost:8080" # 假设 API 服务的基地址
base_url = "" # 假设 API 服务的基地址
full_url = f"{base_url}{endpoint}"
log_output = []
success = False
actual_status = 0
response_body = b""
response_headers = {}
log_output.append(f"Executing API test: {test_case_id} - {http_method} {full_url}")
@ -20,16 +25,23 @@ def execute_api_test_case(test_case_id: str, endpoint: str, http_method: str, he
if http_method.upper() == "GET":
response = requests.get(full_url, headers=headers, timeout=10)
elif http_method.upper() == "POST":
response = requests.post(full_url, headers=headers, data=request_body, timeout=10)
print(request_body)
if request_body == "":
json_data = "{}"
else:
# 如果 request_body 是字符串,尝试将其解析为 JSON
json_data = json.loads(request_body) if isinstance(request_body, str) else request_body
response = requests.post(full_url, headers=headers, data=json_data, timeout=10)
# ... 其他 HTTP 方法
else:
raise ValueError(f"Unsupported HTTP method: {http_method}")
actual_status = response.status_code
response_body = response.content
response_headers = response.headers
log_output.append(f"Response Status: {actual_status}")
log_output.append(f"Response Body: {response_body.decode('utf-8')[:500]}...") # 只显示前500字符
log_output.append(f"Response Body: {response_body.decode('utf-8')[:500]}...") # 只显示前500字符
if actual_status == expected_status_code:
success = True
@ -45,4 +57,4 @@ def execute_api_test_case(test_case_id: str, endpoint: str, http_method: str, he
log_output.append(f"API Test Failed (Unexpected Error): {e}")
success = False
return success, actual_status, response_body, "\n".join(log_output)
return success, actual_status, response_headers, response_body, "\n".join(log_output)

View File

@ -1 +0,0 @@
# Protobuf 生成的 Python 代码

View File

@ -14,16 +14,15 @@ from activities import TestActivities # 导入定义的 Activity
async def main():
# 连接 Temporal Server
client = await Client.connect("temporal.newai.day:17233", namespace="default") # 根据你的 Temporal Server 配置
print("Python Worker: Successfully connected to Temporal Server!")
activities = TestActivities()
task_queue = "python-task-queue"
# 创建 Worker
worker = Worker(
client,
task_queue=task_queue, # 保持与 Go Client 一致
activities=[activities.run_api_test,activities.python_add_random_prefix_activity]
task_queue="python-task-queue", # 保持与 Go Client 一致
activities=[activities.run_api_test,activities.run_ui_test]
)
print("Starting Python Temporal Worker...",task_queue)
print("Starting Python Temporal Worker...")
await worker.run()
if __name__ == "__main__":

View File

@ -0,0 +1,311 @@
package workflows
import (
"encoding/json"
"go.temporal.io/sdk/temporal"
"sort"
"strconv"
"time"
"beacon/activities" // 假设你的 activity 包在这个路径
"beacon/pkg/pb" // 你的 Protobuf 路径
"beacon/utils" // 导入参数处理器
"go.temporal.io/sdk/workflow"
// 假设你有数据库访问层Workflow 不能直接访问 DB需要通过 Activity 或预加载数据
// "your_module_path/go-server/dal" // 例如 Data Access Layer
)
// DynamicTestSuiteWorkflow 是通用的动态测试工作流
// 该工作流根据数据库中的配置动态执行不同类型的测试步骤,支持条件跳转和错误处理
// 输入参数:
// - input: 包含运行ID、复合案例ID和全局参数的动态测试运行输入
//
// 返回值:
// - TestRunOutput: 包含整体成功状态、各类测试结果的输出
// - error: 执行过程中的错误
func DynamicTestSuiteWorkflow(ctx workflow.Context, input *pb.DynamicTestRunInput) (*pb.TestRunOutput, error) {
// 获取工作流日志记录器,用于记录执行过程
logger := workflow.GetLogger(ctx)
logger.Info("DynamicTestSuiteWorkflow started", "runID", input.RunId, "compositeCaseID", input.CompositeCaseId)
// ========================================================================================
// 步骤1: 加载复合案例步骤定义
// ========================================================================================
// 注意: Temporal Workflow 不能直接访问数据库,必须通过 Activity 来获取数据
// 有两种设计方案:
// 方式一 (推荐): 在启动工作流时,由 Go Client 将完整的步骤数据作为输入参数传入
// 方式二 (适用于大数据): 工作流通过 Activity 从数据库动态加载步骤定义
// 这里采用方式二,通过 LoadCompositeCaseSteps Activity 从数据库加载步骤配置
ctxActivity := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
TaskQueue: "data-task-queue", // 指定任务队列名称
StartToCloseTimeout: 10 * time.Minute, // Activity 从开始到完成的最大允许时间
HeartbeatTimeout: 30 * time.Second, // Heartbeat 超时时间,防止 Worker 假死
RetryPolicy: &temporal.RetryPolicy{ // Activity 级别的重试策略配置
InitialInterval: time.Second, // 首次重试前的等待时间
BackoffCoefficient: 1.0, // 重试间隔的递增系数
MaximumInterval: time.Minute, // 重试间隔的最大值
MaximumAttempts: 3, // 最大重试次数
NonRetryableErrorTypes: []string{"NonRetryableErrorType"}, // 自定义不可重试的错误类型
},
}) // 应用 Activity 选项到上下文
var caseSteps []*pb.CompositeCaseStepDefinition // 存储从数据库加载的步骤定义列表
// 执行 LoadCompositeCaseSteps Activity 来获取复合案例的所有步骤定义
// 这个 Activity 会根据 CompositeCaseId 查询数据库并返回步骤配置
err := workflow.ExecuteActivity(ctxActivity, activities.LoadCompositeCaseSteps, input.CompositeCaseId).Get(ctx, &caseSteps)
if err != nil {
logger.Error("Failed to load composite case steps", "error", err)
return nil, err
}
// ========================================================================================
// 步骤2: 对步骤进行排序,确保按正确顺序执行
// ========================================================================================
// 按 step_order 字段升序排序,确保步骤按定义的顺序执行
// 这对于 DAG 结构的正确执行至关重要
sort.Slice(caseSteps, func(i, j int) bool {
return caseSteps[i].StepOrder < caseSteps[j].StepOrder
})
// ========================================================================================
// 步骤3: 初始化执行状态和结果收集器
// ========================================================================================
var (
overallSuccess = true // 整体执行成功标志,任何一个步骤失败都会置为 false
apiResults []*pb.ApiTestResult // 收集所有 API 测试结果
uiResults []*pb.UiTestResult // 收集所有 UI 测试结果
stepResults = make(map[string]bool) // 存储每个步骤的成功/失败状态,用于条件跳转判断
currentStepOrder = 0 // 当前执行步骤的索引,支持非线性跳转
)
// 初始化全局变量,包含输入参数中的全局参数
globalVariables := make(map[string]interface{})
if input.GlobalParameters != nil {
for key, value := range input.GlobalParameters {
globalVariables[key] = value
}
}
// 创建参数处理器
parameterProcessor := utils.NewParameterProcessor(globalVariables)
logger.Info("Initialized global variables", "variables", globalVariables)
// ========================================================================================
// 步骤4: 主执行循环 - 动态执行各个测试步骤
// ========================================================================================
// 使用 while 循环而非 for range因为需要支持条件跳转非线性执行
for currentStepOrder < len(caseSteps) {
step := caseSteps[currentStepOrder] // 获取当前要执行的步骤
logger.Info("Executing step", "stepOrder", step.StepOrder, "activityName", step.ActivityName)
// ------------------------------------------------------------------------------------
// 步骤4.1: 动态构造 Activity 输入参数(支持参数模板解析)
// ------------------------------------------------------------------------------------
// 因为不同的 Activity 需要不同类型的输入参数,这里使用工厂模式
// 根据 activity_name 决定使用哪个 Protobuf 结构来反序列化 JSON 参数
var activityInput interface{} // 存储特定 Activity 的输入参数(类型由 activity_name 决定)
var activityResult interface{} // 存储特定 Activity 的输出结果(类型由 activity_name 决定)
// 使用参数处理器解析参数模板,替换其中的变量引用
processedParametersJson, err := parameterProcessor.ProcessTemplate(step.ParametersJson)
if err != nil {
logger.Error("Failed to process parameter template", "error", err, "stepOrder", step.StepOrder)
overallSuccess = false
break
}
// 验证模板中的变量引用是否有效
if isValid, errors := parameterProcessor.ValidateTemplate(step.ParametersJson); !isValid {
logger.Error("Parameter template validation failed", "errors", errors, "stepOrder", step.StepOrder)
overallSuccess = false
break
}
// 根据不同的 Activity 类型,将 JSON 字符串参数反序列化为对应的 Protobuf 结构
// 这是动态工作流的核心:同一个工作流可以执行不同类型的测试
switch step.ActivityName {
case "RunApiTest":
apiReq := &pb.ApiTestRequest{}
// 1. 首先验证JSON格式
if !json.Valid([]byte(processedParametersJson)) {
logger.Error("Invalid JSON format in parameters")
overallSuccess = false
break
}
// 2. 解析JSON时增加错误详情
if err := json.Unmarshal([]byte(processedParametersJson), apiReq); err != nil {
logger.Error("Failed to unmarshal API test parameters",
"error", err,
"raw_json", processedParametersJson)
overallSuccess = false
break
}
// 3. 验证必要字段
if apiReq.Endpoint == "" || apiReq.HttpMethod == "" {
logger.Error("Missing required fields in API test parameters")
overallSuccess = false
break
}
activityInput = apiReq
activityResult = &pb.ApiTestResult{} // 预创建结果容器
case "RunUiTest":
// UI 测试:创建 UI 测试请求结构并解析参数
uiReq := &pb.UiTestRequest{}
if err := json.Unmarshal([]byte(processedParametersJson), uiReq); err != nil {
logger.Error("Failed to unmarshal UI test parameters", "error", err)
overallSuccess = false
break // 参数解析失败,跳出当前步骤
}
activityInput = uiReq
activityResult = &pb.UiTestResult{} // 预创建结果容器
case "PrepareEnvironment":
// 环境准备:可以扩展更多测试类型
// TODO: 实现环境准备的参数解析
/*
prepReq := &pb.PrepareEnvRequest{}
if err := json.Unmarshal([]byte(processedParametersJson), prepReq); err != nil {
logger.Error("Failed to unmarshal prepare env parameters", "error", err)
overallSuccess = false
break
}
activityInput = prepReq
activityResult = &pb.PrepareEnvResult{}
*/
break
default:
// 未知的 Activity 类型,记录错误并标记失败
logger.Error("Unknown activity name", "activityName", step.ActivityName)
overallSuccess = false
break
}
// 如果参数构造失败,跳过当前步骤
if activityInput == nil {
overallSuccess = false
logger.Error("Activity input could not be constructed for step", "stepOrder", step.StepOrder)
break
}
// ------------------------------------------------------------------------------------
// 步骤4.2: 配置 Activity 执行选项
// ------------------------------------------------------------------------------------
// 为 Activity 设置超时、重试等策略,确保测试的可靠性
ao := workflow.ActivityOptions{
TaskQueue: "python-task-queue", // 指定任务队列名称
StartToCloseTimeout: 10 * time.Minute, // 单个测试最长执行时间
HeartbeatTimeout: 30 * time.Second, // 心跳超时,用于检测 Activity 是否还在运行
RetryPolicy: &temporal.RetryPolicy{ // 重试策略配置
InitialInterval: time.Second, // 首次重试间隔
MaximumAttempts: 3, // 最大重试次数
},
}
stepCtx := workflow.WithActivityOptions(ctx, ao) // 应用 Activity 选项到上下文
// ------------------------------------------------------------------------------------
// 步骤4.3: 动态执行 Activity
// ------------------------------------------------------------------------------------
// 使用反射机制动态调用指定名称的 Activity 函数
// Temporal SDK 会根据 activity_name 找到对应的注册函数并执行
err = workflow.ExecuteActivity(stepCtx, step.ActivityName, activityInput).Get(stepCtx, activityResult)
// ------------------------------------------------------------------------------------
// 步骤4.4: 处理 Activity 执行结果
// ------------------------------------------------------------------------------------
stepPassed := true // 当前步骤成功标志
if err != nil {
// Activity 执行过程中发生错误(如超时、异常等)
logger.Error("Activity execution failed", "activityName", step.ActivityName, "error", err)
stepPassed = false
overallSuccess = false // 标记整个工作流失败
} else {
// Activity 执行成功,需要检查业务逻辑是否成功
// 通过类型断言获取具体的结果并检查 BaseResult.Success 字段
switch res := activityResult.(type) {
case *pb.ApiTestResult:
apiResults = append(apiResults, res) // 收集 API 测试结果
if !res.BaseResult.Success { // 检查业务逻辑是否成功
stepPassed = false
overallSuccess = false
}
// 存储activity结果用于参数传递
activityResult := &utils.ActivityResult{
StepID: step.StepId,
StepName: step.ActivityName,
Success: res.BaseResult.Success,
Data: res,
}
parameterProcessor.AddActivityResult(step.StepId, activityResult)
case *pb.UiTestResult:
uiResults = append(uiResults, res) // 收集 UI 测试结果
if !res.BaseResult.Success { // 检查业务逻辑是否成功
stepPassed = false
overallSuccess = false
}
// 存储activity结果用于参数传递
activityResult := &utils.ActivityResult{
StepID: step.StepId,
StepName: step.ActivityName,
Success: res.BaseResult.Success,
Data: res,
}
parameterProcessor.AddActivityResult(step.StepId, activityResult)
// 可以在这里添加更多结果类型的处理
}
logger.Info("Activity execution finished", "activityName", step.ActivityName, "success", stepPassed)
}
// 记录当前步骤的执行结果,用于后续条件跳转判断
stepResults[strconv.FormatInt(step.StepId, 10)] = stepPassed
// ------------------------------------------------------------------------------------
// 步骤4.5: 根据执行结果确定下一步骤(实现 DAG 的条件跳转)
// ------------------------------------------------------------------------------------
// 默认情况下,顺序执行下一个步骤
nextStep := currentStepOrder + 1
// 根据当前步骤的成功/失败状态和预定义的跳转规则确定下一步
if stepPassed && step.SuccessNextStepOrder != nil {
// 如果当前步骤成功且定义了成功跳转目标,则跳转到指定步骤
nextStep = int(*step.SuccessNextStepOrder) - 1 // 转换为 0 索引(数据库中可能是 1 索引)
} else if !stepPassed && step.FailureNextStepOrder != nil {
// 如果当前步骤失败且定义了失败跳转目标,则跳转到指定步骤
nextStep = int(*step.FailureNextStepOrder) - 1 // 转换为 0 索引
}
// 验证跳转目标的有效性,防止数组越界
if nextStep < 0 || nextStep >= len(caseSteps) {
logger.Info("Next step out of range, terminating workflow", "nextStep", nextStep, "totalSteps", len(caseSteps))
break // 跳转目标无效,退出执行循环
}
// 更新当前步骤索引,继续下一轮循环
currentStepOrder = nextStep
}
// ========================================================================================
// 步骤5: 构造并返回最终执行结果
// ========================================================================================
logger.Info("DynamicTestSuiteWorkflow completed",
"runID", input.RunId,
"overallSuccess", overallSuccess,
"apiResultsCount", len(apiResults),
"uiResultsCount", len(uiResults))
// 返回包含所有测试结果和执行状态的输出结构
return &pb.TestRunOutput{
RunId: input.RunId, // 运行标识符
OverallSuccess: overallSuccess, // 整体成功状态
ApiResults: apiResults, // 所有 API 测试结果
UiResults: uiResults, // 所有 UI 测试结果
CompletionMessage: "Dynamic test suite finished.", // 完成消息
}, nil
}

127
workflows/workflow.go Normal file
View File

@ -0,0 +1,127 @@
package workflows
// workflows 包定义了 Temporal 工作流的实现
import (
"beacon/pkg/pb"
"fmt"
"go.temporal.io/sdk/temporal"
"go.temporal.io/sdk/workflow"
"time"
)
// TestRunWorkflow 定义了整个测试执行的工作流
// 该工作流负责协调和执行 API 测试和 UI 测试的整个流程
// 参数:
// - ctx: Temporal 工作流上下文,用于控制工作流的执行
// - input: 测试运行的输入参数,包含运行配置和标识
//
// 返回值:
// - *pb.TestRunOutput: 测试运行的结果输出
// - error: 执行过程中的错误信息
func TestRunWorkflow(ctx workflow.Context, input *pb.TestRunInput) (*pb.TestRunOutput, error) {
// 获取工作流日志记录器,用于记录执行过程中的关键信息
logger := workflow.GetLogger(ctx)
logger.Info("TestRunWorkflow started", "runID", input.RunId)
// 配置 Activity 的执行选项,包括超时时间和重试策略
ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: 10 * time.Minute, // Activity 从开始到完成的最大允许时间
HeartbeatTimeout: 30 * time.Second, // Heartbeat 超时时间,防止 Worker 假死
RetryPolicy: &temporal.RetryPolicy{ // Activity 级别的重试策略配置
InitialInterval: time.Second, // 首次重试前的等待时间
BackoffCoefficient: 2.0, // 重试间隔的递增系数
MaximumInterval: time.Minute, // 重试间隔的最大值
MaximumAttempts: 3, // 最大重试次数
NonRetryableErrorTypes: []string{"NonRetryableErrorType"}, // 自定义不可重试的错误类型
},
})
// 初始化变量用于存储测试结果和状态
var (
apiResults []*pb.ApiTestResult // 存储所有 API 测试的结果
uiResults []*pb.UiTestResult // 存储所有 UI 测试的结果
overallSuccess = true // 整体测试是否成功的标志
completionMessage = "Test run completed successfully." // 完成时的状态消息
)
// 条件执行 API 测试 Activity
// 只有当输入配置中指定需要运行 API 测试时才执行
if input.RunApiTests {
// 构造 API 测试的请求参数
// 这里使用硬编码的示例数据,实际应用中应该从配置或输入中获取
apiTestInput := &pb.ApiTestRequest{
TestCaseId: "api-example-1", // 测试用例唯一标识
Endpoint: "/api/v1/data", // 要测试的 API 端点
HttpMethod: "GET", // HTTP 请求方法
Headers: map[string]string{"Authorization": "Bearer token123"}, // 请求头信息
ExpectedStatusCode: 200, // 预期的 HTTP 状态码
}
// 声明变量用于接收 API 测试的结果
var apiRes pb.ApiTestResult
// 执行 API 测试 Activity 并等待结果
// "run_api_test" 是注册的 Activity 名称
err := workflow.ExecuteActivity(ctx, "run_api_test", apiTestInput).Get(ctx, &apiRes)
if err != nil {
// API 测试执行失败时的错误处理
logger.Error("API test activity failed", "error", err)
// 标记整体测试为失败,但继续执行后续测试
overallSuccess = false
// 设置测试结果的失败状态和错误信息
apiRes.BaseResult.Success = false
apiRes.BaseResult.Message = fmt.Sprintf("API Test Failed: %v", err)
}
// 将 API 测试结果添加到结果集合中
apiResults = append(apiResults, &apiRes)
}
// 条件执行 UI 测试 Activity
// 只有当输入配置中指定需要运行 UI 测试时才执行
if input.RunUiTests {
// 构造 UI 测试的请求参数
// 包含浏览器配置和测试页面信息
uiTestInput := &pb.UiTestRequest{
TestCaseId: "ui-example-1", // UI 测试用例标识
UrlPath: "/dashboard", // 要测试的页面路径
BrowserType: "chromium", // 使用的浏览器类型
Headless: true, // 是否使用无头模式运行浏览器
UserData: map[string]string{"user": "test", "pass": "password"}, // 测试用的用户数据
}
// 声明变量用于接收 UI 测试的结果
var uiRes pb.UiTestResult
// 执行 UI 测试 Activity 并等待结果
// "run_ui_test" 是注册的 Activity 名称
err := workflow.ExecuteActivity(ctx, "run_ui_test", uiTestInput).Get(ctx, &uiRes)
if err != nil {
// UI 测试执行失败时的错误处理
logger.Error("UI test activity failed", "error", err)
// 标记整体测试为失败
overallSuccess = false
// 设置测试结果的失败状态和错误信息
uiRes.BaseResult.Success = false
uiRes.BaseResult.Message = fmt.Sprintf("UI Test Failed: %v", err)
}
// 将 UI 测试结果添加到结果集合中
uiResults = append(uiResults, &uiRes)
}
// 根据整体测试结果更新完成消息
if !overallSuccess {
completionMessage = "Test run completed with failures."
}
// 记录工作流完成的日志信息
logger.Info("TestRunWorkflow completed", "overallSuccess", overallSuccess)
// 构造并返回测试运行的完整结果
return &pb.TestRunOutput{
RunId: input.RunId, // 测试运行的唯一标识
OverallSuccess: overallSuccess, // 整体测试是否成功
CompletionMessage: completionMessage, // 完成状态消息
ApiResults: apiResults, // 所有 API 测试结果
UiResults: uiResults, // 所有 UI 测试结果
}, nil
}