package expiry import ( "errors" "strings" "time" ) var ( ErrExpiryNameInvalid = errors.New("物品名称不能为空且长度不能超过100") ErrExpiryCategoryInvalid = errors.New("分类无效") ErrExpiryMiniProgramRequired = errors.New("缺少小程序ID") ErrExpiryQuantityInvalid = errors.New("数量必须大于0") ErrExpiryLocationTooLong = errors.New("存放位置长度不能超过50") ErrExpiryRemarkTooLong = errors.New("备注长度不能超过255") ErrExpiryDateRequired = errors.New("必须提供过期日期,或提供生产日期+保质期天数") ErrExpiryShelfLifeDaysInvalid = errors.New("保质期天数必须大于0") ErrExpiryFilterStatusInvalid = errors.New("status 参数无效") ErrExpiryFilterCategoryInvalid = errors.New("category 参数无效") ErrExpiryFilterSortInvalid = errors.New("sort 参数无效") ErrExpiryStatusInvalid = errors.New("状态无效,仅支持 used/discarded") ) // Service 封装保质期模块业务逻辑。 type Service struct { repo *Repository } // CreateItemRequest 创建物品请求。 type CreateItemRequest struct { MiniProgramID uint Name string Category string ProductionDate *time.Time ExpiryDate *time.Time ShelfLifeDays *int Quantity int Location string Remark string } // UpdateItemRequest 更新物品请求(当前按全量更新处理)。 type UpdateItemRequest = CreateItemRequest // ItemFilters 列表筛选参数。 type ItemFilters struct { Status string Category string Sort string Page int PageSize int } // ItemView 是返回给前端的物品视图。 type ItemView struct { ID uint `json:"id"` Name string `json:"name"` Category string `json:"category"` ProductionDate *time.Time `json:"production_date,omitempty"` ExpiryDate time.Time `json:"expiry_date"` ShelfLifeDays *int `json:"shelf_life_days,omitempty"` Quantity int `json:"quantity"` Location string `json:"location"` Remark string `json:"remark"` Status string `json:"status"` DaysLeft int `json:"days_left"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // ItemListResponse 列表返回结构。 type ItemListResponse struct { Items []ItemView `json:"items"` Total int64 `json:"total"` Page int `json:"page"` PageSize int `json:"page_size"` } // SummaryResponse 首页汇总结构。 type SummaryResponse struct { TotalItems int `json:"total_items"` ExpiringSoon int `json:"expiring_soon"` Expired int `json:"expired"` Normal int `json:"normal"` Used int `json:"used"` Discarded int `json:"discarded"` } func NewService(repo *Repository) *Service { return &Service{repo: repo} } // CreateItem 创建物品,并处理过期日期自动计算。 func (s *Service) CreateItem(userID uint, req CreateItemRequest) (*ExpiryItem, error) { if req.MiniProgramID == 0 { return nil, ErrExpiryMiniProgramRequired } if err := validateBaseFields(req.Name, req.Category, req.Quantity, req.Location, req.Remark); err != nil { return nil, err } if req.ShelfLifeDays != nil && *req.ShelfLifeDays <= 0 { return nil, ErrExpiryShelfLifeDaysInvalid } item := &ExpiryItem{ UserID: userID, MiniProgramID: req.MiniProgramID, Name: strings.TrimSpace(req.Name), Category: strings.TrimSpace(req.Category), Quantity: req.Quantity, Location: strings.TrimSpace(req.Location), Remark: strings.TrimSpace(req.Remark), Status: StatusNormal, } if item.Quantity == 0 { item.Quantity = 1 } if req.ProductionDate != nil { pd := dateOnly(*req.ProductionDate) item.ProductionDate = &pd } if err := applyExpiryDate(item, req.ExpiryDate, req.ShelfLifeDays); err != nil { return nil, err } if err := s.repo.Create(item); err != nil { return nil, err } item.Status = item.CalculateStatus() return item, nil } // UpdateItem 更新物品。 func (s *Service) UpdateItem(id, userID uint, req UpdateItemRequest) (*ExpiryItem, error) { item, err := s.repo.FindByID(id, userID) if err != nil { return nil, err } if err := validateBaseFields(req.Name, req.Category, req.Quantity, req.Location, req.Remark); err != nil { return nil, err } if req.ShelfLifeDays != nil && *req.ShelfLifeDays <= 0 { return nil, ErrExpiryShelfLifeDaysInvalid } item.Name = strings.TrimSpace(req.Name) item.Category = strings.TrimSpace(req.Category) item.Quantity = req.Quantity item.Location = strings.TrimSpace(req.Location) item.Remark = strings.TrimSpace(req.Remark) item.MiniProgramID = req.MiniProgramID item.ProductionDate = nil item.ShelfLifeDays = nil if req.MiniProgramID == 0 { return nil, ErrExpiryMiniProgramRequired } if item.Quantity == 0 { item.Quantity = 1 } if req.ProductionDate != nil { pd := dateOnly(*req.ProductionDate) item.ProductionDate = &pd } if err := applyExpiryDate(item, req.ExpiryDate, req.ShelfLifeDays); err != nil { return nil, err } // 已标记为 used/discarded 的物品保留状态,其余回归可计算状态。 if item.Status != StatusUsed && item.Status != StatusDiscarded { item.Status = StatusNormal } if err := s.repo.Update(item); err != nil { return nil, err } item.Status = item.CalculateStatus() return item, nil } // DeleteItem 删除物品。 func (s *Service) DeleteItem(id, userID uint) error { return s.repo.Delete(id, userID) } // GetItem 获取单个物品。 func (s *Service) GetItem(id, userID uint) (*ExpiryItem, error) { item, err := s.repo.FindByID(id, userID) if err != nil { return nil, err } item.Status = item.CalculateStatus() return item, nil } // GetItems 获取列表并计算 days_left/status。 func (s *Service) GetItems(userID uint, filters ItemFilters) (*ItemListResponse, error) { filters = normalizeFilters(filters) if err := validateFilters(filters); err != nil { return nil, err } items, total, err := s.repo.FindByUser(userID, map[string]interface{}{ "status": filters.Status, "category": filters.Category, "sort": filters.Sort, }, filters.Page, filters.PageSize) if err != nil { return nil, err } result := make([]ItemView, 0, len(items)) for _, item := range items { result = append(result, toItemView(item)) } return &ItemListResponse{ Items: result, Total: total, Page: filters.Page, PageSize: filters.PageSize, }, nil } // GetSummary 获取首页汇总。 func (s *Service) GetSummary(userID uint) (*SummaryResponse, error) { data, err := s.repo.GetSummary(userID) if err != nil { return nil, err } return &SummaryResponse{ TotalItems: data["total_items"], ExpiringSoon: data["expiring_soon"], Expired: data["expired"], Normal: data["normal"], Used: data["used"], Discarded: data["discarded"], }, nil } // UpdateItemStatus 标记物品状态。 func (s *Service) UpdateItemStatus(id, userID uint, status string) error { status = strings.TrimSpace(status) if status != StatusUsed && status != StatusDiscarded { return ErrExpiryStatusInvalid } return s.repo.UpdateStatus(id, userID, status) } func validateBaseFields(name, category string, quantity int, location, remark string) error { name = strings.TrimSpace(name) category = strings.TrimSpace(category) location = strings.TrimSpace(location) remark = strings.TrimSpace(remark) if name == "" || len(name) > 100 { return ErrExpiryNameInvalid } if !isValidCategory(category) { return ErrExpiryCategoryInvalid } if quantity < 0 { return ErrExpiryQuantityInvalid } if len(location) > 50 { return ErrExpiryLocationTooLong } if len(remark) > 255 { return ErrExpiryRemarkTooLong } return nil } func applyExpiryDate(item *ExpiryItem, expiryDate *time.Time, shelfLifeDays *int) error { // 若提供生产日期 + 保质期,优先自动计算过期日期。 if item.ProductionDate != nil && shelfLifeDays != nil { item.ShelfLifeDays = shelfLifeDays item.ExpiryDate = dateOnly(item.ProductionDate.AddDate(0, 0, *shelfLifeDays)) return nil } if expiryDate != nil { item.ExpiryDate = dateOnly(*expiryDate) item.ShelfLifeDays = shelfLifeDays return nil } return ErrExpiryDateRequired } func normalizeFilters(filters ItemFilters) ItemFilters { if filters.Page <= 0 { filters.Page = 1 } if filters.PageSize <= 0 { filters.PageSize = 20 } if filters.PageSize > 100 { filters.PageSize = 100 } if strings.TrimSpace(filters.Status) == "" { filters.Status = "all" } if strings.TrimSpace(filters.Category) == "" { filters.Category = "all" } if strings.TrimSpace(filters.Sort) == "" { filters.Sort = "expiry_date" } return filters } func validateFilters(filters ItemFilters) error { switch filters.Status { case "all", StatusExpiring, StatusExpired, StatusNormal, StatusUsed, StatusDiscarded: default: return ErrExpiryFilterStatusInvalid } switch filters.Category { case "all", CategoryFood, CategoryMedicine, CategoryCosmetic, CategoryOther: default: return ErrExpiryFilterCategoryInvalid } switch filters.Sort { case "expiry_date", "created_at": default: return ErrExpiryFilterSortInvalid } return nil } func isValidCategory(category string) bool { switch category { case CategoryFood, CategoryMedicine, CategoryCosmetic, CategoryOther: return true default: return false } } func toItemView(item ExpiryItem) ItemView { calculatedStatus := item.CalculateStatus() return ItemView{ ID: item.ID, Name: item.Name, Category: item.Category, ProductionDate: item.ProductionDate, ExpiryDate: item.ExpiryDate, ShelfLifeDays: item.ShelfLifeDays, Quantity: item.Quantity, Location: item.Location, Remark: item.Remark, Status: calculatedStatus, DaysLeft: item.CalculateDaysLeft(), CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, } }