星瀚物料序时簿批量分类功能二开

需求: 物料列表 点击批量分类后,将选中的物料分类更新到 序时簿上二开文本字段中

步骤1: 定位到弹框这个页面 是个动态页面

步骤2: 动态页面写插件

java 复制代码
package cyjt.cy001.devcy.devcy01.plugin.operate;

import kd.bos.dataentity.entity.DynamicObject;
import kd.bos.form.FormShowParameter;
import kd.bos.form.IFormView;
import kd.bos.form.control.TreeView;
import kd.bos.form.control.events.TreeNodeClickListener;
import kd.bos.form.control.events.TreeNodeEvent;
import kd.bos.form.plugin.AbstractFormPlugin;
import kd.bos.logging.Log;
import kd.bos.logging.LogFactory;
import kd.bos.orm.query.QCP;
import kd.bos.orm.query.QFilter;
import kd.bos.servicehelper.BusinessDataServiceHelper;
import kd.bos.servicehelper.QueryServiceHelper;
import kd.bos.servicehelper.operation.SaveServiceHelper;

import java.util.ArrayList;
import java.util.Collection;
import java.util.EventObject;
import java.util.List;
import java.util.Map;

/**
 * 物料批量分类确认插件。
 * <p>
 * 平台物料列表"管理/分配 -> 批量分类"会打开动态表单bd_grouptree。该弹窗的TreeView点击事件只稳定提供
 * nodeId,点"确定"时平台又可能重新创建插件实例,所以本插件在树节点点击时按nodeId解析出存货类别名称,
 * 并写入PageCache;点确定后再从PageCache读取该名称,批量写入物料主表自定义字段cyjt_invcategory。
 */
public class GroupTreeConfirmVerifyPlugin extends AbstractFormPlugin implements TreeNodeClickListener {

    private static final Log log = LogFactory.getLog(GroupTreeConfirmVerifyPlugin.class);

    /** bd_grouptree弹窗中"确定"按钮的控件标识。 */
    private static final String BTN_SAVE = "btnsave";

    /** bd_grouptree弹窗中左侧分类树控件标识。 */
    private static final String CONTROL_GROUP_TREE = "grouptree";

    /** 星瀚物料基础资料实体标识。 */
    private static final String ENTITY_MATERIAL = "bd_material";

    /** 物料分类标准实体标识,用于加载当前分类标准的编码和名称。 */
    private static final String ENTITY_GROUP_STANDARD = "bd_materialgroupstandard";

    /** 存货类别树节点实体标识;当前环境中存货类别节点对应bd_materialgroup。 */
    private static final String ENTITY_MATERIAL_CATEGORY = "bd_materialgroup";

    /** 物料主表自定义字段:存货类别名称。 */
    private static final String FIELD_INV_CATEGORY = "cyjt_invcategory";

    /** bd_grouptree表单字段:当前下拉框选择的分类标准。 */
    private static final String FIELD_CURRENT_STANDARD = "cmbstandardlst";

    /** bd_grouptree打开参数:来源实体,例如bd_material。 */
    private static final String PARAM_ENTITY = "entity";

    /** bd_grouptree打开参数:本次批量分类选中的基础资料主键集合。 */
    private static final String PARAM_DATA_IDS = "dataIds";

    /** bd_grouptree打开参数:打开弹窗时的分类标准主键;下拉切换后不一定更新。 */
    private static final String PARAM_STANDARD = "standard";

    /** 需要同步到物料主表的分类标准编码:存货类别。 */
    private static final String INV_CATEGORY_STANDARD_NUMBER = "002";

    /** 需要同步到物料主表的分类标准名称:存货类别。 */
    private static final String INV_CATEGORY_STANDARD_NAME = "存货类别";

    /** 页面缓存key:保存用户在当前bd_grouptree弹窗中点击的存货类别名称。 */
    private static final String CACHE_SELECTED_CATEGORY_NAME = "cyjt_selected_inv_category_name";

