Files
cmautobuy/admin/service/xlsx_price_backfill_test.go

177 lines
7.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}