package expiry import ( "errors" "io" "net/http" "strconv" "strings" "time" "github.com/gin-gonic/gin" "wx_service/internal/middleware" ) // Handler 负责 HTTP 层处理。 type Handler struct { service *Service } func NewHandler(service *Service) *Handler { return &Handler{service: service} } // Healthz 用于 expiry 模块健康检查。 func (h *Handler) Healthz(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "code": 0, "message": "success", "data": gin.H{ "module": "expiry", "status": "ok", }, }) } 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"` } type updateSettingsRequest struct { RemindDays []int `json:"remind_days"` } 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, }) } // GetSettings 获取用户提醒设置。 func (h *Handler) GetSettings(c *gin.Context) { user := middleware.MustCurrentUser(c) resp, err := h.service.GetSettings(user.ID) if err != nil { writeExpiryServerError(c) return } writeExpirySuccess(c, "success", resp) } // UpdateSettings 更新用户提醒设置。 func (h *Handler) UpdateSettings(c *gin.Context) { user := middleware.MustCurrentUser(c) var req updateSettingsRequest if err := c.ShouldBindJSON(&req); err != nil { writeExpiryError(c, http.StatusBadRequest, "请求参数错误") return } resp, err := h.service.UpdateSettings(user.ID, req.RemindDays) if err != nil { if isExpiryBadRequestError(err) { writeExpiryError(c, http.StatusBadRequest, err.Error()) return } writeExpiryServerError(c) return } writeExpirySuccess(c, "更新成功", resp) } 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) || errors.Is(err, ErrExpiryRemindDaysInvalid) } 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, "服务器错误") }