feat:修复合并问题

This commit is contained in:
liulong 2026-06-03 23:22:22 +08:00
parent b9527bee70
commit 6ff8743c72
4 changed files with 39 additions and 35 deletions

View File

@ -6,7 +6,7 @@ SERVER_PORT=8080
# ==================== JWT Configuration ==================== # ==================== JWT Configuration ====================
# JWT密钥 - 生产环境请修改为安全的随机字符串 # JWT密钥 - 生产环境请修改为安全的随机字符串
JWT_SECRET=topfans-secret-key-please-change-in-production JWT_SECRET=
# ==================== Dubbo Service URLs ==================== # ==================== Dubbo Service URLs ====================
# 各微服务的Dubbo连接地址直连模式 # 各微服务的Dubbo连接地址直连模式

View File

@ -1408,23 +1408,15 @@ func (ctrl *AssetController) buildOSSPrefix(uploadType string, userID, starID in
return fmt.Sprintf("%s%d/%d/", baseDir, userID, starID) return fmt.Sprintf("%s%d/%d/", baseDir, userID, starID)
} }
// generatePresignedURL 使用STS临时凭证生成预签名URL // generatePresignedURL 生成预签名URL(优先 STS失败降级到永久 AK/SK 直连)
func (ctrl *AssetController) generatePresignedURL( func (ctrl *AssetController) generatePresignedURL(
ossConfig config.OSSConfig, ossConfig config.OSSConfig,
filePath string, filePath string,
expiresInSeconds int64, expiresInSeconds int64,
) (string, error) { ) (string, error) {
// 1. 获取STS临时凭证 // 尝试 STS AssumeRole失败则降级到永久 AccessKey 直连
// 注意STS 的 DurationSeconds 最小 15 分钟900秒最大 1 小时3600秒 var accessKeyID, accessKeySecret, securityToken string
// 但预签名 URL 的过期时间可以更长,由 OSS SDK 的 SignURL 方法控制 useSTS := false
// 所以我们需要限制 STS token 的过期时间,但预签名 URL 可以使用更长的过期时间
stsExpiration := expiresInSeconds
if stsExpiration > 3600 {
stsExpiration = 3600 // STS 最大支持 1 小时
}
if stsExpiration < 900 {
stsExpiration = 900 // STS 最小支持 15 分钟
}
credConfig := new(credentials.Config). credConfig := new(credentials.Config).
SetType("ram_role_arn"). SetType("ram_role_arn").
@ -1433,34 +1425,47 @@ func (ctrl *AssetController) generatePresignedURL(
SetRoleArn(ossConfig.RoleArn). SetRoleArn(ossConfig.RoleArn).
SetRoleSessionName("topfans-download-session"). SetRoleSessionName("topfans-download-session").
SetPolicy(""). SetPolicy("").
SetRoleSessionExpiration(int(stsExpiration)) SetRoleSessionExpiration(3600)
provider, err := credentials.NewCredential(credConfig) if provider, err := credentials.NewCredential(credConfig); err == nil {
if err != nil { if cred, err := provider.GetCredential(); err == nil {
return "", fmt.Errorf("创建凭证提供器失败: %w", err) accessKeyID = *cred.AccessKeyId
accessKeySecret = *cred.AccessKeySecret
securityToken = *cred.SecurityToken
useSTS = true
} else {
logger.Logger.Warn("STS AssumeRole failed for presigned URL, falling back to direct AK/SK", zap.Error(err))
}
} else {
logger.Logger.Warn("STS credential provider failed for presigned URL, falling back to direct AK/SK", zap.Error(err))
} }
cred, err := provider.GetCredential() if !useSTS {
if err != nil { accessKeyID = ossConfig.AccessKeyID
return "", fmt.Errorf("获取临时凭证失败: %w", err) accessKeySecret = ossConfig.AccessKeySecret
} }
// 2. 创建OSS客户端使用临时凭证 // 创建OSS客户端
endpoint := fmt.Sprintf("https://oss-%s.aliyuncs.com", ossConfig.Region) endpoint := fmt.Sprintf("https://oss-%s.aliyuncs.com", ossConfig.Region)
client, err := oss.New(endpoint, *cred.AccessKeyId, *cred.AccessKeySecret, var client *oss.Client
oss.SecurityToken(*cred.SecurityToken)) var err error
if useSTS && securityToken != "" {
client, err = oss.New(endpoint, accessKeyID, accessKeySecret, oss.SecurityToken(securityToken))
} else {
client, err = oss.New(endpoint, accessKeyID, accessKeySecret)
}
if err != nil { if err != nil {
return "", fmt.Errorf("创建OSS客户端失败: %w", err) return "", fmt.Errorf("创建OSS客户端失败: %w", err)
} }
// 3. 获取Bucket // 获取Bucket
bucket, err := client.Bucket(ossConfig.BucketName) bucket, err := client.Bucket(ossConfig.BucketName)
if err != nil { if err != nil {
return "", fmt.Errorf("获取Bucket失败: %w", err) return "", fmt.Errorf("获取Bucket失败: %w", err)
} }
// 4. 生成预签名URL // 生成预签名URL
signedURL, err := bucket.SignURL(filePath, oss.HTTPGet, expiresInSeconds) signedURL, err := bucket.SignURL(filePath, oss.HTTPGet, expiresInSeconds)
if err != nil { if err != nil {
logger.Logger.Error("OSS SignURL failed", logger.Logger.Error("OSS SignURL failed",
@ -1471,7 +1476,7 @@ func (ctrl *AssetController) generatePresignedURL(
return "", fmt.Errorf("生成预签名URL失败: %w", err) return "", fmt.Errorf("生成预签名URL失败: %w", err)
} }
// 5. 修复 path 的 URL 编码OSS SDK 的 buildURL 用 QueryEscape 把 / 编成 %2F // 修复 path 的 URL 编码OSS SDK 的 buildURL 用 QueryEscape 把 / 编成 %2F
// 导致 OSS 按字面 key "asset%2F18%2F88%2Fxxx" 查找失败(403)。只把 path 段(? 之前)的 %2F 改回 /。 // 导致 OSS 按字面 key "asset%2F18%2F88%2Fxxx" 查找失败(403)。只把 path 段(? 之前)的 %2F 改回 /。
if idx := strings.Index(signedURL, "?"); idx >= 0 { if idx := strings.Index(signedURL, "?"); idx >= 0 {
signedURL = strings.ReplaceAll(signedURL[:idx], "%2F", "/") + signedURL[idx:] signedURL = strings.ReplaceAll(signedURL[:idx], "%2F", "/") + signedURL[idx:]
@ -1479,9 +1484,9 @@ func (ctrl *AssetController) generatePresignedURL(
signedURL = strings.ReplaceAll(signedURL, "%2F", "/") signedURL = strings.ReplaceAll(signedURL, "%2F", "/")
} }
// 6. 若 SDK 未把 STS 的 security-token 加入 URL则手动追加使用 STS 临时凭证时,预签名 URL 必须带此参数,否则 403 // 若 SDK 未把 STS 的 security-token 加入 URL则手动追加使用 STS 临时凭证时,预签名 URL 必须带此参数,否则 403
if !strings.Contains(signedURL, "security-token") && cred.SecurityToken != nil && *cred.SecurityToken != "" { if useSTS && securityToken != "" && !strings.Contains(signedURL, "security-token") {
signedURL = signedURL + "&security-token=" + url.QueryEscape(*cred.SecurityToken) signedURL = signedURL + "&security-token=" + url.QueryEscape(securityToken)
} }
// 检查生成的预签名 URL 是否包含 security-token 参数 // 检查生成的预签名 URL 是否包含 security-token 参数
@ -1493,8 +1498,8 @@ func (ctrl *AssetController) generatePresignedURL(
} }
tokenPreview := "" tokenPreview := ""
if cred.SecurityToken != nil && *cred.SecurityToken != "" { if securityToken != "" {
token := *cred.SecurityToken token := securityToken
if len(token) > 50 { if len(token) > 50 {
tokenPreview = token[:50] + "..." tokenPreview = token[:50] + "..."
} else { } else {

View File

@ -967,7 +967,7 @@ func syncAssetsIDSequence(tx *gorm.DB) error {
return tx.Exec(` return tx.Exec(`
SELECT setval( SELECT setval(
pg_get_serial_sequence('assets', 'id'), pg_get_serial_sequence('assets', 'id'),
COALESCE((SELECT MAX(id) FROM assets), 0) GREATEST(COALESCE((SELECT MAX(id) FROM assets), 1), 1)
) )
`).Error `).Error
} }

View File

@ -2,9 +2,8 @@
// 自动检测后端环境:探测开发服务器是否可用,能连通则用开发地址,否则用生产地址 // 自动检测后端环境:探测开发服务器是否可用,能连通则用开发地址,否则用生产地址
// 团队开发机Gateway + 团队数据库;注册/登录/余额/铸造) // 团队开发机Gateway + 团队数据库;注册/登录/余额/铸造)
const DEV_BASE = 'http://192.168.110.60:8080' // const DEV_BASE = 'http://192.168.110.60:8080'
// 本机 Gateway所有接口走本地 const DEV_BASE = 'http://localhost:8081'
// const DEV_BASE = 'http://localhost:8081'
const SEGMENT_BASE = 'http://localhost:8081' const SEGMENT_BASE = 'http://localhost:8081'
const LASER_BASE = 'http://localhost:8081' // 镭射 AI 生成 + compositor 走本地 Gateway const LASER_BASE = 'http://localhost:8081' // 镭射 AI 生成 + compositor 走本地 Gateway
const PROD_BASE = 'http://101.132.250.62:8080' // 生产环境 const PROD_BASE = 'http://101.132.250.62:8080' // 生产环境