417 lines
11 KiB
Go

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
}