Merge branch 'feature/dingdingLogin' into release-0211

This commit is contained in:
wangli 2026-02-11 10:47:04 +08:00
commit a778c5fa7b
31 changed files with 2424 additions and 761 deletions

1690
init.sql Normal file

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@ import cn.axzo.workflow.common.annotation.InvokeMode;
import cn.axzo.workflow.common.annotation.Manageable;
import cn.axzo.workflow.common.enums.AdminDataSource;
import cn.axzo.workflow.common.enums.BpmnProcessInstanceResultEnum;
import cn.axzo.workflow.common.model.dto.CommonDingTalkDTO;
import cn.axzo.workflow.common.model.request.feature.DingTalkStarterAlterDTO;
import cn.azxo.framework.common.model.CommonResponse;
import io.swagger.v3.oas.annotations.Operation;
@ -47,4 +48,9 @@ public interface FunctionApi {
@PostMapping("/api/function/dingtalk/alter")
@InvokeMode(SYNC)
CommonResponse<Boolean> sendDingtalk(@Validated @RequestBody DingTalkStarterAlterDTO dto);
@Operation(summary = "发送通用钉钉消息")
@PostMapping("/api/function/common/dingtalk/send")
@InvokeMode(SYNC)
CommonResponse<Boolean> sendCommonDingtalk(@Validated @RequestBody CommonDingTalkDTO dto);
}

View File

@ -24,6 +24,7 @@ public enum OtherRespCode implements IModuleRespCode {
MESSAGE_IM_EVENT_BUILD_ERROR("009", "不能使用 createEvent 函数创建`IM 消息`的事件, 请调用 createIMEvent 函数"),
ASSIGNEE_NODE_ID_NOT_EXISTS("010", "【{}】 nodeId 不存在, 请检查参数是否正确"),
CANT_GENERATE_PROCESS_LOG_PDF("011", "流程未处于终态不能用默认参数创建,请自行添加 BizCode和 BizKey"),
DANGER_OPERATION_NOT_SIGN_IN("012", "流程实例后端操作必须登陆授权后才能使用"),
;
private final String code;

View File

@ -1,23 +0,0 @@
package cn.axzo.workflow.common.model;
import lombok.Data;
/**
* 节点检测告警对象
*
* @author wangli
* @since 2024-09-13 11:37
*/
@Data
public class NextNodePreCheckAlterDTO {
private String processDefinitionKey;
private String processDefinitionName;
private String processInstanceId;
private String activityId;
private String errorMsg;
}

View File

@ -0,0 +1,45 @@
package cn.axzo.workflow.common.model.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
/**
* 通用的 Starter 发送钉钉消息传输对象
*
* @author wangli
* @since 2026-02-02 16:22
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class CommonDingTalkDTO {
/**
* 钉钉消息 markdown 的标题
*/
private String title;
/**
* 钉钉消息 markdown 的内容
*/
private String context;
/**
* 是否 @ 接收人
*/
@Builder.Default
private Boolean at = false;
/**
* 钉钉消息 mardown 的接收人手机号
*/
@Builder.Default
private List<String> mobiles = new ArrayList<>();
@Builder.Default
private Boolean targetIsMaster = false;
}

View File

