Return signed OSS URLs for site images
This commit is contained in:
@@ -31,3 +31,4 @@ APP_OSS_PREFIX=aftersales-confirmations
|
|||||||
APP_OSS_PUBLIC_BASE_URL=
|
APP_OSS_PUBLIC_BASE_URL=
|
||||||
APP_OSS_MAX_FILE_SIZE_MB=5
|
APP_OSS_MAX_FILE_SIZE_MB=5
|
||||||
APP_OSS_MAX_FILES=6
|
APP_OSS_MAX_FILES=6
|
||||||
|
APP_OSS_SIGNED_URL_EXPIRE=3600
|
||||||
|
|||||||
@@ -32,3 +32,4 @@ oss:
|
|||||||
public_base_url: ""
|
public_base_url: ""
|
||||||
max_file_size_mb: 5
|
max_file_size_mb: 5
|
||||||
max_files: 6
|
max_files: 6
|
||||||
|
signed_url_expire: 3600
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ type OSSConfig struct {
|
|||||||
PublicBaseURL string `mapstructure:"public_base_url"`
|
PublicBaseURL string `mapstructure:"public_base_url"`
|
||||||
MaxFileSizeMB int `mapstructure:"max_file_size_mb"`
|
MaxFileSizeMB int `mapstructure:"max_file_size_mb"`
|
||||||
MaxFiles int `mapstructure:"max_files"`
|
MaxFiles int `mapstructure:"max_files"`
|
||||||
|
SignedURLExpire int `mapstructure:"signed_url_expire"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppConfig 应用程序配置
|
// AppConfig 应用程序配置
|
||||||
@@ -153,6 +154,7 @@ func setDefaults() {
|
|||||||
viper.SetDefault("oss.public_base_url", "")
|
viper.SetDefault("oss.public_base_url", "")
|
||||||
viper.SetDefault("oss.max_file_size_mb", 5)
|
viper.SetDefault("oss.max_file_size_mb", 5)
|
||||||
viper.SetDefault("oss.max_files", 6)
|
viper.SetDefault("oss.max_files", 6)
|
||||||
|
viper.SetDefault("oss.signed_url_expire", 3600)
|
||||||
}
|
}
|
||||||
|
|
||||||
// bindEnvVariables 绑定环境变量
|
// bindEnvVariables 绑定环境变量
|
||||||
@@ -189,6 +191,7 @@ func bindEnvVariables() {
|
|||||||
viper.BindEnv("oss.public_base_url")
|
viper.BindEnv("oss.public_base_url")
|
||||||
viper.BindEnv("oss.max_file_size_mb")
|
viper.BindEnv("oss.max_file_size_mb")
|
||||||
viper.BindEnv("oss.max_files")
|
viper.BindEnv("oss.max_files")
|
||||||
|
viper.BindEnv("oss.signed_url_expire")
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateConfig 验证配置
|
// validateConfig 验证配置
|
||||||
|
|||||||
@@ -146,7 +146,19 @@ func hydrateAftersalesOrder(order *models.AftersalesOrder) {
|
|||||||
if order == nil {
|
if order == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
order.SiteImages = parseSiteImages(order.SiteImagesJSON)
|
order.SiteImages = siteImageAccessURLs(parseSiteImages(order.SiteImagesJSON))
|
||||||
|
}
|
||||||
|
|
||||||
|
func siteImageAccessURLs(images []string) []string {
|
||||||
|
if len(images) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ossService := NewOSSService()
|
||||||
|
urls := make([]string, 0, len(images))
|
||||||
|
for _, image := range images {
|
||||||
|
urls = append(urls, ossService.AccessURL(image))
|
||||||
|
}
|
||||||
|
return urls
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateUniqueSerial 生成唯一的售后工单序列号
|
// generateUniqueSerial 生成唯一的售后工单序列号
|
||||||
@@ -385,7 +397,7 @@ func (s *AftersalesService) PublicQuery(serialNumber string) (*models.Aftersales
|
|||||||
ConfirmedAt: order.ConfirmedAt,
|
ConfirmedAt: order.ConfirmedAt,
|
||||||
Signature: order.Signature,
|
Signature: order.Signature,
|
||||||
ResponsibleSignature: order.ResponsibleSignature,
|
ResponsibleSignature: order.ResponsibleSignature,
|
||||||
SiteImages: parseSiteImages(order.SiteImagesJSON),
|
SiteImages: siteImageAccessURLs(parseSiteImages(order.SiteImagesJSON)),
|
||||||
}
|
}
|
||||||
if order.Technician != nil {
|
if order.Technician != nil {
|
||||||
view.TechnicianName = order.Technician.Name
|
view.TechnicianName = order.Technician.Name
|
||||||
@@ -393,7 +405,7 @@ func (s *AftersalesService) PublicQuery(serialNumber string) (*models.Aftersales
|
|||||||
return view, nil
|
return view, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UploadSiteImages 上传客户确认现场图片到 OSS 并保存 URL。
|
// UploadSiteImages 上传客户确认现场图片到 OSS 并保存 object key。
|
||||||
func (s *AftersalesService) UploadSiteImages(serialNumber string, files []*multipart.FileHeader) ([]string, error) {
|
func (s *AftersalesService) UploadSiteImages(serialNumber string, files []*multipart.FileHeader) ([]string, error) {
|
||||||
normalized := normalizeAftersalesSerial(serialNumber)
|
normalized := normalizeAftersalesSerial(serialNumber)
|
||||||
if len(files) == 0 {
|
if len(files) == 0 {
|
||||||
@@ -425,7 +437,7 @@ func (s *AftersalesService) UploadSiteImages(serialNumber string, files []*multi
|
|||||||
}
|
}
|
||||||
|
|
||||||
ossService := NewOSSService()
|
ossService := NewOSSService()
|
||||||
uploaded := make([]string, 0, len(files))
|
uploadedKeys := make([]string, 0, len(files))
|
||||||
for _, fh := range files {
|
for _, fh := range files {
|
||||||
if fh.Size <= 0 {
|
if fh.Size <= 0 {
|
||||||
return nil, fmt.Errorf("图片 %s 内容为空", fh.Filename)
|
return nil, fmt.Errorf("图片 %s 内容为空", fh.Filename)
|
||||||
@@ -445,21 +457,21 @@ func (s *AftersalesService) UploadSiteImages(serialNumber string, files []*multi
|
|||||||
}
|
}
|
||||||
|
|
||||||
objectKey := buildSiteImageObjectKey(cfg.Prefix, normalized, fh.Filename)
|
objectKey := buildSiteImageObjectKey(cfg.Prefix, normalized, fh.Filename)
|
||||||
url, err := ossService.UploadObject(objectKey, file, contentType)
|
err = ossService.UploadObject(objectKey, file, contentType)
|
||||||
_ = file.Close()
|
_ = file.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
uploaded = append(uploaded, url)
|
uploadedKeys = append(uploadedKeys, objectKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
merged := append(existing, uploaded...)
|
merged := append(existing, uploadedKeys...)
|
||||||
order.SiteImagesJSON = encodeSiteImages(merged)
|
order.SiteImagesJSON = encodeSiteImages(merged)
|
||||||
if err := database.DB.Save(&order).Error; err != nil {
|
if err := database.DB.Save(&order).Error; err != nil {
|
||||||
return nil, fmt.Errorf("保存现场图片失败: %w", err)
|
return nil, fmt.Errorf("保存现场图片失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return merged, nil
|
return siteImageAccessURLs(merged), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildSiteImageObjectKey(prefix string, serialNumber string, filename string) string {
|
func buildSiteImageObjectKey(prefix string, serialNumber string, filename string) string {
|
||||||
|
|||||||
+66
-6
@@ -20,19 +20,19 @@ func NewOSSService() OSSService {
|
|||||||
return OSSService{cfg: config.GetAppConfig().OSS}
|
return OSSService{cfg: config.GetAppConfig().OSS}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s OSSService) UploadObject(objectKey string, reader io.Reader, contentType string) (string, error) {
|
func (s OSSService) UploadObject(objectKey string, reader io.Reader, contentType string) error {
|
||||||
if err := s.validateConfig(); err != nil {
|
if err := s.validateConfig(); err != nil {
|
||||||
return "", err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := oss.New(normalizeOSSEndpoint(s.cfg.Endpoint), s.cfg.AccessKeyID, s.cfg.AccessKeySecret)
|
client, err := oss.New(normalizeOSSEndpoint(s.cfg.Endpoint), s.cfg.AccessKeyID, s.cfg.AccessKeySecret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("初始化 OSS 客户端失败: %w", err)
|
return fmt.Errorf("初始化 OSS 客户端失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
bucket, err := client.Bucket(s.cfg.Bucket)
|
bucket, err := client.Bucket(s.cfg.Bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("打开 OSS Bucket 失败: %w", err)
|
return fmt.Errorf("打开 OSS Bucket 失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
options := []oss.Option{}
|
options := []oss.Option{}
|
||||||
@@ -40,10 +40,10 @@ func (s OSSService) UploadObject(objectKey string, reader io.Reader, contentType
|
|||||||
options = append(options, oss.ContentType(contentType))
|
options = append(options, oss.ContentType(contentType))
|
||||||
}
|
}
|
||||||
if err := bucket.PutObject(objectKey, reader, options...); err != nil {
|
if err := bucket.PutObject(objectKey, reader, options...); err != nil {
|
||||||
return "", fmt.Errorf("上传现场图片失败: %w", err)
|
return fmt.Errorf("上传现场图片失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.publicURL(objectKey), nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s OSSService) validateConfig() error {
|
func (s OSSService) validateConfig() error {
|
||||||
@@ -68,6 +68,66 @@ func (s OSSService) publicURL(objectKey string) string {
|
|||||||
return fmt.Sprintf("%s://%s.%s/%s", u.Scheme, s.cfg.Bucket, u.Host, objectKey)
|
return fmt.Sprintf("%s://%s.%s/%s", u.Scheme, s.cfg.Bucket, u.Host, objectKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s OSSService) AccessURL(objectRef string) string {
|
||||||
|
objectKey := s.objectKeyFromRef(objectRef)
|
||||||
|
if objectKey == "" {
|
||||||
|
return objectRef
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(s.cfg.PublicBaseURL) != "" {
|
||||||
|
return s.publicURL(objectKey)
|
||||||
|
}
|
||||||
|
url, err := s.SignedURL(objectKey)
|
||||||
|
if err != nil {
|
||||||
|
return s.publicURL(objectKey)
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s OSSService) SignedURL(objectKey string) (string, error) {
|
||||||
|
if err := s.validateConfig(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
client, err := oss.New(normalizeOSSEndpoint(s.cfg.Endpoint), s.cfg.AccessKeyID, s.cfg.AccessKeySecret)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("初始化 OSS 客户端失败: %w", err)
|
||||||
|
}
|
||||||
|
bucket, err := client.Bucket(s.cfg.Bucket)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("打开 OSS Bucket 失败: %w", err)
|
||||||
|
}
|
||||||
|
expires := s.cfg.SignedURLExpire
|
||||||
|
if expires <= 0 {
|
||||||
|
expires = 3600
|
||||||
|
}
|
||||||
|
return bucket.SignURL(objectKey, oss.HTTPGet, int64(expires))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s OSSService) objectKeyFromRef(objectRef string) string {
|
||||||
|
objectRef = strings.TrimSpace(objectRef)
|
||||||
|
if objectRef == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(objectRef, "http://") && !strings.HasPrefix(objectRef, "https://") {
|
||||||
|
return strings.TrimLeft(objectRef, "/")
|
||||||
|
}
|
||||||
|
u, err := url.Parse(objectRef)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
host := strings.ToLower(u.Host)
|
||||||
|
bucketHostPrefix := strings.ToLower(s.cfg.Bucket) + "."
|
||||||
|
if strings.HasPrefix(host, bucketHostPrefix) {
|
||||||
|
return strings.TrimLeft(u.Path, "/")
|
||||||
|
}
|
||||||
|
if base := strings.TrimSpace(s.cfg.PublicBaseURL); base != "" {
|
||||||
|
baseURL, err := url.Parse(base)
|
||||||
|
if err == nil && strings.EqualFold(baseURL.Host, u.Host) {
|
||||||
|
return strings.TrimLeft(strings.TrimPrefix(u.Path, baseURL.Path), "/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeOSSEndpoint(endpoint string) string {
|
func normalizeOSSEndpoint(endpoint string) string {
|
||||||
endpoint = strings.TrimSpace(endpoint)
|
endpoint = strings.TrimSpace(endpoint)
|
||||||
if endpoint == "" {
|
if endpoint == "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user