package repository import ( "database/sql" "fmt" "strings" ) // ComparableShopeeSpec 是一条可用于只读对比的蝦皮正式规格。 type ComparableShopeeSpec struct { Color string Size string ParseOK bool } // ShopSpecSource 汇总一件蝦皮商品及其关联 PDD 商品的原始对比数据。 type ShopSpecSource struct { ShopeeGoodsID string PDDGoodsID string PDDExists bool PDDDeleted bool PDDSKUsJSON string ShopeeSpecs []ComparableShopeeSpec } // ListShopSpecSources 按店铺名精确读取规格对比数据。本函数只执行 SELECT。 func ListShopSpecSources(q Execer, shopName string) ([]ShopSpecSource, error) { rows, err := q.Query(` SELECT sp.goods_id, sp.pdd_goods_id, pp.goods_id, pp.deleted_at, pp.skus_json FROM shopee_products sp LEFT JOIN pdd_products pp ON pp.goods_id = sp.pdd_goods_id WHERE sp.deleted_at IS NULL AND sp.shopee_shop_name = ? ORDER BY sp.goods_id`, strings.TrimSpace(shopName)) if err != nil { return nil, fmt.Errorf("读取店铺规格对比数据失败: %w", err) } defer rows.Close() var list []ShopSpecSource indexByGoodsID := make(map[string]int) for rows.Next() { var shopeeID string var pddID, existingPDDID, deletedAt, skusJSON sql.NullString if err := rows.Scan(&shopeeID, &pddID, &existingPDDID, &deletedAt, &skusJSON); err != nil { return nil, fmt.Errorf("解析店铺规格对比数据失败: %w", err) } list = append(list, ShopSpecSource{ ShopeeGoodsID: shopeeID, PDDGoodsID: pddID.String, PDDExists: existingPDDID.Valid, PDDDeleted: strings.TrimSpace(deletedAt.String) != "", PDDSKUsJSON: skusJSON.String, }) indexByGoodsID[shopeeID] = len(list) - 1 } if err := rows.Err(); err != nil { return nil, fmt.Errorf("遍历店铺规格对比数据失败: %w", err) } rows.Close() // PDD JSON 可能很大,不能和 SKU JOIN 后重复传输。商品和规格分两次查询, // 每份 PDD JSON 只从数据库读取一次。 skuRows, err := q.Query(` SELECT sk.goods_id, sk.color, sk.size, sk.parse_ok FROM shopee_skus sk JOIN shopee_products sp ON sp.goods_id = sk.goods_id WHERE sp.deleted_at IS NULL AND sp.shopee_shop_name = ? ORDER BY sk.goods_id, sk.sku_id`, strings.TrimSpace(shopName)) if err != nil { return nil, fmt.Errorf("读取店铺正式规格失败: %w", err) } defer skuRows.Close() for skuRows.Next() { var goodsID string var color, size sql.NullString var parseOK int64 if err := skuRows.Scan(&goodsID, &color, &size, &parseOK); err != nil { return nil, fmt.Errorf("解析店铺正式规格失败: %w", err) } index, exists := indexByGoodsID[goodsID] if !exists { continue } list[index].ShopeeSpecs = append(list[index].ShopeeSpecs, ComparableShopeeSpec{ Color: color.String, Size: size.String, ParseOK: parseOK == 1, }) } if err := skuRows.Err(); err != nil { return nil, fmt.Errorf("遍历店铺正式规格失败: %w", err) } return list, nil } // ReplaceEmptyPDDSpecs 用生成的规格骨架替换仍保持预期原值的 PDD 规格。 // expectedRaw 是乐观锁:读取后若被其他进程改过,更新返回 false,不覆盖新数据。 func ReplaceEmptyPDDSpecs(q Execer, pddGoodsID, expectedRaw, newRaw, updatedAt string) (bool, error) { result, err := q.Exec(` UPDATE pdd_products SET skus_json = ?, updated_at = ? WHERE goods_id = ? AND deleted_at IS NULL AND COALESCE(skus_json, '') = ?`, newRaw, updatedAt, strings.TrimSpace(pddGoodsID), expectedRaw) if err != nil { return false, fmt.Errorf("回填 PDD 商品 %s 的规格失败: %w", pddGoodsID, err) } rows, err := result.RowsAffected() if err != nil { return false, fmt.Errorf("读取 PDD 商品 %s 的回填结果失败: %w", pddGoodsID, err) } return rows == 1, nil }