- 将订单的PaidAt和CancelledAt从指针类型改为值类型 - 统一时间字段的判空逻辑,使用IsZero()替代nil检查 - 调整多个数据库模型结构,添加新字段并优化字段顺序 - 为活动奖励设置、用户邀请等表添加新字段 - 更新对应的DAO层代码以匹配模型变更
409 lines
11 KiB
Go
409 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)
|
|
}
|
|
|
|
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 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
|
|
}
|