Compare commits

...

8 Commits

Author SHA1 Message Date
疯狂的狮子Li 14f9193703 update 优化 增加snailai-server相关docker配置 2026-06-10 09:17:08 +08:00
疯狂的狮子Li 060da45f84 update 优化 登录异常提示 2026-06-09 10:30:05 +08:00
疯狂的狮子Li 1cb5d841d9 update 恢复前端异常返回内容 2026-06-09 10:22:07 +08:00
AprilWind 00a1c1037c update 增加对客户端ID与Token不匹配的错误信息处理 2026-06-09 09:51:06 +08:00
AprilWind 9e192f5197 update 优化NotLoginException处理,提供更详细的错误信息 2026-06-09 09:30:56 +08:00
疯狂的狮子Li 4875594bcb update 优化 代码生成前端模板 2026-06-08 18:48:14 +08:00
疯狂的狮子Li 90f8142d0b update 优化代码小优化项
- 抄送历史任务为空时改为明确的 ServiceException;当前任务办理人查询改为 ID 去重后批量远程查
- 流程图提示里用户 ID 去重,部门名称从循环内逐条Dubbo 调用改为一次批量查询
2026-06-08 17:26:32 +08:00
疯狂的狮子Li 3a6d62d695 fix 修复 一些发现的小bug
- 短信验证码限流 key 改为 #phoneNumber,验证码改为发送成功后再写 Redis,失败消息避免 NPE
- 邮箱验证码改为邮件发送成功后再写 Redis
- ValidatorUtils 增加 NPE 校验
- 部门导入路径不存在时明确报错,不再静默转成 null
- 重复提交切面完整扫描数组、集合、Map 中的过滤对象
- Excel 级联下拉补齐一级选项的 Excel 名称规则校验和重复校验
2026-06-08 16:37:18 +08:00
14 changed files with 128 additions and 43 deletions
+6 -4
View File
@@ -11,18 +11,20 @@ RUN mkdir -p /ruoyi/server/logs \
WORKDIR /ruoyi/server WORKDIR /ruoyi/server
ENV SERVER_PORT=8080 SNAIL_PORT=28080 LANG=C.UTF-8 LC_ALL=C.UTF-8 JAVA_OPTS="" ENV SERVER_PORT=8080 SNAIL_JOB_PORT=28080 SNAIL_AI_PORT=38080 LANG=C.UTF-8 LC_ALL=C.UTF-8 JAVA_OPTS=""
EXPOSE ${SERVER_PORT} EXPOSE ${SERVER_PORT}
# 暴露 snail job 客户端端口 用于定时任务调度中心通信 # 暴露 snail 客户端端口 用于定时任务调度中心通信
EXPOSE ${SNAIL_PORT} EXPOSE ${SNAIL_JOB_PORT}
EXPOSE ${SNAIL_AI_PORT}
ADD ./target/ruoyi-admin.jar ./app.jar ADD ./target/ruoyi-admin.jar ./app.jar
SHELL ["/bin/bash", "-c"] SHELL ["/bin/bash", "-c"]
ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom -Dserver.port=${SERVER_PORT} \ ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom -Dserver.port=${SERVER_PORT} \
-Dsnail-job.port=${SNAIL_PORT} \ -Dsnail-job.port=${SNAIL_JOB_PORT} \
-Dsnail-ai.port=${SNAIL_AI_PORT} \
# 应用名称 如果想区分集群节点监控 改成不同的名称即可 # 应用名称 如果想区分集群节点监控 改成不同的名称即可
#-Dskywalking.agent.service_name=ruoyi-server \ #-Dskywalking.agent.service_name=ruoyi-server \
#-javaagent:/ruoyi/skywalking/agent/skywalking-agent.jar \ #-javaagent:/ruoyi/skywalking/agent/skywalking-agent.jar \
@@ -58,7 +58,7 @@ public class CaptchaController {
* @param phoneNumber 用户手机号 * @param phoneNumber 用户手机号
* @return 操作结果 * @return 操作结果
*/ */
@RateLimiter(key = "#phonenumber", time = 60, count = 1) @RateLimiter(key = "#phoneNumber", time = 60, count = 1)
@GetMapping("/resource/sms/code") @GetMapping("/resource/sms/code")
public R<Void> smsCode(@NotBlank(message = "{user.phonenumber.not.blank}") String phoneNumber) { public R<Void> smsCode(@NotBlank(message = "{user.phonenumber.not.blank}") String phoneNumber) {
if (!RegexValidator.isMobile(phoneNumber)) { if (!RegexValidator.isMobile(phoneNumber)) {
@@ -66,7 +66,6 @@ public class CaptchaController {
} }
String key = GlobalConstants.CAPTCHA_CODE_KEY + phoneNumber; String key = GlobalConstants.CAPTCHA_CODE_KEY + phoneNumber;
String code = RandomUtil.randomNumbers(4); String code = RandomUtil.randomNumbers(4);
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
// 验证码模板id 自行处理 (查数据库或写死均可) // 验证码模板id 自行处理 (查数据库或写死均可)
String templateId = ""; String templateId = "";
LinkedHashMap<String, String> map = new LinkedHashMap<>(1); LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
@@ -75,8 +74,10 @@ public class CaptchaController {
SmsResponse smsResponse = smsBlend.sendMessage(phoneNumber, templateId, map); SmsResponse smsResponse = smsBlend.sendMessage(phoneNumber, templateId, map);
if (!smsResponse.isSuccess()) { if (!smsResponse.isSuccess()) {
log.error("验证码短信发送异常 => {}", smsResponse); log.error("验证码短信发送异常 => {}", smsResponse);
return R.fail(smsResponse.getData().toString()); Object data = smsResponse.getData();
return R.fail(data == null ? "验证码短信发送失败" : data.toString());
} }
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
return R.ok(); return R.ok();
} }
@@ -107,13 +108,13 @@ public class CaptchaController {
public void emailCodeImpl(String email) { public void emailCodeImpl(String email) {
String key = GlobalConstants.CAPTCHA_CODE_KEY + email; String key = GlobalConstants.CAPTCHA_CODE_KEY + email;
String code = RandomUtil.randomNumbers(4); String code = RandomUtil.randomNumbers(4);
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
try { try {
MailBuilder.of() MailBuilder.of()
.to(email) .to(email)
.subject("登录验证码") .subject("登录验证码")
.text("您本次验证码为:" + code + ",有效性为" + Constants.CAPTCHA_EXPIRATION + "分钟,请尽快填写。") .text("您本次验证码为:" + code + ",有效性为" + Constants.CAPTCHA_EXPIRATION + "分钟,请尽快填写。")
.send(); .send();
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
} catch (Exception e) { } catch (Exception e) {
log.error("验证码短信发送异常 => {}", e.getMessage()); log.error("验证码短信发送异常 => {}", e.getMessage());
throw new ServiceException(e.getMessage()); throw new ServiceException(e.getMessage());
@@ -26,6 +26,9 @@ public class ValidatorUtils {
* @throws ConstraintViolationException 如果校验不通过,则抛出参数校验异常 * @throws ConstraintViolationException 如果校验不通过,则抛出参数校验异常
*/ */
public static <T> void validate(T object, Class<?>... groups) { public static <T> void validate(T object, Class<?>... groups) {
if (object == null) {
throw new RuntimeException("请求参数不能为空");
}
Set<ConstraintViolation<T>> validate = VALID.validate(object, groups); Set<ConstraintViolation<T>> validate = VALID.validate(object, groups);
if (!validate.isEmpty()) { if (!validate.isEmpty()) {
throw new ConstraintViolationException("参数校验异常", validate); throw new ConstraintViolationException("参数校验异常", validate);
@@ -51,6 +51,9 @@ public class DropDownOptions {
* 分隔符 * 分隔符
*/ */
private static final String DELIMITER = "_"; private static final String DELIMITER = "_";
private static final String OPTION_PART_REGEX = "^[A-Za-z0-9\\u4e00-\\u9fa5]+$";
private static final String EXCEL_NAME_REGEX = "^[A-Za-z_\\u4e00-\\u9fa5][A-Za-z0-9_\\u4e00-\\u9fa5]*$";
private static final String CELL_REFERENCE_REGEX = "^[A-Za-z]{1,3}[1-9][0-9]*$";
/** /**
* 创建只有一级的下拉选 * 创建只有一级的下拉选
@@ -69,10 +72,9 @@ public class DropDownOptions {
*/ */
public static String createOptionValue(Object... vars) { public static String createOptionValue(Object... vars) {
StringBuilder stringBuffer = new StringBuilder(); StringBuilder stringBuffer = new StringBuilder();
String regex = "^[\\S\\d\\u4e00-\\u9fa5]+$";
for (int i = 0; i < vars.length; i++) { for (int i = 0; i < vars.length; i++) {
String var = StrUtil.trimToEmpty(Convert.toStr(vars[i])); String var = StrUtil.trimToEmpty(Convert.toStr(vars[i]));
if (!var.matches(regex)) { if (!var.matches(OPTION_PART_REGEX)) {
throw new ServiceException("选项数据不符合规则,仅允许使用中英文字符以及数字"); throw new ServiceException("选项数据不符合规则,仅允许使用中英文字符以及数字");
} }
stringBuffer.append(var); stringBuffer.append(var);
@@ -81,10 +83,29 @@ public class DropDownOptions {
stringBuffer.append(DELIMITER); stringBuffer.append(DELIMITER);
} }
} }
if (stringBuffer.toString().matches("^\\d_*$")) { String optionValue = stringBuffer.toString();
validateOptionValue(optionValue);
return optionValue;
}
/**
* 校验级联下拉选项值是否可作为 Excel 名称管理器名称。
*
* @param optionValue 选项值
*/
public static void validateOptionValue(String optionValue) {
if (StrUtil.isBlank(optionValue)) {
throw new ServiceException("选项数据不能为空");
}
if (optionValue.matches("^[0-9].*")) {
throw new ServiceException("禁止以数字开头"); throw new ServiceException("禁止以数字开头");
} }
return stringBuffer.toString(); if (!optionValue.matches(EXCEL_NAME_REGEX)) {
throw new ServiceException("选项数据不符合Excel名称规则,仅允许中英文、数字或下划线,且不能以数字开头");
}
if (optionValue.matches(CELL_REFERENCE_REGEX)) {
throw new ServiceException("选项数据不能为Excel单元格引用");
}
} }
/** /**
@@ -202,6 +202,7 @@ public class ExcelDownHandler implements SheetWriteHandler {
if (CollUtil.isEmpty(firstOptions)) { if (CollUtil.isEmpty(firstOptions)) {
return; return;
} }
validateLinkedOptionNames(firstOptions);
Map<String, List<String>> secoundOptionsMap = new HashMap<>(); Map<String, List<String>> secoundOptionsMap = new HashMap<>();
options.getNextOptions().forEach((k, v) -> secoundOptionsMap.put(k, new ArrayList<>(v))); options.getNextOptions().forEach((k, v) -> secoundOptionsMap.put(k, new ArrayList<>(v)));
@@ -313,6 +314,19 @@ public class ExcelDownHandler implements SheetWriteHandler {
currentLinkedOptionsSheetIndex++; currentLinkedOptionsSheetIndex++;
} }
/**
* 校验级联下拉一级选项可作为 Excel 名称,且不能重复。
*/
private void validateLinkedOptionNames(List<String> firstOptions) {
Set<String> names = new HashSet<>();
for (String firstOption : firstOptions) {
DropDownOptions.validateOptionValue(firstOption);
if (!names.add(firstOption)) {
throw new ServiceException("级联下拉一级选项重复:" + firstOption);
}
}
}
/** /**
* <h2>额外表格形式的普通下拉框</h2> * <h2>额外表格形式的普通下拉框</h2>
* 由于下拉框可选值数量过多,为提升Excel打开效率,使用额外表格形式做下拉 * 由于下拉框可选值数量过多,为提升Excel打开效率,使用额外表格形式做下拉
@@ -24,6 +24,7 @@ import org.dromara.common.redis.utils.RedisUtils;
import org.springframework.validation.BindingResult; import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.lang.reflect.Array;
import java.time.Duration; import java.time.Duration;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
@@ -150,16 +151,30 @@ public class RepeatSubmitAspect {
public boolean isFilterObject(final Object o) { public boolean isFilterObject(final Object o) {
Class<?> clazz = o.getClass(); Class<?> clazz = o.getClass();
if (clazz.isArray()) { if (clazz.isArray()) {
return MultipartFile.class.isAssignableFrom(clazz.getComponentType()); if (MultipartFile.class.isAssignableFrom(clazz.getComponentType())) {
return true;
}
int length = Array.getLength(o);
for (int i = 0; i < length; i++) {
Object value = Array.get(o, i);
if (ObjectUtil.isNotNull(value) && isFilterObject(value)) {
return true;
}
}
return false;
} else if (Collection.class.isAssignableFrom(clazz)) { } else if (Collection.class.isAssignableFrom(clazz)) {
Collection collection = (Collection) o; Collection collection = (Collection) o;
for (Object value : collection) { for (Object value : collection) {
return value instanceof MultipartFile; if (ObjectUtil.isNotNull(value) && isFilterObject(value)) {
return true;
}
} }
} else if (Map.class.isAssignableFrom(clazz)) { } else if (Map.class.isAssignableFrom(clazz)) {
Map map = (Map) o; Map map = (Map) o;
for (Object value : map.values()) { for (Object value : map.values()) {
return value instanceof MultipartFile; if (ObjectUtil.isNotNull(value) && isFilterObject(value)) {
return true;
}
} }
} }
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
@@ -45,7 +45,14 @@ public class SaTokenExceptionHandler {
public R<Void> handleNotLoginException(NotLoginException e, HttpServletRequest request) { public R<Void> handleNotLoginException(NotLoginException e, HttpServletRequest request) {
String requestURI = request.getRequestURI(); String requestURI = request.getRequestURI();
log.error("请求地址'{}',认证失败'{}',无法访问系统资源", requestURI, e.getMessage()); log.error("请求地址'{}',认证失败'{}',无法访问系统资源", requestURI, e.getMessage());
return R.fail(HttpStatus.HTTP_UNAUTHORIZED, "认证失败,无法访问系统资源"); String msg = switch (e.getType()) {
case NotLoginException.TOKEN_TIMEOUT -> "登录已过期,请重新登录";
case NotLoginException.BE_REPLACED -> "当前账号已在其他设备登录,您已被强制下线";
case NotLoginException.KICK_OUT -> "账号已被管理员强制下线";
case NotLoginException.TOKEN_FREEZE -> "账号已被冻结,请联系管理员处理";
default -> "登录状态异常,请重新登录";
};
return R.fail(HttpStatus.HTTP_UNAUTHORIZED, msg);
} }
} }
@@ -618,7 +618,7 @@ const { resetQuery } = useSearchReset({
}); });
/** 新增按钮操作 */ /** 新增按钮操作 */
const handleAdd = (row?: ${BusinessName}VO) => { const handleAdd = (row?: Partial<${BusinessName}VO>) => {
openDialog('添加${functionName}'); openDialog('添加${functionName}');
getTreeselect(); getTreeselect();
if (row != null && row.${treeCode}) { if (row != null && row.${treeCode}) {
@@ -629,7 +629,7 @@ const handleAdd = (row?: ${BusinessName}VO) => {
}; };
/** 修改按钮操作 */ /** 修改按钮操作 */
const handleUpdate = async (row: ${BusinessName}VO) => { const handleUpdate = async (row: Partial<${BusinessName}VO>) => {
reset(); reset();
await getTreeselect(row.${treeCode}); await getTreeselect(row.${treeCode});
if (row != null) { if (row != null) {
@@ -668,7 +668,7 @@ const submitForm = () => {
}; };
/** 删除按钮操作 */ /** 删除按钮操作 */
const handleDelete = async (row: ${BusinessName}VO) => { const handleDelete = async (row: Partial<${BusinessName}VO>) => {
await modal.confirm('是否确认删除${functionName}编号为"' + row.${pkColumn.javaField} + '"的数据项?'); await modal.confirm('是否确认删除${functionName}编号为"' + row.${pkColumn.javaField} + '"的数据项?');
setLoading(true); setLoading(true);
await del${BusinessName}(row.${pkColumn.javaField}).finally(() => setLoading(false)); await del${BusinessName}(row.${pkColumn.javaField}).finally(() => setLoading(false));
@@ -687,7 +687,7 @@ const filterTreeOptions = (options: ${BusinessName}Option[], excludeId: string |
#if($enableStatus) #if($enableStatus)
/** 状态修改 */ /** 状态修改 */
const handleStatusChange = async (row: ${BusinessName}VO) => { const handleStatusChange = async (row: Partial<${BusinessName}VO>) => {
const text = row.${statusField} === ${statusField}ActiveValue ? '启用' : '停用'; const text = row.${statusField} === ${statusField}ActiveValue ? '启用' : '停用';
try { try {
await modal.confirm('确认要"' + text + '"吗?'); await modal.confirm('确认要"' + text + '"吗?');
@@ -701,7 +701,7 @@ const handleStatusChange = async (row: ${BusinessName}VO) => {
#if($enableSort) #if($enableSort)
/** 排序调整 */ /** 排序调整 */
const handleSortChange = async (row: ${BusinessName}VO) => { const handleSortChange = async (row: Partial<${BusinessName}VO>) => {
try { try {
await update${BusinessName}Sort(row.${pkColumn.javaField}, row.${sortField}); await update${BusinessName}Sort(row.${pkColumn.javaField}, row.${sortField});
modal.msgSuccess('排序更新成功'); modal.msgSuccess('排序更新成功');
@@ -561,7 +561,7 @@ const handleAdd = () => {
}; };
/** 修改按钮操作 */ /** 修改按钮操作 */
const handleUpdate = async (row?: ${BusinessName}VO) => { const handleUpdate = async (row?: Partial<${BusinessName}VO>) => {
reset(); reset();
const _${pkColumn.javaField} = row?.${pkColumn.javaField} || ids.value[0]; const _${pkColumn.javaField} = row?.${pkColumn.javaField} || ids.value[0];
const res = await get${BusinessName}(_${pkColumn.javaField}); const res = await get${BusinessName}(_${pkColumn.javaField});
@@ -597,7 +597,7 @@ const submitForm = () => {
}; };
/** 删除按钮操作 */ /** 删除按钮操作 */
const handleDelete = async (row?: ${BusinessName}VO) => { const handleDelete = async (row?: Partial<${BusinessName}VO>) => {
const _${pkColumn.javaField}s = row?.${pkColumn.javaField} || ids.value; const _${pkColumn.javaField}s = row?.${pkColumn.javaField} || ids.value;
await modal.confirm('是否确认删除${functionName}编号为"' + _${pkColumn.javaField}s + '"的数据项?'); await modal.confirm('是否确认删除${functionName}编号为"' + _${pkColumn.javaField}s + '"的数据项?');
await del${BusinessName}(_${pkColumn.javaField}s); await del${BusinessName}(_${pkColumn.javaField}s);
@@ -620,7 +620,7 @@ const handleExport = () => {
#if($enableStatus) #if($enableStatus)
/** 状态修改 */ /** 状态修改 */
const handleStatusChange = async (row: ${BusinessName}VO) => { const handleStatusChange = async (row: Partial<${BusinessName}VO>) => {
const text = row.${statusField} === ${statusField}ActiveValue ? '启用' : '停用'; const text = row.${statusField} === ${statusField}ActiveValue ? '启用' : '停用';
try { try {
await modal.confirm('确认要"' + text + '"吗?'); await modal.confirm('确认要"' + text + '"吗?');
@@ -634,7 +634,7 @@ const handleStatusChange = async (row: ${BusinessName}VO) => {
#if($enableSort) #if($enableSort)
/** 排序调整 */ /** 排序调整 */
const handleSortChange = async (row: ${BusinessName}VO) => { const handleSortChange = async (row: Partial<${BusinessName}VO>) => {
try { try {
await update${BusinessName}Sort(row.${pkColumn.javaField}, row.${sortField}); await update${BusinessName}Sort(row.${pkColumn.javaField}, row.${sortField});
modal.msgSuccess('排序更新成功'); modal.msgSuccess('排序更新成功');
@@ -10,7 +10,6 @@ import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.utils.StringUtils; import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.domain.BaseEntity; import org.dromara.common.mybatis.core.domain.BaseEntity;
import java.io.Serial;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -25,8 +24,6 @@ import java.util.List;
@TableName("sys_menu") @TableName("sys_menu")
public class SysMenu extends BaseEntity { public class SysMenu extends BaseEntity {
@Serial
private static final long serialVersionUID = 6967394714839655267L;
/** /**
* 菜单ID * 菜单ID
*/ */
@@ -10,6 +10,7 @@ import org.apache.fesod.sheet.metadata.GlobalConfiguration;
import org.apache.fesod.sheet.metadata.data.ReadCellData; import org.apache.fesod.sheet.metadata.data.ReadCellData;
import org.apache.fesod.sheet.metadata.data.WriteCellData; import org.apache.fesod.sheet.metadata.data.WriteCellData;
import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; import org.apache.fesod.sheet.metadata.property.ExcelContentProperty;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.SpringUtils; import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils; import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.TreeBuildUtils; import org.dromara.common.core.utils.TreeBuildUtils;
@@ -93,7 +94,11 @@ public class DeptExcelConverter implements Converter<Long> {
if (StringUtils.isBlank(deptName)) { if (StringUtils.isBlank(deptName)) {
return null; return null;
} }
return getDeptMaps().nameToId().get(deptName); Long deptId = getDeptMaps().nameToId().get(deptName);
if (deptId == null) {
throw new ServiceException("部门不存在:" + deptName);
}
return deptId;
} }
/** /**
@@ -27,10 +27,7 @@ import org.dromara.workflow.common.constant.FlowConstant;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.ArrayList; import java.util.*;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -74,10 +71,12 @@ public class FlwChartExtServiceImpl implements ChartExtService {
Map<String, List<FlowHisTask>> groupedByNode = StreamUtils.groupByKey(flowHisTasks, FlowHisTask::getNodeCode); Map<String, List<FlowHisTask>> groupedByNode = StreamUtils.groupByKey(flowHisTasks, FlowHisTask::getNodeCode);
// 批量查询所有审批人的用户信息 // 批量查询所有审批人的用户信息
List<UserDTO> userDTOList = userService.selectListByIds(StreamUtils.toList(flowHisTasks, e -> Convert.toLong(e.getApprover()))); List<UserDTO> userDTOList = userService.selectListByIds(StreamUtils.toSet(flowHisTasks, e -> Convert.toLong(e.getApprover())));
// 将查询到的用户列表转换为以用户ID为key的映射 // 将查询到的用户列表转换为以用户ID为key的映射
Map<Long, UserDTO> userMap = StreamUtils.toIdentityMap(userDTOList, UserDTO::getUserId); Map<Long, UserDTO> userMap = StreamUtils.toIdentityMap(userDTOList, UserDTO::getUserId);
Set<Long> deptIds = StreamUtils.toSet(userDTOList, UserDTO::getDeptId);
Map<Long, String> deptNameMap = deptService.selectDeptNamesByIds(deptIds);
Map<String, String> dictType = dictService.getAllDictByDictType(FlowConstant.WF_TASK_STATUS); Map<String, String> dictType = dictService.getAllDictByDictType(FlowConstant.WF_TASK_STATUS);
@@ -100,7 +99,7 @@ public class FlwChartExtServiceImpl implements ChartExtService {
)); ));
// 处理当前节点的扩展信息 // 处理当前节点的扩展信息
this.processNodeExtInfo(nodeJson, latestPerApprover, userMap, dictType); this.processNodeExtInfo(nodeJson, latestPerApprover, userMap, deptNameMap, dictType);
} }
} }
@@ -168,9 +167,11 @@ public class FlwChartExtServiceImpl implements ChartExtService {
* @param nodeJson 当前流程节点对象,包含节点基础信息和提示内容容器 * @param nodeJson 当前流程节点对象,包含节点基础信息和提示内容容器
* @param taskList 当前节点关联的历史审批任务列表,用于生成提示信息 * @param taskList 当前节点关联的历史审批任务列表,用于生成提示信息
* @param userMap 用户信息映射表,key 为用户ID,value 为用户DTO对象,用于获取审批人信息 * @param userMap 用户信息映射表,key 为用户ID,value 为用户DTO对象,用于获取审批人信息
* @param deptNameMap 部门名称映射表,key 为部门ID,value 为部门名称
* @param dictType 数据字典映射表,key 为字典项编码,value 为对应显示值,用于翻译审批状态等 * @param dictType 数据字典映射表,key 为字典项编码,value 为对应显示值,用于翻译审批状态等
*/ */
private void processNodeExtInfo(NodeJson nodeJson, List<FlowHisTask> taskList, Map<Long, UserDTO> userMap, Map<String, String> dictType) { private void processNodeExtInfo(NodeJson nodeJson, List<FlowHisTask> taskList, Map<Long, UserDTO> userMap,
Map<Long, String> deptNameMap, Map<String, String> dictType) {
// 获取节点提示内容对象中的 info 列表,用于追加提示项 // 获取节点提示内容对象中的 info 列表,用于追加提示项
List<PromptContent.InfoItem> info = nodeJson.getPromptContent().getInfo(); List<PromptContent.InfoItem> info = nodeJson.getPromptContent().getInfo();
@@ -182,8 +183,7 @@ public class FlwChartExtServiceImpl implements ChartExtService {
continue; continue;
} }
// 查询用户所属部门名称 String deptName = deptNameMap.getOrDefault(userDTO.getDeptId(), StringUtils.EMPTY);
String deptName = deptService.selectDeptNameByIds(Convert.toStr(userDTO.getDeptId()));
// 添加标题项,如:👤 张三(市场部) // 添加标题项,如:👤 张三(市场部)
info.add(new PromptContent.InfoItem() info.add(new PromptContent.InfoItem()
@@ -338,10 +338,14 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
return; return;
} }
// 添加抄送人记录 // 添加抄送人记录
FlowHisTask flowHisTask = flowHisTaskMapper.selectList( List<FlowHisTask> flowHisTasks = flowHisTaskMapper.selectList(
QueryBuilder.lambda(FlowHisTask.class) QueryBuilder.lambda(FlowHisTask.class)
.eq(FlowHisTask::getTaskId, task.getId()) .eq(FlowHisTask::getTaskId, task.getId())
.build()).get(0); .build());
if (CollUtil.isEmpty(flowHisTasks)) {
throw new ServiceException("流程历史任务不存在,无法添加抄送记录");
}
FlowHisTask flowHisTask = flowHisTasks.get(0);
FlowNode flowNode = new FlowNode(); FlowNode flowNode = new FlowNode();
flowNode.setNodeCode(flowHisTask.getTargetNodeCode()); flowNode.setNodeCode(flowHisTask.getTargetNodeCode());
flowNode.setNodeName(flowHisTask.getTargetNodeName()); flowNode.setNodeName(flowHisTask.getTargetNodeName());
@@ -897,7 +901,7 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
if (CollUtil.isEmpty(userList)) { if (CollUtil.isEmpty(userList)) {
return Collections.emptyList(); return Collections.emptyList();
} }
return userService.selectListByIds(StreamUtils.toList(userList, e -> Convert.toLong(e.getProcessedBy()))); return userService.selectListByIds(StreamUtils.toSet(userList, e -> Convert.toLong(e.getProcessedBy())));
} }
/** /**
+18 -2
View File
@@ -103,7 +103,8 @@ services:
# 时区上海 # 时区上海
TZ: Asia/Shanghai TZ: Asia/Shanghai
SERVER_PORT: 8080 SERVER_PORT: 8080
SNAIL_PORT: 28080 SNAIL_JOB_PORT: 28080
SNAIL_AI_PORT: 38080
volumes: volumes:
# 配置文件 # 配置文件
- /docker/server1/logs/:/ruoyi/server/logs/ - /docker/server1/logs/:/ruoyi/server/logs/
@@ -119,7 +120,8 @@ services:
# 时区上海 # 时区上海
TZ: Asia/Shanghai TZ: Asia/Shanghai
SERVER_PORT: 8081 SERVER_PORT: 8081
SNAIL_PORT: 28081 SNAIL_JOB_PORT: 28081
SNAIL_AI_PORT: 38081
volumes: volumes:
# 配置文件 # 配置文件
- /docker/server2/logs/:/ruoyi/server/logs/ - /docker/server2/logs/:/ruoyi/server/logs/
@@ -153,3 +155,17 @@ services:
- /docker/snailjob/logs/:/ruoyi/snailjob/logs - /docker/snailjob/logs/:/ruoyi/snailjob/logs
privileged: true privileged: true
network_mode: "host" network_mode: "host"
ruoyi-snailai-server:
image: ruoyi/ruoyi-snailai-server:5.5.3
container_name: ruoyi-snailai-server
environment:
# 时区上海
TZ: Asia/Shanghai
ports:
- "8900:8900"
- "18888:18888"
volumes:
- /docker/snailai/logs/:/ruoyi/snailai/logs
privileged: true
network_mode: "host"