xiaoyi
10 天以前 9efd8376cb0c444ce6f229b2544e68efa352f674
fix(mes): 工艺路线产品补齐报工入仓库/库区/库位,修复编辑不回显

问题:工艺路线产品表单里「报工仓库 / 报工库区 / 报工库位」三级联动早就做好了,
列表也展示 warehouseName / locationName / areaName,但整条后端链路都没这几个字段:
· mes_pro_route_product 表没有 warehouse_id / location_id / area_id 三列
· MesProRouteProductDO 没有对应属性
· MesProRouteProductSaveReqVO 没有入参 → 前端选了也存不进库
· MesProRouteProductRespVO 没有出参 → 编辑时下拉框拿不到 id,无法回显
表现为:填了保存不报错但重开就空,列表三列永远空白。

改动:
- 新增迁移 sql/mysql/route_product_warehouse_20261030.sql(幂等)
补三列 warehouse_id / location_id / area_id,各带普通索引
对应主数据:mes_wm_warehouse → mes_wm_warehouse_location → mes_wm_warehouse_area
- DO / SaveReqVO 补三个 id;RespVO 补三个 id + 三个 name
- Controller 批量查三级名称回填(getWarehouseMap / getWarehouseLocationMap / getWarehouseAreaMap)
- Service 新增 validateWarehouseChain:
· 仓库填了就校验存在
· 库位填了就校验「库区→库位」「仓库→库区」父子关系有没有串
· 只填仓库不填库区库位是允许的(部分场景只需知道入哪个仓)

验证(先在本地 IDEA 环境跑通,再上线):
改前 warehouseId=null → 更新 → 回查 warehouseId=16 生产车间线边库
列表返回「生产车间线边库 / 生产车间线边库通用库区 / 生产车间线边库默认库位」
传错误层级返回「库位不属于所选库区」

