feat(admin): 分类管理(分类树增删改移 + 后台页面)
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -77,6 +77,9 @@ func (h *Handler) Router() http.Handler {
|
||||
r.Post("/api/products/{id}/barcodes/{barcodeID}/primary", h.SetPrimaryBarcode)
|
||||
r.Get("/api/brands", h.ListBrands)
|
||||
r.Get("/api/categories", h.ListCategories)
|
||||
r.Post("/api/categories", h.CreateCategory)
|
||||
r.Put("/api/categories/{id}", h.UpdateCategory)
|
||||
r.Delete("/api/categories/{id}", h.DeleteCategory)
|
||||
|
||||
r.Get("/api/submissions", h.ListSubmissions)
|
||||
r.Get("/api/submissions/{id}", h.GetSubmission)
|
||||
@@ -328,6 +331,74 @@ func (h *Handler) ListCategories(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
||||
}
|
||||
|
||||
// CreateCategory adds a category node.
|
||||
func (h *Handler) CreateCategory(w http.ResponseWriter, r *http.Request) {
|
||||
var in adminstore.CategoryInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "bad_request", "invalid body")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(in.NameZH) == "" {
|
||||
writeError(w, http.StatusBadRequest, "bad_request", "分类名称不能为空")
|
||||
return
|
||||
}
|
||||
c, err := h.store.CreateCategory(r.Context(), auth.UserFrom(r.Context()), in)
|
||||
if h.handleCategoryErr(w, err) {
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, c)
|
||||
}
|
||||
|
||||
// UpdateCategory renames and/or moves a category node.
|
||||
func (h *Handler) UpdateCategory(w http.ResponseWriter, r *http.Request) {
|
||||
var in adminstore.CategoryInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "bad_request", "invalid body")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(in.NameZH) == "" {
|
||||
writeError(w, http.StatusBadRequest, "bad_request", "分类名称不能为空")
|
||||
return
|
||||
}
|
||||
c, err := h.store.UpdateCategory(r.Context(), chi.URLParam(r, "id"), auth.UserFrom(r.Context()), in)
|
||||
if h.handleCategoryErr(w, err) {
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, c)
|
||||
}
|
||||
|
||||
// DeleteCategory removes a leaf category that no product uses.
|
||||
func (h *Handler) DeleteCategory(w http.ResponseWriter, r *http.Request) {
|
||||
err := h.store.DeleteCategory(r.Context(), chi.URLParam(r, "id"), auth.UserFrom(r.Context()))
|
||||
if h.handleCategoryErr(w, err) {
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
|
||||
}
|
||||
|
||||
// handleCategoryErr maps category-specific errors to client statuses, falling
|
||||
// back to handleErr otherwise.
|
||||
func (h *Handler) handleCategoryErr(w http.ResponseWriter, err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
switch {
|
||||
case errors.Is(err, adminstore.ErrDuplicatePath):
|
||||
writeError(w, http.StatusConflict, "duplicate_path", "该分类路径已存在,请换一个英文标识(slug)")
|
||||
return true
|
||||
case errors.Is(err, adminstore.ErrCategoryHasChildren):
|
||||
writeError(w, http.StatusConflict, "has_children", "该分类存在子分类,请先删除或移动其子分类")
|
||||
return true
|
||||
case errors.Is(err, adminstore.ErrCategoryInUse):
|
||||
writeError(w, http.StatusConflict, "in_use", "仍有商品归属于该分类,请先改归其它分类")
|
||||
return true
|
||||
case errors.Is(err, adminstore.ErrInvalidParent):
|
||||
writeError(w, http.StatusBadRequest, "invalid_parent", "上级分类无效(不存在或不能移动到自身/子级下)")
|
||||
return true
|
||||
}
|
||||
return h.handleErr(w, err)
|
||||
}
|
||||
|
||||
// ---------- submissions ----------
|
||||
|
||||
// CreateSubmission accepts an anonymous public contribution into the queue.
|
||||
|
||||
Reference in New Issue
Block a user