281 lines
8.3 KiB
Go
281 lines
8.3 KiB
Go
|
|
package handler
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"bytes"
|
|||
|
|
"fmt"
|
|||
|
|
"image/gif"
|
|||
|
|
"image/jpeg"
|
|||
|
|
"image/png"
|
|||
|
|
"io"
|
|||
|
|
"math/rand"
|
|||
|
|
"net/http"
|
|||
|
|
"os"
|
|||
|
|
"path/filepath"
|
|||
|
|
"strconv"
|
|||
|
|
"strings"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"github.com/gin-gonic/gin"
|
|||
|
|
"gorm.io/gorm"
|
|||
|
|
|
|||
|
|
"soul-api/internal/database"
|
|||
|
|
"soul-api/internal/model"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
const (
|
|||
|
|
uploadDirContent = "uploads"
|
|||
|
|
maxImageBytes = 5 * 1024 * 1024 // 5MB
|
|||
|
|
maxVideoBytes = 100 * 1024 * 1024 // 100MB
|
|||
|
|
defaultImageQuality = 85
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
var (
|
|||
|
|
allowedImageTypes = map[string]bool{
|
|||
|
|
"image/jpeg": true, "image/png": true, "image/gif": true, "image/webp": true,
|
|||
|
|
}
|
|||
|
|
allowedVideoTypes = map[string]bool{
|
|||
|
|
"video/mp4": true, "video/quicktime": true, "video/webm": true, "video/x-msvideo": true,
|
|||
|
|
}
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// UploadImagePost POST /api/miniprogram/upload/image 小程序-图片上传(支持压缩),优先 OSS
|
|||
|
|
// 表单:file(必填), folder(可选,默认 images), quality(可选 1-100,默认 85)
|
|||
|
|
func UploadImagePost(c *gin.Context) {
|
|||
|
|
file, err := c.FormFile("file")
|
|||
|
|
if err != nil {
|
|||
|
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "请选择要上传的图片"})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if file.Size > maxImageBytes {
|
|||
|
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "图片大小不能超过 5MB"})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
ct := file.Header.Get("Content-Type")
|
|||
|
|
if !allowedImageTypes[ct] && !strings.HasPrefix(ct, "image/") {
|
|||
|
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "仅支持 jpg/png/gif/webp 格式"})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
quality := defaultImageQuality
|
|||
|
|
if q := c.PostForm("quality"); q != "" {
|
|||
|
|
if qn, e := strconv.Atoi(q); e == nil && qn >= 1 && qn <= 100 {
|
|||
|
|
quality = qn
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
folder := c.PostForm("folder")
|
|||
|
|
if folder == "" {
|
|||
|
|
folder = "images"
|
|||
|
|
}
|
|||
|
|
ext := filepath.Ext(file.Filename)
|
|||
|
|
if ext == "" {
|
|||
|
|
ext = ".jpg"
|
|||
|
|
}
|
|||
|
|
name := fmt.Sprintf("%d_%s%s", time.Now().UnixNano(), randomStrContent(6), ext)
|
|||
|
|
|
|||
|
|
src, err := file.Open()
|
|||
|
|
if err != nil {
|
|||
|
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "打开文件失败"})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
defer src.Close()
|
|||
|
|
data, err := io.ReadAll(src)
|
|||
|
|
if err != nil {
|
|||
|
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "读取文件失败"})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// JPEG 压缩
|
|||
|
|
var finalData []byte
|
|||
|
|
finalCt := ct
|
|||
|
|
if strings.Contains(ct, "jpeg") || strings.Contains(ct, "jpg") {
|
|||
|
|
if img, err := jpeg.Decode(bytes.NewReader(data)); err == nil {
|
|||
|
|
var buf bytes.Buffer
|
|||
|
|
if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: quality}); err == nil {
|
|||
|
|
finalData = buf.Bytes()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} else if strings.Contains(ct, "png") {
|
|||
|
|
if img, err := png.Decode(bytes.NewReader(data)); err == nil {
|
|||
|
|
var buf bytes.Buffer
|
|||
|
|
if err := png.Encode(&buf, img); err == nil {
|
|||
|
|
finalData = buf.Bytes()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} else if strings.Contains(ct, "gif") {
|
|||
|
|
if img, err := gif.Decode(bytes.NewReader(data)); err == nil {
|
|||
|
|
var buf bytes.Buffer
|
|||
|
|
if err := gif.Encode(&buf, img, nil); err == nil {
|
|||
|
|
finalData = buf.Bytes()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if finalData == nil {
|
|||
|
|
finalData = data
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 优先 OSS 上传
|
|||
|
|
if ossURL, err := ossUploadBytes(finalData, folder, name, finalCt); err == nil {
|
|||
|
|
c.JSON(http.StatusOK, gin.H{
|
|||
|
|
"success": true, "url": ossURL,
|
|||
|
|
"data": gin.H{"url": ossURL, "fileName": name, "size": int64(len(finalData)), "type": ct, "quality": quality, "storage": "oss"},
|
|||
|
|
})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 回退本地存储
|
|||
|
|
dir := filepath.Join(uploadDirContent, folder)
|
|||
|
|
_ = os.MkdirAll(dir, 0755)
|
|||
|
|
dst := filepath.Join(dir, name)
|
|||
|
|
if err := os.WriteFile(dst, finalData, 0644); err != nil {
|
|||
|
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "保存失败"})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
url := "/" + filepath.ToSlash(filepath.Join(uploadDirContent, folder, name))
|
|||
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "url": url, "data": gin.H{"url": url, "fileName": name, "size": int64(len(finalData)), "type": ct, "quality": quality, "storage": "local"}})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// UploadVideoPost POST /api/miniprogram/upload/video 小程序-视频上传,优先 OSS
|
|||
|
|
// 表单:file(必填), folder(可选,默认 videos)
|
|||
|
|
func UploadVideoPost(c *gin.Context) {
|
|||
|
|
file, err := c.FormFile("file")
|
|||
|
|
if err != nil {
|
|||
|
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "请选择要上传的视频"})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if file.Size > maxVideoBytes {
|
|||
|
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "视频大小不能超过 100MB"})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
ct := file.Header.Get("Content-Type")
|
|||
|
|
if !allowedVideoTypes[ct] && !strings.HasPrefix(ct, "video/") {
|
|||
|
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "仅支持 mp4/mov/avi 等视频格式"})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
folder := c.PostForm("folder")
|
|||
|
|
if folder == "" {
|
|||
|
|
folder = "videos"
|
|||
|
|
}
|
|||
|
|
ext := filepath.Ext(file.Filename)
|
|||
|
|
if ext == "" {
|
|||
|
|
ext = ".mp4"
|
|||
|
|
}
|
|||
|
|
name := fmt.Sprintf("%d_%s%s", time.Now().UnixNano(), randomStrContent(8), ext)
|
|||
|
|
|
|||
|
|
// 优先 OSS 上传
|
|||
|
|
if ossCfg := getOssConfig(); ossCfg != nil {
|
|||
|
|
src, err := file.Open()
|
|||
|
|
if err == nil {
|
|||
|
|
defer src.Close()
|
|||
|
|
if ossURL, err := ossUploadFile(src, folder, name); err == nil {
|
|||
|
|
c.JSON(http.StatusOK, gin.H{
|
|||
|
|
"success": true, "url": ossURL,
|
|||
|
|
"data": gin.H{"url": ossURL, "fileName": name, "size": file.Size, "type": ct, "folder": folder, "storage": "oss"},
|
|||
|
|
})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 回退本地存储
|
|||
|
|
dir := filepath.Join(uploadDirContent, folder)
|
|||
|
|
_ = os.MkdirAll(dir, 0755)
|
|||
|
|
dst := filepath.Join(dir, name)
|
|||
|
|
if err := c.SaveUploadedFile(file, dst); err != nil {
|
|||
|
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "保存失败"})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
url := "/" + filepath.ToSlash(filepath.Join(uploadDirContent, folder, name))
|
|||
|
|
c.JSON(http.StatusOK, gin.H{
|
|||
|
|
"success": true, "url": url,
|
|||
|
|
"data": gin.H{"url": url, "fileName": name, "size": file.Size, "type": ct, "folder": folder, "storage": "local"},
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// AdminContentUpload POST /api/admin/content/upload 管理端-内容上传(通过 API 写入内容管理,不直接操作数据库)
|
|||
|
|
// 需 AdminAuth。Body: { "action": "import", "data": [ { "id","title","content","price","isFree","partId","partTitle","chapterId","chapterTitle" } ] }
|
|||
|
|
func AdminContentUpload(c *gin.Context) {
|
|||
|
|
var body struct {
|
|||
|
|
Action string `json:"action"`
|
|||
|
|
Data []importItem `json:"data"`
|
|||
|
|
}
|
|||
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|||
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if body.Action != "import" {
|
|||
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "action 须为 import"})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if len(body.Data) == 0 {
|
|||
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "data 不能为空"})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
db := database.DB()
|
|||
|
|
imported, failed := 0, 0
|
|||
|
|
for _, item := range body.Data {
|
|||
|
|
if item.ID == "" || item.Title == "" {
|
|||
|
|
failed++
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
price := 1.0
|
|||
|
|
if item.Price != nil {
|
|||
|
|
price = *item.Price
|
|||
|
|
}
|
|||
|
|
isFree := false
|
|||
|
|
if item.IsFree != nil {
|
|||
|
|
isFree = *item.IsFree
|
|||
|
|
}
|
|||
|
|
wordCount := len(item.Content)
|
|||
|
|
status := "published"
|
|||
|
|
editionStandard, editionPremium := true, false
|
|||
|
|
ch := model.Chapter{
|
|||
|
|
ID: item.ID,
|
|||
|
|
PartID: strPtrContent(item.PartID, "part-1"),
|
|||
|
|
PartTitle: strPtrContent(item.PartTitle, "未分类"),
|
|||
|
|
ChapterID: strPtrContent(item.ChapterID, "chapter-1"),
|
|||
|
|
ChapterTitle: strPtrContent(item.ChapterTitle, "未分类"),
|
|||
|
|
SectionTitle: item.Title,
|
|||
|
|
Content: item.Content,
|
|||
|
|
WordCount: &wordCount,
|
|||
|
|
IsFree: &isFree,
|
|||
|
|
Price: &price,
|
|||
|
|
Status: &status,
|
|||
|
|
EditionStandard: &editionStandard,
|
|||
|
|
EditionPremium: &editionPremium,
|
|||
|
|
}
|
|||
|
|
err := db.Where("id = ?", item.ID).First(&model.Chapter{}).Error
|
|||
|
|
if err == gorm.ErrRecordNotFound {
|
|||
|
|
err = db.Create(&ch).Error
|
|||
|
|
} else if err == nil {
|
|||
|
|
err = db.Model(&model.Chapter{}).Where("id = ?", item.ID).Updates(map[string]interface{}{
|
|||
|
|
"section_title": ch.SectionTitle,
|
|||
|
|
"content": ch.Content,
|
|||
|
|
"word_count": ch.WordCount,
|
|||
|
|
"is_free": ch.IsFree,
|
|||
|
|
"price": ch.Price,
|
|||
|
|
}).Error
|
|||
|
|
}
|
|||
|
|
if err != nil {
|
|||
|
|
failed++
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
imported++
|
|||
|
|
}
|
|||
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "message": "导入完成", "imported": imported, "failed": failed})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func randomStrContent(n int) string {
|
|||
|
|
const letters = "abcdefghijklmnopqrstuvwxyz0123456789"
|
|||
|
|
b := make([]byte, n)
|
|||
|
|
for i := range b {
|
|||
|
|
b[i] = letters[rand.Intn(len(letters))]
|
|||
|
|
}
|
|||
|
|
return string(b)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func strPtrContent(s *string, def string) string {
|
|||
|
|
if s != nil && *s != "" {
|
|||
|
|
return *s
|
|||
|
|
}
|
|||
|
|
return def
|
|||
|
|
}
|