Files
wx_service/internal/expiry/handler.go
T

357 lines
8.5 KiB
Go

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, "服务器错误")
}