Files
zhengchiliu ff9add8655 补齐 HTTP 层:实现用户模块、路由与中间件接线、COS 上传及优雅启动
- 新增用户与上传错误码文件及 Recovery 中间件
- 修复 router.go 导入与中间件链,注册用户路由并补 handler 错误码导入
- 补充用户状态常量与 UserGetByWechatOpenID 查询,修订刷新令牌 dto
- 移除 upload 包 OTel 调用,新增 FileUploadCos 上传封装
- 实现 UserService 微信登录/刷新令牌/我的信息/改资料/上传头像
- 裁剪 main.go 配置段并移除 OTel 桥接,补启动与优雅退出
- 新增 configs 示例配置、global 配置结构及 go.mod 依赖
2026-09-27 15:02:01 +08:00

187 lines
4.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package upload 文件上传、校验、落盘、公开 token 生成的工具函数。
package upload
import (
"context"
"crypto/md5"
"crypto/rand"
"encoding/hex"
"fmt"
"io"
"mime/multipart"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"time"
"git.lzcwy.cn/zhengchiliu/zt-online-game-backend/global"
)
func FileGetExt(name string) string {
return strings.ToLower(path.Ext(name))
}
// 校验扩展名是否在白名单内。
func FileCheckAllowExt(ext string, allowed []string) bool {
ext = strings.TrimPrefix(strings.ToLower(ext), ".")
for _, e := range allowed {
if strings.ToLower(e) == ext {
return true
}
}
return false
}
// 校验文件大小。maxMB <= 0 表示不限制。
func FileCheckMaxSize(size, maxMB int64) bool {
if maxMB <= 0 {
return true
}
return size <= maxMB*1024*1024
}
// FileCheckImageMagic 校验文件头 magic bytes,判断内容是否为真实图片(jpeg/png/gif/webp)。
// 用于防止伪造扩展名上传非图片文件(如脚本、可执行文件、HTML)。
func FileCheckImageMagic(data []byte) bool {
if len(data) < 4 {
return false
}
// JPEG: FF D8 FF
if data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF {
return true
}
// PNG: 89 50 4E 47 0D 0A 1A 0A
if len(data) >= 8 &&
data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47 &&
data[4] == 0x0D && data[5] == 0x0A && data[6] == 0x1A && data[7] == 0x0A {
return true
}
// GIF: "GIF8"
if data[0] == 'G' && data[1] == 'I' && data[2] == 'F' && data[3] == '8' {
return true
}
// WebP: "RIFF" .... "WEBP"
if len(data) >= 12 &&
data[0] == 'R' && data[1] == 'I' && data[2] == 'F' && data[3] == 'F' &&
data[8] == 'W' && data[9] == 'E' && data[10] == 'B' && data[11] == 'P' {
return true
}
return false
}
// GenFileName 生成防冲突随机相对文件名。
func FileGenName(originName string) (string, error) {
ext := FileGetExt(originName)
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
return "", err
}
return hex.EncodeToString(b) + ext, nil
}
// 临时保存从前端获取的图片
func FileSaveTmp(ctx context.Context, file *multipart.FileHeader, fileName string, userID int64) (string, error) {
dir := filepath.Join(global.UploadSetting.TmpSavePath, fmt.Sprint(userID))
if err := os.MkdirAll(dir, 0o755); err != nil {
return "", err
}
tmpFile := filepath.Join(dir, fileName)
dst, err := os.Create(tmpFile)
if err != nil {
return "", err
}
defer dst.Close()
src, err := file.Open()
if err != nil {
return "", err
}
defer src.Close()
if _, err := io.Copy(dst, src); err != nil {
return "", err
}
return tmpFile, nil
}
// FileUploadCos 将本地文件上传至 COS。
// key 为对象键(即写入库中的存储值),localPath 为本地文件路径。
// key 或 localPath 为空、client 未初始化均返回 error,由调用方映射业务错误码。
func FileUploadCos(ctx context.Context, key, localPath string) error {
if key == "" || localPath == "" {
return fmt.Errorf("upload cos: empty key or local path")
}
// COS 客户端初始化失败时降级为 nil,此处做保护避免 nil 指针 panic。
if global.CosClient == nil {
return fmt.Errorf("cos client not initialized")
}
if _, _, err := global.CosClient.Object.Upload(ctx, key, localPath, nil); err != nil {
return err
}
return nil
}
// 删除 COS 对象。key 为对象键(即 processed_image 存储值)。
// key 为空直接返回 nil;client 未初始化返回 error。
// 删除为尽力而为的清理操作,内部使用短超时避免网络异常长时间阻塞调用方。
func FileDeleteCos(ctx context.Context, key string) error {
if key == "" {
return nil
}
// COS 客户端初始化失败时降级为 nil,此处做保护避免 nil 指针 panic。
if global.CosClient == nil {
return fmt.Errorf("cos client not initialized")
}
delCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
if _, err := global.CosClient.Object.Delete(delCtx, key); err != nil {
return err
}
return nil
}
// 获取cos访问url
func GetCosUrl(fileName string) string {
ts := time.Now().Unix()
raw := global.CosSetting.CdnDownloadSecretKey + "/" + fileName + strconv.FormatInt(ts, 10)
sign := fmt.Sprintf("%x", md5.Sum([]byte(raw)))
return fmt.Sprintf("https://%s/%s?sign=%s&t=%d",
global.CosSetting.CosDownloadDomain,
fileName,
sign,
ts,
)
}
// AvatarURL 将头像存储值转为可访问 URL:
// 历史通过修改资料接口传入的 http(s) URL 直接透传;COS key 统一转为 CDN 签名 URL。
func AvatarURL(raw string) string {
if raw == "" {
return ""
}
if strings.HasPrefix(raw, "http://") || strings.HasPrefix(raw, "https://") {
return raw
}
return GetCosUrl(raw)
}