From 89cc7761b516b8a6cdedefca70af0601ba759e59 Mon Sep 17 00:00:00 2001 From: Yvan <8574526@qq,com> Date: Tue, 29 Sep 2026 18:15:35 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=9F=AD=E4=BF=A1=E6=B8=A0?= =?UTF-8?q?=E9=81=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/wire_gen.go | 26 +- configs/config.yaml | 10 +- docs/SMS_INTEGRATION.md | 42 ++ go.mod | 17 +- go.sum | 149 ++++++ .../biz/integration/integration_config.go | 3 + .../integration_config_definition.go | 16 + internal/biz/integration/provider.go | 2 +- internal/biz/integration/sms.go | 86 ++++ internal/data/database.go | 24 +- internal/data/database_test.go | 35 ++ internal/data/integration/runtime.go | 2 +- internal/data/integration/sms_test.go | 62 +++ internal/data/migrations_test.go | 4 +- internal/integration/provider.go | 2 + internal/integration/sms/sms.go | 172 +++++++ internal/integration/sms/sms_test.go | 117 +++++ internal/modules/catalog_test.go | 8 +- internal/modules/integration/definition.go | 19 +- .../modules/integration/definition_test.go | 2 +- internal/routecatalog/catalog.go | 2 + internal/server/gin_test.go | 2 +- internal/server/handler/provider.go | 1 + internal/server/handler/public.go | 2 + internal/server/handler/set.go | 1 + internal/server/handler/sms.go | 27 + internal/server/middleware/access_log.go | 4 +- internal/server/middleware/audit.go | 3 + internal/server/middleware/redact.go | 1 + internal/server/middleware/sms_audit_test.go | 46 ++ internal/server/router/routes.go | 1 + internal/service/dto/sms.go | 13 + internal/service/integration/provider.go | 2 +- internal/service/integration/sms.go | 69 +++ internal/service/integration/sms_test.go | 44 ++ web/src/api/integration.js | 6 + web/src/pathInfo.json | 1 + web/src/view/systemTools/sms/index.vue | 485 ++++++++++++++++++ 38 files changed, 1469 insertions(+), 39 deletions(-) create mode 100644 docs/SMS_INTEGRATION.md create mode 100644 internal/biz/integration/sms.go create mode 100644 internal/data/integration/sms_test.go create mode 100644 internal/integration/sms/sms.go create mode 100644 internal/integration/sms/sms_test.go create mode 100644 internal/server/handler/sms.go create mode 100644 internal/server/middleware/sms_audit_test.go create mode 100644 internal/service/dto/sms.go create mode 100644 internal/service/integration/sms.go create mode 100644 internal/service/integration/sms_test.go create mode 100644 web/src/view/systemTools/sms/index.vue diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index 4de9642..806139e 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -8,22 +8,23 @@ package main import ( "github.com/go-kratos/kratos/v3" - integration3 "kra/internal/biz/integration" + "kra/internal/biz/integration" payment3 "kra/internal/biz/payment" system2 "kra/internal/biz/system" task2 "kra/internal/biz/task" "kra/internal/config" "kra/internal/data" - "kra/internal/data/integration" + integration3 "kra/internal/data/integration" payment2 "kra/internal/data/payment" "kra/internal/data/system" "kra/internal/data/task" "kra/internal/initialize" - integration2 "kra/internal/integration" + integration4 "kra/internal/integration" "kra/internal/integration/cache" "kra/internal/integration/email" "kra/internal/integration/mq" "kra/internal/integration/payment" + "kra/internal/integration/sms" "kra/internal/integration/storage" "kra/internal/integration/systeminfo" "kra/internal/integration/websocket" @@ -32,7 +33,7 @@ import ( "kra/internal/server" "kra/internal/server/handler" "kra/internal/server/router" - integration4 "kra/internal/service/integration" + integration2 "kra/internal/service/integration" payment4 "kra/internal/service/payment" system3 "kra/internal/service/system" task3 "kra/internal/service/task" @@ -77,6 +78,11 @@ func wireApp(configServer *config.Server, store *config.Store, logger *slog.Logg auditRecorderUsecase := system2.NewAuditRecorderUsecase(auditRecordRepo) auditRecorder := system3.NewAuditRecorder(auditRecorderUsecase) registry := taskRegistry() + runtimeconfigStore := data.NewIntegrationRuntime(dataData) + smsSender := sms.NewSender(runtimeconfigStore) + smsUsecase := integration.NewSMSUsecase(smsSender) + smsService := integration2.NewSMSService(smsUsecase) + handlerSMS := handler.NewSMS(smsService) authorityUsecase := system2.NewAuthorityUsecase(authorityAccessRepo) authorityService := system3.NewAuthorityService(authorityUsecase) authority := handler.NewAuthority(authorityService) @@ -106,7 +112,7 @@ func wireApp(configServer *config.Server, store *config.Store, logger *slog.Logg emailUsecase := system2.NewEmailUsecase(emailRepo) emailService := system3.NewEmailService(emailUsecase) handlerEmail := handler.NewEmail(emailService) - paymentConfigReader := integration.NewPaymentConfigReader(dataData) + paymentConfigReader := integration3.NewPaymentConfigReader(dataData) factory := payment.NewFactory() paymentOrderRepo := payment2.NewPaymentOrderRepo(dataData) paymentRepo := payment2.NewPaymentRepo(dataData, paymentConfigReader, factory, paymentOrderRepo) @@ -164,13 +170,13 @@ func wireApp(configServer *config.Server, store *config.Store, logger *slog.Logg user := handler.NewUser(userService, authService) navigation := handler.NewNavigation(userService) session := handler.NewSession(tokenService) - integrationConfigRepo := integration.NewIntegrationConfigRepo(dataData) - runtimeconfigStore := data.NewIntegrationRuntime(dataData) - connectivityTester := integration2.NewConnectivityTester(runtimeconfigStore) - integrationConfigUsecase := integration3.NewIntegrationConfigUsecase(integrationConfigRepo, connectivityTester) - integrationConfigService := integration4.NewIntegrationConfigService(integrationConfigUsecase) + integrationConfigRepo := integration3.NewIntegrationConfigRepo(dataData) + connectivityTester := integration4.NewConnectivityTester(runtimeconfigStore) + integrationConfigUsecase := integration.NewIntegrationConfigUsecase(integrationConfigRepo, connectivityTester) + integrationConfigService := integration2.NewIntegrationConfigService(integrationConfigUsecase) integrationConfig := handler.NewIntegrationConfig(integrationConfigService) set := &handler.Set{ + SMS: handlerSMS, Authority: authority, Menu: menu, API: api, diff --git a/configs/config.yaml b/configs/config.yaml index c506ae8..45dbb47 100644 --- a/configs/config.yaml +++ b/configs/config.yaml @@ -6,13 +6,13 @@ server: data: database: driver: mysql - source: "" + source: root:Xu950329.@tcp(127.0.0.1:3306)/kra_new?charset=utf8mb4&parseTime=True&loc=Local host: 127.0.0.1 port: "3306" user: root - password: "" - name: "" - config: timeout=5s&readTimeout=5s&writeTimeout=5s&charset=utf8mb4&parseTime=True&loc=Local + password: Xu950329. + name: kra_new + config: charset=utf8mb4&parseTime=True&loc=Local path: "" alias_name: "" disable: false @@ -88,7 +88,7 @@ admin: router_prefix: "" jwt: # Generated during first-install, or provide KRA_ADMIN_JWT_SIGNING_KEY locally. - signing_key: "" + signing_key: 757fc924-629e-457d-a1df-39f9970e48ee expires_time: 604800s buffer_time: 86400s issuer: kra diff --git a/docs/SMS_INTEGRATION.md b/docs/SMS_INTEGRATION.md new file mode 100644 index 0000000..0196e3c --- /dev/null +++ b/docs/SMS_INTEGRATION.md @@ -0,0 +1,42 @@ +# 短信集成 + +管理入口:**扩展功能 → 短信服务**。支持阿里云和腾讯云官方 Go SDK。 +配置保存在现有集成配置表中,保存后立即生效,服务重启后自动恢复。 +新安装自动创建菜单;已有安装启动时通过 `202609290001_sms_admin_surface` 迁移补上菜单和超级管理员发送权限。 +其他角色需要分配短信菜单、集成配置读写权限及短信发送权限。 + +## 配置 + +- 阿里云:AccessKey ID、AccessKey Secret、审核通过的短信签名、模板 CODE。当前接入中国大陆 `SendSms` 接口。 +- 腾讯云:Secret ID、Secret Key、短信应用 SDK App ID、地域(默认 `ap-guangzhou`)、签名、模板 ID。 +- 保存并启用后才能发送。保留密钥的 `******` 占位符表示沿用原值;不在配置文件中存储凭证。 +- 每个服务商当前配置一个签名及模板。切换模板需修改配置。 + +## 发送 + +`POST /integration/sms/:provider/send`,`:provider` 为 `aliyun` 或 `tencent`。 +接口位于现有鉴权和权限控制中,遵循 `{code,data,msg}` 响应格式。每次只发送给一个号码。 + +阿里云参数示例: + +```json +{"phone":"+8613800138000","params":{"code":"123456"}} +``` + +腾讯云参数示例: + +```json +{"phone":"+8613800138000","params":["123456","5"]} +``` + +参数值必须为字符串;阿里云使用命名对象,腾讯云按模板顺序填写数组。 +无参数模板分别传 `{}` 或 `[]`。手机号使用包含 `+` 和国家区号的格式。 + +成功返回 `data.requestId` 和 `data.messageId`,表示服务商已受理,不代表手机已收到。 +失败返回服务商错误码(例如余额不足、签名未审核、频率限制),不回传原始 SDK 请求或凭证。 +网络失败不能保证短信未发送,核对服务商记录后再决定是否重试。 + +测试按钮会实际发送并可能计费。应用和 SDK 均不自动重试发送请求;腾讯云调用受请求上下文控制,阿里云 SDK 的连接和读取各限制为最多 10 秒。 +发送日志隐藏手机号及模板参数,操作审计保留操作者和接口信息。真实发送需要自行配置云账户凭证、审核通过的签名及模板。 + +此集成提供模板短信发送能力,不包含验证码生成、校验或登录流程。 diff --git a/go.mod b/go.mod index c5b7060..00d8767 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,10 @@ module kra go 1.25.7 require ( + github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.11 + github.com/alibabacloud-go/dysmsapi-20170525/v4 v4.1.3 + github.com/alibabacloud-go/tea v1.2.2 + github.com/alibabacloud-go/tea-utils/v2 v2.0.6 github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible github.com/aws/aws-sdk-go-v2 v1.41.2 github.com/aws/aws-sdk-go-v2/config v1.32.10 @@ -19,6 +23,7 @@ require ( github.com/go-kratos/kratos/v3 v3.0.0 github.com/go-pay/crypto v0.0.1 github.com/go-pay/gopay v1.5.122 + github.com/go-sql-driver/mysql v1.10.0 github.com/go-viper/mapstructure/v2 v2.4.0 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/google/uuid v1.6.0 @@ -38,6 +43,8 @@ require ( github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.0 github.com/swaggo/swag v1.16.4 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.172 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.3.172 github.com/tencentyun/cos-go-sdk-v5 v0.7.60 github.com/xuri/excelize/v2 v2.9.0 go.mongodb.org/mongo-driver v1.17.2 @@ -62,6 +69,12 @@ require ( github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect + github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect + github.com/alibabacloud-go/debug v1.0.1 // indirect + github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect + github.com/alibabacloud-go/openapi-util v0.1.1 // indirect + github.com/alibabacloud-go/tea-xml v1.1.3 // indirect + github.com/aliyun/credentials-go v1.4.5 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect @@ -83,6 +96,7 @@ require ( github.com/casbin/govaluate v1.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/clbanning/mxj v1.8.4 // indirect + github.com/clbanning/mxj/v2 v2.5.5 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect @@ -110,7 +124,6 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.20.0 // indirect - github.com/go-sql-driver/mysql v1.10.0 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/gofrs/flock v0.8.1 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect @@ -159,6 +172,7 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect github.com/thoas/go-funk v0.7.0 // indirect github.com/tinylib/msgp v1.3.0 // indirect + github.com/tjfoc/gmsm v1.4.1 // indirect github.com/tklauser/go-sysconf v0.3.15 // indirect github.com/tklauser/numcpus v0.10.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect @@ -187,6 +201,7 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20260615183401-62b3387ff324 // indirect google.golang.org/grpc v1.81.1 // indirect google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect modernc.org/fileutil v1.0.0 // indirect modernc.org/libc v1.22.5 // indirect diff --git a/go.sum b/go.sum index 5256b8c..0f2b6ed 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= @@ -18,6 +19,7 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= @@ -29,8 +31,55 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdko github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM= github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 h1:7dONQ3WNZ1zy960TmkxJPuwoolZwL7xKtpcM04MBnt4= github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82/go.mod h1:nLnM0KdK1CmygvjpDUO6m1TjSsiQtL61juhNsvV/JVI= +github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA= +github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo= +github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc= +github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8= +github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g= +github.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY= +github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI= +github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE= +github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8= +github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc= +github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc= +github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.11 h1:GkVQ9AphMCmgAYakcTpH/OuFz0mQUypO/JiOvo0wgVA= +github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.11/go.mod h1:wHxkgZT1ClZdcwEVP/pDgYK/9HucsnCfMipmJgCz4xY= +github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg= +github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ= +github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo= +github.com/alibabacloud-go/darabonba-string v1.0.2/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA= +github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY= +github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc= +github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg= +github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc= +github.com/alibabacloud-go/dysmsapi-20170525/v4 v4.1.3 h1:32N2pGk28weVZ5/rjNk9gPx/jrRkR0rX9i8Id6IlyUY= +github.com/alibabacloud-go/dysmsapi-20170525/v4 v4.1.3/go.mod h1:gPbHx4BTxLIDNRfYNGGmp6aIpeNBamtdDlPcK4UTUto= +github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q= +github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE= +github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws= +github.com/alibabacloud-go/openapi-util v0.1.1 h1:ujGErJjG8ncRW6XtBBMphzHTvCxn4DjrVw4m04HsS28= +github.com/alibabacloud-go/openapi-util v0.1.1/go.mod h1:/UehBSE2cf1gYT43GV4E+RxTdLRzURImCYY0aRmlXpw= +github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg= +github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= +github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= +github.com/alibabacloud-go/tea v1.2.2 h1:aTsR6Rl3ANWPfqeQugPglfurloyBJY85eFy7Gc1+8oU= +github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk= +github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE= +github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4= +github.com/alibabacloud-go/tea-utils/v2 v2.0.6 h1:ZkmUlhlQbaDC+Eba/GARMPy6hKdCLiSke5RsN5LcyQ0= +github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I= +github.com/alibabacloud-go/tea-xml v1.1.3 h1:7LYnm+JbOq2B+T/B0fHC4Ies4/FofC4zHzYtqw7dgt0= +github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8= github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g= github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= +github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw= +github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0= +github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM= +github.com/aliyun/credentials-go v1.4.5 h1:O76WYKgdy1oQYYiJkERjlA2dxGuvLRrzuO2ScrtGWSk= +github.com/aliyun/credentials-go v1.4.5/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U= github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls= github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q= @@ -85,14 +134,19 @@ github.com/casbin/casbin/v3 v3.10.0 h1:039ORla55vCeIZWd0LfzWFt1yiEA5X4W41xBW2bQu github.com/casbin/casbin/v3 v3.10.0/go.mod h1:5rJbQr2e6AuuDDNxnPc5lQlC9nIgg6nS1zYwKXhpHC8= github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc= github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I= github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= +github.com/clbanning/mxj/v2 v2.5.5 h1:oT81vUeEiQQ/DcHbzSytRngP6Ky9O+L+0Bw0zSJag9E= +github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= 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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/dave/jennifer v1.6.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -112,6 +166,9 @@ github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2I github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU= github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 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.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= @@ -202,10 +259,25 @@ github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -221,6 +293,8 @@ 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/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4= github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= @@ -254,8 +328,10 @@ 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.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 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/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= @@ -300,6 +376,8 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR 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 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 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/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= @@ -331,6 +409,7 @@ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk= github.com/qiniu/go-sdk/v7 v7.25.2 h1:URwgZpxySdiwu2yQpHk93X4LXWHyFRp1x3Vmlk/YWvo= github.com/qiniu/go-sdk/v7 v7.25.2/go.mod h1:dmKtJ2ahhPWFVi9o1D5GemmWoh/ctuB9peqTowyTO8o= @@ -365,6 +444,9 @@ github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1 github.com/shirou/gopsutil/v4 v4.25.7/go.mod h1:XV/egmwJtd3ZQjBpJVY5kndsiOO4IRqy9TQnmm6VP7U= github.com/sijms/go-ora/v2 v2.7.17 h1:M/pYIqjaMUeBxyzOWp2oj4ntF6fHSBloJWGNH9vbmsU= github.com/sijms/go-ora/v2 v2.7.17/go.mod h1:EHxlY6x7y9HAsdfumurRfTd+v8NrEOTR3Xl4FWlH6xk= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= @@ -376,11 +458,13 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 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/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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= @@ -399,13 +483,20 @@ github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRd github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.172 h1:q2hBMhsSSV1dIawliBqDDIU9L1G+tWpu3hdmJWvYG44= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.172/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.563/go.mod h1:uom4Nvi9W+Qkom0exYiJ9VWJjXwyxtPYTkKkaLMlfE0= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.3.172 h1:8YBaibwP8YXcYP+4qdgoqIwNULYNt5/AyVP7t3J3hCA= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.3.172/go.mod h1:eLllmEVIjbeQpcXHpd/68caL4/EgHBbHruRCmExcv+M= github.com/tencentyun/cos-go-sdk-v5 v0.7.60 h1:/e/tmvRmfKexr/QQIBzWhOkZWsmY3EK72NrI6G/Tv0o= github.com/tencentyun/cos-go-sdk-v5 v0.7.60/go.mod h1:8+hG+mQMuRP/OIS9d83syAvXvrMj9HhkND6Q1fLghw0= github.com/thoas/go-funk v0.7.0 h1:GmirKrs6j6zJbhJIficOsz2aAI7700KsU/5YrdHRM1Y= github.com/thoas/go-funk v0.7.0/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= +github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= +github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= @@ -428,6 +519,8 @@ github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNh github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= @@ -461,12 +554,17 @@ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUu golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 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-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= @@ -475,8 +573,13 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= +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/mod v0.2.0/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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -485,9 +588,16 @@ golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +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= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/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-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/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-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -498,15 +608,22 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +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= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= @@ -516,9 +633,13 @@ golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +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= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -533,6 +654,7 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -550,6 +672,7 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= @@ -578,7 +701,13 @@ golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= 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= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= @@ -586,15 +715,30 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= 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= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +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/googleapis/api v0.0.0-20260615183401-62b3387ff324 h1:g0RAkxK/smSu/iRwC/KIX1mwUoVJtk2OjbgaeS4DmUM= google.golang.org/genproto/googleapis/api v0.0.0-20260615183401-62b3387ff324/go.mod h1:Z4WJ5pJOYWFWcHEQUelD5QaZDknIQkpIL/+fyJOT9+A= google.golang.org/genproto/googleapis/rpc v0.0.0-20260615183401-62b3387ff324 h1:9HZDLIdYBJXAnaFOr9WHrKVycfpY+75s9HGadC0305A= google.golang.org/genproto/googleapis/rpc v0.0.0-20260615183401-62b3387ff324/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +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.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -603,6 +747,9 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8 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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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= @@ -623,6 +770,8 @@ gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= +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= modernc.org/fileutil v1.0.0 h1:Z1AFLZwl6BO8A5NldQg/xTSjGLetp+1Ubvl4alfGx8w= modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8= modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= diff --git a/internal/biz/integration/integration_config.go b/internal/biz/integration/integration_config.go index c1a4acd..2e87006 100644 --- a/internal/biz/integration/integration_config.go +++ b/internal/biz/integration/integration_config.go @@ -14,6 +14,7 @@ import ( ) const ( + IntegrationKindSMS = "sms" IntegrationKindPayment = "payment" IntegrationKindMQ = "mq" IntegrationKindWebSocket = "websocket" @@ -255,6 +256,8 @@ func ValidateIntegrationConfig(kind, provider string, values map[string]any) err kind = normalizeIntegrationPart(kind) provider = normalizeIntegrationPart(provider) switch kind { + case IntegrationKindSMS: + return validateSMSConfig(provider, values) case IntegrationKindPayment: return validatePaymentIntegrationConfig(provider, values) case IntegrationKindMQ, IntegrationKindWebSocket: diff --git a/internal/biz/integration/integration_config_definition.go b/internal/biz/integration/integration_config_definition.go index 3414d1f..eb95cc8 100644 --- a/internal/biz/integration/integration_config_definition.go +++ b/internal/biz/integration/integration_config_definition.go @@ -120,6 +120,22 @@ func genericPaymentDefinition(provider, name, description string) IntegrationCon } var integrationDefinitions = map[string][]IntegrationConfigDefinition{ + IntegrationKindSMS: { + {Kind: IntegrationKindSMS, Provider: "aliyun", Name: "阿里云短信", Description: "中国大陆短信,使用已审核的签名和模板。", Defaults: map[string]any{"access_key_id": "", "access_key_secret": "", "sign_name": "", "template_id": ""}, Fields: []IntegrationConfigField{ + integrationField("access_key_id", "AccessKey ID", true, false, "text"), + integrationField("access_key_secret", "AccessKey Secret", true, true, "password"), + integrationField("sign_name", "短信签名", true, false, "text"), + integrationField("template_id", "模板 CODE", true, false, "text"), + }}, + {Kind: IntegrationKindSMS, Provider: "tencent", Name: "腾讯云短信", Description: "使用短信应用、已审核的签名和模板,模板参数按顺序填写。", Defaults: map[string]any{"secret_id": "", "secret_key": "", "sdk_app_id": "", "region": "ap-guangzhou", "sign_name": "", "template_id": ""}, Fields: []IntegrationConfigField{ + integrationField("secret_id", "Secret ID", true, true, "password"), + integrationField("secret_key", "Secret Key", true, true, "password"), + integrationField("sdk_app_id", "短信应用 SDK App ID", true, false, "text"), + integrationField("region", "地域", true, false, "text"), + integrationField("sign_name", "短信签名", true, false, "text"), + integrationField("template_id", "模板 ID", true, false, "text"), + }}, + }, IntegrationKindMQ: { { Kind: IntegrationKindMQ, Provider: "emqx", Name: "EMQX", Description: "EMQX MQTT 消息服务", diff --git a/internal/biz/integration/provider.go b/internal/biz/integration/provider.go index edf53bc..18cd429 100644 --- a/internal/biz/integration/provider.go +++ b/internal/biz/integration/provider.go @@ -3,4 +3,4 @@ package integration import "github.com/google/wire" // ProviderSet wires communication and integration-configuration usecases. -var ProviderSet = wire.NewSet(NewIntegrationConfigUsecase) +var ProviderSet = wire.NewSet(NewIntegrationConfigUsecase, NewSMSUsecase) diff --git a/internal/biz/integration/sms.go b/internal/biz/integration/sms.go new file mode 100644 index 0000000..e19b37c --- /dev/null +++ b/internal/biz/integration/sms.go @@ -0,0 +1,86 @@ +package integration + +import ( + "context" + "regexp" + "strings" + + "github.com/go-kratos/kratos/v3/errors" +) + +var ( + ErrSMSInvalid = errors.BadRequest("SMS_INVALID", "短信参数无效") + ErrSMSDisabled = errors.BadRequest("SMS_DISABLED", "短信渠道未配置或未启用") + ErrSMSFailed = errors.BadRequest("SMS_SEND_FAILED", "短信提交失败,请检查服务商配置和账户状态") +) + +// SMSMessage carries one recipient and provider-specific template parameters. +type SMSMessage struct { + Provider string + Phone string + NamedParams map[string]string + Params []string +} + +type SMSResult struct { + RequestID string + MessageID string +} + +type SMSSender interface { + Send(context.Context, *SMSMessage) (*SMSResult, error) +} + +type SMSUsecase struct{ sender SMSSender } + +func NewSMSUsecase(sender SMSSender) *SMSUsecase { return &SMSUsecase{sender: sender} } + +var smsPhonePattern = regexp.MustCompile(`^\+[1-9][0-9]{6,14}$`) + +func ValidateSMSMessage(m *SMSMessage) error { + if m == nil || (m.Provider != "aliyun" && m.Provider != "tencent") || !smsPhonePattern.MatchString(m.Phone) { + return ErrSMSInvalid + } + // The Aliyun adapter uses the mainland China SendSms API. + if m.Provider == "aliyun" && (!strings.HasPrefix(m.Phone, "+861") || len(m.Phone) != 14 || len(m.Params) != 0) { + return errors.BadRequest("SMS_INVALID", "阿里云短信请使用 +86 开头的中国大陆手机号及对象格式模板参数") + } + if m.Provider == "tencent" && len(m.NamedParams) != 0 { + return errors.BadRequest("SMS_INVALID", "腾讯云短信模板参数必须是按模板顺序排列的字符串数组") + } + if len(m.NamedParams) > 20 || len(m.Params) > 20 { + return ErrSMSInvalid + } + for key, value := range m.NamedParams { + if strings.TrimSpace(key) == "" || len(key) > 64 || len(value) > 500 { + return ErrSMSInvalid + } + } + for _, value := range m.Params { + if len(value) > 500 { + return ErrSMSInvalid + } + } + return nil +} + +func (uc *SMSUsecase) Send(ctx context.Context, m *SMSMessage) (*SMSResult, error) { + if err := ValidateSMSMessage(m); err != nil { + return nil, err + } + return uc.sender.Send(ctx, m) +} + +func validateSMSConfig(provider string, values map[string]any) error { + definition, ok := IntegrationDefinition(IntegrationKindSMS, provider) + if !ok { + return errors.BadRequest("SMS_PROVIDER_INVALID", "不支持的短信服务商") + } + for _, field := range definition.Fields { + value, ok := values[field.Key].(string) + if !ok || len(value) > 512 || (field.Required && strings.TrimSpace(value) == "") { + return errors.BadRequest("SMS_CONFIG_INVALID", "请填写有效的"+field.Label) + } + } + return nil +} diff --git a/internal/data/database.go b/internal/data/database.go index 593517f..a9b07f8 100644 --- a/internal/data/database.go +++ b/internal/data/database.go @@ -179,6 +179,21 @@ func openWithDriverConfig(driver, dsn string, config *config.Database, appLogger return db, nil } +func bootstrapDatabaseDSN(c *config.Database) (string, error) { + bootstrap := *c + // An empty name passed to databaseDSN falls back to the configured name. + // Clear both fields so MySQL can connect before the target database exists. + bootstrap.Name = "" + bootstrap.Source = "" + switch normalizedDriver(c.Driver) { + case "pgsql": + bootstrap.Name = "postgres" + case "mssql": + bootstrap.Name = "master" + } + return databaseDSN(&bootstrap, "") +} + func openDatabase(c *config.Database, create bool, template string, appLogger *slog.Logger) (*gorm.DB, error) { driver := normalizedDriver(c.Driver) if driver == "" { @@ -198,14 +213,7 @@ func openDatabase(c *config.Database, create bool, template string, appLogger *s if !databaseNamePattern.MatchString(c.Name) { return nil, fmt.Errorf("invalid database name %q", c.Name) } - bootstrap := "" - switch driver { - case "pgsql": - bootstrap = "postgres" - case "mssql": - bootstrap = "master" - } - dsn, err := databaseDSN(c, bootstrap) + dsn, err := bootstrapDatabaseDSN(c) if err != nil { return nil, err } diff --git a/internal/data/database_test.go b/internal/data/database_test.go index 09a4d8c..94fe887 100644 --- a/internal/data/database_test.go +++ b/internal/data/database_test.go @@ -4,9 +4,44 @@ import ( "strings" "testing" + mysqldriver "github.com/go-sql-driver/mysql" "kra/internal/config" ) +func TestMySQLBootstrapConnectsWithoutTargetDatabase(t *testing.T) { + for _, host := range []string{"", "mysql"} { + t.Run("host="+host, func(t *testing.T) { + cfg := &config.Database{Driver: "mysql", Host: host, Name: "kra_new", Source: "root@tcp(localhost:3306)/kra_new"} + original := *cfg + dsn, err := bootstrapDatabaseDSN(cfg) + if err != nil { + t.Fatal(err) + } + parsed, err := mysqldriver.ParseDSN(dsn) + if err != nil { + t.Fatal(err) + } + if parsed.DBName != "" { + t.Fatalf("bootstrap selected database %q before creating it", parsed.DBName) + } + if *cfg != original { + t.Fatal("bootstrap changed the target configuration") + } + target, err := databaseDSN(cfg, "") + if err != nil { + t.Fatal(err) + } + parsed, err = mysqldriver.ParseDSN(target) + if err != nil { + t.Fatal(err) + } + if parsed.DBName != "kra_new" { + t.Fatalf("target connection selected database %q", parsed.DBName) + } + }) + } +} + func TestMySQLDatabaseDSNIncludesNetworkTimeouts(t *testing.T) { dsn, err := databaseDSN(&config.Database{ Driver: "mysql", diff --git a/internal/data/integration/runtime.go b/internal/data/integration/runtime.go index 2799185..a0be97c 100644 --- a/internal/data/integration/runtime.go +++ b/internal/data/integration/runtime.go @@ -16,7 +16,7 @@ func ReadRuntime(db *gorm.DB) ([]runtimeconfig.Config, error) { } var rows []ConfigPO if err := db.Session(&gorm.Session{NewDB: true}). - Where("kind IN ?", []string{"mq", "websocket"}). + Where("kind IN ?", []string{"mq", "websocket", "sms"}). Order("kind ASC, provider ASC"). Find(&rows).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, err diff --git a/internal/data/integration/sms_test.go b/internal/data/integration/sms_test.go new file mode 100644 index 0000000..f0593c0 --- /dev/null +++ b/internal/data/integration/sms_test.go @@ -0,0 +1,62 @@ +package integration + +import ( + "context" + "encoding/json" + biz "kra/internal/biz/integration" + "kra/internal/integration/runtimeconfig" + "testing" +) + +func TestSMSSecretsSurviveMaskedSaveAndRuntimeReload(t *testing.T) { + db, err := openWithDriver("sqlite", "file:"+t.Name()+"?mode=memory&cache=shared") + if err != nil { + t.Fatal(err) + } + if err = db.AutoMigrate(&ConfigPO{}); err != nil { + t.Fatal(err) + } + p := &integrationRuntimeTestProvider{Data: &Data{gormDB: newReloadableDB(db)}, store: runtimeconfig.NewStore()} + repo := NewIntegrationConfigRepo(p) + for _, provider := range []string{"aliyun", "tencent"} { + values := biz.DefaultIntegrationConfig("sms", provider) + for k, v := range values { + if v == "" { + values[k] = "test-" + k + } + } + raw, _ := json.Marshal(values) + if err = repo.SaveIntegrationConfig(context.Background(), &biz.IntegrationConfig{Kind: "sms", Provider: provider, Enabled: true, Values: raw}); err != nil { + t.Fatal(err) + } + masked, err := repo.FindIntegrationConfig(context.Background(), "sms", provider) + if err != nil { + t.Fatal(err) + } + var public map[string]string + _ = json.Unmarshal(masked.Values, &public) + for k := range values { + if biz.IsIntegrationSecretKey(k) && public[k] != "******" { + t.Fatalf("unmasked key %s", k) + } + } + if err = repo.SaveIntegrationConfig(context.Background(), masked); err != nil { + t.Fatal(err) + } + } + rows, err := ReadRuntime(db) + if err != nil || len(rows) != 2 { + t.Fatalf("runtime rows=%d error=%v", len(rows), err) + } + for _, row := range rows { + var values map[string]string + _ = json.Unmarshal(row.Values, &values) + key := "access_key_secret" + if row.Provider == "tencent" { + key = "secret_key" + } + if values[key] != "test-"+key { + t.Fatal("saved secret did not survive runtime reload") + } + } +} diff --git a/internal/data/migrations_test.go b/internal/data/migrations_test.go index 7d33548..8dee21b 100644 --- a/internal/data/migrations_test.go +++ b/internal/data/migrations_test.go @@ -34,8 +34,8 @@ func TestMigrateAllRunsModuleSchemasWithoutBootstrapSeed(t *testing.T) { if err = db.Table(migration.TableName).Count(&versions).Error; err != nil { t.Fatal(err) } - if versions != 13 { - t.Fatalf("migration versions = %d, want 13", versions) + if versions != 14 { + t.Fatalf("migration versions = %d, want 14", versions) } var communicationRows []dataintegration.ConfigPO if err = db.Where("kind IN ?", []string{"mq", "websocket"}).Order("kind, provider").Find(&communicationRows).Error; err != nil { diff --git a/internal/integration/provider.go b/internal/integration/provider.go index 89e5b63..caadd4b 100644 --- a/internal/integration/provider.go +++ b/internal/integration/provider.go @@ -11,6 +11,7 @@ import ( "kra/internal/integration/email" mqintegration "kra/internal/integration/mq" paymentintegration "kra/internal/integration/payment" + "kra/internal/integration/sms" "kra/internal/integration/storage" "kra/internal/integration/systeminfo" websocketintegration "kra/internal/integration/websocket" @@ -23,6 +24,7 @@ import ( var ProviderSet = wire.NewSet( cache.New, email.NewEmailRepo, + sms.NewSender, paymentintegration.NewFactory, wire.Bind(new(bizpayment.PaymentAdapterFactory), new(*paymentintegration.Factory)), storage.NewFileStorage, diff --git a/internal/integration/sms/sms.go b/internal/integration/sms/sms.go new file mode 100644 index 0000000..6717d8d --- /dev/null +++ b/internal/integration/sms/sms.go @@ -0,0 +1,172 @@ +// Package sms implements cloud SMS delivery using saved integration settings. +package sms + +import ( + "context" + "encoding/json" + "errors" + "regexp" + "strings" + "time" + + openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" + aliyun "github.com/alibabacloud-go/dysmsapi-20170525/v4/client" + util "github.com/alibabacloud-go/tea-utils/v2/service" + "github.com/alibabacloud-go/tea/tea" + kratoserrors "github.com/go-kratos/kratos/v3/errors" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" + tcerrors "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" + tencent "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms/v20210111" + + biz "kra/internal/biz/integration" + "kra/internal/config" + "kra/internal/integration/runtimeconfig" +) + +type aliyunClient interface { + SendSmsWithOptions(*aliyun.SendSmsRequest, *util.RuntimeOptions) (*aliyun.SendSmsResponse, error) +} +type tencentClient interface { + SendSmsWithContext(context.Context, *tencent.SendSmsRequest) (*tencent.SendSmsResponse, error) +} + +type Sender struct { + store *runtimeconfig.Store + newAliyun func(*openapi.Config) (aliyunClient, error) + newTencent func(string, string, string) (tencentClient, error) +} + +func NewSender(store *runtimeconfig.Store) biz.SMSSender { + return &Sender{store: store, + newAliyun: func(c *openapi.Config) (aliyunClient, error) { return aliyun.NewClient(c) }, + newTencent: func(id, secret, region string) (tencentClient, error) { + p := profile.NewClientProfile() + p.HttpProfile.Endpoint = "sms.tencentcloudapi.com" + p.HttpProfile.ReqTimeout = 10 + p.DisableRegionBreaker = true + p.NetworkFailureMaxRetries = 0 + p.RateLimitExceededMaxRetries = 0 + p.UnsafeRetryOnConnectionFailure = false + return tencent.NewClient(common.NewCredential(id, secret), region, p) + }, + } +} + +func (s *Sender) Send(ctx context.Context, m *biz.SMSMessage) (*biz.SMSResult, error) { + if err := biz.ValidateSMSMessage(m); err != nil { + return nil, err + } + if err := ctx.Err(); err != nil { + return nil, err + } + c, ok := s.store.Get(biz.IntegrationKindSMS, m.Provider) + if !ok || !c.Enabled { + return nil, biz.ErrSMSDisabled + } + values := map[string]any{} + if json.Unmarshal(c.Values, &values) != nil { + return nil, biz.ErrSMSInvalid + } + if err := biz.ValidateIntegrationConfig(biz.IntegrationKindSMS, m.Provider, values); err != nil { + return nil, err + } + for key, value := range values { + if text, ok := value.(string); ok && biz.IsIntegrationSecretKey(key) && config.IsMaskedSecret(text) { + return nil, biz.ErrSMSDisabled + } + } + get := func(key string) string { v, _ := values[key].(string); return v } + if m.Provider == "aliyun" { + return s.sendAliyun(ctx, m, get) + } + return s.sendTencent(ctx, m, get) +} + +func (s *Sender) sendAliyun(ctx context.Context, m *biz.SMSMessage, get func(string) string) (*biz.SMSResult, error) { + // This SDK has no context-aware SendSms; bound both connection and read time. + timeout := 10000 + if deadline, ok := ctx.Deadline(); ok { + remaining := int(time.Until(deadline).Milliseconds()) + if remaining <= 0 { + return nil, context.DeadlineExceeded + } + if remaining < timeout { + timeout = remaining + } + } + client, err := s.newAliyun(&openapi.Config{ + AccessKeyId: tea.String(get("access_key_id")), AccessKeySecret: tea.String(get("access_key_secret")), + Endpoint: tea.String("dysmsapi.aliyuncs.com"), Protocol: tea.String("https"), + ConnectTimeout: tea.Int(timeout), ReadTimeout: tea.Int(timeout), + }) + if err != nil { + return nil, sdkError(err) + } + params := m.NamedParams + if params == nil { + params = map[string]string{} + } + raw, _ := json.Marshal(params) + response, err := client.SendSmsWithOptions(&aliyun.SendSmsRequest{ + PhoneNumbers: tea.String(strings.TrimPrefix(m.Phone, "+86")), SignName: tea.String(get("sign_name")), + TemplateCode: tea.String(get("template_id")), TemplateParam: tea.String(string(raw)), + }, &util.RuntimeOptions{Autoretry: tea.Bool(false), MaxAttempts: tea.Int(1), ConnectTimeout: tea.Int(timeout), ReadTimeout: tea.Int(timeout)}) + if err != nil { + return nil, sdkError(err) + } + if response == nil || response.Body == nil { + return nil, biz.ErrSMSFailed + } + if tea.StringValue(response.Body.Code) != "OK" { + return nil, providerError(tea.StringValue(response.Body.Code)) + } + return &biz.SMSResult{RequestID: tea.StringValue(response.Body.RequestId), MessageID: tea.StringValue(response.Body.BizId)}, nil +} + +func (s *Sender) sendTencent(ctx context.Context, m *biz.SMSMessage, get func(string) string) (*biz.SMSResult, error) { + client, err := s.newTencent(get("secret_id"), get("secret_key"), get("region")) + if err != nil { + return nil, sdkError(err) + } + request := tencent.NewSendSmsRequest() + request.PhoneNumberSet = common.StringPtrs([]string{m.Phone}) + request.SmsSdkAppId = common.StringPtr(get("sdk_app_id")) + request.SignName = common.StringPtr(get("sign_name")) + request.TemplateId = common.StringPtr(get("template_id")) + request.TemplateParamSet = common.StringPtrs(m.Params) + response, err := client.SendSmsWithContext(ctx, request) + if err != nil { + return nil, sdkError(err) + } + if response == nil || response.Response == nil || len(response.Response.SendStatusSet) != 1 || response.Response.SendStatusSet[0] == nil { + return nil, biz.ErrSMSFailed + } + status := response.Response.SendStatusSet[0] + if tea.StringValue(status.Code) != "Ok" { + return nil, providerError(tea.StringValue(status.Code)) + } + return &biz.SMSResult{RequestID: tea.StringValue(response.Response.RequestId), MessageID: tea.StringValue(status.SerialNo)}, nil +} + +var providerCodePattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9._-]{0,127}$`) + +func providerError(code string) error { + if !providerCodePattern.MatchString(code) { + return biz.ErrSMSFailed + } + return kratoserrors.BadRequest("SMS_SEND_FAILED", "短信提交失败,服务商错误码:"+code) +} + +// Return only provider codes: raw SDK errors can contain signed requests and PII. +func sdkError(err error) error { + var tc *tcerrors.TencentCloudSDKError + if errors.As(err, &tc) { + return providerError(tc.Code) + } + var ali *tea.SDKError + if errors.As(err, &ali) { + return providerError(tea.StringValue(ali.Code)) + } + return biz.ErrSMSFailed +} diff --git a/internal/integration/sms/sms_test.go b/internal/integration/sms/sms_test.go new file mode 100644 index 0000000..ab4ab5d --- /dev/null +++ b/internal/integration/sms/sms_test.go @@ -0,0 +1,117 @@ +package sms + +import ( + "context" + "encoding/json" + "strings" + "testing" + + openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" + aliyun "github.com/alibabacloud-go/dysmsapi-20170525/v4/client" + util "github.com/alibabacloud-go/tea-utils/v2/service" + "github.com/alibabacloud-go/tea/tea" + tencent "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms/v20210111" + biz "kra/internal/biz/integration" + "kra/internal/integration/runtimeconfig" +) + +type aliFake struct { + calls int + request *aliyun.SendSmsRequest + code string +} + +func (f *aliFake) SendSmsWithOptions(req *aliyun.SendSmsRequest, opts *util.RuntimeOptions) (*aliyun.SendSmsResponse, error) { + f.calls++ + f.request = req + if tea.BoolValue(opts.Autoretry) { + panic("SMS must not retry automatically") + } + return &aliyun.SendSmsResponse{Body: &aliyun.SendSmsResponseBody{Code: tea.String(f.code), BizId: tea.String("ali-message"), RequestId: tea.String("ali-request")}}, nil +} + +type tcFake struct { + calls int + request *tencent.SendSmsRequest + code string +} + +func (f *tcFake) SendSmsWithContext(_ context.Context, req *tencent.SendSmsRequest) (*tencent.SendSmsResponse, error) { + f.calls++ + f.request = req + return &tencent.SendSmsResponse{Response: &tencent.SendSmsResponseParams{RequestId: tea.String("tc-request"), SendStatusSet: []*tencent.SendStatus{{Code: tea.String(f.code), SerialNo: tea.String("tc-message")}}}}, nil +} + +func testSender(t *testing.T, provider string) *Sender { + t.Helper() + store := runtimeconfig.NewStore() + values := biz.DefaultIntegrationConfig("sms", provider) + for k, v := range values { + if v == "" { + values[k] = "test-" + k + } + } + raw, _ := json.Marshal(values) + store.Set(runtimeconfig.Config{Kind: "sms", Provider: provider, Enabled: true, Values: raw}) + return NewSender(store).(*Sender) +} + +func TestAliyunSendAndRejection(t *testing.T) { + s := testSender(t, "aliyun") + f := &aliFake{code: "OK"} + s.newAliyun = func(c *openapi.Config) (aliyunClient, error) { + if tea.StringValue(c.Endpoint) != "dysmsapi.aliyuncs.com" || tea.StringValue(c.AccessKeySecret) != "test-access_key_secret" { + t.Fatal("incorrect client configuration") + } + return f, nil + } + m := &biz.SMSMessage{Provider: "aliyun", Phone: "+8613800138000", NamedParams: map[string]string{"code": "123456"}} + result, err := s.Send(context.Background(), m) + if err != nil || result.MessageID != "ali-message" { + t.Fatalf("result=%v error=%v", result, err) + } + if tea.StringValue(f.request.PhoneNumbers) != "13800138000" || tea.StringValue(f.request.TemplateParam) != `{"code":"123456"}` || tea.StringValue(f.request.TemplateCode) != "test-template_id" { + t.Fatal("incorrect Aliyun request") + } + f.code = "isv.BUSINESS_LIMIT_CONTROL" + if _, err = s.Send(context.Background(), m); err == nil || !strings.Contains(err.Error(), f.code) { + t.Fatalf("provider rejection not surfaced: %v", err) + } + if f.calls != 2 { + t.Fatal("unexpected retry") + } +} + +func TestTencentSendAndRejection(t *testing.T) { + s := testSender(t, "tencent") + f := &tcFake{code: "Ok"} + s.newTencent = func(id, key, region string) (tencentClient, error) { return f, nil } + m := &biz.SMSMessage{Provider: "tencent", Phone: "+8613800138000", Params: []string{"123456", "5"}} + result, err := s.Send(context.Background(), m) + if err != nil || result.MessageID != "tc-message" { + t.Fatalf("result=%v error=%v", result, err) + } + if len(f.request.PhoneNumberSet) != 1 || *f.request.PhoneNumberSet[0] != m.Phone || len(f.request.TemplateParamSet) != 2 || *f.request.TemplateParamSet[1] != "5" || *f.request.SmsSdkAppId != "test-sdk_app_id" { + t.Fatal("incorrect Tencent request") + } + f.code = "FailedOperation.SignatureIncorrectOrUnapproved" + if _, err = s.Send(context.Background(), m); err == nil || !strings.Contains(err.Error(), f.code) { + t.Fatalf("provider rejection not surfaced: %v", err) + } + c, _ := s.store.Get("sms", "tencent") + c.Enabled = false + s.store.Set(c) + if _, err = s.Send(context.Background(), m); err == nil { + t.Fatal("disabled channel sent SMS") + } + if f.calls != 2 { + t.Fatal("disabled channel reached SDK") + } +} + +func TestSDKErrorDoesNotExposeRawMessage(t *testing.T) { + err := sdkError(&tea.SDKError{Code: tea.String("InvalidAccessKeyId.NotFound"), Message: tea.String("secret +8613800138000 signed request")}) + if strings.Contains(err.Error(), "secret") || strings.Contains(err.Error(), "13800138000") || !strings.Contains(err.Error(), "InvalidAccessKeyId.NotFound") { + t.Fatalf("unsafe error: %v", err) + } +} diff --git a/internal/modules/catalog_test.go b/internal/modules/catalog_test.go index 913aabc..fd9167f 100644 --- a/internal/modules/catalog_test.go +++ b/internal/modules/catalog_test.go @@ -25,16 +25,16 @@ func TestCatalogContainsBuiltInModulesInDependencyOrder(t *testing.T) { t.Fatalf("definition[%d] = %q, want %q", index, got, name) } } - if got := catalog.MigrationSteps(); len(got) != 13 { - t.Fatalf("module migrations = %d, want 13", len(got)) + if got := catalog.MigrationSteps(); len(got) != 14 { + t.Fatalf("module migrations = %d, want 14", len(got)) } var menus, apis int for _, definition := range catalog.Definitions { menus += len(definition.Surface.Menus) apis += len(definition.Surface.APIs) } - if menus != 3 || apis != 17 { - t.Fatalf("admin surface = %d menus/%d APIs, want 3/17", menus, apis) + if menus != 4 || apis != 18 { + t.Fatalf("admin surface = %d menus/%d APIs, want 4/18", menus, apis) } if got := catalog.DefaultTimedTasks(); len(got) != 2 { t.Fatalf("default timed tasks = %d, want 2", len(got)) diff --git a/internal/modules/integration/definition.go b/internal/modules/integration/definition.go index 887d316..3f4f678 100644 --- a/internal/modules/integration/definition.go +++ b/internal/modules/integration/definition.go @@ -3,18 +3,31 @@ package integration import ( + "gorm.io/gorm" dataintegration "kra/internal/data/integration" + datasystem "kra/internal/data/system" "kra/internal/modules/surface" + "kra/pkg/database/migration" "kra/pkg/module" ) func Definition() module.Definition { + smsMenu := module.Menu{Name: "sms", Path: "sms", ParentName: "extensions", Component: "view/systemTools/sms/index.vue", Title: "短信服务", Icon: "message", Sort: 9} + migrations := append(dataintegration.Migrations(), migration.Step{ID: "202609290001_sms_admin_surface", Migrate: func(db *gorm.DB) error { + return datasystem.EnsureAdminSurface(db, module.Surface{ + Menus: []module.Menu{smsMenu}, + APIs: []module.API{{Path: "/integration/sms/:provider/send", Method: "POST", Group: "短信服务", Description: "发送单条模板短信"}}, + }, 888) + }}) return module.Definition{ Name: "integration", - Migrations: dataintegration.Migrations(), + Migrations: migrations, Surface: module.Surface{ - Menus: []module.Menu{{Name: "integrationConfig", Path: "integrationConfig", ParentName: "extensions", Component: "view/systemTools/integration/config.vue", Title: "通信集成", Icon: "connection", Sort: 8}}, - APIs: surface.APIsForPrefix("/integration/configs", 5), + Menus: []module.Menu{ + {Name: "integrationConfig", Path: "integrationConfig", ParentName: "extensions", Component: "view/systemTools/integration/config.vue", Title: "通信集成", Icon: "connection", Sort: 8}, + smsMenu, + }, + APIs: surface.APIsForPrefix("/integration/", 6), }, } } diff --git a/internal/modules/integration/definition_test.go b/internal/modules/integration/definition_test.go index 1eea53f..649535c 100644 --- a/internal/modules/integration/definition_test.go +++ b/internal/modules/integration/definition_test.go @@ -4,7 +4,7 @@ import "testing" func TestDefinitionOwnsCommunicationSurface(t *testing.T) { definition := Definition() - if definition.Name != "integration" || len(definition.Migrations) != 5 || len(definition.Surface.Menus) != 1 || len(definition.Surface.APIs) != 5 { + if definition.Name != "integration" || len(definition.Migrations) != 6 || len(definition.Surface.Menus) != 2 || len(definition.Surface.APIs) != 6 { t.Fatalf("integration surface = %#v", definition.Surface) } } diff --git a/internal/routecatalog/catalog.go b/internal/routecatalog/catalog.go index 2f8215e..cec3f31 100644 --- a/internal/routecatalog/catalog.go +++ b/internal/routecatalog/catalog.go @@ -10,6 +10,7 @@ type RouteBodyPolicy string const ( BodyPolicyDefault RouteBodyPolicy = "default" + BodyPolicySMS RouteBodyPolicy = "sms" BodyPolicyIntegrationConfig RouteBodyPolicy = "integration_config" BodyPolicyPaymentConfig RouteBodyPolicy = "payment_config" BodyPolicyPaymentCallback RouteBodyPolicy = "payment_callback" @@ -168,6 +169,7 @@ var routes = map[string]routeValue{ "POST /init/checkdb": {group: "初始化", description: "检查数据库", public: true}, "POST /init/initdb": {group: "初始化", description: "初始化数据库", public: true}, "POST /integration/configs/:kind/:provider/test": {group: "集成配置", description: "测试通信集成连接", audit: true, bodyPolicy: BodyPolicyIntegrationConfig}, + "POST /integration/sms/:provider/send": {group: "短信服务", description: "发送单条模板短信", audit: true, bodyPolicy: BodyPolicySMS}, "POST /jwt/jsonInBlacklist": {group: "jwt", description: "jwt加入黑名单(退出,必选)"}, "POST /mediaUpload/chunk": {group: "媒体上传", description: "上传分片", bodyPolicy: BodyPolicyUpload}, "POST /mediaUpload/complete": {group: "媒体上传", description: "完成大文件上传"}, diff --git a/internal/server/gin_test.go b/internal/server/gin_test.go index 385b411..fe35134 100644 --- a/internal/server/gin_test.go +++ b/internal/server/gin_test.go @@ -97,7 +97,7 @@ func TestGinStartupLogsEveryRegisteredRoute(t *testing.T) { if got, want := strings.Count(text, `"msg":"router registered"`), len(engine.Routes()); got != want { t.Fatalf("registered route log count = %d, want %d", got, want) } - if !strings.Contains(text, `"msg":"router register success"`) || !strings.Contains(text, `"route_count":196`) { + if !strings.Contains(text, `"msg":"router register success"`) || !strings.Contains(text, `"route_count":197`) { t.Fatalf("startup route summary is missing: %s", text) } } diff --git a/internal/server/handler/provider.go b/internal/server/handler/provider.go index c00712d..26cf638 100644 --- a/internal/server/handler/provider.go +++ b/internal/server/handler/provider.go @@ -10,5 +10,6 @@ var ProviderSet = wire.NewSet( NewExport, NewVersion, NewDictionary, NewParameter, NewAPIToken, NewSystemConfig, NewPublic, NewUser, NewNavigation, NewSession, NewIntegrationConfig, + NewSMS, wire.Struct(new(Set), "*"), ) diff --git a/internal/server/handler/public.go b/internal/server/handler/public.go index 05d6937..84cd7ea 100644 --- a/internal/server/handler/public.go +++ b/internal/server/handler/public.go @@ -138,6 +138,8 @@ func (h *Public) InitializeDatabase(engine *gin.Engine) gin.HandlerFunc { Write(c, CodeSuccess, gin.H{}, "自动创建数据库成功") return } + // Preserve the underlying error for ErrorAudit without exposing it in the response. + _ = c.Error(err).SetType(gin.ErrorTypePrivate) Fail(c, "自动创建数据库失败,请查看后台日志,检查后在进行初始化") return } diff --git a/internal/server/handler/set.go b/internal/server/handler/set.go index d892c7b..2008638 100644 --- a/internal/server/handler/set.go +++ b/internal/server/handler/set.go @@ -1,6 +1,7 @@ package handler type Set struct { + SMS *SMS Authority *Authority Menu *Menu API *API diff --git a/internal/server/handler/sms.go b/internal/server/handler/sms.go new file mode 100644 index 0000000..9f1780e --- /dev/null +++ b/internal/server/handler/sms.go @@ -0,0 +1,27 @@ +package handler + +import ( + "github.com/gin-gonic/gin" + "kra/internal/service/dto" + service "kra/internal/service/integration" + "net/http" +) + +type SMS struct{ service *service.SMSService } + +func NewSMS(s *service.SMSService) *SMS { return &SMS{service: s} } + +func (h *SMS) Send(c *gin.Context) { + c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 16<<10) + var req dto.SMSSendRequest + if err := c.ShouldBindJSON(&req); err != nil { + Fail(c, "短信参数无效") + return + } + result, err := h.service.Send(c.Request.Context(), c.Param("provider"), &req) + if err != nil { + Fail(c, err.Error()) + return + } + OKWithData(c, result) +} diff --git a/internal/server/middleware/access_log.go b/internal/server/middleware/access_log.go index cd2c0a3..5ddb671 100644 --- a/internal/server/middleware/access_log.go +++ b/internal/server/middleware/access_log.go @@ -87,7 +87,9 @@ func AccessLog(runtime *config.Store, logger *slog.Logger, version string) gin.H paymentCallback := bodyPolicy == routecatalog.BodyPolicyPaymentCallback paymentConfigWrite := bodyPolicy == routecatalog.BodyPolicyPaymentConfig requestText := "" - if paymentCallback { + if bodyPolicy == routecatalog.BodyPolicySMS { + requestText = "[短信发送参数已隐藏]" + } else if paymentCallback { requestText = paymentCallbackSummary(requestBody, c.GetHeader("Content-Type")) } else if paymentConfigWrite { requestText = paymentConfigSummary(requestBody) diff --git a/internal/server/middleware/audit.go b/internal/server/middleware/audit.go index 10a550f..52af040 100644 --- a/internal/server/middleware/audit.go +++ b/internal/server/middleware/audit.go @@ -101,6 +101,9 @@ func OperationAudit(runtime *config.Store, recorder *systemservice.AuditRecorder if !capturedBody && routecatalog.BodyPolicyFor(c.Request.Method, path) == routecatalog.BodyPolicyPaymentConfig { operationBody = paymentConfigSummary(requestBody) } + if routecatalog.BodyPolicyFor(c.Request.Method, path) == routecatalog.BodyPolicySMS { + operationBody = "[短信发送参数已隐藏]" + } if err := recorder.RecordOperationRequest(c.Request.Context(), &dto.OperationRecordRequest{IP: c.ClientIP(), Method: c.Request.Method, Path: path, Status: status, LatencyMS: time.Since(started).Milliseconds(), Agent: c.Request.UserAgent(), ErrorMessage: errorMessage, Body: operationBody, Response: responseBody, UserID: userID, RequestID: stringValue(requestID), TraceID: stringValueFromContext(c, "trace_id"), DeviceID: c.GetHeader("X-Device-Id")}); err != nil { // Preserve the business response, but expose audit persistence failures // to the global access/error logging pipeline. diff --git a/internal/server/middleware/redact.go b/internal/server/middleware/redact.go index 34bc796..b5d5708 100644 --- a/internal/server/middleware/redact.go +++ b/internal/server/middleware/redact.go @@ -20,6 +20,7 @@ var sensitivePayloadKeys = map[string]struct{}{ "passwd": {}, "pwd": {}, "token": {}, "accesstoken": {}, "refreshtoken": {}, "secret": {}, "clientsecret": {}, "apikey": {}, "privatekey": {}, "idcard": {}, "key": {}, "signingkey": {}, "secretkey": {}, + "accesskeysecret": {}, "secretid": {}, "appkey": {}, "mchkey": {}, "apiv3key": {}, "clientcert": {}, "clientkey": {}, "platformcert": {}, "platformserialno": {}, "publiccert": {}, "credentialcode": {}, "certfile": {}, "keyfile": {}, "publickey": {}, "rootcert": {}, "appcert": {}, "webhookid": {}, diff --git a/internal/server/middleware/sms_audit_test.go b/internal/server/middleware/sms_audit_test.go new file mode 100644 index 0000000..3cb3096 --- /dev/null +++ b/internal/server/middleware/sms_audit_test.go @@ -0,0 +1,46 @@ +package middleware + +import ( + "bytes" + "github.com/gin-gonic/gin" + "kra/internal/config" + "kra/internal/routecatalog" + "log/slog" + "net/http/httptest" + "strings" + "testing" +) + +func TestSMSAuditHidesPhoneAndTemplateParameters(t *testing.T) { + gin.SetMode(gin.TestMode) + var output bytes.Buffer + engine := gin.New() + engine.Use(AccessLog(config.NewStore(&config.Config{Admin: &config.Admin{}}), slog.New(slog.NewJSONHandler(&output, nil)), "test")) + engine.POST("/api/integration/sms/:provider/send", func(c *gin.Context) { + if body, _ := c.Get(ctxReqBodyKey); body != "[短信发送参数已隐藏]" { + t.Fatalf("unsafe audit body: %v", body) + } + c.JSON(200, gin.H{"code": 0}) + }) + path := "/api/integration/sms/aliyun/send" + request := httptest.NewRequest("POST", path, strings.NewReader(`{"phone":"+8613800138000","params":{"code":"987654"}}`)) + request.Header.Set("Content-Type", "application/json") + engine.ServeHTTP(httptest.NewRecorder(), request) + for _, value := range []string{"13800138000", "987654"} { + if strings.Contains(output.String(), value) { + t.Fatal("SMS content leaked into access log") + } + } + if !routecatalog.ShouldAudit("POST", path) { + t.Fatal("send route is not audited") + } +} + +func TestSMSConfigSecretsAreRedacted(t *testing.T) { + text := redactJSON([]byte(`{"config":{"access_key_secret":"ali-value","secret_id":"tc-id-value","secret_key":"tc-value"}}`), "application/json", 4096) + for _, secret := range []string{"ali-value", "tc-id-value", "tc-value"} { + if strings.Contains(text, secret) { + t.Fatalf("secret leaked: %s", text) + } + } +} diff --git a/internal/server/router/routes.go b/internal/server/router/routes.go index fa75454..f9bc138 100644 --- a/internal/server/router/routes.go +++ b/internal/server/router/routes.go @@ -41,6 +41,7 @@ func (r *Routes) RegisterRoutes(public, private *gin.RouterGroup, engine *gin.En func() { RegisterAnnouncement(private, public, r.handlers.Announcement) }, func() { RegisterEmail(private, r.handlers.Email) }, func() { RegisterIntegrationConfig(private, r.handlers.IntegrationConfig) }, + func() { private.POST("/integration/sms/:provider/send", r.handlers.SMS.Send) }, func() { RegisterPayment(private, public, r.handlers.Payment) }, } for _, register := range registrations { diff --git a/internal/service/dto/sms.go b/internal/service/dto/sms.go new file mode 100644 index 0000000..cf3c082 --- /dev/null +++ b/internal/service/dto/sms.go @@ -0,0 +1,13 @@ +package dto + +import "encoding/json" + +type SMSSendRequest struct { + Phone string `json:"phone"` + Params json.RawMessage `json:"params"` +} + +type SMSSendResponse struct { + RequestID string `json:"requestId"` + MessageID string `json:"messageId"` +} diff --git a/internal/service/integration/provider.go b/internal/service/integration/provider.go index 7d5bf13..a063dc3 100644 --- a/internal/service/integration/provider.go +++ b/internal/service/integration/provider.go @@ -3,4 +3,4 @@ package integration import "github.com/google/wire" // ProviderSet wires integration transport services. -var ProviderSet = wire.NewSet(NewIntegrationConfigService) +var ProviderSet = wire.NewSet(NewIntegrationConfigService, NewSMSService) diff --git a/internal/service/integration/sms.go b/internal/service/integration/sms.go new file mode 100644 index 0000000..5cc45ab --- /dev/null +++ b/internal/service/integration/sms.go @@ -0,0 +1,69 @@ +package integration + +import ( + "bytes" + "context" + "encoding/json" + "strings" + + biz "kra/internal/biz/integration" + "kra/internal/service/dto" +) + +type smsUsecase interface { + Send(context.Context, *biz.SMSMessage) (*biz.SMSResult, error) +} + +type SMSService struct{ uc smsUsecase } + +func NewSMSService(uc *biz.SMSUsecase) *SMSService { return &SMSService{uc: uc} } + +func (s *SMSService) Send(ctx context.Context, provider string, req *dto.SMSSendRequest) (*dto.SMSSendResponse, error) { + if req == nil || len(req.Params) > 12000 { + return nil, biz.ErrSMSInvalid + } + m := &biz.SMSMessage{Provider: provider, Phone: strings.TrimSpace(req.Phone)} + params := bytes.TrimSpace(req.Params) + var values any + if json.Unmarshal(params, &values) != nil { + return nil, biz.ErrSMSInvalid + } + switch provider { + case "aliyun": + object, ok := values.(map[string]any) + if !ok { + return nil, biz.ErrSMSInvalid + } + m.NamedParams = make(map[string]string, len(object)) + for key, value := range object { + text, ok := value.(string) + if !ok { + return nil, biz.ErrSMSInvalid + } + m.NamedParams[key] = text + } + case "tencent": + array, ok := values.([]any) + if !ok { + return nil, biz.ErrSMSInvalid + } + m.Params = make([]string, 0, len(array)) + for _, value := range array { + text, ok := value.(string) + if !ok { + return nil, biz.ErrSMSInvalid + } + m.Params = append(m.Params, text) + } + default: + return nil, biz.ErrSMSInvalid + } + if err := biz.ValidateSMSMessage(m); err != nil { + return nil, err + } + result, err := s.uc.Send(ctx, m) + if err != nil { + return nil, err + } + return &dto.SMSSendResponse{RequestID: result.RequestID, MessageID: result.MessageID}, nil +} diff --git a/internal/service/integration/sms_test.go b/internal/service/integration/sms_test.go new file mode 100644 index 0000000..c7fb3fe --- /dev/null +++ b/internal/service/integration/sms_test.go @@ -0,0 +1,44 @@ +package integration + +import ( + "context" + "encoding/json" + biz "kra/internal/biz/integration" + "kra/internal/service/dto" + "testing" +) + +type smsSenderFake struct{ calls int } + +func (f *smsSenderFake) Send(context.Context, *biz.SMSMessage) (*biz.SMSResult, error) { + f.calls++ + return &biz.SMSResult{MessageID: "test"}, nil +} + +func TestSMSRejectsInvalidInputBeforeDelivery(t *testing.T) { + fake := &smsSenderFake{} + s := &SMSService{uc: fake} + for _, tt := range []struct{ provider, phone, params string }{ + {"aliyun", "+8613800138000", `[]`}, {"tencent", "+8613800138000", `{}`}, + {"tencent", "+8613800138000", `null`}, {"tencent", "+8613800138000", `[123]`}, + {"tencent", "+8613800138000", `[null]`}, {"aliyun", "+8613800138000", `{"code":null}`}, + {"aliyun", "13800138000", `{}`}, {"aliyun", "+12025550123", `{}`}, + {"aliyun", "+8613800138000,+8613800138001", `{}`}, {"other", "+8613800138000", `{}`}, + } { + if _, err := s.Send(context.Background(), tt.provider, &dto.SMSSendRequest{Phone: tt.phone, Params: json.RawMessage(tt.params)}); err == nil { + t.Fatalf("accepted invalid input: %+v", tt) + } + } + if fake.calls != 0 { + t.Fatal("invalid input reached sender") + } + for _, provider := range []string{"aliyun", "tencent"} { + params := `{}` + if provider == "tencent" { + params = `[]` + } + if _, err := s.Send(context.Background(), provider, &dto.SMSSendRequest{Phone: "+8613800138000", Params: json.RawMessage(params)}); err != nil { + t.Fatal(err) + } + } +} diff --git a/web/src/api/integration.js b/web/src/api/integration.js index 23be3ef..663e225 100644 --- a/web/src/api/integration.js +++ b/web/src/api/integration.js @@ -1,5 +1,11 @@ import service from '@/utils/request' +export const sendSMS = (provider, data) => service({ + url: `/integration/sms/${encodeURIComponent(provider)}/send`, + method: 'post', + data +}) + export const getIntegrationConfigs = (kind) => service({ url: `/integration/configs/${encodeURIComponent(kind)}`, method: 'get' diff --git a/web/src/pathInfo.json b/web/src/pathInfo.json index dd1e10f..6582f87 100644 --- a/web/src/pathInfo.json +++ b/web/src/pathInfo.json @@ -59,6 +59,7 @@ "/src/view/systemTools/logViewer/index.vue": "LogViewer", "/src/view/systemTools/payment/config.vue": "PaymentConfig", "/src/view/systemTools/payment/orders.vue": "PaymentOrders", + "/src/view/systemTools/sms/index.vue": "SmsService", "/src/view/systemTools/sysError/sysError.vue": "SysError", "/src/view/systemTools/system/system.vue": "Config", "/src/view/systemTools/timedTask/index.vue": "TimedTask", diff --git a/web/src/view/systemTools/sms/index.vue b/web/src/view/systemTools/sms/index.vue new file mode 100644 index 0000000..6512518 --- /dev/null +++ b/web/src/view/systemTools/sms/index.vue @@ -0,0 +1,485 @@ + + + + +