对接学校OA待办
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
package com.budwk.app.base.portal;
|
||||
|
||||
import com.budwk.app.base.portal.model.PortalTodoResponse;
|
||||
|
||||
/** 成功发送记录的持久化服务,不负责再次请求门户。 */
|
||||
public interface PortalTodoLogService {
|
||||
/**
|
||||
* 保存一次已确认成功的发送快照;使用当前业务事务,外层回滚时记录也可能回滚。
|
||||
* @param requestId 本次调用的32位关联编号,对应日志主键
|
||||
* @param requestUrl 实际发送接口地址,不传认证头
|
||||
* @param requestBody 实际发送的完整JSON字符串
|
||||
* @param responseBody 门户原始响应正文
|
||||
* @param response 解析后的成功结果,含HTTP状态、业务码、提示、data及traceId
|
||||
* @param requestTime 请求开始的毫秒时间戳
|
||||
* @param elapsedMillis 接口耗时,不含数据库保存时间
|
||||
* @return 无返回值;保存异常由调用端单独处理,不改变门户调用结果
|
||||
*/
|
||||
void recordSuccess(String requestId, String requestUrl, String requestBody, String responseBody,
|
||||
PortalTodoResponse response, long requestTime, long elapsedMillis);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.budwk.app.base.portal;
|
||||
|
||||
import com.budwk.app.base.portal.model.PortalTodoRequest;
|
||||
import com.budwk.app.base.portal.model.PortalTodoSendRequest;
|
||||
import com.budwk.app.base.portal.model.PortalTodoResponse;
|
||||
|
||||
/** 信息门户待办接口,可在业务 service 中注入后独立调用。 */
|
||||
public interface PortalTodoService {
|
||||
/**
|
||||
* @param request 业务名称、唯一 ID、单据编码、接收人登录账号及标题、正文、完整 PC 链接
|
||||
* @return 调用结果;success 表示门户成功,skipped 表示开关拦截,msg/traceId 用于排查
|
||||
*/
|
||||
PortalTodoResponse sendTodo(PortalTodoSendRequest request);
|
||||
|
||||
/**
|
||||
* @param request 与发送时完全一致的业务名称、唯一 ID、单据编码和接收人账号
|
||||
* @return 调用结果;审核完成使用此方法,门户保留已办记录
|
||||
*/
|
||||
PortalTodoResponse doneTodo(PortalTodoRequest request);
|
||||
|
||||
/**
|
||||
* @param request 与发送时完全一致的业务名称、唯一 ID、单据编码和接收人账号
|
||||
* @return 调用结果;撤销或删除业务时使用,失败不会抛出网络异常阻断业务
|
||||
*/
|
||||
PortalTodoResponse removeTodo(PortalTodoRequest request);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
# 门户待办调用
|
||||
|
||||
在业务 service 中注入 `PortalTodoService`。三个方法均返回 `PortalTodoResponse`,不会因门户 HTTP 失败主动抛出异常。
|
||||
|
||||
```java
|
||||
@Inject
|
||||
private PortalTodoService portalTodoService;
|
||||
|
||||
// 同一待办发送、完成、删除时保留下面四项定位数据。
|
||||
PortalTodoSendRequest request = new PortalTodoSendRequest();
|
||||
request.setEntityName("职工福利审核");
|
||||
request.setEntityId(taskId);
|
||||
request.setEntityNumber(businessNumber);
|
||||
request.setTargets(List.of(receiverLoginName));
|
||||
request.setSubject("职工福利审核待办");
|
||||
request.setContent("您有一条职工福利申请需要审核。");
|
||||
request.setLink(auditPageUrl);
|
||||
request.setMobileLink(h5AuditPageUrl);
|
||||
request.setCreator(senderLoginName);
|
||||
request.setCreatorName(senderName);
|
||||
PortalTodoResponse result = portalTodoService.sendTodo(request);
|
||||
```
|
||||
|
||||
审核完成时调用 `portalTodoService.doneTodo(request)`;撤销或删除时调用 `portalTodoService.removeTodo(request)`。这两个操作也可单独构造 `PortalTodoRequest`,只设置四项定位数据,不需要标题和内容。
|
||||
|
||||
- `success=true`:门户接口返回成功,实际展示仍以门户为准。
|
||||
- `skipped=true`:开发模式或开关关闭,没有发送请求。
|
||||
- 两者均为 `false`:校验、网络或门户业务失败,使用 `msg`、`code`、`traceId` 排查。
|
||||
- `data` 按门户原值返回,不当作 `notifyId` 使用。
|
||||
|
||||
## 启用配置
|
||||
|
||||
1. 部署配置设置 `portal.todo.base-url`、`portal.todo.username`、`portal.todo.password`。密码按原始字符填写,不编码、不转义增删。
|
||||
2. 按当前项目约定,`Globals.sso` 必须为 `true`(来自 `cas.enable`)。
|
||||
3. 在现有系统参数管理中配置 `AppPortalTodo=true` 并刷新系统配置;未配置默认关闭,与 `AppSms` 无关。
|
||||
4. 设置正确的项目 `AppDomain`,供 wf 构建审核链接。
|
||||
|
||||
wf 会自动同步通用任务变化,业务代码无需再次发送同一 wf 待办。HTTP 超时不自动重试;本地事务无法撤销已成功的外部调用,需要根据日志人工补偿。
|
||||
|
||||
PC链接使用任务的 `formKey`,手机链接使用 `h5FormKey`,补齐 `AppDomain` 并携带编码后的 `taskId`、`bizId`、`taskKey`。未配置手机表单时记录提示并回退到 `/platform/h5` 手机首页。独立调用的 `mobileLink` 应传完整HTTP(S)地址,未传也回退到项目手机首页。修改只影响新发送的待办,不更新门户已有链接。
|
||||
|
||||
## 发送记录与控制台排查
|
||||
|
||||
成功的 `send` 请求写入 `portal_todo_log`,每次请求一条,保存实际发送的完整JSON、原始响应正文、业务标识、标题正文、发送人、接收人账号数组、链接、请求时间、耗时及返回状态。失败、跳过、已办和删除不写成功发送记录。
|
||||
|
||||
控制台搜索 `——————————————学校OA待办发送`,按“调用编号”关联“请求开始”和“请求结束”。结束日志包含成功/失败/跳过、HTTP状态、业务码、提示、traceId、耗时和响应正文;无响应的网络异常仍输出结束记录。日志不打印Basic凭证或认证头。
|
||||
|
||||
成功发送但保存记录失败时另输出“发送成功但记录入库失败”,门户返回结果保持成功,不自动重发。保存沿用业务事务,外层回滚时记录也可能回滚;控制台“已写入当前事务”不表示外层事务已提交。
|
||||
|
||||
表结构通过现有实体扫描机制管理:`MainLauncher.init_sys` 仅在DEBUG日志级别时自动建表。生产部署需确认该表已创建,本功能不生成或执行额外SQL,不提供后台查询页面。
|
||||
|
||||
## 验证说明
|
||||
|
||||
`PortalTodoServiceTest` 使用本地模拟 HTTP 服务;`FlowPortalTodoServiceTest` 使用内存 Dao 替身。两者不连接真实门户或业务数据库。
|
||||
|
||||
串并行会签、转办、撤回和流程删除仍需在测试部署中走完整业务流程验收。提案附议等脱离标准任务状态的业务特例未单独适配。
|
||||
|
||||
现有 `activate` 方法只恢复 `INTERRUPT` 状态任务,并不恢复 `PENDING`;本次保持该业务规则,仅为实际恢复的任务重发待办。
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.budwk.app.base.portal.impl;
|
||||
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.budwk.app.base.portal.PortalTodoLogService;
|
||||
import com.budwk.app.base.portal.model.PortalTodoLog;
|
||||
import com.budwk.app.base.portal.model.PortalTodoResponse;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
|
||||
/** 记录实际请求报文的业务快照,不读取可变化的人员或流程数据。 */
|
||||
@IocBean
|
||||
public class PortalTodoLogServiceImpl implements PortalTodoLogService {
|
||||
@Inject
|
||||
private Dao dao;
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void recordSuccess(String requestId, String requestUrl, String requestBody, String responseBody,
|
||||
PortalTodoResponse response, long requestTime, long elapsedMillis) {
|
||||
// 防止跳过或HTTP失败被误记为成功发送;多接收人作为一次请求保留原数组。
|
||||
if (response == null || !response.isSuccess() || response.isSkipped()
|
||||
|| response.getHttpStatus() == null || response.getHttpStatus() < 200
|
||||
|| response.getHttpStatus() >= 300) return;
|
||||
JSONObject payload = JSONUtil.parseObj(requestBody);
|
||||
PortalTodoLog record = new PortalTodoLog();
|
||||
record.setId(requestId);
|
||||
record.setAppName(payload.getStr("appName"));
|
||||
record.setModuleName(payload.getStr("moduleName"));
|
||||
record.setEntityName(payload.getStr("entityName"));
|
||||
record.setEntityId(payload.getStr("entityId"));
|
||||
record.setEntityNumber(payload.getStr("entityNumber"));
|
||||
record.setSubject(payload.getStr("subject"));
|
||||
record.setContent(payload.getStr("content"));
|
||||
record.setCreator(payload.getStr("creator"));
|
||||
record.setCreatorName(payload.getStr("creatorName"));
|
||||
record.setTargets(JSONUtil.toJsonStr(payload.getJSONArray("targets")));
|
||||
record.setLink(payload.getStr("link"));
|
||||
record.setMobileLink(payload.getStr("mobileLink"));
|
||||
record.setRequestUrl(requestUrl);
|
||||
record.setRequestBody(requestBody);
|
||||
record.setResponseBody(responseBody);
|
||||
record.setRequestTime(requestTime);
|
||||
record.setElapsedMillis(elapsedMillis);
|
||||
record.setHttpStatus(response.getHttpStatus());
|
||||
record.setSuccess(response.isSuccess());
|
||||
record.setCode(response.getCode());
|
||||
record.setMsg(response.getMsg());
|
||||
record.setData(response.getData());
|
||||
record.setTraceId(response.getTraceId());
|
||||
dao.insert(record);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
package com.budwk.app.base.portal.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.budwk.app.base.portal.PortalTodoService;
|
||||
import com.budwk.app.base.portal.PortalTodoLogService;
|
||||
import com.budwk.app.base.portal.model.*;
|
||||
import com.budwk.app.web.commons.base.Globals;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.impl.PropertiesProxy;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/** Basic 认证的门户 REST 客户端,所有操作共用环境拦截及结果处理。 */
|
||||
@IocBean
|
||||
@Slf4j
|
||||
public class PortalTodoServiceImpl implements PortalTodoService {
|
||||
private static final String LOG_PREFIX = "——————————————学校OA待办发送";
|
||||
|
||||
@Inject
|
||||
private PropertiesProxy conf;
|
||||
|
||||
@Inject
|
||||
private PortalTodoLogService portalTodoLogService;
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public PortalTodoResponse sendTodo(PortalTodoSendRequest request) {
|
||||
return post("send", request);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public PortalTodoResponse doneTodo(PortalTodoRequest request) {
|
||||
return post("done", request);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public PortalTodoResponse removeTodo(PortalTodoRequest request) {
|
||||
return post("removeTodo", request);
|
||||
}
|
||||
|
||||
/** 先拦截环境与开关,再校验参数并请求;禁止自动重试不确定是否已送达的发送。 */
|
||||
private PortalTodoResponse post(String operation, PortalTodoRequest request) {
|
||||
PortalTodoResponse result = new PortalTodoResponse();
|
||||
String requestId = UUID.randomUUID().toString().replace("-", "");
|
||||
long requestTime = System.currentTimeMillis();
|
||||
long startedNanos = System.nanoTime();
|
||||
String requestUrl = null;
|
||||
String requestBody = null;
|
||||
String responseBody = null;
|
||||
String exceptionType = null;
|
||||
HttpURLConnection connection = null;
|
||||
try {
|
||||
// 环境与功能开关均开启才调用门户,拦截早于参数及认证配置校验。
|
||||
if (!Globals.sso || !Globals.MyConfig.getBoolean("AppPortalTodo", false)) {
|
||||
result.setSkipped(true);
|
||||
result.setCode("SKIPPED");
|
||||
result.setMsg(!Globals.sso ? "开发模式不调用门户待办" : "门户待办开关未开启");
|
||||
return result;
|
||||
}
|
||||
JSONObject payload = payload(operation, request);
|
||||
requestBody = payload.toString();
|
||||
String baseUrl = required(conf.get("portal.todo.base-url"), "门户地址未配置");
|
||||
String username = required(conf.get("portal.todo.username"), "门户认证用户名未配置");
|
||||
String password = required(conf.get("portal.todo.password"), "门户认证密码未配置");
|
||||
requestUrl = StrUtil.removeSuffix(baseUrl, "/")
|
||||
+ "/openapi/sys-notifybus/sysNotifyComponent/" + operation;
|
||||
// 打印实际发送的同一份JSON,不输出Basic用户名、密码或Authorization头。
|
||||
log.info("{}【请求开始】调用编号={},操作={},地址={},发送内容={}",
|
||||
LOG_PREFIX, requestId, operation, requestUrl, requestBody);
|
||||
connection = (HttpURLConnection) URI.create(requestUrl).toURL().openConnection();
|
||||
connection.setInstanceFollowRedirects(false);
|
||||
connection.setRequestMethod("POST");
|
||||
connection.setDoOutput(true);
|
||||
connection.setUseCaches(false);
|
||||
connection.setConnectTimeout(conf.getInt("portal.todo.connect-timeout", 5000));
|
||||
connection.setReadTimeout(conf.getInt("portal.todo.read-timeout", 10000));
|
||||
connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
|
||||
connection.setRequestProperty("Authorization", "Basic " + Base64.getEncoder()
|
||||
.encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8)));
|
||||
try (OutputStream out = connection.getOutputStream()) {
|
||||
out.write(requestBody.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
int status = connection.getResponseCode();
|
||||
result.setHttpStatus(status);
|
||||
try (InputStream in = status >= 400 ? connection.getErrorStream() : connection.getInputStream()) {
|
||||
// 先保留原始响应,非JSON、解析失败等情况也能在控制台排查。
|
||||
responseBody = in == null ? "" : new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||
JSONObject json = JSONUtil.parseObj(responseBody);
|
||||
result.setCode(json.getStr("code"));
|
||||
result.setMsg(json.getStr("msg"));
|
||||
result.setData(json.getStr("data"));
|
||||
result.setTraceId(json.getStr("traceId"));
|
||||
result.setSuccess(status >= 200 && status < 300 && Boolean.TRUE.equals(json.getBool("success")));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
result.setSuccess(false);
|
||||
result.setCode("CLIENT_ERROR");
|
||||
exceptionType = e.getClass().getSimpleName();
|
||||
// 异常仅记录类型,不打印可能携带认证信息的异常原文。
|
||||
result.setMsg(e instanceof IllegalArgumentException ? "门户参数、配置或响应格式无效" : "门户请求失败或超时");
|
||||
} finally {
|
||||
long elapsedMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedNanos);
|
||||
// 将多行响应编码为单行JSON字符串,确保每条日志均可用统一前缀定位。
|
||||
String outcome = result.isSkipped() ? "已跳过" : result.isSuccess() ? "成功" : "失败";
|
||||
String format = "{}【请求结束-{}】调用编号={},操作={},业务ID={},HTTP状态={},耗时={}ms,code={},msg={},traceId={},异常类型={},返回内容={}";
|
||||
Object[] values = {LOG_PREFIX, outcome, requestId, operation,
|
||||
request == null ? null : request.getEntityId(), result.getHttpStatus(), elapsedMillis,
|
||||
result.getCode(), JSONUtil.toJsonStr(result.getMsg()), result.getTraceId(), exceptionType,
|
||||
JSONUtil.toJsonStr(responseBody)};
|
||||
if (result.isSuccess() || result.isSkipped()) log.info(format, values);
|
||||
else log.error(format, values);
|
||||
if (connection != null) connection.disconnect();
|
||||
|
||||
// 仅send成功落库。日志保存异常不能改写门户成功结果,也不能触发再次发送。
|
||||
if ("send".equals(operation) && result.isSuccess() && !result.isSkipped()) {
|
||||
try {
|
||||
portalTodoLogService.recordSuccess(requestId, requestUrl, requestBody, responseBody,
|
||||
result, requestTime, elapsedMillis);
|
||||
log.info("{}【记录已写入当前事务】调用编号={},traceId={}", LOG_PREFIX, requestId, result.getTraceId());
|
||||
} catch (Exception e) {
|
||||
log.error("{}【发送成功但记录入库失败】调用编号={},业务ID={},traceId={},异常类型={}",
|
||||
LOG_PREFIX, requestId, request.getEntityId(), result.getTraceId(), e.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 只发送接口使用的字段;删除和已办不得混入标题、正文等发送参数。 */
|
||||
private JSONObject payload(String operation, PortalTodoRequest request) {
|
||||
if (request == null) throw new IllegalArgumentException("请求不能为空");
|
||||
if (request.getTargets() == null || request.getTargets().isEmpty()
|
||||
|| request.getTargets().stream().anyMatch(StrUtil::isBlank)) {
|
||||
throw new IllegalArgumentException("接收人账号不能为空");
|
||||
}
|
||||
JSONObject json = new JSONObject();
|
||||
json.set("appName", "智慧工会平台");
|
||||
json.set("moduleName", "智慧工会平台");
|
||||
json.set("entityName", required(request.getEntityName(), "业务名称不能为空"));
|
||||
json.set("entityId", required(request.getEntityId(), "业务ID不能为空"));
|
||||
json.set("entityNumber", required(request.getEntityNumber(), "单据编码不能为空"));
|
||||
json.set("targets", request.getTargets().stream().distinct().toList());
|
||||
json.set("notifyType", "todo");
|
||||
json.set("orgProperty", "fdLoginName");
|
||||
json.set("todoType", 1);
|
||||
json.set("async", false);
|
||||
if ("send".equals(operation)) {
|
||||
PortalTodoSendRequest send = (PortalTodoSendRequest) request;
|
||||
json.set("subject", required(send.getSubject(), "标题不能为空"));
|
||||
json.set("content", required(send.getContent(), "正文不能为空"));
|
||||
String link = required(send.getLink(), "PC链接不能为空");
|
||||
URI uri = URI.create(link);
|
||||
if (!("http".equalsIgnoreCase(uri.getScheme()) || "https".equalsIgnoreCase(uri.getScheme()))
|
||||
|| uri.getHost() == null) throw new IllegalArgumentException("PC链接必须为完整HTTP地址");
|
||||
json.set("link", link);
|
||||
// 独立调用可传完整H5地址;未提供时使用手机首页,避免门户打开空白页。
|
||||
String mobileLink = send.getMobileLink();
|
||||
if (StrUtil.isBlank(mobileLink)) {
|
||||
mobileLink = URI.create(StrUtil.appendIfMissing(Globals.AppDomain, "/"))
|
||||
.resolve("/platform/h5").toString();
|
||||
}
|
||||
URI mobileUri = URI.create(mobileLink);
|
||||
if (!("http".equalsIgnoreCase(mobileUri.getScheme()) || "https".equalsIgnoreCase(mobileUri.getScheme()))
|
||||
|| mobileUri.getHost() == null) throw new IllegalArgumentException("手机链接必须为完整HTTP地址");
|
||||
json.set("mobileLink", mobileLink);
|
||||
if (StrUtil.isNotBlank(send.getCreator())) json.set("creator", send.getCreator());
|
||||
if (StrUtil.isNotBlank(send.getCreatorName())) json.set("creatorName", send.getCreatorName());
|
||||
json.set("queryType", 1);
|
||||
json.set("todoLevel", 3);
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
private String required(String value, String message) {
|
||||
if (StrUtil.isBlank(value)) throw new IllegalArgumentException(message);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.budwk.app.base.portal.model;
|
||||
|
||||
import com.budwk.app.base.model.BaseModel;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.nutz.dao.entity.annotation.*;
|
||||
|
||||
/** 每次发送成功的请求保存一条快照,多个接收人仍归属于同一次请求。 */
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Table("portal_todo_log")
|
||||
@TableMeta("{'mysql-charset':'utf8mb4'}")
|
||||
@Comment("学校OA待办成功发送记录")
|
||||
public class PortalTodoLog extends BaseModel {
|
||||
@Name
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
@Comment("请求关联编号,与控制台调用编号一致")
|
||||
private String id;
|
||||
|
||||
@Column
|
||||
@Comment("来源系统")
|
||||
private String appName;
|
||||
@Column
|
||||
@Comment("来源模块")
|
||||
private String moduleName;
|
||||
@Column
|
||||
@Comment("来源业务名称")
|
||||
private String entityName;
|
||||
@Column
|
||||
@Comment("业务记录或wf任务ID")
|
||||
private String entityId;
|
||||
@Column
|
||||
@Comment("业务单据编码")
|
||||
private String entityNumber;
|
||||
@Column
|
||||
@ColDefine(type = ColType.TEXT)
|
||||
@Comment("待办标题")
|
||||
private String subject;
|
||||
@Column
|
||||
@ColDefine(customType = "MEDIUMTEXT")
|
||||
@Comment("待办正文")
|
||||
private String content;
|
||||
@Column
|
||||
@Comment("发送人登录账号")
|
||||
private String creator;
|
||||
@Column
|
||||
@Comment("发送人姓名")
|
||||
private String creatorName;
|
||||
@Column
|
||||
@ColDefine(customType = "MEDIUMTEXT")
|
||||
@Comment("接收人登录账号JSON数组,保留工号前导零")
|
||||
private String targets;
|
||||
@Column
|
||||
@ColDefine(type = ColType.TEXT)
|
||||
@Comment("PC跳转地址")
|
||||
private String link;
|
||||
@Column
|
||||
@ColDefine(type = ColType.TEXT)
|
||||
@Comment("移动端跳转地址")
|
||||
private String mobileLink;
|
||||
@Column
|
||||
@ColDefine(type = ColType.TEXT)
|
||||
@Comment("发送接口地址,不含认证信息")
|
||||
private String requestUrl;
|
||||
@Column
|
||||
@ColDefine(customType = "MEDIUMTEXT")
|
||||
@Comment("实际发送的完整JSON报文")
|
||||
private String requestBody;
|
||||
@Column
|
||||
@ColDefine(customType = "MEDIUMTEXT")
|
||||
@Comment("门户完整响应正文")
|
||||
private String responseBody;
|
||||
@Column
|
||||
@Comment("请求开始时间,毫秒时间戳")
|
||||
private Long requestTime;
|
||||
@Column
|
||||
@Comment("接口调用耗时,毫秒,不含日志入库")
|
||||
private Long elapsedMillis;
|
||||
@Column
|
||||
@Comment("HTTP响应状态码")
|
||||
private Integer httpStatus;
|
||||
@Column
|
||||
@Comment("门户是否返回成功,不代表接收人已查看")
|
||||
private Boolean success;
|
||||
@Column
|
||||
@Comment("门户业务状态码")
|
||||
private String code;
|
||||
@Column
|
||||
@ColDefine(type = ColType.TEXT)
|
||||
@Comment("门户返回提示")
|
||||
private String msg;
|
||||
@Column
|
||||
@ColDefine(customType = "MEDIUMTEXT")
|
||||
@Comment("门户原样返回的数据,不推断为notifyId")
|
||||
private String data;
|
||||
@Column
|
||||
@Comment("门户链路追踪编号")
|
||||
private String traceId;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.budwk.app.base.portal.model;
|
||||
|
||||
import lombok.Data;
|
||||
import java.util.List;
|
||||
|
||||
/** 门户待办定位参数;发送、已办和删除时必须使用同一组业务标识及人员账号。 */
|
||||
@Data
|
||||
public class PortalTodoRequest {
|
||||
/** 来源业务名称,同一待办生命周期内不得更改。 */
|
||||
private String entityName;
|
||||
/** 业务记录或任务的唯一 ID,不能仅使用可重复的节点名称。 */
|
||||
private String entityId;
|
||||
/** 所属业务单据编码。 */
|
||||
private String entityNumber;
|
||||
/** 门户登录账号数组,保留工号前导零,不传本系统人员 ID。 */
|
||||
private List<String> targets;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.budwk.app.base.portal.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/** 门户调用结果;跳过不表示发送成功,网络异常时 httpStatus 为空。 */
|
||||
@Data
|
||||
public class PortalTodoResponse {
|
||||
private boolean success;
|
||||
private boolean skipped;
|
||||
private Integer httpStatus;
|
||||
/** 门户业务错误码,或本地 SKIPPED / CLIENT_ERROR。 */
|
||||
private String code;
|
||||
private String msg;
|
||||
/** 门户原样返回的数据,不推断为 notifyId。 */
|
||||
private String data;
|
||||
/** 提供给门户管理员排查的链路标识。 */
|
||||
private String traceId;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.budwk.app.base.portal.model;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/** 发送待办参数;继承的定位参数需由业务方保留,供后续已办、删除使用。 */
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class PortalTodoSendRequest extends PortalTodoRequest {
|
||||
/** 待办标题。 */
|
||||
private String subject;
|
||||
/** 待办正文。 */
|
||||
private String content;
|
||||
/** 发送人门户登录账号,可不填。 */
|
||||
private String creator;
|
||||
/** 发送人姓名,可不填。 */
|
||||
private String creatorName;
|
||||
/** 可访问的 PC 业务页面完整地址,必填。 */
|
||||
private String link;
|
||||
/** 可访问的H5业务页面完整HTTP(S)地址;未传时回退到项目手机首页。 */
|
||||
private String mobileLink;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.budwk.app.flow.listenter;
|
||||
|
||||
import com.budwk.app.flow.engine.event.ProcessEvent;
|
||||
import com.budwk.app.flow.engine.event.ProcessEventListener;
|
||||
import com.budwk.app.flow.entity.ProcessTask;
|
||||
import com.budwk.app.flow.enums.ProcessEventTypeEnum;
|
||||
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
|
||||
import com.budwk.app.flow.service.FlowPortalTodoService;
|
||||
import com.budwk.app.web.commons.base.Globals;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
|
||||
/** 门户专用监听器;发送从实际参与人新增入口触发,不重复消费任务开始事件。 */
|
||||
@IocBean
|
||||
@Slf4j
|
||||
public class ProcessPortalTodoEventListener implements ProcessEventListener {
|
||||
private static final String LOG_PREFIX = "——————————————学校OA待办发送";
|
||||
|
||||
@Inject
|
||||
private Dao dao;
|
||||
@Inject
|
||||
private FlowPortalTodoService flowPortalTodoService;
|
||||
|
||||
/** @param event 任务结束/撤销或实例结束事件;按真实状态区分已办和移除,异常不阻断流程。 */
|
||||
@Override
|
||||
public void onEvent(ProcessEvent event) {
|
||||
log.info("{}【WF事件入口】事件={},来源ID={}", LOG_PREFIX, event.getEventType(), event.getSourceId());
|
||||
// 未启用门户同步时停止消费事件,保留日志便于定位跳过原因。
|
||||
if (!Globals.sso || !Globals.MyConfig.getBoolean("AppPortalTodo", false)) {
|
||||
log.info("{}【WF事件跳过】事件={},来源ID={},原因={},sso={},AppPortalTodo={}",
|
||||
LOG_PREFIX, event.getEventType(), event.getSourceId(),
|
||||
!Globals.sso ? "开发模式" : "门户待办开关未开启", Globals.sso,
|
||||
Globals.MyConfig.getBoolean("AppPortalTodo", false));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (event.getEventType() == ProcessEventTypeEnum.PROCESS_TASK_END) {
|
||||
ProcessTask task = dao.fetch(ProcessTask.class, event.getSourceId());
|
||||
if (task == null) {
|
||||
log.warn("{}【WF事件跳过】来源ID={},原因=任务不存在", LOG_PREFIX, event.getSourceId());
|
||||
return;
|
||||
}
|
||||
if (ProcessTaskStateEnum.FINISHED.getCode().equals(task.getTaskState())) {
|
||||
flowPortalTodoService.doneTask(task.getId());
|
||||
} else if (ProcessTaskStateEnum.ABANDON.getCode().equals(task.getTaskState())) {
|
||||
flowPortalTodoService.removeTask(task.getId(), null);
|
||||
} else {
|
||||
log.info("{}【WF事件跳过】任务ID={},原因=结束事件的任务状态无需同步,任务状态={}",
|
||||
LOG_PREFIX, task.getId(), task.getTaskState());
|
||||
}
|
||||
} else if (event.getEventType() == ProcessEventTypeEnum.PROCESS_TASK_REVOKE) {
|
||||
flowPortalTodoService.removeTask(event.getSourceId(), null);
|
||||
} else if (event.getEventType() == ProcessEventTypeEnum.PROCESS_INSTANCE_END) {
|
||||
flowPortalTodoService.removeInstance(event.getSourceId());
|
||||
} else {
|
||||
// 开始事件不再次发送,发送已由新增参与人入口负责。
|
||||
log.info("{}【WF事件跳过】事件={},来源ID={},原因=该事件无需本监听器同步,发送由新增参与人入口负责",
|
||||
LOG_PREFIX, event.getEventType(), event.getSourceId());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("{}【WF事件同步失败】来源ID={},异常类型={}", LOG_PREFIX, event.getSourceId(), e.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,9 @@ public class FlowCommonService {
|
||||
@Inject
|
||||
private Dao dao;
|
||||
|
||||
@Inject
|
||||
private FlowPortalTodoService flowPortalTodoService;
|
||||
|
||||
/**
|
||||
* 执行任务
|
||||
* @param args
|
||||
@@ -111,9 +114,12 @@ public class FlowCommonService {
|
||||
|
||||
// 撤销任务
|
||||
List<ProcessTask> taskList = dao.query(ProcessTask.class, Cnd.where(ProcessTask::getTaskParentId, "=", taskId));
|
||||
// 收集全部被撤回的任务,包含并行会签的非直接子任务,避免门户残留待办。
|
||||
java.util.Set<Long> revokedTaskIds = new java.util.LinkedHashSet<>();
|
||||
for (ProcessTask task : taskList) {
|
||||
task.setTaskState(ProcessTaskStateEnum.WITHDRAW.getCode());
|
||||
dao.update(task);
|
||||
revokedTaskIds.add(task.getId());
|
||||
}
|
||||
// 会签并行任务 撤销后续任务
|
||||
if (selfTask.getPerformType().equals(ProcessTaskPerformTypeEnum.COUNTERSIGN.getCode())) {
|
||||
@@ -122,6 +128,7 @@ public class FlowCommonService {
|
||||
for (ProcessTask doingTask : doingTasks) {
|
||||
doingTask.setTaskState(ProcessTaskStateEnum.WITHDRAW.getCode());
|
||||
dao.update(doingTask);
|
||||
revokedTaskIds.add(doingTask.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +143,12 @@ public class FlowCommonService {
|
||||
ProcessPublisher.notify(ProcessEvent.builder().eventType(ProcessEventTypeEnum.PROCESS_TASK_REVOKE).sourceId(task.getId()).build());
|
||||
}
|
||||
|
||||
// 直接子任务由撤回事件同步;额外并行任务在此补齐,不重复删除同一任务。
|
||||
taskList.forEach(task -> revokedTaskIds.remove(task.getId()));
|
||||
revokedTaskIds.forEach(id -> flowPortalTodoService.removeTask(id, null));
|
||||
// 原任务重新进入办理状态,需要恢复它在门户中的待办。
|
||||
flowPortalTodoService.sendTask(selfTask.getId(), null);
|
||||
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
package com.budwk.app.flow.service;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.budwk.app.base.portal.PortalTodoService;
|
||||
import com.budwk.app.base.portal.model.PortalTodoSendRequest;
|
||||
import com.budwk.app.flow.constant.FlowConst;
|
||||
import com.budwk.app.flow.entity.ProcessInstance;
|
||||
import com.budwk.app.flow.entity.ProcessTask;
|
||||
import com.budwk.app.flow.entity.ProcessTaskActor;
|
||||
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
|
||||
import com.budwk.app.sys.views.View_user;
|
||||
import com.budwk.app.web.commons.base.Globals;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* wf 门户适配,所有标识由任务持久化数据重建,不依赖当前登录请求。
|
||||
*/
|
||||
@IocBean
|
||||
@Slf4j
|
||||
public class FlowPortalTodoService {
|
||||
private static final String LOG_PREFIX = "——————————————学校OA待办发送";
|
||||
|
||||
@Inject
|
||||
private Dao dao;
|
||||
@Inject
|
||||
private PortalTodoService portalTodoService;
|
||||
|
||||
/**
|
||||
* @param taskId wf 任务 ID
|
||||
* @param actors 本次新增的参与人,null 表示当前任务全部参与人;空列表不发送
|
||||
* @return 无返回值;门户异常只记录,不中断工作流
|
||||
*/
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void sendTask(Long taskId, List<ProcessTaskActor> actors) {
|
||||
sync(taskId, actors, "send");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param taskId 已完成的 wf 任务 ID;参与人账号沿用原任务,异常只记录。
|
||||
*/
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void doneTask(Long taskId) {
|
||||
sync(taskId, null, "done");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param taskId 原 wf 任务 ID,必须在删除任务记录之前调用
|
||||
* @param actors 被移除的参与人快照;null 表示原任务全部参与人
|
||||
* @return 无返回值;不改变 wf 数据,门户失败仅记录
|
||||
*/
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void removeTask(Long taskId, List<ProcessTaskActor> actors) {
|
||||
sync(taskId, actors, "removeTodo");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param instanceId wf 实例 ID;实例结束或删除前清理非已完成任务的门户待办。
|
||||
*/
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void removeInstance(Long instanceId) {
|
||||
log.info("{}【WF实例清理入口】实例ID={}", LOG_PREFIX, instanceId);
|
||||
if (!enabled()) {
|
||||
log.info("{}【WF跳过】实例ID={},原因={},sso={},AppPortalTodo={}", LOG_PREFIX, instanceId,
|
||||
!Globals.sso ? "开发模式" : "门户待办开关未开启", Globals.sso,
|
||||
Globals.MyConfig.getBoolean("AppPortalTodo", false));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
List<ProcessTask> tasks = dao.query(ProcessTask.class,
|
||||
Cnd.where(ProcessTask::getProcessInstanceId, "=", instanceId)
|
||||
.and(ProcessTask::getTaskState, "!=", ProcessTaskStateEnum.FINISHED.getCode()));
|
||||
if (tasks.isEmpty()) {
|
||||
log.info("{}【WF跳过】实例ID={},原因=没有需要清理的任务", LOG_PREFIX, instanceId);
|
||||
}
|
||||
for (ProcessTask task : tasks) sync(task.getId(), null, "removeTodo");
|
||||
} catch (Exception e) {
|
||||
log.warn("{}【WF实例清理失败】实例ID={},异常类型={}", LOG_PREFIX, instanceId, e.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean enabled() {
|
||||
// WF同步与独立门户调用使用相同的环境及功能开关。
|
||||
return Globals.sso && Globals.MyConfig.getBoolean("AppPortalTodo", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 包括数据读取和参数组装在内的异常均隔离,避免影响已授权的 wf 业务操作。
|
||||
*/
|
||||
private void sync(Long taskId, List<ProcessTaskActor> actors, String operation) {
|
||||
// 在开关判断之前记录入口,使未进入HTTP客户端的调用也能明确定位跳过原因。
|
||||
log.info("{}【WF同步入口】任务ID={},操作={}", LOG_PREFIX, taskId, operation);
|
||||
if (!enabled()) {
|
||||
log.info("{}【WF跳过】任务ID={},操作={},原因={},sso={},AppPortalTodo={}",
|
||||
LOG_PREFIX, taskId, operation, !Globals.sso ? "开发模式" : "门户待办开关未开启",
|
||||
Globals.sso, Globals.MyConfig.getBoolean("AppPortalTodo", false));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ProcessTask task = dao.fetch(ProcessTask.class, taskId);
|
||||
if (task == null) {
|
||||
log.warn("{}【WF跳过】任务ID={},操作={},原因=任务不存在", LOG_PREFIX, taskId, operation);
|
||||
return;
|
||||
}
|
||||
// 首次申请节点由发起人直接提交,无需发送瞬时待办;退回申请节点仍需发送。
|
||||
if (Long.valueOf(0).equals(task.getTaskParentId()) && "startTask".equals(task.getTaskName())) {
|
||||
log.info("{}【WF跳过】任务ID={},操作={},原因=首次申请节点无需发送待办", LOG_PREFIX, taskId, operation);
|
||||
return;
|
||||
}
|
||||
if ("send".equals(operation) && !ProcessTaskStateEnum.DOING.getCode().equals(task.getTaskState())) {
|
||||
log.info("{}【WF跳过】任务ID={},操作={},原因=任务非办理状态,任务状态={}",
|
||||
LOG_PREFIX, taskId, operation, task.getTaskState());
|
||||
return;
|
||||
}
|
||||
ProcessInstance instance = dao.fetch(ProcessInstance.class, task.getProcessInstanceId());
|
||||
if (instance == null) {
|
||||
log.warn("{}【WF跳过】任务ID={},操作={},实例ID={},原因=流程实例不存在",
|
||||
LOG_PREFIX, taskId, operation, task.getProcessInstanceId());
|
||||
return;
|
||||
}
|
||||
List<ProcessTaskActor> recipients = actors == null ? dao.query(ProcessTaskActor.class,
|
||||
Cnd.where(ProcessTaskActor::getProcessTaskId, "=", taskId)) : actors;
|
||||
if (recipients.isEmpty()) {
|
||||
log.info("{}【WF跳过】任务ID={},操作={},原因=接收人列表为空", LOG_PREFIX, taskId, operation);
|
||||
return;
|
||||
}
|
||||
if (recipients.stream().anyMatch(actor -> StrUtil.isBlank(actor.getActorAccount()))) {
|
||||
log.warn("{}【WF账号缺失】任务ID={},操作={},部分参与人没有登录账号,将过滤这些人员",
|
||||
LOG_PREFIX, taskId, operation);
|
||||
}
|
||||
List<String> accounts = recipients.stream().map(ProcessTaskActor::getActorAccount)
|
||||
.filter(StrUtil::isNotBlank).distinct().toList();
|
||||
if (accounts.isEmpty()) {
|
||||
log.info("{}【WF跳过】任务ID={},操作={},原因=接收人均缺少有效登录账号", LOG_PREFIX, taskId, operation);
|
||||
return;
|
||||
}
|
||||
PortalTodoSendRequest request = new PortalTodoSendRequest();
|
||||
request.setEntityName("wf");
|
||||
request.setEntityId(String.valueOf(taskId));
|
||||
request.setEntityNumber(StrUtil.blankToDefault(instance.getBusinessNo(), String.valueOf(instance.getId())));
|
||||
request.setTargets(accounts);
|
||||
if ("send".equals(operation)) {
|
||||
String name = StrUtil.isBlank(instance.getVariable()) ? null : JSONUtil.parseObj(instance.getVariable())
|
||||
.getStr(FlowConst.PROCESS_INSTANCE_NAME);
|
||||
name = StrUtil.blankToDefault(name, request.getEntityNumber());
|
||||
String node = StrUtil.blankToDefault(task.getDisplayName(), task.getTaskName());
|
||||
request.setSubject(name + "-" + node);
|
||||
request.setContent("您有一条待办任务,实例:" + name + ",处理环节:" + node);
|
||||
request.setLink(buildLink(task, instance));
|
||||
// H5使用任务自身的手机表单,不使用PC表单替代;缺失配置时明确记录回退原因。
|
||||
if (StrUtil.isBlank(task.getH5FormKey())) {
|
||||
log.warn("{}【H5入口回退】任务ID={},原因=未配置h5FormKey,将跳转手机首页", LOG_PREFIX, taskId);
|
||||
}
|
||||
request.setMobileLink(buildFormLink(task, instance, task.getH5FormKey(), "/platform/h5"));
|
||||
if (StrUtil.isNotBlank(instance.getOperator())) {
|
||||
View_user creator = dao.fetch(View_user.class, Cnd.where("id", "=", instance.getOperator()));
|
||||
if (creator != null) {
|
||||
request.setCreator(creator.getLoginname());
|
||||
request.setCreatorName(creator.getUsername());
|
||||
}
|
||||
}
|
||||
portalTodoService.sendTodo(request);
|
||||
} else if ("done".equals(operation)) {
|
||||
portalTodoService.doneTodo(request);
|
||||
} else {
|
||||
portalTodoService.removeTodo(request);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("{}【WF同步失败】任务ID={},操作={},异常类型={}", LOG_PREFIX, taskId, operation, e.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 复用 wf 表单入口参数,保留原链接查询和片段,避免工号或业务编号中的特殊字符破坏地址。
|
||||
*/
|
||||
private String buildLink(ProcessTask task, ProcessInstance instance) {
|
||||
return buildFormLink(task, instance, task.getFormKey(), "/flow/todoCenter");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param task 提供taskId、taskKey的WF任务
|
||||
* @param instance 提供bizId(businessNo)的流程实例
|
||||
* @param formKey 对应端的业务表单地址,支持相对路径及完整HTTP(S)地址
|
||||
* @param fallback 表单未配置时的入口路径;回退入口不附加业务参数
|
||||
* @return 补齐项目域名并编码任务参数的地址,保留已有查询参数和锚点
|
||||
*/
|
||||
private String buildFormLink(ProcessTask task, ProcessInstance instance, String formKey, String fallback) {
|
||||
String form = StrUtil.blankToDefault(formKey, fallback);
|
||||
String link = URI.create(StrUtil.appendIfMissing(Globals.AppDomain, "/")).resolve(form).toString();
|
||||
if (StrUtil.isBlank(formKey)) return link;
|
||||
int fragmentAt = link.indexOf('#');
|
||||
String fragment = fragmentAt < 0 ? "" : link.substring(fragmentAt);
|
||||
if (fragmentAt >= 0) link = link.substring(0, fragmentAt);
|
||||
return link + (link.contains("?") ? "&" : "?") + "taskId=" + task.getId()
|
||||
+ "&bizId=" + encode(StrUtil.blankToDefault(instance.getBusinessNo(), ""))
|
||||
+ "&taskKey=" + encode(task.getTaskName()) + fragment;
|
||||
}
|
||||
|
||||
private String encode(String value) {
|
||||
return URLEncoder.encode(StrUtil.nullToEmpty(value), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
|
||||
import com.budwk.app.flow.enums.ProcessSubmitTypeEnum;
|
||||
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
|
||||
import com.budwk.app.flow.service.ProcessDefineService;
|
||||
import com.budwk.app.flow.service.FlowPortalTodoService;
|
||||
import com.budwk.app.flow.service.ProcessInstanceService;
|
||||
import com.budwk.app.flow.service.ProcessTaskService;
|
||||
import com.budwk.app.flow.vo.HighLightVO;
|
||||
@@ -57,6 +58,9 @@ public class ProcessInstanceServiceImpl extends BaseServiceImpl<ProcessInstance>
|
||||
@Inject
|
||||
private ProcessDefineService processDefineService;
|
||||
|
||||
@Inject
|
||||
private FlowPortalTodoService flowPortalTodoService;
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public boolean save(ProcessInstance param) {
|
||||
@@ -198,10 +202,16 @@ public class ProcessInstanceServiceImpl extends BaseServiceImpl<ProcessInstance>
|
||||
processInstance.setState(ProcessInstanceStateEnum.INTERRUPT.getCode());
|
||||
dao().update(ProcessInstance.class, Chain.from(processInstance), Cnd.where(ProcessInstance::getId, "=", processInstanceId)
|
||||
.and(ProcessInstance::getState, "=", ProcessInstanceStateEnum.DOING.getCode()));
|
||||
// 终止后任务不可办理,同步清理门户待办。
|
||||
flowPortalTodoService.removeInstance(processInstanceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void resume(Long processInstanceId, String operator) {
|
||||
List<ProcessTask> restoredTasks = dao().query(ProcessTask.class,
|
||||
Cnd.where(ProcessTask::getProcessInstanceId, "=", processInstanceId)
|
||||
.and(ProcessTask::getTaskState, "=", ProcessTaskStateEnum.INTERRUPT.getCode()));
|
||||
Date now = new Date();
|
||||
ProcessInstance processInstance = fetch(processInstanceId);
|
||||
// 1. 更新流程实例状态为进行中
|
||||
@@ -212,9 +222,12 @@ public class ProcessInstanceServiceImpl extends BaseServiceImpl<ProcessInstance>
|
||||
processTask.setTaskState(ProcessTaskStateEnum.DOING.getCode());
|
||||
dao().update(ProcessTask.class, Chain.from(processTask), Cnd.where(ProcessTask::getProcessInstanceId, "=", processInstanceId)
|
||||
.and(ProcessTask::getTaskState, "=", ProcessTaskStateEnum.INTERRUPT.getCode()));
|
||||
// 仅恢复本次由终止状态重新进入办理的任务,避免重复发送已有待办。
|
||||
restoredTasks.forEach(task -> flowPortalTodoService.sendTask(task.getId(), null));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void pending(Long processInstanceId, String operator) {
|
||||
Date now = new Date();
|
||||
// 1. 将该流程实例产生的任务状态修改为挂起
|
||||
@@ -227,10 +240,17 @@ public class ProcessInstanceServiceImpl extends BaseServiceImpl<ProcessInstance>
|
||||
processInstance.setState(ProcessInstanceStateEnum.PENDING.getCode());
|
||||
dao().update(ProcessInstance.class, Chain.from(processInstance), Cnd.where(ProcessInstance::getId, "=", processInstanceId)
|
||||
.and(ProcessInstance::getState, "=", ProcessInstanceStateEnum.DOING.getCode()));
|
||||
// 挂起期间不应继续展示可办理的门户待办。
|
||||
flowPortalTodoService.removeInstance(processInstanceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void activate(Long processInstanceId, String operator) {
|
||||
// 沿用当前 activate 的状态选择规则,只同步实际被恢复的任务。
|
||||
List<ProcessTask> restoredTasks = dao().query(ProcessTask.class,
|
||||
Cnd.where(ProcessTask::getProcessInstanceId, "=", processInstanceId)
|
||||
.and(ProcessTask::getTaskState, "=", ProcessTaskStateEnum.INTERRUPT.getCode()));
|
||||
ProcessInstance processInstance = fetch(processInstanceId);
|
||||
// 1. 更新流程实例状态为进行中
|
||||
processInstance.setState(ProcessInstanceStateEnum.DOING.getCode());
|
||||
@@ -240,6 +260,7 @@ public class ProcessInstanceServiceImpl extends BaseServiceImpl<ProcessInstance>
|
||||
processTask.setTaskState(ProcessTaskStateEnum.DOING.getCode());
|
||||
dao().update(ProcessTask.class, Chain.from(processTask), Cnd.where(ProcessTask::getProcessInstanceId, "=", processInstanceId)
|
||||
.and(ProcessTask::getTaskState, "=", ProcessTaskStateEnum.INTERRUPT.getCode()));
|
||||
restoredTasks.forEach(task -> flowPortalTodoService.sendTask(task.getId(), null));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -323,6 +344,7 @@ public class ProcessInstanceServiceImpl extends BaseServiceImpl<ProcessInstance>
|
||||
}
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void withdraw(Long processInstanceId, String operator) {
|
||||
// Date now = new Date();
|
||||
// 1. 将该流程实例状态修改为撤回
|
||||
@@ -342,6 +364,8 @@ public class ProcessInstanceServiceImpl extends BaseServiceImpl<ProcessInstance>
|
||||
processTask.setUpdatedBy(operator);
|
||||
}
|
||||
dao().update(taskList, "taskState|updateAt|updateBy");
|
||||
// 实例撤回没有任务撤回事件,在此同步删除已不可办理的门户待办。
|
||||
flowPortalTodoService.removeInstance(processInstanceId);
|
||||
}
|
||||
|
||||
// int update = dao().update(ProcessInstance.class, Chain.from(processInstance), Cnd.where(ProcessInstance::getId, "=", processInstanceId)
|
||||
@@ -555,6 +579,8 @@ public class ProcessInstanceServiceImpl extends BaseServiceImpl<ProcessInstance>
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void deleteProcessInstanceById(Long processInstanceId) {
|
||||
// 在实例、任务和参与人删除前清理门户,否则无法重建原待办定位参数。
|
||||
flowPortalTodoService.removeInstance(processInstanceId);
|
||||
List<ProcessTask> taskList = dao().query(ProcessTask.class, Cnd.where(ProcessTask::getProcessInstanceId, "=", processInstanceId));
|
||||
List<Long> taskIds = taskList.stream().map(ProcessTask::getId).toList();
|
||||
// 任务参与人依赖流程任务,删除流程时先清理参与人,避免遗留无法回查所属流程的孤立记录。
|
||||
|
||||
@@ -27,6 +27,7 @@ import com.budwk.app.flow.entity.ProcessTask;
|
||||
import com.budwk.app.flow.entity.ProcessTaskActor;
|
||||
import com.budwk.app.flow.enums.*;
|
||||
import com.budwk.app.flow.service.ProcessDefineService;
|
||||
import com.budwk.app.flow.service.FlowPortalTodoService;
|
||||
import com.budwk.app.flow.service.ProcessInstanceService;
|
||||
import com.budwk.app.flow.service.ProcessTaskService;
|
||||
import com.budwk.app.flow.vo.ProcessTaskVO;
|
||||
@@ -56,6 +57,9 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
|
||||
@Inject
|
||||
private ProcessDefineService processDefineService;
|
||||
|
||||
@Inject
|
||||
private FlowPortalTodoService flowPortalTodoService;
|
||||
|
||||
@Override
|
||||
public boolean save(ProcessTask param) {
|
||||
param.setId(null);
|
||||
@@ -125,6 +129,7 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
|
||||
}
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void finishProcessTask(Long processTaskId, String operator, Dict args) {
|
||||
ProcessTask his = fetch(processTaskId);
|
||||
ProcessTask processTask = new ProcessTask();
|
||||
@@ -149,6 +154,7 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
|
||||
}
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void abandonProcessTask(Long processTaskId, String operator, Dict args) {
|
||||
ProcessTask his = fetch(processTaskId);
|
||||
ProcessTask processTask = new ProcessTask();
|
||||
@@ -228,10 +234,13 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
|
||||
if (CollectionUtil.isEmpty(actors)) return;
|
||||
List<String> dbActors = getTaskActors(processTaskId);
|
||||
List<String> newActors = actors.stream().filter(actor -> !dbActors.contains(actor)).toList();
|
||||
if (newActors.isEmpty()) return;
|
||||
Sql sql = Sqls.create("select id,username,loginname,unitName,unitId from vw_user where id in (@ids)");
|
||||
sql.setParam("ids", newActors);
|
||||
List<NutMap> list = listMap(sql);
|
||||
|
||||
List<ProcessTaskActor> addedActors = new ArrayList<>();
|
||||
|
||||
for (NutMap actor : list) {
|
||||
ProcessTaskActor processTaskActor = new ProcessTaskActor();
|
||||
processTaskActor.setProcessTaskId(processTaskId);
|
||||
@@ -243,7 +252,10 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
|
||||
processTaskActor.setCreatedAt(System.currentTimeMillis());
|
||||
System.out.println(StrUtil.format("给任务:{},添加参与者:{}", processTaskId, actor.toString()));
|
||||
insert(processTaskActor);
|
||||
addedActors.add(processTaskActor);
|
||||
}
|
||||
// 只为实际新增的账号发送,覆盖普通任务、串并行会签、转办和加签,避免事件重复发送。
|
||||
flowPortalTodoService.sendTask(processTaskId, addedActors);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -290,7 +302,14 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
|
||||
}
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void removeTaskActor(Long processTaskId, List<String> actors) {
|
||||
if (CollectionUtil.isEmpty(actors)) return;
|
||||
// 先保留被移除人员的门户账号,参与人记录清理后无法再据此定位待办。
|
||||
List<ProcessTaskActor> removedActors = dao().query(ProcessTaskActor.class,
|
||||
Cnd.where(ProcessTaskActor::getProcessTaskId, "=", processTaskId)
|
||||
.and(ProcessTaskActor::getActorId, "in", actors));
|
||||
flowPortalTodoService.removeTask(processTaskId, removedActors);
|
||||
dao().clear(ProcessTaskActor.class, Cnd.where(ProcessTaskActor::getProcessTaskId, "=", processTaskId).and(ProcessTaskActor::getActorId, "in", actors));
|
||||
}
|
||||
|
||||
@@ -481,6 +500,9 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
|
||||
task.setVariable(JSONUtil.toJsonStr(variable));
|
||||
updateIgnoreNull(task);
|
||||
|
||||
// 转办后原参与人不再办理,先清理原任务门户待办;新任务在添加参与人时发送。
|
||||
flowPortalTodoService.removeTask(taskId, null);
|
||||
|
||||
// 创建转办任务
|
||||
ProcessTask newTask = BeanUtil.copyProperties(task, ProcessTask.class, "id");
|
||||
newTask.setTaskParentId(taskId);
|
||||
|
||||
Reference in New Issue
Block a user