同类问题排查:写脚本比对了前端 data.ts 里所有 fieldName 与 API 类型,
未发现其它「前端有字段、后端没有」的情况(其余差异均为 keyword/file 等前端临时字段)。
已添加1个文件
已修改5个文件
142 ■■■■■ 文件已修改
sql/mysql/route_product_warehouse_20261030.sql 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/MesProRouteProductController.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/product/MesProRouteProductRespVO.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/product/MesProRouteProductSaveReqVO.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/route/MesProRouteProductDO.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProductServiceImpl.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/route_product_warehouse_20261030.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
-- ============================================================
-- å·¥è‰ºè·¯çº¿äº§å“ï¼šè¡¥é½ã€ŒæŠ¥å·¥å…¥ä»“库 / å…¥åº“库区 / å…¥åº“库位」三列
-- ç”Ÿæˆæ—¶é—´ï¼š2026-10-30
--
-- é—®é¢˜ï¼šå‰ç«¯å·¥è‰ºè·¯çº¿äº§å“è¡¨å•早已做了三级联动(warehouseId â†’ locationId â†’ areaId),
--      åˆ—表也展示 warehouseName / locationName / areaName,
--      ä½†æ•°æ®åº“表、DO、SaveReqVO、RespVO å…¨éƒ½æ²¡æœ‰è¿™ä¸‰ä¸ªå­—段,
--      å¯¼è‡´ï¼š1) ç¼–辑时下拉框不回显  2) å¡«äº†ä¹Ÿå­˜ä¸è¿›åº“  3) åˆ—表三列永远为空
--
-- åšæ³•:补三列,关联 MES ä»“储主数据
--      warehouse_id â†’ mes_wm_warehouse.id       ï¼ˆæŠ¥å·¥å…¥ä»“库)
--      location_id  â†’ mes_wm_warehouse_location.id(入库库区)
--      area_id      â†’ mes_wm_warehouse_area.id      ï¼ˆå…¥åº“库位)
--      å±‚级关系:仓库 â†’ åº“区 â†’ åº“位,库区挂在仓库下,库位挂在库区下
--
-- å¹‚等:先查 information_schema å† ALTER,可重复执行
-- ============================================================
DROP PROCEDURE IF EXISTS sp_route_product_add_col;
DELIMITER $$
CREATE PROCEDURE sp_route_product_add_col(IN p_col VARCHAR(64), IN p_comment VARCHAR(255), IN p_after VARCHAR(64))
BEGIN
  IF NOT EXISTS (SELECT 1 FROM information_schema.columns
                  WHERE table_schema = DATABASE() AND table_name = 'mes_pro_route_product' AND column_name = p_col) THEN
    SET @s = CONCAT('ALTER TABLE `mes_pro_route_product` ADD COLUMN `', p_col, '` bigint NULL COMMENT ''', p_comment, ''' AFTER `', p_after, '`');
    PREPARE st FROM @s; EXECUTE st; DEALLOCATE PREPARE st;
  END IF;
  IF NOT EXISTS (SELECT 1 FROM information_schema.statistics
                  WHERE table_schema = DATABASE() AND table_name = 'mes_pro_route_product' AND index_name = CONCAT('idx_', p_col)) THEN
    SET @s2 = CONCAT('CREATE INDEX `idx_', p_col, '` ON `mes_pro_route_product` (`', p_col, '`)');
    PREPARE st2 FROM @s2; EXECUTE st2; DEALLOCATE PREPARE st2;
  END IF;
END$$
DELIMITER ;
CALL sp_route_product_add_col('warehouse_id', '报工入仓库(关联 mes_wm_warehouse.id)', 'item_id');
CALL sp_route_product_add_col('location_id', '入库库区(关联 mes_wm_warehouse_location.id)', 'warehouse_id');
CALL sp_route_product_add_col('area_id', '入库库位(关联 mes_wm_warehouse_area.id)', 'location_id');
DROP PROCEDURE IF EXISTS sp_route_product_add_col;
-- ---------- æ ¡éªŒ ----------
SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_COMMENT
  FROM information_schema.columns
 WHERE table_schema = DATABASE() AND table_name = 'mes_pro_route_product'
   AND COLUMN_NAME IN ('warehouse_id','location_id','area_id')
 ORDER BY ORDINAL_POSITION;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/MesProRouteProductController.java
@@ -10,9 +10,15 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseLocationService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -42,6 +48,12 @@
    private MesMdItemService itemService;
    @Resource
    private MesMdUnitMeasureService unitMeasureService;
    @Resource
    private MesWmWarehouseService warehouseService;
    @Resource
    private MesWmWarehouseLocationService warehouseLocationService;
    @Resource
    private MesWmWarehouseAreaService warehouseAreaService;
    @PostMapping("/create")
    @Operation(summary = "创建工艺路线产品")
@@ -96,6 +108,13 @@
                convertSet(list, MesProRouteProductDO::getItemId));
        Map<Long, MesMdUnitMeasureDO> unitMap = unitMeasureService.getUnitMeasureMap(
                convertSet(itemMap.values(), MesMdItemDO::getUnitMeasureId));
        // æŠ¥å·¥å…¥åº“三级的名称:仓库 â†’ åº“区 â†’ åº“位
        Map<Long, MesWmWarehouseDO> warehouseMap = warehouseService.getWarehouseMap(
                convertSet(list, MesProRouteProductDO::getWarehouseId));
        Map<Long, MesWmWarehouseLocationDO> locationMap = warehouseLocationService.getWarehouseLocationMap(
                convertSet(list, MesProRouteProductDO::getLocationId));
        Map<Long, MesWmWarehouseAreaDO> areaMap = warehouseAreaService.getWarehouseAreaMap(
                convertSet(list, MesProRouteProductDO::getAreaId));
        // 2. æ‹¼æŽ¥ VO
        return BeanUtils.toBean(list, MesProRouteProductRespVO.class, vo -> {
            MapUtils.findAndThen(itemMap, vo.getItemId(), item -> {
@@ -104,6 +123,12 @@
                MapUtils.findAndThen(unitMap, item.getUnitMeasureId(),
                        unit -> vo.setUnitName(unit.getName()));
            });
            MapUtils.findAndThen(warehouseMap, vo.getWarehouseId(),
                    warehouse -> vo.setWarehouseName(warehouse.getName()));
            MapUtils.findAndThen(locationMap, vo.getLocationId(),
                    location -> vo.setLocationName(location.getName()));
            MapUtils.findAndThen(areaMap, vo.getAreaId(),
                    area -> vo.setAreaName(area.getName()));
        });
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/product/MesProRouteProductRespVO.java
@@ -25,6 +25,24 @@
    @Schema(description = "产品物料名称")
    private String itemName;
    @Schema(description = "报工入仓库编号", example = "1")
    private Long warehouseId;
    @Schema(description = "报工入仓库名称")
    private String warehouseName;
    @Schema(description = "入库库区编号", example = "1")
    private Long locationId;
    @Schema(description = "入库库区名称")
    private String locationName;
    @Schema(description = "入库库位编号", example = "1")
    private Long areaId;
    @Schema(description = "入库库位名称")
    private String areaName;
    @Schema(description = "规格型号")
    private String specification;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/product/MesProRouteProductSaveReqVO.java
@@ -21,6 +21,15 @@
    @NotNull(message = "产品物料编号不能为空")
    private Long itemId;
    @Schema(description = "报工入仓库编号(关联 mes_wm_warehouse)", example = "1")
    private Long warehouseId;
    @Schema(description = "入库库区编号(关联 mes_wm_warehouse_location)", example = "1")
    private Long locationId;
    @Schema(description = "入库库位编号(关联 mes_wm_warehouse_area)", example = "1")
    private Long areaId;
    @Schema(description = "生产数量", example = "100")
    private Integer quantity;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/route/MesProRouteProductDO.java
@@ -41,6 +41,24 @@
     */
    private Long itemId;
    /**
     * æŠ¥å·¥å…¥ä»“库编号
     *
     * å…³è” {@link cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO#getId()}
     */
    private Long warehouseId;
    /**
     * å…¥åº“库区编号
     *
     * å…³è” {@link cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO#getId()}
     */
    private Long locationId;
    /**
     * å…¥åº“库位编号
     *
     * å…³è” {@link cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO#getId()}
     */
    private Long areaId;
    /**
     * ç”Ÿäº§æ•°é‡
     */
    private Integer quantity;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProductServiceImpl.java
@@ -5,6 +5,7 @@
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.product.MesProRouteProductSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.route.MesProRouteProductMapper;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@@ -34,6 +35,10 @@
    @Resource
    @Lazy
    private MesProRouteProductBomService routeProductBomService;
    @Resource
    private MesWmWarehouseAreaService warehouseAreaService;
    @Resource
    private cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService warehouseService;
    @Override
    public Long createRouteProduct(MesProRouteProductSaveReqVO createReqVO) {
@@ -41,6 +46,8 @@
        routeService.validateRouteNotEnable(createReqVO.getRouteId());
        // 1.1 æ ¡éªŒäº§å“å”¯ä¸€æ€§ï¼ˆä¸€ä¸ªäº§å“åªèƒ½å…³è”一条工艺路线)
        validateItemUnique(null, createReqVO.getItemId());
        // 1.2 æ ¡éªŒæŠ¥å·¥å…¥åº“三级(仓库 â†’ åº“区 â†’ åº“位)的父子关系
        validateWarehouseChain(createReqVO);
        // 2. æ’å…¥
        MesProRouteProductDO routeProduct = BeanUtils.toBean(createReqVO, MesProRouteProductDO.class);
@@ -56,6 +63,8 @@
        validateRouteProductExists(updateReqVO.getId());
        // 1.2 æ ¡éªŒäº§å“å”¯ä¸€æ€§
        validateItemUnique(updateReqVO.getId(), updateReqVO.getItemId());
        // 1.3 æ ¡éªŒæŠ¥å·¥å…¥åº“三级的父子关系
        validateWarehouseChain(updateReqVO);
        // 2. æ›´æ–°
        MesProRouteProductDO updateObj = BeanUtils.toBean(updateReqVO, MesProRouteProductDO.class);
@@ -89,6 +98,22 @@
        }
    }
    /**
     * æ ¡éªŒæŠ¥å·¥å…¥åº“三级:仓库 â†’ åº“区 â†’ åº“位 çš„父子关系
     *
     * <p>三个都填时才校验层级是否匹配,避免前端联动错位时把库位挂到别的仓库下。
     * å…è®¸åªå¡«ä»“库(部分场景只要知道入哪个仓),所以不是三个都必填。</p>
     */
    private void validateWarehouseChain(MesProRouteProductSaveReqVO reqVO) {
        // 1. ä»“库填了就校验存在(只填仓库、不填库区库位是允许的)
        if (reqVO.getWarehouseId() != null) {
            warehouseService.validateWarehouseExists(reqVO.getWarehouseId());
        }
        // 2. åº“位填了就校验「库区→库位」「仓库→库区」的父子关系是否串了
        warehouseAreaService.validateWarehouseAreaExists(
                reqVO.getWarehouseId(), reqVO.getLocationId(), reqVO.getAreaId());
    }
    private void validateItemUnique(Long id, Long itemId) {
        MesProRouteProductDO existing = routeProductMapper.selectByItemId(itemId);
        if (existing == null) {