From cc16b342d7a0cba906d52ba8248f8eee566f8837 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 4 Mar 2026 17:12:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(expiry):=20=E5=AE=8C=E6=88=90=20#25=20?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E8=AE=BE=E7=BD=AE=E6=8E=A5=E5=8F=A3=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/api/main.go | 2 + internal/expiry/handler.go | 43 ++++++++++++++++++++- internal/expiry/repository.go | 49 ++++++++++++++++++++++++ internal/expiry/service.go | 66 ++++++++++++++++++++++++++++++++ internal/routes/expiry_routes.go | 2 + 5 files changed, 161 insertions(+), 1 deletion(-) diff --git a/cmd/api/main.go b/cmd/api/main.go index 6fa43c9..ea61fc9 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -52,6 +52,8 @@ func main() { &model.MiniProgram{}, &model.User{}, &model.UserMembership{}, + &expiry.ExpiryItem{}, + &expiry.ExpiryUserSettings{}, &membershipmodel.MembershipRedeemCode{}, &membershipmodel.MembershipRedemption{}, &rmmodel.VideoParseLog{}, diff --git a/internal/expiry/handler.go b/internal/expiry/handler.go index 333cd17..1702b20 100644 --- a/internal/expiry/handler.go +++ b/internal/expiry/handler.go @@ -49,6 +49,10 @@ type updateStatusRequest struct { Status string `json:"status"` } +type updateSettingsRequest struct { + RemindDays []int `json:"remind_days"` +} + const expiryDateLayout = "2006-01-02" // GetSummary 获取首页汇总统计。 @@ -216,6 +220,42 @@ func (h *Handler) UpdateStatus(c *gin.Context) { }) } +// 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 { @@ -289,7 +329,8 @@ func isExpiryBadRequestError(err error) bool { errors.Is(err, ErrExpiryFilterStatusInvalid) || errors.Is(err, ErrExpiryFilterCategoryInvalid) || errors.Is(err, ErrExpiryFilterSortInvalid) || - errors.Is(err, ErrExpiryStatusInvalid) + errors.Is(err, ErrExpiryStatusInvalid) || + errors.Is(err, ErrExpiryRemindDaysInvalid) } func writeExpirySuccess(c *gin.Context, message string, data interface{}) { diff --git a/internal/expiry/repository.go b/internal/expiry/repository.go index 5ae7422..b970bf5 100644 --- a/internal/expiry/repository.go +++ b/internal/expiry/repository.go @@ -217,3 +217,52 @@ func (r *Repository) UpdateStatus(id, userID uint, status string) error { } return nil } + +// GetSettings 查询用户提醒设置。 +func (r *Repository) GetSettings(userID uint) (*ExpiryUserSettings, error) { + var settings ExpiryUserSettings + err := r.db.Where("user_id = ?", userID).First(&settings).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, fmt.Errorf("find expiry settings: %w", err) + } + return &settings, nil +} + +// UpdateSettings 更新用户提醒设置(不存在则创建)。 +func (r *Repository) UpdateSettings(userID uint, remindDays []int) (*ExpiryUserSettings, error) { + settings, err := r.GetSettings(userID) + if err != nil { + return nil, err + } + + if settings == nil { + settings = &ExpiryUserSettings{ + UserID: userID, + RemindDays: copyIntSlice(remindDays), + } + if err := r.db.Create(settings).Error; err != nil { + return nil, fmt.Errorf("create expiry settings: %w", err) + } + return settings, nil + } + + settings.RemindDays = copyIntSlice(remindDays) + if err := r.db.Model(&ExpiryUserSettings{}). + Where("user_id = ?", userID). + Update("remind_days", settings.RemindDays).Error; err != nil { + return nil, fmt.Errorf("update expiry settings: %w", err) + } + return settings, nil +} + +func copyIntSlice(values []int) []int { + if len(values) == 0 { + return nil + } + copied := make([]int, len(values)) + copy(copied, values) + return copied +} diff --git a/internal/expiry/service.go b/internal/expiry/service.go index 1017a98..a1d31d2 100644 --- a/internal/expiry/service.go +++ b/internal/expiry/service.go @@ -19,6 +19,7 @@ var ( ErrExpiryFilterCategoryInvalid = errors.New("category 参数无效") ErrExpiryFilterSortInvalid = errors.New("sort 参数无效") ErrExpiryStatusInvalid = errors.New("状态无效,仅支持 used/discarded") + ErrExpiryRemindDaysInvalid = errors.New("remind_days 必须是 1-30 的整数,数量 1-5 个") ) // Service 封装保质期模块业务逻辑。 @@ -86,6 +87,11 @@ type SummaryResponse struct { Discarded int `json:"discarded"` } +// SettingsResponse 用户提醒设置返回结构。 +type SettingsResponse struct { + RemindDays []int `json:"remind_days"` +} + func NewService(repo *Repository) *Service { return &Service{repo: repo} } @@ -255,6 +261,41 @@ func (s *Service) UpdateItemStatus(id, userID uint, status string) error { return s.repo.UpdateStatus(id, userID, status) } +// GetSettings 获取用户提醒设置;若未配置则返回默认值。 +func (s *Service) GetSettings(userID uint) (*SettingsResponse, error) { + settings, err := s.repo.GetSettings(userID) + if err != nil { + return nil, err + } + + if settings == nil || len(settings.RemindDays) == 0 { + return &SettingsResponse{ + RemindDays: copyIntSlice(defaultRemindDays), + }, nil + } + + return &SettingsResponse{ + RemindDays: copyIntSlice(settings.RemindDays), + }, nil +} + +// UpdateSettings 更新用户提醒设置。 +func (s *Service) UpdateSettings(userID uint, remindDays []int) (*SettingsResponse, error) { + validated, err := validateRemindDays(remindDays) + if err != nil { + return nil, err + } + + settings, err := s.repo.UpdateSettings(userID, validated) + if err != nil { + return nil, err + } + + return &SettingsResponse{ + RemindDays: copyIntSlice(settings.RemindDays), + }, nil +} + func validateBaseFields(name, category string, quantity int, location, remark string) error { name = strings.TrimSpace(name) category = strings.TrimSpace(category) @@ -367,3 +408,28 @@ func toItemView(item ExpiryItem) ItemView { UpdatedAt: item.UpdatedAt, } } + +func validateRemindDays(days []int) ([]int, error) { + if len(days) == 0 || len(days) > 5 { + return nil, ErrExpiryRemindDaysInvalid + } + + seen := make(map[int]struct{}, len(days)) + result := make([]int, 0, len(days)) + for _, day := range days { + if day < 1 || day > 30 { + return nil, ErrExpiryRemindDaysInvalid + } + if _, ok := seen[day]; ok { + continue + } + seen[day] = struct{}{} + result = append(result, day) + } + + if len(result) == 0 || len(result) > 5 { + return nil, ErrExpiryRemindDaysInvalid + } + + return result, nil +} diff --git a/internal/routes/expiry_routes.go b/internal/routes/expiry_routes.go index 3b8eeb7..d3f0de0 100644 --- a/internal/routes/expiry_routes.go +++ b/internal/routes/expiry_routes.go @@ -20,5 +20,7 @@ func registerExpiryRoutes(protected *gin.RouterGroup, expiryHandler *expiryhandl expiry.PUT("/items/:id", expiryHandler.UpdateItem) expiry.DELETE("/items/:id", expiryHandler.DeleteItem) expiry.POST("/items/:id/status", expiryHandler.UpdateStatus) + expiry.GET("/settings", expiryHandler.GetSettings) + expiry.POST("/settings", expiryHandler.UpdateSettings) } }