feat: 移除 Client 真实采购手工授权 (#114)

This commit is contained in:
chengma
2026-08-10 18:37:35 +08:00
parent 8188153b08
commit 10bb892e6c
29 changed files with 124 additions and 702 deletions
+2
View File
@@ -88,6 +88,8 @@
## 数据与接口
- Admin 新建采购任务固定为真实下单(不支付),当前账号可见的全部 Client 都允许被指派;创建时不得按 Client 的 `purchase_mode` 禁用候选或拒绝任务。`purchase_mode` 只用于领取期判断 Client 的自动运行就绪能力。
- 表结构以 [03 数据模型](../docs/admin/03-data-model.md) 为准。
- 给 Client 的接口以 [docs/client/04-admin-api-contract.md](../docs/client/04-admin-api-contract.md) 为准。
- `[必须]` **金额一律用整数存**,人民币用分、台币用分,字段名带单位后缀(`_cent`)。
+4 -4
View File
@@ -181,10 +181,10 @@ func (h *Handler) renderSybListWithLoginReason(c *gin.Context, keyword, pageRaw,
keyword, dateFrom, dateTo),
"HistoryFetchPagination": sybHistoryPartialPagination(history.Page, history.TotalPages,
keyword, dateFrom, dateTo),
"Pagination": service.NewPaginationView(result.Page, result.TotalPages, values.Encode()),
"DetailURL": "/syb/detail?" + detailValues.Encode(),
"AssignableClients": purchaseClients.Rows,
"LivePurchaseClientCount": purchaseClients.SelectableCount,
"Pagination": service.NewPaginationView(result.Page, result.TotalPages, values.Encode()),
"DetailURL": "/syb/detail?" + detailValues.Encode(),
"AssignableClients": purchaseClients.Rows,
"PurchaseClientCount": purchaseClients.SelectableCount,
}))
}
+6 -6
View File
@@ -159,7 +159,7 @@ func TestMainPagesReturnOK(t *testing.T) {
}
for _, client := range []model.Client{
{ClientID: "CLIENT-LIVE", Name: "真实采购机", Capabilities: `{"purchase_mode":"live"}`},
{ClientID: "CLIENT-DRY", Name: "未授权机", Capabilities: `{"purchase_mode":"dry_run"}`},
{ClientID: "CLIENT-DRY", Name: "未就绪机", Capabilities: `{"purchase_mode":"dry_run"}`},
} {
if err := service.RegisterClient(db, client, true); err != nil {
t.Fatalf("准备采购客户端失败: %v", err)
@@ -180,17 +180,17 @@ func TestMainPagesReturnOK(t *testing.T) {
`name="date_from"`, `name="date_to"`,
`name="stage"`, "处理阶段", "下一步", `data-detail-url=`,
"按顺运宝货运单创建日期(UTC+8)同步",
"创建真实采购任务", "未启用真实采购",
"创建真实采购任务", "所有当前账号可见的客户端均可指派",
} {
if !strings.Contains(sybResponse.Body.String(), want) {
t.Errorf("顺运宝页面缺少 %q", want)
}
}
for _, want := range []string{
`value="CLIENT-LIVE" selected`,
`value="CLIENT-DRY" disabled`,
"未授权机(CLIENT-DRY) — 未启用真实采购",
"离线客户端也可以提前指派",
`value="CLIENT-LIVE"`,
`value="CLIENT-DRY"`,
"未就绪机(CLIENT-DRY) — 在线",
"未就绪或离线客户端会等待其就绪后领取",
} {
if !strings.Contains(sybResponse.Body.String(), want) {
t.Errorf("采购客户端候选缺少 %q", want)
+4 -4
View File
@@ -325,7 +325,7 @@ func TestClientAssignment_一人多客户端并按采购员隔离列表(t *testi
}
}
func TestListPurchaseClientOptions_返回可见客户端且离线Live仍可选(t *testing.T) {
func TestListPurchaseClientOptions_全部可见客户端均可选(t *testing.T) {
db := newTestDB(t)
admin, buyerA, _ := prepareClientAssignmentUsers(t, db)
now := time.Now().UTC()
@@ -350,11 +350,11 @@ func TestListPurchaseClientOptions_返回可见客户端且离线Live仍可选(t
if err != nil {
t.Fatalf("读取采购客户端候选失败: %v", err)
}
if len(options.Rows) != 3 || options.SelectableCount != 2 {
t.Fatalf("应显示全部可见客户端且两个 live 可选,实际 %+v", options)
if len(options.Rows) != 3 || options.SelectableCount != 3 {
t.Fatalf("应显示全部可见客户端且全部可选,实际 %+v", options)
}
if options.Rows[2].ClientID != "live-offline" || options.Rows[2].Status != "离线" {
t.Fatalf("离线 live 客户端必须保留在候选中,实际 %+v", options.Rows)
t.Fatalf("离线客户端必须保留在候选中,实际 %+v", options.Rows)
}
}
-7
View File
@@ -219,13 +219,6 @@ func CreatePurchaseTasksWithOptions(db *sql.DB, actor *model.User, requests []Pu
}
confirmedBy, confirmedAt := "", ""
if executionMode == model.TaskExecutionLive {
purchaseMode, err := repository.ClientPurchaseMode(tx, clientID)
if err != nil {
return result, err
}
if purchaseMode != string(model.TaskExecutionLive) {
return result, fmt.Errorf("所选客户端未声明 live 能力,不能执行真实采购任务")
}
confirmedBy, confirmedAt = actor.UserID, model.NowISO()
}
+5 -4
View File
@@ -403,7 +403,7 @@ func TestCreatePurchaseTasksWithOptions_正常采购员可创建真实任务(t *
}
}
func TestCreatePurchaseTasksWithOptions_真实模式保留账号和Client能力门禁(t *testing.T) {
func TestCreatePurchaseTasksWithOptions_真实模式不要求Client预先声明Live(t *testing.T) {
db := newTestDB(t)
key := seedPurchasableWorkflow(t, db, "SYB-GATE")
SaveSybMapping(db, "SYB-GATE", key, "USR-1")
@@ -411,10 +411,11 @@ func TestCreatePurchaseTasksWithOptions_真实模式保留账号和Client能力
RegisterClient(db, model.Client{ClientID: "CLIENT-DRY", Capabilities: `{"purchase_mode":"dry_run"}`}, true)
request := []PurchaseTaskRequest{{SybID: "SYB-GATE", MaxPriceCent: 4200}}
if _, err := CreatePurchaseTasksWithOptions(db, admin, request, PurchaseTaskOptions{
result, err := CreatePurchaseTasksWithOptions(db, admin, request, PurchaseTaskOptions{
ClientID: "CLIENT-DRY", ExecutionMode: model.TaskExecutionLive,
}); err == nil || !strings.Contains(err.Error(), "未声明 live") {
t.Fatalf("dry_run 客户端必须被拒绝: %v", err)
})
if err != nil || result.Created != 1 {
t.Fatalf("当前账号可见客户端不应被历史能力状态阻止: result=%+v err=%v", result, err)
}
disabled := *admin
disabled.Status = model.UserDisabled
+1 -5
View File
@@ -220,11 +220,7 @@ func ListPurchaseClientOptions(db *sql.DB, actor *model.User, threshold time.Dur
return nil, err
}
result := &PurchaseClientOptions{Rows: clients}
for _, client := range clients {
if client.PurchaseMode == string(model.TaskExecutionLive) {
result.SelectableCount++
}
}
result.SelectableCount = len(clients)
return result, nil
}
+5 -5
View File
@@ -171,10 +171,10 @@
<div class="field">
<label for="purchase-client">执行客户端</label>
<select id="purchase-client" name="client_id" required autofocus>
<option value="">请选择可执行真实采购的客户端</option>
{{range .AssignableClients}}<option value="{{.ClientID}}" {{if ne .PurchaseMode "live"}}disabled{{else if eq $.LivePurchaseClientCount 1}}selected{{end}}>{{.Name}}({{.ClientID}}){{if ne .PurchaseMode "live"}} — 未启用真实采购{{end}}</option>{{end}}
</select>
{{if eq .LivePurchaseClientCount 0}}<small class="missing">当前没有可执行真实采购的客户端。请先在 Client 设置页启用真实采购并重新登记。</small>{{else}}<small>离线客户端也可以提前指派,待其上线后领取。</small>{{end}}
<option value="">请选择执行客户端</option>
{{range .AssignableClients}}<option value="{{.ClientID}}" {{if eq $.PurchaseClientCount 1}}selected{{end}}>{{.Name}}({{.ClientID}}) — {{.Status}}</option>{{end}}
</select>
{{if eq .PurchaseClientCount 0}}<small class="missing">当前账号没有可见的客户端,请先联系管理员绑定客户端。</small>{{else}}<small>所有当前账号可见的客户端均可指派;未就绪或离线客户端会等待其就绪后领取。</small>{{end}}
</div>
<p class="hint">价格上限单位是人民币元,默认取已映射 PDD 规格的采集价;不会使用顺运宝的台币售价。</p>
{{range .Rows}}
@@ -194,7 +194,7 @@
</div>
<div class="modal-foot">
<button type="button" data-modal-close>取消</button>
<button type="submit" class="primary" data-purchase-submit {{if eq .LivePurchaseClientCount 0}}disabled{{end}}>创建真实采购任务</button>
<button type="submit" class="primary" data-purchase-submit {{if eq .PurchaseClientCount 0}}disabled{{end}}>创建真实采购任务</button>
</div>
</form>
</div>