Merge branch 'feature-upload-daiyb' into dev

This commit is contained in:
戴育兵 2025-12-17 14:45:26 +08:00
commit e2abc91367
8 changed files with 518 additions and 19 deletions

View File

@ -1,10 +1,15 @@
[system] [system]
Domain = "fiee" Domain = "app"
AppMode = "debug" AppMode = "dev"
HttpPort = ":8085" HttpPort = ":8085"
Host = "http://127.0.0.1:8085" Host = "https://common.szjixun.cn"
RedirectUri = "/api/redirect/url" RedirectUri = "/api/redirect/url"
ErpHost = "http://erpapi.test.fontree.cn:8081"
FieeHost = "http://erpapi.test.fontree.cn:8081"
AuthRedirectUrl = "http://saas-erp.test.fontree.cn:8081/media_account"
AuthCallback = "https://saas-test.szjixun.cn/api/fiee/media/as-oauth2callback"
CronOpen = true
proxyUrl = "http://47.84.75.255:6785"
[bos] [bos]
Ak = "ALTAKxrqOQHnAN525Tb2GX4Bhe" Ak = "ALTAKxrqOQHnAN525Tb2GX4Bhe"
Sk = "d2ecaa9d75114d3b9f42b99014198306" Sk = "d2ecaa9d75114d3b9f42b99014198306"
@ -23,7 +28,7 @@ CdnHost = "${OSS_CDN}"
[redis] [redis]
RedisDB = "2" RedisDB = "2"
RedisAddr = "127.0.0.1:6379" RedisAddr = "127.0.0.1:6379"
RedisPW = "" RedisPW = "7532T6R"
RedisDBNAme = "2" RedisDBNAme = "2"

View File

@ -2,10 +2,27 @@ dubbo:
registries: registries:
demoZK: demoZK:
protocol: zookeeper protocol: zookeeper
timeout: 5s timeout: 3s
# address: 121.229.45.214:9004
address: 127.0.0.1:2181 address: 127.0.0.1:2181
# address: 127.0.0.1:2181
# address: 114.218.158.24:2181
consumer: consumer:
filter: tracing
request-timeout: 300s
references: references:
OrderClientImpl:
protocol: tri
retries: 0
interface: com.fontree.microservices.common.order # must be compatible with grpc or dubbo-java
# filter: cshutdown,sign,fonDomainFilter,fonValidateFilter
params:
.accessKeyId: "SYD8-order-04"
.secretAccessKey: "Al-order-FDF112"
BundleClientImpl:
protocol: tri
retries: 0
interface: com.fontree.microservices.fiee.bundle # must be compatible with grpc or dubbo-java
AccountClientImpl: AccountClientImpl:
protocol: tri protocol: tri
retries: 0 retries: 0
@ -14,19 +31,62 @@ dubbo:
params: params:
.accessKeyId: "Accountksl" .accessKeyId: "Accountksl"
.secretAccessKey: "BSDY-FDF1-Fontree_account" .secretAccessKey: "BSDY-FDF1-Fontree_account"
AccountFieeClientImpl: AccountFieeClientImpl:
protocol: tri protocol: tri
retries: 0 retries: 3
interface: com.fontree.microservices.common.micro.account.fiee interface: com.fontree.microservices.common.micro.account.fiee
# filter: cshutdown,sign,fonDomainFilter,fonValidateFilter # filter: echo,metrics,token,accesslog,sign,tps,generic_service,execute,pshutdown,auth,fonValidateFilter
# params: PaymentCentClientImpl:
# .accessKeyId: "Accountksl"
# .secretAccessKey: "BSDY-FDF1-Fontree_account"
BundleClientImpl:
protocol: tri protocol: tri
retries: 0 retries: 0
interface: com.fontree.microservices.fiee.bundle # must be compatible with grpc or dubbo-java interface: com.fontree.microservices.common.payment.cent # must be compatible with grpc or dubbo-java
CastClientImpl: CastClientImpl:
protocol: tri protocol: tri
interface: com.fontree.microservices.fiee.multicast interface: com.fontree.microservices.fiee.multicast
SecFilingsClientImpl:
protocol: tri
retries: 0
interface: com.fontree.microservices.fiee.SecFiling
AyrshareClientImpl:
protocol: tri
interface: com.fontree.microservices.fiee.ayrshare
logger:
zap-config:
level: error # 日志级别
development: false
disableCaller: false
disableStacktrace: false
encoding: "json"
# zap encoder 配置
encoderConfig:
messageKey: "message"
levelKey: "level"
timeKey: "time"
nameKey: "logger"
callerKey: "caller"
stacktraceKey: "stacktrace"
lineEnding: ""
levelEncoder: "capitalColor"
timeEncoder: "iso8601"
durationEncoder: "seconds"
callerEncoder: "short"
nameEncoder: ""
EncodeTime: zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05.000"),
EncodeDuration: zapcore.SecondsDurationEncoder,
outputPaths:
- "stderr"
errorOutputPaths:
- "stderr"
lumberjack-config:
# 写日志的文件名称
filename: "runtime/logs/fiee.log"
# 每个日志文件长度的最大大小,单位是 MiB。默认 100MiB
maxSize: 5
# 日志保留的最大天数(只保留最近多少天的日志)
maxAge: 30
# 只保留最近多少个日志文件,用于控制程序总日志的大小
maxBackups: 30
# 是否使用本地时间,默认使用 UTC 时间
localTime: true
# 是否压缩日志文件,压缩方法 gzip
compress: false

