feat: 固定创建真实采购任务 (#112)

This commit is contained in:
chengma
2026-08-10 17:52:48 +08:00
parent 29b4491afe
commit e1b32ea024
20 changed files with 185 additions and 82 deletions
+30
View File
@@ -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)
+10
View File
@@ -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
+20
View File
@@ -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)
+18
View File
@@ -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() {