Compare commits

..

No commits in common. "master" and "v2026.01(jdk8/11)" have entirely different histories.

2418 changed files with 21365 additions and 184932 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -31,8 +31,8 @@
| 【完整版】[yudao-cloud](https://gitee.com/zhijiantianya/yudao-cloud) | [`master`](https://gitee.com/zhijiantianya/yudao-cloud/tree/master/) 分支 | [`master-jdk17`](https://gitee.com/zhijiantianya/yudao-cloud/tree/master-jdk17/) 分支 |
| 【精简版】[yudao-cloud-mini](https://gitee.com/yudaocode/yudao-cloud-mini) | [`master`](https://gitee.com/yudaocode/yudao-cloud-mini/tree/master/) 分支 | [`master-jdk17`](https://gitee.com/yudaocode/yudao-cloud-mini/tree/master-jdk17/) 分支 |
* 【完整版】包括系统功能、基础设施、会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、MES、AI 大模型、IoT 物联网 等功能
* 【精简版】只包括系统功能、基础设施功能不包括会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、MES、AI 大模型、IoT 物联网 等功能
* 【完整版】包括系统功能、基础设施、会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP 等功能
* 【精简版】只包括系统功能、基础设施功能不包括会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP 等功能
可参考 [《迁移文档》](https://cloud.iocoder.cn/migrate-module/) ,只需要 5-10 分钟,即可将【完整版】按需迁移到【精简版】
@ -115,7 +115,7 @@
* 通用模块(必选):系统功能、基础设施
* 通用模块(可选):工作流程、支付系统、数据报表、会员中心
* 业务系统按需ERP 系统、CRM 系统、MES 系统、商城系统、微信公众号、AI 大模型、IoT 物联网
* 业务系统按需ERP 系统、CRM 系统、商城系统、微信公众号、AI 大模型
> 友情提示:本项目基于 RuoYi-Vue 修改,**重构优化**后端的代码,**美化**前端的界面。
>
@ -279,14 +279,6 @@
![功能图](/.image/common/crm-feature.png)
### MES 系统
演示地址:<https://cloud.iocoder.cn/mes-preview/>
![功能图](/.image/common/mes-feature.png)
![功能图](/.image/common/mes-preview.png)
### AI 大模型
演示地址:<https://cloud.iocoder.cn/ai-preview/>
@ -295,14 +287,6 @@
![功能图](/.image/common/ai-preview.gif)
### IoT 物联网
演示地址:<https://cloud.iocoder.cn/iot/build>
![功能图](/.image/common/iot-feature.png)
![预览图](/.image/common/iot-preview.png)
## 🐨 技术栈
### 微服务
@ -320,9 +304,7 @@
| `yudao-module-mall` | 商城系统的 Module 模块 |
| `yudao-module-erp` | ERP 系统的 Module 模块 |
| `yudao-module-crm` | CRM 系统的 Module 模块 |
| `yudao-module-mes` | MES 系统的 Module 模块 |
| `yudao-module-ai` | AI 大模型的 Module 模块 |
| `yudao-module-iot` | IoT 物联网的 Module 模块 |
| `yudao-module-mp` | 微信公众号的 Module 模块 |
| `yudao-module-report` | 大屏报表 Module 模块 |

View File

@ -24,10 +24,9 @@
<module>yudao-module-mall</module>
<module>yudao-module-erp</module>
<module>yudao-module-crm</module>
<module>yudao-module-iot</module>
<module>yudao-module-mes</module>
<!-- 友情提示:基于 Spring AI 实现 LLM 大模型的接入,需要使用 JDK17 版本,详细可见 https://doc.iocoder.cn/ai/build/ -->
<!-- <module>yudao-module-ai</module>-->
<module>yudao-module-iot</module>
</modules>
<name>${project.artifactId}</name>
@ -35,7 +34,7 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties>
<revision>2026.04-jdk8-SNAPSHOT</revision>
<revision>2026.01-jdk8-SNAPSHOT</revision>
<!-- Maven 相关 -->
<java.version>1.8</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -62,10 +62,6 @@ def load_and_clean(sql_file: str) -> str:
content = open(sql_file, encoding="utf-8").read()
for replace_pair in REPLACE_PAIR_LIST:
content = content.replace(*replace_pair)
# 移除所有 CHARACTER SET / COLLATE 变体 (utf8mb3、utf8 等)
content = re.sub(r" CHARACTER SET \w+ COLLATE \w+", "", content)
content = re.sub(r" CHARACTER SET \w+", "", content)
content = re.sub(r" COLLATE \w+", "", content)
# 移除索引字段的前缀长度定义,例如: `name`(32) -> `name`
# 移除索引定义上的 USING BTREE COMMENT 部分
# 相关 issuehttps://t.zsxq.com/96IFc 、https://t.zsxq.com/rC3A3
@ -81,11 +77,7 @@ class Convertor(ABC):
self.src = src
self.db_type = db_type
self.content = load_and_clean(self.src)
# original_content 保留原始 COMMENT 信息,用于注释提取
self.original_content = open(src, encoding="utf-8").read()
# 剥离列级 COMMENT 以避免 COMMENT 值内的分号截断 CREATE TABLE 正则
content_no_comment = re.sub(r" COMMENT '(?:[^'\\]|\\.)*'", "", self.content)
self.table_script_list = re.findall(r"CREATE TABLE [^;]*;", content_no_comment)
self.table_script_list = re.findall(r"CREATE TABLE [^;]*;", self.content)
@abstractmethod
def translate_type(self, type: str, size: Optional[Union[int, Tuple[int]]]) -> str:
@ -190,8 +182,7 @@ class Convertor(ABC):
head = head.strip().replace("`", "").lower()
tail = tail.strip().replace(r"\"", '"')
# tail = tail.replace("b'0'", "'0'").replace("b'1'", "'1'")
col_part = f" {head}" if head else ""
yield f"INSERT INTO {table_name.lower()}{col_part} VALUES {tail}"
yield f"INSERT INTO {table_name.lower()} {head} VALUES {tail}"
@staticmethod
def index(ddl: Dict) -> Generator:
@ -236,8 +227,7 @@ class Convertor(ABC):
yield field, comment_string
def table_comment(self, table_sql: str) -> str:
# 兼容 COMMENT='xxx' / COMMENT = 'xxx' 等等号两侧空格变体;允许 COMMENT 内含转义单引号
match = re.search(r"COMMENT\s*=\s*'((?:[^'\\]|\\.)*)'", table_sql)
match = re.search(r"COMMENT \='([^']+)';", table_sql)
return match.group(1) if match else None
def print(self):
@ -261,9 +251,7 @@ class Convertor(ABC):
error_scripts = []
for table_sql in self.table_script_list:
# 剥离 COMMENT 子句避免 DDLParser 无法处理中文等特殊字符
table_sql_for_parse = re.sub(r"\s+COMMENT\s+'[^']*'", "", table_sql)
ddl = DDLParser(table_sql_for_parse.replace("`", "")).run()
ddl = DDLParser(table_sql.replace("`", "")).run()
# 如果parse失败, 需要跟进
if len(ddl) == 0:
@ -278,23 +266,17 @@ class Convertor(ABC):
continue
# 解析注释
# 从原始 SQL 提取注释(支持中文等 DDLParser 不能处理的内容)
# 按表名定位完整建表段(以「换行 + )」起始的 ENGINE 行作为终止),避免被列 COMMENT 内的分号截断
orig_match = re.search(
rf"CREATE TABLE\s+`{re.escape(table_name)}`[\s\S]*?\n\)[^;]*;",
self.original_content,
flags=re.IGNORECASE,
)
orig_table_sql = orig_match.group() if orig_match else table_sql
comments_dict = dict(Convertor.filed_comments(orig_table_sql))
for column in table_ddl["columns"]:
column["comment"] = comments_dict.get(column["name"], "")
table_ddl["comment"] = self.table_comment(orig_table_sql) or ""
column["comment"] = bytes(column["comment"], "utf-8").decode(
r"unicode_escape"
)[1:-1]
table_ddl["comment"] = bytes(table_ddl["comment"], "utf-8").decode(
r"unicode_escape"
)[1:-1]
# 为每个表生成个6个基本部分
create = self.gen_create(table_ddl)
has_id = any(col["name"].lower() == "id" for col in table_ddl["columns"])
pk = self.gen_pk(table_name) if has_id else ""
pk = self.gen_pk(table_name)
uk = self.gen_uk(table_ddl)
index = self.gen_index(table_ddl)
comment = self.gen_comment(table_ddl)
@ -338,31 +320,25 @@ class PostgreSQLConvertor(Convertor):
if type == "varchar":
return f"varchar({size})"
if type in ("int", "int unsigned", "int unsigned zerofill"):
if type in ("int", "int unsigned"):
return "int4"
if type in ("bigint", "bigint unsigned"):
return "int8"
if type in ("tinyint", "smallint", "tinyint unsigned"):
return "int2"
if type in ("datetime", "timestamp null"):
if type == "datetime":
return "timestamp"
if type == "date":
return "date"
if type == "json":
return "jsonb"
if type == "double":
return "double precision"
if type == "timestamp":
return f"timestamp({size})" if size else "timestamp"
return f"timestamp({size})"
if type == "bit":
return "bool"
if type in ("tinyint", "smallint"):
return "int2"
if type in ("text", "longtext"):
return "text"
if type in ("blob", "mediumblob", "longblob"):
if type in ("blob", "mediumblob"):
return "bytea"
if type == "decimal":
return (
f"numeric({','.join(str(s) for s in size)})" if size and len(size) else "numeric"
f"numeric({','.join(str(s) for s in size)})" if len(size) else "numeric"
)
def gen_create(self, ddl: Dict) -> str:
@ -375,10 +351,6 @@ class PostgreSQLConvertor(Convertor):
type = col["type"].lower()
full_type = self.translate_type(type, col["size"])
if full_type is None:
raise NotImplementedError(
f"未支持的类型: '{col['type']}' (列: {name}, 表: {ddl['table_name']})"
)
nullable = "NULL" if col["nullable"] else "NOT NULL"
default = f"DEFAULT {col['default']}" if col["default"] is not None else ""
return f"{name} {full_type} {nullable} {default}"
@ -435,8 +407,6 @@ CREATE TABLE {table_name} (
"""生成 insert 语句,以及根据最后的 insert id+1 生成 Sequence"""
inserts = list(Convertor.inserts(table_name, self.content))
# 转换 MySQL 字符串转义为 PostgreSQL 格式:\\ -> \\' -> ''
inserts = [re.sub(r"\\\\|\\'", lambda m: "\\" if m.group() == "\\\\" else "''", s) for s in inserts]
## 生成 insert 脚本
script = ""
last_id = 0

View File

@ -14,30 +14,30 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties>
<revision>2026.04-jdk8-SNAPSHOT</revision>
<revision>2026.01-jdk8-SNAPSHOT</revision>
<flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
<!-- 统一依赖管理 -->
<spring.framework.version>5.3.39</spring.framework.version>
<spring.security.version>5.8.16</spring.security.version>
<spring.boot.version>2.7.18</spring.boot.version>
<spring.cloud.version>2021.0.9</spring.cloud.version> <!-- Spring Boot 2.X 最多使用 2021.0.9 版本 -->
<spring.cloud.alibaba.version>2021.0.6.2</spring.cloud.alibaba.version> <!-- Spring Boot 2.X 最多使用 2021.0.6.2 版本 -->
<spring.cloud.version>2021.0.9</spring.cloud.version>
<spring.cloud.alibaba.version>2021.0.6.2</spring.cloud.alibaba.version>
<!-- Web 相关 -->
<servlet.versoin>2.5</servlet.versoin>
<springdoc.version>1.8.0</springdoc.version>
<knife4j.version>4.5.0</knife4j.version>
<!-- DB 相关 -->
<druid.version>1.2.28</druid.version>
<druid.version>1.2.27</druid.version>
<mybatis.version>3.5.19</mybatis.version>
<mybatis-plus.version>3.5.16</mybatis-plus.version>
<mybatis-plus-join.version>1.5.7</mybatis-plus-join.version>
<mybatis-plus.version>3.5.15</mybatis-plus.version>
<mybatis-plus-join.version>1.5.5</mybatis-plus-join.version>
<dynamic-datasource.version>4.5.0</dynamic-datasource.version>
<easy-trans.version>3.0.6</easy-trans.version>
<redisson.version>4.3.1</redisson.version>
<redisson.version>3.52.0</redisson.version>
<dm8.jdbc.version>8.1.3.140</dm8.jdbc.version>
<kingbase.jdbc.version>9.0.1.jre7</kingbase.jdbc.version>
<opengauss.jdbc.version>7.0.0-RC3-og</opengauss.jdbc.version>
<taos.version>3.8.3</taos.version>
<kingbase.jdbc.version>8.6.0</kingbase.jdbc.version>
<opengauss.jdbc.version>5.1.0</opengauss.jdbc.version>
<taos.version>3.7.9</taos.version>
<!-- 消息队列 -->
<rocketmq-spring.version>2.3.5</rocketmq-spring.version>
<!-- RPC 相关 -->
@ -55,41 +55,38 @@
<jedis-mock.version>1.1.12</jedis-mock.version>
<mockito-inline.version>4.11.0</mockito-inline.version>
<!-- Bpm 工作流相关 -->
<flowable.version>6.8.1</flowable.version>
<flowable.version>6.8.0</flowable.version>
<!-- 工具类相关 -->
<anji-plus-captcha.version>1.4.0</anji-plus-captcha.version>
<jsoup.version>1.22.2</jsoup.version>
<lombok.version>1.18.46</lombok.version>
<jsoup.version>1.21.2</jsoup.version>
<lombok.version>1.18.42</lombok.version>
<mapstruct.version>1.6.3</mapstruct.version>
<hutool-5.version>5.8.44</hutool-5.version>
<hutool-5.version>5.8.42</hutool-5.version>
<fastexcel.version>1.3.0</fastexcel.version>
<velocity.version>2.4</velocity.version> <!-- JDK8 不能从 2.4 升级到 2.4.1,会报包不存在!!!! -->
<fastjson.version>1.2.83</fastjson.version>
<guava.version>33.6.0-jre</guava.version>
<guava.version>33.5.0-jre</guava.version>
<transmittable-thread-local.version>2.14.5</transmittable-thread-local.version>
<commons-net.version>3.13.0</commons-net.version>
<commons-net.version>3.12.0</commons-net.version>
<commons-lang3.version>3.20.0</commons-lang3.version>
<jsch.version>2.28.2</jsch.version>
<jsch.version>2.27.7</jsch.version>
<tika-core.version>2.9.3</tika-core.version> <!-- JDK8 不能从 2.9.3 升级到 3.X会报 JDK8 不支持 -->
<ip2region.version>2.7.0</ip2region.version>
<bizlog-sdk.version>3.0.6</bizlog-sdk.version>
<reflections.version>0.10.2</reflections.version>
<netty.version>4.2.12.Final</netty.version>
<netty.version>4.2.9.Final</netty.version>
<mqtt.version>1.2.5</mqtt.version>
<vertx.version>4.5.26</vertx.version>
<vertx.version>4.5.22</vertx.version>
<okhttp.version>4.12.0</okhttp.version>
<californium.version>3.14.0</californium.version>
<j2mod.version>3.3.0</j2mod.version>
<httpclient5.version>5.5.2</httpclient5.version> <!-- WxJava 4.8.x 需要 HttpClient5 5.4+Spring Boot 2.7 默认 5.1.4 不兼容 -->
<httpcore5.version>5.3.6</httpcore5.version> <!-- 配套 httpclient5 5.5.2Spring Boot 2.7 默认 5.1.5 不兼容 -->
<californium.version>3.12.0</californium.version>
<!-- 三方云服务相关 -->
<awssdk.version>2.44.0</awssdk.version>
<awssdk.version>2.40.15</awssdk.version>
<justauth.version>1.16.7</justauth.version>
<justauth-starter.version>1.4.0</justauth-starter.version>
<jimureport.version>2.3.2</jimureport.version>
<jimubi.version>2.3.2</jimubi.version>
<weixin-java.version>4.8.2-20260501.180637</weixin-java.version>
<alipay-sdk-java.version>4.40.771.ALL</alipay-sdk-java.version>
<jimureport.version>2.1.3</jimureport.version>
<jimubi.version>2.3.0</jimubi.version>
<weixin-java.version>4.7.9-20251224.161447</weixin-java.version>
<alipay-sdk-java.version>4.40.607.ALL</alipay-sdk-java.version>
<!-- 专属于 JDK8 安全漏洞升级 -->
<logback.version>1.2.13</logback.version> <!-- 无法使用 1.3.X 版本,启动会报错 -->
</properties>
@ -310,7 +307,7 @@
<exclusion>
<groupId>org.redisson</groupId>
<!-- 使用 redisson-spring-data-27 替代,解决 Tuple NoClassDefFoundError 报错 -->
<artifactId>redisson-spring-data-40</artifactId> <!-- Redisson 4.x 默认依赖 spring-data-40排除后使用 spring-data-27 适配 Spring Boot 2.7 -->
<artifactId>redisson-spring-data-35</artifactId>
</exclusion>
</exclusions>
</dependency>
@ -674,30 +671,6 @@
<version>${californium.version}</version>
</dependency>
<!-- Modbus 相关 -->
<dependency>
<groupId>com.ghgande</groupId>
<artifactId>j2mod</artifactId>
<version>${j2mod.version}</version>
</dependency>
<!-- WxJava 4.8.x 需要 HttpClient5 5.4+,覆盖 Spring Boot 2.7 默认的 5.1.4 -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>${httpclient5.version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.core5</groupId>
<artifactId>httpcore5</artifactId>
<version>${httpcore5.version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.core5</groupId>
<artifactId>httpcore5-h2</artifactId>
<version>${httpcore5.version}</version>
</dependency>
<!-- 三方云服务相关 -->
<dependency>
<groupId>software.amazon.awssdk</groupId>

View File

@ -349,27 +349,4 @@ public class CollectionUtils {
return (LinkedHashSet<T>) toCollection(LinkedHashSet.class, elementType, value);
}
public static boolean dfs(Long node, Map<Long, Set<Long>> graph) {
return dfs(node, graph, new HashSet<>(), new HashSet<>());
}
private static boolean dfs(Long node, Map<Long, Set<Long>> graph, Set<Long> visited, Set<Long> inStack) {
if (inStack.contains(node)) {
return true;
}
if (visited.contains(node)) {
return false;
}
visited.add(node);
inStack.add(node);
Set<Long> neighbors = graph.getOrDefault(node, Collections.emptySet());
for (Long neighbor : neighbors) {
if (dfs(neighbor, graph, visited, inStack)) {
return true;
}
}
inStack.remove(node);
return false;
}
}

View File

@ -335,27 +335,6 @@ public class LocalDateTimeUtils {
}
}
/**
*
*
* @param date
* @return
*/
public static LocalDate getQuarterStart(LocalDate date) {
Month firstMonthOfQuarter = date.getMonth().firstMonthOfQuarter();
return LocalDate.of(date.getYear(), firstMonthOfQuarter, 1);
}
/**
*
*
* @param date
* @return
*/
public static LocalDate getWeekStart(LocalDate date) {
return date.with(DayOfWeek.MONDAY);
}
/**
* {@link LocalDateTime} Unix 1970-01-01T00:00:00Z
*

View File

@ -1,7 +1,9 @@
package cn.iocoder.yudao.framework.common.util.http;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.map.TableMap;
import cn.hutool.core.net.url.UrlBuilder;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
@ -37,10 +39,8 @@ public class HttpUtils {
}
/**
* URL query parameter
* + query parameter URL path
* URL
*
* @see #decodeUrlPath(String)
* @param value
* @return
*/
@ -49,25 +49,14 @@ public class HttpUtils {
return URLDecoder.decode(value, StandardCharsets.UTF_8.name());
}
/**
* URL
* {@link #decodeUtf8(String)} + +
* URL path
*
* @param path URL
* @return
*/
@SneakyThrows
public static String decodeUrlPath(String path) {
// 先将 + 替换为 %2B避免被 URLDecoder 解码为空格
String encoded = path.replace("+", "%2B");
return URLDecoder.decode(encoded, StandardCharsets.UTF_8.name());
}
@SuppressWarnings("unchecked")
public static String replaceUrlQuery(String url, String key, String value) {
UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset());
// 先移除;再添加
builder.getQuery().remove(key);
// 先移除
TableMap<CharSequence, CharSequence> query = (TableMap<CharSequence, CharSequence>)
ReflectUtil.getFieldValue(builder.getQuery(), "query");
query.remove(key);
// 后添加
builder.addQuery(key, value);
return builder.build();
}

View File

@ -173,24 +173,6 @@ public class JsonUtils {
}
}
/**
* JSON null
*
* @param text
* @param clazz
* @return
*/
public static <T> T parseObjectQuietly(String text, Class<T> clazz) {
if (StrUtil.isEmpty(text)) {
return null;
}
try {
return objectMapper.readValue(text, clazz);
} catch (IOException e) {
return null;
}
}
public static <T> List<T> parseArray(String text, Class<T> clazz) {
if (StrUtil.isEmpty(text)) {
return new ArrayList<>();

View File

@ -60,11 +60,6 @@ public class ObjectUtils {
return Arrays.asList(array).contains(obj);
}
@SafeVarargs
public static <T> boolean notEqualsAny(T obj, T... array) {
return !Arrays.asList(array).contains(obj);
}
public static boolean isNotAllEmpty(Object... objs) {
return !ObjectUtil.isAllEmpty(objs);
}

View File

@ -1,52 +0,0 @@
package cn.iocoder.yudao.framework.common.util.http;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* {@link HttpUtils}
*/
public class HttpUtilsTest {
@Test
public void testReplaceUrlQuery_replace() {
// 准备参数
String url = "https://www.iocoder.cn/path?a=1&b=2";
// 调用
String result = HttpUtils.replaceUrlQuery(url, "a", "3");
// 断言:被替换的 key 会移到末尾,原顺序的其它参数保留
assertEquals("https://www.iocoder.cn/path?b=2&a=3", result);
}
@Test
public void testReplaceUrlQuery_add() {
// 准备参数
String url = "https://www.iocoder.cn/path?a=1";
// 调用
String result = HttpUtils.replaceUrlQuery(url, "b", "2");
// 断言
assertEquals("https://www.iocoder.cn/path?a=1&b=2", result);
}
@Test
public void testReplaceUrlQuery_noQuery() {
// 准备参数:原 URL 没有 query
String url = "https://www.iocoder.cn/path";
// 调用
String result = HttpUtils.replaceUrlQuery(url, "a", "1");
// 断言
assertEquals("https://www.iocoder.cn/path?a=1", result);
}
@Test
public void testReplaceUrlQuery_emptyValue() {
// 准备参数value 为空字符串
String url = "https://www.iocoder.cn/path?a=1";
// 调用
String result = HttpUtils.replaceUrlQuery(url, "a", "");
// 断言:保留 keyvalue 为空
assertEquals("https://www.iocoder.cn/path?a=", result);
}
}

View File

@ -8,7 +8,6 @@ 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;
@ -26,46 +25,44 @@ 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<Integer, Area> areas;
static {
init();
}
/**
*
*/
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<CsvRow> 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);
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<CsvRow> 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);
}
// 构建父子关系:因为 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);
}
/**

View File

@ -3,10 +3,11 @@ 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
*
@ -15,29 +16,30 @@ import org.lionsoul.ip2region.xdb.Searcher;
* @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 static void init() {
private IPUtils() {
try {
long now = System.currentTimeMillis();
byte[] bytes = ResourceUtil.readBytes("ip2region.xdb");
SEARCHER = Searcher.newWithBuffer(bytes);
log.info("启动加载 IPUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now);
} catch (Exception e) {
throw new RuntimeException("IPUtils 初始化失败", e);
} catch (IOException e) {
log.error("启动加载 IPUtils 失败", e);
}
}

View File

@ -5,12 +5,7 @@ import cn.iocoder.yudao.framework.ip.core.Area;
import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* {@link AreaUtils}
@ -36,46 +31,6 @@ public class AreaUtilsTest {
assertEquals(AreaUtils.format(110105), "北京市 北京市 朝阳区");
assertEquals(AreaUtils.format(1), "中国");
assertEquals(AreaUtils.format(2), "蒙古");
// 中国台湾省:省/市/区三级
assertEquals(AreaUtils.format(710101), "台湾省 台北市 中正区");
// 自定义分隔符
assertEquals(AreaUtils.format(110105, "/"), "北京市/北京市/朝阳区");
// 不存在的编号
assertNull(AreaUtils.format(-1));
}
@Test
public void testParseArea() {
// 调用:通过路径解析得到地区
Area area = AreaUtils.parseArea("北京市/北京市/朝阳区");
// 断言
assertNotNull(area);
assertEquals(area.getId(), 110105);
// 路径不存在时返回 null
assertNull(AreaUtils.parseArea("不存在/路径"));
}
@Test
public void testGetParentIdByType() {
// 调用:朝阳区向上找省
Integer provinceId = AreaUtils.getParentIdByType(110105, AreaTypeEnum.PROVINCE);
// 断言
assertEquals(provinceId, 110000);
// 自身就是目标类型
assertEquals(AreaUtils.getParentIdByType(110000, AreaTypeEnum.PROVINCE), 110000);
// 不存在的编号返回 null
assertNull(AreaUtils.getParentIdByType(-1, AreaTypeEnum.PROVINCE));
}
@Test
public void testGetByType() {
// 调用:获取所有省份
List<Area> provinces = AreaUtils.getByType(AreaTypeEnum.PROVINCE, area -> area);
// 断言:包含北京、台湾、香港、澳门
assertTrue(provinces.stream().anyMatch(area -> "北京市".equals(area.getName())));
assertTrue(provinces.stream().anyMatch(area -> "台湾省".equals(area.getName())));
assertTrue(provinces.stream().anyMatch(area -> "香港特别行政区".equals(area.getName())));
assertTrue(provinces.stream().anyMatch(area -> "澳门特别行政区".equals(area.getName())));
}
}

View File

@ -36,17 +36,6 @@
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
<!--
TODO 芋艿WxJava 4.8.x 的 AbstractWxMpConfigStorageConfiguration 仍引用了 HttpClient 4.x 的
org.apache.http.ssl.TrustStrategy 类。升级 Spring Cloud Alibaba 到 2025.0.0.0 后Nacos 不再
传递 HttpClient 4.xhttpcore导致 ClassNotFoundException。
临时解决:显式引入 httpclient 4.x。待 WxJava 修复后移除。
-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>
<!-- 工具相关 -->
<dependency>

View File

@ -9,7 +9,6 @@ import uk.co.jemos.podam.api.PodamFactory;
import uk.co.jemos.podam.api.PodamFactoryImpl;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Date;
@ -53,10 +52,6 @@ public class RandomUtils {
}
return RandomUtil.randomInt();
});
// BigDecimal限制精度在 DECIMAL(10,2) 范围内,避免 H2 等数据库溢出
PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(BigDecimal.class,
(dataProviderStrategy, attributeMetadata, map) ->
BigDecimal.valueOf(RandomUtil.randomInt(0, 10000000), 2));
// LocalDateTime
PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(LocalDateTime.class,
(dataProviderStrategy, attributeMetadata, map) -> randomLocalDateTime());

View File

@ -192,13 +192,6 @@ spring:
- Path=/admin-api/iot/**
filters:
- RewritePath=/admin-api/iot/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs
## mes-server 服务
- id: mes-admin-api # 路由的编号
uri: grayLb://mes-server
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
- Path=/admin-api/mes/**
filters:
- RewritePath=/admin-api/mes/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs
x-forwarded:
prefix-enabled: false # 避免 Swagger 重复带上额外的 /admin-api/system 前缀
default-filters: # 全局过滤器,对应 GatewayFilterDefinition 数组
@ -258,9 +251,6 @@ knife4j:
- name: iot-server
service-name: iot-server
url: /admin-api/iot/v3/api-docs
- name: mes-server
service-name: mes-server
url: /admin-api/mes/v3/api-docs
--- #################### 芋道相关配置 ####################

View File

@ -19,9 +19,9 @@
国外OpenAI、Ollama、Midjourney、StableDiffusion、Suno
</description>
<properties>
<spring-ai.version>1.1.5</spring-ai.version>
<spring-ai.version>1.1.2</spring-ai.version>
<!-- https://mvnrepository.com/artifact/com.alibaba.cloud.ai/spring-ai-alibaba -->
<alibaba-ai.version>1.1.2.2</alibaba-ai.version>
<alibaba-ai.version>1.1.0.0-RC2</alibaba-ai.version>
<tinyflow.version>1.2.6</tinyflow.version>
</properties>

View File

@ -83,15 +83,6 @@ public class AiKnowledgeSegmentController {
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除段落")
@Parameter(name = "id", description = "段落编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('ai:knowledge:delete')")
public CommonResult<Boolean> deleteKnowledgeSegment(@RequestParam("id") Long id) {
segmentService.deleteKnowledgeSegment(id);
return success(true);
}
@GetMapping("/split")
@Operation(summary = "切片内容")
@Parameters({

View File

@ -109,7 +109,6 @@ public class AiImageServiceImpl implements AiImageService {
}
@Async
@SuppressWarnings("ConstantValue")
public void executeDrawImage(AiImageDO image, AiImageDrawReqVO reqVO, AiModelDO model) {
try {
// 1.1 构建请求
@ -165,8 +164,8 @@ public class AiImageServiceImpl implements AiImageService {
.build();
} else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.TONG_YI.getPlatform())) {
return DashScopeImageOptions.builder()
.model(model.getModel()).n(1)
.height(draw.getHeight()).width(draw.getWidth())
.withModel(model.getModel()).withN(1)
.withHeight(draw.getHeight()).withWidth(draw.getWidth())
.build();
} else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.YI_YAN.getPlatform())) {
return QianFanImageOptions.builder()

View File

@ -98,13 +98,6 @@ public interface AiKnowledgeSegmentService {
*/
void updateKnowledgeSegmentStatus(AiKnowledgeSegmentUpdateStatusReqVO reqVO);
/**
*
*
* @param id
*/
void deleteKnowledgeSegment(Long id);
/**
*
*

View File

@ -141,19 +141,6 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
}
}
@Override
public void deleteKnowledgeSegment(Long id) {
// 1. 校验段落存在
AiKnowledgeSegmentDO segment = validateKnowledgeSegmentExists(id);
// 2. 删除向量
VectorStore vectorStore = getVectorStoreById(segment.getKnowledgeId());
deleteVectorStore(vectorStore, segment);
// 3. 删除段落记录
segmentMapper.deleteById(id);
}
@Override
public void deleteKnowledgeSegmentByDocumentId(Long documentId) {
// 1. 查询需要删除的段落

View File

@ -1,9 +1,7 @@
package cn.iocoder.yudao.module.ai.util;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
@ -35,28 +33,6 @@ public class AiUtils {
public static final String TOOL_CONTEXT_LOGIN_USER = "LOGIN_USER";
public static final String TOOL_CONTEXT_TENANT_ID = "TENANT_ID";
/**
*
*
* @see <a href="https://bailian.console.aliyun.com/cn-beijing/?tab=model#/model-market/all?providers=qwen&capabilities=VU">广</a>
* @see <a href="https://help.aliyun.com/zh/model-studio/error-code#error-url"> withMultiModel </a>
*/
public static final Set<String> TONG_YI_MULTI_MODELS = SetUtils.asSet(
// qwen3.5 / 3.6 系列(统一多模态主干)
"qwen3.6-plus", "qwen3.6-flash",
"qwen3.5-plus", "qwen3.5-flash",
// qwen-vl 视觉理解
"qwen3-vl-plus", "qwen3-vl-flash",
"qwen-vl-max", "qwen-vl-plus",
"qwen2.5-vl-72b-instruct", "qwen2.5-vl-32b-instruct",
"qwen2.5-vl-7b-instruct", "qwen2.5-vl-3b-instruct",
// qvq 视觉推理
"qvq-max", "qvq-plus",
// qwen-omni 全模态
"qwen3.5-omni-plus", "qwen3.5-omni-flash",
"qwen3-omni-flash", "qwen-omni-turbo"
);
public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens) {
return buildChatOptions(platform, model, temperature, maxTokens, null, null);
}
@ -68,10 +44,9 @@ public class AiUtils {
// noinspection EnhancedSwitchMigration
switch (platform) {
case TONG_YI:
return DashScopeChatOptions.builder().model(model).temperature(temperature).maxToken(maxTokens)
.enableThinking(true) // TODO 芋艿:默认都开启 thinking 模式,后续可以让用户配置
.multiModel(TONG_YI_MULTI_MODELS.contains(model)) // 是否多模态模型
.toolCallbacks(toolCallbacks).toolContext(toolContext).build();
return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens)
.withEnableThinking(true) // TODO 芋艿:默认都开启 thinking 模式,后续可以让用户配置
.withToolCallbacks(toolCallbacks).withToolContext(toolContext).build();
case YI_YAN:
return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build();
case DEEP_SEEK:
@ -150,13 +125,10 @@ public class AiUtils {
|| response.getResult().getOutput() == null) {
return null;
}
AssistantMessage output = response.getResult().getOutput();
// DeepSeek 通过专属 AssistantMessage 暴露 reasoningContent
if (output instanceof DeepSeekAssistantMessage) {
return ((DeepSeekAssistantMessage) output).getReasoningContent();
if (response.getResult().getOutput() instanceof DeepSeekAssistantMessage) {
return ((DeepSeekAssistantMessage) (response.getResult().getOutput())).getReasoningContent();
}
// 通义千问等通过 metadata 透传 reasoningContent
return MapUtil.getStr(output.getMetadata(), "reasoningContent");
return null;
}
}

