docs(test): 更新电表箱装配业务回归测试文档
- 将文档从代码审计任务转换为端到端业务回归测试链路 - 重新组织文档结构为链路总览、示例数据基线和详细步骤 - 添加完整的业务场景清单涵盖WMS/MES/工位终端三大系统 - 定义统一的测试数据包括产品型号、物料清单、工位派工等 - 提供详细的步骤断言和数量等式验证方法 - 建立贯穿全链路的数据流向和状态变更追踪体系
This commit is contained in:
@@ -225,9 +225,6 @@ func applyColumnPatches(ctx context.Context) {
|
||||
`ALTER TABLE stocktake_items ADD COLUMN IF NOT EXISTS shelf_no text`,
|
||||
`ALTER TABLE stocktake_items ADD COLUMN IF NOT EXISTS layer_no text`,
|
||||
`ALTER TABLE stocktake_items ADD COLUMN IF NOT EXISTS position_no text`,
|
||||
// 接驳台是独立主数据(不依附工位):产线/库房/其他三类;与工位 1:1 绑定。
|
||||
// 部分唯一索引只约束产线接驳台(station_no > 0),库房/其他(station_no=0)不参与。
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS ux_docks_station_no ON docks (station_no) WHERE station_no > 0`,
|
||||
// 入库作废(2026-09-19):入库单可作废,记录原因/人/时间;库存回滚后单仍保留供追溯
|
||||
`ALTER TABLE inbound_orders ADD COLUMN IF NOT EXISTS voided boolean NOT NULL DEFAULT false`,
|
||||
`ALTER TABLE inbound_orders ADD COLUMN IF NOT EXISTS void_reason text`,
|
||||
|
||||
@@ -2,7 +2,6 @@ package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"bj_power_wms/ent"
|
||||
@@ -15,7 +14,8 @@ import (
|
||||
// 生产环境口径(用户裁决 2026-09-20):种子只允许写入系统跑起来所必需的固定主数据,
|
||||
// 严禁预置演示物料/演示库存/演示账号等假业务数据。具体:
|
||||
// - 管理员账号:admin(初始密码 123456,登录后应立即修改);其余账号由管理员在「账号管理」按需创建、分配角色;
|
||||
// - 接驳台:DOCK01~20(产线,DOCKn↔工位n)+ DOCK21(库房收货),系统预置只读主数据(dock.go 不提供增删改,只能靠种子写入)。
|
||||
//
|
||||
// 接驳台主数据已迁 MES(2026-09-21):仓库不再维护 dock 表,也不在此种子写入接驳台。
|
||||
//
|
||||
// 不预置区域/库位:区域是现场库房布局,因项目而异,且「区域维护」页面可增删改,由实施按现场实际建立,不硬编码进种子。
|
||||
// 权限与角色不在此:由 handler.SeedRBAC 幂等初始化(reset-all/seed 命令与 POST /api/rbac/seed 共用同一函数)。
|
||||
@@ -35,30 +35,5 @@ func SeedIfEmpty(client *ent.Client) error {
|
||||
slog.Info("seed: admin user created (admin/123456)")
|
||||
}
|
||||
|
||||
// 2. 接驳台主数据 DOCK01~20(产线, DOCKn↔工位n) + DOCK21(库房)
|
||||
docks, _ := client.Dock.Query().Count(ctx)
|
||||
if docks == 0 {
|
||||
for i := 1; i <= 20; i++ {
|
||||
code := fmt.Sprintf("DOCK%02d", i)
|
||||
client.Dock.Create().
|
||||
SetDockCode(code).
|
||||
SetName("产线接驳台 " + code + "(工位" + fmt.Sprintf("%d", i) + ")").
|
||||
SetDockType("line").
|
||||
SetStationNo(i).
|
||||
SetHasPallet(false).
|
||||
SetStatus("ENABLED").
|
||||
SaveX(ctx)
|
||||
}
|
||||
client.Dock.Create().
|
||||
SetDockCode("DOCK21").
|
||||
SetName("库房收货接驳台").
|
||||
SetDockType("store").
|
||||
SetStationNo(0).
|
||||
SetHasPallet(false).
|
||||
SetStatus("ENABLED").
|
||||
SaveX(ctx)
|
||||
slog.Info("seed: 21 docks created")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
|
||||
"bj_power_wms/ent"
|
||||
"bj_power_wms/ent/agvtask"
|
||||
"bj_power_wms/ent/dock"
|
||||
"bj_power_wms/ent/outboundorder"
|
||||
"bj_power_wms/internal/svc"
|
||||
|
||||
@@ -278,16 +277,14 @@ func agvModeHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
func listDocksHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
query := ctx.EntClient.Dock.Query()
|
||||
if typ := q.Get("dockType"); typ != "" {
|
||||
query = query.Where(dock.DockType(typ))
|
||||
}
|
||||
list, err := query.Order(ent.Asc("id")).All(ctx0())
|
||||
typ := q.Get("dockType")
|
||||
// dock 真源已迁 MES:仓库不再本地维护接驳台,改读 MES 内部接口。
|
||||
list, err := ctx.Mes.ListDocks(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
fail(w, http.StatusInternalServerError, "拉取接驳台失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
// 显式构造,避免 ent omitempty 吞掉 hasPallet=false 导致前端看不到空闲状态
|
||||
// 显式构造,避免 omitempty 吞掉 hasPallet=false 导致前端看不到空闲状态
|
||||
type dockView struct {
|
||||
ID int64 `json:"id"`
|
||||
DockCode string `json:"dockCode"`
|
||||
@@ -300,8 +297,11 @@ func listDocksHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
}
|
||||
views := make([]dockView, 0, len(list))
|
||||
for _, d := range list {
|
||||
if typ != "" && d.DockType != typ {
|
||||
continue
|
||||
}
|
||||
views = append(views, dockView{
|
||||
ID: int64(d.ID),
|
||||
ID: d.ID,
|
||||
DockCode: d.DockCode,
|
||||
Name: d.Name,
|
||||
DockType: d.DockType,
|
||||
@@ -344,28 +344,5 @@ func listAgvMaterialsHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// syncDockPalletHandler 内部接口:MES 同步某接驳台是否有托盘(PLC 托盘传感器)。
|
||||
func syncDockPalletHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return wrapInternal(ctx)(func(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
DockCode string `json:"dockCode"`
|
||||
HasPallet bool `json:"hasPallet"`
|
||||
PalletRef string `json:"palletRef"`
|
||||
}
|
||||
if err := parseJSON(r, &req); err != nil {
|
||||
fail(w, http.StatusBadRequest, "参数错误")
|
||||
return
|
||||
}
|
||||
upd := ctx.EntClient.Dock.Update().
|
||||
Where(dock.DockCode(req.DockCode)).
|
||||
SetHasPallet(req.HasPallet)
|
||||
if req.PalletRef != "" {
|
||||
upd = upd.SetPalletRef(req.PalletRef)
|
||||
}
|
||||
if err := upd.Exec(ctx0()); err != nil {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
ok(w, nil)
|
||||
})
|
||||
}
|
||||
// syncDockPalletHandler 已移除(dock 真源迁 MES):接驳台托盘占用状态由 MES 侧 PLC 传感器实时读取,
|
||||
// WMS 不再持有 dock 表,原 /api/internal/dock/pallet-sync 无调用方,一并删除。
|
||||
@@ -2,132 +2,15 @@ package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"bj_power_wms/ent"
|
||||
"bj_power_wms/ent/dock"
|
||||
"bj_power_wms/internal/svc"
|
||||
)
|
||||
|
||||
// dockRow 接驳台返回 DTO(显式 json 标签,避免依赖 ent 默认编组)。
|
||||
type dockRow struct {
|
||||
DockCode string `json:"dockCode"`
|
||||
Name string `json:"name"`
|
||||
DockType string `json:"dockType"`
|
||||
StationNo int `json:"stationNo"`
|
||||
HasPallet bool `json:"hasPallet"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// listDocksInternalHandler 返回接驳台主数据列表(dock 真源在 WMS)。
|
||||
// MES 工位维护页拉取此接口填充「送达接驳台」下拉,避免写死 DOCK01..20 留后患。
|
||||
func listDocksInternalHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
list, err := ctx.EntClient.Dock.Query().
|
||||
Order(ent.Asc("dock_code")).All(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, "查询接驳台失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
rows := make([]dockRow, 0, len(list))
|
||||
for _, d := range list {
|
||||
rows = append(rows, dockRow{
|
||||
DockCode: d.DockCode,
|
||||
Name: d.Name,
|
||||
DockType: d.DockType,
|
||||
StationNo: d.StationNo,
|
||||
HasPallet: d.HasPallet,
|
||||
Status: d.Status,
|
||||
})
|
||||
}
|
||||
ok(w, map[string]any{"list": rows})
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 接驳台主数据维护 ----------
|
||||
// 接驳台是独立主数据:除产线工位旁,库房、缓存区等其他位置也有接驳台位。
|
||||
// 类型:line=产线(绑定工位,与工位 1:1)/ store=库房 / other=其他(均不绑定工位)。
|
||||
|
||||
// dockMaintRow 接驳台维护页返回行(显式 json,避免 ent omitempty 吞掉 hasPallet=false)
|
||||
type dockMaintRow struct {
|
||||
ID int64 `json:"id"`
|
||||
DockCode string `json:"dockCode"`
|
||||
Name string `json:"name"`
|
||||
DockType string `json:"dockType"`
|
||||
StationNo int `json:"stationNo"`
|
||||
Status string `json:"status"`
|
||||
HasPallet bool `json:"hasPallet"`
|
||||
PalletRef string `json:"palletRef"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
}
|
||||
|
||||
func dockMaintRowOf(d *ent.Dock) dockMaintRow {
|
||||
return dockMaintRow{
|
||||
ID: int64(d.ID),
|
||||
DockCode: d.DockCode,
|
||||
Name: d.Name,
|
||||
DockType: d.DockType,
|
||||
StationNo: d.StationNo,
|
||||
Status: d.Status,
|
||||
HasPallet: d.HasPallet,
|
||||
PalletRef: d.PalletRef,
|
||||
CreatedAt: d.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// listDocksMaintHandler GET /api/docks?dockType=&dockCode=&name=&status=&page=&pageSize=
|
||||
// 接驳台主数据只读分页查询(真分页在 SQL 层),编码精确/名称模糊/状态精确各自独立筛选。
|
||||
func listDocksMaintHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
query := ctx.EntClient.Dock.Query()
|
||||
if typ := q.Get("dockType"); typ != "" {
|
||||
query = query.Where(dock.DockType(typ))
|
||||
}
|
||||
// 列表页禁止关键字混搜:编码精确、名称模糊、状态精确,各字段独立筛选(需求5、N)
|
||||
if code := strings.TrimSpace(q.Get("dockCode")); code != "" {
|
||||
query = query.Where(dock.DockCodeEqualFold(code))
|
||||
}
|
||||
if name := strings.TrimSpace(q.Get("name")); name != "" {
|
||||
query = query.Where(dock.NameContainsFold(name))
|
||||
}
|
||||
if status := strings.TrimSpace(q.Get("status")); status != "" {
|
||||
query = query.Where(dock.StatusEQ(status))
|
||||
}
|
||||
total, err := query.Count(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, "查询接驳台失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
page := atoi(q.Get("page"), 1)
|
||||
size := atoi(q.Get("pageSize"), 20)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if size < 1 || size > 200 {
|
||||
size = 20
|
||||
}
|
||||
list, err := query.
|
||||
Order(ent.Desc("created_at"), ent.Desc("id")).
|
||||
Offset((page - 1) * size).Limit(size).All(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, "查询接驳台失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
rows := make([]dockMaintRow, 0, len(list))
|
||||
for _, d := range list {
|
||||
rows = append(rows, dockMaintRowOf(d))
|
||||
}
|
||||
ok(w, map[string]any{"list": rows, "total": total, "page": page, "pageSize": size})
|
||||
}
|
||||
}
|
||||
|
||||
// 接驳台为系统预置只读主数据(需求5):不提供新增/修改/删除,仅保留查询。
|
||||
// 原 createDockHandler / updateDockHandler / deleteDockHandler / validateDock / dockReq 已彻底移除,
|
||||
// 接驳台数据由 db/seed.go 种子初始化,调整由实施维护种子或数据库完成。
|
||||
// 接驳台主数据真源已迁 MES(2026-09-21):仓库不再维护 dock 表,也不给接驳台派任务。
|
||||
// WMS 仅在「出库目标工位下拉」处只读派生 MES 接驳台中 line 类型(产线接驳台与工位 1:1)。
|
||||
|
||||
// stationOption 出库目标工位下拉项(需求20:目标工位可下拉选择、不允许手输)。
|
||||
// 客户口径「出库工位号来自 MES 工位列表」——WMS 侧以接驳台主数据中 line 类型
|
||||
// 客户口径「出库工位号来自 MES 工位列表」——以 MES 接驳台主数据中 line 类型
|
||||
// (产线接驳台与工位 1:1 绑定)为真源派生工位选项,避免前端写死 1~20。
|
||||
type stationOption struct {
|
||||
StationNo int `json:"stationNo"`
|
||||
@@ -136,18 +19,17 @@ type stationOption struct {
|
||||
}
|
||||
|
||||
// listStationOptionsHandler GET /api/outbound/stations 返回产线工位下拉选项(无权限包裹,供出库页调用)。
|
||||
// 数据改读 MES 接驳台主数据(dock 真源已迁 MES):过滤 line 类型且绑定工位号者。
|
||||
func listStationOptionsHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
list, err := ctx.EntClient.Dock.Query().
|
||||
Where(dock.DockType("line")).
|
||||
Order(ent.Asc("station_no")).All(ctx0())
|
||||
list, err := ctx.Mes.ListDocks(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, "查询工位失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
rows := make([]stationOption, 0, len(list))
|
||||
for _, d := range list {
|
||||
if d.StationNo <= 0 {
|
||||
if d.DockType != "line" || d.StationNo <= 0 {
|
||||
continue
|
||||
}
|
||||
rows = append(rows, stationOption{StationNo: d.StationNo, Name: d.Name, DockCode: d.DockCode})
|
||||
|
||||
@@ -464,6 +464,29 @@ func parseManageMode(s string) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// parseItemType 解析导入模板"品类"列:原材料/半成品/成品/其他(兼容 1/2/3/4);无法识别返回 0。
|
||||
// 注意:"半成品"含"成品"子串,必须先判半成品再判成品。
|
||||
func parseItemType(s string) int {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return 0
|
||||
}
|
||||
switch {
|
||||
case strings.Contains(s, "原材料"):
|
||||
return 1
|
||||
case strings.Contains(s, "半成品"):
|
||||
return 2
|
||||
case strings.Contains(s, "成品"):
|
||||
return 3
|
||||
case strings.Contains(s, "其他"):
|
||||
return 4
|
||||
}
|
||||
if n := atoi(s, 0); n >= 1 && n <= 4 {
|
||||
return n
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// excelMaterialImportHandler 物料档案 Excel 批量导入
|
||||
// 模板(首行忽略表头):图号 | 名称 | 规格 | 单位 | 类型(结构件/电气件) | 说明
|
||||
// (图号=物料编码,问题记录 L18 合并裁决,不再单设图号列;「简称」列已按 2026-09-19 裁决移除)
|
||||
@@ -507,29 +530,71 @@ func excelMaterialImportHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
spec string
|
||||
unit string
|
||||
manageMode int
|
||||
itemType int
|
||||
description string
|
||||
}
|
||||
plans := make([]planRow, 0, len(rows)-1)
|
||||
errs := make([]importErr, 0)
|
||||
|
||||
// 表头→列下标映射:容忍列顺序变化与多余列(如「品类」插在单位与类型之间)。
|
||||
// 先按"去括号后的表头名"精确匹配,再按关键字包含兜底(顺序稳定),都找不到回退模板固定下标。
|
||||
normHeader := func(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
for _, op := range []string{"(", "("} {
|
||||
if i := strings.Index(s, op); i >= 0 {
|
||||
s = s[:i]
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
headerNorm := make([]string, len(rows[0]))
|
||||
for j, h := range rows[0] {
|
||||
headerNorm[j] = normHeader(h)
|
||||
}
|
||||
colOf := func(names []string, fallback int) int {
|
||||
for _, n := range names {
|
||||
for j, hn := range headerNorm {
|
||||
if hn == n {
|
||||
return j
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, n := range names {
|
||||
for j, hn := range headerNorm {
|
||||
if hn != "" && strings.Contains(hn, n) {
|
||||
return j
|
||||
}
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
codeCol := colOf([]string{"图号", "物料编码", "编码"}, 0)
|
||||
nameCol := colOf([]string{"名称", "物料名称"}, 1)
|
||||
specCol := colOf([]string{"规格", "规格型号"}, 2)
|
||||
unitCol := colOf([]string{"单位"}, 3)
|
||||
typeCol := colOf([]string{"类型", "管理类型", "管理粒度"}, 4)
|
||||
catCol := colOf([]string{"品类", "类别"}, -1)
|
||||
descCol := colOf([]string{"说明", "备注", "描述"}, 5)
|
||||
|
||||
// 阶段1:解析 + 校验(不落库)
|
||||
for i, row := range rows {
|
||||
if i == 0 {
|
||||
continue // 表头
|
||||
}
|
||||
cell := func(idx int) string {
|
||||
if idx < len(row) {
|
||||
return strings.TrimSpace(row[idx])
|
||||
if idx < 0 || idx >= len(row) {
|
||||
return ""
|
||||
}
|
||||
return ""
|
||||
return strings.TrimSpace(row[idx])
|
||||
}
|
||||
pr := planRow{row: i + 1}
|
||||
pr.code = cell(0)
|
||||
pr.name = cell(1)
|
||||
pr.spec = cell(2)
|
||||
pr.unit = cell(3)
|
||||
pr.manageMode = parseManageMode(cell(4))
|
||||
pr.description = cell(5)
|
||||
pr.code = cell(codeCol)
|
||||
pr.name = cell(nameCol)
|
||||
pr.spec = cell(specCol)
|
||||
pr.unit = cell(unitCol)
|
||||
pr.manageMode = parseManageMode(cell(typeCol))
|
||||
pr.itemType = parseItemType(cell(catCol))
|
||||
pr.description = cell(descCol)
|
||||
|
||||
if pr.code == "" {
|
||||
errs = append(errs, importErr{pr.row, pr.code, "图号为空"})
|
||||
@@ -551,6 +616,10 @@ func excelMaterialImportHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
errs = append(errs, importErr{pr.row, pr.code, "类型无法识别,请填写「结构件」或「电气件」(兼容 1/2)"})
|
||||
continue
|
||||
}
|
||||
if catCol >= 0 && cell(catCol) != "" && pr.itemType == 0 {
|
||||
errs = append(errs, importErr{pr.row, pr.code, "品类无法识别,请填写「原材料/半成品/成品/其他」(兼容 1/2/3/4)"})
|
||||
continue
|
||||
}
|
||||
exists, _ := ctx.EntClient.Material.Query().
|
||||
Where(material.CodeEQ(pr.code)).Exist(ctx0())
|
||||
if exists {
|
||||
@@ -580,7 +649,7 @@ func excelMaterialImportHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
}()
|
||||
|
||||
for _, pr := range plans {
|
||||
if _, e = tx.Material.Create().
|
||||
mc := tx.Material.Create().
|
||||
SetCode(pr.code).
|
||||
SetName(pr.name).
|
||||
SetNillableSpec(strPtr(pr.spec)).
|
||||
@@ -588,8 +657,11 @@ func excelMaterialImportHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
SetNillableDescription(strPtr(pr.description)).
|
||||
SetManageMode(pr.manageMode).
|
||||
SetIsBatchManaged(pr.manageMode == 1).
|
||||
SetIsSerialManaged(pr.manageMode == 2).
|
||||
Save(ctx0()); e != nil {
|
||||
SetIsSerialManaged(pr.manageMode == 2)
|
||||
if pr.itemType > 0 {
|
||||
mc = mc.SetItemType(pr.itemType)
|
||||
}
|
||||
if _, e = mc.Save(ctx0()); e != nil {
|
||||
fail(w, http.StatusInternalServerError, "创建物料失败: "+e.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -43,7 +43,6 @@ var seedPermissions = []seedPerm{
|
||||
{"role:manage", "角色管理", "MENU", "/roles", "Key", 120, ""},
|
||||
{"eventlog:manage", "操作日志", "MENU", "/event-log", "Clock", 130, ""},
|
||||
{"agv:manage", "AGV配送", "MENU", "/agv", "Van", 135, ""},
|
||||
{"dock:manage", "接驳台维护", "MENU", "/dock", "Aim", 136, ""},
|
||||
{"attachment:view", "附件中心", "MENU", "/attachment", "Folder", 138, ""},
|
||||
// ===== 按钮权限 =====
|
||||
{"inbound:create", "批量入库", "BUTTON", "", "", 200, "inbound:view"},
|
||||
@@ -73,9 +72,6 @@ var seedPermissions = []seedPerm{
|
||||
{"role:delete", "删除角色", "BUTTON", "", "", 272, "role:manage"},
|
||||
{"agv:dispatch", "下发AGV", "BUTTON", "", "", 280, "agv:manage"},
|
||||
{"return:confirm", "确认收货", "BUTTON", "", "", 205, "return:view"},
|
||||
{"dock:add", "新增接驳台", "BUTTON", "", "", 290, "dock:manage"},
|
||||
{"dock:edit", "编辑接驳台", "BUTTON", "", "", 291, "dock:manage"},
|
||||
{"dock:delete", "删除接驳台", "BUTTON", "", "", 292, "dock:manage"},
|
||||
{"attachment:manage", "归档清理", "BUTTON", "", "", 295, "attachment:view"},
|
||||
// 注:导出权限已按菜单拆分(inbound/outbound/inspection/inventory:export),
|
||||
// 不再使用全局 *:export,以更精细地控制“谁能在哪个菜单导出”。
|
||||
|
||||
@@ -136,13 +136,8 @@ func RegisterHandlers(server *rest.Server, ctx *svc.ServiceContext) {
|
||||
},
|
||||
)
|
||||
|
||||
// 接驳台主数据(独立主数据:产线/库房/其他,产线类型与工位 1:1 绑定)
|
||||
server.AddRoutes(
|
||||
[]rest.Route{
|
||||
// 接驳台为系统预置只读主数据(需求5):仅保留查询,不提供新增/修改/删除
|
||||
{Method: http.MethodGet, Path: "/api/docks", Handler: requirePerm(ctx, "dock:manage", listDocksMaintHandler(ctx))},
|
||||
},
|
||||
)
|
||||
// 接驳台主数据已迁 MES(2026-09-21):仓库不再维护 dock 表,也不给接驳台派任务。
|
||||
// 出库目标工位下拉(/api/outbound/stations)、AGV 页接驳台列表(/api/agv/docks) 均改读 MES 内部接口。
|
||||
|
||||
// 区域(多级父子结构:区域/货架/层/位置号 4 个 Tab)
|
||||
server.AddRoutes(
|
||||
@@ -244,9 +239,7 @@ func RegisterHandlers(server *rest.Server, ctx *svc.ServiceContext) {
|
||||
{Method: http.MethodPost, Path: "/api/internal/product-types/delete", Handler: wrapInternal(ctx)(deleteProductTypeInternalHandler(ctx))},
|
||||
{Method: http.MethodPost, Path: "/api/internal/material/exists", Handler: wrapInternal(ctx)(materialExistsHandler(ctx))},
|
||||
{Method: http.MethodPost, Path: "/api/internal/material/types", Handler: wrapInternal(ctx)(materialTypesHandler(ctx))},
|
||||
{Method: http.MethodPost, Path: "/api/internal/dock/pallet-sync", Handler: syncDockPalletHandler(ctx)},
|
||||
{Method: http.MethodPost, Path: "/api/internal/return-order", Handler: wrapInternal(ctx)(createReturnOrderInternalHandler(ctx))},
|
||||
{Method: http.MethodGet, Path: "/api/internal/docks", Handler: wrapInternal(ctx)(listDocksInternalHandler(ctx))},
|
||||
{Method: http.MethodPost, Path: "/api/internal/material/backflush", Handler: wrapInternal(ctx)(backflushInternalHandler(ctx))},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -162,16 +162,20 @@ func zoneStockMaps(ctx *svc.ServiceContext) (byZone, byShelf, byLayer, byPos map
|
||||
return
|
||||
}
|
||||
|
||||
// zoneNodeReq 新建层级节点请求
|
||||
// zoneNodeReq 新建层级节点请求。
|
||||
// 父级以完整路径给出(区域→货架→层),后端据此精确定位父级,避免仅凭 code 跨父级歧义。
|
||||
type zoneNodeReq struct {
|
||||
Level int `json:"level"`
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
ParentCode string `json:"parentCode"`
|
||||
Remark string `json:"remark"`
|
||||
Level int `json:"level"`
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
ZoneCode string `json:"zoneCode"` // 所属区域编码(level>=2 必填)
|
||||
ShelfNo string `json:"shelfNo"` // 所属货架号(level>=3 必填)
|
||||
LayerNo string `json:"layerNo"` // 所属层号(level=4 必填)
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// createZoneNodeHandler 新建层级节点(按 level 解析上级并写冗余路径;建货架自动建默认位置)
|
||||
// createZoneNodeHandler 新建层级节点(按完整路径 区域→货架→层 精确定位父级并写冗余路径)。
|
||||
// 父级必须存在且不能为空;位置号(level4)必须挂在已存在的层下(不再有“无层默认位置”)。
|
||||
func createZoneNodeHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req zoneNodeReq
|
||||
@@ -181,7 +185,9 @@ func createZoneNodeHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
}
|
||||
req.Code = strings.TrimSpace(req.Code)
|
||||
req.Name = strings.TrimSpace(req.Name)
|
||||
req.ParentCode = strings.TrimSpace(req.ParentCode)
|
||||
req.ZoneCode = strings.ToUpper(strings.TrimSpace(req.ZoneCode))
|
||||
req.ShelfNo = strings.TrimSpace(req.ShelfNo)
|
||||
req.LayerNo = strings.TrimSpace(req.LayerNo)
|
||||
req.Remark = strings.TrimSpace(req.Remark)
|
||||
if req.Level < 1 || req.Level > 4 {
|
||||
fail(w, http.StatusBadRequest, "level 必须为 1~4")
|
||||
@@ -199,55 +205,55 @@ func createZoneNodeHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// 逐级解析父级:父级必须已存在,路径(区域+货架+层)唯一确定,避免仅凭 code 跨父级歧义。
|
||||
var zoneCode, shelfNo, layerNo, parentCode string
|
||||
parentCode = req.ParentCode
|
||||
switch req.Level {
|
||||
case 1:
|
||||
zoneCode = req.Code
|
||||
parentCode = ""
|
||||
case 2:
|
||||
if req.ParentCode == "" {
|
||||
if req.ZoneCode == "" {
|
||||
fail(w, http.StatusBadRequest, "请选择所属区域")
|
||||
return
|
||||
}
|
||||
reg, err := ctx.EntClient.Zone.Query().Where(zone.LevelEQ(1), zone.CodeEQ(req.ParentCode)).Only(ctx0())
|
||||
reg, err := ctx.EntClient.Zone.Query().Where(zone.LevelEQ(1), zone.CodeEQ(req.ZoneCode)).Only(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusBadRequest, "所属区域不存在:"+req.ParentCode)
|
||||
fail(w, http.StatusBadRequest, "所属区域不存在:"+req.ZoneCode)
|
||||
return
|
||||
}
|
||||
zoneCode = reg.ZoneCode
|
||||
shelfNo = req.Code
|
||||
parentCode = reg.Code
|
||||
case 3:
|
||||
if req.ParentCode == "" {
|
||||
fail(w, http.StatusBadRequest, "请选择所属货架")
|
||||
if req.ZoneCode == "" || req.ShelfNo == "" {
|
||||
fail(w, http.StatusBadRequest, "请选择所属区域与货架")
|
||||
return
|
||||
}
|
||||
sh, err := ctx.EntClient.Zone.Query().Where(zone.LevelEQ(2), zone.CodeEQ(req.ParentCode)).Only(ctx0())
|
||||
sh, err := ctx.EntClient.Zone.Query().
|
||||
Where(zone.LevelEQ(2), zone.ZoneCodeEQ(req.ZoneCode), zone.CodeEQ(req.ShelfNo)).Only(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusBadRequest, "所属货架不存在:"+req.ParentCode)
|
||||
fail(w, http.StatusBadRequest, "所属货架不存在:"+req.ZoneCode+"/"+req.ShelfNo)
|
||||
return
|
||||
}
|
||||
zoneCode = sh.ZoneCode
|
||||
shelfNo = sh.ShelfNo
|
||||
layerNo = req.Code
|
||||
parentCode = sh.Code
|
||||
case 4:
|
||||
if req.ParentCode == "" {
|
||||
fail(w, http.StatusBadRequest, "请选择所属层或货架(默认位置)")
|
||||
if req.ZoneCode == "" || req.ShelfNo == "" || req.LayerNo == "" {
|
||||
fail(w, http.StatusBadRequest, "请选择所属层")
|
||||
return
|
||||
}
|
||||
parent, err := ctx.EntClient.Zone.Query().
|
||||
Where(zone.CodeEQ(req.ParentCode)).Where(zone.Or(zone.LevelEQ(3), zone.LevelEQ(2))).Only(ctx0())
|
||||
ly, err := ctx.EntClient.Zone.Query().
|
||||
Where(zone.LevelEQ(3), zone.ZoneCodeEQ(req.ZoneCode), zone.ShelfNoEQ(req.ShelfNo), zone.CodeEQ(req.LayerNo)).Only(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusBadRequest, "所属层/货架不存在:"+req.ParentCode)
|
||||
fail(w, http.StatusBadRequest, "所属层不存在:"+req.ZoneCode+"/"+req.ShelfNo+"/"+req.LayerNo)
|
||||
return
|
||||
}
|
||||
zoneCode = parent.ZoneCode
|
||||
shelfNo = parent.ShelfNo
|
||||
if parent.Level == 3 {
|
||||
layerNo = parent.Code
|
||||
} else {
|
||||
layerNo = "" // 默认位置:无层
|
||||
}
|
||||
zoneCode = ly.ZoneCode
|
||||
shelfNo = ly.ShelfNo
|
||||
layerNo = ly.Code
|
||||
parentCode = ly.Code
|
||||
}
|
||||
|
||||
// 唯一性:同层级 + 同区域 + 同货架 + 同层 + 同编码 不可重复
|
||||
@@ -275,15 +281,6 @@ func createZoneNodeHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// 建货架(level2)时自动创建一个「默认位置」(level4,挂在货架下、无层),物料可不细分时挂这里
|
||||
if req.Level == 2 {
|
||||
_, _ = ctx.EntClient.Zone.Create().
|
||||
SetLevel(4).SetCode("默认").SetParentCode(req.Code).
|
||||
SetZoneCode(zoneCode).SetShelfNo(shelfNo).SetNillableLayerNo(strPtr("")).
|
||||
SetPositionNo("默认").SetName("默认位置").SetStatus("启用").
|
||||
Save(ctx0())
|
||||
}
|
||||
|
||||
ctx.EventLog.Write(ctx0(), "zone.create", r.Header.Get("X-Username"), "zone", req.Code,
|
||||
"新建"+zoneLevelName(req.Level)+" "+req.Code, nil)
|
||||
ok(w, toZoneNodeVO(z))
|
||||
@@ -459,17 +456,18 @@ func listZonesForPicker(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
type zoneBatchGenReq struct {
|
||||
ZoneCode string `json:"zoneCode"`
|
||||
ShelfNo string `json:"shelfNo"`
|
||||
LayerStart int `json:"layerStart"`
|
||||
LayerEnd int `json:"layerEnd"`
|
||||
LayerStart int `json:"layerStart"` // level=3 批量生成层:层范围起
|
||||
LayerEnd int `json:"layerEnd"` // level=3 批量生成层:层范围止
|
||||
LayerNo string `json:"layerNo"` // level=4 批量生成位置号:指定的已存在层(只在该层下生成位置,不生成父级)
|
||||
PosStart int `json:"posStart"`
|
||||
PosEnd int `json:"posEnd"`
|
||||
Level int `json:"level"` // 3=批量生成层(level3) 4=批量生成位置号(level4,默认,同时确保层存在)
|
||||
Level int `json:"level"` // 3=批量生成层(level3) 4=批量生成位置号(level4,默认,需指定已存在的层)
|
||||
Preview bool `json:"preview"`
|
||||
}
|
||||
|
||||
// batchGenerateHandler 批量生成库位节点:选区域→货架→范围,先预览后提交。
|
||||
// level=3:只生成「层」节点(按层范围);level=4(默认):生成「位置号」节点(层×位置),并自动确保层节点存在。
|
||||
// 已存在的节点跳过不重复建。
|
||||
// level=3:只生成「层」节点(按层范围);level=4(默认):在「已存在的具体层」下生成「位置号」节点。
|
||||
// 批量生成只生成自己这一级,绝不自动创建父级;已存在的节点跳过不重复建。
|
||||
func batchGenerateHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req zoneBatchGenReq
|
||||
@@ -487,14 +485,6 @@ func batchGenerateHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
if level != 3 {
|
||||
level = 4
|
||||
}
|
||||
if req.LayerStart < 0 || req.LayerEnd < req.LayerStart {
|
||||
fail(w, http.StatusBadRequest, "层起止不合法(起始需 ≤ 结束,且 ≥ 0)")
|
||||
return
|
||||
}
|
||||
if level == 4 && (req.PosStart < 0 || req.PosEnd < req.PosStart) {
|
||||
fail(w, http.StatusBadRequest, "位置起止不合法(起始需 ≤ 结束,且 ≥ 0)")
|
||||
return
|
||||
}
|
||||
sh, err := ctx.EntClient.Zone.Query().Where(zone.LevelEQ(2), zone.ZoneCodeEQ(req.ZoneCode), zone.CodeEQ(req.ShelfNo)).Only(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusBadRequest, "货架不存在:"+req.ZoneCode+"/"+req.ShelfNo)
|
||||
@@ -502,13 +492,16 @@ func batchGenerateHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
}
|
||||
zoneCode := sh.ZoneCode
|
||||
|
||||
layers := make([]int, 0)
|
||||
for l := req.LayerStart; l <= req.LayerEnd; l++ {
|
||||
layers = append(layers, l)
|
||||
}
|
||||
|
||||
// ---------- level=3:批量生成层节点 ----------
|
||||
// ---------- level=3:批量生成层节点(父级货架必须已存在,只生成层自己) ----------
|
||||
if level == 3 {
|
||||
if req.LayerStart < 0 || req.LayerEnd < req.LayerStart {
|
||||
fail(w, http.StatusBadRequest, "层起止不合法(起始需 ≤ 结束,且 ≥ 0)")
|
||||
return
|
||||
}
|
||||
layers := make([]int, 0)
|
||||
for l := req.LayerStart; l <= req.LayerEnd; l++ {
|
||||
layers = append(layers, l)
|
||||
}
|
||||
if len(layers) > 2000 {
|
||||
fail(w, http.StatusBadRequest, "一次最多生成 2000 个层,请缩小范围")
|
||||
return
|
||||
@@ -552,11 +545,26 @@ func batchGenerateHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
}
|
||||
|
||||
// ---------- level=4:批量生成位置号 ----------
|
||||
// 只允许在「已存在的具体层」下批量生成位置号(批量生成只生成自己,不创建父级层)。
|
||||
layerNo := strings.TrimSpace(req.LayerNo)
|
||||
if layerNo == "" {
|
||||
fail(w, http.StatusBadRequest, "请选择具体的层")
|
||||
return
|
||||
}
|
||||
if req.PosStart < 0 || req.PosEnd < req.PosStart {
|
||||
fail(w, http.StatusBadRequest, "位置起止不合法(起始需 ≤ 结束,且 ≥ 0)")
|
||||
return
|
||||
}
|
||||
if exist, _ := ctx.EntClient.Zone.Query().
|
||||
Where(zone.LevelEQ(3), zone.ZoneCodeEQ(zoneCode), zone.ShelfNoEQ(req.ShelfNo), zone.CodeEQ(layerNo)).Exist(ctx0()); !exist {
|
||||
fail(w, http.StatusBadRequest, "层不存在:"+req.ShelfNo+"架"+layerNo+"层,请先在「层」页签创建该层后再生成位置号")
|
||||
return
|
||||
}
|
||||
poss := make([]int, 0)
|
||||
for p := req.PosStart; p <= req.PosEnd; p++ {
|
||||
poss = append(poss, p)
|
||||
}
|
||||
if len(layers)*len(poss) > 2000 {
|
||||
if len(poss) > 2000 {
|
||||
fail(w, http.StatusBadRequest, "一次最多生成 2000 个位置,请缩小范围")
|
||||
return
|
||||
}
|
||||
@@ -568,55 +576,41 @@ func batchGenerateHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
PositionName string `json:"positionName"`
|
||||
Exists bool `json:"exists"`
|
||||
}
|
||||
preview := make([]genItem, 0, len(layers)*len(poss))
|
||||
for _, l := range layers {
|
||||
ls := strconv.Itoa(l)
|
||||
for _, p := range poss {
|
||||
ps := strconv.Itoa(p)
|
||||
exists, _ := ctx.EntClient.Zone.Query().
|
||||
Where(zone.LevelEQ(4), zone.ZoneCodeEQ(zoneCode), zone.ShelfNoEQ(req.ShelfNo),
|
||||
zone.LayerNoEQ(ls), zone.CodeEQ(ps)).Exist(ctx0())
|
||||
preview = append(preview, genItem{LayerNo: ls, PositionNo: ps, PositionName: req.ShelfNo + "架" + ls + "层" + ps + "位", Exists: exists})
|
||||
}
|
||||
preview := make([]genItem, 0, len(poss))
|
||||
for _, p := range poss {
|
||||
ps := strconv.Itoa(p)
|
||||
exists, _ := ctx.EntClient.Zone.Query().
|
||||
Where(zone.LevelEQ(4), zone.ZoneCodeEQ(zoneCode), zone.ShelfNoEQ(req.ShelfNo),
|
||||
zone.LayerNoEQ(layerNo), zone.CodeEQ(ps)).Exist(ctx0())
|
||||
preview = append(preview, genItem{LayerNo: layerNo, PositionNo: ps, PositionName: req.ShelfNo + "架" + layerNo + "层" + ps + "位", Exists: exists})
|
||||
}
|
||||
if req.Preview {
|
||||
ok(w, map[string]any{"preview": true, "list": preview, "total": len(preview)})
|
||||
return
|
||||
}
|
||||
|
||||
// 提交:确保层节点存在 + 创建不存在的位置号
|
||||
// 提交:仅在选定层下创建不存在的位置号(层必须已存在,不再自动创建父级层)
|
||||
created, skipped := 0, 0
|
||||
for _, l := range layers {
|
||||
ls := strconv.Itoa(l)
|
||||
if exist, _ := ctx.EntClient.Zone.Query().Where(zone.LevelEQ(3), zone.ZoneCodeEQ(zoneCode), zone.ShelfNoEQ(req.ShelfNo), zone.CodeEQ(ls)).Exist(ctx0()); !exist {
|
||||
if _, err := ctx.EntClient.Zone.Create().
|
||||
SetLevel(3).SetCode(ls).SetParentCode(req.ShelfNo).
|
||||
SetZoneCode(zoneCode).SetShelfNo(req.ShelfNo).SetLayerNo(ls).SetStatus("启用").Save(ctx0()); err != nil {
|
||||
fail(w, http.StatusInternalServerError, "创建层失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
for _, p := range poss {
|
||||
ps := strconv.Itoa(p)
|
||||
if exist, _ := ctx.EntClient.Zone.Query().
|
||||
Where(zone.LevelEQ(4), zone.ZoneCodeEQ(zoneCode), zone.ShelfNoEQ(req.ShelfNo),
|
||||
zone.LayerNoEQ(layerNo), zone.CodeEQ(ps)).Exist(ctx0()); exist {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
for _, p := range poss {
|
||||
ps := strconv.Itoa(p)
|
||||
if exist, _ := ctx.EntClient.Zone.Query().
|
||||
Where(zone.LevelEQ(4), zone.ZoneCodeEQ(zoneCode), zone.ShelfNoEQ(req.ShelfNo),
|
||||
zone.LayerNoEQ(ls), zone.CodeEQ(ps)).Exist(ctx0()); exist {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
if _, err := ctx.EntClient.Zone.Create().
|
||||
SetLevel(4).SetCode(ps).SetParentCode(ls).
|
||||
SetZoneCode(zoneCode).SetShelfNo(req.ShelfNo).SetLayerNo(ls).SetPositionNo(ps).
|
||||
SetName(req.ShelfNo + "架" + ls + "层" + ps + "位").SetStatus("启用").Save(ctx0()); err != nil {
|
||||
fail(w, http.StatusInternalServerError, "创建位置失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
created++
|
||||
if _, err := ctx.EntClient.Zone.Create().
|
||||
SetLevel(4).SetCode(ps).SetParentCode(layerNo).
|
||||
SetZoneCode(zoneCode).SetShelfNo(req.ShelfNo).SetLayerNo(layerNo).SetPositionNo(ps).
|
||||
SetName(req.ShelfNo + "架" + layerNo + "层" + ps + "位").SetStatus("启用").Save(ctx0()); err != nil {
|
||||
fail(w, http.StatusInternalServerError, "创建位置失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
created++
|
||||
}
|
||||
ctx.EventLog.Write(ctx0(), "zone.batch", r.Header.Get("X-Username"), "zone", req.ShelfNo,
|
||||
"批量生成位置 "+req.ZoneCode+"/"+req.ShelfNo+" 层"+strconv.Itoa(req.LayerStart)+"-"+strconv.Itoa(req.LayerEnd)+
|
||||
"批量生成位置 "+req.ZoneCode+"/"+req.ShelfNo+" 层"+layerNo+
|
||||
" 位"+strconv.Itoa(req.PosStart)+"-"+strconv.Itoa(req.PosEnd)+":新建"+strconv.Itoa(created)+" 跳过"+strconv.Itoa(skipped), nil)
|
||||
ok(w, map[string]any{"preview": false, "created": created, "skipped": skipped, "total": len(layers) * len(poss)})
|
||||
ok(w, map[string]any{"preview": false, "created": created, "skipped": skipped, "total": len(poss)})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,6 +133,54 @@ func (c *Client) FetchProducible(ctx context.Context) (*ProducibleResult, error)
|
||||
return &env.Data, nil
|
||||
}
|
||||
|
||||
// DockRow MES 接驳台主数据行(dock 真源已迁 MES,WMS 仅只读取用)。
|
||||
type DockRow struct {
|
||||
ID int64 `json:"id"`
|
||||
DockCode string `json:"dockCode"`
|
||||
Name string `json:"name"`
|
||||
DockType string `json:"dockType"`
|
||||
StationNo int `json:"stationNo"`
|
||||
Status string `json:"status"`
|
||||
HasPallet bool `json:"hasPallet"`
|
||||
PalletRef string `json:"palletRef"`
|
||||
}
|
||||
|
||||
// ListDocks 拉取 MES 接驳台主数据(dock 真源在 MES)。
|
||||
// WMS 出库目标工位下拉 / AGV 页接驳台列表均读此接口,仓库不再本地维护 dock 表。
|
||||
func (c *Client) ListDocks(ctx context.Context) ([]DockRow, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/api/internal/docks", 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()
|
||||
var env struct {
|
||||
Code int `json:"code"`
|
||||
Data struct {
|
||||
List []DockRow `json:"list"`
|
||||
} `json:"data"`
|
||||
Msg string `json:"message"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&env); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if env.Code != 0 {
|
||||
msg := env.Msg
|
||||
if msg == "" {
|
||||
msg = "MES 返回失败"
|
||||
}
|
||||
return nil, errors.New(msg)
|
||||
}
|
||||
if env.Data.List == nil {
|
||||
return []DockRow{}, nil
|
||||
}
|
||||
return env.Data.List, nil
|
||||
}
|
||||
|
||||
// MarkStatus 回写备料单状态(DELIVERING / DONE)
|
||||
func (c *Client) MarkStatus(ctx context.Context, requestNo, status string) error {
|
||||
b, _ := json.Marshal(map[string]string{"requestNo": requestNo, "status": status})
|
||||
|
||||
Reference in New Issue
Block a user