2026-08-09 22:55:14 +08:00
package service
import (
"database/sql"
2026-08-10 14:26:03 +08:00
"errors"
2026-08-10 15:11:38 +08:00
"strings"
2026-08-09 22:55:14 +08:00
"testing"
"time"
"cmautobuy/admin/model"
"cmautobuy/admin/repository"
)
2026-08-10 14:26:03 +08:00
func TestAssociateSybPdd_兼容商品ID并复用已有采集结果 ( t * testing . T ) {
db := newTestDB ( t )
seedWorkflowOrder ( t , db , "SYB-PDD-ID" , "SP-PDD-ID" , "黑色,M" )
if _ , err := repository . EnsurePddProduct ( db , "737116531267" , pddURLA ); err != nil {
t . Fatal ( err )
}
if err := repository . SetCollectResult ( db , "737116531267" , "已采集商品" , "已有店铺" , collectedThreeDimensions ); err != nil {
t . Fatal ( err )
}
goodsID , err := AssociateSybPdd ( db , "SYB-PDD-ID" , " 737116531267 " , false )
if err != nil || goodsID != "737116531267" {
t . Fatalf ( "纯商品 ID 关联结果 goodsID=%q err=%v" , goodsID , err )
}
product , err := repository . GetPddProductByGoodsID ( db , goodsID )
if err != nil || product == nil {
t . Fatalf ( "读取复用商品失败: product=%+v err=%v" , product , err )
}
if product . URL != pddURLA || product . CollectStatus != model . CollectCollected ||
product . Title != "已采集商品" || product . SkusJSON != collectedThreeDimensions {
t . Fatalf ( "已有采集结果不应被清空或重置: %+v" , product )
}
var productCount , taskCount int
db . QueryRow ( `SELECT COUNT(*) FROM pdd_products WHERE goods_id = ?` , goodsID ). Scan ( & productCount )
db . QueryRow ( `SELECT COUNT(*) FROM tasks` ). Scan ( & taskCount )
if productCount != 1 || taskCount != 0 {
t . Fatalf ( "应复用一条商品且不自动建任务: products=%d tasks=%d" , productCount , taskCount )
}
}
func TestAssociateSybPdd_新商品ID生成标准链接且完整链接保持兼容 ( t * testing . T ) {
t . Run ( "纯商品ID" , func ( t * testing . T ) {
db := newTestDB ( t )
seedWorkflowOrder ( t , db , "SYB-NEW-ID" , "SP-NEW-ID" , "白色,L" )
goodsID , err := AssociateSybPdd ( db , "SYB-NEW-ID" , "123456789" , false )
if err != nil || goodsID != "123456789" {
t . Fatalf ( "关联结果 goodsID=%q err=%v" , goodsID , err )
}
product , _ := repository . GetPddProductByGoodsID ( db , goodsID )
if product == nil || product . URL != canonicalPddGoodsURL + goodsID || product . CollectStatus != model . CollectPending {
t . Fatalf ( "新商品 ID 没有生成待采集标准档案: %+v" , product )
}
})
t . Run ( "完整链接" , func ( t * testing . T ) {
db := newTestDB ( t )
seedWorkflowOrder ( t , db , "SYB-FULL-URL" , "SP-FULL-URL" , "白色,L" )
goodsID , err := AssociateSybPdd ( db , "SYB-FULL-URL" , pddURLA , false )
if err != nil || goodsID != "737116531267" {
t . Fatalf ( "原完整链接行为回归: goodsID=%q err=%v" , goodsID , err )
}
})
}
func TestAssociateSybPdd_商品ID输入边界和换品确认 ( t * testing . T ) {
for _ , input := range [] string { "12345" , "1234567890123456789012345" , "737116abc" , "普通文本" } {
t . Run ( input , func ( t * testing . T ) {
db := newTestDB ( t )
seedWorkflowOrder ( t , db , "SYB-BAD-ID" , "SP-BAD-ID" , "黑色,M" )
if _ , err := AssociateSybPdd ( db , "SYB-BAD-ID" , input , false ); ! errors . Is ( err , ErrBadPddURL ) {
t . Fatalf ( "输入 %q 应返回 ErrBadPddURL,实际 %v" , input , err )
}
})
}
db := newTestDB ( t )
seedWorkflowOrder ( t , db , "SYB-REPLACE" , "SP-REPLACE" , "黑色,M" )
if _ , err := AssociateSybPdd ( db , "SYB-REPLACE" , "737116531267" , false ); err != nil {
t . Fatal ( err )
}
if _ , err := AssociateSybPdd ( db , "SYB-REPLACE" , "937122477375" , false ); ! errors . Is ( err , ErrPddAssociationReplaceRequired ) {
t . Fatalf ( "纯 ID 换成其他商品仍应要求确认,实际 %v" , err )
}
if _ , err := AssociateSybPdd ( db , "SYB-REPLACE" , "937122477375" , true ); err != nil {
t . Fatalf ( "确认后应允许更换: %v" , err )
}
}
2026-08-09 22:55:14 +08:00
const collectedThreeDimensions = `{
"goods_id":"737116531267",
"price_granularity":"sku",
"dimensions":[{"key":"color","name":"颜色"},{"key":"size","name":"尺码"},{"key":"style","name":"款式"}],
"skus":[
{"options":{"style":"常规","size":"M码","color":"黑色"},"price_cent":3990,"available":true},
{"options":{"style":"加绒","size":"L码","color":"白色"},"price_cent":4590,"available":true},
{"options":{"style":"下架","size":"S码","color":"红色"},"price_cent":2990,"available":false}
]
}`
func seedPurchasableWorkflow ( t * testing . T , db * sql . DB , sybID string ) string {
t . Helper ()
order := seedWorkflowOrder ( t , db , sybID , "SP-1" , "黑色,M" )
order . PriceTwdCent = 999900
if _ , err := repository . UpsertSybOrder ( db , order ); err != nil {
t . Fatal ( err )
}
if _ , err := AssociateShopeePdd ( db , "SP-1" , pddURLA , false ); err != nil {
t . Fatal ( err )
}
if err := repository . SetCollectResult ( db , "737116531267" , "PDD 上衣" , "店铺" , collectedThreeDimensions ); err != nil {
t . Fatal ( err )
}
key , _ := OptionKey ( map [ string ] string { "color" : "黑色" , "size" : "M码" , "style" : "常规" })
return key
}
func TestSybMapping_动态维度保存并复用 ( t * testing . T ) {
db := newTestDB ( t )
key := seedPurchasableWorkflow ( t , db , "SYB-1" )
detail , err := GetSybProcessingDetail ( db , "SYB-1" )
if err != nil {
t . Fatal ( err )
}
if len ( detail . PddDimensionNames ) != 3 || detail . PddDimensionNames [ 2 ] != "款式" || len ( detail . PddChoices ) != 2 {
t . Fatalf ( "动态维度/可用组合不对: names=%v choices=%+v" , detail . PddDimensionNames , detail . PddChoices )
}
if err := SaveSybMapping ( db , "SYB-1" , key , "USR-1" ); err != nil {
t . Fatal ( err )
}
2026-08-10 02:21:31 +08:00
detail , err = GetSybProcessingDetail ( db , "SYB-1" )
if err != nil {
t . Fatal ( err )
}
2026-08-09 22:55:14 +08:00
if ! detail . MappingValid {
2026-08-10 12:24:23 +08:00
t . Fatal ( "保存后相同蝦皮商品规格 + 当前 PDD 商品应自动复用" )
2026-08-09 22:55:14 +08:00
}
2026-08-11 14:39:13 +08:00
list , err := ListSybOrdersView ( db , "" , "" , SybStagePurchaseReady , 1 )
2026-08-10 02:21:31 +08:00
if err != nil {
t . Fatal ( err )
}
2026-08-09 22:55:14 +08:00
if list . Total != 1 || ! list . Rows [ 0 ]. CanPurchase || list . Rows [ 0 ]. DefaultMaxPrice != "39.90" {
t . Fatalf ( "可采购阶段不对: %+v" , list )
}
2026-08-10 17:56:03 +08:00
row := list . Rows [ 0 ]
if row . MappedPddChoice != "颜色:黑色 / 尺码:M码 / 款式:常规" ||
strings . Contains ( row . MappedPddChoice , "白色" ) || row . MappingOptionKey != key || row . ContextVersion == "" {
t . Fatalf ( "采购确认只能带出当前映射组合,实际 %+v" , row )
}
2026-08-09 22:55:14 +08:00
}
2026-08-10 12:24:23 +08:00
func TestSybMapping_同商品同规格第二条明细自动命中 ( t * testing . T ) {
db := newTestDB ( t )
key := seedPurchasableWorkflow ( t , db , "SYB-1" )
if err := SaveSybMapping ( db , "SYB-1" , key , "USR-1" ); err != nil {
t . Fatal ( err )
}
seedWorkflowOrder ( t , db , "SYB-2" , "SP-1" , " 黑色,M " )
detail , err := GetSybProcessingDetail ( db , "SYB-2" )
if err != nil || detail == nil || ! detail . MappingValid || detail . Stage != SybStagePurchaseReady {
t . Fatalf ( "第二条相同规格应自动命中: detail=%+v err=%v" , detail , err )
}
}
func TestSybMapping_解析失败原文仍可保存且空规格被阻断 ( t * testing . T ) {
db := newTestDB ( t )
key := seedPurchasableWorkflow ( t , db , "SYB-BASE" )
seedWorkflowOrder ( t , db , "SYB-DIRTY" , "SP-1" , "紅色,3XL建議80-90公斤】" )
if err := SaveSybMapping ( db , "SYB-DIRTY" , key , "USR-1" ); err != nil {
t . Fatalf ( "规格解析失败不应影响身份键映射: %v" , err )
}
seedWorkflowOrder ( t , db , "SYB-EMPTY" , "SP-1" , "" )
if err := SaveSybMapping ( db , "SYB-EMPTY" , key , "USR-1" ); err == nil {
t . Fatal ( "空规格不得保存映射" )
}
if _ , err := AssociateSybPdd ( db , "SYB-EMPTY" , pddURLA , false ); err == nil {
t . Fatal ( "空规格不得进入采购关联流程" )
}
}
2026-08-09 22:55:14 +08:00
func TestSybMapping_换品和选项消失都会失效 ( t * testing . T ) {
t . Run ( "换 PDD 商品隔离旧映射" , func ( t * testing . T ) {
db := newTestDB ( t )
key := seedPurchasableWorkflow ( t , db , "SYB-1" )
SaveSybMapping ( db , "SYB-1" , key , "USR-1" )
if _ , err := AssociateShopeePdd ( db , "SP-1" , pddURLB , true ); err != nil {
t . Fatal ( err )
}
repository . SetCollectResult ( db , "937122477375" , "另一个商品" , "店铺" , stringsReplaceGoodsID ( collectedThreeDimensions ))
detail , _ := GetSybProcessingDetail ( db , "SYB-1" )
if detail . MappingValid {
t . Fatal ( "更换 PDD 商品后不得复用旧映射" )
}
2026-08-10 12:24:23 +08:00
if _ , err := AssociateShopeePdd ( db , "SP-1" , pddURLA , true ); err != nil {
t . Fatal ( err )
}
detail , _ = GetSybProcessingDetail ( db , "SYB-1" )
if ! detail . MappingValid {
t . Fatal ( "换回原 PDD 商品后应复用原映射" )
}
2026-08-09 22:55:14 +08:00
})
t . Run ( "最新采集结果删除原选项" , func ( t * testing . T ) {
db := newTestDB ( t )
key := seedPurchasableWorkflow ( t , db , "SYB-1" )
SaveSybMapping ( db , "SYB-1" , key , "USR-1" )
changed := `{"goods_id":"737116531267","dimensions":[{"key":"color","name":"颜色"}],"skus":[{"options":{"color":"白色"},"price_cent":1000,"available":true}]}`
repository . SetCollectResult ( db , "737116531267" , "PDD 上衣" , "店铺" , changed )
detail , _ := GetSybProcessingDetail ( db , "SYB-1" )
if detail . MappingValid || detail . Stage != SybStageMappingPending {
t . Fatalf ( "失效映射应回到待匹配: valid=%t stage=%s" , detail . MappingValid , detail . Stage )
}
})
}
2026-08-10 12:41:17 +08:00
func TestSybMapping_建议审计追加陈旧保护与事务回滚 ( t * testing . T ) {
db := newTestDB ( t )
order := seedWorkflowOrder ( t , db , "SYB-AUDIT" , "SP-AUDIT" , "灰色,L建議53-57公斤" )
if _ , err := AssociateShopeePdd ( db , "SP-AUDIT" , pddURLA , false ); err != nil {
t . Fatal ( err )
}
raw := `{"goods_id":"737116531267","dimensions":[{"key":"color","name":"颜色分类"},{"key":"size","name":"尺码"}],"skus":[{"options":{"color":"灰色中长款","size":"L( 80-115斤)"},"price_cent":3990,"available":true},{"options":{"color":"黑色","size":"L( 80-115斤)"},"price_cent":3990,"available":true}]}`
if err := repository . SetCollectResult ( db , "737116531267" , "PDD" , "" , raw ); err != nil {
t . Fatal ( err )
}
detail , err := GetSybProcessingDetail ( db , order . SybID )
if err != nil {
t . Fatal ( err )
}
var suggested , other string
for _ , c := range detail . PddChoices {
if c . Recommended {
suggested = c . Key
} else {
other = c . Key
}
}
if suggested == "" || detail . ContextVersion == "" {
t . Fatalf ( "无建议: %+v" , detail )
}
var unopenedMappings , unopenedDecisions int
db . QueryRow ( `SELECT COUNT(*) FROM spec_mappings WHERE shopee_goods_id='SP-AUDIT'` ). Scan ( & unopenedMappings )
db . QueryRow ( `SELECT COUNT(*) FROM spec_mapping_decisions WHERE shopee_goods_id='SP-AUDIT'` ). Scan ( & unopenedDecisions )
if unopenedMappings != 0 || unopenedDecisions != 0 {
t . Fatal ( "只打开或关闭弹窗不得写库" )
}
if err := SaveSybMapping ( db , order . SybID , other , "USR" , detail . ContextVersion ); err != nil {
t . Fatal ( err )
}
if err := SaveSybMapping ( db , order . SybID , suggested , "USR" , mappingContextVersionMust ( t , db , order . SybID )); err != nil {
t . Fatal ( err )
}
var total , accepted int
db . QueryRow ( `SELECT COUNT(*),SUM(accepted) FROM spec_mapping_decisions WHERE shopee_goods_id='SP-AUDIT'` ). Scan ( & total , & accepted )
if total != 2 || accepted != 1 {
t . Fatalf ( "审计 total=%d accepted=%d" , total , accepted )
}
old := mappingContextVersionMust ( t , db , order . SybID )
if err := repository . SetCollectResult ( db , "737116531267" , "PDD2" , "" , raw + " " ); err != nil {
t . Fatal ( err )
}
if err := SaveSybMapping ( db , order . SybID , suggested , "USR" , old ); err == nil {
t . Fatal ( "陈旧弹窗必须拒绝" )
}
before := suggested
mustExecService ( t , db , `DROP TABLE spec_mapping_decisions` )
if err := SaveSybMapping ( db , order . SybID , other , "USR" , mappingContextVersionMust ( t , db , order . SybID )); err == nil {
t . Fatal ( "审计失败必须回滚" )
}
var current string
db . QueryRow ( `SELECT pdd_option_key FROM spec_mappings WHERE shopee_goods_id='SP-AUDIT'` ). Scan ( & current )
if current != before {
t . Fatal ( "映射未回滚" )
}
}
func mappingContextVersionMust ( t * testing . T , db * sql . DB , id string ) string {
t . Helper ()
c , e := repository . GetSybOrderContext ( db , id )
if e != nil {
t . Fatal ( e )
}
return mappingContextVersion ( * c )
}
func mustExecService ( t * testing . T , db * sql . DB , q string ) {
t . Helper ()
if _ , e := db . Exec ( q ); e != nil {
t . Fatal ( e )
}
}
2026-08-09 22:55:14 +08:00
func stringsReplaceGoodsID ( raw string ) string {
return `{"goods_id":"937122477375","price_granularity":"sku","dimensions":[{"key":"color","name":"颜色"},{"key":"size","name":"尺码"},{"key":"style","name":"款式"}],"skus":[{"options":{"style":"常规","size":"M码","color":"黑色"},"price_cent":3990,"available":true}]}`
}
func TestCreatePurchaseTasks_安全字段和重复保护 ( t * testing . T ) {
db := newTestDB ( t )
key := seedPurchasableWorkflow ( t , db , "SYB-1" )
if err := SaveSybMapping ( db , "SYB-1" , key , "USR-1" ); err != nil {
t . Fatal ( err )
}
admin , _ , _ := prepareClientAssignmentUsers ( t , db )
if err := RegisterClient ( db , model . Client { ClientID : "CLIENT-1" , Name : "采购机" }, true ); err != nil {
t . Fatal ( err )
}
result , err := CreatePurchaseTasks ( db , admin ,
[] PurchaseTaskRequest {{ SybID : "SYB-1" , MaxPriceCent : 4200 }}, "CLIENT-1" )
if err != nil || result . Created != 1 || len ( result . Failures ) != 0 {
t . Fatalf ( "创建结果=%+v err=%v" , result , err )
}
var task model . Task
var status string
err = db . QueryRow ( `SELECT status, assigned_client, syb_id, order_no, goods_id,
shopee_sku_id, pdd_goods_url, pdd_goods_id, pdd_options, quantity, max_price_cent
FROM tasks WHERE task_type='purchase' AND syb_id='SYB-1'` ). Scan (
& status , & task . AssignedClient , & task . SybID , & task . OrderNo , & task . GoodsID ,
& task . ShopeeSKUID , & task . PddGoodsURL , & task . PddGoodsID , & task . PddOptions ,
& task . Quantity , & task . MaxPriceCent )
if err != nil {
t . Fatal ( err )
}
if status != "assigned" || task . MaxPriceCent != 4200 || task . MaxPriceCent == 999900 ||
task . Quantity != 1 || task . PddOptions != key || task . AssignedClient != "CLIENT-1" {
t . Fatalf ( "采购任务字段不安全或不完整: status=%s task=%+v" , status , task )
}
claimed , err := repository . ClaimNextTask ( db , "CLIENT-1" , [] string { "purchase" })
if err != nil || claimed == nil || claimed . PddOptions != key ||
claimed . Quantity != 1 || claimed . MaxPriceCent != 4200 {
t . Fatalf ( "现有 Client 领取契约读不到完整采购参数: task=%+v err=%v" , claimed , err )
}
second , err := CreatePurchaseTasks ( db , admin ,
[] PurchaseTaskRequest {{ SybID : "SYB-1" , MaxPriceCent : 4300 }}, "CLIENT-1" )
if err != nil || second . Created != 0 || len ( second . Failures ) != 1 {
t . Fatalf ( "重复提交应逐条拒绝: result=%+v err=%v" , second , err )
}
}
2026-08-11 18:05:27 +08:00
func TestCreatePurchaseTasks_成功或待核对任务禁止重复采购 ( t * testing . T ) {
cases := [] struct {
status model . TaskStatus
wantStage string
wantReason string
wantAttention bool
}{
{ model . TaskSucceeded , SybStagePurchaseCompleted , "已有成功采购任务" , false },
{ model . TaskManualReview , SybStagePurchaseReview , "待人工核对" , true },
}
for _ , tc := range cases {
t . Run ( string ( tc . status ), func ( t * testing . T ) {
db := newTestDB ( t )
key := seedPurchasableWorkflow ( t , db , "SYB-LOCKED" )
if err := SaveSybMapping ( db , "SYB-LOCKED" , key , "USR-1" ); err != nil {
t . Fatal ( err )
}
admin , _ , _ := prepareClientAssignmentUsers ( t , db )
if err := RegisterClient ( db , model . Client { ClientID : "CLIENT-1" , Name : "采购机" }, true ); err != nil {
t . Fatal ( err )
}
created , err := CreatePurchaseTasks ( db , admin ,
[] PurchaseTaskRequest {{ SybID : "SYB-LOCKED" , MaxPriceCent : 4200 }}, "CLIENT-1" )
if err != nil || created . Created != 1 {
t . Fatalf ( "准备采购任务失败: result=%+v err=%v" , created , err )
}
if _ , err := db . Exec ( `UPDATE tasks SET status=? WHERE task_type='purchase' AND syb_id=?` , tc . status , "SYB-LOCKED" ); err != nil {
t . Fatal ( err )
}
before := countPurchaseTasksForSyb ( t , db , "SYB-LOCKED" )
result , err := CreatePurchaseTasks ( db , admin ,
[] PurchaseTaskRequest {{ SybID : "SYB-LOCKED" , MaxPriceCent : 4300 }}, "CLIENT-1" )
if err != nil || result . Created != 0 || len ( result . Failures ) != 1 ||
! strings . Contains ( result . Failures [ 0 ]. Reason , tc . wantReason ) {
t . Fatalf ( "%s 任务必须阻止重复采购: result=%+v err=%v" , tc . status , result , err )
}
if after := countPurchaseTasksForSyb ( t , db , "SYB-LOCKED" ); after != before {
t . Fatalf ( "拒绝后任务数量从 %d 变为 %d" , before , after )
}
list , err := ListSybOrdersView ( db , "" , "" , tc . wantStage , 1 )
if err != nil || list . Total != 1 || len ( list . Rows ) != 1 ||
list . Rows [ 0 ]. Stage != tc . wantStage || list . Rows [ 0 ]. NeedsAttention != tc . wantAttention {
t . Fatalf ( "%s 阶段筛选错误: result=%+v err=%v" , tc . status , list , err )
}
})
}
}
func countPurchaseTasksForSyb ( t * testing . T , db * sql . DB , sybID string ) int {
t . Helper ()
var count int
if err := db . QueryRow ( `SELECT COUNT(*) FROM tasks WHERE task_type='purchase' AND syb_id=?` , sybID ). Scan ( & count ); err != nil {
t . Fatal ( err )
}
return count
}
2026-08-09 22:55:14 +08:00
func TestCreatePurchaseTasks_客户端权限和逐条失败 ( t * testing . T ) {
db := newTestDB ( t )
key := seedPurchasableWorkflow ( t , db , "SYB-1" )
SaveSybMapping ( db , "SYB-1" , key , "USR-1" )
admin , buyerA , buyerB := prepareClientAssignmentUsers ( t , db )
RegisterClient ( db , model . Client { ClientID : "CLIENT-A" }, true )
AssignClient ( db , admin , "CLIENT-A" , buyerA . UserID , time . Now ())
if _ , err := CreatePurchaseTasks ( db , buyerB ,
[] PurchaseTaskRequest {{ SybID : "SYB-1" , MaxPriceCent : 4200 }}, "CLIENT-A" ); err == nil {
t . Fatal ( "采购员不得使用其他采购员的客户端" )
}
result , err := CreatePurchaseTasks ( db , buyerA ,
[] PurchaseTaskRequest {{ SybID : "SYB-1" , MaxPriceCent : 0 }, { SybID : "NOT-FOUND" , MaxPriceCent : 100 }}, "CLIENT-A" )
if err != nil || result . Created != 0 || len ( result . Failures ) != 2 {
t . Fatalf ( "业务失败应逐条返回且不创建: result=%+v err=%v" , result , err )
}
}
func TestCreatePurchaseTasks_部分业务失败不影响有效明细 ( t * testing . T ) {
db := newTestDB ( t )
key := seedPurchasableWorkflow ( t , db , "SYB-1" )
SaveSybMapping ( db , "SYB-1" , key , "USR-1" )
admin , _ , _ := prepareClientAssignmentUsers ( t , db )
RegisterClient ( db , model . Client { ClientID : "CLIENT-1" }, true )
result , err := CreatePurchaseTasks ( db , admin , [] PurchaseTaskRequest {
{ SybID : "NOT-FOUND" , MaxPriceCent : 100 },
{ SybID : "SYB-1" , MaxPriceCent : 4200 },
}, "CLIENT-1" )
if err != nil || result . Created != 1 || len ( result . Failures ) != 1 || result . Failures [ 0 ]. SybID != "NOT-FOUND" {
t . Fatalf ( "部分失败边界不对: result=%+v err=%v" , result , err )
}
}
2026-08-10 15:11:38 +08:00
func TestCreatePurchaseTasksWithOptions_正常采购员可创建真实任务 ( t * testing . T ) {
db := newTestDB ( t )
key := seedPurchasableWorkflow ( t , db , "SYB-LIVE" )
SaveSybMapping ( db , "SYB-LIVE" , key , "USR-1" )
admin , buyer , _ := prepareClientAssignmentUsers ( t , db )
if err := RegisterClient ( db , model . Client { ClientID : "CLIENT-LIVE" , Capabilities : `{"purchase_mode":"live"}` }, true ); err != nil {
t . Fatal ( err )
}
if _ , _ , err := AssignClient ( db , admin , "CLIENT-LIVE" , buyer . UserID , time . Now ()); err != nil {
t . Fatal ( err )
}
result , err := CreatePurchaseTasksWithOptions ( db , buyer ,
[] PurchaseTaskRequest {{ SybID : "SYB-LIVE" , MaxPriceCent : 4200 }},
2026-08-10 18:15:28 +08:00
PurchaseTaskOptions { ClientID : "CLIENT-LIVE" , ExecutionMode : model . TaskExecutionLive })
2026-08-10 15:11:38 +08:00
if err != nil || result . Created != 1 {
t . Fatalf ( "正常采购员创建真实任务失败: result=%+v err=%v" , result , err )
}
2026-08-10 23:10:28 +08:00
var mode , confirmedBy , createdBy string
2026-08-10 15:11:38 +08:00
var confirmedAt sql . NullString
2026-08-10 23:10:28 +08:00
if err := db . QueryRow ( `SELECT execution_mode,live_confirmed_by,live_confirmed_at,created_by_user_id FROM tasks WHERE syb_id='SYB-LIVE'` ). Scan ( & mode , & confirmedBy , & confirmedAt , & createdBy ); err != nil {
2026-08-10 15:11:38 +08:00
t . Fatal ( err )
}
if mode != "live" || confirmedBy != buyer . UserID || ! confirmedAt . Valid {
t . Fatalf ( "真实任务审计不完整: mode=%s by=%s at=%+v" , mode , confirmedBy , confirmedAt )
}
2026-08-10 23:10:28 +08:00
if createdBy != buyer . UserID {
t . Fatalf ( "采购任务创建人=%q,期望 %q" , createdBy , buyer . UserID )
}
2026-08-10 15:11:38 +08:00
}
2026-08-10 18:37:35 +08:00
func TestCreatePurchaseTasksWithOptions_真实模式不要求Client预先声明Live ( t * testing . T ) {
2026-08-10 15:11:38 +08:00
db := newTestDB ( t )
key := seedPurchasableWorkflow ( t , db , "SYB-GATE" )
SaveSybMapping ( db , "SYB-GATE" , key , "USR-1" )
admin , _ , _ := prepareClientAssignmentUsers ( t , db )
RegisterClient ( db , model . Client { ClientID : "CLIENT-DRY" , Capabilities : `{"purchase_mode":"dry_run"}` }, true )
request := [] PurchaseTaskRequest {{ SybID : "SYB-GATE" , MaxPriceCent : 4200 }}
2026-08-10 18:37:35 +08:00
result , err := CreatePurchaseTasksWithOptions ( db , admin , request , PurchaseTaskOptions {
2026-08-10 15:11:38 +08:00
ClientID : "CLIENT-DRY" , ExecutionMode : model . TaskExecutionLive ,
2026-08-10 18:37:35 +08:00
})
if err != nil || result . Created != 1 {
t . Fatalf ( "当前账号可见客户端不应被历史能力状态阻止: result=%+v err=%v" , result , err )
2026-08-10 15:11:38 +08:00
}
disabled := * admin
disabled . Status = model . UserDisabled
if _ , err := CreatePurchaseTasks ( db , & disabled , request , "CLIENT-DRY" ); err == nil || ! strings . Contains ( err . Error (), "不是正常状态" ) {
t . Fatalf ( "禁用账号必须被拒绝: %v" , err )
}
}
2026-08-10 18:15:28 +08:00
func TestCreatePurchaseTasksWithOptions_离线Live客户端可提前指派 ( t * testing . T ) {
2026-08-10 17:52:48 +08:00
db := newTestDB ( t )
key := seedPurchasableWorkflow ( t , db , "SYB-OFFLINE" )
SaveSybMapping ( db , "SYB-OFFLINE" , key , "USR-1" )
admin , _ , _ := prepareClientAssignmentUsers ( t , db )
RegisterClient ( db , model . Client { ClientID : "CLIENT-OFFLINE" , Capabilities : `{"purchase_mode":"live"}` }, true )
if _ , err := db . Exec ( `UPDATE clients SET last_seen_at=? WHERE client_id=?` , "2026-08-10T08:00:00Z" , "CLIENT-OFFLINE" ); err != nil {
t . Fatal ( err )
}
2026-08-10 18:15:28 +08:00
result , err := CreatePurchaseTasksWithOptions ( db , admin ,
2026-08-10 17:52:48 +08:00
[] PurchaseTaskRequest {{ SybID : "SYB-OFFLINE" , MaxPriceCent : 4200 }},
2026-08-10 18:15:28 +08:00
PurchaseTaskOptions { ClientID : "CLIENT-OFFLINE" , ExecutionMode : model . TaskExecutionLive })
if err != nil || result . Created != 1 {
t . Fatalf ( "离线 live 客户端应可提前指派: result=%+v err=%v" , result , err )
2026-08-10 17:52:48 +08:00
}
}
2026-08-10 17:56:03 +08:00
func TestCreatePurchaseTasksWithOptions_拒绝弹窗打开后变化的规格映射 ( t * testing . T ) {
db := newTestDB ( t )
blackKey := seedPurchasableWorkflow ( t , db , "SYB-STALE" )
SaveSybMapping ( db , "SYB-STALE" , blackKey , "USR-1" )
context , err := repository . GetSybOrderContext ( db , "SYB-STALE" )
if err != nil || context == nil {
t . Fatal ( err )
}
oldVersion := mappingContextVersion ( * context )
whiteKey , _ := OptionKey ( map [ string ] string { "color" : "白色" , "size" : "L码" , "style" : "加绒" })
SaveSybMapping ( db , "SYB-STALE" , whiteKey , "USR-2" )
admin , _ , _ := prepareClientAssignmentUsers ( t , db )
RegisterClient ( db , model . Client { ClientID : "CLIENT-LIVE" , Capabilities : `{"purchase_mode":"live"}` }, true )
result , err := CreatePurchaseTasksWithOptions ( db , admin ,
[] PurchaseTaskRequest {{ SybID : "SYB-STALE" , MaxPriceCent : 3990 ,
MappingOptionKey : blackKey , ContextVersion : oldVersion }},
2026-08-10 18:15:28 +08:00
PurchaseTaskOptions { ClientID : "CLIENT-LIVE" , ExecutionMode : model . TaskExecutionLive })
2026-08-10 17:56:03 +08:00
if err != nil || result . Created != 0 || len ( result . Failures ) != 1 ||
! strings . Contains ( result . Failures [ 0 ]. Reason , "已变化" ) {
t . Fatalf ( "旧弹窗必须因映射变化被拒绝: result=%+v err=%v" , result , err )
}
}
2026-08-09 22:55:14 +08:00
func TestParsePriceYuanToCent_精确转换 ( t * testing . T ) {
for raw , want := range map [ string ] int64 { "39.90" : 3990 , "1" : 100 , "0.01" : 1 , "12.3" : 1230 } {
got , err := ParsePriceYuanToCent ( raw )
if err != nil || got != want {
t . Fatalf ( "%s => %d, %v; want %d" , raw , got , err , want )
}
}
for _ , raw := range [] string { "" , "0" , "-1" , "1.234" , "abc" } {
if _ , err := ParsePriceYuanToCent ( raw ); err == nil {
t . Fatalf ( "应拒绝 %q" , raw )
}
}
}