阿里云OSS设置
This commit is contained in:
@@ -165,7 +165,7 @@ const uploadFile = async (file: File) => {
|
||||
if (props.categoryId) {
|
||||
formData.append('categoryId', props.categoryId.toString())
|
||||
}
|
||||
formData.append('storageType', 'local')
|
||||
formData.append('storageType', 'aliyun')
|
||||
|
||||
const result = await parseApiResponse<{ fileUrl: string }>(await authFetch(`${API_BASE}/admin/attachments/upload`, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -366,7 +366,7 @@ const uploadSingleFile = async (file: File): Promise<string | null> => {
|
||||
if (props.categoryId) {
|
||||
formData.append('categoryId', props.categoryId.toString())
|
||||
}
|
||||
formData.append('storageType', 'local')
|
||||
formData.append('storageType', 'aliyun')
|
||||
|
||||
const result = await parseApiResponse<{ fileUrl: string }>(await authFetch(`${API_BASE}/admin/attachments/upload`, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -46,7 +46,7 @@ const uploadFile = async (file: File) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
if (props.categoryId) formData.append('categoryId', props.categoryId.toString())
|
||||
formData.append('storageType', 'local')
|
||||
formData.append('storageType', 'aliyun')
|
||||
const result = await parseApiResponse<{ fileUrl: string }>(
|
||||
await authFetch(`${API_BASE}/admin/attachments/upload`, { method: 'POST', headers: {}, body: formData })
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ export function useMdMediaUpload(editorRef: Ref<ExposeParam | undefined>) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
formData.append('categoryId', '1')
|
||||
formData.append('storageType', 'local')
|
||||
formData.append('storageType', 'aliyun')
|
||||
|
||||
const response = await authFetch('/api/admin/attachments/upload', {
|
||||
method: 'POST',
|
||||
|
||||
@@ -293,7 +293,7 @@ const uploadFileWithFile = async (file: File) => {
|
||||
if (uploadCategoryId.value > 0) {
|
||||
formData.append('categoryId', uploadCategoryId.value.toString())
|
||||
}
|
||||
formData.append('storageType', 'local')
|
||||
formData.append('storageType', 'aliyun')
|
||||
|
||||
await parseApiResponse(await authFetch(`${API_BASE}/admin/attachments/upload`, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -59,9 +59,9 @@
|
||||
type="text"
|
||||
id="ossAccessKeyId"
|
||||
v-model="ossForm.ossAccessKeyId"
|
||||
required
|
||||
:required="!currentConfig"
|
||||
class="admin-input font-mono"
|
||||
placeholder="OSS Access Key ID"
|
||||
placeholder="留空则保持原密钥;新建时必填"
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -73,9 +73,9 @@
|
||||
type="password"
|
||||
id="ossAccessKeySecret"
|
||||
v-model="ossForm.ossAccessKeySecret"
|
||||
required
|
||||
:required="!currentConfig"
|
||||
class="admin-input font-mono"
|
||||
placeholder="OSS Access Key Secret"
|
||||
placeholder="留空则保持原密钥;新建时必填"
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -285,6 +285,15 @@
|
||||
</button>
|
||||
<div v-else></div>
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
v-if="currentConfig && currentConfig.id > 0"
|
||||
type="button"
|
||||
class="admin-btn-secondary"
|
||||
:disabled="isTesting || isSubmitting"
|
||||
@click="testConnection"
|
||||
>
|
||||
{{ isTesting ? '测试中...' : '测试连接' }}
|
||||
</button>
|
||||
<button type="submit" class="admin-btn-primary" :disabled="isSubmitting">
|
||||
{{ isSubmitting ? '保存中...' : (currentConfig ? '保存' : '创建') }}
|
||||
</button>
|
||||
@@ -297,7 +306,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { getOSSConfigs, createOSSConfig, updateOSSConfig, deleteOSSConfig as deleteOSSConfigApi, OSSConfig } from '../../services/api'
|
||||
import { getOSSConfigs, createOSSConfig, updateOSSConfig, deleteOSSConfig as deleteOSSConfigApi, testOSSConfig, OSSConfig } from '../../services/api'
|
||||
import { useToast } from '../../composables/useToast'
|
||||
|
||||
const toast = useToast()
|
||||
@@ -314,6 +323,7 @@ const tabs = [
|
||||
// OSS配置管理
|
||||
const ossConfigs = ref<OSSConfig[]>([])
|
||||
const isSubmitting = ref(false)
|
||||
const isTesting = ref(false)
|
||||
|
||||
const ossForm = ref({
|
||||
id: 0,
|
||||
@@ -455,23 +465,32 @@ const fetchOSSConfigs = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const secretOrKeep = (value: string, isUpdate: boolean) => {
|
||||
if (value) return value
|
||||
return isUpdate ? '***' : ''
|
||||
}
|
||||
|
||||
const saveOSSConfig = async () => {
|
||||
// 设置存储类型
|
||||
ossForm.value.storageType = activeTab.value
|
||||
const isUpdate = ossForm.value.id > 0
|
||||
|
||||
// 验证必填字段
|
||||
// 验证必填字段(更新时密钥可留空表示保持原值)
|
||||
if (activeTab.value === 'aliyun') {
|
||||
if (!ossForm.value.ossAccessKeyId || !ossForm.value.ossAccessKeySecret || !ossForm.value.ossEndpoint || !ossForm.value.ossBucket) {
|
||||
if ((!isUpdate && (!ossForm.value.ossAccessKeyId || !ossForm.value.ossAccessKeySecret)) ||
|
||||
!ossForm.value.ossEndpoint || !ossForm.value.ossBucket) {
|
||||
toast.showToast('请填写所有必填字段', 'error')
|
||||
return
|
||||
}
|
||||
} else if (activeTab.value === 'qcloud') {
|
||||
if (!ossForm.value.qcloudSecretId || !ossForm.value.qcloudSecretKey || !ossForm.value.qcloudRegion || !ossForm.value.qcloudBucket) {
|
||||
if ((!isUpdate && (!ossForm.value.qcloudSecretId || !ossForm.value.qcloudSecretKey)) ||
|
||||
!ossForm.value.qcloudRegion || !ossForm.value.qcloudBucket) {
|
||||
toast.showToast('请填写所有必填字段', 'error')
|
||||
return
|
||||
}
|
||||
} else if (activeTab.value === 'qiniu') {
|
||||
if (!ossForm.value.qiniuAccessKey || !ossForm.value.qiniuSecretKey || !ossForm.value.qiniuBucket) {
|
||||
if ((!isUpdate && (!ossForm.value.qiniuAccessKey || !ossForm.value.qiniuSecretKey)) ||
|
||||
!ossForm.value.qiniuBucket) {
|
||||
toast.showToast('请填写所有必填字段', 'error')
|
||||
return
|
||||
}
|
||||
@@ -487,31 +506,29 @@ const saveOSSConfig = async () => {
|
||||
|
||||
// 根据存储类型设置对应字段
|
||||
if (activeTab.value === 'aliyun') {
|
||||
updateData.ossAccessKeyId = ossForm.value.ossAccessKeyId
|
||||
updateData.ossAccessKeySecret = ossForm.value.ossAccessKeySecret
|
||||
updateData.ossAccessKeyId = secretOrKeep(ossForm.value.ossAccessKeyId, isUpdate)
|
||||
updateData.ossAccessKeySecret = secretOrKeep(ossForm.value.ossAccessKeySecret, isUpdate)
|
||||
updateData.ossEndpoint = ossForm.value.ossEndpoint
|
||||
updateData.ossBucket = ossForm.value.ossBucket
|
||||
updateData.ossDomain = ossForm.value.ossDomain || ''
|
||||
} else if (activeTab.value === 'qcloud') {
|
||||
updateData.qcloudSecretId = ossForm.value.qcloudSecretId
|
||||
updateData.qcloudSecretKey = ossForm.value.qcloudSecretKey
|
||||
updateData.qcloudSecretId = secretOrKeep(ossForm.value.qcloudSecretId, isUpdate)
|
||||
updateData.qcloudSecretKey = secretOrKeep(ossForm.value.qcloudSecretKey, isUpdate)
|
||||
updateData.qcloudRegion = ossForm.value.qcloudRegion
|
||||
updateData.qcloudBucket = ossForm.value.qcloudBucket
|
||||
updateData.qcloudDomain = ossForm.value.qcloudDomain || ''
|
||||
} else if (activeTab.value === 'qiniu') {
|
||||
updateData.qiniuAccessKey = ossForm.value.qiniuAccessKey
|
||||
updateData.qiniuSecretKey = ossForm.value.qiniuSecretKey
|
||||
updateData.qiniuAccessKey = secretOrKeep(ossForm.value.qiniuAccessKey, isUpdate)
|
||||
updateData.qiniuSecretKey = secretOrKeep(ossForm.value.qiniuSecretKey, isUpdate)
|
||||
updateData.qiniuBucket = ossForm.value.qiniuBucket
|
||||
updateData.qiniuRegion = ossForm.value.qiniuRegion || ''
|
||||
updateData.qiniuDomain = ossForm.value.qiniuDomain || ''
|
||||
}
|
||||
|
||||
if (ossForm.value.id > 0) {
|
||||
// 更新
|
||||
if (isUpdate) {
|
||||
await updateOSSConfig(ossForm.value.id, updateData)
|
||||
toast.showToast('OSS配置更新成功', 'success')
|
||||
} else {
|
||||
// 创建
|
||||
await createOSSConfig({
|
||||
...updateData,
|
||||
createdAt: '',
|
||||
@@ -529,6 +546,24 @@ const saveOSSConfig = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const testConnection = async () => {
|
||||
if (!currentConfig.value?.id) {
|
||||
toast.showToast('请先保存配置再测试连接', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
isTesting.value = true
|
||||
try {
|
||||
await testOSSConfig(currentConfig.value.id)
|
||||
toast.showToast('连接测试成功', 'success')
|
||||
} catch (error: any) {
|
||||
console.error('Error testing OSS config:', error)
|
||||
toast.showToast(error.message || '连接测试失败', 'error')
|
||||
} finally {
|
||||
isTesting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const deleteOSSConfig = async () => {
|
||||
if (!currentConfig.value) return
|
||||
|
||||
|
||||
@@ -549,7 +549,7 @@ const uploadMediaFile = async (file: File): Promise<string | null> => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
formData.append('categoryId', '1')
|
||||
formData.append('storageType', 'local')
|
||||
formData.append('storageType', 'aliyun')
|
||||
|
||||
const response = await authFetch('/api/admin/attachments/upload', {
|
||||
method: 'POST',
|
||||
|
||||
@@ -2001,6 +2001,19 @@ export const deleteOSSConfig = async (id: number): Promise<void> => {
|
||||
}
|
||||
}
|
||||
|
||||
export const testOSSConfig = async (id: number): Promise<{ storageType: string; name: string }> => {
|
||||
try {
|
||||
const response = await authFetch(`${API_BASE}/admin/oss-configs/${id}/test`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders()
|
||||
})
|
||||
return await parseApiResponse<{ storageType: string; name: string }>(response)
|
||||
} catch (error) {
|
||||
console.error('Test OSS config error:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 附件相关类型
|
||||
export interface Attachment {
|
||||
id: number
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -29,8 +31,12 @@ func AdminUploadAttachment(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取存储类型
|
||||
storageType := c.DefaultPostForm("storageType", "local")
|
||||
// 获取存储类型:Env 齐全时默认 aliyun
|
||||
defaultStorage := "local"
|
||||
if utils.HasAliyunEnvConfig() {
|
||||
defaultStorage = "aliyun"
|
||||
}
|
||||
storageType := c.DefaultPostForm("storageType", defaultStorage)
|
||||
|
||||
// 打开文件
|
||||
src, err := file.Open()
|
||||
@@ -40,28 +46,33 @@ func AdminUploadAttachment(c *gin.Context) {
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// 获取OSS配置
|
||||
var ossConfig *models.OSSConfig
|
||||
if storageType != "local" {
|
||||
ossConfig, err = repositories.GetActiveOSSConfig(storageType)
|
||||
if err != nil || ossConfig == nil {
|
||||
utils.Error(c, 400, "OSS config not found or not active")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 创建OSS配置对象
|
||||
config := &utils.OSSConfig{
|
||||
StorageType: storageType,
|
||||
}
|
||||
|
||||
if ossConfig != nil {
|
||||
var ossConfig *models.OSSConfig
|
||||
usedEnvConfig := false
|
||||
|
||||
if storageType == "aliyun" {
|
||||
if envCfg, ok := utils.LoadAliyunConfigFromEnv(); ok {
|
||||
config = envCfg
|
||||
usedEnvConfig = true
|
||||
}
|
||||
}
|
||||
|
||||
if !usedEnvConfig && storageType != "local" {
|
||||
ossConfig, err = repositories.GetActiveOSSConfig(storageType)
|
||||
if err != nil || ossConfig == nil {
|
||||
utils.Error(c, 400, "OSS config not found or not active")
|
||||
return
|
||||
}
|
||||
|
||||
// 根据存储类型从专用字段或通用字段读取配置
|
||||
var accessKey, secretKey, bucket, region, domain string
|
||||
|
||||
switch ossConfig.StorageType {
|
||||
case "aliyun":
|
||||
// 优先使用专用字段
|
||||
if ossConfig.OSSAccessKeyID != "" {
|
||||
decrypted, err := utils.DecryptAES(ossConfig.OSSAccessKeyID)
|
||||
if err == nil {
|
||||
@@ -76,7 +87,6 @@ func AdminUploadAttachment(c *gin.Context) {
|
||||
}
|
||||
bucket = ossConfig.OSSBucket
|
||||
domain = ossConfig.OSSDomain
|
||||
// 如果专用字段为空,使用通用字段(向后兼容)
|
||||
if accessKey == "" && ossConfig.AccessKey != "" {
|
||||
decrypted, err := utils.DecryptAES(ossConfig.AccessKey)
|
||||
if err == nil {
|
||||
@@ -96,7 +106,6 @@ func AdminUploadAttachment(c *gin.Context) {
|
||||
domain = ossConfig.Domain
|
||||
}
|
||||
case "qcloud":
|
||||
// 优先使用专用字段
|
||||
if ossConfig.QCloudSecretID != "" {
|
||||
decrypted, err := utils.DecryptAES(ossConfig.QCloudSecretID)
|
||||
if err == nil {
|
||||
@@ -112,7 +121,6 @@ func AdminUploadAttachment(c *gin.Context) {
|
||||
bucket = ossConfig.QCloudBucket
|
||||
region = ossConfig.QCloudRegion
|
||||
domain = ossConfig.QCloudDomain
|
||||
// 向后兼容
|
||||
if accessKey == "" && ossConfig.AccessKey != "" {
|
||||
decrypted, err := utils.DecryptAES(ossConfig.AccessKey)
|
||||
if err == nil {
|
||||
@@ -135,7 +143,6 @@ func AdminUploadAttachment(c *gin.Context) {
|
||||
domain = ossConfig.Domain
|
||||
}
|
||||
case "qiniu":
|
||||
// 优先使用专用字段
|
||||
if ossConfig.QiniuAccessKey != "" {
|
||||
decrypted, err := utils.DecryptAES(ossConfig.QiniuAccessKey)
|
||||
if err == nil {
|
||||
@@ -151,7 +158,6 @@ func AdminUploadAttachment(c *gin.Context) {
|
||||
bucket = ossConfig.QiniuBucket
|
||||
region = ossConfig.QiniuRegion
|
||||
domain = ossConfig.QiniuDomain
|
||||
// 向后兼容
|
||||
if accessKey == "" && ossConfig.AccessKey != "" {
|
||||
decrypted, err := utils.DecryptAES(ossConfig.AccessKey)
|
||||
if err == nil {
|
||||
@@ -174,7 +180,6 @@ func AdminUploadAttachment(c *gin.Context) {
|
||||
domain = ossConfig.Domain
|
||||
}
|
||||
default:
|
||||
// 使用通用字段
|
||||
if ossConfig.AccessKey != "" {
|
||||
decrypted, err := utils.DecryptAES(ossConfig.AccessKey)
|
||||
if err == nil {
|
||||
@@ -198,7 +203,6 @@ func AdminUploadAttachment(c *gin.Context) {
|
||||
config.Region = region
|
||||
config.Domain = domain
|
||||
|
||||
// 填充专用字段
|
||||
switch ossConfig.StorageType {
|
||||
case "aliyun":
|
||||
config.OSSAccessKeyID = accessKey
|
||||
@@ -235,6 +239,11 @@ func AdminUploadAttachment(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
storedName := filepath.Base(filePath)
|
||||
if storedName == "." || storedName == "/" || storedName == "" {
|
||||
storedName = file.Filename
|
||||
}
|
||||
|
||||
// 获取文件类型
|
||||
fileType := utils.GetFileType(file.Header.Get("Content-Type"))
|
||||
|
||||
@@ -242,7 +251,7 @@ func AdminUploadAttachment(c *gin.Context) {
|
||||
attachment := &models.Attachment{
|
||||
CategoryID: categoryID,
|
||||
OriginalName: file.Filename,
|
||||
StoredName: file.Filename,
|
||||
StoredName: storedName,
|
||||
FilePath: filePath,
|
||||
FileURL: fileURL,
|
||||
FileSize: file.Size,
|
||||
@@ -257,7 +266,6 @@ func AdminUploadAttachment(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := repositories.CreateAttachment(attachment); err != nil {
|
||||
// 如果数据库保存失败,尝试删除已上传的文件
|
||||
uploader.Delete(filePath)
|
||||
utils.ServerError(c, err)
|
||||
return
|
||||
@@ -301,11 +309,12 @@ func AdminGetAttachments(c *gin.Context) {
|
||||
// AdminDeleteAttachment 删除附件
|
||||
func AdminDeleteAttachment(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
var id uint
|
||||
if _, err := strconv.ParseUint(idStr, 10, 32); err != nil {
|
||||
parsedID, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.Error(c, 400, "Invalid attachment ID")
|
||||
return
|
||||
}
|
||||
id := uint(parsedID)
|
||||
|
||||
// 获取附件信息
|
||||
attachment, err := repositories.GetAttachmentByID(id)
|
||||
@@ -471,11 +480,12 @@ func AdminDeleteAttachment(c *gin.Context) {
|
||||
// AdminUpdateAttachment 更新附件
|
||||
func AdminUpdateAttachment(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
var id uint
|
||||
if _, err := strconv.ParseUint(idStr, 10, 32); err != nil {
|
||||
parsedID, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.Error(c, 400, "Invalid attachment ID")
|
||||
return
|
||||
}
|
||||
id := uint(parsedID)
|
||||
|
||||
// 获取现有附件
|
||||
attachment, err := repositories.GetAttachmentByID(id)
|
||||
@@ -550,11 +560,12 @@ func AdminCreateAttachmentCategory(c *gin.Context) {
|
||||
// AdminUpdateAttachmentCategory 更新附件分类
|
||||
func AdminUpdateAttachmentCategory(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
var id uint
|
||||
if _, err := strconv.ParseUint(idStr, 10, 32); err != nil {
|
||||
parsedID, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.Error(c, 400, "Invalid category ID")
|
||||
return
|
||||
}
|
||||
id := uint(parsedID)
|
||||
|
||||
var category models.AttachmentCategory
|
||||
if err := c.ShouldBindJSON(&category); err != nil {
|
||||
@@ -575,11 +586,12 @@ func AdminUpdateAttachmentCategory(c *gin.Context) {
|
||||
// AdminDeleteAttachmentCategory 删除附件分类
|
||||
func AdminDeleteAttachmentCategory(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
var id uint
|
||||
if _, err := strconv.ParseUint(idStr, 10, 32); err != nil {
|
||||
parsedID, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.Error(c, 400, "Invalid category ID")
|
||||
return
|
||||
}
|
||||
id := uint(parsedID)
|
||||
|
||||
if err := repositories.DeleteAttachmentCategory(id); err != nil {
|
||||
utils.ServerError(c, err)
|
||||
@@ -779,11 +791,12 @@ func AdminCreateOSSConfig(c *gin.Context) {
|
||||
// AdminUpdateOSSConfig 更新OSS配置
|
||||
func AdminUpdateOSSConfig(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
var id uint
|
||||
if _, err := strconv.ParseUint(idStr, 10, 32); err != nil {
|
||||
parsedID, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.Error(c, 400, "Invalid OSS config ID")
|
||||
return
|
||||
}
|
||||
id := uint(parsedID)
|
||||
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
@@ -827,34 +840,18 @@ func AdminUpdateOSSConfig(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否要启用配置,如果是非local类型,需要验证AccessKey和SecretKey
|
||||
if req.IsActive == 1 && existingConfig.IsActive == 0 {
|
||||
// 检查当前配置的AccessKey和SecretKey(需要解密)
|
||||
var accessKey, secretKey string
|
||||
if existingConfig.AccessKey != "" {
|
||||
decrypted, err := utils.DecryptAES(existingConfig.AccessKey)
|
||||
if err == nil {
|
||||
accessKey = decrypted
|
||||
}
|
||||
}
|
||||
if existingConfig.SecretKey != "" {
|
||||
decrypted, err := utils.DecryptAES(existingConfig.SecretKey)
|
||||
if err == nil {
|
||||
secretKey = decrypted
|
||||
}
|
||||
storageType := existingConfig.StorageType
|
||||
if req.StorageType != "" {
|
||||
storageType = req.StorageType
|
||||
}
|
||||
|
||||
// 如果提供了新的AccessKey或SecretKey,使用新的
|
||||
if req.AccessKey != "" && req.AccessKey != "***" {
|
||||
accessKey = req.AccessKey
|
||||
}
|
||||
if req.SecretKey != "" && req.SecretKey != "***" {
|
||||
secretKey = req.SecretKey
|
||||
}
|
||||
|
||||
// 对于非local类型,必须填写AccessKey和SecretKey
|
||||
if existingConfig.StorageType != "local" && (accessKey == "" || secretKey == "") {
|
||||
utils.Error(c, 400, "启用非本地存储配置前必须填写AccessKey和SecretKey")
|
||||
// 启用非 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)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -867,6 +864,19 @@ func AdminUpdateOSSConfig(c *gin.Context) {
|
||||
return utils.EncryptAES(key)
|
||||
}
|
||||
|
||||
// 密钥字段:*** 或空表示不修改已有值
|
||||
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
|
||||
}
|
||||
|
||||
// 更新字段
|
||||
if req.Name != "" {
|
||||
existingConfig.Name = req.Name
|
||||
@@ -875,88 +885,51 @@ func AdminUpdateOSSConfig(c *gin.Context) {
|
||||
existingConfig.StorageType = req.StorageType
|
||||
}
|
||||
|
||||
// 处理通用字段(向后兼容)
|
||||
if req.AccessKey != "***" {
|
||||
encrypted, err := encryptKey(req.AccessKey)
|
||||
if err != nil {
|
||||
if err := updateSecret(req.AccessKey, &existingConfig.AccessKey); err != nil {
|
||||
utils.Error(c, 500, "Failed to encrypt access key: "+err.Error())
|
||||
return
|
||||
}
|
||||
existingConfig.AccessKey = encrypted
|
||||
}
|
||||
if req.SecretKey != "***" {
|
||||
encrypted, err := encryptKey(req.SecretKey)
|
||||
if err != nil {
|
||||
if err := updateSecret(req.SecretKey, &existingConfig.SecretKey); err != nil {
|
||||
utils.Error(c, 500, "Failed to encrypt secret key: "+err.Error())
|
||||
return
|
||||
}
|
||||
existingConfig.SecretKey = encrypted
|
||||
}
|
||||
|
||||
// 更新通用字段(允许空字符串)
|
||||
existingConfig.Bucket = req.Bucket
|
||||
existingConfig.Region = req.Region
|
||||
existingConfig.Domain = req.Domain
|
||||
|
||||
// 处理阿里云OSS专用字段
|
||||
if req.OSSAccessKeyID != "***" {
|
||||
encrypted, err := encryptKey(req.OSSAccessKeyID)
|
||||
if err != nil {
|
||||
if err := updateSecret(req.OSSAccessKeyID, &existingConfig.OSSAccessKeyID); err != nil {
|
||||
utils.Error(c, 500, "Failed to encrypt OSS access key ID: "+err.Error())
|
||||
return
|
||||
}
|
||||
existingConfig.OSSAccessKeyID = encrypted
|
||||
}
|
||||
if req.OSSAccessKeySecret != "***" {
|
||||
encrypted, err := encryptKey(req.OSSAccessKeySecret)
|
||||
if err != nil {
|
||||
if err := updateSecret(req.OSSAccessKeySecret, &existingConfig.OSSAccessKeySecret); err != nil {
|
||||
utils.Error(c, 500, "Failed to encrypt OSS access key secret: "+err.Error())
|
||||
return
|
||||
}
|
||||
existingConfig.OSSAccessKeySecret = encrypted
|
||||
}
|
||||
existingConfig.OSSEndpoint = req.OSSEndpoint
|
||||
existingConfig.OSSBucket = req.OSSBucket
|
||||
existingConfig.OSSDomain = req.OSSDomain
|
||||
|
||||
// 处理腾讯云COS专用字段
|
||||
if req.QCloudSecretID != "***" {
|
||||
encrypted, err := encryptKey(req.QCloudSecretID)
|
||||
if err != nil {
|
||||
if err := updateSecret(req.QCloudSecretID, &existingConfig.QCloudSecretID); err != nil {
|
||||
utils.Error(c, 500, "Failed to encrypt QCloud secret ID: "+err.Error())
|
||||
return
|
||||
}
|
||||
existingConfig.QCloudSecretID = encrypted
|
||||
}
|
||||
if req.QCloudSecretKey != "***" {
|
||||
encrypted, err := encryptKey(req.QCloudSecretKey)
|
||||
if err != nil {
|
||||
if err := updateSecret(req.QCloudSecretKey, &existingConfig.QCloudSecretKey); err != nil {
|
||||
utils.Error(c, 500, "Failed to encrypt QCloud secret key: "+err.Error())
|
||||
return
|
||||
}
|
||||
existingConfig.QCloudSecretKey = encrypted
|
||||
}
|
||||
existingConfig.QCloudRegion = req.QCloudRegion
|
||||
existingConfig.QCloudBucket = req.QCloudBucket
|
||||
existingConfig.QCloudDomain = req.QCloudDomain
|
||||
|
||||
// 处理七牛云专用字段
|
||||
if req.QiniuAccessKey != "***" {
|
||||
encrypted, err := encryptKey(req.QiniuAccessKey)
|
||||
if err != nil {
|
||||
if err := updateSecret(req.QiniuAccessKey, &existingConfig.QiniuAccessKey); err != nil {
|
||||
utils.Error(c, 500, "Failed to encrypt Qiniu access key: "+err.Error())
|
||||
return
|
||||
}
|
||||
existingConfig.QiniuAccessKey = encrypted
|
||||
}
|
||||
if req.QiniuSecretKey != "***" {
|
||||
encrypted, err := encryptKey(req.QiniuSecretKey)
|
||||
if err != nil {
|
||||
if err := updateSecret(req.QiniuSecretKey, &existingConfig.QiniuSecretKey); err != nil {
|
||||
utils.Error(c, 500, "Failed to encrypt Qiniu secret key: "+err.Error())
|
||||
return
|
||||
}
|
||||
existingConfig.QiniuSecretKey = encrypted
|
||||
}
|
||||
existingConfig.QiniuBucket = req.QiniuBucket
|
||||
existingConfig.QiniuRegion = req.QiniuRegion
|
||||
existingConfig.QiniuDomain = req.QiniuDomain
|
||||
@@ -968,27 +941,19 @@ func AdminUpdateOSSConfig(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 不返回加密的密钥
|
||||
existingConfig.AccessKey = "***"
|
||||
existingConfig.SecretKey = "***"
|
||||
existingConfig.OSSAccessKeyID = "***"
|
||||
existingConfig.OSSAccessKeySecret = "***"
|
||||
existingConfig.QCloudSecretID = "***"
|
||||
existingConfig.QCloudSecretKey = "***"
|
||||
existingConfig.QiniuAccessKey = "***"
|
||||
existingConfig.QiniuSecretKey = "***"
|
||||
|
||||
maskOSSSecrets(existingConfig)
|
||||
utils.SuccessWithMsg(c, "OSS config updated successfully", existingConfig)
|
||||
}
|
||||
|
||||
// AdminDeleteOSSConfig 删除OSS配置
|
||||
func AdminDeleteOSSConfig(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
var id uint
|
||||
if _, err := strconv.ParseUint(idStr, 10, 32); err != nil {
|
||||
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)
|
||||
@@ -1015,3 +980,258 @@ func AdminDeleteOSSConfig(c *gin.Context) {
|
||||
|
||||
utils.SuccessWithMsg(c, "OSS config deleted successfully", nil)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ func main() {
|
||||
repositories.MigrateUserProfileFields()
|
||||
repositories.MigrateVideoModule()
|
||||
repositories.MigratePptTemplates()
|
||||
repositories.MigrateOSSConfigFields()
|
||||
|
||||
// 初始化ip2region (如果文件不存在,将降级为普通IP记录)
|
||||
// 函数会自动从环境变量或可执行文件目录查找 ip2region.xdb
|
||||
@@ -299,6 +300,7 @@ func main() {
|
||||
authAdmin.GET("/oss-configs", middleware.PermissionMiddleware("settings", "read"), handlers.AdminGetOSSConfigs)
|
||||
authAdmin.POST("/oss-configs", middleware.PermissionMiddleware("settings", "create"), handlers.AdminCreateOSSConfig)
|
||||
authAdmin.PUT("/oss-configs/:id", middleware.PermissionMiddleware("settings", "update"), handlers.AdminUpdateOSSConfig)
|
||||
authAdmin.POST("/oss-configs/:id/test", middleware.PermissionMiddleware("settings", "update"), handlers.AdminTestOSSConfig)
|
||||
authAdmin.DELETE("/oss-configs/:id", middleware.PermissionMiddleware("settings", "delete"), handlers.AdminDeleteOSSConfig)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,6 +220,21 @@ CREATE TABLE `oss_configs` (
|
||||
`bucket` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '存储桶名称',
|
||||
`region` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '区域',
|
||||
`domain` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '访问域名',
|
||||
`oss_access_key_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '阿里云 AccessKeyId(AES加密)',
|
||||
`oss_access_key_secret` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '阿里云 AccessKeySecret(AES加密)',
|
||||
`oss_endpoint` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '阿里云 Endpoint',
|
||||
`oss_bucket` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '阿里云 Bucket',
|
||||
`oss_domain` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '阿里云自定义域名',
|
||||
`qcloud_secret_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '腾讯云 SecretId(AES加密)',
|
||||
`qcloud_secret_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '腾讯云 SecretKey(AES加密)',
|
||||
`qcloud_region` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '腾讯云 Region',
|
||||
`qcloud_bucket` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '腾讯云 Bucket',
|
||||
`qcloud_domain` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '腾讯云自定义域名',
|
||||
`qiniu_access_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '七牛 AccessKey(AES加密)',
|
||||
`qiniu_secret_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '七牛 SecretKey(AES加密)',
|
||||
`qiniu_bucket` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '七牛 Bucket',
|
||||
`qiniu_region` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '七牛 Region',
|
||||
`qiniu_domain` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '七牛自定义域名',
|
||||
`is_active` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否启用',
|
||||
`deleted_at` bigint NOT NULL DEFAULT 0,
|
||||
`created_at` bigint NOT NULL DEFAULT 0,
|
||||
|
||||
@@ -272,6 +272,21 @@ func UpdateOSSConfig(ossConfig *models.OSSConfig) error {
|
||||
"bucket": ossConfig.Bucket,
|
||||
"region": ossConfig.Region,
|
||||
"domain": ossConfig.Domain,
|
||||
"oss_access_key_id": ossConfig.OSSAccessKeyID,
|
||||
"oss_access_key_secret": ossConfig.OSSAccessKeySecret,
|
||||
"oss_endpoint": ossConfig.OSSEndpoint,
|
||||
"oss_bucket": ossConfig.OSSBucket,
|
||||
"oss_domain": ossConfig.OSSDomain,
|
||||
"qcloud_secret_id": ossConfig.QCloudSecretID,
|
||||
"qcloud_secret_key": ossConfig.QCloudSecretKey,
|
||||
"qcloud_region": ossConfig.QCloudRegion,
|
||||
"qcloud_bucket": ossConfig.QCloudBucket,
|
||||
"qcloud_domain": ossConfig.QCloudDomain,
|
||||
"qiniu_access_key": ossConfig.QiniuAccessKey,
|
||||
"qiniu_secret_key": ossConfig.QiniuSecretKey,
|
||||
"qiniu_bucket": ossConfig.QiniuBucket,
|
||||
"qiniu_region": ossConfig.QiniuRegion,
|
||||
"qiniu_domain": ossConfig.QiniuDomain,
|
||||
"is_active": ossConfig.IsActive,
|
||||
"updated_at": time.Now().Unix(),
|
||||
}).Error
|
||||
|
||||
@@ -322,6 +322,44 @@ func MigratePptTemplates() {
|
||||
EnsureSystemPptTemplate()
|
||||
}
|
||||
|
||||
// MigrateOSSConfigFields 为 oss_configs 补齐各云厂商专用列(幂等)
|
||||
func MigrateOSSConfigFields() {
|
||||
log.Printf("Migrating oss_configs provider fields...")
|
||||
|
||||
if !tableExists("oss_configs") {
|
||||
log.Printf("Table oss_configs does not exist, skip MigrateOSSConfigFields")
|
||||
return
|
||||
}
|
||||
|
||||
columns := []struct {
|
||||
name string
|
||||
ddl string
|
||||
}{
|
||||
{"oss_access_key_id", "ALTER TABLE `oss_configs` ADD COLUMN `oss_access_key_id` varchar(255) NOT NULL DEFAULT '' COMMENT '阿里云 AccessKeyId(AES加密)' AFTER `domain`"},
|
||||
{"oss_access_key_secret", "ALTER TABLE `oss_configs` ADD COLUMN `oss_access_key_secret` varchar(255) NOT NULL DEFAULT '' COMMENT '阿里云 AccessKeySecret(AES加密)' AFTER `oss_access_key_id`"},
|
||||
{"oss_endpoint", "ALTER TABLE `oss_configs` ADD COLUMN `oss_endpoint` varchar(255) NOT NULL DEFAULT '' COMMENT '阿里云 Endpoint' AFTER `oss_access_key_secret`"},
|
||||
{"oss_bucket", "ALTER TABLE `oss_configs` ADD COLUMN `oss_bucket` varchar(255) NOT NULL DEFAULT '' COMMENT '阿里云 Bucket' AFTER `oss_endpoint`"},
|
||||
{"oss_domain", "ALTER TABLE `oss_configs` ADD COLUMN `oss_domain` varchar(255) NOT NULL DEFAULT '' COMMENT '阿里云自定义域名' AFTER `oss_bucket`"},
|
||||
{"qcloud_secret_id", "ALTER TABLE `oss_configs` ADD COLUMN `qcloud_secret_id` varchar(255) NOT NULL DEFAULT '' COMMENT '腾讯云 SecretId(AES加密)' AFTER `oss_domain`"},
|
||||
{"qcloud_secret_key", "ALTER TABLE `oss_configs` ADD COLUMN `qcloud_secret_key` varchar(255) NOT NULL DEFAULT '' COMMENT '腾讯云 SecretKey(AES加密)' AFTER `qcloud_secret_id`"},
|
||||
{"qcloud_region", "ALTER TABLE `oss_configs` ADD COLUMN `qcloud_region` varchar(255) NOT NULL DEFAULT '' COMMENT '腾讯云 Region' AFTER `qcloud_secret_key`"},
|
||||
{"qcloud_bucket", "ALTER TABLE `oss_configs` ADD COLUMN `qcloud_bucket` varchar(255) NOT NULL DEFAULT '' COMMENT '腾讯云 Bucket' AFTER `qcloud_region`"},
|
||||
{"qcloud_domain", "ALTER TABLE `oss_configs` ADD COLUMN `qcloud_domain` varchar(255) NOT NULL DEFAULT '' COMMENT '腾讯云自定义域名' AFTER `qcloud_bucket`"},
|
||||
{"qiniu_access_key", "ALTER TABLE `oss_configs` ADD COLUMN `qiniu_access_key` varchar(255) NOT NULL DEFAULT '' COMMENT '七牛 AccessKey(AES加密)' AFTER `qcloud_domain`"},
|
||||
{"qiniu_secret_key", "ALTER TABLE `oss_configs` ADD COLUMN `qiniu_secret_key` varchar(255) NOT NULL DEFAULT '' COMMENT '七牛 SecretKey(AES加密)' AFTER `qiniu_access_key`"},
|
||||
{"qiniu_bucket", "ALTER TABLE `oss_configs` ADD COLUMN `qiniu_bucket` varchar(255) NOT NULL DEFAULT '' COMMENT '七牛 Bucket' AFTER `qiniu_secret_key`"},
|
||||
{"qiniu_region", "ALTER TABLE `oss_configs` ADD COLUMN `qiniu_region` varchar(255) NOT NULL DEFAULT '' COMMENT '七牛 Region' AFTER `qiniu_bucket`"},
|
||||
{"qiniu_domain", "ALTER TABLE `oss_configs` ADD COLUMN `qiniu_domain` varchar(255) NOT NULL DEFAULT '' COMMENT '七牛自定义域名' AFTER `qiniu_region`"},
|
||||
}
|
||||
|
||||
for _, col := range columns {
|
||||
if !columnExists("oss_configs", col.name) {
|
||||
log.Printf("Adding oss_configs.%s...", col.name)
|
||||
execSQL(col.ddl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ensurePptTemplateColumnComments 为已存在但缺少字段备注的 ppt_templates 表补全 COMMENT
|
||||
func ensurePptTemplateColumnComments() {
|
||||
if !tableExists("ppt_templates") {
|
||||
|
||||
146
server/scripts/migrate_oss_configs.sql
Normal file
146
server/scripts/migrate_oss_configs.sql
Normal file
@@ -0,0 +1,146 @@
|
||||
-- ============================================================
|
||||
-- OSS 配置表专用字段迁移脚本
|
||||
-- 文件:server/scripts/migrate_oss_configs.sql
|
||||
-- 说明:可重复执行(幂等),为 oss_configs 补齐阿里云/腾讯云/七牛专用列
|
||||
-- 用法:mysql -u root -p your_database < scripts/migrate_oss_configs.sql
|
||||
-- ============================================================
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- 阿里云 OSS
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'oss_configs' AND COLUMN_NAME = 'oss_access_key_id'
|
||||
);
|
||||
SET @sql := IF(@col_exists = 0,
|
||||
'ALTER TABLE `oss_configs` ADD COLUMN `oss_access_key_id` varchar(255) NOT NULL DEFAULT '''' COMMENT ''阿里云 AccessKeyId(AES加密)'' AFTER `domain`',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'oss_configs' AND COLUMN_NAME = 'oss_access_key_secret'
|
||||
);
|
||||
SET @sql := IF(@col_exists = 0,
|
||||
'ALTER TABLE `oss_configs` ADD COLUMN `oss_access_key_secret` varchar(255) NOT NULL DEFAULT '''' COMMENT ''阿里云 AccessKeySecret(AES加密)'' AFTER `oss_access_key_id`',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'oss_configs' AND COLUMN_NAME = 'oss_endpoint'
|
||||
);
|
||||
SET @sql := IF(@col_exists = 0,
|
||||
'ALTER TABLE `oss_configs` ADD COLUMN `oss_endpoint` varchar(255) NOT NULL DEFAULT '''' COMMENT ''阿里云 Endpoint'' AFTER `oss_access_key_secret`',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'oss_configs' AND COLUMN_NAME = 'oss_bucket'
|
||||
);
|
||||
SET @sql := IF(@col_exists = 0,
|
||||
'ALTER TABLE `oss_configs` ADD COLUMN `oss_bucket` varchar(255) NOT NULL DEFAULT '''' COMMENT ''阿里云 Bucket'' AFTER `oss_endpoint`',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'oss_configs' AND COLUMN_NAME = 'oss_domain'
|
||||
);
|
||||
SET @sql := IF(@col_exists = 0,
|
||||
'ALTER TABLE `oss_configs` ADD COLUMN `oss_domain` varchar(255) NOT NULL DEFAULT '''' COMMENT ''阿里云自定义域名'' AFTER `oss_bucket`',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 腾讯云 COS
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'oss_configs' AND COLUMN_NAME = 'qcloud_secret_id'
|
||||
);
|
||||
SET @sql := IF(@col_exists = 0,
|
||||
'ALTER TABLE `oss_configs` ADD COLUMN `qcloud_secret_id` varchar(255) NOT NULL DEFAULT '''' COMMENT ''腾讯云 SecretId(AES加密)'' AFTER `oss_domain`',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'oss_configs' AND COLUMN_NAME = 'qcloud_secret_key'
|
||||
);
|
||||
SET @sql := IF(@col_exists = 0,
|
||||
'ALTER TABLE `oss_configs` ADD COLUMN `qcloud_secret_key` varchar(255) NOT NULL DEFAULT '''' COMMENT ''腾讯云 SecretKey(AES加密)'' AFTER `qcloud_secret_id`',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'oss_configs' AND COLUMN_NAME = 'qcloud_region'
|
||||
);
|
||||
SET @sql := IF(@col_exists = 0,
|
||||
'ALTER TABLE `oss_configs` ADD COLUMN `qcloud_region` varchar(255) NOT NULL DEFAULT '''' COMMENT ''腾讯云 Region'' AFTER `qcloud_secret_key`',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'oss_configs' AND COLUMN_NAME = 'qcloud_bucket'
|
||||
);
|
||||
SET @sql := IF(@col_exists = 0,
|
||||
'ALTER TABLE `oss_configs` ADD COLUMN `qcloud_bucket` varchar(255) NOT NULL DEFAULT '''' COMMENT ''腾讯云 Bucket'' AFTER `qcloud_region`',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'oss_configs' AND COLUMN_NAME = 'qcloud_domain'
|
||||
);
|
||||
SET @sql := IF(@col_exists = 0,
|
||||
'ALTER TABLE `oss_configs` ADD COLUMN `qcloud_domain` varchar(255) NOT NULL DEFAULT '''' COMMENT ''腾讯云自定义域名'' AFTER `qcloud_bucket`',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 七牛云
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'oss_configs' AND COLUMN_NAME = 'qiniu_access_key'
|
||||
);
|
||||
SET @sql := IF(@col_exists = 0,
|
||||
'ALTER TABLE `oss_configs` ADD COLUMN `qiniu_access_key` varchar(255) NOT NULL DEFAULT '''' COMMENT ''七牛 AccessKey(AES加密)'' AFTER `qcloud_domain`',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'oss_configs' AND COLUMN_NAME = 'qiniu_secret_key'
|
||||
);
|
||||
SET @sql := IF(@col_exists = 0,
|
||||
'ALTER TABLE `oss_configs` ADD COLUMN `qiniu_secret_key` varchar(255) NOT NULL DEFAULT '''' COMMENT ''七牛 SecretKey(AES加密)'' AFTER `qiniu_access_key`',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'oss_configs' AND COLUMN_NAME = 'qiniu_bucket'
|
||||
);
|
||||
SET @sql := IF(@col_exists = 0,
|
||||
'ALTER TABLE `oss_configs` ADD COLUMN `qiniu_bucket` varchar(255) NOT NULL DEFAULT '''' COMMENT ''七牛 Bucket'' AFTER `qiniu_secret_key`',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'oss_configs' AND COLUMN_NAME = 'qiniu_region'
|
||||
);
|
||||
SET @sql := IF(@col_exists = 0,
|
||||
'ALTER TABLE `oss_configs` ADD COLUMN `qiniu_region` varchar(255) NOT NULL DEFAULT '''' COMMENT ''七牛 Region'' AFTER `qiniu_bucket`',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'oss_configs' AND COLUMN_NAME = 'qiniu_domain'
|
||||
);
|
||||
SET @sql := IF(@col_exists = 0,
|
||||
'ALTER TABLE `oss_configs` ADD COLUMN `qiniu_domain` varchar(255) NOT NULL DEFAULT '''' COMMENT ''七牛自定义域名'' AFTER `qiniu_region`',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
@@ -4,51 +4,59 @@ import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// AESKey 从环境变量或配置中获取,这里使用默认密钥(生产环境应该从配置读取)
|
||||
var defaultAESKey = []byte("your-32-byte-secret-key-here!!") // 32 bytes for AES-256
|
||||
// defaultAESKey 必须精确为 16/24/32 字节;此处为 AES-256(32 字节)
|
||||
var defaultAESKey = []byte("art-code-blog-aes256-key-32b!!!!") // len == 32
|
||||
|
||||
// GetAESKey 获取AES密钥(应该从配置文件或环境变量读取)
|
||||
// GetAESKey 获取 AES 密钥:优先 AES_ENCRYPTION_KEY,否则使用默认密钥;长度非法时用 SHA-256 规范化为 32 字节
|
||||
func GetAESKey() []byte {
|
||||
// TODO: 从配置文件或环境变量读取密钥
|
||||
// key := os.Getenv("AES_ENCRYPTION_KEY")
|
||||
// if key == "" {
|
||||
// return defaultAESKey
|
||||
// }
|
||||
// return []byte(key)
|
||||
return defaultAESKey
|
||||
key := strings.TrimSpace(os.Getenv("AES_ENCRYPTION_KEY"))
|
||||
if key == "" {
|
||||
return normalizeAESKey(defaultAESKey)
|
||||
}
|
||||
return normalizeAESKey([]byte(key))
|
||||
}
|
||||
|
||||
func normalizeAESKey(key []byte) []byte {
|
||||
switch len(key) {
|
||||
case 16, 24, 32:
|
||||
return key
|
||||
default:
|
||||
sum := sha256.Sum256(key)
|
||||
out := make([]byte, 32)
|
||||
copy(out, sum[:])
|
||||
return out
|
||||
}
|
||||
}
|
||||
|
||||
// EncryptAES 使用AES-256-GCM加密数据
|
||||
func EncryptAES(plaintext string) (string, error) {
|
||||
key := GetAESKey()
|
||||
|
||||
// Create cipher block
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Create GCM
|
||||
aesGCM, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Create nonce
|
||||
nonce := make([]byte, aesGCM.NonceSize())
|
||||
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Encrypt
|
||||
ciphertext := aesGCM.Seal(nonce, nonce, []byte(plaintext), nil)
|
||||
|
||||
// Encode to base64
|
||||
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
@@ -56,25 +64,21 @@ func EncryptAES(plaintext string) (string, error) {
|
||||
func DecryptAES(encrypted string) (string, error) {
|
||||
key := GetAESKey()
|
||||
|
||||
// Decode from base64
|
||||
ciphertext, err := base64.StdEncoding.DecodeString(encrypted)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Create cipher block
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Create GCM
|
||||
aesGCM, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Extract nonce
|
||||
nonceSize := aesGCM.NonceSize()
|
||||
if len(ciphertext) < nonceSize {
|
||||
return "", errors.New("ciphertext too short")
|
||||
@@ -82,7 +86,6 @@ func DecryptAES(encrypted string) (string, error) {
|
||||
|
||||
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
|
||||
|
||||
// Decrypt
|
||||
plaintext, err := aesGCM.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -3,6 +3,8 @@ package utils
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
@@ -64,6 +66,63 @@ type OSSUploader interface {
|
||||
Delete(filePath string) error
|
||||
}
|
||||
|
||||
// NormalizeOSSEndpoint 补全 endpoint 协议前缀
|
||||
func NormalizeOSSEndpoint(endpoint string) string {
|
||||
endpoint = strings.TrimSpace(endpoint)
|
||||
if endpoint == "" {
|
||||
return ""
|
||||
}
|
||||
if !strings.HasPrefix(endpoint, "http://") && !strings.HasPrefix(endpoint, "https://") {
|
||||
return "https://" + endpoint
|
||||
}
|
||||
return endpoint
|
||||
}
|
||||
|
||||
// LoadAliyunConfigFromEnv 从环境变量加载阿里云 OSS 配置;前 4 项齐全返回 true
|
||||
func LoadAliyunConfigFromEnv() (*OSSConfig, bool) {
|
||||
accessKeyID := strings.TrimSpace(os.Getenv("OSS_ACCESS_KEY_ID"))
|
||||
accessKeySecret := strings.TrimSpace(os.Getenv("OSS_ACCESS_KEY_SECRET"))
|
||||
endpoint := NormalizeOSSEndpoint(os.Getenv("OSS_ENDPOINT"))
|
||||
bucket := strings.TrimSpace(os.Getenv("OSS_BUCKET"))
|
||||
domain := strings.TrimSpace(os.Getenv("OSS_DOMAIN"))
|
||||
|
||||
if accessKeyID == "" || accessKeySecret == "" || endpoint == "" || bucket == "" {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return &OSSConfig{
|
||||
StorageType: string(StorageAliyun),
|
||||
OSSAccessKeyID: accessKeyID,
|
||||
OSSAccessKeySecret: accessKeySecret,
|
||||
OSSEndpoint: endpoint,
|
||||
OSSBucket: bucket,
|
||||
OSSDomain: domain,
|
||||
}, true
|
||||
}
|
||||
|
||||
// HasAliyunEnvConfig 环境变量中是否具备完整的阿里云 OSS 配置
|
||||
func HasAliyunEnvConfig() bool {
|
||||
_, ok := LoadAliyunConfigFromEnv()
|
||||
return ok
|
||||
}
|
||||
|
||||
// BuildBlogObjectKey 生成博客上传对象键:blog/{YYYYMMDD}/{随机名}{ext}
|
||||
func BuildBlogObjectKey(originalFilename string) string {
|
||||
ext := strings.ToLower(filepath.Ext(originalFilename))
|
||||
randomName := randomHex(16)
|
||||
dateDir := time.Now().Format("20060102")
|
||||
return fmt.Sprintf("blog/%s/%s%s", dateDir, randomName, ext)
|
||||
}
|
||||
|
||||
func randomHex(nBytes int) string {
|
||||
buf := make([]byte, nBytes)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
// 退化到时间戳,仍保证文件名唯一性
|
||||
return fmt.Sprintf("%d", time.Now().UnixNano())
|
||||
}
|
||||
return hex.EncodeToString(buf)
|
||||
}
|
||||
|
||||
// GetOSSUploader 根据存储类型获取上传器
|
||||
func GetOSSUploader(config *OSSConfig) (OSSUploader, error) {
|
||||
switch StorageType(config.StorageType) {
|
||||
@@ -158,6 +217,7 @@ func GetOSSUploader(config *OSSConfig) (OSSUploader, error) {
|
||||
}
|
||||
endpoint = fmt.Sprintf("https://oss-%s.aliyuncs.com", region)
|
||||
}
|
||||
endpoint = NormalizeOSSEndpoint(endpoint)
|
||||
|
||||
client, err := oss.New(endpoint, accessKeyID, accessKeySecret)
|
||||
if err != nil {
|
||||
@@ -262,39 +322,25 @@ type LocalUploader struct {
|
||||
|
||||
// Upload 上传文件到本地
|
||||
func (l *LocalUploader) Upload(file multipart.File, filename string, size int64) (string, string, error) {
|
||||
// 生成唯一文件名
|
||||
ext := filepath.Ext(filename)
|
||||
timestamp := time.Now().Unix()
|
||||
randomStr := fmt.Sprintf("%d", timestamp)
|
||||
newFilename := fmt.Sprintf("%s_%s%s", strings.TrimSuffix(filename, ext), randomStr, ext)
|
||||
objectKey := BuildBlogObjectKey(filename)
|
||||
filePath := filepath.Join(l.BasePath, filepath.FromSlash(objectKey))
|
||||
uploadDir := filepath.Dir(filePath)
|
||||
|
||||
// 按日期创建目录
|
||||
dateDir := time.Now().Format("2006/01/02")
|
||||
uploadDir := filepath.Join(l.BasePath, dateDir)
|
||||
|
||||
// 创建目录
|
||||
if err := os.MkdirAll(uploadDir, 0755); err != nil {
|
||||
return "", "", fmt.Errorf("failed to create upload directory: %v", err)
|
||||
}
|
||||
|
||||
// 完整文件路径
|
||||
filePath := filepath.Join(uploadDir, newFilename)
|
||||
|
||||
// 创建目标文件
|
||||
dst, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to create file: %v", err)
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
// 复制文件内容
|
||||
if _, err := io.Copy(dst, file); err != nil {
|
||||
return "", "", fmt.Errorf("failed to copy file: %v", err)
|
||||
}
|
||||
|
||||
// 生成访问URL
|
||||
fileURL := fmt.Sprintf("%s/%s/%s", l.BaseURL, dateDir, newFilename)
|
||||
|
||||
fileURL := fmt.Sprintf("%s/%s", strings.TrimSuffix(l.BaseURL, "/"), objectKey)
|
||||
return filePath, fileURL, nil
|
||||
}
|
||||
|
||||
@@ -326,30 +372,19 @@ type AliyunUploader struct {
|
||||
|
||||
// Upload 上传文件到阿里云OSS
|
||||
func (a *AliyunUploader) Upload(file multipart.File, filename string, size int64) (string, string, error) {
|
||||
// 生成唯一文件名
|
||||
ext := filepath.Ext(filename)
|
||||
timestamp := time.Now().Unix()
|
||||
randomStr := fmt.Sprintf("%d", timestamp)
|
||||
newFilename := fmt.Sprintf("%s_%s%s", strings.TrimSuffix(filename, ext), randomStr, ext)
|
||||
objectKey := BuildBlogObjectKey(filename)
|
||||
|
||||
// 按日期创建目录
|
||||
dateDir := time.Now().Format("2006/01/02")
|
||||
objectKey := fmt.Sprintf("%s/%s", dateDir, newFilename)
|
||||
|
||||
// 上传文件
|
||||
err := a.Bucket.PutObject(objectKey, file)
|
||||
err := a.Bucket.PutObject(objectKey, file, oss.ObjectACL(oss.ACLPublicRead))
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to upload to aliyun oss: %v", err)
|
||||
}
|
||||
|
||||
// 生成访问URL
|
||||
var fileURL string
|
||||
if a.Domain != "" {
|
||||
fileURL = fmt.Sprintf("%s/%s", strings.TrimSuffix(a.Domain, "/"), objectKey)
|
||||
fileURL = fmt.Sprintf("%s/%s", strings.TrimSuffix(NormalizeOSSEndpoint(a.Domain), "/"), objectKey)
|
||||
} else {
|
||||
// 从endpoint中提取region,格式为 https://oss-region.aliyuncs.com
|
||||
endpoint := a.Bucket.Client.Config.Endpoint
|
||||
fileURL = fmt.Sprintf("%s/%s", endpoint, objectKey)
|
||||
endpoint := NormalizeOSSEndpoint(a.Bucket.Client.Config.Endpoint)
|
||||
fileURL = fmt.Sprintf("%s/%s", strings.TrimSuffix(endpoint, "/"), objectKey)
|
||||
}
|
||||
|
||||
return objectKey, fileURL, nil
|
||||
@@ -364,6 +399,42 @@ func (a *AliyunUploader) Delete(objectKey string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestOSSConnection 验证云存储配置是否可用(ListObjects / 等价只读探测)
|
||||
func TestOSSConnection(config *OSSConfig) error {
|
||||
uploader, err := GetOSSUploader(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch u := uploader.(type) {
|
||||
case *LocalUploader:
|
||||
if err := os.MkdirAll(u.BasePath, 0755); err != nil {
|
||||
return fmt.Errorf("local storage path not writable: %v", err)
|
||||
}
|
||||
return nil
|
||||
case *AliyunUploader:
|
||||
_, err := u.Bucket.ListObjects(oss.MaxKeys(1))
|
||||
if err != nil {
|
||||
return fmt.Errorf("aliyun oss connection failed: %v", err)
|
||||
}
|
||||
return nil
|
||||
case *QCloudUploader:
|
||||
_, _, err := u.Client.Bucket.Get(context.Background(), &cos.BucketGetOptions{MaxKeys: 1})
|
||||
if err != nil {
|
||||
return fmt.Errorf("qcloud cos connection failed: %v", err)
|
||||
}
|
||||
return nil
|
||||
case *QiniuUploader:
|
||||
_, _, _, _, err := u.BucketManager.ListFiles(u.Bucket, "", "", "", 1)
|
||||
if err != nil {
|
||||
return fmt.Errorf("qiniu connection failed: %v", err)
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unsupported storage type: %s", config.StorageType)
|
||||
}
|
||||
}
|
||||
|
||||
// QCloudUploader 腾讯云COS上传器
|
||||
type QCloudUploader struct {
|
||||
Client *cos.Client
|
||||
@@ -373,29 +444,18 @@ type QCloudUploader struct {
|
||||
|
||||
// Upload 上传文件到腾讯云COS
|
||||
func (q *QCloudUploader) Upload(file multipart.File, filename string, size int64) (string, string, error) {
|
||||
// 生成唯一文件名
|
||||
ext := filepath.Ext(filename)
|
||||
timestamp := time.Now().Unix()
|
||||
randomStr := fmt.Sprintf("%d", timestamp)
|
||||
newFilename := fmt.Sprintf("%s_%s%s", strings.TrimSuffix(filename, ext), randomStr, ext)
|
||||
objectKey := BuildBlogObjectKey(filename)
|
||||
|
||||
// 按日期创建目录
|
||||
dateDir := time.Now().Format("2006/01/02")
|
||||
objectKey := fmt.Sprintf("%s/%s", dateDir, newFilename)
|
||||
|
||||
// 上传文件
|
||||
_, err := q.Client.Object.Put(context.Background(), objectKey, file, nil)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to upload to qcloud cos: %v", err)
|
||||
}
|
||||
|
||||
// 生成访问URL
|
||||
var fileURL string
|
||||
if q.Domain != "" {
|
||||
fileURL = fmt.Sprintf("%s/%s", strings.TrimSuffix(q.Domain, "/"), objectKey)
|
||||
} else {
|
||||
// 使用BucketURL生成URL
|
||||
fileURL = fmt.Sprintf("%s/%s", q.Client.BaseURL.BucketURL.String(), objectKey)
|
||||
fileURL = fmt.Sprintf("%s/%s", strings.TrimSuffix(q.Client.BaseURL.BucketURL.String(), "/"), objectKey)
|
||||
}
|
||||
|
||||
return objectKey, fileURL, nil
|
||||
@@ -421,17 +481,8 @@ type QiniuUploader struct {
|
||||
|
||||
// Upload 上传文件到七牛云
|
||||
func (q *QiniuUploader) Upload(file multipart.File, filename string, size int64) (string, string, error) {
|
||||
// 生成唯一文件名
|
||||
ext := filepath.Ext(filename)
|
||||
timestamp := time.Now().Unix()
|
||||
randomStr := fmt.Sprintf("%d", timestamp)
|
||||
newFilename := fmt.Sprintf("%s_%s%s", strings.TrimSuffix(filename, ext), randomStr, ext)
|
||||
key := BuildBlogObjectKey(filename)
|
||||
|
||||
// 按日期创建目录
|
||||
dateDir := time.Now().Format("2006/01/02")
|
||||
key := fmt.Sprintf("%s/%s", dateDir, newFilename)
|
||||
|
||||
// 生成上传凭证
|
||||
putPolicy := storage.PutPolicy{
|
||||
Scope: q.Bucket,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user