1. 移除所有多字段关键字混搜,改为各筛选字段独立查询 2. 调整excel导出相关逻辑,优化文件名处理与导入模板 3. 修复物料导入的类型解析与token获取逻辑 4. 更新依赖与前端页面文案、筛选参数
307 lines
9.3 KiB
Go
307 lines
9.3 KiB
Go
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
|
||
}
|
||
|
||
// 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))
|
||
} |