【同步】BOOT 和 CLOUD 的功能

This commit is contained in:
YunaiV
2026-03-08 10:14:09 +08:00
parent 53d3146ad9
commit 34d74b378e
7 changed files with 72 additions and 46 deletions

View File

@@ -37,8 +37,10 @@ public class HttpUtils {
} }
/** /**
* 解码 URL 参数 * 解码 URL 参数query parameter
* 注意:此方法会将 + 解码为空格,适用于 query parameter不适用于 URL path
* *
* @see #decodeUrlPath(String)
* @param value 参数 * @param value 参数
* @return 解码后的参数 * @return 解码后的参数
*/ */
@@ -46,6 +48,20 @@ public class HttpUtils {
return URLDecoder.decode(value, StandardCharsets.UTF_8); 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") @SuppressWarnings("unchecked")
public static String replaceUrlQuery(String url, String key, String value) { public static String replaceUrlQuery(String url, String key, String value) {
UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset()); UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset());

View File

@@ -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.Area;
import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum; import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum;
import lombok.NonNull; import lombok.NonNull;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList; import java.util.ArrayList;
@@ -25,36 +26,35 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
* @author 芋道源码 * @author 芋道源码
*/ */
@Slf4j @Slf4j
@UtilityClass
public class AreaUtils { public class AreaUtils {
/**
* 初始化 SEARCHER
*/
@SuppressWarnings("InstantiationOfUtilityClass")
private final static AreaUtils INSTANCE = new AreaUtils();
/** /**
* Area 内存缓存,提升访问速度 * Area 内存缓存,提升访问速度
*/ */
private static Map<Integer, Area> areas; private static Map<Integer, Area> areas;
private AreaUtils() { static {
init();
}
/**
* 初始化
*/
private static void init() {
try {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
areas = new HashMap<>(); areas = new HashMap<>();
areas.put(Area.ID_GLOBAL, new Area(Area.ID_GLOBAL, "全球", 0, areas.put(Area.ID_GLOBAL, new Area(Area.ID_GLOBAL, "全球", 0, null, new ArrayList<>()));
null, new ArrayList<>()));
// 从 csv 中加载数据 // 从 csv 中加载数据
List<CsvRow> rows = CsvUtil.getReader().read(ResourceUtil.getUtf8Reader("area.csv")).getRows(); List<CsvRow> rows = CsvUtil.getReader().read(ResourceUtil.getUtf8Reader("area.csv")).getRows();
rows.remove(0); // 删除 header rows.remove(0); // 删除 header
for (CsvRow row : rows) { 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<>());
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); areas.put(area.getId(), area);
} }
// 构建父子关系:因为 Area 中没有 parentId 字段所以需要重复读取 // 构建父子关系:因为 Area 中没有 parentId 字段,所以需要重复读取
for (CsvRow row : rows) { for (CsvRow row : rows) {
Area area = areas.get(Integer.valueOf(row.get(0))); // 自己 Area area = areas.get(Integer.valueOf(row.get(0))); // 自己
Area parent = areas.get(Integer.valueOf(row.get(3))); // 父 Area parent = areas.get(Integer.valueOf(row.get(3))); // 父
@@ -63,6 +63,9 @@ public class AreaUtils {
parent.getChildren().add(area); parent.getChildren().add(area);
} }
log.info("启动加载 AreaUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now); log.info("启动加载 AreaUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now);
} catch (Exception e) {
throw new RuntimeException("AreaUtils 初始化失败", e);
}
} }
/** /**

View File

@@ -3,11 +3,10 @@ package cn.iocoder.yudao.framework.ip.core.utils;
import cn.hutool.core.io.resource.ResourceUtil; import cn.hutool.core.io.resource.ResourceUtil;
import cn.iocoder.yudao.framework.ip.core.Area; import cn.iocoder.yudao.framework.ip.core.Area;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.lionsoul.ip2region.xdb.Searcher; import org.lionsoul.ip2region.xdb.Searcher;
import java.io.IOException;
/** /**
* IP 工具类 * IP 工具类
* *
@@ -16,30 +15,29 @@ import java.io.IOException;
* @author wanglhup * @author wanglhup
*/ */
@Slf4j @Slf4j
@UtilityClass
public class IPUtils { public class IPUtils {
/**
* 初始化 SEARCHER
*/
@SuppressWarnings("InstantiationOfUtilityClass")
private final static IPUtils INSTANCE = new IPUtils();
/** /**
* IP 查询器,启动加载到内存中 * IP 查询器,启动加载到内存中
*/ */
private static Searcher SEARCHER; private static Searcher SEARCHER;
static {
init();
}
/** /**
* 私有化构造 * 初始化
*/ */
private IPUtils() { private static void init() {
try { try {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
byte[] bytes = ResourceUtil.readBytes("ip2region.xdb"); byte[] bytes = ResourceUtil.readBytes("ip2region.xdb");
SEARCHER = Searcher.newWithBuffer(bytes); SEARCHER = Searcher.newWithBuffer(bytes);
log.info("启动加载 IPUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now); log.info("启动加载 IPUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now);
} catch (IOException e) { } catch (Exception e) {
log.error("启动加载 IPUtils 失败", e); throw new RuntimeException("IPUtils 初始化失败", e);
} }
} }

View File

@@ -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.bpmn.behavior.SequentialMultiInstanceBehavior;
import org.flowable.engine.impl.persistence.entity.ExecutionEntity; import org.flowable.engine.impl.persistence.entity.ExecutionEntity;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@@ -53,7 +54,7 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
Set<Long> assigneeUserIds = (Set<Long>) execution.getVariableLocal(super.collectionVariable, Set.class); Set<Long> assigneeUserIds = (Set<Long>) execution.getVariableLocal(super.collectionVariable, Set.class);
if (assigneeUserIds == null) { if (assigneeUserIds == null) {
assigneeUserIds = taskCandidateInvoker.calculateUsersByTask(execution); assigneeUserIds = new LinkedHashSet<>(taskCandidateInvoker.calculateUsersByTask(execution));
if (CollUtil.isEmpty(assigneeUserIds)) { if (CollUtil.isEmpty(assigneeUserIds)) {
// 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过! // 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过!
// 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务 // 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务

View File

@@ -116,7 +116,7 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
public String presignGetUrl(String url, Integer expirationSeconds) { public String presignGetUrl(String url, Integer expirationSeconds) {
// 1. 将 url 转换为 path // 1. 将 url 转换为 path
String path = StrUtil.removePrefix(url, config.getDomain() + "/"); String path = StrUtil.removePrefix(url, config.getDomain() + "/");
path = HttpUtils.decodeUtf8(HttpUtils.removeUrlQuery(path)); path = HttpUtils.decodeUrlPath(HttpUtils.removeUrlQuery(path));
// 2.1 情况一:公开访问:无需签名 // 2.1 情况一:公开访问:无需签名
// 考虑到老版本的兼容,所以必须是 config.getEnablePublicAccess() 为 false 时,才进行签名 // 考虑到老版本的兼容,所以必须是 config.getEnablePublicAccess() 为 false 时,才进行签名

View File

@@ -20,7 +20,9 @@ import jakarta.annotation.Resource;
import jakarta.annotation.security.PermitAll; import jakarta.annotation.security.PermitAll;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.Pattern;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.io.IOException; import java.io.IOException;
@@ -33,6 +35,7 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
@Tag(name = "管理后台 - 租户") @Tag(name = "管理后台 - 租户")
@RestController @RestController
@RequestMapping("/system/tenant") @RequestMapping("/system/tenant")
@Validated
public class TenantController { public class TenantController {
@Resource @Resource
@@ -63,7 +66,8 @@ public class TenantController {
@TenantIgnore @TenantIgnore
@Operation(summary = "使用域名,获得租户信息", description = "登录界面,根据用户的域名,获得租户信息") @Operation(summary = "使用域名,获得租户信息", description = "登录界面,根据用户的域名,获得租户信息")
@Parameter(name = "website", description = "域名", required = true, example = "www.iocoder.cn") @Parameter(name = "website", description = "域名", required = true, example = "www.iocoder.cn")
public CommonResult<TenantRespVO> getTenantByWebsite(@RequestParam("website") String website) { public CommonResult<TenantRespVO> getTenantByWebsite(
@RequestParam("website") @Pattern(regexp = "^[a-zA-Z0-9.-]+$", message = "网站域名格式不正确") String website) {
TenantDO tenant = tenantService.getTenantByWebsite(website); TenantDO tenant = tenantService.getTenantByWebsite(website);
if (tenant == null || CommonStatusEnum.isDisable(tenant.getStatus())) { if (tenant == null || CommonStatusEnum.isDisable(tenant.getStatus())) {
return success(null); return success(null);

View File

@@ -12,6 +12,8 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import jakarta.annotation.security.PermitAll; 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.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
@@ -22,6 +24,7 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "用户 App - 租户") @Tag(name = "用户 App - 租户")
@RestController @RestController
@RequestMapping("/system/tenant") @RequestMapping("/system/tenant")
@Validated
public class AppTenantController { public class AppTenantController {
@Resource @Resource
@@ -32,7 +35,8 @@ public class AppTenantController {
@TenantIgnore @TenantIgnore
@Operation(summary = "使用域名,获得租户信息", description = "根据用户的域名,获得租户信息") @Operation(summary = "使用域名,获得租户信息", description = "根据用户的域名,获得租户信息")
@Parameter(name = "website", description = "域名", required = true, example = "www.iocoder.cn") @Parameter(name = "website", description = "域名", required = true, example = "www.iocoder.cn")
public CommonResult<AppTenantRespVO> getTenantByWebsite(@RequestParam("website") String website) { public CommonResult<AppTenantRespVO> getTenantByWebsite(
@RequestParam("website") @Pattern(regexp = "^[a-zA-Z0-9.-]+$", message = "网站域名格式不正确") String website) {
TenantDO tenant = tenantService.getTenantByWebsite(website); TenantDO tenant = tenantService.getTenantByWebsite(website);
if (tenant == null || CommonStatusEnum.isDisable(tenant.getStatus())) { if (tenant == null || CommonStatusEnum.isDisable(tenant.getStatus())) {
return success(null); return success(null);