feat: 实现商品目录批量导入接口 (#133)

This commit is contained in:
chengma
2026-08-11 10:06:10 +08:00
parent 585d9d7834
commit 273bcc14a6
8 changed files with 695 additions and 0 deletions
+31
View File
@@ -1,6 +1,7 @@
package integration
import (
"bytes"
"net/http"
"net/http/httptest"
"strings"
@@ -45,3 +46,33 @@ func TestRequireCatalogToken(t *testing.T) {
})
}
}
func TestCatalogBatchAPI_鉴权JSON与体积限制(t *testing.T) {
gin.SetMode(gin.TestMode)
const token = "0123456789abcdef0123456789abcdef"
r := gin.New()
Register(r, nil, config.CatalogIntegrationConfig{Source: "script", Token: token})
for _, tc := range []struct {
name, auth string
body []byte
want int
}{
{"无鉴权", "", []byte(`{}`), http.StatusUnauthorized},
{"坏JSON", "Bearer " + token, []byte(`{"schema_version":`), http.StatusBadRequest},
{"超限", "Bearer " + token, bytes.Repeat([]byte("x"), catalogMaxBody+1), http.StatusRequestEntityTooLarge},
} {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/api/v1/integrations/catalog/batches", bytes.NewReader(tc.body))
req.Header.Set("Authorization", tc.auth)
resp := httptest.NewRecorder()
r.ServeHTTP(resp, req)
if resp.Code != tc.want {
t.Fatalf("status=%d body=%s", resp.Code, resp.Body.String())
}
if !strings.Contains(resp.Header().Get("Content-Type"), "application/json") {
t.Fatalf("错误响应必须是 JSON:%s", resp.Header())
}
})
}
}
+61
View File
@@ -0,0 +1,61 @@
package integration
import (
"bytes"
"database/sql"
"encoding/json"
"errors"
"io"
"net/http"
"github.com/gin-gonic/gin"
"cmautobuy/admin/config"
"cmautobuy/admin/service"
)
const catalogMaxBody = 5 << 20
type Handler struct{ db *sql.DB }
// Register 只挂载第三方目录接口,不复用 Client 或网页登录中间件。
func Register(r *gin.Engine, db *sql.DB, cfg config.CatalogIntegrationConfig) {
h := &Handler{db: db}
g := r.Group("/api/v1/integrations/catalog", RequireCatalogToken(cfg))
g.POST("/batches", h.CreateBatch)
}
func (h *Handler) CreateBatch(c *gin.Context) {
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, catalogMaxBody)
raw, err := io.ReadAll(c.Request.Body)
if err != nil {
if _, ok := err.(*http.MaxBytesError); ok {
integrationError(c, 413, "PAYLOAD_TOO_LARGE", "请求体不能超过 5 MB", false, nil)
return
}
integrationError(c, 400, "INVALID_BODY", "读取请求失败", false, nil)
return
}
var req service.CatalogBatchRequest
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err = dec.Decode(&req); err != nil {
integrationError(c, 400, "INVALID_JSON", "请求体不是合法的商品目录 JSON", false, nil)
return
}
if dec.Decode(&struct{}{}) != io.EOF {
integrationError(c, 400, "INVALID_JSON", "请求体只能包含一个 JSON 对象", false, nil)
return
}
resp, err := service.ImportCatalogBatch(h.db, integrationSource(c), req, raw)
if err != nil {
var catalogErr *service.CatalogError
if errors.As(err, &catalogErr) {
integrationError(c, catalogErr.Status, catalogErr.Code, catalogErr.Message, catalogErr.Retryable, catalogErr.Details)
return
}
integrationError(c, 500, "INTERNAL_ERROR", "商品目录批次处理失败", true, nil)
return
}
c.JSON(http.StatusOK, resp)
}