    /**
     * 注册按钮点击监听和树节点点击监听。
     * <p>
     * 确定按钮走普通click事件;分类树需要直接拿TreeView控件注册TreeNodeClickListener,
     * 普通addClickListeners无法拿到TreeNodeEvent中的nodeId。
     */
    @Override
    public void registerListener(EventObject e) {
        super.registerListener(e);
        this.addClickListeners(BTN_SAVE);

        TreeView treeView = getView().getControl(CONTROL_GROUP_TREE);
        if (treeView != null) {
            treeView.addTreeNodeClickListener(this);
        }
    }

    /**
     * 处理确定按钮点击事件。
     */
    @Override
    public void click(EventObject evt) {
        handleConfirm(evt);
    }

    /**
     * 用户单击分类树节点时触发,缓存当前存货类别名称。
     */
    @Override
    public void treeNodeClick(TreeNodeEvent evt) {
        cacheSelectedCategoryName(evt);
    }

    /**
     * 用户双击分类树节点时触发,按同样逻辑缓存当前存货类别名称。
     */
    @Override
    public void treeNodeDoubleClick(TreeNodeEvent evt) {
        cacheSelectedCategoryName(evt);
    }

    /**
     * 确定按钮主流程。
     * <p>
     * 先判断当前批量分类是否是"物料 + 存货类别",避免其它分类标准误写cyjt_invcategory。
     * 通过校验后先执行平台原始确定逻辑,再写入自定义字段并刷新父页面列表。
     */
    private void handleConfirm(EventObject evt) {
        BatchGroupContext context = getBatchGroupContext();
        if (!context.isMaterialInvCategory()) {
            log.info("非物料存货类别批量分类,跳过存货类别同步。context=" + context);
            super.click(evt);
            return;
        }

        String invCategoryName = getCachedSelectedCategoryName();
        if (isBlank(invCategoryName)) {
            log.warn("未缓存到当前点击的存货类别名称,跳过写入cyjt_invcategory。context=" + context);
            super.click(evt);
            return;
        }

        super.click(evt);

        if (context.materialIds.isEmpty()) {
            log.info("物料批量分类参数中未找到dataIds,跳过存货类别同步。context=" + context);
            return;
        }

        syncMaterials(context.materialIds, invCategoryName);
        refreshParentList();
    }

    /**
     * 缓存用户点击的存货类别名称。
     * <p>
     * TreeNodeEvent只稳定提供nodeId,不稳定提供节点显示文本,所以这里按nodeId查询分类节点实体,
     * 取name后写入PageCache。使用PageCache是因为树点击和确定按钮可能是不同请求、不同插件实例。
     */
    private void cacheSelectedCategoryName(TreeNodeEvent evt) {
        String categoryName = loadCategoryNameByNodeId(evt.getNodeId());
        if (isBlank(categoryName)) {
            log.warn("未能从树节点点击事件中提取存货类别名称,nodeId=" + evt.getNodeId()
                    + ", parentNodeId=" + evt.getParentNodeId());
            return;
        }

        getPageCache().put(CACHE_SELECTED_CATEGORY_NAME, categoryName.trim());
        getPageCache().saveChanges();
        log.info("已缓存当前点击的存货类别名称:" + categoryName + ", nodeId=" + evt.getNodeId());
    }

    /**
     * 从PageCache读取树节点点击阶段缓存的存货类别名称。
     */
    private String getCachedSelectedCategoryName() {
        String cachedName = getPageCache().get(CACHE_SELECTED_CATEGORY_NAME);
        return isBlank(cachedName) ? null : cachedName.trim();
    }

    /**
     * 根据树节点nodeId加载存货类别名称。
     * <p>
     * nodeId在不同场景下可能对应实体id或masterid,因此按id查询失败后再按masterid查询。
     */
    private String loadCategoryNameByNodeId(Object nodeId) {
        Long categoryId = longValue(nodeId);
        if (categoryId == null || categoryId <= 0) {
            return null;
        }

        DynamicObject category = queryCategoryByField("id", categoryId);
        if (category == null) {
            category = queryCategoryByField("masterid", categoryId);
        }
        if (category == null) {
            log.warn("未按nodeId加载到存货类别节点,nodeId=" + nodeId);
            return null;
        }

        return category.getString("name");
    }

