实现 MinIO 对象存储图片流水线(可迁移 OSS)
- 新增 S3 兼容 Storage 抽象与 MinIO 实现,compose 启动 MinIO - 上传接口:校验 → 缩放 → WebP → 主图/缩略图入库 - 消息图片 content 改为对象 URL,拒绝 base64 - 工作台/访客端改为先上传再发消息
This commit is contained in:
@@ -2,8 +2,10 @@ package handler_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
@@ -13,9 +15,11 @@ import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"kefu-sys/server/internal/config"
|
||||
"kefu-sys/server/internal/handler"
|
||||
"kefu-sys/server/internal/middleware"
|
||||
"kefu-sys/server/internal/model"
|
||||
"kefu-sys/server/internal/storage"
|
||||
)
|
||||
|
||||
func setupRouter(t *testing.T) *gin.Engine {
|
||||
@@ -32,10 +36,45 @@ func setupRouter(t *testing.T) *gin.Engine {
|
||||
model.DB = db
|
||||
middleware.InitJWT("test-secret")
|
||||
router := gin.New()
|
||||
handler.SetupRoutes(router)
|
||||
mem := storage.NewMemory("http://localhost:9000/kefu")
|
||||
handler.SetupRoutes(router, mem, config.StorageConfig{
|
||||
PublicBase: "http://localhost:9000/kefu",
|
||||
MaxUploadMB: 10,
|
||||
MaxImageEdge: 1920,
|
||||
WebPQuality: 80,
|
||||
})
|
||||
return router
|
||||
}
|
||||
|
||||
// tinyPNG 1x1 像素 PNG
|
||||
func tinyPNG() []byte {
|
||||
b, _ := base64.StdEncoding.DecodeString("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==")
|
||||
return b
|
||||
}
|
||||
|
||||
func multipartImageRequest(t *testing.T, method, target string, fileField string, filename string, data []byte, user model.User) *http.Request {
|
||||
t.Helper()
|
||||
var body bytes.Buffer
|
||||
w := multipart.NewWriter(&body)
|
||||
part, err := w.CreateFormFile(fileField, filename)
|
||||
if err != nil {
|
||||
t.Fatalf("创建 form file 失败: %v", err)
|
||||
}
|
||||
if _, err := part.Write(data); err != nil {
|
||||
t.Fatalf("写入文件失败: %v", err)
|
||||
}
|
||||
_ = w.Close()
|
||||
req := httptest.NewRequest(method, target, &body)
|
||||
req.Header.Set("Content-Type", w.FormDataContentType())
|
||||
token, err := middleware.GenerateToken(user.ID, user.TenantID, user.Role)
|
||||
if err != nil {
|
||||
t.Fatalf("生成令牌失败: %v", err)
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
return req
|
||||
}
|
||||
|
||||
|
||||
func createTenant(t *testing.T, name, status string) model.Tenant {
|
||||
t.Helper()
|
||||
tenant := model.Tenant{Name: name, Status: status, ExpireAt: time.Now().AddDate(1, 0, 0)}
|
||||
@@ -417,8 +456,22 @@ func TestWorkbenchSessionLifecycleUnreadNotesTransferAndImage(t *testing.T) {
|
||||
t.Fatalf("转接后原客服仍可回复: %d %s", oldAgentMessageRecorder.Code, oldAgentMessageRecorder.Body.String())
|
||||
}
|
||||
|
||||
uploadRecorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(uploadRecorder, multipartImageRequest(t, http.MethodPost, "/api/uploads", "file", "dot.png", tinyPNG(), agentTwo))
|
||||
if uploadRecorder.Code != http.StatusOK {
|
||||
t.Fatalf("上传图片失败: %d %s", uploadRecorder.Code, uploadRecorder.Body.String())
|
||||
}
|
||||
var uploadResp struct {
|
||||
Data struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(uploadRecorder.Body.Bytes(), &uploadResp); err != nil || uploadResp.Data.URL == "" {
|
||||
t.Fatalf("解析上传响应失败: %v body=%s", err, uploadRecorder.Body.String())
|
||||
}
|
||||
|
||||
imageRecorder := httptest.NewRecorder()
|
||||
imageBody := []byte(`{"content":"data:image/png;base64,aGVsbG8=","type":"image"}`)
|
||||
imageBody := []byte(fmt.Sprintf(`{"content":%q,"type":"image"}`, uploadResp.Data.URL))
|
||||
router.ServeHTTP(imageRecorder, bearerRequest(t, http.MethodPost, fmt.Sprintf("/api/sessions/%d/messages", session.ID), imageBody, agentTwo))
|
||||
if imageRecorder.Code != http.StatusOK {
|
||||
t.Fatalf("发送图片消息失败: %d %s", imageRecorder.Code, imageRecorder.Body.String())
|
||||
|
||||
Reference in New Issue
Block a user