@ -114,5 +114,9 @@
<groupId>com.aliyun</groupId>
<artifactId>alibaba-dingtalk-service-sdk</artifactId>
</dependency>
<dependency>
<groupId>cn.axzo</groupId>
<artifactId>riven-api</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -122,7 +122,7 @@ public class FlowableConfiguration {
configuration.addCustomJobHandler(new AsyncApproveTaskWithFormJobHandler());
configuration.addCustomJobHandler(new AsyncRemindTaskJobHandler(refreshProperties));
configuration.addCustomJobHandler(new AsyncResetApproversUserTaskJobHandler(extAxHiTaskInstService));
configuration.addCustomJobHandler(new NextActivityConfigCheckJobHandler());
configuration.addCustomJobHandler(new NextActivityConfigCheckJobHandler(refreshProperties));
configurers.forEach(i -> configuration.addCustomJobHandler(i.getJobHandler()));
// 异步任务异常重试时间间隔
configuration.setDefaultFailedJobWaitTime(30);

View File

@ -19,6 +19,8 @@ import java.util.concurrent.TimeUnit;
@Data
@RefreshScope
public class SupportRefreshProperties {
@Value("${spring.profiles.active:local}")
private String profile;
@Value("${workflow.apiLog.enable: false}")
private Boolean apiLogEnable;
@ -84,9 +86,6 @@ public class SupportRefreshProperties {
@Value(value = "${workflow.alter.repeat:false}")
private Boolean repeatAlter;
@Value(value = "${workflow.alter.sendDingTalk:true}")
private Boolean alterSendDingTalk;
/**
* 用于控制转交管理员的 API
*/

View File

@ -1,173 +0,0 @@
package cn.axzo.workflow.core.engine.job;
import cn.axzo.basics.common.util.NumberUtil;
import cn.axzo.workflow.common.model.dto.AlterDTO;
import cn.axzo.workflow.common.model.dto.TermNodePausingDTO;
import cn.axzo.workflow.common.model.response.category.CategoryItemVO;
import cn.axzo.workflow.core.common.utils.SpringContextUtils;
import cn.axzo.workflow.core.conf.SupportRefreshProperties;
import cn.axzo.workflow.core.listener.Alter;
import cn.axzo.workflow.core.service.CategoryService;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.flowable.common.engine.impl.interceptor.CommandContext;
import org.flowable.engine.ManagementService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.TaskService;
import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl;
import org.flowable.engine.impl.util.CommandContextUtil;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.job.service.JobHandler;
import org.flowable.job.service.TimerJobService;
import org.flowable.job.service.impl.persistence.entity.JobEntity;
import org.flowable.job.service.impl.persistence.entity.TimerJobEntity;
import org.flowable.task.api.Task;
import org.flowable.variable.api.delegate.VariableScope;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import static cn.axzo.workflow.common.constant.BpmnConstants.BIZ_NODE_ALTER;
/**
* 检查指定节点是否长时间卡住如果卡住则进行钉钉告警
*
* @author wangli
* @since 2024-09-11 13:50
*/
@Slf4j
public class AsyncTermNodeAlterJobHandler extends AbstractJobHandler implements JobHandler {
public static final String TYPE = "term-node-alter-cycle";
private final SupportRefreshProperties refreshProperties;
public AsyncTermNodeAlterJobHandler(SupportRefreshProperties refreshProperties) {
this.refreshProperties = refreshProperties;
}
@Override
public String getType() {
return TYPE;
}
@Override
public void execute(JobEntity job, String configuration, VariableScope variableScope, CommandContext commandContext) {
log.warn("AsyncTermNodeAlterJobHandler exec start...");
ProcessEngineConfigurationImpl processEngineConfiguration = CommandContextUtil.getProcessEngineConfiguration(commandContext);
// JSONObject jsonObject = JSON.parseObject(job.getJobHandlerConfiguration());
// if (!jsonObject.containsKey("activityId")) {
// return;
// }
String activityId = job.getJobHandlerConfiguration();
RuntimeService runtimeService = processEngineConfiguration.getRuntimeService();
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(job.getProcessInstanceId()).singleResult();
if(Objects.isNull(processInstance)) {
return;
}
TermNodePausingDTO dto = runtimeService.getVariable(job.getProcessInstanceId(), BIZ_NODE_ALTER + activityId, TermNodePausingDTO.class);
TaskService taskService = processEngineConfiguration.getTaskService();
List<Task> tasks = taskService.createTaskQuery()
.processInstanceId(dto.getProcessInstanceId())
.taskDefinitionKey(dto.getActivityId())
.active()
.list();
StringBuilder sb = new StringBuilder();
tasks.forEach(e -> {
sb.append("id").append(e.getId()).append(", assignee: ").append(e.getAssignee());
});
log.info("tasks size:{}, info: {}", tasks.size(), JSON.toJSONString(sb));
if (CollectionUtils.isEmpty(tasks) || tasks.size() > 1 || hasAssignee(tasks.get(0).getAssignee())) {
return;
}
if (DateUtil.compare(DateUtil.date(), getDateTime(tasks.get(0).getCreateTime())) <= 0) {
ManagementService managementService = processEngineConfiguration.getManagementService();
managementService.executeCommand(context -> {
TimerJobService timerJobService = CommandContextUtil.getTimerJobService();
TimerJobEntity timerJobEntity = timerJobService.createTimerJob();
timerJobEntity.setJobType("timer");
timerJobEntity.setJobHandlerType(AsyncTermNodeAlterJobHandler.TYPE); // 这里填写你自定义的 JobHandler 类型
timerJobEntity.setProcessInstanceId(dto.getProcessInstanceId());
timerJobEntity.setExecutionId(null);
timerJobEntity.setDuedate(getDateTime(new Date())); // 立即执行
timerJobEntity.setRepeat(null); // 不重复
timerJobEntity.setRetries(1);
timerJobEntity.setJobHandlerConfiguration(dto.getActivityId()); // 可选传递参数
timerJobService.scheduleTimerJob(timerJobEntity);
return null;
});
return;
}
// 不允许重复告警
if (!refreshProperties.getRepeatAlter() && dto.getRetries() > 0) {
return;
}
// 超过告警次数
if (refreshProperties.getAlterRetries() != 0 && dto.getRetries() >= refreshProperties.getAlterRetries()) {
return;
}
CategoryService bean = SpringContextUtils.getBean(CategoryService.class);
Optional<CategoryItemVO> bpmModelCategory = bean.get("bpm_model_category", processInstance.getProcessDefinitionKey());
// 发送告警对象
Alter alter = SpringContextUtils.getBean(Alter.class);
AlterDTO alterDTO = new AlterDTO();
alterDTO.setProcessDefinitionKey(processInstance.getProcessDefinitionKey());
alterDTO.setProcessDefinitionName(bpmModelCategory.orElse(new CategoryItemVO()).getLabel());
alterDTO.setProcessInstanceId(dto.getProcessInstanceId());
alterDTO.setActivityId(dto.getActivityId());
alterDTO.setTaskId(tasks.get(0).getId());
alterDTO.setStartTime(tasks.get(0).getCreateTime());
alterDTO.setPrettyStartTime(DateUtil.formatDateTime(tasks.get(0).getCreateTime()));
if (Boolean.TRUE.equals(refreshProperties.getAlterSendDingTalk())) {
alter.invoke(alterDTO);
// 记录告警次数
incRetries(job, dto, runtimeService, activityId);
}
}
private DateTime getDateTime(Date date) {
DateTime dateTime;
switch (refreshProperties.getAlterIntervalUnit()) {
case MINUTES:
dateTime = DateUtil.offsetMinute(date, refreshProperties.getPauseDelay());
break;
case HOURS:
dateTime = DateUtil.offsetHour(date, refreshProperties.getPauseDelay());
break;
default:
dateTime = DateUtil.offsetSecond(date, refreshProperties.getPauseDelay());
break;
}
return dateTime;
}
private void incRetries(JobEntity job, TermNodePausingDTO dto, RuntimeService runtimeService, String activityId) {
dto.setRetries(dto.getRetries() + 1);
runtimeService.setVariable(job.getProcessInstanceId(), BIZ_NODE_ALTER + activityId, dto);
}
// private void deleteTimerJob(TermNodePausingDTO dto) {
// Context.getTransactionContext().addTransactionListener(TransactionState.COMMITTED,
// new DeleteTimerJobTransactionListener(dto));
// }
private Boolean hasAssignee(String assignee) {
if (!StringUtils.hasText(assignee)) {
return false;
}
String[] split = assignee.split("\\|");
return split.length == 2 && NumberUtil.isPositiveNumber(Long.valueOf(split[1]));
}
}

View File

@ -1,14 +1,14 @@
package cn.axzo.workflow.core.engine.job;
import cn.axzo.workflow.common.enums.ApproverEmptyHandleTypeEnum;
import cn.axzo.workflow.common.model.NextNodePreCheckAlterDTO;
import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskDelegateAssigner;
import cn.axzo.workflow.common.model.response.category.CategoryItemVO;
import cn.axzo.workflow.core.common.utils.SpringContextUtils;
import cn.axzo.workflow.core.conf.SupportRefreshProperties;
import cn.axzo.workflow.core.deletage.BpmnTaskAssigneeSelector;
import cn.axzo.workflow.core.listener.Alter;
import cn.axzo.workflow.core.service.CategoryService;
import cn.axzo.workflow.core.service.support.FlowNodeForecastService;
import cn.axzo.workflow.core.util.RivenDingTalkHelper;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.ListUtils;
@ -49,6 +49,12 @@ import static cn.axzo.workflow.core.common.utils.BpmnMetaParserHelper.getApprove
public class NextActivityConfigCheckJobHandler extends AbstractJobHandler implements JobHandler {
public static final String TYPE = "next-activity-config-check";
private final SupportRefreshProperties refreshProperties;
public NextActivityConfigCheckJobHandler(SupportRefreshProperties refreshProperties) {
this.refreshProperties = refreshProperties;
}
@Override
public String getType() {
return TYPE;
@ -77,15 +83,17 @@ public class NextActivityConfigCheckJobHandler extends AbstractJobHandler implem
log.warn("NextActivityConfigCheckJobHandler msg: {}", e.getMessage(), e);
CategoryService bean = SpringContextUtils.getBean(CategoryService.class);
Optional<CategoryItemVO> bpmModelCategory = bean.get("bpm_model_category", processInstance.getProcessDefinitionKey());
Alter alter = SpringContextUtils.getBean(Alter.class);
RivenDingTalkHelper rivenDingTalkHelper = SpringContextUtils.getBean(RivenDingTalkHelper.class);
FlowElement flowElement = ListUtils.emptyIfNull(flowElements).stream().filter(i -> i instanceof UserTask || i instanceof ReceiveTask || i instanceof ServiceTask).findFirst().orElse(null);
NextNodePreCheckAlterDTO alterDTO = new NextNodePreCheckAlterDTO();
alterDTO.setProcessDefinitionKey(processInstance.getProcessDefinitionKey());
alterDTO.setProcessDefinitionName(bpmModelCategory.orElse(new CategoryItemVO()).getLabel());
alterDTO.setProcessInstanceId(job.getProcessInstanceId());
alterDTO.setActivityId(Objects.nonNull(flowElement) ? flowElement.getId() : null);
alterDTO.setErrorMsg(e.getMessage());
alter.invoke(alterDTO);
String title = "Notice 审批模板节点预检查告警, Env:" + refreshProperties.getProfile();
String content = "#### [" + refreshProperties.getProfile() + "]审批模板节点预检查告警\n" +
"> 审批业务: " + bpmModelCategory.orElse(new CategoryItemVO()).getLabel() + "(" + processInstance.getProcessDefinitionKey() + ")" + "\n\n" +
"> 实例 ID" + job.getProcessInstanceId() + "\n\n" +
"> 检测节点 ID" + (Objects.nonNull(flowElement) ? flowElement.getId() : null) + "\n\n" +
"> ##### 错误信息:" + e.getMessage() + "\n\n";
// 生产环境需要
rivenDingTalkHelper.sendMarkdownMessage(title, content, true, false);
}
}

View File

@ -14,7 +14,7 @@ import cn.axzo.workflow.core.deletage.BpmnTaskDelegate;
import cn.axzo.workflow.core.deletage.MockTaskAssigneeSelector;
import cn.axzo.workflow.core.engine.cmd.CustomAbortProcessInstanceAsyncCmd;
import cn.axzo.workflow.core.engine.job.NextActivityConfigCheckJobHandler;
import cn.axzo.workflow.core.util.DingTalkUtils;
import cn.axzo.workflow.core.util.RivenDingTalkHelper;
import cn.hutool.json.JSONUtil;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
@ -90,6 +90,8 @@ public class EngineExecutionStartListener implements ExecutionListener {
private List<BpmnTaskAssigneeSelector> selectors;
@Resource
private SupportRefreshProperties refreshProperties;
@Resource
private RivenDingTalkHelper rivenDingTalkHelper;
@Override
public void notify(DelegateExecution execution) {
@ -252,7 +254,15 @@ public class EngineExecutionStartListener implements ExecutionListener {
//发送钉钉消息
if (Boolean.TRUE.equals(refreshProperties.getSendDingTalk())) {
CooperationOrgDTO orgScopes = execution.getVariable(BIZ_ORG_RELATION, CooperationOrgDTO.class);
DingTalkUtils.sendDingTalkForTransferToAdminError(profile, execution.getProcessInstanceId(), userTask.getId(), orgScopes, targetUrl);
String title = "Notice 转交管理员后的审批人为空, Env: " + profile;
String content = "#### [" + profile + "]转交管理员后的审批人为空\n" +
"> 流程实例 ID: " + execution.getProcessInstanceId() + "\n\n" +
"> 节点参数(节点 ID): " + userTask.getId() + "\n\n" +
"> OrgScopes 参数信息: " + JSONUtil.toJsonStr(orgScopes) + "\n\n" +
"> 目标接口: " + targetUrl + "\n\n" +
"> ##### 提示:仅作为提示信息,不代表是异常";
rivenDingTalkHelper.sendMarkdownMessage(title, content, false, false);
}
BpmnProcessInstanceAbortDTO abortDTO = new BpmnProcessInstanceAbortDTO();
abortDTO.setProcessInstanceId(execution.getProcessInstanceId());

View File

@ -1,7 +1,5 @@
package cn.axzo.workflow.core.listener;
import cn.axzo.workflow.common.model.dto.AlterDTO;
/**
* Core 往外转发的钩子
*
@ -10,5 +8,5 @@ import cn.axzo.workflow.common.model.dto.AlterDTO;
*/
public interface Alter {
void invoke(Object obj);
void _invoke(Object obj);
}

View File

@ -1,224 +0,0 @@
package cn.axzo.workflow.core.util;
import cn.axzo.workflow.common.model.NextNodePreCheckAlterDTO;
import cn.axzo.workflow.common.model.dto.AlterDTO;
import cn.axzo.workflow.common.model.request.feature.DingTalkStarterAlterDTO;
import cn.hutool.core.date.DateUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.dingtalk.api.DefaultDingTalkClient;
import com.dingtalk.api.DingTalkClient;
import com.dingtalk.api.request.OapiRobotSendRequest;
import com.dingtalk.api.response.OapiRobotSendResponse;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static cn.azxo.framework.common.constatns.Constants.CTX_LOG_ID_MDC;
/**
* 钉钉告警处理
*
* @author wangli
* @since 2024/1/12 14:34
*/
@Slf4j
public class DingTalkUtils {
private static final String dingtalk_robot_webhook = "https://oapi.dingtalk" +
".com/robot/send?access_token=341ee2907f3ebc15dc495fb7771a646230058710999fec7838066c109849878e";
private static final Map<String, String> ENV_URL_MAPPING = new HashMap<>();
private static final Map<String, String> WEB_URL_MAPPING = new HashMap<>();
static {
ENV_URL_MAPPING.put("local", "https://dev-app.axzo.cn");
ENV_URL_MAPPING.put("dev", "https://dev-app.axzo.cn");
ENV_URL_MAPPING.put("test", "https://test-api.axzo.cn");
ENV_URL_MAPPING.put("pre", "https://pre-api.axzo.cn");
ENV_URL_MAPPING.put("live", "https://live-api.axzo.cn");
ENV_URL_MAPPING.put("default", "https://api.axzo.cn");
WEB_URL_MAPPING.put("local", "https://dev-new-workflow-web.axzo.cn");
WEB_URL_MAPPING.put("dev", "https://dev-new-workflow-web.axzo.cn");
WEB_URL_MAPPING.put("test", "https://test-new-workflow-web.axzo.cn");
WEB_URL_MAPPING.put("pre", "https://pre-new-workflow-web.axzo.cn");
WEB_URL_MAPPING.put("live", "https://live-new-workflow-web.axzo.cn");
WEB_URL_MAPPING.put("default", "https://new-workflow-web.axzo.cn");
}
public static String getEnvUrl(String profile) {
String urlPrefix = ENV_URL_MAPPING.get(profile);
if (!StringUtils.hasText(urlPrefix)) {
urlPrefix = ENV_URL_MAPPING.get("default");
}
return urlPrefix;
}
public static String getWebUrl(String profile) {
String urlPrefix = WEB_URL_MAPPING.get(profile);
if (!StringUtils.hasText(urlPrefix)) {
urlPrefix = WEB_URL_MAPPING.get("default");
}
return urlPrefix;
}
@SneakyThrows
public static void sendDingTalk(String profile, String title, Object req, String invokeServerName, Throwable throwable) {
OapiRobotSendRequest request = new OapiRobotSendRequest();
request.setMsgtype("markdown");
OapiRobotSendRequest.Markdown markdown = new OapiRobotSendRequest.Markdown();
markdown.setTitle("Notice " + title + ", Env: " + profile);
String text = "#### [" + profile + "]" + title + "\n" +
"> 时间: " + DateUtil.now() + "\n" +
"> 入参: " + JSONUtil.toJsonStr(req) + "\n\n" +
"> ###### 异常信息: " + JSONUtil.toJsonStr(Objects.isNull(throwable.getCause()) ? throwable.getMessage() :
throwable.getCause().getMessage()) + " \n\n" +
"> ##### traceId: " + MDC.get(CTX_LOG_ID_MDC) + " \n" +
"> ##### 调用方服务名称:" + invokeServerName + " \n";
String deadLetterStacktrace = getDeadLetterStacktrace(profile, req);
text = text + (StringUtils.hasText(deadLetterStacktrace) ? "> ##### [点击查看异常明细](" + deadLetterStacktrace + ") \n" : "");
markdown.setText(text);
request.setMarkdown(markdown);
sendDingTalk(request);
}
private static String getDeadLetterStacktrace(String profile, Object req) {
try {
JSONObject entries = JSONUtil.parseObj(req);
String jobId = entries.getStr("id");
if (!StringUtils.hasText(jobId)) {
return "";
}
return getEnvUrl(profile) + "/workflow-engine/web/v1/api/process/job/dead-letter/exception/stacktrace/byId?jobId=" + jobId;
} catch (Exception e) {
log.warn("构造查询错误堆栈地址异常", e);
return "";
}
}
@SneakyThrows
public static void sendDingTalkForSlowUrl(String profile, Double time, String apiUrl, Object requestParam, Object responseBody) {
OapiRobotSendRequest request = new OapiRobotSendRequest();
request.setMsgtype("markdown");
OapiRobotSendRequest.Markdown markdown = new OapiRobotSendRequest.Markdown();
markdown.setTitle("Notice 请求二方接口慢 URL, Env: " + profile);
markdown.setText("#### [" + profile + "]请求二方接口过慢\n" +
"> 接口地址: " + apiUrl + ",经过了" + time + "\n\n" +
"> 请求参数: " + JSONUtil.toJsonStr(requestParam) + "\n\n" +
"> ###### 结果: " + JSONUtil.toJsonStr(responseBody) + " \n");
request.setMarkdown(markdown);
sendDingTalk(request);
}
@SneakyThrows
public static void sendDingTalkForSlowApi(String profile, Double time, String apiUrl, String uniqueId) {
OapiRobotSendRequest request = new OapiRobotSendRequest();
request.setMsgtype("markdown");
OapiRobotSendRequest.Markdown markdown = new OapiRobotSendRequest.Markdown();
markdown.setTitle("Notice API 处理耗时报告, Env: " + profile);
markdown.setText("#### [" + profile + "]API 处理耗时报告\n" +
"> 接口地址: " + apiUrl + ",经过了" + time + "\n\n" +
"> ###### 标识: " + uniqueId + " \n");
request.setMarkdown(markdown);
sendDingTalk(request);
}
@SneakyThrows
private static void sendDingTalk(OapiRobotSendRequest request) {
DingTalkClient client = new DefaultDingTalkClient(dingtalk_robot_webhook);
OapiRobotSendResponse response = client.execute(request);
}
public static void sendDingTalkForBizNodeAlter(String profile, AlterDTO alterDTO, List<String> atMobiles) {
// if (CollectionUtils.isEmpty(atMobiles)) {
// return;
// }
String processInstanceId = alterDTO.getProcessInstanceId();
OapiRobotSendRequest request = new OapiRobotSendRequest();
request.setMsgtype("markdown");
OapiRobotSendRequest.Markdown markdown = new OapiRobotSendRequest.Markdown();
markdown.setTitle("Notice 业务节点长时间停止告警, Env: " + profile);
markdown.setText("#### [" + profile + "]业务节点长时间停止\n" +
"> 审批业务: " + alterDTO.getProcessDefinitionName() + "(" + alterDTO.getProcessDefinitionKey() + ")" + "\n\n" +
"> 节点相关信息: " + JSONUtil.toJsonStr(alterDTO) + "\n\n" +
"> ##### [点击查看审批日志](" + getWebUrl(profile) + "/#/workflow/examples?processInstanceId=" + processInstanceId + ") \n\n" +
"> ##### 提示:如果以上地址提示未登录,请在对应环境 OMS 系统登录后并点击审批管理的功能后,再使用。或者复制实例 ID 到 OMS 中手动查询 \n\n" +
mobiles(atMobiles));
request.setMarkdown(markdown);
OapiRobotSendRequest.At at = new OapiRobotSendRequest.At();
at.setAtMobiles(atMobiles);
at.setIsAtAll(false);
request.setAt(at);
sendDingTalk(request);
}
public static String mobiles(List<String> atMobiles) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < atMobiles.size(); i++) {
if (i != 0) {
sb.append(",");
}
sb.append("@").append(atMobiles.get(i));
}
return sb.toString();
}
public static void sendDingTalkForNodePreCheck(String profile, NextNodePreCheckAlterDTO alterDTO, List<String> alterMobiles) {
OapiRobotSendRequest request = new OapiRobotSendRequest();
request.setMsgtype("markdown");
OapiRobotSendRequest.Markdown markdown = new OapiRobotSendRequest.Markdown();
markdown.setTitle("Notice 审批模板节点预检查告警, Env: " + profile);
markdown.setText("#### [" + profile + "]审批模板节点预检查告警\n" +
"> 审批业务: " + alterDTO.getProcessDefinitionName() + "(" + alterDTO.getProcessDefinitionKey() + ")" + "\n\n" +
"> 实例 ID" + alterDTO.getProcessInstanceId() + "\n\n" +
"> 检测节点 ID" + alterDTO.getActivityId() + "\n\n" +
"> ##### 错误信息:" + alterDTO.getErrorMsg() + "\n\n" +
mobiles(alterMobiles));
request.setMarkdown(markdown);
OapiRobotSendRequest.At at = new OapiRobotSendRequest.At();
at.setAtMobiles(alterMobiles);
at.setIsAtAll(false);
request.setAt(at);
sendDingTalk(request);
}
public static void sendDingTalkForTransferToAdminError(String profile, String processInstanceId, String taskDefinitionKey, Object orgScopes, String targetUrl) {
OapiRobotSendRequest request = new OapiRobotSendRequest();
request.setMsgtype("markdown");
OapiRobotSendRequest.Markdown markdown = new OapiRobotSendRequest.Markdown();
markdown.setTitle("Notice 转交管理员后的审批人为空, Env: " + profile);
markdown.setText("#### [" + profile + "]转交管理员后的审批人为空\n" +
"> 流程实例 ID: " + processInstanceId + "\n\n" +
"> 节点参数(节点 ID): " + taskDefinitionKey + "\n\n" +
"> OrgScopes 参数信息: " + JSONUtil.toJsonStr(orgScopes) + "\n\n" +
"> 目标接口: " + targetUrl + "\n\n" +
"> ##### 提示:仅作为提示信息,不代表是异常");
request.setMarkdown(markdown);
sendDingTalk(request);
}
public static void sendDingTalkForStarter(DingTalkStarterAlterDTO dto, List<String> alterMobiles) {
OapiRobotSendRequest request = new OapiRobotSendRequest();
request.setMsgtype("markdown");
OapiRobotSendRequest.Markdown markdown = new OapiRobotSendRequest.Markdown();
markdown.setTitle("Notice 应用必接广播 MQ 事件告警, Env: " + dto.getProfile());
markdown.setText("#### [" + dto.getProfile() + "]应用必接广播 MQ 事件告警\n" +
"> 应用名称:" + dto.getApplicationName() + "\n\n" +
"> 检测信息:" + dto.getAlterContent() + "\n\n" +
mobiles(alterMobiles));
request.setMarkdown(markdown);
OapiRobotSendRequest.At at = new OapiRobotSendRequest.At();
at.setAtMobiles(alterMobiles);
at.setIsAtAll(false);
request.setAt(at);
sendDingTalk(request);
}
}

View File

@ -0,0 +1,177 @@
package cn.axzo.workflow.core.util;
import cn.axzo.riven.client.feign.DingDingMsgApi;
import cn.axzo.riven.client.model.SampleMarkdown;
import cn.axzo.riven.client.req.DingDingSendRebootGroupMsgReq;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Riven 钉钉消息发送统一工具类
* <p>
* 用于替代原有的 webhook 直调方式统一通过 Riven 服务发送钉钉消息
* 支持所有业务场景包括
* - 业务节点告警
* - 节点预检查告警
* - 应用必接事件告警
* - DLQ 监控告警
* - 慢接口告警
* - 异常告警
*
* @author wangli
* @since 2026-01-21
*/
@Slf4j
@Component
public class RivenDingTalkHelper {
@Value("${spring.profiles.active}")
private String profile;
@Value(value = "${workflow.alter.mobiles:}")
private List<String> alterMobiles;
/**
* Riven 钉钉消息 Feign 客户端
* 注意需要在项目中引入 riven-api 依赖
*/
@Resource
private DingDingMsgApi dingDingMsgApi;
/**
* 钉钉消息场景标识(工作流小分队使用)
*/
private static final String DINGTALK_NOT_MASTER_SCENE = "WORKFLOW_ENGINE_BIZNODE_ALTER";
/**
* 钉钉消息场景标识(工作流生产环境使用)
*/
private static final String DINGTALK_MASTER_SCENE = "WORKFLOW_ENGINE_MASTER_ALTER";
/**
* 环境 URL 映射API
*/
private static final Map<String, String> ENV_URL_MAPPING = new HashMap<>();
/**
* 环境 URL 映射Web
*/
private static final Map<String, String> WEB_URL_MAPPING = new HashMap<>();
static {
// API 地址映射
ENV_URL_MAPPING.put("local", "https://dev-app.axzo.cn");
ENV_URL_MAPPING.put("dev", "https://dev-app.axzo.cn");
ENV_URL_MAPPING.put("test", "https://test-api.axzo.cn");
ENV_URL_MAPPING.put("pre", "https://pre-api.axzo.cn");
ENV_URL_MAPPING.put("live", "https://live-api.axzo.cn");
ENV_URL_MAPPING.put("default", "https://api.axzo.cn");
// Web 地址映射
WEB_URL_MAPPING.put("local", "https://dev-new-workflow-web.axzo.cn");
WEB_URL_MAPPING.put("dev", "https://dev-new-workflow-web.axzo.cn");
WEB_URL_MAPPING.put("test", "https://test-new-workflow-web.axzo.cn");
WEB_URL_MAPPING.put("pre", "https://pre-new-workflow-web.axzo.cn");
WEB_URL_MAPPING.put("live", "https://live-new-workflow-web.axzo.cn");
WEB_URL_MAPPING.put("default", "https://new-workflow-web.axzo.cn");
}
/**
* 获取环境对应的 API 地址前缀
*
* @param profile 环境标识dev/test/pre/live/master
* @return API 地址前缀
*/
public static String getEnvUrl(String profile) {
String urlPrefix = ENV_URL_MAPPING.get(profile);
if (!StringUtils.hasText(urlPrefix)) {
urlPrefix = ENV_URL_MAPPING.get("default");
}
return urlPrefix;
}
/**
* 获取环境对应的 Web 地址前缀
*
* @param profile 环境标识
* @return Web 地址前缀
*/
public static String getWebUrl(String profile) {
String urlPrefix = WEB_URL_MAPPING.get(profile);
if (!StringUtils.hasText(urlPrefix)) {
urlPrefix = WEB_URL_MAPPING.get("default");
}
return urlPrefix;
}
/**
* 格式化 @人员列表为字符串
*
* @param atMobiles 手机号列表
* @return @人员字符串格式@手机号1,@手机号2
*/
public static String formatMobiles(List<String> atMobiles) {
if (atMobiles == null || atMobiles.isEmpty()) {
return "";
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < atMobiles.size(); i++) {
if (i != 0) {
sb.append(",");
}
sb.append("@").append(atMobiles.get(i));
}
return sb.toString();
}
/**
* 发送 Markdown 格式的钉钉消息核心方法
*
* @param title 消息标题
* @param text 消息内容Markdown 格式
*/
public void sendMarkdownMessage(String title, String text, Boolean targetIsMaster, Boolean at) {
sendMarkdownMessage(title, text, targetIsMaster, at, alterMobiles);
}
public void sendMarkdownMessage(String title, String text, Boolean targetIsMaster, Boolean at, List<String> mobiles) {
DingDingSendRebootGroupMsgReq req = new DingDingSendRebootGroupMsgReq();
if (Objects.equals(targetIsMaster, Boolean.TRUE) && Objects.equals(profile, "master")) {
req.setDingDingScene(DINGTALK_MASTER_SCENE);
} else {
req.setDingDingScene(DINGTALK_NOT_MASTER_SCENE);
}
// 构建 Markdown 消息
SampleMarkdown markdown = new SampleMarkdown(title, text);
JSONObject markdownJson = JSONObject.parseObject(markdown.toJson());
if (Objects.equals(at, Boolean.TRUE)) {
if (CollectionUtils.isEmpty(mobiles)) {
mobiles = alterMobiles;
}
// 添加 @人员信息
if (mobiles != null && !mobiles.isEmpty()) {
JSONObject atMobilesJson = new JSONObject();
atMobilesJson.put("atMobiles", alterMobiles);
markdownJson.put("at", atMobilesJson);
markdownJson.put("isAtAll", false);
markdown.setText(markdown.getText() + formatMobiles(alterMobiles));
}
}
req.setDingDingJson(markdownJson.toJSONString());
req.setMsgType(cn.axzo.riven.client.common.enums.DingTalkMsgTypeEnum.sampleMarkdown);
// 调用 Riven API
dingDingMsgApi.sendRebootGroupMsg(req);
}
}

View File

@ -24,6 +24,6 @@ public class FlowableOptimisticLockingExceptionHandlerAdvice extends AbstractExc
@Override
protected IRespCode decode(FlowableOptimisticLockingException ex, IRespCode fallbackCode) {
return new RespCode(BaseCode.SUCCESS.getCode(), ex.getMessage());
return new RespCode(BaseCode.UNAVAILABLE_FOR_LEGAL_REASONS.getCode(), ex.getMessage());
}
}

View File

@ -1,141 +0,0 @@
package cn.axzo.workflow.server.alter;
import cn.axzo.framework.jackson.utility.JSON;
import cn.axzo.riven.client.common.enums.DingTalkMsgTypeEnum;
import cn.axzo.riven.client.feign.DingDingMsgApi;
import cn.axzo.riven.client.model.SampleMarkdown;
import cn.axzo.riven.client.req.DingDingSendRebootGroupMsgReq;
import cn.axzo.workflow.common.model.NextNodePreCheckAlterDTO;
import cn.axzo.workflow.common.model.dto.AlterDTO;
import cn.axzo.workflow.common.model.request.feature.DingTalkStarterAlterDTO;
import cn.axzo.workflow.core.conf.SupportRefreshProperties;
import cn.axzo.workflow.core.listener.Alter;
import cn.axzo.workflow.core.util.DingTalkUtils;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Objects;
import static cn.axzo.workflow.core.util.DingTalkUtils.getWebUrl;
import static cn.axzo.workflow.core.util.DingTalkUtils.mobiles;
/**
* 钉钉告警实现
*
* @author wangli
* @since 2024-09-13 11:40
*/
@Slf4j
@Component
public class DingTalkAlter implements Alter {
@Value("${spring.profiles.active}")
private String profile;
@Resource
private SupportRefreshProperties refreshProperties;
@Resource
private DingDingMsgApi dingDingMsgApi;
@Override
public void invoke(Object obj) {
if (!Boolean.TRUE.equals(refreshProperties.getAlterSendDingTalk())) {
log.info("ignore send alter");
return;
}
log.info("send biz node alter : {}", JSON.toJSONString(obj));
if (obj instanceof AlterDTO) {
AlterDTO alterDTO = (AlterDTO) obj;
if (Objects.equals(profile, "master")) {
DingTalkUtils.sendDingTalkForBizNodeAlter(profile, alterDTO, refreshProperties.getAlterMobiles());
} else {
rivenDingtalk(alterDTO);
}
}
if (obj instanceof NextNodePreCheckAlterDTO) {
NextNodePreCheckAlterDTO alterDTO = (NextNodePreCheckAlterDTO) obj;
if (Objects.equals(profile, "master")) {
DingTalkUtils.sendDingTalkForNodePreCheck(profile, alterDTO, refreshProperties.getAlterMobiles());
} else {
rivenDingtalkForNodePreCheck(alterDTO);
}
}
if (obj instanceof DingTalkStarterAlterDTO) {
DingTalkStarterAlterDTO starterAlterDTO = (DingTalkStarterAlterDTO) obj;
if (refreshProperties.getIgnoreMqAlterApplicationNames().contains(starterAlterDTO.getApplicationName())) {
// 忽略必接事件的应用
return;
}
DingTalkUtils.sendDingTalkForStarter(starterAlterDTO, refreshProperties.getAlterMobiles());
}
}
private void rivenDingtalkForStarter(DingTalkStarterAlterDTO alterDTO) {
DingDingSendRebootGroupMsgReq req = new DingDingSendRebootGroupMsgReq();
req.setDingDingScene("WORKFLOW_ENGINE_BIZNODE_ALTER");
String title = "Notice 应用必接事件告警, Env: " + profile;
String text = "#### [" + profile + "]应用必接事件告警\n" +
"> 应用名称:" + alterDTO.getApplicationName() + "\n\n" +
"> 检测信息:" + alterDTO.getAlterContent() + "\n\n" +
mobiles(refreshProperties.getAlterMobiles());
SampleMarkdown markdown = new SampleMarkdown(title, text);
JSONObject markdownJson = JSONObject.parseObject(markdown.toJson());
JSONObject atMobiles = new JSONObject();
atMobiles.put("atMobiles", refreshProperties.getAlterMobiles());
markdownJson.put("at", atMobiles);
markdownJson.put("isAtAll", false);
req.setDingDingJson(markdownJson.toJSONString());
req.setMsgType(DingTalkMsgTypeEnum.sampleMarkdown);
dingDingMsgApi.sendRebootGroupMsg(req);
}
private void rivenDingtalkForNodePreCheck(NextNodePreCheckAlterDTO alterDTO) {
DingDingSendRebootGroupMsgReq req = new DingDingSendRebootGroupMsgReq();
req.setDingDingScene("WORKFLOW_ENGINE_BIZNODE_ALTER");
String processInstanceId = alterDTO.getProcessInstanceId();
String title = "Notice 审批模板节点预检查告警, Env: " + profile;
String text = "#### [" + profile + "]审批模板节点预检查告警\n" +
// "> 相关信息: " + JSONUtil.toJsonStr(alterDTO) + "\n\n" +
"> 实例 ID" + alterDTO.getProcessInstanceId() + "\n\n" +
"> 检测节点 ID" + alterDTO.getActivityId() + "\n\n" +
"> ##### 错误信息:" + alterDTO.getErrorMsg() + "\n\n" +
mobiles(refreshProperties.getAlterMobiles());
SampleMarkdown markdown = new SampleMarkdown(title, text);
JSONObject markdownJson = JSONObject.parseObject(markdown.toJson());
JSONObject atMobiles = new JSONObject();
atMobiles.put("atMobiles", refreshProperties.getAlterMobiles());
markdownJson.put("at", atMobiles);
markdownJson.put("isAtAll", false);
req.setDingDingJson(markdownJson.toJSONString());
req.setMsgType(DingTalkMsgTypeEnum.sampleMarkdown);
dingDingMsgApi.sendRebootGroupMsg(req);
}
private void rivenDingtalk(AlterDTO alterDTO) {
DingDingSendRebootGroupMsgReq req = new DingDingSendRebootGroupMsgReq();
req.setDingDingScene("WORKFLOW_ENGINE_BIZNODE_ALTER");
String processInstanceId = alterDTO.getProcessInstanceId();
String title = "Notice 业务节点长时间停止告警, Env: " + profile;
String text = "#### [" + profile + "]业务节点长时间停止\n" +
"> 节点相关信息: " + JSONUtil.toJsonStr(alterDTO) + "\n\n" +
"> ##### [点击查看审批日志](" + getWebUrl(profile) + "/#/workflow/examples?processInstanceId=" + processInstanceId + ") \n\n" +
"> ##### 提示:如果以上地址提示未登录,请在对应环境 OMS 系统登录后并点击审批管理的功能后,再使用。或者复制实例 ID 到 OMS 中手动查询 \n\n " +
mobiles(refreshProperties.getAlterMobiles());
SampleMarkdown markdown = new SampleMarkdown(title, text);
JSONObject markdownJson = JSONObject.parseObject(markdown.toJson());
JSONObject atMobiles = new JSONObject();
atMobiles.put("atMobiles", refreshProperties.getAlterMobiles());
markdownJson.put("at", atMobiles);
markdownJson.put("isAtAll", false);
req.setDingDingJson(markdownJson.toJSONString());
req.setMsgType(DingTalkMsgTypeEnum.sampleMarkdown);
dingDingMsgApi.sendRebootGroupMsg(req);
}
}

View File

@ -1,9 +1,19 @@
package cn.axzo.workflow.server.common.annotation;
import cn.axzo.workflow.core.util.DingTalkUtils;
import cn.axzo.workflow.core.util.RivenDingTalkHelper;
import cn.axzo.workflow.server.common.util.SpringUtils;
import cn.hutool.core.date.DateUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.util.StringUtils;
import java.util.Objects;
import static cn.axzo.workflow.core.util.RivenDingTalkHelper.getEnvUrl;
import static cn.axzo.workflow.server.common.aspectj.RepeatSubmitAspect.argsArrayToString;
import static cn.azxo.framework.common.constatns.Constants.CTX_LOG_ID_MDC;
/**
@ -22,10 +32,9 @@ public enum ReporterType {
@Override
public void executeAction(String profile, String title, Boolean sendDingTalk, Object[] args, String shortString, String invokeServerName, Throwable e, Boolean downgrade) {
if (sendDingTalk) {
DingTalkUtils.sendDingTalk(profile, title, argsArrayToString(args), invokeServerName, e);
sendRivenDingTalk(profile, title, args, invokeServerName, e);
}
}
},
/**
* 仅打印日志
@ -36,7 +45,6 @@ public enum ReporterType {
if (downgrade) {
log.warn("ER: {}", e.getMessage());
} else {
// LogUtil.error(LogUtil.ErrorType.ERROR_BUSINESS, shortString, e.getMessage(), e);
logWarn(e, shortString);
}
}
@ -48,7 +56,7 @@ public enum ReporterType {
@Override
public void executeAction(String profile, String title, Boolean sendDingTalk, Object[] args, String shortString, String invokeServerName, Throwable e, Boolean downgrade) {
if (sendDingTalk) {
DingTalkUtils.sendDingTalk(profile, title, argsArrayToString(args),invokeServerName, e);
sendRivenDingTalk(profile, title, args, invokeServerName, e);
}
if (downgrade) {
log.warn("ER: {}", e.getMessage());
@ -76,4 +84,35 @@ public enum ReporterType {
// LogUtil.error(LogUtil.ErrorType.ERROR_BUSINESS, shortString, throwable.getMessage(), throwable);
// }
}
public void sendRivenDingTalk(String profile, String title, Object[] args, String invokeServerName, Throwable e) {
RivenDingTalkHelper rivenDingTalkHelper = SpringUtils.getBean(RivenDingTalkHelper.class);
String mdTitle = "Notice " + title + ", Env: " + profile;
String mdContent = "#### [" + profile + "]" + title + "\n" +
"> 时间: " + DateUtil.now() + "\n" +
"> 入参: " + JSONUtil.toJsonStr(argsArrayToString(args)) + "\n\n" +
"> ###### 异常信息: " + JSONUtil.toJsonStr(Objects.isNull(e.getCause()) ? e.getMessage() :
e.getCause().getMessage()) + " \n\n" +
"> ##### traceId: " + MDC.get(CTX_LOG_ID_MDC) + " \n" +
"> ##### 调用方服务名称:" + invokeServerName + " \n";
String deadLetterStacktrace = getDeadLetterStacktrace(profile, argsArrayToString(args));
mdContent = mdContent + (StringUtils.hasText(deadLetterStacktrace) ? "> ##### [点击查看异常明细](" + deadLetterStacktrace + ") \n" : "");
rivenDingTalkHelper.sendMarkdownMessage(mdTitle, mdContent, false, false);
}
private static String getDeadLetterStacktrace(String profile, Object req) {
try {
JSONObject entries = JSONUtil.parseObj(req);
String jobId = entries.getStr("id");
if (!StringUtils.hasText(jobId)) {
return "";
}
return getEnvUrl(profile) + "/workflow-engine/web/v1/api/process/job/dead-letter/exception/stacktrace/byId?jobId=" + jobId;
} catch (Exception e) {
log.warn("构造查询错误堆栈地址异常", e);
return "";
}
}
}

View File

@ -11,12 +11,13 @@ import cn.axzo.workflow.common.exception.WorkflowApproverCalcException;
import cn.axzo.workflow.common.exception.WorkflowEngineException;
import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskDelegateAssigner;
import cn.axzo.workflow.core.common.utils.BpmnMetaParserHelper;
import cn.axzo.workflow.core.common.utils.SpringContextUtils;
import cn.axzo.workflow.core.conf.SupportRefreshProperties;
import cn.axzo.workflow.core.deletage.BpmnTaskAssigneeSelector;
import cn.axzo.workflow.core.deletage.approverscope.ApproverScopeDTO;
import cn.axzo.workflow.core.deletage.approverscope.ApproverScopeProcessor;
import cn.axzo.workflow.core.engine.model.NoticeFlowElement;
import cn.axzo.workflow.core.util.DingTalkUtils;
import cn.axzo.workflow.core.util.RivenDingTalkHelper;
import cn.hutool.core.lang.Assert;
import cn.hutool.http.HttpStatus;
import cn.hutool.json.JSONUtil;
@ -64,6 +65,8 @@ public abstract class AbstractBpmnTaskAssigneeSelector implements BpmnTaskAssign
protected FlowSupportApi flowSupportApi;
@Resource
protected SupportRefreshProperties refreshProperties;
@Resource
private RivenDingTalkHelper rivenDingTalkHelper;
private ApplicationContext applicationContext;
@Override
@ -136,12 +139,12 @@ public abstract class AbstractBpmnTaskAssigneeSelector implements BpmnTaskAssign
JSONUtil.toJsonStr(result));
try {
if (stopWatch.getTotalTimeSeconds() > refreshProperties.getApiTimeout() && Boolean.TRUE.equals(refreshProperties.getSendDingTalk())) {
DingTalkUtils.sendDingTalkForSlowUrl(applicationContext.getEnvironment()
.getProperty("spring.profiles.active"),
stopWatch.getTotalTimeSeconds(),
extInfo,
param,
result);
String title = "Notice 请求二方接口慢 URL, Env: " + refreshProperties.getProfile();
String content = "#### [" + refreshProperties.getProfile() + "]请求二方接口过慢\n" +
"> 接口地址: " + extInfo + ",经过了" + stopWatch.getTotalTimeSeconds() + "\n\n" +
"> 请求参数: " + JSONUtil.toJsonStr(param) + "\n\n" +
"> ###### 结果: " + JSONUtil.toJsonStr(result) + " \n";
rivenDingTalkHelper.sendMarkdownMessage(title, content, true, false);
}
} catch (Exception e) {
// ignore
@ -170,12 +173,13 @@ public abstract class AbstractBpmnTaskAssigneeSelector implements BpmnTaskAssign
JSONUtil.toJsonStr(result));
try {
if (stopWatch.getTotalTimeSeconds() > refreshProperties.getApiTimeout() && Boolean.TRUE.equals(refreshProperties.getSendDingTalk())) {
DingTalkUtils.sendDingTalkForSlowUrl(applicationContext.getEnvironment()
.getProperty("spring.profiles.active"),
stopWatch.getTotalTimeSeconds(),
extInfo,
param,
result);
String title = "Notice 请求二方接口慢 URL, Env: " + refreshProperties.getProfile();
String content = "#### [" + refreshProperties.getProfile() + "]请求二方接口过慢\n" +
"> 接口地址: " + extInfo + ",经过了" + stopWatch.getTotalTimeSeconds() + "\n\n" +
"> 请求参数: " + JSONUtil.toJsonStr(param) + "\n\n" +
"> ###### 结果: " + JSONUtil.toJsonStr(result) + " \n";
rivenDingTalkHelper.sendMarkdownMessage(title, content, true, false);
}
} catch (Exception e) {
// ignore
@ -207,12 +211,14 @@ public abstract class AbstractBpmnTaskAssigneeSelector implements BpmnTaskAssign
JSONUtil.toJsonStr(result));
try {
if (stopWatch.getTotalTimeSeconds() > refreshProperties.getApiTimeout() && refreshProperties.getSendDingTalk()) {
DingTalkUtils.sendDingTalkForSlowUrl(context.getEnvironment()
.getProperty("spring.profiles.active"),
stopWatch.getTotalTimeSeconds(),
extInfo,
param,
result);
RivenDingTalkHelper rivenDingTalkHelper = SpringContextUtils.getBean(RivenDingTalkHelper.class);
String title = "Notice 请求二方接口慢 URL, Env: " + refreshProperties.getProfile();
String content = "#### [" + refreshProperties.getProfile() + "]请求二方接口过慢\n" +
"> 接口地址: " + extInfo + ",经过了" + stopWatch.getTotalTimeSeconds() + "\n\n" +
"> 请求参数: " + JSONUtil.toJsonStr(param) + "\n\n" +
"> ###### 结果: " + JSONUtil.toJsonStr(result) + " \n";
rivenDingTalkHelper.sendMarkdownMessage(title, content, true, false);
}
} catch (Exception e) {
// ignore

View File

@ -63,6 +63,7 @@ public class TransferToAdminTaskAssigneeSelector extends AbstractBpmnTaskAssigne
protected List<BpmnTaskDelegateAssigner> invokeService(FlowElement flowElement, DelegateExecution execution,
ApproverScopeDTO scopeDto) {
log.info("transferToAdmin invokeService executing...");
if (Boolean.TRUE.equals(supportRefreshProperties.getUseNewToAdminApi())) {
return invokeNewQuery(flowElement, execution, scopeDto);
} else {
@ -72,6 +73,7 @@ public class TransferToAdminTaskAssigneeSelector extends AbstractBpmnTaskAssigne
}
private List<BpmnTaskDelegateAssigner> invokeOldQuery(FlowElement flowElement, DelegateExecution execution, ApproverScopeDTO scopeDto) {
log.info("transferToAdmin invokeOldQuery executing...");
ListFlowTaskAssignerReq.ListFlowTaskAssignerReqBuilder builder = ListFlowTaskAssignerReq.builder();
if (!CollectionUtils.isEmpty(scopeDto.getOrgScopes())) {
builder.orgScopes(ListUtils.emptyIfNull(scopeDto.getOrgScopes()).stream()
@ -106,6 +108,7 @@ public class TransferToAdminTaskAssigneeSelector extends AbstractBpmnTaskAssigne
}
private List<BpmnTaskDelegateAssigner> invokeNewQuery(FlowElement flowElement, DelegateExecution execution, ApproverScopeDTO scopeDto) {
log.info("transferToAdmin invokeNewQuery executing...");
Optional<ApproverScopeEnum> approverScope = BpmnMetaParserHelper.getApproverScope((UserTask) flowElement);
Optional<ApproverSpecifyEnum> optSpecify = BpmnMetaParserHelper.getApproverSpecify((UserTask) flowElement);
// 如果是项目部且审批人指定的配法不是岗位或角色则默认直接返回空集合走转交管理员后为空的最终兜底逻辑

View File

@ -3,13 +3,15 @@ package cn.axzo.workflow.server.controller.web;
import cn.axzo.framework.domain.data.AssertUtil;
import cn.axzo.riven.client.domain.ThirdPartyUserDTO;
import cn.axzo.riven.client.feign.ThirdPartySyncApi;
import cn.axzo.riven.client.req.ThirdPartyUserReq;
import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceAbortDTO;
import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceCancelDTO;
import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskAuditDTO;
import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskDelegateAssigner;
import cn.axzo.workflow.core.engine.cmd.CustomApproveTaskCmd;
import cn.axzo.workflow.core.engine.cmd.CustomRejectionTaskCmd;
import cn.axzo.workflow.core.repository.entity.ExtAxProcessLog;
import cn.axzo.workflow.core.service.BpmnProcessTaskService;
import cn.axzo.workflow.core.service.ExtAxHiTaskInstService;
import cn.axzo.workflow.core.service.ExtAxProcessLogService;
import cn.axzo.workflow.server.common.util.RpcExternalUtil;
import cn.axzo.workflow.server.controller.web.bpmn.BpmnProcessInstanceController;
@ -21,8 +23,10 @@ import cn.hutool.http.HttpRequest;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.flowable.common.engine.impl.interceptor.CommandExecutor;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.spring.SpringProcessEngineConfiguration;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Controller;
@ -42,6 +46,7 @@ import java.util.List;
import java.util.Objects;
import static cn.axzo.workflow.common.code.BpmnInstanceRespCode.PROCESS_INSTANCE_NOT_EXISTS;
import static cn.axzo.workflow.common.code.OtherRespCode.DANGER_OPERATION_NOT_SIGN_IN;
import static cn.axzo.workflow.common.constant.BpmnConstants.INTERNAL_INITIATOR;
import static cn.axzo.workflow.common.constant.StarterConstants.K8S_POD_NAME_SPACE;
import static cn.axzo.workflow.common.enums.BpmnProcessInstanceResultEnum.PROCESSING;
@ -58,6 +63,8 @@ public class DangerOperationController {
@Resource
private RuntimeService runtimeService;
@Resource
private SpringProcessEngineConfiguration processEngineConfiguration;
@Resource
private BpmnProcessInstanceController instanceController;
@Resource
private BpmnProcessTaskController taskController;
@ -70,6 +77,8 @@ public class DangerOperationController {
@Resource
private Environment environment;
@Resource
private ExtAxHiTaskInstService extAxHiTaskInstService;
@Resource
private ThirdPartySyncApi thirdPartySyncApi;
@Value("${dingtalk.appKey:dingfg3ijkpjkqnrgapc}")
@ -81,7 +90,7 @@ public class DangerOperationController {
// 显示表单页面
@GetMapping("/web/process/form")
public String showProcessForm(HttpSession session, Model model) {
log.info("{} 访问流程表单页面", getOperatorInfo(session));
log.info("{} 访问流程表单页面, SessionID: {}", getOperatorInfo(session), session.getId());
// 检查session中是否已验证授权码
Boolean isAuthenticated = (Boolean) session.getAttribute("isAuthenticated");
model.addAttribute("isAuthenticated", isAuthenticated != null && isAuthenticated);
@ -109,6 +118,7 @@ public class DangerOperationController {
// 处理表单提交的逻辑
log.info("{} 请求操作流程: {}", getOperatorInfo(session), JSON.toJSONString(jobParam));
AssertUtil.notNull(session, DANGER_OPERATION_NOT_SIGN_IN);
try {
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(jobParam.getProcessInstanceId()).singleResult();
@ -175,13 +185,24 @@ public class DangerOperationController {
log.warn("未找到可操作的任务日志无法驳回任务processInstanceId={}, personId={}", jobParam.getProcessInstanceId(), jobParam.getPersonId());
return;
}
List<BpmnTaskDelegateAssigner> assigneeFull = logs.get(0).getAssigneeFull();
if (CollectionUtils.isEmpty(assigneeFull)) {
// 自动驳回
BpmnTaskAuditDTO reject = new BpmnTaskAuditDTO();
reject.setTaskId(logs.get(0).getTaskId());
reject.setApprover(new BpmnTaskDelegateAssigner("", "system", logs.get(0).getTenantId()));
reject.setOperationDesc("未找到审批人,自动驳回");
CommandExecutor commandExecutor = processEngineConfiguration.getCommandExecutor();
commandExecutor.execute(new CustomRejectionTaskCmd(reject, extAxHiTaskInstService));
} else {
taskController.rejectTask(BpmnTaskAuditDTO.builder()
.processInstanceId(jobParam.getProcessInstanceId())
.advice(jobParam.getComment())
.approver(assigneeFull.get(0))
.async(false)
.build());
}
taskController.rejectTask(BpmnTaskAuditDTO.builder()
.processInstanceId(jobParam.getProcessInstanceId())
.advice(jobParam.getComment())
.approver(logs.get(0).getAssigneeFull().get(0))
.async(false)
.build());
}
private void approveTask(DangerSuperOperationJobHandler.DangerOperationJobParam jobParam) {
@ -205,12 +226,24 @@ public class DangerOperationController {
return;
}
taskController.approveTask(BpmnTaskAuditDTO.builder()
.processInstanceId(jobParam.getProcessInstanceId())
.advice(jobParam.getComment())
.approver(logs.get(0).getAssigneeFull().get(0))
.async(false)
.build());
List<BpmnTaskDelegateAssigner> assigneeFull = logs.get(0).getAssigneeFull();
if (CollectionUtils.isEmpty(assigneeFull)) {
// 自动通过
BpmnTaskAuditDTO pass = new BpmnTaskAuditDTO();
pass.setTaskId(logs.get(0).getTaskId());
pass.setAdvice("");
pass.setApprover(null);
pass.setOperationDesc("未找到审批人,自动同意");
CommandExecutor commandExecutor = processEngineConfiguration.getCommandExecutor();
commandExecutor.execute(new CustomApproveTaskCmd(pass));
} else {
taskController.approveTask(BpmnTaskAuditDTO.builder()
.processInstanceId(jobParam.getProcessInstanceId())
.advice(jobParam.getComment())
.approver(assigneeFull.get(0))
.async(false)
.build());
}
}
private void cancelProcessInstance(DangerSuperOperationJobHandler.DangerOperationJobParam jobParam) {
@ -231,19 +264,23 @@ public class DangerOperationController {
* @param authCode 钉钉返回的授权码
*/
@GetMapping("/web/process/dingtalk-callback")
public String dingTalkCallback(@RequestParam("authCode") String authCode, HttpSession session, Model model) {
log.info("收到钉钉登录回调, authCode: {}", authCode);
public String dingTalkCallback(@RequestParam("authCode") String authCode, HttpSession session, Model model, javax.servlet.http.HttpServletRequest request) {
log.info("【dingtalk】收到钉钉登录回调, SessionID: {}, authCode: {}", session.getId(), authCode);
String myPodNamespace = environment.getProperty(K8S_POD_NAME_SPACE);
String baseUrl = StringUtils.hasText(myPodNamespace) ? "/workflow-engine" : "";
model.addAttribute("apiBaseUrl", baseUrl);
model.addAttribute("dingTalkAppKey", appKey);
// 如果没有配置 AppSecret则无法进行后续交互直接返回错误或者为了测试方便这里可以留个后门? 严格处理
// 如果没有配置 AppSecret则无法进行后续交互
if (!StringUtils.hasText(appSecret)) {
log.error("DingTalk AppSecret not configured");
log.info("【dingtalk】DingTalk AppSecret not configured");
model.addAttribute("isAuthenticated", false);
model.addAttribute("authError", "服务端未配置 AppSecret无法登录");
return "form";
}
try {
// 1. 获取 AccessToken
// 文档: https://open.dingtalk.com/document/isvapp/obtain-user-token
JSONObject tokenParams = new JSONObject();
tokenParams.put("clientId", appKey);
tokenParams.put("clientSecret", appSecret);
@ -256,56 +293,62 @@ public class DangerOperationController {
.execute()
.body();
log.info("DingTalk Token Response: {}", tokenResponse);
log.info("【dingtalk】DingTalk Token Response: {}", tokenResponse);
JSONObject tokenJson = JSON.parseObject(tokenResponse);
String accessToken = tokenJson.getString("accessToken");
if (!StringUtils.hasText(accessToken)) {
log.error("Failed to get access token: {}", tokenResponse);
log.info("【dingtalk】Failed to get access token: {}", tokenResponse);
model.addAttribute("isAuthenticated", false);
model.addAttribute("authError", "钉钉登录验证失败: 无法获取 AccessToken");
return "form";
}
// 2. 获取用户详情
// 文档: https://open.dingtalk.com/document/isvapp/obtain-user-information
String userInfoResponse = HttpRequest.get("https://api.dingtalk.com/v1.0/contact/users/me")
.header("x-acs-dingtalk-access-token", accessToken)
.timeout(5000)
.execute()
.body();
log.info("DingTalk User Response: {}", userInfoResponse);
log.info("【dingtalk】DingTalk User Response: {}", userInfoResponse);
JSONObject userJson = JSON.parseObject(userInfoResponse);
String unionId = userJson.getString("unionId");
String openId = userJson.getString("openId");
String nick = userJson.getString("nick");
String mobile = userJson.getString("mobile");
if (!StringUtils.hasText(openId) && !StringUtils.hasText(unionId)) {
log.error("Failed to get user info: {}", userInfoResponse);
model.addAttribute("authError", "钉钉登录验证失败: 无法获取用户信息");
if (!StringUtils.hasText(mobile)) {
log.info("【dingtalk】Failed to get user info: {}", userInfoResponse);
model.addAttribute("isAuthenticated", false);
model.addAttribute("authError", "钉钉登录验证失败: 无法获取用户手机号");
return "form";
}
ThirdPartyUserReq build = ThirdPartyUserReq.builder().unionId(unionId).build();
List<ThirdPartyUserDTO> users = RpcExternalUtil.rpcApiResultProcessor(() -> thirdPartySyncApi.getUserInfos(build), "查询用户是否存在", build);
List<ThirdPartyUserDTO> users = RpcExternalUtil.rpcApiResultProcessor(() -> thirdPartySyncApi.getUserInfosByPhone(mobile), "查询用户是否存在", mobile);
if (CollectionUtils.isEmpty(users)) {
log.info("【dingtalk】Failed to get user info2: {}", JSON.toJSONString(users));
model.addAttribute("isAuthenticated", false);
model.addAttribute("authError", "用户未授权!");
return "form";
}
// 3. 登录成功
log.info("DingTalk Login Success: nick={}, unionId={}", nick, unionId);
log.info("【dingtalk】DingTalk Login Success: nick={}, mobile={}", nick, mobile);
session.setAttribute("isAuthenticated", true);
// 可以把用户信息也存进去
session.setAttribute("dingUser", userJson);
// 重定向回表单页
return "redirect:/web/process/form";
// TODO: 主人请注意为了适配复杂的反向代理环境小码酱在这里改回了页面跳转模式
// 我们在 Model 中塞入一个信号量和相对地址让前端根据浏览器当前感知的 host 来决定往哪跳汪汪
model.addAttribute("userNick", nick);
model.addAttribute("isAuthenticated", true);
model.addAttribute("needsRedirect", true);
model.addAttribute("redirectRelativeUrl", "/web/process/form");
return "form";
} catch (Exception e) {
log.error("DingTalk Callback Error", e);
model.addAttribute("authError", "登录过程中发生异常");
log.info("【dingtalk】DingTalk Callback Error", e);
model.addAttribute("isAuthenticated", false);
model.addAttribute("authError", "登录过程中发生异常: " + e.getMessage());
return "form";
}
}

View File

@ -1,10 +1,12 @@
package cn.axzo.workflow.server.controller.web.manage;
import cn.axzo.framework.jackson.utility.JSON;
import cn.axzo.workflow.client.feign.manage.FunctionApi;
import cn.axzo.workflow.common.enums.AdminDataSource;
import cn.axzo.workflow.common.enums.BpmnProcessInstanceResultEnum;
import cn.axzo.workflow.common.model.dto.CommonDingTalkDTO;
import cn.axzo.workflow.common.model.request.feature.DingTalkStarterAlterDTO;
import cn.axzo.workflow.server.alter.DingTalkAlter;
import cn.axzo.workflow.core.util.RivenDingTalkHelper;
import cn.axzo.workflow.server.common.annotation.ErrorReporter;
import cn.azxo.framework.common.model.CommonResponse;
import io.swagger.v3.oas.annotations.Operation;
@ -31,7 +33,7 @@ import javax.annotation.Resource;
@Validated
public class FunctionController implements FunctionApi {
@Resource
private DingTalkAlter dingTalkAlter;
private RivenDingTalkHelper rivenDingTalkHelper;
/**
* 获取指定枚举类型的枚举值信息
@ -57,7 +59,22 @@ public class FunctionController implements FunctionApi {
@PostMapping("/dingtalk/alter")
@Override
public CommonResponse<Boolean> sendDingtalk(@Validated @RequestBody DingTalkStarterAlterDTO dto) {
dingTalkAlter.invoke(dto);
log.info("send dingtalk alter, request: {}", JSON.toJSONString(dto));
String title = "Notice 应用必接广播 MQ 事件告警, Env: " + dto.getProfile();
String content = "#### [" + dto.getProfile() + "]应用必接广播 MQ 事件告警\n" +
"> 应用名称:" + dto.getApplicationName() + "\n\n" +
"> 检测信息:" + dto.getAlterContent() + "\n\n";
rivenDingTalkHelper.sendMarkdownMessage(title, content, true, false);
return CommonResponse.success(true);
}
@Operation(summary = "发送通用钉钉消息")
@PostMapping("/common/dingtalk/send")
@Override
public CommonResponse<Boolean> sendCommonDingtalk(@Validated @RequestBody CommonDingTalkDTO dto) {
log.info("send common dingtalk msg : {}", JSON.toJSONString(dto));
rivenDingTalkHelper.sendMarkdownMessage(dto.getTitle(), dto.getContext(), dto.getTargetIsMaster(), dto.getAt(), dto.getMobiles());
return CommonResponse.success(true);
}
}

View File

@ -3,7 +3,6 @@ package cn.axzo.workflow.server.xxljob;
import cn.axzo.basics.common.util.NumberUtil;
import cn.axzo.framework.jackson.utility.JSON;
import cn.axzo.infra.xxl220to250.IJobHandler;
import cn.axzo.workflow.common.model.dto.AlterDTO;
import cn.axzo.workflow.common.model.response.category.CategoryItemVO;
import cn.axzo.workflow.core.common.utils.SpringContextUtils;
import cn.axzo.workflow.core.conf.SupportRefreshProperties;
@ -11,7 +10,10 @@ import cn.axzo.workflow.core.listener.Alter;
import cn.axzo.workflow.core.repository.entity.ExtAxNodeAlterJob;
import cn.axzo.workflow.core.service.CategoryService;
import cn.axzo.workflow.core.service.ExtAxNodeAlterJobService;
import cn.axzo.workflow.core.util.RivenDingTalkHelper;
import cn.hutool.core.date.DateUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.context.XxlJobHelper;
@ -36,6 +38,7 @@ import java.util.Optional;
import java.util.stream.Collectors;
import static cn.axzo.workflow.common.constant.BpmnConstants.BPM_MODEL_CATEGORY;
import static cn.axzo.workflow.core.util.RivenDingTalkHelper.getWebUrl;
/**
* 调度业务节点告警定时任务
@ -52,6 +55,7 @@ public class NodeAlterJobHandler extends IJobHandler {
private final CategoryService categoryService;
private final SpringProcessEngineConfiguration processEngineConfiguration;
private final SupportRefreshProperties refreshProperties;
private final RivenDingTalkHelper rivenDingTalkHelper;
@XxlJob("nodeAlterJobHandler")
@Override
@ -109,17 +113,20 @@ public class NodeAlterJobHandler extends IJobHandler {
private void sendAlter(ProcessInstance processInstance, Optional<CategoryItemVO> category, ExtAxNodeAlterJob job, Task task) {
// 发送告警对象
Alter alter = SpringContextUtils.getBean(Alter.class);
AlterDTO alterDTO = new AlterDTO();
alterDTO.setProcessDefinitionKey(processInstance.getProcessDefinitionKey());
alterDTO.setProcessDefinitionName(category.orElse(new CategoryItemVO()).getLabel());
alterDTO.setProcessInstanceId(job.getProcessInstanceId());
alterDTO.setActivityId(job.getActivityId());
alterDTO.setTaskId(task.getId());
alterDTO.setStartTime(task.getCreateTime());
alterDTO.setPrettyStartTime(DateUtil.formatDateTime(task.getCreateTime()));
if (Boolean.TRUE.equals(refreshProperties.getAlterSendDingTalk())) {
alter.invoke(alterDTO);
}
JSONObject jsonObject = new JSONObject();
jsonObject.set("processInstanceId", job.getProcessInstanceId());
jsonObject.set("activityId", job.getActivityId());
jsonObject.set("taskId", task.getId());
jsonObject.set("startTime", task.getCreateTime());
jsonObject.set("prettyStartTime", DateUtil.formatDateTime(task.getCreateTime()));
String text = "Notice 业务节点长时间停止告警, Env: " + refreshProperties.getProfile();
String content = "#### [" + refreshProperties.getProfile() + "]业务节点长时间停止\n" +
"> 审批业务: " + category.orElse(new CategoryItemVO()).getLabel() + "(" + processInstance.getProcessDefinitionKey() + ")" + "\n\n" +
"> 节点相关信息: " + JSONUtil.toJsonStr(jsonObject) + "\n\n" +
"> ##### [点击查看审批日志](" + getWebUrl(refreshProperties.getProfile()) + "/#/workflow/examples?processInstanceId=" + job.getProcessInstanceId() + ") \n\n" +
"> ##### 提示:如果以上地址提示未登录,请在对应环境 OMS 系统登录后并点击审批管理的功能后,再使用。或者复制实例 ID 到 OMS 中手动查询 \n\n";
rivenDingTalkHelper.sendMarkdownMessage(text, content, true, true);
}
private Boolean hasAssignee(String assignee) {

View File

@ -8,4 +8,10 @@ spring:
cache: false
encoding: UTF-8
prefix: classpath:/templates/
suffix: .html
suffix: .html
server:
servlet:
session:
cookie:
same-site: lax
http-only: true

View File

@ -34,35 +34,53 @@
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.form-input-focus {
@apply focus:border-primary focus:ring-2 focus:ring-primary/20 focus:outline-none;
}
<style type="text/css">
/* 自定义表单输入焦点样式 */
.form-input-focus:focus {
border-color: #165DFF;
box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.2);
outline: none;
}
.form-transition {
@apply transition-all duration-300 ease-in-out;
}
/* 过渡动画 */
.form-transition {
transition: all 0.3s ease-in-out;
}
.card-shadow {
@apply shadow-lg hover:shadow-xl transition-shadow duration-300;
}
/* 卡片阴影效果 */
.card-shadow {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
transition: box-shadow 0.3s;
}
.form-hidden {
@apply hidden opacity-0 h-0;
}
.card-shadow:hover {
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.form-visible {
@apply opacity-100 h-auto;
}
/* 表单隐藏状态 */
.form-hidden {
display: none;
opacity: 0;
height: 0;
overflow: hidden;
}
.mask-fade {
@apply transition-opacity duration-300 ease-in-out;
}
/* 表单显示状态 */
.form-visible {
display: block;
opacity: 1;
height: auto;
}
.tab-active {
@apply text-primary border-primary;
}
/* 遮罩层渐变效果 */
.mask-fade {
transition: opacity 0.3s ease-in-out;
}
/* 标签页激活状态 */
.tab-active {
color: #165DFF;
border-color: #165DFF;
}
</style>
@ -141,14 +159,14 @@
<h2 class="text-[clamp(1.5rem,3vw,2rem)] font-bold text-dark mb-2"
th:text="${isAuthenticated} ? '流程操作' : '授权验证'"></h2>
<p class="text-secondary"
th:text="${isAuthenticated} ? '请根据需要选择相应操作并填写表单信息' : '请输入扫码登陆'">
th:text="${isAuthenticated} ? '请根据需要选择相应操作并填写表单信息' : '请输入扫码登陆!本系统请使用 Chrome 或 Edge 浏览器'">
</p>
</div>
<!-- 授权验证区域 - 未认证时显示 -->
<div th:unless="${isAuthenticated}">
<!-- 错误提示 Banner -->
<div class="mb-4 bg-red-50 border border-red-200 rounded-lg p-4 flex items-center"
<div id="authError" class="mb-4 bg-red-50 border border-red-200 rounded-lg p-4 flex items-center"
th:if="${authError != null}">
<i class="fa fa-exclamation-circle text-danger text-lg mr-3"></i>
<div>
@ -303,6 +321,28 @@
</div>
</div>
<script th:inline="javascript">
// 适配多层反代环境的重定向逻辑
const needsRedirect = [[${needsRedirect}]] || false;
const redirectRelativeUrl = [[${redirectRelativeUrl}]] || null;
if (needsRedirect && redirectRelativeUrl) {
// TODO: 主人看这里!我们利用 window.location.origin 获取浏览器感知的真实协议、域名和端口。
// 这样无论中间隔了多少层反代,前端算出来的跳转地址都是绝对正确的!汪汪!🐶
const timestamp = new Date().getTime();
// 使用 apiBaseUrl 拼接,确保在 K8S 环境下路径前缀也是对的
const targetPath = getFullUrl(redirectRelativeUrl);
const separator = targetPath.indexOf('?') === -1 ? '?' : '&';
// 加上时间戳,强制浏览器必须回到容器里刷新最新的 Session 状态!
const finalUrl = window.location.origin + targetPath + separator + '_t=' + timestamp;
console.log('Redirecting to absolute URL:', finalUrl);
window.location.href = finalUrl;
}
</script>
<script>
// 获取DOM元素
const operationType = document.getElementById('operationType');
@ -565,20 +605,20 @@
// 页面加载时检查后端消息
document.addEventListener('DOMContentLoaded', function () {
// 检查流程操作的后端消息(使用 Thymeleaf JS 内联语法)
/*[[#{
if (${message != null}) {
const serverMessage = /*[[${message}]]*/ null;
if (serverMessage && serverMessage !== 'null') {
operationMessage.classList.remove('hidden');
if (${message.startsWith('操作成功')}) {
if (serverMessage.includes('成功')) {
operationMessage.className = 'mt-4 p-4 rounded-lg bg-green-50 border border-green-200 text-green-700';
} else {
operationMessage.className = 'mt-4 p-4 rounded-lg bg-red-50 border border-red-200 text-red-700';
}
operationMessage.innerHTML = '<i class="fa ' + (${message.startsWith('操作成功')} ? 'fa-check-circle' : 'fa-exclamation-circle') + ' mr-2"></i>' + '${message}';
const iconClass = serverMessage.includes('成功') ? 'fa-check-circle' : 'fa-exclamation-circle';
operationMessage.innerHTML = `<i class="fa ${iconClass} mr-2"></i>${serverMessage}`;
}
}]]*/
// 显示授权错误(如果有)
if (authError && authError.textContent.trim() !== '') {
if (authError && authError.textContent.trim() !== '' && !authError.textContent.includes('${authError}')) {
authError.classList.remove('hidden');
}
});

View File

@ -163,8 +163,9 @@ public class WorkflowEngineStarterAutoConfiguration {
ObjectProvider<BroadcastDLQReporter> broadcastDLQProcessorObjectProvider,
ObjectProvider<RpcDLQReporter> rpcDLQProcessorObjectProvider,
WorkflowEngineStarterProperties workflowEngineStarterProperties,
WorkflowCoreService workflowCoreService,
Environment environment) {
return new WorkflowEngineStarterDefaultMQMonitor(mqAdminExtObjectProvider, broadcastDLQProcessorObjectProvider, rpcDLQProcessorObjectProvider, workflowEngineStarterProperties, environment);
return new WorkflowEngineStarterDefaultMQMonitor(mqAdminExtObjectProvider, broadcastDLQProcessorObjectProvider, rpcDLQProcessorObjectProvider, workflowEngineStarterProperties, workflowCoreService, environment);
}
@Bean

View File

@ -1,12 +1,16 @@
package cn.axzo.workflow.starter.api;
import cn.axzo.workflow.common.annotation.InvokeMode;
import cn.axzo.workflow.common.model.dto.CommonDingTalkDTO;
import cn.axzo.workflow.common.model.dto.SignFileDTO;
import cn.axzo.workflow.common.model.dto.SimpleDocDTO;
import cn.axzo.workflow.common.model.dto.print.PrintFieldDTO;
import cn.axzo.workflow.common.model.request.bpmn.activity.BpmnActivityTimeoutCallbackDTO;
import cn.axzo.workflow.common.model.request.bpmn.activity.BpmnActivityTimeoutTriggerDTO;
import cn.axzo.workflow.common.model.request.bpmn.model.doc.DocQueryDTO;
import cn.axzo.workflow.common.model.request.bpmn.print.PrintProcessLogPdfDTO;
import cn.axzo.workflow.common.model.request.bpmn.print.PrintTemplateConfigUpsertDTO;
import cn.axzo.workflow.common.model.request.bpmn.print.QueryProcessLogPdfDTO;
import cn.axzo.workflow.common.model.request.bpmn.process.BeforeProcessInstanceCreateDTO;
import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceAbortDTO;
import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceBatchQueryDTO;
@ -22,6 +26,7 @@ import cn.axzo.workflow.common.model.request.bpmn.process.doc.ChangeApproverRead
import cn.axzo.workflow.common.model.request.bpmn.process.doc.ProcessDocQueryDTO;
import cn.axzo.workflow.common.model.request.bpmn.task.BpmnActivitySetAssigneeDTO;
import cn.axzo.workflow.common.model.request.bpmn.task.BpmnActivityTriggerDTO;
import cn.axzo.workflow.common.model.request.bpmn.task.BpmnNodeBackSystemOperateDTO;
import cn.axzo.workflow.common.model.request.bpmn.task.BpmnOptionalNodeDTO;
import cn.axzo.workflow.common.model.request.bpmn.task.BpmnRobotTaskCompleteDTO;
import cn.axzo.workflow.common.model.request.bpmn.task.BpmnRobotTaskCreateDTO;
@ -35,12 +40,14 @@ import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskRemindDTO;
import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskResetApproversDTO;
import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskTransferDTO;
import cn.axzo.workflow.common.model.request.feature.DingTalkStarterAlterDTO;
import cn.axzo.workflow.common.model.request.form.ConditionPermissionMetaInfo;
import cn.axzo.workflow.common.model.request.form.definition.StartFormSearchDTO;
import cn.axzo.workflow.common.model.request.form.instance.FormDetailDTO;
import cn.axzo.workflow.common.model.request.form.instance.FormVariablesUpdateDTO;
import cn.axzo.workflow.common.model.request.form.instance.FromDataSearchDTO;
import cn.axzo.workflow.common.model.request.form.model.WpsFileConfigVariableDTO;
import cn.axzo.workflow.common.model.response.bpmn.BatchOperationResultVO;
import cn.axzo.workflow.common.model.response.bpmn.model.doc.DocBaseVO;
import cn.axzo.workflow.common.model.response.bpmn.process.BpmnProcessInstanceLogVO;
import cn.axzo.workflow.common.model.response.bpmn.process.BpmnProcessInstanceVO;
import cn.axzo.workflow.common.model.response.bpmn.process.NodesByModelVO;
@ -49,6 +56,7 @@ import cn.axzo.workflow.common.model.response.bpmn.task.BpmnTaskButtonVo;
import cn.axzo.workflow.common.model.response.form.definition.FormDefinitionVO;
import cn.axzo.workflow.common.model.response.form.instance.FormDataVO;
import cn.axzo.workflow.common.model.response.form.instance.FormInstanceVO;
import cn.axzo.workflow.common.model.response.print.ProcessLogPdfResultDTO;
import cn.axzo.workflow.common.util.ThreadUtil;
import cn.axzo.workflow.starter.feign.ext.WorkflowEngineStarterFeignConfiguration;
import io.swagger.v3.oas.annotations.Operation;
@ -99,6 +107,30 @@ public interface WorkflowCoreService {
@InvokeMode(SYNC)
Void printTemplateConfig(@Validated @RequestBody PrintTemplateConfigUpsertDTO dto);
/**
* 后端请求指定流程日志 PDF 文件生成, 实现是异步的
* <p>
* 请使用 {@link PrintAdminApi#queryProcessLogPdfResult(QueryProcessLogPdfDTO)} 函数查询
* 或者使用 {@link cn.axzo.nanopart.doc.api.conversion.DocConversionApi#queryConvertResultByBiz(cn.axzo.nanopart.doc.api.conversion.req.QueryConversionTaskRequestV2)} 函数查询该接口入参默认情况下应该为bizCode:固定为"workflow-process-log", bizKey:为实例 ID+":"+访问人 personId
*
* @return
*/
@Operation(summary = "后端请求指定流程日志 PDF 文件生成")
@PostMapping("/api/print/admin/process/log/pdf")
@InvokeMode(SYNC)
String createProcessLogPdf(@Validated @RequestBody PrintProcessLogPdfDTO dto);
/**
* 后端查询指定审批日志 PDF 文件的生成结果
*
* @param dto
* @return
*/
@Operation(summary = "后端查询指定审批日志 PDF 文件的生成结果")
@PostMapping("/api/print/admin/process/log/pdf/result")
@InvokeMode(SYNC)
ProcessLogPdfResultDTO queryProcessLogPdfResult(@Validated @RequestBody QueryProcessLogPdfDTO dto);
/**
* 业务节点唤醒
*
@ -155,6 +187,11 @@ public interface WorkflowCoreService {
@InvokeMode(SYNC)
Boolean sendDingtalk(@Validated @RequestBody DingTalkStarterAlterDTO dto);
@Operation(summary = "发送通用钉钉消息")
@PostMapping("/api/function/common/dingtalk/send")
@InvokeMode(SYNC)
Boolean sendCommonDingtalk(@Validated @RequestBody CommonDingTalkDTO dto);
/**
* 获取指定审批业务的流程表单设置
*
@ -401,6 +438,28 @@ public interface WorkflowCoreService {
@InvokeMode(SYNC)
List<SignFileDTO> getProcessInstanceFinalDocs(@NotBlank(message = "流程实例 ID 不能为空") @RequestParam(required = false) String processInstanceId);
/**
* 获取流程实例的条件字段信息仅用于同意抽屉展示
*
* @param processInstanceId
* @return
*/
@Operation(summary = "获取流程实例的条件字段信息, 仅用于同意抽屉展示")
@GetMapping("/api/process/instance/conditions")
@InvokeMode(SYNC)
List<ConditionPermissionMetaInfo> getConditions(@NotBlank(message = "流程实例 ID 不能为空") @RequestParam String processInstanceId);
/**
* 获取指定模板的原始文档列表
*
* @param dto
* @return
*/
@Operation(summary = "根据业务 ID 获取模型文档列表,自动适配公共模板和代运营")
@PostMapping(value = "/api/process/model/doc/list")
@InvokeMode(SYNC)
List<DocBaseVO> docList(@Validated @RequestBody DocQueryDTO dto);
/**
* 同意
*
@ -461,6 +520,17 @@ public interface WorkflowCoreService {
@InvokeMode(ASYNC)
Boolean backTask(@Validated @RequestBody BpmnTaskBackAuditDTO dto);
/**
* 用于系统内部操作跳转到指定节点
*
* @param dto 请求参数
* @return 是否成功
*/
@Operation(summary = "系统操作回退任务到指定节点")
@PostMapping("/api/process/task/system/back")
@InvokeMode(ASYNC)
Boolean systemBackTask(@Validated @RequestBody BpmnNodeBackSystemOperateDTO dto);
/**
* 驳回
*

View File

@ -18,12 +18,12 @@ import cn.axzo.workflow.common.model.request.bpmn.model.BpmnModelUpdateDTO;
import cn.axzo.workflow.common.model.request.bpmn.model.doc.DocByIdDTO;
import cn.axzo.workflow.common.model.request.bpmn.model.doc.DocCreateDTO;
import cn.axzo.workflow.common.model.request.bpmn.model.doc.DocOrderDTO;
import cn.axzo.workflow.common.model.request.bpmn.model.doc.DocQueryDTO;
import cn.axzo.workflow.common.model.request.bpmn.model.doc.DocResetDTO;
import cn.axzo.workflow.common.model.request.bpmn.model.doc.DocSearchDTO;
import cn.axzo.workflow.common.model.request.bpmn.model.doc.DocStatusDTO;
import cn.axzo.workflow.common.model.request.bpmn.model.doc.DocTenantQueryDTO;
import cn.axzo.workflow.common.model.request.bpmn.model.doc.DocUpdateDTO;
import cn.axzo.workflow.common.model.request.bpmn.print.Print4ProcessLogDTO;
import cn.axzo.workflow.common.model.request.bpmn.print.PrintFieldQueryDTO;
import cn.axzo.workflow.common.model.request.bpmn.print.PrintTemplateConfigQueryDTO;
import cn.axzo.workflow.common.model.request.bpmn.print.RestPrintTemplateConfigDTO;
@ -31,7 +31,6 @@ import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessDefinitionP
import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceAdminPageReqVO;
import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceMyPageReqVO;
import cn.axzo.workflow.common.model.request.bpmn.process.SuperBpmnProcessInstanceCancelDTO;
import cn.axzo.workflow.common.model.request.bpmn.task.BpmnNodeBackSystemOperateDTO;
import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskAttachmentDTO;
import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskPageSearchDTO;
import cn.axzo.workflow.common.model.request.category.CategoryConfigCreateDTO;
@ -52,6 +51,7 @@ import cn.axzo.workflow.common.model.response.bpmn.process.BpmnProcessDefinition
import cn.axzo.workflow.common.model.response.bpmn.process.BpmnProcessInstanceAdminPageItemVO;
import cn.axzo.workflow.common.model.response.bpmn.process.BpmnProcessInstancePageItemVO;
import cn.axzo.workflow.common.model.response.bpmn.process.ExtProcessLogVO;
import cn.axzo.workflow.common.model.response.bpmn.process.PrintData4LogVO;
import cn.axzo.workflow.common.model.response.bpmn.process.ProcessNodeDetailVO;
import cn.axzo.workflow.common.model.response.bpmn.task.BpmnHistoricTaskInstanceGroupVO;
import cn.axzo.workflow.common.model.response.bpmn.task.BpmnHistoricTaskInstanceVO;
@ -114,7 +114,7 @@ public interface WorkflowManageService {
* 获取指定流程下用于替换打印的相关变量
*
* @param processInstanceId
* @return
* @return 仅是 kv 集合
*/
@Operation(summary = "获取指定流程下用于替换打印的相关变量")
@GetMapping("/api/print/admin/field/variables")
@ -122,6 +122,18 @@ public interface WorkflowManageService {
@InvokeMode(SYNC)
Map<String, Object> getPrintFieldVariables(@NotBlank(message = "流程实例不能为空") @RequestParam String processInstanceId, @RequestParam(required = false, defaultValue = "true") Boolean throwException);
/**
* 获取用于打印审批日志公共模板的数据
*
* @param dto
* @return
*/
@Operation(summary = "获取用于打印审批日志公共模板的数据")
@PostMapping("/api/print/admin/process/log/data/v2")
@Manageable
@InvokeMode(SYNC)
PrintData4LogVO getPrintDataForProcessLog(@Validated @RequestBody Print4ProcessLogDTO dto);
/**
* 查询管理员
* @param dto 管理员数据
@ -522,6 +534,7 @@ public interface WorkflowManageService {
*/
@Operation(summary = "流程模型列表")
@GetMapping("/api/process/model/page")
@Manageable
@InvokeMode(SYNC)
BpmPageResult<BpmnModelDetailVO> page(@Validated @RequestBody BpmnModelSearchDTO dto);
@ -531,6 +544,7 @@ public interface WorkflowManageService {
*/
@Operation(summary = "创建流程模型")
@PostMapping("/api/process/model/create")
@Manageable
@InvokeMode(SYNC)
String create(@Validated @RequestBody BpmnModelCreateDTO dto);
@ -539,6 +553,7 @@ public interface WorkflowManageService {
*/
@Operation(summary = "通过模型ID查询指定流程模型")
@GetMapping("/api/process/model/get")
@Manageable
@InvokeMode(SYNC)
BpmnModelDetailVO getById(@NotBlank(message = "流程模型 ID 不能为空") @RequestParam(required = false) String processModelId, @RequestParam(required = false) String tenantId);
@ -547,6 +562,7 @@ public interface WorkflowManageService {
*/
@Operation(summary = "通过模型KEY查询指定流程模型")
@GetMapping("/api/process/model/getByKey")
@Manageable
@InvokeMode(SYNC)
BpmnModelDetailVO getByKey(@NotBlank(message = "流程模型 KEY 不能为空") @RequestParam(required = false) String processModelKey, @NotBlank(message = "租户不能为空") @RequestParam(required = false) String tenantId);
@ -559,6 +575,7 @@ public interface WorkflowManageService {
*/
@Operation(summary = "获取指定模型的扩展属性")
@GetMapping("/api/process/model/ext")
@Manageable
@InvokeMode(SYNC)
BpmnModelExtVO getModelExt(@NotBlank(message = "模型 ID 不能为空") @RequestParam(required = false) String modelId);
@ -567,6 +584,7 @@ public interface WorkflowManageService {
*/
@Operation(summary = "更新流程模型")
@PutMapping("/api/process/model/update")
@Manageable
@InvokeMode(SYNC)
String update(@RequestBody BpmnModelUpdateDTO dto);
@ -577,6 +595,7 @@ public interface WorkflowManageService {
*/
@Operation(summary = "通过模型 ID 部署流程模型")
@PostMapping("/api/process/model/deploy")
@Manageable
@InvokeMode(SYNC)
String deployById(@NotBlank(message = "流程模型 ID 不能为空") @RequestParam(required = false) String processModelId, @RequestParam(required = false, defaultValue = "") String modelTenantId, @RequestParam(required = false) String operator);
@ -587,6 +606,7 @@ public interface WorkflowManageService {
*/
@Operation(summary = "通过模型 KEY 部署流程模型")
@PostMapping("/api/process/model/deployByKey")
@Manageable
@InvokeMode(SYNC)
String deployByKey(@NotBlank(message = "流程模型 KEY 不能为空") @RequestParam(required = false) String processModelKey, @NotBlank(message = "租户不能为空") @RequestParam(required = false) String modelTenantId, @RequestParam(required = false) String operator);
@ -600,6 +620,7 @@ public interface WorkflowManageService {
*/
@Operation(summary = "通过模型 ID 取消部署流程模型")
@PostMapping("/api/process/model/undeploy")
@Manageable
@InvokeMode(SYNC)
Void unDeployById(@NotBlank(message = "流程模型 ID 不能为空") @RequestParam(required = false) String processModelId, @RequestParam(required = false, defaultValue = "") String tenantId, @RequestParam(required = false) String operator);
@ -608,6 +629,7 @@ public interface WorkflowManageService {
*/
@Operation(summary = "删除指定模型 ID 的流程模型")
@DeleteMapping("/api/process/model/delete")
@Manageable
@InvokeMode(SYNC)
Void deleteById(@NotBlank(message = "流程模型 ID 不能为空") @RequestParam String processModelId, @RequestParam(required = false, defaultValue = "") String tenantId);
@ -620,6 +642,7 @@ public interface WorkflowManageService {
*/
@Operation(summary = "删除指定模型 KEY 的流程模型")
@DeleteMapping("/api/process/model/deleteByKey")
@Manageable
@InvokeMode(SYNC)
Void deleteByKey(@NotBlank(message = "流程模型 KEY 不能为空") @RequestParam String processModelKey, @RequestParam(required = false, defaultValue = "") String tenantId);
@ -633,6 +656,7 @@ public interface WorkflowManageService {
*/
@Operation(summary = "修改模型状态")
@PostMapping("/api/process/model/changeStatus")
@Manageable
@InvokeMode(SYNC)
Boolean changeStatus(@NotBlank(message = "模型 ID 不能为空") @RequestParam String modelId, @NotNull(message = "状态不能为空") @RequestParam Integer status, @RequestParam(required = false) String operator);
@ -646,6 +670,7 @@ public interface WorkflowManageService {
*/
@Operation(summary = "修改模型打印开关状态")
@PostMapping("/api/process/model/print/changeStatus")
@Manageable
@InvokeMode(SYNC)
Boolean changePrintStatus(@NotBlank(message = "模型 ID 不能为空") @RequestParam String modelId, @NotNull(message = "状态不能为空") @RequestParam Integer status, @RequestParam(required = false) String operator);
@ -656,6 +681,7 @@ public interface WorkflowManageService {
*/
@Operation(summary = "查询流程模型使用的分类列表")
@GetMapping("/api/process/model/category/ids")
@Manageable
@InvokeMode(SYNC)
List<String> getModelCategoryList();
@ -666,6 +692,7 @@ public interface WorkflowManageService {
*/
@Operation(summary = "查询模型的租户集合")
@GetMapping("/api/process/model/tenant/ids")
@Manageable
@InvokeMode(SYNC)
List<String> getModelTenantIds();
@ -677,6 +704,7 @@ public interface WorkflowManageService {
*/
@Operation(summary = "获取打印模板配置内容")
@PostMapping("/api/process/model/print/template/config/query")
@Manageable
@InvokeMode(SYNC)
PrintModelDTO getPrintTemplateConfig(@Validated @RequestBody PrintTemplateConfigQueryDTO dto);
@ -688,6 +716,7 @@ public interface WorkflowManageService {
*/
@Operation(summary = "代运营重置打印模板")
@PostMapping(value = "/api/process/model/print/template/config/reset")
@Manageable
@InvokeMode(SYNC)
Boolean resetPrintTemplateConfig(@Validated @RequestBody RestPrintTemplateConfigDTO dto);
@ -699,6 +728,7 @@ public interface WorkflowManageService {
*/
@Operation(summary = "搜索文档列表")
@PostMapping(value = "/api/process/model/doc/page")
@Manageable
@InvokeMode(SYNC)
BpmPageResult<DocBaseVO> docPage(@Validated @RequestBody DocSearchDTO dto);
@ -709,20 +739,10 @@ public interface WorkflowManageService {
*/
@Operation(summary = "获取指定 docIds 文档列表")
@PostMapping(value = "/api/process/model/doc/ids")
@Manageable
@InvokeMode(SYNC)
List<DocBaseVO> docByIds(@Validated @RequestBody DocByIdDTO dto);
/**
* 获取指定模板的原始文档列表
*
* @param dto
* @return
*/
@Operation(summary = "根据业务 ID 获取模型文档列表,自动适配公共模板和代运营")
@PostMapping(value = "/api/process/model/doc/list")
@InvokeMode(SYNC)
List<DocBaseVO> docList(@Validated @RequestBody DocQueryDTO dto);
/**
* 获取关联 HiPrint 类型文档模板内容
*
@ -731,6 +751,7 @@ public interface WorkflowManageService {
*/
@Operation(summary = "获取关联 HiPrint 类型文档模板内容")
@PostMapping(value = "/api/process/model/hi-print/content/get")
@Manageable
@InvokeMode(SYNC)
String getHiPrintContent(@RequestParam String fileRelationId);
@ -741,6 +762,7 @@ public interface WorkflowManageService {
*/
@Operation(summary = "添加关联文档")
@PutMapping(value = "/api/process/model/doc/create")
@Manageable
@InvokeMode(SYNC)
Boolean createDoc(@Validated @RequestBody DocCreateDTO dto);
@ -751,6 +773,7 @@ public interface WorkflowManageService {
*/
@Operation(summary = "修改关联文档")
@PostMapping(value = "/api/process/model/doc/update")
@Manageable
@InvokeMode(SYNC)
Boolean updateDoc(@Validated @RequestBody DocUpdateDTO dto);
@ -762,6 +785,7 @@ public interface WorkflowManageService {
*/
@Operation(summary = "克隆关联文档")
@PostMapping(value = "/api/process/model/doc/clone")
@Manageable
@InvokeMode(SYNC)
Boolean cloneDoc(@RequestParam("id") Long docId);
@ -772,6 +796,7 @@ public interface WorkflowManageService {
*/
@Operation(summary = "删除指定文档")
@DeleteMapping(value = "/api/process/model/doc/delete")
@Manageable
@InvokeMode(SYNC)
Boolean deleteDoc(@RequestParam("id") Long docId);
@ -783,6 +808,7 @@ public interface WorkflowManageService {
*/
@Operation(summary = "关联文档配置排序")
@PostMapping(value = "/api/process/model/doc/order")
@Manageable
@InvokeMode(SYNC)
Boolean orderDoc(@Validated @RequestBody DocOrderDTO dto);
@ -794,6 +820,7 @@ public interface WorkflowManageService {
*/
@Operation(summary = "重置关联文档配置")
@PostMapping(value = "/api/process/model/doc/reset")
@Manageable
@InvokeMode(SYNC)
Boolean resetDoc(@Validated @RequestBody DocResetDTO dto);
@ -805,6 +832,7 @@ public interface WorkflowManageService {
*/
@Operation(summary = "设置关联文档的停启用状态")
@PostMapping(value = "/api/process/model/doc/status")
@Manageable
@InvokeMode(SYNC)
Boolean statusDoc(@Validated @RequestBody DocStatusDTO dto);
@ -815,6 +843,7 @@ public interface WorkflowManageService {
*/
@Operation(summary = "设置关联文档的必选状态")
@PostMapping(value = "/api/process/model/doc/require")
@Manageable
@InvokeMode(SYNC)
Boolean requireDoc(@Validated @RequestBody DocStatusDTO dto);
@ -825,6 +854,7 @@ public interface WorkflowManageService {
*/
@Operation(summary = "特殊的查询设置过关联过文档的工作台 ID 集合")
@PostMapping(value = "/api/process/model/has/docs/tenantId")
@Manageable
@InvokeMode(SYNC)
List<Long> hasFilesTenantIds(@Validated @RequestBody DocTenantQueryDTO dto);
@ -853,17 +883,6 @@ public interface WorkflowManageService {
@InvokeMode(SYNC)
Void deleteVariables(@PathVariable("executionId") String executionId, @RequestParam String variableNames, @RequestParam(value = "scope", required = false) String scope);
/**
* 用于系统内部操作跳转到指定节点
*
* @param dto 请求参数
* @return 是否成功
*/
@Operation(summary = "系统操作回退任务到指定节点")
@PostMapping("/api/process/task/system/back")
@Manageable
Boolean systemBackTask(@Validated @RequestBody BpmnNodeBackSystemOperateDTO dto);
/**
* 添加附件
*

View File

@ -1,16 +1,16 @@
package cn.axzo.workflow.starter.mq.monitor;
import cn.axzo.workflow.common.model.dto.CommonDingTalkDTO;
import cn.axzo.workflow.starter.api.WorkflowCoreService;
import cn.axzo.workflow.starter.handler.monitor.BroadcastDLQReporter;
import com.dingtalk.api.DefaultDingTalkClient;
import com.dingtalk.api.DingTalkClient;
import com.dingtalk.api.request.OapiRobotSendRequest;
import com.dingtalk.api.response.OapiRobotSendResponse;
import lombok.SneakyThrows;
import org.apache.rocketmq.common.admin.TopicOffset;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.Environment;
import java.util.Objects;
/**
* Starter 内置的 Broadcast DLQ 钉钉通知
*
@ -20,13 +20,15 @@ import org.springframework.core.env.Environment;
public class AlertBroadcastDLQReporter implements BroadcastDLQReporter {
private static final Logger log = LoggerFactory.getLogger(AlertBroadcastDLQReporter.class);
private final String dingtalk_robot_webhook = "https://oapi.dingtalk.com/robot/send?access_token=341ee2907f3ebc15dc495fb7771a646230058710999fec7838066c109849878e";
// private final String dingtalk_robot_webhook = "https://oapi.dingtalk.com/robot/send?access_token=341ee2907f3ebc15dc495fb7771a646230058710999fec7838066c109849878e";
private final String profile;
private final String applicationName;
private final WorkflowCoreService workflowCoreService;
public AlertBroadcastDLQReporter(Environment environment) {
public AlertBroadcastDLQReporter(Environment environment, WorkflowCoreService workflowCoreService) {
this.profile = environment.getProperty("spring.profiles.active");
this.applicationName = environment.getProperty("spring.application.name");
this.workflowCoreService = workflowCoreService;
}
@Override
@ -40,22 +42,35 @@ public class AlertBroadcastDLQReporter implements BroadcastDLQReporter {
@SneakyThrows
public void sendDingTalk(Long count, String dlqName) {
DingTalkClient client = new DefaultDingTalkClient(dingtalk_robot_webhook);
OapiRobotSendRequest request = new OapiRobotSendRequest();
// DingTalkClient client = new DefaultDingTalkClient(dingtalk_robot_webhook);
// OapiRobotSendRequest request = new OapiRobotSendRequest();
//
// request.setMsgtype("markdown");
// OapiRobotSendRequest.Markdown markdown = new OapiRobotSendRequest.Markdown();
// markdown.setTitle("Notice Env: " + profile);
// markdown.setText("#### [" + profile + "]环境[" + applicationName + "]发现“DLQ”数据(Rpc)\n" +
// "> ###### 1. 如需查看详情,[请点击这里](" + findDomain() + "m) \n" +
// "> ###### 2. 如需调整 MQ DLQ 的监控状态: \n" +
// "> ###### 3. DLQ名称" + dlqName + " \n" +
// "[关闭监控](" + findDomain() + "m/set?status=false) \n | " +
// "[开启监控](" + findDomain() + "m/set?status=true) \n" +
// "> 积累条数:" + count + " \n");
// request.setMarkdown(markdown);
// log.warn(markdown.getText());
// OapiRobotSendResponse response = client.execute(request);
request.setMsgtype("markdown");
OapiRobotSendRequest.Markdown markdown = new OapiRobotSendRequest.Markdown();
markdown.setTitle("Notice Env: " + profile);
markdown.setText("#### [" + profile + "]环境[" + applicationName + "]发现“DLQ”数据(Rpc)\n" +
"> ###### 1. 如需查看详情,[请点击这里](" + findDomain() + "m) \n" +
"> ###### 2. 如需调整 MQ DLQ 的监控状态: \n" +
"> ###### 3. DLQ名称" + dlqName + " \n" +
"[关闭监控](" + findDomain() + "m/set?status=false) \n | " +
"[开启监控](" + findDomain() + "m/set?status=true) \n" +
"> 积累条数:" + count + " \n");
request.setMarkdown(markdown);
log.warn(markdown.getText());
OapiRobotSendResponse response = client.execute(request);
workflowCoreService.sendCommonDingtalk(CommonDingTalkDTO.builder()
.title("Notice Env: " + profile)
.context("#### [" + profile + "]环境[" + applicationName + "]发现“DLQ”数据(Rpc)\n" +
"> ###### 1. 如需查看详情,[请点击这里](" + findDomain() + "m) \n" +
"> ###### 2. 如需调整 MQ DLQ 的监控状态: \n" +
"> ###### 3. DLQ名称" + dlqName + " \n" +
"[关闭监控](" + findDomain() + "m/set?status=false) \n | " +
"[开启监控](" + findDomain() + "m/set?status=true) \n" +
"> 积累条数:" + count + " \n")
.targetIsMaster(Objects.equals(profile, "master"))
.build());
}
private String findDomain() {

View File

@ -1,16 +1,16 @@
package cn.axzo.workflow.starter.mq.monitor;
import cn.axzo.workflow.common.model.dto.CommonDingTalkDTO;
import cn.axzo.workflow.starter.api.WorkflowCoreService;
import cn.axzo.workflow.starter.handler.monitor.RpcDLQReporter;
import com.dingtalk.api.DefaultDingTalkClient;
import com.dingtalk.api.DingTalkClient;
import com.dingtalk.api.request.OapiRobotSendRequest;
import com.dingtalk.api.response.OapiRobotSendResponse;
import lombok.SneakyThrows;
import org.apache.rocketmq.common.admin.TopicOffset;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.Environment;
import java.util.Objects;
/**
* Starter 内置的 Rpc DLQ 钉钉通知
*
@ -20,13 +20,15 @@ import org.springframework.core.env.Environment;
public class AlertRcpDLQReporter implements RpcDLQReporter {
private static final Logger log = LoggerFactory.getLogger(AlertRcpDLQReporter.class);
private final String dingtalk_robot_webhook = "https://oapi.dingtalk.com/robot/send?access_token=341ee2907f3ebc15dc495fb7771a646230058710999fec7838066c109849878e";
// private final String dingtalk_robot_webhook = "https://oapi.dingtalk.com/robot/send?access_token=341ee2907f3ebc15dc495fb7771a646230058710999fec7838066c109849878e";
private final String profile;
private final String applicationName;
private final WorkflowCoreService workflowCoreService;
public AlertRcpDLQReporter(Environment environment) {
public AlertRcpDLQReporter(Environment environment, WorkflowCoreService workflowCoreService) {
this.profile = environment.getProperty("spring.profiles.active");
this.applicationName = environment.getProperty("spring.application.name");
this.workflowCoreService = workflowCoreService;
}
@Override
@ -40,22 +42,35 @@ public class AlertRcpDLQReporter implements RpcDLQReporter {
@SneakyThrows
public void sendDingTalk(Long count, String dlqName) {
DingTalkClient client = new DefaultDingTalkClient(dingtalk_robot_webhook);
OapiRobotSendRequest request = new OapiRobotSendRequest();
// DingTalkClient client = new DefaultDingTalkClient(dingtalk_robot_webhook);
// OapiRobotSendRequest request = new OapiRobotSendRequest();
//
// request.setMsgtype("markdown");
// OapiRobotSendRequest.Markdown markdown = new OapiRobotSendRequest.Markdown();
// markdown.setTitle("Notice Env: " + profile);
// markdown.setText("#### [" + profile + "]环境[" + applicationName + "]发现“DLQ”数据(Rpc)\n" +
// "> ###### 1. 如需查看详情,[请点击这里](" + findDomain() + "m) \n" +
// "> ###### 2. 如需调整 MQ DLQ 的监控状态: \n" +
// "> ###### 3. DLQ名称" + dlqName + " \n" +
// "[关闭监控](" + findDomain() + "m/set?status=false) \n | " +
// "[开启监控](" + findDomain() + "m/set?status=true) \n" +
// "> 积累条数:" + count + " \n");
// request.setMarkdown(markdown);
// log.warn(markdown.getText());
// OapiRobotSendResponse response = client.execute(request);
request.setMsgtype("markdown");
OapiRobotSendRequest.Markdown markdown = new OapiRobotSendRequest.Markdown();
markdown.setTitle("Notice Env: " + profile);
markdown.setText("#### [" + profile + "]环境[" + applicationName + "]发现“DLQ”数据(Rpc)\n" +
"> ###### 1. 如需查看详情,[请点击这里](" + findDomain() + "m) \n" +
"> ###### 2. 如需调整 MQ DLQ 的监控状态: \n" +
"> ###### 3. DLQ名称" + dlqName + " \n" +
"[关闭监控](" + findDomain() + "m/set?status=false) \n | " +
"[开启监控](" + findDomain() + "m/set?status=true) \n" +
"> 积累条数:" + count + " \n");
request.setMarkdown(markdown);
log.warn(markdown.getText());
OapiRobotSendResponse response = client.execute(request);
workflowCoreService.sendCommonDingtalk(CommonDingTalkDTO.builder()
.title("Notice Env: " + profile)
.context("#### [" + profile + "]环境[" + applicationName + "]发现“DLQ”数据(Rpc)\n" +
"> ###### 1. 如需查看详情,[请点击这里](" + findDomain() + "m) \n" +
"> ###### 2. 如需调整 MQ DLQ 的监控状态: \n" +
"> ###### 3. DLQ名称" + dlqName + " \n" +
"[关闭监控](" + findDomain() + "m/set?status=false) \n | " +
"[开启监控](" + findDomain() + "m/set?status=true) \n" +
"> 积累条数:" + count + " \n")
.targetIsMaster(Objects.equals(profile, "master"))
.build()
);
}
private String findDomain() {

View File

@ -1,6 +1,7 @@
package cn.axzo.workflow.starter.mq.monitor;
import cn.axzo.workflow.starter.WorkflowEngineStarterProperties;
import cn.axzo.workflow.starter.api.WorkflowCoreService;
import cn.axzo.workflow.starter.handler.monitor.BroadcastDLQReporter;
import cn.axzo.workflow.starter.handler.monitor.RpcDLQReporter;
import lombok.SneakyThrows;
@ -50,12 +51,13 @@ public class WorkflowEngineStarterDefaultMQMonitor implements SmartLifecycle {
ObjectProvider<BroadcastDLQReporter> broadcastDLQProcessorObjectProvider,
ObjectProvider<RpcDLQReporter> rpcDLQProcessorObjectProvider,
WorkflowEngineStarterProperties workflowEngineStarterProperties,
WorkflowCoreService workflowCoreService,
Environment environment) {
this.defaultMQAdminExt = mqAdminExtObjectProvider.getIfAvailable();
this.environment = environment;
if (workflowEngineStarterProperties.getAlert()) {
this.broadcastDLQProcessor = new AlertBroadcastDLQReporter(environment);
this.rpcDLQProcessor = new AlertRcpDLQReporter(environment);
this.broadcastDLQProcessor = new AlertBroadcastDLQReporter(environment, workflowCoreService);
this.rpcDLQProcessor = new AlertRcpDLQReporter(environment, workflowCoreService);
} else {
this.broadcastDLQProcessor = broadcastDLQProcessorObjectProvider.getIfAvailable(() -> new BroadcastDLQReporter() {
});

View File

@ -1,6 +1,7 @@
package cn.axzo.workflow.starter.mq.monitor.console;
import cn.axzo.workflow.starter.WorkflowEngineStarterProperties;
import cn.axzo.workflow.starter.api.WorkflowCoreService;
import cn.axzo.workflow.starter.handler.monitor.BroadcastDLQReporter;
import cn.axzo.workflow.starter.handler.monitor.RpcDLQReporter;
import cn.axzo.workflow.starter.mq.monitor.AlertBroadcastDLQReporter;
@ -59,6 +60,8 @@ public class WorkflowEngineStarterMQMonitorController {
private String serviceVersion;
public static String BROADCAST_CONSUMER_GROUP = "GID_%s_workflow_engine_%s_consumer";
public static String RPC_RETRY_CONSUMER_GROUP = "GID_%s_workflow_engine_starter_%s_consumer";
@Resource
private WorkflowCoreService workflowCoreService;
@GetMapping("/m")
public CommonResponse<Map<String, Object>> monitor() {
@ -137,8 +140,8 @@ public class WorkflowEngineStarterMQMonitorController {
return CommonResponse.success("未开启·死信队列·的监控,如需,请设置 workflow.engine.starter.enableDlqMonitor = true 后再重试!");
}
if (status) {
monitor.setBroadcastDLQProcessor(new AlertBroadcastDLQReporter(environment));
monitor.setRpcDLQProcessor(new AlertRcpDLQReporter(environment));
monitor.setBroadcastDLQProcessor(new AlertBroadcastDLQReporter(environment, workflowCoreService));
monitor.setRpcDLQProcessor(new AlertRcpDLQReporter(environment, workflowCoreService));
return CommonResponse.success("开启 DLQ 钉钉通知");
} else {
monitor.setBroadcastDLQProcessor(broadcastDLQProcessorObjectProvider.getIfAvailable(() -> new BroadcastDLQReporter() {