Files
tyc-server-v2/app/main/api/internal/logic/upload/uploadimagelogic.go

139 lines
3.6 KiB
Go
Raw Normal View History

2026-02-12 15:16:54 +08:00
package upload
import (
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"time"
"tyc-server/app/main/api/internal/svc"
"tyc-server/app/main/api/internal/types"
"tyc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
)
const maxImageSize = 3 * 1024 * 1024 // 3MB
const defaultTempFileMaxAgeH = 24
type UploadImageLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewUploadImageLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UploadImageLogic {
return &UploadImageLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *UploadImageLogic) UploadImage(req *types.UploadImageReq) (resp *types.UploadImageResp, err error) {
decoded, decErr := base64.StdEncoding.DecodeString(req.ImageBase64)
if decErr != nil {
return nil, errors.Wrapf(xerr.NewErrMsg("图片 base64 格式错误"), "%v", decErr)
}
if len(decoded) > maxImageSize {
return nil, errors.Wrapf(xerr.NewErrMsg("图片不能超过 3M"), "size=%d", len(decoded))
}
// 按文件内容 hash 命名,相同文件复用同一 URL避免重复传输与刷流量
hashSum := sha256.Sum256(decoded)
hashHex := hex.EncodeToString(hashSum[:])
fileName := hashHex + ".jpg"
dir := l.uploadStoragePath()
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "创建上传目录失败: %v", err)
}
filePath := filepath.Join(dir, fileName)
// 若已存在同 hash 文件,直接返回 URL不重复写入
if _, statErr := os.Stat(filePath); statErr == nil {
url := l.buildURL(fileName)
logx.Infof("upload image dedup by hash, file=%s", fileName)
return &types.UploadImageResp{Url: url}, nil
}
if err := os.WriteFile(filePath, decoded, 0644); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "保存图片失败: %v", err)
}
// 异步清理过期临时文件,不阻塞响应
go l.deleteOldUploads(dir)
url := l.buildURL(fileName)
logx.Infof("upload image ok, file=%s", fileName)
return &types.UploadImageResp{Url: url}, nil
}
func (l *UploadImageLogic) buildURL(fileName string) string {
baseURL := l.svcCtx.Config.Upload.FileBaseURL
if baseURL == "" {
baseURL = l.svcCtx.Config.AdminPromotion.URLDomain
if baseURL != "" {
baseURL = baseURL + "/api/v1/upload/file"
}
}
if baseURL == "" {
return ""
}
return fmt.Sprintf("%s/%s", baseURL, fileName)
}
func (l *UploadImageLogic) uploadStoragePath() string {
candidates := []string{
"data/uploads",
"../data/uploads",
"../../data/uploads",
"../../../data/uploads",
}
for _, c := range candidates {
abs, _ := filepath.Abs(c)
if err := os.MkdirAll(abs, 0755); err == nil {
return abs
}
}
abs, _ := filepath.Abs(candidates[0])
return abs
}
// deleteOldUploads 删除目录下超过保留时长的临时文件
func (l *UploadImageLogic) deleteOldUploads(dir string) {
maxAgeH := l.svcCtx.Config.Upload.TempFileMaxAgeH
if maxAgeH <= 0 {
maxAgeH = defaultTempFileMaxAgeH
}
cutoff := time.Now().Add(-time.Duration(maxAgeH) * time.Hour)
entries, err := os.ReadDir(dir)
if err != nil {
l.Errorf("deleteOldUploads ReadDir: %v", err)
return
}
for _, e := range entries {
if e.IsDir() {
continue
}
path := filepath.Join(dir, e.Name())
info, err := os.Stat(path)
if err != nil {
continue
}
if info.ModTime().Before(cutoff) {
if err := os.Remove(path); err != nil {
l.Errorf("deleteOldUploads Remove %s: %v", path, err)
} else {
l.Infof("deleteOldUploads removed %s", path)
}
}
}
}