package product import ( "context" "encoding/json" "errors" "strings" "time" "bindbox-game/internal/pkg/logger" "bindbox-game/internal/repository/mysql" "bindbox-game/internal/repository/mysql/dao" "bindbox-game/internal/repository/mysql/model" ) type Service interface { CreateCategory(ctx context.Context, in CreateCategoryInput) (*model.ProductCategories, error) ModifyCategory(ctx context.Context, id int64, in ModifyCategoryInput) error DeleteCategory(ctx context.Context, id int64) error ListCategories(ctx context.Context, in ListCategoriesInput) (items []*model.ProductCategories, total int64, err error) CreateProduct(ctx context.Context, in CreateProductInput) (*model.Products, error) ModifyProduct(ctx context.Context, id int64, in ModifyProductInput) error DeleteProduct(ctx context.Context, id int64) error ListProducts(ctx context.Context, in ListProductsInput) (items []*model.Products, total int64, err error) BatchUpdate(ctx context.Context, ids []int64, stock *int64, status *int32) (int64, error) ListForApp(ctx context.Context, in AppListInput) (items []AppListItem, total int64, err error) GetDetailForApp(ctx context.Context, id int64) (*AppDetail, error) ListProductsByIDs(ctx context.Context, ids []int64) ([]*model.Products, error) } type service struct { logger logger.CustomLogger readDB *dao.Query writeDB *dao.Query listCache map[string]cachedList detailCache map[int64]cachedDetail } func New(l logger.CustomLogger, db mysql.Repo) Service { return &service{logger: l, readDB: dao.Use(db.GetDbR()), writeDB: dao.Use(db.GetDbW()), listCache: make(map[string]cachedList), detailCache: make(map[int64]cachedDetail)} } type CreateCategoryInput struct { Name string ParentID int64 Status int32 } type ModifyCategoryInput struct { Name *string ParentID *int64 Status *int32 } type ListCategoriesInput struct { Name string Status *int32 Page int PageSize int } func (s *service) CreateCategory(ctx context.Context, in CreateCategoryInput) (*model.ProductCategories, error) { m := &model.ProductCategories{Name: in.Name, ParentID: in.ParentID, Status: in.Status} if err := s.writeDB.ProductCategories.WithContext(ctx).Create(m); err != nil { return nil, err } return m, nil } func (s *service) ModifyCategory(ctx context.Context, id int64, in ModifyCategoryInput) error { updater := s.writeDB.ProductCategories.WithContext(ctx).Where(s.writeDB.ProductCategories.ID.Eq(id)) set := map[string]any{} if in.Name != nil { set["name"] = *in.Name } if in.ParentID != nil { set["parent_id"] = *in.ParentID } if in.Status != nil { set["status"] = *in.Status } if len(set) == 0 { return nil } _, err := updater.Updates(set) return err } func (s *service) DeleteCategory(ctx context.Context, id int64) error { _, err := s.writeDB.ProductCategories.WithContext(ctx).Where(s.writeDB.ProductCategories.ID.Eq(id)).Updates(map[string]any{"deleted_at": time.Now()}) return err } func (s *service) ListCategories(ctx context.Context, in ListCategoriesInput) (items []*model.ProductCategories, total int64, err error) { if in.Page <= 0 { in.Page = 1 } if in.PageSize <= 0 { in.PageSize = 20 } q := s.readDB.ProductCategories.WithContext(ctx).ReadDB() if in.Name != "" { q = q.Where(s.readDB.ProductCategories.Name.Like("%" + in.Name + "%")) } if in.Status != nil { q = q.Where(s.readDB.ProductCategories.Status.Eq(*in.Status)) } total, err = q.Count() if err != nil { return } items, err = q.Order(s.readDB.ProductCategories.ID.Desc()).Limit(in.PageSize).Offset((in.Page - 1) * in.PageSize).Find() return } type CreateProductInput struct { Name string CategoryID int64 ImagesJSON string Price int64 Stock int64 Status int32 Description string } type ModifyProductInput struct { Name *string CategoryID *int64 ImagesJSON *string Price *int64 Stock *int64 Status *int32 Description *string } type ListProductsInput struct { Name string CategoryID *int64 Status *int32 Page int PageSize int } type AppListInput struct { CategoryID *int64 PriceMin *int64 PriceMax *int64 SalesMin *int64 InStock *bool SortBy string Order string Page int PageSize int } type AppListItem struct { ID int64 Name string MainImage string Price int64 Sales int64 InStock bool } type AppDetail struct { ID int64 Name string Album []string Price int64 Sales int64 Stock int64 Description string Service []string Recommendations []AppListItem } func (s *service) CreateProduct(ctx context.Context, in CreateProductInput) (*model.Products, error) { m := &model.Products{Name: in.Name, CategoryID: in.CategoryID, ImagesJSON: normalizeJSON(in.ImagesJSON), Price: in.Price, Stock: in.Stock, Status: in.Status, Description: in.Description} if err := s.writeDB.Products.WithContext(ctx).Create(m); err != nil { return nil, err } return m, nil } func (s *service) ModifyProduct(ctx context.Context, id int64, in ModifyProductInput) error { updater := s.writeDB.Products.WithContext(ctx).Where(s.writeDB.Products.ID.Eq(id)) set := map[string]any{} if in.Name != nil { set["name"] = *in.Name } if in.CategoryID != nil { set["category_id"] = *in.CategoryID } if in.ImagesJSON != nil { set["images_json"] = normalizeJSON(*in.ImagesJSON) } if in.Price != nil { set["price"] = *in.Price } if in.Stock != nil { set["stock"] = *in.Stock } if in.Status != nil { set["status"] = *in.Status } if in.Description != nil { set["description"] = *in.Description } if len(set) == 0 { return nil } _, err := updater.Updates(set) return err } func (s *service) DeleteProduct(ctx context.Context, id int64) error { _, err := s.writeDB.Products.WithContext(ctx).Where(s.writeDB.Products.ID.Eq(id)).Updates(map[string]any{"deleted_at": time.Now()}) return err } func (s *service) ListProducts(ctx context.Context, in ListProductsInput) (items []*model.Products, total int64, err error) { if in.Page <= 0 { in.Page = 1 } if in.PageSize <= 0 { in.PageSize = 20 } q := s.readDB.Products.WithContext(ctx).ReadDB() if in.Name != "" { q = q.Where(s.readDB.Products.Name.Like("%" + in.Name + "%")) } if in.CategoryID != nil { q = q.Where(s.readDB.Products.CategoryID.Eq(*in.CategoryID)) } if in.Status != nil { q = q.Where(s.readDB.Products.Status.Eq(*in.Status)) } total, err = q.Count() if err != nil { return } items, err = q.Order(s.readDB.Products.ID.Desc()).Limit(in.PageSize).Offset((in.Page - 1) * in.PageSize).Find() return } func (s *service) ListForApp(ctx context.Context, in AppListInput) (items []AppListItem, total int64, err error) { key := s.listKey(in) if v, ok := s.listCache[key]; ok && time.Now().Before(v.expireAt) { return v.items, v.total, nil } if in.Page <= 0 { in.Page = 1 } if in.PageSize <= 0 { in.PageSize = 20 } q := s.readDB.Products.WithContext(ctx).ReadDB().Where(s.readDB.Products.Status.Eq(1)) if in.CategoryID != nil { q = q.Where(s.readDB.Products.CategoryID.Eq(*in.CategoryID)) } if in.PriceMin != nil { q = q.Where(s.readDB.Products.Price.Gte(*in.PriceMin)) } if in.PriceMax != nil { q = q.Where(s.readDB.Products.Price.Lte(*in.PriceMax)) } if in.SalesMin != nil { q = q.Where(s.readDB.Products.Sales.Gte(*in.SalesMin)) } if in.InStock != nil && *in.InStock { q = q.Where(s.readDB.Products.Stock.Gt(0)) } total, err = q.Count() if err != nil { return } orderDesc := true if strings.ToLower(in.Order) == "asc" { orderDesc = false } switch strings.ToLower(in.SortBy) { case "price": if orderDesc { q = q.Order(s.readDB.Products.Price.Desc()) } else { q = q.Order(s.readDB.Products.Price.Asc()) } case "createdat", "created_at": if orderDesc { q = q.Order(s.readDB.Products.CreatedAt.Desc()) } else { q = q.Order(s.readDB.Products.CreatedAt.Asc()) } default: if orderDesc { q = q.Order(s.readDB.Products.Sales.Desc()) } else { q = q.Order(s.readDB.Products.Sales.Asc()) } } rows, err := q.Limit(in.PageSize).Offset((in.Page - 1) * in.PageSize).Find() if err != nil { return } items = make([]AppListItem, len(rows)) for i, it := range rows { items[i] = AppListItem{ID: it.ID, Name: it.Name, MainImage: firstImage(it.ImagesJSON), Price: it.Price, Sales: it.Sales, InStock: it.Stock > 0} } s.listCache[key] = cachedList{items: items, total: total, expireAt: time.Now().Add(30 * time.Second)} return } func (s *service) GetDetailForApp(ctx context.Context, id int64) (*AppDetail, error) { if v, ok := s.detailCache[id]; ok && time.Now().Before(v.expireAt) { return v.detail, nil } p, err := s.readDB.Products.WithContext(ctx).Where(s.readDB.Products.ID.Eq(id)).Take() if err != nil { return nil, err } if p.Status != 1 { return nil, errors.New("PRODUCT_OFFSHELF") } if p.Stock <= 0 { return nil, errors.New("PRODUCT_OUT_OF_STOCK") } album := splitImages(p.ImagesJSON) d := &AppDetail{ID: p.ID, Name: p.Name, Album: album, Price: p.Price, Sales: p.Sales, Stock: p.Stock, Description: p.Description, Service: []string{}, Recommendations: []AppListItem{}} recQ := s.readDB.Products.WithContext(ctx).ReadDB().Where(s.readDB.Products.Status.Eq(1)).Where(s.readDB.Products.ID.Neq(p.ID)) if p.CategoryID > 0 { recQ = recQ.Where(s.readDB.Products.CategoryID.Eq(p.CategoryID)) } recs, _ := recQ.Order(s.readDB.Products.Sales.Desc()).Limit(6).Find() for _, it := range recs { d.Recommendations = append(d.Recommendations, AppListItem{ID: it.ID, Name: it.Name, MainImage: firstImage(it.ImagesJSON), Price: it.Price, Sales: it.Sales, InStock: it.Stock > 0}) } s.detailCache[id] = cachedDetail{detail: d, expireAt: time.Now().Add(60 * time.Second)} return d, nil } func (s *service) BatchUpdate(ctx context.Context, ids []int64, stock *int64, status *int32) (int64, error) { if len(ids) == 0 { return 0, nil } set := map[string]any{} if stock != nil { set["stock"] = *stock } if status != nil { set["status"] = *status } if len(set) == 0 { return 0, nil } updater := s.writeDB.Products.WithContext(ctx).Where(s.writeDB.Products.ID.In(ids...)) result, err := updater.Updates(set) if err != nil { return 0, err } return result.RowsAffected, nil } func (s *service) ListProductsByIDs(ctx context.Context, ids []int64) ([]*model.Products, error) { if len(ids) == 0 { return nil, nil } return s.readDB.Products.WithContext(ctx).Where(s.readDB.Products.ID.In(ids...)).Find() } func normalizeJSON(s string) string { if strings.TrimSpace(s) == "" { return "[]" } var v any if json.Unmarshal([]byte(s), &v) != nil { return "[]" } return s } func (s *service) listKey(in AppListInput) string { b, _ := json.Marshal(in) return string(b) } func splitImages(s string) []string { var arr []string if strings.TrimSpace(s) == "" { return arr } var v []string if json.Unmarshal([]byte(s), &v) != nil { return arr } return v } func firstImage(s string) string { imgs := splitImages(s) if len(imgs) > 0 { return imgs[0] } return "" } type cachedList struct { items []AppListItem total int64 expireAt time.Time } type cachedDetail struct { detail *AppDetail expireAt time.Time }