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

步骤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
+ '}';
}
}
}