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 c7a7bb32f8..5e9fed03c7 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 @@ -39,8 +39,10 @@ public class HttpUtils { } /** - * 解码 URL 参数 + * 解码 URL 参数(query parameter) + * 注意:此方法会将 + 解码为空格,适用于 query parameter,不适用于 URL path * + * @see #decodeUrlPath(String) * @param value 参数 * @return 解码后的参数 */ @@ -49,6 +51,20 @@ public class HttpUtils { return URLDecoder.decode(value, StandardCharsets.UTF_8.name()); } + /** + * 解码 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 8e27a31acb..413167283a 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 f74f848641..f616414e94 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/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java index 8848f81836..65e6b29172 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java +++ b/yudao-module-bpm/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/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java index 74a9361f99..dede821525 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java +++ b/yudao-module-infra/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-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/aftersale/AfterSaleLogMapper.java b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/aftersale/AfterSaleLogMapper.java index d5453c946f..22d5f40d72 100644 --- a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/aftersale/AfterSaleLogMapper.java +++ b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/aftersale/AfterSaleLogMapper.java @@ -13,7 +13,7 @@ public interface AfterSaleLogMapper extends BaseMapperX { default List selectListByAfterSaleId(Long afterSaleId) { return selectList(new LambdaQueryWrapper() .eq(AfterSaleLogDO::getAfterSaleId, afterSaleId) - .orderByDesc(AfterSaleLogDO::getCreateTime)); + .orderByDesc(AfterSaleLogDO::getId)); } } diff --git a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/framework/aftersale/core/aop/AfterSaleLogAspect.java b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/framework/aftersale/core/aop/AfterSaleLogAspect.java index a6f1fa5965..1938a00eb8 100644 --- a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/framework/aftersale/core/aop/AfterSaleLogAspect.java +++ b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/framework/aftersale/core/aop/AfterSaleLogAspect.java @@ -79,7 +79,7 @@ public class AfterSaleLogAspect { Integer beforeStatus = BEFORE_STATUS.get(); Integer afterStatus = AFTER_STATUS.get(); Map exts = ObjectUtil.defaultIfNull(EXTS.get(), emptyMap()); - String content = StrUtil.format(afterSaleLog.operateType().getContent(), exts); + String content = StrUtil.format(operateType.getContent(), exts); // 2. 记录日志 AfterSaleLogCreateReqBO createBO = new AfterSaleLogCreateReqBO() diff --git a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java index 0b24e2ea03..6585c9da21 100644 --- a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java +++ b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java @@ -258,7 +258,7 @@ public class TradePriceCalculatorHelper { TradeOrderItemDO orderItem = items.get(i); int partPrice; if (i < items.size() - 1) { // 减一的原因,是因为拆分时,如果按照比例,可能会出现.所以最后一个,使用反减 - partPrice = (int) (price * (1.0D * orderItem.getPrice() / total)); + partPrice = (int) (price * (1.0D * orderItem.getPayPrice() / total)); remainPrice -= partPrice; } else { partPrice = remainPrice; diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.java index 3730f6bb4c..fba522ae6b 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.java @@ -16,13 +16,15 @@ import cn.iocoder.yudao.module.system.service.tenant.TenantService; import io.swagger.v3.oas.annotations.Operation; 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.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 javax.annotation.Resource; -import javax.annotation.security.PermitAll; -import javax.servlet.http.HttpServletResponse; -import javax.validation.Valid; import java.io.IOException; import java.util.List; @@ -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/src/main/java/cn/iocoder/yudao/module/system/controller/app/tenant/AppTenantController.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/app/tenant/AppTenantController.java index d184fa40f7..022037d875 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/app/tenant/AppTenantController.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/app/tenant/AppTenantController.java @@ -10,19 +10,21 @@ import cn.iocoder.yudao.module.system.service.tenant.TenantService; import io.swagger.v3.oas.annotations.Operation; 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; import org.springframework.web.bind.annotation.RestController; -import javax.annotation.Resource; -import javax.annotation.security.PermitAll; - import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; @Tag(name = "用户 App - 租户") @RestController @RequestMapping("/system/tenant") +@Validated public class AppTenantController { @Resource @@ -33,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);