feat: 规范 CSV 预检与安全入库 (#281)
This commit is contained in:
@@ -73,6 +73,8 @@ type CatalogBatchRequest struct {
|
||||
ShopeeSKUs []CatalogShopeeSKU `json:"shopee_skus"`
|
||||
PddProducts []CatalogPddProduct `json:"pdd_products"`
|
||||
Associations []CatalogAssociation `json:"associations"`
|
||||
// DryRun 只执行数据库预检;不得登记导入批次或写入任何业务数据。
|
||||
DryRun bool `json:"dry_run"`
|
||||
}
|
||||
type CatalogCounts struct {
|
||||
ShopeeCreated int `json:"shopee_created"`
|
||||
@@ -99,6 +101,14 @@ type CatalogBatchResponse struct {
|
||||
Replayed bool `json:"replayed"`
|
||||
Counts CatalogCounts `json:"counts"`
|
||||
FinishedAt string `json:"finished_at"`
|
||||
Conflicts []CatalogAssociationConflict `json:"conflicts,omitempty"`
|
||||
}
|
||||
|
||||
// CatalogAssociationConflict 是预检发现的既有人工关联冲突,不包含请求体或凭据。
|
||||
type CatalogAssociationConflict struct {
|
||||
ShopeeGoodsID string `json:"shopee_goods_id"`
|
||||
ExistingPddID string `json:"existing_pdd_goods_id"`
|
||||
IncomingPddID string `json:"incoming_pdd_goods_id"`
|
||||
}
|
||||
|
||||
type CatalogError struct {
|
||||
@@ -177,10 +187,52 @@ func ValidateCatalogBatch(req CatalogBatchRequest) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// PreviewCatalogBatch 按当前数据库做只读预检。它不登记 catalog_import_runs,
|
||||
// 也不写业务表;正式执行仍会重新检查,避免预检后人工修改造成覆盖。
|
||||
func PreviewCatalogBatch(db *sql.DB, req CatalogBatchRequest) (CatalogBatchResponse, error) {
|
||||
if err := ValidateCatalogBatch(req); err != nil {
|
||||
return CatalogBatchResponse{}, err
|
||||
}
|
||||
tx, err := db.BeginTx(context.Background(), &sql.TxOptions{ReadOnly: true, Isolation: sql.LevelReadCommitted})
|
||||
if err != nil {
|
||||
return CatalogBatchResponse{}, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
counts := CatalogCounts{}
|
||||
for _, p := range req.ShopeeProducts {
|
||||
exists, err := repository.CatalogEntityExists(tx, "shopee_products", p.GoodsID)
|
||||
if err != nil { return CatalogBatchResponse{}, err }
|
||||
if exists { counts.ShopeeUpdated++ } else { counts.ShopeeCreated++ }
|
||||
}
|
||||
for _, p := range req.PddProducts {
|
||||
exists, err := repository.CatalogEntityExists(tx, "pdd_products", p.GoodsID)
|
||||
if err != nil { return CatalogBatchResponse{}, err }
|
||||
if exists { counts.PddUpdated++ } else { counts.PddCreated++ }
|
||||
}
|
||||
for _, s := range req.ShopeeSKUs {
|
||||
key, _ := spec.SpecKey(s.SpecRaw)
|
||||
exists, err := repository.CatalogSKUExists(tx, s.GoodsID, key)
|
||||
if err != nil { return CatalogBatchResponse{}, err }
|
||||
if exists { counts.SKUSkipped++ } else { counts.SKUCreated++ }
|
||||
}
|
||||
conflicts := make([]CatalogAssociationConflict, 0)
|
||||
for _, a := range req.Associations {
|
||||
current, exists, err := repository.CatalogAssociationCurrentPDD(tx, a.ShopeeGoodsID)
|
||||
if err != nil { return CatalogBatchResponse{}, err }
|
||||
if !exists || current == "" { counts.AssociationCreated++; continue }
|
||||
if current == a.PddGoodsID { counts.AssociationUnchanged++; continue }
|
||||
conflicts = append(conflicts, CatalogAssociationConflict{ShopeeGoodsID: a.ShopeeGoodsID, ExistingPddID: current, IncomingPddID: a.PddGoodsID})
|
||||
}
|
||||
return CatalogBatchResponse{BatchID: req.BatchID, Status: "previewed", Counts: counts, FinishedAt: model.NowISO(), Conflicts: conflicts}, nil
|
||||
}
|
||||
|
||||
func ImportCatalogBatch(db *sql.DB, source string, req CatalogBatchRequest, raw []byte) (CatalogBatchResponse, error) {
|
||||
if err := ValidateCatalogBatch(req); err != nil {
|
||||
return CatalogBatchResponse{}, err
|
||||
}
|
||||
if req.DryRun {
|
||||
return PreviewCatalogBatch(db, req)
|
||||
}
|
||||
observed, _ := time.Parse(time.RFC3339Nano, req.ObservedAt)
|
||||
req.ObservedAt = observed.UTC().Format(model.TimeLayout)
|
||||
policy, _ := normalizeCatalogUpdatePolicy(req.UpdatePolicy)
|
||||
@@ -239,7 +291,7 @@ func ImportCatalogBatch(db *sql.DB, source string, req CatalogBatchRequest, raw
|
||||
b, _ := json.Marshal(map[string]any{"schema_version": 1, "goods_id": p.GoodsID, "title": p.Title, "shop_name": p.ShopName, "dimensions": p.Dimensions, "skus": p.SKUs})
|
||||
skus = string(b)
|
||||
}
|
||||
c, u, e := repository.UpsertCatalogPddProduct(tx, p.GoodsID, p.URL, p.Title, p.ShopName, skus, req.ObservedAt, now)
|
||||
c, u, e := repository.UpsertCatalogPddProduct(tx, p.GoodsID, p.URL, p.Title, p.ShopName, skus, req.ObservedAt, now, policy)
|
||||
if e != nil {
|
||||
return fail(e)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user