refactor: 清理MES端本地product_type冗余代码,对齐WMS主数据架构

1.  移除MES端本地product_type实体、schema及相关CRUD代码,改为通过WMS内部API代理获取物料主数据
2.  更新物料管理粒度逻辑,新增"其他"类型(3)支持,同步更新入库校验规则
3.  修复前端物料选择、库存筛选等页面的类型判断逻辑,统一使用manage_mode区分管理类型
4.  清理静态资源文件冗余的样式代码,调整前端页面展示逻辑
5.  更新帮助文档,修正物料类型的说明描述
This commit is contained in:
SunYF
2026-09-22 07:41:40 +08:00
parent 12ca493c61
commit 932700ca65
84 changed files with 698 additions and 3955 deletions
+3 -6
View File
@@ -170,12 +170,9 @@ var schemaPatchSQL = []string{
// 日排产各工位产量分配
`ALTER TABLE daily_plan ADD COLUMN IF NOT EXISTS station_plan_qty jsonb NOT NULL DEFAULT '{}'::jsonb`,
// ---- 物料档案「简称」删除(2026-09-19 用户裁决----
// 本地 product_type 仅作 WMS 不可达时的离线兜底缓存:原 remark(备注) 一并删除,
// 改存 spec/unit,与 WMS 物料档案必填口径(图号/名称/规格/单位/品类/类型)保持一致。
`ALTER TABLE product_type ADD COLUMN IF NOT EXISTS spec varchar(128) NOT NULL DEFAULT ''`,
`ALTER TABLE product_type ADD COLUMN IF NOT EXISTS unit varchar(32) NOT NULL DEFAULT ''`,
`ALTER TABLE product_type DROP COLUMN IF EXISTS remark`,
// ---- 物料档案 product_type 本地表清理(2026-09-21 方案 A:纯 WMS 代理,删除本地双写/缓存表----
// ent 已移除 product_type 实体,Schema.Create 不再建此表;此处清理存量库遗留表。
`DROP TABLE IF EXISTS product_type`,
// ---- 附件统一存储方案(2026-09-19):年/月/日/文件类型/uuid.ext ----
`ALTER TABLE attachment ADD COLUMN IF NOT EXISTS file_type varchar(32) NOT NULL DEFAULT 'OTHER'`,
@@ -16,12 +16,12 @@ import (
"bj_power_mes/ent/bomitem"
"bj_power_mes/ent/processflow"
"bj_power_mes/ent/processstep"
"bj_power_mes/ent/producttype"
"bj_power_mes/ent/station"
"bj_power_mes/ent/stepcriterion"
"bj_power_mes/ent/workorder"
"bj_power_mes/ent/workpieceprocess"
"bj_power_mes/internal/logic"
"bj_power_mes/internal/wmsclient"
"bj_power_mes/internal/svc"
)
@@ -135,13 +135,13 @@ func buildModelCard(ctx context.Context, svcCtx *svc.ServiceContext, productType
client := svcCtx.EntClient
card := &modelCard{GeneratedAt: nowText()}
// 1) 定位产品型号
var pt *ent.ProductType
// 1) 定位产品型号(实时代理 WMS,成品档案 item_type=3WMS 不可达时降级用 code 兜底)
var pt *wmsclient.ProductTypeRow
if productTypeId > 0 {
pt, _ = client.ProductType.Get(ctx, productTypeId)
pt, _ = logic.New(svcCtx).ProductTypeByID(ctx, productTypeId)
}
if pt == nil && productCode != "" {
pt, _ = client.ProductType.Query().Where(producttype.Code(productCode)).First(ctx)
pt, _ = logic.New(svcCtx).ProductTypeByCode(ctx, productCode)
}
switch {
case pt != nil:
+4 -5
View File
@@ -11,7 +11,6 @@ import (
"time"
"bj_power_mes/ent/dailyplan"
"bj_power_mes/ent/producttype"
"bj_power_mes/ent/workorder"
"bj_power_mes/internal/wmsclient"
)
@@ -100,8 +99,8 @@ func (s *Service) ComputeProducible(ctx context.Context) (*ProducibleResult, err
// 4) 产品名称映射(product_type 优先,work_order 兜底)
nameMap := map[string]string{}
if pts, e := s.ctx.EntClient.ProductType.Query().All(ctx); e == nil {
for _, p := range pts {
if rows, e := s.ctx.Wms.ProductTypes(ctx, ""); e == nil {
for _, p := range rows {
nameMap[p.Code] = p.Name
}
}
@@ -185,11 +184,11 @@ func (s *Service) dropInactiveProducts(ctx context.Context, chosen map[string][]
if len(chosen) == 0 {
return
}
pts, err := s.ctx.EntClient.ProductType.Query().Where(producttype.IsActiveEQ(false)).All(ctx)
rows, err := s.ctx.Wms.ProductTypes(ctx, "false")
if err != nil {
return
}
for _, p := range pts {
for _, p := range rows {
delete(chosen, p.Code)
}
}
+28 -64
View File
@@ -6,16 +6,14 @@ import (
"fmt"
"strings"
"bj_power_mes/ent"
"bj_power_mes/ent/bomitem"
"bj_power_mes/ent/producttype"
"bj_power_mes/ent/workorder"
"bj_power_mes/internal/wmsclient"
)
// 产品型号(成品档案):编码主数据单一来源 = WMSmaterials item_type=3)。
// MES 本页改为实时代理 WMSWMS 不可达时降级读本地 product_types 只读缓存(仅查询
// 写操作必须 WMS 可用),前端以 wmsOnline 标识提示
// MES 本页实时代理 WMS不再保留本地 product_types 缓存/双写(方案 A:WMS 不可达即报错
// 不降级读本地缓存,避免给界面喂空/陈旧数据)
type ProductTypeReq struct {
Id int `json:"id"`
@@ -69,7 +67,7 @@ func (s *Service) UpdateProductType(ctx context.Context, req ProductTypeReq) err
// DeleteProductType 删除保护:被工单/BOM 引用过的型号禁删(MES 侧引用校验,
// WMS 侧库存引用由 WMS 内部接口再校验一层)
func (s *Service) DeleteProductType(ctx context.Context, id int) error {
row, err := s.productTypeByID(ctx, id)
row, err := s.ProductTypeByID(ctx, id)
if err != nil {
return err
}
@@ -88,84 +86,50 @@ func (s *Service) DeleteProductType(ctx context.Context, id int) error {
}
// ListProductTypes 全量列表(下拉/兼容形态,id asc 新建在底部)。
// degraded=false 表示 WMS 不可达、数据来自本地只读缓存
// 纯 WMS 代理:WMS 不可达直接报错(不再降级本地缓存)。degraded 恒为 true(仅兼容前端字段)
func (s *Service) ListProductTypes(ctx context.Context) ([]wmsclient.ProductTypeRow, bool, error) {
list, err := s.ctx.Wms.ProductTypes(ctx, "")
if err == nil {
return list, true, nil
if err != nil {
return nil, false, errors.New("WMS 不可达,无法读取成品档案:" + err.Error())
}
fallback, ferr := s.localProductTypes(ctx, "", "", "")
if ferr != nil {
return nil, false, errors.New("WMS 不可达且本地缓存读取失败")
}
return fallback, false, nil
return list, true, nil
}
// ListProductTypesPage 分页+搜索(管理页真分页)。
// 2026-09-08 禁止关键字混搜:编码/名称/类别各自独立模糊。
// 纯 WMS 代理:WMS 不可达直接报错。wmsOnline 恒为 true(仅兼容前端字段)。
func (s *Service) ListProductTypesPage(ctx context.Context, code, name, category, isActive string, page, pageSize int) (int64, []wmsclient.ProductTypeRow, bool, error) {
total, list, err := s.ctx.Wms.ProductTypePage(ctx, code, name, category, isActive, page, pageSize)
if err == nil {
return total, list, true, nil
if err != nil {
return 0, nil, false, errors.New("WMS 不可达,无法读取成品档案:" + err.Error())
}
fallback, ferr := s.localProductTypes(ctx, code, name, category)
if ferr != nil {
return 0, nil, false, errors.New("WMS 不可达且本地缓存读取失败")
}
start := (page - 1) * pageSize
if start > len(fallback) {
start = len(fallback)
}
end := start + pageSize
if end > len(fallback) {
end = len(fallback)
}
return int64(len(fallback)), fallback[start:end], false, nil
return total, list, true, nil
}
func (s *Service) productTypeByID(ctx context.Context, id int) (*wmsclient.ProductTypeRow, error) {
// ProductTypeByID 从 WMS 取成品档案(item_type=3)按 idWMS 不可达即报错。
func (s *Service) ProductTypeByID(ctx context.Context, id int) (*wmsclient.ProductTypeRow, error) {
list, err := s.ctx.Wms.ProductTypes(ctx, "")
if err == nil {
for i := range list {
if list[i].ID == id {
return &list[i], nil
}
}
return nil, errors.New("物料档案不存在")
}
// 降级:本地缓存
pt, err := s.ctx.EntClient.ProductType.Get(ctx, id)
if err != nil {
return nil, errors.New("物料档案不存在(WMS 不可达,无法校验引用)")
return nil, errors.New("WMS 不可达,无法读取成品档案:" + err.Error())
}
return &wmsclient.ProductTypeRow{ID: pt.ID, Code: pt.Code, Name: pt.Name}, nil
for i := range list {
if list[i].ID == id {
return &list[i], nil
}
}
return nil, errors.New("物料档案不存在")
}
// localProductTypes 本地只读缓存过滤(降级用):编码/名称/类别各自独立模糊匹配
func (s *Service) localProductTypes(ctx context.Context, code, name, category string) ([]wmsclient.ProductTypeRow, error) {
list, err := s.ctx.EntClient.ProductType.Query().Order(ent.Asc(producttype.FieldID)).All(ctx)
// ProductTypeByCode 从 WMS 取成品档案按 code。
func (s *Service) ProductTypeByCode(ctx context.Context, code string) (*wmsclient.ProductTypeRow, error) {
list, err := s.ctx.Wms.ProductTypes(ctx, "")
if err != nil {
return nil, err
return nil, errors.New("WMS 不可达,无法读取成品档案:" + err.Error())
}
codeKw := strings.ToLower(strings.TrimSpace(code))
nameKw := strings.ToLower(strings.TrimSpace(name))
catKw := strings.ToLower(strings.TrimSpace(category))
rows := make([]wmsclient.ProductTypeRow, 0, len(list))
for _, p := range list {
if codeKw != "" && !strings.Contains(strings.ToLower(p.Code), codeKw) {
continue
for i := range list {
if list[i].Code == code {
return &list[i], nil
}
if nameKw != "" && !strings.Contains(strings.ToLower(p.Name), nameKw) {
continue
}
if catKw != "" && !strings.Contains(strings.ToLower(p.Category), catKw) {
continue
}
rows = append(rows, wmsclient.ProductTypeRow{
ID: p.ID, Code: p.Code, Name: p.Name, Spec: p.Spec, Unit: p.Unit,
Category: p.Category, IsActive: p.IsActive,
CreatedAt: p.CreatedAt.Unix(),
})
}
return rows, nil
return nil, errors.New("物料档案不存在")
}
+80 -1
View File
@@ -1,4 +1,4 @@
package wmsclient
package wmsclient
import (
"bytes"
@@ -6,8 +6,10 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"time"
)
@@ -630,3 +632,80 @@ type RespError struct {
func (e *RespError) Error() string {
return "wms call failed with status " + string(rune('0'+e.Status))
}
// MaterialRow WMS 物料档案行(代理 WMS materials 表)
type MaterialRow struct {
ID int `json:"id"`
Code string `json:"code"`
Name string `json:"name"`
Spec string `json:"spec"`
Unit string `json:"unit"`
Description string `json:"description"`
ItemType int `json:"itemType"`
ManageMode int `json:"manageMode"`
TemplateCode string `json:"templateCode"`
SafetyStock int `json:"safetyStock"`
IsActive bool `json:"isActive"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
}
// MaterialPage 分页查询 WMS 物料(GET /api/internal/materials
func (c *Client) MaterialPage(ctx context.Context, code, name, spec string, itemType, manageMode, page, pageSize int) (int64, []MaterialRow, error) {
q := "/api/internal/materials?page=" + strconv.Itoa(page) + "&pageSize=" + strconv.Itoa(pageSize)
if code != "" {
q += "&materialCode=" + url.QueryEscape(code)
}
if name != "" {
q += "&name=" + url.QueryEscape(name)
}
if spec != "" {
q += "&spec=" + url.QueryEscape(spec)
}
if itemType > 0 {
q += "&itemType=" + strconv.Itoa(itemType)
}
if manageMode > 0 {
q += "&manageMode=" + strconv.Itoa(manageMode)
}
var out struct {
Total int64 `json:"total"`
List []MaterialRow `json:"list"`
}
if err := c.get(ctx, q, &out); err != nil {
return 0, nil, err
}
return out.Total, out.List, nil
}
// CreateMaterial 调 WMS 创建物料(POST /api/internal/materials
func (c *Client) CreateMaterial(ctx context.Context, code, name, spec, unit, description string, itemType, manageMode, safetyStock int, templateCode string) error {
return c.post(ctx, "/api/internal/materials", map[string]any{
"code": code, "name": name, "spec": spec, "unit": unit, "description": description,
"itemType": itemType, "manageMode": manageMode, "templateCode": templateCode, "safetyStock": safetyStock,
})
}
// UpdateMaterial 调 WMS 更新物料(PUT /api/internal/materials
func (c *Client) UpdateMaterial(ctx context.Context, id int, name, spec, unit, description string, itemType, manageMode, safetyStock int, templateCode string) error {
b, _ := json.Marshal(map[string]any{
"id": id, "name": name, "spec": spec, "unit": unit, "description": description,
"itemType": itemType, "manageMode": manageMode, "templateCode": templateCode, "safetyStock": safetyStock,
})
req, err := http.NewRequestWithContext(ctx, http.MethodPut, c.baseURL+"/api/internal/materials", 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 >= 400 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("WMS PUT /api/internal/materials %d: %s", resp.StatusCode, string(body))
}
return nil
}