Files
nl-blogs/server/handlers/attachment.go

1248 lines
34 KiB
Go
Raw Normal View History

2026-01-19 16:14:08 +08:00
package handlers
import (
2026-07-14 10:05:33 +08:00
"fmt"
2026-01-19 16:14:08 +08:00
"log"
2026-07-14 10:05:33 +08:00
"path/filepath"
2026-01-19 16:14:08 +08:00
"strconv"
2026-07-30 14:11:26 +08:00
"strings"
2026-01-19 16:14:08 +08:00
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/models"
"github.com/niangaodev/art-code/repositories"
"github.com/niangaodev/art-code/utils"
)
// AdminUploadAttachment 上传附件
func AdminUploadAttachment(c *gin.Context) {
// 获取文件
file, err := c.FormFile("file")
if err != nil {
utils.Error(c, 400, "No file uploaded")
return
}
// 获取分类ID(可选)
categoryIDStr := c.PostForm("categoryId")
var categoryID *uint
if categoryIDStr != "" {
if id, err := strconv.ParseUint(categoryIDStr, 10, 32); err == nil {
idUint := uint(id)
categoryID = &idUint
}
}
2026-07-30 14:11:26 +08:00
// 存储类型解析:客户端显式传入 > 数据库启用配置 > Aliyun Env > local
storageType := strings.TrimSpace(c.PostForm("storageType"))
var ossConfig *models.OSSConfig
usedEnvConfig := false
if storageType == "" {
if cfg, cfgErr := repositories.GetActiveOSSConfig(""); cfgErr == nil && cfg != nil {
storageType = cfg.StorageType
ossConfig = cfg // 复用,避免下方再查
} else if utils.HasAliyunEnvConfig() {
storageType = "aliyun"
} else {
storageType = "local"
}
2026-07-14 10:05:33 +08:00
}
2026-01-19 16:14:08 +08:00
// 打开文件
src, err := file.Open()
if err != nil {
utils.Error(c, 500, "Failed to open file")
return
}
defer src.Close()
2026-07-14 10:05:33 +08:00
// 创建OSS配置对象
config := &utils.OSSConfig{
StorageType: storageType,
}
2026-07-30 14:11:26 +08:00
// 仅在无库配置时才用 Env 阿里云(库配置优先)
if storageType == "aliyun" && ossConfig == nil {
2026-07-14 10:05:33 +08:00
if envCfg, ok := utils.LoadAliyunConfigFromEnv(); ok {
config = envCfg
usedEnvConfig = true
}
}
if !usedEnvConfig && storageType != "local" {
2026-07-30 14:11:26 +08:00
if ossConfig == nil {
ossConfig, err = repositories.GetActiveOSSConfig(storageType)
if err != nil || ossConfig == nil {
utils.Error(c, 400, "OSS config not found or not active")
return
}
2026-01-19 16:14:08 +08:00
}
2026-01-19 20:21:09 +08:00
// 根据存储类型从专用字段或通用字段读取配置
var accessKey, secretKey, bucket, region, domain string
switch ossConfig.StorageType {
case "aliyun":
if ossConfig.OSSAccessKeyID != "" {
decrypted, err := utils.DecryptAES(ossConfig.OSSAccessKeyID)
if err == nil {
accessKey = decrypted
}
}
if ossConfig.OSSAccessKeySecret != "" {
decrypted, err := utils.DecryptAES(ossConfig.OSSAccessKeySecret)
if err == nil {
secretKey = decrypted
}
}
bucket = ossConfig.OSSBucket
domain = ossConfig.OSSDomain
if accessKey == "" && ossConfig.AccessKey != "" {
decrypted, err := utils.DecryptAES(ossConfig.AccessKey)
if err == nil {
accessKey = decrypted
}
}
if secretKey == "" && ossConfig.SecretKey != "" {
decrypted, err := utils.DecryptAES(ossConfig.SecretKey)
if err == nil {
secretKey = decrypted
}
}
if bucket == "" {
bucket = ossConfig.Bucket
}
if domain == "" {
domain = ossConfig.Domain
}
case "qcloud":
if ossConfig.QCloudSecretID != "" {
decrypted, err := utils.DecryptAES(ossConfig.QCloudSecretID)
if err == nil {
accessKey = decrypted
}
}
if ossConfig.QCloudSecretKey != "" {
decrypted, err := utils.DecryptAES(ossConfig.QCloudSecretKey)
if err == nil {
secretKey = decrypted
}
}
bucket = ossConfig.QCloudBucket
region = ossConfig.QCloudRegion
domain = ossConfig.QCloudDomain
if accessKey == "" && ossConfig.AccessKey != "" {
decrypted, err := utils.DecryptAES(ossConfig.AccessKey)
if err == nil {
accessKey = decrypted
}
}
if secretKey == "" && ossConfig.SecretKey != "" {
decrypted, err := utils.DecryptAES(ossConfig.SecretKey)
if err == nil {
secretKey = decrypted
}
}
if bucket == "" {
bucket = ossConfig.Bucket
}
if region == "" {
region = ossConfig.Region
}
if domain == "" {
domain = ossConfig.Domain
}
case "qiniu":
if ossConfig.QiniuAccessKey != "" {
decrypted, err := utils.DecryptAES(ossConfig.QiniuAccessKey)
if err == nil {
accessKey = decrypted
}
}
if ossConfig.QiniuSecretKey != "" {
decrypted, err := utils.DecryptAES(ossConfig.QiniuSecretKey)
if err == nil {
secretKey = decrypted
}
}
bucket = ossConfig.QiniuBucket
region = ossConfig.QiniuRegion
domain = ossConfig.QiniuDomain
if accessKey == "" && ossConfig.AccessKey != "" {
decrypted, err := utils.DecryptAES(ossConfig.AccessKey)
if err == nil {
accessKey = decrypted
}
}
if secretKey == "" && ossConfig.SecretKey != "" {
decrypted, err := utils.DecryptAES(ossConfig.SecretKey)
if err == nil {
secretKey = decrypted
}
}
if bucket == "" {
bucket = ossConfig.Bucket
}
if region == "" {
region = ossConfig.Region
}
if domain == "" {
domain = ossConfig.Domain
}
default:
if ossConfig.AccessKey != "" {
decrypted, err := utils.DecryptAES(ossConfig.AccessKey)
if err == nil {
accessKey = decrypted
}
}
if ossConfig.SecretKey != "" {
decrypted, err := utils.DecryptAES(ossConfig.SecretKey)
if err == nil {
secretKey = decrypted
}
}
bucket = ossConfig.Bucket
region = ossConfig.Region
domain = ossConfig.Domain
2026-01-19 16:14:08 +08:00
}
config.AccessKey = accessKey
config.SecretKey = secretKey
2026-01-19 20:21:09 +08:00
config.Bucket = bucket
config.Region = region
config.Domain = domain
switch ossConfig.StorageType {
case "aliyun":
config.OSSAccessKeyID = accessKey
config.OSSAccessKeySecret = secretKey
config.OSSEndpoint = ossConfig.OSSEndpoint
config.OSSBucket = bucket
config.OSSDomain = domain
case "qcloud":
config.QCloudSecretID = accessKey
config.QCloudSecretKey = secretKey
config.QCloudRegion = region
config.QCloudBucket = bucket
config.QCloudDomain = domain
case "qiniu":
config.QiniuAccessKey = accessKey
config.QiniuSecretKey = secretKey
config.QiniuBucket = bucket
config.QiniuRegion = region
config.QiniuDomain = domain
}
2026-01-19 16:14:08 +08:00
}
// 获取上传器
uploader, err := utils.GetOSSUploader(config)
if err != nil {
utils.Error(c, 500, err.Error())
return
}
// 上传文件
filePath, fileURL, err := uploader.Upload(src, file.Filename, file.Size)
if err != nil {
utils.Error(c, 500, "Failed to upload file: "+err.Error())
return
}
2026-07-14 10:05:33 +08:00
storedName := filepath.Base(filePath)
if storedName == "." || storedName == "/" || storedName == "" {
storedName = file.Filename
}
2026-01-19 16:14:08 +08:00
// 获取文件类型
fileType := utils.GetFileType(file.Header.Get("Content-Type"))
// 创建附件记录
attachment := &models.Attachment{
CategoryID: categoryID,
OriginalName: file.Filename,
2026-07-14 10:05:33 +08:00
StoredName: storedName,
2026-01-19 16:14:08 +08:00
FilePath: filePath,
FileURL: fileURL,
FileSize: file.Size,
FileType: fileType,
MimeType: file.Header.Get("Content-Type"),
StorageType: storageType,
}
if ossConfig != nil {
ossConfigID := ossConfig.ID
attachment.OSSConfigID = &ossConfigID
}
if err := repositories.CreateAttachment(attachment); err != nil {
uploader.Delete(filePath)
utils.ServerError(c, err)
return
}
utils.SuccessWithMsg(c, "File uploaded successfully", attachment)
}
// AdminGetAttachments 获取附件列表
func AdminGetAttachments(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "20"))
categoryIDStr := c.Query("categoryId")
fileType := c.Query("fileType")
2026-06-25 17:57:51 +08:00
keyword := c.Query("keyword")
2026-01-19 16:14:08 +08:00
var categoryID *uint
if categoryIDStr != "" {
if id, err := strconv.ParseUint(categoryIDStr, 10, 32); err == nil {
idUint := uint(id)
categoryID = &idUint
}
}
2026-06-25 17:57:51 +08:00
attachments, total, err := repositories.GetAttachments(page, pageSize, categoryID, fileType, keyword)
2026-01-19 16:14:08 +08:00
if err != nil {
utils.ServerError(c, err)
return
}
res := gin.H{
"list": attachments,
"total": total,
"page": page,
"size": pageSize,
}
utils.Success(c, res)
}
// AdminDeleteAttachment 删除附件
func AdminDeleteAttachment(c *gin.Context) {
idStr := c.Param("id")
2026-07-14 10:05:33 +08:00
parsedID, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
2026-01-19 16:14:08 +08:00
utils.Error(c, 400, "Invalid attachment ID")
return
}
2026-07-14 10:05:33 +08:00
id := uint(parsedID)
2026-01-19 16:14:08 +08:00
// 获取附件信息
attachment, err := repositories.GetAttachmentByID(id)
if err != nil || attachment == nil {
utils.Error(c, 404, "Attachment not found")
return
}
// 获取OSS配置并删除文件
if attachment.StorageType != "" {
var ossConfig *models.OSSConfig
if attachment.OSSConfigID != nil {
ossConfig, err = repositories.GetOSSConfigByID(*attachment.OSSConfigID)
} else {
ossConfig, err = repositories.GetActiveOSSConfig(attachment.StorageType)
}
if err == nil && ossConfig != nil {
config := &utils.OSSConfig{
StorageType: attachment.StorageType,
}
2026-01-19 20:21:09 +08:00
// 根据存储类型从专用字段或通用字段读取配置
var accessKey, secretKey, bucket, region, domain string
switch ossConfig.StorageType {
case "aliyun":
if ossConfig.OSSAccessKeyID != "" {
if decrypted, err := utils.DecryptAES(ossConfig.OSSAccessKeyID); err == nil {
accessKey = decrypted
}
}
if ossConfig.OSSAccessKeySecret != "" {
if decrypted, err := utils.DecryptAES(ossConfig.OSSAccessKeySecret); err == nil {
secretKey = decrypted
}
}
bucket = ossConfig.OSSBucket
domain = ossConfig.OSSDomain
// 向后兼容
if accessKey == "" && ossConfig.AccessKey != "" {
if decrypted, err := utils.DecryptAES(ossConfig.AccessKey); err == nil {
accessKey = decrypted
}
}
if secretKey == "" && ossConfig.SecretKey != "" {
if decrypted, err := utils.DecryptAES(ossConfig.SecretKey); err == nil {
secretKey = decrypted
}
}
if bucket == "" {
bucket = ossConfig.Bucket
}
if domain == "" {
domain = ossConfig.Domain
}
case "qcloud":
if ossConfig.QCloudSecretID != "" {
if decrypted, err := utils.DecryptAES(ossConfig.QCloudSecretID); err == nil {
accessKey = decrypted
}
}
if ossConfig.QCloudSecretKey != "" {
if decrypted, err := utils.DecryptAES(ossConfig.QCloudSecretKey); err == nil {
secretKey = decrypted
}
}
bucket = ossConfig.QCloudBucket
region = ossConfig.QCloudRegion
domain = ossConfig.QCloudDomain
// 向后兼容
if accessKey == "" && ossConfig.AccessKey != "" {
if decrypted, err := utils.DecryptAES(ossConfig.AccessKey); err == nil {
accessKey = decrypted
}
}
if secretKey == "" && ossConfig.SecretKey != "" {
if decrypted, err := utils.DecryptAES(ossConfig.SecretKey); err == nil {
secretKey = decrypted
}
}
if bucket == "" {
bucket = ossConfig.Bucket
}
if region == "" {
region = ossConfig.Region
}
if domain == "" {
domain = ossConfig.Domain
}
case "qiniu":
if ossConfig.QiniuAccessKey != "" {
if decrypted, err := utils.DecryptAES(ossConfig.QiniuAccessKey); err == nil {
accessKey = decrypted
}
}
if ossConfig.QiniuSecretKey != "" {
if decrypted, err := utils.DecryptAES(ossConfig.QiniuSecretKey); err == nil {
secretKey = decrypted
}
}
bucket = ossConfig.QiniuBucket
region = ossConfig.QiniuRegion
domain = ossConfig.QiniuDomain
// 向后兼容
if accessKey == "" && ossConfig.AccessKey != "" {
if decrypted, err := utils.DecryptAES(ossConfig.AccessKey); err == nil {
accessKey = decrypted
}
}
if secretKey == "" && ossConfig.SecretKey != "" {
if decrypted, err := utils.DecryptAES(ossConfig.SecretKey); err == nil {
secretKey = decrypted
}
}
if bucket == "" {
bucket = ossConfig.Bucket
}
if region == "" {
region = ossConfig.Region
}
if domain == "" {
domain = ossConfig.Domain
}
default:
if ossConfig.AccessKey != "" {
if decrypted, err := utils.DecryptAES(ossConfig.AccessKey); err == nil {
accessKey = decrypted
}
}
if ossConfig.SecretKey != "" {
if decrypted, err := utils.DecryptAES(ossConfig.SecretKey); err == nil {
secretKey = decrypted
}
}
bucket = ossConfig.Bucket
region = ossConfig.Region
domain = ossConfig.Domain
2026-01-19 16:14:08 +08:00
}
2026-01-19 20:21:09 +08:00
config.AccessKey = accessKey
config.SecretKey = secretKey
config.Bucket = bucket
config.Region = region
config.Domain = domain
2026-01-19 16:14:08 +08:00
uploader, err := utils.GetOSSUploader(config)
if err == nil {
uploader.Delete(attachment.FilePath)
}
}
}
// 删除数据库记录
if err := repositories.DeleteAttachment(id); err != nil {
utils.ServerError(c, err)
return
}
utils.SuccessWithMsg(c, "Attachment deleted successfully", nil)
}
2026-01-19 20:21:09 +08:00
// AdminUpdateAttachment 更新附件
func AdminUpdateAttachment(c *gin.Context) {
idStr := c.Param("id")
2026-07-14 10:05:33 +08:00
parsedID, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
2026-01-19 20:21:09 +08:00
utils.Error(c, 400, "Invalid attachment ID")
return
}
2026-07-14 10:05:33 +08:00
id := uint(parsedID)
2026-01-19 20:21:09 +08:00
// 获取现有附件
attachment, err := repositories.GetAttachmentByID(id)
if err != nil || attachment == nil {
utils.Error(c, 404, "Attachment not found")
return
}
// 解析请求体
var req struct {
CategoryID *uint `json:"categoryId"`
}
if err := c.ShouldBindJSON(&req); err != nil {
utils.Error(c, 400, "Invalid request")
return
}
// 更新分类ID
if req.CategoryID != nil {
if *req.CategoryID == 0 {
// 如果categoryId为0,设置为nil(无分类)
attachment.CategoryID = nil
} else {
attachment.CategoryID = req.CategoryID
}
}
// 更新附件
if err := repositories.UpdateAttachment(attachment); err != nil {
utils.ServerError(c, err)
return
}
// 重新获取更新后的附件(包含关联的分类信息)
updatedAttachment, err := repositories.GetAttachmentByID(id)
if err != nil {
utils.ServerError(c, err)
return
}
utils.SuccessWithMsg(c, "Attachment updated successfully", updatedAttachment)
}
2026-01-19 16:14:08 +08:00
// AdminGetAttachmentCategories 获取附件分类列表
func AdminGetAttachmentCategories(c *gin.Context) {
categories, err := repositories.GetAttachmentCategories()
if err != nil {
utils.ServerError(c, err)
return
}
utils.Success(c, categories)
}
// AdminCreateAttachmentCategory 创建附件分类
func AdminCreateAttachmentCategory(c *gin.Context) {
var category models.AttachmentCategory
if err := c.ShouldBindJSON(&category); err != nil {
utils.Error(c, 400, "Invalid request")
return
}
if err := repositories.CreateAttachmentCategory(&category); err != nil {
utils.ServerError(c, err)
return
}
utils.SuccessWithMsg(c, "Category created successfully", category)
}
// AdminUpdateAttachmentCategory 更新附件分类
func AdminUpdateAttachmentCategory(c *gin.Context) {
idStr := c.Param("id")
2026-07-14 10:05:33 +08:00
parsedID, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
2026-01-19 16:14:08 +08:00
utils.Error(c, 400, "Invalid category ID")
return
}
2026-07-14 10:05:33 +08:00
id := uint(parsedID)
2026-01-19 16:14:08 +08:00
var category models.AttachmentCategory
if err := c.ShouldBindJSON(&category); err != nil {
utils.Error(c, 400, "Invalid request")
return
}
category.ID = id
if err := repositories.UpdateAttachmentCategory(&category); err != nil {
utils.ServerError(c, err)
return
}
utils.SuccessWithMsg(c, "Category updated successfully", nil)
}
// AdminDeleteAttachmentCategory 删除附件分类
func AdminDeleteAttachmentCategory(c *gin.Context) {
idStr := c.Param("id")
2026-07-14 10:05:33 +08:00
parsedID, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
2026-01-19 16:14:08 +08:00
utils.Error(c, 400, "Invalid category ID")
return
}
2026-07-14 10:05:33 +08:00
id := uint(parsedID)
2026-01-19 16:14:08 +08:00
if err := repositories.DeleteAttachmentCategory(id); err != nil {
utils.ServerError(c, err)
return
}
utils.SuccessWithMsg(c, "Category deleted successfully", nil)
}
// AdminGetOSSConfigs 获取OSS配置列表
func AdminGetOSSConfigs(c *gin.Context) {
2026-01-19 20:21:09 +08:00
// 检查是否存在默认本地存储配置,如果不存在则创建
defaultConfig, err := repositories.GetDefaultLocalOSSConfig()
if err != nil {
log.Printf("Error checking default local OSS config: %v", err)
} else if defaultConfig == nil {
// 创建默认本地存储配置
// 本地存储不需要AccessKey和SecretKey,直接使用空字符串(不加密)
defaultOSSConfig := &models.OSSConfig{
Name: "本地存储",
StorageType: "local",
AccessKey: "", // 空字符串,不加密
SecretKey: "", // 空字符串,不加密
Bucket: "",
Region: "",
Domain: "",
IsActive: 1,
}
if err := repositories.CreateOSSConfig(defaultOSSConfig); err != nil {
log.Printf("Error creating default local OSS config: %v", err)
}
}
2026-01-19 16:14:08 +08:00
configs, err := repositories.GetOSSConfigs()
if err != nil {
utils.ServerError(c, err)
return
}
// 不返回加密的密钥
for i := range configs {
configs[i].AccessKey = "***"
configs[i].SecretKey = "***"
2026-01-19 20:21:09 +08:00
configs[i].OSSAccessKeyID = "***"
configs[i].OSSAccessKeySecret = "***"
configs[i].QCloudSecretID = "***"
configs[i].QCloudSecretKey = "***"
configs[i].QiniuAccessKey = "***"
configs[i].QiniuSecretKey = "***"
2026-01-19 16:14:08 +08:00
}
utils.Success(c, configs)
}
// AdminCreateOSSConfig 创建OSS配置
func AdminCreateOSSConfig(c *gin.Context) {
var req struct {
Name string `json:"name" binding:"required"`
StorageType string `json:"storageType" binding:"required"`
2026-01-19 20:21:09 +08:00
// 通用字段(向后兼容)
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
Bucket string `json:"bucket"`
Region string `json:"region"`
Domain string `json:"domain"`
// 阿里云OSS专用字段
OSSAccessKeyID string `json:"ossAccessKeyId"`
OSSAccessKeySecret string `json:"ossAccessKeySecret"`
OSSEndpoint string `json:"ossEndpoint"`
OSSBucket string `json:"ossBucket"`
OSSDomain string `json:"ossDomain"`
// 腾讯云COS专用字段
QCloudSecretID string `json:"qcloudSecretId"`
QCloudSecretKey string `json:"qcloudSecretKey"`
QCloudRegion string `json:"qcloudRegion"`
QCloudBucket string `json:"qcloudBucket"`
QCloudDomain string `json:"qcloudDomain"`
// 七牛云专用字段
QiniuAccessKey string `json:"qiniuAccessKey"`
QiniuSecretKey string `json:"qiniuSecretKey"`
QiniuBucket string `json:"qiniuBucket"`
QiniuRegion string `json:"qiniuRegion"`
QiniuDomain string `json:"qiniuDomain"`
IsActive int `json:"isActive"`
2026-01-19 16:14:08 +08:00
}
if err := c.ShouldBindJSON(&req); err != nil {
utils.Error(c, 400, "Invalid request")
return
}
2026-01-19 20:21:09 +08:00
// 加密函数
encryptKey := func(key string) (string, error) {
if key == "" {
return "", nil
}
return utils.EncryptAES(key)
}
// 加密通用密钥(向后兼容)
encryptedAccessKey, err := encryptKey(req.AccessKey)
if err != nil {
utils.Error(c, 500, "Failed to encrypt access key: "+err.Error())
return
}
encryptedSecretKey, err := encryptKey(req.SecretKey)
if err != nil {
utils.Error(c, 500, "Failed to encrypt secret key: "+err.Error())
return
}
// 加密阿里云密钥
encryptedOSSAccessKeyID, err := encryptKey(req.OSSAccessKeyID)
if err != nil {
utils.Error(c, 500, "Failed to encrypt OSS access key ID: "+err.Error())
return
}
encryptedOSSAccessKeySecret, err := encryptKey(req.OSSAccessKeySecret)
2026-01-19 16:14:08 +08:00
if err != nil {
2026-01-19 20:21:09 +08:00
utils.Error(c, 500, "Failed to encrypt OSS access key secret: "+err.Error())
2026-01-19 16:14:08 +08:00
return
}
2026-01-19 20:21:09 +08:00
// 加密腾讯云密钥
encryptedQCloudSecretID, err := encryptKey(req.QCloudSecretID)
if err != nil {
utils.Error(c, 500, "Failed to encrypt QCloud secret ID: "+err.Error())
return
}
encryptedQCloudSecretKey, err := encryptKey(req.QCloudSecretKey)
if err != nil {
utils.Error(c, 500, "Failed to encrypt QCloud secret key: "+err.Error())
return
}
// 加密七牛云密钥
encryptedQiniuAccessKey, err := encryptKey(req.QiniuAccessKey)
if err != nil {
utils.Error(c, 500, "Failed to encrypt Qiniu access key: "+err.Error())
return
}
encryptedQiniuSecretKey, err := encryptKey(req.QiniuSecretKey)
2026-01-19 16:14:08 +08:00
if err != nil {
2026-01-19 20:21:09 +08:00
utils.Error(c, 500, "Failed to encrypt Qiniu secret key: "+err.Error())
2026-01-19 16:14:08 +08:00
return
}
ossConfig := &models.OSSConfig{
Name: req.Name,
StorageType: req.StorageType,
2026-01-19 20:21:09 +08:00
// 通用字段(向后兼容)
AccessKey: encryptedAccessKey,
SecretKey: encryptedSecretKey,
Bucket: req.Bucket,
Region: req.Region,
Domain: req.Domain,
// 阿里云OSS专用字段
OSSAccessKeyID: encryptedOSSAccessKeyID,
OSSAccessKeySecret: encryptedOSSAccessKeySecret,
OSSEndpoint: req.OSSEndpoint,
OSSBucket: req.OSSBucket,
OSSDomain: req.OSSDomain,
// 腾讯云COS专用字段
QCloudSecretID: encryptedQCloudSecretID,
QCloudSecretKey: encryptedQCloudSecretKey,
QCloudRegion: req.QCloudRegion,
QCloudBucket: req.QCloudBucket,
QCloudDomain: req.QCloudDomain,
// 七牛云专用字段
QiniuAccessKey: encryptedQiniuAccessKey,
QiniuSecretKey: encryptedQiniuSecretKey,
QiniuBucket: req.QiniuBucket,
QiniuRegion: req.QiniuRegion,
QiniuDomain: req.QiniuDomain,
IsActive: req.IsActive,
2026-01-19 16:14:08 +08:00
}
if err := repositories.CreateOSSConfig(ossConfig); err != nil {
utils.ServerError(c, err)
return
}
// 不返回加密的密钥
ossConfig.AccessKey = "***"
ossConfig.SecretKey = "***"
2026-01-19 20:21:09 +08:00
ossConfig.OSSAccessKeyID = "***"
ossConfig.OSSAccessKeySecret = "***"
ossConfig.QCloudSecretID = "***"
ossConfig.QCloudSecretKey = "***"
ossConfig.QiniuAccessKey = "***"
ossConfig.QiniuSecretKey = "***"
2026-01-19 16:14:08 +08:00
utils.SuccessWithMsg(c, "OSS config created successfully", ossConfig)
}
// AdminUpdateOSSConfig 更新OSS配置
func AdminUpdateOSSConfig(c *gin.Context) {
idStr := c.Param("id")
2026-07-14 10:05:33 +08:00
parsedID, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
2026-01-19 16:14:08 +08:00
utils.Error(c, 400, "Invalid OSS config ID")
return
}
2026-07-14 10:05:33 +08:00
id := uint(parsedID)
2026-01-19 16:14:08 +08:00
var req struct {
Name string `json:"name"`
StorageType string `json:"storageType"`
2026-01-19 20:21:09 +08:00
// 通用字段(向后兼容)
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
Bucket string `json:"bucket"`
Region string `json:"region"`
Domain string `json:"domain"`
// 阿里云OSS专用字段
OSSAccessKeyID string `json:"ossAccessKeyId"`
OSSAccessKeySecret string `json:"ossAccessKeySecret"`
OSSEndpoint string `json:"ossEndpoint"`
OSSBucket string `json:"ossBucket"`
OSSDomain string `json:"ossDomain"`
// 腾讯云COS专用字段
QCloudSecretID string `json:"qcloudSecretId"`
QCloudSecretKey string `json:"qcloudSecretKey"`
QCloudRegion string `json:"qcloudRegion"`
QCloudBucket string `json:"qcloudBucket"`
QCloudDomain string `json:"qcloudDomain"`
// 七牛云专用字段
QiniuAccessKey string `json:"qiniuAccessKey"`
QiniuSecretKey string `json:"qiniuSecretKey"`
QiniuBucket string `json:"qiniuBucket"`
QiniuRegion string `json:"qiniuRegion"`
QiniuDomain string `json:"qiniuDomain"`
IsActive int `json:"isActive"`
2026-01-19 16:14:08 +08:00
}
if err := c.ShouldBindJSON(&req); err != nil {
utils.Error(c, 400, "Invalid request")
return
}
// 获取现有配置
existingConfig, err := repositories.GetOSSConfigByID(id)
if err != nil || existingConfig == nil {
utils.Error(c, 404, "OSS config not found")
return
}
2026-07-14 10:05:33 +08:00
storageType := existingConfig.StorageType
if req.StorageType != "" {
storageType = req.StorageType
}
2026-01-19 20:21:09 +08:00
2026-07-14 10:05:33 +08:00
// 启用非 local 配置前,按存储类型校验密钥(优先专用字段,兼容 legacy)
if req.IsActive == 1 && existingConfig.IsActive == 0 && storageType != "local" {
if errMsg := validateOSSCredentialsForEnable(storageType, existingConfig, req.AccessKey, req.SecretKey,
req.OSSAccessKeyID, req.OSSAccessKeySecret, req.OSSEndpoint, req.OSSBucket,
req.QCloudSecretID, req.QCloudSecretKey, req.QCloudRegion, req.QCloudBucket,
req.QiniuAccessKey, req.QiniuSecretKey, req.QiniuBucket); errMsg != "" {
utils.Error(c, 400, errMsg)
2026-01-19 20:21:09 +08:00
return
}
}
// 加密函数
encryptKey := func(key string) (string, error) {
if key == "" {
return "", nil
}
return utils.EncryptAES(key)
}
2026-07-14 10:05:33 +08:00
// 密钥字段:*** 或空表示不修改已有值
updateSecret := func(reqVal string, dest *string) error {
if reqVal == "***" || reqVal == "" {
return nil
}
encrypted, err := encryptKey(reqVal)
if err != nil {
return err
}
*dest = encrypted
return nil
}
2026-01-19 16:14:08 +08:00
// 更新字段
if req.Name != "" {
existingConfig.Name = req.Name
}
if req.StorageType != "" {
existingConfig.StorageType = req.StorageType
}
2026-01-19 20:21:09 +08:00
2026-07-14 10:05:33 +08:00
if err := updateSecret(req.AccessKey, &existingConfig.AccessKey); err != nil {
utils.Error(c, 500, "Failed to encrypt access key: "+err.Error())
return
2026-01-19 20:21:09 +08:00
}
2026-07-14 10:05:33 +08:00
if err := updateSecret(req.SecretKey, &existingConfig.SecretKey); err != nil {
utils.Error(c, 500, "Failed to encrypt secret key: "+err.Error())
return
2026-01-19 16:14:08 +08:00
}
2026-01-19 20:21:09 +08:00
existingConfig.Bucket = req.Bucket
existingConfig.Region = req.Region
existingConfig.Domain = req.Domain
2026-07-14 10:05:33 +08:00
if err := updateSecret(req.OSSAccessKeyID, &existingConfig.OSSAccessKeyID); err != nil {
utils.Error(c, 500, "Failed to encrypt OSS access key ID: "+err.Error())
return
2026-01-19 16:14:08 +08:00
}
2026-07-14 10:05:33 +08:00
if err := updateSecret(req.OSSAccessKeySecret, &existingConfig.OSSAccessKeySecret); err != nil {
utils.Error(c, 500, "Failed to encrypt OSS access key secret: "+err.Error())
return
2026-01-19 16:14:08 +08:00
}
2026-01-19 20:21:09 +08:00
existingConfig.OSSEndpoint = req.OSSEndpoint
existingConfig.OSSBucket = req.OSSBucket
existingConfig.OSSDomain = req.OSSDomain
2026-07-14 10:05:33 +08:00
if err := updateSecret(req.QCloudSecretID, &existingConfig.QCloudSecretID); err != nil {
utils.Error(c, 500, "Failed to encrypt QCloud secret ID: "+err.Error())
return
2026-01-19 20:21:09 +08:00
}
2026-07-14 10:05:33 +08:00
if err := updateSecret(req.QCloudSecretKey, &existingConfig.QCloudSecretKey); err != nil {
utils.Error(c, 500, "Failed to encrypt QCloud secret key: "+err.Error())
return
2026-01-19 20:21:09 +08:00
}
existingConfig.QCloudRegion = req.QCloudRegion
existingConfig.QCloudBucket = req.QCloudBucket
existingConfig.QCloudDomain = req.QCloudDomain
2026-07-14 10:05:33 +08:00
if err := updateSecret(req.QiniuAccessKey, &existingConfig.QiniuAccessKey); err != nil {
utils.Error(c, 500, "Failed to encrypt Qiniu access key: "+err.Error())
return
2026-01-19 16:14:08 +08:00
}
2026-07-14 10:05:33 +08:00
if err := updateSecret(req.QiniuSecretKey, &existingConfig.QiniuSecretKey); err != nil {
utils.Error(c, 500, "Failed to encrypt Qiniu secret key: "+err.Error())
return
2026-01-19 16:14:08 +08:00
}
2026-01-19 20:21:09 +08:00
existingConfig.QiniuBucket = req.QiniuBucket
existingConfig.QiniuRegion = req.QiniuRegion
existingConfig.QiniuDomain = req.QiniuDomain
2026-01-19 16:14:08 +08:00
existingConfig.IsActive = req.IsActive
if err := repositories.UpdateOSSConfig(existingConfig); err != nil {
utils.ServerError(c, err)
return
}
2026-07-14 10:05:33 +08:00
maskOSSSecrets(existingConfig)
2026-01-19 16:14:08 +08:00
utils.SuccessWithMsg(c, "OSS config updated successfully", existingConfig)
}
// AdminDeleteOSSConfig 删除OSS配置
func AdminDeleteOSSConfig(c *gin.Context) {
idStr := c.Param("id")
2026-07-14 10:05:33 +08:00
parsedID, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
2026-01-19 16:14:08 +08:00
utils.Error(c, 400, "Invalid OSS config ID")
return
}
2026-07-14 10:05:33 +08:00
id := uint(parsedID)
2026-01-19 16:14:08 +08:00
2026-01-19 20:21:09 +08:00
// 获取配置信息,检查是否为默认本地存储配置
ossConfig, err := repositories.GetOSSConfigByID(id)
if err != nil {
utils.ServerError(c, err)
return
}
if ossConfig == nil {
utils.Error(c, 404, "OSS config not found")
return
}
// 检查是否为默认本地存储配置,不允许删除
if ossConfig.Name == "本地存储" && ossConfig.StorageType == "local" {
utils.Error(c, 400, "默认本地存储配置不能删除")
return
}
2026-01-19 16:14:08 +08:00
if err := repositories.DeleteOSSConfig(id); err != nil {
utils.ServerError(c, err)
return
}
utils.SuccessWithMsg(c, "OSS config deleted successfully", nil)
}
2026-07-14 10:05:33 +08:00
// AdminTestOSSConfig 测试OSS配置连通性
func AdminTestOSSConfig(c *gin.Context) {
idStr := c.Param("id")
parsedID, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
utils.Error(c, 400, "Invalid OSS config ID")
return
}
id := uint(parsedID)
ossConfig, err := repositories.GetOSSConfigByID(id)
if err != nil || ossConfig == nil {
utils.Error(c, 404, "OSS config not found")
return
}
var runtimeCfg *utils.OSSConfig
if ossConfig.StorageType == "aliyun" {
if envCfg, ok := utils.LoadAliyunConfigFromEnv(); ok {
runtimeCfg = envCfg
}
}
if runtimeCfg == nil {
runtimeCfg, err = buildRuntimeOSSConfig(ossConfig)
if err != nil {
utils.Error(c, 400, err.Error())
return
}
}
if err := utils.TestOSSConnection(runtimeCfg); err != nil {
utils.Error(c, 400, "连接测试失败: "+err.Error())
return
}
source := "db"
if ossConfig.StorageType == "aliyun" && utils.HasAliyunEnvConfig() {
source = "env"
}
utils.SuccessWithMsg(c, "连接测试成功", gin.H{
"storageType": ossConfig.StorageType,
"name": ossConfig.Name,
"source": source,
})
}
func decryptOptional(encrypted string) string {
if encrypted == "" {
return ""
}
decrypted, err := utils.DecryptAES(encrypted)
if err != nil {
return ""
}
return decrypted
}
func pickCredential(reqVal, existingEncrypted string) string {
if reqVal != "" && reqVal != "***" {
return reqVal
}
return decryptOptional(existingEncrypted)
}
func validateOSSCredentialsForEnable(
storageType string,
existing *models.OSSConfig,
legacyAccessKey, legacySecretKey string,
ossAccessKeyID, ossAccessKeySecret, ossEndpoint, ossBucket string,
qcloudSecretID, qcloudSecretKey, qcloudRegion, qcloudBucket string,
qiniuAccessKey, qiniuSecretKey, qiniuBucket string,
) string {
switch storageType {
case "aliyun":
ak := pickCredential(ossAccessKeyID, existing.OSSAccessKeyID)
sk := pickCredential(ossAccessKeySecret, existing.OSSAccessKeySecret)
if ak == "" {
ak = pickCredential(legacyAccessKey, existing.AccessKey)
}
if sk == "" {
sk = pickCredential(legacySecretKey, existing.SecretKey)
}
endpoint := ossEndpoint
if endpoint == "" {
endpoint = existing.OSSEndpoint
}
if endpoint == "" {
endpoint = existing.Region
}
bucket := ossBucket
if bucket == "" {
bucket = existing.OSSBucket
}
if bucket == "" {
bucket = existing.Bucket
}
if ak == "" || sk == "" || endpoint == "" || bucket == "" {
return "启用阿里云OSS前必须填写 AccessKeyId、AccessKeySecret、Endpoint 和 Bucket"
}
case "qcloud":
id := pickCredential(qcloudSecretID, existing.QCloudSecretID)
key := pickCredential(qcloudSecretKey, existing.QCloudSecretKey)
if id == "" {
id = pickCredential(legacyAccessKey, existing.AccessKey)
}
if key == "" {
key = pickCredential(legacySecretKey, existing.SecretKey)
}
region := qcloudRegion
if region == "" {
region = existing.QCloudRegion
}
if region == "" {
region = existing.Region
}
bucket := qcloudBucket
if bucket == "" {
bucket = existing.QCloudBucket
}
if bucket == "" {
bucket = existing.Bucket
}
if id == "" || key == "" || region == "" || bucket == "" {
return "启用腾讯云COS前必须填写 SecretId、SecretKey、Region 和 Bucket"
}
case "qiniu":
ak := pickCredential(qiniuAccessKey, existing.QiniuAccessKey)
sk := pickCredential(qiniuSecretKey, existing.QiniuSecretKey)
if ak == "" {
ak = pickCredential(legacyAccessKey, existing.AccessKey)
}
if sk == "" {
sk = pickCredential(legacySecretKey, existing.SecretKey)
}
bucket := qiniuBucket
if bucket == "" {
bucket = existing.QiniuBucket
}
if bucket == "" {
bucket = existing.Bucket
}
if ak == "" || sk == "" || bucket == "" {
return "启用七牛云前必须填写 AccessKey、SecretKey 和 Bucket"
}
default:
ak := pickCredential(legacyAccessKey, existing.AccessKey)
sk := pickCredential(legacySecretKey, existing.SecretKey)
if ak == "" || sk == "" {
return "启用非本地存储配置前必须填写密钥"
}
}
return ""
}
func maskOSSSecrets(ossConfig *models.OSSConfig) {
ossConfig.AccessKey = "***"
ossConfig.SecretKey = "***"
ossConfig.OSSAccessKeyID = "***"
ossConfig.OSSAccessKeySecret = "***"
ossConfig.QCloudSecretID = "***"
ossConfig.QCloudSecretKey = "***"
ossConfig.QiniuAccessKey = "***"
ossConfig.QiniuSecretKey = "***"
}
func buildRuntimeOSSConfig(ossConfig *models.OSSConfig) (*utils.OSSConfig, error) {
cfg := &utils.OSSConfig{
StorageType: ossConfig.StorageType,
AccessKey: decryptOptional(ossConfig.AccessKey),
SecretKey: decryptOptional(ossConfig.SecretKey),
Bucket: ossConfig.Bucket,
Region: ossConfig.Region,
Domain: ossConfig.Domain,
}
switch ossConfig.StorageType {
case "aliyun":
cfg.OSSAccessKeyID = decryptOptional(ossConfig.OSSAccessKeyID)
cfg.OSSAccessKeySecret = decryptOptional(ossConfig.OSSAccessKeySecret)
cfg.OSSEndpoint = ossConfig.OSSEndpoint
cfg.OSSBucket = ossConfig.OSSBucket
cfg.OSSDomain = ossConfig.OSSDomain
if cfg.OSSAccessKeyID == "" {
cfg.OSSAccessKeyID = cfg.AccessKey
}
if cfg.OSSAccessKeySecret == "" {
cfg.OSSAccessKeySecret = cfg.SecretKey
}
if cfg.OSSBucket == "" {
cfg.OSSBucket = cfg.Bucket
}
if cfg.OSSDomain == "" {
cfg.OSSDomain = cfg.Domain
}
if cfg.OSSAccessKeyID == "" || cfg.OSSAccessKeySecret == "" || cfg.OSSBucket == "" {
return nil, fmt.Errorf("阿里云OSS配置不完整:需要 AccessKeyId、AccessKeySecret 和 Bucket")
}
if cfg.OSSEndpoint == "" && cfg.Region == "" {
return nil, fmt.Errorf("阿里云OSS配置不完整:需要 Endpoint 或 Region")
}
case "qcloud":
cfg.QCloudSecretID = decryptOptional(ossConfig.QCloudSecretID)
cfg.QCloudSecretKey = decryptOptional(ossConfig.QCloudSecretKey)
cfg.QCloudRegion = ossConfig.QCloudRegion
cfg.QCloudBucket = ossConfig.QCloudBucket
cfg.QCloudDomain = ossConfig.QCloudDomain
if cfg.QCloudSecretID == "" {
cfg.QCloudSecretID = cfg.AccessKey
}
if cfg.QCloudSecretKey == "" {
cfg.QCloudSecretKey = cfg.SecretKey
}
if cfg.QCloudRegion == "" {
cfg.QCloudRegion = cfg.Region
}
if cfg.QCloudBucket == "" {
cfg.QCloudBucket = cfg.Bucket
}
if cfg.QCloudDomain == "" {
cfg.QCloudDomain = cfg.Domain
}
if cfg.QCloudSecretID == "" || cfg.QCloudSecretKey == "" || cfg.QCloudBucket == "" || cfg.QCloudRegion == "" {
return nil, fmt.Errorf("腾讯云COS配置不完整:需要 SecretId、SecretKey、Bucket 和 Region")
}
case "qiniu":
cfg.QiniuAccessKey = decryptOptional(ossConfig.QiniuAccessKey)
cfg.QiniuSecretKey = decryptOptional(ossConfig.QiniuSecretKey)
cfg.QiniuBucket = ossConfig.QiniuBucket
cfg.QiniuRegion = ossConfig.QiniuRegion
cfg.QiniuDomain = ossConfig.QiniuDomain
if cfg.QiniuAccessKey == "" {
cfg.QiniuAccessKey = cfg.AccessKey
}
if cfg.QiniuSecretKey == "" {
cfg.QiniuSecretKey = cfg.SecretKey
}
if cfg.QiniuBucket == "" {
cfg.QiniuBucket = cfg.Bucket
}
if cfg.QiniuDomain == "" {
cfg.QiniuDomain = cfg.Domain
}
if cfg.QiniuAccessKey == "" || cfg.QiniuSecretKey == "" || cfg.QiniuBucket == "" {
return nil, fmt.Errorf("七牛云配置不完整:需要 AccessKey、SecretKey 和 Bucket")
}
case "local":
// no credentials required
default:
return nil, fmt.Errorf("不支持的存储类型: %s", ossConfig.StorageType)
}
return cfg, nil
}