fix: 上游商品增量列表纳入 SKU 库存与卡密变化,下游增量同步能及时感知库存

This commit is contained in:
dujiao-next committed 2026-10-11 12:32:47 +08:00
1 parent 0c045b201a
commit fbfdc008d1
2 files changed
+49 -1

No files matched your search

@@ -94,7 +94,13 @@ func (r *ProductStore) List(filter productcontract.ListFilter) ([]productdomain.
}
if filter.UpdatedAfter != nil {
query = query.Where("updated_at > ?", *filter.UpdatedAfter)
// 库存变化落在 SKU(人工库存预占/售出)与卡密行上,不会改动商品行本身;
// 下游增量同步依赖此过滤,需一并纳入,否则库存变化要等每日全量同步才可见。
after := *filter.UpdatedAfter
query = query.Where("(products.updated_at > ? OR "+
"EXISTS (SELECT 1 FROM product_skus ps WHERE ps.product_id = products.id AND ps.updated_at > ?) OR "+
"EXISTS (SELECT 1 FROM card_secrets cs WHERE cs.product_id = products.id AND cs.updated_at > ?))",
after, after, after)
}
stockStatus := strings.ToLower(strings.TrimSpace(filter.StockStatus))
@@ -709,3 +709,45 @@ func TestProductRepositoryListOnlyActiveTakesPrecedenceOverIsActive(t *testing.T
t.Fatalf("OnlyActive should win over IsActive, got %+v", got)
}
}
// 增量同步的 updated_after 必须能看到 SKU 库存与卡密变化,它们不会改动商品行本身。
func TestProductRepositoryListUpdatedAfterIncludesStockChanges(t *testing.T) {
repo, db := setupProductStoreTest(t)
old := time.Now().Add(-2 * time.Hour)
mk := func(slug string) *productdomain.Product {
p := createManualProduct(t, repo, slug, 10, 0, 0)
sku := productdomain.ProductSKU{ProductID: p.ID, SKUCode: "DEFAULT", PriceAmount: money.FromDecimal(decimal.NewFromInt(1)), ManualStockTotal: 10, IsActive: true}
if err := db.Create(&sku).Error; err != nil {
t.Fatalf("create sku failed: %v", err)
}
db.Model(&productdomain.Product{}).Where("id = ?", p.ID).UpdateColumn("updated_at", old)
db.Model(&productdomain.ProductSKU{}).Where("product_id = ?", p.ID).UpdateColumn("updated_at", old)
return p
}
skuChanged := mk("updated-after-sku")
cardChanged := mk("updated-after-card")
untouched := mk("updated-after-untouched")
var sku productdomain.ProductSKU
db.Where("product_id = ?", skuChanged.ID).First(&sku)
if _, err := NewSKUStore(db).ReserveManualStock(sku.ID, 1); err != nil {
t.Fatalf("reserve sku stock failed: %v", err)
}
secret := cardsecretdomain.Secret{ProductID: cardChanged.ID, SKUID: 1, Secret: "S", Status: cardsecretdomain.StatusAvailable}
if err := db.Create(&secret).Error; err != nil {
t.Fatalf("create card secret failed: %v", err)
}
after := time.Now().Add(-time.Hour)
rows, _, err := repo.List(productcontract.ListFilter{Page: 1, PageSize: 20, UpdatedAfter: &after})
if err != nil {
t.Fatalf("list failed: %v", err)
}
got := map[uint]bool{}
for _, row := range rows {
got[row.ID] = true
}
if !got[skuChanged.ID] || !got[cardChanged.ID] || got[untouched.ID] {
t.Fatalf("unexpected incremental rows: sku=%v card=%v untouched=%v", got[skuChanged.ID], got[cardChanged.ID], got[untouched.ID])
}
}