Files
cmautobuy/admin/service/pdd_test.go
T

301 lines
10 KiB
Go
Raw 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"
"testing"
"cmautobuy/admin/model"
"cmautobuy/admin/repository"
)
// ── OptionKey ──────────────────────────────────────────
// 键序不同的同一个规格必须算出同一个 key。
// 这是整个匹配机制的地基:采集回来的 JSON 键序和存映射时不一定一样,
// 算不出同一个 key,匹配就会**静默失效**——不报错,只是查不到。
func TestOptionKey_键序不影响结果(t *testing.T) {
a, err := OptionKey(map[string]string{"color": "黑色", "size": "M"})
if err != nil {
t.Fatal(err)
}
b, err := OptionKey(map[string]string{"size": "M", "color": "黑色"})
if err != nil {
t.Fatal(err)
}
if a != b {
t.Errorf("同一个规格算出了不同的 key:\n%s\n%s", a, b)
}
}
func TestOptionKey_不同规格算出不同key(t *testing.T) {
a, _ := OptionKey(map[string]string{"color": "黑色", "size": "M"})
b, _ := OptionKey(map[string]string{"color": "黑色", "size": "L"})
if a == b {
t.Error("不同规格不该算出同一个 key")
}
}
// 维度数量不固定——可能有第三个维度(款式),key 要能容纳。
func TestOptionKey_支持任意多个维度(t *testing.T) {
k, err := OptionKey(map[string]string{"color": "黑色", "size": "M", "style": "加绒"})
if err != nil {
t.Fatalf("三个维度应该合法: %v", err)
}
if k == "" {
t.Error("key 不该为空")
}
}
func TestOptionKey_空组合报错(t *testing.T) {
if _, err := OptionKey(map[string]string{}); err == nil {
t.Error("空规格组合应该报错,不能算出一个看起来正常的 key")
}
}
// 从 JSON 原文算 key,和从 map 算的结果必须一致——
// 存映射走一条路径、查规格走另一条路径,两边算不出同一个值就白搭。
func TestOptionKeyFromJSON_与map版本一致(t *testing.T) {
fromMap, _ := OptionKey(map[string]string{"color": "黑色", "size": "M"})
fromJSON, err := OptionKeyFromJSON(`{"size":"M","color":"黑色"}`)
if err != nil {
t.Fatal(err)
}
if fromMap != fromJSON {
t.Errorf("两条路径算出不同的 key:\nmap %s\njson %s", fromMap, fromJSON)
}
}
// ── PDD 商品的建立与复活 ───────────────────────────────
func TestEnsurePddProduct_新建(t *testing.T) {
db := newTestDB(t)
p, err := repository.EnsurePddProduct(db, "PDD-1", "https://x/1")
if err != nil {
t.Fatalf("新建失败: %v", err)
}
if p.GoodsID != "PDD-1" {
t.Errorf("goods_id 不对: %s", p.GoodsID)
}
if p.CollectStatus != model.CollectPending {
t.Errorf("新建的商品应为待采集,实际 %s", p.CollectStatus)
}
}
// 同一个商品重复保存链接,不能产生第二行——
// goods_id 上有 UNIQUE,重复了会直接插入失败。
func TestEnsurePddProduct_重复保存不产生重复行(t *testing.T) {
db := newTestDB(t)
for i := 0; i < 3; i++ {
if _, err := repository.EnsurePddProduct(db, "PDD-1", "https://x/1"); err != nil {
t.Fatalf("第 %d 次保存失败: %v", i+1, err)
}
}
var n int
db.QueryRow(`SELECT COUNT(*) FROM pdd_products WHERE goods_id='PDD-1'`).Scan(&n)
if n != 1 {
t.Errorf("应只有 1 行,实际 %d 行", n)
}
}
// 已采集的商品再次保存链接,不能把采集结果冲掉。
func TestEnsurePddProduct_不覆盖已有采集结果(t *testing.T) {
db := newTestDB(t)
repository.EnsurePddProduct(db, "PDD-1", "https://x/1")
if err := repository.SetCollectResult(db, "PDD-1", "商品甲", "", `{"skus":[1]}`); err != nil {
t.Fatal(err)
}
if _, err := repository.EnsurePddProduct(db, "PDD-1", "https://x/1?from=share"); err != nil {
t.Fatalf("再次保存失败: %v", err)
}
p, _ := repository.GetPddProductByGoodsID(db, "PDD-1")
if p.CollectStatus != model.CollectCollected {
t.Errorf("采集状态被冲掉了,实际 %s", p.CollectStatus)
}
if p.SkusJSON == "" {
t.Error("采集结果被清空了")
}
if p.URL != "https://x/1?from=share" {
t.Errorf("链接原文应更新,实际 %s", p.URL)
}
}
// 软删除后再填同一个链接要能复活。
// 不复活的话,goods_id 上的 UNIQUE 会让插入失败,
// 操作员会看到一个莫名其妙的错误。
func TestEnsurePddProduct_软删除后可复活(t *testing.T) {
db := newTestDB(t)
repository.EnsurePddProduct(db, "PDD-1", "https://x/1")
repository.SetCollectResult(db, "PDD-1", "商品甲", "", `{"skus":[1]}`)
if err := repository.SoftDeletePddProduct(db, "PDD-1"); err != nil {
t.Fatal(err)
}
p, _ := repository.GetPddProductByGoodsID(db, "PDD-1")
if !p.IsDeleted() {
t.Fatal("前置条件不对,应已软删除")
}
revived, err := repository.EnsurePddProduct(db, "PDD-1", "https://x/1")
if err != nil {
t.Fatalf("复活失败: %v", err)
}
if revived.IsDeleted() {
t.Error("复活后不该还带删除标记")
}
if revived.CollectStatus != model.CollectPending {
t.Errorf("复活后应回到待采集,实际 %s", revived.CollectStatus)
}
// 删过一次的记录,旧采集结果不能再当有效的用
if revived.SkusJSON != "" {
t.Error("复活后应清空旧采集结果,避免用到删除前的过期数据")
}
var n int
db.QueryRow(`SELECT COUNT(*) FROM pdd_products WHERE goods_id='PDD-1'`).Scan(&n)
if n != 1 {
t.Errorf("复活不该新增行,实际 %d 行", n)
}
}
// ── 采集中不允许重复发起 ───────────────────────────────
func TestMarkCollecting_采集中不允许再次发起(t *testing.T) {
db := newTestDB(t)
repository.EnsurePddProduct(db, "PDD-1", "https://x/1")
ok, err := repository.MarkCollecting(db, "PDD-1")
if err != nil || !ok {
t.Fatalf("第一次应成功: ok=%v err=%v", ok, err)
}
ok, err = repository.MarkCollecting(db, "PDD-1")
if err != nil {
t.Fatal(err)
}
if ok {
t.Error("已在采集中,不该允许再建一个采集任务")
}
}
func TestMarkCollecting_失败后可重新采集(t *testing.T) {
db := newTestDB(t)
repository.EnsurePddProduct(db, "PDD-1", "https://x/1")
repository.SetCollectFailed(db, "PDD-1", "页面超时", "")
ok, err := repository.MarkCollecting(db, "PDD-1")
if err != nil {
t.Fatal(err)
}
if !ok {
t.Error("采集失败的商品应该允许重新采集")
}
}
// ── 映射隔离:本工单的安全核心 ─────────────────────────
// insertMapping 造一条规格映射。
func insertMapping(t *testing.T, db *sql.DB, goodsID, specKey, pddGoodsID string, options map[string]string) {
t.Helper()
key, err := OptionKey(options)
if err != nil {
t.Fatal(err)
}
if err := repository.UpsertSpecMapping(db, model.SpecMapping{
ShopeeGoodsID: goodsID, SpecKey: specKey, SpecRaw: specKey,
PddGoodsID: pddGoodsID, PddOptionKey: key, PddOptions: key, MappedAt: model.NowISO(),
}); err != nil {
t.Fatalf("插入映射失败: %v", err)
}
}
// findMapping 模拟业务查询:**永远带上当前的 PDD 商品**。
// 这个条件就是隔离机制本身,少了它旧映射就会被误用。
func findMapping(t *testing.T, db *sql.DB, goodsID, specKey, currentPddGoodsID string) (string, bool) {
t.Helper()
var key string
err := db.QueryRow(`
SELECT pdd_option_key FROM spec_mappings
WHERE shopee_goods_id = ? AND spec_key = ? AND pdd_goods_id = ?`,
goodsID, specKey, currentPddGoodsID).Scan(&key)
if err == sql.ErrNoRows {
return "", false
}
if err != nil {
t.Fatal(err)
}
return key, true
}
// 换了 PDD 商品之后,旧映射必须查不到。
//
// 不隔离的话:B 恰好也有"黑色/M码"这个组合,但完全是另一件衣服,
// 就会**静默买错东西**,而且事后查不出来。
func TestSpecMapping_换PDD商品后查不到旧映射(t *testing.T) {
db := newTestDB(t)
repository.EnsurePddProduct(db, "PDD-A", "https://x/a")
repository.EnsurePddProduct(db, "PDD-B", "https://x/b")
// 顺运宝规格原来匹配到 PDD 商品 A 的"黑色/M码"
insertMapping(t, db, "SHOPEE-1", "黑色,M", "PDD-A", map[string]string{"color": "黑色", "size": "M码"})
if _, ok := findMapping(t, db, "SHOPEE-1", "黑色,M", "PDD-A"); !ok {
t.Fatal("前置条件不对:A 的映射应该查得到")
}
// A 下架,换成 B。查 B 的映射应该查不到 —— 界面显示"待匹配"
if key, ok := findMapping(t, db, "SHOPEE-1", "黑色,M", "PDD-B"); ok {
t.Errorf("换成 B 之后不该查到映射,却拿到了 %q —— "+
"这正是会导致买错商品的情况", key)
}
}
// 换回原来的 PDD 商品,旧映射要还能用。
// 这是把 pdd_goods_id 放进主键(而不是换商品时删掉旧映射)换来的好处:
// PDD 商品补货换回去时,之前的人工匹配成果直接复用。
func TestSpecMapping_换回原商品旧映射仍可用(t *testing.T) {
db := newTestDB(t)
repository.EnsurePddProduct(db, "PDD-A", "https://x/a")
repository.EnsurePddProduct(db, "PDD-B", "https://x/b")
insertMapping(t, db, "SHOPEE-1", "黑色,M", "PDD-A", map[string]string{"color": "黑色", "size": "M码"})
// 换到 B,也匹配一次
insertMapping(t, db, "SHOPEE-1", "黑色,M", "PDD-B", map[string]string{"color": "黑色", "size": "M"})
// 换回 A
key, ok := findMapping(t, db, "SHOPEE-1", "黑色,M", "PDD-A")
if !ok {
t.Fatal("换回 A 之后,A 的旧映射应该还在")
}
expect, _ := OptionKey(map[string]string{"color": "黑色", "size": "M码"})
if key != expect {
t.Errorf("拿到的是别的商品的映射:期望 %s,实际 %s", expect, key)
}
}
// 同一蝦皮商品规格可以同时有多个 PDD 商品的映射,互不干扰。
func TestSpecMapping_同一商品规格可对多个PDD商品各有映射(t *testing.T) {
db := newTestDB(t)
repository.EnsurePddProduct(db, "PDD-A", "https://x/a")
repository.EnsurePddProduct(db, "PDD-B", "https://x/b")
insertMapping(t, db, "SHOPEE-1", "黑色,M", "PDD-A", map[string]string{"color": "黑色", "size": "M码"})
insertMapping(t, db, "SHOPEE-1", "黑色,M", "PDD-B", map[string]string{"color": "黑", "size": "均码"})
var n int
db.QueryRow(`SELECT COUNT(*) FROM spec_mappings WHERE shopee_goods_id='SHOPEE-1' AND spec_key='黑色,M'`).Scan(&n)
if n != 2 {
t.Errorf("应能同时存在 2 条映射,实际 %d 条", n)
}
kA, _ := findMapping(t, db, "SHOPEE-1", "黑色,M", "PDD-A")
kB, _ := findMapping(t, db, "SHOPEE-1", "黑色,M", "PDD-B")
if kA == kB {
t.Error("两个 PDD 商品的映射不该相同")
}
}