feat(admin): 品牌管理 + 新建商品
品牌管理:列出品牌及引用商品数,支持改名、合并重复品牌(把源品牌的商品并入目标后删除源)、删除未被引用的品牌。新建商品:商品列表新增「新建商品」入口,填写名称/条码/品牌/品类后创建并进入详情页继续补全。 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -65,6 +65,7 @@ func (h *Handler) Router() http.Handler {
|
||||
r.Use(h.authn.Middleware)
|
||||
r.Get("/api/me", h.Me)
|
||||
r.Get("/api/products", h.ListProducts)
|
||||
r.Post("/api/products", h.CreateProduct)
|
||||
r.Get("/api/products/{id}", h.GetProduct)
|
||||
r.Put("/api/products/{id}", h.UpdateProduct)
|
||||
r.Get("/api/products/{id}/audit", h.ListAudit)
|
||||
@@ -76,6 +77,10 @@ func (h *Handler) Router() http.Handler {
|
||||
r.Delete("/api/products/{id}/barcodes/{barcodeID}", h.DeleteBarcode)
|
||||
r.Post("/api/products/{id}/barcodes/{barcodeID}/primary", h.SetPrimaryBarcode)
|
||||
r.Get("/api/brands", h.ListBrands)
|
||||
r.Post("/api/brands", h.CreateBrand)
|
||||
r.Put("/api/brands/{id}", h.UpdateBrand)
|
||||
r.Post("/api/brands/{id}/merge", h.MergeBrands)
|
||||
r.Delete("/api/brands/{id}", h.DeleteBrand)
|
||||
r.Get("/api/categories", h.ListCategories)
|
||||
r.Post("/api/categories", h.CreateCategory)
|
||||
r.Put("/api/categories/{id}", h.UpdateCategory)
|
||||
@@ -175,6 +180,29 @@ func (h *Handler) GetProduct(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, d)
|
||||
}
|
||||
|
||||
// CreateProduct adds a new product with core fields; the rest is filled in via
|
||||
// the detail editor.
|
||||
func (h *Handler) CreateProduct(w http.ResponseWriter, r *http.Request) {
|
||||
var in adminstore.ProductInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "bad_request", "invalid body")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(in.Name) == "" {
|
||||
writeError(w, http.StatusBadRequest, "bad_request", "名称不能为空")
|
||||
return
|
||||
}
|
||||
d, err := h.store.CreateProduct(r.Context(), auth.UserFrom(r.Context()), in)
|
||||
if errors.Is(err, adminstore.ErrDuplicateGTIN) {
|
||||
writeError(w, http.StatusConflict, "duplicate_gtin", "该条码(GTIN)已被其它商品使用")
|
||||
return
|
||||
}
|
||||
if h.handleErr(w, err) {
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, d)
|
||||
}
|
||||
|
||||
// UpdateProduct applies an edit.
|
||||
func (h *Handler) UpdateProduct(w http.ResponseWriter, r *http.Request) {
|
||||
var in adminstore.ProductInput
|
||||
@@ -399,6 +427,96 @@ func (h *Handler) handleCategoryErr(w http.ResponseWriter, err error) bool {
|
||||
return h.handleErr(w, err)
|
||||
}
|
||||
|
||||
type brandInput struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type brandMergeInput struct {
|
||||
TargetID string `json:"target_id"`
|
||||
}
|
||||
|
||||
// CreateBrand adds a brand.
|
||||
func (h *Handler) CreateBrand(w http.ResponseWriter, r *http.Request) {
|
||||
var in brandInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "bad_request", "invalid body")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(in.Name) == "" {
|
||||
writeError(w, http.StatusBadRequest, "bad_request", "品牌名称不能为空")
|
||||
return
|
||||
}
|
||||
b, err := h.store.CreateBrand(r.Context(), auth.UserFrom(r.Context()), in.Name)
|
||||
if h.handleBrandErr(w, err) {
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, b)
|
||||
}
|
||||
|
||||
// UpdateBrand renames a brand.
|
||||
func (h *Handler) UpdateBrand(w http.ResponseWriter, r *http.Request) {
|
||||
var in brandInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "bad_request", "invalid body")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(in.Name) == "" {
|
||||
writeError(w, http.StatusBadRequest, "bad_request", "品牌名称不能为空")
|
||||
return
|
||||
}
|
||||
b, err := h.store.UpdateBrand(r.Context(), chi.URLParam(r, "id"), auth.UserFrom(r.Context()), in.Name)
|
||||
if h.handleBrandErr(w, err) {
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, b)
|
||||
}
|
||||
|
||||
// MergeBrands folds one brand's products into another, then deletes the source.
|
||||
func (h *Handler) MergeBrands(w http.ResponseWriter, r *http.Request) {
|
||||
var in brandMergeInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "bad_request", "invalid body")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(in.TargetID) == "" {
|
||||
writeError(w, http.StatusBadRequest, "bad_request", "请选择合并目标品牌")
|
||||
return
|
||||
}
|
||||
b, err := h.store.MergeBrands(r.Context(), chi.URLParam(r, "id"), in.TargetID, auth.UserFrom(r.Context()))
|
||||
if h.handleBrandErr(w, err) {
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, b)
|
||||
}
|
||||
|
||||
// DeleteBrand removes a brand no product references.
|
||||
func (h *Handler) DeleteBrand(w http.ResponseWriter, r *http.Request) {
|
||||
err := h.store.DeleteBrand(r.Context(), chi.URLParam(r, "id"), auth.UserFrom(r.Context()))
|
||||
if h.handleBrandErr(w, err) {
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
|
||||
}
|
||||
|
||||
// handleBrandErr maps brand-specific errors to client statuses.
|
||||
func (h *Handler) handleBrandErr(w http.ResponseWriter, err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
switch {
|
||||
case errors.Is(err, adminstore.ErrDuplicateBrand):
|
||||
writeError(w, http.StatusConflict, "duplicate_brand", "该品牌名称已存在")
|
||||
return true
|
||||
case errors.Is(err, adminstore.ErrBrandInUse):
|
||||
writeError(w, http.StatusConflict, "in_use", "仍有商品使用该品牌,请先改用其它品牌或合并")
|
||||
return true
|
||||
case errors.Is(err, adminstore.ErrInvalidMerge):
|
||||
writeError(w, http.StatusBadRequest, "invalid_merge", "合并目标无效(不存在或与源品牌相同)")
|
||||
return true
|
||||
}
|
||||
return h.handleErr(w, err)
|
||||
}
|
||||
|
||||
// ---------- submissions ----------
|
||||
|
||||
// CreateSubmission accepts an anonymous public contribution into the queue.
|
||||
|
||||
Reference in New Issue
Block a user