View File

@ -103,7 +103,7 @@ xxl:
# Lock4j 配置项
lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
--- #################### 监控相关配置 ####################

View File

@ -98,7 +98,7 @@ xxl:
# Lock4j 配置项
lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
--- #################### 监控相关配置 ####################

View File

@ -34,15 +34,15 @@ public class TongYiChatModelTests {
private final DashScopeChatModel chatModel = DashScopeChatModel.builder()
.dashScopeApi(DashScopeApi.builder()
.apiKey("sk-cd9f39e99ea54840bd1888282325f55a") // https://bailian.console.aliyun.com/cn-beijing/?tab=model#/api-key 获取密钥
.apiKey("sk-47aa124781be4bfb95244cc62f63f7d0")
.build())
.defaultOptions(DashScopeChatOptions.builder()
.multiModel(true) // 注意:当使用 qwen3.6-plus 等多模态模型,需要设置为 true可见 https://help.aliyun.com/zh/model-studio/error-code#error-url 链接
.model("qwen3.6-plus") // 模型
// .model("deepseek-r1") // 模型deepseek-r1
// .model("deepseek-v3") // 模型deepseek-v3
// .model("deepseek-r1-distill-qwen-1.5b") // 模型deepseek-r1-distill-qwen-1.5b
// .enableThinking(true)
// .withModel("qwen1.5-72b-chat") // 模型
.withModel("qwen3-235b-a22b-thinking-2507") // 模型
// .withModel("deepseek-r1") // 模型deepseek-r1
// .withModel("deepseek-v3") // 模型deepseek-v3
// .withModel("deepseek-r1-distill-qwen-1.5b") // 模型deepseek-r1-distill-qwen-1.5b
// .withEnableThinking(true)
.build())
.build();
@ -85,9 +85,9 @@ public class TongYiChatModelTests {
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
DashScopeChatOptions options = DashScopeChatOptions.builder()
.model("qwen3.6-plus").multiModel(true)
.withModel("qwen3-235b-a22b-thinking-2507")
// .withModel("qwen-max-2025-01-25")
.enableThinking(true) // 必须设置,否则会报错
.withEnableThinking(true) // 必须设置,否则会报错
.build();
// 调用
@ -112,8 +112,8 @@ public class TongYiChatModelTests {
Document document01 = new Document("abc");
Document document02 = new Document("sapring");
RerankOptions options = DashScopeRerankOptions.builder()
.topN(1)
.model("gte-rerank-v2")
.withTopN(1)
.withModel("gte-rerank-v2")
.build();
RerankRequest rerankRequest = new RerankRequest(
query,

View File

@ -12,34 +12,23 @@ import org.springframework.ai.image.ImageResponse;
/**
* {@link DashScopeImageModel}
*
* TODO @spring-ai-alibaba-dashscope1.1.2.2 {@code DashScopeImageApi#resolveImagePath} {@code wan2.7-image}
* {@code text2image/image-synthesis} + {@code prompt}
* {@code multimodal-generation/generation} + {@code messages}
* SDK
*
* SDK {@code wan2.6-image} {@code qwen-image}
* {@code DashScopeImageApi} {@code wan2.7*} {@code wan2.6-image} {@code image-generation/generation}
*
* @author fansili
*/
public class TongYiImagesModelTest {
private final DashScopeImageModel imageModel = DashScopeImageModel.builder()
.dashScopeApi(DashScopeImageApi.builder()
.apiKey("sk-cd9f39e99ea54840bd1888282325f55a") // https://bailian.console.aliyun.com/cn-beijing/?tab=model#/api-key 获取密钥
.apiKey("sk-47aa124781be4bfb95244cc62f63f7d0")
.build())
.build();
// TODO @芋艿:
@Test
@Disabled
public void imageCallTest() {
// 准备参数
ImageOptions options = DashScopeImageOptions.builder()
.model("wan2.7-image")
// .withSize("2k")
.height(768).width(768)
.n(1)
.withModel("wanx-v1")
.withHeight(256).withWidth(256)
.build();
ImagePrompt prompt = new ImagePrompt("中国长城!", options);

View File

@ -1,38 +0,0 @@
package cn.iocoder.yudao.module.bpm.enums.definition;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* BPM
*
* @author Lesan
*/
@RequiredArgsConstructor
@Getter
public enum BpmConditionOpCodeEnum {
EQ("==", "等于", " var:getOrDefault(%s, null) == %s "),
NE("!=", "不等于", " var:getOrDefault(%s, null) != %s "),
GT(">", "大于", " var:getOrDefault(%s, null) > %s "),
GE(">=", "大于等于", " var:getOrDefault(%s, null) >= %s "),
LT("<", "小于", " var:getOrDefault(%s, null) < %s "),
LE("<=", "小于等于", " var:getOrDefault(%s, null) <= %s "),
CONTAINS("contain", "包含", " var:contains(%s, %s) "),
NOT_CONTAINS("!contain", "不包含", " !var:contains(%s, %s) ");
private final String code;
private final String des;
private final String symbol;
public static BpmConditionOpCodeEnum fromCode(String code) {
for (BpmConditionOpCodeEnum op : BpmConditionOpCodeEnum.values()) {
if (op.code.equalsIgnoreCase(code)) {
return op;
}
}
throw new IllegalArgumentException("未知操作符: " + code);
}
}

View File

@ -2,8 +2,6 @@ package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form;
import lombok.Data;
import java.util.List;
/**
* VO
*/
@ -22,9 +20,5 @@ public class BpmFormFieldVO {
*
*/
private String title;
/**
*
*/
private List<BpmFormFieldVO> children;
}

View File

@ -66,15 +66,15 @@ public class BpmProcessInstanceCopyController {
Map<String, HistoricProcessInstance> processInstanceMap = processInstanceService.getHistoricProcessInstanceMap(
convertSet(pageResult.getList(), BpmProcessInstanceCopyDO::getProcessInstanceId));
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertListByFlatMap(pageResult.getList(),
copy -> Stream.of(copy.getStartUserId(), copy.getUserId())));
copy -> Stream.of(copy.getStartUserId(), Long.parseLong(copy.getCreator()))));
Map<String, BpmProcessDefinitionInfoDO> processDefinitionInfoMap = processDefinitionService.getProcessDefinitionInfoMap(
convertSet(pageResult.getList(), BpmProcessInstanceCopyDO::getProcessDefinitionId));
return success(convertPage(pageResult, copy -> {
BpmProcessInstanceCopyRespVO copyVO = BeanUtils.toBean(copy, BpmProcessInstanceCopyRespVO.class);
MapUtils.findAndThen(userMap, copy.getUserId(),
user -> copyVO.setCreateUser(BeanUtils.toBean(user, UserSimpleBaseVO.class)));
MapUtils.findAndThen(userMap, copy.getStartUserId(),
MapUtils.findAndThen(userMap, Long.valueOf(copy.getCreator()),
user -> copyVO.setStartUser(BeanUtils.toBean(user, UserSimpleBaseVO.class)));
MapUtils.findAndThen(userMap, copy.getStartUserId(),
user -> copyVO.setCreateUser(BeanUtils.toBean(user, UserSimpleBaseVO.class)));
MapUtils.findAndThen(processInstanceMap, copyVO.getProcessInstanceId(),
processInstance -> {
copyVO.setSummary(FlowableUtils.getSummary(

View File

@ -42,7 +42,7 @@ public class BpmOALeaveDO extends BaseDO {
/**
*
*/
private Integer type;
private String type;
/**
*
*/

View File

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

View File

@ -7,10 +7,10 @@ import org.springframework.stereotype.Component;
/**
* variable
*
* @deprecated
* ConditionNodeConvert buildConditionExpression
*
* @author jason
*/
@Deprecated // TODO @芋艿:兼容老版本,预计 27 年删除;
@Component
public class VariableConvertByTypeExpressionFunction extends AbstractFlowableVariableExpressionFunction {

View File

@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.core.KeyValue;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
@ -248,7 +247,9 @@ public class FlowableUtils {
Map<String, BpmFormFieldVO> formFieldsMap = new HashMap<>();
processDefinitionInfo.getFormFields().forEach(formFieldStr -> {
BpmFormFieldVO formField = JsonUtils.parseObject(formFieldStr, BpmFormFieldVO.class);
parseFormField(formField, formFieldsMap);
if (formField != null) {
formFieldsMap.put(formField.getField(), formField);
}
});
// 情况一:当自定义了摘要
@ -272,26 +273,6 @@ public class FlowableUtils {
.collect(Collectors.toList());
}
/**
*
*/
private static void parseFormField(BpmFormFieldVO formField, Map<String, BpmFormFieldVO> formFieldsMap) {
if (formField == null) {
return;
}
// 如果存在 children -> 说明是布局组件
if (formField.getChildren() != null && !formField.getChildren().isEmpty()) {
for (BpmFormFieldVO child : formField.getChildren()) {
parseFormField(child, formFieldsMap);
}
return;
}
// 真实字段才加入 map
if (StrUtil.isNotBlank(formField.getField())) {
formFieldsMap.put(formField.getField(), formField);
}
}
// ========== Task 相关的工具方法 ==========
/**

View File

@ -707,9 +707,10 @@ public class SimpleModelUtils {
List<String> list = convertList(item.getRules(), (rule) -> {
String rightSide = NumberUtil.isNumber(rule.getRightSide()) ? rule.getRightSide()
: "\"" + rule.getRightSide() + "\""; // 如果非数值类型加引号
return String.format(BpmConditionOpCodeEnum.fromCode(rule.getOpCode()).getSymbol(),
return String.format(" vars:getOrDefault(%s, null) %s var:convertByType(%s,%s) ",
rule.getLeftSide(), // 左侧:读取变量
rightSide); // 右侧:取值变量
rule.getOpCode(), // 中间:操作符,比较
rule.getLeftSide(), rightSide); // 右侧转换变量VariableConvertByTypeExpressionFunction
});
// 构造条件组的表达式
Boolean and = item.getAnd();

View File

@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.bpm.service.definition.dto;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
import lombok.Data;
/**
* BPM MetaInfo Response DTO
* { Model#setMetaInfo(String)}
*
* {@link cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO}
*
* @author
*/
@Data
public class BpmModelMetaInfoRespDTO {
/**
*
*/
private String icon;
/**
*
*/
private String description;
/**
*
*/
private Integer formType;
/**
*
* {@link BpmModelFormTypeEnum#NORMAL}
*/
private Long formId;
/**
* 使 Vue
* {@link BpmModelFormTypeEnum#CUSTOM}
*/
private String formCustomCreatePath;
/**
* 使 Vue
* {@link BpmModelFormTypeEnum#CUSTOM}
*/
private String formCustomViewPath;
}

View File

@ -87,7 +87,7 @@ xxl:
# Lock4j 配置项
lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
--- #################### 监控相关配置 ####################

View File

@ -98,7 +98,7 @@ xxl:
# Lock4j 配置项
lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
--- #################### 监控相关配置 ####################

View File

@ -27,12 +27,10 @@ public interface CrmCustomerLimitConfigMapper extends BaseMapperX<CrmCustomerLim
Integer type, Long userId, Long deptId) {
LambdaQueryWrapperX<CrmCustomerLimitConfigDO> query = new LambdaQueryWrapperX<CrmCustomerLimitConfigDO>()
.eq(CrmCustomerLimitConfigDO::getType, type);
query.and(w -> {
w.apply("FIND_IN_SET({0}, user_ids) > 0", userId);
if (deptId != null) {
w.or().apply("FIND_IN_SET({0}, dept_ids) > 0", deptId);
}
});
query.apply("FIND_IN_SET({0}, user_ids) > 0", userId);
if (deptId != null) {
query.apply("FIND_IN_SET({0}, dept_ids) > 0", deptId);
}
return selectList(query);
}

View File

@ -87,7 +87,7 @@ xxl:
# Lock4j 配置项
lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
--- #################### 监控相关配置 ####################

View File

@ -96,7 +96,7 @@ xxl:
# Lock4j 配置项
lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
--- #################### 监控相关配置 ####################

View File

@ -87,7 +87,7 @@ xxl:
# Lock4j 配置项
lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
--- #################### 监控相关配置 ####################

View File

@ -96,7 +96,7 @@ xxl:
# Lock4j 配置项
lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
--- #################### 监控相关配置 ####################

View File

@ -55,10 +55,4 @@ public class CodegenProperties {
@NotNull(message = "是否生成单元测试不能为空")
private Boolean unitTestEnable;
/**
* Excel
*/
@NotNull(message = "是否生成 Excel 导入接口不能为空")
private Boolean importEnable;
}

View File

@ -53,7 +53,6 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
AwsCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(
AwsBasicCredentials.create(config.getAccessKey(), config.getAccessSecret()));
URI endpoint = URI.create(buildEndpoint());
URI presignerEndpoint = URI.create(buildPresignerEndpoint());
S3Configuration serviceConfiguration = S3Configuration.builder() // Path-style 访问
.pathStyleAccessEnabled(Boolean.TRUE.equals(config.getEnablePathStyleAccess()))
.chunkedEncodingEnabled(false) // 禁用分块编码,参见 https://t.zsxq.com/kBy57
@ -67,7 +66,7 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
presigner = S3Presigner.builder()
.credentialsProvider(credentialsProvider)
.region(region)
.endpointOverride(presignerEndpoint)
.endpointOverride(endpoint)
.serviceConfiguration(serviceConfiguration)
.build();
}
@ -117,7 +116,7 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
public String presignGetUrl(String url, Integer expirationSeconds) {
// 1. 将 url 转换为 path
String path = StrUtil.removePrefix(url, config.getDomain() + "/");
path = HttpUtils.decodeUrlPath(HttpUtils.removeUrlQuery(path));
path = HttpUtils.decodeUtf8(HttpUtils.removeUrlQuery(path));
// 2.1 情况一:公开访问:无需签名
// 考虑到老版本的兼容,所以必须是 config.getEnablePublicAccess() 为 false 时,才进行签名
@ -162,23 +161,6 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
return StrUtil.format("https://{}", config.getEndpoint());
}
/**
* presigner
*
* @return
*/
private String buildPresignerEndpoint() {
// 补全 domain
if (StrUtil.isEmpty(config.getDomain())) {
config.setDomain(buildDomain());
}
if (Boolean.TRUE.equals(config.getEnablePathStyleAccess())) {
return StrUtil.removeSuffix(config.getDomain(), StrUtil.format("/{}", config.getBucket()));
}
return StrUtil.replace(config.getDomain(), StrUtil.format("://{}.", config.getBucket()), "://");
}
/**
* AWS
* region > endpoint region > us-east-1

View File

@ -7,7 +7,6 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
@ -41,9 +40,6 @@ public class AdminServerConfiguration {
@Value("${spring.boot.admin.client.password:admin}")
private String password;
@Value("${spring.boot.admin.frame-ancestors:'self'}")
private String frameAncestors;
/**
* Spring Boot Admin InMemoryUserDetailsManager
* 使
@ -104,16 +100,6 @@ public class AdminServerConfiguration {
adminSeverContextPath + "/instances", // Admin Client 注册端点忽略 CSRF
adminSeverContextPath + "/actuator/**" // Actuator 端点忽略 CSRF
)
)
.headers(headers -> headers
// 特殊Spring Boot Admin 前端基于 Vue需 unsafe-inline unsafe-eval 支持内联脚本与表达式
.contentSecurityPolicy(csp -> csp.policyDirectives(
"default-src 'self'; "
+ "script-src 'self' 'unsafe-inline' 'unsafe-eval'; "
+ "style-src 'self' 'unsafe-inline'; "
+ "frame-ancestors " + frameAncestors))
.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin) // 显式设置 X-Frame-Options 为 SAMEORIGIN
.cacheControl(HeadersConfigurer.CacheControlConfig::disable) // 禁用缓存,避免旧配置生效
);
return httpSecurity.build();
}

View File

@ -12,7 +12,6 @@ import cn.iocoder.yudao.module.infra.enums.codegen.CodegenColumnListConditionEnu
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
import com.baomidou.mybatisplus.generator.config.po.TableField;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Sets;
import org.springframework.stereotype.Component;
@ -118,8 +117,8 @@ public class CodegenBuilder {
table.setBusinessName(toCamelCase(subAfter(tableName, '_', false)).toLowerCase());
// 驼峰 + 首字母大写;第一步,第一个 _ 前缀的后面,作为 class 名字;第二步,驼峰命名
table.setClassName(upperFirst(toCamelCase(subAfter(tableName, '_', false))));
// 去除结尾的表,作为类描述;注释中的英文引号替换为中文引号,避免破坏生成代码中的字符串字面量
table.setClassComment(StrUtil.removeSuffixIgnoreCase(sanitizeComment(table.getTableComment()), "表"));
// 去除结尾的表,作为类描述
table.setClassComment(StrUtil.removeSuffixIgnoreCase(table.getTableComment(), "表"));
table.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
}
@ -129,7 +128,6 @@ public class CodegenBuilder {
for (CodegenColumnDO column : columns) {
column.setTableId(tableId);
column.setOrdinalPosition(index++);
column.setColumnComment(sanitizeComment(column.getColumnComment()));
// 特殊处理Byte => Integer
if (Byte.class.getSimpleName().equals(column.getJavaType())) {
column.setJavaType(Integer.class.getSimpleName());
@ -219,18 +217,4 @@ public class CodegenBuilder {
}
}
/**
*
*
* @param comment
* @return
*/
@VisibleForTesting
String sanitizeComment(String comment) {
if (StrUtil.isEmpty(comment)) {
return comment;
}
return comment.replace('"', '“').replace('\'', '');
}
}

View File

@ -72,8 +72,6 @@ public class CodegenEngine {
.put(javaTemplatePath("controller/vo/listReqVO"), javaModuleImplVOFilePath("ListReqVO"))
.put(javaTemplatePath("controller/vo/respVO"), javaModuleImplVOFilePath("RespVO"))
.put(javaTemplatePath("controller/vo/saveReqVO"), javaModuleImplVOFilePath("SaveReqVO"))
.put(javaTemplatePath("controller/vo/importExcelVO"), javaModuleImplVOFilePath("ImportExcelVO"))
.put(javaTemplatePath("controller/vo/importRespVO"), javaModuleImplVOFilePath("ImportRespVO"))
.put(javaTemplatePath("controller/controller"), javaModuleImplControllerFilePath())
.put(javaTemplatePath("dal/do"),
javaModuleImplMainFilePath("dal/dataobject/${table.businessName}/${table.className}DO"))
@ -128,8 +126,6 @@ public class CodegenEngine {
vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
.put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("views/form.vue"),
vue3FilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Form.vue"))
.put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("views/import.vue"),
vue3FilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}ImportForm.vue"))
.put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("views/components/form_sub_normal.vue"), // 特殊:主子表专属逻辑
vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue"))
.put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("views/components/form_sub_inner.vue"), // 特殊:主子表专属逻辑
@ -168,8 +164,6 @@ public class CodegenEngine {
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/form.vue"),
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue"))
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/import.vue"),
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/import-form.vue"))
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("api/api.ts"),
vue3VbenFilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/modules/form_sub_normal.vue"), // 特殊:主子表专属逻辑
@ -187,8 +181,6 @@ public class CodegenEngine {
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/form.vue"),
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue"))
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/import.vue"),
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/import-form.vue"))
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("api/api.ts"),
vue3VbenFilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_normal.vue"), // 特殊:主子表专属逻辑
@ -208,8 +200,6 @@ public class CodegenEngine {
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
.put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/form.vue"),
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue"))
.put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/import.vue"),
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/import-form.vue"))
.put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("api/api.ts"),
vue3VbenFilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
.put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/modules/form_sub_normal.vue"), // 特殊:主子表专属逻辑
@ -227,8 +217,6 @@ public class CodegenEngine {
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
.put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/form.vue"),
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue"))
.put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/import.vue"),
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/import-form.vue"))
.put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("api/api.ts"),
vue3VbenFilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
.put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/modules/form_sub_normal.vue"), // 特殊:主子表专属逻辑
@ -296,7 +284,6 @@ public class CodegenEngine {
globalBindingMap.put("jakartaPackage", jakartaEnable ? "jakarta" : "javax");
globalBindingMap.put("voType", codegenProperties.getVoType());
globalBindingMap.put("deleteBatchEnable", codegenProperties.getDeleteBatchEnable());
globalBindingMap.put("importEnable", codegenProperties.getImportEnable());
// 全局 Java Bean
globalBindingMap.put("CommonResultClassName", CommonResult.class.getName());
globalBindingMap.put("PageResultClassName", PageResult.class.getName());
@ -356,11 +343,6 @@ public class CodegenEngine {
if (!CodegenTemplateTypeEnum.isTree(table.getTemplateType())) {
return;
}
} else if (isImportTemplate(vmPath)) {
// 关闭 import 时,跳过 ImportExcelVO / ImportRespVO 的生成
if (!Boolean.TRUE.equals(codegenProperties.getImportEnable())) {
return;
}
}
// 2.3 默认生成
generateCode(result, vmPath, filePath, bindingMap);
@ -694,9 +676,4 @@ public class CodegenEngine {
return path.contains("listReqVO");
}
private static boolean isImportTemplate(String path) {
return path.contains("importExcelVO") || path.contains("importRespVO")
|| path.contains("views/import.vue");
}
}

View File

@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.infra.service.file;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
@ -94,7 +93,7 @@ public class FileConfigServiceImpl implements FileConfigService {
fileConfigMapper.updateById(updateObj);
// 清空缓存
clearCache(config.getId(), config.getMaster());
clearCache(config.getId(), null);
}
@Override
@ -133,7 +132,7 @@ public class FileConfigServiceImpl implements FileConfigService {
fileConfigMapper.deleteById(id);
// 清空缓存
clearCache(id, config.getMaster());
clearCache(id, null);
}
@Override
@ -150,7 +149,7 @@ public class FileConfigServiceImpl implements FileConfigService {
fileConfigMapper.deleteByIds(ids);
// 清空缓存
ids.forEach(id -> clearCache(id, false));
ids.forEach(id -> clearCache(id, null));
}
/**
@ -192,7 +191,7 @@ public class FileConfigServiceImpl implements FileConfigService {
validateFileConfigExists(id);
// 上传文件
byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
return getFileClient(id).upload(content, "public" + StrUtil.SLASH + IdUtil.fastSimpleUUID() + ".jpg", "image/jpeg");
return getFileClient(id).upload(content, IdUtil.fastSimpleUUID() + ".jpg", "image/jpeg");
}
@Override

View File

@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.infra.service.file;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
@ -42,19 +41,12 @@ public class FileServiceImpl implements FileService {
*/
static boolean PATH_PREFIX_DATE_ENABLE = true;
/**
*
*
*
* + 5
*
* UUID
*/
static boolean PATH_SUFFIX_TIMESTAMP_ENABLE = false;
/**
*
*
* true{@code yyyyMMdd/<>/.ext}
* false{@code yyyyMMdd/_<>.ext}
*/
static boolean PATH_SUFFIX_AS_DIRECTORY = true;
static boolean PATH_SUFFIX_TIMESTAMP_ENABLE = true;
@Resource
private FileConfigService fileConfigService;
@ -109,21 +101,16 @@ public class FileServiceImpl implements FileService {
}
String suffix = null;
if (PATH_SUFFIX_TIMESTAMP_ENABLE) {
// 5 位随机数,避免同一毫秒内的重复
suffix = String.valueOf(System.currentTimeMillis()) + RandomUtil.randomInt(10000, 100000);
suffix = String.valueOf(System.currentTimeMillis());
}
// 2.1 先拼接 suffix 后缀
if (StrUtil.isNotEmpty(suffix)) {
if (PATH_SUFFIX_AS_DIRECTORY) {
name = suffix + StrUtil.SLASH + name;
String ext = FileUtil.extName(name);
if (StrUtil.isNotEmpty(ext)) {
name = FileUtil.mainName(name) + StrUtil.C_UNDERLINE + suffix + StrUtil.DOT + ext;
} else {
String ext = FileUtil.extName(name);
if (StrUtil.isNotEmpty(ext)) {
name = FileUtil.mainName(name) + StrUtil.C_UNDERLINE + suffix + StrUtil.DOT + ext;
} else {
name = name + StrUtil.C_UNDERLINE + suffix;
}
name = name + StrUtil.C_UNDERLINE + suffix;
}
}
// 2.2 再拼接 prefix 前缀

View File

@ -102,7 +102,7 @@ xxl:
# Lock4j 配置项
lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
--- #################### 监控相关配置 ####################

View File

@ -113,7 +113,7 @@ xxl:
# Lock4j 配置项
lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
--- #################### 监控相关配置 ####################
@ -137,8 +137,6 @@ spring:
password: admin
# Spring Boot Admin Server 服务端的相关配置
context-path: /admin # 配置 Spring
# 允许嵌入 iframe 的域名(支持通配符),实际部署时,可以改为 "'self' [你的公网域名]"
frame-ancestors: "'self' localhost localhost:48082 127.0.0.1 127.0.0.1:48082"
# 日志文件配置
logging:

View File

@ -164,7 +164,6 @@ yudao:
vo-type: 10 # VO 的类型,参见 CodegenVOTypeEnum 枚举类
delete-batch-enable: true # 是否生成批量删除接口
unit-test-enable: false # 是否生成单元测试
import-enable: false # 是否生成 Excel 导入接口
tenant: # 多租户相关配置项
enable: true
ignore-urls:

View File

@ -1,9 +1,6 @@
package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName};
import org.springframework.web.bind.annotation.*;
#if ($importEnable)
import org.springframework.web.multipart.MultipartFile;
#end
import ${jakartaPackage}.annotation.Resource;
import org.springframework.validation.annotation.Validated;
#if ($sceneEnum.scene == 1)import org.springframework.security.access.prepost.PreAuthorize;#end
@ -162,29 +159,6 @@ public class ${sceneEnum.prefixClass}${table.className}Controller {
BeanUtils.toBean(list, ${table.className}RespVO.class));
}
#end
#if ($importEnable)
@GetMapping("/get-import-template")
@Operation(summary = "获得导入${table.classComment}模板")
#if ($sceneEnum.scene == 1)
@PreAuthorize("@ss.hasPermission('${permissionPrefix}:import')")
#end
public void importTemplate(HttpServletResponse response) throws IOException {
ExcelUtils.write(response, "${table.classComment}导入模板.xls", "数据",
${sceneEnum.prefixClass}${table.className}ImportExcelVO.class, Collections.emptyList());
}
@PostMapping("/import")
@Operation(summary = "导入${table.classComment}")
@Parameter(name = "file", description = "Excel 文件", required = true)
#if ($sceneEnum.scene == 1)
@PreAuthorize("@ss.hasPermission('${permissionPrefix}:import')")
#end
public CommonResult<${sceneEnum.prefixClass}${table.className}ImportRespVO> importExcel(@RequestParam("file") MultipartFile file) throws Exception {
List<${sceneEnum.prefixClass}${table.className}ImportExcelVO> list = ExcelUtils.read(file, ${sceneEnum.prefixClass}${table.className}ImportExcelVO.class);
return success(${classNameVar}Service.import${simpleClassName}List(list));
}
#end
## 特殊:主子表专属逻辑
#foreach ($subTable in $subTables)
@ -294,4 +268,4 @@ public class ${sceneEnum.prefixClass}${table.className}Controller {
#end
#end
}
}

View File

@ -1,38 +0,0 @@
package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo;
import cn.idev.excel.annotation.ExcelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
#foreach ($column in $columns)
#if (${column.createOperation} && "$!column.dictType" != "")
import ${DictFormatClassName};
import ${DictConvertClassName};
#break
#end
#end
/**
* ${table.classComment} Excel 导入 VO
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ${sceneEnum.prefixClass}${table.className}ImportExcelVO {
## 逐个处理字段
#foreach ($column in $columns)
#if (${column.createOperation})
#if ("$!column.dictType" != "")
@ExcelProperty(value = "${column.columnComment}", converter = DictConvert.class)
@DictFormat("${column.dictType}") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
#else
@ExcelProperty("${column.columnComment}")
#end
private ${column.javaType} ${column.javaField};
#end
#end
}

View File

@ -1,23 +0,0 @@
package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import java.util.Map;
@Schema(description = "${sceneEnum.name} - ${table.classComment}导入 Response VO")
@Data
@Builder
public class ${sceneEnum.prefixClass}${table.className}ImportRespVO {
@Schema(description = "创建成功的数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
private Integer successCount;
@Schema(description = "导入失败的数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer failureCount;
@Schema(description = "导入失败的数据集合key 为行号value 为失败原因", requiredMode = Schema.RequiredMode.REQUIRED)
private Map<Integer, String> failureRows;
}

View File

@ -25,16 +25,6 @@ public interface ${table.className}Service {
* @return 编号
*/
${primaryColumn.javaType} create${simpleClassName}(@Valid ${saveReqVOClass} ${saveReqVOVar});
#if ($importEnable)
/**
* 导入${table.classComment}
*
* @param importList 导入信息
* @return 导入结果
*/
${sceneEnum.prefixClass}${table.className}ImportRespVO import${simpleClassName}List(List<${sceneEnum.prefixClass}${table.className}ImportExcelVO> importList);
#end
/**
* 更新${table.classComment}
@ -172,4 +162,4 @@ public interface ${table.className}Service {
#end
#end
}
}

View File

@ -7,9 +7,6 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
#if ($importEnable)
import java.util.concurrent.atomic.AtomicInteger;
#end
import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*;
import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO;
## 特殊:主子表专属逻辑
@ -94,32 +91,6 @@ public class ${table.className}ServiceImpl implements ${table.className}Service
// 返回
return ${classNameVar}.getId();
}
#if ($importEnable)
@Override
@Transactional(rollbackFor = Exception.class) // 添加事务,异常则回滚所有导入
public ${sceneEnum.prefixClass}${table.className}ImportRespVO import${simpleClassName}List(List<${sceneEnum.prefixClass}${table.className}ImportExcelVO> importList) {
if (CollUtil.isEmpty(importList)) {
return ${sceneEnum.prefixClass}${table.className}ImportRespVO.builder()
.successCount(0).failureCount(0).failureRows(new LinkedHashMap<>()).build();
}
// 遍历,逐个创建
${sceneEnum.prefixClass}${table.className}ImportRespVO respVO = ${sceneEnum.prefixClass}${table.className}ImportRespVO.builder()
.successCount(0).failureCount(0).failureRows(new LinkedHashMap<>()).build();
AtomicInteger index = new AtomicInteger(1);
importList.forEach(importItem -> {
int currentIndex = index.getAndIncrement();
try {
create${simpleClassName}(BeanUtils.toBean(importItem, ${saveReqVOClass}.class));
respVO.setSuccessCount(respVO.getSuccessCount() + 1);
} catch (Exception ex) {
respVO.getFailureRows().put(currentIndex, ex.getMessage());
}
});
respVO.setFailureCount(respVO.getFailureRows().size());
return respVO;
}
#end
@Override
## 特殊:主子表专属逻辑(非 ERP 模式)
@ -388,9 +359,6 @@ public class ${table.className}ServiceImpl implements ${table.className}Service
#else
#if ( $subTable.subJoinMany)
private void create${subSimpleClassName}List(${primaryColumn.javaType} ${subJoinColumn.javaField}, List<${subTable.className}DO> list) {
if (CollUtil.isEmpty(list)) {
return;
}
list.forEach(o -> o.set${SubJoinColumnName}(${subJoinColumn.javaField}).clean());
${subClassNameVars.get($index)}Mapper.insertBatch(list);
}
@ -448,4 +416,4 @@ public class ${table.className}ServiceImpl implements ${table.className}Service
#end
#end
}
}

View File

@ -1,11 +1,6 @@
## 通用变量定义
#if ($importEnable)
#set ($functionNames = ['查询', '创建', '更新', '删除', '导出', '导入'])
#set ($functionOps = ['query', 'create', 'update', 'delete', 'export', 'import'])
#else
#set ($functionNames = ['查询', '创建', '更新', '删除', '导出'])
#set ($functionOps = ['query', 'create', 'update', 'delete', 'export'])
#end
##
## 宏定义:生成按钮 SQL通用部分
#macro(insertButtonSql $parentIdVar)

View File

@ -73,26 +73,6 @@ export function export${simpleClassName}Excel(params) {
responseType: 'blob'
})
}
#if ($importEnable)
// 下载${table.classComment}导入模板
export function import${simpleClassName}Template() {
return request({
url: '${baseURL}/get-import-template',
method: 'get',
responseType: 'blob'
})
}
// 导入${table.classComment}
export function import${simpleClassName}(data) {
return request({
url: '${baseURL}/import',
method: 'post',
data
})
}
#end
## 特殊:主子表专属逻辑
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
@ -177,4 +157,4 @@ export function import${simpleClassName}(data) {
})
}
#end
#end
#end

View File

@ -49,16 +49,6 @@
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="openForm(undefined)"
v-hasPermi="['${permissionPrefix}:create']">新增</el-button>
</el-col>
#if ($importEnable)
<el-col :span="1.5">
<el-button type="info" plain icon="el-icon-upload2" size="mini" @click="handleImport"
:loading="importLoading" v-hasPermi="['${permissionPrefix}:import']">导入</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="info" plain icon="el-icon-document" size="mini" @click="handleImportTemplate"
v-hasPermi="['${permissionPrefix}:import']">导入模板</el-button>
</el-col>
#end
<el-col :span="1.5">
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" :loading="exportLoading"
v-hasPermi="['${permissionPrefix}:export']">导出</el-button>
@ -88,9 +78,6 @@
#end
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
#if ($importEnable)
<input ref="importFileRef" type="file" style="display: none" accept=".xls,.xlsx" @change="handleImportFileChange" />
#end
## 特殊:主子表专属逻辑
#if ( $table.templateType == 11 && $subTables && $subTables.size() > 0 )
@ -257,10 +244,6 @@ export default {
loading: true,
// 导出遮罩层
exportLoading: false,
#if ($importEnable)
// 导入遮罩层
importLoading: false,
#end
// 显示搜索条件
showSearch: true,
## 特殊:树表专属逻辑(树不需要分页接口)
@ -339,44 +322,6 @@ export default {
openForm(id) {
this.#[[$]]#refs["formRef"].open(id);
},
#if ($importEnable)
/** 导入按钮操作 */
handleImport() {
this.$refs.importFileRef && this.$refs.importFileRef.click();
},
/** 导入模板下载 */
async handleImportTemplate() {
const data = await ${simpleClassName}Api.import${simpleClassName}Template();
this.#[[$]]#download.excel(data, '${table.classComment}导入模板.xls');
},
/** 导入文件变更 */
async handleImportFileChange(event) {
const target = event.target;
const file = target.files && target.files[0];
if (!file) {
return;
}
this.importLoading = true;
try {
const formData = new FormData();
formData.append('file', file);
const res = await ${simpleClassName}Api.import${simpleClassName}(formData);
const data = res.data || res;
let text = '导入成功数量:' + (data.successCount || 0) + ';导入失败数量:' + (data.failureCount || 0) + '';
if (data.failureRows) {
Object.keys(data.failureRows).forEach((rowNo) => {
text += '< 第' + rowNo + '行: ' + data.failureRows[rowNo] + ' >';
});
}
await this.$alert(text, '${table.classComment}导入结果', { dangerouslyUseHTMLString: true });
await this.getList();
} catch {
} finally {
target.value = '';
this.importLoading = false;
}
},
#end
/** 删除按钮操作 */
async handleDelete(row) {
const ${primaryColumn.javaField} = row.${primaryColumn.javaField};

View File

@ -98,18 +98,6 @@ export const ${simpleClassName}Api = {
export${simpleClassName}: async (params) => {
return await request.download({ url: `${baseURL}/export-excel`, params })
},
#if ($importEnable)
// 下载${table.classComment}导入模板
import${simpleClassName}Template: async () => {
return await request.download({ url: `${baseURL}/get-import-template` })
},
// 导入${table.classComment}
import${simpleClassName}: async (data: FormData) => {
return await request.upload({ url: `${baseURL}/import`, data })
},
#end
## 特殊:主子表专属逻辑
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)

View File

@ -1,103 +0,0 @@
<template>
<Dialog v-model="dialogVisible" title="${table.classComment}导入" width="400">
<el-upload
ref="uploadRef"
v-model:file-list="fileList"
:auto-upload="false"
:disabled="formLoading"
:limit="1"
:on-exceed="handleExceed"
accept=".xlsx, .xls"
action="none"
drag
>
<Icon icon="ep:upload" />
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip text-center">
<span>仅允许导入 xls、xlsx 格式文件。</span>
<el-link
:underline="false"
style="font-size: 12px; vertical-align: baseline"
type="primary"
@click="handleDownloadTemplate"
>
下载模板
</el-link>
</div>
</template>
</el-upload>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import type { UploadUserFile } from 'element-plus'
import download from '@/utils/download'
import { ${simpleClassName}Api } from '@/api/${table.moduleName}/${table.businessName}'
/** ${table.classComment} 导入 */
defineOptions({ name: '${simpleClassName}ImportForm' })
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中:上传、下载模板
const uploadRef = ref()
const fileList = ref<UploadUserFile[]>([])
/** 打开弹窗 */
const open = async () => {
dialogVisible.value = true
await resetForm()
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交导入 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
if (fileList.value.length === 0) {
message.error('请上传文件')
return
}
formLoading.value = true
try {
const formData = new FormData()
formData.append('file', fileList.value[0].raw as Blob)
const res = await ${simpleClassName}Api.import${simpleClassName}(formData)
const data = res.data
let text = '导入成功数量:' + data.successCount + ';导入失败数量:' + data.failureCount + ''
if (data.failureCount > 0) {
for (const rowNo in data.failureRows) {
text += '< 第' + rowNo + '行: ' + data.failureRows[rowNo] + ' >'
}
}
message.alert(text)
dialogVisible.value = false
emit('success')
} finally {
formLoading.value = false
await resetForm()
}
}
/** 下载导入模板 */
const handleDownloadTemplate = async () => {
const data = await ${simpleClassName}Api.import${simpleClassName}Template()
download.excel(data, '${table.classComment}导入模板.xls')
}
/** 文件超限 */
const handleExceed = (): void => {
message.error('最多只能上传一个文件!')
}
/** 重置表单 */
const resetForm = async () => {
fileList.value = []
await nextTick()
uploadRef.value?.clearFiles()
}
</script>

View File

@ -92,16 +92,6 @@
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
#if ($importEnable)
<el-button
type="warning"
plain
@click="handleImport"
v-hasPermi="['${permissionPrefix}:import']"
>
<Icon icon="ep:upload" class="mr-5px" /> 导入
</el-button>
#end
<el-button
type="success"
plain
@ -247,11 +237,6 @@
@pagination="getList"
/>
</ContentWrap>
#if ($importEnable)
<!-- 导入弹窗 -->
<${simpleClassName}ImportForm ref="importRef" @success="getList" />
#end
<!-- 表单弹窗:添加/修改 -->
<${simpleClassName}Form ref="formRef" @success="getList" />
@ -278,9 +263,6 @@
import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
import { isEmpty } from '@/utils/is'
import { dateFormatter } from '@/utils/formatTime'
#if ($importEnable)
import ${simpleClassName}ImportForm from './${simpleClassName}ImportForm.vue'
#end
## 特殊:树表专属逻辑
#if ( $table.templateType == 2 )
import { handleTree } from '@/utils/tree'
@ -326,9 +308,6 @@ const queryParams = reactive({
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
#if ($importEnable)
const importRef = ref() // ${table.classComment} 导入组件的 Ref
#end
/** 查询列表 */
const getList = async () => {
@ -365,13 +344,6 @@ const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
#if ($importEnable)
/** 导入按钮操作 */
const handleImport = () => {
importRef.value.open()
}
#end
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
@ -449,4 +421,4 @@ const toggleExpandAll = async () => {
onMounted(() => {
getList()
})
</script>
</script>

View File

@ -30,15 +30,3 @@ export function delete${simpleClassName}(id: number) {
export function export${simpleClassName}(params) {
return defHttp.download({ url: '${baseURL}/export-excel', params }, '${table.classComment}.xls')
}
#if ($importEnable)
// 下载${table.classComment}导入模板
export function import${simpleClassName}Template() {
return defHttp.download({ url: '${baseURL}/get-import-template' }, '${table.classComment}导入模板.xls')
}
// 导入${table.classComment}
export function import${simpleClassName}(data: FormData) {
return defHttp.post({ url: '${baseURL}/import', data })
}
#end

View File

@ -1,31 +1,18 @@
<script lang="ts" setup>
import ${simpleClassName}Modal from './${simpleClassName}Modal.vue'
import { columns, searchFormSchema } from './${classNameVar}.data'
import { ref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { useModal } from '@/components/Modal'
import { IconEnum } from '@/enums/appEnum'
import { BasicTable, TableAction, useTable } from '@/components/Table'
import {
delete${simpleClassName},
export${simpleClassName},
get${simpleClassName}Page,
#if ($importEnable)
import${simpleClassName},
import${simpleClassName}Template,
#end
} from '@/api/${table.moduleName}/${table.businessName}'
import { delete${simpleClassName}, export${simpleClassName}, get${simpleClassName}Page } from '@/api/${table.moduleName}/${table.businessName}'
defineOptions({ name: '${table.className}' })
const { t } = useI18n()
const { createConfirm, createMessage } = useMessage()
const [registerModal, { openModal }] = useModal()
#if ($importEnable)
const importFileRef = ref<HTMLInputElement>()
const importLoading = ref(false)
#end
const [registerTable, { getForm, reload }] = useTable({
title: '${table.classComment}列表',
@ -61,44 +48,6 @@ async function handleExport() {
},
})
}
#if ($importEnable)
function handleImport() {
importFileRef.value?.click()
}
async function handleImportTemplateDownload() {
await import${simpleClassName}Template()
createMessage.success('模板下载已开始')
}
async function handleImportFileChange(event: Event) {
const target = event.target as HTMLInputElement
const file = target.files?.[0]
if (!file) {
return
}
importLoading.value = true
try {
const formData = new FormData()
formData.append('file', file)
const response: any = await import${simpleClassName}(formData)
const data = response?.data ?? response
let text =
'导入成功数量:' + (data?.successCount || 0) + ';导入失败数量:' + (data?.failureCount || 0) + ''
if (data?.failureRows) {
Object.keys(data.failureRows).forEach((rowNo) => {
text += '< 第' + rowNo + '行: ' + data.failureRows[rowNo] + ' >'
})
}
createMessage.success(text)
await reload()
} finally {
target.value = ''
importLoading.value = false
}
}
#end
async function handleDelete(record: Recordable) {
await delete${simpleClassName}(record.id)
@ -113,14 +62,6 @@ async function handleDelete(record: Recordable) {
<a-button type="primary" v-auth="['${permissionPrefix}:create']" :preIcon="IconEnum.ADD" @click="handleCreate">
{{ t('action.create') }}
</a-button>
#if ($importEnable)
<a-button v-auth="['${permissionPrefix}:import']" :loading="importLoading" @click="handleImport">
导入
</a-button>
<a-button v-auth="['${permissionPrefix}:import']" @click="handleImportTemplateDownload">
导入模板
</a-button>
#end
<a-button v-auth="['${permissionPrefix}:export']" :preIcon="IconEnum.EXPORT" @click="handleExport">
{{ t('action.export') }}
</a-button>
@ -146,9 +87,6 @@ async function handleDelete(record: Recordable) {
</template>
</template>
</BasicTable>
#if ($importEnable)
<input ref="importFileRef" type="file" accept=".xls,.xlsx" class="hidden" @change="handleImportFileChange" />
#end
<${simpleClassName}Modal @register="registerModal" @success="reload()" />
</div>
</template>

View File

@ -100,18 +100,6 @@ export function delete${simpleClassName}List(ids: number[]) {
export function export${simpleClassName}(params: any) {
return requestClient.download('${baseURL}/export-excel', { params });
}
#if ($importEnable)
/** 下载${table.classComment}导入模板 */
export function import${simpleClassName}Template() {
return requestClient.download('${baseURL}/get-import-template');
}
/** 导入${table.classComment} */
export function import${simpleClassName}(data: FormData) {
return requestClient.post('${baseURL}/import', data);
}
#end
## 特殊:主子表专属逻辑
#foreach ($subTable in $subTables)

View File

@ -1,71 +0,0 @@
<script lang="ts" setup>
import type { FileType } from 'ant-design-vue/es/upload/interface';
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message, Upload } from 'ant-design-vue';
import { import${simpleClassName}, import${simpleClassName}Template } from '#/api/${table.moduleName}/${table.businessName}';
defineOptions({ name: '${simpleClassName}Import' });
const emit = defineEmits(['success']);
const fileRef = ref<File | null>(null);
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (!fileRef.value) {
message.error('请上传文件');
return;
}
modalApi.lock();
try {
const formData = new FormData();
formData.append('file', fileRef.value);
const response: any = await import${simpleClassName}(formData);
const data = response?.data ?? response ?? {};
let text = '导入成功数量:' + (data.successCount || 0) + ';导入失败数量:' + (data.failureCount || 0) + '';
if (data.failureRows) {
Object.keys(data.failureRows).forEach((rowNo) => {
text += '< 第' + rowNo + '行: ' + data.failureRows[rowNo] + ' >';
});
}
message.info(text);
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
},
});
/** 上传前:拦截 antd Upload 的自动上传,文件存到 ref */
function beforeUpload(file: FileType) {
fileRef.value = file as unknown as File;
return false;
}
/** 下载导入模板 */
async function handleDownload() {
const data = await import${simpleClassName}Template();
downloadFileFromBlobPart({ fileName: '${table.classComment}导入模板.xls', source: data });
}
</script>
<template>
<Modal title="导入${table.classComment}" class="w-1/3">
<div class="mx-4">
<Upload :max-count="1" accept=".xls,.xlsx" :before-upload="beforeUpload">
<Button type="primary"> 选择 Excel 文件 </Button>
</Upload>
</div>
<template #prepend-footer>
<div class="flex flex-auto items-center">
<Button @click="handleDownload"> 下载导入模板 </Button>
</div>
</template>
</Modal>
</template>

View File

@ -31,9 +31,6 @@ import { get${simpleClassName}List, delete${simpleClassName}, export${simpleClas
#else## 标准表接口
import { get${simpleClassName}Page, delete${simpleClassName},#if ($deleteBatchEnable) delete${simpleClassName}List,#end export${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';
#end
#if ($importEnable)
import ${simpleClassName}Import from './modules/import-form.vue';
#end
#if ($table.templateType == 12 || $table.templateType == 11) ## 内嵌和erp情况
/** 子表的列表 */
@ -122,17 +119,6 @@ const [FormModal, formModalApi] = useVbenModal({
connectedComponent: ${simpleClassName}Form,
destroyOnClose: true,
});
#if ($importEnable)
const [ImportFormModal, importFormModalApi] = useVbenModal({
connectedComponent: ${simpleClassName}Import,
destroyOnClose: true,
});
/** 导入${table.classComment} */
function handleImport() {
importFormModalApi.open();
}
#end
/** 创建${table.classComment} */
function handleCreate() {
@ -204,7 +190,6 @@ try {
}
}
#if (${table.templateType} == 2)
/** 切换树形展开/收缩状态 */
const isExpanded = ref(true);
@ -224,9 +209,6 @@ onMounted(() => {
<template>
<Page auto-content-height>
<FormModal @success="getList" />
#if ($importEnable)
<ImportFormModal @success="getList" />
#end
<Card v-if="!hiddenSearchBar" class="mb-4">
<!-- 搜索工作栏 -->
@ -332,16 +314,6 @@ onMounted(() => {
>
{{ $t('ui.actionTitle.create', ['${table.classComment}']) }}
</Button>
#if ($importEnable)
<Button
class="ml-2"
type="primary"
@click="handleImport"
v-access:code="['${permissionPrefix}:import']"
>
导入
</Button>
#end
<Button
:icon="h(Download)"
type="primary"

View File

@ -134,21 +134,6 @@ export function useFormSchema(): VbenFormSchema[] {
];
}
#if ($importEnable)
/** 导入的表单 */
export function useImportFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'file',
label: '${table.classComment}数据',
component: 'Upload',
rules: 'required',
help: '仅允许导入 xls、xlsx 格式文件',
},
];
}
#end
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [

View File

@ -1,82 +0,0 @@
<script lang="ts" setup>
import type { FileType } from 'ant-design-vue/es/upload/interface';
import { useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message, Upload } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { import${simpleClassName}, import${simpleClassName}Template } from '#/api/${table.moduleName}/${table.businessName}';
import { $t } from '#/locales';
import { useImportFormSchema } from '../data';
const emit = defineEmits(['success']);
const [Form, formApi] = useVbenForm({
commonConfig: {
formItemClass: 'col-span-2',
labelWidth: 120,
},
layout: 'horizontal',
schema: useImportFormSchema(),
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data = await formApi.getValues();
try {
await import${simpleClassName}(data.file);
// 关闭并提示
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
});
/** 上传前 */
function beforeUpload(file: FileType) {
formApi.setFieldValue('file', file);
return false;
}
/** 下载模板 */
async function handleDownload() {
const data = await import${simpleClassName}Template();
downloadFileFromBlobPart({ fileName: '${table.classComment}导入模板.xls', source: data });
}
</script>
<template>
<Modal title="导入${table.classComment}" class="w-1/3">
<Form class="mx-4">
<template #file>
<div class="w-full">
<Upload
:max-count="1"
accept=".xls,.xlsx"
:before-upload="beforeUpload"
>
<Button type="primary"> 选择 Excel 文件 </Button>
</Upload>
</div>
</template>
</Form>
<template #prepend-footer>
<div class="flex flex-auto items-center">
<Button @click="handleDownload"> 下载导入模板 </Button>
</div>
</template>
</Modal>
</template>

View File

@ -26,9 +26,6 @@ import {
get${simpleClassName}Page,
} from '#/api/${table.moduleName}/${table.businessName}';
#end
#if ($importEnable)
import ${simpleClassName}Import from './modules/import-form.vue';
#end
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
@ -53,17 +50,6 @@ const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
#if ($importEnable)
const [ImportFormModal, importFormModalApi] = useVbenModal({
connectedComponent: ${simpleClassName}Import,
destroyOnClose: true,
});
/** 导入${table.classComment} */
function handleImport() {
importFormModalApi.open();
}
#end
#if (${table.templateType} == 2)## 树表特有:控制表格展开收缩
/** 切换树形展开/收缩状态 */
@ -149,7 +135,6 @@ async function handleExport() {
downloadFileFromBlobPart({ fileName: '${table.classComment}.xls', source: data });
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
@ -225,9 +210,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
<template>
<Page auto-content-height>
<FormModal @success="handleRefresh" />
#if ($importEnable)
<ImportFormModal @success="handleRefresh" />
#end
#if ($table.templateType == 11) ## erp情况
<div>
#end
@ -265,14 +247,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
onClick: handleExpand,
},
#end
#if ($importEnable)
{
label: '导入',
type: 'primary',
auth: ['${table.moduleName}:${simpleClassName_strikeCase}:import'],
onClick: handleImport,
},
#end
{
label: $t('ui.actionTitle.export'),
type: 'primary',
@ -344,4 +318,4 @@ const [Grid, gridApi] = useVbenVxeGrid({
</div>
#end
</Page>
</template>
</template>

View File

@ -100,18 +100,6 @@ export function delete${simpleClassName}List(ids: number[]) {
export function export${simpleClassName}(params: any) {
return requestClient.download('${baseURL}/export-excel', { params });
}
#if ($importEnable)
/** 下载${table.classComment}导入模板 */
export function import${simpleClassName}Template() {
return requestClient.download('${baseURL}/get-import-template');
}
/** 导入${table.classComment} */
export function import${simpleClassName}(data: FormData) {
return requestClient.post('${baseURL}/import', data);
}
#end
## 特殊:主子表专属逻辑
#foreach ($subTable in $subTables)

View File

@ -1,75 +0,0 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { ElButton, ElMessage, ElUpload } from 'element-plus';
import { import${simpleClassName}, import${simpleClassName}Template } from '#/api/${table.moduleName}/${table.businessName}';
defineOptions({ name: '${simpleClassName}Import' });
const emit = defineEmits(['success']);
const fileRef = ref<File | null>(null);
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (!fileRef.value) {
ElMessage.error('请上传文件');
return;
}
modalApi.lock();
try {
const formData = new FormData();
formData.append('file', fileRef.value);
const response: any = await import${simpleClassName}(formData);
const data = response?.data ?? response ?? {};
let text = '导入成功数量:' + (data.successCount || 0) + ';导入失败数量:' + (data.failureCount || 0) + '';
if (data.failureRows) {
Object.keys(data.failureRows).forEach((rowNo) => {
text += '< 第' + rowNo + '行: ' + data.failureRows[rowNo] + ' >';
});
}
ElMessage.info(text);
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
},
});
/** 文件改变 */
function handleChange(file: any) {
if (file.raw) {
fileRef.value = file.raw;
}
}
/** 下载导入模板 */
async function handleDownload() {
const data = await import${simpleClassName}Template();
downloadFileFromBlobPart({ fileName: '${table.classComment}导入模板.xls', source: data });
}
</script>
<template>
<Modal title="导入${table.classComment}" class="w-1/3">
<div class="mx-4">
<ElUpload
:limit="1"
accept=".xls,.xlsx"
:on-change="handleChange"
:auto-upload="false"
>
<ElButton type="primary"> 选择 Excel 文件 </ElButton>
</ElUpload>
</div>
<template #prepend-footer>
<div class="flex flex-auto items-center">
<ElButton @click="handleDownload"> 下载导入模板 </ElButton>
</div>
</template>
</Modal>
</template>

View File

@ -31,9 +31,7 @@ import { get${simpleClassName}List, delete${simpleClassName}, export${simpleClas
import { isEmpty } from '@vben/utils';
import { get${simpleClassName}Page, delete${simpleClassName},#if ($deleteBatchEnable) delete${simpleClassName}List,#end export${simpleClassName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
#end
#if ($importEnable)
import ${simpleClassName}Import from './modules/import-form.vue';
#end
import { downloadFileFromBlobPart } from '@vben/utils';
#if ($table.templateType == 12 || $table.templateType == 11) ## 内嵌和erp情况
/** 子表的列表 */
@ -122,17 +120,6 @@ const [FormModal, formModalApi] = useVbenModal({
connectedComponent: ${simpleClassName}Form,
destroyOnClose: true,
});
#if ($importEnable)
const [ImportFormModal, importFormModalApi] = useVbenModal({
connectedComponent: ${simpleClassName}Import,
destroyOnClose: true,
});
/** 导入${table.classComment} */
function handleImport() {
importFormModalApi.open();
}
#end
/** 创建${table.classComment} */
function handleCreate() {
@ -202,7 +189,6 @@ try {
}
}
#if (${table.templateType} == 2)
/** 切换树形展开/收缩状态 */
const isExpanded = ref(true);
@ -222,9 +208,6 @@ onMounted(() => {
<template>
<Page auto-content-height>
<FormModal @success="getList" />
#if ($importEnable)
<ImportFormModal @success="getList" />
#end
<ContentWrap v-if="!hiddenSearchBar">
<!-- 搜索工作栏 -->
@ -333,16 +316,6 @@ onMounted(() => {
>
{{ $t('ui.actionTitle.create', ['${table.classComment}']) }}
</el-button>
#if ($importEnable)
<el-button
class="ml-2"
type="primary"
@click="handleImport"
v-access:code="['${permissionPrefix}:import']"
>
导入
</el-button>
#end
<el-button
:icon="h(Download)"
type="primary"

View File

@ -113,18 +113,6 @@ export function delete${simpleClassName}List(ids: number[]) {
export function export${simpleClassName}(params: any) {
return requestClient.download('${baseURL}/export-excel', { params });
}
#if ($importEnable)
/** 下载${table.classComment}导入模板 */
export function import${simpleClassName}Template() {
return requestClient.download('${baseURL}/get-import-template');
}
/** 导入${table.classComment} */
export function import${simpleClassName}(file: File) {
return requestClient.upload('${baseURL}/import', { file });
}
#end
## 特殊:主子表专属逻辑
#foreach ($subTable in $subTables)

View File

@ -137,21 +137,6 @@ export function useFormSchema(): VbenFormSchema[] {
];
}
#if ($importEnable)
/** 导入的表单 */
export function useImportFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'file',
label: '${table.classComment}数据',
component: 'Upload',
rules: 'required',
help: '仅允许导入 xls、xlsx 格式文件',
},
];
}
#end
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [

View File

@ -1,82 +0,0 @@
<script lang="ts" setup>
import { useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { ElButton, ElMessage, ElUpload } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import { import${simpleClassName}, import${simpleClassName}Template } from '#/api/${table.moduleName}/${table.businessName}';
import { $t } from '#/locales';
import { useImportFormSchema } from '../data';
const emit = defineEmits(['success']);
const [Form, formApi] = useVbenForm({
commonConfig: {
formItemClass: 'col-span-2',
labelWidth: 120,
},
layout: 'horizontal',
schema: useImportFormSchema(),
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data = await formApi.getValues();
try {
await import${simpleClassName}(data.file);
// 关闭并提示
await modalApi.close();
emit('success');
ElMessage.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
});
/** 文件改变时 */
function handleChange(file: any) {
if (file.raw) {
formApi.setFieldValue('file', file.raw);
}
}
/** 下载模板 */
async function handleDownload() {
const data = await import${simpleClassName}Template();
downloadFileFromBlobPart({ fileName: '${table.classComment}导入模板.xls', source: data });
}
</script>
<template>
<Modal title="导入${table.classComment}" class="w-1/3">
<Form class="mx-4">
<template #file>
<div class="w-full">
<ElUpload
:limit="1"
accept=".xls,.xlsx"
:on-change="handleChange"
:auto-upload="false"
>
<ElButton type="primary"> 选择 Excel 文件 </ElButton>
</ElUpload>
</div>
</template>
</Form>
<template #prepend-footer>
<div class="flex flex-auto items-center">
<ElButton @click="handleDownload"> 下载导入模板 </ElButton>
</div>
</template>
</Modal>
</template>

View File

@ -26,9 +26,6 @@ import {
get${simpleClassName}Page,
} from '#/api/${table.moduleName}/${table.businessName}';
#end
#if ($importEnable)
import ${simpleClassName}Import from './modules/import-form.vue';
#end
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
@ -53,17 +50,6 @@ const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
#if ($importEnable)
const [ImportFormModal, importFormModalApi] = useVbenModal({
connectedComponent: ${simpleClassName}Import,
destroyOnClose: true,
});
/** 导入${table.classComment} */
function handleImport() {
importFormModalApi.open();
}
#end
#if (${table.templateType} == 2)## 树表特有:控制表格展开收缩
/** 切换树形展开/收缩状态 */
@ -147,7 +133,6 @@ async function handleExport() {
downloadFileFromBlobPart({ fileName: '${table.classComment}.xls', source: data });
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
@ -223,9 +208,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
<template>
<Page auto-content-height>
<FormModal @success="handleRefresh" />
#if ($importEnable)
<ImportFormModal @success="handleRefresh" />
#end
#if ($table.templateType == 11) ## erp情况
<div>
#end
@ -263,14 +245,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
onClick: handleExpand,
},
#end
#if ($importEnable)
{
label: '导入',
type: 'primary',
auth: ['${table.moduleName}:${simpleClassName_strikeCase}:import'],
onClick: handleImport,
},
#end
{
label: $t('ui.actionTitle.export'),
type: 'primary',
@ -343,4 +317,4 @@ const [Grid, gridApi] = useVbenVxeGrid({
</div>
#end
</Page>
</template>
</template>

View File

@ -84,19 +84,4 @@ public class CodegenBuilderTest extends BaseMockitoUnitTest {
assertEquals("input", column.getHtmlType());
}
@Test
public void testSanitizeComment() {
// 1. null / 空字符串:原样返回
assertNull(codegenBuilder.sanitizeComment(null));
assertEquals("", codegenBuilder.sanitizeComment(""));
// 2. 无英文引号:原样返回
assertEquals("无引号注释", codegenBuilder.sanitizeComment("无引号注释"));
// 3. 含英文双引号:替换为中文左双引号
assertEquals("含“双“引号", codegenBuilder.sanitizeComment("含\"双\"引号"));
// 4. 含英文单引号:替换为中文左单引号
assertEquals("含‘单‘引号", codegenBuilder.sanitizeComment("含'单'引号"));
// 5. 双 / 单引号混合
assertEquals("“混‘搭“‘", codegenBuilder.sanitizeComment("\"混'搭\"'"));
}
}

View File

@ -10,7 +10,6 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenVOTypeEnum;
import cn.iocoder.yudao.module.infra.framework.codegen.config.CodegenProperties;
import org.junit.jupiter.api.BeforeEach;
import org.mockito.InjectMocks;
@ -20,10 +19,8 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -44,18 +41,16 @@ public abstract class CodegenEngineAbstractTest extends BaseMockitoUnitTest {
@Spy
protected CodegenProperties codegenProperties = new CodegenProperties()
.setBasePackage("cn.iocoder.yudao")
.setVoType(CodegenVOTypeEnum.VO.getType())
.setDeleteBatchEnable(true)
.setUnitTestEnable(true)
.setImportEnable(false);
.setBasePackage("cn.iocoder.yudao");
@BeforeEach
public void setUp() {
codegenEngine.setJakartaEnable(true); // 强制使用 jakarta保证单测可以基于 jakarta 断言
codegenEngine.initGlobalBindingMap();
// 获取测试文件 resources 路径writeResult 调试用
// 单测强制使用
// 获取测试文件 resources 路径
String absolutePath = FileUtil.getAbsolutePath("application-unit-test.yaml");
// 系统不一样生成的文件也有差异,那就各自生成各自的
resourcesPath = absolutePath.split("/target")[0] + "/src/test/resources/codegen/";
}
@ -87,32 +82,17 @@ public abstract class CodegenEngineAbstractTest extends BaseMockitoUnitTest {
return list;
}
/**
* {@code -Dcodegen.regenerate=true}
*/
private static final boolean REGENERATE = Boolean.parseBoolean(System.getProperty("codegen.regenerate", "false"));
@SuppressWarnings("rawtypes")
protected void assertResult(Map<String, String> result, String path) {
if (REGENERATE) {
writeResult(result, resourcesPath + path);
return;
}
protected static void assertResult(Map<String, String> result, String path) {
String assertContent = ResourceUtil.readUtf8Str("codegen/" + path + "/assert.json");
List<HashMap> asserts = JsonUtils.parseArray(assertContent, HashMap.class);
Set<String> expectedFiles = asserts.stream()
.map(m -> (String) m.get("filePath"))
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
assertEquals(expectedFiles, result.keySet(), "生成文件集合不匹配");
// 校验每个文件;归一化 \r\n 为 \n让断言不依赖文件落盘的换行风格
assertEquals(asserts.size(), result.size());
// 校验每个文件
asserts.forEach(assertMap -> {
String contentPath = (String) assertMap.get("contentPath");
String filePath = (String) assertMap.get("filePath");
String expected = ResourceUtil.readUtf8Str("codegen/" + path + "/" + contentPath)
.replace("\r\n", "\n");
String actual = result.get(filePath);
assertEquals(expected, actual == null ? null : actual.replace("\r\n", "\n"),
filePath + ":不匹配");
String content = ResourceUtil.readUtf8Str("codegen/" + path + "/" + contentPath);
assertEquals(content, result.get(filePath), filePath + ":不匹配");
});
}

View File

@ -1,109 +0,0 @@
package cn.iocoder.yudao.module.infra.service.codegen.inner;
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
import com.baomidou.mybatisplus.annotation.DbType;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* {@link CodegenEngine} Vue3 + Vben5 + AntD + General
*
* @author
*/
public class CodegenEngineVben5AntdGeneralTest extends CodegenEngineAbstractTest {
private static final Integer FRONT_TYPE = CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType();
@Test
public void testExecute_one() {
// 准备参数
CodegenTableDO table = getTable("student")
.setFrontType(FRONT_TYPE)
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
List<CodegenColumnDO> columns = getColumnList("student");
// 调用
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
// 断言
assertResult(result, "/vben5_antd_general_one");
}
@Test
public void testExecute_one_importEnable() {
// 开启 import 开关
codegenProperties.setImportEnable(true);
codegenEngine.initGlobalBindingMap();
// 准备参数
CodegenTableDO table = getTable("student")
.setFrontType(FRONT_TYPE)
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
List<CodegenColumnDO> columns = getColumnList("student");
// 调用
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
// 断言
assertResult(result, "/vben5_antd_general_one_importEnable");
}
@Test
public void testExecute_tree() {
// 准备参数
CodegenTableDO table = getTable("category")
.setFrontType(FRONT_TYPE)
.setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
List<CodegenColumnDO> columns = getColumnList("category");
// 调用
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
// 断言
assertResult(result, "/vben5_antd_general_tree");
}
@Test
public void testExecute_master_normal() {
testExecute_master(CodegenTemplateTypeEnum.MASTER_NORMAL, "/vben5_antd_general_master_normal");
}
@Test
public void testExecute_master_erp() {
testExecute_master(CodegenTemplateTypeEnum.MASTER_ERP, "/vben5_antd_general_master_erp");
}
@Test
public void testExecute_master_inner() {
testExecute_master(CodegenTemplateTypeEnum.MASTER_INNER, "/vben5_antd_general_master_inner");
}
private void testExecute_master(CodegenTemplateTypeEnum templateType, String path) {
// 准备参数
CodegenTableDO table = getTable("student")
.setFrontType(FRONT_TYPE)
.setTemplateType(templateType.getType());
List<CodegenColumnDO> columns = getColumnList("student");
// 准备参数(子表)
CodegenTableDO contactTable = getTable("contact")
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
.setFrontType(FRONT_TYPE)
.setSubJoinColumnId(100L).setSubJoinMany(true);
List<CodegenColumnDO> contactColumns = getColumnList("contact");
// 准备参数(班主任)
CodegenTableDO teacherTable = getTable("teacher")
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
.setFrontType(FRONT_TYPE)
.setSubJoinColumnId(200L).setSubJoinMany(false);
List<CodegenColumnDO> teacherColumns = getColumnList("teacher");
// 调用
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns,
Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
// 断言
assertResult(result, path);
}
}

View File

@ -1,109 +0,0 @@
package cn.iocoder.yudao.module.infra.service.codegen.inner;
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
import com.baomidou.mybatisplus.annotation.DbType;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* {@link CodegenEngine} Vue3 + Vben5 + AntD + Schema
*
* @author
*/
public class CodegenEngineVben5AntdSchemaTest extends CodegenEngineAbstractTest {
private static final Integer FRONT_TYPE = CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType();
@Test
public void testExecute_one() {
// 准备参数
CodegenTableDO table = getTable("student")
.setFrontType(FRONT_TYPE)
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
List<CodegenColumnDO> columns = getColumnList("student");
// 调用
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
// 断言
assertResult(result, "/vben5_antd_schema_one");
}
@Test
public void testExecute_one_importEnable() {
// 开启 import 开关
codegenProperties.setImportEnable(true);
codegenEngine.initGlobalBindingMap();
// 准备参数
CodegenTableDO table = getTable("student")
.setFrontType(FRONT_TYPE)
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
List<CodegenColumnDO> columns = getColumnList("student");
// 调用
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
// 断言
assertResult(result, "/vben5_antd_schema_one_importEnable");
}
@Test
public void testExecute_tree() {
// 准备参数
CodegenTableDO table = getTable("category")
.setFrontType(FRONT_TYPE)
.setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
List<CodegenColumnDO> columns = getColumnList("category");
// 调用
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
// 断言
assertResult(result, "/vben5_antd_schema_tree");
}
@Test
public void testExecute_master_normal() {
testExecute_master(CodegenTemplateTypeEnum.MASTER_NORMAL, "/vben5_antd_schema_master_normal");
}
@Test
public void testExecute_master_erp() {
testExecute_master(CodegenTemplateTypeEnum.MASTER_ERP, "/vben5_antd_schema_master_erp");
}
@Test
public void testExecute_master_inner() {
testExecute_master(CodegenTemplateTypeEnum.MASTER_INNER, "/vben5_antd_schema_master_inner");
}
private void testExecute_master(CodegenTemplateTypeEnum templateType, String path) {
// 准备参数
CodegenTableDO table = getTable("student")
.setFrontType(FRONT_TYPE)
.setTemplateType(templateType.getType());
List<CodegenColumnDO> columns = getColumnList("student");
// 准备参数(子表)
CodegenTableDO contactTable = getTable("contact")
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
.setFrontType(FRONT_TYPE)
.setSubJoinColumnId(100L).setSubJoinMany(true);
List<CodegenColumnDO> contactColumns = getColumnList("contact");
// 准备参数(班主任)
CodegenTableDO teacherTable = getTable("teacher")
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
.setFrontType(FRONT_TYPE)
.setSubJoinColumnId(200L).setSubJoinMany(false);
List<CodegenColumnDO> teacherColumns = getColumnList("teacher");
// 调用
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns,
Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
// 断言
assertResult(result, path);
}
}

View File

@ -1,109 +0,0 @@
package cn.iocoder.yudao.module.infra.service.codegen.inner;
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
import com.baomidou.mybatisplus.annotation.DbType;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* {@link CodegenEngine} Vue3 + Vben5 + Element Plus + General
*
* @author
*/
public class CodegenEngineVben5EleGeneralTest extends CodegenEngineAbstractTest {
private static final Integer FRONT_TYPE = CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType();
@Test
public void testExecute_one() {
// 准备参数
CodegenTableDO table = getTable("student")
.setFrontType(FRONT_TYPE)
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
List<CodegenColumnDO> columns = getColumnList("student");
// 调用
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
// 断言
assertResult(result, "/vben5_ele_general_one");
}
@Test
public void testExecute_one_importEnable() {
// 开启 import 开关
codegenProperties.setImportEnable(true);
codegenEngine.initGlobalBindingMap();
// 准备参数
CodegenTableDO table = getTable("student")
.setFrontType(FRONT_TYPE)
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
List<CodegenColumnDO> columns = getColumnList("student");
// 调用
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
// 断言
assertResult(result, "/vben5_ele_general_one_importEnable");
}
@Test
public void testExecute_tree() {
// 准备参数
CodegenTableDO table = getTable("category")
.setFrontType(FRONT_TYPE)
.setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
List<CodegenColumnDO> columns = getColumnList("category");
// 调用
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
// 断言
assertResult(result, "/vben5_ele_general_tree");
}
@Test
public void testExecute_master_normal() {
testExecute_master(CodegenTemplateTypeEnum.MASTER_NORMAL, "/vben5_ele_general_master_normal");
}
@Test
public void testExecute_master_erp() {
testExecute_master(CodegenTemplateTypeEnum.MASTER_ERP, "/vben5_ele_general_master_erp");
}
@Test
public void testExecute_master_inner() {
testExecute_master(CodegenTemplateTypeEnum.MASTER_INNER, "/vben5_ele_general_master_inner");
}
private void testExecute_master(CodegenTemplateTypeEnum templateType, String path) {
// 准备参数
CodegenTableDO table = getTable("student")
.setFrontType(FRONT_TYPE)
.setTemplateType(templateType.getType());
List<CodegenColumnDO> columns = getColumnList("student");
// 准备参数(子表)
CodegenTableDO contactTable = getTable("contact")
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
.setFrontType(FRONT_TYPE)
.setSubJoinColumnId(100L).setSubJoinMany(true);
List<CodegenColumnDO> contactColumns = getColumnList("contact");
// 准备参数(班主任)
CodegenTableDO teacherTable = getTable("teacher")
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
.setFrontType(FRONT_TYPE)
.setSubJoinColumnId(200L).setSubJoinMany(false);
List<CodegenColumnDO> teacherColumns = getColumnList("teacher");
// 调用
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns,
Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
// 断言
assertResult(result, path);
}
}

View File

@ -1,109 +0,0 @@
package cn.iocoder.yudao.module.infra.service.codegen.inner;
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
import com.baomidou.mybatisplus.annotation.DbType;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* {@link CodegenEngine} Vue3 + Vben5 + Element Plus + Schema
*
* @author
*/
public class CodegenEngineVben5EleSchemaTest extends CodegenEngineAbstractTest {
private static final Integer FRONT_TYPE = CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType();
@Test
public void testExecute_one() {
// 准备参数
CodegenTableDO table = getTable("student")
.setFrontType(FRONT_TYPE)
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
List<CodegenColumnDO> columns = getColumnList("student");
// 调用
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
// 断言
assertResult(result, "/vben5_ele_schema_one");
}
@Test
public void testExecute_one_importEnable() {
// 开启 import 开关
codegenProperties.setImportEnable(true);
codegenEngine.initGlobalBindingMap();
// 准备参数
CodegenTableDO table = getTable("student")
.setFrontType(FRONT_TYPE)
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
List<CodegenColumnDO> columns = getColumnList("student");
// 调用
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
// 断言
assertResult(result, "/vben5_ele_schema_one_importEnable");
}
@Test
public void testExecute_tree() {
// 准备参数
CodegenTableDO table = getTable("category")
.setFrontType(FRONT_TYPE)
.setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
List<CodegenColumnDO> columns = getColumnList("category");
// 调用
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
// 断言
assertResult(result, "/vben5_ele_schema_tree");
}
@Test
public void testExecute_master_normal() {
testExecute_master(CodegenTemplateTypeEnum.MASTER_NORMAL, "/vben5_ele_schema_master_normal");
}
@Test
public void testExecute_master_erp() {
testExecute_master(CodegenTemplateTypeEnum.MASTER_ERP, "/vben5_ele_schema_master_erp");
}
@Test
public void testExecute_master_inner() {
testExecute_master(CodegenTemplateTypeEnum.MASTER_INNER, "/vben5_ele_schema_master_inner");
}
private void testExecute_master(CodegenTemplateTypeEnum templateType, String path) {
// 准备参数
CodegenTableDO table = getTable("student")
.setFrontType(FRONT_TYPE)
.setTemplateType(templateType.getType());
List<CodegenColumnDO> columns = getColumnList("student");
// 准备参数(子表)
CodegenTableDO contactTable = getTable("contact")
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
.setFrontType(FRONT_TYPE)
.setSubJoinColumnId(100L).setSubJoinMany(true);
List<CodegenColumnDO> contactColumns = getColumnList("contact");
// 准备参数(班主任)
CodegenTableDO teacherTable = getTable("teacher")
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
.setFrontType(FRONT_TYPE)
.setSubJoinColumnId(200L).setSubJoinMany(false);
List<CodegenColumnDO> teacherColumns = getColumnList("teacher");
// 调用
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns,
Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
// 断言
assertResult(result, path);
}
}

View File

@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
import com.baomidou.mybatisplus.annotation.DbType;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
@ -16,6 +17,7 @@ import java.util.Map;
*
* @author
*/
@Disabled
public class CodegenEngineVue2Test extends CodegenEngineAbstractTest {
@Test
@ -34,23 +36,6 @@ public class CodegenEngineVue2Test extends CodegenEngineAbstractTest {
assertResult(result, "/vue2_one");
}
@Test
public void testExecute_vue2_one_importEnable() {
// 开启 import 开关
codegenProperties.setImportEnable(true);
codegenEngine.initGlobalBindingMap();
// 准备参数
CodegenTableDO table = getTable("student")
.setFrontType(CodegenFrontTypeEnum.VUE2_ELEMENT_UI.getType())
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
List<CodegenColumnDO> columns = getColumnList("student");
// 调用
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
// 断言
assertResult(result, "/vue2_one_importEnable");
}
@Test
public void testExecute_vue2_tree() {
// 准备参数

View File

@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
import com.baomidou.mybatisplus.annotation.DbType;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
@ -16,6 +17,7 @@ import java.util.Map;
*
* @author
*/
@Disabled
public class CodegenEngineVue3Test extends CodegenEngineAbstractTest {
@Test
@ -34,23 +36,6 @@ public class CodegenEngineVue3Test extends CodegenEngineAbstractTest {
assertResult(result, "/vue3_one");
}
@Test
public void testExecute_vue3_one_importEnable() {
// 开启 import 开关
codegenProperties.setImportEnable(true);
codegenEngine.initGlobalBindingMap();
// 准备参数
CodegenTableDO table = getTable("student")
.setFrontType(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType())
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
List<CodegenColumnDO> columns = getColumnList("student");
// 调用
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
// 断言
assertResult(result, "/vue3_one_importEnable");
}
@Test
public void testExecute_vue3_tree() {
// 准备参数

View File

@ -42,7 +42,6 @@ public class FileServiceImplTest extends BaseDbUnitTest {
public void setUp() {
FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
FileServiceImpl.PATH_SUFFIX_AS_DIRECTORY = true;
}
@Test
@ -94,7 +93,7 @@ public class FileServiceImplTest extends BaseDbUnitTest {
String url = randomString();
AtomicReference<String> pathRef = new AtomicReference<>();
when(client.upload(same(content), argThat(path -> {
assertTrue(path.matches(directory + "/\\d{8}/\\d+/" + name + ".jpg"));
assertTrue(path.matches(directory + "/\\d{8}/" + name + "_\\d+.jpg"));
pathRef.set(path);
return true;
}), eq(type))).thenReturn(url);
@ -126,7 +125,7 @@ public class FileServiceImplTest extends BaseDbUnitTest {
String url = randomString();
AtomicReference<String> pathRef = new AtomicReference<>();
when(client.upload(same(content), argThat(path -> {
assertTrue(path.matches("\\d{8}/\\d+/6318848e882d8a7e7e82789d87608f684ee52d41966bfc8cad3ce15aad2b970e\\.jpg"));
assertTrue(path.matches("\\d{8}/6318848e882d8a7e7e82789d87608f684ee52d41966bfc8cad3ce15aad2b970e_\\d+\\.jpg"));
pathRef.set(path);
return true;
}), eq(type))).thenReturn(url);
@ -201,10 +200,10 @@ public class FileServiceImplTest extends BaseDbUnitTest {
String path = fileService.generateUploadPath(name, directory);
// 断言
// 格式为avatar/yyyyMMdd/{时间戳+随机数}/test.jpg
// 格式为avatar/yyyyMMdd/test_timestamp.jpg
assertTrue(path.startsWith(directory + "/"));
// 包含日期格式8 位数字,如 20240517
assertTrue(path.matches(directory + "/\\d{8}/\\d+/test\\.jpg"));
assertTrue(path.matches(directory + "/\\d{8}/test_\\d+\\.jpg"));
}
@Test
@ -237,9 +236,9 @@ public class FileServiceImplTest extends BaseDbUnitTest {
String path = fileService.generateUploadPath(name, directory);
// 断言
// 格式为avatar/{时间戳+随机数}/test.jpg
// 格式为avatar/test_timestamp.jpg
assertTrue(path.startsWith(directory + "/"));
assertTrue(path.matches(directory + "/\\d+/test\\.jpg"));
assertTrue(path.matches(directory + "/test_\\d+\\.jpg"));
}
@Test
@ -270,9 +269,9 @@ public class FileServiceImplTest extends BaseDbUnitTest {
String path = fileService.generateUploadPath(name, directory);
// 断言
// 格式为avatar/yyyyMMdd/{时间戳+随机数}/test
// 格式为avatar/yyyyMMdd/test_timestamp
assertTrue(path.startsWith(directory + "/"));
assertTrue(path.matches(directory + "/\\d{8}/\\d+/test"));
assertTrue(path.matches(directory + "/\\d{8}/test_\\d+"));
}
@Test
@ -287,59 +286,8 @@ public class FileServiceImplTest extends BaseDbUnitTest {
String path = fileService.generateUploadPath(name, directory);
// 断言
// 格式为yyyyMMdd/{时间戳+随机数}/test.jpg
assertTrue(path.matches("\\d{8}/\\d+/test\\.jpg"));
}
@Test
public void testGenerateUploadPath_SuffixAsName_AllEnabled() {
// 准备参数
String name = "test.jpg";
String directory = "avatar";
FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
FileServiceImpl.PATH_SUFFIX_AS_DIRECTORY = false;
// 调用
String path = fileService.generateUploadPath(name, directory);
// 断言
// 格式为avatar/yyyyMMdd/test_{时间戳+随机数}.jpg
assertTrue(path.matches(directory + "/\\d{8}/test_\\d+\\.jpg"));
}
@Test
public void testGenerateUploadPath_SuffixAsName_PrefixDisabled() {
// 准备参数
String name = "test.jpg";
String directory = "avatar";
FileServiceImpl.PATH_PREFIX_DATE_ENABLE = false;
FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
FileServiceImpl.PATH_SUFFIX_AS_DIRECTORY = false;
// 调用
String path = fileService.generateUploadPath(name, directory);
// 断言
// 格式为avatar/test_{时间戳+随机数}.jpg
assertTrue(path.matches(directory + "/test_\\d+\\.jpg"));
}
@Test
public void testGenerateUploadPath_SuffixAsName_NoExtension() {
// 准备参数
String name = "test";
String directory = "avatar";
FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
FileServiceImpl.PATH_SUFFIX_AS_DIRECTORY = false;
// 调用
String path = fileService.generateUploadPath(name, directory);
// 断言
// 格式为avatar/yyyyMMdd/test_{时间戳+随机数}
assertTrue(path.matches(directory + "/\\d{8}/test_\\d+"));
// 格式为yyyyMMdd/test_timestamp.jpg
assertTrue(path.matches("\\d{8}/test_\\d+\\.jpg"));
}
@Test
@ -354,8 +302,8 @@ public class FileServiceImplTest extends BaseDbUnitTest {
String path = fileService.generateUploadPath(name, directory);
// 断言
// 格式为yyyyMMdd/{时间戳+随机数}/test.jpg
assertTrue(path.matches("\\d{8}/\\d+/test\\.jpg"));
// 格式为yyyyMMdd/test_timestamp.jpg
assertTrue(path.matches("\\d{8}/test_\\d+\\.jpg"));
}
}

Some files were not shown because too many files have changed in this diff Show More