Files
bj_power/bj_power_mes/internal/wmsclient/client.go
T
SunYF 1cc93795ce feat(mes): 添加定时同步可生产数量到WMS及BOM项相关标准字段
- 在main.go中添加定时任务每10分钟同步可生产数量到WMS系统
- 为BomItem实体添加relatedStandard字段及相关CRUD方法
- 为InspectionRecord实体添加reportNo、materialCode、materialName等字段
- 更新ent schema确保新字段的验证和默认值设置
- 添加必要的数据库迁移和字段映射逻辑
2026-09-17 12:49:07 +08:00

381 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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=3id 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
}
// StockAvailable 批量查询物料可用量(调 WMS /api/internal/stock/checkqty 传 0 只取可用量)。
// 返回 materialCode → availQty。用于 MES 侧计算可生产数量(BOM 单台用量在 MES、库存可用量在 WMS)。
func (c *Client) StockAvailable(ctx context.Context, codes []string) (map[string]int, error) {
items := make([]map[string]any, 0, len(codes))
for _, code := range codes {
items = append(items, map[string]any{"materialCode": code, "qty": 0})
}
b, err := json.Marshal(map[string]any{"items": items})
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/api/internal/stock/check", 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 {
Items []struct {
MaterialCode string `json:"materialCode"`
AvailQty int `json:"availQty"`
} `json:"items"`
} `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}
}
out := make(map[string]int, len(body.Data.Items))
for _, it := range body.Data.Items {
out[it.MaterialCode] = it.AvailQty
}
return out, nil
}
// ProducibleItem 可生产数量快照行(MES 计算后推送 WMS 只读展示)
type ProducibleItem struct {
ProductCode string `json:"productCode"`
ProductName string `json:"productName"`
ProducibleQty int `json:"producibleQty"`
ShortText string `json:"shortText"`
ComputedAt int64 `json:"computedAt"`
}
// ProducibleDemand 未来5天物料需求行(推送 WMS,供备料四态「预警」判定)
type ProducibleDemand struct {
MaterialCode string `json:"materialCode"`
Future5Qty int `json:"future5Qty"`
}
// SyncProducible 推送可生产数量快照 + 未来5天需求到 WMS /api/internal/producible/sync(全量覆盖)。
func (c *Client) SyncProducible(ctx context.Context, items []ProducibleItem, demands []ProducibleDemand) error {
return c.post(ctx, "/api/internal/producible/sync", map[string]any{
"items": items, "demands": demands,
})
}
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))
}