View File

@ -0,0 +1,315 @@
package cast
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"fonchain-fiee/pkg/utils"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"time"
"go.uber.org/zap"
)
const (
// Ayrshare API 配置
ayrshareAPIBaseURL = "https://api.ayrshare.com/api"
maxSmallFileSize = 30 * 1024 * 1024 // 30MB
apiKey = "208CBE9F-8E4F426A-990A0184-2C6287B9"
)
// UploadMediaResponse 上传媒体文件响应
type UploadMediaResponse struct {
ID string `json:"id"`
URL string `json:"url"`
FileName string `json:"fileName"`
Description string `json:"description"`
IsAs bool `json:"isAs"`
}
// UploadURLResponse 获取上传URL响应
type UploadURLResponse struct {
AccessURL string `json:"accessUrl"`
ContentType string `json:"contentType"`
UploadURL string `json:"uploadUrl"`
}
// VerifyURLResponse 验证URL响应
type VerifyURLResponse struct {
Status string `json:"status"`
StatusCode int `json:"statusCode"`
ContentType string `json:"contentType"`
}
// UploadMediaByURL 根据文件大小自动选择上传方式
// fileURL: 文件的URL链接
// apiKey: Ayrshare API Key
// fileName: 文件名(可选)
// description: 文件描述(可选)
func UploadMediaByURL(ctx context.Context, fileURL, fileName, description string) (*UploadMediaResponse, error) {
if fileURL == "" {
return nil, errors.New("文件URL不能为空")
}
ok, err := VerifyMediaURL(ctx, fileURL)
if err != nil {
return nil, err
}
if ok {
return &UploadMediaResponse{
ID: "",
URL: fileURL,
FileName: "",
Description: "",
IsAs: false,
}, nil
}
// 下载文件到临时目录
tempFile, fileSize, err := downloadFile(ctx, fileURL)
if err != nil {
return nil, fmt.Errorf("下载文件失败: %v", err)
}
defer os.Remove(tempFile) // 清理临时文件
zap.L().Info("文件下载完成", zap.String("tempFile", tempFile), zap.Int64("fileSize", fileSize))
// 如果没有提供文件名从URL中提取
if fileName == "" {
fileName = filepath.Base(fileURL)
}
// 根据文件大小选择上传方式
if fileSize < maxSmallFileSize {
zap.L().Info("使用小文件上传方式", zap.Int64("fileSize", fileSize))
return uploadSmallMedia(ctx, tempFile, apiKey, fileName, description)
}
zap.L().Info("使用大文件上传方式", zap.Int64("fileSize", fileSize))
return uploadLargeMedia(ctx, tempFile, apiKey, fileName, description)
}
// downloadFile 下载文件到临时目录
func downloadFile(ctx context.Context, fileURL string) (string, int64, error) {
req, err := http.NewRequestWithContext(ctx, "GET", fileURL, nil)
if err != nil {
return "", 0, err
}
client := &http.Client{Timeout: 10 * time.Minute}
resp, err := client.Do(req)
if err != nil {
return "", 0, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", 0, fmt.Errorf("下载文件失败HTTP状态码: %d", resp.StatusCode)
}
// 创建临时文件
tempFile, err := os.CreateTemp("", "ayrshare_upload_*")
if err != nil {
return "", 0, err
}
defer tempFile.Close()
// 复制内容到临时文件
fileSize, err := io.Copy(tempFile, resp.Body)
if err != nil {
os.Remove(tempFile.Name())
return "", 0, err
}
return tempFile.Name(), fileSize, nil
}
// uploadSmallMedia 上传小于30MB的文件使用multipart form-data
func uploadSmallMedia(ctx context.Context, filePath, apiKey, fileName, description string) (*UploadMediaResponse, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("打开文件失败: %v", err)
}
defer file.Close()
// 创建multipart form-data
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
// 添加文件
part, err := writer.CreateFormFile("file", fileName)
if err != nil {
return nil, err
}
if _, err := io.Copy(part, file); err != nil {
return nil, err
}
// 添加其他字段
if fileName != "" {
_ = writer.WriteField("fileName", fileName)
}
if description != "" {
_ = writer.WriteField("description", description)
}
if err := writer.Close(); err != nil {
return nil, err
}
// 发送请求
url := fmt.Sprintf("%s/media/upload", ayrshareAPIBaseURL)
req, err := http.NewRequestWithContext(ctx, "POST", url, body)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
req.Header.Set("Content-Type", writer.FormDataContentType())
client := &http.Client{Timeout: 10 * time.Minute}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("请求失败: %v", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应失败: %v", err)
}
if resp.StatusCode != http.StatusOK {
zap.L().Error("上传小文件失败", zap.Int("statusCode", resp.StatusCode), zap.String("response", string(respBody)))
return nil, fmt.Errorf("上传失败: HTTP %d, %s", resp.StatusCode, string(respBody))
}
var result UploadMediaResponse
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("解析响应失败: %v", err)
}
zap.L().Info("小文件上传成功", zap.Any("response", result))
return &result, nil
}
// uploadLargeMedia 上传大于30MB的文件使用presigned URL
func uploadLargeMedia(ctx context.Context, filePath, apiKey, fileName, description string) (*UploadMediaResponse, error) {
// 获取文件的content type
ext := filepath.Ext(fileName)
if len(ext) > 0 {
ext = ext[1:] // 去掉点号
}
// Step 1: 获取上传URL
uploadURLResp, err := getUploadURL(ctx, apiKey, fileName, ext)
if err != nil {
return nil, fmt.Errorf("获取上传URL失败: %v", err)
}
zap.L().Info("获取上传URL成功", zap.Any("uploadURLResp", uploadURLResp))
// Step 2: 上传文件到presigned URL
file, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("打开文件失败: %v", err)
}
defer file.Close()
req, err := http.NewRequestWithContext(ctx, "PUT", uploadURLResp.UploadURL, file)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", uploadURLResp.ContentType)
client := &http.Client{Timeout: 30 * time.Minute}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("上传文件失败: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
respBody, _ := io.ReadAll(resp.Body)
zap.L().Error("上传大文件失败", zap.Int("statusCode", resp.StatusCode), zap.String("response", string(respBody)))
return nil, fmt.Errorf("上传失败: HTTP %d", resp.StatusCode)
}
zap.L().Info("大文件上传成功")
// 返回结果
return &UploadMediaResponse{
URL: uploadURLResp.AccessURL,
FileName: fileName,
Description: description,
}, nil
}
// getUploadURL 获取大文件上传的presigned URL
func getUploadURL(ctx context.Context, apiKey, fileName, contentType string) (*UploadURLResponse, error) {
url := fmt.Sprintf("%s/media/uploadUrl?fileName=%s", ayrshareAPIBaseURL, fileName)
if contentType != "" {
url = fmt.Sprintf("%s&contentType=%s", url, contentType)
}
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
zap.L().Error("获取上传URL失败", zap.Int("statusCode", resp.StatusCode), zap.String("response", string(respBody)))
return nil, fmt.Errorf("获取上传URL失败: HTTP %d, %s", resp.StatusCode, string(respBody))
}
var result UploadURLResponse
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("解析响应失败: %v", err)
}
return &result, nil
}
// VerifyMediaURL 验证媒体URL是否有效
func VerifyMediaURL(ctx context.Context, mediaURL string) (bool, error) {
url := fmt.Sprintf("%s/media/urlExists", ayrshareAPIBaseURL)
requestBody := map[string]string{
"mediaUrl": mediaURL,
}
jsonData, _ := json.Marshal(requestBody)
code, postBody, err := utils.PostBytesHeader(url, map[string]interface{}{
"Content-Type": "application/json",
"Authorization": "Bearer " + apiKey,
}, jsonData)
if err != nil {
zap.L().Error("VerifyMediaURL 提交异常", zap.Error(err))
return false, errors.New("验证链接提交异常")
}
if code != http.StatusOK {
return false, nil
}
var result *VerifyURLResponse
_ = json.Unmarshal(postBody, &result)
if result.StatusCode != http.StatusOK || result.Status != "success" {
return false, nil
}
return true, nil
}

