feat: 固定创建真实采购任务 (#112)
This commit is contained in:
@@ -325,6 +325,36 @@ func TestClientAssignment_一人多客户端并按采购员隔离列表(t *testi
|
||||
}
|
||||
}
|
||||
|
||||
func TestListLivePurchaseClients_只返回可见在线且声明Live的客户端(t *testing.T) {
|
||||
db := newTestDB(t)
|
||||
admin, buyerA, _ := prepareClientAssignmentUsers(t, db)
|
||||
now := time.Now().UTC()
|
||||
clients := []model.Client{
|
||||
{ClientID: "live-online", Name: "真实在线", LastSeenAt: now.Format(model.TimeLayout), Capabilities: `{"purchase_mode":"live"}`},
|
||||
{ClientID: "dry-online", Name: "演练在线", LastSeenAt: now.Format(model.TimeLayout), Capabilities: `{"purchase_mode":"dry_run"}`},
|
||||
{ClientID: "live-offline", Name: "真实离线", LastSeenAt: now.Add(-time.Hour).Format(model.TimeLayout), Capabilities: `{"purchase_mode":"live"}`},
|
||||
}
|
||||
for _, client := range clients {
|
||||
if err := RegisterClient(db, client, true); err != nil {
|
||||
t.Fatalf("登记客户端失败: %v", err)
|
||||
}
|
||||
if _, _, err := AssignClient(db, admin, client.ClientID, buyerA.UserID, now); err != nil {
|
||||
t.Fatalf("绑定客户端失败: %v", err)
|
||||
}
|
||||
}
|
||||
if _, err := db.Exec(`UPDATE clients SET last_seen_at=? WHERE client_id=?`, now.Add(-time.Hour).Format(model.TimeLayout), "live-offline"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rows, err := ListLivePurchaseClients(db, buyerA, time.Minute)
|
||||
if err != nil {
|
||||
t.Fatalf("读取真实采购客户端失败: %v", err)
|
||||
}
|
||||
if len(rows) != 1 || rows[0].ClientID != "live-online" {
|
||||
t.Fatalf("只应返回在线 live 客户端,实际 %+v", rows)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientAssignment_转交解绑保留审计且不改任务(t *testing.T) {
|
||||
db := newTestDB(t)
|
||||
admin, buyerA, buyerB := prepareClientAssignmentUsers(t, db)
|
||||
|
||||
@@ -170,6 +170,7 @@ type PurchaseTaskOptions struct {
|
||||
ExecutionMode model.TaskExecutionMode
|
||||
LiveAcknowledged bool
|
||||
LiveConfirmation string
|
||||
ClientSeenAfter string
|
||||
}
|
||||
|
||||
// PurchaseTaskResult 同时返回已创建数量和每条无法创建的原因。
|
||||
@@ -212,6 +213,15 @@ 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
|
||||
|
||||
@@ -426,6 +426,26 @@ func TestCreatePurchaseTasksWithOptions_真实模式安全门禁(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreatePurchaseTasksWithOptions_页面打开后客户端离线会被拒绝(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
|
||||
_, 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)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -205,6 +205,24 @@ 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) {
|
||||
clients, err := ListClientViewsForUser(db, actor, "", threshold)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]ClientView, 0, len(clients))
|
||||
for _, client := range clients {
|
||||
if client.Status != "在线" || client.PurchaseMode != string(model.TaskExecutionLive) {
|
||||
continue
|
||||
}
|
||||
result = append(result, client)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ListActivePurchasers 返回管理员可选择的绑定目标。
|
||||
func ListActivePurchasers(db *sql.DB, actor *model.User) ([]model.User, error) {
|
||||
if actor == nil || !actor.IsAdmin() {
|
||||
|
||||
Reference in New Issue
Block a user