diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/doc/代码修改日志 b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/doc/代码修改日志 index d921adb..ce96a24 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/doc/代码修改日志 +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/doc/代码修改日志 @@ -944,6 +944,42 @@ jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixingSpecServiceImpl.java jeecgboot-vue3/src/views/xslmes/mesXslMixingSpec/components/MesXslMixingSpecModal.vue +-- author:GHT---date:20260610--for: 【IM审批通用化】IM工作通知公众号(同事列表置顶+审批消息统一推送) ----- +jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_147__sys_im_work_notify_user.sql +jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/impl/SysImChatServiceImpl.java +jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/vo/SysImContactVO.java +jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/ISysImChatService.java +jeecgboot-vue3/src/views/system/im/ImChat.vue +jeecgboot-vue3/src/views/system/im/imCache.ts +jeecgboot-vue3/src/views/system/im/ImCreateGroupModal.vue + +-- author:GHT---date:20260610--for: 【IM审批通用化】审批待办IM发送修复(admin自审重复成员+独立事务防审批回滚) ----- +jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/impl/SysImChatServiceImpl.java + +-- author:GHT---date:20260610--for: 【IM审批通用化】IM卡片字段取值修复(下划线列名+valueMode与钉钉发起对齐) ----- +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/service/DingTplImCardBuilder.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/service/DingTplBindFieldValueResolver.java + +-- author:GHT---date:20260610--for: 【IM审批通用化】流转中实例支持补发IM审批卡片(审批台账入口) ----- +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/IMesXslApprovalHandleService.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/impl/MesXslApprovalHandleServiceImpl.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalHandleController.java +jeecgboot-vue3/src/views/approval/flow/approvalHandle.api.ts +jeecgboot-vue3/src/views/xslmes/approval/mesXslApprovalRecord/MesXslApprovalRecordList.vue + +-- author:GHT---date:20260610--for: 【IM审批通用化】发起人=处理人时审批待办改由admin代发IM消息 ----- +jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/ISysImChatService.java +jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/impl/SysImChatServiceImpl.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/impl/MesXslApprovalHandleServiceImpl.java + +-- author:GHT---date:20260610--for: 【IM审批通用化】IM卡片复用钉钉模板字段+MES回调补齐stageKey走集成方案 ----- +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/service/DingTplImCardBuilder.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/service/IMesXslDingTplBindService.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/service/impl/MesXslDingTplBindServiceImpl.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/impl/MesXslApprovalHandleServiceImpl.java +jeecgboot-vue3/src/views/system/im/imBizRecordMessage.ts +jeecgboot-vue3/src/views/system/im/ImBizRecordMessageContent.vue + -- author:GHT---date:20260610--for: 【混炼示方】TCU温度条件新增是否附加/重量字段 ----- jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_146__mes_xsl_mixing_spec_tcu_attach.sql jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslMixingSpecTcu.java diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalHandleController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalHandleController.java index 6860c3e..11e28e0 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalHandleController.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalHandleController.java @@ -108,4 +108,20 @@ public class MesXslApprovalHandleController { return Result.OK(approvalHandleService.pendingList(user)); } //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】待办列表----- + + //update-begin---author:GHT ---date:20260610 for:【IM审批通用化】补发IM审批卡片(历史流转中实例)----------- + @Operation(summary = "审批办理-补发IM审批卡片(流转中)") + @PostMapping("/resendCard") + public Result resendCard(@RequestBody Map body) { + String instanceId = body.get("instanceId") == null ? null : String.valueOf(body.get("instanceId")); + String bizTable = body.get("bizTable") == null ? null : String.valueOf(body.get("bizTable")); + String bizDataId = body.get("bizDataId") == null ? null : String.valueOf(body.get("bizDataId")); + if (oConvertUtils.isEmpty(instanceId) + && (oConvertUtils.isEmpty(bizTable) || oConvertUtils.isEmpty(bizDataId))) { + return Result.error("请提供 instanceId 或 bizTable+bizDataId"); + } + LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + return approvalHandleService.resendCard(instanceId, bizTable, bizDataId, user); + } + //update-end---author:GHT ---date:20260610 for:【IM审批通用化】补发IM审批卡片(历史流转中实例)----------- } diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/IMesXslApprovalHandleService.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/IMesXslApprovalHandleService.java index 4740346..9b7abb5 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/IMesXslApprovalHandleService.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/IMesXslApprovalHandleService.java @@ -57,6 +57,14 @@ public interface IMesXslApprovalHandleService { Result urge(String instanceId, LoginUser user); //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】催办接口----- + //update-begin---author:GHT ---date:20260610 for:【IM审批通用化】流转中实例补发IM审批卡片----------- + /** + * 补发当前节点审批卡片(用于历史数据未收到 IM 消息等场景)。 + * instanceId 与 (bizTable+bizDataId) 二选一;仅审批中且 MES 通道实例可补发。 + */ + Result resendCard(String instanceId, String bizTable, String bizDataId, LoginUser user); + //update-end---author:GHT ---date:20260610 for:【IM审批通用化】流转中实例补发IM审批卡片----------- + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】待办列表查询----- /** * 查询当前用户的待办审批列表(状态为审批中且当前处理人包含该用户)。 diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/impl/MesXslApprovalHandleServiceImpl.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/impl/MesXslApprovalHandleServiceImpl.java index 11b4010..7936d6f 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/impl/MesXslApprovalHandleServiceImpl.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/impl/MesXslApprovalHandleServiceImpl.java @@ -23,6 +23,7 @@ import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalHandleService; import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationPlan; import org.jeecg.modules.xslmes.approval.integration.service.IMesXslIntegrationPlanService; import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalInstanceService; +import org.jeecg.modules.xslmes.dingtalk.service.DingTplImCardBuilder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; @@ -100,6 +101,11 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer @Autowired private IMesXslIntegrationPlanService integrationPlanService; //update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】驳回回退改由集成方案 onReject 驱动----------- + + //update-begin---author:GHT ---date:20260610 for:【IM审批通用化】IM卡片字段与钉钉模板绑定对齐----------- + @Autowired + private DingTplImCardBuilder dingTplImCardBuilder; + //update-end---author:GHT ---date:20260610 for:【IM审批通用化】IM卡片字段与钉钉模板绑定对齐----------- //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】审批与业务单据联动回调----- // ==================== 发起后进入首节点 ==================== @@ -691,11 +697,11 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer oConvertUtils.getString(inst.getApplyUserName(), inst.getApplyUser()), inst.getFlowName(), actionLabel); msgType = "text"; } - SysUser applicant = getUserSafely(inst.getApplyUser()); - String fromId = applicant == null ? null : applicant.getId(); + //update-begin---author:GHT ---date:20260610 for:【IM审批通用化】待办卡片由系统账号发给处理人,支持发起人=处理人----------- for (String uname : handlerUsernames) { - sendOne(fromId, uname, inst.getTenantId(), content, msgType); + sendApprovalHandlerNotify(uname, inst.getTenantId(), content, msgType); } + //update-end---author:GHT ---date:20260610 for:【IM审批通用化】待办卡片由系统账号发给处理人,支持发起人=处理人----------- } /** 抄送通知(无办理按钮) */ @@ -755,6 +761,24 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer } } + //update-begin---author:GHT ---date:20260610 for:【IM审批通用化】审批待办IM通知----------- + private void sendApprovalHandlerNotify(String toUsername, Integer tenantId, String content, String msgType) { + String uname = toUsername == null ? "" : toUsername.trim(); + if (oConvertUtils.isEmpty(uname)) { + return; + } + try { + SysUser to = sysUserService.getUserByName(uname); + if (to == null) { + return; + } + sysImChatService.sendApprovalHandlerMessage(to.getId(), tenantId, content, msgType); + } catch (Exception e) { + log.warn("发送审批待办IM消息失败 to={}", uname, e); + } + } + //update-end---author:GHT ---date:20260610 for:【IM审批通用化】审批待办IM通知----------- + /** 构建 biz_record 卡片 JSON(含 instanceId / actionLabel / canApprove,与前端 ImBizRecordPayload 对齐 v=2) */ private String buildCardJson(MesXslApprovalInstance inst, String actionLabel, boolean canApprove, String routePath) { //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】区分审批卡片与抄送通知卡片----- @@ -766,6 +790,15 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer */ private String buildCardJson(MesXslApprovalInstance inst, String actionLabel, boolean canApprove, String routePath, boolean approvalCard) { //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】区分审批卡片与抄送通知卡片----- + //update-begin---author:GHT ---date:20260610 for:【IM审批通用化】优先按钉钉模板绑定构建卡片字段----------- + MesXslApprovalFlow flow = flowService.getById(inst.getFlowId()); + JSONObject dingPayload = dingTplImCardBuilder.buildCardPayload( + inst, flow, actionLabel, canApprove, routePath, approvalCard); + if (dingPayload != null) { + return dingPayload.toJSONString(); + } + //update-end---author:GHT ---date:20260610 for:【IM审批通用化】优先按钉钉模板绑定构建卡片字段----------- + JSONArray fields = new JSONArray(); addField(fields, "审批流", inst.getFlowName()); addField(fields, "单据", safeTitle(inst)); @@ -1353,10 +1386,26 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer if (user != null) { ctx.setOperatorUsername(user.getUsername()); ctx.setOperatorName(oConvertUtils.getString(user.getRealname(), user.getUsername())); + ctx.setOperatorTime(new Date()); } else { ctx.setOperatorUsername("system"); ctx.setOperatorName("系统"); } + //update-begin---author:GHT ---date:20260610 for:【IM审批通用化】MES通道补齐stageKey,与钉钉回调共用集成方案----------- + if (oConvertUtils.isNotEmpty(nodeId) && oConvertUtils.isNotEmpty(inst.getFlowId())) { + MesXslApprovalFlow flow = flowService.getById(inst.getFlowId()); + if (flow != null && oConvertUtils.isNotEmpty(flow.getFlowConfig())) { + JSONObject root = safeParse(flow.getFlowConfig()); + JSONObject node = root == null ? null : findNodeById(root, nodeId); + if (node != null) { + JSONObject props = node.getJSONObject("props"); + if (props != null && props.containsKey("stageKey")) { + ctx.setStageKey(props.getString("stageKey")); + } + } + } + } + //update-end---author:GHT ---date:20260610 for:【IM审批通用化】MES通道补齐stageKey,与钉钉回调共用集成方案----------- return ctx; } @@ -1455,7 +1504,7 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer if (oConvertUtils.isEmpty(uname)) { continue; } - sendOne(user.getId(), uname, inst.getTenantId(), + sendApprovalHandlerNotify(uname, inst.getTenantId(), "【催办提醒】" + applicantName + " 催促您处理「" + safeTitle(inst) + "」,请尽快审批。", "text"); } urgeTimeMap.put(instanceId, System.currentTimeMillis()); @@ -1463,6 +1512,77 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer } //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】催办:发起人向当前处理人发催办提醒----- + //update-begin---author:GHT ---date:20260610 for:【IM审批通用化】流转中实例补发IM审批卡片----------- + @Override + public Result resendCard(String instanceId, String bizTable, String bizDataId, LoginUser user) { + if (user == null) { + return Result.error("请先登录"); + } + MesXslApprovalInstance inst = resolveRunningInstance(instanceId, bizTable, bizDataId); + if (inst == null) { + return Result.error("未找到审批中的 MES 审批实例"); + } + if (!"0".equals(inst.getStatus())) { + return Result.error("该审批已结束,无法补发卡片"); + } + if (!canResendCard(user, inst)) { + return Result.error("仅发起人或当前处理人可以补发审批卡片"); + } + if (oConvertUtils.isEmpty(inst.getCurrentHandlers())) { + return Result.error("当前无待处理人,无法补发"); + } + MesXslApprovalFlow flow = flowService.getById(inst.getFlowId()); + if (flow == null) { + return Result.error("审批流不存在"); + } + List handlers = new ArrayList<>(); + for (String uname : inst.getCurrentHandlers().split(",")) { + if (oConvertUtils.isNotEmpty(uname)) { + handlers.add(uname.trim()); + } + } + if (handlers.isEmpty()) { + return Result.error("当前无待处理人,无法补发"); + } + String actionLabel = oConvertUtils.getString(inst.getCurrentNodeName(), "审批"); + sendApprovalCard(inst, flow, actionLabel, handlers); + return Result.OK("已向 " + handlers.size() + " 位处理人补发审批卡片,请在 IM 中查看与 admin 的会话"); + } + + private MesXslApprovalInstance resolveRunningInstance(String instanceId, String bizTable, String bizDataId) { + if (oConvertUtils.isNotEmpty(instanceId)) { + MesXslApprovalInstance inst = instanceService.getById(instanceId); + return inst == null || !"0".equals(inst.getStatus()) ? null : inst; + } + if (oConvertUtils.isEmpty(bizTable) || oConvertUtils.isEmpty(bizDataId) + || !IDENTIFIER.matcher(bizTable).matches()) { + return null; + } + return instanceService.lambdaQuery() + .eq(MesXslApprovalInstance::getBizTable, bizTable) + .eq(MesXslApprovalInstance::getBizDataId, bizDataId) + .eq(MesXslApprovalInstance::getStatus, "0") + .orderByDesc(MesXslApprovalInstance::getCreateTime) + .last("LIMIT 1") + .one(); + } + + private boolean canResendCard(LoginUser user, MesXslApprovalInstance inst) { + if (user.getUsername().equals(inst.getApplyUser())) { + return true; + } + if (oConvertUtils.isEmpty(inst.getCurrentHandlers())) { + return false; + } + for (String uname : inst.getCurrentHandlers().split(",")) { + if (user.getUsername().equals(uname == null ? "" : uname.trim())) { + return true; + } + } + return false; + } + //update-end---author:GHT ---date:20260610 for:【IM审批通用化】流转中实例补发IM审批卡片----------- + // ==================== 待办列表 ==================== //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】待办列表:查询当前用户的待处理审批实例----- diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/service/DingTplBindFieldValueResolver.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/service/DingTplBindFieldValueResolver.java index b691de8..4ef4199 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/service/DingTplBindFieldValueResolver.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/service/DingTplBindFieldValueResolver.java @@ -3,6 +3,7 @@ package org.jeecg.modules.xslmes.dingtalk.service; import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.jeecg.common.util.oConvertUtils; import org.jeecg.modules.print.vo.PrintBizFieldItemVO; import org.jeecg.modules.system.service.ISysDictService; import org.springframework.beans.factory.annotation.Autowired; @@ -79,7 +80,7 @@ public class DingTplBindFieldValueResolver { return null; } if (cur instanceof Map) { - cur = ((Map) cur).get(p); + cur = getMapValue((Map) cur, p); } else { return null; } @@ -87,6 +88,32 @@ public class DingTplBindFieldValueResolver { return cur; } + /** JDBC 行多为下划线列名,绑定配置为驼峰字段名,双向兼容取值 */ + @SuppressWarnings("unchecked") + private Object getMapValue(Map map, String key) { + if (map == null || StringUtils.isBlank(key)) { + return null; + } + Object val = map.get(key); + if (val != null) { + return val; + } + String underline = oConvertUtils.camelToUnderline(key); + if (!underline.equals(key)) { + val = map.get(underline); + if (val != null) { + return val; + } + } + if (key.contains("_")) { + String camel = oConvertUtils.camelName(key); + if (!camel.equals(key)) { + val = map.get(camel); + } + } + return val; + } + @SuppressWarnings("unchecked") private String getDictTextFromRow(Object rowData, String bizField) { if (!(rowData instanceof Map) || StringUtils.isBlank(bizField)) { @@ -95,13 +122,13 @@ public class DingTplBindFieldValueResolver { Map map = (Map) rowData; String[] parts = bizField.split("\\."); if (parts.length == 1) { - Object v = map.get(parts[0] + "_dictText"); + Object v = getMapValue(map, parts[0] + "_dictText"); return v != null ? String.valueOf(v) : null; } String parentPath = String.join(".", java.util.Arrays.copyOf(parts, parts.length - 1)); Object parent = getNestedValue(rowData, parentPath); if (parent instanceof Map) { - Object v = ((Map) parent).get(parts[parts.length - 1] + "_dictText"); + Object v = getMapValue((Map) parent, parts[parts.length - 1] + "_dictText"); return v != null ? String.valueOf(v) : null; } return null; diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/service/DingTplImCardBuilder.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/service/DingTplImCardBuilder.java new file mode 100644 index 0000000..ae351d0 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/service/DingTplImCardBuilder.java @@ -0,0 +1,339 @@ +package org.jeecg.modules.xslmes.dingtalk.service; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.jeecg.common.util.oConvertUtils; +import org.jeecg.modules.print.catalog.IPrintBizEntityFieldCatalogProvider; +import org.jeecg.modules.print.service.IPrintBizPermEntityService; +import org.jeecg.modules.print.util.PrintBizDetailPropertyScanner; +import org.jeecg.modules.print.util.PrintBizEntityFieldIntrospector; +import org.jeecg.modules.print.vo.PrintBizDetailSlotVO; +import org.jeecg.modules.print.vo.PrintBizFieldItemVO; +import org.jeecg.modules.print.vo.PrintBizTypeVO; +import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalFlow; +import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalInstance; +import org.jeecg.modules.xslmes.dingtalk.entity.MesXslDingProcessTpl; +import org.jeecg.modules.xslmes.dingtalk.entity.MesXslDingTplBind; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; + +/** + * 按钉钉审批模板绑定配置构建 IM 聊天审批卡片字段,与钉钉发起审批表单展示对齐。 + */ +@Slf4j +@Service +public class DingTplImCardBuilder { + + private static final Pattern IDENTIFIER = Pattern.compile("^[A-Za-z0-9_]+$"); + private static final String CARD_STYLE_DING = "ding"; + /** 与前端 dingTplFieldValue.defaultValueMode 一致:下拉类控件默认原值 */ + private static final Set DD_SELECT_COMPONENTS = Set.of( + "DDSelectField", "DDMultiSelectField", "DepartmentField", "InnerContactField"); + + @Autowired + private IMesXslDingTplBindService bindService; + @Autowired + private IMesXslDingProcessTplService tplService; + @Autowired + private DingTplBindFieldValueResolver fieldValueResolver; + @Autowired + private IPrintBizPermEntityService printBizPermEntityService; + @Autowired + private JdbcTemplate jdbcTemplate; + + @Autowired(required = false) + private IPrintBizEntityFieldCatalogProvider fieldCatalogProvider; + + /** + * 若存在启用的钉钉模板绑定,则按绑定字段构建完整 biz_record 载荷;否则返回 null 由调用方降级。 + */ + public JSONObject buildCardPayload(MesXslApprovalInstance inst, MesXslApprovalFlow flow, + String actionLabel, boolean canApprove, String routePath, + boolean approvalCard) { + if (inst == null || oConvertUtils.isEmpty(routePath)) { + return null; + } + MesXslDingTplBind bind = bindService.resolveActiveByRoutePath(routePath); + if (bind == null || oConvertUtils.isEmpty(bind.getFieldMappingJson())) { + return null; + } + MesXslDingProcessTpl tpl = tplService.getById(bind.getTemplateId()); + if (tpl == null || !"1".equals(tpl.getStatus())) { + return null; + } + + Map rowData = loadBizRow(inst.getBizTable(), inst.getBizDataId()); + if (rowData.isEmpty()) { + return null; + } + + Map metaMap = buildFieldMetaMap(bind.getBizCode()); + JSONArray fields = buildDingStyleFields(bind.getFieldMappingJson(), rowData, metaMap); + appendApprovalMetaFields(fields, inst, flow, approvalCard); + + JSONObject item = buildItem(inst, routePath, fields, actionLabel, canApprove, approvalCard); + JSONArray items = new JSONArray(); + items.add(item); + + JSONObject payload = new JSONObject(); + payload.put("v", 2); + payload.put("cardStyle", CARD_STYLE_DING); + payload.put("templateId", bind.getTemplateId()); + payload.put("templateName", oConvertUtils.getString(bind.getTemplateName(), tpl.getTplName())); + payload.put("pageTitle", oConvertUtils.getString(inst.getBizTableName(), flow != null ? flow.getFlowName() : inst.getFlowName())); + payload.put("pagePath", routePath); + payload.put("rowKey", "id"); + payload.put("items", items); + return payload; + } + + private JSONArray buildDingStyleFields(String mappingJson, Map rowData, + Map metaMap) { + JSONArray fields = new JSONArray(); + JSONArray mappings; + try { + mappings = JSON.parseArray(mappingJson); + } catch (Exception e) { + log.warn("解析钉钉模板绑定 JSON 失败: {}", e.getMessage()); + return fields; + } + if (mappings == null || mappings.isEmpty()) { + return fields; + } + for (int i = 0; i < mappings.size(); i++) { + JSONObject mapping = mappings.getJSONObject(i); + if (mapping == null) { + continue; + } + String componentName = mapping.getString("componentName"); + if ("TableField".equals(componentName)) { + continue; + } + String label = mapping.getString("componentLabel"); + String bizField = mapping.getString("bizField"); + if (oConvertUtils.isEmpty(label) || oConvertUtils.isEmpty(bizField)) { + if ("TextNote".equals(componentName) && oConvertUtils.isNotEmpty(label)) { + addField(fields, label, ""); + } + continue; + } + PrintBizFieldItemVO meta = metaMap.get(bizField.trim()); + String valueMode = resolveValueMode(mapping, meta); + Object val = fieldValueResolver.resolveValue(rowData, bizField.trim(), valueMode, meta); + addField(fields, label, formatValue(val)); + } + return fields; + } + + private void appendApprovalMetaFields(JSONArray fields, MesXslApprovalInstance inst, + MesXslApprovalFlow flow, boolean approvalCard) { + addField(fields, "审批流", oConvertUtils.getString(inst.getFlowName(), + flow != null ? flow.getFlowName() : "")); + addField(fields, approvalCard ? "当前节点" : "知会", + oConvertUtils.getString(inst.getCurrentNodeName(), approvalCard ? "审批" : "抄送")); + addField(fields, "状态", statusText(inst.getStatus())); + } + + private JSONObject buildItem(MesXslApprovalInstance inst, String routePath, JSONArray fields, + String actionLabel, boolean canApprove, boolean approvalCard) { + JSONObject item = new JSONObject(); + item.put("recordId", inst.getBizDataId()); + item.put("fields", fields); + StringBuilder body = new StringBuilder(); + for (int i = 0; i < fields.size(); i++) { + JSONObject f = fields.getJSONObject(i); + if (i > 0) { + body.append("\n"); + } + body.append(f.getString("label")).append(": ").append(f.getString("value")); + } + item.put("body", body.toString()); + String sep = routePath.contains("?") ? "&" : "?"; + item.put("linkPath", routePath + sep + "imRecordId=" + inst.getBizDataId()); + if (approvalCard) { + item.put("instanceId", inst.getId()); + item.put("canApprove", canApprove); + item.put("nodeId", inst.getCurrentNodeId()); + if (oConvertUtils.isNotEmpty(actionLabel)) { + item.put("actionLabel", actionLabel); + } + } + return item; + } + + private Map loadBizRow(String table, String bizDataId) { + if (oConvertUtils.isEmpty(table) || oConvertUtils.isEmpty(bizDataId) + || !IDENTIFIER.matcher(table).matches()) { + return Collections.emptyMap(); + } + try { + List> rows = jdbcTemplate.queryForList( + "SELECT * FROM `" + table + "` WHERE id = ? LIMIT 1", bizDataId); + if (rows.isEmpty()) { + return Collections.emptyMap(); + } + //update-begin---author:GHT ---date:20260610 for:【IM审批通用化】JDBC列名下划线转驼峰,与绑定字段对齐----------- + return normalizeRowKeys(rows.get(0)); + //update-end---author:GHT ---date:20260610 for:【IM审批通用化】JDBC列名下划线转驼峰,与绑定字段对齐----------- + } catch (Exception e) { + log.warn("加载业务单据失败 table={} id={}", table, bizDataId, e); + return Collections.emptyMap(); + } + } + + private Map normalizeRowKeys(Map raw) { + Map normalized = new LinkedHashMap<>(); + if (raw == null) { + return normalized; + } + for (Map.Entry e : raw.entrySet()) { + String key = e.getKey(); + if (oConvertUtils.isEmpty(key)) { + continue; + } + normalized.put(key, e.getValue()); + if (key.contains("_")) { + String camel = oConvertUtils.camelName(key); + normalized.putIfAbsent(camel, e.getValue()); + } + } + return normalized; + } + + /** 与钉钉发起弹窗 defaultValueMode 对齐 */ + private String resolveValueMode(JSONObject mapping, PrintBizFieldItemVO meta) { + String mode = mapping.getString("valueMode"); + if (oConvertUtils.isNotEmpty(mode)) { + return mode.trim(); + } + if (meta == null || oConvertUtils.isEmpty(meta.getTranslateKind()) + || "NONE".equalsIgnoreCase(meta.getTranslateKind())) { + return "raw"; + } + String componentName = mapping.getString("componentName"); + if (oConvertUtils.isNotEmpty(componentName) && DD_SELECT_COMPONENTS.contains(componentName)) { + return "raw"; + } + return "text"; + } + + private Map buildFieldMetaMap(String bizCode) { + if (oConvertUtils.isEmpty(bizCode)) { + return Collections.emptyMap(); + } + Map map = new LinkedHashMap<>(); + for (PrintBizFieldItemVO f : listMainFieldsEnriched(bizCode)) { + if (f != null && StringUtils.isNotBlank(f.getFieldKey())) { + map.put(f.getFieldKey(), f); + } + } + Class mainCls = resolveEntityClass(bizCode); + if (mainCls != null && fieldCatalogProvider != null && fieldCatalogProvider.hasCatalogForBiz(bizCode)) { + for (PrintBizDetailSlotVO slot : fieldCatalogProvider.listDetailSlots(bizCode)) { + mergeDetailMeta(map, bizCode, slot.getPropertyName(), slot.getSlotKind()); + } + } else if (mainCls != null) { + for (PrintBizDetailSlotVO slot : PrintBizDetailPropertyScanner.listSlots(mainCls)) { + mergeDetailMeta(map, bizCode, slot.getPropertyName(), slot.getSlotKind()); + } + } + return map; + } + + private List listMainFieldsEnriched(String bizCode) { + Class cls = resolveEntityClass(bizCode); + List fields; + if (fieldCatalogProvider != null && fieldCatalogProvider.hasCatalogForBiz(bizCode)) { + fields = fieldCatalogProvider.listMainFields(bizCode); + } else if (cls != null) { + fields = PrintBizEntityFieldIntrospector.listFields(cls); + } else { + return Collections.emptyList(); + } + if (cls != null && fields != null && !fields.isEmpty()) { + PrintBizEntityFieldIntrospector.enrichDictMeta(fields, cls, null); + } + return fields != null ? fields : Collections.emptyList(); + } + + private void mergeDetailMeta(Map map, String bizCode, String prop, String kind) { + List detailFields; + if (fieldCatalogProvider != null && fieldCatalogProvider.hasCatalogForBiz(bizCode)) { + detailFields = fieldCatalogProvider.listPrefixedDetailFields(bizCode, prop, kind); + } else { + Class mainCls = resolveEntityClass(bizCode); + detailFields = mainCls == null + ? Collections.emptyList() + : PrintBizDetailPropertyScanner.listPrefixedDetailFields(mainCls, prop, kind); + } + Class itemCls = resolveDetailItemClass(bizCode, prop, kind); + if (itemCls != null && detailFields != null && !detailFields.isEmpty()) { + PrintBizEntityFieldIntrospector.enrichDictMeta(detailFields, itemCls, null); + } + if (detailFields != null) { + for (PrintBizFieldItemVO f : detailFields) { + if (f != null && StringUtils.isNotBlank(f.getFieldKey())) { + map.put(f.getFieldKey(), f); + } + } + } + } + + private Class resolveEntityClass(String bizCode) { + PrintBizTypeVO vo = printBizPermEntityService.resolveBizTypeVo(bizCode); + if (vo == null || StringUtils.isBlank(vo.getDescription())) { + return null; + } + return PrintBizEntityFieldIntrospector.tryLoadClass(vo.getDescription().trim()); + } + + private Class resolveDetailItemClass(String bizCode, String detailProperty, String slotKind) { + Class mainCls = resolveEntityClass(bizCode); + if (mainCls == null) { + return null; + } + return PrintBizDetailPropertyScanner.resolveItemClassForSlot(mainCls, detailProperty, slotKind); + } + + private void addField(JSONArray fields, String label, String value) { + JSONObject f = new JSONObject(); + f.put("label", label); + f.put("value", oConvertUtils.getString(value, "")); + fields.add(f); + } + + private String formatValue(Object val) { + if (val == null) { + return ""; + } + if (val instanceof Date) { + return new SimpleDateFormat("yyyy-MM-dd").format((Date) val); + } + return String.valueOf(val); + } + + private String statusText(String status) { + if ("1".equals(status)) { + return "已通过"; + } + if ("2".equals(status)) { + return "已驳回"; + } + if ("3".equals(status)) { + return "已撤销"; + } + return "审批中"; + } +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/service/IMesXslDingTplBindService.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/service/IMesXslDingTplBindService.java index 9b18bfd..2371f11 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/service/IMesXslDingTplBindService.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/service/IMesXslDingTplBindService.java @@ -13,4 +13,7 @@ public interface IMesXslDingTplBindService extends IService { /** 按业务编码查询绑定记录(未删除的第一条) */ MesXslDingTplBind getByBizCode(String bizCode); + + /** 按前端路由解析启用的钉钉模板绑定(模板停用则返回 null) */ + MesXslDingTplBind resolveActiveByRoutePath(String routePath); } diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/service/impl/MesXslDingTplBindServiceImpl.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/service/impl/MesXslDingTplBindServiceImpl.java index e539b3a..fa58d5b 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/service/impl/MesXslDingTplBindServiceImpl.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/service/impl/MesXslDingTplBindServiceImpl.java @@ -2,11 +2,18 @@ package org.jeecg.modules.xslmes.dingtalk.service.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.apache.commons.lang3.StringUtils; +import org.jeecg.modules.xslmes.dingtalk.entity.MesXslDingProcessTpl; import org.jeecg.modules.xslmes.dingtalk.entity.MesXslDingTplBind; import org.jeecg.modules.xslmes.dingtalk.mapper.MesXslDingTplBindMapper; +import org.jeecg.modules.xslmes.dingtalk.service.IMesXslDingProcessTplService; import org.jeecg.modules.xslmes.dingtalk.service.IMesXslDingTplBindService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; +import java.util.List; + /** * 钉钉审批模板绑定 ServiceImpl * @@ -17,8 +24,43 @@ import org.springframework.stereotype.Service; public class MesXslDingTplBindServiceImpl extends ServiceImpl implements IMesXslDingTplBindService { + @Autowired + private JdbcTemplate jdbcTemplate; + + @Autowired + private IMesXslDingProcessTplService tplService; + @Override public MesXslDingTplBind getByBizCode(String bizCode) { return getOne(new QueryWrapper().eq("biz_code", bizCode).last("LIMIT 1")); } + + //update-begin---author:GHT ---date:20260610 for:【IM审批通用化】按路由解析启用的钉钉模板绑定----------- + @Override + public MesXslDingTplBind resolveActiveByRoutePath(String routePath) { + if (StringUtils.isBlank(routePath)) { + return null; + } + String path = routePath.trim(); + String sql = "SELECT id FROM sys_permission WHERE url = ? AND del_flag = 0 LIMIT 1"; + List ids; + try { + ids = jdbcTemplate.queryForList(sql, String.class, path); + } catch (Exception e) { + return null; + } + if (ids.isEmpty()) { + return null; + } + MesXslDingTplBind bind = getByBizCode(ids.get(0)); + if (bind == null || StringUtils.isBlank(bind.getTemplateId())) { + return null; + } + MesXslDingProcessTpl tpl = tplService.getById(bind.getTemplateId()); + if (tpl == null || !"1".equals(tpl.getStatus())) { + return null; + } + return bind; + } + //update-end---author:GHT ---date:20260610 for:【IM审批通用化】按路由解析启用的钉钉模板绑定----------- } diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/ISysImChatService.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/ISysImChatService.java index 621d174..d73957c 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/ISysImChatService.java +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/ISysImChatService.java @@ -61,6 +61,11 @@ public interface ISysImChatService { * @return 消息VO,发送失败返回 null */ SysImMessageVO sendSystemSingleMessage(String fromUserId, String toUserId, Integer tenantId, String content, String msgType); + + /** + * 审批待办卡片:从「工作通知」公众号推送给处理人。 + */ + SysImMessageVO sendApprovalHandlerMessage(String toUserId, Integer tenantId, String content, String msgType); //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】系统单聊消息(绕过同部门校验,供审批等系统通知场景)----- diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/impl/SysImChatServiceImpl.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/impl/SysImChatServiceImpl.java index d715f1f..68a3cbc 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/impl/SysImChatServiceImpl.java +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/impl/SysImChatServiceImpl.java @@ -36,6 +36,7 @@ import org.jeecg.modules.system.mapper.SysUserTenantMapper; import org.jeecg.modules.system.service.ISysPermissionService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; @@ -64,6 +65,13 @@ public class SysImChatServiceImpl implements ISysImChatService { private static final String MSG_IMAGE_PREVIEW = "[图片]"; private static final String MSG_BIZ_RECORD_PREVIEW = "[业务数据]"; private static final String IM_RECORD_QUERY_KEY = "imRecordId"; + /** IM 工作通知公众号账号(审批等系统消息统一从此账号推送) */ + private static final String WORK_NOTIFY_USERNAME = "im_work_notify"; + private static final String WORK_NOTIFY_REALNAME = "工作通知"; + private static final String WORK_NOTIFY_USER_ID = "1995000000000000999"; + private static final String CONTACT_TYPE_WORK_NOTIFY = "work_notify"; + private static final String CONTACT_TYPE_USER = "user"; + private volatile String workNotifyUserIdCache; @Autowired private ISysPermissionService sysPermissionService; @@ -107,7 +115,11 @@ public class SysImChatServiceImpl implements ISysImChatService { if (conversation != null) { return buildConversationVo(conversation, userId, targetUserId); } - validateTenantChat(userId, tenantId, orgCode, targetUserId); + //update-begin---author:GHT ---date:20260610 for:【IM审批通用化】工作通知公众号会话免同部门校验----------- + if (!isWorkNotifyUser(targetUserId)) { + validateTenantChat(userId, tenantId, orgCode, targetUserId); + } + //update-end---author:GHT ---date:20260610 for:【IM审批通用化】工作通知公众号会话免同部门校验----------- //update-end---author:cursor ---date:20260528 for:【IM聊天-OA】已有会话快速打开----------- conversation = new SysImConversation(); conversation.setConvType(CONV_TYPE_SINGLE); @@ -439,6 +451,11 @@ public class SysImChatServiceImpl implements ISysImChatService { throw new JeecgBootException("消息内容不能为空"); } assertMember(userId, dto.getConversationId()); + //update-begin---author:GHT ---date:20260610 for:【IM审批通用化】工作通知公众号只读,禁止用户发消息----------- + if (isWorkNotifyConversation(dto.getConversationId())) { + throw new JeecgBootException("工作通知为系统消息通道,不支持发送消息"); + } + //update-end---author:GHT ---date:20260610 for:【IM审批通用化】工作通知公众号只读,禁止用户发消息----------- Date now = new Date(); SysImMessage message = new SysImMessage(); message.setConversationId(dto.getConversationId()); @@ -480,8 +497,8 @@ public class SysImChatServiceImpl implements ISysImChatService { if (oConvertUtils.isEmpty(fromUserId) || oConvertUtils.isEmpty(toUserId) || oConvertUtils.isEmpty(content)) { return null; } - // 不给自己发送 if (fromUserId.equals(toUserId)) { + log.warn("IM系统消息跳过:收发为同一人 toUserId={}", toUserId); return null; } Integer tenant = tenantId == null ? 0 : tenantId; @@ -500,8 +517,17 @@ public class SysImChatServiceImpl implements ISysImChatService { conversation.setCreateTime(now); conversation.setUpdateTime(now); conversationMapper.insert(conversation); - createMember(conversation.getId(), fromUserId, now); - createMember(conversation.getId(), toUserId, now); + ensureMember(conversation.getId(), fromUserId, now); + if (!fromUserId.equals(toUserId)) { + ensureMember(conversation.getId(), toUserId, now); + } + } else { + //update-begin---author:GHT ---date:20260610 for:【IM审批通用化】已有会话补全缺失成员----------- + ensureMember(conversation.getId(), fromUserId, now); + if (!fromUserId.equals(toUserId)) { + ensureMember(conversation.getId(), toUserId, now); + } + //update-end---author:GHT ---date:20260610 for:【IM审批通用化】已有会话补全缺失成员----------- } // 写入消息 SysImMessage message = new SysImMessage(); @@ -522,6 +548,22 @@ public class SysImChatServiceImpl implements ISysImChatService { pushChatMessage(conversation.getId(), fromUserId, messageVo, conversation.getConvType()); return messageVo; } + + //update-begin---author:GHT ---date:20260610 for:【IM审批通用化】审批待办统一由系统账号发给处理人----------- + @Override + @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) + public SysImMessageVO sendApprovalHandlerMessage(String toUserId, Integer tenantId, String content, String msgType) { + if (oConvertUtils.isEmpty(toUserId) || oConvertUtils.isEmpty(content)) { + return null; + } + String fromUserId = ensureWorkNotifyUserId(); + if (fromUserId.equals(toUserId)) { + log.warn("审批待办IM发送失败:接收人不能是工作通知公众号 toUserId={}", toUserId); + return null; + } + return sendSystemSingleMessage(fromUserId, toUserId, tenantId, content, msgType); + } + //update-end---author:GHT ---date:20260610 for:【IM审批通用化】审批待办统一由系统账号发给处理人----------- //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】系统单聊消息(绕过同部门校验)----- //update-begin---author:cursor ---date:20260528 for:【IM聊天-OA】标记已读----------- @@ -541,13 +583,19 @@ public class SysImChatServiceImpl implements ISysImChatService { //update-begin---author:cursor ---date:20260528 for:【IM聊天-OA】本部门成员列表(含会话摘要)----------- @Override public List listDeptMembers(String userId, Integer tenantId, String orgCode, String keyword) { + List result = new ArrayList<>(); + //update-begin---author:GHT ---date:20260610 for:【IM审批通用化】同事列表置顶工作通知公众号----------- + if (matchWorkNotifyKeyword(keyword)) { + result.add(buildWorkNotifyContact(userId, tenantId)); + } + String workNotifyId = ensureWorkNotifyUserId(); String resolvedOrgCode = resolveOrgCode(userId, tenantId, orgCode); if (oConvertUtils.isEmpty(resolvedOrgCode)) { - throw new JeecgBootException("未获取到当前部门,请切换部门后重试"); + return result; } List users = userDepartMapper.querySameDepartUserList(resolvedOrgCode, userId, tenantId, keyword); if (users == null || users.isEmpty()) { - return Collections.emptyList(); + return result; } Map convMap = new HashMap<>(16); if (tenantId != null && tenantId > 0) { @@ -557,20 +605,26 @@ public class SysImChatServiceImpl implements ISysImChatService { } } } - List result = users.stream().map(user -> { - SysImContactVO vo = toContactVo(user); - SysImConversationVO conv = convMap.get(user.getId()); - if (conv != null) { - vo.setConversationId(conv.getConversationId()); - vo.setLastContent(conv.getLastContent()); - vo.setLastTime(conv.getLastTime()); - vo.setUnreadCount(conv.getUnreadCount()); - } - return vo; - }).collect(Collectors.toList()); - result.sort(Comparator + List colleagues = users.stream() + .filter(user -> !workNotifyId.equals(user.getId()) && !WORK_NOTIFY_USERNAME.equals(user.getUsername())) + .map(user -> { + SysImContactVO vo = toContactVo(user); + vo.setContactType(CONTACT_TYPE_USER); + SysImConversationVO conv = convMap.get(user.getId()); + if (conv != null) { + vo.setConversationId(conv.getConversationId()); + vo.setLastContent(conv.getLastContent()); + vo.setLastTime(conv.getLastTime()); + vo.setUnreadCount(conv.getUnreadCount()); + } + return vo; + }) + .collect(Collectors.toList()); + colleagues.sort(Comparator .comparing(SysImContactVO::getLastTime, Comparator.nullsLast(Comparator.reverseOrder())) .thenComparing(item -> oConvertUtils.getString(item.getRealname(), item.getUsername()))); + result.addAll(colleagues); + //update-end---author:GHT ---date:20260610 for:【IM审批通用化】同事列表置顶工作通知公众号----------- return result; } //update-end---author:cursor ---date:20260528 for:【IM聊天-OA】本部门成员列表(含会话摘要)----------- @@ -583,9 +637,111 @@ public class SysImChatServiceImpl implements ISysImChatService { vo.setAvatar(user.getAvatar()); vo.setOrgCodeTxt(user.getOrgCodeTxt()); vo.setUnreadCount(0); + vo.setContactType(CONTACT_TYPE_USER); return vo; } + private SysImContactVO buildWorkNotifyContact(String userId, Integer tenantId) { + String workNotifyId = ensureWorkNotifyUserId(); + SysImContactVO vo = new SysImContactVO(); + vo.setId(workNotifyId); + vo.setUsername(WORK_NOTIFY_USERNAME); + vo.setRealname(WORK_NOTIFY_REALNAME); + vo.setUnreadCount(0); + vo.setContactType(CONTACT_TYPE_WORK_NOTIFY); + if (tenantId != null && tenantId > 0) { + SysImConversationVO conv = findSingleConversationSummary(userId, tenantId, workNotifyId); + if (conv != null) { + vo.setConversationId(conv.getConversationId()); + vo.setLastContent(conv.getLastContent()); + vo.setLastTime(conv.getLastTime()); + vo.setUnreadCount(conv.getUnreadCount()); + } + } + return vo; + } + + private SysImConversationVO findSingleConversationSummary(String userId, Integer tenantId, String targetUserId) { + String pairKey = buildPairKey(userId, targetUserId); + SysImConversation conversation = conversationMapper.selectOne(new LambdaQueryWrapper() + .eq(SysImConversation::getTenantId, tenantId) + .eq(SysImConversation::getUserPairKey, pairKey)); + if (conversation == null) { + return null; + } + SysImConversationMember member = getMember(userId, conversation.getId()); + SysImConversationVO vo = buildConversationVo(conversation, userId, targetUserId); + vo.setUnreadCount(member == null ? 0 : member.getUnreadCount()); + return vo; + } + + private boolean matchWorkNotifyKeyword(String keyword) { + if (oConvertUtils.isEmpty(keyword)) { + return true; + } + String key = keyword.trim().toLowerCase(); + return WORK_NOTIFY_REALNAME.contains(keyword.trim()) + || WORK_NOTIFY_REALNAME.toLowerCase().contains(key) + || WORK_NOTIFY_USERNAME.contains(key) + || key.contains("工作") + || key.contains("通知"); + } + + private String ensureWorkNotifyUserId() { + if (oConvertUtils.isNotEmpty(workNotifyUserIdCache)) { + return workNotifyUserIdCache; + } + synchronized (this) { + if (oConvertUtils.isNotEmpty(workNotifyUserIdCache)) { + return workNotifyUserIdCache; + } + SysUser user = userMapper.selectOne(new LambdaQueryWrapper() + .eq(SysUser::getUsername, WORK_NOTIFY_USERNAME) + .eq(SysUser::getDelFlag, 0) + .last("LIMIT 1")); + if (user == null) { + user = userMapper.selectById(WORK_NOTIFY_USER_ID); + } + if (user == null) { + user = createWorkNotifyUser(); + } + workNotifyUserIdCache = user.getId(); + return workNotifyUserIdCache; + } + } + + private SysUser createWorkNotifyUser() { + SysUser user = new SysUser(); + user.setId(WORK_NOTIFY_USER_ID); + user.setUsername(WORK_NOTIFY_USERNAME); + user.setRealname(WORK_NOTIFY_REALNAME); + user.setPassword("disabled"); + user.setSalt(""); + user.setStatus(2); + user.setDelFlag(0); + user.setCreateBy("system"); + user.setCreateTime(new Date()); + userMapper.insert(user); + return user; + } + + private boolean isWorkNotifyUser(String userId) { + return oConvertUtils.isNotEmpty(userId) && userId.equals(ensureWorkNotifyUserId()); + } + + private boolean isWorkNotifyConversation(String conversationId) { + if (oConvertUtils.isEmpty(conversationId)) { + return false; + } + SysImConversation conversation = conversationMapper.selectById(conversationId); + if (conversation == null || !CONV_TYPE_SINGLE.equals(conversation.getConvType())) { + return false; + } + String workNotifyId = ensureWorkNotifyUserId(); + String pairKey = conversation.getUserPairKey(); + return oConvertUtils.isNotEmpty(pairKey) && pairKey.contains(workNotifyId); + } + private String resolveOrgCode(String userId, Integer tenantId, String orgCode) { if (oConvertUtils.isNotEmpty(orgCode)) { return orgCode; @@ -610,6 +766,17 @@ public class SysImChatServiceImpl implements ISysImChatService { } private void createMember(String conversationId, String userId, Date now) { + ensureMember(conversationId, userId, now); + } + + /** 幂等创建会话成员,避免 uk_im_member 重复插入 */ + private void ensureMember(String conversationId, String userId, Date now) { + if (oConvertUtils.isEmpty(conversationId) || oConvertUtils.isEmpty(userId)) { + return; + } + if (getMember(userId, conversationId) != null) { + return; + } SysImConversationMember member = new SysImConversationMember(); member.setConversationId(conversationId); member.setUserId(userId); @@ -742,7 +909,11 @@ public class SysImChatServiceImpl implements ISysImChatService { sender = userMapper.selectById(message.getSenderId()); } if (sender != null) { - vo.setSenderName(sender.getRealname()); + if (WORK_NOTIFY_USERNAME.equals(sender.getUsername())) { + vo.setSenderName(WORK_NOTIFY_REALNAME); + } else { + vo.setSenderName(sender.getRealname()); + } vo.setSenderAvatar(sender.getAvatar()); } return vo; diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/vo/SysImContactVO.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/vo/SysImContactVO.java index e002086..4aa39b8 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/vo/SysImContactVO.java +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/vo/SysImContactVO.java @@ -28,4 +28,6 @@ public class SysImContactVO { private java.util.Date lastTime; @Schema(description = "未读数") private Integer unreadCount; + @Schema(description = "联系人类型 user=同事 work_notify=工作通知公众号") + private String contactType; } diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_147__sys_im_work_notify_user.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_147__sys_im_work_notify_user.sql new file mode 100644 index 0000000..fa70cbb --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_147__sys_im_work_notify_user.sql @@ -0,0 +1,17 @@ +-- IM 工作通知公众号系统账号(审批等系统消息统一从此账号推送) +SET NAMES utf8mb4; + +INSERT IGNORE INTO `sys_user` ( + `id`, `username`, `realname`, `password`, `salt`, `status`, `del_flag`, `activiti_sync`, `create_by`, `create_time` +) VALUES ( + '1995000000000000999', + 'im_work_notify', + '工作通知', + 'disabled', + '', + 2, + 0, + 1, + 'system', + NOW() +); diff --git a/jeecgboot-vue3/src/views/approval/flow/approvalHandle.api.ts b/jeecgboot-vue3/src/views/approval/flow/approvalHandle.api.ts index 346414a..b77746a 100644 --- a/jeecgboot-vue3/src/views/approval/flow/approvalHandle.api.ts +++ b/jeecgboot-vue3/src/views/approval/flow/approvalHandle.api.ts @@ -13,6 +13,9 @@ enum Api { // update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销----- cancel = '/xslmes/approvalHandle/cancel', // update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销----- + // update-begin---author:GHT ---date:2026-06-10 for:【IM审批通用化】补发IM审批卡片----- + resendCard = '/xslmes/approvalHandle/resendCard', + // update-end---author:GHT ---date:2026-06-10 for:【IM审批通用化】补发IM审批卡片----- } /** 查看单据全部字段 + 审批进度/历史 */ @@ -31,3 +34,9 @@ export const rejectApproval = (params: { instanceId: string; reason: string }) = /** 撤销(仅发起人本人,审批中可撤回,业务单据恢复到发起时状态) */ export const cancelApproval = (params: { instanceId: string; reason?: string }) => defHttp.post({ url: Api.cancel, data: params }); // update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销----- + +// update-begin---author:GHT ---date:2026-06-10 for:【IM审批通用化】补发IM审批卡片----- +/** 补发当前节点 IM 审批卡片(instanceId 与 bizTable+bizDataId 二选一) */ +export const resendApprovalCard = (params: { instanceId?: string; bizTable?: string; bizDataId?: string }) => + defHttp.post({ url: Api.resendCard, data: params }); +// update-end---author:GHT ---date:2026-06-10 for:【IM审批通用化】补发IM审批卡片----- diff --git a/jeecgboot-vue3/src/views/system/im/ImBizRecordMessageContent.vue b/jeecgboot-vue3/src/views/system/im/ImBizRecordMessageContent.vue index cc97a7f..98df720 100644 --- a/jeecgboot-vue3/src/views/system/im/ImBizRecordMessageContent.vue +++ b/jeecgboot-vue3/src/views/system/im/ImBizRecordMessageContent.vue @@ -5,7 +5,13 @@