    /**
     * 按指定字段查询存货类别节点。
     * <p>
     * 使用QueryServiceHelper而不是BusinessDataServiceHelper.loadSingle,是为了避免nodeId不是实体主键时
     * loadSingle抛出前端可见异常。
     */
    private DynamicObject queryCategoryByField(String fieldKey, Long value) {
        try {
            return QueryServiceHelper.queryOne(
                    ENTITY_MATERIAL_CATEGORY,
                    "id,masterid,number,name",
                    new QFilter[]{new QFilter(fieldKey, QCP.equals, value)});
        } catch (Exception ex) {
            log.warn("按字段查询存货类别失败,fieldKey=" + fieldKey + ", value=" + value, ex);
            return null;
        }
    }

    /**
     * 从bd_grouptree模型和打开参数中提取本次批量分类上下文。
     */
    private BatchGroupContext getBatchGroupContext() {
        BatchGroupContext context = new BatchGroupContext();
        FormShowParameter showParameter = getView().getFormShowParameter();
        if (showParameter == null || showParameter.getCustomParams() == null) {
            return context;
        }

        Map<String, Object> params = showParameter.getCustomParams();
        context.entity = stringValue(params.get(PARAM_ENTITY));
        context.standardId = getCurrentStandardId(params);
        context.materialIds.addAll(longListValue(params.get(PARAM_DATA_IDS)));
        loadStandardInfo(context);
        return context;
    }

    /**
     * 获取当前分类标准主键。
     * <p>
     * 用户可能在弹窗下拉框中切换分类标准,所以优先读当前模型字段;读不到时再回退到打开参数standard。
     */
    private Long getCurrentStandardId(Map<String, Object> params) {
        Long currentStandardId = getCurrentStandardIdFromModel();
        return currentStandardId != null && currentStandardId > 0
                ? currentStandardId
                : longValue(params.get(PARAM_STANDARD));
    }

    /**
     * 从当前表单模型读取分类标准下拉框值。
     */
    private Long getCurrentStandardIdFromModel() {
        try {
            return longValue(getModel().getValue(FIELD_CURRENT_STANDARD));
        } catch (Exception ex) {
            log.warn("读取bd_grouptree当前分类标准失败,field=" + FIELD_CURRENT_STANDARD, ex);
            return null;
        }
    }

    /**
     * 加载分类标准编码和名称,用于判断当前批量分类是否为存货类别。
     */
    private void loadStandardInfo(BatchGroupContext context) {
        if (context.standardId == null || context.standardId <= 0) {
            return;
        }

        DynamicObject standard = BusinessDataServiceHelper.loadSingle(
                context.standardId,
                ENTITY_GROUP_STANDARD,
                "id,number,name");
        context.standardNumber = standard == null ? null : standard.getString("number");
        context.standardName = standard == null ? null : standard.getString("name");
    }

    /**
     * 批量写入物料主表自定义字段cyjt_invcategory。
     */
    private void syncMaterials(List<Long> materialIds, String invCategoryName) {
        int successCount = 0;
        int failCount = 0;

        for (Long materialId : materialIds) {
            if (materialId == null || materialId <= 0) {
                continue;
            }

            try {
                DynamicObject writeMaterial = BusinessDataServiceHelper.loadSingle(
                        materialId, ENTITY_MATERIAL, "id," + FIELD_INV_CATEGORY);
                if (writeMaterial == null) {
                    failCount++;
                    log.warn("物料批量分类同步失败,未加载到可写物料,id=" + materialId);
                    continue;
                }

                writeMaterial.set(FIELD_INV_CATEGORY, invCategoryName);
                SaveServiceHelper.update(writeMaterial);
                successCount++;
            } catch (Exception ex) {
                failCount++;
                log.error("批量分类后同步物料存货类别失败,物料ID=" + materialId
                        + ", invCategoryName=" + invCategoryName, ex);
            }
        }

        log.info("批量分类后同步物料存货类别完成,invCategoryName=" + invCategoryName
                + ",成功=" + successCount + ",失败=" + failCount);
    }

