refactor: 统一术语BOM为物料清单并补充工位扩展能力
主要变更: 1. 后端:重构工位管理逻辑,新增工位自定义扩展能力,将工位号上限调整为999并支持14+虚拟工位 2. 前后端:全量替换"BOM"术语为"物料清单",包括提示语、注释和文档 3. WMS模块:新增AGV任务取消接口与前端页面,对接海康RCS取消任务逻辑 4. 权限与种子数据:新增工位新增权限并更新权限配置 5. 修复错误校验逻辑,优化AGV客户端签名与请求处理
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
// Package agv 海康 AGV 调度系统(RCS-2000 V4.2) 对接客户端。
|
||||
// 能力:下发搬运任务(task/submit)、查询任务状态(task/query)。
|
||||
// 能力:下发搬运任务(task/submit)、查询任务状态(task/query)、任务取消(task/cancel)。
|
||||
// 签名协议详见《AGV海康.md》:HMAC-SHA256 加盐 + MD5 二次摘要,sign 放 URL query。
|
||||
// 完整请求路径 = 服务前缀 /rcs/rtas + 业务路径(文档 §2 开头)。
|
||||
// 未配置真实 RCS(appKey/appSecret/BaseURL) 或 EnableMock=true 时走 Mock 客户端。
|
||||
package agv
|
||||
|
||||
@@ -23,12 +24,19 @@ import (
|
||||
"bj_power_wms/internal/config"
|
||||
)
|
||||
|
||||
// ServicePrefix RCS-2000 统一服务前缀(文档 §2:「服务前缀统一:/rcs/rtas」)
|
||||
const ServicePrefix = "/rcs/rtas"
|
||||
|
||||
// Client AGV 调度客户端抽象
|
||||
type Client interface {
|
||||
// SubmitTask 下发搬运任务 fromDock -> toDock,返回海康任务号 robotTaskCode。
|
||||
SubmitTask(ctx context.Context, fromDock, toDock, carrierCode string) (string, error)
|
||||
// QueryTask 查询任务状态,返回海康 taskStatus 枚举。
|
||||
QueryTask(ctx context.Context, robotTaskCode string) (string, error)
|
||||
// CancelTask 取消任务(软取消 CANCEL / 硬取消 DROP)。
|
||||
CancelTask(ctx context.Context, robotTaskCode, cancelType, reason string) error
|
||||
// Disable 是否处于 Mock(未接真实 RCS)模式。
|
||||
Disable() bool
|
||||
}
|
||||
|
||||
// New 依据配置返回真实或 Mock 客户端
|
||||
@@ -42,9 +50,9 @@ func New(cfg config.AgvConfig) Client {
|
||||
// ---------- 签名与请求(真实海康) ----------
|
||||
|
||||
type realClient struct {
|
||||
cfg config.AgvConfig
|
||||
hc *http.Client
|
||||
carrier string
|
||||
cfg config.AgvConfig
|
||||
hc *http.Client
|
||||
carrier string
|
||||
}
|
||||
|
||||
func newRealClient(cfg config.AgvConfig) *realClient {
|
||||
@@ -55,13 +63,17 @@ func newRealClient(cfg config.AgvConfig) *realClient {
|
||||
}
|
||||
}
|
||||
|
||||
// buildPath 拼接服务前缀 + 业务路径,system
|
||||
// url 拼接 RCS 服务前缀 + 业务路径(文档 §2:完整请求路径 = 服务前缀 + 路径)。
|
||||
// 兼容用户把 BaseURL 直接填成已含 /rcs/rtas 的情况,避免重复前缀。
|
||||
func (c *realClient) url(path string) string {
|
||||
base := strings.TrimRight(c.cfg.BaseURL, "/")
|
||||
return base + path
|
||||
if strings.HasSuffix(base, ServicePrefix) {
|
||||
return base + path
|
||||
}
|
||||
return base + ServicePrefix + path
|
||||
}
|
||||
|
||||
// submit 任务下发实现的实际请求
|
||||
// post 任务下发实现的实际请求
|
||||
// 海康路径:/rcs/rtas/api/robot/controller/task/submit
|
||||
func (c *realClient) post(ctx context.Context, path string, payload map[string]any, out any) error {
|
||||
body, err := json.Marshal(payload)
|
||||
@@ -93,7 +105,7 @@ func (c *realClient) post(ctx context.Context, path string, payload map[string]a
|
||||
if err := json.Unmarshal(raw, &envelope); err != nil {
|
||||
return fmt.Errorf("agv parse envelope: %w", err)
|
||||
}
|
||||
if envelope.Code != "" && envelope.Code != "SUCCESS" {
|
||||
if envelope.Code != "" && envelope.Code != "SUCCESS" && envelope.Code != "0" {
|
||||
return fmt.Errorf("agv code=%s message=%s", envelope.Code, envelope.Message)
|
||||
}
|
||||
if out != nil && len(envelope.Data) > 0 {
|
||||
@@ -146,28 +158,33 @@ func (c *realClient) sign(req *http.Request, body []byte, path string) {
|
||||
|
||||
req.Header.Set("Authorization", authHeader)
|
||||
req.Header.Set("X-lr-appkey", c.cfg.AppKey)
|
||||
// X-lr-request-id 是文档标注的唯一「必需」首部字段(1.1 节 是否必需=是)
|
||||
req.Header.Set("X-lr-request-id", requestID)
|
||||
req.Header.Set("X-lr-version", "v1.0")
|
||||
req.Header.Set("X-lr-trace-id", traceID)
|
||||
req.Header.Set("X-lr-source", "bj_power_wms")
|
||||
req.Header.Set("Content-Type", "application/json;charset=UTF-8")
|
||||
}
|
||||
|
||||
// SubmitTask 实现 Client:下发搬运任务。
|
||||
// 海康路径 /rcs/rtas/api/robot/controller/task/submit
|
||||
func (c *realClient) SubmitTask(ctx context.Context, fromDock, toDock, carrierCode string) (string, error) {
|
||||
carrier := carrierCode
|
||||
if carrier == "" {
|
||||
carrier = c.carrier
|
||||
}
|
||||
payload := map[string]any{
|
||||
"taskType": "PF-LMR-COMMON",
|
||||
"targetRoute": []map[string]any{
|
||||
{"type": "SITE", "code": fromDock, "operation": "COLLECT"},
|
||||
{"type": "SITE", "code": toDock, "operation": "DELIVERY"},
|
||||
},
|
||||
"carrierInfo": []map[string]any{
|
||||
}
|
||||
// 载具:给了就带;未给(无托盘搬运)不带 carrierInfo,避免 RCS 按空载具校验失败
|
||||
carrier := carrierCode
|
||||
if carrier == "" {
|
||||
carrier = c.carrier
|
||||
}
|
||||
if carrier != "" {
|
||||
payload["carrierInfo"] = []map[string]any{
|
||||
{"carrierType": "1", "carrierCode": carrier},
|
||||
},
|
||||
}
|
||||
}
|
||||
var data struct {
|
||||
RobotTaskCode string `json:"robotTaskCode"`
|
||||
@@ -206,6 +223,28 @@ func (c *realClient) QueryTask(ctx context.Context, robotTaskCode string) (strin
|
||||
return st, nil
|
||||
}
|
||||
|
||||
// CancelTask 实现 Client:取消任务(文档 2.1.4 任务取消接口)。
|
||||
// cancelType:CANCEL 软取消(可下发回库)/ DROP 人工介入硬取消。
|
||||
func (c *realClient) CancelTask(ctx context.Context, robotTaskCode, cancelType, reason string) error {
|
||||
if robotTaskCode == "" {
|
||||
return fmt.Errorf("缺少任务号")
|
||||
}
|
||||
if cancelType == "" {
|
||||
cancelType = "CANCEL"
|
||||
}
|
||||
payload := map[string]any{
|
||||
"robotTaskCode": robotTaskCode,
|
||||
"cancelType": cancelType,
|
||||
}
|
||||
if reason != "" {
|
||||
payload["reason"] = reason
|
||||
}
|
||||
return c.post(ctx, "/api/robot/controller/task/cancel", payload, nil)
|
||||
}
|
||||
|
||||
// Disable 真实客户端非 Mock 模式
|
||||
func (c *realClient) Disable() bool { return false }
|
||||
|
||||
// ---------- Mock 客户端(未接真实 RCS 时用) ----------
|
||||
|
||||
type mockClient struct{}
|
||||
@@ -222,6 +261,12 @@ func (m *mockClient) QueryTask(ctx context.Context, robotTaskCode string) (strin
|
||||
return "FINISHED", nil
|
||||
}
|
||||
|
||||
func (m *mockClient) CancelTask(ctx context.Context, robotTaskCode, cancelType, reason string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockClient) Disable() bool { return true }
|
||||
|
||||
func randHex(n int) string {
|
||||
b := make([]byte, n)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user