2026-08-09 11:49:13 +08:00
package repository
import (
2026-08-10 02:21:31 +08:00
"crypto/rand"
2026-08-09 11:49:13 +08:00
"database/sql"
2026-08-10 02:21:31 +08:00
"encoding/hex"
"os"
"strings"
2026-08-09 11:49:13 +08:00
"testing"
2026-08-10 02:21:31 +08:00
"cmautobuy/admin/config"
2026-08-09 11:49:13 +08:00
"cmautobuy/admin/model"
)
func newSybTestDB ( t * testing . T ) * sql . DB {
t . Helper ()
2026-08-10 02:21:31 +08:00
if os . Getenv ( "CMAUTOBUY_MYSQL_TEST" ) != "1" {
t . Skip ( "未启用真实 MySQL 8 集成测试" )
}
cfg , err := config . LoadDatabaseFromEnv ()
2026-08-09 11:49:13 +08:00
if err != nil {
2026-08-10 02:21:31 +08:00
t . Fatal ( err )
2026-08-09 11:49:13 +08:00
}
2026-08-10 02:21:31 +08:00
if ! strings . HasSuffix ( cfg . Name , "_test" ) {
t . Fatalf ( "MySQL 测试基础库名必须以 _test 结尾" )
2026-08-09 11:49:13 +08:00
}
2026-08-10 02:21:31 +08:00
adminDB , err := OpenMySQL ( cfg )
if err != nil {
t . Fatal ( err )
}
random := make ([] byte , 8 )
if _ , err := rand . Read ( random ); err != nil {
t . Fatal ( err )
}
databaseName := cfg . Name + "_" + hex . EncodeToString ( random )
quotedName := "`" + strings . ReplaceAll ( databaseName , "`" , "``" ) + "`"
if _ , err := adminDB . Exec ( "CREATE DATABASE " + quotedName + " CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci" ); err != nil {
adminDB . Close ()
t . Fatal ( err )
}
cfg . Name = databaseName
db , err := OpenMySQL ( cfg )
if err != nil {
adminDB . Exec ( "DROP DATABASE " + quotedName )
adminDB . Close ()
t . Fatal ( err )
}
if err := MigrateMySQL ( db ); err != nil {
db . Close ()
adminDB . Exec ( "DROP DATABASE " + quotedName )
adminDB . Close ()
t . Fatal ( err )
}
t . Cleanup ( func () {
db . Close ()
if _ , err := adminDB . Exec ( "DROP DATABASE " + quotedName ); err != nil {
t . Errorf ( "清理隔离 MySQL 测试库失败: %v" , err )
}
adminDB . Close ()
})
2026-08-09 11:49:13 +08:00
return db
}
func TestUpsertSybOrder_已有的ShopeeSKUID同步后仍在 ( t * testing . T ) {
// 这是工单 #46 最要紧的一条:shopee_sku_id 是人工规格匹配的结果,
// 顺运宝同步绝不能把它覆盖成空值——覆盖了不会报错,等到建采购任务
// 才会发现匹配成果被洗掉,那时已经找不回来了。
db := newSybTestDB ( t )
first := model . SybOrder {
SybID : "SYB-1" , OrderNo : "ORDER-1" , Title : "初始标题" ,
ProductSpec : "黑色,M" , ShopeeGoodsID : "50209124255" ,
Quantity : 1 , PriceTwdCent : 23900 , ImageURL : "https://x/img.jpg" ,
SybData : `{"stock":{}}` ,
}
created , err := UpsertSybOrder ( db , first )
if err != nil {
t . Fatalf ( "首次写入失败: %v" , err )
}
if ! created {
t . Fatal ( "首次写入应该是新建" )
}
// 模拟操作员在界面上完成了规格匹配,手工把 shopee_sku_id 填上。
if _ , err := db . Exec ( `UPDATE syb_orders SET shopee_sku_id = ? WHERE syb_id = ?` ,
"TEST-SKU-123" , "SYB-1" ); err != nil {
t . Fatalf ( "模拟人工匹配失败: %v" , err )
}
// 再次同步:标题、数量、价格都变了(模拟顺运宝那边数据更新),
// 但 upsert 调用方不会传 shopee_sku_id 的新值(顺运宝根本没有这个字段)。
second := model . SybOrder {
SybID : "SYB-1" , OrderNo : "ORDER-1" , Title : "更新后的标题" ,
ProductSpec : "黑色,M" , ShopeeGoodsID : "50209124255" ,
Quantity : 3 , PriceTwdCent : 25900 , ImageURL : "https://x/img2.jpg" ,
SybData : `{"stock":{"updated":true}}` ,
// 注意:这里刻意不设置 ShopeeSKUID(零值,即空字符串),
// 模拟"同步流程从来不知道匹配结果,只管顺运宝返回的字段"。
}
created2 , err := UpsertSybOrder ( db , second )
if err != nil {
t . Fatalf ( "二次写入失败: %v" , err )
}
if created2 {
t . Fatal ( "二次写入应该是更新,不是新建" )
}
var skuID , title string
var qty int
if err := db . QueryRow ( `SELECT shopee_sku_id, title, quantity FROM syb_orders WHERE syb_id = ?` ,
"SYB-1" ). Scan ( & skuID , & title , & qty ); err != nil {
t . Fatalf ( "查询失败: %v" , err )
}
if skuID != "TEST-SKU-123" {
t . Fatalf ( "shopee_sku_id 应该还是人工匹配的 TEST-SKU-123,实际 %q" +
"——同步把匹配成果覆盖掉了,这是本工单最不能接受的问题" , skuID )
}
// 其余允许覆盖的字段应该已经更新,证明这不是"upsert 整个没生效"的假通过。
if title != "更新后的标题" || qty != 3 {
t . Errorf ( "title/quantity 应该被同步更新,实际 title=%q quantity=%d" , title , qty )
}
}
2026-08-11 10:46:11 +08:00
func TestUpsertSybOrder_只更新Syb骨架标题 ( t * testing . T ) {
db := newSybTestDB ( t )
now := model . NowISO ()
if _ , err := db . Exec ( `INSERT INTO shopee_products(goods_id,title,source,created_at,updated_at) VALUES('SYB-P','旧骨架','syb',?,?),('API-P','正式标题','api',?,?)` , now , now , now , now ); err != nil {
t . Fatal ( err )
}
for _ , o := range [] model . SybOrder {{ SybID : "S1" , OrderNo : "O1" , Title : "新骨架" , ProductSpec : "黑色,M" , ShopeeGoodsID : "SYB-P" , Quantity : 1 , PriceTwdCent : 100 , SybData : "{}" }, { SybID : "S2" , OrderNo : "O2" , Title : "顺运宝标题" , ProductSpec : "白色,L" , ShopeeGoodsID : "API-P" , Quantity : 1 , PriceTwdCent : 200 , SybData : "{}" }} {
if _ , err := UpsertSybOrder ( db , o ); err != nil {
t . Fatal ( err )
}
}
var sybTitle , apiTitle string
_ = db . QueryRow ( `SELECT title FROM shopee_products WHERE goods_id='SYB-P'` ). Scan ( & sybTitle )
_ = db . QueryRow ( `SELECT title FROM shopee_products WHERE goods_id='API-P'` ). Scan ( & apiTitle )
if sybTitle != "新骨架" || apiTitle != "正式标题" {
t . Fatalf ( "来源保护失败:syb=%q api=%q" , sybTitle , apiTitle )
}
}
2026-08-09 11:49:13 +08:00
func TestUpsertSybOrder_ShopeeGoodsID会被同步更新 ( t * testing . T ) {
db := newSybTestDB ( t )
if _ , err := UpsertSybOrder ( db , model . SybOrder {
SybID : "SYB-2" , OrderNo : "ORDER-2" , ShopeeGoodsID : "11111111111" , Quantity : 1 ,
SybData : "{}" ,
}); err != nil {
t . Fatalf ( "首次写入失败: %v" , err )
}
if _ , err := UpsertSybOrder ( db , model . SybOrder {
SybID : "SYB-2" , OrderNo : "ORDER-2" , ShopeeGoodsID : "22222222222" , Quantity : 1 ,
SybData : "{}" ,
}); err != nil {
t . Fatalf ( "二次写入失败: %v" , err )
}
var goodsID string
if err := db . QueryRow ( `SELECT shopee_goods_id FROM syb_orders WHERE syb_id = ?` ,
"SYB-2" ). Scan ( & goodsID ); err != nil {
t . Fatalf ( "查询失败: %v" , err )
}
if goodsID != "22222222222" {
t . Errorf ( "shopee_goods_id 应该被同步更新为顺运宝返回的新值,实际 %q" , goodsID )
}
}
func TestUpsertSybOrder_重复同步同一订单行数不翻倍 ( t * testing . T ) {
db := newSybTestDB ( t )
for i := 0 ; i < 3 ; i ++ {
if _ , err := UpsertSybOrder ( db , model . SybOrder {
SybID : "SYB-3" , OrderNo : "ORDER-3" , Quantity : 1 , SybData : "{}" ,
}); err != nil {
t . Fatalf ( "第 %d 次写入失败: %v" , i + 1 , err )
}
}
n , err := CountSybOrdersTotal ( db )
if err != nil {
t . Fatalf ( "统计失败: %v" , err )
}
if n != 1 {
t . Fatalf ( "重复同步同一个 syb_id 应该只有 1 行,实际 %d 行" , n )
}
}
2026-08-10 12:24:23 +08:00
func TestUpsertSybOrder_骨架失败时同事务不写订单 ( t * testing . T ) {
db := newSybTestDB ( t )
tx , err := db . Begin ()
if err != nil {
t . Fatal ( err )
}
if _ , err := UpsertSybOrder ( tx , model . SybOrder {
SybID : "SYB-REJECT" , OrderNo : "ORDER-REJECT" , Title : "测试" ,
ShopeeGoodsID : strings . Repeat ( "G" , 192 ), ProductSpec : "黑色,M" , Quantity : 1 , SybData : "{}" ,
}); err == nil {
t . Fatal ( "骨架失败必须让订单写入失败" )
}
tx . Rollback ()
var count int
if err := db . QueryRow ( `SELECT COUNT(*) FROM syb_orders WHERE syb_id='SYB-REJECT'` ). Scan ( & count ); err != nil || count != 0 {
t . Fatalf ( "失败后不应留下订单 count=%d err=%v" , count , err )
}
}
2026-08-09 11:49:13 +08:00
func TestSybSession_保存读取和清除 ( t * testing . T ) {
db := newSybTestDB ( t )
if got , err := GetSybSession ( db , "tester" ); err != nil || got != nil {
t . Fatalf ( "没有会话时应该返回 (nil, nil),实际 got=%v err=%v" , got , err )
}
if err := SaveSybSession ( db , "tester" , `[{"name":"JSESSIONID","value":"abc"}]` , "2026-08-10T00:00:00Z" ); err != nil {
t . Fatalf ( "保存会话失败: %v" , err )
}
got , err := GetSybSession ( db , "tester" )
if err != nil {
t . Fatalf ( "读取会话失败: %v" , err )
}
if got == nil || got . ExpiresAt != "2026-08-10T00:00:00Z" {
t . Fatalf ( "读取到的会话不对: %+v" , got )
}
// upsert:同一用户名再保存一次应该覆盖,不是新增一行。
if err := SaveSybSession ( db , "tester" , `[{"name":"JSESSIONID","value":"xyz"}]` , "2026-08-11T00:00:00Z" ); err != nil {
t . Fatalf ( "二次保存会话失败: %v" , err )
}
got2 , err := GetSybSession ( db , "tester" )
if err != nil || got2 == nil || got2 . ExpiresAt != "2026-08-11T00:00:00Z" {
t . Fatalf ( "二次保存后应该读到新值: got=%+v err=%v" , got2 , err )
}
if err := DeleteSybSession ( db , "tester" ); err != nil {
t . Fatalf ( "删除会话失败: %v" , err )
}
if got3 , err := GetSybSession ( db , "tester" ); err != nil || got3 != nil {
t . Fatalf ( "删除后应该查不到,实际 got=%v err=%v" , got3 , err )
}
}
func TestSybSyncState_首次为空之后可更新 ( t * testing . T ) {
db := newSybTestDB ( t )
_ , found , err := GetSybLastSyncedAt ( db )
if err != nil {
t . Fatalf ( "查询失败: %v" , err )
}
if found {
t . Fatal ( "从没同步过时 found 应该是 false" )
}
if err := SetSybLastSyncedAt ( db , "2026-08-09T14:30:00Z" ); err != nil {
t . Fatalf ( "更新失败: %v" , err )
}
at , found , err := GetSybLastSyncedAt ( db )
if err != nil || ! found || at != "2026-08-09T14:30:00Z" {
t . Fatalf ( "读取错误: at=%q found=%v err=%v" , at , found , err )
}
// 再更新一次,确认是 upsert 而不是报主键冲突。
if err := SetSybLastSyncedAt ( db , "2026-08-10T09:00:00Z" ); err != nil {
t . Fatalf ( "二次更新失败: %v" , err )
}
at2 , _ , err := GetSybLastSyncedAt ( db )
if err != nil || at2 != "2026-08-10T09:00:00Z" {
t . Fatalf ( "二次更新后应该读到新值: at2=%q err=%v" , at2 , err )
}
}
2026-08-09 18:22:58 +08:00
func createSybSyncTestUser ( t * testing . T , db * sql . DB ) model . User {
t . Helper ()
user := model . User {
UserID : "USR-SYNC" , Username : "buyer" , PasswordHash : "test-hash" ,
Role : model . RolePurchaser , Status : model . UserActive ,
PasswordChangedAt : "2026-08-09T00:00:00Z" , CreatedAt : "2026-08-09T00:00:00Z" ,
UpdatedAt : "2026-08-09T00:00:00Z" ,
}
if err := CreateInitialAdmin ( db , user ); err != nil {
t . Fatalf ( "创建同步记录测试用户失败: %v" , err )
}
return user
}
func TestSybSyncRun_创建完成并分页读取 ( t * testing . T ) {
db := newSybTestDB ( t )
user := createSybSyncTestUser ( t , db )
run := model . SybSyncRun {
RunID : "SYB-RUN-1" , UserID : user . UserID , DateFrom : "2026-08-07" ,
DateTo : "2026-08-09" , StartedAt : "2026-08-09T01:00:00Z" ,
}
if err := CreateSybSyncRun ( db , run ); err != nil {
t . Fatalf ( "创建同步记录失败: %v" , err )
}
if err := FinishSybSyncRun ( db , model . SybSyncRun {
RunID : "SYB-RUN-1" , Status : model . SybSyncSucceeded ,
StockCount : 3 , DetailCount : 4 , Created : 2 , Updated : 2 , Skipped : 1 ,
CursorAdvanced : true , FinishedAt : "2026-08-09T01:02:00Z" ,
}); err != nil {
t . Fatalf ( "完成同步记录失败: %v" , err )
}
rows , err := ListSybSyncRuns ( db , 10 , 0 )
if err != nil || len ( rows ) != 1 {
t . Fatalf ( "读取同步记录失败: rows=%+v err=%v" , rows , err )
}
got := rows [ 0 ]
if got . Username != "buyer" || got . Status != model . SybSyncSucceeded ||
got . DetailCount != 4 || ! got . CursorAdvanced || got . FinishedAt == "" {
t . Fatalf ( "同步记录字段不正确: %+v" , got )
}
if count , err := CountSybSyncRuns ( db ); err != nil || count != 1 {
t . Fatalf ( "同步记录总数错误: count=%d err=%v" , count , err )
}
}
func TestInterruptRunningSybSyncRuns_只中断未完成记录 ( t * testing . T ) {
db := newSybTestDB ( t )
user := createSybSyncTestUser ( t , db )
for _ , id := range [] string { "RUNNING" , "DONE" } {
if err := CreateSybSyncRun ( db , model . SybSyncRun {
RunID : id , UserID : user . UserID , DateFrom : "2026-08-09" , DateTo : "2026-08-09" ,
StartedAt : "2026-08-09T01:00:00Z" ,
}); err != nil {
t . Fatal ( err )
}
}
if err := FinishSybSyncRun ( db , model . SybSyncRun {
RunID : "DONE" , Status : model . SybSyncSucceeded , FinishedAt : "2026-08-09T01:01:00Z" ,
}); err != nil {
t . Fatal ( err )
}
affected , err := InterruptRunningSybSyncRuns ( db , "2026-08-09T02:00:00Z" )
if err != nil || affected != 1 {
t . Fatalf ( "应该只中断一条 running 记录: affected=%d err=%v" , affected , err )
}
rows , _ := ListSybSyncRuns ( db , 10 , 0 )
statuses := map [ string ] model . SybSyncRunStatus {}
for _ , row := range rows {
statuses [ row . RunID ] = row . Status
}
if statuses [ "RUNNING" ] != model . SybSyncInterrupted || statuses [ "DONE" ] != model . SybSyncSucceeded {
t . Fatalf ( "中断状态错误: %+v" , statuses )
}
}
2026-08-11 14:39:13 +08:00
func TestListSybOrders_订单标题店铺组合筛选 ( t * testing . T ) {
2026-08-09 11:49:13 +08:00
db := newSybTestDB ( t )
2026-08-11 14:39:13 +08:00
mustUpsert := func ( sybID , orderNo , shop , title string ) {
2026-08-09 11:49:13 +08:00
if _ , err := UpsertSybOrder ( db , model . SybOrder {
2026-08-11 14:39:13 +08:00
SybID : sybID , OrderNo : orderNo , ShopName : shop , Title : title , Quantity : 1 , SybData : "{}" ,
2026-08-09 11:49:13 +08:00
}); err != nil {
t . Fatalf ( "写入 %s 失败: %v" , sybID , err )
}
}
2026-08-11 14:39:13 +08:00
mustUpsert ( "SYB-A" , "260728AAA" , "台北服饰旗舰店" , "纯棉上衣" )
mustUpsert ( "SYB-B" , "260728BBB" , "台南生活馆" , "牛仔裤" )
2026-08-09 11:49:13 +08:00
rows , err := ListSybOrders ( db , SybOrderFilter { Keyword : "AAA" }, 20 , 0 )
if err != nil {
t . Fatalf ( "查询失败: %v" , err )
}
if len ( rows ) != 1 || rows [ 0 ]. SybID != "SYB-A" {
t . Fatalf ( "按订单号筛选应该只查到 SYB-A,实际 %+v" , rows )
}
rows2 , err := ListSybOrders ( db , SybOrderFilter { Keyword : "牛仔" }, 20 , 0 )
if err != nil {
t . Fatalf ( "查询失败: %v" , err )
}
if len ( rows2 ) != 1 || rows2 [ 0 ]. SybID != "SYB-B" {
t . Fatalf ( "按标题筛选应该只查到 SYB-B,实际 %+v" , rows2 )
}
2026-08-11 14:39:13 +08:00
rows3 , err := ListSybOrders ( db , SybOrderFilter { Shop : " 服饰 " }, 20 , 0 )
if err != nil || len ( rows3 ) != 1 || rows3 [ 0 ]. SybID != "SYB-A" || rows3 [ 0 ]. ShopName != "台北服饰旗舰店" {
t . Fatalf ( "按店铺模糊筛选错误:rows=%+v err=%v" , rows3 , err )
}
count , err := CountSybOrders ( db , SybOrderFilter { Keyword : "牛仔" , Shop : "台南" })
if err != nil || count != 1 {
t . Fatalf ( "标题与店铺组合 COUNT 错误:count=%d err=%v" , count , err )
}
2026-08-09 11:49:13 +08:00
}