任务一完成

This commit is contained in:
Yvan
2026-01-07 10:13:35 +08:00
parent e8f8c2819a
commit 35ab7c0b70
24 changed files with 3397 additions and 160 deletions
+96
View File
@@ -0,0 +1,96 @@
package captcha
import (
"context"
"time"
"github.com/mojocn/base64Captcha"
"github.com/redis/go-redis/v9"
)
// Config 验证码配置
type Config struct {
KeyLong int
ImgWidth int
ImgHeight int
OpenCaptcha int // 0-关闭 1-开启
OpenCaptchaTimeout int // 验证码超时时间(秒)
}
// Captcha 验证码
type Captcha struct {
config Config
store base64Captcha.Store
}
// RedisStore Redis存储
type RedisStore struct {
client redis.UniversalClient
expiration time.Duration
prefix string
}
// NewRedisStore 创建Redis存储
func NewRedisStore(client redis.UniversalClient, expiration time.Duration) *RedisStore {
return &RedisStore{
client: client,
expiration: expiration,
prefix: "captcha:",
}
}
func (s *RedisStore) Set(id string, value string) error {
return s.client.Set(context.Background(), s.prefix+id, value, s.expiration).Err()
}
func (s *RedisStore) Get(id string, clear bool) string {
ctx := context.Background()
key := s.prefix + id
val, err := s.client.Get(ctx, key).Result()
if err != nil {
return ""
}
if clear {
s.client.Del(ctx, key)
}
return val
}
func (s *RedisStore) Verify(id, answer string, clear bool) bool {
v := s.Get(id, clear)
return v == answer
}
// NewCaptcha 创建验证码实例
func NewCaptcha(config Config, store base64Captcha.Store) *Captcha {
if store == nil {
store = base64Captcha.DefaultMemStore
}
return &Captcha{
config: config,
store: store,
}
}
// Generate 生成验证码
func (c *Captcha) Generate() (id, b64s, answer string, err error) {
driver := base64Captcha.NewDriverDigit(
c.config.ImgHeight,
c.config.ImgWidth,
c.config.KeyLong,
0.7,
80,
)
cp := base64Captcha.NewCaptcha(driver, c.store)
return cp.Generate()
}
// Verify 验证验证码
func (c *Captcha) Verify(id, answer string, clear bool) bool {
return c.store.Verify(id, answer, clear)
}
// IsOpen 是否开启验证码
func (c *Captcha) IsOpen() bool {
return c.config.OpenCaptcha > 0
}
+99
View File
@@ -0,0 +1,99 @@
package casbin
import (
"sync"
"github.com/casbin/casbin/v2"
"github.com/casbin/casbin/v2/model"
gormadapter "github.com/casbin/gorm-adapter/v3"
"gorm.io/gorm"
)
var (
syncedCachedEnforcer *casbin.SyncedCachedEnforcer
once sync.Once
)
// InitCasbin 初始化Casbin
func InitCasbin(db *gorm.DB, modelPath string) (*casbin.SyncedCachedEnforcer, error) {
var err error
once.Do(func() {
adapter, adapterErr := gormadapter.NewAdapterByDB(db)
if adapterErr != nil {
err = adapterErr
return
}
var m model.Model
if modelPath != "" {
m, err = model.NewModelFromFile(modelPath)
} else {
// 默认RBAC模型
m, err = model.NewModelFromString(`
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && keyMatch2(r.obj,p.obj) && r.act == p.act
`)
}
if err != nil {
return
}
syncedCachedEnforcer, err = casbin.NewSyncedCachedEnforcer(m, adapter)
if err != nil {
return
}
syncedCachedEnforcer.SetExpireTime(60 * 60)
_ = syncedCachedEnforcer.LoadPolicy()
})
return syncedCachedEnforcer, err
}
// GetEnforcer 获取Enforcer实例
func GetEnforcer() *casbin.SyncedCachedEnforcer {
return syncedCachedEnforcer
}
// CheckPermission 检查权限
func CheckPermission(sub, obj, act string) (bool, error) {
if syncedCachedEnforcer == nil {
return false, nil
}
return syncedCachedEnforcer.Enforce(sub, obj, act)
}
// AddPolicy 添加策略
func AddPolicy(sub, obj, act string) (bool, error) {
return syncedCachedEnforcer.AddPolicy(sub, obj, act)
}
// RemovePolicy 删除策略
func RemovePolicy(sub, obj, act string) (bool, error) {
return syncedCachedEnforcer.RemovePolicy(sub, obj, act)
}
// GetPoliciesForUser 获取用户的所有策略
func GetPoliciesForUser(sub string) [][]string {
policies, _ := syncedCachedEnforcer.GetFilteredPolicy(0, sub)
return policies
}
// UpdateCasbinApi 更新API权限
func UpdateCasbinApi(oldPath, newPath, oldMethod, newMethod string) error {
_, err := syncedCachedEnforcer.UpdatePolicy(
[]string{"", oldPath, oldMethod},
[]string{"", newPath, newMethod},
)
return err
}
+88
View File
@@ -0,0 +1,88 @@
package jwt
import (
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
)
var (
ErrTokenExpired = errors.New("token已过期")
ErrTokenNotValidYet = errors.New("token尚未激活")
ErrTokenMalformed = errors.New("token格式错误")
ErrTokenInvalid = errors.New("无效的token")
)
// CustomClaims 自定义Claims
type CustomClaims struct {
UserID uint `json:"userId"`
Username string `json:"username"`
AuthorityID uint `json:"authorityId"`
BufferTime int64 `json:"bufferTime"`
jwt.RegisteredClaims
}
// JWT JWT工具
type JWT struct {
SigningKey []byte
Issuer string
ExpiresAt time.Duration
BufferTime time.Duration
}
// NewJWT 创建JWT实例
func NewJWT(signingKey, issuer string, expiresAt, bufferTime time.Duration) *JWT {
return &JWT{
SigningKey: []byte(signingKey),
Issuer: issuer,
ExpiresAt: expiresAt,
BufferTime: bufferTime,
}
}
// CreateToken 创建token
func (j *JWT) CreateToken(userID uint, username string, authorityID uint) (string, error) {
claims := CustomClaims{
UserID: userID,
Username: username,
AuthorityID: authorityID,
BufferTime: int64(j.BufferTime / time.Second),
RegisteredClaims: jwt.RegisteredClaims{
Issuer: j.Issuer,
ExpiresAt: jwt.NewNumericDate(time.Now().Add(j.ExpiresAt)),
NotBefore: jwt.NewNumericDate(time.Now().Add(-1000)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(j.SigningKey)
}
// ParseToken 解析token
func (j *JWT) ParseToken(tokenString string) (*CustomClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return j.SigningKey, nil
})
if err != nil {
switch {
case errors.Is(err, jwt.ErrTokenExpired):
return nil, ErrTokenExpired
case errors.Is(err, jwt.ErrTokenMalformed):
return nil, ErrTokenMalformed
case errors.Is(err, jwt.ErrTokenNotValidYet):
return nil, ErrTokenNotValidYet
default:
return nil, ErrTokenInvalid
}
}
if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
return claims, nil
}
return nil, ErrTokenInvalid
}
// NeedRefresh 判断是否需要刷新token
func (j *JWT) NeedRefresh(claims *CustomClaims) bool {
return claims.ExpiresAt.Time.Sub(time.Now()) < j.BufferTime
}
+81
View File
@@ -0,0 +1,81 @@
package timer
import (
"sync"
"github.com/robfig/cron/v3"
)
// Timer 定时任务管理器
type Timer struct {
cron *cron.Cron
tasks map[string]cron.EntryID
mu sync.RWMutex
}
// NewTimer 创建定时任务管理器
func NewTimer() *Timer {
return &Timer{
cron: cron.New(cron.WithSeconds()),
tasks: make(map[string]cron.EntryID),
}
}
// Start 启动定时任务
func (t *Timer) Start() {
t.cron.Start()
}
// Stop 停止定时任务
func (t *Timer) Stop() {
t.cron.Stop()
}
// AddTask 添加定时任务
func (t *Timer) AddTask(name, spec string, cmd func()) error {
t.mu.Lock()
defer t.mu.Unlock()
// 如果任务已存在,先删除
if id, ok := t.tasks[name]; ok {
t.cron.Remove(id)
}
id, err := t.cron.AddFunc(spec, cmd)
if err != nil {
return err
}
t.tasks[name] = id
return nil
}
// RemoveTask 删除定时任务
func (t *Timer) RemoveTask(name string) {
t.mu.Lock()
defer t.mu.Unlock()
if id, ok := t.tasks[name]; ok {
t.cron.Remove(id)
delete(t.tasks, name)
}
}
// HasTask 检查任务是否存在
func (t *Timer) HasTask(name string) bool {
t.mu.RLock()
defer t.mu.RUnlock()
_, ok := t.tasks[name]
return ok
}
// ListTasks 列出所有任务
func (t *Timer) ListTasks() []string {
t.mu.RLock()
defer t.mu.RUnlock()
names := make([]string, 0, len(t.tasks))
for name := range t.tasks {
names = append(names, name)
}
return names
}
+79
View File
@@ -0,0 +1,79 @@
package upload
import (
"mime/multipart"
"path"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
)
// Aliyun 阿里云OSS
type Aliyun struct {
Endpoint string
AccessKeyID string
AccessKeySecret string
BucketName string
BucketURL string
BasePath string
}
// NewAliyun 创建阿里云OSS
func NewAliyun(endpoint, accessKeyID, accessKeySecret, bucketName, bucketURL, basePath string) *Aliyun {
return &Aliyun{
Endpoint: endpoint,
AccessKeyID: accessKeyID,
AccessKeySecret: accessKeySecret,
BucketName: bucketName,
BucketURL: bucketURL,
BasePath: basePath,
}
}
// Upload 上传文件
func (a *Aliyun) Upload(file *multipart.FileHeader) (string, string, error) {
client, err := oss.New(a.Endpoint, a.AccessKeyID, a.AccessKeySecret)
if err != nil {
return "", "", err
}
bucket, err := client.Bucket(a.BucketName)
if err != nil {
return "", "", err
}
// 生成文件名和路径
fileName := GenerateFileName(file.Filename)
filePath := GeneratePath(a.BasePath)
key := path.Join(filePath, fileName)
// 打开文件
src, err := file.Open()
if err != nil {
return "", "", err
}
defer src.Close()
// 上传
if err = bucket.PutObject(key, src); err != nil {
return "", "", err
}
// 返回访问URL和key
accessURL := a.BucketURL + "/" + key
return accessURL, key, nil
}
// Delete 删除文件
func (a *Aliyun) Delete(key string) error {
client, err := oss.New(a.Endpoint, a.AccessKeyID, a.AccessKeySecret)
if err != nil {
return err
}
bucket, err := client.Bucket(a.BucketName)
if err != nil {
return err
}
return bucket.DeleteObject(key)
}
+109
View File
@@ -0,0 +1,109 @@
package upload
import (
"context"
"mime/multipart"
"path"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
// AwsS3 AWS S3存储
type AwsS3 struct {
Bucket string
Region string
Endpoint string
SecretID string
SecretKey string
BaseURL string
PathPrefix string
S3ForcePathStyle bool
DisableSSL bool
}
// NewAwsS3 创建AWS S3
func NewAwsS3(bucket, region, endpoint, secretID, secretKey, baseURL, pathPrefix string, s3ForcePathStyle, disableSSL bool) *AwsS3 {
return &AwsS3{
Bucket: bucket,
Region: region,
Endpoint: endpoint,
SecretID: secretID,
SecretKey: secretKey,
BaseURL: baseURL,
PathPrefix: pathPrefix,
S3ForcePathStyle: s3ForcePathStyle,
DisableSSL: disableSSL,
}
}
func (a *AwsS3) getClient(ctx context.Context) (*s3.Client, error) {
cfg, err := config.LoadDefaultConfig(ctx,
config.WithRegion(a.Region),
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(a.SecretID, a.SecretKey, "")),
)
if err != nil {
return nil, err
}
client := s3.NewFromConfig(cfg, func(o *s3.Options) {
if a.Endpoint != "" {
o.BaseEndpoint = aws.String(a.Endpoint)
}
o.UsePathStyle = a.S3ForcePathStyle
})
return client, nil
}
// Upload 上传文件
func (a *AwsS3) Upload(file *multipart.FileHeader) (string, string, error) {
ctx := context.Background()
client, err := a.getClient(ctx)
if err != nil {
return "", "", err
}
// 生成文件名和路径
fileName := GenerateFileName(file.Filename)
filePath := GeneratePath(a.PathPrefix)
key := path.Join(filePath, fileName)
// 打开文件
src, err := file.Open()
if err != nil {
return "", "", err
}
defer src.Close()
// 上传
contentType := file.Header.Get("Content-Type")
_, err = client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(a.Bucket),
Key: aws.String(key),
Body: src,
ContentType: aws.String(contentType),
})
if err != nil {
return "", "", err
}
// 返回访问URL和key
accessURL := a.BaseURL + "/" + key
return accessURL, key, nil
}
// Delete 删除文件
func (a *AwsS3) Delete(key string) error {
ctx := context.Background()
client, err := a.getClient(ctx)
if err != nil {
return err
}
_, err = client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: aws.String(a.Bucket),
Key: aws.String(key),
})
return err
}
+103
View File
@@ -0,0 +1,103 @@
package upload
import (
"context"
"fmt"
"mime/multipart"
"path"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
// CloudflareR2 Cloudflare R2存储
type CloudflareR2 struct {
Bucket string
BaseURL string
Path string
AccountID string
AccessKeyID string
SecretAccessKey string
}
// NewCloudflareR2 创建Cloudflare R2
func NewCloudflareR2(bucket, baseURL, r2Path, accountID, accessKeyID, secretAccessKey string) *CloudflareR2 {
return &CloudflareR2{
Bucket: bucket,
BaseURL: baseURL,
Path: r2Path,
AccountID: accountID,
AccessKeyID: accessKeyID,
SecretAccessKey: secretAccessKey,
}
}
func (c *CloudflareR2) getClient(ctx context.Context) (*s3.Client, error) {
endpoint := fmt.Sprintf("https://%s.r2.cloudflarestorage.com", c.AccountID)
cfg, err := config.LoadDefaultConfig(ctx,
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(c.AccessKeyID, c.SecretAccessKey, "")),
config.WithRegion("auto"),
)
if err != nil {
return nil, err
}
client := s3.NewFromConfig(cfg, func(o *s3.Options) {
o.BaseEndpoint = aws.String(endpoint)
})
return client, nil
}
// Upload 上传文件
func (c *CloudflareR2) Upload(file *multipart.FileHeader) (string, string, error) {
ctx := context.Background()
client, err := c.getClient(ctx)
if err != nil {
return "", "", err
}
// 生成文件名和路径
fileName := GenerateFileName(file.Filename)
filePath := GeneratePath(c.Path)
key := path.Join(filePath, fileName)
// 打开文件
src, err := file.Open()
if err != nil {
return "", "", err
}
defer src.Close()
// 上传
contentType := file.Header.Get("Content-Type")
_, err = client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(c.Bucket),
Key: aws.String(key),
Body: src,
ContentType: aws.String(contentType),
})
if err != nil {
return "", "", err
}
// 返回访问URL和key
accessURL := c.BaseURL + "/" + key
return accessURL, key, nil
}
// Delete 删除文件
func (c *CloudflareR2) Delete(key string) error {
ctx := context.Background()
client, err := c.getClient(ctx)
if err != nil {
return err
}
_, err = client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: aws.String(c.Bucket),
Key: aws.String(key),
})
return err
}
+82
View File
@@ -0,0 +1,82 @@
package upload
import (
"mime/multipart"
"path"
"github.com/huaweicloud/huaweicloud-sdk-go-obs/obs"
)
// HuaweiObs 华为云OBS
type HuaweiObs struct {
Path string
Bucket string
Endpoint string
AccessKey string
SecretKey string
}
// NewHuaweiObs 创建华为云OBS
func NewHuaweiObs(obsPath, bucket, endpoint, accessKey, secretKey string) *HuaweiObs {
return &HuaweiObs{
Path: obsPath,
Bucket: bucket,
Endpoint: endpoint,
AccessKey: accessKey,
SecretKey: secretKey,
}
}
func (h *HuaweiObs) getClient() (*obs.ObsClient, error) {
return obs.New(h.AccessKey, h.SecretKey, h.Endpoint)
}
// Upload 上传文件
func (h *HuaweiObs) Upload(file *multipart.FileHeader) (string, string, error) {
client, err := h.getClient()
if err != nil {
return "", "", err
}
defer client.Close()
// 生成文件名和路径
fileName := GenerateFileName(file.Filename)
filePath := GeneratePath(h.Path)
key := path.Join(filePath, fileName)
// 打开文件
src, err := file.Open()
if err != nil {
return "", "", err
}
defer src.Close()
// 上传
input := &obs.PutObjectInput{}
input.Bucket = h.Bucket
input.Key = key
input.Body = src
_, err = client.PutObject(input)
if err != nil {
return "", "", err
}
// 返回访问URL和key
accessURL := "https://" + h.Bucket + "." + h.Endpoint + "/" + key
return accessURL, key, nil
}
// Delete 删除文件
func (h *HuaweiObs) Delete(key string) error {
client, err := h.getClient()
if err != nil {
return err
}
defer client.Close()
input := &obs.DeleteObjectInput{}
input.Bucket = h.Bucket
input.Key = key
_, err = client.DeleteObject(input)
return err
}
+66
View File
@@ -0,0 +1,66 @@
package upload
import (
"io"
"mime/multipart"
"os"
"path"
)
// Local 本地存储
type Local struct {
StorePath string
BasePath string
}
// NewLocal 创建本地存储
func NewLocal(storePath, basePath string) *Local {
return &Local{
StorePath: storePath,
BasePath: basePath,
}
}
// Upload 上传文件
func (l *Local) Upload(file *multipart.FileHeader) (string, string, error) {
// 生成文件名和路径
fileName := GenerateFileName(file.Filename)
filePath := GeneratePath(l.BasePath)
fullPath := path.Join(l.StorePath, filePath)
// 创建目录
if err := os.MkdirAll(fullPath, os.ModePerm); err != nil {
return "", "", err
}
// 打开源文件
src, err := file.Open()
if err != nil {
return "", "", err
}
defer src.Close()
// 创建目标文件
dstPath := path.Join(fullPath, fileName)
dst, err := os.Create(dstPath)
if err != nil {
return "", "", err
}
defer dst.Close()
// 复制文件
if _, err = io.Copy(dst, src); err != nil {
return "", "", err
}
// 返回访问路径和存储key
accessPath := path.Join("/", filePath, fileName)
key := path.Join(filePath, fileName)
return accessPath, key, nil
}
// Delete 删除文件
func (l *Local) Delete(key string) error {
fullPath := path.Join(l.StorePath, key)
return os.Remove(fullPath)
}
+84
View File
@@ -0,0 +1,84 @@
package upload
import (
"context"
"mime/multipart"
"path"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
// Minio MinIO存储
type Minio struct {
ID string
Secret string
Bucket string
Endpoint string
BasePath string
UseSSL bool
}
// NewMinio 创建MinIO
func NewMinio(id, secret, bucket, endpoint, basePath string, useSSL bool) *Minio {
return &Minio{
ID: id,
Secret: secret,
Bucket: bucket,
Endpoint: endpoint,
BasePath: basePath,
UseSSL: useSSL,
}
}
func (m *Minio) getClient() (*minio.Client, error) {
return minio.New(m.Endpoint, &minio.Options{
Creds: credentials.NewStaticV4(m.ID, m.Secret, ""),
Secure: m.UseSSL,
})
}
// Upload 上传文件
func (m *Minio) Upload(file *multipart.FileHeader) (string, string, error) {
client, err := m.getClient()
if err != nil {
return "", "", err
}
// 生成文件名和路径
fileName := GenerateFileName(file.Filename)
filePath := GeneratePath(m.BasePath)
key := path.Join(filePath, fileName)
// 打开文件
src, err := file.Open()
if err != nil {
return "", "", err
}
defer src.Close()
// 上传
_, err = client.PutObject(context.Background(), m.Bucket, key, src, file.Size, minio.PutObjectOptions{
ContentType: file.Header.Get("Content-Type"),
})
if err != nil {
return "", "", err
}
// 返回访问URL和key
protocol := "http"
if m.UseSSL {
protocol = "https"
}
accessURL := protocol + "://" + m.Endpoint + "/" + m.Bucket + "/" + key
return accessURL, key, nil
}
// Delete 删除文件
func (m *Minio) Delete(key string) error {
client, err := m.getClient()
if err != nil {
return err
}
return client.RemoveObject(context.Background(), m.Bucket, key, minio.RemoveObjectOptions{})
}
+96
View File
@@ -0,0 +1,96 @@
package upload
import (
"context"
"mime/multipart"
"path"
"github.com/qiniu/go-sdk/v7/auth/qbox"
"github.com/qiniu/go-sdk/v7/storage"
)
// Qiniu 七牛云
type Qiniu struct {
Zone string
Bucket string
ImgPath string
AccessKey string
SecretKey string
UseHTTPS bool
UseCdnDomains bool
}
// NewQiniu 创建七牛云
func NewQiniu(zone, bucket, imgPath, accessKey, secretKey string, useHTTPS, useCdnDomains bool) *Qiniu {
return &Qiniu{
Zone: zone,
Bucket: bucket,
ImgPath: imgPath,
AccessKey: accessKey,
SecretKey: secretKey,
UseHTTPS: useHTTPS,
UseCdnDomains: useCdnDomains,
}
}
func (q *Qiniu) getZone() *storage.Region {
switch q.Zone {
case "ZoneHuadong":
return &storage.ZoneHuadong
case "ZoneHuabei":
return &storage.ZoneHuabei
case "ZoneHuanan":
return &storage.ZoneHuanan
case "ZoneBeimei":
return &storage.ZoneBeimei
case "ZoneXinjiapo":
return &storage.ZoneXinjiapo
default:
return &storage.ZoneHuadong
}
}
// Upload 上传文件
func (q *Qiniu) Upload(file *multipart.FileHeader) (string, string, error) {
mac := qbox.NewMac(q.AccessKey, q.SecretKey)
putPolicy := storage.PutPolicy{Scope: q.Bucket}
upToken := putPolicy.UploadToken(mac)
cfg := storage.Config{
Zone: q.getZone(),
UseHTTPS: q.UseHTTPS,
UseCdnDomains: q.UseCdnDomains,
}
formUploader := storage.NewFormUploader(&cfg)
// 生成文件名和路径
fileName := GenerateFileName(file.Filename)
filePath := GeneratePath("")
key := path.Join(filePath, fileName)
// 打开文件
src, err := file.Open()
if err != nil {
return "", "", err
}
defer src.Close()
// 上传
ret := storage.PutRet{}
err = formUploader.Put(context.Background(), &ret, upToken, key, src, file.Size, nil)
if err != nil {
return "", "", err
}
// 返回访问URL和key
accessURL := q.ImgPath + "/" + key
return accessURL, key, nil
}
// Delete 删除文件
func (q *Qiniu) Delete(key string) error {
mac := qbox.NewMac(q.AccessKey, q.SecretKey)
cfg := storage.Config{Zone: q.getZone()}
bucketManager := storage.NewBucketManager(mac, &cfg)
return bucketManager.Delete(q.Bucket, key)
}
+79
View File
@@ -0,0 +1,79 @@
package upload
import (
"context"
"fmt"
"mime/multipart"
"net/http"
"net/url"
"path"
"github.com/tencentyun/cos-go-sdk-v5"
)
// Tencent 腾讯云COS
type Tencent struct {
Bucket string
Region string
SecretID string
SecretKey string
BaseURL string
PathPrefix string
}
// NewTencent 创建腾讯云COS
func NewTencent(bucket, region, secretID, secretKey, baseURL, pathPrefix string) *Tencent {
return &Tencent{
Bucket: bucket,
Region: region,
SecretID: secretID,
SecretKey: secretKey,
BaseURL: baseURL,
PathPrefix: pathPrefix,
}
}
func (t *Tencent) getClient() *cos.Client {
u, _ := url.Parse(fmt.Sprintf("https://%s.cos.%s.myqcloud.com", t.Bucket, t.Region))
b := &cos.BaseURL{BucketURL: u}
return cos.NewClient(b, &http.Client{
Transport: &cos.AuthorizationTransport{
SecretID: t.SecretID,
SecretKey: t.SecretKey,
},
})
}
// Upload 上传文件
func (t *Tencent) Upload(file *multipart.FileHeader) (string, string, error) {
client := t.getClient()
// 生成文件名和路径
fileName := GenerateFileName(file.Filename)
filePath := GeneratePath(t.PathPrefix)
key := path.Join(filePath, fileName)
// 打开文件
src, err := file.Open()
if err != nil {
return "", "", err
}
defer src.Close()
// 上传
_, err = client.Object.Put(context.Background(), key, src, nil)
if err != nil {
return "", "", err
}
// 返回访问URL和key
accessURL := t.BaseURL + "/" + key
return accessURL, key, nil
}
// Delete 删除文件
func (t *Tencent) Delete(key string) error {
client := t.getClient()
_, err := client.Object.Delete(context.Background(), key)
return err
}
+71
View File
@@ -0,0 +1,71 @@
package upload
import (
"errors"
"mime/multipart"
"path"
"strings"
"time"
"github.com/google/uuid"
)
var (
ErrFileEmpty = errors.New("文件为空")
ErrFileTooLarge = errors.New("文件过大")
ErrFileTypeError = errors.New("文件类型不允许")
)
// OSS 对象存储接口
type OSS interface {
Upload(file *multipart.FileHeader) (string, string, error)
Delete(key string) error
}
// Config 上传配置
type Config struct {
Type string // local, aliyun, tencent, qiniu, minio, aws-s3, huawei-obs, cloudflare-r2
Path string
MaxSize int64
}
// GenerateFileName 生成文件名
func GenerateFileName(originalName string) string {
ext := path.Ext(originalName)
name := uuid.New().String()
return name + ext
}
// GeneratePath 生成存储路径
func GeneratePath(basePath string) string {
now := time.Now()
return path.Join(basePath, now.Format("2006/01/02"))
}
// CheckFileSize 检查文件大小
func CheckFileSize(size int64, maxSize int64) error {
if size > maxSize {
return ErrFileTooLarge
}
return nil
}
// CheckFileType 检查文件类型
func CheckFileType(filename string, allowTypes []string) error {
ext := strings.ToLower(path.Ext(filename))
for _, t := range allowTypes {
if ext == t || t == "*" {
return nil
}
}
return ErrFileTypeError
}
// DefaultAllowTypes 默认允许的文件类型
var DefaultAllowTypes = []string{
".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp",
".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx",
".txt", ".md", ".json", ".xml", ".csv",
".zip", ".rar", ".7z", ".tar", ".gz",
".mp3", ".mp4", ".avi", ".mov", ".wmv",
}
+91
View File
@@ -0,0 +1,91 @@
package utils
import (
"errors"
"os"
"strconv"
"strings"
)
const (
breakpointDir = "./breakpointDir/"
finishDir = "./fileDir/"
)
// BreakPointContinue 断点续传
// content: 文件片段内容
// fileName: 文件名
// contentNumber: 当前片段序号
// contentTotal: 总片段数
// fileMd5: 文件MD5
func BreakPointContinue(content []byte, fileName string, contentNumber int, contentTotal int, fileMd5 string) (string, error) {
path := breakpointDir + fileMd5 + "/"
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
return path, err
}
pathC, err := makeFileContent(content, fileName, path, contentNumber)
return pathC, err
}
// CheckMd5 检查Md5
func CheckMd5(content []byte, chunkMd5 string) bool {
fileMd5 := MD5V(content)
return fileMd5 == chunkMd5
}
// makeFileContent 创建切片内容
func makeFileContent(content []byte, fileName string, FileDir string, contentNumber int) (string, error) {
if strings.Contains(fileName, "..") || strings.Contains(FileDir, "..") {
return "", errors.New("文件名或路径不合法")
}
path := FileDir + fileName + "_" + strconv.Itoa(contentNumber)
f, err := os.Create(path)
if err != nil {
return path, err
}
defer f.Close()
_, err = f.Write(content)
if err != nil {
return path, err
}
return path, nil
}
// MakeFile 合并切片文件
func MakeFile(fileName string, FileMd5 string) (string, error) {
rd, err := os.ReadDir(breakpointDir + FileMd5)
if err != nil {
return finishDir + fileName, err
}
_ = os.MkdirAll(finishDir, os.ModePerm)
fd, err := os.OpenFile(finishDir+fileName, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o644)
if err != nil {
return finishDir + fileName, err
}
defer fd.Close()
for k := range rd {
content, _ := os.ReadFile(breakpointDir + FileMd5 + "/" + fileName + "_" + strconv.Itoa(k))
_, err = fd.Write(content)
if err != nil {
_ = os.Remove(finishDir + fileName)
return finishDir + fileName, err
}
}
return finishDir + fileName, nil
}
// RemoveChunk 移除切片
func RemoveChunk(FileMd5 string) error {
return os.RemoveAll(breakpointDir + FileMd5)
}
// GetBreakpointDir 获取断点续传目录
func GetBreakpointDir() string {
return breakpointDir
}
// GetFinishDir 获取完成文件目录
func GetFinishDir() string {
return finishDir
}
+82
View File
@@ -0,0 +1,82 @@
package utils
import (
"bytes"
"fmt"
"mime/multipart"
"strconv"
"github.com/xuri/excelize/v2"
)
// GetColumnName 获取Excel列名 (1->A, 2->B, ..., 27->AA)
func GetColumnName(n int) string {
columnName := ""
for n > 0 {
n--
columnName = string(rune('A'+n%26)) + columnName
n /= 26
}
return columnName
}
// CreateExcelFile 创建Excel文件
func CreateExcelFile() *excelize.File {
return excelize.NewFile()
}
// OpenExcelFile 打开Excel文件
func OpenExcelFile(file *multipart.FileHeader) (*excelize.File, error) {
src, err := file.Open()
if err != nil {
return nil, err
}
defer src.Close()
return excelize.OpenReader(src)
}
// WriteExcelRows 写入Excel行数据
func WriteExcelRows(f *excelize.File, sheetName string, rows [][]interface{}) error {
for i, row := range rows {
for j, cell := range row {
cellName := fmt.Sprintf("%s%d", GetColumnName(j+1), i+1)
if err := f.SetCellValue(sheetName, cellName, cell); err != nil {
return err
}
}
}
return nil
}
// WriteExcelRow 写入单行数据
func WriteExcelRow(f *excelize.File, sheetName string, rowIndex int, row []interface{}) error {
for j, cell := range row {
cellName := fmt.Sprintf("%s%d", GetColumnName(j+1), rowIndex)
if err := f.SetCellValue(sheetName, cellName, cell); err != nil {
return err
}
}
return nil
}
// ExcelToBuffer 将Excel文件写入Buffer
func ExcelToBuffer(f *excelize.File) (*bytes.Buffer, error) {
return f.WriteToBuffer()
}
// ReadExcelRows 读取Excel所有行
func ReadExcelRows(f *excelize.File, sheetName string) ([][]string, error) {
return f.GetRows(sheetName)
}
// SetCellValueAuto 自动类型设置单元格值
func SetCellValueAuto(f *excelize.File, sheetName, cell string, value string) error {
// 尝试解析为数字
if v, err := strconv.ParseFloat(value, 64); err == nil {
return f.SetCellValue(sheetName, cell, v)
}
if v, err := strconv.ParseInt(value, 10, 64); err == nil {
return f.SetCellValue(sheetName, cell, v)
}
return f.SetCellValue(sheetName, cell, value)
}
+18
View File
@@ -0,0 +1,18 @@
package utils
import (
"crypto/md5"
"encoding/hex"
)
// MD5V 计算字节数组的MD5值
func MD5V(data []byte) string {
h := md5.New()
h.Write(data)
return hex.EncodeToString(h.Sum(nil))
}
// MD5String 计算字符串的MD5值
func MD5String(s string) string {
return MD5V([]byte(s))
}
+107
View File
@@ -0,0 +1,107 @@
package utils
import (
"archive/zip"
"io"
"os"
"path/filepath"
"strings"
)
// ZipFiles 压缩文件
func ZipFiles(filename string, files []string, oldForm, newForm string) error {
newZipFile, err := os.Create(filename)
if err != nil {
return err
}
defer newZipFile.Close()
zipWriter := zip.NewWriter(newZipFile)
defer zipWriter.Close()
for _, file := range files {
if err = addFileToZip(zipWriter, file, oldForm, newForm); err != nil {
return err
}
}
return nil
}
func addFileToZip(zipWriter *zip.Writer, filename, oldForm, newForm string) error {
fileToZip, err := os.Open(filename)
if err != nil {
return err
}
defer fileToZip.Close()
info, err := fileToZip.Stat()
if err != nil {
return err
}
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
header.Name = strings.Replace(filename, oldForm, newForm, 1)
header.Method = zip.Deflate
writer, err := zipWriter.CreateHeader(header)
if err != nil {
return err
}
_, err = io.Copy(writer, fileToZip)
return err
}
// Unzip 解压文件
func Unzip(src, dest string) ([]string, error) {
var filenames []string
r, err := zip.OpenReader(src)
if err != nil {
return filenames, err
}
defer r.Close()
for _, f := range r.File {
fpath := filepath.Join(dest, f.Name)
// 安全检查:防止路径遍历攻击
if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
continue
}
filenames = append(filenames, fpath)
if f.FileInfo().IsDir() {
os.MkdirAll(fpath, os.ModePerm)
continue
}
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
return filenames, err
}
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return filenames, err
}
rc, err := f.Open()
if err != nil {
outFile.Close()
return filenames, err
}
_, err = io.Copy(outFile, rc)
outFile.Close()
rc.Close()
if err != nil {
return filenames, err
}
}
return filenames, nil
}