feat(production): 新增排产支撑功能并移除手动报工页面
- 在main.go中更新排产物料需求注释说明,明确WMS与MES系统间交互方式 - 从前端help.js中移除手动报工(helpScan)相关帮助文档 - 更新工艺流程、绩效报表等页面帮助文档中的报工流程说明 - 在主布局中添加排产支撑菜单项并移除手动报工菜单项 - 新增ProducibleSupport.vue组件实现排产支撑页面功能 - 移除Scan.vue手动报工页面组件 - 更新相关帮助文档内容以匹配新的业务流程
This commit is contained in:
@@ -43,9 +43,6 @@ var pathPermMap = map[string]string{
|
||||
// 列表/预览/下载不设按钮门槛(有菜单权限即可查阅)
|
||||
"POST:/api/v1/attachment/delete": "sys.attachment:manage",
|
||||
"POST:/api/v1/attachment/archive": "sys.attachment:manage",
|
||||
// 装机绑定(报工前扫料/撤销,属报工操作域)
|
||||
"POST:/api/v1/binds": "produce.scan",
|
||||
"POST:/api/v1/binds/remove": "produce.scan",
|
||||
// 账号管理(拆分自原 sys.rbac:user;sys.account 菜单下增/改/删)
|
||||
"POST:/api/v1/users": "sys.account:add",
|
||||
"PUT:/api/v1/users": "sys.account:edit",
|
||||
|
||||
@@ -37,22 +37,6 @@ func doBind(svcCtx *svc.ServiceContext, r *http.Request) (any, error) {
|
||||
return logic.New(svcCtx).BindWorkpiece(r.Context(), req.Sn, req.OrderNo, req.ProcessCode, stationNo, op, req.Items)
|
||||
}
|
||||
|
||||
// BindHandler POST /api/v1/binds —— MES 手动报工页/巡检录绑
|
||||
func BindHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := doBind(svcCtx, r)
|
||||
if err != nil {
|
||||
httpx.Fail(w, 3309, err.Error())
|
||||
return
|
||||
}
|
||||
if data == nil {
|
||||
httpx.BadRequest(w, "sn / processCode 必填")
|
||||
return
|
||||
}
|
||||
httpx.Ok(w, data)
|
||||
}
|
||||
}
|
||||
|
||||
// BindInternalHandler POST /api/internal/workpiece/binds —— 工位终端扫码绑料
|
||||
func BindInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -83,17 +67,6 @@ func doUnbind(svcCtx *svc.ServiceContext, r *http.Request) error {
|
||||
return logic.New(svcCtx).RemoveBind(r.Context(), req.Sn, req.ProcessCode, req.MaterialCode, req.BindValue, operator(r, req.Operator))
|
||||
}
|
||||
|
||||
// UnbindHandler POST /api/v1/binds/remove —— 撤销误绑(未报工前)
|
||||
func UnbindHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := doUnbind(svcCtx, r); err != nil {
|
||||
httpx.Fail(w, 3310, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OkMessage(w, "已撤销绑定", nil)
|
||||
}
|
||||
}
|
||||
|
||||
// UnbindInternalHandler POST /api/internal/workpiece/binds/remove
|
||||
func UnbindInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -117,23 +90,6 @@ func bindPanelFromReq(svcCtx *svc.ServiceContext, r *http.Request) (any, error)
|
||||
return logic.New(svcCtx).StationBindPanelData(r.Context(), sn, processCode)
|
||||
}
|
||||
|
||||
// BindPanelHandler GET /api/v1/bind-panel?sn=&processCode=
|
||||
// 手动报工页在报工前拉取"本工件本工序应装什么料、已装什么",展示引导并做齐套校验。
|
||||
func BindPanelHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := bindPanelFromReq(svcCtx, r)
|
||||
if err != nil {
|
||||
httpx.Fail(w, 3308, err.Error())
|
||||
return
|
||||
}
|
||||
if data == nil {
|
||||
httpx.BadRequest(w, "缺少 sn / processCode 参数")
|
||||
return
|
||||
}
|
||||
httpx.Ok(w, data)
|
||||
}
|
||||
}
|
||||
|
||||
// BindPanelInternalHandler GET /api/internal/workpiece/bind-panel?sn=&processCode=
|
||||
// 工位终端(12 个触控一体机)扫码工件后拉取绑定面板,驱动工人扫码绑料。
|
||||
func BindPanelInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
|
||||
@@ -126,24 +126,7 @@ func TorqueStatHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 报工记录 ----------
|
||||
|
||||
// ReportRecordsHandler GET /workpiece/process/records?sn=&orderNo=&stationNo=&page=&pageSize=
|
||||
// 报工记录列表:直查真实报工表 workpiece_process,手动报工与工位终端报工均在此可见(报工闭环)。
|
||||
func ReportRecordsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
data, total, err := logic.New(svcCtx).ListProcessRecords(r.Context(), q.Get("sn"), q.Get("orderNo"),
|
||||
atoiDefault(q.Get("stationNo"), 0), atoiDefault(q.Get("page"), 1), atoiDefault(q.Get("pageSize"), 20))
|
||||
if err != nil {
|
||||
httpx.Fail(w, 3206, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Ok(w, map[string]any{"list": data, "total": total})
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessStepsHandler 查询工位的工序步骤(参数采集模板),供手动报工页动态渲染。
|
||||
// ProcessStepsHandler 查询工位的工序步骤(参数采集模板),供巡检终端等按工位动态渲染。
|
||||
// 按 stationNo 加载该工位启用工艺流程的工序步骤。
|
||||
func ProcessStepsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -2,19 +2,39 @@ package production
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"bj_power_mes/common/httpx"
|
||||
"bj_power_mes/internal/logic"
|
||||
"bj_power_mes/internal/svc"
|
||||
)
|
||||
|
||||
// ProducibleInternalHandler 内部接口:WMS 打开「可生产数量」时实时拉取。
|
||||
// 不做定时/批量同步——WMS 需要时来拉一次,MES 现算现返(BOM 单台用量在 MES、可用量在 WMS)。
|
||||
func ProducibleInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
// MaterialDemandInternalHandler 内部接口:WMS「缺料补货」视图实时拉取排产物料需求量。
|
||||
// 数据链路:daily_plan → work_order → BOM,按物料汇总未来 N 天需求量。WMS 拿到后自己比本地可用量。
|
||||
func MaterialDemandInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
res, err := logic.New(svcCtx).ComputeProducible(r.Context())
|
||||
days := 0
|
||||
if s := r.URL.Query().Get("days"); s != "" {
|
||||
if n, e := strconv.Atoi(s); e == nil {
|
||||
days = n
|
||||
}
|
||||
}
|
||||
res, err := logic.New(svcCtx).ComputeMaterialDemand(r.Context(), days)
|
||||
if err != nil {
|
||||
httpx.Fail(w, 3401, err.Error())
|
||||
httpx.Fail(w, 3402, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Ok(w, res)
|
||||
}
|
||||
}
|
||||
|
||||
// ProducibleSupportHandler 用户态接口:MES「排产支撑」视图。
|
||||
// 返回逐成品可生产套数 + 整体还能满产多少天/支撑到哪天/最先告罄短板物料。
|
||||
func ProducibleSupportHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
res, err := logic.New(svcCtx).ComputeProducibleSupport(r.Context())
|
||||
if err != nil {
|
||||
httpx.Fail(w, 3403, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Ok(w, res)
|
||||
|
||||
@@ -31,21 +31,6 @@ func OnlineWorkpieceHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func ReportWorkpieceProcessHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req logic.ReportProcessReq
|
||||
if err := httpx.ParseJSON(r, &req); err != nil {
|
||||
httpx.BadRequest(w, "请求体解析失败")
|
||||
return
|
||||
}
|
||||
if err := logic.New(svcCtx).ReportProcess(r.Context(), req, operator(r, "")); err != nil {
|
||||
httpx.Fail(w, 3302, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OkMessage(w, "报工成功", nil)
|
||||
}
|
||||
}
|
||||
|
||||
func DoneWorkpieceHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req logic.DoneReq
|
||||
|
||||
@@ -114,9 +114,9 @@ func RegisterProductionRoutes(server *rest.Server, serverCtx *svc.ServiceContext
|
||||
server.AddRoute(rest.Route{
|
||||
Method: http.MethodPost, Path: "/api/internal/dock/pallet/set", Handler: internal(production.SetDockPalletInternalHandler(serverCtx)),
|
||||
})
|
||||
// 可生产数量:WMS 打开页面时实时拉取(不做定时推送)
|
||||
// 排产物料需求量:WMS「缺料补货」视图实时拉取(?days=N 覆盖窗口,默认5天)
|
||||
server.AddRoute(rest.Route{
|
||||
Method: http.MethodGet, Path: "/api/internal/producible", Handler: internal(production.ProducibleInternalHandler(serverCtx)),
|
||||
Method: http.MethodGet, Path: "/api/internal/material-demand", Handler: internal(production.MaterialDemandInternalHandler(serverCtx)),
|
||||
})
|
||||
|
||||
// ---------- 产线点位/移料(供工位终端调用,X-API-TOKEN 保护)----------
|
||||
@@ -196,18 +196,13 @@ func RegisterProductionRoutes(server *rest.Server, serverCtx *svc.ServiceContext
|
||||
{Method: http.MethodPost, Path: "/torque/audit", Handler: production.TorqueAuditHandler(serverCtx)},
|
||||
{Method: http.MethodGet, Path: "/torque/audit-log", Handler: production.TorqueAuditLogHandler(serverCtx)},
|
||||
|
||||
{Method: http.MethodGet, Path: "/workpiece/process/records", Handler: production.ReportRecordsHandler(serverCtx)},
|
||||
{Method: http.MethodGet, Path: "/process-steps", Handler: production.ProcessStepsHandler(serverCtx)},
|
||||
|
||||
{Method: http.MethodPost, Path: "/workpiece/online", Handler: production.OnlineWorkpieceHandler(serverCtx)},
|
||||
{Method: http.MethodPost, Path: "/workpiece/process/report", Handler: production.ReportWorkpieceProcessHandler(serverCtx)},
|
||||
{Method: http.MethodPost, Path: "/workpiece/done", Handler: production.DoneWorkpieceHandler(serverCtx)},
|
||||
{Method: http.MethodGet, Path: "/workpieces", Handler: production.ListWorkpiecesHandler(serverCtx)},
|
||||
{Method: http.MethodGet, Path: "/workpieces/in-process", Handler: production.InProcessHandler(serverCtx)},
|
||||
{Method: http.MethodGet, Path: "/trace", Handler: production.TraceHandler(serverCtx)},
|
||||
{Method: http.MethodGet, Path: "/bind-panel", Handler: production.BindPanelHandler(serverCtx)},
|
||||
{Method: http.MethodPost, Path: "/binds", Handler: production.BindHandler(serverCtx)},
|
||||
{Method: http.MethodPost, Path: "/binds/remove", Handler: production.UnbindHandler(serverCtx)},
|
||||
|
||||
{Method: http.MethodPost, Path: "/semi/flow", Handler: production.SemiFlowHandler(serverCtx)},
|
||||
{Method: http.MethodGet, Path: "/semi/flows", Handler: production.ListSemiFlowsHandler(serverCtx)},
|
||||
@@ -274,6 +269,9 @@ func RegisterProductionRoutes(server *rest.Server, serverCtx *svc.ServiceContext
|
||||
{Method: http.MethodGet, Path: "/station-process", Handler: production.ListStationProcessHandler(serverCtx)},
|
||||
{Method: http.MethodPost, Path: "/station-process/save", Handler: production.SaveStationProcessHandler(serverCtx)},
|
||||
{Method: http.MethodGet, Path: "/station-process/route", Handler: production.RouteStationProcessHandler(serverCtx)},
|
||||
|
||||
// ---------- 排产支撑视图(§A2 / §D1):可生产套数 + 支撑天数 + 最先告罄短板物料 ----------
|
||||
{Method: http.MethodGet, Path: "/production/producible-support", Handler: production.ProducibleSupportHandler(serverCtx)},
|
||||
}
|
||||
server.AddRoutes(
|
||||
rest.WithMiddlewares([]rest.Middleware{permissionGuard(serverCtx)}, jwtRoutes...),
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"bj_power_mes/ent/dailyplan"
|
||||
"bj_power_mes/ent/producttype"
|
||||
"bj_power_mes/ent/workorder"
|
||||
"bj_power_mes/internal/wmsclient"
|
||||
)
|
||||
@@ -19,14 +20,44 @@ import (
|
||||
type producibleBomItem struct {
|
||||
materialCode string
|
||||
materialName string
|
||||
unit string // 计量单位(来自 BOM,与物料档案单位一致)
|
||||
unitQty float64 // 单台用量
|
||||
}
|
||||
|
||||
// ProducibleResult 可生产数量计算结果(供 WMS 按需实时拉取)
|
||||
type ProducibleResult struct {
|
||||
Items []wmsclient.ProducibleItem `json:"items"`
|
||||
Demands []wmsclient.ProducibleDemand `json:"demands"`
|
||||
ComputedAt int64 `json:"computedAt"`
|
||||
Items []wmsclient.ProducibleItem `json:"items"`
|
||||
Demands []wmsclient.ProducibleDemand `json:"demands"`
|
||||
Avail map[string]int `json:"avail"` // 各物料 WMS 可用量(供排产支撑逐日消耗模拟复用)
|
||||
ComputedAt int64 `json:"computedAt"`
|
||||
}
|
||||
|
||||
// MaterialDemandRow MES 排产物料需求行(按物料汇总,未来 N 天窗口)。
|
||||
// WMS「缺料补货」视图实时拉此数据,对比本地可用库存得出排产缺料量。
|
||||
type MaterialDemandRow struct {
|
||||
MaterialCode string `json:"materialCode"`
|
||||
MaterialName string `json:"materialName"`
|
||||
Unit string `json:"unit"`
|
||||
DemandQty int `json:"demandQty"` // 未来 N 天排产总需求(台数×单台用量,向上取整)
|
||||
StartDate string `json:"startDate"` // 需求窗口起(YYYY-MM-DD)
|
||||
EndDate string `json:"endDate"` // 需求窗口止(YYYY-MM-DD)
|
||||
}
|
||||
|
||||
// ProducibleSupportItem 排产支撑:单个成品的可生产套数与短板物料说明
|
||||
type ProducibleSupportItem struct {
|
||||
ProductCode string `json:"productCode"`
|
||||
ProductName string `json:"productName"`
|
||||
ProducibleQty int `json:"producibleQty"`
|
||||
ShortText string `json:"shortText"`
|
||||
}
|
||||
|
||||
// ProducibleSupportResult 排产支撑总览:逐成品可生产套数 + 整体还能满产多少天/支撑到哪天/最先告罄短板物料
|
||||
type ProducibleSupportResult struct {
|
||||
Items []ProducibleSupportItem `json:"items"`
|
||||
SupportDays int `json:"supportDays"`
|
||||
SupportUntil string `json:"supportUntil"`
|
||||
ShortBoardMaterial string `json:"shortBoardMaterial"`
|
||||
ComputedAt int64 `json:"computedAt"`
|
||||
}
|
||||
|
||||
// ComputeProducible 计算「可生产数量」+「未来5天物料需求」,结果直接返回(不做定时推送)。
|
||||
@@ -41,52 +72,20 @@ func (s *Service) ComputeProducible(ctx context.Context) (*ProducibleResult, err
|
||||
if s.ctx.Wms == nil || s.ctx.EntClient == nil {
|
||||
return nil, errors.New("未配置 WMS 地址,无法计算可生产数量")
|
||||
}
|
||||
all, err := s.ctx.EntClient.BomItem.Query().All(ctx)
|
||||
chosen, materialSet, err := s.buildChosenBom(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(all) == 0 {
|
||||
return &ProducibleResult{Items: []wmsclient.ProducibleItem{}, Demands: []wmsclient.ProducibleDemand{}, ComputedAt: time.Now().Unix()}, nil
|
||||
if len(chosen) == 0 {
|
||||
return &ProducibleResult{
|
||||
Items: []wmsclient.ProducibleItem{}, Demands: []wmsclient.ProducibleDemand{},
|
||||
Avail: map[string]int{}, ComputedAt: time.Now().Unix(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 1) 按 产品编码 → BOM名称 → 明细行 分组
|
||||
byProd := map[string]map[string][]producibleBomItem{}
|
||||
for _, b := range all {
|
||||
if b.ProductCode == "" || b.MaterialCode == "" {
|
||||
continue
|
||||
}
|
||||
bn := b.BomName
|
||||
if bn == "" {
|
||||
bn = DefaultBOMName
|
||||
}
|
||||
if byProd[b.ProductCode] == nil {
|
||||
byProd[b.ProductCode] = map[string][]producibleBomItem{}
|
||||
}
|
||||
byProd[b.ProductCode][bn] = append(byProd[b.ProductCode][bn], producibleBomItem{
|
||||
materialCode: b.MaterialCode, materialName: b.MaterialName, unitQty: b.UnitQty,
|
||||
})
|
||||
}
|
||||
|
||||
// 2) 每个产品选定一份 BOM:优先「默认」,否则按名排序取第一份
|
||||
chosen := map[string][]producibleBomItem{}
|
||||
materialSet := map[string]bool{}
|
||||
for pc, boms := range byProd {
|
||||
var bn string
|
||||
if _, has := boms[DefaultBOMName]; has {
|
||||
bn = DefaultBOMName
|
||||
} else {
|
||||
names := make([]string, 0, len(boms))
|
||||
for n := range boms {
|
||||
names = append(names, n)
|
||||
}
|
||||
sort.Strings(names)
|
||||
bn = names[0]
|
||||
}
|
||||
chosen[pc] = boms[bn]
|
||||
for _, it := range boms[bn] {
|
||||
materialSet[it.materialCode] = true
|
||||
}
|
||||
}
|
||||
// §A2:可生产套数只算启用成品(停用产品不再展示产能)。
|
||||
// materialSet 保持完整——排产消耗模拟(simulateSupport)按真实排产消耗全部 BOM 物料。
|
||||
s.dropInactiveProducts(ctx, chosen)
|
||||
|
||||
// 3) 拉取 WMS 各物料可用量
|
||||
codes := make([]string, 0, len(materialSet))
|
||||
@@ -175,21 +174,132 @@ func (s *Service) ComputeProducible(ctx context.Context) (*ProducibleResult, err
|
||||
// 6) 未来5天物料需求(备料四态「预警」数据源)
|
||||
demands := s.computeFuture5Demand(ctx, chosen)
|
||||
|
||||
return &ProducibleResult{Items: items, Demands: demands, ComputedAt: now}, nil
|
||||
return &ProducibleResult{Items: items, Demands: demands, Avail: avail, ComputedAt: now}, nil
|
||||
}
|
||||
|
||||
// computeFuture5Demand 近5天(含今日)日排产台数 × 单台用量,按物料汇总为未来5天需求量。
|
||||
// 数据链路:daily_plan(工单号→台数) → work_order(工单号→产品编码) → BOM(产品编码→物料单台用量)。
|
||||
func (s *Service) computeFuture5Demand(ctx context.Context, chosen map[string][]producibleBomItem) []wmsclient.ProducibleDemand {
|
||||
// dropInactiveProducts 剔除停用产品(product_type.isActive=false)的 BOM 条目,
|
||||
// 使「可生产套数」(排产支撑视图 Items)只统计启用成品(§A2:只算启用成品)。
|
||||
// 注意:只影响可生产套数计算;逐日消耗模拟(simulateSupport)仍用完整 BOM——
|
||||
// 停用产品若有遗留排产,物料消耗是真实的,不能漏算。
|
||||
func (s *Service) dropInactiveProducts(ctx context.Context, chosen map[string][]producibleBomItem) {
|
||||
if len(chosen) == 0 {
|
||||
return
|
||||
}
|
||||
pts, err := s.ctx.EntClient.ProductType.Query().Where(producttype.IsActiveEQ(false)).All(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, p := range pts {
|
||||
delete(chosen, p.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// buildChosenBom 按产品编码分组 BOM 明细,并为每个产品选定一份 BOM(优先“默认”,否则按名排序取第一份)。
|
||||
// 返回 chosen(产品编码→明细行) 与 materialSet(BOM 涉及的全部物料编码)。
|
||||
func (s *Service) buildChosenBom(ctx context.Context) (map[string][]producibleBomItem, map[string]bool, error) {
|
||||
all, err := s.ctx.EntClient.BomItem.Query().All(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
byProd := map[string]map[string][]producibleBomItem{}
|
||||
for _, b := range all {
|
||||
if b.ProductCode == "" || b.MaterialCode == "" {
|
||||
continue
|
||||
}
|
||||
bn := b.BomName
|
||||
if bn == "" {
|
||||
bn = DefaultBOMName
|
||||
}
|
||||
if byProd[b.ProductCode] == nil {
|
||||
byProd[b.ProductCode] = map[string][]producibleBomItem{}
|
||||
}
|
||||
byProd[b.ProductCode][bn] = append(byProd[b.ProductCode][bn], producibleBomItem{
|
||||
materialCode: b.MaterialCode, materialName: b.MaterialName, unit: b.Unit, unitQty: b.UnitQty,
|
||||
})
|
||||
}
|
||||
chosen := map[string][]producibleBomItem{}
|
||||
materialSet := map[string]bool{}
|
||||
for pc, boms := range byProd {
|
||||
var bn string
|
||||
if _, has := boms[DefaultBOMName]; has {
|
||||
bn = DefaultBOMName
|
||||
} else {
|
||||
names := make([]string, 0, len(boms))
|
||||
for n := range boms {
|
||||
names = append(names, n)
|
||||
}
|
||||
sort.Strings(names)
|
||||
bn = names[0]
|
||||
}
|
||||
chosen[pc] = boms[bn]
|
||||
for _, it := range boms[bn] {
|
||||
materialSet[it.materialCode] = true
|
||||
}
|
||||
}
|
||||
return chosen, materialSet, nil
|
||||
}
|
||||
|
||||
// ComputeMaterialDemand 按物料汇总未来 N 天(含今日)排产需求量,供 WMS 缺料补货视图实时拉取。
|
||||
// 需求 = Σ(逐日排产台数 × 单台用量),向上取整,按物料汇总;返回需求窗口起止日期。
|
||||
// 与 ComputeProducible 共享 BOM 选定逻辑,各自只算自己视角:MES 只产出"需求量",可用量由 WMS 自己比。
|
||||
func (s *Service) ComputeMaterialDemand(ctx context.Context, days int) ([]MaterialDemandRow, error) {
|
||||
if s.ctx.EntClient == nil {
|
||||
return nil, errors.New("未配置数据库连接")
|
||||
}
|
||||
if days <= 0 {
|
||||
days = 5
|
||||
}
|
||||
chosen, _, err := s.buildChosenBom(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(chosen) == 0 {
|
||||
return []MaterialDemandRow{}, nil
|
||||
}
|
||||
today := time.Now()
|
||||
start := today.Format("2006-01-02")
|
||||
end := today.AddDate(0, 0, 4).Format("2006-01-02")
|
||||
end := today.AddDate(0, 0, days-1).Format("2006-01-02")
|
||||
demand, err := s.dailyMaterialDemand(ctx, chosen, start, end)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(demand) == 0 {
|
||||
return []MaterialDemandRow{}, nil
|
||||
}
|
||||
mcodes := make([]string, 0, len(demand))
|
||||
for c := range demand {
|
||||
mcodes = append(mcodes, c)
|
||||
}
|
||||
sort.Strings(mcodes)
|
||||
// 物料名称/单位取自选定 BOM
|
||||
nameUnit := map[string]struct{ name, unit string }{}
|
||||
for _, bom := range chosen {
|
||||
for _, it := range bom {
|
||||
nameUnit[it.materialCode] = struct{ name, unit string }{it.materialName, it.unit}
|
||||
}
|
||||
}
|
||||
out := make([]MaterialDemandRow, 0, len(mcodes))
|
||||
for _, c := range mcodes {
|
||||
nu := nameUnit[c]
|
||||
out = append(out, MaterialDemandRow{
|
||||
MaterialCode: c, MaterialName: nu.name, Unit: nu.unit,
|
||||
DemandQty: demand[c], StartDate: start, EndDate: end,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// dailyMaterialDemand 累计 [start,end] 窗口内各物料排产需求量(向上取整)。
|
||||
// 数据链路:daily_plan(工单号→台数) → work_order(工单号→产品编码) → BOM(产品编码→物料单台用量)。
|
||||
func (s *Service) dailyMaterialDemand(ctx context.Context, chosen map[string][]producibleBomItem, start, end string) (map[string]int, error) {
|
||||
plans, err := s.ctx.EntClient.DailyPlan.Query().
|
||||
Where(dailyplan.PlanDateGTE(start), dailyplan.PlanDateLTE(end)).All(ctx)
|
||||
if err != nil || len(plans) == 0 {
|
||||
return nil
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(plans) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
// 工单号 → 未来5天台数(排除已取消)
|
||||
orderByNo := map[string]int{}
|
||||
for _, p := range plans {
|
||||
if p.Status == "CANCELLED" {
|
||||
@@ -198,7 +308,7 @@ func (s *Service) computeFuture5Demand(ctx context.Context, chosen map[string][]
|
||||
orderByNo[p.OrderNo] += p.PlanQty
|
||||
}
|
||||
if len(orderByNo) == 0 {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
nos := make([]string, 0, len(orderByNo))
|
||||
for no := range orderByNo {
|
||||
@@ -206,9 +316,9 @@ func (s *Service) computeFuture5Demand(ctx context.Context, chosen map[string][]
|
||||
}
|
||||
wos, err := s.ctx.EntClient.WorkOrder.Query().Where(workorder.WorkOrderNoIn(nos...)).All(ctx)
|
||||
if err != nil {
|
||||
return nil
|
||||
return nil, err
|
||||
}
|
||||
// 产品编码 → 未来5天台数
|
||||
// 产品编码 → 窗口内总台数
|
||||
prodQty := map[string]int{}
|
||||
for _, wo := range wos {
|
||||
if wo.ProductCode == "" {
|
||||
@@ -216,7 +326,6 @@ func (s *Service) computeFuture5Demand(ctx context.Context, chosen map[string][]
|
||||
}
|
||||
prodQty[wo.ProductCode] += orderByNo[wo.WorkOrderNo]
|
||||
}
|
||||
// 物料汇总:台数 × 单台用量(向上取整)
|
||||
matQty := map[string]int{}
|
||||
for pc, qty := range prodQty {
|
||||
bom := chosen[pc]
|
||||
@@ -230,7 +339,17 @@ func (s *Service) computeFuture5Demand(ctx context.Context, chosen map[string][]
|
||||
matQty[it.materialCode] += int(math.Ceil(float64(qty) * it.unitQty))
|
||||
}
|
||||
}
|
||||
if len(matQty) == 0 {
|
||||
return matQty, nil
|
||||
}
|
||||
|
||||
// computeFuture5Demand 近5天(含今日)日排产台数 × 单台用量,按物料汇总为未来5天需求量。
|
||||
// 数据链路:daily_plan(工单号→台数) → work_order(工单号→产品编码) → BOM(产品编码→物料单台用量)。
|
||||
func (s *Service) computeFuture5Demand(ctx context.Context, chosen map[string][]producibleBomItem) []wmsclient.ProducibleDemand {
|
||||
today := time.Now()
|
||||
start := today.Format("2006-01-02")
|
||||
end := today.AddDate(0, 0, 4).Format("2006-01-02")
|
||||
matQty, err := s.dailyMaterialDemand(ctx, chosen, start, end)
|
||||
if err != nil || len(matQty) == 0 {
|
||||
return nil
|
||||
}
|
||||
mcodes := make([]string, 0, len(matQty))
|
||||
@@ -245,6 +364,211 @@ func (s *Service) computeFuture5Demand(ctx context.Context, chosen map[string][]
|
||||
return out
|
||||
}
|
||||
|
||||
// ComputeProducibleSupport 计算排产支撑视图(§A2 / §D1)。
|
||||
// 复用 ComputeProducible 得到逐成品可生产套数,再用 WMS 可用量对日排产做逐日消耗模拟,
|
||||
// 得出整体还能满产多少天、支撑到哪天、最先告罄的短板物料;排产只排 N 天时,第 N+1 天起用近3天平均日需求外推。
|
||||
func (s *Service) ComputeProducibleSupport(ctx context.Context) (*ProducibleSupportResult, error) {
|
||||
base, err := s.ComputeProducible(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := &ProducibleSupportResult{
|
||||
ComputedAt: base.ComputedAt,
|
||||
SupportDays: 0,
|
||||
SupportUntil: time.Now().Format("2006-01-02"),
|
||||
}
|
||||
for _, it := range base.Items {
|
||||
res.Items = append(res.Items, ProducibleSupportItem{
|
||||
ProductCode: it.ProductCode, ProductName: it.ProductName,
|
||||
ProducibleQty: it.ProducibleQty, ShortText: it.ShortText,
|
||||
})
|
||||
}
|
||||
chosen, _, err := s.buildChosenBom(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
days, until, shortBoard := s.simulateSupport(ctx, chosen, base.Avail)
|
||||
res.SupportDays = days
|
||||
res.SupportUntil = until
|
||||
res.ShortBoardMaterial = shortBoard
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// simulateSupport 逐日消耗模拟:用 WMS 可用量 avail 对日排产做逐日扣减,直到某物料不足。
|
||||
// 返回 支撑满产天数、支撑到哪天、最先告罄短板物料(名称+编码,无排产返回"无排产计划")。
|
||||
func (s *Service) simulateSupport(ctx context.Context, chosen map[string][]producibleBomItem, avail map[string]int) (int, string, string) {
|
||||
today := time.Now()
|
||||
start := today.Format("2006-01-02")
|
||||
end := today.AddDate(0, 0, 59).Format("2006-01-02")
|
||||
plans, err := s.ctx.EntClient.DailyPlan.Query().
|
||||
Where(dailyplan.PlanDateGTE(start), dailyplan.PlanDateLTE(end)).All(ctx)
|
||||
if err != nil {
|
||||
return 0, start, "无排产计划"
|
||||
}
|
||||
// 工单号 → 产品编码
|
||||
orderNoSet := map[string]bool{}
|
||||
planByDate := map[string]map[string]int{} // date -> orderNo -> qty
|
||||
for _, p := range plans {
|
||||
if p.Status == "CANCELLED" || p.PlanQty <= 0 {
|
||||
continue
|
||||
}
|
||||
if planByDate[p.PlanDate] == nil {
|
||||
planByDate[p.PlanDate] = map[string]int{}
|
||||
}
|
||||
planByDate[p.PlanDate][p.OrderNo] += p.PlanQty
|
||||
orderNoSet[p.OrderNo] = true
|
||||
}
|
||||
if len(planByDate) == 0 {
|
||||
return 0, start, "无排产计划"
|
||||
}
|
||||
// 拉工单产品编码
|
||||
orderNos := make([]string, 0, len(orderNoSet))
|
||||
for no := range orderNoSet {
|
||||
orderNos = append(orderNos, no)
|
||||
}
|
||||
wos, err := s.ctx.EntClient.WorkOrder.Query().Where(workorder.WorkOrderNoIn(orderNos...)).All(ctx)
|
||||
if err != nil {
|
||||
return 0, start, "无排产计划"
|
||||
}
|
||||
orderProduct := map[string]string{}
|
||||
for _, wo := range wos {
|
||||
if wo.ProductCode != "" {
|
||||
orderProduct[wo.WorkOrderNo] = wo.ProductCode
|
||||
}
|
||||
}
|
||||
// 物料名称
|
||||
matName := map[string]string{}
|
||||
for _, bom := range chosen {
|
||||
for _, it := range bom {
|
||||
matName[it.materialCode] = it.materialName
|
||||
}
|
||||
}
|
||||
// 排序排产日
|
||||
dates := make([]string, 0, len(planByDate))
|
||||
for d := range planByDate {
|
||||
dates = append(dates, d)
|
||||
}
|
||||
sort.Strings(dates)
|
||||
|
||||
// 当前剩余可用量
|
||||
remaining := map[string]float64{}
|
||||
for k, v := range avail {
|
||||
remaining[k] = float64(v)
|
||||
}
|
||||
|
||||
// 逐日真实排产消耗
|
||||
history := make([]map[string]int, 0, len(dates)) // 每日各物料需求(用于外推均值)
|
||||
supportDays := 0
|
||||
shortBoard := ""
|
||||
for _, d := range dates {
|
||||
dayDemand := dayMaterialDemand(planByDate[d], orderProduct, chosen)
|
||||
history = append(history, dayDemand)
|
||||
// 扣减
|
||||
fail := false
|
||||
for mat, q := range dayDemand {
|
||||
remaining[mat] -= float64(q)
|
||||
if remaining[mat] < 0 && shortBoard == "" {
|
||||
name := matName[mat]
|
||||
if name == "" {
|
||||
name = mat
|
||||
}
|
||||
shortBoard = fmt.Sprintf("%s(%s)", name, mat)
|
||||
fail = true
|
||||
}
|
||||
}
|
||||
if fail {
|
||||
break
|
||||
}
|
||||
supportDays++
|
||||
}
|
||||
|
||||
// 排产日已耗尽仍有余量 → 用近3天平均日需求外推
|
||||
if shortBoard == "" {
|
||||
// 计算均值(取最近 min(3,len) 天)
|
||||
n := len(history)
|
||||
if n == 0 {
|
||||
return supportDays, addDays(start, supportDays), ""
|
||||
}
|
||||
lo := 0
|
||||
if n > 3 {
|
||||
lo = n - 3
|
||||
}
|
||||
avg := map[string]float64{}
|
||||
cnt := float64(n - lo)
|
||||
for i := lo; i < n; i++ {
|
||||
for mat, q := range history[i] {
|
||||
avg[mat] += float64(q)
|
||||
}
|
||||
}
|
||||
for mat := range avg {
|
||||
avg[mat] /= cnt
|
||||
}
|
||||
// 近3天平均日需求全为0(无有效 BOM 用量)→ 无更多需求,不再外推
|
||||
totalAvg := 0.0
|
||||
for _, q := range avg {
|
||||
totalAvg += q
|
||||
}
|
||||
if totalAvg <= 0 {
|
||||
return supportDays, addDays(start, supportDays), ""
|
||||
}
|
||||
// 外推:每日按 avg 扣减,直到某物料不足(封顶 365 天防死循环)
|
||||
capDays := 365
|
||||
for supportDays < capDays {
|
||||
fail := false
|
||||
for mat, q := range avg {
|
||||
if q <= 0 {
|
||||
continue
|
||||
}
|
||||
remaining[mat] -= q
|
||||
if remaining[mat] < 0 && shortBoard == "" {
|
||||
name := matName[mat]
|
||||
if name == "" {
|
||||
name = mat
|
||||
}
|
||||
shortBoard = fmt.Sprintf("%s(%s)", name, mat)
|
||||
fail = true
|
||||
}
|
||||
}
|
||||
if fail {
|
||||
break
|
||||
}
|
||||
supportDays++
|
||||
}
|
||||
}
|
||||
return supportDays, addDays(start, supportDays), shortBoard
|
||||
}
|
||||
|
||||
// dayMaterialDemand 计算某日各物料需求(逐工单台数 × 单台用量,向上取整)
|
||||
func dayMaterialDemand(planOfDay map[string]int, orderProduct map[string]string, chosen map[string][]producibleBomItem) map[string]int {
|
||||
demand := map[string]int{}
|
||||
for orderNo, qty := range planOfDay {
|
||||
pc := orderProduct[orderNo]
|
||||
if pc == "" || qty <= 0 {
|
||||
continue
|
||||
}
|
||||
bom := chosen[pc]
|
||||
if len(bom) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, it := range bom {
|
||||
if it.unitQty <= 0 {
|
||||
continue
|
||||
}
|
||||
demand[it.materialCode] += int(math.Ceil(float64(qty) * it.unitQty))
|
||||
}
|
||||
}
|
||||
return demand
|
||||
}
|
||||
|
||||
// addDays YYYY-MM-DD + n 天 → YYYY-MM-DD
|
||||
func addDays(date string, n int) string {
|
||||
t, err := time.Parse("2006-01-02", date)
|
||||
if err != nil {
|
||||
return date
|
||||
}
|
||||
return t.AddDate(0, 0, n).Format("2006-01-02")
|
||||
}
|
||||
|
||||
// trimNum 单台用量展示:整数不带小数点,非整数保留原精度
|
||||
func trimNum(f float64) string {
|
||||
if f == math.Trunc(f) {
|
||||
|
||||
@@ -24,7 +24,7 @@ func (s *Service) Seed(ctx context.Context) error {
|
||||
codes := []string{"*"}
|
||||
if r.code == "OPERATOR" {
|
||||
codes = []string{
|
||||
"produce.workorder", "produce.dailyplan", "produce.bom", "produce.material", "produce.scan",
|
||||
"produce.workorder", "produce.dailyplan", "produce.bom", "produce.material", "produce.producible",
|
||||
"produce.trace", "produce.wip", "produce.torque", "produce.plc", "produce.product",
|
||||
"produce.processflow", "produce.station", "produce.performance", "produce.processcard",
|
||||
// 按钮级权限(块2)
|
||||
@@ -74,6 +74,7 @@ func (s *Service) Seed(ctx context.Context) error {
|
||||
{"produce.workorder", "工单管理", "MENU", "/work-order"},
|
||||
{"produce.dailyplan", "日排产", "MENU", "/daily-plan"},
|
||||
{"produce.bom", "产品物料清单", "MENU", "/bom"},
|
||||
{"produce.producible", "排产支撑", "MENU", "/producible-support"},
|
||||
{"produce.material", "备料单", "MENU", "/material-request"},
|
||||
{"produce.qty", "数量不符处理", "MENU", "/qty-report"},
|
||||
{"produce.plc", "工位组合下发", "MENU", "/plc-send"},
|
||||
@@ -81,7 +82,6 @@ func (s *Service) Seed(ctx context.Context) error {
|
||||
{"produce.processflow", "工艺流程", "MENU", "/process-flow"},
|
||||
{"produce.station", "关联工位", "MENU", "/station"}, {"produce.performance", "绩效报表", "MENU", "/performance"},
|
||||
{"produce.dock", "接驳台维护", "MENU", "/dock"},
|
||||
{"produce.scan", "手动报工", "MENU", "/scan"},
|
||||
{"produce.trace", "工件追溯", "MENU", "/trace"},
|
||||
{"produce.wip", "在制品", "MENU", "/wip"},
|
||||
{"produce.processcard", "生产流程卡", "MENU", "/process-card"},
|
||||
@@ -169,6 +169,10 @@ func (s *Service) Seed(ctx context.Context) error {
|
||||
// 清掉存量角色里的旧码与旧菜单定义行(幂等);新菜单 produce.qty 在 1. 的 seedMenus 中定义。
|
||||
migrateLegacyQtyReport(ctx, s)
|
||||
|
||||
// 3.5 「手动报工」网页功能已整块移除(2026-09-21,Scan.vue/produce.scan):清掉存量角色里的
|
||||
// produce.scan 码与旧菜单定义行(幂等);报工唯一入口改为工位终端内部接口。
|
||||
migrateLegacyScan(ctx, s)
|
||||
|
||||
// 4. 初始化产线工位(内置只读主数据);工艺流程/工序步骤由用户在「工艺流程」页自建,不预置
|
||||
seedStations(ctx, s)
|
||||
// 5. 接驳台主数据(真源在 MES,2026-09-21 自 WMS 迁入);系统预置只读
|
||||
@@ -342,3 +346,27 @@ func migrateLegacyQtyReport(ctx context.Context, s *Service) {
|
||||
_, _ = s.ctx.EntClient.Permission.Delete().
|
||||
Where(permission.Code("produce.qtyreport")).Exec(ctx)
|
||||
}
|
||||
|
||||
// migrateLegacyScan 清理已下线的「手动报工」菜单码 produce.scan(幂等,可重复执行):
|
||||
// 该网页功能(Scan.vue 手动报工页)已整块移除(2026-09-21),报工唯一入口改为工位终端内部接口。
|
||||
// 从存量角色的 permissionCodes 摘除 produce.scan 并删除其权限定义行。
|
||||
func migrateLegacyScan(ctx context.Context, s *Service) {
|
||||
roleList, _ := s.ctx.EntClient.Role.Query().All(ctx)
|
||||
for _, rl := range roleList {
|
||||
kept := make([]string, 0, len(rl.PermissionCodes))
|
||||
changed := false
|
||||
for _, c := range rl.PermissionCodes {
|
||||
if c == "produce.scan" {
|
||||
changed = true
|
||||
continue
|
||||
}
|
||||
kept = append(kept, c)
|
||||
}
|
||||
if changed {
|
||||
_ = s.ctx.EntClient.Role.UpdateOneID(rl.ID).
|
||||
SetPermissionCodes(mergeCodes(kept, nil)).Exec(ctx)
|
||||
}
|
||||
}
|
||||
_, _ = s.ctx.EntClient.Permission.Delete().
|
||||
Where(permission.Code("produce.scan")).Exec(ctx)
|
||||
}
|
||||
|
||||
@@ -319,7 +319,7 @@ type CriterionVO struct {
|
||||
|
||||
// ListStationSteps 按工位号加载该工位绑定且「启用」的工艺流程的工序步骤(含考核标准)。
|
||||
// 链路:工位号 → station.flow_id → 工艺流程(绑定关系在「关联工位」页维护,流程可绑任意工位)。
|
||||
// 手动报工页:选工位 → 自动带出该工位的工序步骤。
|
||||
// 选工位 → 自动带出该工位的工序步骤(供巡检终端等按工位渲染)。
|
||||
func (s *Service) ListStationSteps(ctx context.Context, stationNo int) ([]*ProcessStepTemplate, error) {
|
||||
if stationNo < 1 {
|
||||
return []*ProcessStepTemplate{}, nil
|
||||
|
||||
@@ -439,56 +439,6 @@ func (s *Service) Trace(ctx context.Context, sn string) (map[string]any, error)
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListProcessRecords 报工记录列表(分页):直接查真实报工表 workpiece_process,
|
||||
// 手动报工与工位终端报工都在此可见,形成报工闭环(取代已废弃的 scan_record 查询)。
|
||||
func (s *Service) ListProcessRecords(ctx context.Context, sn, orderNo string, stationNo, page, pageSize int) ([]map[string]any, int, error) {
|
||||
q := s.ctx.EntClient.WorkpieceProcess.Query()
|
||||
if sn != "" {
|
||||
q = q.Where(workpieceprocess.SnEqualFold(sn))
|
||||
}
|
||||
if orderNo != "" {
|
||||
q = q.Where(workpieceprocess.OrderNoEqualFold(orderNo))
|
||||
}
|
||||
if stationNo > 0 {
|
||||
q = q.Where(workpieceprocess.StationNo(itoa(stationNo)))
|
||||
}
|
||||
total, err := q.Count(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 20
|
||||
}
|
||||
rows, err := q.Order(ent.Desc(workpieceprocess.FieldID)).
|
||||
Offset((page - 1) * pageSize).Limit(pageSize).All(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
list := make([]map[string]any, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
ended := ""
|
||||
if r.EndedAt != nil {
|
||||
ended = r.EndedAt.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
list = append(list, map[string]any{
|
||||
"sn": r.Sn,
|
||||
"orderNo": r.OrderNo,
|
||||
"stationNo": r.StationNo,
|
||||
"processCode": r.ProcessCode,
|
||||
"processName": r.ProcessName,
|
||||
"status": r.Status,
|
||||
"result": r.Result,
|
||||
"operator": r.Operator,
|
||||
"durationSec": r.DurationSec,
|
||||
"endedAt": ended,
|
||||
})
|
||||
}
|
||||
return list, total, nil
|
||||
}
|
||||
|
||||
func (s *Service) ListWorkpieces(ctx context.Context, sn, orderNo string) ([]*ent.Workpiece, error) {
|
||||
q := s.ctx.EntClient.Workpiece.Query()
|
||||
if sn != "" {
|
||||
|
||||
@@ -316,7 +316,7 @@ func (s *Service) expandBindBom(ctx context.Context, productCode, bomName string
|
||||
return bomTreeToBindNodes(s.expandBomTree(ctx, productCode, bomNameOrDefault(bomName), qty, depth, path, s.localBomExistsResolver))
|
||||
}
|
||||
|
||||
// BindRequiredItem 应绑清单条目(工位终端/手动报工展示引导 + 前端齐套计算)
|
||||
// BindRequiredItem 应绑清单条目(工位终端展示引导 + 前端齐套计算)
|
||||
type BindRequiredItem struct {
|
||||
MaterialCode string `json:"materialCode"`
|
||||
MaterialName string `json:"materialName"`
|
||||
@@ -342,7 +342,7 @@ type StationBindPanel struct {
|
||||
StationNames map[int]string `json:"-"`
|
||||
}
|
||||
|
||||
// StationBindPanelData 供工位终端/手动报工页在报工前拉取:本工件本工序该装什么、已装什么
|
||||
// StationBindPanelData 供工位终端在报工前拉取:本工件本工序该装什么、已装什么
|
||||
func (s *Service) StationBindPanelData(ctx context.Context, sn string, processCode int) (*StationBindPanel, error) {
|
||||
_, wo, err := s.workpieceProduct(ctx, s.ctx.EntClient, sn)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user