feat: 五项目业务实现并对接完成
- MES(B): 工单/BOM/备料/工序字典/PLC下发/拧紧/扫码报工/半成品/AGV/追溯逻辑,WMS与海康RCS客户端,看板Redis缓存API(概览/设备/进度/报警/趋势)+SSE - WMS(C): JWT滑动续签、Excel导入、盘点、内部API、种子数据、独立Postgres配置 - WMS客户端(E): Go网关8891反向代理+内嵌Vue3十页 - 工位终端(D): SQLite本地缓存+模拟拧紧源+内嵌Vue3页面 - Dashboard(A): 看板数据改接MES内部缓存API,SSE实时刷新+vite代理 - 清理各项目球形磨遗留代码,新增部署手册.md
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
package hikrcs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
/*
|
||||
海康 RCS-2000(调度系统)HTTP API 客户端。
|
||||
签名规则(《AGV海康.md》数字签名授权):
|
||||
1. 请求头需带:AUTHORIZATION / HOST / X-LR-APPKEY / X-LR-REQUEST-ID / X-LR-VERSION
|
||||
(存在其他以 X-LR- 开头的头时也一并按 key 排序参与拼接)
|
||||
2. 待签名字符串 = "METHOD PATH HTTP/1.1\n" + 各 HEADER 行按 key 升序逐行 (KEY: VALUE \n)
|
||||
3. 签名 = md5( HMAC-SHA256( AppSecret, 待签字符串 ) ) 小写 hex
|
||||
正式对接前若厂商样例算法细节略有出入,只需调整 buildSignature() 一处。
|
||||
*/
|
||||
|
||||
type Client struct {
|
||||
BaseURL string // 如 http://192.168.1.210:8099
|
||||
AppKey string
|
||||
AppSecret string
|
||||
Version string // X-LR-VERSION,如 v1.0
|
||||
HTTP *http.Client
|
||||
}
|
||||
|
||||
func New(baseURL, appKey, appSecret, version string) *Client {
|
||||
return &Client{
|
||||
BaseURL: strings.TrimRight(baseURL, "/"),
|
||||
AppKey: appKey,
|
||||
AppSecret: appSecret,
|
||||
Version: version,
|
||||
HTTP: &http.Client{Timeout: 15 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
// SubmitTaskBody 提交任务请求体
|
||||
type SubmitTaskBody struct {
|
||||
TaskCode string `json:"taskCode"` // 任务号(唯一)
|
||||
PrjCode string `json:"prjCode"` // 项目编码(多项目调度需要)
|
||||
Pri int `json:"pri"` // 优先级 0~14,默认 4
|
||||
Task any `json:"task"` // 任务类型定义(子任务/模板ID等),暂放由调用方传
|
||||
}
|
||||
|
||||
func (c *Client) sign(method, path string, headers map[string]string) string {
|
||||
keys := make([]string, 0, len(headers))
|
||||
for k := range headers {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
var b strings.Builder
|
||||
b.WriteString(strings.ToUpper(method))
|
||||
b.WriteString(" ")
|
||||
b.WriteString(path)
|
||||
b.WriteString(" HTTP/1.1\n")
|
||||
for _, k := range keys {
|
||||
b.WriteString(k)
|
||||
b.WriteString(": ")
|
||||
b.WriteString(headers[k])
|
||||
b.WriteString("\n")
|
||||
}
|
||||
content := []byte(b.String())
|
||||
mac := hmac.New(sha256.New, []byte(c.AppSecret))
|
||||
mac.Write(content)
|
||||
sum := mac.Sum(nil)
|
||||
digest := md5.Sum(sum)
|
||||
return hex.EncodeToString(digest[:])
|
||||
}
|
||||
|
||||
func (c *Client) do(method, path string, body any) (json.RawMessage, error) {
|
||||
u := c.BaseURL + path
|
||||
|
||||
host := ""
|
||||
if p, err := url.Parse(c.BaseURL); err == nil {
|
||||
host = p.Host
|
||||
}
|
||||
requestID := strings.ToUpper(uuid.NewString())
|
||||
|
||||
headers := map[string]string{
|
||||
"AUTHORIZATION": "Bearer " + c.AppKey,
|
||||
"HOST": host,
|
||||
"X-LR-APPKEY": c.AppKey,
|
||||
"X-LR-REQUEST-ID": requestID,
|
||||
"X-LR-VERSION": c.Version,
|
||||
}
|
||||
signature := c.sign(method, path, headers)
|
||||
|
||||
var rd io.Reader
|
||||
if body != nil {
|
||||
buf, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rd = bytes.NewReader(buf)
|
||||
}
|
||||
req, err := http.NewRequest(method, u, rd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
for k, v := range headers {
|
||||
http.CanonicalHeaderKey(k)
|
||||
req.Header.Set(canonical(k), v)
|
||||
}
|
||||
req.Header.Set("X-LR-SIGNATURE", signature)
|
||||
|
||||
resp, err := c.HTTP.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("RCS 不可达(%s): %w", path, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
raw, _ := io.ReadAll(resp.Body)
|
||||
|
||||
var out struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
ErrorMessage string `json:"errorMessage"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
_ = json.Unmarshal(raw, &out)
|
||||
if resp.StatusCode >= 400 || (out.Code != "" && out.Code != "0") {
|
||||
msg := out.Message
|
||||
if msg == "" {
|
||||
msg = out.ErrorMessage
|
||||
}
|
||||
return out.Data, fmt.Errorf("RCS 返回错误(%s): code=%s %s", path, out.Code, truncateStr(msg, 200))
|
||||
}
|
||||
return out.Data, nil
|
||||
}
|
||||
|
||||
// canonical 将全大写签名头名转成常规首字母大写形式
|
||||
func canonical(s string) string {
|
||||
s = strings.ToLower(s)
|
||||
parts := strings.Split(s, "-")
|
||||
for i, p := range parts {
|
||||
if p != "" {
|
||||
parts[i] = strings.ToUpper(p[:1]) + p[1:]
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, "-")
|
||||
}
|
||||
|
||||
func truncateStr(s string, n int) string {
|
||||
if len(s) > n {
|
||||
return s[:n]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Submit 提交搬运任务
|
||||
func (c *Client) Submit(taskCode, prjCode string, pri int, task any) (json.RawMessage, error) {
|
||||
body := map[string]any{"taskCode": taskCode, "pri": pri, "task": task}
|
||||
if prjCode != "" {
|
||||
body["prjCode"] = prjCode
|
||||
}
|
||||
return c.do(http.MethodPost, "/rcs/rtas/api/robot/controller/task/submit", body)
|
||||
}
|
||||
|
||||
// QueryTask 查询任务状态
|
||||
func (c *Client) QueryTask(taskCode string) (json.RawMessage, error) {
|
||||
path := fmt.Sprintf("/rcs/rtas/api/robot/controller/task/query?taskCode=%s", url.QueryEscape(taskCode))
|
||||
return c.do(http.MethodGet, path, nil)
|
||||
}
|
||||
Reference in New Issue
Block a user