81 changed files with 1861 additions and 133 deletions
+33
View File
@@ -133,6 +133,38 @@
</dependencies> </dependencies>
<dependencyManagement> <dependencyManagement>
<dependencies> <dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-websocket</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.nutz</groupId> <groupId>org.nutz</groupId>
<artifactId>nutzboot-parent</artifactId> <artifactId>nutzboot-parent</artifactId>
@@ -151,6 +183,7 @@
<easypoi.version>4.5.0</easypoi.version> <easypoi.version>4.5.0</easypoi.version>
<jsoup.veision>1.15.3</jsoup.veision> <jsoup.veision>1.15.3</jsoup.veision>
<mysql-connector-java.version>8.0.28</mysql-connector-java.version> <mysql-connector-java.version>8.0.28</mysql-connector-java.version>
<tomcat.version>9.0.121</tomcat.version>
<sa-token.version>1.25.0</sa-token.version> <sa-token.version>1.25.0</sa-token.version>
<easy-captcha.version>1.6.2</easy-captcha.version> <easy-captcha.version>1.6.2</easy-captcha.version>
<emoji-java.veision>4.0.0</emoji-java.veision> <emoji-java.veision>4.0.0</emoji-java.veision>
+50 -1
View File
@@ -7,6 +7,7 @@
<properties> <properties>
<nutzboot.version>2.6.0-SNAPSHOT</nutzboot.version> <nutzboot.version>2.6.0-SNAPSHOT</nutzboot.version>
<nutz.version>1.r.70-SNAPSHOT</nutz.version> <nutz.version>1.r.70-SNAPSHOT</nutz.version>
<tomcat.version>9.0.121</tomcat.version>
<mysql-connector-java.version>8.0.28</mysql-connector-java.version> <mysql-connector-java.version>8.0.28</mysql-connector-java.version>
<jaxb-api.version>2.3.1</jaxb-api.version> <jaxb-api.version>2.3.1</jaxb-api.version>
<slf4j.version>2.0.8</slf4j.version> <slf4j.version>2.0.8</slf4j.version>
@@ -144,8 +145,22 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.nutz</groupId> <groupId>org.nutz</groupId>
<artifactId>nutzboot-starter-jetty</artifactId> <artifactId>nutzboot-starter-tomcat</artifactId>
<version>${nutzboot.version}</version> <version>${nutzboot.version}</version>
<exclusions>
<exclusion>
<groupId>org.gandon.tomcat</groupId>
<artifactId>juli-to-slf4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.nutz</groupId>
<artifactId>nutz-plugins-websocket</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.nutz</groupId> <groupId>org.nutz</groupId>
@@ -424,6 +439,40 @@
</dependencies> </dependencies>
<dependencyManagement> <dependencyManagement>
<dependencies> <dependencies>
<!-- Servlet API 由 Tomcat 提供,防止旧 API 混入可执行 JAR。 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
<scope>provided</scope>
</dependency>
<!-- 显式覆盖导入的 NutzBoot BOM,避免回退到其默认 Tomcat 8.5。 -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-websocket</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.nutz</groupId> <groupId>org.nutz</groupId>
<artifactId>nutzboot-parent</artifactId> <artifactId>nutzboot-parent</artifactId>
@@ -11,6 +11,7 @@ import com.budwk.app.web.commons.auth.satoken.SaTokenDaoRedisImpl;
import com.budwk.app.web.commons.auth.satoken.StpInterfaceImpl; import com.budwk.app.web.commons.auth.satoken.StpInterfaceImpl;
import com.budwk.app.web.commons.base.Globals; import com.budwk.app.web.commons.base.Globals;
import com.budwk.app.web.commons.ext.pubsub.WebPubSub; import com.budwk.app.web.commons.ext.pubsub.WebPubSub;
import com.budwk.app.web.commons.ext.handler.WkTomcatConfiguration;
import com.budwk.app.zhgh.dayofficework.message.service.GlobalMessageSendService; import com.budwk.app.zhgh.dayofficework.message.service.GlobalMessageSendService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.beetl.core.GroupTemplate; import org.beetl.core.GroupTemplate;
@@ -65,6 +66,7 @@ public class MainLauncher {
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
NbApp nb = new NbApp().setArgs(args).setPrintProcDoc(true); NbApp nb = new NbApp().setArgs(args).setPrintProcDoc(true);
nb.getAppContext().setMainPackage("com.budwk"); nb.getAppContext().setMainPackage("com.budwk");
nb.addListener(new WkTomcatConfiguration());
nb.run(); nb.run();
} }
@@ -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状态={},耗时={}mscode={}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;
}
@@ -93,6 +93,15 @@ public class FlowTodoCenterController {
return Result.success(pagination); return Result.success(pagination);
} }
/**
* 分页查询当前用户的已办任务,仅返回列表展示及详情跳转所需的信息。
*
* @param pageNumber 查询页码
* @param pageSize 每页记录数
* @param searchKeyword 流程名称查询关键字
* @param category 流程分类
* @return 已办任务分页结果,不包含完整流程变量和任务表单数据
*/
@At @At
@SaCheckLogin @SaCheckLogin
public Result done(Integer pageNumber, Integer pageSize, String searchKeyword, String category) { public Result done(Integer pageNumber, Integer pageSize, String searchKeyword, String category) {
@@ -106,8 +115,13 @@ public class FlowTodoCenterController {
t.h5FormKey, t.h5FormKey,
t.createdAt, t.createdAt,
t.finishTime, t.finishTime,
t.variable AS taskVariable, -- 保留任务变量字段类型,但已办列表不返回其中的业务表单数据。
ins.variable, '{}' AS taskVariable,
-- 列表仅需流程名称和申请人,避免完整变量中的身份证等信息泄露。
JSON_OBJECT(
'instanceName', ins.variable -> '$.instanceName',
'initiatorName', ins.variable -> '$.initiatorName'
) AS variable,
ins.state AS instanceState, ins.state AS instanceState,
ins.id AS instanceId, ins.id AS instanceId,
ins.businessNo, ins.businessNo,
@@ -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 @Inject
private Dao dao; private Dao dao;
@Inject
private FlowPortalTodoService flowPortalTodoService;
/** /**
* 执行任务 * 执行任务
* @param args * @param args
@@ -111,9 +114,12 @@ public class FlowCommonService {
// 撤销任务 // 撤销任务
List<ProcessTask> taskList = dao.query(ProcessTask.class, Cnd.where(ProcessTask::getTaskParentId, "=", taskId)); 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) { for (ProcessTask task : taskList) {
task.setTaskState(ProcessTaskStateEnum.WITHDRAW.getCode()); task.setTaskState(ProcessTaskStateEnum.WITHDRAW.getCode());
dao.update(task); dao.update(task);
revokedTaskIds.add(task.getId());
} }
// 会签并行任务 撤销后续任务 // 会签并行任务 撤销后续任务
if (selfTask.getPerformType().equals(ProcessTaskPerformTypeEnum.COUNTERSIGN.getCode())) { if (selfTask.getPerformType().equals(ProcessTaskPerformTypeEnum.COUNTERSIGN.getCode())) {
@@ -122,6 +128,7 @@ public class FlowCommonService {
for (ProcessTask doingTask : doingTasks) { for (ProcessTask doingTask : doingTasks) {
doingTask.setTaskState(ProcessTaskStateEnum.WITHDRAW.getCode()); doingTask.setTaskState(ProcessTaskStateEnum.WITHDRAW.getCode());
dao.update(doingTask); 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()); 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(); 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 提供bizIdbusinessNo)的流程实例
* @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.ProcessSubmitTypeEnum;
import com.budwk.app.flow.enums.ProcessTaskStateEnum; import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.flow.service.ProcessDefineService; 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.ProcessInstanceService;
import com.budwk.app.flow.service.ProcessTaskService; import com.budwk.app.flow.service.ProcessTaskService;
import com.budwk.app.flow.vo.HighLightVO; import com.budwk.app.flow.vo.HighLightVO;
@@ -57,6 +58,9 @@ public class ProcessInstanceServiceImpl extends BaseServiceImpl<ProcessInstance>
@Inject @Inject
private ProcessDefineService processDefineService; private ProcessDefineService processDefineService;
@Inject
private FlowPortalTodoService flowPortalTodoService;
@Override @Override
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
public boolean save(ProcessInstance param) { public boolean save(ProcessInstance param) {
@@ -198,10 +202,16 @@ public class ProcessInstanceServiceImpl extends BaseServiceImpl<ProcessInstance>
processInstance.setState(ProcessInstanceStateEnum.INTERRUPT.getCode()); processInstance.setState(ProcessInstanceStateEnum.INTERRUPT.getCode());
dao().update(ProcessInstance.class, Chain.from(processInstance), Cnd.where(ProcessInstance::getId, "=", processInstanceId) dao().update(ProcessInstance.class, Chain.from(processInstance), Cnd.where(ProcessInstance::getId, "=", processInstanceId)
.and(ProcessInstance::getState, "=", ProcessInstanceStateEnum.DOING.getCode())); .and(ProcessInstance::getState, "=", ProcessInstanceStateEnum.DOING.getCode()));
// 终止后任务不可办理,同步清理门户待办。
flowPortalTodoService.removeInstance(processInstanceId);
} }
@Override @Override
@Aop(TransAop.READ_COMMITTED)
public void resume(Long processInstanceId, String operator) { 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(); Date now = new Date();
ProcessInstance processInstance = fetch(processInstanceId); ProcessInstance processInstance = fetch(processInstanceId);
// 1. 更新流程实例状态为进行中 // 1. 更新流程实例状态为进行中
@@ -212,9 +222,12 @@ public class ProcessInstanceServiceImpl extends BaseServiceImpl<ProcessInstance>
processTask.setTaskState(ProcessTaskStateEnum.DOING.getCode()); processTask.setTaskState(ProcessTaskStateEnum.DOING.getCode());
dao().update(ProcessTask.class, Chain.from(processTask), Cnd.where(ProcessTask::getProcessInstanceId, "=", processInstanceId) dao().update(ProcessTask.class, Chain.from(processTask), Cnd.where(ProcessTask::getProcessInstanceId, "=", processInstanceId)
.and(ProcessTask::getTaskState, "=", ProcessTaskStateEnum.INTERRUPT.getCode())); .and(ProcessTask::getTaskState, "=", ProcessTaskStateEnum.INTERRUPT.getCode()));
// 仅恢复本次由终止状态重新进入办理的任务,避免重复发送已有待办。
restoredTasks.forEach(task -> flowPortalTodoService.sendTask(task.getId(), null));
} }
@Override @Override
@Aop(TransAop.READ_COMMITTED)
public void pending(Long processInstanceId, String operator) { public void pending(Long processInstanceId, String operator) {
Date now = new Date(); Date now = new Date();
// 1. 将该流程实例产生的任务状态修改为挂起 // 1. 将该流程实例产生的任务状态修改为挂起
@@ -227,10 +240,17 @@ public class ProcessInstanceServiceImpl extends BaseServiceImpl<ProcessInstance>
processInstance.setState(ProcessInstanceStateEnum.PENDING.getCode()); processInstance.setState(ProcessInstanceStateEnum.PENDING.getCode());
dao().update(ProcessInstance.class, Chain.from(processInstance), Cnd.where(ProcessInstance::getId, "=", processInstanceId) dao().update(ProcessInstance.class, Chain.from(processInstance), Cnd.where(ProcessInstance::getId, "=", processInstanceId)
.and(ProcessInstance::getState, "=", ProcessInstanceStateEnum.DOING.getCode())); .and(ProcessInstance::getState, "=", ProcessInstanceStateEnum.DOING.getCode()));
// 挂起期间不应继续展示可办理的门户待办。
flowPortalTodoService.removeInstance(processInstanceId);
} }
@Override @Override
@Aop(TransAop.READ_COMMITTED)
public void activate(Long processInstanceId, String operator) { 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); ProcessInstance processInstance = fetch(processInstanceId);
// 1. 更新流程实例状态为进行中 // 1. 更新流程实例状态为进行中
processInstance.setState(ProcessInstanceStateEnum.DOING.getCode()); processInstance.setState(ProcessInstanceStateEnum.DOING.getCode());
@@ -240,6 +260,7 @@ public class ProcessInstanceServiceImpl extends BaseServiceImpl<ProcessInstance>
processTask.setTaskState(ProcessTaskStateEnum.DOING.getCode()); processTask.setTaskState(ProcessTaskStateEnum.DOING.getCode());
dao().update(ProcessTask.class, Chain.from(processTask), Cnd.where(ProcessTask::getProcessInstanceId, "=", processInstanceId) dao().update(ProcessTask.class, Chain.from(processTask), Cnd.where(ProcessTask::getProcessInstanceId, "=", processInstanceId)
.and(ProcessTask::getTaskState, "=", ProcessTaskStateEnum.INTERRUPT.getCode())); .and(ProcessTask::getTaskState, "=", ProcessTaskStateEnum.INTERRUPT.getCode()));
restoredTasks.forEach(task -> flowPortalTodoService.sendTask(task.getId(), null));
} }
@Override @Override
@@ -323,6 +344,7 @@ public class ProcessInstanceServiceImpl extends BaseServiceImpl<ProcessInstance>
} }
@Override @Override
@Aop(TransAop.READ_COMMITTED)
public void withdraw(Long processInstanceId, String operator) { public void withdraw(Long processInstanceId, String operator) {
// Date now = new Date(); // Date now = new Date();
// 1. 将该流程实例状态修改为撤回 // 1. 将该流程实例状态修改为撤回
@@ -342,6 +364,8 @@ public class ProcessInstanceServiceImpl extends BaseServiceImpl<ProcessInstance>
processTask.setUpdatedBy(operator); processTask.setUpdatedBy(operator);
} }
dao().update(taskList, "taskState|updateAt|updateBy"); dao().update(taskList, "taskState|updateAt|updateBy");
// 实例撤回没有任务撤回事件,在此同步删除已不可办理的门户待办。
flowPortalTodoService.removeInstance(processInstanceId);
} }
// int update = dao().update(ProcessInstance.class, Chain.from(processInstance), Cnd.where(ProcessInstance::getId, "=", 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 @Override
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
public void deleteProcessInstanceById(Long processInstanceId) { public void deleteProcessInstanceById(Long processInstanceId) {
// 在实例、任务和参与人删除前清理门户,否则无法重建原待办定位参数。
flowPortalTodoService.removeInstance(processInstanceId);
List<ProcessTask> taskList = dao().query(ProcessTask.class, Cnd.where(ProcessTask::getProcessInstanceId, "=", processInstanceId)); List<ProcessTask> taskList = dao().query(ProcessTask.class, Cnd.where(ProcessTask::getProcessInstanceId, "=", processInstanceId));
List<Long> taskIds = taskList.stream().map(ProcessTask::getId).toList(); 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.entity.ProcessTaskActor;
import com.budwk.app.flow.enums.*; import com.budwk.app.flow.enums.*;
import com.budwk.app.flow.service.ProcessDefineService; 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.ProcessInstanceService;
import com.budwk.app.flow.service.ProcessTaskService; import com.budwk.app.flow.service.ProcessTaskService;
import com.budwk.app.flow.vo.ProcessTaskVO; import com.budwk.app.flow.vo.ProcessTaskVO;
@@ -56,6 +57,9 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
@Inject @Inject
private ProcessDefineService processDefineService; private ProcessDefineService processDefineService;
@Inject
private FlowPortalTodoService flowPortalTodoService;
@Override @Override
public boolean save(ProcessTask param) { public boolean save(ProcessTask param) {
param.setId(null); param.setId(null);
@@ -125,6 +129,7 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
} }
@Override @Override
@Aop(TransAop.READ_COMMITTED)
public void finishProcessTask(Long processTaskId, String operator, Dict args) { public void finishProcessTask(Long processTaskId, String operator, Dict args) {
ProcessTask his = fetch(processTaskId); ProcessTask his = fetch(processTaskId);
ProcessTask processTask = new ProcessTask(); ProcessTask processTask = new ProcessTask();
@@ -149,6 +154,7 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
} }
@Override @Override
@Aop(TransAop.READ_COMMITTED)
public void abandonProcessTask(Long processTaskId, String operator, Dict args) { public void abandonProcessTask(Long processTaskId, String operator, Dict args) {
ProcessTask his = fetch(processTaskId); ProcessTask his = fetch(processTaskId);
ProcessTask processTask = new ProcessTask(); ProcessTask processTask = new ProcessTask();
@@ -228,10 +234,13 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
if (CollectionUtil.isEmpty(actors)) return; if (CollectionUtil.isEmpty(actors)) return;
List<String> dbActors = getTaskActors(processTaskId); List<String> dbActors = getTaskActors(processTaskId);
List<String> newActors = actors.stream().filter(actor -> !dbActors.contains(actor)).toList(); 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 sql = Sqls.create("select id,username,loginname,unitName,unitId from vw_user where id in (@ids)");
sql.setParam("ids", newActors); sql.setParam("ids", newActors);
List<NutMap> list = listMap(sql); List<NutMap> list = listMap(sql);
List<ProcessTaskActor> addedActors = new ArrayList<>();
for (NutMap actor : list) { for (NutMap actor : list) {
ProcessTaskActor processTaskActor = new ProcessTaskActor(); ProcessTaskActor processTaskActor = new ProcessTaskActor();
processTaskActor.setProcessTaskId(processTaskId); processTaskActor.setProcessTaskId(processTaskId);
@@ -243,7 +252,10 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
processTaskActor.setCreatedAt(System.currentTimeMillis()); processTaskActor.setCreatedAt(System.currentTimeMillis());
System.out.println(StrUtil.format("给任务:{},添加参与者:{}", processTaskId, actor.toString())); System.out.println(StrUtil.format("给任务:{},添加参与者:{}", processTaskId, actor.toString()));
insert(processTaskActor); insert(processTaskActor);
addedActors.add(processTaskActor);
} }
// 只为实际新增的账号发送,覆盖普通任务、串并行会签、转办和加签,避免事件重复发送。
flowPortalTodoService.sendTask(processTaskId, addedActors);
} }
@Override @Override
@@ -290,7 +302,14 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
} }
@Override @Override
@Aop(TransAop.READ_COMMITTED)
public void removeTaskActor(Long processTaskId, List<String> actors) { 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)); 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)); task.setVariable(JSONUtil.toJsonStr(variable));
updateIgnoreNull(task); updateIgnoreNull(task);
// 转办后原参与人不再办理,先清理原任务门户待办;新任务在添加参与人时发送。
flowPortalTodoService.removeTask(taskId, null);
// 创建转办任务 // 创建转办任务
ProcessTask newTask = BeanUtil.copyProperties(task, ProcessTask.class, "id"); ProcessTask newTask = BeanUtil.copyProperties(task, ProcessTask.class, "id");
newTask.setTaskParentId(taskId); newTask.setTaskParentId(taskId);
@@ -33,6 +33,10 @@ import org.nutz.mvc.upload.TempFile;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@@ -43,6 +47,9 @@ import java.nio.file.StandardOpenOption;
import java.util.Base64; import java.util.Base64;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Iterator;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -54,6 +61,9 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
static String IMG_BASE64_PATTERN = "<img\\s+[^>]*src\\s*=\\s*['\"](data:image/[^'\"]+;base64,[^'\"]+)['\"][^>]*>"; static String IMG_BASE64_PATTERN = "<img\\s+[^>]*src\\s*=\\s*['\"](data:image/[^'\"]+;base64,[^'\"]+)['\"][^>]*>";
private static final int DOWNLOAD_FILE_CACHE_LIMIT = 2000; private static final int DOWNLOAD_FILE_CACHE_LIMIT = 2000;
private final Map<String, Sys_file> downloadFileCache = new ConcurrentHashMap<>(); private final Map<String, Sys_file> downloadFileCache = new ConcurrentHashMap<>();
private static final Set<String> UPLOAD_SUFFIXES = Set.of("gif", "jpg", "jpeg", "png", "doc", "docx",
"xls", "xlsx", "rar", "zip", "7z", "txt", "pdf", "pptx", "ppt", "mp4");
private static final Set<String> ACTIVE_SUFFIXES = Set.of("svg", "svgz", "html", "htm", "xhtml", "xml", "js", "mjs", "swf");
public SysFileServiceImpl(Dao dao) { public SysFileServiceImpl(Dao dao) {
super(dao); super(dao);
@@ -86,11 +96,13 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
@Override @Override
public void download(String id, HttpServletRequest request, HttpServletResponse response) throws IOException { public void download(String id, HttpServletRequest request, HttpServletResponse response) throws IOException {
if (handleCachedFileRequest(id, request, response)) { response.setHeader("X-Content-Type-Options", "nosniff");
return; response.setHeader("Cache-Control", "private, no-store");
}
Sys_file sys_file = fetchDownloadFile(id); Sys_file sys_file = fetchDownloadFile(id);
if (!ObjectUtil.isEmpty(sys_file) && handleCachedImageRequest(sys_file, request, response)) { // 历史主动内容也必须在缓存协商前拦截,避免旧 SVG 继续在业务域下执行。
if (sys_file != null && (ACTIVE_SUFFIXES.contains(StrUtil.blankToDefault(sys_file.getSuffix(), "").toLowerCase(Locale.ROOT))
|| ACTIVE_SUFFIXES.contains(FileUtil.extName(sys_file.getName()).toLowerCase(Locale.ROOT)))) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "不允许访问此类型的附件");
return; return;
} }
if (ObjectUtil.isEmpty(sys_file)) { if (ObjectUtil.isEmpty(sys_file)) {
@@ -117,16 +129,6 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
} }
} }
private boolean handleCachedFileRequest(String id, HttpServletRequest request, HttpServletResponse response) {
String etag = buildFileEtag(id);
if (etag.equals(request.getHeader("If-None-Match"))) {
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
setImageCacheHeaders(response, etag);
return true;
}
return false;
}
private Sys_file fetchDownloadFile(String id) { private Sys_file fetchDownloadFile(String id) {
if (StrUtil.isBlank(id)) { if (StrUtil.isBlank(id)) {
return null; return null;
@@ -145,23 +147,15 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
return sysFile; return sysFile;
} }
private boolean handleCachedImageRequest(Sys_file sysFile, HttpServletRequest request, HttpServletResponse response) {
if (!isImage(sysFile)) {
return false;
}
String etag = buildFileEtag(sysFile);
if (etag.equals(request.getHeader("If-None-Match"))) {
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
setImageCacheHeaders(response, etag);
return true;
}
return false;
}
private boolean writeImageResponse(Sys_file sysFile, byte[] bytes, HttpServletResponse response) throws IOException { private boolean writeImageResponse(Sys_file sysFile, byte[] bytes, HttpServletResponse response) throws IOException {
if (!isImage(sysFile)) { if (!isImage(sysFile)) {
return false; return false;
} }
String suffix = StrUtil.blankToDefault(sysFile.getSuffix(), FileUtil.extName(sysFile.getName())).toLowerCase(Locale.ROOT);
if (!isValidImage(bytes, suffix)) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "图片内容与文件类型不符");
return true;
}
setImageCacheHeaders(response, buildFileEtag(sysFile)); setImageCacheHeaders(response, buildFileEtag(sysFile));
response.setHeader("Content-Disposition", "inline;filename=" + java.net.URLEncoder.encode(sysFile.getName(), java.nio.charset.StandardCharsets.UTF_8)); response.setHeader("Content-Disposition", "inline;filename=" + java.net.URLEncoder.encode(sysFile.getName(), java.nio.charset.StandardCharsets.UTF_8));
response.setHeader("Content-Length", String.valueOf(bytes.length)); response.setHeader("Content-Length", String.valueOf(bytes.length));
@@ -171,9 +165,9 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
} }
private void setImageCacheHeaders(HttpServletResponse response, String etag) { private void setImageCacheHeaders(HttpServletResponse response, String etag) {
response.setHeader("Cache-Control", "public, max-age=604800, immutable"); response.setHeader("Cache-Control", "private, no-cache");
response.setHeader("ETag", etag); response.setHeader("ETag", etag);
response.setDateHeader("Expires", System.currentTimeMillis() + 604800000L); response.setDateHeader("Expires", 0);
} }
private String buildFileEtag(Sys_file sysFile) { private String buildFileEtag(Sys_file sysFile) {
@@ -181,12 +175,12 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
} }
private String buildFileEtag(String id) { private String buildFileEtag(String id) {
return "\"" + id + "\""; return "\"safe-" + id + "\"";
} }
private boolean isImage(Sys_file sysFile) { private boolean isImage(Sys_file sysFile) {
String suffix = StrUtil.blankToDefault(sysFile.getSuffix(), FileUtil.extName(sysFile.getName())).toLowerCase(); String suffix = StrUtil.blankToDefault(sysFile.getSuffix(), FileUtil.extName(sysFile.getName())).toLowerCase();
return "jpg".equals(suffix) || "jpeg".equals(suffix) || "png".equals(suffix) || "gif".equals(suffix) || "webp".equals(suffix) || "bmp".equals(suffix) || "svg".equals(suffix); return "jpg".equals(suffix) || "jpeg".equals(suffix) || "png".equals(suffix) || "gif".equals(suffix) || "webp".equals(suffix) || "bmp".equals(suffix);
} }
private String getImageContentType(Sys_file sysFile) { private String getImageContentType(Sys_file sysFile) {
@@ -197,7 +191,6 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
case "gif" -> "image/gif"; case "gif" -> "image/gif";
case "webp" -> "image/webp"; case "webp" -> "image/webp";
case "bmp" -> "image/bmp"; case "bmp" -> "image/bmp";
case "svg" -> "image/svg+xml";
default -> "application/octet-stream"; default -> "application/octet-stream";
}; };
} }
@@ -310,6 +303,7 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
* 存储文件 * 存储文件
**/ **/
private String storageFile(String engine, TempFile file, boolean returnFileId) { private String storageFile(String engine, TempFile file, boolean returnFileId) {
validateUpload(file);
// 如果引擎为空,默认使用本地 // 如果引擎为空,默认使用本地
if (ObjectUtil.isEmpty(engine)) { if (ObjectUtil.isEmpty(engine)) {
engine = SysFileEngineTypeEnum.LOCAL.getValue(); engine = SysFileEngineTypeEnum.LOCAL.getValue();
@@ -383,6 +377,53 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
} }
} }
/** 所有业务上传共用校验,客户端声明的 MIME 类型不能作为图片真实性依据。 */
private void validateUpload(TempFile file) {
if (file == null || StrUtil.isBlank(file.getSubmittedFileName())) {
throw new BaseException("请选择要上传的文件");
}
String suffix = FileUtil.extName(file.getSubmittedFileName()).toLowerCase(Locale.ROOT);
if (!UPLOAD_SUFFIXES.contains(suffix)) {
throw new BaseException("不允许上传此类型的文件");
}
if (Set.of("gif", "jpg", "jpeg", "png").contains(suffix)) {
try {
if (!isValidImage(Files.readAllBytes(file.getFile().toPath()), suffix)) {
throw new BaseException("图片内容与文件类型不符");
}
} catch (IOException e) {
throw new BaseException("图片读取失败");
}
}
}
/** 解码真实图片并限制像素数量,阻止伪装为图片的 SVG 和异常大尺寸图片。 */
private boolean isValidImage(byte[] bytes, String suffix) {
try (ImageInputStream input = ImageIO.createImageInputStream(new ByteArrayInputStream(bytes))) {
if (input == null) {
return false;
}
Iterator<ImageReader> readers = ImageIO.getImageReaders(input);
if (!readers.hasNext()) {
return false;
}
ImageReader reader = readers.next();
try {
reader.setInput(input, true, true);
String expected = "jpg".equals(suffix) ? "jpeg" : suffix;
if (!expected.equalsIgnoreCase(reader.getFormatName())) {
return false;
}
long pixels = (long) reader.getWidth(0) * reader.getHeight(0);
return pixels > 0 && pixels <= 40000000 && reader.read(0) != null;
} finally {
reader.dispose();
}
} catch (IOException | RuntimeException e) {
return false;
}
}
/** /**
* 存储文件 * 存储文件
*/ */
@@ -10,6 +10,9 @@ import cn.dev33.satoken.servlet.model.SaStorageForServlet;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.Mvcs; import org.nutz.mvc.Mvcs;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponseWrapper;
/** /**
* @author wizzer@qq.com * @author wizzer@qq.com
*/ */
@@ -22,7 +25,20 @@ public class SaTokenContextImpl implements SaTokenContext {
@Override @Override
public SaResponse getResponse() { public SaResponse getResponse() {
return new SaResponseForServlet(Mvcs.getResp()); // 当前 Sa-Token 版本没有 Cookie 安全属性配置,在写入登录 Cookie 时统一补齐。
return new SaResponseForServlet(new HttpServletResponseWrapper(Mvcs.getResp()) {
/**
* 限制登录 Cookie 的脚本访问,并在 HTTPS 请求中仅允许安全传输。
*
* @param cookie Sa-Token 写入或清除的 Cookie
*/
@Override
public void addCookie(Cookie cookie) {
cookie.setHttpOnly(true);
cookie.setSecure(cookie.getSecure() || Mvcs.getReq().isSecure());
super.addCookie(cookie);
}
});
} }
@Override @Override
@@ -1,7 +1,6 @@
package com.budwk.app.web.commons.ext.handler; package com.budwk.app.web.commons.ext.handler;
import org.eclipse.jetty.server.Request; import org.nutz.boot.starter.WebServletFace;
import org.eclipse.jetty.servlet.ErrorPageErrorHandler;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json; import org.nutz.json.Json;
import org.nutz.lang.util.NutMap; import org.nutz.lang.util.NutMap;
@@ -9,28 +8,63 @@ import org.nutz.log.Log;
import org.nutz.log.Logs; import org.nutz.log.Logs;
import javax.servlet.RequestDispatcher; import javax.servlet.RequestDispatcher;
import javax.servlet.DispatcherType;
import javax.servlet.Servlet;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.ServletRequest; import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServlet;
import java.io.IOException; import java.io.IOException;
/** /**
* 错误页拦截器,登陆后台显示友好提示 * 错误页拦截器,登陆后台显示友好提示
*/ */
@IocBean @IocBean
public class WkErrorPageHandler extends ErrorPageErrorHandler { public class WkErrorPageHandler extends HttpServlet implements WebServletFace {
private static final Log log = Logs.get(); private static final Log log = Logs.get();
/**
* 返回容器错误处理 Servlet 的注册名称。
* @return 独立于业务路由的 Servlet 名称
*/
@Override @Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { public String getName() {
return "wkErrorPage";
}
/**
* 返回容器错误页的内部派发地址。
* @return 错误页映射路径
*/
@Override
public String getPathSpec() {
return "/__container_error";
}
/**
* 将当前错误处理实例交给 NutzBoot 注册。
* @return 当前 Servlet 实例
*/
@Override
public Servlet getServlet() {
return this;
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
if (request.getDispatcherType() != DispatcherType.ERROR) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
if (response.getStatus() == 403 || response.getStatus() == 404 || response.getStatus() == 500) { if (response.getStatus() == 403 || response.getStatus() == 404 || response.getStatus() == 500) {
try { try {
if (isAjax(request)) { if (isAjax(request)) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(Json.toJson(new NutMap("code", "-1").setv("msg", response.getStatus() + " error"))); response.getWriter().write(Json.toJson(new NutMap("code", "-1").setv("msg", response.getStatus() + " error")));
return; return;
} else { } else {
request.setAttribute("original_request_uri", request.getRequestURI()); request.setAttribute("original_request_uri", request.getAttribute(RequestDispatcher.ERROR_REQUEST_URI));
RequestDispatcher rd = request.getRequestDispatcher("/platform/home/" + response.getStatus()); RequestDispatcher rd = request.getRequestDispatcher("/platform/home/" + response.getStatus());
rd.forward(request, response); rd.forward(request, response);
return; return;
@@ -39,7 +73,7 @@ public class WkErrorPageHandler extends ErrorPageErrorHandler {
log.error(e); log.error(e);
} }
} }
super.handle(target, baseRequest, request, response); // 不回落到容器默认错误页,避免泄露异常堆栈和容器信息。
} }
private boolean isAjax(ServletRequest req) { private boolean isAjax(ServletRequest req) {
@@ -0,0 +1,88 @@
package com.budwk.app.web.commons.ext.handler;
import com.budwk.app.web.commons.ext.websocket.WkWebSocket;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.Executor;
import org.apache.catalina.core.StandardThreadExecutor;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.valves.ErrorReportValve;
import org.apache.tomcat.websocket.server.WsSci;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import org.nutz.boot.NbApp;
import org.nutz.boot.starter.tomcat.TomcatStarter;
import org.nutz.boot.tools.NbAppEventListener;
import org.nutz.ioc.impl.PropertiesProxy;
import java.util.Set;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
/**
* 在容器启动前补齐 NutzBoot Tomcat 的 WebSocket 注册和线程池绑定。
*/
public class WkTomcatConfiguration implements NbAppEventListener {
/**
* 配置官方嵌入式 Tomcat,保留项目的监听地址与消息推送能力。
*
* @param app 当前 NutzBoot 应用
* @param eventType 只在服务器启动前配置,避免重复注册端点
*/
@Override
public void whenStartServers(NbApp app, EventType eventType) {
if (eventType != EventType.before) {
return;
}
TomcatStarter starter = app.getAppContext().getIoc().get(TomcatStarter.class);
Tomcat tomcat = starter.getServer();
PropertiesProxy conf = app.getAppContext().getConf();
tomcat.getConnector().setProperty("address", starter.getHost());
// Starter 创建了共享线程池,但尚未把它交给 Connector 使用。
for (Executor executor : tomcat.getService().findExecutors()) {
if (executor instanceof StandardThreadExecutor) {
StandardThreadExecutor threads = (StandardThreadExecutor) executor;
threads.setMinSpareThreads(conf.getInt("tomcat.executor.minSpareThreads", 10));
threads.setMaxIdleTime(conf.getInt("tomcat.executor.maxIdleTime", 60000));
tomcat.getConnector().getProtocolHandler().setExecutor(threads);
break;
}
}
// NutzBoot 使用编程式 Context,必须显式运行官方 WebSocket 初始化器。
StandardContext context = (StandardContext) tomcat.getHost().findChild(starter.getContextPath());
context.setUseHttpOnly(conf.getBoolean("tomcat.session.cookie.httponly", true));
context.addServletContainerInitializer(new WsSci(), Set.of(WkWebSocket.class));
// Tomcat 的错误页再次 forward 时仍保持 ERROR 派发;让 MVC 渲染原业务错误页。
// Nutz 的过滤器在前一个监听器中注册,须等其完成后才能添加映射。
context.addApplicationLifecycleListener(new ServletContextListener() {
/**
* 在 Nutz 注册完成后补充业务错误页的 ERROR 派发映射。
* @param event 当前 Servlet 上下文初始化事件
*/
@Override
public void contextInitialized(ServletContextEvent event) {
FilterMap errorPages = new FilterMap();
errorPages.setFilterName("nutz");
errorPages.setDispatcher("ERROR");
// ERROR 再转发时 URL 匹配路径不变,按目标 Servlet 匹配才能进入 MVC。
errorPages.addServletName("default");
context.addFilterMap(errorPages);
}
});
// Host 的兜底错误页也不能重新暴露此前已关闭的版本和堆栈。
ErrorReportValve errorReport = null;
for (org.apache.catalina.Valve valve : tomcat.getHost().getPipeline().getValves()) {
if (valve instanceof ErrorReportValve) {
errorReport = (ErrorReportValve) valve;
break;
}
}
if (errorReport == null) {
errorReport = new ErrorReportValve();
tomcat.getHost().getPipeline().addValve(errorReport);
}
errorReport.setShowReport(false);
errorReport.setShowServerInfo(false);
}
}
@@ -54,7 +54,7 @@ import java.util.List;
@IocBean @IocBean
@At("/platform/unionReimburse/apply") @At("/platform/unionReimburse/apply")
@Api("工会报销申请") @Api("工会报销申请")
@Ok("json:full") @Ok("json:{locked:'condolenceIdCard|condolenceBirthday|condolenceMobile',ignoreNull:false}")
@Slf4j @Slf4j
public class UnionReimburseApplyController { public class UnionReimburseApplyController {
@@ -151,14 +151,11 @@ public class UnionReimburseApplyController {
username as userName, username as userName,
loginname as loginName, loginname as loginName,
sex, sex,
mobile,
technicalTitle, technicalTitle,
IFNULL(unitname, '暂无') as unitName, IFNULL(unitname, '暂无') as unitName,
unitid as unitId, unitid as unitId,
unionid as unionId, unionid as unionId,
unionname as unionName, unionname as unionName,
DATE(birthday) AS birthday,
idCard,
unionCode unionCode
from from
vw_user vw_user
@@ -173,6 +170,9 @@ public class UnionReimburseApplyController {
} }
cnd.and(View_user::getId, "!=", SecurityUtil.getUserId()); cnd.and(View_user::getId, "!=", SecurityUtil.getUserId());
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) { if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
if (StrUtil.isBlank(SecurityUtil.getUnionId())) {
return Result.success(java.util.Collections.emptyList());
}
cnd.and(View_user::getUnionId, "=", SecurityUtil.getUnionId()); cnd.and(View_user::getUnionId, "=", SecurityUtil.getUnionId());
} }
sql.setCondition(cnd); sql.setCondition(cnd);
@@ -34,7 +34,7 @@ import java.util.List;
*/ */
@IocBean @IocBean
@At("/platform/unionReimburse/certifierUserSign") @At("/platform/unionReimburse/certifierUserSign")
@Ok("json:full") @Ok("json:{locked:'condolenceIdCard|condolenceBirthday|condolenceMobile',ignoreNull:false}")
@Api("证明人签字") @Api("证明人签字")
@Slf4j @Slf4j
public class UnionReimburseCertifierUserSignController { public class UnionReimburseCertifierUserSignController {
@@ -68,7 +68,7 @@ import java.util.HashMap;
@IocBean @IocBean
@At("/platform/unionReimburse/mine") @At("/platform/unionReimburse/mine")
@Api("工会报销我的") @Api("工会报销我的")
@Ok("json:full") @Ok("json:{locked:'condolenceIdCard|condolenceBirthday|condolenceMobile',ignoreNull:false}")
@Slf4j @Slf4j
public class UnionReimburseMineController { public class UnionReimburseMineController {
@@ -29,7 +29,7 @@ import java.util.List;
@IocBean @IocBean
@At("/platform/unionReimburse/review") @At("/platform/unionReimburse/review")
@Ok("json:full") @Ok("json:{locked:'condolenceIdCard|condolenceBirthday|condolenceMobile',ignoreNull:false}")
@Api("工会报销审核") @Api("工会报销审核")
public class UnionReimburseReviewController { public class UnionReimburseReviewController {
@@ -38,7 +38,7 @@ import java.util.List;
@IocBean @IocBean
@At("/platform/unionReimburse/statistics") @At("/platform/unionReimburse/statistics")
@Ok("json:full") @Ok("json:{locked:'condolenceIdCard|condolenceBirthday|condolenceMobile',ignoreNull:false}")
@Api("慰问统计") @Api("慰问统计")
@Slf4j @Slf4j
public class UnionReimburseStatisticsController { public class UnionReimburseStatisticsController {
@@ -4,10 +4,12 @@ import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant; import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.page.Pagination; import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result; import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.models.Sys_file; import com.budwk.app.sys.models.Sys_file;
import com.budwk.app.sys.views.View_user;
import com.budwk.app.sys.services.SysFileService; import com.budwk.app.sys.services.SysFileService;
import com.budwk.app.zhgh.club.model.ClubUser; import com.budwk.app.zhgh.club.model.ClubUser;
import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.club.model.OutlayManageClub; import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.club.model.OutlayManageClub;
@@ -125,6 +127,37 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
if (oldRecord == null) { if (oldRecord == null) {
return Result.error("未找到对应的报销记录"); return Result.error("未找到对应的报销记录");
} }
if (!isAdmin() && !Objects.equals(oldRecord.getUserId(), SecurityUtil.getUserId())) {
return Result.error("无权修改此报销申请");
}
}
// 人员身份和敏感资料由服务端补齐,避免前端删减字段导致丢失或被篡改。
if (oldRecord == null) {
View_user handler = dao().fetch(View_user.class, Cnd.where("id", "=", SecurityUtil.getUserId()));
if (handler == null) {
return Result.error("经办人信息不存在");
}
unionReimburse.setUserId(handler.getId());
unionReimburse.setUserName(handler.getUsername());
unionReimburse.setLoginName(handler.getLoginname());
unionReimburse.setUnitId(handler.getUnitId());
unionReimburse.setUnitName(handler.getUnitName());
unionReimburse.setUnionId(handler.getUnionId());
unionReimburse.setUnionName(handler.getUnionName());
} else {
// 修改历史单据时保留原经办组织,避免人员调动改变经费归属。
unionReimburse.setUserId(oldRecord.getUserId());
unionReimburse.setUserName(oldRecord.getUserName());
unionReimburse.setLoginName(oldRecord.getLoginName());
unionReimburse.setUnitId(oldRecord.getUnitId());
unionReimburse.setUnitName(oldRecord.getUnitName());
unionReimburse.setUnionId(oldRecord.getUnionId());
unionReimburse.setUnionName(oldRecord.getUnionName());
}
Result recipientResult = fillCondolencePersonalData(unionReimburse, oldRecord);
if (recipientResult != null) {
return recipientResult;
} }
normalizeInvoiceDetails(unionReimburse); normalizeInvoiceDetails(unionReimburse);
@@ -179,14 +212,104 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
if (unionReimburse == null) { if (unionReimburse == null) {
return null; return null;
} }
if (!canReadApply(unionReimburse)) {
throw new BaseException("无权查看此报销申请");
}
List<UnionReimburseInvoiceDetail> invoiceDetails = this.dao().query( List<UnionReimburseInvoiceDetail> invoiceDetails = this.dao().query(
UnionReimburseInvoiceDetail.class, UnionReimburseInvoiceDetail.class,
Cnd.where("reimburseId", "=", id).asc("createdAt") Cnd.where("reimburseId", "=", id).asc("createdAt")
); );
unionReimburse.setInvoiceDetails(invoiceDetails == null ? new ArrayList<>() : invoiceDetails); unionReimburse.setInvoiceDetails(invoiceDetails == null ? new ArrayList<>() : invoiceDetails);
// 申请页和详情页共用此接口,原始身份资料仅保留在服务端业务记录中。
unionReimburse.setCondolenceIdCard(null);
unionReimburse.setCondolenceBirthday(null);
unionReimburse.setCondolenceMobile(null);
return unionReimburse; return unionReimburse;
} }
/** 保留历史单据的身份资料快照,新选人员须符合人员查询的数据范围。 */
private Result fillCondolencePersonalData(UnionReimburse form, UnionReimburse oldRecord) {
form.setCondolenceIdCard(null);
form.setCondolenceBirthday(null);
form.setCondolenceMobile(null);
if (!"UNION_REIMBURSE_PROJECT_1".equals(form.getReimburseProject())) {
return null;
}
if (StrUtil.isBlank(form.getCondolenceUserId())) {
return null;
}
if (oldRecord != null && Objects.equals(oldRecord.getCondolenceUserId(), form.getCondolenceUserId())) {
form.setCondolenceIdCard(oldRecord.getCondolenceIdCard());
form.setCondolenceBirthday(oldRecord.getCondolenceBirthday());
form.setCondolenceMobile(oldRecord.getCondolenceMobile());
form.setCondolenceUnionId(oldRecord.getCondolenceUnionId());
form.setCondolenceUnitId(oldRecord.getCondolenceUnitId());
form.setCondolenceUserName(oldRecord.getCondolenceUserName());
form.setCondolenceLoginName(oldRecord.getCondolenceLoginName());
form.setCondolenceUnitName(oldRecord.getCondolenceUnitName());
form.setCondolenceUnionName(oldRecord.getCondolenceUnionName());
form.setCondolenceUnionCode(oldRecord.getCondolenceUnionCode());
form.setCondolenceSex(oldRecord.getCondolenceSex());
return null;
}
View_user user = dao().fetch(View_user.class, Cnd.where("id", "=", form.getCondolenceUserId()));
if (user == null || Objects.equals(user.getId(), SecurityUtil.getUserId())
|| (!isAdmin() && (StrUtil.isBlank(SecurityUtil.getUnionId())
|| !Objects.equals(SecurityUtil.getUnionId(), user.getUnionId())))) {
return Result.error("无权选择此慰问对象");
}
form.setCondolenceUserName(user.getUsername());
form.setCondolenceLoginName(user.getLoginname());
form.setCondolenceUnitId(user.getUnitId());
form.setCondolenceUnitName(user.getUnitName());
form.setCondolenceUnionId(user.getUnionId());
form.setCondolenceUnionName(user.getUnionName());
form.setCondolenceUnionCode(user.getUnionCode());
form.setCondolenceSex(user.getSex());
form.setCondolenceIdCard(user.getIdCard());
form.setCondolenceBirthday(user.getBirthday() == null ? null : DateUtil.formatDate(user.getBirthday()));
form.setCondolenceMobile(user.getMobile());
return null;
}
/** 详情访问沿用经办、审核、统计和流程参与范围,不能仅凭单据 ID 查询。 */
private boolean canReadApply(UnionReimburse form) {
String userId = SecurityUtil.getUserId();
if (StrUtil.isBlank(userId)) {
return false;
}
if (isAdmin() || userId.equals(form.getUserId())) {
return true;
}
if (form.getStateId() != 1 && (AuthUtil.hasPermission("unionReimburse.review")
|| AuthUtil.hasPermission("h5.unionReimburse.review"))) {
return true;
}
if ((form.getStateId() == 2 || form.getStateId() == 3)
&& StrUtil.isNotBlank(SecurityUtil.getUnionId())
&& Objects.equals(SecurityUtil.getUnionId(), form.getCondolenceUnionId())
&& (AuthUtil.hasPermission("unionReimburse.statistics")
|| AuthUtil.hasPermission("h5.unionReimburse.statistics"))) {
return true;
}
// 旧流程单据允许任务参与人和抄送人回看,关系必须由数据库确认。
Sql sql = Sqls.create("""
SELECT COUNT(*) FROM wf_process_instance i
WHERE i.businessNo = @id AND (
EXISTS (SELECT 1 FROM wf_process_task t
INNER JOIN wf_process_task_actor a ON a.processTaskId = t.id
WHERE t.processInstanceId = i.id AND a.actorId = @userId)
OR EXISTS (SELECT 1 FROM wf_process_cc_instance c
WHERE c.processInstanceId = i.id AND c.actorId = @userId)
)
""");
sql.setParam("id", form.getId());
sql.setParam("userId", userId);
sql.setCallback(Sqls.callback.integer());
dao().execute(sql);
return sql.getInt() > 0;
}
@Override @Override
public void deleteApply(String id) { public void deleteApply(String id) {
if (StrUtil.isBlank(id)) { if (StrUtil.isBlank(id)) {
@@ -242,14 +365,11 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
u.username AS userName, u.username AS userName,
u.loginname AS loginName, u.loginname AS loginName,
u.sex, u.sex,
u.mobile,
u.technicalTitle, u.technicalTitle,
IFNULL(u.unitname, '暂无') AS unitName, IFNULL(u.unitname, '暂无') AS unitName,
u.unitid AS unitId, u.unitid AS unitId,
u.unionid AS unionId, u.unionid AS unionId,
u.unionname AS unionName, u.unionname AS unionName,
DATE(u.birthday) AS birthday,
u.idCard,
u.unionCode u.unionCode
FROM FROM
vw_user u vw_user u
@@ -11,7 +11,6 @@ import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.eclipse.jetty.util.StringUtil;
import org.nutz.dao.entity.annotation.*; import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert; import org.nutz.dao.interceptor.annotation.PrevInsert;
@@ -161,13 +160,13 @@ public class MallGoods extends BaseModel {
//循环sku,判定sku是否有效 //循环sku,判定sku是否有效
for (Map<String, Object> sku : goodsOperationDTO.getSkuList()) { for (Map<String, Object> sku : goodsOperationDTO.getSkuList()) {
//判定参数不能为空 //判定参数不能为空
if ((!sku.containsKey("price") || StringUtil.isEmpty(sku.get("price").toString()) || Convert.toDouble(sku.get("price")) <= 0)) { if ((!sku.containsKey("price") || CharSequenceUtil.isEmpty(sku.get("price").toString()) || Convert.toDouble(sku.get("price")) <= 0)) {
throw new BaseException("商品SKU价格不能小于等于0"); throw new BaseException("商品SKU价格不能小于等于0");
} }
// if ((!sku.containsKey("cost") || StringUtil.isEmpty(sku.get("cost").toString()) || Convert.toDouble(sku.get("cost")) <= 0)) { // if ((!sku.containsKey("cost") || StringUtil.isEmpty(sku.get("cost").toString()) || Convert.toDouble(sku.get("cost")) <= 0)) {
// throw new BaseException("商品SKU成本价不能小于等于0"); // throw new BaseException("商品SKU成本价不能小于等于0");
// } // }
if (!sku.containsKey("quantity") || StringUtil.isEmpty(sku.get("quantity").toString()) || Convert.toInt(sku.get("quantity").toString()) < 0) { if (!sku.containsKey("quantity") || CharSequenceUtil.isEmpty(sku.get("quantity").toString()) || Convert.toInt(sku.get("quantity").toString()) < 0) {
throw new BaseException("商品库存数量不能为负数"); throw new BaseException("商品库存数量不能为负数");
} }
sku.values().forEach(i -> { sku.values().forEach(i -> {
@@ -127,7 +127,6 @@ public class CondolenceApplyController {
username as userName, username as userName,
loginname as loginName, loginname as loginName,
sex, sex,
mobile,
technicalTitle, technicalTitle,
IFNULL(unitname, '暂无') as unitName, IFNULL(unitname, '暂无') as unitName,
unitid as unitId, unitid as unitId,
@@ -146,6 +145,9 @@ public class CondolenceApplyController {
} }
if(!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) { if(!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
if(AuthUtil.hasRoleOr(RoleConstant.BRANCH_UNION_ADMIN.name(), RoleConstant.BRANCH_UNION_CHAIRMAN.name())) { if(AuthUtil.hasRoleOr(RoleConstant.BRANCH_UNION_ADMIN.name(), RoleConstant.BRANCH_UNION_CHAIRMAN.name())) {
if (StrUtil.isBlank(SecurityUtil.getUnionId())) {
return Result.success(java.util.Collections.emptyList());
}
cnd.and(View_user::getUnionId, "=", SecurityUtil.getUnionId()); cnd.and(View_user::getUnionId, "=", SecurityUtil.getUnionId());
} else { } else {
cnd.and(View_user::getId, "=", SecurityUtil.getUserId()); cnd.and(View_user::getId, "=", SecurityUtil.getUserId());
+12 -8
View File
@@ -16,16 +16,20 @@ sm4cbc:
keyHex: 00112233445566778899aabbccddeeff keyHex: 00112233445566778899aabbccddeeff
ivHex: 0102030405060708090a0b0c0d0e0f10 ivHex: 0102030405060708090a0b0c0d0e0f10
jetty: tomcat:
contextPath: / contextPath: ""
threadpool: # 容器错误响应不披露版本;Session Cookie 保持 HttpOnly。
idleTimeout: 60000 session:
minThreads: 10 cookie:
httponly: true
executor:
maxIdleTime: 60000
minSpareThreads: 10
maxThreads: 500 maxThreads: 500
page: page:
403: /error/403.html 403: /__container_error
404: /error/404.html 404: /__container_error
500: /error/500.html 500: /__container_error
#结合ftp使用,或用nginx代理ftp路径 #结合ftp使用,或用nginx代理ftp路径
#staticPath: /Users/wizzer/temp/files #staticPath: /Users/wizzer/temp/files
#开发模式静态资源 #开发模式静态资源
+2 -2
View File
@@ -14,7 +14,7 @@ var ioc={
// 单个文件最大尺寸(大约的值,单位为字节,即 2097152 为 2M) // 单个文件最大尺寸(大约的值,单位为字节,即 2097152 为 2M)
maxFileSize : 2097152, maxFileSize : 2097152,
// 正则表达式匹配可以支持的文件名 // 正则表达式匹配可以支持的文件名
nameFilter : '^(.+[.])(gif|jpg|png|svg)$' nameFilter : '(?i)^(.+[.])(gif|jpg|jpeg|png)$'
} }
}, },
imageUpload : { imageUpload : {
@@ -32,7 +32,7 @@ var ioc={
// 单个文件最大尺寸(大约的值,单位为字节,即 209715200 为 200M) // 单个文件最大尺寸(大约的值,单位为字节,即 209715200 为 200M)
maxFileSize : 209715200, maxFileSize : 209715200,
// 正则表达式匹配可以支持的文件名 // 正则表达式匹配可以支持的文件名
nameFilter : '^(.+[.])(gif||svg|jpg|png|jpeg|doc|docx|xls|xlsx|rar|zip|7z|txt|pdf|pptx|ppt|mp4)$' nameFilter : '(?i)^(.+[.])(gif|jpg|png|jpeg|doc|docx|xls|xlsx|rar|zip|7z|txt|pdf|pptx|ppt|mp4)$'
} }
}, },
fileUpload : { fileUpload : {
@@ -187,31 +187,6 @@ layout("/layouts/platform.html"){
</el-form-item> </el-form-item>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="生日"
v-if="formData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'">
<el-form-item prop="condolenceBirthday" label="生日">
<el-input
:value="formData.condolenceBirthday && $moment(formData.condolenceBirthday).isValid() ?$moment(formData.condolenceBirthday).format('YYYY-MM-DD') : ''"
show-word-limit placeholder="请输入生日" disabled></el-input>
</el-form-item>
</el-descriptions-item>
<el-descriptions-item label="身份证号"
v-if="formData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'">
<el-form-item prop="condolenceIdCard" label="身份证号">
<el-input v-model="formData.condolenceIdCard" show-word-limit
placeholder="请输入身份证号" disabled></el-input>
</el-form-item>
</el-descriptions-item>
<el-descriptions-item label="联系方式"
v-if="formData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'">
<el-form-item prop="condolenceMobile" label="联系方式">
<el-input v-model="formData.condolenceMobile" show-word-limit
placeholder="请输入联系方式" disabled></el-input>
</el-form-item>
</el-descriptions-item>
<el-descriptions-item label="慰问类型" <el-descriptions-item label="慰问类型"
v-if="formData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'"> v-if="formData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'">
<el-form-item label="慰问类型" prop="condolenceTypeId"> <el-form-item label="慰问类型" prop="condolenceTypeId">
@@ -816,7 +791,6 @@ layout("/layouts/platform.html"){
trigger: ["change", "blur"] trigger: ["change", "blur"]
}, {pattern: /^([1-9]{1})(\d{15}|\d{18})$/, message: "请输入正确的银行卡号", trigger: "blur"}], }, {pattern: /^([1-9]{1})(\d{15}|\d{18})$/, message: "请输入正确的银行卡号", trigger: "blur"}],
bankOfDeposit: [{required: true, message: "请填写开户行", trigger: ["change", "blur"]}], bankOfDeposit: [{required: true, message: "请填写开户行", trigger: ["change", "blur"]}],
condolenceMobile: [{pattern: /^1[3-9]\d{9}$/, message: "请输入正确的手机号码", trigger: "blur"}],
condolenceTime: [{required: true, message: "请选择慰问时间", trigger: ["change", "blur"]}], condolenceTime: [{required: true, message: "请选择慰问时间", trigger: ["change", "blur"]}],
// invoiceNumber: [{required: true, message: "请填写发票张数", trigger: ["change", "blur"]}], // invoiceNumber: [{required: true, message: "请填写发票张数", trigger: ["change", "blur"]}],
// invoice: [{required: true, message: "请填写发票号码", trigger: ["change", "blur"]}], // invoice: [{required: true, message: "请填写发票号码", trigger: ["change", "blur"]}],
@@ -1187,10 +1161,7 @@ layout("/layouts/platform.html"){
unionId, unionId,
unionName, unionName,
unionCode, unionCode,
sex, sex
birthday,
idCard,
mobile
} = user } = user
this.$set(this.formData, "condolenceUserName", userName) this.$set(this.formData, "condolenceUserName", userName)
this.$set(this.formData, "condolenceLoginName", loginName) this.$set(this.formData, "condolenceLoginName", loginName)
@@ -1200,9 +1171,6 @@ layout("/layouts/platform.html"){
this.$set(this.formData, "condolenceUnionName", unionName) this.$set(this.formData, "condolenceUnionName", unionName)
this.$set(this.formData, "condolenceUnionCode", unionCode) this.$set(this.formData, "condolenceUnionCode", unionCode)
this.$set(this.formData, "condolenceSex", sex) this.$set(this.formData, "condolenceSex", sex)
this.$set(this.formData, "condolenceBirthday", birthday)
this.$set(this.formData, "condolenceIdCard", idCard)
this.$set(this.formData, "condolenceMobile", mobile)
} }
}, },
typeChange(val) { typeChange(val) {
@@ -86,18 +86,6 @@ const unionReimburseInfo = {
<span>{{ viewData.condolenceSex }}</span> <span>{{ viewData.condolenceSex }}</span>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="生日" v-if="viewData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'">
<span>{{ viewData.condolenceBirthday && $moment(viewData.condolenceBirthday).isValid() ? $moment(viewData.condolenceBirthday).format('YYYY-MM-DD') : '' }}</span>
</el-descriptions-item>
<el-descriptions-item label="身份证号" v-if="viewData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'">
<span>{{ viewData.condolenceIdCard }}</span>
</el-descriptions-item>
<el-descriptions-item label="联系方式" v-if="viewData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'">
<span>{{ viewData.condolenceMobile }}</span>
</el-descriptions-item>
<el-descriptions-item label="慰问类型" v-if="viewData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'"> <el-descriptions-item label="慰问类型" v-if="viewData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'">
<span>{{ viewData.typeName }}</span> <span>{{ viewData.typeName }}</span>
</el-descriptions-item> </el-descriptions-item>
@@ -579,9 +579,6 @@ layout("/layouts/platform_h5.html"){
</van-action-sheet> </van-action-sheet>
<van-field label="性别" v-model="formData.condolenceSex" readonly placeholder="自动带出"></van-field> <van-field label="性别" v-model="formData.condolenceSex" readonly placeholder="自动带出"></van-field>
<van-field label="生日" :value="formatDateText(formData.condolenceBirthday)" readonly placeholder="自动带出"></van-field>
<van-field label="身份证号" v-model="formData.condolenceIdCard" readonly placeholder="自动带出"></van-field>
<van-field label="联系方式" v-model="formData.condolenceMobile" readonly placeholder="自动带出"></van-field>
<van-field <van-field
v-model="formData.typeName" v-model="formData.typeName"
@@ -1412,7 +1409,7 @@ layout("/layouts/platform_h5.html"){
}, },
normalizeContactFields() { normalizeContactFields() {
// 提交前裁剪联系方式空格,避免系统手机号带空格时后端格式校验失败。 // 提交前裁剪联系方式空格,避免系统手机号带空格时后端格式校验失败。
const fields = ["mobile", "condolenceMobile"] const fields = ["mobile"]
fields.forEach((fieldName) => { fields.forEach((fieldName) => {
if (this.formData[fieldName]) { if (this.formData[fieldName]) {
this.$set(this.formData, fieldName, String(this.formData[fieldName]).trim()) this.$set(this.formData, fieldName, String(this.formData[fieldName]).trim())
@@ -2209,7 +2206,6 @@ layout("/layouts/platform_h5.html"){
const selectedUser = this.condolenceOptions.find(user => user.id === o.value) const selectedUser = this.condolenceOptions.find(user => user.id === o.value)
if (selectedUser) { if (selectedUser) {
this.fillCondolenceUser(selectedUser) this.fillCondolenceUser(selectedUser)
this.$set(this.formData, "condolenceMobile", selectedUser.mobile)
} }
}, },
onCondolenceTypeConfirm(o) { onCondolenceTypeConfirm(o) {
@@ -2341,9 +2337,6 @@ layout("/layouts/platform_h5.html"){
this.$set(this.formData, "condolenceUnionName", user.unionName) this.$set(this.formData, "condolenceUnionName", user.unionName)
this.$set(this.formData, "condolenceUnionCode", user.unionCode) this.$set(this.formData, "condolenceUnionCode", user.unionCode)
this.$set(this.formData, "condolenceSex", user.sex) this.$set(this.formData, "condolenceSex", user.sex)
this.$set(this.formData, "condolenceBirthday", user.birthday)
this.$set(this.formData, "condolenceIdCard", user.idCard)
this.$set(this.formData, "condolenceMobile", user.mobile)
}, },
// 查询慰问类型 // 查询慰问类型
queryCondolenceType() { queryCondolenceType() {
@@ -100,18 +100,6 @@ const UNION_REIMBURSE_INFO = {
<div class="union-info-label">性别</div> <div class="union-info-label">性别</div>
<div class="union-info-value">{{ viewData.condolenceSex }}</div> <div class="union-info-value">{{ viewData.condolenceSex }}</div>
</div> </div>
<div class="union-info-row">
<div class="union-info-label">生日</div>
<div class="union-info-value">{{ formatDateText(viewData.condolenceBirthday) }}</div>
</div>
<div class="union-info-row">
<div class="union-info-label">身份证号</div>
<div class="union-info-value">{{ viewData.condolenceIdCard }}</div>
</div>
<div class="union-info-row">
<div class="union-info-label">联系方式</div>
<div class="union-info-value">{{ viewData.condolenceMobile }}</div>
</div>
<div class="union-info-row"> <div class="union-info-row">
<div class="union-info-label">慰问类型</div> <div class="union-info-label">慰问类型</div>
<div class="union-info-value">{{ viewData.typeName || viewData.condolenceTypeName }}</div> <div class="union-info-value">{{ viewData.typeName || viewData.condolenceTypeName }}</div>
@@ -0,0 +1 @@
71eb5eaecf2b7fd91d9dd7409a56314b9f2f132b3dffbb9e13ee56d0999b8c59
@@ -0,0 +1 @@
0d9b4629144a1fd15ed01362ce0dc2c9c5246e4f57250dd95fb52dbbbb75c532
@@ -0,0 +1,150 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
<version>9.4.57.v20241219</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-client</artifactId>
<version>9.4.57.v20241219-cug-patch1</version>
<name>Jetty :: Asynchronous HTTP Client</name>
<properties>
<bundle-symbolic-name>${project.groupId}.client</bundle-symbolic-name>
<jetty.test.policy.loc>target/test-policy</jetty.test.policy.loc>
<spotbugs.onlyAnalyze>org.eclipse.client.*</spotbugs.onlyAnalyze>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>unpack</id>
<phase>generate-test-resources</phase>
<goals>
<goal>unpack</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-test-policy</artifactId>
<version>${jetty-test-policy.version}</version>
<type>jar</type>
<overWrite>true</overWrite>
<includes>**/*.keystore,**/*.pem</includes>
<outputDirectory>${jetty.test.policy.loc}</outputDirectory>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<minimizeJar>true</minimizeJar>
<shadedArtifactAttached>true</shadedArtifactAttached>
<shadedClassifierName>hybrid</shadedClassifierName>
<artifactSet>
<includes>
<include>org.eclipse.jetty:jetty-http</include>
<include>org.eclipse.jetty:jetty-io</include>
<include>org.eclipse.jetty:jetty-util</include>
</includes>
</artifactSet>
<relocations>
<relocation>
<pattern>org.eclipse.jetty.http</pattern>
<shadedPattern>org.eclipse.jetty.client.shaded.http</shadedPattern>
</relocation>
<relocation>
<pattern>org.eclipse.jetty.io</pattern>
<shadedPattern>org.eclipse.jetty.client.shaded.io</shadedPattern>
</relocation>
<relocation>
<pattern>org.eclipse.jetty.util</pattern>
<shadedPattern>org.eclipse.jetty.client.shaded.util</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-http</artifactId>
<version>9.4.57.v20241219-cug-patch1</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-io</artifactId>
<version>9.4.57.v20241219</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-jmx</artifactId>
<version>9.4.57.v20241219</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>9.4.57.v20241219-cug-patch1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-security</artifactId>
<version>9.4.57.v20241219-cug-patch1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.kerby</groupId>
<artifactId>kerb-simplekdc</artifactId>
<version>2.1.0</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.jline</groupId>
<artifactId>jline</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- transitive dependency defined as a range and we don't want that -->
<dependency>
<groupId>net.minidev</groupId>
<artifactId>json-smart</artifactId>
<version>2.5.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-test-helper</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1 @@
989a013455688dc528a988c41514fbfece0e3354a54398af5a404cda582c94bd
@@ -0,0 +1 @@
dcfe694ef6f27d9d6268e2fb135a1c9a7aabdfa55e7d738fc8df79f573086027
@@ -0,0 +1 @@
90058e595b73d85b96e192b7e6806098f83467680a95466974f445d6706ed682
@@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>jetty-project</artifactId>
<groupId>org.eclipse.jetty</groupId>
<version>9.4.57.v20241219</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-http</artifactId>
<version>9.4.57.v20241219-cug-patch1</version>
<name>Jetty :: Http Utility</name>
<properties>
<bundle-symbolic-name>${project.groupId}.http</bundle-symbolic-name>
<spotbugs.onlyAnalyze>org.eclipse.jetty.http.*</spotbugs.onlyAnalyze>
</properties>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
<version>9.4.57.v20241219</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-io</artifactId>
<version>9.4.57.v20241219</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-test-helper</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<extensions>true</extensions>
<configuration>
<instructions>
<Require-Capability>osgi.serviceloader; filter:="(osgi.serviceloader=org.eclipse.jetty.http.HttpFieldPreEncoder)";resolution:=optional;cardinality:=multiple, osgi.extender; filter:="(osgi.extender=osgi.serviceloader.processor)";resolution:=optional, osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)";resolution:=optional</Require-Capability>
<Provide-Capability>osgi.serviceloader; osgi.serviceloader=org.eclipse.jetty.http.HttpFieldPreEncoder</Provide-Capability>
</instructions>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<executions>
<execution>
<id>test-jar</id>
<goals>
<goal>test-jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<finalName>${jmhjar.name}</finalName>
<shadeTestJar>true</shadeTestJar>
<artifactSet>
<includes>
<include>org.openjdk.jmh:jmh-core</include>
</includes>
</artifactSet>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.openjdk.jmh.Main</mainClass>
</transformer>
</transformers>
<filters>
<filter>
<artifact>org.openjdk.jmh:jmh-core</artifact>
<includes>
<include>**</include>
</includes>
</filter>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1 @@
967fff3457fb62eb7485fe303cc4162edc1c02b15498c2f8401fbca263649ab8
@@ -0,0 +1 @@
770e71875fa37effaab5c11e932b9a52c2e019236062575b0403760242b0bf08
@@ -0,0 +1 @@
d757e04e165d940c019e9346c36e9bf8d1a71c8b6b196519bda4200e2a16f5d4
@@ -0,0 +1,89 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
<version>9.4.57.v20241219</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-jaspi</artifactId>
<version>9.4.57.v20241219-cug-patch1</version>
<name>Jetty :: JASPI Security</name>
<description>Jetty security infrastructure</description>
<properties>
<bundle-symbolic-name>${project.groupId}.security.jaspi</bundle-symbolic-name>
<spotbugs.onlyAnalyze>org.eclipse.jetty.jaspi.*</spotbugs.onlyAnalyze>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>manifest</goal>
</goals>
<configuration>
<instructions>
<Require-Capability>osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)"</Require-Capability>
<Provide-Capability>osgi.serviceloader;osgi.serviceloader=org.eclipse.jetty.security.Authenticator$Factory</Provide-Capability>
</instructions>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-security</artifactId>
<version>9.4.57.v20241219-cug-patch1</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-test-helper</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.orbit</groupId>
<artifactId>javax.security.auth.message</artifactId>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<!-- Must stay on 3.x as 4.x is for Java 11 -->
<version>3.0.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>javax.activation-api</artifactId>
<version>1.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.geronimo.components</groupId>
<artifactId>geronimo-jaspi</artifactId>
<version>2.0.0</version>
<exclusions>
<exclusion>
<groupId>org.apache.geronimo.specs</groupId>
<artifactId>geronimo-jaspic_1.0_spec</artifactId>
</exclusion>
</exclusions>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1 @@
8e49cd7df44f050c96d896726129fa5c9d0ac5895261cda774c8c899e617e5a6
@@ -0,0 +1 @@
a89e010985a3a347c700c32a535f92f89b6c2548cabcf5e434563b2a59c37b2d
@@ -0,0 +1,56 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
<version>9.4.57.v20241219</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-security</artifactId>
<version>9.4.57.v20241219-cug-patch1</version>
<name>Jetty :: Security</name>
<description>Jetty security infrastructure</description>
<properties>
<bundle-symbolic-name>${project.groupId}.security</bundle-symbolic-name>
<spotbugs.onlyAnalyze>org.eclipse.jetty.security.*</spotbugs.onlyAnalyze>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>manifest</goal>
</goals>
<configuration>
<instructions>
<Require-Capability>osgi.serviceloader; filter:="(osgi.serviceloader=org.eclipse.jetty.security.Authenticator$Factory)";resolution:=optional;cardinality:=multiple, osgi.extender; filter:="(osgi.extender=osgi.serviceloader.processor)";resolution:=optional</Require-Capability>
</instructions>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>9.4.57.v20241219-cug-patch1</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-http</artifactId>
<version>9.4.57.v20241219-cug-patch1</version>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-test-helper</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1 @@
c2697118caa818c3f6b17bce98e60f971a56905a96b69eef9b3f34d0bcd1e729
@@ -0,0 +1 @@
e9f670a373dd91b8ce752d1cc601ffb4890a8cffe67a3fb3addca09c9b0d3405
@@ -0,0 +1 @@
d2a396bd5f8f30ed1b6664e4cc07ddd0df9e1ef4a25452291f24e475890ef037
@@ -0,0 +1,108 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
<version>9.4.57.v20241219</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-server</artifactId>
<version>9.4.57.v20241219-cug-patch1</version>
<name>Jetty :: Server Core</name>
<description>The core jetty server artifact.</description>
<properties>
<bundle-symbolic-name>${project.groupId}.server</bundle-symbolic-name>
<spotbugs.onlyAnalyze>org.eclipse.jetty.server.*</spotbugs.onlyAnalyze>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<executions>
<execution>
<id>test-jar</id>
<goals>
<goal>test-jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>copy-resources-keystore</id>
<phase>process-resources</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<encoding>UTF-8</encoding>
<outputDirectory>${project.build.directory}/jetty-config-files</outputDirectory>
<resources>
<resource>
<directory>src/main/config</directory>
<includes>
<include>**/**keystore**</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-http</artifactId>
<version>9.4.57.v20241219-cug-patch1</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-io</artifactId>
<version>9.4.57.v20241219</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-xml</artifactId>
<version>9.4.57.v20241219</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-jmx</artifactId>
<version>9.4.57.v20241219</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-test-helper</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-http</artifactId>
<version>9.4.57.v20241219-cug-patch1</version>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util-ajax</artifactId>
<version>9.4.57.v20241219</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1 @@
e1bf730db18ea8cd98c95bee03673b3601f34322e30e91d3cfd8210c0d7dd00c