MES本地审批共用钉钉审批等配置
This commit is contained in:
@@ -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
|
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
|
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温度条件新增是否附加/重量字段 -----
|
-- 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-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
|
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslMixingSpecTcu.java
|
||||||
|
|||||||
@@ -108,4 +108,20 @@ public class MesXslApprovalHandleController {
|
|||||||
return Result.OK(approvalHandleService.pendingList(user));
|
return Result.OK(approvalHandleService.pendingList(user));
|
||||||
}
|
}
|
||||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】待办列表-----
|
//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<String> resendCard(@RequestBody Map<String, Object> 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审批卡片(历史流转中实例)-----------
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,14 @@ public interface IMesXslApprovalHandleService {
|
|||||||
Result<String> urge(String instanceId, LoginUser user);
|
Result<String> urge(String instanceId, LoginUser user);
|
||||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】催办接口-----
|
//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<String> 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审批流完善】待办列表查询-----
|
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】待办列表查询-----
|
||||||
/**
|
/**
|
||||||
* 查询当前用户的待办审批列表(状态为审批中且当前处理人包含该用户)。
|
* 查询当前用户的待办审批列表(状态为审批中且当前处理人包含该用户)。
|
||||||
|
|||||||
@@ -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.entity.MesXslIntegrationPlan;
|
||||||
import org.jeecg.modules.xslmes.approval.integration.service.IMesXslIntegrationPlanService;
|
import org.jeecg.modules.xslmes.approval.integration.service.IMesXslIntegrationPlanService;
|
||||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalInstanceService;
|
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.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -100,6 +101,11 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
|||||||
@Autowired
|
@Autowired
|
||||||
private IMesXslIntegrationPlanService integrationPlanService;
|
private IMesXslIntegrationPlanService integrationPlanService;
|
||||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】驳回回退改由集成方案 onReject 驱动-----------
|
//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审批流设计】审批与业务单据联动回调-----
|
//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);
|
oConvertUtils.getString(inst.getApplyUserName(), inst.getApplyUser()), inst.getFlowName(), actionLabel);
|
||||||
msgType = "text";
|
msgType = "text";
|
||||||
}
|
}
|
||||||
SysUser applicant = getUserSafely(inst.getApplyUser());
|
//update-begin---author:GHT ---date:20260610 for:【IM审批通用化】待办卡片由系统账号发给处理人,支持发起人=处理人-----------
|
||||||
String fromId = applicant == null ? null : applicant.getId();
|
|
||||||
for (String uname : handlerUsernames) {
|
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) */
|
/** 构建 biz_record 卡片 JSON(含 instanceId / actionLabel / canApprove,与前端 ImBizRecordPayload 对齐 v=2) */
|
||||||
private String buildCardJson(MesXslApprovalInstance inst, String actionLabel, boolean canApprove, String routePath) {
|
private String buildCardJson(MesXslApprovalInstance inst, String actionLabel, boolean canApprove, String routePath) {
|
||||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】区分审批卡片与抄送通知卡片-----
|
//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) {
|
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-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();
|
JSONArray fields = new JSONArray();
|
||||||
addField(fields, "审批流", inst.getFlowName());
|
addField(fields, "审批流", inst.getFlowName());
|
||||||
addField(fields, "单据", safeTitle(inst));
|
addField(fields, "单据", safeTitle(inst));
|
||||||
@@ -1353,10 +1386,26 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
|||||||
if (user != null) {
|
if (user != null) {
|
||||||
ctx.setOperatorUsername(user.getUsername());
|
ctx.setOperatorUsername(user.getUsername());
|
||||||
ctx.setOperatorName(oConvertUtils.getString(user.getRealname(), user.getUsername()));
|
ctx.setOperatorName(oConvertUtils.getString(user.getRealname(), user.getUsername()));
|
||||||
|
ctx.setOperatorTime(new Date());
|
||||||
} else {
|
} else {
|
||||||
ctx.setOperatorUsername("system");
|
ctx.setOperatorUsername("system");
|
||||||
ctx.setOperatorName("系统");
|
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;
|
return ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1455,7 +1504,7 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
|||||||
if (oConvertUtils.isEmpty(uname)) {
|
if (oConvertUtils.isEmpty(uname)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
sendOne(user.getId(), uname, inst.getTenantId(),
|
sendApprovalHandlerNotify(uname, inst.getTenantId(),
|
||||||
"【催办提醒】" + applicantName + " 催促您处理「" + safeTitle(inst) + "」,请尽快审批。", "text");
|
"【催办提醒】" + applicantName + " 催促您处理「" + safeTitle(inst) + "」,请尽快审批。", "text");
|
||||||
}
|
}
|
||||||
urgeTimeMap.put(instanceId, System.currentTimeMillis());
|
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-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】催办:发起人向当前处理人发催办提醒-----
|
||||||
|
|
||||||
|
//update-begin---author:GHT ---date:20260610 for:【IM审批通用化】流转中实例补发IM审批卡片-----------
|
||||||
|
@Override
|
||||||
|
public Result<String> 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<String> 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审批流完善】待办列表:查询当前用户的待处理审批实例-----
|
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】待办列表:查询当前用户的待处理审批实例-----
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package org.jeecg.modules.xslmes.dingtalk.service;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
import org.jeecg.modules.print.vo.PrintBizFieldItemVO;
|
import org.jeecg.modules.print.vo.PrintBizFieldItemVO;
|
||||||
import org.jeecg.modules.system.service.ISysDictService;
|
import org.jeecg.modules.system.service.ISysDictService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -79,7 +80,7 @@ public class DingTplBindFieldValueResolver {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (cur instanceof Map) {
|
if (cur instanceof Map) {
|
||||||
cur = ((Map<String, Object>) cur).get(p);
|
cur = getMapValue((Map<String, Object>) cur, p);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -87,6 +88,32 @@ public class DingTplBindFieldValueResolver {
|
|||||||
return cur;
|
return cur;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** JDBC 行多为下划线列名,绑定配置为驼峰字段名,双向兼容取值 */
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private Object getMapValue(Map<String, Object> 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")
|
@SuppressWarnings("unchecked")
|
||||||
private String getDictTextFromRow(Object rowData, String bizField) {
|
private String getDictTextFromRow(Object rowData, String bizField) {
|
||||||
if (!(rowData instanceof Map) || StringUtils.isBlank(bizField)) {
|
if (!(rowData instanceof Map) || StringUtils.isBlank(bizField)) {
|
||||||
@@ -95,13 +122,13 @@ public class DingTplBindFieldValueResolver {
|
|||||||
Map<String, Object> map = (Map<String, Object>) rowData;
|
Map<String, Object> map = (Map<String, Object>) rowData;
|
||||||
String[] parts = bizField.split("\\.");
|
String[] parts = bizField.split("\\.");
|
||||||
if (parts.length == 1) {
|
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;
|
return v != null ? String.valueOf(v) : null;
|
||||||
}
|
}
|
||||||
String parentPath = String.join(".", java.util.Arrays.copyOf(parts, parts.length - 1));
|
String parentPath = String.join(".", java.util.Arrays.copyOf(parts, parts.length - 1));
|
||||||
Object parent = getNestedValue(rowData, parentPath);
|
Object parent = getNestedValue(rowData, parentPath);
|
||||||
if (parent instanceof Map) {
|
if (parent instanceof Map) {
|
||||||
Object v = ((Map<?, ?>) parent).get(parts[parts.length - 1] + "_dictText");
|
Object v = getMapValue((Map<String, Object>) parent, parts[parts.length - 1] + "_dictText");
|
||||||
return v != null ? String.valueOf(v) : null;
|
return v != null ? String.valueOf(v) : null;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -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<String> 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<String, Object> rowData = loadBizRow(inst.getBizTable(), inst.getBizDataId());
|
||||||
|
if (rowData.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, PrintBizFieldItemVO> 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<String, Object> rowData,
|
||||||
|
Map<String, PrintBizFieldItemVO> 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<String, Object> loadBizRow(String table, String bizDataId) {
|
||||||
|
if (oConvertUtils.isEmpty(table) || oConvertUtils.isEmpty(bizDataId)
|
||||||
|
|| !IDENTIFIER.matcher(table).matches()) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
List<Map<String, Object>> 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<String, Object> normalizeRowKeys(Map<String, Object> raw) {
|
||||||
|
Map<String, Object> normalized = new LinkedHashMap<>();
|
||||||
|
if (raw == null) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
for (Map.Entry<String, Object> 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<String, PrintBizFieldItemVO> buildFieldMetaMap(String bizCode) {
|
||||||
|
if (oConvertUtils.isEmpty(bizCode)) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
Map<String, PrintBizFieldItemVO> 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<PrintBizFieldItemVO> listMainFieldsEnriched(String bizCode) {
|
||||||
|
Class<?> cls = resolveEntityClass(bizCode);
|
||||||
|
List<PrintBizFieldItemVO> 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<String, PrintBizFieldItemVO> map, String bizCode, String prop, String kind) {
|
||||||
|
List<PrintBizFieldItemVO> 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 "审批中";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,4 +13,7 @@ public interface IMesXslDingTplBindService extends IService<MesXslDingTplBind> {
|
|||||||
|
|
||||||
/** 按业务编码查询绑定记录(未删除的第一条) */
|
/** 按业务编码查询绑定记录(未删除的第一条) */
|
||||||
MesXslDingTplBind getByBizCode(String bizCode);
|
MesXslDingTplBind getByBizCode(String bizCode);
|
||||||
|
|
||||||
|
/** 按前端路由解析启用的钉钉模板绑定(模板停用则返回 null) */
|
||||||
|
MesXslDingTplBind resolveActiveByRoutePath(String routePath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,18 @@ package org.jeecg.modules.xslmes.dingtalk.service.impl;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
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.entity.MesXslDingTplBind;
|
||||||
import org.jeecg.modules.xslmes.dingtalk.mapper.MesXslDingTplBindMapper;
|
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.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 org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 钉钉审批模板绑定 ServiceImpl
|
* 钉钉审批模板绑定 ServiceImpl
|
||||||
*
|
*
|
||||||
@@ -17,8 +24,43 @@ import org.springframework.stereotype.Service;
|
|||||||
public class MesXslDingTplBindServiceImpl extends ServiceImpl<MesXslDingTplBindMapper, MesXslDingTplBind>
|
public class MesXslDingTplBindServiceImpl extends ServiceImpl<MesXslDingTplBindMapper, MesXslDingTplBind>
|
||||||
implements IMesXslDingTplBindService {
|
implements IMesXslDingTplBindService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IMesXslDingProcessTplService tplService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MesXslDingTplBind getByBizCode(String bizCode) {
|
public MesXslDingTplBind getByBizCode(String bizCode) {
|
||||||
return getOne(new QueryWrapper<MesXslDingTplBind>().eq("biz_code", bizCode).last("LIMIT 1"));
|
return getOne(new QueryWrapper<MesXslDingTplBind>().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<String> 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审批通用化】按路由解析启用的钉钉模板绑定-----------
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ public interface ISysImChatService {
|
|||||||
* @return 消息VO,发送失败返回 null
|
* @return 消息VO,发送失败返回 null
|
||||||
*/
|
*/
|
||||||
SysImMessageVO sendSystemSingleMessage(String fromUserId, String toUserId, Integer tenantId, String content, String msgType);
|
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审批流设计】系统单聊消息(绕过同部门校验,供审批等系统通知场景)-----
|
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】系统单聊消息(绕过同部门校验,供审批等系统通知场景)-----
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import org.jeecg.modules.system.mapper.SysUserTenantMapper;
|
|||||||
import org.jeecg.modules.system.service.ISysPermissionService;
|
import org.jeecg.modules.system.service.ISysPermissionService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Propagation;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
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_IMAGE_PREVIEW = "[图片]";
|
||||||
private static final String MSG_BIZ_RECORD_PREVIEW = "[业务数据]";
|
private static final String MSG_BIZ_RECORD_PREVIEW = "[业务数据]";
|
||||||
private static final String IM_RECORD_QUERY_KEY = "imRecordId";
|
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
|
@Autowired
|
||||||
private ISysPermissionService sysPermissionService;
|
private ISysPermissionService sysPermissionService;
|
||||||
@@ -107,7 +115,11 @@ public class SysImChatServiceImpl implements ISysImChatService {
|
|||||||
if (conversation != null) {
|
if (conversation != null) {
|
||||||
return buildConversationVo(conversation, userId, targetUserId);
|
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】已有会话快速打开-----------
|
//update-end---author:cursor ---date:20260528 for:【IM聊天-OA】已有会话快速打开-----------
|
||||||
conversation = new SysImConversation();
|
conversation = new SysImConversation();
|
||||||
conversation.setConvType(CONV_TYPE_SINGLE);
|
conversation.setConvType(CONV_TYPE_SINGLE);
|
||||||
@@ -439,6 +451,11 @@ public class SysImChatServiceImpl implements ISysImChatService {
|
|||||||
throw new JeecgBootException("消息内容不能为空");
|
throw new JeecgBootException("消息内容不能为空");
|
||||||
}
|
}
|
||||||
assertMember(userId, dto.getConversationId());
|
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();
|
Date now = new Date();
|
||||||
SysImMessage message = new SysImMessage();
|
SysImMessage message = new SysImMessage();
|
||||||
message.setConversationId(dto.getConversationId());
|
message.setConversationId(dto.getConversationId());
|
||||||
@@ -480,8 +497,8 @@ public class SysImChatServiceImpl implements ISysImChatService {
|
|||||||
if (oConvertUtils.isEmpty(fromUserId) || oConvertUtils.isEmpty(toUserId) || oConvertUtils.isEmpty(content)) {
|
if (oConvertUtils.isEmpty(fromUserId) || oConvertUtils.isEmpty(toUserId) || oConvertUtils.isEmpty(content)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// 不给自己发送
|
|
||||||
if (fromUserId.equals(toUserId)) {
|
if (fromUserId.equals(toUserId)) {
|
||||||
|
log.warn("IM系统消息跳过:收发为同一人 toUserId={}", toUserId);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
Integer tenant = tenantId == null ? 0 : tenantId;
|
Integer tenant = tenantId == null ? 0 : tenantId;
|
||||||
@@ -500,8 +517,17 @@ public class SysImChatServiceImpl implements ISysImChatService {
|
|||||||
conversation.setCreateTime(now);
|
conversation.setCreateTime(now);
|
||||||
conversation.setUpdateTime(now);
|
conversation.setUpdateTime(now);
|
||||||
conversationMapper.insert(conversation);
|
conversationMapper.insert(conversation);
|
||||||
createMember(conversation.getId(), fromUserId, now);
|
ensureMember(conversation.getId(), fromUserId, now);
|
||||||
createMember(conversation.getId(), toUserId, 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();
|
SysImMessage message = new SysImMessage();
|
||||||
@@ -522,6 +548,22 @@ public class SysImChatServiceImpl implements ISysImChatService {
|
|||||||
pushChatMessage(conversation.getId(), fromUserId, messageVo, conversation.getConvType());
|
pushChatMessage(conversation.getId(), fromUserId, messageVo, conversation.getConvType());
|
||||||
return messageVo;
|
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-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】系统单聊消息(绕过同部门校验)-----
|
||||||
|
|
||||||
//update-begin---author:cursor ---date:20260528 for:【IM聊天-OA】标记已读-----------
|
//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】本部门成员列表(含会话摘要)-----------
|
//update-begin---author:cursor ---date:20260528 for:【IM聊天-OA】本部门成员列表(含会话摘要)-----------
|
||||||
@Override
|
@Override
|
||||||
public List<SysImContactVO> listDeptMembers(String userId, Integer tenantId, String orgCode, String keyword) {
|
public List<SysImContactVO> listDeptMembers(String userId, Integer tenantId, String orgCode, String keyword) {
|
||||||
|
List<SysImContactVO> 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);
|
String resolvedOrgCode = resolveOrgCode(userId, tenantId, orgCode);
|
||||||
if (oConvertUtils.isEmpty(resolvedOrgCode)) {
|
if (oConvertUtils.isEmpty(resolvedOrgCode)) {
|
||||||
throw new JeecgBootException("未获取到当前部门,请切换部门后重试");
|
return result;
|
||||||
}
|
}
|
||||||
List<SysUser> users = userDepartMapper.querySameDepartUserList(resolvedOrgCode, userId, tenantId, keyword);
|
List<SysUser> users = userDepartMapper.querySameDepartUserList(resolvedOrgCode, userId, tenantId, keyword);
|
||||||
if (users == null || users.isEmpty()) {
|
if (users == null || users.isEmpty()) {
|
||||||
return Collections.emptyList();
|
return result;
|
||||||
}
|
}
|
||||||
Map<String, SysImConversationVO> convMap = new HashMap<>(16);
|
Map<String, SysImConversationVO> convMap = new HashMap<>(16);
|
||||||
if (tenantId != null && tenantId > 0) {
|
if (tenantId != null && tenantId > 0) {
|
||||||
@@ -557,20 +605,26 @@ public class SysImChatServiceImpl implements ISysImChatService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
List<SysImContactVO> result = users.stream().map(user -> {
|
List<SysImContactVO> colleagues = users.stream()
|
||||||
SysImContactVO vo = toContactVo(user);
|
.filter(user -> !workNotifyId.equals(user.getId()) && !WORK_NOTIFY_USERNAME.equals(user.getUsername()))
|
||||||
SysImConversationVO conv = convMap.get(user.getId());
|
.map(user -> {
|
||||||
if (conv != null) {
|
SysImContactVO vo = toContactVo(user);
|
||||||
vo.setConversationId(conv.getConversationId());
|
vo.setContactType(CONTACT_TYPE_USER);
|
||||||
vo.setLastContent(conv.getLastContent());
|
SysImConversationVO conv = convMap.get(user.getId());
|
||||||
vo.setLastTime(conv.getLastTime());
|
if (conv != null) {
|
||||||
vo.setUnreadCount(conv.getUnreadCount());
|
vo.setConversationId(conv.getConversationId());
|
||||||
}
|
vo.setLastContent(conv.getLastContent());
|
||||||
return vo;
|
vo.setLastTime(conv.getLastTime());
|
||||||
}).collect(Collectors.toList());
|
vo.setUnreadCount(conv.getUnreadCount());
|
||||||
result.sort(Comparator
|
}
|
||||||
|
return vo;
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
colleagues.sort(Comparator
|
||||||
.comparing(SysImContactVO::getLastTime, Comparator.nullsLast(Comparator.reverseOrder()))
|
.comparing(SysImContactVO::getLastTime, Comparator.nullsLast(Comparator.reverseOrder()))
|
||||||
.thenComparing(item -> oConvertUtils.getString(item.getRealname(), item.getUsername())));
|
.thenComparing(item -> oConvertUtils.getString(item.getRealname(), item.getUsername())));
|
||||||
|
result.addAll(colleagues);
|
||||||
|
//update-end---author:GHT ---date:20260610 for:【IM审批通用化】同事列表置顶工作通知公众号-----------
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
//update-end---author:cursor ---date:20260528 for:【IM聊天-OA】本部门成员列表(含会话摘要)-----------
|
//update-end---author:cursor ---date:20260528 for:【IM聊天-OA】本部门成员列表(含会话摘要)-----------
|
||||||
@@ -583,9 +637,111 @@ public class SysImChatServiceImpl implements ISysImChatService {
|
|||||||
vo.setAvatar(user.getAvatar());
|
vo.setAvatar(user.getAvatar());
|
||||||
vo.setOrgCodeTxt(user.getOrgCodeTxt());
|
vo.setOrgCodeTxt(user.getOrgCodeTxt());
|
||||||
vo.setUnreadCount(0);
|
vo.setUnreadCount(0);
|
||||||
|
vo.setContactType(CONTACT_TYPE_USER);
|
||||||
return vo;
|
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<SysImConversation>()
|
||||||
|
.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<SysUser>()
|
||||||
|
.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) {
|
private String resolveOrgCode(String userId, Integer tenantId, String orgCode) {
|
||||||
if (oConvertUtils.isNotEmpty(orgCode)) {
|
if (oConvertUtils.isNotEmpty(orgCode)) {
|
||||||
return orgCode;
|
return orgCode;
|
||||||
@@ -610,6 +766,17 @@ public class SysImChatServiceImpl implements ISysImChatService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void createMember(String conversationId, String userId, Date now) {
|
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();
|
SysImConversationMember member = new SysImConversationMember();
|
||||||
member.setConversationId(conversationId);
|
member.setConversationId(conversationId);
|
||||||
member.setUserId(userId);
|
member.setUserId(userId);
|
||||||
@@ -742,7 +909,11 @@ public class SysImChatServiceImpl implements ISysImChatService {
|
|||||||
sender = userMapper.selectById(message.getSenderId());
|
sender = userMapper.selectById(message.getSenderId());
|
||||||
}
|
}
|
||||||
if (sender != null) {
|
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());
|
vo.setSenderAvatar(sender.getAvatar());
|
||||||
}
|
}
|
||||||
return vo;
|
return vo;
|
||||||
|
|||||||
@@ -28,4 +28,6 @@ public class SysImContactVO {
|
|||||||
private java.util.Date lastTime;
|
private java.util.Date lastTime;
|
||||||
@Schema(description = "未读数")
|
@Schema(description = "未读数")
|
||||||
private Integer unreadCount;
|
private Integer unreadCount;
|
||||||
|
@Schema(description = "联系人类型 user=同事 work_notify=工作通知公众号")
|
||||||
|
private String contactType;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
);
|
||||||
@@ -13,6 +13,9 @@ enum Api {
|
|||||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||||
cancel = '/xslmes/approvalHandle/cancel',
|
cancel = '/xslmes/approvalHandle/cancel',
|
||||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
// 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 });
|
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-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审批卡片-----
|
||||||
|
|||||||
@@ -5,7 +5,13 @@
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<!-- 单条:详情表 -->
|
<!-- 单条:详情表 -->
|
||||||
<template v-if="isSingleItem">
|
<template v-if="isSingleItem">
|
||||||
<div class="im-biz-record-item">
|
<div class="im-biz-record-item" :class="{ 'im-biz-record-item--ding': isDingStyleCard }">
|
||||||
|
<!-- update-begin---author:GHT ---date:2026-06-10 for:【IM审批通用化】钉钉模板样式卡片头----------- -->
|
||||||
|
<div v-if="isDingStyleCard" class="im-biz-record-ding-header">
|
||||||
|
<span class="im-biz-record-ding-badge">审批</span>
|
||||||
|
<span class="im-biz-record-ding-title">{{ dingCardTitle }}</span>
|
||||||
|
</div>
|
||||||
|
<!-- update-end---author:GHT ---date:2026-06-10 for:【IM审批通用化】钉钉模板样式卡片头----------- -->
|
||||||
<div class="im-biz-record-table-wrap">
|
<div class="im-biz-record-table-wrap">
|
||||||
<table class="im-biz-record-table im-biz-record-table--detail">
|
<table class="im-biz-record-table im-biz-record-table--detail">
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -167,6 +173,15 @@
|
|||||||
const singleItem = computed(() => props.payload.items[0]);
|
const singleItem = computed(() => props.payload.items[0]);
|
||||||
const listColumnLabels = computed(() => resolveImBizRecordListColumnLabels(props.payload.items));
|
const listColumnLabels = computed(() => resolveImBizRecordListColumnLabels(props.payload.items));
|
||||||
|
|
||||||
|
// update-begin---author:GHT ---date:2026-06-10 for:【IM审批通用化】钉钉模板样式卡片-----------
|
||||||
|
const isDingStyleCard = computed(
|
||||||
|
() => props.payload.cardStyle === 'ding' || !!props.payload.templateName,
|
||||||
|
);
|
||||||
|
const dingCardTitle = computed(
|
||||||
|
() => props.payload.templateName || props.payload.pageTitle || '审批单',
|
||||||
|
);
|
||||||
|
// update-end---author:GHT ---date:2026-06-10 for:【IM审批通用化】钉钉模板样式卡片-----------
|
||||||
|
|
||||||
// 审批卡片:单条且带审批实例ID
|
// 审批卡片:单条且带审批实例ID
|
||||||
const isApprovalCard = computed(() => isSingleItem.value && !!singleItem.value?.instanceId);
|
const isApprovalCard = computed(() => isSingleItem.value && !!singleItem.value?.instanceId);
|
||||||
|
|
||||||
@@ -425,6 +440,50 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
|
&--ding {
|
||||||
|
.im-biz-record-table-wrap {
|
||||||
|
border-color: #ffe7ba;
|
||||||
|
}
|
||||||
|
|
||||||
|
.im-biz-record-table--detail th {
|
||||||
|
background: #fff7e6;
|
||||||
|
color: #873800;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.im-biz-record-ding-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 6px 6px 0 0;
|
||||||
|
background: linear-gradient(90deg, #fff7e6 0%, #fff 100%);
|
||||||
|
border: 1px solid #ffe7ba;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.im-biz-record-ding-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 36px;
|
||||||
|
padding: 0 6px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #ff6900;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.im-biz-record-ding-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #262626;
|
||||||
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 审批办理按钮栏 */
|
/* 审批办理按钮栏 */
|
||||||
|
|||||||
@@ -59,21 +59,32 @@
|
|||||||
placement="right"
|
placement="right"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
:class="['conv-item', activeTargetUserId === item.id ? 'active' : '']"
|
:class="[
|
||||||
|
'conv-item',
|
||||||
|
{ active: activeTargetUserId === item.id, 'conv-item--work-notify': isWorkNotifyContact(item) },
|
||||||
|
]"
|
||||||
@click="selectMember(item)"
|
@click="selectMember(item)"
|
||||||
>
|
>
|
||||||
<a-badge :count="shouldShowUnread(item) ? item.unreadCount : 0" :offset="[-2, 2]">
|
<a-badge :count="shouldShowUnread(item) ? item.unreadCount : 0" :offset="[-2, 2]">
|
||||||
<a-avatar :size="leftCollapsed ? 36 : 40" :src="getAvatarUrl(item.avatar)">
|
<a-avatar
|
||||||
|
v-if="isWorkNotifyContact(item)"
|
||||||
|
:size="leftCollapsed ? 36 : 40"
|
||||||
|
:style="{ backgroundColor: '#fa8c16' }"
|
||||||
|
>
|
||||||
|
<Icon icon="ant-design:notification-outlined" />
|
||||||
|
</a-avatar>
|
||||||
|
<a-avatar v-else :size="leftCollapsed ? 36 : 40" :src="getAvatarUrl(item.avatar)">
|
||||||
{{ (item.realname || item.username || '?').slice(0, 1) }}
|
{{ (item.realname || item.username || '?').slice(0, 1) }}
|
||||||
</a-avatar>
|
</a-avatar>
|
||||||
</a-badge>
|
</a-badge>
|
||||||
<div v-show="!leftCollapsed" class="conv-meta">
|
<div v-show="!leftCollapsed" class="conv-meta">
|
||||||
<div class="conv-top">
|
<div class="conv-top">
|
||||||
<span class="conv-name">{{ item.realname || item.username }}</span>
|
<span class="conv-name">{{ item.realname || item.username }}</span>
|
||||||
|
<span v-if="isWorkNotifyContact(item)" class="conv-tag">公众号</span>
|
||||||
<span class="conv-time">{{ formatTime(item.lastTime) }}</span>
|
<span class="conv-time">{{ formatTime(item.lastTime) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="conv-bottom">
|
<div class="conv-bottom">
|
||||||
<span class="conv-preview">{{ formatConvPreview(item.lastContent) }}</span>
|
<span class="conv-preview">{{ formatConvPreview(item.lastContent, isWorkNotifyContact(item)) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -203,7 +214,9 @@
|
|||||||
<Icon v-if="embeddedPageContextClickable" icon="ant-design:select-outlined" class="im-page-context-action" />
|
<Icon v-if="embeddedPageContextClickable" icon="ant-design:select-outlined" class="im-page-context-action" />
|
||||||
</div>
|
</div>
|
||||||
<!--update-end---author:xsl ---date:20260528 for:【IM聊天-OA】弹窗输入框上方展示背后功能页名称------------->
|
<!--update-end---author:xsl ---date:20260528 for:【IM聊天-OA】弹窗输入框上方展示背后功能页名称------------->
|
||||||
|
<div v-if="isWorkNotifyChat" class="im-work-notify-readonly-tip">工作通知为系统消息通道,仅接收审批等工作消息</div>
|
||||||
<ImChatInput
|
<ImChatInput
|
||||||
|
v-else
|
||||||
v-model="draft"
|
v-model="draft"
|
||||||
:disabled="!activeConversationId"
|
:disabled="!activeConversationId"
|
||||||
:sending="sending"
|
:sending="sending"
|
||||||
@@ -261,6 +274,7 @@
|
|||||||
import { syncImUnreadFromMembers } from './useImUnread';
|
import { syncImUnreadFromMembers } from './useImUnread';
|
||||||
import {
|
import {
|
||||||
type ImMemberItem,
|
type ImMemberItem,
|
||||||
|
IM_CONTACT_TYPE_WORK_NOTIFY,
|
||||||
type ImMessageItem,
|
type ImMessageItem,
|
||||||
getCachedMembers,
|
getCachedMembers,
|
||||||
isMembersCacheStale,
|
isMembersCacheStale,
|
||||||
@@ -539,9 +553,15 @@
|
|||||||
return parseImBizRecordPayload(content);
|
return parseImBizRecordPayload(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatConvPreview(content?: string) {
|
function isWorkNotifyContact(item?: DeptMemberItem | null) {
|
||||||
|
return item?.contactType === IM_CONTACT_TYPE_WORK_NOTIFY || item?.username === 'im_work_notify';
|
||||||
|
}
|
||||||
|
|
||||||
|
const isWorkNotifyChat = computed(() => isWorkNotifyContact(activeMember.value));
|
||||||
|
|
||||||
|
function formatConvPreview(content?: string, isWorkNotify = false) {
|
||||||
if (!content) {
|
if (!content) {
|
||||||
return '点击开始聊天';
|
return isWorkNotify ? '暂无系统通知' : '点击开始聊天';
|
||||||
}
|
}
|
||||||
return formatImMessagePreview(content);
|
return formatImMessagePreview(content);
|
||||||
}
|
}
|
||||||
@@ -948,7 +968,9 @@
|
|||||||
function handleOpenCreateGroupModal() {
|
function handleOpenCreateGroupModal() {
|
||||||
// useModalInner 必须传入 data 才会触发打开回调,否则不会加载成员列表
|
// useModalInner 必须传入 data 才会触发打开回调,否则不会加载成员列表
|
||||||
openCreateGroupModal(true, {
|
openCreateGroupModal(true, {
|
||||||
members: deptMembers.value.filter((item) => item.id !== currentUserId.value),
|
members: deptMembers.value.filter(
|
||||||
|
(item) => item.id !== currentUserId.value && !isWorkNotifyContact(item),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
//update-end---author:xsl ---date:20260528 for:【IM聊天-OA】发起群聊弹窗需传 data 触发成员加载-----------
|
//update-end---author:xsl ---date:20260528 for:【IM聊天-OA】发起群聊弹窗需传 data 触发成员加载-----------
|
||||||
@@ -1917,6 +1939,36 @@
|
|||||||
&.active {
|
&.active {
|
||||||
background: #eef4ff;
|
background: #eef4ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.conv-item--work-notify {
|
||||||
|
border-bottom: 1px solid #f5f5f5;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.active {
|
||||||
|
background: #fff7e6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.conv-tag {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 4px;
|
||||||
|
padding: 0 6px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff7e6;
|
||||||
|
color: #fa8c16;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.im-work-notify-readonly-tip {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
background: #fafafa;
|
||||||
|
color: #8c8c8c;
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.conv-meta {
|
.conv-meta {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
|
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
|
||||||
import { useUserStore } from '/@/store/modules/user';
|
import { useUserStore } from '/@/store/modules/user';
|
||||||
import { createGroupConversation, fetchDeptMembers } from './im.api';
|
import { createGroupConversation, fetchDeptMembers } from './im.api';
|
||||||
import { getCachedMembers } from './imCache';
|
import { getCachedMembers, IM_CONTACT_TYPE_WORK_NOTIFY } from './imCache';
|
||||||
|
|
||||||
defineOptions({ name: 'ImCreateGroupModal' });
|
defineOptions({ name: 'ImCreateGroupModal' });
|
||||||
|
|
||||||
@@ -73,7 +73,13 @@
|
|||||||
function resolveSelectableMembers(source?: Recordable[]) {
|
function resolveSelectableMembers(source?: Recordable[]) {
|
||||||
const currentUserId = userStore.getUserInfo?.id || '';
|
const currentUserId = userStore.getUserInfo?.id || '';
|
||||||
const list = source?.length ? source : getCachedMembers() || [];
|
const list = source?.length ? source : getCachedMembers() || [];
|
||||||
return list.filter((item) => item.id && item.id !== currentUserId);
|
return list.filter(
|
||||||
|
(item) =>
|
||||||
|
item.id &&
|
||||||
|
item.id !== currentUserId &&
|
||||||
|
item.contactType !== IM_CONTACT_TYPE_WORK_NOTIFY &&
|
||||||
|
item.username !== 'im_work_notify',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data?: { members?: Recordable[] }) => {
|
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data?: { members?: Recordable[] }) => {
|
||||||
|
|||||||
@@ -34,6 +34,12 @@ export interface ImBizRecordPayload {
|
|||||||
pagePath: string;
|
pagePath: string;
|
||||||
rowKey: string;
|
rowKey: string;
|
||||||
items: ImBizRecordItem[];
|
items: ImBizRecordItem[];
|
||||||
|
// update-begin---author:GHT ---date:2026-06-10 for:【IM审批通用化】钉钉模板样式卡片扩展-----------
|
||||||
|
/** ding=与钉钉审批模板字段对齐的卡片样式 */
|
||||||
|
cardStyle?: 'ding' | string;
|
||||||
|
templateId?: string;
|
||||||
|
templateName?: string;
|
||||||
|
// update-end---author:GHT ---date:2026-06-10 for:【IM审批通用化】钉钉模板样式卡片扩展-----------
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 构建带跳转链接的业务明细消息体 */
|
/** 构建带跳转链接的业务明细消息体 */
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { formatImMessagePreview } from './imMessageUtil';
|
|||||||
import { syncImUnreadFromMembers } from './useImUnread';
|
import { syncImUnreadFromMembers } from './useImUnread';
|
||||||
import { isImChatPageActive } from './imSession';
|
import { isImChatPageActive } from './imSession';
|
||||||
|
|
||||||
|
export const IM_CONTACT_TYPE_WORK_NOTIFY = 'work_notify';
|
||||||
|
|
||||||
export interface ImMemberItem {
|
export interface ImMemberItem {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
@@ -18,6 +20,8 @@ export interface ImMemberItem {
|
|||||||
lastContent?: string;
|
lastContent?: string;
|
||||||
lastTime?: string;
|
lastTime?: string;
|
||||||
unreadCount?: number;
|
unreadCount?: number;
|
||||||
|
/** user=同事 work_notify=工作通知公众号 */
|
||||||
|
contactType?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ImMessageItem {
|
export interface ImMessageItem {
|
||||||
|
|||||||
@@ -19,6 +19,16 @@
|
|||||||
onClick: handleDetail.bind(null, record),
|
onClick: handleDetail.bind(null, record),
|
||||||
auth: 'xslmes:mes_xsl_approval_record:list',
|
auth: 'xslmes:mes_xsl_approval_record:list',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '补发IM卡片',
|
||||||
|
color: 'warning',
|
||||||
|
ifShow: record.status === '0' && record.channel === 'MES' && !!record.externalInstanceId,
|
||||||
|
popConfirm: {
|
||||||
|
title: '向当前处理人重新发送 IM 审批卡片?',
|
||||||
|
confirm: handleResendCard.bind(null, record),
|
||||||
|
},
|
||||||
|
auth: 'xslmes:mes_xsl_approval_record:list',
|
||||||
|
},
|
||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -34,7 +44,10 @@
|
|||||||
import MesXslApprovalRecordDetailModal from './components/MesXslApprovalRecordDetailModal.vue';
|
import MesXslApprovalRecordDetailModal from './components/MesXslApprovalRecordDetailModal.vue';
|
||||||
import { columns, searchFormSchema } from './MesXslApprovalRecord.data';
|
import { columns, searchFormSchema } from './MesXslApprovalRecord.data';
|
||||||
import { list, getExportUrl } from './MesXslApprovalRecord.api';
|
import { list, getExportUrl } from './MesXslApprovalRecord.api';
|
||||||
|
import { resendApprovalCard } from '/@/views/approval/flow/approvalHandle.api';
|
||||||
|
import { useMessage } from '/@/hooks/web/useMessage';
|
||||||
|
|
||||||
|
const { createMessage } = useMessage();
|
||||||
const [registerModal, { openModal }] = useModal();
|
const [registerModal, { openModal }] = useModal();
|
||||||
|
|
||||||
const { tableContext, onExportXls } = useListPage({
|
const { tableContext, onExportXls } = useListPage({
|
||||||
@@ -50,7 +63,7 @@
|
|||||||
showAdvancedButton: true,
|
showAdvancedButton: true,
|
||||||
},
|
},
|
||||||
actionColumn: {
|
actionColumn: {
|
||||||
width: 100,
|
width: 180,
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -65,4 +78,13 @@
|
|||||||
function handleDetail(record: Recordable) {
|
function handleDetail(record: Recordable) {
|
||||||
openModal(true, { record });
|
openModal(true, { record });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleResendCard(record: Recordable) {
|
||||||
|
try {
|
||||||
|
const msg = await resendApprovalCard({ instanceId: record.externalInstanceId });
|
||||||
|
createMessage.success(msg || '补发成功');
|
||||||
|
} catch (e: any) {
|
||||||
|
createMessage.error(e?.message || '补发失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user