refactor: expiry 模块按 handler/model/service 分层

This commit is contained in:
root
2026-03-09 21:13:59 +08:00
parent f45940cbc5
commit 27617cd373
6 changed files with 258 additions and 85 deletions
+133
View File
@@ -0,0 +1,133 @@
package expiry
import (
"time"
"github.com/gin-gonic/gin"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
expiryhandler "wx_service/internal/expiry/handler"
expirymodel "wx_service/internal/expiry/model"
expiryrepo "wx_service/internal/expiry/repository"
expiryservice "wx_service/internal/expiry/service"
)
const (
CategoryFood = expirymodel.CategoryFood
CategoryMedicine = expirymodel.CategoryMedicine
CategoryCosmetic = expirymodel.CategoryCosmetic
CategoryOther = expirymodel.CategoryOther
StatusNormal = expirymodel.StatusNormal
StatusExpiring = expirymodel.StatusExpiring
StatusExpired = expirymodel.StatusExpired
StatusUsed = expirymodel.StatusUsed
StatusDiscarded = expirymodel.StatusDiscarded
)
var defaultRemindDays = expirymodel.DefaultRemindDays
type ExpiryItem = expirymodel.ExpiryItem
type ExpiryUserSettings = expirymodel.ExpiryUserSettings
type Repository = expiryrepo.Repository
type Service = expiryservice.Service
type Handler = expiryhandler.Handler
type SummaryCache = expiryservice.SummaryCache
type CreateItemRequest = expiryservice.CreateItemRequest
type UpdateItemRequest = expiryservice.UpdateItemRequest
type ItemFilters = expiryservice.ItemFilters
type ItemView = expiryservice.ItemView
type ItemListResponse = expiryservice.ItemListResponse
type SummaryResponse = expiryservice.SummaryResponse
type SettingsResponse = expiryservice.SettingsResponse
var (
ErrExpiryItemNotFound = expiryrepo.ErrExpiryItemNotFound
ErrExpiryNameInvalid = expiryservice.ErrExpiryNameInvalid
ErrExpiryCategoryInvalid = expiryservice.ErrExpiryCategoryInvalid
ErrExpiryMiniProgramRequired = expiryservice.ErrExpiryMiniProgramRequired
ErrExpiryQuantityInvalid = expiryservice.ErrExpiryQuantityInvalid
ErrExpiryLocationTooLong = expiryservice.ErrExpiryLocationTooLong
ErrExpiryRemarkTooLong = expiryservice.ErrExpiryRemarkTooLong
ErrExpiryDateRequired = expiryservice.ErrExpiryDateRequired
ErrExpiryShelfLifeDaysInvalid = expiryservice.ErrExpiryShelfLifeDaysInvalid
ErrExpiryFilterStatusInvalid = expiryservice.ErrExpiryFilterStatusInvalid
ErrExpiryFilterCategoryInvalid = expiryservice.ErrExpiryFilterCategoryInvalid
ErrExpiryFilterSortInvalid = expiryservice.ErrExpiryFilterSortInvalid
ErrExpiryStatusInvalid = expiryservice.ErrExpiryStatusInvalid
ErrExpiryRemindDaysInvalid = expiryservice.ErrExpiryRemindDaysInvalid
)
func NewRepository(db *gorm.DB) *Repository {
return expiryrepo.NewRepository(db)
}
func NewService(repo *Repository) *Service {
return expiryservice.NewService(repo)
}
func NewHandler(service *Service) *Handler {
return expiryhandler.NewHandler(service)
}
func NewSummaryCache(rc *redis.Client, keyPrefix string, ttl time.Duration) *SummaryCache {
return expiryservice.NewSummaryCache(rc, keyPrefix, ttl)
}
func dateOnly(t time.Time) time.Time {
return expirymodel.DateOnly(t)
}
func normalizeFilters(filters ItemFilters) ItemFilters {
return expiryservice.NormalizeFilters(filters)
}
func validateFilters(filters ItemFilters) error {
return expiryservice.ValidateFilters(filters)
}
func validateBaseFields(name, category string, quantity int, location, remark string) error {
return expiryservice.ValidateBaseFields(name, category, quantity, location, remark)
}
func applyExpiryDate(item *ExpiryItem, expiryDate *time.Time, shelfLifeDays *int) error {
return expiryservice.ApplyExpiryDate(item, expiryDate, shelfLifeDays)
}
func isValidCategory(category string) bool {
return expiryservice.IsValidCategory(category)
}
func validateRemindDays(days []int) ([]int, error) {
return expiryservice.ValidateRemindDays(days)
}
func toItemView(item ExpiryItem) ItemView {
return expiryservice.ToItemView(item)
}
func writeExpirySuccess(c *gin.Context, message string, data interface{}) {
resp := gin.H{
"code": 0,
"message": message,
}
if data != nil {
resp["data"] = data
}
c.JSON(200, 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, 500, "服务器错误")
}
@@ -1,4 +1,4 @@
package expiry package handler
import ( import (
"errors" "errors"
@@ -10,15 +10,17 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
expiryrepo "wx_service/internal/expiry/repository"
expiryservice "wx_service/internal/expiry/service"
"wx_service/internal/middleware" "wx_service/internal/middleware"
) )
// Handler 负责 HTTP 层处理。 // Handler 负责 HTTP 层处理。
type Handler struct { type Handler struct {
service *Service service *expiryservice.Service
} }
func NewHandler(service *Service) *Handler { func NewHandler(service *expiryservice.Service) *Handler {
return &Handler{service: service} return &Handler{service: service}
} }
@@ -72,7 +74,7 @@ func (h *Handler) GetItems(c *gin.Context) {
page := parseIntWithDefault(c.Query("page"), 1) page := parseIntWithDefault(c.Query("page"), 1)
pageSize := parseIntWithDefault(c.Query("page_size"), 20) pageSize := parseIntWithDefault(c.Query("page_size"), 20)
filters := ItemFilters{ filters := expiryservice.ItemFilters{
Status: strings.TrimSpace(c.DefaultQuery("status", "all")), Status: strings.TrimSpace(c.DefaultQuery("status", "all")),
Category: strings.TrimSpace(c.DefaultQuery("category", "all")), Category: strings.TrimSpace(c.DefaultQuery("category", "all")),
Sort: strings.TrimSpace(c.DefaultQuery("sort", "expiry_date")), Sort: strings.TrimSpace(c.DefaultQuery("sort", "expiry_date")),
@@ -119,7 +121,7 @@ func (h *Handler) CreateItem(c *gin.Context) {
return return
} }
writeExpirySuccess(c, "添加成功", toItemView(*item)) writeExpirySuccess(c, "添加成功", expiryservice.ToItemView(*item))
} }
// UpdateItem 更新物品。 // UpdateItem 更新物品。
@@ -144,7 +146,7 @@ func (h *Handler) UpdateItem(c *gin.Context) {
item, err := h.service.UpdateItem(id, user.ID, serviceReq) item, err := h.service.UpdateItem(id, user.ID, serviceReq)
if err != nil { if err != nil {
if errors.Is(err, ErrExpiryItemNotFound) { if errors.Is(err, expiryrepo.ErrExpiryItemNotFound) {
writeExpiryError(c, http.StatusNotFound, "物品不存在") writeExpiryError(c, http.StatusNotFound, "物品不存在")
return return
} }
@@ -156,7 +158,7 @@ func (h *Handler) UpdateItem(c *gin.Context) {
return return
} }
writeExpirySuccess(c, "更新成功", toItemView(*item)) writeExpirySuccess(c, "更新成功", expiryservice.ToItemView(*item))
} }
// DeleteItem 删除物品。 // DeleteItem 删除物品。
@@ -169,7 +171,7 @@ func (h *Handler) DeleteItem(c *gin.Context) {
err := h.service.DeleteItem(id, user.ID) err := h.service.DeleteItem(id, user.ID)
if err != nil { if err != nil {
if errors.Is(err, ErrExpiryItemNotFound) { if errors.Is(err, expiryrepo.ErrExpiryItemNotFound) {
writeExpiryError(c, http.StatusNotFound, "物品不存在") writeExpiryError(c, http.StatusNotFound, "物品不存在")
return return
} }
@@ -195,7 +197,7 @@ func (h *Handler) UpdateStatus(c *gin.Context) {
} }
if err := h.service.UpdateItemStatus(id, user.ID, strings.TrimSpace(req.Status)); err != nil { if err := h.service.UpdateItemStatus(id, user.ID, strings.TrimSpace(req.Status)); err != nil {
if errors.Is(err, ErrExpiryItemNotFound) { if errors.Is(err, expiryrepo.ErrExpiryItemNotFound) {
writeExpiryError(c, http.StatusNotFound, "物品不存在") writeExpiryError(c, http.StatusNotFound, "物品不存在")
return return
} }
@@ -256,15 +258,15 @@ func (h *Handler) UpdateSettings(c *gin.Context) {
writeExpirySuccess(c, "更新成功", resp) writeExpirySuccess(c, "更新成功", resp)
} }
func (h *Handler) toCreateItemRequest(miniProgramID uint, req createOrUpdateItemRequest) (CreateItemRequest, error) { func (h *Handler) toCreateItemRequest(miniProgramID uint, req createOrUpdateItemRequest) (expiryservice.CreateItemRequest, error) {
productionDate, err := parseDateString(req.ProductionDate) productionDate, err := parseDateString(req.ProductionDate)
if err != nil { if err != nil {
return CreateItemRequest{}, errors.New("production_date 格式错误,应为 YYYY-MM-DD") return expiryservice.CreateItemRequest{}, errors.New("production_date 格式错误,应为 YYYY-MM-DD")
} }
expiryDate, err := parseDateString(req.ExpiryDate) expiryDate, err := parseDateString(req.ExpiryDate)
if err != nil { if err != nil {
return CreateItemRequest{}, errors.New("expiry_date 格式错误,应为 YYYY-MM-DD") return expiryservice.CreateItemRequest{}, errors.New("expiry_date 格式错误,应为 YYYY-MM-DD")
} }
quantity := 1 quantity := 1
@@ -272,7 +274,7 @@ func (h *Handler) toCreateItemRequest(miniProgramID uint, req createOrUpdateItem
quantity = *req.Quantity quantity = *req.Quantity
} }
return CreateItemRequest{ return expiryservice.CreateItemRequest{
MiniProgramID: miniProgramID, MiniProgramID: miniProgramID,
Name: req.Name, Name: req.Name,
Category: req.Category, Category: req.Category,
@@ -318,19 +320,19 @@ func parseItemID(c *gin.Context) (uint, bool) {
} }
func isExpiryBadRequestError(err error) bool { func isExpiryBadRequestError(err error) bool {
return errors.Is(err, ErrExpiryNameInvalid) || return errors.Is(err, expiryservice.ErrExpiryNameInvalid) ||
errors.Is(err, ErrExpiryCategoryInvalid) || errors.Is(err, expiryservice.ErrExpiryCategoryInvalid) ||
errors.Is(err, ErrExpiryMiniProgramRequired) || errors.Is(err, expiryservice.ErrExpiryMiniProgramRequired) ||
errors.Is(err, ErrExpiryQuantityInvalid) || errors.Is(err, expiryservice.ErrExpiryQuantityInvalid) ||
errors.Is(err, ErrExpiryLocationTooLong) || errors.Is(err, expiryservice.ErrExpiryLocationTooLong) ||
errors.Is(err, ErrExpiryRemarkTooLong) || errors.Is(err, expiryservice.ErrExpiryRemarkTooLong) ||
errors.Is(err, ErrExpiryDateRequired) || errors.Is(err, expiryservice.ErrExpiryDateRequired) ||
errors.Is(err, ErrExpiryShelfLifeDaysInvalid) || errors.Is(err, expiryservice.ErrExpiryShelfLifeDaysInvalid) ||
errors.Is(err, ErrExpiryFilterStatusInvalid) || errors.Is(err, expiryservice.ErrExpiryFilterStatusInvalid) ||
errors.Is(err, ErrExpiryFilterCategoryInvalid) || errors.Is(err, expiryservice.ErrExpiryFilterCategoryInvalid) ||
errors.Is(err, ErrExpiryFilterSortInvalid) || errors.Is(err, expiryservice.ErrExpiryFilterSortInvalid) ||
errors.Is(err, ErrExpiryStatusInvalid) || errors.Is(err, expiryservice.ErrExpiryStatusInvalid) ||
errors.Is(err, ErrExpiryRemindDaysInvalid) errors.Is(err, expiryservice.ErrExpiryRemindDaysInvalid)
} }
func writeExpirySuccess(c *gin.Context, message string, data interface{}) { func writeExpirySuccess(c *gin.Context, message string, data interface{}) {
@@ -1,4 +1,4 @@
package expiry package model
import ( import (
"time" "time"
@@ -23,7 +23,7 @@ const (
StatusDiscarded = "discarded" StatusDiscarded = "discarded"
) )
var defaultRemindDays = []int{7, 3, 1} var DefaultRemindDays = []int{7, 3, 1}
// ExpiryItem 表示保质期物品数据模型。 // ExpiryItem 表示保质期物品数据模型。
type ExpiryItem struct { type ExpiryItem struct {
@@ -50,8 +50,8 @@ func (ExpiryItem) TableName() string {
// CalculateDaysLeft 计算当前日期与过期日期之间的天数差。 // CalculateDaysLeft 计算当前日期与过期日期之间的天数差。
func (item *ExpiryItem) CalculateDaysLeft() int { func (item *ExpiryItem) CalculateDaysLeft() int {
now := dateOnly(time.Now()) now := DateOnly(time.Now())
expiry := dateOnly(item.ExpiryDate) expiry := DateOnly(item.ExpiryDate)
return int(expiry.Sub(now).Hours() / 24) return int(expiry.Sub(now).Hours() / 24)
} }
@@ -85,7 +85,7 @@ func (ExpiryUserSettings) TableName() string {
return "expiry_user_settings" return "expiry_user_settings"
} }
func dateOnly(t time.Time) time.Time { func DateOnly(t time.Time) time.Time {
year, month, day := t.In(time.Local).Date() year, month, day := t.In(time.Local).Date()
return time.Date(year, month, day, 0, 0, 0, 0, time.Local) return time.Date(year, month, day, 0, 0, 0, 0, time.Local)
} }
@@ -1,10 +1,12 @@
package expiry package repository
import ( import (
"errors" "errors"
"fmt" "fmt"
"time" "time"
expirymodel "wx_service/internal/expiry/model"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -22,7 +24,7 @@ func NewRepository(db *gorm.DB) *Repository {
} }
// Create 创建物品。 // Create 创建物品。
func (r *Repository) Create(item *ExpiryItem) error { func (r *Repository) Create(item *expirymodel.ExpiryItem) error {
if err := r.db.Create(item).Error; err != nil { if err := r.db.Create(item).Error; err != nil {
return fmt.Errorf("create expiry item: %w", err) return fmt.Errorf("create expiry item: %w", err)
} }
@@ -30,7 +32,7 @@ func (r *Repository) Create(item *ExpiryItem) error {
} }
// Update 更新物品。 // Update 更新物品。
func (r *Repository) Update(item *ExpiryItem) error { func (r *Repository) Update(item *expirymodel.ExpiryItem) error {
updates := map[string]interface{}{ updates := map[string]interface{}{
"mini_program_id": item.MiniProgramID, "mini_program_id": item.MiniProgramID,
"name": item.Name, "name": item.Name,
@@ -44,7 +46,7 @@ func (r *Repository) Update(item *ExpiryItem) error {
"status": item.Status, "status": item.Status,
} }
tx := r.db.Model(&ExpiryItem{}). tx := r.db.Model(&expirymodel.ExpiryItem{}).
Where("id = ? AND user_id = ?", item.ID, item.UserID). Where("id = ? AND user_id = ?", item.ID, item.UserID).
Updates(updates) Updates(updates)
if tx.Error != nil { if tx.Error != nil {
@@ -58,7 +60,7 @@ func (r *Repository) Update(item *ExpiryItem) error {
// Delete 软删除物品。 // Delete 软删除物品。
func (r *Repository) Delete(id, userID uint) error { func (r *Repository) Delete(id, userID uint) error {
tx := r.db.Where("id = ? AND user_id = ?", id, userID).Delete(&ExpiryItem{}) tx := r.db.Where("id = ? AND user_id = ?", id, userID).Delete(&expirymodel.ExpiryItem{})
if tx.Error != nil { if tx.Error != nil {
return fmt.Errorf("delete expiry item: %w", tx.Error) return fmt.Errorf("delete expiry item: %w", tx.Error)
} }
@@ -69,8 +71,8 @@ func (r *Repository) Delete(id, userID uint) error {
} }
// FindByID 根据 ID 查询单个物品。 // FindByID 根据 ID 查询单个物品。
func (r *Repository) FindByID(id, userID uint) (*ExpiryItem, error) { func (r *Repository) FindByID(id, userID uint) (*expirymodel.ExpiryItem, error) {
var item ExpiryItem var item expirymodel.ExpiryItem
err := r.db.Where("id = ? AND user_id = ?", id, userID).First(&item).Error err := r.db.Where("id = ? AND user_id = ?", id, userID).First(&item).Error
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -86,7 +88,7 @@ func (r *Repository) FindByUser(
userID uint, userID uint,
filters map[string]interface{}, filters map[string]interface{},
page, pageSize int, page, pageSize int,
) ([]ExpiryItem, int64, error) { ) ([]expirymodel.ExpiryItem, int64, error) {
if page <= 0 { if page <= 0 {
page = 1 page = 1
} }
@@ -97,22 +99,22 @@ func (r *Repository) FindByUser(
pageSize = 100 pageSize = 100
} }
query := r.db.Model(&ExpiryItem{}).Where("user_id = ?", userID) query := r.db.Model(&expirymodel.ExpiryItem{}).Where("user_id = ?", userID)
now := dateOnly(time.Now()) now := expirymodel.DateOnly(time.Now())
sevenDaysLater := now.AddDate(0, 0, 7) sevenDaysLater := now.AddDate(0, 0, 7)
if status, ok := filters["status"].(string); ok && status != "" && status != "all" { if status, ok := filters["status"].(string); ok && status != "" && status != "all" {
switch status { switch status {
case StatusExpiring: case expirymodel.StatusExpiring:
query = query.Where("status NOT IN ?", []string{StatusUsed, StatusDiscarded}). query = query.Where("status NOT IN ?", []string{expirymodel.StatusUsed, expirymodel.StatusDiscarded}).
Where("expiry_date BETWEEN ? AND ?", now, sevenDaysLater) Where("expiry_date BETWEEN ? AND ?", now, sevenDaysLater)
case StatusExpired: case expirymodel.StatusExpired:
query = query.Where("status NOT IN ?", []string{StatusUsed, StatusDiscarded}). query = query.Where("status NOT IN ?", []string{expirymodel.StatusUsed, expirymodel.StatusDiscarded}).
Where("expiry_date < ?", now) Where("expiry_date < ?", now)
case StatusNormal: case expirymodel.StatusNormal:
query = query.Where("status NOT IN ?", []string{StatusUsed, StatusDiscarded}). query = query.Where("status NOT IN ?", []string{expirymodel.StatusUsed, expirymodel.StatusDiscarded}).
Where("expiry_date > ?", sevenDaysLater) Where("expiry_date > ?", sevenDaysLater)
case StatusUsed, StatusDiscarded: case expirymodel.StatusUsed, expirymodel.StatusDiscarded:
query = query.Where("status = ?", status) query = query.Where("status = ?", status)
} }
} }
@@ -137,7 +139,7 @@ func (r *Repository) FindByUser(
return nil, 0, fmt.Errorf("count expiry items: %w", err) return nil, 0, fmt.Errorf("count expiry items: %w", err)
} }
var items []ExpiryItem var items []expirymodel.ExpiryItem
offset := (page - 1) * pageSize offset := (page - 1) * pageSize
if err := query.Offset(offset).Limit(pageSize).Find(&items).Error; err != nil { if err := query.Offset(offset).Limit(pageSize).Find(&items).Error; err != nil {
return nil, 0, fmt.Errorf("list expiry items: %w", err) return nil, 0, fmt.Errorf("list expiry items: %w", err)
@@ -148,11 +150,11 @@ func (r *Repository) FindByUser(
// GetSummary 获取首页汇总统计数据。 // GetSummary 获取首页汇总统计数据。
func (r *Repository) GetSummary(userID uint) (map[string]int, error) { func (r *Repository) GetSummary(userID uint) (map[string]int, error) {
now := dateOnly(time.Now()) now := expirymodel.DateOnly(time.Now())
sevenDaysLater := now.AddDate(0, 0, 7) sevenDaysLater := now.AddDate(0, 0, 7)
base := func() *gorm.DB { base := func() *gorm.DB {
return r.db.Model(&ExpiryItem{}).Where("user_id = ?", userID) return r.db.Model(&expirymodel.ExpiryItem{}).Where("user_id = ?", userID)
} }
count := func(tx *gorm.DB) (int64, error) { count := func(tx *gorm.DB) (int64, error) {
@@ -163,35 +165,35 @@ func (r *Repository) GetSummary(userID uint) (map[string]int, error) {
return v, nil return v, nil
} }
totalItems, err := count(base().Where("status NOT IN ?", []string{StatusUsed, StatusDiscarded})) totalItems, err := count(base().Where("status NOT IN ?", []string{expirymodel.StatusUsed, expirymodel.StatusDiscarded}))
if err != nil { if err != nil {
return nil, fmt.Errorf("count total items: %w", err) return nil, fmt.Errorf("count total items: %w", err)
} }
expiringSoon, err := count(base().Where("status NOT IN ?", []string{StatusUsed, StatusDiscarded}). expiringSoon, err := count(base().Where("status NOT IN ?", []string{expirymodel.StatusUsed, expirymodel.StatusDiscarded}).
Where("expiry_date BETWEEN ? AND ?", now, sevenDaysLater)) Where("expiry_date BETWEEN ? AND ?", now, sevenDaysLater))
if err != nil { if err != nil {
return nil, fmt.Errorf("count expiring items: %w", err) return nil, fmt.Errorf("count expiring items: %w", err)
} }
expired, err := count(base().Where("status NOT IN ?", []string{StatusUsed, StatusDiscarded}). expired, err := count(base().Where("status NOT IN ?", []string{expirymodel.StatusUsed, expirymodel.StatusDiscarded}).
Where("expiry_date < ?", now)) Where("expiry_date < ?", now))
if err != nil { if err != nil {
return nil, fmt.Errorf("count expired items: %w", err) return nil, fmt.Errorf("count expired items: %w", err)
} }
normal, err := count(base().Where("status NOT IN ?", []string{StatusUsed, StatusDiscarded}). normal, err := count(base().Where("status NOT IN ?", []string{expirymodel.StatusUsed, expirymodel.StatusDiscarded}).
Where("expiry_date > ?", sevenDaysLater)) Where("expiry_date > ?", sevenDaysLater))
if err != nil { if err != nil {
return nil, fmt.Errorf("count normal items: %w", err) return nil, fmt.Errorf("count normal items: %w", err)
} }
used, err := count(base().Where("status = ?", StatusUsed)) used, err := count(base().Where("status = ?", expirymodel.StatusUsed))
if err != nil { if err != nil {
return nil, fmt.Errorf("count used items: %w", err) return nil, fmt.Errorf("count used items: %w", err)
} }
discarded, err := count(base().Where("status = ?", StatusDiscarded)) discarded, err := count(base().Where("status = ?", expirymodel.StatusDiscarded))
if err != nil { if err != nil {
return nil, fmt.Errorf("count discarded items: %w", err) return nil, fmt.Errorf("count discarded items: %w", err)
} }
@@ -208,7 +210,7 @@ func (r *Repository) GetSummary(userID uint) (map[string]int, error) {
// UpdateStatus 更新物品状态(used/discarded)。 // UpdateStatus 更新物品状态(used/discarded)。
func (r *Repository) UpdateStatus(id, userID uint, status string) error { func (r *Repository) UpdateStatus(id, userID uint, status string) error {
tx := r.db.Model(&ExpiryItem{}). tx := r.db.Model(&expirymodel.ExpiryItem{}).
Where("id = ? AND user_id = ?", id, userID). Where("id = ? AND user_id = ?", id, userID).
Update("status", status) Update("status", status)
if tx.Error != nil { if tx.Error != nil {
@@ -221,8 +223,8 @@ func (r *Repository) UpdateStatus(id, userID uint, status string) error {
} }
// GetSettings 查询用户提醒设置。 // GetSettings 查询用户提醒设置。
func (r *Repository) GetSettings(userID uint) (*ExpiryUserSettings, error) { func (r *Repository) GetSettings(userID uint) (*expirymodel.ExpiryUserSettings, error) {
var settings ExpiryUserSettings var settings expirymodel.ExpiryUserSettings
err := r.db.Where("user_id = ?", userID).First(&settings).Error err := r.db.Where("user_id = ?", userID).First(&settings).Error
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -234,14 +236,14 @@ func (r *Repository) GetSettings(userID uint) (*ExpiryUserSettings, error) {
} }
// UpdateSettings 更新用户提醒设置(不存在则创建)。 // UpdateSettings 更新用户提醒设置(不存在则创建)。
func (r *Repository) UpdateSettings(userID uint, remindDays []int) (*ExpiryUserSettings, error) { func (r *Repository) UpdateSettings(userID uint, remindDays []int) (*expirymodel.ExpiryUserSettings, error) {
settings, err := r.GetSettings(userID) settings, err := r.GetSettings(userID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if settings == nil { if settings == nil {
settings = &ExpiryUserSettings{ settings = &expirymodel.ExpiryUserSettings{
UserID: userID, UserID: userID,
RemindDays: copyIntSlice(remindDays), RemindDays: copyIntSlice(remindDays),
} }
@@ -1,4 +1,4 @@
package expiry package service
import ( import (
"context" "context"
@@ -1,10 +1,13 @@
package expiry package service
import ( import (
"context" "context"
"errors" "errors"
"strings" "strings"
"time" "time"
expirymodel "wx_service/internal/expiry/model"
expiryrepo "wx_service/internal/expiry/repository"
) )
var ( var (
@@ -25,7 +28,7 @@ var (
// Service 封装保质期模块业务逻辑。 // Service 封装保质期模块业务逻辑。
type Service struct { type Service struct {
repo *Repository repo *expiryrepo.Repository
summaryCache *SummaryCache summaryCache *SummaryCache
} }
@@ -94,7 +97,7 @@ type SettingsResponse struct {
RemindDays []int `json:"remind_days"` RemindDays []int `json:"remind_days"`
} }
func NewService(repo *Repository) *Service { func NewService(repo *expiryrepo.Repository) *Service {
return &Service{repo: repo} return &Service{repo: repo}
} }
@@ -104,7 +107,7 @@ func (s *Service) BindSummaryCache(cache *SummaryCache) {
} }
// CreateItem 创建物品,并处理过期日期自动计算。 // CreateItem 创建物品,并处理过期日期自动计算。
func (s *Service) CreateItem(userID uint, req CreateItemRequest) (*ExpiryItem, error) { func (s *Service) CreateItem(userID uint, req CreateItemRequest) (*expirymodel.ExpiryItem, error) {
if req.MiniProgramID == 0 { if req.MiniProgramID == 0 {
return nil, ErrExpiryMiniProgramRequired return nil, ErrExpiryMiniProgramRequired
} }
@@ -115,7 +118,7 @@ func (s *Service) CreateItem(userID uint, req CreateItemRequest) (*ExpiryItem, e
return nil, ErrExpiryShelfLifeDaysInvalid return nil, ErrExpiryShelfLifeDaysInvalid
} }
item := &ExpiryItem{ item := &expirymodel.ExpiryItem{
UserID: userID, UserID: userID,
MiniProgramID: req.MiniProgramID, MiniProgramID: req.MiniProgramID,
Name: strings.TrimSpace(req.Name), Name: strings.TrimSpace(req.Name),
@@ -123,7 +126,7 @@ func (s *Service) CreateItem(userID uint, req CreateItemRequest) (*ExpiryItem, e
Quantity: req.Quantity, Quantity: req.Quantity,
Location: strings.TrimSpace(req.Location), Location: strings.TrimSpace(req.Location),
Remark: strings.TrimSpace(req.Remark), Remark: strings.TrimSpace(req.Remark),
Status: StatusNormal, Status: expirymodel.StatusNormal,
} }
if item.Quantity == 0 { if item.Quantity == 0 {
@@ -131,7 +134,7 @@ func (s *Service) CreateItem(userID uint, req CreateItemRequest) (*ExpiryItem, e
} }
if req.ProductionDate != nil { if req.ProductionDate != nil {
pd := dateOnly(*req.ProductionDate) pd := expirymodel.DateOnly(*req.ProductionDate)
item.ProductionDate = &pd item.ProductionDate = &pd
} }
@@ -149,7 +152,7 @@ func (s *Service) CreateItem(userID uint, req CreateItemRequest) (*ExpiryItem, e
} }
// UpdateItem 更新物品。 // UpdateItem 更新物品。
func (s *Service) UpdateItem(id, userID uint, req UpdateItemRequest) (*ExpiryItem, error) { func (s *Service) UpdateItem(id, userID uint, req UpdateItemRequest) (*expirymodel.ExpiryItem, error) {
item, err := s.repo.FindByID(id, userID) item, err := s.repo.FindByID(id, userID)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -178,7 +181,7 @@ func (s *Service) UpdateItem(id, userID uint, req UpdateItemRequest) (*ExpiryIte
item.Quantity = 1 item.Quantity = 1
} }
if req.ProductionDate != nil { if req.ProductionDate != nil {
pd := dateOnly(*req.ProductionDate) pd := expirymodel.DateOnly(*req.ProductionDate)
item.ProductionDate = &pd item.ProductionDate = &pd
} }
@@ -187,8 +190,8 @@ func (s *Service) UpdateItem(id, userID uint, req UpdateItemRequest) (*ExpiryIte
} }
// 已标记为 used/discarded 的物品保留状态,其余回归可计算状态。 // 已标记为 used/discarded 的物品保留状态,其余回归可计算状态。
if item.Status != StatusUsed && item.Status != StatusDiscarded { if item.Status != expirymodel.StatusUsed && item.Status != expirymodel.StatusDiscarded {
item.Status = StatusNormal item.Status = expirymodel.StatusNormal
} }
if err := s.repo.Update(item); err != nil { if err := s.repo.Update(item); err != nil {
@@ -210,7 +213,7 @@ func (s *Service) DeleteItem(id, userID uint) error {
} }
// GetItem 获取单个物品。 // GetItem 获取单个物品。
func (s *Service) GetItem(id, userID uint) (*ExpiryItem, error) { func (s *Service) GetItem(id, userID uint) (*expirymodel.ExpiryItem, error) {
item, err := s.repo.FindByID(id, userID) item, err := s.repo.FindByID(id, userID)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -237,7 +240,7 @@ func (s *Service) GetItems(userID uint, filters ItemFilters) (*ItemListResponse,
result := make([]ItemView, 0, len(items)) result := make([]ItemView, 0, len(items))
for _, item := range items { for _, item := range items {
result = append(result, toItemView(item)) result = append(result, ToItemView(item))
} }
return &ItemListResponse{ return &ItemListResponse{
@@ -275,7 +278,7 @@ func (s *Service) GetSummary(userID uint) (*SummaryResponse, error) {
// UpdateItemStatus 标记物品状态。 // UpdateItemStatus 标记物品状态。
func (s *Service) UpdateItemStatus(id, userID uint, status string) error { func (s *Service) UpdateItemStatus(id, userID uint, status string) error {
status = strings.TrimSpace(status) status = strings.TrimSpace(status)
if status != StatusUsed && status != StatusDiscarded { if status != expirymodel.StatusUsed && status != expirymodel.StatusDiscarded {
return ErrExpiryStatusInvalid return ErrExpiryStatusInvalid
} }
if err := s.repo.UpdateStatus(id, userID, status); err != nil { if err := s.repo.UpdateStatus(id, userID, status); err != nil {
@@ -294,7 +297,7 @@ func (s *Service) GetSettings(userID uint) (*SettingsResponse, error) {
if settings == nil || len(settings.RemindDays) == 0 { if settings == nil || len(settings.RemindDays) == 0 {
return &SettingsResponse{ return &SettingsResponse{
RemindDays: copyIntSlice(defaultRemindDays), RemindDays: copyIntSlice(expirymodel.DefaultRemindDays),
}, nil }, nil
} }
@@ -344,16 +347,16 @@ func validateBaseFields(name, category string, quantity int, location, remark st
return nil return nil
} }
func applyExpiryDate(item *ExpiryItem, expiryDate *time.Time, shelfLifeDays *int) error { func applyExpiryDate(item *expirymodel.ExpiryItem, expiryDate *time.Time, shelfLifeDays *int) error {
// 若提供生产日期 + 保质期,优先自动计算过期日期。 // 若提供生产日期 + 保质期,优先自动计算过期日期。
if item.ProductionDate != nil && shelfLifeDays != nil { if item.ProductionDate != nil && shelfLifeDays != nil {
item.ShelfLifeDays = shelfLifeDays item.ShelfLifeDays = shelfLifeDays
item.ExpiryDate = dateOnly(item.ProductionDate.AddDate(0, 0, *shelfLifeDays)) item.ExpiryDate = expirymodel.DateOnly(item.ProductionDate.AddDate(0, 0, *shelfLifeDays))
return nil return nil
} }
if expiryDate != nil { if expiryDate != nil {
item.ExpiryDate = dateOnly(*expiryDate) item.ExpiryDate = expirymodel.DateOnly(*expiryDate)
item.ShelfLifeDays = shelfLifeDays item.ShelfLifeDays = shelfLifeDays
return nil return nil
} }
@@ -385,13 +388,13 @@ func normalizeFilters(filters ItemFilters) ItemFilters {
func validateFilters(filters ItemFilters) error { func validateFilters(filters ItemFilters) error {
switch filters.Status { switch filters.Status {
case "all", StatusExpiring, StatusExpired, StatusNormal, StatusUsed, StatusDiscarded: case "all", expirymodel.StatusExpiring, expirymodel.StatusExpired, expirymodel.StatusNormal, expirymodel.StatusUsed, expirymodel.StatusDiscarded:
default: default:
return ErrExpiryFilterStatusInvalid return ErrExpiryFilterStatusInvalid
} }
switch filters.Category { switch filters.Category {
case "all", CategoryFood, CategoryMedicine, CategoryCosmetic, CategoryOther: case "all", expirymodel.CategoryFood, expirymodel.CategoryMedicine, expirymodel.CategoryCosmetic, expirymodel.CategoryOther:
default: default:
return ErrExpiryFilterCategoryInvalid return ErrExpiryFilterCategoryInvalid
} }
@@ -407,14 +410,14 @@ func validateFilters(filters ItemFilters) error {
func isValidCategory(category string) bool { func isValidCategory(category string) bool {
switch category { switch category {
case CategoryFood, CategoryMedicine, CategoryCosmetic, CategoryOther: case expirymodel.CategoryFood, expirymodel.CategoryMedicine, expirymodel.CategoryCosmetic, expirymodel.CategoryOther:
return true return true
default: default:
return false return false
} }
} }
func toItemView(item ExpiryItem) ItemView { func ToItemView(item expirymodel.ExpiryItem) ItemView {
calculatedStatus := item.CalculateStatus() calculatedStatus := item.CalculateStatus()
return ItemView{ return ItemView{
ID: item.ID, ID: item.ID,
@@ -482,3 +485,36 @@ func (s *Service) invalidateSummaryCache(userID uint) {
} }
_ = s.summaryCache.Delete(context.Background(), userID) _ = s.summaryCache.Delete(context.Background(), userID)
} }
func copyIntSlice(values []int) []int {
if len(values) == 0 {
return nil
}
copied := make([]int, len(values))
copy(copied, values)
return copied
}
func NormalizeFilters(filters ItemFilters) ItemFilters {
return normalizeFilters(filters)
}
func ValidateFilters(filters ItemFilters) error {
return validateFilters(filters)
}
func ValidateBaseFields(name, category string, quantity int, location, remark string) error {
return validateBaseFields(name, category, quantity, location, remark)
}
func ApplyExpiryDate(item *expirymodel.ExpiryItem, expiryDate *time.Time, shelfLifeDays *int) error {
return applyExpiryDate(item, expiryDate, shelfLifeDays)
}
func IsValidCategory(category string) bool {
return isValidCategory(category)
}
func ValidateRemindDays(days []int) ([]int, error) {
return validateRemindDays(days)
}