fix: 简化真实采购创建并恢复客户端选择 (#112)

This commit is contained in:
chengma
2026-08-10 18:15:28 +08:00
parent 40cd8afefd
commit a26219091a
14 changed files with 96 additions and 129 deletions
+8 -5
View File
@@ -325,7 +325,7 @@ func TestClientAssignment_一人多客户端并按采购员隔离列表(t *testi
}
}
func TestListLivePurchaseClients_只返回可见在线且声明Live的客户端(t *testing.T) {
func TestListPurchaseClientOptions_返回可见客户端且离线Live仍可选(t *testing.T) {
db := newTestDB(t)
admin, buyerA, _ := prepareClientAssignmentUsers(t, db)
now := time.Now().UTC()
@@ -346,12 +346,15 @@ func TestListLivePurchaseClients_只返回可见在线且声明Live的客户端(
t.Fatal(err)
}
rows, err := ListLivePurchaseClients(db, buyerA, time.Minute)
options, err := ListPurchaseClientOptions(db, buyerA, time.Minute)
if err != nil {
t.Fatalf("读取真实采购客户端失败: %v", err)
t.Fatalf("读取采购客户端候选失败: %v", err)
}
if len(rows) != 1 || rows[0].ClientID != "live-online" {
t.Fatalf("只应返回在线 live 客户端,实际 %+v", rows)
if len(options.Rows) != 3 || options.SelectableCount != 2 {
t.Fatalf("应显示全部可见客户端且两个 live 可选,实际 %+v", options)
}
if options.Rows[2].ClientID != "live-offline" || options.Rows[2].Status != "离线" {
t.Fatalf("离线 live 客户端必须保留在候选中,实际 %+v", options.Rows)
}
}
+2 -19
View File
@@ -163,16 +163,11 @@ type PurchaseTaskRequest struct {
ContextVersion string
}
const LivePurchaseConfirmation = "创建未付款订单"
// PurchaseTaskOptions 是一次批量创建共用的执行门禁。
// ExecutionMode 留空表示 dry_run,确保旧调用和普通操作都保持安全默认值。
type PurchaseTaskOptions struct {
ClientID string
ExecutionMode model.TaskExecutionMode
LiveAcknowledged bool
LiveConfirmation string
ClientSeenAfter string
ClientID string
ExecutionMode model.TaskExecutionMode
}
// PurchaseTaskResult 同时返回已创建数量和每条无法创建的原因。
@@ -215,15 +210,6 @@ func CreatePurchaseTasksWithOptions(db *sql.DB, actor *model.User, requests []Pu
if !visible {
return result, fmt.Errorf("所选客户端不存在或不在当前账号可见范围")
}
if options.ClientSeenAfter != "" {
online, err := repository.ClientSeenAfter(tx, clientID, options.ClientSeenAfter)
if err != nil {
return result, err
}
if !online {
return result, fmt.Errorf("所选客户端已离线,请刷新页面后选择在线客户端")
}
}
executionMode := options.ExecutionMode
if executionMode == "" {
executionMode = model.TaskExecutionDryRun
@@ -233,9 +219,6 @@ func CreatePurchaseTasksWithOptions(db *sql.DB, actor *model.User, requests []Pu
}
confirmedBy, confirmedAt := "", ""
if executionMode == model.TaskExecutionLive {
if !options.LiveAcknowledged || strings.TrimSpace(options.LiveConfirmation) != LivePurchaseConfirmation {
return result, fmt.Errorf("真实下单必须勾选风险确认并输入“%s”", LivePurchaseConfirmation)
}
purchaseMode, err := repository.ClientPurchaseMode(tx, clientID)
if err != nil {
return result, err
+8 -19
View File
@@ -389,8 +389,7 @@ func TestCreatePurchaseTasksWithOptions_正常采购员可创建真实任务(t *
result, err := CreatePurchaseTasksWithOptions(db, buyer,
[]PurchaseTaskRequest{{SybID: "SYB-LIVE", MaxPriceCent: 4200}},
PurchaseTaskOptions{ClientID: "CLIENT-LIVE", ExecutionMode: model.TaskExecutionLive,
LiveAcknowledged: true, LiveConfirmation: LivePurchaseConfirmation})
PurchaseTaskOptions{ClientID: "CLIENT-LIVE", ExecutionMode: model.TaskExecutionLive})
if err != nil || result.Created != 1 {
t.Fatalf("正常采购员创建真实任务失败: result=%+v err=%v", result, err)
}
@@ -404,7 +403,7 @@ func TestCreatePurchaseTasksWithOptions_正常采购员可创建真实任务(t *
}
}
func TestCreatePurchaseTasksWithOptions_真实模式安全门禁(t *testing.T) {
func TestCreatePurchaseTasksWithOptions_真实模式保留账号和Client能力门禁(t *testing.T) {
db := newTestDB(t)
key := seedPurchasableWorkflow(t, db, "SYB-GATE")
SaveSybMapping(db, "SYB-GATE", key, "USR-1")
@@ -414,16 +413,9 @@ func TestCreatePurchaseTasksWithOptions_真实模式安全门禁(t *testing.T) {
if _, err := CreatePurchaseTasksWithOptions(db, admin, request, PurchaseTaskOptions{
ClientID: "CLIENT-DRY", ExecutionMode: model.TaskExecutionLive,
LiveAcknowledged: true, LiveConfirmation: LivePurchaseConfirmation,
}); err == nil || !strings.Contains(err.Error(), "未声明 live") {
t.Fatalf("dry_run 客户端必须被拒绝: %v", err)
}
if _, err := CreatePurchaseTasksWithOptions(db, admin, request, PurchaseTaskOptions{
ClientID: "CLIENT-DRY", ExecutionMode: model.TaskExecutionLive,
LiveAcknowledged: true, LiveConfirmation: "错误短语",
}); err == nil || !strings.Contains(err.Error(), "必须勾选") {
t.Fatalf("错误确认短语必须被拒绝: %v", err)
}
disabled := *admin
disabled.Status = model.UserDisabled
if _, err := CreatePurchaseTasks(db, &disabled, request, "CLIENT-DRY"); err == nil || !strings.Contains(err.Error(), "不是正常状态") {
@@ -431,7 +423,7 @@ func TestCreatePurchaseTasksWithOptions_真实模式安全门禁(t *testing.T) {
}
}
func TestCreatePurchaseTasksWithOptions_页面打开后客户端离线会被拒绝(t *testing.T) {
func TestCreatePurchaseTasksWithOptions_离线Live客户端可提前指派(t *testing.T) {
db := newTestDB(t)
key := seedPurchasableWorkflow(t, db, "SYB-OFFLINE")
SaveSybMapping(db, "SYB-OFFLINE", key, "USR-1")
@@ -441,13 +433,11 @@ func TestCreatePurchaseTasksWithOptions_页面打开后客户端离线会被拒
t.Fatal(err)
}
_, err := CreatePurchaseTasksWithOptions(db, admin,
result, err := CreatePurchaseTasksWithOptions(db, admin,
[]PurchaseTaskRequest{{SybID: "SYB-OFFLINE", MaxPriceCent: 4200}},
PurchaseTaskOptions{ClientID: "CLIENT-OFFLINE", ExecutionMode: model.TaskExecutionLive,
LiveAcknowledged: true, LiveConfirmation: LivePurchaseConfirmation,
ClientSeenAfter: "2026-08-10T09:00:00Z"})
if err == nil || !strings.Contains(err.Error(), "已离线") {
t.Fatalf("离线客户端必须在创建事务内被拒绝: %v", err)
PurchaseTaskOptions{ClientID: "CLIENT-OFFLINE", ExecutionMode: model.TaskExecutionLive})
if err != nil || result.Created != 1 {
t.Fatalf("离线 live 客户端应可提前指派: result=%+v err=%v", result, err)
}
}
@@ -468,8 +458,7 @@ func TestCreatePurchaseTasksWithOptions_拒绝弹窗打开后变化的规格映
result, err := CreatePurchaseTasksWithOptions(db, admin,
[]PurchaseTaskRequest{{SybID: "SYB-STALE", MaxPriceCent: 3990,
MappingOptionKey: blackKey, ContextVersion: oldVersion}},
PurchaseTaskOptions{ClientID: "CLIENT-LIVE", ExecutionMode: model.TaskExecutionLive,
LiveAcknowledged: true, LiveConfirmation: LivePurchaseConfirmation})
PurchaseTaskOptions{ClientID: "CLIENT-LIVE", ExecutionMode: model.TaskExecutionLive})
if err != nil || result.Created != 0 || len(result.Failures) != 1 ||
!strings.Contains(result.Failures[0].Reason, "已变化") {
t.Fatalf("旧弹窗必须因映射变化被拒绝: result=%+v err=%v", result, err)
+13 -8
View File
@@ -205,20 +205,25 @@ func ListAssignableClients(db *sql.DB, actor *model.User, threshold time.Duratio
return ListClientViewsForUser(db, actor, "", threshold)
}
// ListLivePurchaseClients 返回当前用户能分配真实采购任务的在线客户端。
//
// 页面过滤只用于减少误选;创建任务时仍会在事务内重新校验可见范围和 live 能力。
func ListLivePurchaseClients(db *sql.DB, actor *model.User, threshold time.Duration) ([]ClientView, error) {
// PurchaseClientOptions 是采购弹窗的客户端候选和可选数量。
// 非 live Client 也返回给页面显示禁用原因;创建时仍会在事务内复核能力。
type PurchaseClientOptions struct {
Rows []ClientView
SelectableCount int
}
// ListPurchaseClientOptions 返回当前用户可见的采购客户端。
// live Client 即使暂时离线也可提前指派,等它上线后再领取任务。
func ListPurchaseClientOptions(db *sql.DB, actor *model.User, threshold time.Duration) (*PurchaseClientOptions, error) {
clients, err := ListClientViewsForUser(db, actor, "", threshold)
if err != nil {
return nil, err
}
result := make([]ClientView, 0, len(clients))
result := &PurchaseClientOptions{Rows: clients}
for _, client := range clients {
if client.Status != "在线" || client.PurchaseMode != string(model.TaskExecutionLive) {
continue
if client.PurchaseMode == string(model.TaskExecutionLive) {
result.SelectableCount++
}
result = append(result, client)
}
return result, nil
}