Files
cmautobuy/admin/service/xlsx_price_backfill_test.go
T

177 lines
7.0 KiB
Go
Raw Normal View History

package service
import (
"database/sql"
"encoding/json"
"path/filepath"
"testing"
"time"
"cmautobuy/admin/repository"
"github.com/xuri/excelize/v2"
_ "modernc.org/sqlite"
)
func TestParseXLSXMaxTWDAndConvert(t *testing.T) {
tests := []struct {
raw string
wantMax string
wantCents int64
}{
{"249", "249", 2490},
{"314~630", "630", 6300},
{"12.35~10", "12.35", 124},
}
for _, test := range tests {
maximum, err := parseXLSXMaxTWD(test.raw)
if err != nil {
t.Fatalf("解析 %q 失败: %v", test.raw, err)
}
cents, err := twdToCNYCent(maximum)
if err != nil || decimalText(maximum) != test.wantMax || cents != test.wantCents {
t.Errorf("%q => max=%s cents=%d err=%v,期望 %s/%d",
test.raw, decimalText(maximum), cents, err, test.wantMax, test.wantCents)
}
}
if _, err := parseXLSXMaxTWD("249 元左右"); err == nil {
t.Fatal("不受支持的价格文本必须拒绝,不能猜测")
}
}
func TestLoadXLSXPriceSourcesUsesOnlyThreeFilesAndGlobalPDDMax(t *testing.T) {
dir := t.TempDir()
writePriceWorkbook(t, filepath.Join(dir, XLSXPriceBackfillFiles[0]), [][]any{
{XLSXPriceBackfillShop, "S1", "P1", "100~249"},
{XLSXPriceBackfillShop, "S1", "P1", "249"},
{"other-shop", "OTHER", "OTHER-PDD", "9999"},
})
writePriceWorkbook(t, filepath.Join(dir, XLSXPriceBackfillFiles[1]), [][]any{
{XLSXPriceBackfillShop, "S2", "P1", "300"},
})
writePriceWorkbook(t, filepath.Join(dir, XLSXPriceBackfillFiles[2]), [][]any{
{XLSXPriceBackfillShop, "S3", "P2", "12.35"},
})
sources, err := LoadXLSXPriceSources(dir)
if err != nil {
t.Fatal(err)
}
if sources.RowCount != 4 || sources.ShopeeProductCount != 3 || sources.PDDProductCount != 2 ||
sources.SharedPDDWithDifferentMaxCount != 1 {
t.Fatalf("汇总不正确: %+v", sources)
}
if got := sources.PDDPrices["P1"]; got.MaxTWD != "300" || got.PriceCent != 3000 || got.ShopeeProductCount != 2 {
t.Fatalf("共用 PDD 应取所有蝦皮商品的全局最大值: %+v", got)
}
if got := sources.PDDPrices["P2"]; got.PriceCent != 124 {
t.Fatalf("小数应四舍五入到人民币分: %+v", got)
}
}
func TestLoadXLSXPriceSourcesRequiresEveryFixedFile(t *testing.T) {
dir := t.TempDir()
writePriceWorkbook(t, filepath.Join(dir, XLSXPriceBackfillFiles[0]), [][]any{{XLSXPriceBackfillShop, "S1", "P1", "249"}})
if _, err := LoadXLSXPriceSources(dir); err == nil {
t.Fatal("缺少另外两个固定文件时必须停止")
}
}
func TestBuildXLSXPriceBackfillPlanOnlyFillsSafeSkeleton(t *testing.T) {
skeleton := `{"spec_source":"shopee_backfill","skus":[` +
`{"options":{"color":"黑","size":"M"},"price_cent":null,"list_price_cent":null,"availability_status":"unknown"},` +
`{"options":{"color":"白","size":"L"},"price_cent":null,"list_price_cent":null,"availability_status":"unknown"}]}`
withPrice := `{"spec_source":"shopee_backfill","skus":[{"options":{"color":"黑"},"price_cent":100,"availability_status":"unknown"}]}`
sources := XLSXPriceSources{PDDProductCount: 5, PDDPrices: map[string]XLSXPDDPriceSource{
"GOOD": {PDDGoodsID: "GOOD", MaxTWD: "249", PriceCent: 2490},
"PRICED": {PDDGoodsID: "PRICED", MaxTWD: "300", PriceCent: 3000},
"COLLECTED": {PDDGoodsID: "COLLECTED", MaxTWD: "400", PriceCent: 4000},
"WRONG": {PDDGoodsID: "WRONG", MaxTWD: "500", PriceCent: 5000},
"MISSING": {PDDGoodsID: "MISSING", MaxTWD: "600", PriceCent: 6000},
}}
targets := []repository.PDDPriceBackfillTarget{
{GoodsID: "GOOD", SKUsJSON: skeleton, CollectStatus: "failed", UpdatedAt: "old"},
{GoodsID: "PRICED", SKUsJSON: withPrice, CollectStatus: "pending", UpdatedAt: "old"},
{GoodsID: "COLLECTED", SKUsJSON: skeleton, CollectStatus: "collected", UpdatedAt: "old"},
{GoodsID: "WRONG", SKUsJSON: `{"spec_source":"other","skus":[]}`, CollectStatus: "pending", UpdatedAt: "old"},
}
plan := BuildXLSXPriceBackfillPlan(sources, targets, time.Date(2026, 8, 14, 0, 0, 0, 0, time.UTC))
if len(plan.Candidates) != 1 || plan.Candidates[0].PDDGoodsID != "GOOD" || plan.Candidates[0].SKUCount != 2 {
t.Fatalf("只应生成一个安全候选: %+v", plan)
}
if plan.Summary[XLSXPriceCandidate] != 1 || plan.Summary[XLSXPriceAlreadyExists] != 1 ||
plan.Summary[XLSXPriceAlreadyCollected] != 1 || plan.Summary[XLSXPriceSourceMismatch] != 1 ||
plan.Summary[XLSXPricePDDMissing] != 1 {
t.Fatalf("跳过分类不正确: %+v", plan.Summary)
}
var root struct {
SKUs []struct {
PriceCent *int64 `json:"price_cent"`
ListPriceCent *int64 `json:"list_price_cent"`
AvailabilityStatus string `json:"availability_status"`
} `json:"skus"`
}
if err := json.Unmarshal([]byte(plan.Candidates[0].NewSKUsJSON), &root); err != nil {
t.Fatal(err)
}
for _, sku := range root.SKUs {
if sku.PriceCent == nil || *sku.PriceCent != 2490 || sku.ListPriceCent != nil || sku.AvailabilityStatus != "unknown" {
t.Fatalf("只应填正式价格并保留列表价和库存: %+v", sku)
}
}
}
func TestApplyXLSXPriceBackfillUsesWholeBatchRollback(t *testing.T) {
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
db.SetMaxOpenConns(1)
if _, err := db.Exec(`CREATE TABLE pdd_products(
goods_id TEXT PRIMARY KEY, skus_json TEXT, collect_status TEXT,
collect_msg TEXT, artifact_ref TEXT, collected_at TEXT,
deleted_at TEXT, updated_at TEXT)`); err != nil {
t.Fatal(err)
}
if _, err := db.Exec(`INSERT INTO pdd_products(goods_id,skus_json,collect_status,collect_msg,updated_at)
VALUES('P1','old-1','pending','旧错误','t1'),('P2','changed','pending',NULL,'t2')`); err != nil {
t.Fatal(err)
}
plan := XLSXPriceBackfillPlan{Candidates: []XLSXPriceBackfillCandidate{
{PDDGoodsID: "P1", NewSKUsJSON: `{"skus":[1]}`, Original: repository.PDDPriceBackfillTarget{GoodsID: "P1", SKUsJSON: "old-1", CollectStatus: "pending", UpdatedAt: "t1"}},
{PDDGoodsID: "P2", NewSKUsJSON: `{"skus":[2]}`, Original: repository.PDDPriceBackfillTarget{GoodsID: "P2", SKUsJSON: "old-2", CollectStatus: "pending", UpdatedAt: "t2"}},
}}
if _, err := ApplyXLSXPriceBackfill(db, plan, time.Now()); err == nil {
t.Fatal("第二件原值变化时必须失败")
}
var raw, status, message string
if err := db.QueryRow(`SELECT skus_json,collect_status,collect_msg FROM pdd_products WHERE goods_id='P1'`).Scan(&raw, &status, &message); err != nil {
t.Fatal(err)
}
if raw != "old-1" || status != "pending" || message != "旧错误" {
t.Fatalf("整批必须回滚,实际 raw=%q status=%q message=%q", raw, status, message)
}
}
func writePriceWorkbook(t *testing.T, path string, rows [][]any) {
t.Helper()
book := excelize.NewFile()
defer book.Close()
if err := book.SetSheetName("Sheet1", xlsxPriceSheet); err != nil {
t.Fatal(err)
}
header := []any{"店铺显示名", "商品ID", "货源ID", "价格"}
if err := book.SetSheetRow(xlsxPriceSheet, "A1", &header); err != nil {
t.Fatal(err)
}
for index, row := range rows {
cell, _ := excelize.CoordinatesToCellName(1, index+2)
if err := book.SetSheetRow(xlsxPriceSheet, cell, &row); err != nil {
t.Fatal(err)
}
}
if err := book.SaveAs(path); err != nil {
t.Fatal(err)
}
}