View File

@ -99,6 +99,26 @@ func Test(ctx *gin.Context) {
service.Success(ctx, resp) service.Success(ctx, resp)
return return
} }
if action == "upload" {
fileUrl := ctx.PostForm("fileUrl")
uploadResp, err := UploadMediaByURL(ctx, fileUrl, "", "")
if err != nil {
service.Error(ctx, err)
return
}
service.Success(ctx, uploadResp)
return
}
if action == "getUrl" {
fileUrl := ctx.PostForm("fileUrl")
uploadResp, err := VerifyMediaURL(ctx, fileUrl)
if err != nil {
service.Error(ctx, err)
return
}
service.Success(ctx, uploadResp)
return
}
service.Success(ctx, "unknow") service.Success(ctx, "unknow")
return return
} }

View File

@ -453,11 +453,37 @@ func PostAS(workUuids []string) (errs []error) {
var isVideo bool var isVideo bool
if workDetail.WorkCategory == 1 { if workDetail.WorkCategory == 1 {
isVideo = false isVideo = false
mediaUrls = workDetail.Images for _, imageUrl := range workDetail.Images {
urlResp, err := UploadMediaByURL(context.Background(), imageUrl, "", "")
if err != nil {
zap.L().Error("Publish UploadMediaByURL failed", zap.String("imageUrl", imageUrl), zap.Error(err))
continue
}
mediaUrls = append(mediaUrls, urlResp.URL)
}
//mediaUrls = workDetail.Images
} }
var coverUrl string
if workDetail.WorkCategory == 2 { if workDetail.WorkCategory == 2 {
isVideo = true isVideo = true
mediaUrls = []string{workDetail.VideoUrl} //mediaUrls = []string{workDetail.VideoUrl}
urlResp, err := UploadMediaByURL(context.Background(), workDetail.VideoUrl, "", "")
if err != nil {
zap.L().Error("Publish UploadMediaByURL failed", zap.String("VideoUrl", workDetail.VideoUrl), zap.Error(err))
continue
}
mediaUrls = []string{urlResp.URL}
if workDetail.CoverUrl != "" {
urlResp, err = UploadMediaByURL(context.Background(), workDetail.VideoUrl, "", "")
if err != nil {
zap.L().Error("Publish UploadMediaByURL failed", zap.String("VideoUrl", workDetail.VideoUrl), zap.Error(err))
continue
}
coverUrl = urlResp.URL
}
}
if len(mediaUrls) == 0 {
continue
} }
ArtistInfoResp, _err := service.CastProvider.GetArtist(context.Background(), &cast.GetArtistReq{ ArtistInfoResp, _err := service.CastProvider.GetArtist(context.Background(), &cast.GetArtistReq{
ArtistUuid: workDetail.ArtistUuid, ArtistUuid: workDetail.ArtistUuid,
@ -470,7 +496,6 @@ func PostAS(workUuids []string) (errs []error) {
errs = append(errs, errors.New("艺人平台信息未配置")) errs = append(errs, errors.New("艺人平台信息未配置"))
continue continue
} }
for _, platformID := range needPlatformIDs { for _, platformID := range needPlatformIDs {
var postResp *aryshare.PostResponse = &aryshare.PostResponse{} var postResp *aryshare.PostResponse = &aryshare.PostResponse{}
postReq := &aryshare.PostRequest{ postReq := &aryshare.PostRequest{
@ -489,7 +514,7 @@ func PostAS(workUuids []string) (errs []error) {
postReq.InstagramOptions = &aryshare.InstagramOptions{ postReq.InstagramOptions = &aryshare.InstagramOptions{
ShareReelsFeed: false, ShareReelsFeed: false,
AudioName: "", AudioName: "",
ThumbNail: workDetail.CoverUrl, ThumbNail: coverUrl,
ThumbNailOffset: 0, ThumbNailOffset: 0,
Stories: false, Stories: false,
AltText: nil, AltText: nil,

44
pkg/utils/files.go Normal file
View File

@ -0,0 +1,44 @@
package utils
import (
"errors"
"fmt"
"fonchain-fiee/pkg/e"
"io"
"net/http"
"os"
"strings"
"go.uber.org/zap"
)
// SaveUrlFileDisk 保存图片到本地
func SaveUrlFileDisk(url string, path string, filename string) (fullPath string, err error) {
if err = CreateDirPath(path); err != nil {
zap.L().Error("SaveUrlFileDisk err ", zap.Error(err))
return
}
if filename == "" {
stepName := strings.Split(url, "/")
if len(stepName) > 1 {
filename = stepName[len(stepName)-1]
}
}
resp, err := http.Get(url)
if err != nil {
err = errors.New(e.GetMsg(e.ERROR_DOWNLOAD_FILE))
return
}
defer func() {
if err := recover(); err != nil {
}
resp.Body.Close()
}()
bytes, err := io.ReadAll(resp.Body)
fullPath = fmt.Sprintf("%s/%s", path, filename)
// 写入数据
err = os.WriteFile(fullPath, bytes, 0777)
return
}

View File

@ -133,3 +133,24 @@ func GetUrl(apiUrl string, headerData map[string]string, proxyURL ...string) (st
zap.L().Info("Get", zap.Any("url", apiUrl), zap.Any("body", body)) zap.L().Info("Get", zap.Any("url", apiUrl), zap.Any("body", body))
return return
} }
func PostBytesHeader(url string, header map[string]interface{}, data []byte) (int, []byte, error) {
req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
if err != nil {
return 0, nil, fmt.Errorf("创建请求失败: %v", "")
}
for k, v := range header {
req.Header.Set(k, fmt.Sprintf("%v", v))
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return 0, nil, fmt.Errorf("请求失败: %v", "")
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return 0, nil, fmt.Errorf("读取响应失败: %v", "")
}
return resp.StatusCode, body, nil
}

View File

@ -12,6 +12,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"path"
"path/filepath" "path/filepath"
"time" "time"
@ -163,3 +164,11 @@ func CopyFile(src, dstDir string) (string, error) {
} }
return dst, nil return dst, nil
} }
func FileNameFromURL(rawURL string) (string, error) {
u, err := url.Parse(rawURL)
if err != nil {
return "", err
}
return path.Base(u.Path), nil
}