diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java index 10744d76b..9de037758 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java @@ -37,8 +37,10 @@ public class HttpUtils { } /** - * 解码 URL 参数 + * 解码 URL 参数(query parameter) + * 注意:此方法会将 + 解码为空格,适用于 query parameter,不适用于 URL path * + * @see #decodeUrlPath(String) * @param value 参数 * @return 解码后的参数 */ @@ -46,6 +48,20 @@ public class HttpUtils { return URLDecoder.decode(value, StandardCharsets.UTF_8); } + /** + * 解码 URL 路径 + * 与 {@link #decodeUtf8(String)} 不同,此方法不会将 + 解码为空格,保持 + 为字面字符 + * 适用于 URL path 部分的解码 + * + * @param path URL 路径 + * @return 解码后的路径 + */ + public static String decodeUrlPath(String path) { + // 先将 + 替换为 %2B,避免被 URLDecoder 解码为空格 + String encoded = path.replace("+", "%2B"); + return URLDecoder.decode(encoded, StandardCharsets.UTF_8); + } + @SuppressWarnings("unchecked") public static String replaceUrlQuery(String url, String key, String value) { UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset()); diff --git a/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/AreaUtils.java b/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/AreaUtils.java index 8e27a31ac..413167283 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/AreaUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/AreaUtils.java @@ -8,6 +8,7 @@ import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import cn.iocoder.yudao.framework.ip.core.Area; import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum; import lombok.NonNull; +import lombok.experimental.UtilityClass; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; @@ -25,44 +26,46 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils. * @author 芋道源码 */ @Slf4j +@UtilityClass public class AreaUtils { - /** - * 初始化 SEARCHER - */ - @SuppressWarnings("InstantiationOfUtilityClass") - private final static AreaUtils INSTANCE = new AreaUtils(); - /** * Area 内存缓存,提升访问速度 */ private static Map areas; - private AreaUtils() { - long now = System.currentTimeMillis(); - areas = new HashMap<>(); - areas.put(Area.ID_GLOBAL, new Area(Area.ID_GLOBAL, "全球", 0, - null, new ArrayList<>())); - // 从 csv 中加载数据 - List rows = CsvUtil.getReader().read(ResourceUtil.getUtf8Reader("area.csv")).getRows(); - rows.remove(0); // 删除 header - for (CsvRow row : rows) { - // 创建 Area 对象 - Area area = new Area(Integer.valueOf(row.get(0)), row.get(1), Integer.valueOf(row.get(2)), - null, new ArrayList<>()); - // 添加到 areas 中 - areas.put(area.getId(), area); - } + static { + init(); + } - // 构建父子关系:因为 Area 中没有 parentId 字段,所以需要重复读取 - for (CsvRow row : rows) { - Area area = areas.get(Integer.valueOf(row.get(0))); // 自己 - Area parent = areas.get(Integer.valueOf(row.get(3))); // 父 - Assert.isTrue(area != parent, "{}:父子节点相同", area.getName()); - area.setParent(parent); - parent.getChildren().add(area); + /** + * 初始化 + */ + private static void init() { + try { + long now = System.currentTimeMillis(); + areas = new HashMap<>(); + areas.put(Area.ID_GLOBAL, new Area(Area.ID_GLOBAL, "全球", 0, null, new ArrayList<>())); + // 从 csv 中加载数据 + List rows = CsvUtil.getReader().read(ResourceUtil.getUtf8Reader("area.csv")).getRows(); + rows.remove(0); // 删除 header + for (CsvRow row : rows) { + Area area = new Area(Integer.valueOf(row.get(0)), row.get(1), Integer.valueOf(row.get(2)), null, new ArrayList<>()); + areas.put(area.getId(), area); + } + + // 构建父子关系:因为 Area 中没有 parentId 字段,所以需要重复读取 + for (CsvRow row : rows) { + Area area = areas.get(Integer.valueOf(row.get(0))); // 自己 + Area parent = areas.get(Integer.valueOf(row.get(3))); // 父 + Assert.isTrue(area != parent, "{}:父子节点相同", area.getName()); + area.setParent(parent); + parent.getChildren().add(area); + } + log.info("启动加载 AreaUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now); + } catch (Exception e) { + throw new RuntimeException("AreaUtils 初始化失败", e); } - log.info("启动加载 AreaUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now); } /** diff --git a/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/IPUtils.java b/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/IPUtils.java index f74f84864..f616414e9 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/IPUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/IPUtils.java @@ -3,11 +3,10 @@ package cn.iocoder.yudao.framework.ip.core.utils; import cn.hutool.core.io.resource.ResourceUtil; import cn.iocoder.yudao.framework.ip.core.Area; import lombok.SneakyThrows; +import lombok.experimental.UtilityClass; import lombok.extern.slf4j.Slf4j; import org.lionsoul.ip2region.xdb.Searcher; -import java.io.IOException; - /** * IP 工具类 * @@ -16,30 +15,29 @@ import java.io.IOException; * @author wanglhup */ @Slf4j +@UtilityClass public class IPUtils { - /** - * 初始化 SEARCHER - */ - @SuppressWarnings("InstantiationOfUtilityClass") - private final static IPUtils INSTANCE = new IPUtils(); - /** * IP 查询器,启动加载到内存中 */ private static Searcher SEARCHER; + static { + init(); + } + /** - * 私有化构造 + * 初始化 */ - private IPUtils() { + private static void init() { try { long now = System.currentTimeMillis(); byte[] bytes = ResourceUtil.readBytes("ip2region.xdb"); SEARCHER = Searcher.newWithBuffer(bytes); log.info("启动加载 IPUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now); - } catch (IOException e) { - log.error("启动加载 IPUtils 失败", e); + } catch (Exception e) { + throw new RuntimeException("IPUtils 初始化失败", e); } } diff --git a/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java b/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java index 8848f8183..65e6b2917 100644 --- a/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java +++ b/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java @@ -14,6 +14,7 @@ import org.flowable.engine.impl.bpmn.behavior.AbstractBpmnActivityBehavior; import org.flowable.engine.impl.bpmn.behavior.SequentialMultiInstanceBehavior; import org.flowable.engine.impl.persistence.entity.ExecutionEntity; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -53,7 +54,7 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB @SuppressWarnings("unchecked") Set assigneeUserIds = (Set) execution.getVariableLocal(super.collectionVariable, Set.class); if (assigneeUserIds == null) { - assigneeUserIds = taskCandidateInvoker.calculateUsersByTask(execution); + assigneeUserIds = new LinkedHashSet<>(taskCandidateInvoker.calculateUsersByTask(execution)); if (CollUtil.isEmpty(assigneeUserIds)) { // 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过! // 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务 diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java index 74a9361f9..dede82152 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java @@ -116,7 +116,7 @@ public class S3FileClient extends AbstractFileClient { public String presignGetUrl(String url, Integer expirationSeconds) { // 1. 将 url 转换为 path String path = StrUtil.removePrefix(url, config.getDomain() + "/"); - path = HttpUtils.decodeUtf8(HttpUtils.removeUrlQuery(path)); + path = HttpUtils.decodeUrlPath(HttpUtils.removeUrlQuery(path)); // 2.1 情况一:公开访问:无需签名 // 考虑到老版本的兼容,所以必须是 config.getEnablePublicAccess() 为 false 时,才进行签名 diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.java index c15c5fa34..fba522ae6 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.java @@ -20,7 +20,9 @@ import jakarta.annotation.Resource; import jakarta.annotation.security.PermitAll; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; +import jakarta.validation.constraints.Pattern; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.io.IOException; @@ -33,6 +35,7 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils. @Tag(name = "管理后台 - 租户") @RestController @RequestMapping("/system/tenant") +@Validated public class TenantController { @Resource @@ -63,7 +66,8 @@ public class TenantController { @TenantIgnore @Operation(summary = "使用域名,获得租户信息", description = "登录界面,根据用户的域名,获得租户信息") @Parameter(name = "website", description = "域名", required = true, example = "www.iocoder.cn") - public CommonResult getTenantByWebsite(@RequestParam("website") String website) { + public CommonResult getTenantByWebsite( + @RequestParam("website") @Pattern(regexp = "^[a-zA-Z0-9.-]+$", message = "网站域名格式不正确") String website) { TenantDO tenant = tenantService.getTenantByWebsite(website); if (tenant == null || CommonStatusEnum.isDisable(tenant.getStatus())) { return success(null); diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/app/tenant/AppTenantController.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/app/tenant/AppTenantController.java index 4655da1d9..022037d87 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/app/tenant/AppTenantController.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/app/tenant/AppTenantController.java @@ -12,6 +12,8 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.annotation.security.PermitAll; +import jakarta.validation.constraints.Pattern; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -22,6 +24,7 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; @Tag(name = "用户 App - 租户") @RestController @RequestMapping("/system/tenant") +@Validated public class AppTenantController { @Resource @@ -32,7 +35,8 @@ public class AppTenantController { @TenantIgnore @Operation(summary = "使用域名,获得租户信息", description = "根据用户的域名,获得租户信息") @Parameter(name = "website", description = "域名", required = true, example = "www.iocoder.cn") - public CommonResult getTenantByWebsite(@RequestParam("website") String website) { + public CommonResult getTenantByWebsite( + @RequestParam("website") @Pattern(regexp = "^[a-zA-Z0-9.-]+$", message = "网站域名格式不正确") String website) { TenantDO tenant = tenantService.getTenantByWebsite(website); if (tenant == null || CommonStatusEnum.isDisable(tenant.getStatus())) { return success(null);