This commit is contained in:
陈兔子
2026-03-19 13:35:35 +08:00
parent c8fe51ee48
commit 370c2f8b40
8 changed files with 961 additions and 216 deletions
+32 -1
View File
@@ -7,4 +7,35 @@ go get github.com/inis-io/go-utils
```
### 使用
> 详细的使用方法请参考 [文档](./document/README.md)
> 详细的使用方法请参考 [文档](./document/README.md)
### Storage 快速使用
```go
package main
import (
"os"
"github.com/inis-io/aide/dto"
"github.com/inis-io/aide/facade"
)
func main() {
// 1) 初始化全局存储(推荐在应用启动时执行一次)
facade.StorageInst.Init(dto.StorageConfig{
Engine: "local",
Local: dto.LocalStorageConfig{Domain: "http://localhost:2000"},
})
// 2) 使用全局实例
file, _ := os.Open("./avatar.png")
defer file.Close()
resp := facade.Storage.Dir("avatar").Ext("png").Upload(file)
_ = resp
// 3) 按配置创建独立实例(适合多租户或临时切换引擎)
custom := facade.Storage.NewStorage(dto.StorageConfig{Engine: "local"})
_ = custom
}
```
+9 -11
View File
@@ -87,35 +87,33 @@ type SmsBaoConfig struct {
// SmsBody - 短信请求参数
type SmsBody struct {
// Target - 目标手机号或邮箱
Target string
Target string
// Code - 自定义验证码
Code string
Code string
// Length - 验证码长度
Length int
Length int
// Template - 发送模板
Template string
// 主题(标题)
Subject string
Subject string
// 昵称(发件人昵称)
Nickname string
// 用户名(收件人昵称)
Username string
// 过期时间(分钟)
Expired int64
Expired int64
// 通信地址
Address string
Address string
// 标题
Title string
Title string
}
// SmsResp - 短信响应
type SmsResp struct {
// 错误信息
Error error
// 结果
Result any
Result any
// 文本
Text string
Text string
// 验证码
VerifyCode string
}
+54
View File
@@ -0,0 +1,54 @@
package dto
type StorageConfig struct {
// Engine - 存储驱动
Engine string `json:"engine" default:"local"`
// Local - 本地存储配置
Local LocalStorageConfig `json:"local"`
// OSS - 阿里OSS配置
OSS OSS `json:"oss"`
// COS - 腾讯COS配置
COS COS `json:"cos"`
// Hash - 计算配置是否发生变更
Hash string `json:"hash"`
}
// LocalStorageConfig - 本地存储配置
type LocalStorageConfig struct {
// Domain - 本地存储域名
Domain string `json:"domain" comment:"域名" validate:"omitempty,url" default:"http://localhost:2000"`
}
// OSS - 阿里OSS配置
type OSS struct {
// AccessKeyId - 阿里云AccessKey ID
AccessKeyId string `json:"access_key_id" comment:"AccessKey ID" validate:"required,alphaNum"`
// AccessKeySecret - 阿里云AccessKey Secret
AccessKeySecret string `json:"access_key_secret" comment:"AccessKey Secret" validate:"required,alphaNum"`
// Endpoint - OSS 外网 Endpoint
Endpoint string `json:"endpoint" comment:"endpoint" validate:"required,host" default:"oss-cn-guangzhou.aliyuncs.com"`
// Bucket - OSS Bucket - 存储桶名称
Bucket string `json:"bucket" comment:"存储桶名称" validate:"required,alphaDash"`
// Domain - OSS 外网域名 - 用于访问 - 不填写则使用默认域名
Domain string `json:"domain" comment:"外网域名" validate:"omitempty,url"`
// Path - OSS 存储目录
Path string `json:"path" comment:"存储目录" validate:"required" default:"inis"`
}
// COS - 腾讯COS配置
type COS struct {
// AppId - 腾讯云COS AppId
AppId string `json:"app_id" comment:"AppId" validate:"required,numeric"`
// SecretId - 腾讯云COS SecretId
SecretId string `json:"secret_id" comment:"SecretId" validate:"required,alphaNum"`
// SecretKey - 腾讯云COS SecretKey
SecretKey string `json:"secret_key" comment:"SecretKey" validate:"required,alphaNum"`
// Bucket - COS Bucket - 存储桶名称
Bucket string `json:"bucket" comment:"存储桶名称" validate:"required,alphaDash"`
// Region - COS 所在地区,如这里的 ap-guangzhou(广州)
Region string `json:"region" comment:"区域" validate:"required,alphaDash" default:"ap-guangzhou"`
// Domain - COS 外网域名 - 用于访问 - 不填写则使用默认域名
Domain string `json:"domain" comment:"外网域名" validate:"omitempty,url"`
// Path - COS 存储目录
Path string `json:"path" comment:"存储目录" default:"inis"`
}
+5 -10
View File
@@ -423,9 +423,7 @@ func (this *RedisClass) Expired(second any) CacheAPI {
// Has - 判断缓存是否存在
func (this *RedisClass) Has(key string) (ok bool) {
if utils.Is.Empty(key) {
return false
}
if utils.Is.Empty(key) { return false }
ctx := context.Background()
@@ -436,9 +434,7 @@ func (this *RedisClass) Has(key string) (ok bool) {
// Get - 获取缓存
func (this *RedisClass) Get(key string) (value any) {
if utils.Is.Empty(key) {
return false
}
if utils.Is.Empty(key) { return false }
ctx := context.Background()
@@ -449,8 +445,8 @@ func (this *RedisClass) Get(key string) (value any) {
// Set - 设置缓存
func (this *RedisClass) Set(key string, value any) (ok bool) {
cache := this.clone()
cache := this.clone()
if cache == nil || utils.Is.Empty(key) { return false }
ctx := context.Background()
@@ -468,10 +464,9 @@ func (this *RedisClass) Set(key string, value any) (ok bool) {
// Delete - 删除缓存
func (this *RedisClass) Delete(key ...string) (ok bool) {
cache := this.clone()
if cache == nil {
return false
}
if cache == nil { return false }
var err error
ctx := context.Background()
+127 -193
View File
@@ -6,7 +6,7 @@ import (
"fmt"
"strings"
"time"
AliYunOpenApi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
AliYunSmsApi "github.com/alibabacloud-go/dysmsapi-20170525/v5/client"
AliYunUtilV2 "github.com/alibabacloud-go/tea-utils/v2/service"
@@ -36,19 +36,19 @@ func init() { SmsInst.Init() }
// normConfig - 统一配置默认值,避免不同项目接入时行为不一致
func (this *SmsClass) normConfig(config dto.SmsConfig) dto.SmsConfig {
config.Engine.Email = strings.ToLower(strings.TrimSpace(config.Engine.Email))
if utils.Is.Empty(config.Engine.Email) || config.Engine.Email != "email" {
config.Engine.Email = "email"
}
config.Engine.SMS = strings.ToLower(strings.TrimSpace(config.Engine.SMS))
switch config.Engine.SMS {
case "aliyun", "tencent", "smsbao":
default:
config.Engine.SMS = "aliyun"
}
if utils.Is.Empty(config.Email.Host) {
config.Email.Host = "smtp.qq.com"
}
@@ -61,7 +61,7 @@ func (this *SmsClass) normConfig(config dto.SmsConfig) dto.SmsConfig {
if utils.Is.Empty(config.Email.Subject) {
config.Email.Subject = "邮件主题"
}
if utils.Is.Empty(config.AliYun.Endpoint) {
config.AliYun.Endpoint = "dysmsapi.aliyuncs.com"
}
@@ -74,11 +74,11 @@ func (this *SmsClass) normConfig(config dto.SmsConfig) dto.SmsConfig {
if utils.Is.Empty(config.Smsbao.BaseUrl) {
config.Smsbao.BaseUrl = "https://api.smsbao.com"
}
if utils.Is.Empty(config.Hash) {
config.Hash = utils.Hash.Sum32(utils.Json.Encode(config))
}
return config
}
@@ -161,14 +161,14 @@ func (this *SmsClass) useDefaultSms() {
func (this *SmsClass) setActiveSms(config dto.SmsConfig) {
conf := SmsInst.normConfig(config)
SmsInst.Config = conf
GoMail = SmsInst.NewGoMail(conf)
SmsAliYun = nil
SmsTencent = nil
SmsBao = nil
SMS = GoMail
switch SmsInst.normSmsMode(conf.Engine.SMS) {
case "tencent":
SmsTencent = SmsInst.NewSmsTencent(conf)
@@ -203,15 +203,15 @@ func (this *SmsClass) setConfig(config dto.SmsConfig) *SmsClass {
// ReloadIfChanged - 当配置发生变化时重新加载短信服务
func (this *SmsClass) ReloadIfChanged(config ...dto.SmsConfig) {
if len(config) > 0 {
this.setConfig(config[0])
}
if !this.HasConfig {
return
}
// hash 变化,说明配置有更新
if this.Hash != this.Config.Hash {
this.Init()
@@ -220,16 +220,16 @@ func (this *SmsClass) ReloadIfChanged(config ...dto.SmsConfig) {
// Init 初始化 SMS
func (this *SmsClass) Init(config ...dto.SmsConfig) {
if len(config) > 0 {
this.setConfig(config[0])
}
if !this.HasConfig {
SmsInst.useDefaultSms()
return
}
this.Config = SmsInst.normConfig(this.Config)
this.Hash = this.Config.Hash
SmsInst.setActiveSms(this.Config)
@@ -262,7 +262,7 @@ type SmsAPI interface {
// Len - 验证码长度
Len(length int) SmsAPI
// Send - 发送验证码
Send(target ...any) *dto.SmsResp
Send(target ...any) (*dto.SmsResp, error)
// Subject - 主题(标题)
Subject(subject string) SmsAPI
// SetBody - 设置参数体
@@ -280,14 +280,12 @@ type GoMailClass struct {
// 配置
Config dto.SmsConfig
// 参数
Body dto.SmsBody
Body dto.SmsBody
}
// clone - 克隆邮件实例(共享客户端,隔离上下文)
func (this *GoMailClass) clone() *GoMailClass {
if this == nil {
return nil
}
if this == nil { return nil }
clone := *this
return &clone
}
@@ -353,47 +351,38 @@ func (this *GoMailClass) Nickname(nickname string) SmsAPI {
}
// Send - 发送验证码
func (this *GoMailClass) Send(target ...any) (response *dto.SmsResp) {
response = &dto.SmsResp{}
func (this *GoMailClass) Send(target ...any) (*dto.SmsResp, error) {
mail := this.clone()
if mail == nil {
response.Error = errors.New("email client is not initialized")
return response
}
if mail.Client == nil {
response.Error = errors.New("email client is not initialized")
return response
}
if mail == nil { return nil, errors.New("email client is not initialized") }
if mail.Client == nil { return nil, errors.New("email client is not initialized") }
// 这里的 target 是邮箱地址 - 优先级最高
if len(target) > 0 {
mail.Body.Target = cast.ToString(target[0])
}
socialType, err := utils.Identify.EmailOrPhone(mail.Body.Target)
// 如果不是邮箱或手机号
if err != nil {
response.Error = err
return
}
if err != nil { return nil, err }
if socialType == "phone" {
sender := SmsInst.newWithConfig(mail.Config, mail.Config.Engine.SMS)
if sender == nil {
response.Error = errors.New("sms sender is not initialized")
return response
}
if sender == nil { return nil, errors.New("sms sender is not initialized") }
return sender.SetBody(mail.Body).Send(mail.Body.Target)
}
// 如果自定义验证码为空,则生成一个验证码
if utils.Is.Empty(mail.Body.Code) {
mail.Body.Code = utils.Rand.Code(mail.Body.Length)
}
subject := utils.Default(mail.Body.Subject, mail.Config.Email.Subject)
subject := utils.Default(mail.Body.Subject, mail.Config.Email.Subject)
nickname := utils.Default(mail.Body.Nickname, mail.Config.Email.Nickname)
item := gomail.NewMessage()
// 设置邮件内容类型
item.SetHeader("Content-Type", "text/html; charset=UTF-8")
@@ -417,22 +406,21 @@ func (this *GoMailClass) Send(target ...any) (response *dto.SmsResp) {
})
// 设置邮件正文
item.SetBody("text/html", temp)
// 发送邮件
// 使用明确的 “拨号 + 发送” 操作,以确保在执行AUTH和MAIL命令之前完成SMTP握手(EHLO/STARTTLS)。
sender, err := mail.Client.Dial()
if err != nil {
response.Error = err
return response
}
if err != nil { return nil, err }
defer func() { _ = sender.Close() }()
if err := gomail.Send(sender, item); err != nil {
response.Error = err
return response
return nil, err
}
response.VerifyCode = mail.Body.Code
return response
return &dto.SmsResp{
VerifyCode: mail.Body.Code,
}, nil
}
// SetBody - 设置参数体
@@ -473,14 +461,14 @@ func (this *SmsAliYunClass) clone() *SmsAliYunClass {
func (this *SmsAliYunClass) Init() {
this.Config = SmsInst.normConfig(this.Config)
this.Body = SmsInst.mergeSmsBody(SmsInst.defaultSmsBody(), this.Body)
// 创建访问凭证
credential, err := AliYunCredential.NewCredential(nil)
// 凭证创建失败
if err != nil {
return
}
// 创建客户端
client, err := AliYunSmsApi.NewClient(&AliYunOpenApi.Config{
Credential: credential,
@@ -495,7 +483,7 @@ func (this *SmsAliYunClass) Init() {
if err != nil {
return
}
this.Client = client
}
@@ -572,41 +560,34 @@ func (this *SmsAliYunClass) Nickname(nickname string) SmsAPI {
}
// Send - 发送验证码
func (this *SmsAliYunClass) Send(target ...any) (response *dto.SmsResp) {
response = &dto.SmsResp{}
func (this *SmsAliYunClass) Send(target ...any) (*dto.SmsResp, error) {
sms := this.clone()
if sms == nil {
response.Error = errors.New("aliyun sms client is not initialized")
return response
}
if sms.Client == nil {
response.Error = errors.New("aliyun sms client is not initialized")
return response
}
if sms == nil { return nil, errors.New("aliyun sms client is not initialized") }
if sms.Client == nil { return nil, errors.New("aliyun sms client is not initialized") }
// 这里的 target 是手机号 - 优先级最高
if len(target) > 0 {
sms.Body.Target = cast.ToString(target[0])
}
// 如果不是邮箱或手机号
if attr, err := utils.Identify.EmailOrPhone(sms.Body.Target); err != nil {
response.Error = err
return
return nil, err
} else if attr == "email" {
sender := SmsInst.newWithConfig(sms.Config, sms.Config.Engine.Email)
if sender == nil {
response.Error = errors.New("email sender is not initialized")
return response
return nil, errors.New("email sender is not initialized")
}
return sender.SetBody(sms.Body).Send(sms.Body.Target)
}
// 如果自定义验证码为空,则生成一个验证码
if utils.Is.Empty(sms.Body.Code) {
sms.Body.Code = utils.Rand.Code(sms.Body.Length)
}
params := &AliYunSmsApi.SendSmsRequest{
PhoneNumbers: tea.String(sms.Body.Target),
SignName: tea.String(sms.Config.AliYun.SignName),
@@ -616,26 +597,23 @@ func (this *SmsAliYunClass) Send(target ...any) (response *dto.SmsResp) {
"time": sms.Body.Expired,
})),
}
resp, err := sms.Client.SendSmsWithOptions(params, &AliYunUtilV2.RuntimeOptions{})
if err != nil {
response.Error = err
return response
}
if err != nil { return nil, err }
if resp == nil || resp.Body == nil || resp.Body.Code == nil {
response.Error = errors.New("aliyun sms response is nil")
return response
return nil, errors.New("aliyun sms response is nil")
}
if strings.ToLower(*resp.Body.Code) != "ok" {
response.Error = errors.New(cast.ToString(tea.StringValue(resp.Body.Message)))
return response
return nil, errors.New(cast.ToString(tea.StringValue(resp.Body.Message)))
}
response.Result = cast.ToStringMap(*resp.Body)
response.Text = utils.Json.Encode(*resp.Body)
response.VerifyCode = sms.Body.Code
return response
return &dto.SmsResp{
Result: cast.ToStringMap(*resp.Body),
Text: utils.Json.Encode(*resp.Body),
VerifyCode: sms.Body.Code,
}, nil
}
// SetBody - 设置参数体
@@ -676,18 +654,18 @@ func (this *SmsTencentClass) clone() *SmsTencentClass {
func (this *SmsTencentClass) Init() {
this.Config = SmsInst.normConfig(this.Config)
this.Body = SmsInst.mergeSmsBody(SmsInst.defaultSmsBody(), this.Body)
credential := common.NewCredential(this.Config.Tencent.SecretId, this.Config.Tencent.SecretKey)
clientProfile := profile.NewClientProfile()
// sms.tencentcloudapi.com
clientProfile.HttpProfile.Endpoint = this.Config.Tencent.Endpoint
// ap-guangzhou
client, err := TencentCloud.NewClient(credential, this.Config.Tencent.Region, clientProfile)
if err != nil {
return
}
this.Client = client
}
@@ -742,83 +720,61 @@ func (this *SmsTencentClass) Nickname(nickname string) SmsAPI {
}
// Send - 发送验证码
func (this *SmsTencentClass) Send(target ...any) (response *dto.SmsResp) {
response = &dto.SmsResp{}
func (this *SmsTencentClass) Send(target ...any) (*dto.SmsResp, error) {
sms := this.clone()
if sms == nil {
response.Error = errors.New("tencent sms client is not initialized")
return response
}
if sms.Client == nil {
response.Error = errors.New("tencent sms client is not initialized")
return response
}
if sms == nil { return nil, errors.New("tencent sms client is not initialized") }
if sms.Client == nil { return nil, errors.New("tencent sms client is not initialized") }
// 这里的 target 是手机号 - 优先级最高
if len(target) > 0 {
sms.Body.Target = cast.ToString(target[0])
}
socialType, err := utils.Identify.EmailOrPhone(sms.Body.Target)
// 如果不是邮箱或手机号
if err != nil {
response.Error = err
return
}
if err != nil { return nil, err }
if socialType == "email" {
sender := SmsInst.newWithConfig(sms.Config, sms.Config.Engine.Email)
if sender == nil {
response.Error = errors.New("email sender is not initialized")
return response
}
if sender == nil { return nil, errors.New("email sender is not initialized") }
return sender.SetBody(sms.Body).Send(sms.Body.Target)
}
// 如果自定义验证码为空,则生成一个验证码
if utils.Is.Empty(sms.Body.Code) {
sms.Body.Code = utils.Rand.Code(sms.Body.Length)
}
// 实例化一个请求对象,每个接口都会对应一个request对象
request := TencentCloud.NewSendSmsRequest()
request.PhoneNumberSet = common.StringPtrs([]string{sms.Body.Target})
request.SmsSdkAppId = common.StringPtr(sms.Config.Tencent.SmsSdkAppId)
request.SignName = common.StringPtr(sms.Config.Tencent.SignName)
request.TemplateId = common.StringPtr(sms.Config.Tencent.VerifyCode)
request.TemplateParamSet = common.StringPtrs([]string{sms.Body.Code})
item, err := sms.Client.SendSms(request)
if err != nil {
response.Error = err
return response
}
if item.Response == nil {
response.Error = errors.New("response is nil")
return response
}
if len(item.Response.SendStatusSet) == 0 {
response.Error = errors.New("response send status set is nil")
return response
}
if item.Response.SendStatusSet[0].Code == nil {
response.Error = errors.New("response send status code is nil")
return response
}
if *item.Response.SendStatusSet[0].Code != "Ok" {
response.Error = errors.New(cast.ToString(item.Response.SendStatusSet[0].Message))
return response
}
response.VerifyCode = sms.Body.Code
response.Text = item.ToJsonString()
response.Result = utils.Json.Decode(item.ToJsonString())
return response
if err != nil { return nil, err }
if item.Response == nil { return nil, errors.New("response is nil") }
if len(item.Response.SendStatusSet) == 0 { return nil, errors.New("response send status set is nil") }
if item.Response.SendStatusSet[0].Code == nil { return nil, errors.New("response send status code is nil") }
if *item.Response.SendStatusSet[0].Code != "Ok" { return nil, errors.New(cast.ToString(item.Response.SendStatusSet[0].Message)) }
return &dto.SmsResp{
Result: utils.Json.Decode(item.ToJsonString()),
Text: item.ToJsonString(),
VerifyCode: sms.Body.Code,
}, nil
}
// SetBody - 设置参数体
@@ -915,52 +871,35 @@ func (this *SmsBaoClass) Nickname(nickname string) SmsAPI {
}
// Send - 发送验证码
func (this *SmsBaoClass) Send(target ...any) (response *dto.SmsResp) {
func (this *SmsBaoClass) Send(target ...any) (*dto.SmsResp, error) {
response = &dto.SmsResp{}
sms := this.clone()
if sms == nil { return nil, errors.New("smsbao sender is not initialized") }
if sms == nil {
response.Error = errors.New("smsbao sender is not initialized")
return response
}
// 这里的 target 是手机号 - 优先级最高
if len(target) > 0 {
sms.Body.Target = cast.ToString(target[0])
}
socialType, err := utils.Identify.EmailOrPhone(sms.Body.Target)
// 如果不是邮箱或手机号
if err != nil {
response.Error = err
return
}
if err != nil { return nil, err }
if socialType == "email" {
sender := SmsInst.newWithConfig(sms.Config, sms.Config.Engine.Email)
if sender == nil {
response.Error = errors.New("email sender is not initialized")
return response
}
if sender == nil { return nil, errors.New("email sender is not initialized") }
return sender.SetBody(sms.Body).Send(sms.Body.Target)
}
// 如果自定义验证码为空,则生成一个验证码
if utils.Is.Empty(sms.Body.Code) {
sms.Body.Code = utils.Rand.Code(sms.Body.Length)
}
if utils.Is.Empty(sms.ApiKey) {
response.Error = errors.New("API密钥不能为空")
return
}
if utils.Is.Empty(sms.Account) {
response.Error = errors.New("账号不能为空")
return
}
if utils.Is.Empty(sms.ApiKey) { return nil, errors.New("API密钥不能为空") }
if utils.Is.Empty(sms.Account) { return nil, errors.New("账号不能为空") }
item := utils.Curl(utils.CurlRequest{
Method: "GET",
Url: fmt.Sprintf("%s/sms", sms.BaseUrl),
@@ -973,20 +912,15 @@ func (this *SmsBaoClass) Send(target ...any) (response *dto.SmsResp) {
}),
},
}).Send()
if item.Error != nil {
response.Error = item.Error
return
}
if cast.ToInt(item.Text) != 0 {
response.Error = errors.New("发送失败")
return
}
response.VerifyCode = sms.Body.Code
response.Text = item.Text
return response
if item.Error != nil { return nil, item.Error }
if cast.ToInt(item.Text) != 0 { return nil, errors.New("发送失败") }
return &dto.SmsResp{
Text: item.Text,
VerifyCode: sms.Body.Code,
}, nil
}
// SetBody - 设置参数体
@@ -1000,4 +934,4 @@ func (this *SmsBaoClass) SetBody(body dto.SmsBody) SmsAPI {
// NewSms - 使用传入配置创建短信实例
func (this *SmsBaoClass) NewSms(config dto.SmsConfig) SmsAPI {
return SmsInst.newWithConfig(config, "smsbao")
}
}
+706
View File
@@ -0,0 +1,706 @@
package facade
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
pathpkg "path"
"strings"
"sync"
"time"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"github.com/inis-io/aide/dto"
"github.com/inis-io/aide/utils"
"github.com/spf13/cast"
"github.com/tencentyun/cos-go-sdk-v5"
)
var StorageInst = &StorageClass{}
type StorageClass struct {
// 记录配置 Hash 值,用于检测配置文件是否有变化
Hash string `json:"hash"`
// 当前存储配置(由调用方注入)
Config dto.StorageConfig `json:"config"`
// 是否已经注入过配置
HasConfig bool `json:"hasConfig"`
// 读写锁,保护配置和Hash的并发访问
mu sync.RWMutex
}
func init() { StorageInst.Init() }
// normConfig 统一配置默认值,避免不同项目接入时行为不一致
func (this *StorageClass) normConfig(config dto.StorageConfig) dto.StorageConfig {
config.Engine = strings.ToLower(strings.TrimSpace(config.Engine))
switch config.Engine {
case "oss", "cos", "local":
default:
config.Engine = "local"
}
if utils.Is.Empty(config.Local.Domain) {
config.Local.Domain = "http://localhost:2000"
}
if utils.Is.Empty(config.OSS.Endpoint) {
config.OSS.Endpoint = "oss-cn-guangzhou.aliyuncs.com"
}
if utils.Is.Empty(config.OSS.Path) {
config.OSS.Path = "inis"
}
if utils.Is.Empty(config.COS.Region) {
config.COS.Region = "ap-guangzhou"
}
if utils.Is.Empty(config.COS.Path) {
config.COS.Path = "inis"
}
if utils.Is.Empty(config.Hash) {
config.Hash = utils.Hash.Sum32(utils.Json.Encode(config))
}
return config
}
// defaultConfig - 获取默认存储配置
func (this *StorageClass) defaultConfig() dto.StorageConfig {
return StorageInst.normConfig(dto.StorageConfig{})
}
// useDefaultStorage - 使用默认配置激活存储
func (this *StorageClass) useDefaultStorage() {
conf := StorageInst.defaultConfig()
this.mu.Lock()
this.Config = conf
this.Hash = conf.Hash
this.HasConfig = false
this.mu.Unlock()
StorageInst.setActiveStorage(conf)
}
// setActiveStorage - 按配置切换当前活动存储实现
func (this *StorageClass) setActiveStorage(config dto.StorageConfig) {
conf := StorageInst.normConfig(config)
this.mu.Lock()
this.Config = conf
this.mu.Unlock()
Storage = StorageInst.newWithConfig(conf)
LocalStorage = nil
OSS = nil
COS = nil
switch impl := Storage.(type) {
case *LocalStorageClass:
LocalStorage = impl
case *OssClass:
OSS = impl
case *CosClass:
COS = impl
}
}
// newWithConfig - 按配置创建新的存储实现
func (this *StorageClass) newWithConfig(config dto.StorageConfig) StorageAPI {
conf := StorageInst.normConfig(config)
switch conf.Engine {
case "oss":
item := &OssClass{Config: conf}
item.Init()
if item.Client != nil {
return item
}
case "cos":
item := &CosClass{Config: conf}
item.Init()
if item.Client != nil {
return item
}
}
return &LocalStorageClass{Config: conf}
}
// setConfig - 注入存储配置
func (this *StorageClass) setConfig(config dto.StorageConfig) *StorageClass {
this.mu.Lock()
defer this.mu.Unlock()
this.Config = StorageInst.normConfig(config)
this.HasConfig = true
return this
}
// ReloadIfChanged - 当配置发生变化时重新加载存储
func (this *StorageClass) ReloadIfChanged(config ...dto.StorageConfig) {
if len(config) > 0 {
this.setConfig(config[0])
}
this.mu.RLock()
hasConfig := this.HasConfig
hash := this.Hash
confHash := this.Config.Hash
this.mu.RUnlock()
if !hasConfig {
return
}
// hash 变化,说明配置有更新
if hash != confHash {
this.Init()
}
}
// Init 初始化
func (this *StorageClass) Init(config ...dto.StorageConfig) {
if len(config) > 0 {
this.setConfig(config[0])
}
this.mu.RLock()
hasConfig := this.HasConfig
current := this.Config
this.mu.RUnlock()
if !hasConfig {
StorageInst.useDefaultStorage()
return
}
conf := StorageInst.normConfig(current)
this.mu.Lock()
this.Config = conf
this.Hash = conf.Hash
this.mu.Unlock()
StorageInst.setActiveStorage(conf)
}
// Storage - Storage实例
/**
* @return StorageAPI
* @example
* storage := facade.Storage.Upload(facade.Storage.Path() + suffix, bytes)
*/
var Storage StorageAPI
var OSS *OssClass
var COS *CosClass
var LocalStorage *LocalStorageClass
// StorageResp - 存储响应
type StorageResp struct {
Error error
Path string
Domain string
Name string
}
// StorageParams - 存储参数
type StorageParams struct {
// Dir - 存储目录
Dir string
// Name - 存储文件名
Name string
// Ext - 存储文件后缀
Ext string
}
// StorageAPI 定义了存储操作的接口。
type StorageAPI interface {
// Upload 上传文件
/**
* @param reader io.Reader - 读取器
* @returns StorageAPI - 存储接口
*/
Upload(reader io.Reader) *StorageResp
// Dir 设置存储的目录
/**
* @param dir string - 目录
* @returns StorageAPI - 存储接口
*/
Dir(dir string) StorageAPI
// Name 设置存储文件的名称
/**
* @param name string - 名称
* @returns StorageAPI - 存储接口
*/
Name(name string) StorageAPI
// Ext 设置存储文件的后缀
/**
* @param ext string - 后缀
* @returns StorageAPI - 存储接口
*/
Ext(ext string) StorageAPI
// NewStorage - 使用传入配置创建新的存储实例
NewStorage(config dto.StorageConfig) StorageAPI
}
// cleanDir - 标准化目录,确保目录以 / 结尾
func (this *StorageClass) cleanDir(dir string) string {
if !utils.Is.Empty(dir) && !strings.HasSuffix(dir, "/") {
dir += "/"
}
return dir
}
// cleanExt - 标准化后缀,确保以 . 开头
func (this *StorageClass) cleanExt(ext string) string {
if !utils.Is.Empty(ext) && !strings.HasPrefix(ext, ".") {
ext = "." + ext
}
return ext
}
// fileNameFromPath - 提取路径中的文件名
func (this *StorageClass) fileNameFromPath(path string) string {
name := pathpkg.Base(strings.TrimSpace(path))
if name == "." || name == "/" {
return ""
}
return name
}
// =================================== 本地存储存储 - 开始 ===================================
// LocalStorageClass 本地存储
type LocalStorageClass struct {
// 配置
Config dto.StorageConfig
// 参数
Params StorageParams
}
// clone - 克隆本地存储实例(共享配置,隔离链式参数)
func (this *LocalStorageClass) clone() *LocalStorageClass {
if this == nil {
return nil
}
clone := *this
return &clone
}
// Upload - 上传文件
func (this *LocalStorageClass) Upload(reader io.Reader) (response *StorageResp) {
response = &StorageResp{}
path := this.Path()
item := utils.File().Save(reader, path)
if item.Error != nil {
response.Error = item.Error
return
}
// 去除前面的 public
response.Path = strings.Replace(path, "public", "", 1)
response.Domain = this.Config.Local.Domain
response.Name = StorageInst.fileNameFromPath(path)
return
}
// Path - 本地存储位置 - 生成文件路径
func (this *LocalStorageClass) Path() (path string) {
// 生成文件名 - 年月日+毫秒时间戳
name := cast.ToString(time.Now().UnixNano() / 1e6)
// 生成年月日目录 - 如:2023-04/10
dir := time.Now().Format("2006-01/02/")
// 自定义目录
if !utils.Is.Empty(this.Params.Dir) {
dir = this.Params.Dir
}
// 自定义文件名
if !utils.Is.Empty(this.Params.Name) {
name = this.Params.Name
}
// 得到文件路径 - 但是可能还存在重复的 /
path = strings.Join([]string{"public", "storage", dir}, "/")
// 替换重复的 / - 重新生成文件路径
path = strings.Join(cast.ToStringSlice(utils.ArrayEmpty(strings.Split(path, "/"))), "/")
// 如果不是以 / 结尾
if !strings.HasSuffix(path, "/") { path += "/" }
return path + name + this.Params.Ext
}
// Dir - 本地存储位置 - 生成文件目录
func (this *LocalStorageClass) Dir(dir string) StorageAPI {
item := this.clone()
if item == nil {
return this
}
item.Params.Dir = StorageInst.cleanDir(dir)
return item
}
// Name - 本地存储位置 - 生成文件名
func (this *LocalStorageClass) Name(name string) StorageAPI {
item := this.clone()
if item == nil {
return this
}
item.Params.Name = name
return item
}
// Ext - 本地存储位置 - 生成文件后缀
func (this *LocalStorageClass) Ext(ext string) StorageAPI {
item := this.clone()
if item == nil {
return this
}
item.Params.Ext = StorageInst.cleanExt(ext)
return item
}
// NewStorage - 使用传入配置创建存储实例
func (this *LocalStorageClass) NewStorage(config dto.StorageConfig) StorageAPI {
return StorageInst.newWithConfig(config)
}
// ================================== 阿里云对象存储 - 开始 ==================================
// OssClass 阿里云对象存储
type OssClass struct {
// OSS客户端
Client *oss.Client
// 配置
Config dto.StorageConfig
// 参数
Params StorageParams
}
// clone - 克隆 OSS 存储实例(共享客户端,隔离链式参数)
func (this *OssClass) clone() *OssClass {
if this == nil {
return nil
}
clone := *this
return &clone
}
// Init 初始化 阿里云对象存储
func (this *OssClass) Init() {
this.Config = StorageInst.normConfig(this.Config)
client, err := oss.New(this.Config.OSS.Endpoint, this.Config.OSS.AccessKeyId, this.Config.OSS.AccessKeySecret)
if err != nil {
return
}
this.Client = client
}
// Bucket - 获取Bucket(存储桶)
func (this *OssClass) Bucket() *oss.Bucket {
if this.Client == nil {
return nil
}
exist, err := this.Client.IsBucketExist(this.Config.OSS.Bucket)
if err != nil {
return nil
}
if !exist {
// 创建存储空间。
err = this.Client.CreateBucket(this.Config.OSS.Bucket)
if err != nil {
return nil
}
}
bucket, err := this.Client.Bucket(this.Config.OSS.Bucket)
if err != nil {
return nil
}
return bucket
}
// Upload - 上传文件
func (this *OssClass) Upload(reader io.Reader) (response *StorageResp) {
response = &StorageResp{}
path := this.Path()
bucket := this.Bucket()
if bucket == nil {
response.Error = fmt.Errorf("OSS Bucket 获取失败")
return
}
if err := bucket.PutObject(path, reader); err != nil {
response.Error = err
return
}
if utils.Is.Empty(this.Config.OSS.Domain) {
response.Domain = "https://" + this.Config.OSS.Bucket + "." + this.Config.OSS.Endpoint
} else {
response.Domain = this.Config.OSS.Domain
}
response.Path = "/" + path
response.Name = StorageInst.fileNameFromPath(path)
return
}
// Path - OSS存储位置 - 生成文件路径
func (this *OssClass) Path() (path string) {
// 生成文件名 - 年月日+毫秒时间戳
name := cast.ToString(time.Now().UnixNano() / 1e6)
// 存储根目录
root := this.Config.OSS.Path
// 生成年月日目录 - 如:2023-04/10
dir := time.Now().Format("2006-01/02/")
// 自定义目录
if !utils.Is.Empty(this.Params.Dir) {
dir = this.Params.Dir
}
// 自定义文件名
if !utils.Is.Empty(this.Params.Name) {
name = this.Params.Name
}
// 得到文件路径 - 但是可能还存在重复的 /
path = strings.Join([]string{root, dir}, "/")
// 替换重复的 / - 重新生成文件路径
path = strings.Join(cast.ToStringSlice(utils.ArrayEmpty(strings.Split(path, "/"))), "/")
// 如果不是以 / 结尾
if !strings.HasSuffix(path, "/") { path += "/" }
return path + name + this.Params.Ext
}
// Dir - 本地存储位置 - 生成文件目录
func (this *OssClass) Dir(dir string) StorageAPI {
item := this.clone()
if item == nil {
return this
}
item.Params.Dir = StorageInst.cleanDir(dir)
return item
}
// Name - 本地存储位置 - 生成文件名
func (this *OssClass) Name(name string) StorageAPI {
item := this.clone()
if item == nil {
return this
}
item.Params.Name = name
return item
}
// Ext - 本地存储位置 - 生成文件后缀
func (this *OssClass) Ext(ext string) StorageAPI {
item := this.clone()
if item == nil {
return this
}
item.Params.Ext = StorageInst.cleanExt(ext)
return item
}
// NewStorage - 使用传入配置创建存储实例
func (this *OssClass) NewStorage(config dto.StorageConfig) StorageAPI {
return StorageInst.newWithConfig(config)
}
// ================================== 腾讯云对象存储 - 开始 ==================================
// CosClass 腾讯云对象存储
type CosClass struct {
// COS客户端
Client *cos.Client
// 配置
Config dto.StorageConfig
// 参数
Params StorageParams
}
// clone - 克隆 COS 存储实例(共享客户端,隔离链式参数)
func (this *CosClass) clone() *CosClass {
if this == nil {
return nil
}
clone := *this
return &clone
}
// Init 初始化 腾讯云对象存储
func (this *CosClass) Init() {
this.Config = StorageInst.normConfig(this.Config)
cosUrl, err := url.Parse(fmt.Sprintf("https://%s-%s.cos.%s.myqcloud.com", this.Config.COS.Bucket, this.Config.COS.AppId, this.Config.COS.Region))
if err != nil {
return
}
this.Client = cos.NewClient(&cos.BaseURL{
BucketURL: cosUrl,
}, &http.Client{
// 设置超时时间
Timeout: 100 * time.Second,
Transport: &cos.AuthorizationTransport{
SecretID: this.Config.COS.SecretId,
SecretKey: this.Config.COS.SecretKey,
},
})
}
// Object - 获取Object(对象存储)
func (this *CosClass) Object() *cos.ObjectService {
if this.Client == nil {
return nil
}
// 查询存储桶
exist, err := this.Client.Bucket.IsExist(context.Background())
if err != nil {
return nil
}
if !exist {
// 创建存储桶 - 默认公共读私有写
_, err = this.Client.Bucket.Put(context.Background(), &cos.BucketPutOptions{
XCosACL: "public-read",
})
if err != nil {
return nil
}
}
return this.Client.Object
}
// Upload - 上传文件
func (this *CosClass) Upload(reader io.Reader) (response *StorageResp) {
response = &StorageResp{}
path := this.Path()
object := this.Object()
if object == nil {
response.Error = fmt.Errorf("COS Object 获取失败")
return
}
_, err := object.Put(context.Background(), path, reader, nil)
if err != nil {
response.Error = err
return
}
if utils.Is.Empty(this.Config.COS.Domain) {
response.Domain = fmt.Sprintf("https://%s-%s.cos.%s.myqcloud.com", this.Config.COS.Bucket, this.Config.COS.AppId, this.Config.COS.Region)
} else {
response.Domain = this.Config.COS.Domain
}
response.Path = "/" + path
response.Name = StorageInst.fileNameFromPath(path)
return
}
// Path - COS存储位置 - 生成文件路径
func (this *CosClass) Path() (path string) {
// 生成文件名 - 年月日+毫秒时间戳
name := cast.ToString(time.Now().UnixNano() / 1e6)
// 存储根目录
root := this.Config.COS.Path
// 生成年月日目录 - 如:2023-04/10
dir := time.Now().Format("2006-01/02/")
// 自定义目录
if !utils.Is.Empty(this.Params.Dir) {
dir = this.Params.Dir
}
// 自定义文件名
if !utils.Is.Empty(this.Params.Name) {
name = this.Params.Name
}
// 得到文件路径 - 但是可能还存在重复的 /
path = strings.Join([]string{root, dir}, "/")
// 替换重复的 / - 重新生成文件路径
path = strings.Join(cast.ToStringSlice(utils.ArrayEmpty(strings.Split(path, "/"))), "/")
// 如果不是以 / 结尾
if !strings.HasSuffix(path, "/") { path += "/" }
return path + name + this.Params.Ext
}
// Dir - 本地存储位置 - 生成文件目录
func (this *CosClass) Dir(dir string) StorageAPI {
item := this.clone()
if item == nil {
return this
}
item.Params.Dir = StorageInst.cleanDir(dir)
return item
}
// Name - 本地存储位置 - 生成文件名
func (this *CosClass) Name(name string) StorageAPI {
item := this.clone()
if item == nil {
return this
}
item.Params.Name = name
return item
}
// Ext - 本地存储位置 - 生成文件后缀
func (this *CosClass) Ext(ext string) StorageAPI {
item := this.clone()
if item == nil {
return this
}
item.Params.Ext = StorageInst.cleanExt(ext)
return item
}
// NewStorage - 使用传入配置创建存储实例
func (this *CosClass) NewStorage(config dto.StorageConfig) StorageAPI {
return StorageInst.newWithConfig(config)
}
+8 -1
View File
@@ -7,6 +7,7 @@ require (
github.com/alibabacloud-go/dysmsapi-20170525/v5 v5.5.0
github.com/alibabacloud-go/tea v1.4.0
github.com/alibabacloud-go/tea-utils/v2 v2.0.9
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
github.com/aliyun/credentials-go v1.4.12
github.com/bwmarrin/snowflake v0.3.0
github.com/fsnotify/fsnotify v1.9.0
@@ -20,8 +21,10 @@ require (
github.com/spf13/viper v1.21.0
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.58
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.3.57
github.com/tencentyun/cos-go-sdk-v5 v0.7.72
golang.org/x/crypto v0.49.0
golang.org/x/text v0.35.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
)
require (
@@ -33,17 +36,21 @@ require (
github.com/bodgit/sevenzip v1.6.1 // indirect
github.com/bodgit/windows v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clbanning/mxj v1.8.4 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/google/go-querystring v1.0.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/klauspost/compress v1.18.4 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/mikelolasagasti/xz v1.0.1 // indirect
github.com/minio/minlz v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mozillazg/go-httpheader v0.2.1 // indirect
github.com/nwaples/rardecode/v2 v2.2.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pierrec/lz4/v4 v4.1.26 // indirect
@@ -58,7 +65,7 @@ require (
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/time v0.14.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
)
+20
View File
@@ -46,6 +46,8 @@ github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eU
github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
github.com/alibabacloud-go/tea-utils/v2 v2.0.9 h1:y6pUIlhjxbZl9ObDAcmA1H3c21eaAxADHTDQmBnAIgA=
github.com/alibabacloud-go/tea-utils/v2 v2.0.9/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
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=
@@ -69,6 +71,8 @@ github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/
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.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
@@ -90,6 +94,7 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -111,7 +116,10 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
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/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@@ -143,6 +151,8 @@ github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZz
github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc=
github.com/minio/minlz v1.1.0 h1:rUOGu3EP4EqJC5k3qCsIwEnZiJULKqtRyDdqbhlvMmQ=
github.com/minio/minlz v1.1.0/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs=
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
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=
@@ -150,6 +160,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
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/mozillazg/go-httpheader v0.2.1 h1:geV7TrjbL8KXSyvghnFm+NyTux/hxwueTSrwhe88TQQ=
github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nwaples/rardecode/v2 v2.2.2 h1:/5oL8dzYivRM/tqX9VcTSWfbpwcbwKG1QtSJr3b3KcU=
@@ -165,6 +177,7 @@ github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfS
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA=
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
@@ -195,11 +208,16 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
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.57/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.58 h1:vLowU1ND9bMmdO525NtYejec5yZxRMSO6PkgOhrCayg=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.58/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.57 h1:ZnJK+aTZYyzGN/4dmQXYWzuHsuZFrlj034uLoGaNVvQ=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.3.57/go.mod h1:jwLLFaeXXAnkWj37iTh0jfeXDYWf9eggaKJ1dRnc/1A=
github.com/tencentyun/cos-go-sdk-v5 v0.7.72 h1:k9aD8ri7Sqy2hYGYo6I2+OslDgY6IT5R0jUOHHSjW5Y=
github.com/tencentyun/cos-go-sdk-v5 v0.7.72/go.mod h1:STbTNaNKq03u+gscPEGOahKzLcGSYOj6Dzc5zNay7Pg=
github.com/tencentyun/qcloud-cos-sts-sdk v0.0.0-20250515025012-e0eec8a5d123/go.mod h1:b18KQa4IxHbxeseW1GcZox53d7J0z39VNONTxvvlkXw=
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=
@@ -326,6 +344,8 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
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=