    /**
     * 刷新父页面物料序时簿,让列表中的cyjt_invcategory立即显示最新值。
     */
    private void refreshParentList() {
        IFormView parentView = getView().getParentView();
        if (parentView == null) {
            log.warn("未获取到父页面视图,无法自动刷新物料序时簿。");
            return;
        }

        try {
            parentView.invokeOperation("refresh");
            parentView.updateView();
            getView().sendFormAction(parentView);
            log.info("已通知父页面刷新物料序时簿。parentPageId=" + parentView.getPageId());
        } catch (Exception ex) {
            log.warn("刷新父页面物料序时簿失败。parentPageId=" + parentView.getPageId(), ex);
        }
    }

    /**
     * 判断字符串是否为空白。
     */
    private boolean isBlank(String value) {
        return value == null || value.trim().isEmpty();
    }

    /**
     * 将对象转为字符串,空值保持为null。
     */
    private String stringValue(Object value) {
        return value == null ? null : String.valueOf(value);
    }

    /**
     * 将平台参数转为Long,兼容DynamicObject、Number和String。
     */
    private Long longValue(Object value) {
        if (value instanceof DynamicObject) {
            return ((DynamicObject) value).getLong("id");
        }
        if (value instanceof Number) {
            return ((Number) value).longValue();
        }
        if (value instanceof String && !((String) value).trim().isEmpty()) {
            try {
                return Long.parseLong(((String) value).trim());
            } catch (NumberFormatException ex) {
                log.warn("ID参数转换失败,value=" + value, ex);
            }
        }
        return null;
    }

    /**
     * 将平台参数转为Long列表,兼容集合和单个值。
     */
    private List<Long> longListValue(Object value) {
        List<Long> result = new ArrayList<>();
        if (value instanceof Collection) {
            for (Object item : (Collection<?>) value) {
                Long id = longValue(item);
                if (id != null) {
                    result.add(id);
                }
            }
            return result;
        }

        Long singleId = longValue(value);
        if (singleId != null) {
            result.add(singleId);
        }
        return result;
    }

    /**
     * bd_grouptree批量分类上下文。
     */
    private static class BatchGroupContext {
        /** 来源实体标识,例如bd_material。 */
        private String entity;

        /** 当前分类标准主键。 */
        private Long standardId;

        /** 当前分类标准编码。 */
        private String standardNumber;

        /** 当前分类标准名称。 */
        private String standardName;

        /** 本次批量分类选中的物料主键集合。 */
        private final List<Long> materialIds = new ArrayList<>();

        /**
         * 判断当前上下文是否为物料的存货类别分类。
         */
        private boolean isMaterialInvCategory() {
            if (!ENTITY_MATERIAL.equals(entity)) {
                return false;
            }
            return INV_CATEGORY_STANDARD_NUMBER.equals(standardNumber)
                    || INV_CATEGORY_STANDARD_NAME.equals(standardName);
        }

        @Override
        public String toString() {
            return "BatchGroupContext{"
                    + "entity='" + entity + '\''
                    + ", standardId=" + standardId
                    + ", standardNumber='" + standardNumber + '\''
                    + ", standardName='" + standardName + '\''
                    + ", materialIds=" + materialIds
                    + '}';
        }
    }
}
相关推荐
日月云棠5 小时前
11 Spring容器整合与核心接口体系
java·后端
日月云棠5 小时前
10 AOP与动态编译源码剖析
java·后端
AI人工智能+电脑小能手5 小时前
【大白话说Java面试题 第70题】【JVM篇】第30题:垃圾回收器是怎样寻找 GC Roots 的?
java·开发语言·jvm·面试
彦为君6 小时前
JavaSE-11-网络编程(详细版)
java·前端·网络·ai·ai编程
毅炼6 小时前
今日LeetCode 摸鱼打卡
java·算法·leetcode
一个做软件开发的牛马6 小时前
我用 Java 写了一个猜数字游戏,踩了 3 个流程控制的坑
java
Byron07076 小时前
后端架构核心技术栈详解
java·架构
专注VB编程开发20年6 小时前
Python 的 C 扩展,本质上就是“去中心化的 COM”
java·服务器·开发语言·ide·python
LB21126 小时前
消灭并发重复调用:基于 Agent 调用 LLM 的分布式 Single-Flight 实战
java·开发语言·redis·分布式·agent