refactor(core): remove structural compatibility aliases
This commit is contained in:
1 parent
0472220a68
commit
3da93ba7ed
35 files changed
+186
-307
No files matched your search
@@ -170,7 +170,7 @@ func TestCatalogProductImplementationLivesInNestedBoundedContext(t *testing.T) {
|
||||
}
|
||||
assertFileDeclaresTypes(t, filepath.Join(applicationRoot, "query.go"), []string{
|
||||
"ProductRepository", "CategoryRepository", "HiddenProductRepository", "StockCounter",
|
||||
"TenantContext", "Options", "Service",
|
||||
"Options", "Service",
|
||||
})
|
||||
assertFileDeclaresFunctions(t, filepath.Join(applicationRoot, "query.go"), []string{
|
||||
"NewService", "ListPublic", "ListPublicForTenant", "ListForUpstreamSync", "ListPublicExact",
|
||||
@@ -183,7 +183,7 @@ func TestCatalogProductImplementationLivesInNestedBoundedContext(t *testing.T) {
|
||||
"ProductRepository", "CategoryRepository", "CardSecretStockRepository", "OrderHistoryRepository",
|
||||
"ProductDeleteRepository", "CardSecretDeleteRepository", "CardSecretBatchDeleteRepository",
|
||||
"SKUDeleteRepository", "MemberLevelPriceDeleteRepository", "CartDeleteRepository",
|
||||
"ProductMappingDeleteRepository", "DeleteRepositories", "UnitOfWork", "ErrorSet", "Options", "AdminService",
|
||||
"ProductMappingDeleteRepository", "DeleteRepositories", "UnitOfWork", "Options", "AdminService",
|
||||
})
|
||||
assertFileDeclaresFunctions(t, filepath.Join(adminRoot, "service.go"), []string{"NewAdminService"})
|
||||
assertFileDeclaresFunctions(t, filepath.Join(adminRoot, "operations.go"), []string{
|
||||
@@ -198,8 +198,8 @@ func TestCatalogProductImplementationLivesInNestedBoundedContext(t *testing.T) {
|
||||
})
|
||||
assertFileDeclaresTypes(t, filepath.Join(writeRoot, "service.go"), []string{
|
||||
"ProductRepository", "SKURepository", "CategoryRepository", "PaymentChannelStoresitory",
|
||||
"CardSecretStockRepository", "TransactionRepositories", "UnitOfWork", "ErrorSet", "Options",
|
||||
"WriteService", "CreateProductInput", "WholesalePriceInput", "ProductSKUInput",
|
||||
"CardSecretStockRepository", "TransactionRepositories", "UnitOfWork", "Options",
|
||||
"WriteService", "CreateProductInput", "ProductSKUInput",
|
||||
})
|
||||
assertFileDeclaresFunctions(t, filepath.Join(writeRoot, "service.go"), []string{"NewWriteService"})
|
||||
assertFileDeclaresFunctions(t, filepath.Join(writeRoot, "create.go"), []string{"Create"})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package architecture
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -16,7 +17,7 @@ type packageFileBudget struct {
|
||||
// Test-only architecture assertions intentionally share one package so they can
|
||||
// reuse AST helpers. Production packages have no file-budget exceptions.
|
||||
var packageFileBudgetOverrides = map[string]packageFileBudget{
|
||||
"internal/architecture": {production: 0, total: 55},
|
||||
"internal/architecture": {production: 0, total: 51},
|
||||
}
|
||||
|
||||
// completedMigrationPaths are deleted compatibility-free entry points. Once a
|
||||
@@ -630,6 +631,41 @@ func TestNoNewCompatibilityOrLegacyProductionFiles(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProductionCodeContainsNoTypeAliases(t *testing.T) {
|
||||
repositoryRoot := findRepositoryRoot(t)
|
||||
internalRoot := filepath.Join(repositoryRoot, "internal")
|
||||
|
||||
err := filepath.WalkDir(internalRoot, func(path string, entry fs.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
if entry.IsDir() || filepath.Ext(path) != ".go" || strings.HasSuffix(path, "_test.go") {
|
||||
return nil
|
||||
}
|
||||
|
||||
parsed := parseProductionGoFile(t, path)
|
||||
ast.Inspect(parsed, func(node ast.Node) bool {
|
||||
typeSpec, ok := node.(*ast.TypeSpec)
|
||||
if ok && typeSpec.Assign.IsValid() {
|
||||
relativePath, err := filepath.Rel(repositoryRoot, path)
|
||||
if err != nil {
|
||||
t.Fatalf("resolve type alias path: %v", err)
|
||||
}
|
||||
t.Errorf(
|
||||
"production type alias %s is forbidden; depend on its owning package or declare an owned type: %s",
|
||||
typeSpec.Name.Name,
|
||||
filepath.ToSlash(relativePath),
|
||||
)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("inspect production Go aliases: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoPackagesStayWithinFileBudgets(t *testing.T) {
|
||||
repositoryRoot := findRepositoryRoot(t)
|
||||
internalRoot := filepath.Join(repositoryRoot, "internal")
|
||||
|
||||
@@ -192,11 +192,9 @@ func (adapter memberLevelPriceDeleteAdapter) DeleteByProduct(productID uint) err
|
||||
// New 显式装配 Product 查询、管理和写入用例。
|
||||
func New(dependencies Dependencies) Services {
|
||||
read := productapplication.NewService(productapplication.Options{
|
||||
Products: dependencies.Products,
|
||||
Categories: dependencies.Categories,
|
||||
Stock: dependencies.CardSecrets,
|
||||
NotFoundError: productcontract.ErrNotFound,
|
||||
ResellerProductNotListedError: productcontract.ErrResellerProductNotListed,
|
||||
Products: dependencies.Products,
|
||||
Categories: dependencies.Categories,
|
||||
Stock: dependencies.CardSecrets,
|
||||
})
|
||||
admin := productadmin.NewAdminService(productadmin.Options{
|
||||
Products: dependencies.Products,
|
||||
@@ -204,12 +202,6 @@ func New(dependencies Dependencies) Services {
|
||||
CardSecrets: dependencies.CardSecrets,
|
||||
Orders: dependencies.Orders,
|
||||
Transactions: newProductAdminUnitOfWork(dependencies.Products, dependencies.SKUs, dependencies.CardSecrets, dependencies.CardSecretBatches, dependencies.MemberLevelPrices, dependencies.Carts, dependencies.ProductMappings),
|
||||
Errors: productadmin.ErrorSet{
|
||||
NotFound: productcontract.ErrNotFound,
|
||||
ProductCategoryInvalid: productcontract.ErrProductCategoryInvalid,
|
||||
ProductHasStock: productcontract.ErrProductHasStock,
|
||||
ProductHasOrderRecord: productcontract.ErrProductHasOrderRecord,
|
||||
},
|
||||
})
|
||||
write := productwrite.NewWriteService(productwrite.Options{
|
||||
Products: dependencies.Products,
|
||||
@@ -217,19 +209,6 @@ func New(dependencies Dependencies) Services {
|
||||
Categories: dependencies.Categories,
|
||||
PaymentChannels: dependencies.PaymentChannels,
|
||||
Transactions: newProductWriteUnitOfWork(dependencies.Products, dependencies.SKUs, dependencies.CardSecrets),
|
||||
Errors: productwrite.ErrorSet{
|
||||
NotFound: productcontract.ErrNotFound,
|
||||
SlugExists: productcontract.ErrSlugExists,
|
||||
ProductCategoryInvalid: productcontract.ErrProductCategoryInvalid,
|
||||
ProductPurchaseInvalid: productcontract.ErrProductPurchaseInvalid,
|
||||
FulfillmentInvalid: productcontract.ErrFulfillmentInvalid,
|
||||
ProductPriceInvalid: productcontract.ErrProductPriceInvalid,
|
||||
ManualStockInvalid: productcontract.ErrManualStockInvalid,
|
||||
ProductPurchaseLimitInvalid: productcontract.ErrProductPurchaseLimitInvalid,
|
||||
ProductStockDisplayInvalid: productcontract.ErrProductStockDisplayInvalid,
|
||||
ProductSKUInvalid: productcontract.ErrProductSKUInvalid,
|
||||
ProductSKUHasCardSecretStock: productcontract.ErrProductSKUHasCardSecretStock,
|
||||
},
|
||||
})
|
||||
return Services{Read: read, Admin: admin, Write: write}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ type Handlers struct {
|
||||
Callback *paymentcallbacktransport.Handler
|
||||
}
|
||||
|
||||
// New assembles payment transports without exposing legacy adapters to router.
|
||||
// New assembles payment transports from application services and HTTP handlers.
|
||||
func New(c *container.Container) Handlers {
|
||||
guestOrders := guestOrderLookupAdapter{orders: c.OrderService}
|
||||
userOrders := userOrderLookupAdapter{orders: c.OrderService}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
// Service 购物车服务。
|
||||
@@ -17,12 +18,12 @@ type Service struct {
|
||||
cartRepo contract.Repository
|
||||
productRepo contract.ProductReader
|
||||
productSKURepo contract.SKUReader
|
||||
promotionRepo contract.PromotionRepository
|
||||
promotionRepo promotioncontract.Repository
|
||||
currencyReader contract.CurrencyReader
|
||||
}
|
||||
|
||||
// NewService 创建购物车服务。
|
||||
func NewService(cartRepo contract.Repository, productRepo contract.ProductReader, productSKURepo contract.SKUReader, promotionRepo contract.PromotionRepository, currencyReader contract.CurrencyReader) *Service {
|
||||
func NewService(cartRepo contract.Repository, productRepo contract.ProductReader, productSKURepo contract.SKUReader, promotionRepo promotioncontract.Repository, currencyReader contract.CurrencyReader) *Service {
|
||||
return &Service{
|
||||
cartRepo: cartRepo,
|
||||
productRepo: productRepo,
|
||||
|
||||
@@ -3,7 +3,6 @@ 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 是购物车持久化端口返回的条目及其商品快照。
|
||||
@@ -35,6 +34,3 @@ type SKUReader interface {
|
||||
type CurrencyReader interface {
|
||||
GetSiteCurrency(defaultCurrency string) (string, error)
|
||||
}
|
||||
|
||||
// PromotionRepository 是购物车价格计算所需的活动价端口。
|
||||
type PromotionRepository = promotioncontract.Repository
|
||||
@@ -3,6 +3,7 @@ package productadmin
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
productcontract "github.com/dujiao-next/internal/modules/catalog/product/contract"
|
||||
productdomain "github.com/dujiao-next/internal/modules/catalog/product/domain"
|
||||
)
|
||||
|
||||
@@ -13,7 +14,7 @@ func (s *AdminService) Delete(id string) error {
|
||||
return err
|
||||
}
|
||||
if product == nil {
|
||||
return s.errors.NotFound
|
||||
return productcontract.ErrNotFound
|
||||
}
|
||||
|
||||
// 事务前校验,避免 SQLite 在读后写场景中发生自锁。
|
||||
@@ -22,14 +23,14 @@ func (s *AdminService) Delete(id string) error {
|
||||
return err
|
||||
}
|
||||
if available > 0 {
|
||||
return s.errors.ProductHasStock
|
||||
return productcontract.ErrProductHasStock
|
||||
}
|
||||
reserved, err := s.cardSecrets.CountReserved(product.ID, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if reserved > 0 {
|
||||
return s.errors.ProductHasStock
|
||||
return productcontract.ErrProductHasStock
|
||||
}
|
||||
|
||||
orderCount, err := s.orders.CountOrderItemsByProduct(product.ID)
|
||||
@@ -37,7 +38,7 @@ func (s *AdminService) Delete(id string) error {
|
||||
return err
|
||||
}
|
||||
if orderCount > 0 {
|
||||
return s.errors.ProductHasOrderRecord
|
||||
return productcontract.ErrProductHasOrderRecord
|
||||
}
|
||||
|
||||
return s.transactions.WithinTransaction(func(repositories DeleteRepositories) error {
|
||||
@@ -70,18 +71,18 @@ func (s *AdminService) QuickUpdate(id string, fields map[string]interface{}) (*p
|
||||
return nil, err
|
||||
}
|
||||
if product == nil {
|
||||
return nil, s.errors.NotFound
|
||||
return nil, productcontract.ErrNotFound
|
||||
}
|
||||
if isActivatingProduct(fields) {
|
||||
categoryID := product.CategoryID
|
||||
if rawCategoryID, ok := fields["category_id"]; ok {
|
||||
parsedCategoryID, parseErr := categoryIDFromValue(rawCategoryID, s.errors.ProductCategoryInvalid)
|
||||
parsedCategoryID, parseErr := categoryIDFromValue(rawCategoryID, productcontract.ErrProductCategoryInvalid)
|
||||
if parseErr != nil {
|
||||
return nil, parseErr
|
||||
}
|
||||
categoryID = parsedCategoryID
|
||||
}
|
||||
if err := validateActivationCategory(s.categories, categoryID, s.errors.ProductCategoryInvalid); err != nil {
|
||||
if err := validateActivationCategory(s.categories, categoryID, productcontract.ErrProductCategoryInvalid); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -98,7 +99,7 @@ func (s *AdminService) UpdateWholesalePrices(id string, inputs []productdomain.W
|
||||
return nil, err
|
||||
}
|
||||
if product == nil {
|
||||
return nil, s.errors.NotFound
|
||||
return nil, productcontract.ErrNotFound
|
||||
}
|
||||
|
||||
wholesalePrices, err := productdomain.NormalizeWholesalePricesForSKUs(inputs, product.SKUs)
|
||||
|
||||
@@ -6,24 +6,21 @@ import (
|
||||
|
||||
categorydomain "github.com/dujiao-next/internal/modules/catalog/category/domain"
|
||||
|
||||
productcontract "github.com/dujiao-next/internal/modules/catalog/product/contract"
|
||||
productdomain "github.com/dujiao-next/internal/modules/catalog/product/domain"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
func TestAdminServiceDeleteStopsBeforeTransactionWhenStockExists(t *testing.T) {
|
||||
hasStock := errors.New("product has stock")
|
||||
transactions := &unitOfWorkStub{}
|
||||
service := NewAdminService(Options{
|
||||
Products: &productRepositoryStub{product: &productdomain.Product{ID: 7}},
|
||||
CardSecrets: stockRepositoryStub{available: 1},
|
||||
Orders: orderRepositoryStub{},
|
||||
Transactions: transactions,
|
||||
Errors: ErrorSet{
|
||||
ProductHasStock: hasStock,
|
||||
},
|
||||
})
|
||||
|
||||
if err := service.Delete("7"); !errors.Is(err, hasStock) {
|
||||
if err := service.Delete("7"); !errors.Is(err, productcontract.ErrProductHasStock) {
|
||||
t.Fatalf("expected product stock error, got %v", err)
|
||||
}
|
||||
if transactions.called {
|
||||
@@ -64,14 +61,10 @@ func TestAdminServiceDeleteUsesAllCascadePorts(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAdminServiceQuickUpdateValidatesActivationCategory(t *testing.T) {
|
||||
invalidCategory := errors.New("invalid product category")
|
||||
products := &productRepositoryStub{product: &productdomain.Product{ID: 11, CategoryID: 3}}
|
||||
service := NewAdminService(Options{
|
||||
Products: products,
|
||||
Categories: categoryRepositoryStub{categories: map[string]*categorydomain.Category{"5": {ID: 5, IsActive: true}}},
|
||||
Errors: ErrorSet{
|
||||
ProductCategoryInvalid: invalidCategory,
|
||||
},
|
||||
})
|
||||
|
||||
updated, err := service.QuickUpdate("11", map[string]interface{}{
|
||||
@@ -89,7 +82,7 @@ func TestAdminServiceQuickUpdateValidatesActivationCategory(t *testing.T) {
|
||||
"category_id": 5.5,
|
||||
"is_active": true,
|
||||
})
|
||||
if !errors.Is(err, invalidCategory) {
|
||||
if !errors.Is(err, productcontract.ErrProductCategoryInvalid) {
|
||||
t.Fatalf("expected invalid category error, got %v", err)
|
||||
}
|
||||
if products.quickUpdates != 1 {
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
package productadmin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
productdomain "github.com/dujiao-next/internal/modules/catalog/product/domain"
|
||||
)
|
||||
import productdomain "github.com/dujiao-next/internal/modules/catalog/product/domain"
|
||||
|
||||
// ProductRepository 是商品管理用例所需的最小商品端口。
|
||||
type ProductRepository interface {
|
||||
@@ -73,21 +69,12 @@ type UnitOfWork interface {
|
||||
WithinTransaction(fn func(DeleteRepositories) error) error
|
||||
}
|
||||
|
||||
// ErrorSet 保留旧服务层公开错误的 errors.Is 身份。
|
||||
type ErrorSet struct {
|
||||
NotFound error
|
||||
ProductCategoryInvalid error
|
||||
ProductHasStock error
|
||||
ProductHasOrderRecord error
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Products ProductRepository
|
||||
Categories CategoryRepository
|
||||
CardSecrets CardSecretStockRepository
|
||||
Orders OrderHistoryRepository
|
||||
Transactions UnitOfWork
|
||||
Errors ErrorSet
|
||||
}
|
||||
|
||||
// AdminService 承载商品删除、快速更新和批发价管理用例。
|
||||
@@ -97,7 +84,6 @@ type AdminService struct {
|
||||
cardSecrets CardSecretStockRepository
|
||||
orders OrderHistoryRepository
|
||||
transactions UnitOfWork
|
||||
errors ErrorSet
|
||||
}
|
||||
|
||||
func NewAdminService(options Options) *AdminService {
|
||||
@@ -107,22 +93,5 @@ func NewAdminService(options Options) *AdminService {
|
||||
cardSecrets: options.CardSecrets,
|
||||
orders: options.Orders,
|
||||
transactions: options.Transactions,
|
||||
errors: resolveErrorSet(options.Errors),
|
||||
}
|
||||
}
|
||||
|
||||
func resolveErrorSet(values ErrorSet) ErrorSet {
|
||||
return ErrorSet{
|
||||
NotFound: errorOrDefault(values.NotFound, "not found"),
|
||||
ProductCategoryInvalid: errorOrDefault(values.ProductCategoryInvalid, "product category invalid"),
|
||||
ProductHasStock: errorOrDefault(values.ProductHasStock, "product has stock"),
|
||||
ProductHasOrderRecord: errorOrDefault(values.ProductHasOrderRecord, "product has order record"),
|
||||
}
|
||||
}
|
||||
|
||||
func errorOrDefault(value error, message string) error {
|
||||
if value != nil {
|
||||
return value
|
||||
}
|
||||
return errors.New(message)
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
cardsecretcontract "github.com/dujiao-next/internal/modules/cardsecret/contract"
|
||||
cardsecretdomain "github.com/dujiao-next/internal/modules/cardsecret/domain"
|
||||
productcontract "github.com/dujiao-next/internal/modules/catalog/product/contract"
|
||||
resellercontract "github.com/dujiao-next/internal/modules/reseller/contract"
|
||||
)
|
||||
|
||||
type productRepositoryStub struct {
|
||||
@@ -81,7 +82,7 @@ func TestListPublicForTenantBuildsVisibilityFilterBeforePagination(t *testing.T)
|
||||
resellerID := uint(9)
|
||||
|
||||
_, total, err := service.ListPublicForTenant(
|
||||
TenantContext{ResellerID: &resellerID},
|
||||
resellercontract.TenantContext{ResellerID: &resellerID},
|
||||
hiddenProductRepositoryStub{ids: []uint{7, 8}},
|
||||
"10",
|
||||
"keyword",
|
||||
@@ -105,21 +106,17 @@ func TestListPublicForTenantBuildsVisibilityFilterBeforePagination(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryServicePreservesInjectedCompatibilityErrors(t *testing.T) {
|
||||
notFound := errors.New("legacy not found")
|
||||
notListed := errors.New("legacy reseller product not listed")
|
||||
func TestQueryServiceUsesProductContractErrors(t *testing.T) {
|
||||
service := NewService(Options{
|
||||
Products: &productRepositoryStub{},
|
||||
NotFoundError: notFound,
|
||||
ResellerProductNotListedError: notListed,
|
||||
Products: &productRepositoryStub{},
|
||||
})
|
||||
|
||||
if _, err := service.GetPublicBySlug("missing"); !errors.Is(err, notFound) {
|
||||
t.Fatalf("GetPublicBySlug want injected not-found error, got %v", err)
|
||||
if _, err := service.GetPublicBySlug("missing"); !errors.Is(err, productcontract.ErrNotFound) {
|
||||
t.Fatalf("GetPublicBySlug want contract not-found error, got %v", err)
|
||||
}
|
||||
resellerID := uint(9)
|
||||
if _, _, err := service.ListPublicForTenant(TenantContext{ResellerID: &resellerID}, nil, "", "", 1, 20); !errors.Is(err, notListed) {
|
||||
t.Fatalf("ListPublicForTenant want injected not-listed error, got %v", err)
|
||||
if _, _, err := service.ListPublicForTenant(resellercontract.TenantContext{ResellerID: &resellerID}, nil, "", "", 1, 20); !errors.Is(err, productcontract.ErrResellerProductNotListed) {
|
||||
t.Fatalf("ListPublicForTenant want contract not-listed error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,11 +14,6 @@ import (
|
||||
reseller "github.com/dujiao-next/internal/modules/reseller/contract"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFound = productcontract.ErrNotFound
|
||||
ErrResellerProductNotListed = productcontract.ErrResellerProductNotListed
|
||||
)
|
||||
|
||||
// ProductRepository 是读取应用服务所需的最小商品持久化端口。
|
||||
type ProductRepository interface {
|
||||
List(filter productcontract.ListFilter) ([]productdomain.Product, int64, error)
|
||||
@@ -42,43 +37,26 @@ type StockCounter interface {
|
||||
CountStockByProductIDs(productIDs []uint) ([]cardsecretcontract.SKUStockCount, error)
|
||||
}
|
||||
|
||||
// TenantContext 商品读取用例关心的租户上下文;值对象归属 modules/reseller。
|
||||
type TenantContext = reseller.TenantContext
|
||||
|
||||
// Options 描述 Product 读取应用服务的端口和兼容错误。
|
||||
// Options 描述 Product 读取应用服务的端口和领域错误。
|
||||
type Options struct {
|
||||
Products ProductRepository
|
||||
Categories CategoryRepository
|
||||
Stock StockCounter
|
||||
NotFoundError error
|
||||
ResellerProductNotListedError error
|
||||
Products ProductRepository
|
||||
Categories CategoryRepository
|
||||
Stock StockCounter
|
||||
}
|
||||
|
||||
// Service 编排商品查询、租户可见性和库存聚合。
|
||||
type Service struct {
|
||||
products ProductRepository
|
||||
categories CategoryRepository
|
||||
stock StockCounter
|
||||
notFoundError error
|
||||
resellerProductNotListedError error
|
||||
products ProductRepository
|
||||
categories CategoryRepository
|
||||
stock StockCounter
|
||||
}
|
||||
|
||||
// NewService 创建 Product 读取应用服务。
|
||||
func NewService(options Options) *Service {
|
||||
notFoundError := options.NotFoundError
|
||||
if notFoundError == nil {
|
||||
notFoundError = ErrNotFound
|
||||
}
|
||||
resellerProductNotListedError := options.ResellerProductNotListedError
|
||||
if resellerProductNotListedError == nil {
|
||||
resellerProductNotListedError = ErrResellerProductNotListed
|
||||
}
|
||||
return &Service{
|
||||
products: options.Products,
|
||||
categories: options.Categories,
|
||||
stock: options.Stock,
|
||||
notFoundError: notFoundError,
|
||||
resellerProductNotListedError: resellerProductNotListedError,
|
||||
products: options.Products,
|
||||
categories: options.Categories,
|
||||
stock: options.Stock,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,12 +80,12 @@ func (s *Service) ListPublic(categoryID, search string, page, pageSize int) ([]p
|
||||
}
|
||||
|
||||
// ListPublicForTenant 获取当前租户上下文的公开商品列表。
|
||||
func (s *Service) ListPublicForTenant(tenant TenantContext, resellerRepo HiddenProductRepository, categoryID, search string, page, pageSize int) ([]productdomain.Product, int64, error) {
|
||||
func (s *Service) ListPublicForTenant(tenant reseller.TenantContext, resellerRepo HiddenProductRepository, categoryID, search string, page, pageSize int) ([]productdomain.Product, int64, error) {
|
||||
if !tenant.IsReseller() {
|
||||
return s.ListPublic(categoryID, search, page, pageSize)
|
||||
}
|
||||
if tenant.ResellerID == nil || resellerRepo == nil {
|
||||
return nil, 0, s.resellerProductNotListedError
|
||||
return nil, 0, productcontract.ErrResellerProductNotListed
|
||||
}
|
||||
categoryIDs, err := expandPublicCategoryIDs(s.categories, categoryID)
|
||||
if err != nil {
|
||||
@@ -162,13 +140,13 @@ func (s *Service) GetPublicBySlug(slug string) (*productdomain.Product, error) {
|
||||
return nil, err
|
||||
}
|
||||
if item == nil {
|
||||
return nil, s.notFoundError
|
||||
return nil, productcontract.ErrNotFound
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
// GetPublicBySlugForTenant 获取当前租户上下文的公开商品详情。
|
||||
func (s *Service) GetPublicBySlugForTenant(tenant TenantContext, resellerRepo HiddenProductRepository, slug string) (*productdomain.Product, error) {
|
||||
func (s *Service) GetPublicBySlugForTenant(tenant reseller.TenantContext, resellerRepo HiddenProductRepository, slug string) (*productdomain.Product, error) {
|
||||
item, err := s.GetPublicBySlug(slug)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -177,7 +155,7 @@ func (s *Service) GetPublicBySlugForTenant(tenant TenantContext, resellerRepo Hi
|
||||
return item, nil
|
||||
}
|
||||
if tenant.ResellerID == nil || resellerRepo == nil {
|
||||
return nil, s.notFoundError
|
||||
return nil, productcontract.ErrNotFound
|
||||
}
|
||||
hiddenIDs, err := resellerRepo.ListHiddenProductIDs(*tenant.ResellerID)
|
||||
if err != nil {
|
||||
@@ -185,7 +163,7 @@ func (s *Service) GetPublicBySlugForTenant(tenant TenantContext, resellerRepo Hi
|
||||
}
|
||||
for _, id := range hiddenIDs {
|
||||
if id == item.ID {
|
||||
return nil, s.notFoundError
|
||||
return nil, productcontract.ErrNotFound
|
||||
}
|
||||
}
|
||||
return item, nil
|
||||
@@ -215,7 +193,7 @@ func (s *Service) GetAdminByID(id string) (*productdomain.Product, error) {
|
||||
return nil, err
|
||||
}
|
||||
if item == nil {
|
||||
return nil, s.notFoundError
|
||||
return nil, productcontract.ErrNotFound
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/dujiao-next/internal/constants"
|
||||
productcontract "github.com/dujiao-next/internal/modules/catalog/product/contract"
|
||||
productdomain "github.com/dujiao-next/internal/modules/catalog/product/domain"
|
||||
"github.com/dujiao-next/internal/modules/catalog/product/manualform"
|
||||
"github.com/dujiao-next/internal/shared/jsonmap"
|
||||
@@ -15,7 +16,7 @@ import (
|
||||
|
||||
// Create 创建商品
|
||||
func (s *WriteService) Create(input CreateProductInput) (*productdomain.Product, error) {
|
||||
if err := productdomain.ValidateCategoryAssignment(s.categories, input.CategoryID, 0, s.errors.ProductCategoryInvalid); err != nil {
|
||||
if err := productdomain.ValidateCategoryAssignment(s.categories, input.CategoryID, 0, productcontract.ErrProductCategoryInvalid); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -24,7 +25,7 @@ func (s *WriteService) Create(input CreateProductInput) (*productdomain.Product,
|
||||
return nil, err
|
||||
}
|
||||
if count > 0 {
|
||||
return nil, s.errors.SlugExists
|
||||
return nil, productcontract.ErrSlugExists
|
||||
}
|
||||
|
||||
isActive := true
|
||||
@@ -37,16 +38,16 @@ func (s *WriteService) Create(input CreateProductInput) (*productdomain.Product,
|
||||
}
|
||||
purchaseType := productdomain.NormalizePurchaseType(input.PurchaseType)
|
||||
if purchaseType == "" {
|
||||
return nil, s.errors.ProductPurchaseInvalid
|
||||
return nil, productcontract.ErrProductPurchaseInvalid
|
||||
}
|
||||
fulfillmentType := productdomain.NormalizeFulfillmentType(input.FulfillmentType)
|
||||
if fulfillmentType == "" {
|
||||
return nil, s.errors.FulfillmentInvalid
|
||||
return nil, productcontract.ErrFulfillmentInvalid
|
||||
}
|
||||
|
||||
priceAmount := input.PriceAmount.Round(2)
|
||||
if len(input.SKUs) == 0 && priceAmount.LessThanOrEqual(decimal.Zero) {
|
||||
return nil, s.errors.ProductPriceInvalid
|
||||
return nil, productcontract.ErrProductPriceInvalid
|
||||
}
|
||||
|
||||
manualStockTotal := 0
|
||||
@@ -54,7 +55,7 @@ func (s *WriteService) Create(input CreateProductInput) (*productdomain.Product,
|
||||
manualStockTotal = *input.ManualStockTotal
|
||||
}
|
||||
if manualStockTotal < constants.ManualStockUnlimited {
|
||||
return nil, s.errors.ManualStockInvalid
|
||||
return nil, productcontract.ErrManualStockInvalid
|
||||
}
|
||||
maxPurchaseQuantity := 0
|
||||
if input.MaxPurchaseQuantity != nil {
|
||||
@@ -65,15 +66,15 @@ func (s *WriteService) Create(input CreateProductInput) (*productdomain.Product,
|
||||
minPurchaseQuantity = productdomain.NormalizePurchaseQuantityLimit(*input.MinPurchaseQuantity)
|
||||
}
|
||||
if minPurchaseQuantity > 0 && maxPurchaseQuantity > 0 && minPurchaseQuantity > maxPurchaseQuantity {
|
||||
return nil, s.errors.ProductPurchaseLimitInvalid
|
||||
return nil, productcontract.ErrProductPurchaseLimitInvalid
|
||||
}
|
||||
stockDisplayMode := productdomain.NormalizeStockDisplayMode(input.StockDisplayMode)
|
||||
if stockDisplayMode == "" {
|
||||
return nil, s.errors.ProductStockDisplayInvalid
|
||||
return nil, productcontract.ErrProductStockDisplayInvalid
|
||||
}
|
||||
|
||||
costPriceAmount := input.CostPriceAmount.Round(2)
|
||||
var wholesaleInputs []WholesalePriceInput
|
||||
var wholesaleInputs []productdomain.WholesalePriceInput
|
||||
if input.WholesalePrices != nil {
|
||||
wholesaleInputs = *input.WholesalePrices
|
||||
}
|
||||
@@ -81,7 +82,7 @@ func (s *WriteService) Create(input CreateProductInput) (*productdomain.Product,
|
||||
var normalizedSKUs []normalizedProductSKU
|
||||
if len(input.SKUs) > 0 {
|
||||
if s.skus == nil {
|
||||
return nil, s.errors.ProductSKUInvalid
|
||||
return nil, productcontract.ErrProductSKUInvalid
|
||||
}
|
||||
var normalizeErr error
|
||||
normalizedSKUs, priceAmount, manualStockTotal, normalizeErr = s.normalizeProductSKUInputs(input.SKUs, fulfillmentType, nil)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package productwrite
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
paymentdomain "github.com/dujiao-next/internal/modules/payment/domain"
|
||||
|
||||
productdomain "github.com/dujiao-next/internal/modules/catalog/product/domain"
|
||||
@@ -55,21 +53,6 @@ type UnitOfWork interface {
|
||||
WithinTransaction(fn func(repositories TransactionRepositories) error) error
|
||||
}
|
||||
|
||||
// ErrorSet 保留旧 Service 错误哨兵身份,保证 errors.Is 与 HTTP 映射兼容。
|
||||
type ErrorSet struct {
|
||||
NotFound error
|
||||
SlugExists error
|
||||
ProductCategoryInvalid error
|
||||
ProductPurchaseInvalid error
|
||||
FulfillmentInvalid error
|
||||
ProductPriceInvalid error
|
||||
ManualStockInvalid error
|
||||
ProductPurchaseLimitInvalid error
|
||||
ProductStockDisplayInvalid error
|
||||
ProductSKUInvalid error
|
||||
ProductSKUHasCardSecretStock error
|
||||
}
|
||||
|
||||
// Options 描述商品写入应用服务依赖。
|
||||
type Options struct {
|
||||
Products ProductRepository
|
||||
@@ -77,7 +60,6 @@ type Options struct {
|
||||
Categories CategoryRepository
|
||||
PaymentChannels PaymentChannelStoresitory
|
||||
Transactions UnitOfWork
|
||||
Errors ErrorSet
|
||||
}
|
||||
|
||||
// WriteService 编排商品创建、更新和 SKU 同步。
|
||||
@@ -87,7 +69,6 @@ type WriteService struct {
|
||||
categories CategoryRepository
|
||||
paymentChannels PaymentChannelStoresitory
|
||||
transactions UnitOfWork
|
||||
errors ErrorSet
|
||||
}
|
||||
|
||||
// NewWriteService 创建商品写入应用服务。
|
||||
@@ -98,7 +79,6 @@ func NewWriteService(options Options) *WriteService {
|
||||
categories: options.Categories,
|
||||
paymentChannels: options.PaymentChannels,
|
||||
transactions: options.Transactions,
|
||||
errors: resolveErrorSet(options.Errors),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +95,7 @@ type CreateProductInput struct {
|
||||
PriceAmount decimal.Decimal
|
||||
CostPriceAmount decimal.Decimal
|
||||
// WholesalePrices 为可选字段:nil 表示更新时保留,非 nil 表示整体覆盖。
|
||||
WholesalePrices *[]WholesalePriceInput
|
||||
WholesalePrices *[]productdomain.WholesalePriceInput
|
||||
Images []string
|
||||
Tags []string
|
||||
PurchaseType string
|
||||
@@ -131,8 +111,6 @@ type CreateProductInput struct {
|
||||
SortOrder int
|
||||
}
|
||||
|
||||
type WholesalePriceInput = productdomain.WholesalePriceInput
|
||||
|
||||
// ProductSKUInput 描述商品 SKU 的完整写入值。
|
||||
type ProductSKUInput struct {
|
||||
ID uint
|
||||
@@ -145,32 +123,6 @@ type ProductSKUInput struct {
|
||||
SortOrder int
|
||||
}
|
||||
|
||||
func resolveErrorSet(values ErrorSet) ErrorSet {
|
||||
return ErrorSet{
|
||||
NotFound: errorOrDefault(values.NotFound, "not found"),
|
||||
SlugExists: errorOrDefault(values.SlugExists, "slug exists"),
|
||||
ProductCategoryInvalid: errorOrDefault(values.ProductCategoryInvalid, "product category invalid"),
|
||||
ProductPurchaseInvalid: errorOrDefault(values.ProductPurchaseInvalid, "product purchase invalid"),
|
||||
FulfillmentInvalid: errorOrDefault(values.FulfillmentInvalid, "fulfillment invalid"),
|
||||
ProductPriceInvalid: errorOrDefault(values.ProductPriceInvalid, "product price invalid"),
|
||||
ManualStockInvalid: errorOrDefault(values.ManualStockInvalid, "manual stock invalid"),
|
||||
ProductPurchaseLimitInvalid: errorOrDefault(values.ProductPurchaseLimitInvalid, "product purchase limit invalid"),
|
||||
ProductStockDisplayInvalid: errorOrDefault(values.ProductStockDisplayInvalid, "product stock display invalid"),
|
||||
ProductSKUInvalid: errorOrDefault(values.ProductSKUInvalid, "product sku invalid"),
|
||||
ProductSKUHasCardSecretStock: errorOrDefault(
|
||||
values.ProductSKUHasCardSecretStock,
|
||||
"product sku has card secret stock",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func errorOrDefault(value error, message string) error {
|
||||
if value != nil {
|
||||
return value
|
||||
}
|
||||
return errors.New(message)
|
||||
}
|
||||
|
||||
func (s *WriteService) filterAvailablePaymentChannelIDs(ids []uint) ([]uint, error) {
|
||||
if len(ids) == 0 {
|
||||
return nil, nil
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
productdomain "github.com/dujiao-next/internal/modules/catalog/product/domain"
|
||||
|
||||
"github.com/dujiao-next/internal/constants"
|
||||
productcontract "github.com/dujiao-next/internal/modules/catalog/product/contract"
|
||||
"github.com/dujiao-next/internal/shared/jsonmap"
|
||||
"github.com/dujiao-next/internal/shared/money"
|
||||
|
||||
@@ -39,7 +40,7 @@ func (s *WriteService) syncSingleProductSKU(skuRepo SKURepository, productID uin
|
||||
}
|
||||
targetIndex := pickSingleModeTargetSKUIndex(skus)
|
||||
if targetIndex < 0 || targetIndex >= len(skus) {
|
||||
return s.errors.ProductSKUInvalid
|
||||
return productcontract.ErrProductSKUInvalid
|
||||
}
|
||||
|
||||
target := skus[targetIndex]
|
||||
@@ -105,7 +106,7 @@ type normalizedProductSKU struct {
|
||||
|
||||
func (s *WriteService) normalizeProductSKUInputs(inputs []ProductSKUInput, fulfillmentType string, existingSKUMap map[uint]productdomain.ProductSKU) ([]normalizedProductSKU, decimal.Decimal, int, error) {
|
||||
if len(inputs) == 0 {
|
||||
return nil, decimal.Zero, 0, s.errors.ProductSKUInvalid
|
||||
return nil, decimal.Zero, 0, productcontract.ErrProductSKUInvalid
|
||||
}
|
||||
seenCode := make(map[string]struct{}, len(inputs))
|
||||
normalized := make([]normalizedProductSKU, 0, len(inputs))
|
||||
@@ -117,26 +118,26 @@ func (s *WriteService) normalizeProductSKUInputs(inputs []ProductSKUInput, fulfi
|
||||
for _, input := range inputs {
|
||||
skuCode := strings.TrimSpace(input.SKUCode)
|
||||
if skuCode == "" {
|
||||
return nil, decimal.Zero, 0, s.errors.ProductSKUInvalid
|
||||
return nil, decimal.Zero, 0, productcontract.ErrProductSKUInvalid
|
||||
}
|
||||
codeKey := strings.ToLower(skuCode)
|
||||
if _, exists := seenCode[codeKey]; exists {
|
||||
return nil, decimal.Zero, 0, s.errors.ProductSKUInvalid
|
||||
return nil, decimal.Zero, 0, productcontract.ErrProductSKUInvalid
|
||||
}
|
||||
seenCode[codeKey] = struct{}{}
|
||||
|
||||
priceAmount := input.PriceAmount.Round(2)
|
||||
if priceAmount.LessThanOrEqual(decimal.Zero) {
|
||||
return nil, decimal.Zero, 0, s.errors.ProductPriceInvalid
|
||||
return nil, decimal.Zero, 0, productcontract.ErrProductPriceInvalid
|
||||
}
|
||||
costPriceAmount := input.CostPriceAmount.Round(2)
|
||||
if costPriceAmount.LessThan(decimal.Zero) {
|
||||
return nil, decimal.Zero, 0, s.errors.ProductPriceInvalid
|
||||
return nil, decimal.Zero, 0, productcontract.ErrProductPriceInvalid
|
||||
}
|
||||
|
||||
manualTotal := input.ManualStockTotal
|
||||
if manualTotal < constants.ManualStockUnlimited {
|
||||
return nil, decimal.Zero, 0, s.errors.ManualStockInvalid
|
||||
return nil, decimal.Zero, 0, productcontract.ErrManualStockInvalid
|
||||
}
|
||||
if fulfillmentType != constants.FulfillmentTypeManual {
|
||||
manualTotal = 0
|
||||
@@ -144,7 +145,7 @@ func (s *WriteService) normalizeProductSKUInputs(inputs []ProductSKUInput, fulfi
|
||||
if existingSKUMap != nil && input.ID > 0 {
|
||||
_, ok := existingSKUMap[input.ID]
|
||||
if !ok {
|
||||
return nil, decimal.Zero, 0, s.errors.ProductSKUInvalid
|
||||
return nil, decimal.Zero, 0, productcontract.ErrProductSKUInvalid
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +185,7 @@ func (s *WriteService) normalizeProductSKUInputs(inputs []ProductSKUInput, fulfi
|
||||
}
|
||||
|
||||
if !hasActive {
|
||||
return nil, decimal.Zero, 0, s.errors.ProductSKUInvalid
|
||||
return nil, decimal.Zero, 0, productcontract.ErrProductSKUInvalid
|
||||
}
|
||||
if fulfillmentType != constants.FulfillmentTypeManual {
|
||||
manualStockTotal = 0
|
||||
@@ -219,7 +220,7 @@ func (s *WriteService) applyProductSKUsWithStockGuard(
|
||||
rows []normalizedProductSKU,
|
||||
) error {
|
||||
if skuRepo == nil || productID == 0 || len(rows) == 0 {
|
||||
return s.errors.ProductSKUInvalid
|
||||
return productcontract.ErrProductSKUInvalid
|
||||
}
|
||||
existingRows, err := skuRepo.ListByProduct(productID, false)
|
||||
if err != nil {
|
||||
@@ -240,7 +241,7 @@ func (s *WriteService) applyProductSKUsWithStockGuard(
|
||||
if row.ID > 0 {
|
||||
existing, ok := existingByID[row.ID]
|
||||
if !ok {
|
||||
return s.errors.ProductSKUInvalid
|
||||
return productcontract.ErrProductSKUInvalid
|
||||
}
|
||||
existing.SKUCode = row.SKUCode
|
||||
existing.SpecValuesJSON = row.SpecValuesJSON
|
||||
@@ -324,7 +325,7 @@ func (s *WriteService) ensureAutoSKUCardSecretStockSafe(
|
||||
if row.ID > 0 {
|
||||
existing, ok := existingByID[row.ID]
|
||||
if !ok {
|
||||
return s.errors.ProductSKUInvalid
|
||||
return productcontract.ErrProductSKUInvalid
|
||||
}
|
||||
nextActive[existing.ID] = row.IsActive
|
||||
kept[existing.ID] = struct{}{}
|
||||
@@ -354,7 +355,7 @@ func (s *WriteService) ensureAutoSKUCardSecretStockSafe(
|
||||
}
|
||||
outstanding := total - used
|
||||
if available > 0 || outstanding > 0 {
|
||||
return s.errors.ProductSKUHasCardSecretStock
|
||||
return productcontract.ErrProductSKUHasCardSecretStock
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
categorydomain "github.com/dujiao-next/internal/modules/catalog/category/domain"
|
||||
|
||||
"github.com/dujiao-next/internal/constants"
|
||||
productcontract "github.com/dujiao-next/internal/modules/catalog/product/contract"
|
||||
productdomain "github.com/dujiao-next/internal/modules/catalog/product/domain"
|
||||
"github.com/dujiao-next/internal/modules/catalog/product/manualform"
|
||||
"github.com/dujiao-next/internal/shared/jsonmap"
|
||||
@@ -19,16 +20,16 @@ import (
|
||||
func (s *WriteService) Update(id string, input CreateProductInput) (*productdomain.Product, error) {
|
||||
priceAmount := input.PriceAmount.Round(2)
|
||||
if len(input.SKUs) == 0 && priceAmount.LessThanOrEqual(decimal.Zero) {
|
||||
return nil, s.errors.ProductPriceInvalid
|
||||
return nil, productcontract.ErrProductPriceInvalid
|
||||
}
|
||||
product, err := s.products.GetByID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if product == nil {
|
||||
return nil, s.errors.NotFound
|
||||
return nil, productcontract.ErrNotFound
|
||||
}
|
||||
if err := productdomain.ValidateCategoryAssignment(s.categories, input.CategoryID, product.CategoryID, s.errors.ProductCategoryInvalid); err != nil {
|
||||
if err := productdomain.ValidateCategoryAssignment(s.categories, input.CategoryID, product.CategoryID, productcontract.ErrProductCategoryInvalid); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -37,7 +38,7 @@ func (s *WriteService) Update(id string, input CreateProductInput) (*productdoma
|
||||
return nil, err
|
||||
}
|
||||
if count > 0 {
|
||||
return nil, s.errors.SlugExists
|
||||
return nil, productcontract.ErrSlugExists
|
||||
}
|
||||
|
||||
product.CategoryID = input.CategoryID
|
||||
@@ -70,7 +71,7 @@ func (s *WriteService) Update(id string, input CreateProductInput) (*productdoma
|
||||
}
|
||||
purchaseType := productdomain.NormalizePurchaseType(rawPurchaseType)
|
||||
if purchaseType == "" {
|
||||
return nil, s.errors.ProductPurchaseInvalid
|
||||
return nil, productcontract.ErrProductPurchaseInvalid
|
||||
}
|
||||
product.PurchaseType = purchaseType
|
||||
if input.MaxPurchaseQuantity != nil {
|
||||
@@ -80,11 +81,11 @@ func (s *WriteService) Update(id string, input CreateProductInput) (*productdoma
|
||||
product.MinPurchaseQuantity = productdomain.NormalizePurchaseQuantityLimit(*input.MinPurchaseQuantity)
|
||||
}
|
||||
if product.MinPurchaseQuantity > 0 && product.MaxPurchaseQuantity > 0 && product.MinPurchaseQuantity > product.MaxPurchaseQuantity {
|
||||
return nil, s.errors.ProductPurchaseLimitInvalid
|
||||
return nil, productcontract.ErrProductPurchaseLimitInvalid
|
||||
}
|
||||
stockDisplayMode := productdomain.NormalizeStockDisplayMode(input.StockDisplayMode)
|
||||
if stockDisplayMode == "" {
|
||||
return nil, s.errors.ProductStockDisplayInvalid
|
||||
return nil, productcontract.ErrProductStockDisplayInvalid
|
||||
}
|
||||
product.StockDisplayMode = stockDisplayMode
|
||||
rawFulfillmentType := strings.TrimSpace(input.FulfillmentType)
|
||||
@@ -93,7 +94,7 @@ func (s *WriteService) Update(id string, input CreateProductInput) (*productdoma
|
||||
}
|
||||
fulfillmentType := productdomain.NormalizeFulfillmentType(rawFulfillmentType)
|
||||
if fulfillmentType == "" {
|
||||
return nil, s.errors.FulfillmentInvalid
|
||||
return nil, productcontract.ErrFulfillmentInvalid
|
||||
}
|
||||
// 对接商品的真实交付类型必须保持 upstream,后台返回的 auto/manual 仅用于展示。
|
||||
if product.IsMapped {
|
||||
@@ -113,13 +114,13 @@ func (s *WriteService) Update(id string, input CreateProductInput) (*productdoma
|
||||
manualStockTotal = *input.ManualStockTotal
|
||||
}
|
||||
if manualStockTotal < constants.ManualStockUnlimited {
|
||||
return nil, s.errors.ManualStockInvalid
|
||||
return nil, productcontract.ErrManualStockInvalid
|
||||
}
|
||||
|
||||
var normalizedSKUs []normalizedProductSKU
|
||||
if len(input.SKUs) > 0 {
|
||||
if s.skus == nil {
|
||||
return nil, s.errors.ProductSKUInvalid
|
||||
return nil, productcontract.ErrProductSKUInvalid
|
||||
}
|
||||
existingSKUs, listErr := s.skus.ListByProduct(product.ID, false)
|
||||
if listErr != nil {
|
||||
|
||||
@@ -132,7 +132,7 @@ func TestProductServiceCreateRollsBackProductAndSKUWhenWholesaleValidationFails(
|
||||
PriceAmount: decimal.NewFromInt(10),
|
||||
PurchaseType: constants.ProductPurchaseMember,
|
||||
FulfillmentType: constants.FulfillmentTypeAuto,
|
||||
WholesalePrices: &[]productwrite.WholesalePriceInput{{
|
||||
WholesalePrices: &[]productdomain.WholesalePriceInput{{
|
||||
MinQuantity: 0,
|
||||
UnitPrice: decimal.NewFromInt(8),
|
||||
}},
|
||||
|
||||
+22
-22
@@ -18,7 +18,7 @@ import (
|
||||
)
|
||||
|
||||
func TestNormalizeWholesalePriceInputsSortsTiers(t *testing.T) {
|
||||
tiers, err := productdomain.NormalizeWholesalePrices([]productwrite.WholesalePriceInput{
|
||||
tiers, err := productdomain.NormalizeWholesalePrices([]productdomain.WholesalePriceInput{
|
||||
{MinQuantity: 10, UnitPrice: decimal.NewFromInt(70)},
|
||||
{MinQuantity: 5, UnitPrice: decimal.NewFromInt(80)},
|
||||
})
|
||||
@@ -39,33 +39,33 @@ func TestNormalizeWholesalePriceInputsSortsTiers(t *testing.T) {
|
||||
func TestNormalizeWholesalePriceInputsRejectsInvalidTiers(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
inputs []productwrite.WholesalePriceInput
|
||||
inputs []productdomain.WholesalePriceInput
|
||||
}{
|
||||
{
|
||||
name: "zero quantity",
|
||||
inputs: []productwrite.WholesalePriceInput{{MinQuantity: 0, UnitPrice: decimal.NewFromInt(80)}},
|
||||
inputs: []productdomain.WholesalePriceInput{{MinQuantity: 0, UnitPrice: decimal.NewFromInt(80)}},
|
||||
},
|
||||
{
|
||||
name: "zero price",
|
||||
inputs: []productwrite.WholesalePriceInput{{MinQuantity: 5, UnitPrice: decimal.Zero}},
|
||||
inputs: []productdomain.WholesalePriceInput{{MinQuantity: 5, UnitPrice: decimal.Zero}},
|
||||
},
|
||||
{
|
||||
name: "duplicate quantity",
|
||||
inputs: []productwrite.WholesalePriceInput{
|
||||
inputs: []productdomain.WholesalePriceInput{
|
||||
{MinQuantity: 5, UnitPrice: decimal.NewFromInt(80)},
|
||||
{MinQuantity: 5, UnitPrice: decimal.NewFromInt(70)},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "higher tier more expensive",
|
||||
inputs: []productwrite.WholesalePriceInput{
|
||||
inputs: []productdomain.WholesalePriceInput{
|
||||
{MinQuantity: 5, UnitPrice: decimal.NewFromInt(80)},
|
||||
{MinQuantity: 10, UnitPrice: decimal.NewFromInt(90)},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "higher tier equal price",
|
||||
inputs: []productwrite.WholesalePriceInput{
|
||||
inputs: []productdomain.WholesalePriceInput{
|
||||
{MinQuantity: 5, UnitPrice: decimal.NewFromInt(80)},
|
||||
{MinQuantity: 10, UnitPrice: decimal.NewFromInt(80)},
|
||||
},
|
||||
@@ -83,7 +83,7 @@ func TestNormalizeWholesalePriceInputsRejectsInvalidTiers(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNormalizeWholesalePriceInputsRejectsDuplicateCanonicalSKUScope(t *testing.T) {
|
||||
_, err := productdomain.NormalizeWholesalePrices([]productwrite.WholesalePriceInput{
|
||||
_, err := productdomain.NormalizeWholesalePrices([]productdomain.WholesalePriceInput{
|
||||
{SKUID: 5, SKUCode: "SKU-A", MinQuantity: 10, UnitPrice: decimal.NewFromInt(80)},
|
||||
{SKUCode: "SKU-A", MinQuantity: 10, UnitPrice: decimal.NewFromInt(70)},
|
||||
})
|
||||
@@ -93,7 +93,7 @@ func TestNormalizeWholesalePriceInputsRejectsDuplicateCanonicalSKUScope(t *testi
|
||||
}
|
||||
|
||||
func TestNormalizeWholesalePriceInputsRejectsNonDecreasingCanonicalSKUScope(t *testing.T) {
|
||||
_, err := productdomain.NormalizeWholesalePrices([]productwrite.WholesalePriceInput{
|
||||
_, err := productdomain.NormalizeWholesalePrices([]productdomain.WholesalePriceInput{
|
||||
{SKUID: 5, SKUCode: "SKU-A", MinQuantity: 5, UnitPrice: decimal.NewFromInt(80)},
|
||||
{SKUCode: "SKU-A", MinQuantity: 10, UnitPrice: decimal.NewFromInt(90)},
|
||||
})
|
||||
@@ -103,7 +103,7 @@ func TestNormalizeWholesalePriceInputsRejectsNonDecreasingCanonicalSKUScope(t *t
|
||||
}
|
||||
|
||||
func TestNormalizeWholesalePriceInputsAllowsSameQuantityForDifferentSKUs(t *testing.T) {
|
||||
tiers, err := productdomain.NormalizeWholesalePrices([]productwrite.WholesalePriceInput{
|
||||
tiers, err := productdomain.NormalizeWholesalePrices([]productdomain.WholesalePriceInput{
|
||||
{SKUID: 2, SKUCode: "SKU-B", MinQuantity: 5, UnitPrice: decimal.NewFromInt(70)},
|
||||
{SKUID: 1, SKUCode: "SKU-A", MinQuantity: 5, UnitPrice: decimal.NewFromInt(80)},
|
||||
{MinQuantity: 5, UnitPrice: decimal.NewFromInt(90)},
|
||||
@@ -292,7 +292,7 @@ func TestProductServiceUpdateWholesalePricesOptionalSemantics(t *testing.T) {
|
||||
PriceAmount: decimal.NewFromInt(100),
|
||||
PurchaseType: constants.ProductPurchaseMember,
|
||||
FulfillmentType: constants.FulfillmentTypeAuto,
|
||||
WholesalePrices: &[]productwrite.WholesalePriceInput{
|
||||
WholesalePrices: &[]productdomain.WholesalePriceInput{
|
||||
{MinQuantity: 5, UnitPrice: decimal.NewFromInt(80)},
|
||||
},
|
||||
IsActive: boolPtr(true),
|
||||
@@ -330,7 +330,7 @@ func TestProductServiceUpdateWholesalePricesOptionalSemantics(t *testing.T) {
|
||||
|
||||
// 传入空切片:显式清空。
|
||||
clear := baseUpdate()
|
||||
clear.WholesalePrices = &[]productwrite.WholesalePriceInput{}
|
||||
clear.WholesalePrices = &[]productdomain.WholesalePriceInput{}
|
||||
cleared, err := svc.Write.Update(idStr, clear)
|
||||
if err != nil {
|
||||
t.Fatalf("update with empty wholesale prices failed: %v", err)
|
||||
@@ -368,7 +368,7 @@ func TestProductServiceUpdateWholesalePricesOnlyTouchesWholesaleField(t *testing
|
||||
t.Fatalf("create product failed: %v", err)
|
||||
}
|
||||
|
||||
updated, err := svc.Admin.UpdateWholesalePrices(strconv.FormatUint(uint64(product.ID), 10), []productwrite.WholesalePriceInput{
|
||||
updated, err := svc.Admin.UpdateWholesalePrices(strconv.FormatUint(uint64(product.ID), 10), []productdomain.WholesalePriceInput{
|
||||
{MinQuantity: 10, UnitPrice: decimal.RequireFromString("70.00")},
|
||||
{MinQuantity: 5, UnitPrice: decimal.RequireFromString("80.00")},
|
||||
})
|
||||
@@ -414,7 +414,7 @@ func TestProductServiceUpdateWholesalePricesClearsTiers(t *testing.T) {
|
||||
t.Fatalf("create product failed: %v", err)
|
||||
}
|
||||
|
||||
updated, err := svc.Admin.UpdateWholesalePrices(strconv.FormatUint(uint64(product.ID), 10), []productwrite.WholesalePriceInput{})
|
||||
updated, err := svc.Admin.UpdateWholesalePrices(strconv.FormatUint(uint64(product.ID), 10), []productdomain.WholesalePriceInput{})
|
||||
if err != nil {
|
||||
t.Fatalf("clear wholesale prices failed: %v", err)
|
||||
}
|
||||
@@ -442,11 +442,11 @@ func TestProductServiceUpdateWholesalePricesRejectsInvalidInputs(t *testing.T) {
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
inputs []productwrite.WholesalePriceInput
|
||||
inputs []productdomain.WholesalePriceInput
|
||||
}{
|
||||
{name: "zero quantity", inputs: []productwrite.WholesalePriceInput{{MinQuantity: 0, UnitPrice: decimal.NewFromInt(80)}}},
|
||||
{name: "zero price", inputs: []productwrite.WholesalePriceInput{{MinQuantity: 5, UnitPrice: decimal.Zero}}},
|
||||
{name: "duplicate quantity", inputs: []productwrite.WholesalePriceInput{
|
||||
{name: "zero quantity", inputs: []productdomain.WholesalePriceInput{{MinQuantity: 0, UnitPrice: decimal.NewFromInt(80)}}},
|
||||
{name: "zero price", inputs: []productdomain.WholesalePriceInput{{MinQuantity: 5, UnitPrice: decimal.Zero}}},
|
||||
{name: "duplicate quantity", inputs: []productdomain.WholesalePriceInput{
|
||||
{MinQuantity: 5, UnitPrice: decimal.NewFromInt(80)},
|
||||
{MinQuantity: 5, UnitPrice: decimal.NewFromInt(70)},
|
||||
}},
|
||||
@@ -516,18 +516,18 @@ func TestProductServiceUpdateWholesalePricesValidatesSKUBelonging(t *testing.T)
|
||||
}
|
||||
|
||||
idStr := strconv.FormatUint(uint64(product.ID), 10)
|
||||
if _, err := svc.Admin.UpdateWholesalePrices(idStr, []productwrite.WholesalePriceInput{
|
||||
if _, err := svc.Admin.UpdateWholesalePrices(idStr, []productdomain.WholesalePriceInput{
|
||||
{SKUID: foreignSKU.ID, MinQuantity: 5, UnitPrice: decimal.NewFromInt(80)},
|
||||
}); !errors.Is(err, productdomain.ErrWholesalePriceInvalid) {
|
||||
t.Fatalf("expected foreign sku_id to be rejected, got %v", err)
|
||||
}
|
||||
if _, err := svc.Admin.UpdateWholesalePrices(idStr, []productwrite.WholesalePriceInput{
|
||||
if _, err := svc.Admin.UpdateWholesalePrices(idStr, []productdomain.WholesalePriceInput{
|
||||
{SKUID: skuA.ID, SKUCode: "SKU-X", MinQuantity: 5, UnitPrice: decimal.NewFromInt(80)},
|
||||
}); !errors.Is(err, productdomain.ErrWholesalePriceInvalid) {
|
||||
t.Fatalf("expected sku_id/sku_code mismatch to be rejected, got %v", err)
|
||||
}
|
||||
|
||||
updated, err := svc.Admin.UpdateWholesalePrices(idStr, []productwrite.WholesalePriceInput{
|
||||
updated, err := svc.Admin.UpdateWholesalePrices(idStr, []productdomain.WholesalePriceInput{
|
||||
{SKUCode: "SKU-A", MinQuantity: 5, UnitPrice: decimal.NewFromInt(80)},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -544,7 +544,7 @@ func TestProductServiceUpdateWholesalePricesValidatesSKUBelonging(t *testing.T)
|
||||
func TestProductServiceUpdateWholesalePricesReturnsNotFound(t *testing.T) {
|
||||
svc, _ := newProductServiceForTest(t)
|
||||
|
||||
_, err := svc.Admin.UpdateWholesalePrices("999999", []productwrite.WholesalePriceInput{
|
||||
_, err := svc.Admin.UpdateWholesalePrices("999999", []productdomain.WholesalePriceInput{
|
||||
{MinQuantity: 5, UnitPrice: decimal.NewFromInt(80)},
|
||||
})
|
||||
if !errors.Is(err, productcontract.ErrNotFound) {
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
|
||||
mappingdomain "github.com/dujiao-next/internal/modules/catalog/mapping/domain"
|
||||
|
||||
productcontract "github.com/dujiao-next/internal/modules/catalog/product/contract"
|
||||
productdomain "github.com/dujiao-next/internal/modules/catalog/product/domain"
|
||||
|
||||
categorydomain "github.com/dujiao-next/internal/modules/catalog/category/domain"
|
||||
@@ -152,11 +151,9 @@ func setupAdminProductHandlerTest(t *testing.T) (*producthttp.AdminProductHandle
|
||||
|
||||
facade := &productHandlerFacade{
|
||||
Service: productapplication.NewService(productapplication.Options{
|
||||
Products: productStore,
|
||||
Categories: categoryStore,
|
||||
Stock: cardSecretStore,
|
||||
NotFoundError: productcontract.ErrNotFound,
|
||||
ResellerProductNotListedError: productcontract.ErrResellerProductNotListed,
|
||||
Products: productStore,
|
||||
Categories: categoryStore,
|
||||
Stock: cardSecretStore,
|
||||
}),
|
||||
AdminService: productadmin.NewAdminService(productadmin.Options{
|
||||
Products: productStore,
|
||||
@@ -172,12 +169,6 @@ func setupAdminProductHandlerTest(t *testing.T) (*producthttp.AdminProductHandle
|
||||
carts: cartStore,
|
||||
productMappings: mappingStore,
|
||||
},
|
||||
Errors: productadmin.ErrorSet{
|
||||
NotFound: productcontract.ErrNotFound,
|
||||
ProductCategoryInvalid: productcontract.ErrProductCategoryInvalid,
|
||||
ProductHasStock: productcontract.ErrProductHasStock,
|
||||
ProductHasOrderRecord: productcontract.ErrProductHasOrderRecord,
|
||||
},
|
||||
}),
|
||||
WriteService: productwrite.NewWriteService(productwrite.Options{
|
||||
Products: productStore,
|
||||
@@ -189,19 +180,6 @@ func setupAdminProductHandlerTest(t *testing.T) (*producthttp.AdminProductHandle
|
||||
skus: skuStore,
|
||||
cardSecrets: cardSecretStore,
|
||||
},
|
||||
Errors: productwrite.ErrorSet{
|
||||
NotFound: productcontract.ErrNotFound,
|
||||
SlugExists: productcontract.ErrSlugExists,
|
||||
ProductCategoryInvalid: productcontract.ErrProductCategoryInvalid,
|
||||
ProductPurchaseInvalid: productcontract.ErrProductPurchaseInvalid,
|
||||
FulfillmentInvalid: productcontract.ErrFulfillmentInvalid,
|
||||
ProductPriceInvalid: productcontract.ErrProductPriceInvalid,
|
||||
ManualStockInvalid: productcontract.ErrManualStockInvalid,
|
||||
ProductPurchaseLimitInvalid: productcontract.ErrProductPurchaseLimitInvalid,
|
||||
ProductStockDisplayInvalid: productcontract.ErrProductStockDisplayInvalid,
|
||||
ProductSKUInvalid: productcontract.ErrProductSKUInvalid,
|
||||
ProductSKUHasCardSecretStock: productcontract.ErrProductSKUHasCardSecretStock,
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ type ProductWriter interface {
|
||||
|
||||
// ProductAdminCommands 是商品管理与删除用例。
|
||||
type ProductAdminCommands interface {
|
||||
UpdateWholesalePrices(id string, prices []productwrite.WholesalePriceInput) (*productdomain.Product, error)
|
||||
UpdateWholesalePrices(id string, prices []productdomain.WholesalePriceInput) (*productdomain.Product, error)
|
||||
QuickUpdate(id string, fields map[string]interface{}) (*productdomain.Product, error)
|
||||
Delete(id string) error
|
||||
}
|
||||
@@ -233,13 +233,13 @@ type CreateProductRequest struct {
|
||||
|
||||
// toWholesalePriceInputs 透传「是否提供」语义:请求未携带 wholesale_prices 时返回 nil
|
||||
// (Update 保留原配置),携带(含空数组)时返回非 nil 指针以整体覆盖。
|
||||
func toWholesalePriceInputs(items *[]WholesalePriceRequest) *[]productwrite.WholesalePriceInput {
|
||||
func toWholesalePriceInputs(items *[]WholesalePriceRequest) *[]productdomain.WholesalePriceInput {
|
||||
if items == nil {
|
||||
return nil
|
||||
}
|
||||
result := make([]productwrite.WholesalePriceInput, 0, len(*items))
|
||||
result := make([]productdomain.WholesalePriceInput, 0, len(*items))
|
||||
for _, item := range *items {
|
||||
result = append(result, productwrite.WholesalePriceInput{
|
||||
result = append(result, productdomain.WholesalePriceInput{
|
||||
SKUID: item.SKUID,
|
||||
SKUCode: strings.TrimSpace(item.SKUCode),
|
||||
MinQuantity: item.MinQuantity,
|
||||
|
||||
@@ -22,7 +22,7 @@ type Repository interface {
|
||||
GetTotalUserBalance() (float64, error)
|
||||
}
|
||||
|
||||
// SettingReader keeps the dashboard core independent from the legacy setting service.
|
||||
// SettingReader keeps the dashboard core independent from settings persistence.
|
||||
type SettingReader interface {
|
||||
GetDashboardSetting() (settingsstorefront.DashboardSetting, error)
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ import (
|
||||
|
||||
"github.com/dujiao-next/internal/logger"
|
||||
"github.com/dujiao-next/internal/modules/notification/application/format"
|
||||
"github.com/dujiao-next/internal/modules/notification/contract"
|
||||
settingsmessaging "github.com/dujiao-next/internal/modules/settings/schema/messaging"
|
||||
"github.com/dujiao-next/internal/queue"
|
||||
)
|
||||
|
||||
func (s *Service) dispatchExceptionAlertCheck(ctx context.Context, setting contract.NotificationCenterSetting, payload queue.NotificationDispatchPayload) error {
|
||||
func (s *Service) dispatchExceptionAlertCheck(ctx context.Context, setting settingsmessaging.NotificationCenterSetting, payload queue.NotificationDispatchPayload) error {
|
||||
if s.dashboardSvc == nil || s.settingService == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/dujiao-next/internal/constants"
|
||||
"github.com/dujiao-next/internal/modules/notification/contract"
|
||||
settingsstorefront "github.com/dujiao-next/internal/modules/settings/schema/storefront"
|
||||
)
|
||||
|
||||
func thresholdValueByAlertType(setting contract.DashboardAlertSetting, alertType string) int64 {
|
||||
func thresholdValueByAlertType(setting settingsstorefront.DashboardAlertSetting, alertType string) int64 {
|
||||
switch alertType {
|
||||
case constants.NotificationAlertTypeOutOfStockProducts:
|
||||
return setting.OutOfStockProductsThreshold
|
||||
|
||||
@@ -9,13 +9,14 @@ import (
|
||||
|
||||
"github.com/dujiao-next/internal/constants"
|
||||
dashboard "github.com/dujiao-next/internal/modules/dashboard/contract"
|
||||
"github.com/dujiao-next/internal/modules/notification/contract"
|
||||
settingsmessaging "github.com/dujiao-next/internal/modules/settings/schema/messaging"
|
||||
settingsstorefront "github.com/dujiao-next/internal/modules/settings/schema/storefront"
|
||||
"github.com/dujiao-next/internal/queue"
|
||||
)
|
||||
|
||||
func BuildInventoryAlertDispatchPayloads(
|
||||
setting contract.NotificationCenterSetting,
|
||||
dashboardSetting contract.DashboardSetting,
|
||||
setting settingsmessaging.NotificationCenterSetting,
|
||||
dashboardSetting settingsstorefront.DashboardSetting,
|
||||
payload queue.NotificationDispatchPayload,
|
||||
rows []dashboard.InventoryAlertRow,
|
||||
) []queue.NotificationDispatchPayload {
|
||||
|
||||
@@ -6,14 +6,15 @@ import (
|
||||
|
||||
"github.com/dujiao-next/internal/constants"
|
||||
dashboard "github.com/dujiao-next/internal/modules/dashboard/contract"
|
||||
"github.com/dujiao-next/internal/modules/notification/contract"
|
||||
settingsmessaging "github.com/dujiao-next/internal/modules/settings/schema/messaging"
|
||||
settingsstorefront "github.com/dujiao-next/internal/modules/settings/schema/storefront"
|
||||
"github.com/dujiao-next/internal/queue"
|
||||
)
|
||||
|
||||
// BuildPaymentOrderAlertDispatchPayloads 构建支付订单告警通知载荷。
|
||||
func BuildPaymentOrderAlertDispatchPayloads(
|
||||
setting contract.NotificationCenterSetting,
|
||||
dashboardSetting contract.DashboardSetting,
|
||||
setting settingsmessaging.NotificationCenterSetting,
|
||||
dashboardSetting settingsstorefront.DashboardSetting,
|
||||
payload queue.NotificationDispatchPayload,
|
||||
counts dashboard.PaymentOrderAlertCountsRow,
|
||||
) []queue.NotificationDispatchPayload {
|
||||
@@ -41,7 +42,7 @@ func BuildPaymentOrderAlertDispatchPayloads(
|
||||
|
||||
// buildPaymentOrderAlertDispatchPayload 构建单个支付订单告警通知载荷
|
||||
func buildPaymentOrderAlertDispatchPayload(
|
||||
setting contract.NotificationCenterSetting,
|
||||
setting settingsmessaging.NotificationCenterSetting,
|
||||
payload queue.NotificationDispatchPayload,
|
||||
alertType string,
|
||||
value int64,
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/dujiao-next/internal/logger"
|
||||
"github.com/dujiao-next/internal/modules/notification/application/format"
|
||||
"github.com/dujiao-next/internal/modules/notification/contract"
|
||||
settingsmessaging "github.com/dujiao-next/internal/modules/settings/schema/messaging"
|
||||
"github.com/dujiao-next/internal/queue"
|
||||
"github.com/dujiao-next/internal/shared/jsonmap"
|
||||
"github.com/dujiao-next/internal/shared/outboundctx"
|
||||
@@ -97,7 +98,7 @@ func (s *Service) SendTest(ctx context.Context, input contract.TestSendInput) er
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) dispatchSingleEvent(ctx context.Context, setting contract.NotificationCenterSetting, payload queue.NotificationDispatchPayload) error {
|
||||
func (s *Service) dispatchSingleEvent(ctx context.Context, setting settingsmessaging.NotificationCenterSetting, payload queue.NotificationDispatchPayload) error {
|
||||
if !payload.Force {
|
||||
ok, err := acquireNotificationDedupe(ctx, setting.DedupeTTLSeconds, payload)
|
||||
if err != nil {
|
||||
|
||||
@@ -13,10 +13,6 @@ import (
|
||||
"github.com/dujiao-next/internal/shared/money"
|
||||
)
|
||||
|
||||
type NotificationCenterSetting = settingsmessaging.NotificationCenterSetting
|
||||
type DashboardSetting = settingsstorefront.DashboardSetting
|
||||
type DashboardAlertSetting = settingsstorefront.DashboardAlertSetting
|
||||
|
||||
type SettingsReader interface {
|
||||
GetNotificationCenterSetting() (settingsmessaging.NotificationCenterSetting, error)
|
||||
GetDashboardSetting() (settingsstorefront.DashboardSetting, error)
|
||||
|
||||
@@ -264,7 +264,7 @@ func (s *PaymentService) handleWebhookByCandidateIteration(
|
||||
func (s *PaymentService) tryParseWebhookWithChannel(
|
||||
channel *paymentdomain.PaymentChannel,
|
||||
input WebhookCallbackInput,
|
||||
) (*paymentcontract.GatewayWebhookResult, error) {
|
||||
) (*paymentcontract.GatewayCallbackResult, error) {
|
||||
if s.paymentProviderRegistry == nil {
|
||||
return nil, ErrPaymentProviderNotSupported
|
||||
}
|
||||
@@ -291,7 +291,7 @@ func (s *PaymentService) tryParseWebhookWithChannel(
|
||||
// 反查 payment 并落库。任何错误都是真实的业务/DB 错误,不再 retry 其他 channel。
|
||||
func (s *PaymentService) commitVerifiedWebhook(
|
||||
channel *paymentdomain.PaymentChannel,
|
||||
result *paymentcontract.GatewayWebhookResult,
|
||||
result *paymentcontract.GatewayCallbackResult,
|
||||
log *zap.SugaredLogger,
|
||||
) (*paymentdomain.Payment, string, error) {
|
||||
log.Infow("payment_webhook_parsed",
|
||||
@@ -387,7 +387,7 @@ func supportsBlindWebhookCandidateMatching(channelType string) bool {
|
||||
|
||||
// findWebhookPayment 通过 webhook result 反查 payment。
|
||||
// 优先用 OrderNo(= GatewayOrderNo,商户单号),次选 ProviderRef(网关流水号)。
|
||||
func (s *PaymentService) findWebhookPayment(channelID uint, result *paymentcontract.GatewayWebhookResult) (*paymentdomain.Payment, error) {
|
||||
func (s *PaymentService) findWebhookPayment(channelID uint, result *paymentcontract.GatewayCallbackResult) (*paymentdomain.Payment, error) {
|
||||
if result == nil {
|
||||
return nil, ErrPaymentNotFound
|
||||
}
|
||||
|
||||
@@ -56,8 +56,6 @@ type GatewayCallbackResult struct {
|
||||
Payload jsonmap.JSON
|
||||
}
|
||||
|
||||
type GatewayWebhookResult = GatewayCallbackResult
|
||||
|
||||
// GatewayProvider 是所有支付网关适配器必须实现的最小能力。
|
||||
type GatewayProvider interface {
|
||||
Type() string
|
||||
@@ -72,7 +70,7 @@ type GatewayCapturer interface {
|
||||
|
||||
type GatewayWebhooker interface {
|
||||
GatewayProvider
|
||||
ParseWebhook(ctx context.Context, cfg jsonmap.JSON, headers map[string]string, body []byte, now time.Time) (*GatewayWebhookResult, error)
|
||||
ParseWebhook(ctx context.Context, cfg jsonmap.JSON, headers map[string]string, body []byte, now time.Time) (*GatewayCallbackResult, error)
|
||||
}
|
||||
|
||||
type GatewayCallbackVerifier interface {
|
||||
|
||||
@@ -166,7 +166,7 @@ func (a *dujiaoPayAdapter) CreatePayment(ctx context.Context, raw jsonmap.JSON,
|
||||
}
|
||||
|
||||
// ParseWebhook 验签并解析 DujiaoPay webhook。
|
||||
func (a *dujiaoPayAdapter) ParseWebhook(_ context.Context, raw jsonmap.JSON, headers map[string]string, body []byte, now time.Time) (*paymentcontract.GatewayWebhookResult, error) {
|
||||
func (a *dujiaoPayAdapter) ParseWebhook(_ context.Context, raw jsonmap.JSON, headers map[string]string, body []byte, now time.Time) (*paymentcontract.GatewayCallbackResult, error) {
|
||||
cfg, err := dujiaopay.ParseConfig(raw)
|
||||
if err != nil {
|
||||
return nil, mapDujiaoPayError(err)
|
||||
@@ -184,7 +184,7 @@ func (a *dujiaoPayAdapter) ParseWebhook(_ context.Context, raw jsonmap.JSON, hea
|
||||
payload["tx_hash"] = event.TxHash
|
||||
}
|
||||
|
||||
return &paymentcontract.GatewayWebhookResult{
|
||||
return &paymentcontract.GatewayCallbackResult{
|
||||
OrderNo: event.MerchantOrderID,
|
||||
ProviderRef: event.OrderID,
|
||||
Status: event.Status,
|
||||
|
||||
@@ -158,7 +158,7 @@ func (a *paypalAdapter) QueryPayment(ctx context.Context, raw jsonmap.JSON, prov
|
||||
}
|
||||
|
||||
// ParseWebhook 合并 paypal 的 VerifyWebhookSignature + ParseWebhookEvent 两步(stripe 是一步)。
|
||||
func (a *paypalAdapter) ParseWebhook(ctx context.Context, raw jsonmap.JSON, headers map[string]string, body []byte, _ time.Time) (*paymentcontract.GatewayWebhookResult, error) {
|
||||
func (a *paypalAdapter) ParseWebhook(ctx context.Context, raw jsonmap.JSON, headers map[string]string, body []byte, _ time.Time) (*paymentcontract.GatewayCallbackResult, error) {
|
||||
cfg, err := a.parseConfig(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -195,7 +195,7 @@ func (a *paypalAdapter) ParseWebhook(ctx context.Context, raw jsonmap.JSON, head
|
||||
}
|
||||
}
|
||||
|
||||
return &paymentcontract.GatewayWebhookResult{
|
||||
return &paymentcontract.GatewayCallbackResult{
|
||||
OrderNo: parsed.RelatedInvoiceID(),
|
||||
ProviderRef: parsed.RelatedOrderID(),
|
||||
Status: status,
|
||||
|
||||
@@ -162,7 +162,7 @@ func (a *stripeAdapter) QueryPayment(ctx context.Context, raw jsonmap.JSON, prov
|
||||
}
|
||||
|
||||
// ParseWebhook 验签并解析 webhook(实现 paymentcontract.GatewayWebhooker)。
|
||||
func (a *stripeAdapter) ParseWebhook(_ context.Context, raw jsonmap.JSON, headers map[string]string, body []byte, now time.Time) (*paymentcontract.GatewayWebhookResult, error) {
|
||||
func (a *stripeAdapter) ParseWebhook(_ context.Context, raw jsonmap.JSON, headers map[string]string, body []byte, now time.Time) (*paymentcontract.GatewayCallbackResult, error) {
|
||||
cfg, err := a.parseConfig(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -182,7 +182,7 @@ func (a *stripeAdapter) ParseWebhook(_ context.Context, raw jsonmap.JSON, header
|
||||
}
|
||||
}
|
||||
|
||||
return &paymentcontract.GatewayWebhookResult{
|
||||
return &paymentcontract.GatewayCallbackResult{
|
||||
OrderNo: result.OrderNo,
|
||||
ProviderRef: gatewaycommon.PickFirstNonEmpty(result.ProviderRef, result.SessionID, result.PaymentIntentID),
|
||||
Status: result.Status,
|
||||
|
||||
@@ -177,7 +177,7 @@ func (a *wechatpayAdapter) QueryPayment(ctx context.Context, raw jsonmap.JSON, p
|
||||
}
|
||||
|
||||
// ParseWebhook 验签并解析 webhook(实现 paymentcontract.GatewayWebhooker)。
|
||||
func (a *wechatpayAdapter) ParseWebhook(ctx context.Context, raw jsonmap.JSON, headers map[string]string, body []byte, _ time.Time) (*paymentcontract.GatewayWebhookResult, error) {
|
||||
func (a *wechatpayAdapter) ParseWebhook(ctx context.Context, raw jsonmap.JSON, headers map[string]string, body []byte, _ time.Time) (*paymentcontract.GatewayCallbackResult, error) {
|
||||
cfg, err := a.parseConfig(raw, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -197,7 +197,7 @@ func (a *wechatpayAdapter) ParseWebhook(ctx context.Context, raw jsonmap.JSON, h
|
||||
}
|
||||
}
|
||||
|
||||
return &paymentcontract.GatewayWebhookResult{
|
||||
return &paymentcontract.GatewayCallbackResult{
|
||||
OrderNo: result.OrderNo,
|
||||
ProviderRef: result.TransactionID,
|
||||
Status: result.Status,
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
|
||||
// Lifecycle owns the procurement-side persistence operations that span
|
||||
// fulfillments and parent/child order status. It deliberately uses private
|
||||
// records instead of the legacy repository package.
|
||||
// records through the procurement-owned store.
|
||||
type Lifecycle struct {
|
||||
db *gorm.DB
|
||||
queue StatusEmailQueue
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"time"
|
||||
|
||||
walletdomain "github.com/dujiao-next/internal/modules/wallet/domain"
|
||||
"github.com/dujiao-next/internal/shared/money"
|
||||
)
|
||||
|
||||
// Repository owns all wallet aggregate persistence. Transactional callers
|
||||
@@ -49,14 +50,14 @@ type UseCase interface {
|
||||
StatsUserRechargeOrders(userID uint, rechargeNo string) (map[string]int64, error)
|
||||
GetRechargeOrderByRechargeNo(userID uint, rechargeNo string) (*walletdomain.RechargeOrder, error)
|
||||
GetRechargeOrderByPaymentIDAndUser(paymentID, userID uint) (*walletdomain.RechargeOrder, error)
|
||||
GetBalancesByUserIDs(userIDs []uint) (map[uint]Balance, error)
|
||||
GetBalancesByUserIDs(userIDs []uint) (map[uint]money.Amount, error)
|
||||
|
||||
Recharge(input RechargeInput) (*walletdomain.Account, *walletdomain.Transaction, error)
|
||||
AdminAdjustBalance(input AdjustBalanceInput) (*walletdomain.Account, *walletdomain.Transaction, error)
|
||||
CreditInTransaction(tx Transaction, input CreditInput) (*walletdomain.Account, *walletdomain.Transaction, error)
|
||||
ApplyRechargePayment(tx Transaction, recharge *walletdomain.RechargeOrder) (*walletdomain.Transaction, error)
|
||||
ApplyOrderBalance(tx Transaction, input OrderBalanceInput) (Balance, error)
|
||||
ReleaseOrderBalance(tx Transaction, input OrderReleaseInput, claim ReleaseClaim) (Balance, error)
|
||||
ApplyOrderBalance(tx Transaction, input OrderBalanceInput) (money.Amount, error)
|
||||
ReleaseOrderBalance(tx Transaction, input OrderReleaseInput, claim ReleaseClaim) (money.Amount, error)
|
||||
}
|
||||
|
||||
// ReleaseClaim atomically clears the order-side wallet allocation before the
|
||||
|
||||
@@ -6,8 +6,6 @@ import (
|
||||
"github.com/dujiao-next/internal/shared/money"
|
||||
)
|
||||
|
||||
type Balance = money.Amount
|
||||
|
||||
type AccountListFilter struct {
|
||||
Page int
|
||||
PageSize int
|
||||
|
||||
Reference in new issue
Block a user