diff --git a/internal/expiry/handler.go b/internal/expiry/handler.go index c0dfa92..333cd17 100644 --- a/internal/expiry/handler.go +++ b/internal/expiry/handler.go @@ -1,9 +1,16 @@ package expiry import ( + "errors" + "io" "net/http" + "strconv" + "strings" + "time" "github.com/gin-gonic/gin" + + "wx_service/internal/middleware" ) // Handler 负责 HTTP 层处理。 @@ -26,3 +33,283 @@ func (h *Handler) Healthz(c *gin.Context) { }, }) } + +type createOrUpdateItemRequest struct { + Name string `json:"name"` + Category string `json:"category"` + ProductionDate string `json:"production_date"` + ExpiryDate string `json:"expiry_date"` + ShelfLifeDays *int `json:"shelf_life_days"` + Quantity *int `json:"quantity"` + Location string `json:"location"` + Remark string `json:"remark"` +} + +type updateStatusRequest struct { + Status string `json:"status"` +} + +const expiryDateLayout = "2006-01-02" + +// GetSummary 获取首页汇总统计。 +func (h *Handler) GetSummary(c *gin.Context) { + user := middleware.MustCurrentUser(c) + resp, err := h.service.GetSummary(user.ID) + if err != nil { + writeExpiryServerError(c) + return + } + writeExpirySuccess(c, "success", resp) +} + +// GetItems 获取物品列表。 +func (h *Handler) GetItems(c *gin.Context) { + user := middleware.MustCurrentUser(c) + + page := parseIntWithDefault(c.Query("page"), 1) + pageSize := parseIntWithDefault(c.Query("page_size"), 20) + filters := ItemFilters{ + Status: strings.TrimSpace(c.DefaultQuery("status", "all")), + Category: strings.TrimSpace(c.DefaultQuery("category", "all")), + Sort: strings.TrimSpace(c.DefaultQuery("sort", "expiry_date")), + Page: page, + PageSize: pageSize, + } + + resp, err := h.service.GetItems(user.ID, filters) + if err != nil { + if isExpiryBadRequestError(err) { + writeExpiryError(c, http.StatusBadRequest, err.Error()) + return + } + writeExpiryServerError(c) + return + } + + writeExpirySuccess(c, "success", resp) +} + +// CreateItem 添加物品。 +func (h *Handler) CreateItem(c *gin.Context) { + user := middleware.MustCurrentUser(c) + + var req createOrUpdateItemRequest + if err := c.ShouldBindJSON(&req); err != nil { + writeExpiryError(c, http.StatusBadRequest, "请求参数错误") + return + } + + serviceReq, err := h.toCreateItemRequest(user.MiniProgramID, req) + if err != nil { + writeExpiryError(c, http.StatusBadRequest, err.Error()) + return + } + + item, err := h.service.CreateItem(user.ID, serviceReq) + if err != nil { + if isExpiryBadRequestError(err) { + writeExpiryError(c, http.StatusBadRequest, err.Error()) + return + } + writeExpiryServerError(c) + return + } + + writeExpirySuccess(c, "添加成功", toItemView(*item)) +} + +// UpdateItem 更新物品。 +func (h *Handler) UpdateItem(c *gin.Context) { + user := middleware.MustCurrentUser(c) + id, ok := parseItemID(c) + if !ok { + return + } + + var req createOrUpdateItemRequest + if err := c.ShouldBindJSON(&req); err != nil { + writeExpiryError(c, http.StatusBadRequest, "请求参数错误") + return + } + + serviceReq, err := h.toCreateItemRequest(user.MiniProgramID, req) + if err != nil { + writeExpiryError(c, http.StatusBadRequest, err.Error()) + return + } + + item, err := h.service.UpdateItem(id, user.ID, serviceReq) + if err != nil { + if errors.Is(err, ErrExpiryItemNotFound) { + writeExpiryError(c, http.StatusNotFound, "物品不存在") + return + } + if isExpiryBadRequestError(err) { + writeExpiryError(c, http.StatusBadRequest, err.Error()) + return + } + writeExpiryServerError(c) + return + } + + writeExpirySuccess(c, "更新成功", toItemView(*item)) +} + +// DeleteItem 删除物品。 +func (h *Handler) DeleteItem(c *gin.Context) { + user := middleware.MustCurrentUser(c) + id, ok := parseItemID(c) + if !ok { + return + } + + err := h.service.DeleteItem(id, user.ID) + if err != nil { + if errors.Is(err, ErrExpiryItemNotFound) { + writeExpiryError(c, http.StatusNotFound, "物品不存在") + return + } + writeExpiryServerError(c) + return + } + + writeExpirySuccess(c, "删除成功", nil) +} + +// UpdateStatus 标记物品状态(used/discarded)。 +func (h *Handler) UpdateStatus(c *gin.Context) { + user := middleware.MustCurrentUser(c) + id, ok := parseItemID(c) + if !ok { + return + } + + var req updateStatusRequest + if err := c.ShouldBindJSON(&req); err != nil && !errors.Is(err, io.EOF) { + writeExpiryError(c, http.StatusBadRequest, "请求参数错误") + return + } + + if err := h.service.UpdateItemStatus(id, user.ID, strings.TrimSpace(req.Status)); err != nil { + if errors.Is(err, ErrExpiryItemNotFound) { + writeExpiryError(c, http.StatusNotFound, "物品不存在") + return + } + if isExpiryBadRequestError(err) { + writeExpiryError(c, http.StatusBadRequest, err.Error()) + return + } + writeExpiryServerError(c) + return + } + + item, err := h.service.GetItem(id, user.ID) + if err != nil { + writeExpiryServerError(c) + return + } + + writeExpirySuccess(c, "标记成功", gin.H{ + "id": item.ID, + "status": item.Status, + "updated_at": item.UpdatedAt, + }) +} + +func (h *Handler) toCreateItemRequest(miniProgramID uint, req createOrUpdateItemRequest) (CreateItemRequest, error) { + productionDate, err := parseDateString(req.ProductionDate) + if err != nil { + return CreateItemRequest{}, errors.New("production_date 格式错误,应为 YYYY-MM-DD") + } + + expiryDate, err := parseDateString(req.ExpiryDate) + if err != nil { + return CreateItemRequest{}, errors.New("expiry_date 格式错误,应为 YYYY-MM-DD") + } + + quantity := 1 + if req.Quantity != nil { + quantity = *req.Quantity + } + + return CreateItemRequest{ + MiniProgramID: miniProgramID, + Name: req.Name, + Category: req.Category, + ProductionDate: productionDate, + ExpiryDate: expiryDate, + ShelfLifeDays: req.ShelfLifeDays, + Quantity: quantity, + Location: req.Location, + Remark: req.Remark, + }, nil +} + +func parseDateString(v string) (*time.Time, error) { + v = strings.TrimSpace(v) + if v == "" { + return nil, nil + } + parsed, err := time.ParseInLocation(expiryDateLayout, v, time.Local) + if err != nil { + return nil, err + } + return &parsed, nil +} + +func parseIntWithDefault(v string, defaultValue int) int { + if strings.TrimSpace(v) == "" { + return defaultValue + } + parsed, err := strconv.Atoi(v) + if err != nil { + return defaultValue + } + return parsed +} + +func parseItemID(c *gin.Context) (uint, bool) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil || id <= 0 { + writeExpiryError(c, http.StatusBadRequest, "id 参数错误") + return 0, false + } + return uint(id), true +} + +func isExpiryBadRequestError(err error) bool { + return errors.Is(err, ErrExpiryNameInvalid) || + errors.Is(err, ErrExpiryCategoryInvalid) || + errors.Is(err, ErrExpiryMiniProgramRequired) || + errors.Is(err, ErrExpiryQuantityInvalid) || + errors.Is(err, ErrExpiryLocationTooLong) || + errors.Is(err, ErrExpiryRemarkTooLong) || + errors.Is(err, ErrExpiryDateRequired) || + errors.Is(err, ErrExpiryShelfLifeDaysInvalid) || + errors.Is(err, ErrExpiryFilterStatusInvalid) || + errors.Is(err, ErrExpiryFilterCategoryInvalid) || + errors.Is(err, ErrExpiryFilterSortInvalid) || + errors.Is(err, ErrExpiryStatusInvalid) +} + +func writeExpirySuccess(c *gin.Context, message string, data interface{}) { + resp := gin.H{ + "code": 0, + "message": message, + } + if data != nil { + resp["data"] = data + } + c.JSON(http.StatusOK, resp) +} + +func writeExpiryError(c *gin.Context, code int, message string) { + c.JSON(code, gin.H{ + "code": code, + "message": message, + }) +} + +func writeExpiryServerError(c *gin.Context) { + writeExpiryError(c, http.StatusInternalServerError, "服务器错误") +} diff --git a/internal/routes/expiry_routes.go b/internal/routes/expiry_routes.go index 5834a45..3b8eeb7 100644 --- a/internal/routes/expiry_routes.go +++ b/internal/routes/expiry_routes.go @@ -11,9 +11,14 @@ func registerExpiryRoutes(protected *gin.RouterGroup, expiryHandler *expiryhandl return } - // 后续 issue 会补齐完整的物品/设置接口。 + // 物品管理 expiry := protected.Group("") { - _ = expiry + expiry.GET("/summary", expiryHandler.GetSummary) + expiry.GET("/items", expiryHandler.GetItems) + expiry.POST("/items", expiryHandler.CreateItem) + expiry.PUT("/items/:id", expiryHandler.UpdateItem) + expiry.DELETE("/items/:id", expiryHandler.DeleteItem) + expiry.POST("/items/:id/status", expiryHandler.UpdateStatus) } }