package wmsclient import ( "fmt" "bytes" "context" "encoding/json" "net/http" "net/url" "time" ) // Client WMS 内部接口客户端(半成品流转等) type Client struct { baseURL string token string hc *http.Client } func New(baseURL, token string) *Client { if baseURL == "" { baseURL = "http://127.0.0.1:9091" } return &Client{ baseURL: baseURL, token: token, hc: &http.Client{Timeout: 5 * time.Second}, } } // SemiInbound 调 WMS /api/internal/semi/inbound 半成品入库 func (c *Client) SemiInbound(ctx context.Context, sn, orderNo string, doneProcessCodes []int, operator string) error { return c.post(ctx, "/api/internal/semi/inbound", map[string]any{ "sn": sn, "orderNo": orderNo, "doneProcessCodes": doneProcessCodes, "operator": operator, }) } // SemiOutbound 调 WMS /api/internal/semi/outbound 半成品出库(重上线) func (c *Client) SemiOutbound(ctx context.Context, sn, orderNo string, doneProcessCodes []int, operator string) error { return c.post(ctx, "/api/internal/semi/outbound", map[string]any{ "sn": sn, "orderNo": orderNo, "doneProcessCodes": doneProcessCodes, "operator": operator, }) } // FinishedInbound 成品完工回流入库:调 WMS /api/internal/finished/inbound。 // 成品 SN 由 MES 系统生成(工件SN),统一库存主表 category=3;失败返回 err 由调用方降级。 func (c *Client) FinishedInbound(ctx context.Context, productCode, productName, sn, orderNo, operator string) error { return c.post(ctx, "/api/internal/finished/inbound", map[string]any{ "productCode": productCode, "productName": productName, "sn": sn, "orderNo": orderNo, "snSource": "AUTO", "operator": operator, }) } // MaterialExists 批量校验物料编码是否存在于 WMS 物料档案。 // 返回 WMS 中不存在的编码列表(全部存在则为空)。WMS 响应 {code,message,data:{missing:[]}}。 // 失败(网络/超时/非0响应)返回 err,由调用方决定降级策略。 func (c *Client) MaterialExists(ctx context.Context, codes []string) ([]string, error) { b, err := json.Marshal(map[string]any{"codes": codes}) if err != nil { return nil, err } req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/api/internal/material/exists", bytes.NewReader(b)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") req.Header.Set("X-API-TOKEN", c.token) resp, err := c.hc.Do(req) if err != nil { return nil, err } defer resp.Body.Close() var body struct { Code int `json:"code"` Data struct { Missing []string `json:"missing"` } `json:"data"` } if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { return nil, err } if resp.StatusCode >= 300 || body.Code != 0 { return nil, &RespError{Status: resp.StatusCode} } return body.Data.Missing, nil } // ProductTypeRow 产品型号档案行(WMS materials item_type=3) type ProductTypeRow struct { ID int `json:"id"` Code string `json:"code"` Name string `json:"name"` Category string `json:"category"` Remark string `json:"remark"` IsActive bool `json:"isActive"` CreatedAt int64 `json:"createdAt"` UpdatedAt int64 `json:"updatedAt"` } // get 调 WMS GET 接口并解出 {code,data} func (c *Client) get(ctx context.Context, path string, out any) error { req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+path, nil) if err != nil { return err } req.Header.Set("X-API-TOKEN", c.token) resp, err := c.hc.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= 300 { return &RespError{Status: resp.StatusCode} } var body struct { Code int `json:"code"` Data any `json:"data"` } if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { return err } if body.Code != 0 { return &RespError{Status: resp.StatusCode} } if out != nil && body.Data != nil { b, _ := json.Marshal(body.Data) return json.Unmarshal(b, out) } return nil } // putJSON 调 WMS PUT 接口 func (c *Client) putJSON(ctx context.Context, path string, body any) error { b, err := json.Marshal(body) if err != nil { return err } req, err := http.NewRequestWithContext(ctx, http.MethodPut, c.baseURL+path, bytes.NewReader(b)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") req.Header.Set("X-API-TOKEN", c.token) resp, err := c.hc.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= 300 { return &RespError{Status: resp.StatusCode} } return nil } // ProductTypes 全量产品型号(item_type=3,id asc,新建在底部)。isActive 可空。 func (c *Client) ProductTypes(ctx context.Context, isActive string) ([]ProductTypeRow, error) { q := "/api/internal/product-types?" if isActive != "" { q += "isActive=" + isActive } var list []ProductTypeRow if err := c.get(ctx, q, &list); err != nil { return nil, err } return list, nil } // ProductTypePage 分页+搜索(产品型号管理页真分页)。 // 2026-09-08 禁止关键字混搜:编码/名称/类别各自独立模糊。 func (c *Client) ProductTypePage(ctx context.Context, code, name, category, isActive string, page, pageSize int) (int64, []ProductTypeRow, error) { q := fmt.Sprintf("/api/internal/product-types?page=%d&pageSize=%d", page, pageSize) if code != "" { q += "&code=" + url.QueryEscape(code) } if name != "" { q += "&name=" + url.QueryEscape(name) } if category != "" { q += "&category=" + url.QueryEscape(category) } if isActive != "" { q += "&isActive=" + isActive } var data struct { Total int64 `json:"total"` List []ProductTypeRow `json:"list"` } if err := c.get(ctx, q, &data); err != nil { return 0, nil, err } return data.Total, data.List, nil } // CreateProductType 新建产品型号(code 可空由 WMS 自动生成 PROD-日期-流水) func (c *Client) CreateProductType(ctx context.Context, code, name, category, remark string, isActive bool) error { return c.post(ctx, "/api/internal/product-types", map[string]any{ "code": code, "name": name, "category": category, "remark": remark, "isActive": isActive, }) } // UpdateProductType 修改产品型号(编码不可改) func (c *Client) UpdateProductType(ctx context.Context, id int, name, category, remark string, isActive bool) error { return c.putJSON(ctx, "/api/internal/product-types", map[string]any{ "id": id, "name": name, "category": category, "remark": remark, "isActive": isActive, }) } // DeleteProductType 删除产品型号(引用保护由调用方与 WMS 双侧校验) func (c *Client) DeleteProductType(ctx context.Context, id int) error { return c.post(ctx, "/api/internal/product-types/delete", map[string]any{"id": id}) } // ExportProductTypes 拉取产品型号导出 xlsx 字节流(WMS 端执行统一导出时间规则)。 // 编码/名称/类别各自独立模糊筛选(禁止关键字混搜)。 func (c *Client) ExportProductTypes(ctx context.Context, code, name, category, isActive, startDate, endDate string) ([]byte, error) { q := "/api/internal/product-types/export?" if code != "" { q += "code=" + url.QueryEscape(code) + "&" } if name != "" { q += "name=" + url.QueryEscape(name) + "&" } if category != "" { q += "category=" + url.QueryEscape(category) + "&" } if isActive != "" { q += "isActive=" + isActive + "&" } q += "startDate=" + startDate + "&endDate=" + endDate req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+q, nil) if err != nil { return nil, err } req.Header.Set("X-API-TOKEN", c.token) resp, err := c.hc.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode >= 300 { return nil, &RespError{Status: resp.StatusCode} } buf := new(bytes.Buffer) if _, err := buf.ReadFrom(resp.Body); err != nil { return nil, err } return buf.Bytes(), nil } // CreateReturnOrder 调 WMS /api/internal/return-order 预建退库单(工位退料回库房,待 WMS 确认收货)。 func (c *Client) CreateReturnOrder(ctx context.Context, orderNo, sn, materialCode, materialName, spec, reason, operator string, stationNo, qty int, unit string) error { return c.post(ctx, "/api/internal/return-order", map[string]any{ "orderNo": orderNo, "sn": sn, "materialCode": materialCode, "materialName": materialName, "spec": spec, "qty": qty, "unit": unit, "reason": reason, "operator": operator, "stationNo": stationNo, }) } // DisplayOverview 拉取 WMS 免登录看板总览 /api/display/overview。 // 用于看板「仓储动态」屏。WMS 不可达时返回 err,由调用方降级为空对象(看板其余屏不受影响)。 func (c *Client) DisplayOverview(ctx context.Context) (map[string]any, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/api/display/overview", nil) if err != nil { return nil, err } req.Header.Set("X-API-TOKEN", c.token) resp, err := c.hc.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode >= 300 { return nil, &RespError{Status: resp.StatusCode} } var body struct { Code int `json:"code"` Data map[string]any `json:"data"` } if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { return nil, err } if body.Data == nil { return map[string]any{}, nil } return body.Data, nil } func (c *Client) post(ctx context.Context, path string, body any) error { b, err := json.Marshal(body) if err != nil { return err } req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, bytes.NewReader(b)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") req.Header.Set("X-API-TOKEN", c.token) resp, err := c.hc.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= 300 { return &RespError{Status: resp.StatusCode} } return nil } type RespError struct { Status int } func (e *RespError) Error() string { return "wms call failed with status " + string(rune('0'+e.Status)) }