refactor(cart): complete vertical migration
This commit is contained in:
1 parent
10852e3b39
commit
80804dbb1f
30 files changed
+512
-379
No files matched your search
@@ -9,12 +9,22 @@ import (
|
||||
func TestCartHTTPLivesInTransport(t *testing.T) {
|
||||
repositoryRoot := findRepositoryRoot(t)
|
||||
moduleRoot := filepath.Join(repositoryRoot, "internal", "modules", "cart")
|
||||
transportRoot := filepath.Join(repositoryRoot, "internal", "transport", "http", "cart")
|
||||
applicationRoot := filepath.Join(moduleRoot, "application")
|
||||
contractRoot := filepath.Join(moduleRoot, "contract")
|
||||
domainRoot := filepath.Join(moduleRoot, "domain")
|
||||
storeRoot := filepath.Join(moduleRoot, "infrastructure", "gormstore")
|
||||
transportRoot := filepath.Join(moduleRoot, "transport", "http")
|
||||
presenterRoot := filepath.Join(moduleRoot, "transport", "presenter")
|
||||
|
||||
assertFileDeclaresTypes(t, filepath.Join(moduleRoot, "service.go"), []string{
|
||||
"Service", "Repository", "ProductReader", "SKUReader", "CurrencyReader", "ItemDetail", "UpsertItemInput",
|
||||
assertFileDeclaresTypes(t, filepath.Join(applicationRoot, "service.go"), []string{"Service"})
|
||||
assertFileDeclaresTypes(t, filepath.Join(applicationRoot, "types.go"), []string{"ItemDetail", "UpsertItemInput"})
|
||||
assertFileDeclaresFunctions(t, filepath.Join(applicationRoot, "service.go"), []string{"NewService"})
|
||||
assertFileDeclaresTypes(t, filepath.Join(contractRoot, "ports.go"), []string{
|
||||
"StoredItem", "Repository", "ProductReader", "SKUReader", "CurrencyReader",
|
||||
})
|
||||
assertFileDeclaresFunctions(t, filepath.Join(moduleRoot, "service.go"), []string{"NewService"})
|
||||
assertFileDeclaresTypes(t, filepath.Join(domainRoot, "item.go"), []string{"Item"})
|
||||
assertFileDeclaresTypes(t, filepath.Join(storeRoot, "store.go"), []string{"Store"})
|
||||
assertFileDeclaresFunctions(t, filepath.Join(storeRoot, "store.go"), []string{"New", "WithTx"})
|
||||
assertFileDeclaresFunctions(t, filepath.Join(transportRoot, "routes.go"), []string{"RegisterUserRoutes"})
|
||||
assertFileDeclaresTypes(t, filepath.Join(transportRoot, "user_handler.go"), []string{
|
||||
"UserHandler", "Service",
|
||||
@@ -22,8 +32,20 @@ func TestCartHTTPLivesInTransport(t *testing.T) {
|
||||
assertFileDeclaresFunctions(t, filepath.Join(transportRoot, "user_handler.go"), []string{
|
||||
"NewUserHandler", "GetCart", "UpsertCartItem", "DeleteCartItem",
|
||||
})
|
||||
assertDirectoryGoFileBudget(t, moduleRoot, 1)
|
||||
production, total := countDirectGoFiles(t, moduleRoot)
|
||||
if production != 0 || total != 0 {
|
||||
t.Fatalf("cart module root must remain structural only, got production=%d total=%d", production, total)
|
||||
}
|
||||
assertDirectoryGoFileBudget(t, applicationRoot, 3)
|
||||
assertDirectoryGoFileBudget(t, contractRoot, 3)
|
||||
assertDirectoryGoFileBudget(t, domainRoot, 2)
|
||||
assertDirectoryGoFileBudget(t, storeRoot, 3)
|
||||
assertDirectoryGoFileBudget(t, transportRoot, 4)
|
||||
assertDirectoryGoFileBudget(t, presenterRoot, 2)
|
||||
assertProductionImportsAbsent(t, applicationRoot, moduleImportPath+"/internal/models")
|
||||
assertProductionImportsAbsent(t, contractRoot, moduleImportPath+"/internal/models")
|
||||
assertProductionImportsAbsent(t, domainRoot, moduleImportPath+"/internal/models")
|
||||
assertProductionImportsAbsent(t, transportRoot, moduleImportPath+"/internal/models")
|
||||
|
||||
legacy := filepath.Join(repositoryRoot, "internal", "http", "handlers", "public", "cart.go")
|
||||
if _, err := os.Stat(legacy); err == nil {
|
||||
@@ -34,6 +56,7 @@ func TestCartHTTPLivesInTransport(t *testing.T) {
|
||||
for _, relativePath := range []string{
|
||||
"internal/service/cart_service.go",
|
||||
"internal/wiring/cart/wiring.go",
|
||||
"internal/wiring/cart/factory.go",
|
||||
} {
|
||||
path := filepath.Join(repositoryRoot, filepath.FromSlash(relativePath))
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
|
||||
@@ -305,6 +305,13 @@ var completedMigrationPaths = []string{
|
||||
"internal/transport/http/dashboard",
|
||||
"internal/http/handlers/shared/reporting_query.go",
|
||||
"internal/http/handlers/shared/reporting_query_test.go",
|
||||
"internal/models/cart_item.go",
|
||||
"internal/repository/cart_repository.go",
|
||||
"internal/repository/cart_repository_test.go",
|
||||
"internal/modules/cart/service.go",
|
||||
"internal/dto/cart.go",
|
||||
"internal/transport/http/cart",
|
||||
"internal/wiring/cart",
|
||||
}
|
||||
|
||||
func TestCompletedMigrationPathsStayDeleted(t *testing.T) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
categorycontract "github.com/dujiao-next/internal/modules/catalog/category/contract"
|
||||
|
||||
cartgormstore "github.com/dujiao-next/internal/modules/cart/infrastructure/gormstore"
|
||||
mappingcontract "github.com/dujiao-next/internal/modules/catalog/mapping/contract"
|
||||
productapplication "github.com/dujiao-next/internal/modules/catalog/product/application"
|
||||
productadmin "github.com/dujiao-next/internal/modules/catalog/product/application/admin"
|
||||
@@ -47,7 +48,7 @@ type Dependencies struct {
|
||||
CardSecretBatches repository.CardSecretBatchRepository
|
||||
Categories categorycontract.Repository
|
||||
MemberLevelPrices memberLevelPriceCleaner
|
||||
Carts repository.CartRepository
|
||||
Carts *cartgormstore.Store
|
||||
ProductMappings MappingStore
|
||||
Orders repository.OrderRepository
|
||||
PaymentChannels repository.PaymentChannelRepository
|
||||
@@ -110,7 +111,7 @@ type productAdminUnitOfWork struct {
|
||||
cardSecrets repository.CardSecretRepository
|
||||
cardSecretBatches repository.CardSecretBatchRepository
|
||||
memberLevelPrices memberLevelPriceCleaner
|
||||
carts repository.CartRepository
|
||||
carts *cartgormstore.Store
|
||||
productMappings MappingStore
|
||||
}
|
||||
|
||||
@@ -120,7 +121,7 @@ func newProductAdminUnitOfWork(
|
||||
cardSecrets repository.CardSecretRepository,
|
||||
cardSecretBatches repository.CardSecretBatchRepository,
|
||||
memberLevelPrices memberLevelPriceCleaner,
|
||||
carts repository.CartRepository,
|
||||
carts *cartgormstore.Store,
|
||||
productMappings MappingStore,
|
||||
) productadmin.UnitOfWork {
|
||||
return &productAdminUnitOfWork{
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
productdomain "github.com/dujiao-next/internal/modules/catalog/product/domain"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// CartItem 购物车项
|
||||
type CartItem struct {
|
||||
ID uint `gorm:"primarykey" json:"id"` // 主键
|
||||
UserID uint `gorm:"not null;uniqueIndex:idx_cart_user_product_sku" json:"user_id"` // 用户ID
|
||||
ProductID uint `gorm:"not null;uniqueIndex:idx_cart_user_product_sku" json:"product_id"` // 商品ID
|
||||
SKUID uint `gorm:"column:sku_id;not null;default:0;uniqueIndex:idx_cart_user_product_sku" json:"sku_id"` // SKU ID
|
||||
Quantity int `gorm:"not null" json:"quantity"` // 数量
|
||||
FulfillmentType string `gorm:"type:varchar(20);not null;default:'manual'" json:"fulfillment_type"` // 交付类型
|
||||
CreatedAt time.Time `gorm:"index" json:"created_at"` // 创建时间
|
||||
UpdatedAt time.Time `gorm:"index" json:"updated_at"` // 更新时间
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` // 软删除时间
|
||||
|
||||
Product *productdomain.Product `gorm:"foreignKey:ProductID" json:"product,omitempty"` // 关联商品
|
||||
SKU *productdomain.ProductSKU `gorm:"foreignKey:SKUID" json:"sku,omitempty"` // 关联SKU
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (CartItem) TableName() string {
|
||||
return "cart_items"
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
affiliatedomain "github.com/dujiao-next/internal/modules/affiliate/domain"
|
||||
apicredentialdomain "github.com/dujiao-next/internal/modules/apicredential/domain"
|
||||
auditlogdomain "github.com/dujiao-next/internal/modules/auditlog/domain"
|
||||
cartdomain "github.com/dujiao-next/internal/modules/cart/domain"
|
||||
categorydomain "github.com/dujiao-next/internal/modules/catalog/category/domain"
|
||||
mappingdomain "github.com/dujiao-next/internal/modules/catalog/mapping/domain"
|
||||
productdomain "github.com/dujiao-next/internal/modules/catalog/product/domain"
|
||||
@@ -170,7 +171,7 @@ func AutoMigrate() error {
|
||||
&Order{},
|
||||
&OrderItem{},
|
||||
&OrderRefundRecord{},
|
||||
&CartItem{},
|
||||
&cartdomain.Item{},
|
||||
&PaymentChannel{},
|
||||
&Payment{},
|
||||
&CardSecret{},
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
cartdomain "github.com/dujiao-next/internal/modules/cart/domain"
|
||||
|
||||
productdomain "github.com/dujiao-next/internal/modules/catalog/product/domain"
|
||||
settingsstore "github.com/dujiao-next/internal/modules/settings/infrastructure/gormstore"
|
||||
"github.com/dujiao-next/internal/shared/jsonmap"
|
||||
@@ -34,7 +36,7 @@ func TestEnsureProductSKUMigrationBackfillLegacyData(t *testing.T) {
|
||||
&productdomain.Product{},
|
||||
&productdomain.ProductSKU{},
|
||||
&OrderItem{},
|
||||
&CartItem{},
|
||||
&cartdomain.Item{},
|
||||
&CardSecret{},
|
||||
&CardSecretBatch{},
|
||||
&settingsstore.SettingRecord{},
|
||||
@@ -75,7 +77,7 @@ func TestEnsureProductSKUMigrationBackfillLegacyData(t *testing.T) {
|
||||
t.Fatalf("create order item failed: %v", err)
|
||||
}
|
||||
|
||||
cartItem := &CartItem{
|
||||
cartItem := &cartdomain.Item{
|
||||
UserID: 1001,
|
||||
ProductID: product.ID,
|
||||
SKUID: 0,
|
||||
@@ -137,7 +139,7 @@ func TestEnsureProductSKUMigrationBackfillLegacyData(t *testing.T) {
|
||||
t.Fatalf("order item sku_id want %d got %d", sku.ID, gotOrderItem.SKUID)
|
||||
}
|
||||
|
||||
var gotCartItem CartItem
|
||||
var gotCartItem cartdomain.Item
|
||||
if err := db.First(&gotCartItem, cartItem.ID).Error; err != nil {
|
||||
t.Fatalf("reload cart item failed: %v", err)
|
||||
}
|
||||
@@ -176,7 +178,7 @@ func TestEnsureProductSKUMigrationBackfillLegacyData(t *testing.T) {
|
||||
|
||||
func TestMigrateCartSKUUniqueIndex(t *testing.T) {
|
||||
db := setupSKUMigrationTestDB(t)
|
||||
if err := db.AutoMigrate(&CartItem{}); err != nil {
|
||||
if err := db.AutoMigrate(&cartdomain.Item{}); err != nil {
|
||||
t.Fatalf("auto migrate cart item failed: %v", err)
|
||||
}
|
||||
|
||||
@@ -189,10 +191,10 @@ func TestMigrateCartSKUUniqueIndex(t *testing.T) {
|
||||
t.Fatalf("migrate cart unique index failed: %v", err)
|
||||
}
|
||||
|
||||
if db.Migrator().HasIndex(&CartItem{}, "idx_cart_user_product") {
|
||||
if db.Migrator().HasIndex(&cartdomain.Item{}, "idx_cart_user_product") {
|
||||
t.Fatalf("legacy unique index idx_cart_user_product should be dropped")
|
||||
}
|
||||
if !db.Migrator().HasIndex(&CartItem{}, "idx_cart_user_product_sku") {
|
||||
if !db.Migrator().HasIndex(&cartdomain.Item{}, "idx_cart_user_product_sku") {
|
||||
t.Fatalf("new unique index idx_cart_user_product_sku should exist")
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cartdomain "github.com/dujiao-next/internal/modules/cart/domain"
|
||||
|
||||
categorydomain "github.com/dujiao-next/internal/modules/catalog/category/domain"
|
||||
productdomain "github.com/dujiao-next/internal/modules/catalog/product/domain"
|
||||
settingsstore "github.com/dujiao-next/internal/modules/settings/infrastructure/gormstore"
|
||||
@@ -114,14 +116,14 @@ func migrateCartSKUUniqueIndex() error {
|
||||
migrator := DB.Migrator()
|
||||
|
||||
// 历史唯一索引会阻止同一商品不同 SKU 共存,迁移时必须移除。
|
||||
if migrator.HasIndex(&CartItem{}, "idx_cart_user_product") {
|
||||
if err := migrator.DropIndex(&CartItem{}, "idx_cart_user_product"); err != nil {
|
||||
if migrator.HasIndex(&cartdomain.Item{}, "idx_cart_user_product") {
|
||||
if err := migrator.DropIndex(&cartdomain.Item{}, "idx_cart_user_product"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !migrator.HasIndex(&CartItem{}, "idx_cart_user_product_sku") {
|
||||
if err := migrator.CreateIndex(&CartItem{}, "idx_cart_user_product_sku"); err != nil {
|
||||
if !migrator.HasIndex(&cartdomain.Item{}, "idx_cart_user_product_sku") {
|
||||
if err := migrator.CreateIndex(&cartdomain.Item{}, "idx_cart_user_product_sku"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -276,7 +278,7 @@ func backfillLegacySKUID(productToSKU map[uint]uint) error {
|
||||
Update("sku_id", skuID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Unscoped().Model(&CartItem{}).
|
||||
if err := tx.Model(&cartdomain.Item{}).
|
||||
Where("product_id = ? AND sku_id = 0", productID).
|
||||
Update("sku_id", skuID).Error; err != nil {
|
||||
return err
|
||||
@@ -316,7 +318,7 @@ func validateSKUMigrationIntegrity() error {
|
||||
name: "cart_items",
|
||||
query: func() (int64, error) {
|
||||
var count int64
|
||||
err := DB.Model(&CartItem{}).Where("sku_id = 0").Count(&count).Error
|
||||
err := DB.Model(&cartdomain.Item{}).Where("sku_id = 0").Count(&count).Error
|
||||
return count, err
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,84 +1,28 @@
|
||||
package cart
|
||||
package application
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dujiao-next/internal/constants"
|
||||
"github.com/dujiao-next/internal/models"
|
||||
"github.com/dujiao-next/internal/modules/cart/contract"
|
||||
"github.com/dujiao-next/internal/modules/cart/domain"
|
||||
productdomain "github.com/dujiao-next/internal/modules/catalog/product/domain"
|
||||
promotionapp "github.com/dujiao-next/internal/modules/promotion/application"
|
||||
promotioncontract "github.com/dujiao-next/internal/modules/promotion/contract"
|
||||
"github.com/dujiao-next/internal/shared/money"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidItem = errors.New("invalid cart item")
|
||||
ErrProductUnavailable = errors.New("product not available")
|
||||
ErrFulfillmentInvalid = errors.New("fulfillment invalid")
|
||||
ErrSKURequired = errors.New("product sku required")
|
||||
ErrSKUInvalid = errors.New("product sku invalid")
|
||||
ErrManualStockInsufficient = errors.New("manual stock insufficient")
|
||||
)
|
||||
|
||||
// ItemDetail 购物车项详情(用于响应)。
|
||||
type ItemDetail struct {
|
||||
ProductID uint `json:"product_id"`
|
||||
SKUID uint `json:"sku_id"`
|
||||
Quantity int `json:"quantity"`
|
||||
FulfillmentType string `json:"fulfillment_type"`
|
||||
UnitPrice money.Amount `json:"unit_price"`
|
||||
OriginalPrice money.Amount `json:"original_price"`
|
||||
Currency string `json:"currency"`
|
||||
Product *productdomain.Product `json:"product"`
|
||||
SKU *productdomain.ProductSKU `json:"sku"`
|
||||
}
|
||||
|
||||
// UpsertItemInput 购物车更新输入。
|
||||
type UpsertItemInput struct {
|
||||
UserID uint
|
||||
ProductID uint
|
||||
SKUID uint
|
||||
Quantity int
|
||||
FulfillmentType string
|
||||
}
|
||||
|
||||
// Repository 是购物车服务所需的最小持久化端口。
|
||||
type Repository interface {
|
||||
ListByUser(userID uint) ([]models.CartItem, error)
|
||||
Upsert(item *models.CartItem) error
|
||||
DeleteByUserProductSKU(userID, productID, skuID uint) error
|
||||
}
|
||||
|
||||
// ProductReader 是购物车服务所需的商品读取端口。
|
||||
type ProductReader interface {
|
||||
GetByID(id string) (*productdomain.Product, error)
|
||||
}
|
||||
|
||||
// SKUReader 是购物车服务所需的 SKU 读取端口。
|
||||
type SKUReader interface {
|
||||
GetByID(id uint) (*productdomain.ProductSKU, error)
|
||||
ListByProduct(productID uint, onlyActive bool) ([]productdomain.ProductSKU, error)
|
||||
}
|
||||
|
||||
// CurrencyReader 是购物车服务所需的站点币种端口。
|
||||
type CurrencyReader interface {
|
||||
GetSiteCurrency(defaultCurrency string) (string, error)
|
||||
}
|
||||
|
||||
// Service 购物车服务。
|
||||
type Service struct {
|
||||
cartRepo Repository
|
||||
productRepo ProductReader
|
||||
productSKURepo SKUReader
|
||||
promotionRepo promotioncontract.Repository
|
||||
currencyReader CurrencyReader
|
||||
cartRepo contract.Repository
|
||||
productRepo contract.ProductReader
|
||||
productSKURepo contract.SKUReader
|
||||
promotionRepo contract.PromotionRepository
|
||||
currencyReader contract.CurrencyReader
|
||||
}
|
||||
|
||||
// NewService 创建购物车服务。
|
||||
func NewService(cartRepo Repository, productRepo ProductReader, productSKURepo SKUReader, promotionRepo promotioncontract.Repository, currencyReader CurrencyReader) *Service {
|
||||
func NewService(cartRepo contract.Repository, productRepo contract.ProductReader, productSKURepo contract.SKUReader, promotionRepo contract.PromotionRepository, currencyReader contract.CurrencyReader) *Service {
|
||||
return &Service{
|
||||
cartRepo: cartRepo,
|
||||
productRepo: productRepo,
|
||||
@@ -91,7 +35,7 @@ func NewService(cartRepo Repository, productRepo ProductReader, productSKURepo S
|
||||
// ListByUser 获取用户购物车
|
||||
func (s *Service) ListByUser(userID uint) ([]ItemDetail, error) {
|
||||
if userID == 0 {
|
||||
return nil, ErrInvalidItem
|
||||
return nil, contract.ErrInvalidItem
|
||||
}
|
||||
items, err := s.cartRepo.ListByUser(userID)
|
||||
if err != nil {
|
||||
@@ -169,14 +113,14 @@ func (s *Service) ListByUser(userID uint) ([]ItemDetail, error) {
|
||||
// UpsertItem 添加或更新购物车项
|
||||
func (s *Service) UpsertItem(input UpsertItemInput) error {
|
||||
if input.UserID == 0 || input.ProductID == 0 || input.Quantity <= 0 {
|
||||
return ErrInvalidItem
|
||||
return contract.ErrInvalidItem
|
||||
}
|
||||
product, err := s.productRepo.GetByID(strconv.FormatUint(uint64(input.ProductID), 10))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if product == nil || !product.IsActive {
|
||||
return ErrProductUnavailable
|
||||
return contract.ErrProductUnavailable
|
||||
}
|
||||
if err := productdomain.ValidatePurchaseQuantity(product, input.Quantity); err != nil {
|
||||
return err
|
||||
@@ -191,16 +135,16 @@ func (s *Service) UpsertItem(input UpsertItemInput) error {
|
||||
fulfillmentType = constants.FulfillmentTypeManual
|
||||
}
|
||||
if fulfillmentType != constants.FulfillmentTypeManual && fulfillmentType != constants.FulfillmentTypeAuto {
|
||||
return ErrFulfillmentInvalid
|
||||
return contract.ErrFulfillmentInvalid
|
||||
}
|
||||
if fulfillmentType == constants.FulfillmentTypeManual &&
|
||||
productdomain.ShouldEnforceManualSKUStock(product, sku) &&
|
||||
productdomain.ManualSKUAvailable(sku) < input.Quantity {
|
||||
return ErrManualStockInsufficient
|
||||
return contract.ErrManualStockInsufficient
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
item := &models.CartItem{
|
||||
item := &domain.Item{
|
||||
UserID: input.UserID,
|
||||
ProductID: input.ProductID,
|
||||
SKUID: sku.ID,
|
||||
@@ -215,7 +159,7 @@ func (s *Service) UpsertItem(input UpsertItemInput) error {
|
||||
// RemoveItem 删除购物车项
|
||||
func (s *Service) RemoveItem(userID, productID, skuID uint) error {
|
||||
if userID == 0 || productID == 0 {
|
||||
return ErrInvalidItem
|
||||
return contract.ErrInvalidItem
|
||||
}
|
||||
return s.cartRepo.DeleteByUserProductSKU(userID, productID, skuID)
|
||||
}
|
||||
@@ -231,12 +175,12 @@ func (s *Service) siteCurrency() string {
|
||||
return currency
|
||||
}
|
||||
|
||||
func resolveProductSKU(repo SKUReader, product *productdomain.Product, rawSKUID uint) (*productdomain.ProductSKU, error) {
|
||||
func resolveProductSKU(repo contract.SKUReader, product *productdomain.Product, rawSKUID uint) (*productdomain.ProductSKU, error) {
|
||||
if product == nil || product.ID == 0 {
|
||||
return nil, ErrProductUnavailable
|
||||
return nil, contract.ErrProductUnavailable
|
||||
}
|
||||
if repo == nil {
|
||||
return nil, ErrSKUInvalid
|
||||
return nil, contract.ErrSKUInvalid
|
||||
}
|
||||
if rawSKUID > 0 {
|
||||
sku, err := repo.GetByID(rawSKUID)
|
||||
@@ -244,7 +188,7 @@ func resolveProductSKU(repo SKUReader, product *productdomain.Product, rawSKUID
|
||||
return nil, err
|
||||
}
|
||||
if sku == nil || sku.ProductID != product.ID || !sku.IsActive {
|
||||
return nil, ErrSKUInvalid
|
||||
return nil, contract.ErrSKUInvalid
|
||||
}
|
||||
return sku, nil
|
||||
}
|
||||
@@ -256,7 +200,7 @@ func resolveProductSKU(repo SKUReader, product *productdomain.Product, rawSKUID
|
||||
return &activeSKUs[0], nil
|
||||
}
|
||||
if len(activeSKUs) == 0 {
|
||||
return nil, ErrSKUInvalid
|
||||
return nil, contract.ErrSKUInvalid
|
||||
}
|
||||
return nil, ErrSKURequired
|
||||
return nil, contract.ErrSKURequired
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
productdomain "github.com/dujiao-next/internal/modules/catalog/product/domain"
|
||||
"github.com/dujiao-next/internal/shared/money"
|
||||
)
|
||||
|
||||
// ItemDetail 购物车项详情。
|
||||
type ItemDetail struct {
|
||||
ProductID uint
|
||||
SKUID uint
|
||||
Quantity int
|
||||
FulfillmentType string
|
||||
UnitPrice money.Amount
|
||||
OriginalPrice money.Amount
|
||||
Currency string
|
||||
Product *productdomain.Product
|
||||
SKU *productdomain.ProductSKU
|
||||
}
|
||||
|
||||
// UpsertItemInput 购物车更新输入。
|
||||
type UpsertItemInput struct {
|
||||
UserID uint
|
||||
ProductID uint
|
||||
SKUID uint
|
||||
Quantity int
|
||||
FulfillmentType string
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package contract
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrInvalidItem = errors.New("invalid cart item")
|
||||
ErrProductUnavailable = errors.New("product not available")
|
||||
ErrFulfillmentInvalid = errors.New("fulfillment invalid")
|
||||
ErrSKURequired = errors.New("product sku required")
|
||||
ErrSKUInvalid = errors.New("product sku invalid")
|
||||
ErrManualStockInsufficient = errors.New("manual stock insufficient")
|
||||
)
|
||||
@@ -0,0 +1,40 @@
|
||||
package contract
|
||||
|
||||
import (
|
||||
"github.com/dujiao-next/internal/modules/cart/domain"
|
||||
productdomain "github.com/dujiao-next/internal/modules/catalog/product/domain"
|
||||
promotioncontract "github.com/dujiao-next/internal/modules/promotion/contract"
|
||||
)
|
||||
|
||||
// StoredItem 是购物车持久化端口返回的条目及其商品快照。
|
||||
type StoredItem struct {
|
||||
domain.Item
|
||||
Product *productdomain.Product
|
||||
SKU *productdomain.ProductSKU
|
||||
}
|
||||
|
||||
// Repository 是购物车应用层所需的最小持久化端口。
|
||||
type Repository interface {
|
||||
ListByUser(userID uint) ([]StoredItem, error)
|
||||
Upsert(item *domain.Item) error
|
||||
DeleteByUserProductSKU(userID, productID, skuID uint) error
|
||||
}
|
||||
|
||||
// ProductReader 是购物车服务所需的商品读取端口。
|
||||
type ProductReader interface {
|
||||
GetByID(id string) (*productdomain.Product, error)
|
||||
}
|
||||
|
||||
// SKUReader 是购物车服务所需的 SKU 读取端口。
|
||||
type SKUReader interface {
|
||||
GetByID(id uint) (*productdomain.ProductSKU, error)
|
||||
ListByProduct(productID uint, onlyActive bool) ([]productdomain.ProductSKU, error)
|
||||
}
|
||||
|
||||
// CurrencyReader 是购物车服务所需的站点币种端口。
|
||||
type CurrencyReader interface {
|
||||
GetSiteCurrency(defaultCurrency string) (string, error)
|
||||
}
|
||||
|
||||
// PromotionRepository 是购物车价格计算所需的活动价端口。
|
||||
type PromotionRepository = promotioncontract.Repository
|
||||
@@ -0,0 +1,23 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Item 购物车项。
|
||||
type Item struct {
|
||||
ID uint `gorm:"primarykey" json:"id"` // 主键
|
||||
UserID uint `gorm:"not null;uniqueIndex:idx_cart_user_product_sku" json:"user_id"` // 用户ID
|
||||
ProductID uint `gorm:"not null;uniqueIndex:idx_cart_user_product_sku" json:"product_id"` // 商品ID
|
||||
SKUID uint `gorm:"column:sku_id;not null;default:0;uniqueIndex:idx_cart_user_product_sku" json:"sku_id"` // SKU ID
|
||||
Quantity int `gorm:"not null" json:"quantity"` // 数量
|
||||
FulfillmentType string `gorm:"type:varchar(20);not null;default:'manual'" json:"fulfillment_type"` // 交付类型
|
||||
CreatedAt time.Time `gorm:"index" json:"created_at"` // 创建时间
|
||||
UpdatedAt time.Time `gorm:"index" json:"updated_at"` // 更新时间
|
||||
DeletedAt *time.Time `gorm:"index" json:"-"` // 软删除时间
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (Item) TableName() string {
|
||||
return "cart_items"
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package gormstore
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/dujiao-next/internal/modules/cart/contract"
|
||||
"github.com/dujiao-next/internal/modules/cart/domain"
|
||||
productdomain "github.com/dujiao-next/internal/modules/catalog/product/domain"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Store 是 Cart 的 GORM 持久化适配器。
|
||||
type Store struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
var _ contract.Repository = (*Store)(nil)
|
||||
|
||||
type itemRecord struct {
|
||||
domain.Item `gorm:"embedded"`
|
||||
Product *productdomain.Product `gorm:"foreignKey:ProductID"`
|
||||
SKU *productdomain.ProductSKU `gorm:"foreignKey:SKUID"`
|
||||
}
|
||||
|
||||
func (itemRecord) TableName() string { return "cart_items" }
|
||||
|
||||
// New 创建购物车持久化适配器。
|
||||
func New(db *gorm.DB) *Store {
|
||||
return &Store{db: db}
|
||||
}
|
||||
|
||||
// WithTx 绑定事务
|
||||
func (r *Store) WithTx(tx *gorm.DB) *Store {
|
||||
if tx == nil {
|
||||
return r
|
||||
}
|
||||
return &Store{db: tx}
|
||||
}
|
||||
|
||||
// ListByUser 获取用户购物车项
|
||||
func (r *Store) ListByUser(userID uint) ([]contract.StoredItem, error) {
|
||||
var records []itemRecord
|
||||
if err := r.db.
|
||||
Preload("Product", "deleted_at IS NULL").
|
||||
Preload("SKU", "deleted_at IS NULL").
|
||||
Where("cart_items.user_id = ? AND cart_items.deleted_at IS NULL", userID).
|
||||
Order("updated_at desc").
|
||||
Find(&records).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]contract.StoredItem, 0, len(records))
|
||||
for index := range records {
|
||||
record := &records[index]
|
||||
items = append(items, contract.StoredItem{
|
||||
Item: record.Item,
|
||||
Product: record.Product,
|
||||
SKU: record.SKU,
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// Upsert 添加或更新购物车项
|
||||
func (r *Store) Upsert(item *domain.Item) error {
|
||||
if item == nil {
|
||||
return nil
|
||||
}
|
||||
var existing domain.Item
|
||||
err := r.db.Where("user_id = ? AND product_id = ? AND sku_id = ? AND deleted_at IS NULL", item.UserID, item.ProductID, item.SKUID).First(&existing).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return r.db.Create(item).Error
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updates := map[string]interface{}{
|
||||
"sku_id": item.SKUID,
|
||||
"quantity": item.Quantity,
|
||||
"fulfillment_type": item.FulfillmentType,
|
||||
"updated_at": item.UpdatedAt,
|
||||
}
|
||||
return r.db.Model(&existing).Updates(updates).Error
|
||||
}
|
||||
|
||||
// DeleteByProduct 删除指定商品的所有购物车项
|
||||
func (r *Store) DeleteByProduct(productID uint) error {
|
||||
return r.softDelete(r.db.Where("product_id = ? AND deleted_at IS NULL", productID))
|
||||
}
|
||||
|
||||
// DeleteByUserAndProduct 删除购物车项
|
||||
func (r *Store) DeleteByUserAndProduct(userID, productID uint) error {
|
||||
return r.softDelete(r.db.Where("user_id = ? AND product_id = ? AND deleted_at IS NULL", userID, productID))
|
||||
}
|
||||
|
||||
// DeleteByUserProductSKU 按用户+商品+SKU删除购物车项
|
||||
func (r *Store) DeleteByUserProductSKU(userID, productID, skuID uint) error {
|
||||
if skuID == 0 {
|
||||
return r.DeleteByUserAndProduct(userID, productID)
|
||||
}
|
||||
return r.softDelete(r.db.Where("user_id = ? AND product_id = ? AND sku_id = ? AND deleted_at IS NULL", userID, productID, skuID))
|
||||
}
|
||||
|
||||
// ClearByUser 清空购物车
|
||||
func (r *Store) ClearByUser(userID uint) error {
|
||||
return r.softDelete(r.db.Where("user_id = ? AND deleted_at IS NULL", userID))
|
||||
}
|
||||
|
||||
func (r *Store) softDelete(query *gorm.DB) error {
|
||||
return query.Model(&domain.Item{}).Update("deleted_at", r.db.NowFunc()).Error
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package gormstore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dujiao-next/internal/modules/cart/domain"
|
||||
productdomain "github.com/dujiao-next/internal/modules/catalog/product/domain"
|
||||
"github.com/dujiao-next/internal/shared/jsonmap"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func setupCartRepositoryTest(t *testing.T) (*Store, *gorm.DB) {
|
||||
t.Helper()
|
||||
dsn := fmt.Sprintf("file:cart_repository_test_%d?mode=memory&cache=shared", time.Now().UnixNano())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite failed: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&productdomain.Product{}, &productdomain.ProductSKU{}, &domain.Item{}); err != nil {
|
||||
t.Fatalf("migrate cart item failed: %v", err)
|
||||
}
|
||||
return New(db), db
|
||||
}
|
||||
|
||||
func TestStoreListByUserLoadsProductAndSKUAndHidesDeletedItems(t *testing.T) {
|
||||
repo, db := setupCartRepositoryTest(t)
|
||||
now := time.Now()
|
||||
product := productdomain.Product{
|
||||
ID: 888,
|
||||
CategoryID: 1,
|
||||
Slug: "cart-product",
|
||||
TitleJSON: jsonmap.JSON{"zh-CN": "购物车商品"},
|
||||
FulfillmentType: "manual",
|
||||
IsActive: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if err := db.Create(&product).Error; err != nil {
|
||||
t.Fatalf("create product failed: %v", err)
|
||||
}
|
||||
sku := productdomain.ProductSKU{
|
||||
ID: 101,
|
||||
ProductID: product.ID,
|
||||
SKUCode: "CART-SKU",
|
||||
IsActive: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if err := db.Create(&sku).Error; err != nil {
|
||||
t.Fatalf("create sku failed: %v", err)
|
||||
}
|
||||
item := &domain.Item{
|
||||
UserID: 10001,
|
||||
ProductID: product.ID,
|
||||
SKUID: sku.ID,
|
||||
Quantity: 2,
|
||||
FulfillmentType: "manual",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if err := repo.Upsert(item); err != nil {
|
||||
t.Fatalf("upsert item failed: %v", err)
|
||||
}
|
||||
|
||||
items, err := repo.ListByUser(item.UserID)
|
||||
if err != nil {
|
||||
t.Fatalf("list cart items failed: %v", err)
|
||||
}
|
||||
if len(items) != 1 {
|
||||
t.Fatalf("cart items want 1 got %d", len(items))
|
||||
}
|
||||
if items[0].Product == nil || items[0].Product.ID != product.ID {
|
||||
t.Fatalf("product association was not loaded: %#v", items[0].Product)
|
||||
}
|
||||
if items[0].SKU == nil || items[0].SKU.ID != sku.ID {
|
||||
t.Fatalf("sku association was not loaded: %#v", items[0].SKU)
|
||||
}
|
||||
|
||||
if err := repo.DeleteByUserProductSKU(item.UserID, item.ProductID, item.SKUID); err != nil {
|
||||
t.Fatalf("delete item failed: %v", err)
|
||||
}
|
||||
items, err = repo.ListByUser(item.UserID)
|
||||
if err != nil {
|
||||
t.Fatalf("list after delete failed: %v", err)
|
||||
}
|
||||
if len(items) != 0 {
|
||||
t.Fatalf("deleted cart item must be hidden, got %d rows", len(items))
|
||||
}
|
||||
var deleted domain.Item
|
||||
if err := db.Where("id = ?", item.ID).First(&deleted).Error; err != nil {
|
||||
t.Fatalf("load deleted row failed: %v", err)
|
||||
}
|
||||
if deleted.DeletedAt == nil {
|
||||
t.Fatal("deleted_at was not persisted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCartRepositoryUpsertUsesProductAndSKUDimension(t *testing.T) {
|
||||
repo, db := setupCartRepositoryTest(t)
|
||||
now := time.Now()
|
||||
|
||||
first := &domain.Item{
|
||||
UserID: 10001,
|
||||
ProductID: 888,
|
||||
SKUID: 101,
|
||||
Quantity: 1,
|
||||
FulfillmentType: "manual",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
second := &domain.Item{
|
||||
UserID: 10001,
|
||||
ProductID: 888,
|
||||
SKUID: 102,
|
||||
Quantity: 2,
|
||||
FulfillmentType: "manual",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if err := repo.Upsert(first); err != nil {
|
||||
t.Fatalf("upsert first sku failed: %v", err)
|
||||
}
|
||||
if err := repo.Upsert(second); err != nil {
|
||||
t.Fatalf("upsert second sku failed: %v", err)
|
||||
}
|
||||
|
||||
var count int64
|
||||
if err := db.Model(&domain.Item{}).
|
||||
Where("user_id = ? AND product_id = ? AND deleted_at IS NULL", first.UserID, first.ProductID).
|
||||
Count(&count).Error; err != nil {
|
||||
t.Fatalf("count cart items failed: %v", err)
|
||||
}
|
||||
if count != 2 {
|
||||
t.Fatalf("cart rows want 2 got %d", count)
|
||||
}
|
||||
|
||||
first.Quantity = 5
|
||||
first.UpdatedAt = now.Add(time.Minute)
|
||||
if err := repo.Upsert(first); err != nil {
|
||||
t.Fatalf("update first sku failed: %v", err)
|
||||
}
|
||||
|
||||
var gotFirst domain.Item
|
||||
if err := db.Where("user_id = ? AND product_id = ? AND sku_id = ?", first.UserID, first.ProductID, first.SKUID).First(&gotFirst).Error; err != nil {
|
||||
t.Fatalf("query first sku row failed: %v", err)
|
||||
}
|
||||
if gotFirst.Quantity != 5 {
|
||||
t.Fatalf("first sku quantity want 5 got %d", gotFirst.Quantity)
|
||||
}
|
||||
|
||||
var gotSecond domain.Item
|
||||
if err := db.Where("user_id = ? AND product_id = ? AND sku_id = ?", second.UserID, second.ProductID, second.SKUID).First(&gotSecond).Error; err != nil {
|
||||
t.Fatalf("query second sku row failed: %v", err)
|
||||
}
|
||||
if gotSecond.Quantity != 2 {
|
||||
t.Fatalf("second sku quantity should keep 2 got %d", gotSecond.Quantity)
|
||||
}
|
||||
}
|
||||
File renamed without changes.
+19
-18
@@ -4,8 +4,9 @@ import (
|
||||
"errors"
|
||||
|
||||
"github.com/dujiao-next/internal/constants"
|
||||
"github.com/dujiao-next/internal/dto"
|
||||
"github.com/dujiao-next/internal/modules/cart"
|
||||
cartapp "github.com/dujiao-next/internal/modules/cart/application"
|
||||
cartcontract "github.com/dujiao-next/internal/modules/cart/contract"
|
||||
cartpresenter "github.com/dujiao-next/internal/modules/cart/transport/presenter"
|
||||
productdomain "github.com/dujiao-next/internal/modules/catalog/product/domain"
|
||||
promotioncontract "github.com/dujiao-next/internal/modules/promotion/contract"
|
||||
"github.com/dujiao-next/internal/platform/http/ginutil"
|
||||
@@ -16,8 +17,8 @@ import (
|
||||
|
||||
// Service 购物车应用端口。
|
||||
type Service interface {
|
||||
ListByUser(userID uint) ([]cart.ItemDetail, error)
|
||||
UpsertItem(input cart.UpsertItemInput) error
|
||||
ListByUser(userID uint) ([]cartapp.ItemDetail, error)
|
||||
UpsertItem(input cartapp.UpsertItemInput) error
|
||||
RemoveItem(userID, productID, skuID uint) error
|
||||
}
|
||||
|
||||
@@ -51,11 +52,11 @@ func (h *UserHandler) GetCart(c *gin.Context) {
|
||||
items, err := h.carts.ListByUser(uid)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, cart.ErrInvalidItem):
|
||||
case errors.Is(err, cartcontract.ErrInvalidItem):
|
||||
ginutil.RespondError(c, response.CodeBadRequest, "error.order_item_invalid", nil)
|
||||
case errors.Is(err, cart.ErrProductUnavailable):
|
||||
case errors.Is(err, cartcontract.ErrProductUnavailable):
|
||||
ginutil.RespondError(c, response.CodeBadRequest, "error.product_not_available", nil)
|
||||
case errors.Is(err, cart.ErrFulfillmentInvalid):
|
||||
case errors.Is(err, cartcontract.ErrFulfillmentInvalid):
|
||||
ginutil.RespondError(c, response.CodeBadRequest, "error.fulfillment_invalid", nil)
|
||||
case errors.Is(err, promotioncontract.ErrInvalid):
|
||||
ginutil.RespondError(c, response.CodeBadRequest, "error.promotion_invalid", nil)
|
||||
@@ -65,7 +66,7 @@ func (h *UserHandler) GetCart(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
respItems := make([]dto.CartItemResp, 0, len(items))
|
||||
respItems := make([]cartpresenter.CartItemResp, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item.Product == nil {
|
||||
continue
|
||||
@@ -78,7 +79,7 @@ func (h *UserHandler) GetCart(c *gin.Context) {
|
||||
if cartFT == constants.FulfillmentTypeUpstream {
|
||||
cartFT = constants.FulfillmentTypeManual
|
||||
}
|
||||
product := dto.CartProductResp{
|
||||
product := cartpresenter.CartProductResp{
|
||||
Slug: item.Product.Slug,
|
||||
Title: item.Product.TitleJSON,
|
||||
PriceAmount: item.Product.PriceAmount,
|
||||
@@ -90,7 +91,7 @@ func (h *UserHandler) GetCart(c *gin.Context) {
|
||||
FulfillmentType: productFT,
|
||||
IsActive: item.Product.IsActive,
|
||||
}
|
||||
respItems = append(respItems, dto.CartItemResp{
|
||||
respItems = append(respItems, cartpresenter.CartItemResp{
|
||||
ProductID: item.ProductID,
|
||||
SKUID: item.SKUID,
|
||||
Quantity: item.Quantity,
|
||||
@@ -124,7 +125,7 @@ func (h *UserHandler) UpsertCartItem(c *gin.Context) {
|
||||
response.Success(c, gin.H{"updated": true})
|
||||
return
|
||||
}
|
||||
if err := h.carts.UpsertItem(cart.UpsertItemInput{
|
||||
if err := h.carts.UpsertItem(cartapp.UpsertItemInput{
|
||||
UserID: uid,
|
||||
ProductID: req.ProductID,
|
||||
SKUID: req.SKUID,
|
||||
@@ -155,7 +156,7 @@ func (h *UserHandler) DeleteCartItem(c *gin.Context) {
|
||||
}
|
||||
if err := h.carts.RemoveItem(uid, productID, skuID); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, cart.ErrInvalidItem):
|
||||
case errors.Is(err, cartcontract.ErrInvalidItem):
|
||||
ginutil.RespondError(c, response.CodeBadRequest, "error.order_item_invalid", nil)
|
||||
default:
|
||||
ginutil.RespondError(c, response.CodeInternal, "error.order_update_failed", err)
|
||||
@@ -167,19 +168,19 @@ func (h *UserHandler) DeleteCartItem(c *gin.Context) {
|
||||
|
||||
func respondCartItemUpdateError(c *gin.Context, err error) {
|
||||
switch {
|
||||
case errors.Is(err, cart.ErrSKURequired),
|
||||
errors.Is(err, cart.ErrSKUInvalid),
|
||||
errors.Is(err, cart.ErrInvalidItem):
|
||||
case errors.Is(err, cartcontract.ErrSKURequired),
|
||||
errors.Is(err, cartcontract.ErrSKUInvalid),
|
||||
errors.Is(err, cartcontract.ErrInvalidItem):
|
||||
ginutil.RespondError(c, response.CodeBadRequest, "error.order_item_invalid", nil)
|
||||
case errors.Is(err, productdomain.ErrMaxPurchaseExceeded):
|
||||
ginutil.RespondError(c, response.CodeBadRequest, "error.product_max_purchase_exceeded", nil)
|
||||
case errors.Is(err, productdomain.ErrMinPurchaseNotMet):
|
||||
ginutil.RespondError(c, response.CodeBadRequest, "error.product_min_purchase_not_met", nil)
|
||||
case errors.Is(err, cart.ErrProductUnavailable):
|
||||
case errors.Is(err, cartcontract.ErrProductUnavailable):
|
||||
ginutil.RespondError(c, response.CodeBadRequest, "error.product_not_available", nil)
|
||||
case errors.Is(err, cart.ErrManualStockInsufficient):
|
||||
case errors.Is(err, cartcontract.ErrManualStockInsufficient):
|
||||
ginutil.RespondError(c, response.CodeBadRequest, "error.manual_stock_insufficient", nil)
|
||||
case errors.Is(err, cart.ErrFulfillmentInvalid):
|
||||
case errors.Is(err, cartcontract.ErrFulfillmentInvalid):
|
||||
ginutil.RespondError(c, response.CodeBadRequest, "error.fulfillment_invalid", nil)
|
||||
default:
|
||||
ginutil.RespondError(c, response.CodeInternal, "error.order_update_failed", err)
|
||||
+3
-3
@@ -8,7 +8,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/dujiao-next/internal/platform/http/response"
|
||||
"github.com/dujiao-next/internal/modules/cart"
|
||||
cartcontract "github.com/dujiao-next/internal/modules/cart/contract"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -24,13 +24,13 @@ func TestRespondCartItemUpdateError(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "invalid fulfillment",
|
||||
err: cart.ErrFulfillmentInvalid,
|
||||
err: cartcontract.ErrFulfillmentInvalid,
|
||||
code: response.CodeBadRequest,
|
||||
msg: "交付信息不合法",
|
||||
},
|
||||
{
|
||||
name: "manual stock insufficient",
|
||||
err: cart.ErrManualStockInsufficient,
|
||||
err: cartcontract.ErrManualStockInsufficient,
|
||||
code: response.CodeBadRequest,
|
||||
msg: "人工库存不足",
|
||||
},
|
||||
@@ -1,4 +1,4 @@
|
||||
package dto
|
||||
package presenter
|
||||
|
||||
import (
|
||||
"github.com/dujiao-next/internal/shared/jsonmap"
|
||||
@@ -18,7 +18,8 @@ import (
|
||||
productgormstore "github.com/dujiao-next/internal/modules/catalog/product/store/gormstore"
|
||||
|
||||
"github.com/dujiao-next/internal/constants"
|
||||
"github.com/dujiao-next/internal/models"
|
||||
cartdomain "github.com/dujiao-next/internal/modules/cart/domain"
|
||||
cartgormstore "github.com/dujiao-next/internal/modules/cart/infrastructure/gormstore"
|
||||
categorygormstore "github.com/dujiao-next/internal/modules/catalog/category/infrastructure/gormstore"
|
||||
mappinggormstore "github.com/dujiao-next/internal/modules/catalog/mapping/infrastructure/gormstore"
|
||||
memberlevelgormstore "github.com/dujiao-next/internal/modules/memberlevel/infrastructure/gormstore"
|
||||
@@ -105,7 +106,7 @@ func TestProductServiceDeleteCascade(t *testing.T) {
|
||||
}
|
||||
|
||||
// 创建购物车项
|
||||
cart := models.CartItem{
|
||||
cart := cartdomain.Item{
|
||||
UserID: 1,
|
||||
ProductID: product.ID,
|
||||
SKUID: sku.ID,
|
||||
@@ -157,7 +158,7 @@ func TestProductServiceDeleteCascade(t *testing.T) {
|
||||
}
|
||||
|
||||
var cartCount int64
|
||||
db.Model(&models.CartItem{}).Where("product_id = ?", product.ID).Count(&cartCount)
|
||||
db.Model(&cartdomain.Item{}).Where("product_id = ? AND deleted_at IS NULL", product.ID).Count(&cartCount)
|
||||
if cartCount != 0 {
|
||||
t.Errorf("expected 0 cart items after delete, got %d", cartCount)
|
||||
}
|
||||
@@ -203,7 +204,7 @@ func TestProductServiceDeleteRollsBackCascadeWhenProductDeleteFails(t *testing.T
|
||||
CardSecretBatches: repository.NewCardSecretBatchRepository(db),
|
||||
Categories: categorygormstore.NewCategoryStore(db),
|
||||
MemberLevelPrices: memberlevelgormstore.NewPriceStore(db),
|
||||
Carts: repository.NewCartRepository(db),
|
||||
Carts: cartgormstore.New(db),
|
||||
ProductMappings: mappinggormstore.NewMappingStore(db),
|
||||
Orders: repository.NewOrderRepository(db),
|
||||
PaymentChannels: repository.NewPaymentChannelRepository(db),
|
||||
@@ -229,7 +230,7 @@ func TestProductServiceDeleteRollsBackCascadeWhenProductDeleteFails(t *testing.T
|
||||
if err := db.Create(&sku).Error; err != nil {
|
||||
t.Fatalf("create sku failed: %v", err)
|
||||
}
|
||||
cart := models.CartItem{
|
||||
cart := cartdomain.Item{
|
||||
UserID: 1,
|
||||
ProductID: product.ID,
|
||||
SKUID: sku.ID,
|
||||
@@ -255,7 +256,7 @@ func TestProductServiceDeleteRollsBackCascadeWhenProductDeleteFails(t *testing.T
|
||||
|
||||
assertProductRelationCount(t, db, &productdomain.Product{}, "id = ?", product.ID, 1)
|
||||
assertProductRelationCount(t, db, &productdomain.ProductSKU{}, "product_id = ?", product.ID, 1)
|
||||
assertProductRelationCount(t, db, &models.CartItem{}, "product_id = ?", product.ID, 1)
|
||||
assertProductRelationCount(t, db, &cartdomain.Item{}, "product_id = ? AND deleted_at IS NULL", product.ID, 1)
|
||||
assertProductRelationCount(t, db, &mappingdomain.Mapping{}, "local_product_id = ?", product.ID, 1)
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -21,6 +21,8 @@ import (
|
||||
|
||||
"github.com/dujiao-next/internal/constants"
|
||||
"github.com/dujiao-next/internal/models"
|
||||
cartdomain "github.com/dujiao-next/internal/modules/cart/domain"
|
||||
cartgormstore "github.com/dujiao-next/internal/modules/cart/infrastructure/gormstore"
|
||||
categorygormstore "github.com/dujiao-next/internal/modules/catalog/category/infrastructure/gormstore"
|
||||
mappinggormstore "github.com/dujiao-next/internal/modules/catalog/mapping/infrastructure/gormstore"
|
||||
memberlevelgormstore "github.com/dujiao-next/internal/modules/memberlevel/infrastructure/gormstore"
|
||||
@@ -49,7 +51,7 @@ func newProductServiceForResellerPublicTest(t *testing.T) (catalogproductbootstr
|
||||
&models.CardSecret{},
|
||||
&models.CardSecretBatch{},
|
||||
&memberleveldomain.MemberLevelPrice{},
|
||||
&models.CartItem{},
|
||||
&cartdomain.Item{},
|
||||
&mappingdomain.Mapping{},
|
||||
&mappingdomain.SKUMapping{},
|
||||
&models.Order{},
|
||||
@@ -68,7 +70,7 @@ func newProductServiceForResellerPublicTest(t *testing.T) (catalogproductbootstr
|
||||
CardSecretBatches: repository.NewCardSecretBatchRepository(db),
|
||||
Categories: categorygormstore.NewCategoryStore(db),
|
||||
MemberLevelPrices: memberlevelgormstore.NewPriceStore(db),
|
||||
Carts: repository.NewCartRepository(db),
|
||||
Carts: cartgormstore.New(db),
|
||||
ProductMappings: mappinggormstore.NewMappingStore(db),
|
||||
Orders: repository.NewOrderRepository(db),
|
||||
PaymentChannels: repository.NewPaymentChannelRepository(db),
|
||||
|
||||
+4
-2
@@ -17,6 +17,8 @@ import (
|
||||
|
||||
catalogproductbootstrap "github.com/dujiao-next/internal/bootstrap/catalogproduct"
|
||||
"github.com/dujiao-next/internal/models"
|
||||
cartdomain "github.com/dujiao-next/internal/modules/cart/domain"
|
||||
cartgormstore "github.com/dujiao-next/internal/modules/cart/infrastructure/gormstore"
|
||||
categorygormstore "github.com/dujiao-next/internal/modules/catalog/category/infrastructure/gormstore"
|
||||
mappinggormstore "github.com/dujiao-next/internal/modules/catalog/mapping/infrastructure/gormstore"
|
||||
memberlevelgormstore "github.com/dujiao-next/internal/modules/memberlevel/infrastructure/gormstore"
|
||||
@@ -63,7 +65,7 @@ func newProductServiceForTest(t *testing.T) (catalogproductbootstrap.Services, *
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite failed: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&categorydomain.Category{}, &productdomain.Product{}, &productdomain.ProductSKU{}, &models.CardSecret{}, &models.CardSecretBatch{}, &memberleveldomain.MemberLevelPrice{}, &models.CartItem{}, &mappingdomain.Mapping{}, &mappingdomain.SKUMapping{}, &models.Order{}, &models.OrderItem{}, &models.PaymentChannel{}); err != nil {
|
||||
if err := db.AutoMigrate(&categorydomain.Category{}, &productdomain.Product{}, &productdomain.ProductSKU{}, &models.CardSecret{}, &models.CardSecretBatch{}, &memberleveldomain.MemberLevelPrice{}, &cartdomain.Item{}, &mappingdomain.Mapping{}, &mappingdomain.SKUMapping{}, &models.Order{}, &models.OrderItem{}, &models.PaymentChannel{}); err != nil {
|
||||
t.Fatalf("auto migrate product service tables failed: %v", err)
|
||||
}
|
||||
|
||||
@@ -74,7 +76,7 @@ func newProductServiceForTest(t *testing.T) (catalogproductbootstrap.Services, *
|
||||
CardSecretBatches: repository.NewCardSecretBatchRepository(db),
|
||||
Categories: categorygormstore.NewCategoryStore(db),
|
||||
MemberLevelPrices: memberlevelgormstore.NewPriceStore(db),
|
||||
Carts: repository.NewCartRepository(db),
|
||||
Carts: cartgormstore.New(db),
|
||||
ProductMappings: mappinggormstore.NewMappingStore(db),
|
||||
Orders: repository.NewOrderRepository(db),
|
||||
PaymentChannels: repository.NewPaymentChannelRepository(db),
|
||||
|
||||
@@ -21,6 +21,8 @@ import (
|
||||
"github.com/dujiao-next/internal/constants"
|
||||
"github.com/dujiao-next/internal/models"
|
||||
cardsecretgormstore "github.com/dujiao-next/internal/modules/cardsecret/store/gormstore"
|
||||
cartdomain "github.com/dujiao-next/internal/modules/cart/domain"
|
||||
cartgormstore "github.com/dujiao-next/internal/modules/cart/infrastructure/gormstore"
|
||||
categorygormstore "github.com/dujiao-next/internal/modules/catalog/category/infrastructure/gormstore"
|
||||
mappinggormstore "github.com/dujiao-next/internal/modules/catalog/mapping/infrastructure/gormstore"
|
||||
productapplication "github.com/dujiao-next/internal/modules/catalog/product/application"
|
||||
@@ -68,20 +70,10 @@ type productAdminUoW struct {
|
||||
cardSecrets *cardsecretgormstore.Store
|
||||
cardSecretBatches *cardsecretgormstore.BatchStore
|
||||
memberLevelPrices *memberlevelgormstore.PriceStore
|
||||
carts *cartDeleteStore
|
||||
carts *cartgormstore.Store
|
||||
productMappings *mappinggormstore.MappingStore
|
||||
}
|
||||
|
||||
type cartDeleteStore struct{ db *gorm.DB }
|
||||
|
||||
func (s *cartDeleteStore) DeleteByProduct(productID uint) error {
|
||||
return s.db.Where("product_id = ?", productID).Delete(&models.CartItem{}).Error
|
||||
}
|
||||
|
||||
func (s *cartDeleteStore) WithTx(tx *gorm.DB) *cartDeleteStore {
|
||||
return &cartDeleteStore{db: tx}
|
||||
}
|
||||
|
||||
func (unit *productAdminUoW) WithinTransaction(fn func(productadmin.DeleteRepositories) error) error {
|
||||
if fn == nil {
|
||||
return nil
|
||||
@@ -134,7 +126,7 @@ func setupAdminProductHandlerTest(t *testing.T) (*producthttp.AdminProductHandle
|
||||
&models.CardSecret{},
|
||||
&models.CardSecretBatch{},
|
||||
&memberleveldomain.MemberLevelPrice{},
|
||||
&models.CartItem{},
|
||||
&cartdomain.Item{},
|
||||
&mappingdomain.Mapping{},
|
||||
&mappingdomain.SKUMapping{},
|
||||
&models.Order{},
|
||||
@@ -152,7 +144,7 @@ func setupAdminProductHandlerTest(t *testing.T) (*producthttp.AdminProductHandle
|
||||
memberLevelPriceStore := memberlevelgormstore.NewPriceStore(db)
|
||||
mappingStore := mappinggormstore.NewMappingStore(db)
|
||||
skuMappingStore := mappinggormstore.NewSKUMappingStore(db)
|
||||
cartStore := &cartDeleteStore{db: db}
|
||||
cartStore := cartgormstore.New(db)
|
||||
|
||||
facade := &productHandlerFacade{
|
||||
Service: productapplication.NewService(productapplication.Options{
|
||||
|
||||
@@ -13,7 +13,8 @@ import (
|
||||
auditlogcontract "github.com/dujiao-next/internal/modules/auditlog/contract"
|
||||
"github.com/dujiao-next/internal/modules/captcha"
|
||||
"github.com/dujiao-next/internal/modules/cardsecret"
|
||||
"github.com/dujiao-next/internal/modules/cart"
|
||||
cartapp "github.com/dujiao-next/internal/modules/cart/application"
|
||||
cartgormstore "github.com/dujiao-next/internal/modules/cart/infrastructure/gormstore"
|
||||
categoryapp "github.com/dujiao-next/internal/modules/catalog/category/application"
|
||||
categorycontract "github.com/dujiao-next/internal/modules/catalog/category/contract"
|
||||
mappingapp "github.com/dujiao-next/internal/modules/catalog/mapping/application"
|
||||
@@ -86,7 +87,7 @@ type Container struct {
|
||||
FulfillmentRepo repository.FulfillmentRepository
|
||||
ProductRepo *productgormstore.ProductStore
|
||||
ProductSKURepo *productgormstore.SKUStore
|
||||
CartRepo repository.CartRepository
|
||||
CartRepo *cartgormstore.Store
|
||||
CouponRepo *coupongormstore.Store
|
||||
CouponUsageRepo *coupongormstore.UsageStore
|
||||
PromotionRepo *promotiongormstore.Store
|
||||
@@ -137,7 +138,7 @@ type Container struct {
|
||||
CategoryService *categoryapp.Service
|
||||
SettingService *settingsapp.Service
|
||||
SitemapService *sitemap.Service
|
||||
CartService *cart.Service
|
||||
CartService *cartapp.Service
|
||||
WalletService *service.WalletService
|
||||
OrderRefundService *service.OrderRefundService
|
||||
OrderService *service.OrderService
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
affiliategormstore "github.com/dujiao-next/internal/modules/affiliate/infrastructure/gormstore"
|
||||
apicredentialgormstore "github.com/dujiao-next/internal/modules/apicredential/infrastructure/gormstore"
|
||||
auditloggormstore "github.com/dujiao-next/internal/modules/auditlog/infrastructure/gormstore"
|
||||
cartgormstore "github.com/dujiao-next/internal/modules/cart/infrastructure/gormstore"
|
||||
categorygormstore "github.com/dujiao-next/internal/modules/catalog/category/infrastructure/gormstore"
|
||||
mappinggormstore "github.com/dujiao-next/internal/modules/catalog/mapping/infrastructure/gormstore"
|
||||
productgormstore "github.com/dujiao-next/internal/modules/catalog/product/store/gormstore"
|
||||
@@ -42,7 +43,7 @@ func (c *Container) initRepositories() {
|
||||
c.FulfillmentRepo = repository.NewFulfillmentRepository(db)
|
||||
c.ProductRepo = productgormstore.NewProductStore(db)
|
||||
c.ProductSKURepo = productgormstore.NewSKUStore(db)
|
||||
c.CartRepo = repository.NewCartRepository(db)
|
||||
c.CartRepo = cartgormstore.New(db)
|
||||
c.CouponRepo = coupongormstore.New(db)
|
||||
c.CouponUsageRepo = coupongormstore.NewUsageStore(db)
|
||||
c.PromotionRepo = promotiongormstore.New(db)
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/dujiao-next/internal/logger"
|
||||
"github.com/dujiao-next/internal/models"
|
||||
"github.com/dujiao-next/internal/modules/cardsecret"
|
||||
"github.com/dujiao-next/internal/modules/cart"
|
||||
cartapp "github.com/dujiao-next/internal/modules/cart/application"
|
||||
contentapp "github.com/dujiao-next/internal/modules/content/application"
|
||||
"github.com/dujiao-next/internal/modules/content/infrastructure/gormstore"
|
||||
couponapp "github.com/dujiao-next/internal/modules/coupon/application"
|
||||
@@ -61,7 +61,7 @@ func (c *Container) initApplicationServices() {
|
||||
panic(err)
|
||||
}
|
||||
c.SitemapService = sitemapService
|
||||
c.CartService = cart.NewService(c.CartRepo, c.ProductRepo, c.ProductSKURepo, c.PromotionRepo, c.SettingService)
|
||||
c.CartService = cartapp.NewService(c.CartRepo, c.ProductRepo, c.ProductSKURepo, c.PromotionRepo, c.SettingService)
|
||||
c.WalletService = service.NewWalletService(c.WalletRepo, c.OrderRepo, c.OrderRefundRecordRepo, c.UserStore, c.AffiliateRefundHandler, c.SettingService)
|
||||
c.OrderRefundService = service.NewOrderRefundService(c.OrderRepo, c.UserStore, c.OrderRefundRecordRepo, c.AffiliateRefundHandler, c.SettingService)
|
||||
c.MemberLevelService = memberlevelapp.NewService(c.MemberLevelRepo, c.MemberLevelPriceRepo, c.MemberLevelUserRepo)
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/dujiao-next/internal/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// CartRepository 购物车数据访问接口
|
||||
type CartRepository interface {
|
||||
ListByUser(userID uint) ([]models.CartItem, error)
|
||||
Upsert(item *models.CartItem) error
|
||||
DeleteByProduct(productID uint) error
|
||||
DeleteByUserAndProduct(userID, productID uint) error
|
||||
DeleteByUserProductSKU(userID, productID, skuID uint) error
|
||||
ClearByUser(userID uint) error
|
||||
WithTx(tx *gorm.DB) *GormCartRepository
|
||||
}
|
||||
|
||||
// GormCartRepository GORM 实现
|
||||
type GormCartRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewCartRepository 创建购物车仓库
|
||||
func NewCartRepository(db *gorm.DB) *GormCartRepository {
|
||||
return &GormCartRepository{db: db}
|
||||
}
|
||||
|
||||
// WithTx 绑定事务
|
||||
func (r *GormCartRepository) WithTx(tx *gorm.DB) *GormCartRepository {
|
||||
if tx == nil {
|
||||
return r
|
||||
}
|
||||
return &GormCartRepository{db: tx}
|
||||
}
|
||||
|
||||
// ListByUser 获取用户购物车项
|
||||
func (r *GormCartRepository) ListByUser(userID uint) ([]models.CartItem, error) {
|
||||
var items []models.CartItem
|
||||
if err := r.db.
|
||||
Preload("Product", "deleted_at IS NULL").
|
||||
Preload("SKU", "deleted_at IS NULL").
|
||||
Where("user_id = ?", userID).
|
||||
Order("updated_at desc").
|
||||
Find(&items).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// Upsert 添加或更新购物车项
|
||||
func (r *GormCartRepository) Upsert(item *models.CartItem) error {
|
||||
if item == nil {
|
||||
return nil
|
||||
}
|
||||
var existing models.CartItem
|
||||
err := r.db.Where("user_id = ? AND product_id = ? AND sku_id = ?", item.UserID, item.ProductID, item.SKUID).First(&existing).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return r.db.Create(item).Error
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updates := map[string]interface{}{
|
||||
"sku_id": item.SKUID,
|
||||
"quantity": item.Quantity,
|
||||
"fulfillment_type": item.FulfillmentType,
|
||||
"updated_at": item.UpdatedAt,
|
||||
}
|
||||
return r.db.Model(&existing).Updates(updates).Error
|
||||
}
|
||||
|
||||
// DeleteByProduct 删除指定商品的所有购物车项
|
||||
func (r *GormCartRepository) DeleteByProduct(productID uint) error {
|
||||
return r.db.Where("product_id = ?", productID).Delete(&models.CartItem{}).Error
|
||||
}
|
||||
|
||||
// DeleteByUserAndProduct 删除购物车项
|
||||
func (r *GormCartRepository) DeleteByUserAndProduct(userID, productID uint) error {
|
||||
return r.db.Where("user_id = ? AND product_id = ?", userID, productID).Delete(&models.CartItem{}).Error
|
||||
}
|
||||
|
||||
// DeleteByUserProductSKU 按用户+商品+SKU删除购物车项
|
||||
func (r *GormCartRepository) DeleteByUserProductSKU(userID, productID, skuID uint) error {
|
||||
if skuID == 0 {
|
||||
return r.DeleteByUserAndProduct(userID, productID)
|
||||
}
|
||||
return r.db.Where("user_id = ? AND product_id = ? AND sku_id = ?", userID, productID, skuID).Delete(&models.CartItem{}).Error
|
||||
}
|
||||
|
||||
// ClearByUser 清空购物车
|
||||
func (r *GormCartRepository) ClearByUser(userID uint) error {
|
||||
return r.db.Where("user_id = ?", userID).Delete(&models.CartItem{}).Error
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dujiao-next/internal/models"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func setupCartRepositoryTest(t *testing.T) (*GormCartRepository, *gorm.DB) {
|
||||
t.Helper()
|
||||
dsn := fmt.Sprintf("file:cart_repository_test_%d?mode=memory&cache=shared", time.Now().UnixNano())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite failed: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&models.CartItem{}); err != nil {
|
||||
t.Fatalf("migrate cart item failed: %v", err)
|
||||
}
|
||||
return NewCartRepository(db), db
|
||||
}
|
||||
|
||||
func TestCartRepositoryUpsertUsesProductAndSKUDimension(t *testing.T) {
|
||||
repo, db := setupCartRepositoryTest(t)
|
||||
now := time.Now()
|
||||
|
||||
first := &models.CartItem{
|
||||
UserID: 10001,
|
||||
ProductID: 888,
|
||||
SKUID: 101,
|
||||
Quantity: 1,
|
||||
FulfillmentType: "manual",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
second := &models.CartItem{
|
||||
UserID: 10001,
|
||||
ProductID: 888,
|
||||
SKUID: 102,
|
||||
Quantity: 2,
|
||||
FulfillmentType: "manual",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if err := repo.Upsert(first); err != nil {
|
||||
t.Fatalf("upsert first sku failed: %v", err)
|
||||
}
|
||||
if err := repo.Upsert(second); err != nil {
|
||||
t.Fatalf("upsert second sku failed: %v", err)
|
||||
}
|
||||
|
||||
var count int64
|
||||
if err := db.Model(&models.CartItem{}).
|
||||
Where("user_id = ? AND product_id = ?", first.UserID, first.ProductID).
|
||||
Count(&count).Error; err != nil {
|
||||
t.Fatalf("count cart items failed: %v", err)
|
||||
}
|
||||
if count != 2 {
|
||||
t.Fatalf("cart rows want 2 got %d", count)
|
||||
}
|
||||
|
||||
first.Quantity = 5
|
||||
first.UpdatedAt = now.Add(time.Minute)
|
||||
if err := repo.Upsert(first); err != nil {
|
||||
t.Fatalf("update first sku failed: %v", err)
|
||||
}
|
||||
|
||||
var gotFirst models.CartItem
|
||||
if err := db.Where("user_id = ? AND product_id = ? AND sku_id = ?", first.UserID, first.ProductID, first.SKUID).First(&gotFirst).Error; err != nil {
|
||||
t.Fatalf("query first sku row failed: %v", err)
|
||||
}
|
||||
if gotFirst.Quantity != 5 {
|
||||
t.Fatalf("first sku quantity want 5 got %d", gotFirst.Quantity)
|
||||
}
|
||||
|
||||
var gotSecond models.CartItem
|
||||
if err := db.Where("user_id = ? AND product_id = ? AND sku_id = ?", second.UserID, second.ProductID, second.SKUID).First(&gotSecond).Error; err != nil {
|
||||
t.Fatalf("query second sku row failed: %v", err)
|
||||
}
|
||||
if gotSecond.Quantity != 2 {
|
||||
t.Fatalf("second sku quantity should keep 2 got %d", gotSecond.Quantity)
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
apicredentialtransport "github.com/dujiao-next/internal/modules/apicredential/transport/http"
|
||||
auditlogtransport "github.com/dujiao-next/internal/modules/auditlog/transport/http"
|
||||
captchahttp "github.com/dujiao-next/internal/modules/captcha/transport/http"
|
||||
carttransport "github.com/dujiao-next/internal/modules/cart/transport/http"
|
||||
categoryhttp "github.com/dujiao-next/internal/modules/catalog/category/transport/http"
|
||||
mappinghttp "github.com/dujiao-next/internal/modules/catalog/mapping/transport/http"
|
||||
producthttp "github.com/dujiao-next/internal/modules/catalog/product/transport/http"
|
||||
@@ -35,7 +36,6 @@ import (
|
||||
adminauthwiring "github.com/dujiao-next/internal/wiring/adminauth"
|
||||
adminauthzwiring "github.com/dujiao-next/internal/wiring/adminauthz"
|
||||
adminuserwiring "github.com/dujiao-next/internal/wiring/adminuser"
|
||||
cartwiring "github.com/dujiao-next/internal/wiring/cart"
|
||||
channelwiring "github.com/dujiao-next/internal/wiring/channel"
|
||||
channeluserwiring "github.com/dujiao-next/internal/wiring/channeluser"
|
||||
fulfillmentwiring "github.com/dujiao-next/internal/wiring/fulfillment"
|
||||
@@ -84,7 +84,7 @@ func SetupRouter(cfg *config.Config, c *provider.Container) *gin.Engine {
|
||||
paymentWebhookHandler := paymentHandlers.Webhook
|
||||
paymentCallbackHandler := paymentHandlers.Callback
|
||||
publicConfigHandler := publicconfigwiring.NewHandler(c)
|
||||
userCartHandler := cartwiring.NewUserHandler(c)
|
||||
userCartHandler := carttransport.NewUserHandler(c.CartService)
|
||||
channelHandler := channelwiring.NewHandler(c)
|
||||
upstreamHandler := upstreamwiring.NewHandler(c)
|
||||
publicContentHandler := contenttransport.NewPublicHandler(
|
||||
|
||||
@@ -8,13 +8,13 @@ import (
|
||||
apicredentialtransport "github.com/dujiao-next/internal/modules/apicredential/transport/http"
|
||||
auditlogtransport "github.com/dujiao-next/internal/modules/auditlog/transport/http"
|
||||
captchatransport "github.com/dujiao-next/internal/modules/captcha/transport/http"
|
||||
carttransport "github.com/dujiao-next/internal/modules/cart/transport/http"
|
||||
categoryhttp "github.com/dujiao-next/internal/modules/catalog/category/transport/http"
|
||||
producthttp "github.com/dujiao-next/internal/modules/catalog/product/transport/http"
|
||||
contenttransport "github.com/dujiao-next/internal/modules/content/transport/http"
|
||||
giftcardtransport "github.com/dujiao-next/internal/modules/giftcard/transport/http"
|
||||
memberleveltransport "github.com/dujiao-next/internal/modules/memberlevel/transport/http"
|
||||
"github.com/dujiao-next/internal/provider"
|
||||
carttransport "github.com/dujiao-next/internal/transport/http/cart"
|
||||
ordertransport "github.com/dujiao-next/internal/transport/http/order"
|
||||
paymenttransport "github.com/dujiao-next/internal/transport/http/payment"
|
||||
paymentcallbacktransport "github.com/dujiao-next/internal/transport/http/payment/callback"
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package cartwiring
|
||||
|
||||
import (
|
||||
"github.com/dujiao-next/internal/provider"
|
||||
carttransport "github.com/dujiao-next/internal/transport/http/cart"
|
||||
)
|
||||
|
||||
func NewUserHandler(c *provider.Container) *carttransport.UserHandler {
|
||||
return carttransport.NewUserHandler(c.CartService)
|
||||
}
|
||||
Reference in new issue
Block a user