feat: 数据概览/操作日志/批量操作 + 首页合格档案数
后台新增「数据概览」(商品/合格/按状态/品牌/分类/待审核) 与「操作日志」(全局审计分页);商品列表支持多选批量改状态/分类。公开首页标题改为「天工」并展示合格档案数;新增公开接口 /api/v1/stats 与后台 /api/stats、/api/audit、/api/products/bulk。合格口径=quality_score≥0.6 且在用。 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -64,8 +64,11 @@ func (h *Handler) Router() http.Handler {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(h.authn.Middleware)
|
||||
r.Get("/api/me", h.Me)
|
||||
r.Get("/api/stats", h.Stats)
|
||||
r.Get("/api/audit", h.ListAllAudit)
|
||||
r.Get("/api/products", h.ListProducts)
|
||||
r.Post("/api/products", h.CreateProduct)
|
||||
r.Post("/api/products/bulk", h.BulkProducts)
|
||||
r.Get("/api/products/{id}", h.GetProduct)
|
||||
r.Put("/api/products/{id}", h.UpdateProduct)
|
||||
r.Get("/api/products/{id}/audit", h.ListAudit)
|
||||
@@ -203,6 +206,76 @@ func (h *Handler) CreateProduct(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusCreated, d)
|
||||
}
|
||||
|
||||
// Stats returns the dashboard overview counters.
|
||||
func (h *Handler) Stats(w http.ResponseWriter, r *http.Request) {
|
||||
st, err := h.store.Stats(r.Context())
|
||||
if h.handleErr(w, err) {
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, st)
|
||||
}
|
||||
|
||||
// ListAllAudit returns a page of the global operations audit log.
|
||||
func (h *Handler) ListAllAudit(w http.ResponseWriter, r *http.Request) {
|
||||
page, size := pageParams(r)
|
||||
items, total, err := h.store.ListAllAudit(r.Context(), size, (page-1)*size)
|
||||
if h.handleErr(w, err) {
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"items": items, "page": page, "size": size, "total": total,
|
||||
})
|
||||
}
|
||||
|
||||
type bulkInput struct {
|
||||
IDs []string `json:"ids"`
|
||||
Action string `json:"action"`
|
||||
Status string `json:"status"`
|
||||
CategoryID *string `json:"category_id"`
|
||||
}
|
||||
|
||||
// BulkProducts applies a status or category change to many products at once.
|
||||
func (h *Handler) BulkProducts(w http.ResponseWriter, r *http.Request) {
|
||||
var in bulkInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "bad_request", "invalid body")
|
||||
return
|
||||
}
|
||||
if len(in.IDs) == 0 {
|
||||
writeError(w, http.StatusBadRequest, "bad_request", "未选择任何商品")
|
||||
return
|
||||
}
|
||||
actor := auth.UserFrom(r.Context())
|
||||
var (
|
||||
affected int
|
||||
err error
|
||||
)
|
||||
switch in.Action {
|
||||
case "status":
|
||||
affected, err = h.store.BulkSetStatus(r.Context(), actor, in.IDs, in.Status)
|
||||
case "category":
|
||||
affected, err = h.store.BulkSetCategory(r.Context(), actor, in.IDs, in.CategoryID)
|
||||
default:
|
||||
writeError(w, http.StatusBadRequest, "bad_request", "未知的批量操作")
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case errors.Is(err, adminstore.ErrInvalidStatus):
|
||||
writeError(w, http.StatusBadRequest, "invalid_status", "无效的状态值")
|
||||
return
|
||||
case errors.Is(err, adminstore.ErrInvalidParent):
|
||||
writeError(w, http.StatusBadRequest, "invalid_parent", "目标分类无效")
|
||||
return
|
||||
case errors.Is(err, adminstore.ErrNoTargets):
|
||||
writeError(w, http.StatusBadRequest, "bad_request", "未选择任何商品")
|
||||
return
|
||||
}
|
||||
if h.handleErr(w, err) {
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"status": "ok", "affected": affected})
|
||||
}
|
||||
|
||||
// UpdateProduct applies an edit.
|
||||
func (h *Handler) UpdateProduct(w http.ResponseWriter, r *http.Request) {
|
||||
var in adminstore.ProductInput
|
||||
|
||||
Reference in New Issue
Block a user