feat: add image upload functionality for articles

- Implemented image upload API in the article service, allowing users to upload images associated with articles.
- Added validation for image size and type, ensuring only supported formats (PNG, JPG, GIF, WebP) are accepted.
- Created a new Aliyun object storage client to handle image storage and retrieval.
- Updated the article handler to include an endpoint for image uploads.
- Enhanced error handling for image upload failures and added relevant error messages.
- Introduced public URL generation for stored images, allowing access via signed tokens.
- Updated localization files to include new messages related to image upload functionality.
- Added tests for image upload and retrieval processes.
This commit is contained in:
2026-04-05 22:10:05 +08:00
parent 1401b50556
commit 94f7186cce
19 changed files with 1033 additions and 14 deletions
@@ -2,6 +2,7 @@ package transport
import (
"encoding/json"
"io"
"net/http"
"strconv"
"time"
@@ -19,6 +20,7 @@ type ArticleHandler struct {
promptGenerateSvc *app.PromptRuleGenerationService
streams *stream.GenerationHub
streamEnabled bool
assets *AssetHandler
}
func NewArticleHandler(a *bootstrap.App) *ArticleHandler {
@@ -36,7 +38,7 @@ func NewArticleHandler(a *bootstrap.App) *ArticleHandler {
)
return &ArticleHandler{
svc: app.NewArticleService(a.DB, a.AuditLogs),
svc: app.NewArticleService(a.DB, a.AuditLogs, a.ObjectStorage),
promptGenerateSvc: app.NewPromptRuleGenerationService(
a.DB,
a.LLM,
@@ -47,6 +49,7 @@ func NewArticleHandler(a *bootstrap.App) *ArticleHandler {
),
streams: a.GenerationStreams,
streamEnabled: a.Config.Generation.StreamEnabled,
assets: NewAssetHandler(a),
}
}
@@ -171,6 +174,42 @@ func (h *ArticleHandler) Update(c *gin.Context) {
response.Success(c, data)
}
func (h *ArticleHandler) UploadImage(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.Error(c, response.ErrBadRequest(40001, "invalid_id", "article id must be a number"))
return
}
fileHeader, err := c.FormFile("file")
if err != nil {
response.Error(c, response.ErrBadRequest(40016, "invalid_image", "image file is required"))
return
}
file, err := fileHeader.Open()
if err != nil {
response.Error(c, response.ErrBadRequest(40016, "invalid_image", "image file cannot be opened"))
return
}
defer file.Close()
content, err := io.ReadAll(file)
if err != nil {
response.Error(c, response.ErrBadRequest(40016, "invalid_image", "image file cannot be read"))
return
}
data, err := h.svc.UploadImage(c.Request.Context(), id, fileHeader.Filename, content)
if err != nil {
response.Error(c, err)
return
}
data.URL = h.assets.BuildArticleImageURL(data.ObjectKey)
response.SuccessWithStatus(c, http.StatusCreated, data)
}
func (h *ArticleHandler) Stream(c *gin.Context) {
if !h.streamEnabled {
response.Error(c, response.ErrNotFound(40412, "generation_stream_disabled", "generation stream is disabled"))