package app import ( "net/http" "strconv" "bindbox-game/internal/code" "bindbox-game/internal/pkg/core" "bindbox-game/internal/pkg/logger" "bindbox-game/internal/pkg/validation" "bindbox-game/internal/repository/mysql" "bindbox-game/internal/repository/mysql/dao" prodsvc "bindbox-game/internal/service/product" usersvc "bindbox-game/internal/service/user" ) type productHandler struct { logger logger.CustomLogger readDB *dao.Query product prodsvc.Service user usersvc.Service } func NewProduct(logger logger.CustomLogger, db mysql.Repo, user usersvc.Service) *productHandler { return &productHandler{logger: logger, readDB: dao.Use(db.GetDbR()), product: prodsvc.New(logger, db), user: user} } type listAppProductsRequest struct { Page int `form:"page"` PageSize int `form:"page_size"` CategoryID *int64 `form:"category_id"` PriceMin *int64 `form:"price_min"` PriceMax *int64 `form:"price_max"` SalesMin *int64 `form:"sales_min"` InStock *bool `form:"in_stock"` SortBy string `form:"sort_by"` Order string `form:"order"` } type listAppProductsItem struct { ID int64 `json:"id"` Name string `json:"name"` MainImage string `json:"main_image"` Price int64 `json:"price"` PointsRequired int64 `json:"points_required"` Sales int64 `json:"sales"` InStock bool `json:"in_stock"` } type listAppProductsResponse struct { Total int64 `json:"total"` CurrentPage int `json:"currentPage"` PageSize int `json:"pageSize"` List []listAppProductsItem `json:"list"` } // ListProductsForApp 商品列表 // @Summary 商品列表 // @Description 分页查询商品列表,支持分类筛选,返回分页信息与商品数组 // @Tags APP端.商品 // @Accept json // @Produce json // @Security LoginVerifyToken // @Param page query int false "页码,默认1" // @Param page_size query int false "每页数量,默认20" // @Param category_id query int false "分类ID" // @Success 200 {object} listAppProductsResponse // @Failure 400 {object} code.Failure // @Router /api/app/products [get] func (h *productHandler) ListProductsForApp() core.HandlerFunc { return func(ctx core.Context) { req := new(listAppProductsRequest) if err := ctx.ShouldBindForm(req); err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) return } if req.Page <= 0 { req.Page = 1 } if req.PageSize <= 0 { req.PageSize = 20 } items, total, err := h.product.ListForApp(ctx.RequestContext(), prodsvc.AppListInput{CategoryID: req.CategoryID, PriceMin: req.PriceMin, PriceMax: req.PriceMax, SalesMin: req.SalesMin, InStock: req.InStock, SortBy: req.SortBy, Order: req.Order, Page: req.Page, PageSize: req.PageSize}) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, validation.Error(err))) return } rsp := &listAppProductsResponse{Total: total, CurrentPage: req.Page, PageSize: req.PageSize, List: make([]listAppProductsItem, len(items))} for i, it := range items { pts, _ := h.user.CentsToPoints(ctx.RequestContext(), it.Price) rsp.List[i] = listAppProductsItem{ID: it.ID, Name: it.Name, MainImage: it.MainImage, Price: it.Price, PointsRequired: pts, Sales: it.Sales, InStock: it.InStock} } ctx.Payload(rsp) } } type getAppProductDetailResponse struct { ID int64 `json:"id"` Name string `json:"name"` Album []string `json:"album"` Price int64 `json:"price"` PointsRequired int64 `json:"points_required"` Sales int64 `json:"sales"` Stock int64 `json:"stock"` Description string `json:"description"` Service []string `json:"service"` Recommendations []listAppProductsItem `json:"recommendations"` } // GetProductDetailForApp 商品详情 // @Summary 商品详情 // @Description 根据商品ID返回完整商品信息,含相册与同类推荐;校验下架/缺货状态 // @Tags APP端.商品 // @Accept json // @Produce json // @Security LoginVerifyToken // @Param id path int true "商品ID" // @Success 200 {object} getAppProductDetailResponse // @Failure 400 {object} code.Failure // @Router /api/app/products/{id} [get] func (h *productHandler) GetProductDetailForApp() core.HandlerFunc { return func(ctx core.Context) { idStr := ctx.Param("id") id, _ := strconv.ParseInt(idStr, 10, 64) d, err := h.product.GetDetailForApp(ctx.RequestContext(), id) if err != nil { if err.Error() == "PRODUCT_OFFSHELF" { ctx.AbortWithError(core.Error(http.StatusOK, 20001, "商品已下架")) return } if err.Error() == "PRODUCT_OUT_OF_STOCK" { ctx.AbortWithError(core.Error(http.StatusOK, 20002, "商品缺货")) return } ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, validation.Error(err))) return } ptsDetail, _ := h.user.CentsToPoints(ctx.RequestContext(), d.Price) rsp := &getAppProductDetailResponse{ID: d.ID, Name: d.Name, Album: d.Album, Price: d.Price, PointsRequired: ptsDetail, Sales: d.Sales, Stock: d.Stock, Description: d.Description, Service: d.Service, Recommendations: make([]listAppProductsItem, len(d.Recommendations))} for i, it := range d.Recommendations { ptsRec, _ := h.user.CentsToPoints(ctx.RequestContext(), it.Price) rsp.Recommendations[i] = listAppProductsItem{ID: it.ID, Name: it.Name, MainImage: it.MainImage, Price: it.Price, PointsRequired: ptsRec, Sales: it.Sales, InStock: it.InStock} } ctx.Payload(rsp) } } func parseImages(s string) []string { var arr []string // lightweight JSON array parser without comments if s == "" { return arr } // naive parse: trim brackets and split by comma, remove quotes b := []byte(s) if len(b) < 2 { return arr } start, end := 0, len(b) for start < end && (b[start] == '[' || b[start] == ' ' || b[start] == '\n' || b[start] == '\t') { start++ } for end > start && (b[end-1] == ']' || b[end-1] == ' ' || b[end-1] == '\n' || b[end-1] == '\t') { end-- } if start >= end { return arr } content := string(b[start:end]) parts := []rune(content) cur := "" inStr := false for _, r := range parts { if r == '"' { if inStr { arr = append(arr, cur) cur = "" inStr = false } else { inStr = true } continue } if inStr { cur += string(r) } } return arr } func parseFirstImage(s string) string { imgs := parseImages(s) if len(imgs) > 0 { return imgs[0] } return "" }