Compare commits
122 Commits
v2026.01(j
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
649362ea33 | |
|
|
10035e1709 | |
|
|
de134cf3b3 | |
|
|
c937f30436 | |
|
|
9431722bae | |
|
|
2ba231aa55 | |
|
|
0c261b4e02 | |
|
|
08c88e0cb3 | |
|
|
0ca9eda251 | |
|
|
1bc987805a | |
|
|
3020c6cdb0 | |
|
|
ffb4e8c158 | |
|
|
ff4ed31c1b | |
|
|
14add8edf0 | |
|
|
6e492e1e6b | |
|
|
7a4c1a0ba4 | |
|
|
9a319b68ff | |
|
|
931472e38d | |
|
|
47660ed156 | |
|
|
fbeec7d731 | |
|
|
8ad9167ddd | |
|
|
1b060dc93f | |
|
|
4363135bf8 | |
|
|
6d213c6746 | |
|
|
b8156e679d | |
|
|
d86fe5e5ca | |
|
|
38ec655d10 | |
|
|
9782e75514 | |
|
|
e7ac652a9c | |
|
|
17943589a4 | |
|
|
d67f91a63e | |
|
|
f6b769fc2d | |
|
|
36fbb9a68b | |
|
|
25a823fe82 | |
|
|
3314376e59 | |
|
|
050edb2db7 | |
|
|
5088b8c2e2 | |
|
|
8637b2a28f | |
|
|
1702fc1acb | |
|
|
d5ab0b06a7 | |
|
|
5cf473d48e | |
|
|
f194e7a3d3 | |
|
|
cf7b6d9cbc | |
|
|
9d1dd25bc7 | |
|
|
63cae8bc31 | |
|
|
568b0c29c0 | |
|
|
3f07aa3cd2 | |
|
|
42133c7141 | |
|
|
ff2dd155a6 | |
|
|
a53558cf4b | |
|
|
52e883d5be | |
|
|
32c353c53d | |
|
|
de4abc2f5f | |
|
|
9db40c8b80 | |
|
|
e657805544 | |
|
|
b8cff89a12 | |
|
|
3b9ab7fa4a | |
|
|
3e8eca7b8d | |
|
|
4b8346ec80 | |
|
|
e3e1b2b3d5 | |
|
|
3ba4104542 | |
|
|
23c934f727 | |
|
|
61bfcdfa00 | |
|
|
915885c825 | |
|
|
c8b85ad8a7 | |
|
|
63e6880a10 | |
|
|
4dabfea1df | |
|
|
11a6a049fd | |
|
|
d545eb5631 | |
|
|
1f7f85bddb | |
|
|
8f3b6cb0aa | |
|
|
6780ed6879 | |
|
|
d07426e43f | |
|
|
996ac02c0b | |
|
|
e3a34d9067 | |
|
|
116b6766b3 | |
|
|
6b91b4169d | |
|
|
8c7087ca2a | |
|
|
3e5e60ce96 | |
|
|
f57f0c551c | |
|
|
e1b3589bff | |
|
|
d66d1fcac0 | |
|
|
5fe868e096 | |
|
|
c3125dbc92 | |
|
|
adbcc60225 | |
|
|
2fe63be6c9 | |
|
|
d947d0463a | |
|
|
05375287bc | |
|
|
f5f53d59ca | |
|
|
838e2923bb | |
|
|
83ea45911b | |
|
|
81009a7082 | |
|
|
11ff5b4a7c | |
|
|
b794031b71 | |
|
|
199799b0c9 | |
|
|
86087983a7 | |
|
|
2eb326d62d | |
|
|
1e14c5c38f | |
|
|
d9b57e6897 | |
|
|
dc0ca32697 | |
|
|
79f233149c | |
|
|
b38cbe9c7f | |
|
|
9d2392a81a | |
|
|
2b9a03bd93 | |
|
|
16e095c343 | |
|
|
804d3eaaeb | |
|
|
8937853307 | |
|
|
9ffec01fa0 | |
|
|
3f599a623a | |
|
|
bb3f1954ee | |
|
|
34d74b378e | |
|
|
53d3146ad9 | |
|
|
a61ecaa019 | |
|
|
f969670fd3 | |
|
|
1fca0acc92 | |
|
|
6ca2c97849 | |
|
|
71393eed21 | |
|
|
92eda45afd | |
|
|
2d4251eda7 | |
|
|
06586b85f6 | |
|
|
7dc6b24e66 | |
|
|
4052c5c4d0 |
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 216 KiB |
|
After Width: | Height: | Size: 54 KiB |
49
README.md
|
|
@ -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 等功能
|
||||
* 【精简版】:只包括系统功能、基础设施功能,不包括会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP 等功能
|
||||
* 【完整版】:包括系统功能、基础设施、会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、WMS、MES、IM 即时通讯、AI 大模型、IoT 物联网等功能
|
||||
* 【精简版】:只包括系统功能、基础设施功能,不包括会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、WMS、MES、IM 即时通讯、AI 大模型、IoT 物联网等功能
|
||||
|
||||
可参考 [《迁移文档》](https://cloud.iocoder.cn/migrate-module/) ,只需要 5-10 分钟,即可将【完整版】按需迁移到【精简版】
|
||||
|
||||
|
|
@ -105,7 +105,7 @@
|
|||
|
||||
团队包含专业的项目经理、架构师、前端工程师、后端工程师、测试工程师、运维工程师,可以提供全流程的外包服务。
|
||||
|
||||
项目可以是商城、SCRM 系统、OA 系统、物流系统、ERP 系统、CMS 系统、HIS 系统、支付系统、IM 聊天、微信公众号、微信小程序等等。
|
||||
项目可以是商城、SCRM 系统、OA 系统、物流系统、ERP 系统、CMS 系统、HIS 系统、支付系统、IM 即时通讯、微信公众号、微信小程序等等。
|
||||
|
||||
## 🐼 内置功能
|
||||
|
||||
|
|
@ -115,7 +115,7 @@
|
|||
|
||||
* 通用模块(必选):系统功能、基础设施
|
||||
* 通用模块(可选):工作流程、支付系统、数据报表、会员中心
|
||||
* 业务系统(按需):ERP 系统、CRM 系统、商城系统、微信公众号、AI 大模型
|
||||
* 业务系统(按需):Mall 电子商城、OA 办公自动化、ERP 企业资源计划系统、WMS 仓库管理系统、CRM 客户关系管理、CMS 内容管理系统、MES 执行制造系统、AI 大模型平台、IoT 物联网系统、IM 即时通讯系统、Mobile 手机移动端、Report 数据大屏
|
||||
|
||||
> 友情提示:本项目基于 RuoYi-Vue 修改,**重构优化**后端的代码,**美化**前端的界面。
|
||||
>
|
||||
|
|
@ -273,12 +273,28 @@
|
|||
|
||||

|
||||
|
||||
### WMS 系统
|
||||
|
||||
演示地址:<https://cloud.iocoder.cn/wms-preview/>
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### CRM 系统
|
||||
|
||||
演示地址:<https://cloud.iocoder.cn/crm-preview/>
|
||||
|
||||

|
||||
|
||||
### MES 系统
|
||||
|
||||
演示地址:<https://cloud.iocoder.cn/mes-preview/>
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### AI 大模型
|
||||
|
||||
演示地址:<https://cloud.iocoder.cn/ai-preview/>
|
||||
|
|
@ -287,6 +303,27 @@
|
|||
|
||||

|
||||
|
||||
### IoT 物联网
|
||||
|
||||
演示地址:<https://cloud.iocoder.cn/iot/build>
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### IM 即时通讯
|
||||
|
||||
演示地址(Cloud):<https://cloud.iocoder.cn/im-preview/>
|
||||
|
||||
演示地址(Vue3 + Element Plus):<http://dashboard-vue3.yudao.iocoder.cn>
|
||||
|
||||
|
||||

|
||||
|
||||
| 聊天界面 | 聊天管理 |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
|
||||
## 🐨 技术栈
|
||||
|
||||
### 微服务
|
||||
|
|
@ -304,7 +341,11 @@
|
|||
| `yudao-module-mall` | 商城系统的 Module 模块 |
|
||||
| `yudao-module-erp` | ERP 系统的 Module 模块 |
|
||||
| `yudao-module-crm` | CRM 系统的 Module 模块 |
|
||||
| `yudao-module-mes` | MES 系统的 Module 模块 |
|
||||
| `yudao-module-wms` | WMS 系统的 Module 模块 |
|
||||
| `yudao-module-im` | IM 即时通讯的 Module 模块 |
|
||||
| `yudao-module-ai` | AI 大模型的 Module 模块 |
|
||||
| `yudao-module-iot` | IoT 物联网的 Module 模块 |
|
||||
| `yudao-module-mp` | 微信公众号的 Module 模块 |
|
||||
| `yudao-module-report` | 大屏报表 Module 模块 |
|
||||
|
||||
|
|
|
|||
9
pom.xml
|
|
@ -24,9 +24,12 @@
|
|||
<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>
|
||||
<module>yudao-module-wms</module>
|
||||
<module>yudao-module-im</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>
|
||||
|
|
@ -34,7 +37,7 @@
|
|||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
<properties>
|
||||
<revision>2026.01-jdk8-SNAPSHOT</revision>
|
||||
<revision>2026.05-jdk8-SNAPSHOT</revision>
|
||||
<!-- Maven 相关 -->
|
||||
<java.version>1.8</java.version>
|
||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||
|
|
@ -43,7 +46,7 @@
|
|||
<maven-compiler-plugin.version>3.14.0</maven-compiler-plugin.version>
|
||||
<flatten-maven-plugin.version>1.7.2</flatten-maven-plugin.version>
|
||||
<!-- maven-surefire-plugin 暂时无法通过 bom 的依赖读取(兼容老版本 IDEA 2024 及以前版本) -->
|
||||
<lombok.version>1.18.42</lombok.version>
|
||||
<lombok.version>1.18.46</lombok.version>
|
||||
<spring.boot.version>2.7.18</spring.boot.version>
|
||||
<mapstruct.version>1.6.3</mapstruct.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
|
|
|
|||
|
|
@ -62,6 +62,10 @@ 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 部分
|
||||
# 相关 issue:https://t.zsxq.com/96IFc 、https://t.zsxq.com/rC3A3
|
||||
|
|
@ -73,11 +77,18 @@ def load_and_clean(sql_file: str) -> str:
|
|||
|
||||
|
||||
class Convertor(ABC):
|
||||
# 不同数据库的关键字不完全一致;子类按需声明需要转义的列名。
|
||||
reserved_column_names = set()
|
||||
|
||||
def __init__(self, src: str, db_type) -> None:
|
||||
self.src = src
|
||||
self.db_type = db_type
|
||||
self.content = load_and_clean(self.src)
|
||||
self.table_script_list = re.findall(r"CREATE TABLE [^;]*;", self.content)
|
||||
# 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)
|
||||
|
||||
@abstractmethod
|
||||
def translate_type(self, type: str, size: Optional[Union[int, Tuple[int]]]) -> str:
|
||||
|
|
@ -171,6 +182,31 @@ class Convertor(ABC):
|
|||
"""
|
||||
return ""
|
||||
|
||||
def escape_column_name(self, name: str) -> str:
|
||||
"""转义目标库保留字列名,例如 Oracle / Kingbase 的 level。"""
|
||||
|
||||
column_name = name.lower()
|
||||
if column_name in self.reserved_column_names:
|
||||
return f'"{column_name}"'
|
||||
return column_name
|
||||
|
||||
def escape_insert_columns(self, insert_script: str) -> str:
|
||||
"""INSERT 显式列清单需要和 CREATE / COMMENT 使用同一套列名转义。"""
|
||||
|
||||
match = re.match(
|
||||
r"(INSERT INTO\s+\S+\s*\()([^)]+)(\)\s+VALUES\s+[\s\S]*)",
|
||||
insert_script,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
if not match:
|
||||
return insert_script
|
||||
|
||||
columns = [
|
||||
self.escape_column_name(column.strip())
|
||||
for column in match.group(2).split(",")
|
||||
]
|
||||
return f"{match.group(1)}{', '.join(columns)}{match.group(3)}"
|
||||
|
||||
@staticmethod
|
||||
def inserts(table_name: str, script_content: str) -> Generator:
|
||||
PREFIX = f"INSERT INTO `{table_name}`"
|
||||
|
|
@ -182,7 +218,8 @@ class Convertor(ABC):
|
|||
head = head.strip().replace("`", "").lower()
|
||||
tail = tail.strip().replace(r"\"", '"')
|
||||
# tail = tail.replace("b'0'", "'0'").replace("b'1'", "'1'")
|
||||
yield f"INSERT INTO {table_name.lower()} {head} VALUES {tail}"
|
||||
col_part = f" {head}" if head else ""
|
||||
yield f"INSERT INTO {table_name.lower()}{col_part} VALUES {tail}"
|
||||
|
||||
@staticmethod
|
||||
def index(ddl: Dict) -> Generator:
|
||||
|
|
@ -195,18 +232,55 @@ class Convertor(ABC):
|
|||
Generator[str]: create index 语句
|
||||
"""
|
||||
|
||||
def generate_columns(columns):
|
||||
keys = [
|
||||
f"{col['name'].lower()}{' ' + col['order'].lower() if col['order'] != 'ASC' else ''}"
|
||||
for col in columns[0]
|
||||
]
|
||||
return ", ".join(keys)
|
||||
|
||||
for no, index in enumerate(ddl["index"], 1):
|
||||
columns = generate_columns(index["columns"])
|
||||
for no, index in enumerate(ddl.get("index", []), 1):
|
||||
columns = ", ".join(Convertor.index_columns(index.get("columns", [])))
|
||||
if not columns:
|
||||
continue
|
||||
table_name = ddl["table_name"].lower()
|
||||
yield f"CREATE INDEX idx_{table_name}_{no:02d} ON {table_name} ({columns})"
|
||||
|
||||
@staticmethod
|
||||
def index_columns(columns) -> list:
|
||||
"""兼容 simple-ddl-parser 不同版本的索引列结构。"""
|
||||
|
||||
keys = []
|
||||
|
||||
def append(name, order="ASC"):
|
||||
if not name:
|
||||
return
|
||||
column_name = str(name).strip("`").lower()
|
||||
column_order = str(order or "ASC").upper()
|
||||
if column_order == "DESC":
|
||||
keys.append(f"{column_name} desc")
|
||||
else:
|
||||
keys.append(column_name)
|
||||
|
||||
def visit(value):
|
||||
# 普通索引常见结构:[[{'name': 'user_id', 'order': 'ASC'}]]
|
||||
if isinstance(value, (list, tuple)):
|
||||
for item in value:
|
||||
visit(item)
|
||||
return
|
||||
if isinstance(value, dict):
|
||||
name = value.get("name")
|
||||
if isinstance(name, (dict, list, tuple)):
|
||||
visit(name)
|
||||
return
|
||||
append(name, value.get("order", "ASC"))
|
||||
return
|
||||
# 唯一索引在部分版本中会被解析成 ['mobile', 'ASC', 'tenant_id', 'ASC']。
|
||||
if isinstance(value, str):
|
||||
token = value.strip("`")
|
||||
order = token.upper()
|
||||
if order in ("ASC", "DESC"):
|
||||
if order == "DESC" and keys and not keys[-1].endswith(" desc"):
|
||||
keys[-1] = f"{keys[-1]} desc"
|
||||
return
|
||||
append(token)
|
||||
|
||||
visit(columns)
|
||||
return keys
|
||||
|
||||
@staticmethod
|
||||
def unique_index(ddl: Dict) -> Generator:
|
||||
if "constraints" in ddl and "uniques" in ddl["constraints"]:
|
||||
|
|
@ -214,7 +288,9 @@ class Convertor(ABC):
|
|||
for uk in uk_list:
|
||||
table_name = ddl["table_name"]
|
||||
uk_name = uk["constraint_name"]
|
||||
uk_columns = uk["columns"]
|
||||
uk_columns = Convertor.index_columns(uk["columns"])
|
||||
if not uk_columns:
|
||||
continue
|
||||
yield table_name, uk_name, uk_columns
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -227,7 +303,8 @@ class Convertor(ABC):
|
|||
yield field, comment_string
|
||||
|
||||
def table_comment(self, table_sql: str) -> str:
|
||||
match = re.search(r"COMMENT \='([^']+)';", table_sql)
|
||||
# 兼容 COMMENT='xxx' / COMMENT = 'xxx' 等等号两侧空格变体;允许 COMMENT 内含转义单引号
|
||||
match = re.search(r"COMMENT\s*=\s*'((?:[^'\\]|\\.)*)'", table_sql)
|
||||
return match.group(1) if match else None
|
||||
|
||||
def print(self):
|
||||
|
|
@ -251,7 +328,9 @@ class Convertor(ABC):
|
|||
|
||||
error_scripts = []
|
||||
for table_sql in self.table_script_list:
|
||||
ddl = DDLParser(table_sql.replace("`", "")).run()
|
||||
# 剥离 COMMENT 子句避免 DDLParser 无法处理中文等特殊字符
|
||||
table_sql_for_parse = re.sub(r"\s+COMMENT\s+'[^']*'", "", table_sql)
|
||||
ddl = DDLParser(table_sql_for_parse.replace("`", "")).run()
|
||||
|
||||
# 如果parse失败, 需要跟进
|
||||
if len(ddl) == 0:
|
||||
|
|
@ -266,17 +345,23 @@ 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"] = 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]
|
||||
column["comment"] = comments_dict.get(column["name"], "")
|
||||
table_ddl["comment"] = self.table_comment(orig_table_sql) or ""
|
||||
|
||||
# 为每个表生成个6个基本部分
|
||||
create = self.gen_create(table_ddl)
|
||||
pk = self.gen_pk(table_name)
|
||||
has_id = any(col["name"].lower() == "id" for col in table_ddl["columns"])
|
||||
pk = self.gen_pk(table_name) if has_id else ""
|
||||
uk = self.gen_uk(table_ddl)
|
||||
index = self.gen_index(table_ddl)
|
||||
comment = self.gen_comment(table_ddl)
|
||||
|
|
@ -320,25 +405,31 @@ class PostgreSQLConvertor(Convertor):
|
|||
|
||||
if type == "varchar":
|
||||
return f"varchar({size})"
|
||||
if type in ("int", "int unsigned"):
|
||||
if type in ("int", "int unsigned", "int unsigned zerofill"):
|
||||
return "int4"
|
||||
if type in ("bigint", "bigint unsigned"):
|
||||
return "int8"
|
||||
if type == "datetime":
|
||||
if type in ("tinyint", "smallint", "tinyint unsigned"):
|
||||
return "int2"
|
||||
if type in ("datetime", "timestamp null"):
|
||||
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})"
|
||||
return f"timestamp({size})" if size else "timestamp"
|
||||
if type == "bit":
|
||||
return "bool"
|
||||
if type in ("tinyint", "smallint"):
|
||||
return "int2"
|
||||
if type in ("text", "longtext"):
|
||||
return "text"
|
||||
if type in ("blob", "mediumblob"):
|
||||
if type in ("blob", "mediumblob", "longblob"):
|
||||
return "bytea"
|
||||
if type == "decimal":
|
||||
return (
|
||||
f"numeric({','.join(str(s) for s in size)})" if len(size) else "numeric"
|
||||
f"numeric({','.join(str(s) for s in size)})" if size and len(size) else "numeric"
|
||||
)
|
||||
|
||||
def gen_create(self, ddl: Dict) -> str:
|
||||
|
|
@ -351,9 +442,13 @@ 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}"
|
||||
return f"{self.escape_column_name(name)} {full_type} {nullable} {default}"
|
||||
|
||||
table_name = ddl["table_name"].lower()
|
||||
columns = [f"{_generate_column(col).strip()}" for col in ddl["columns"]]
|
||||
|
|
@ -378,7 +473,7 @@ CREATE TABLE {table_name} (
|
|||
for column in table_ddl["columns"]:
|
||||
table_comment = column["comment"]
|
||||
script += (
|
||||
f"COMMENT ON COLUMN {table_ddl['table_name']}.{column['name']} IS '{table_comment}';"
|
||||
f"COMMENT ON COLUMN {table_ddl['table_name']}.{self.escape_column_name(column['name'])} IS '{table_comment}';"
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
|
|
@ -407,6 +502,9 @@ CREATE TABLE {table_name} (
|
|||
"""生成 insert 语句,以及根据最后的 insert id+1 生成 Sequence"""
|
||||
|
||||
inserts = list(Convertor.inserts(table_name, self.content))
|
||||
inserts = [self.escape_insert_columns(s) for s in inserts]
|
||||
# 转换 MySQL 字符串转义为 PostgreSQL 格式:\\ -> \,\' -> ''
|
||||
inserts = [re.sub(r"\\\\|\\'", lambda m: "\\" if m.group() == "\\\\" else "''", s) for s in inserts]
|
||||
## 生成 insert 脚本
|
||||
script = ""
|
||||
last_id = 0
|
||||
|
|
@ -452,6 +550,8 @@ INSERT INTO dual VALUES (1);
|
|||
|
||||
|
||||
class OracleConvertor(Convertor):
|
||||
reserved_column_names = {"level", "size"}
|
||||
|
||||
def __init__(self, src):
|
||||
super().__init__(src, "Oracle")
|
||||
|
||||
|
|
@ -496,10 +596,8 @@ class OracleConvertor(Convertor):
|
|||
# Oracle的 INSERT '' 不能通过NOT NULL校验,因此对文字类型字段覆写为 NULL
|
||||
nullable = "NULL" if type in ("varchar", "text", "longtext") else nullable
|
||||
default = f"DEFAULT {col['default']}" if col["default"] is not None else ""
|
||||
# Oracle 中 size 不能作为字段名
|
||||
field_name = '"size"' if name == "size" else name
|
||||
# Oracle DEFAULT 定义在 NULLABLE 之前
|
||||
return f"{field_name} {full_type} {default} {nullable}"
|
||||
return f"{self.escape_column_name(name)} {full_type} {default} {nullable}"
|
||||
|
||||
table_name = ddl["table_name"].lower()
|
||||
columns = [f"{generate_column(col).strip()}" for col in ddl["columns"]]
|
||||
|
|
@ -524,7 +622,7 @@ CREATE TABLE {table_name} (
|
|||
for column in table_ddl["columns"]:
|
||||
table_comment = column["comment"]
|
||||
script += (
|
||||
f"COMMENT ON COLUMN {table_ddl['table_name']}.{column['name']} IS '{table_comment}';"
|
||||
f"COMMENT ON COLUMN {table_ddl['table_name']}.{self.escape_column_name(column['name'])} IS '{table_comment}';"
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
|
|
@ -556,6 +654,7 @@ CREATE TABLE {table_name} (
|
|||
"""拷贝 INSERT 语句"""
|
||||
inserts = []
|
||||
for insert_script in Convertor.inserts(table_name, self.content):
|
||||
insert_script = self.escape_insert_columns(insert_script)
|
||||
# 对日期数据添加 TO_DATE 转换
|
||||
insert_script = re.sub(
|
||||
r"('\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}')",
|
||||
|
|
@ -877,6 +976,8 @@ SET IDENTITY_INSERT {table_name.lower()} OFF;
|
|||
|
||||
|
||||
class KingbaseConvertor(PostgreSQLConvertor):
|
||||
reserved_column_names = {"level"}
|
||||
|
||||
def __init__(self, src):
|
||||
super().__init__(src)
|
||||
self.db_type = "Kingbase"
|
||||
|
|
@ -895,7 +996,7 @@ class KingbaseConvertor(PostgreSQLConvertor):
|
|||
if full_type == "text":
|
||||
nullable = "NULL"
|
||||
default = f"DEFAULT {col['default']}" if col["default"] is not None else ""
|
||||
return f"{name} {full_type} {nullable} {default}"
|
||||
return f"{self.escape_column_name(name)} {full_type} {nullable} {default}"
|
||||
|
||||
table_name = ddl["table_name"].lower()
|
||||
columns = [f"{_generate_column(col).strip()}" for col in ddl["columns"]]
|
||||
|
|
@ -915,6 +1016,8 @@ CREATE TABLE {table_name} (
|
|||
|
||||
|
||||
class OpengaussConvertor(KingbaseConvertor):
|
||||
reserved_column_names = set()
|
||||
|
||||
def __init__(self, src):
|
||||
super().__init__(src)
|
||||
self.db_type = "OpenGauss"
|
||||
|
|
|
|||
|
|
@ -14,30 +14,30 @@
|
|||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
<properties>
|
||||
<revision>2026.01-jdk8-SNAPSHOT</revision>
|
||||
<revision>2026.05-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.cloud.alibaba.version>2021.0.6.2</spring.cloud.alibaba.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 版本 -->
|
||||
<!-- 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.27</druid.version>
|
||||
<druid.version>1.2.28</druid.version>
|
||||
<mybatis.version>3.5.19</mybatis.version>
|
||||
<mybatis-plus.version>3.5.15</mybatis-plus.version>
|
||||
<mybatis-plus-join.version>1.5.5</mybatis-plus-join.version>
|
||||
<mybatis-plus.version>3.5.16</mybatis-plus.version>
|
||||
<mybatis-plus-join.version>1.5.7</mybatis-plus-join.version>
|
||||
<dynamic-datasource.version>4.5.0</dynamic-datasource.version>
|
||||
<easy-trans.version>3.0.6</easy-trans.version>
|
||||
<redisson.version>3.52.0</redisson.version>
|
||||
<redisson.version>4.4.0</redisson.version>
|
||||
<dm8.jdbc.version>8.1.3.140</dm8.jdbc.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>
|
||||
<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>
|
||||
<!-- 消息队列 -->
|
||||
<rocketmq-spring.version>2.3.5</rocketmq-spring.version>
|
||||
<!-- RPC 相关 -->
|
||||
|
|
@ -55,38 +55,44 @@
|
|||
<jedis-mock.version>1.1.12</jedis-mock.version>
|
||||
<mockito-inline.version>4.11.0</mockito-inline.version>
|
||||
<!-- Bpm 工作流相关 -->
|
||||
<flowable.version>6.8.0</flowable.version>
|
||||
<flowable.version>6.8.1</flowable.version>
|
||||
<!-- 工具类相关 -->
|
||||
<anji-plus-captcha.version>1.4.0</anji-plus-captcha.version>
|
||||
<jsoup.version>1.21.2</jsoup.version>
|
||||
<lombok.version>1.18.42</lombok.version>
|
||||
<jsoup.version>1.22.2</jsoup.version>
|
||||
<sensitive-word.version>0.29.5</sensitive-word.version>
|
||||
<pinyin4j.version>2.5.1</pinyin4j.version>
|
||||
<lombok.version>1.18.46</lombok.version>
|
||||
<mapstruct.version>1.6.3</mapstruct.version>
|
||||
<hutool-5.version>5.8.42</hutool-5.version>
|
||||
<hutool-5.version>5.8.44</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.5.0-jre</guava.version>
|
||||
<guava.version>33.6.0-jre</guava.version>
|
||||
<transmittable-thread-local.version>2.14.5</transmittable-thread-local.version>
|
||||
<commons-net.version>3.12.0</commons-net.version>
|
||||
<commons-net.version>3.13.0</commons-net.version>
|
||||
<commons-lang3.version>3.20.0</commons-lang3.version>
|
||||
<jsch.version>2.27.7</jsch.version>
|
||||
<jsch.version>2.28.2</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.9.Final</netty.version>
|
||||
<netty.version>4.2.14.Final</netty.version>
|
||||
<mqtt.version>1.2.5</mqtt.version>
|
||||
<vertx.version>4.5.22</vertx.version>
|
||||
<vertx.version>4.5.26</vertx.version>
|
||||
<okhttp.version>4.12.0</okhttp.version>
|
||||
<californium.version>3.12.0</californium.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.2,Spring Boot 2.7 默认 5.1.5 不兼容 -->
|
||||
<!-- 三方云服务相关 -->
|
||||
<awssdk.version>2.40.15</awssdk.version>
|
||||
<awssdk.version>2.44.0</awssdk.version>
|
||||
<justauth.version>1.16.7</justauth.version>
|
||||
<justauth-starter.version>1.4.0</justauth-starter.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>
|
||||
<jimureport.version>2.3.4</jimureport.version>
|
||||
<jimubi.version>2.3.2</jimubi.version>
|
||||
<weixin-java.version>4.8.2-20260501.180637</weixin-java.version>
|
||||
<bouncycastle.version>1.80</bouncycastle.version>
|
||||
<alipay-sdk-java.version>4.40.806.ALL</alipay-sdk-java.version>
|
||||
<!-- 专属于 JDK8 安全漏洞升级 -->
|
||||
<logback.version>1.2.13</logback.version> <!-- 无法使用 1.3.X 版本,启动会报错 -->
|
||||
</properties>
|
||||
|
|
@ -307,7 +313,7 @@
|
|||
<exclusion>
|
||||
<groupId>org.redisson</groupId>
|
||||
<!-- 使用 redisson-spring-data-27 替代,解决 Tuple NoClassDefFoundError 报错 -->
|
||||
<artifactId>redisson-spring-data-35</artifactId>
|
||||
<artifactId>redisson-spring-data-40</artifactId> <!-- Redisson 4.x 默认依赖 spring-data-40,排除后使用 spring-data-27 适配 Spring Boot 2.7 -->
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
|
@ -621,6 +627,18 @@
|
|||
<version>${jsoup.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.houbb</groupId>
|
||||
<artifactId>sensitive-word</artifactId> <!-- 敏感词检测:trie 树高效匹配 -->
|
||||
<version>${sensitive-word.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.belerweb</groupId>
|
||||
<artifactId>pinyin4j</artifactId> <!-- 汉字转拼音:作为 hutool PinyinUtil 的底层引擎 -->
|
||||
<version>${pinyin4j.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.reflections</groupId>
|
||||
<artifactId>reflections</artifactId>
|
||||
|
|
@ -671,6 +689,30 @@
|
|||
<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>
|
||||
|
|
@ -708,6 +750,24 @@
|
|||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- 锁定 weixin-java 传递依赖,避免 Maven 版本范围自动升级到 1.80.2 后 Fat Jar 启动失败。
|
||||
反馈:https://t.zsxq.com/pCVBo -->
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk18on</artifactId>
|
||||
<version>${bouncycastle.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcutil-jdk18on</artifactId>
|
||||
<version>${bouncycastle.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcpkix-jdk18on</artifactId>
|
||||
<version>${bouncycastle.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.binarywang</groupId>
|
||||
<artifactId>weixin-java-pay</artifactId>
|
||||
|
|
|
|||
|
|
@ -53,8 +53,8 @@
|
|||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
<scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
|
||||
</dependency>
|
||||
|
||||
|
|
@ -125,8 +125,8 @@
|
|||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>jakarta.validation</groupId>
|
||||
<artifactId>jakarta.validation-api</artifactId>
|
||||
<groupId>javax.validation</groupId>
|
||||
<artifactId>validation-api</artifactId>
|
||||
<scope>provided</scope> <!-- 设置为 provided,主要是 PageParam 使用到 -->
|
||||
</dependency>
|
||||
|
||||
|
|
|
|||
|
|
@ -124,6 +124,22 @@ public class CollectionUtils {
|
|||
return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public static <T, U> Set<U> convertLinkedSet(Collection<T> from, Function<T, U> func) {
|
||||
if (CollUtil.isEmpty(from)) {
|
||||
return new LinkedHashSet<>();
|
||||
}
|
||||
return from.stream().map(func).filter(Objects::nonNull)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
}
|
||||
|
||||
public static <T, U> Set<U> convertLinkedSet(Collection<T> from, Function<T, U> func, Predicate<T> filter) {
|
||||
if (CollUtil.isEmpty(from)) {
|
||||
return new LinkedHashSet<>();
|
||||
}
|
||||
return from.stream().filter(filter).map(func).filter(Objects::nonNull)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
}
|
||||
|
||||
public static <T, K> Map<K, T> convertMapByFilter(Collection<T> from, Predicate<T> filter, Function<T, K> keyFunc) {
|
||||
if (CollUtil.isEmpty(from)) {
|
||||
return new HashMap<>();
|
||||
|
|
@ -349,4 +365,37 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把单元素 head 与集合 tail 合并成新 List(head 在前,tail 顺序保留)
|
||||
*/
|
||||
public static <T> List<T> of(T head, Collection<T> tail) {
|
||||
List<T> list = new ArrayList<>();
|
||||
list.add(head);
|
||||
CollUtil.addAll(list, tail);
|
||||
return list;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -236,6 +236,18 @@ public class LocalDateTimeUtils {
|
|||
return LocalDateTime.now().with(TemporalAdjusters.firstDayOfYear()).with(LocalTime.MIN);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最近 N 天的 0 点时刻序列(升序,含今天)
|
||||
*/
|
||||
public static List<LocalDateTime> getLatestDays(int days) {
|
||||
LocalDateTime today = getToday();
|
||||
List<LocalDateTime> dates = new ArrayList<>(days);
|
||||
for (int i = days - 1; i >= 0; i--) {
|
||||
dates.add(today.minusDays(i));
|
||||
}
|
||||
return dates;
|
||||
}
|
||||
|
||||
public static List<LocalDateTime[]> getDateRangeList(LocalDateTime startTime,
|
||||
LocalDateTime endTime,
|
||||
Integer interval) {
|
||||
|
|
@ -302,6 +314,21 @@ public class LocalDateTimeUtils {
|
|||
return timeRanges;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取从开始日期起的日期列表
|
||||
*
|
||||
* @param startDate 开始日期
|
||||
* @param days 天数
|
||||
* @return 日期列表,包含开始日期
|
||||
*/
|
||||
public static List<LocalDate> getDateList(LocalDate startDate, int days) {
|
||||
List<LocalDate> dateList = new ArrayList<>(days);
|
||||
for (int i = 0; i < days; i++) {
|
||||
dateList.add(startDate.plusDays(i));
|
||||
}
|
||||
return dateList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间范围
|
||||
*
|
||||
|
|
@ -335,6 +362,27 @@ 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)以来的秒数。
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
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;
|
||||
|
|
@ -39,8 +37,10 @@ public class HttpUtils {
|
|||
}
|
||||
|
||||
/**
|
||||
* 解码 URL 参数
|
||||
* 解码 URL 参数(query parameter)
|
||||
* 注意:此方法会将 + 解码为空格,适用于 query parameter,不适用于 URL path
|
||||
*
|
||||
* @see #decodeUrlPath(String)
|
||||
* @param value 参数
|
||||
* @return 解码后的参数
|
||||
*/
|
||||
|
|
@ -49,14 +49,25 @@ public class HttpUtils {
|
|||
return URLDecoder.decode(value, StandardCharsets.UTF_8.name());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
/**
|
||||
* 解码 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());
|
||||
}
|
||||
|
||||
public static String replaceUrlQuery(String url, String key, String value) {
|
||||
UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset());
|
||||
// 先移除
|
||||
TableMap<CharSequence, CharSequence> query = (TableMap<CharSequence, CharSequence>)
|
||||
ReflectUtil.getFieldValue(builder.getQuery(), "query");
|
||||
query.remove(key);
|
||||
// 后添加
|
||||
// 先移除;再添加
|
||||
builder.getQuery().remove(key);
|
||||
builder.addQuery(key, value);
|
||||
return builder.build();
|
||||
}
|
||||
|
|
@ -193,4 +204,11 @@ public class HttpUtils {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket URL 切换成 HTTP URL:ws:// → http://;wss:// → https://;其它格式原样保留
|
||||
*/
|
||||
public static String wsUrlToHttp(String url) {
|
||||
return StrUtil.startWithIgnoreCase(url, "ws") ? "http" + url.substring(2) : url;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import java.lang.reflect.Type;
|
|||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* JSON 工具类
|
||||
|
|
@ -173,6 +174,38 @@ public class JsonUtils {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 JSON 字符串成 Map,空字符串或解析失败返回 null
|
||||
*/
|
||||
public static Map<String, Object> parseMap(String text) {
|
||||
if (StrUtil.isEmpty(text)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(text, new TypeReference<Map<String, Object>>() {});
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 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<>();
|
||||
|
|
@ -217,6 +250,14 @@ public class JsonUtils {
|
|||
}
|
||||
}
|
||||
|
||||
public static String getText(JsonNode node, String fieldName) {
|
||||
if (node == null) {
|
||||
return null;
|
||||
}
|
||||
JsonNode value = node.get(fieldName);
|
||||
return value != null && !value.isNull() ? value.asText() : null;
|
||||
}
|
||||
|
||||
public static boolean isJson(String text) {
|
||||
return JSONUtil.isTypeJSON(text);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,11 @@ 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package cn.iocoder.yudao.framework.common.util.string;
|
|||
import cn.hutool.core.text.StrPool;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.pinyin.PinyinUtil;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
|
@ -78,6 +79,16 @@ public class StrUtils {
|
|||
.collect(Collectors.joining("\n"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 转小写拼音,字之间以空格分隔,便于调用方按需拼接 / 取首字母 / 拼音搜索
|
||||
*/
|
||||
public static String toPinyin(String str) {
|
||||
if (StrUtil.isBlank(str)) {
|
||||
return null;
|
||||
}
|
||||
return PinyinUtil.getPinyin(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接方法的参数
|
||||
*
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
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", "");
|
||||
// 断言:保留 key,value 为空
|
||||
assertEquals("https://www.iocoder.cn/path?a=", result);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
|
|||
import cn.iocoder.yudao.framework.ip.core.Area;
|
||||
import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum;
|
||||
import lombok.NonNull;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
|
@ -25,44 +26,46 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
|
|||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@UtilityClass
|
||||
public class AreaUtils {
|
||||
|
||||
/**
|
||||
* 初始化 SEARCHER
|
||||
*/
|
||||
@SuppressWarnings("InstantiationOfUtilityClass")
|
||||
private final static AreaUtils INSTANCE = new AreaUtils();
|
||||
|
||||
/**
|
||||
* Area 内存缓存,提升访问速度
|
||||
*/
|
||||
private static Map<Integer, Area> areas;
|
||||
|
||||
private AreaUtils() {
|
||||
long now = System.currentTimeMillis();
|
||||
areas = new HashMap<>();
|
||||
areas.put(Area.ID_GLOBAL, new Area(Area.ID_GLOBAL, "全球", 0,
|
||||
null, new ArrayList<>()));
|
||||
// 从 csv 中加载数据
|
||||
List<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);
|
||||
}
|
||||
static {
|
||||
init();
|
||||
}
|
||||
|
||||
// 构建父子关系:因为 Area 中没有 parentId 字段,所以需要重复读取
|
||||
for (CsvRow row : rows) {
|
||||
Area area = areas.get(Integer.valueOf(row.get(0))); // 自己
|
||||
Area parent = areas.get(Integer.valueOf(row.get(3))); // 父
|
||||
Assert.isTrue(area != parent, "{}:父子节点相同", area.getName());
|
||||
area.setParent(parent);
|
||||
parent.getChildren().add(area);
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
private static void init() {
|
||||
try {
|
||||
long now = System.currentTimeMillis();
|
||||
areas = new HashMap<>();
|
||||
areas.put(Area.ID_GLOBAL, new Area(Area.ID_GLOBAL, "全球", 0, null, new ArrayList<>()));
|
||||
// 从 csv 中加载数据
|
||||
List<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);
|
||||
}
|
||||
log.info("启动加载 AreaUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -3,11 +3,10 @@ package cn.iocoder.yudao.framework.ip.core.utils;
|
|||
import cn.hutool.core.io.resource.ResourceUtil;
|
||||
import cn.iocoder.yudao.framework.ip.core.Area;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.lionsoul.ip2region.xdb.Searcher;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* IP 工具类
|
||||
*
|
||||
|
|
@ -16,30 +15,29 @@ import java.io.IOException;
|
|||
* @author wanglhup
|
||||
*/
|
||||
@Slf4j
|
||||
@UtilityClass
|
||||
public class IPUtils {
|
||||
|
||||
/**
|
||||
* 初始化 SEARCHER
|
||||
*/
|
||||
@SuppressWarnings("InstantiationOfUtilityClass")
|
||||
private final static IPUtils INSTANCE = new IPUtils();
|
||||
|
||||
/**
|
||||
* IP 查询器,启动加载到内存中
|
||||
*/
|
||||
private static Searcher SEARCHER;
|
||||
|
||||
static {
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 私有化构造
|
||||
* 初始化
|
||||
*/
|
||||
private IPUtils() {
|
||||
private static void init() {
|
||||
try {
|
||||
long now = System.currentTimeMillis();
|
||||
byte[] bytes = ResourceUtil.readBytes("ip2region.xdb");
|
||||
SEARCHER = Searcher.newWithBuffer(bytes);
|
||||
log.info("启动加载 IPUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now);
|
||||
} catch (IOException e) {
|
||||
log.error("启动加载 IPUtils 失败", e);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("IPUtils 初始化失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,12 @@ 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} 的单元测试
|
||||
|
|
@ -31,6 +36,46 @@ 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())));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@
|
|||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- RPC 相关 -->
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@
|
|||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
<scope>provided</scope> <!-- 设置为 provided,只有 ExcelUtils 使用 -->
|
||||
</dependency>
|
||||
|
||||
|
|
|
|||
|
|
@ -41,8 +41,8 @@
|
|||
|
||||
<!-- 工具类相关 -->
|
||||
<dependency>
|
||||
<groupId>jakarta.validation</groupId>
|
||||
<artifactId>jakarta.validation-api</artifactId>
|
||||
<groupId>javax.validation</groupId>
|
||||
<artifactId>validation-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@
|
|||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
<scope>provided</scope> <!-- 设置为 provided,只有 TraceFilter 使用 -->
|
||||
</dependency>
|
||||
|
||||
|
|
|
|||
|
|
@ -94,6 +94,13 @@
|
|||
<groupId>com.fhs-opensource</groupId>
|
||||
<artifactId>easy-trans-mybatis-plus-extend</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Test 测试相关 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
|
|
|||
|
|
@ -119,6 +119,31 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
|
|||
return selectOne(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2).eq(field3, value3));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得满足条件的一条记录,并使用 FOR UPDATE 锁定。
|
||||
*
|
||||
* 注意:需要在事务中调用,否则锁会立即释放。
|
||||
*
|
||||
* @param queryWrapper 查询条件
|
||||
* @return 实体
|
||||
*/
|
||||
default T selectOneForUpdate(LambdaQueryWrapper<T> queryWrapper) {
|
||||
return selectOne(queryWrapper.last("FOR UPDATE"));
|
||||
}
|
||||
|
||||
default T selectOneForUpdate(SFunction<T, ?> field, Object value) {
|
||||
return selectOneForUpdate(new LambdaQueryWrapper<T>().eq(field, value));
|
||||
}
|
||||
|
||||
default T selectOneForUpdate(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2) {
|
||||
return selectOneForUpdate(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2));
|
||||
}
|
||||
|
||||
default T selectOneForUpdate(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2,
|
||||
SFunction<T, ?> field3, Object value3) {
|
||||
return selectOneForUpdate(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2).eq(field3, value3));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取满足条件的第 1 条记录
|
||||
*
|
||||
|
|
@ -145,6 +170,14 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
|
|||
return CollUtil.getFirst(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取满足条件的最新一条记录
|
||||
* <p>
|
||||
* 目的:解决并发场景下,插入多条记录后,使用 selectOne 会报错的问题
|
||||
*/
|
||||
default T selectLastOne(LambdaQueryWrapper<T> queryWrapper) {
|
||||
return CollUtil.getLast(selectList(queryWrapper));
|
||||
}
|
||||
|
||||
default Long selectCount() {
|
||||
return selectCount(new QueryWrapper<>());
|
||||
|
|
|
|||
|
|
@ -24,6 +24,12 @@ public class LambdaQueryWrapperX<T> extends LambdaQueryWrapper<T> {
|
|||
}
|
||||
return this;
|
||||
}
|
||||
public LambdaQueryWrapperX<T> likeRightIfPresent(SFunction<T, ?> column, String val) {
|
||||
if (StringUtils.hasText(val)) {
|
||||
return (LambdaQueryWrapperX<T>) super.likeRight(column, val);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public LambdaQueryWrapperX<T> inIfPresent(SFunction<T, ?> column, Collection<?> values) {
|
||||
if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,13 @@ public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
|
|||
return this;
|
||||
}
|
||||
|
||||
public <S> MPJLambdaWrapperX<T> likeRightIfPresent(SFunction<S, ?> column, String val) {
|
||||
if (StringUtils.hasText(val)) {
|
||||
return (MPJLambdaWrapperX<T>) super.likeRight(column, val);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public <S> MPJLambdaWrapperX<T> inIfPresent(SFunction<S, ?> column, Collection<?> values) {
|
||||
if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {
|
||||
return (MPJLambdaWrapperX<T>) super.in(column, values);
|
||||
|
|
@ -102,7 +109,6 @@ public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
|
|||
return this;
|
||||
}
|
||||
|
||||
|
||||
// ========== 重写父类方法,方便链式调用 ==========
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -25,6 +25,13 @@ public class QueryWrapperX<T> extends QueryWrapper<T> {
|
|||
return this;
|
||||
}
|
||||
|
||||
public QueryWrapperX<T> likeRightIfPresent(String column, String val) {
|
||||
if (StringUtils.hasText(val)) {
|
||||
return (QueryWrapperX<T>) super.likeRight(column, val);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryWrapperX<T> inIfPresent(String column, Collection<?> values) {
|
||||
if (!CollectionUtils.isEmpty(values)) {
|
||||
return (QueryWrapperX<T>) super.in(column, values);
|
||||
|
|
@ -95,13 +102,13 @@ public class QueryWrapperX<T> extends QueryWrapper<T> {
|
|||
}
|
||||
|
||||
public QueryWrapperX<T> betweenIfPresent(String column, Object[] values) {
|
||||
if (values!= null && values.length != 0 && values[0] != null && values[1] != null) {
|
||||
if (values != null && values.length != 0 && values[0] != null && values[1] != null) {
|
||||
return (QueryWrapperX<T>) super.between(column, values[0], values[1]);
|
||||
}
|
||||
if (values!= null && values.length != 0 && values[0] != null) {
|
||||
if (values != null && values.length != 0 && values[0] != null) {
|
||||
return (QueryWrapperX<T>) ge(column, values[0]);
|
||||
}
|
||||
if (values!= null && values.length != 0 && values[1] != null) {
|
||||
if (values != null && values.length != 0 && values[1] != null) {
|
||||
return (QueryWrapperX<T>) le(column, values[1]);
|
||||
}
|
||||
return this;
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import net.sf.jsqlparser.schema.Table;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* MyBatis 工具类
|
||||
|
|
@ -31,6 +32,8 @@ public class MyBatisUtils {
|
|||
|
||||
private static final String MYSQL_ESCAPE_CHARACTER = "`";
|
||||
|
||||
private static final Pattern SAFE_COLUMN_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_]+(\\.[a-zA-Z0-9_]+)*$");
|
||||
|
||||
public static <T> Page<T> buildPage(PageParam pageParam) {
|
||||
return buildPage(pageParam, null);
|
||||
}
|
||||
|
|
@ -42,8 +45,11 @@ public class MyBatisUtils {
|
|||
// 排序字段
|
||||
if (CollUtil.isNotEmpty(sortingFields)) {
|
||||
for (SortingField sortingField : sortingFields) {
|
||||
page.addOrder(new OrderItem().setAsc(SortingField.ORDER_ASC.equals(sortingField.getOrder()))
|
||||
.setColumn(StrUtil.toUnderlineCase(sortingField.getField())));
|
||||
String columnName = buildSafeOrderColumn(sortingField.getField());
|
||||
if (columnName == null) {
|
||||
continue;
|
||||
}
|
||||
page.addOrder(new OrderItem().setAsc(isAscOrder(sortingField.getOrder())).setColumn(columnName));
|
||||
}
|
||||
}
|
||||
return page;
|
||||
|
|
@ -57,23 +63,29 @@ public class MyBatisUtils {
|
|||
if (wrapper instanceof QueryWrapper) {
|
||||
QueryWrapper<T> query = (QueryWrapper<T>) wrapper;
|
||||
for (SortingField sortingField : sortingFields) {
|
||||
query.orderBy(true,
|
||||
SortingField.ORDER_ASC.equals(sortingField.getOrder()),
|
||||
StrUtil.toUnderlineCase(sortingField.getField()));
|
||||
String columnName = buildSafeOrderColumn(sortingField.getField());
|
||||
if (columnName == null) {
|
||||
continue;
|
||||
}
|
||||
query.orderBy(true, isAscOrder(sortingField.getOrder()), columnName);
|
||||
}
|
||||
} else if (wrapper instanceof LambdaQueryWrapper) {
|
||||
// LambdaQueryWrapper 不直接支持字符串字段排序,使用 last 方法拼接 ORDER BY
|
||||
LambdaQueryWrapper<T> lambdaQuery = (LambdaQueryWrapper<T>) wrapper;
|
||||
StringBuilder orderBy = new StringBuilder();
|
||||
for (SortingField sortingField : sortingFields) {
|
||||
String columnName = buildSafeOrderColumn(sortingField.getField());
|
||||
if (columnName == null) {
|
||||
continue;
|
||||
}
|
||||
if (StrUtil.isNotEmpty(orderBy)) {
|
||||
orderBy.append(", ");
|
||||
}
|
||||
orderBy.append(StrUtil.toUnderlineCase(sortingField.getField()))
|
||||
.append(" ")
|
||||
.append(SortingField.ORDER_ASC.equals(sortingField.getOrder()) ? "ASC" : "DESC");
|
||||
orderBy.append(columnName).append(" ").append(getOrderDirection(sortingField.getOrder()));
|
||||
}
|
||||
if (StrUtil.isNotEmpty(orderBy)) {
|
||||
lambdaQuery.last("ORDER BY " + orderBy);
|
||||
}
|
||||
lambdaQuery.last("ORDER BY " + orderBy);
|
||||
// 另外个思路:https://blog.csdn.net/m0_59084856/article/details/138450913
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported wrapper type: " + wrapper.getClass().getName());
|
||||
|
|
@ -81,6 +93,22 @@ public class MyBatisUtils {
|
|||
|
||||
}
|
||||
|
||||
public static boolean isAscOrder(String order) {
|
||||
return SortingField.ORDER_ASC.equals(order);
|
||||
}
|
||||
|
||||
public static String getOrderDirection(String order) {
|
||||
return isAscOrder(order) ? "ASC" : "DESC";
|
||||
}
|
||||
|
||||
private static String buildSafeOrderColumn(String field) {
|
||||
String columnName = StrUtil.toUnderlineCase(field);
|
||||
if (StrUtil.isEmpty(columnName) || !SAFE_COLUMN_NAME_PATTERN.matcher(columnName).matches()) {
|
||||
return null;
|
||||
}
|
||||
return columnName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将拦截器添加到链中
|
||||
* 由于 MybatisPlusInterceptor 不支持添加拦截器,所以只能全量设置
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
package cn.iocoder.yudao.framework.mybatis.core.util;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import cn.iocoder.yudao.framework.common.pojo.SortingField;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* {@link MyBatisUtils} 的单元测试
|
||||
*/
|
||||
public class MyBatisUtilsTest {
|
||||
|
||||
@Test
|
||||
public void testBuildPage_sortingFields() {
|
||||
// 准备参数
|
||||
PageParam pageParam = new PageParam();
|
||||
pageParam.setPageNo(2);
|
||||
pageParam.setPageSize(20);
|
||||
List<SortingField> sortingFields = Arrays.asList(
|
||||
new SortingField("userName", SortingField.ORDER_ASC),
|
||||
new SortingField("u.id", SortingField.ORDER_DESC),
|
||||
new SortingField("name desc", SortingField.ORDER_DESC));
|
||||
|
||||
// 调用
|
||||
Page<Object> page = MyBatisUtils.buildPage(pageParam, sortingFields);
|
||||
|
||||
// 断言
|
||||
assertEquals(2, page.getCurrent());
|
||||
assertEquals(20, page.getSize());
|
||||
assertEquals(2, page.orders().size());
|
||||
assertOrderItem(page.orders().get(0), "user_name", true);
|
||||
assertOrderItem(page.orders().get(1), "u.id", false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddOrder_queryWrapper() {
|
||||
// 准备参数
|
||||
QueryWrapper<Object> query = new QueryWrapper<>();
|
||||
List<SortingField> sortingFields = Arrays.asList(
|
||||
new SortingField("userName", SortingField.ORDER_ASC),
|
||||
new SortingField("u.id", SortingField.ORDER_DESC),
|
||||
new SortingField("name;drop", SortingField.ORDER_ASC));
|
||||
|
||||
// 调用
|
||||
MyBatisUtils.addOrder(query, sortingFields);
|
||||
|
||||
// 断言
|
||||
assertEquals(" ORDER BY user_name ASC,u.id DESC", query.getSqlSegment());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddOrder_lambdaQueryWrapper() {
|
||||
// 准备参数
|
||||
LambdaQueryWrapper<Object> query = new LambdaQueryWrapper<>();
|
||||
List<SortingField> sortingFields = Arrays.asList(
|
||||
new SortingField("userName", SortingField.ORDER_ASC),
|
||||
new SortingField("u.id", SortingField.ORDER_DESC),
|
||||
new SortingField("name`", SortingField.ORDER_ASC));
|
||||
|
||||
// 调用
|
||||
MyBatisUtils.addOrder(query, sortingFields);
|
||||
|
||||
// 断言
|
||||
assertEquals(" ORDER BY user_name ASC, u.id DESC", query.getSqlSegment());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddOrder_lambdaQueryWrapper_invalidSortingFields() {
|
||||
// 准备参数
|
||||
LambdaQueryWrapper<Object> query = new LambdaQueryWrapper<>();
|
||||
List<SortingField> sortingFields = Arrays.asList(
|
||||
new SortingField("name desc", SortingField.ORDER_ASC),
|
||||
new SortingField("name;drop", SortingField.ORDER_DESC));
|
||||
|
||||
// 调用
|
||||
MyBatisUtils.addOrder(query, sortingFields);
|
||||
|
||||
// 断言
|
||||
assertEquals("", query.getSqlSegment());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOrderDirection() {
|
||||
assertTrue(MyBatisUtils.isAscOrder(SortingField.ORDER_ASC));
|
||||
assertFalse(MyBatisUtils.isAscOrder(SortingField.ORDER_DESC));
|
||||
assertEquals("ASC", MyBatisUtils.getOrderDirection(SortingField.ORDER_ASC));
|
||||
assertEquals("DESC", MyBatisUtils.getOrderDirection(SortingField.ORDER_DESC));
|
||||
assertEquals("DESC", MyBatisUtils.getOrderDirection(null));
|
||||
}
|
||||
|
||||
private void assertOrderItem(OrderItem orderItem, String column, boolean asc) {
|
||||
assertEquals(column, orderItem.getColumn());
|
||||
assertEquals(asc, orderItem.isAsc());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -36,11 +36,22 @@
|
|||
<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.x(httpcore),导致 ClassNotFoundException。
|
||||
临时解决:显式引入 httpclient 4.x。待 WxJava 修复后移除。
|
||||
-->
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<version>4.5.14</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 工具相关 -->
|
||||
<dependency>
|
||||
<groupId>jakarta.validation</groupId>
|
||||
<artifactId>jakarta.validation-api</artifactId>
|
||||
<groupId>javax.validation</groupId>
|
||||
<artifactId>validation-api</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ 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;
|
||||
|
|
@ -52,6 +53,10 @@ 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());
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ public class ApiAccessLogFilter extends ApiRequestFilter {
|
|||
LocalDateTime beginTime = LocalDateTime.now();
|
||||
// 提前获得参数,避免 XssFilter 过滤处理
|
||||
Map<String, String> queryString = ServletUtils.getParamMap(request);
|
||||
String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : null;
|
||||
String requestBody = ServletUtils.getBody(request);
|
||||
|
||||
try {
|
||||
// 继续过滤器
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ public class ApiAccessLogInterceptor implements HandlerInterceptor {
|
|||
// 打印 request 日志
|
||||
if (!SpringUtils.isProd()) {
|
||||
Map<String, String> queryString = ServletUtils.getParamMap(request);
|
||||
String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : null;
|
||||
String requestBody = ServletUtils.getBody(request);
|
||||
if (CollUtil.isEmpty(queryString) && StrUtil.isEmpty(requestBody)) {
|
||||
log.info("[preHandle][开始请求 URL({}) 无参数]", request.getRequestURI());
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -37,8 +37,14 @@ public class BannerApplicationRunner implements ApplicationRunner {
|
|||
System.out.println("[商城系统 yudao-module-mall 教程][参考 https://cloud.iocoder.cn/mall/build/ 开启]");
|
||||
// ERP 系统
|
||||
System.out.println("[ERP 系统 yudao-module-erp - 教程][参考 https://cloud.iocoder.cn/erp/build/ 开启]");
|
||||
// WMS 仓库管理系统
|
||||
System.out.println("[WMS 仓库管理系统 yudao-module-wms - 教程][参考 https://cloud.iocoder.cn/wms/build/ 开启]");
|
||||
// CRM 系统
|
||||
System.out.println("[CRM 系统 yudao-module-crm - 教程][参考 https://cloud.iocoder.cn/crm/build/ 开启]");
|
||||
// MES 系统
|
||||
System.out.println("[MES 系统 yudao-module-mes - 教程][参考 https://cloud.iocoder.cn/mes/build/ 开启]");
|
||||
// IM 即时通讯
|
||||
System.out.println("[IM 即时通讯 yudao-module-im - 教程][参考 https://cloud.iocoder.cn/im/build/ 开启]");
|
||||
// 微信公众号
|
||||
System.out.println("[微信公众号 yudao-module-mp 教程][参考 https://cloud.iocoder.cn/mp/build/ 开启]");
|
||||
// 支付平台
|
||||
|
|
|
|||
|
|
@ -410,25 +410,43 @@ public class GlobalExceptionHandler {
|
|||
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
||||
"[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://cloud.iocoder.cn/erp/build/ 开启]");
|
||||
}
|
||||
// 6. CRM 系统
|
||||
// 6. WMS 仓库管理系统
|
||||
if (message.contains("wms_")) {
|
||||
log.error("[WMS 仓库管理系统 yudao-module-wms - 表结构未导入][参考 https://cloud.iocoder.cn/wms/build/ 开启]");
|
||||
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
||||
"[WMS 仓库管理系统 yudao-module-wms - 表结构未导入][参考 https://cloud.iocoder.cn/wms/build/ 开启]");
|
||||
}
|
||||
// 7. CRM 系统
|
||||
if (message.contains("crm_")) {
|
||||
log.error("[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]");
|
||||
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
||||
"[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]");
|
||||
}
|
||||
// 7. 支付平台
|
||||
// 8. MES 系统
|
||||
if (message.contains("mes_")) {
|
||||
log.error("[MES 系统 yudao-module-mes - 表结构未导入][参考 https://cloud.iocoder.cn/mes/build/ 开启]");
|
||||
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
||||
"[MES 系统 yudao-module-mes - 表结构未导入][参考 https://cloud.iocoder.cn/mes/build/ 开启]");
|
||||
}
|
||||
// 9. IM 即时通讯
|
||||
if (message.contains("im_")) {
|
||||
log.error("[IM 即时通讯 yudao-module-im - 表结构未导入][参考 https://cloud.iocoder.cn/im/build/ 开启]");
|
||||
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
||||
"[IM 即时通讯 yudao-module-im - 表结构未导入][参考 https://cloud.iocoder.cn/im/build/ 开启]");
|
||||
}
|
||||
// 10. 支付平台
|
||||
if (message.contains("pay_")) {
|
||||
log.error("[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]");
|
||||
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
||||
"[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]");
|
||||
}
|
||||
// 8. AI 大模型
|
||||
// 11. AI 大模型
|
||||
if (message.contains("ai_")) {
|
||||
log.error("[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]");
|
||||
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
||||
"[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]");
|
||||
}
|
||||
// 9. IoT 物联网
|
||||
// 12. IoT 物联网
|
||||
if (message.contains("iot_")) {
|
||||
log.error("[IoT 物联网 yudao-module-iot - 表结构未导入][参考 https://doc.iocoder.cn/iot/build/ 开启]");
|
||||
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
||||
|
|
|
|||
|
|
@ -40,11 +40,6 @@
|
|||
<artifactId>javax.servlet-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.xiaoymin</groupId> <!-- 接口文档 -->
|
||||
<artifactId>knife4j-gateway-spring-boot-starter</artifactId>
|
||||
|
|
|
|||
|
|
@ -39,8 +39,14 @@ public class BannerApplicationRunner implements ApplicationRunner {
|
|||
System.out.println("[商城系统 yudao-module-mall 教程][参考 https://cloud.iocoder.cn/mall/build/ 开启]");
|
||||
// ERP 系统
|
||||
System.out.println("[ERP 系统 yudao-module-erp - 教程][参考 https://cloud.iocoder.cn/erp/build/ 开启]");
|
||||
// WMS 仓库管理系统
|
||||
System.out.println("[WMS 仓库管理系统 yudao-module-wms - 教程][参考 https://cloud.iocoder.cn/wms/build/ 开启]");
|
||||
// CRM 系统
|
||||
System.out.println("[CRM 系统 yudao-module-crm - 教程][参考 https://cloud.iocoder.cn/crm/build/ 开启]");
|
||||
// MES 系统
|
||||
System.out.println("[MES 系统 yudao-module-mes - 教程][参考 https://cloud.iocoder.cn/mes/build/ 开启]");
|
||||
// IM 即时通讯
|
||||
System.out.println("[IM 即时通讯 yudao-module-im - 教程][参考 https://cloud.iocoder.cn/im/build/ 开启]");
|
||||
// 微信公众号
|
||||
System.out.println("[微信公众号 yudao-module-mp 教程][参考 https://cloud.iocoder.cn/mp/build/ 开启]");
|
||||
// 支付平台
|
||||
|
|
|
|||
|
|
@ -192,6 +192,27 @@ 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
|
||||
## wms-server 服务
|
||||
- id: wms-admin-api # 路由的编号
|
||||
uri: grayLb://wms-server
|
||||
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
|
||||
- Path=/admin-api/wms/**
|
||||
filters:
|
||||
- RewritePath=/admin-api/wms/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs
|
||||
## im-server 服务
|
||||
- id: im-admin-api # 路由的编号
|
||||
uri: grayLb://im-server
|
||||
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
|
||||
- Path=/admin-api/im/**
|
||||
filters:
|
||||
- RewritePath=/admin-api/im/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs
|
||||
x-forwarded:
|
||||
prefix-enabled: false # 避免 Swagger 重复带上额外的 /admin-api/system 前缀
|
||||
default-filters: # 全局过滤器,对应 GatewayFilterDefinition 数组
|
||||
|
|
@ -251,9 +272,18 @@ 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
|
||||
- name: wms-server
|
||||
service-name: wms-server
|
||||
url: /admin-api/wms/v3/api-docs
|
||||
- name: im-server
|
||||
service-name: im-server
|
||||
url: /admin-api/im/v3/api-docs
|
||||
|
||||
--- #################### 芋道相关配置 ####################
|
||||
|
||||
yudao:
|
||||
info:
|
||||
version: 1.0.0
|
||||
version: 1.0.0
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@
|
|||
国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno
|
||||
</description>
|
||||
<properties>
|
||||
<spring-ai.version>1.1.2</spring-ai.version>
|
||||
<spring-ai.version>1.1.5</spring-ai.version>
|
||||
<!-- https://mvnrepository.com/artifact/com.alibaba.cloud.ai/spring-ai-alibaba -->
|
||||
<alibaba-ai.version>1.1.0.0-RC2</alibaba-ai.version>
|
||||
<alibaba-ai.version>1.1.2.2</alibaba-ai.version>
|
||||
<tinyflow.version>1.2.6</tinyflow.version>
|
||||
</properties>
|
||||
|
||||
|
|
|
|||
|
|
@ -83,6 +83,15 @@ 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({
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ public class AiImageServiceImpl implements AiImageService {
|
|||
}
|
||||
|
||||
@Async
|
||||
@SuppressWarnings("ConstantValue")
|
||||
public void executeDrawImage(AiImageDO image, AiImageDrawReqVO reqVO, AiModelDO model) {
|
||||
try {
|
||||
// 1.1 构建请求
|
||||
|
|
@ -164,8 +165,8 @@ public class AiImageServiceImpl implements AiImageService {
|
|||
.build();
|
||||
} else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.TONG_YI.getPlatform())) {
|
||||
return DashScopeImageOptions.builder()
|
||||
.withModel(model.getModel()).withN(1)
|
||||
.withHeight(draw.getHeight()).withWidth(draw.getWidth())
|
||||
.model(model.getModel()).n(1)
|
||||
.height(draw.getHeight()).width(draw.getWidth())
|
||||
.build();
|
||||
} else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.YI_YAN.getPlatform())) {
|
||||
return QianFanImageOptions.builder()
|
||||
|
|
|
|||
|
|
@ -98,6 +98,13 @@ public interface AiKnowledgeSegmentService {
|
|||
*/
|
||||
void updateKnowledgeSegmentStatus(AiKnowledgeSegmentUpdateStatusReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 删除知识库段落
|
||||
*
|
||||
* @param id 段落编号
|
||||
*/
|
||||
void deleteKnowledgeSegment(Long id);
|
||||
|
||||
/**
|
||||
* 重新索引知识库下的所有文档段落
|
||||
*
|
||||
|
|
|
|||
|
|
@ -141,6 +141,19 @@ 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. 查询需要删除的段落
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
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;
|
||||
|
|
@ -33,6 +35,28 @@ 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);
|
||||
}
|
||||
|
|
@ -44,9 +68,10 @@ public class AiUtils {
|
|||
// noinspection EnhancedSwitchMigration
|
||||
switch (platform) {
|
||||
case TONG_YI:
|
||||
return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens)
|
||||
.withEnableThinking(true) // TODO 芋艿:默认都开启 thinking 模式,后续可以让用户配置
|
||||
.withToolCallbacks(toolCallbacks).withToolContext(toolContext).build();
|
||||
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();
|
||||
case YI_YAN:
|
||||
return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build();
|
||||
case DEEP_SEEK:
|
||||
|
|
@ -125,10 +150,13 @@ public class AiUtils {
|
|||
|| response.getResult().getOutput() == null) {
|
||||
return null;
|
||||
}
|
||||
if (response.getResult().getOutput() instanceof DeepSeekAssistantMessage) {
|
||||
return ((DeepSeekAssistantMessage) (response.getResult().getOutput())).getReasoningContent();
|
||||
AssistantMessage output = response.getResult().getOutput();
|
||||
// DeepSeek 通过专属 AssistantMessage 暴露 reasoningContent
|
||||
if (output instanceof DeepSeekAssistantMessage) {
|
||||
return ((DeepSeekAssistantMessage) output).getReasoningContent();
|
||||
}
|
||||
return null;
|
||||
// 通义千问等通过 metadata 透传 reasoningContent
|
||||
return MapUtil.getStr(output.getMetadata(), "reasoningContent");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -103,7 +103,7 @@ xxl:
|
|||
# Lock4j 配置项
|
||||
lock4j:
|
||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ xxl:
|
|||
# Lock4j 配置项
|
||||
lock4j:
|
||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
|
|
|
|||
|
|
@ -34,15 +34,15 @@ public class TongYiChatModelTests {
|
|||
|
||||
private final DashScopeChatModel chatModel = DashScopeChatModel.builder()
|
||||
.dashScopeApi(DashScopeApi.builder()
|
||||
.apiKey("sk-47aa124781be4bfb95244cc62f63f7d0")
|
||||
.apiKey("sk-cd9f39e99ea54840bd1888282325f55a") // https://bailian.console.aliyun.com/cn-beijing/?tab=model#/api-key 获取密钥
|
||||
.build())
|
||||
.defaultOptions(DashScopeChatOptions.builder()
|
||||
// .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)
|
||||
.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)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
|
|
@ -85,9 +85,9 @@ public class TongYiChatModelTests {
|
|||
List<Message> messages = new ArrayList<>();
|
||||
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
|
||||
DashScopeChatOptions options = DashScopeChatOptions.builder()
|
||||
.withModel("qwen3-235b-a22b-thinking-2507")
|
||||
.model("qwen3.6-plus").multiModel(true)
|
||||
// .withModel("qwen-max-2025-01-25")
|
||||
.withEnableThinking(true) // 必须设置,否则会报错
|
||||
.enableThinking(true) // 必须设置,否则会报错
|
||||
.build();
|
||||
|
||||
// 调用
|
||||
|
|
@ -112,8 +112,8 @@ public class TongYiChatModelTests {
|
|||
Document document01 = new Document("abc");
|
||||
Document document02 = new Document("sapring");
|
||||
RerankOptions options = DashScopeRerankOptions.builder()
|
||||
.withTopN(1)
|
||||
.withModel("gte-rerank-v2")
|
||||
.topN(1)
|
||||
.model("gte-rerank-v2")
|
||||
.build();
|
||||
RerankRequest rerankRequest = new RerankRequest(
|
||||
query,
|
||||
|
|
|
|||
|
|
@ -12,23 +12,34 @@ import org.springframework.ai.image.ImageResponse;
|
|||
/**
|
||||
* {@link DashScopeImageModel} 集成测试类
|
||||
*
|
||||
* TODO @芋艿:注:spring-ai-alibaba-dashscope(1.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-47aa124781be4bfb95244cc62f63f7d0")
|
||||
.apiKey("sk-cd9f39e99ea54840bd1888282325f55a") // https://bailian.console.aliyun.com/cn-beijing/?tab=model#/api-key 获取密钥
|
||||
.build())
|
||||
.build();
|
||||
|
||||
// TODO @芋艿:
|
||||
@Test
|
||||
@Disabled
|
||||
public void imageCallTest() {
|
||||
// 准备参数
|
||||
ImageOptions options = DashScopeImageOptions.builder()
|
||||
.withModel("wanx-v1")
|
||||
.withHeight(256).withWidth(256)
|
||||
.model("wan2.7-image")
|
||||
// .withSize("2k")
|
||||
.height(768).width(768)
|
||||
.n(1)
|
||||
.build();
|
||||
ImagePrompt prompt = new ImagePrompt("中国长城!", options);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form;
|
|||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 流程表单字段 VO
|
||||
*/
|
||||
|
|
@ -20,5 +22,9 @@ public class BpmFormFieldVO {
|
|||
* 字段标题
|
||||
*/
|
||||
private String title;
|
||||
/**
|
||||
* 子表单字段(处理布局组件)
|
||||
*/
|
||||
private List<BpmFormFieldVO> children;
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(), Long.parseLong(copy.getCreator()))));
|
||||
copy -> Stream.of(copy.getStartUserId(), copy.getUserId())));
|
||||
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, Long.valueOf(copy.getCreator()),
|
||||
user -> copyVO.setStartUser(BeanUtils.toBean(user, UserSimpleBaseVO.class)));
|
||||
MapUtils.findAndThen(userMap, copy.getStartUserId(),
|
||||
MapUtils.findAndThen(userMap, copy.getUserId(),
|
||||
user -> copyVO.setCreateUser(BeanUtils.toBean(user, UserSimpleBaseVO.class)));
|
||||
MapUtils.findAndThen(userMap, copy.getStartUserId(),
|
||||
user -> copyVO.setStartUser(BeanUtils.toBean(user, UserSimpleBaseVO.class)));
|
||||
MapUtils.findAndThen(processInstanceMap, copyVO.getProcessInstanceId(),
|
||||
processInstance -> {
|
||||
copyVO.setSummary(FlowableUtils.getSummary(
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ public class BpmOALeaveDO extends BaseDO {
|
|||
/**
|
||||
* 请假类型
|
||||
*/
|
||||
private String type;
|
||||
private Integer type;
|
||||
/**
|
||||
* 原因
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import org.flowable.engine.impl.bpmn.behavior.AbstractBpmnActivityBehavior;
|
|||
import org.flowable.engine.impl.bpmn.behavior.SequentialMultiInstanceBehavior;
|
||||
import org.flowable.engine.impl.persistence.entity.ExecutionEntity;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
|
|
@ -53,7 +54,7 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB
|
|||
@SuppressWarnings("unchecked")
|
||||
Set<Long> assigneeUserIds = (Set<Long>) execution.getVariableLocal(super.collectionVariable, Set.class);
|
||||
if (assigneeUserIds == null) {
|
||||
assigneeUserIds = taskCandidateInvoker.calculateUsersByTask(execution);
|
||||
assigneeUserIds = new LinkedHashSet<>(taskCandidateInvoker.calculateUsersByTask(execution));
|
||||
if (CollUtil.isEmpty(assigneeUserIds)) {
|
||||
// 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过!
|
||||
// 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import org.springframework.stereotype.Component;
|
|||
/**
|
||||
* 根据流程变量 variable 的类型,转换参数的值
|
||||
*
|
||||
* 目前用于 ConditionNodeConvert 的 buildConditionExpression 方法中
|
||||
*
|
||||
* @deprecated 已无调用方
|
||||
* @author jason
|
||||
*/
|
||||
@Deprecated // TODO @芋艿:兼容老版本,预计 27 年删除;
|
||||
@Component
|
||||
public class VariableConvertByTypeExpressionFunction extends AbstractFlowableVariableExpressionFunction {
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ 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;
|
||||
|
|
@ -11,6 +12,7 @@ import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormFi
|
|||
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
|
||||
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
|
||||
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import lombok.SneakyThrows;
|
||||
import org.flowable.common.engine.api.delegate.Expression;
|
||||
import org.flowable.common.engine.api.variable.VariableContainer;
|
||||
|
|
@ -26,6 +28,7 @@ import org.flowable.engine.runtime.ProcessInstance;
|
|||
import org.flowable.task.api.TaskInfo;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
|
@ -244,12 +247,10 @@ public class FlowableUtils {
|
|||
}
|
||||
|
||||
// 解析表单配置
|
||||
Map<String, BpmFormFieldVO> formFieldsMap = new HashMap<>();
|
||||
Map<String, BpmFormFieldVO> formFieldsMap = new LinkedHashMap<>();
|
||||
processDefinitionInfo.getFormFields().forEach(formFieldStr -> {
|
||||
BpmFormFieldVO formField = JsonUtils.parseObject(formFieldStr, BpmFormFieldVO.class);
|
||||
if (formField != null) {
|
||||
formFieldsMap.put(formField.getField(), formField);
|
||||
}
|
||||
JsonNode formFieldNode = JsonUtils.parseObject(formFieldStr, JsonNode.class);
|
||||
parseFormField(formFieldNode, formFieldsMap);
|
||||
});
|
||||
|
||||
// 情况一:当自定义了摘要
|
||||
|
|
@ -273,6 +274,40 @@ public class FlowableUtils {
|
|||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归解析表单字段
|
||||
*/
|
||||
private static void parseFormField(JsonNode formFieldNode, Map<String, BpmFormFieldVO> formFieldsMap) {
|
||||
if (formFieldNode == null || !formFieldNode.isObject()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果 children 里存在对象节点,说明是布局组件;字符串节点是分割线、标签、文字等展示组件内容,直接跳过。
|
||||
JsonNode children = formFieldNode.get("children");
|
||||
if (children != null && children.isArray() && children.size() > 0) {
|
||||
boolean hasObjectChild = false;
|
||||
for (JsonNode child : children) {
|
||||
if (!child.isObject()) {
|
||||
continue;
|
||||
}
|
||||
hasObjectChild = true;
|
||||
parseFormField(child, formFieldsMap);
|
||||
}
|
||||
if (hasObjectChild) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 真实字段才加入 map
|
||||
BpmFormFieldVO formField = new BpmFormFieldVO()
|
||||
.setType(JsonUtils.getText(formFieldNode, "type"))
|
||||
.setField(JsonUtils.getText(formFieldNode, "field"))
|
||||
.setTitle(JsonUtils.getText(formFieldNode, "title"));
|
||||
if (StrUtil.isNotBlank(formField.getField())) {
|
||||
formFieldsMap.put(formField.getField(), formField);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Task 相关的工具方法 ==========
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -707,10 +707,9 @@ public class SimpleModelUtils {
|
|||
List<String> list = convertList(item.getRules(), (rule) -> {
|
||||
String rightSide = NumberUtil.isNumber(rule.getRightSide()) ? rule.getRightSide()
|
||||
: "\"" + rule.getRightSide() + "\""; // 如果非数值类型加引号
|
||||
return String.format(" vars:getOrDefault(%s, null) %s var:convertByType(%s,%s) ",
|
||||
return String.format(BpmConditionOpCodeEnum.fromCode(rule.getOpCode()).getSymbol(),
|
||||
rule.getLeftSide(), // 左侧:读取变量
|
||||
rule.getOpCode(), // 中间:操作符,比较
|
||||
rule.getLeftSide(), rightSide); // 右侧:转换变量,VariableConvertByTypeExpressionFunction
|
||||
rightSide); // 右侧:取值变量
|
||||
});
|
||||
// 构造条件组的表达式
|
||||
Boolean and = item.getAnd();
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
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;
|
||||
|
||||
}
|
||||
|
|
@ -87,7 +87,7 @@ xxl:
|
|||
# Lock4j 配置项
|
||||
lock4j:
|
||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ xxl:
|
|||
# Lock4j 配置项
|
||||
lock4j:
|
||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ public class BpmTaskCandidateInvokerTest extends BaseMockitoUnitTest {
|
|||
when(adminUserApi.getUserMap(eq(asSet(1L, 2L)))).thenReturn(userMap);
|
||||
// mock 方法(empty)
|
||||
when(emptyStrategy.calculateUsersByActivity(same(bpmnModel), eq(activityId),
|
||||
eq(param), same(startUserId), same(processDefinitionId), same(processVariables)))
|
||||
eq(param), same(startUserId), same(processDefinitionId), same(processVariables)))
|
||||
.thenReturn(Sets.newSet(2L));
|
||||
|
||||
// 调用
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import org.mockito.InjectMocks;
|
|||
import org.mockito.Mock;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,167 @@
|
|||
package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.core.KeyValue;
|
||||
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
|
||||
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
|
||||
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
/**
|
||||
* {@link FlowableUtils} 的单元测试。
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
class FlowableUtilsTest {
|
||||
|
||||
@Test
|
||||
public void testGetSummary_customSummary_parseDbFormFields() {
|
||||
// 准备参数:模拟 DB 中 form_fields 字段,列表里每个元素都是一个 form-create 字段 JSON。
|
||||
BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionInfo(dbFormFields(),
|
||||
summarySetting(true, "reason", "days", "notExists", "startTime"));
|
||||
Map<String, Object> processVariables = processVariables();
|
||||
|
||||
// 调用
|
||||
List<KeyValue<String, String>> summary = FlowableUtils.getSummary(processDefinitionInfo, processVariables);
|
||||
|
||||
// 断言
|
||||
assertEquals(Arrays.asList(
|
||||
new KeyValue<>("请假原因", "事假"),
|
||||
new KeyValue<>("请假天数", "3"),
|
||||
new KeyValue<>("开始时间", "2026-05-31 09:00:00")),
|
||||
summary);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSummary_defaultSummary_parseFirstThreeFieldsByFormOrder() {
|
||||
// 准备参数:未开启自定义摘要时,默认取表单配置顺序里的前三个真实字段。
|
||||
BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionInfo(dbFormFields(), null);
|
||||
Map<String, Object> processVariables = processVariables();
|
||||
|
||||
// 调用
|
||||
List<KeyValue<String, String>> summary = FlowableUtils.getSummary(processDefinitionInfo, processVariables);
|
||||
|
||||
// 断言
|
||||
assertEquals(Arrays.asList(
|
||||
new KeyValue<>("请假原因", "事假"),
|
||||
new KeyValue<>("开始时间", "2026-05-31 09:00:00"),
|
||||
new KeyValue<>("请假天数", "3")),
|
||||
summary);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSummary_summaryDisabled_useDefaultSummary() {
|
||||
// 准备参数:摘要设置存在但未启用时,仍走默认摘要逻辑。
|
||||
BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionInfo(dbFormFields(),
|
||||
summarySetting(false, "remark"));
|
||||
Map<String, Object> processVariables = processVariables();
|
||||
|
||||
// 调用
|
||||
List<KeyValue<String, String>> summary = FlowableUtils.getSummary(processDefinitionInfo, processVariables);
|
||||
|
||||
// 断言
|
||||
assertEquals(Arrays.asList(
|
||||
new KeyValue<>("请假原因", "事假"),
|
||||
new KeyValue<>("开始时间", "2026-05-31 09:00:00"),
|
||||
new KeyValue<>("请假天数", "3")),
|
||||
summary);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSummary_displayComponentsOnly_returnEmpty() {
|
||||
// 准备参数:分割线、标签、文字等展示组件的 children 是字符串数组,不是表单字段对象。
|
||||
BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionInfo(Arrays.asList(
|
||||
DIVIDER_FIELD,
|
||||
TEXT_FIELD,
|
||||
TAG_FIELD), null);
|
||||
|
||||
// 调用
|
||||
List<KeyValue<String, String>> summary = FlowableUtils.getSummary(processDefinitionInfo,
|
||||
Collections.emptyMap());
|
||||
|
||||
// 断言
|
||||
assertEquals(Collections.emptyList(), summary);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSummary_notNormalForm_returnNull() {
|
||||
// 准备参数
|
||||
BpmProcessDefinitionInfoDO processDefinitionInfo = BpmProcessDefinitionInfoDO.builder()
|
||||
.formType(BpmModelFormTypeEnum.CUSTOM.getType())
|
||||
.build();
|
||||
|
||||
// 调用 & 断言
|
||||
assertNull(FlowableUtils.getSummary(null, Collections.emptyMap()));
|
||||
assertNull(FlowableUtils.getSummary(processDefinitionInfo, Collections.emptyMap()));
|
||||
}
|
||||
|
||||
private static BpmProcessDefinitionInfoDO processDefinitionInfo(List<String> formFields,
|
||||
BpmModelMetaInfoVO.SummarySetting summarySetting) {
|
||||
return BpmProcessDefinitionInfoDO.builder()
|
||||
.formType(BpmModelFormTypeEnum.NORMAL.getType())
|
||||
.formFields(formFields)
|
||||
.summarySetting(summarySetting)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static BpmModelMetaInfoVO.SummarySetting summarySetting(Boolean enable, String... fields) {
|
||||
BpmModelMetaInfoVO.SummarySetting summarySetting = new BpmModelMetaInfoVO.SummarySetting();
|
||||
summarySetting.setEnable(enable);
|
||||
summarySetting.setSummary(Arrays.asList(fields));
|
||||
return summarySetting;
|
||||
}
|
||||
|
||||
private static List<String> dbFormFields() {
|
||||
return Arrays.asList(
|
||||
DIVIDER_FIELD,
|
||||
"{\"type\":\"input\",\"field\":\"reason\",\"title\":\"请假原因\",\"value\":\"\","
|
||||
+ "\"props\":{\"type\":\"textarea\",\"placeholder\":\"请输入请假原因\"},"
|
||||
+ "\"$required\":\"请输入请假原因\",\"_fc_id\":\"id_F1\",\"_fc_drag_tag\":\"input\","
|
||||
+ "\"hidden\":false,\"display\":true}",
|
||||
TEXT_FIELD,
|
||||
"{\"type\":\"elRow\",\"title\":\"栅格布局\",\"children\":["
|
||||
+ "{\"type\":\"elCol\",\"props\":{\"span\":12},\"children\":["
|
||||
+ "{\"type\":\"DatePicker\",\"field\":\"startTime\",\"title\":\"开始时间\","
|
||||
+ "\"props\":{\"type\":\"datetime\",\"placeholder\":\"请选择开始时间\"},"
|
||||
+ "\"_fc_id\":\"id_F2\",\"_fc_drag_tag\":\"datePicker\"}]},"
|
||||
+ "\"字段说明\","
|
||||
+ "{\"type\":\"elCol\",\"props\":{\"span\":12},\"children\":["
|
||||
+ "{\"type\":\"inputNumber\",\"field\":\"days\",\"title\":\"请假天数\","
|
||||
+ "\"props\":{\"min\":0,\"precision\":1},\"_fc_id\":\"id_F3\","
|
||||
+ "\"_fc_drag_tag\":\"inputNumber\"}]}],\"_fc_id\":\"id_LAYOUT\","
|
||||
+ "\"_fc_drag_tag\":\"row\"}",
|
||||
TAG_FIELD,
|
||||
"{\"type\":\"input\",\"field\":\"remark\",\"title\":\"备注\",\"value\":\"\","
|
||||
+ "\"props\":{\"placeholder\":\"请输入备注\"},\"_fc_id\":\"id_F4\","
|
||||
+ "\"_fc_drag_tag\":\"input\",\"hidden\":false,\"display\":true}");
|
||||
}
|
||||
|
||||
private static Map<String, Object> processVariables() {
|
||||
Map<String, Object> processVariables = new HashMap<>();
|
||||
processVariables.put("reason", "事假");
|
||||
processVariables.put("startTime", "2026-05-31 09:00:00");
|
||||
processVariables.put("days", 3);
|
||||
processVariables.put("remark", "下午到家");
|
||||
return processVariables;
|
||||
}
|
||||
|
||||
private static final String DIVIDER_FIELD = "{\"type\":\"elDivider\",\"children\":[\"基础信息\"],"
|
||||
+ "\"props\":{\"contentPosition\":\"left\"},\"_fc_id\":\"id_DIVIDER\","
|
||||
+ "\"_fc_drag_tag\":\"elDivider\"}";
|
||||
|
||||
private static final String TEXT_FIELD = "{\"type\":\"div\",\"children\":[\"请按实际情况填写\"],"
|
||||
+ "\"props\":{\"style\":{\"color\":\"#909399\"}},\"_fc_id\":\"id_TEXT\","
|
||||
+ "\"_fc_drag_tag\":\"text\"}";
|
||||
|
||||
private static final String TAG_FIELD = "{\"type\":\"elTag\",\"children\":[\"重要\"],"
|
||||
+ "\"props\":{\"type\":\"warning\"},\"_fc_id\":\"id_TAG\",\"_fc_drag_tag\":\"elTag\"}";
|
||||
|
||||
}
|
||||
|
|
@ -27,10 +27,12 @@ public interface CrmCustomerLimitConfigMapper extends BaseMapperX<CrmCustomerLim
|
|||
Integer type, Long userId, Long deptId) {
|
||||
LambdaQueryWrapperX<CrmCustomerLimitConfigDO> query = new LambdaQueryWrapperX<CrmCustomerLimitConfigDO>()
|
||||
.eq(CrmCustomerLimitConfigDO::getType, type);
|
||||
query.apply("FIND_IN_SET({0}, user_ids) > 0", userId);
|
||||
if (deptId != null) {
|
||||
query.apply("FIND_IN_SET({0}, dept_ids) > 0", deptId);
|
||||
}
|
||||
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);
|
||||
}
|
||||
});
|
||||
return selectList(query);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ xxl:
|
|||
# Lock4j 配置项
|
||||
lock4j:
|
||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ xxl:
|
|||
# Lock4j 配置项
|
||||
lock4j:
|
||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ xxl:
|
|||
# Lock4j 配置项
|
||||
lock4j:
|
||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ xxl:
|
|||
# Lock4j 配置项
|
||||
lock4j:
|
||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>cn.iocoder.cloud</groupId>
|
||||
<artifactId>yudao</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<modules>
|
||||
<module>yudao-module-im-api</module>
|
||||
<module>yudao-module-im-server</module>
|
||||
</modules>
|
||||
<artifactId>yudao-module-im</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>
|
||||
im 模块,我们放即时通讯业务。
|
||||
例如说:单聊、群聊、消息收发、消息撤回、消息已读等等
|
||||
</description>
|
||||
|
||||
</project>
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>cn.iocoder.cloud</groupId>
|
||||
<artifactId>yudao-module-im</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>yudao-module-im-api</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>
|
||||
im 模块 API,暴露给其它模块调用
|
||||
</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.cloud</groupId>
|
||||
<artifactId>yudao-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 参数校验 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
/**
|
||||
* @author anhaohao
|
||||
* @since 2024/3/9 下午8:59
|
||||
*/
|
||||
package cn.iocoder.yudao.module.im.api;
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
## AdoptOpenJDK 停止发布 OpenJDK 二进制,而 Eclipse Temurin 是它的延伸,提供更好的稳定性
|
||||
## 感谢复旦核博士的建议!灰子哥,牛皮!
|
||||
FROM eclipse-temurin:21-jre
|
||||
|
||||
## 创建目录,并使用它作为工作目录
|
||||
RUN mkdir -p /yudao-module-im-server
|
||||
WORKDIR /yudao-module-im-server
|
||||
## 将后端项目的 Jar 文件,复制到镜像中
|
||||
COPY ./target/yudao-module-im-server.jar app.jar
|
||||
|
||||
## 设置 TZ 时区
|
||||
## 设置 JAVA_OPTS 环境变量,可通过 docker run -e "JAVA_OPTS=" 进行覆盖
|
||||
ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms512m -Xmx512m"
|
||||
|
||||
## 暴露后端项目的 48093 端口
|
||||
EXPOSE 48093
|
||||
|
||||
## 启动后端项目
|
||||
CMD java ${JAVA_OPTS} -Djava.security.egd=file:/dev/./urandom -jar app.jar
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>cn.iocoder.cloud</groupId>
|
||||
<artifactId>yudao-module-im</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>yudao-module-im-server</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>
|
||||
im 模块,我们放即时通讯业务。
|
||||
例如说:单聊、群聊、消息收发、消息撤回、消息已读等等
|
||||
</description>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Cloud 基础 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.cloud</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-env</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 依赖服务 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.cloud</groupId>
|
||||
<artifactId>yudao-module-im-api</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.cloud</groupId>
|
||||
<artifactId>yudao-module-system-api</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.cloud</groupId>
|
||||
<artifactId>yudao-module-infra-api</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 业务组件 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.cloud</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Web 相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.cloud</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- DB 相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.cloud</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-mybatis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.cloud</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- RPC 远程调用相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.cloud</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-rpc</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Registry 注册中心相关 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Config 配置中心相关 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Job 相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.cloud</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-job</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 消息队列相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.cloud</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-mq</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Test 测试相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.cloud</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- 工具类相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.cloud</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-excel</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 敏感词检测:sensitive-word(trie 树高效匹配,支持忽略大小写 / 全半角 / 数字风格) -->
|
||||
<dependency>
|
||||
<groupId>com.github.houbb</groupId>
|
||||
<artifactId>sensitive-word</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 汉字转拼音:作为 hutool PinyinUtil 的底层引擎,业务侧调用 StrUtils.toPinyin 时按需引入 -->
|
||||
<dependency>
|
||||
<groupId>com.belerweb</groupId>
|
||||
<artifactId>pinyin4j</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 监控相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.cloud</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-monitor</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<!-- 设置构建的 jar 包名 -->
|
||||
<finalName>${project.artifactId}</finalName>
|
||||
<plugins>
|
||||
<!-- 打包 -->
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<version>${spring.boot.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal> <!-- 将引入的 jar 打入其中 -->
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package cn.iocoder.yudao.module.im;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* 项目的启动类
|
||||
*
|
||||
* 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
|
||||
* 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
|
||||
* 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class ImServerApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
// 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
|
||||
// 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
|
||||
// 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
|
||||
|
||||
SpringApplication.run(ImServerApplication.class, args);
|
||||
|
||||
// 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
|
||||
// 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
|
||||
// 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package cn.iocoder.yudao.module.im.controller.admin.channel;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.material.ImChannelMaterialRespVO;
|
||||
import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelMaterialDO;
|
||||
import cn.iocoder.yudao.module.im.service.channel.ImChannelMaterialService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import javax.annotation.Resource;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
|
||||
@Tag(name = "用户 APP - IM 频道素材")
|
||||
@RestController
|
||||
@RequestMapping("/im/channel/material")
|
||||
@Validated
|
||||
public class ImChannelMaterialController {
|
||||
|
||||
@Resource
|
||||
private ImChannelMaterialService channelMaterialService;
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "获取素材详情;用于客户端点击图文卡片渲染详情页")
|
||||
@Parameter(name = "id", description = "素材编号", required = true, example = "1024")
|
||||
public CommonResult<ImChannelMaterialRespVO> getMaterial(@RequestParam("id") Long id) {
|
||||
ImChannelMaterialDO material = channelMaterialService.validateMaterialExists(id);
|
||||
return success(BeanUtils.toBean(material, ImChannelMaterialRespVO.class));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package cn.iocoder.yudao.module.im.controller.admin.face;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.im.controller.admin.face.vo.pack.ImFacePackUserRespVO;
|
||||
import cn.iocoder.yudao.module.im.dal.dataobject.face.ImFacePackDO;
|
||||
import cn.iocoder.yudao.module.im.dal.dataobject.face.ImFacePackItemDO;
|
||||
import cn.iocoder.yudao.module.im.service.face.ImFacePackItemService;
|
||||
import cn.iocoder.yudao.module.im.service.face.ImFacePackService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import javax.annotation.Resource;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap;
|
||||
|
||||
@Tag(name = "管理后台 - IM 表情包")
|
||||
@RestController
|
||||
@RequestMapping("/im/face-pack")
|
||||
@Validated
|
||||
public class ImFacePackController {
|
||||
|
||||
@Resource
|
||||
private ImFacePackService facePackService;
|
||||
@Resource
|
||||
private ImFacePackItemService facePackItemService;
|
||||
|
||||
@GetMapping("/list")
|
||||
@Operation(summary = "获得启用的表情包列表(含表情)")
|
||||
public CommonResult<List<ImFacePackUserRespVO>> getFacePackList() {
|
||||
// 1.1 拉所有启用表情包
|
||||
List<ImFacePackDO> packs = facePackService.getEnabledFacePackList();
|
||||
if (packs.isEmpty()) {
|
||||
return success(List.of());
|
||||
}
|
||||
// 1.2 拉这些包下所有启用表情,按 packId 分组
|
||||
List<ImFacePackItemDO> items = facePackItemService.getEnabledItemListByPackIds(
|
||||
convertList(packs, ImFacePackDO::getId));
|
||||
Map<Long, List<ImFacePackItemDO>> itemsByPackId = convertMultiMap(items, ImFacePackItemDO::getPackId);
|
||||
|
||||
// 2. 拼装:BeanUtils 把 pack 字段映射 + 自己塞 items
|
||||
List<ImFacePackUserRespVO> result = convertList(packs, pack -> {
|
||||
ImFacePackUserRespVO vo = BeanUtils.toBean(pack, ImFacePackUserRespVO.class);
|
||||
vo.setItems(BeanUtils.toBean(itemsByPackId.getOrDefault(pack.getId(), List.of()), ImFacePackUserRespVO.Item.class));
|
||||
return vo;
|
||||
});
|
||||
return success(result);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package cn.iocoder.yudao.module.im.controller.admin.face;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.im.controller.admin.face.vo.useritem.ImFaceUserItemRespVO;
|
||||
import cn.iocoder.yudao.module.im.controller.admin.face.vo.useritem.ImFaceUserItemSaveReqVO;
|
||||
import cn.iocoder.yudao.module.im.dal.dataobject.face.ImFaceUserItemDO;
|
||||
import cn.iocoder.yudao.module.im.service.face.ImFaceUserItemService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import javax.annotation.Resource;
|
||||
import javax.validation.Valid;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
|
||||
|
||||
@Tag(name = "管理后台 - IM 个人表情")
|
||||
@RestController
|
||||
@RequestMapping("/im/face-user-item")
|
||||
@Validated
|
||||
public class ImFaceUserItemController {
|
||||
|
||||
@Resource
|
||||
private ImFaceUserItemService faceUserItemService;
|
||||
|
||||
@GetMapping("/list")
|
||||
@Operation(summary = "获得我的个人表情列表")
|
||||
public CommonResult<List<ImFaceUserItemRespVO>> getFaceUserItemList() {
|
||||
List<ImFaceUserItemDO> items = faceUserItemService.getFaceUserItemList(getLoginUserId());
|
||||
return success(BeanUtils.toBean(items, ImFaceUserItemRespVO.class));
|
||||
}
|
||||
|
||||
@PostMapping("/create")
|
||||
@Operation(summary = "添加个人表情")
|
||||
public CommonResult<Long> createFaceUserItem(@Valid @RequestBody ImFaceUserItemSaveReqVO reqVO) {
|
||||
return success(faceUserItemService.createFaceUserItem(getLoginUserId(), reqVO));
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete")
|
||||
@Operation(summary = "删除个人表情")
|
||||
@Parameter(name = "id", description = "编号", required = true, example = "4096")
|
||||
public CommonResult<Boolean> deleteFaceUserItem(@RequestParam("id") Long id) {
|
||||
faceUserItemService.deleteFaceUserItem(getLoginUserId(), id);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package cn.iocoder.yudao.module.im.controller.admin.face.vo.pack;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "IM 表情包(用户端) Response VO")
|
||||
@Data
|
||||
public class ImFacePackUserRespVO {
|
||||
|
||||
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "表情包名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "猫主子")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "表情包图标", example = "https://cdn.example.com/face/pack/cat.png")
|
||||
private String icon;
|
||||
|
||||
@Schema(description = "表情列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private List<Item> items;
|
||||
|
||||
@Schema(description = "IM 表情包项(用户端)")
|
||||
@Data
|
||||
public static class Item {
|
||||
|
||||
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "表情图 URL", requiredMode = Schema.RequiredMode.REQUIRED,
|
||||
example = "https://cdn.example.com/face/pack/cat-001.png")
|
||||
private String url;
|
||||
|
||||
@Schema(description = "表情名", example = "狗头")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "渲染宽度(像素)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
|
||||
private Integer width;
|
||||
|
||||
@Schema(description = "渲染高度(像素)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
|
||||
private Integer height;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package cn.iocoder.yudao.module.im.controller.admin.face.vo.useritem;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "IM 个人表情 Response VO")
|
||||
@Data
|
||||
public class ImFaceUserItemRespVO {
|
||||
|
||||
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4096")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "表情图 URL", requiredMode = Schema.RequiredMode.REQUIRED,
|
||||
example = "https://cdn.example.com/face/user/abc.gif")
|
||||
private String url;
|
||||
|
||||
@Schema(description = "表情名", example = "狗头")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "渲染宽度(像素)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
|
||||
private Integer width;
|
||||
|
||||
@Schema(description = "渲染高度(像素)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
|
||||
private Integer height;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package cn.iocoder.yudao.module.im.controller.admin.face.vo.useritem;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import javax.validation.constraints.Max;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "IM 个人表情新增 Request VO")
|
||||
@Data
|
||||
public class ImFaceUserItemSaveReqVO {
|
||||
|
||||
@Schema(description = "表情图 URL", requiredMode = Schema.RequiredMode.REQUIRED,
|
||||
example = "https://cdn.example.com/face/user/abc.gif")
|
||||
@NotBlank(message = "表情图 URL 不能为空")
|
||||
@Size(max = 512, message = "表情图 URL 长度不能超过 512")
|
||||
private String url;
|
||||
|
||||
@Schema(description = "表情名", example = "狗头")
|
||||
@Size(max = 64, message = "表情名长度不能超过 64")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "渲染宽度(像素)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
|
||||
@NotNull(message = "渲染宽度不能为空")
|
||||
@Min(value = 1, message = "渲染宽度不能小于 1 像素")
|
||||
@Max(value = 2048, message = "渲染宽度不能大于 2048 像素")
|
||||
private Integer width;
|
||||
|
||||
@Schema(description = "渲染高度(像素)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
|
||||
@NotNull(message = "渲染高度不能为空")
|
||||
@Min(value = 1, message = "渲染高度不能小于 1 像素")
|
||||
@Max(value = 2048, message = "渲染高度不能大于 2048 像素")
|
||||
private Integer height;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
package cn.iocoder.yudao.module.im.controller.admin.friend;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.string.StrUtils;
|
||||
import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendRespVO;
|
||||
import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendUpdateReqVO;
|
||||
import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
|
||||
import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
|
||||
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
|
||||
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.Parameters;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import javax.annotation.Resource;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.singleton;
|
||||
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
|
||||
|
||||
@Tag(name = "管理后台 - IM 好友")
|
||||
@RestController
|
||||
@RequestMapping("/im/friend")
|
||||
@Validated
|
||||
public class ImFriendController {
|
||||
|
||||
@Resource
|
||||
private ImFriendService friendService;
|
||||
|
||||
@Resource
|
||||
private AdminUserApi adminUserApi;
|
||||
|
||||
@GetMapping("/list")
|
||||
@Operation(summary = "获得当前登录用户的好友列表")
|
||||
public CommonResult<List<ImFriendRespVO>> getMyFriendList() {
|
||||
// 含 DISABLE 历史好友:保留给前端展示「已删除好友」的历史对话信息;前端按 status 决定会话级联清理
|
||||
List<ImFriendDO> friends = friendService.getFriendList(getLoginUserId());
|
||||
return success(buildFriendRespVOList(friends));
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "获得好友详情")
|
||||
@Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048")
|
||||
public CommonResult<ImFriendRespVO> getFriend(@RequestParam("friendUserId") Long friendUserId) {
|
||||
ImFriendDO friend = friendService.getFriend(getLoginUserId(), friendUserId);
|
||||
return success(buildFriendRespVO(friend));
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete")
|
||||
@Operation(summary = "删除好友(单向软删除)")
|
||||
@Parameters({
|
||||
@Parameter(description = "好友的用户编号", required = true, example = "2048"),
|
||||
@Parameter(description = "是否级联清理本端相关数据(如私聊会话)")
|
||||
})
|
||||
public CommonResult<Boolean> deleteFriend(
|
||||
@RequestParam("friendUserId") @NotNull(message = "好友用户编号不能为空") Long friendUserId,
|
||||
@RequestParam(value = "clear", required = false) Boolean clear) {
|
||||
friendService.deleteFriend(getLoginUserId(), friendUserId, clear);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PutMapping("/update")
|
||||
@Operation(summary = "更新好友单边属性(备注 / 免打扰 / 联系人置顶)")
|
||||
public CommonResult<Boolean> updateFriend(@Valid @RequestBody ImFriendUpdateReqVO reqVO) {
|
||||
friendService.updateFriend(getLoginUserId(), reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PutMapping("/block")
|
||||
@Operation(summary = "拉黑好友(必须先是好友;单边屏蔽对方私聊消息)")
|
||||
@Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048")
|
||||
public CommonResult<Boolean> blockFriend(
|
||||
@RequestParam("friendUserId") @NotNull(message = "好友用户编号不能为空") Long friendUserId) {
|
||||
friendService.blockFriend(getLoginUserId(), friendUserId);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PutMapping("/unblock")
|
||||
@Operation(summary = "移出黑名单")
|
||||
@Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048")
|
||||
public CommonResult<Boolean> unblockFriend(
|
||||
@RequestParam("friendUserId") @NotNull(message = "好友用户编号不能为空") Long friendUserId) {
|
||||
friendService.unblockFriend(getLoginUserId(), friendUserId);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
// ========== 私有方法:VO 组装 ==========
|
||||
|
||||
private List<ImFriendRespVO> buildFriendRespVOList(Collection<ImFriendDO> friends) {
|
||||
if (CollUtil.isEmpty(friends)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
// 批量聚合 AdminUser 信息(昵称 / 头像),避免 N+1
|
||||
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
|
||||
convertList(friends, ImFriendDO::getFriendUserId));
|
||||
return convertList(friends, friend -> {
|
||||
ImFriendRespVO vo = BeanUtils.toBean(friend, ImFriendRespVO.class);
|
||||
MapUtils.findAndThen(userMap, friend.getFriendUserId(), user ->
|
||||
vo.setNickname(user.getNickname()).setAvatar(user.getAvatar()));
|
||||
// 备注 / 昵称的拼音,给前端做字母分桶 + 拼音搜索
|
||||
vo.setDisplayNamePinyin(StrUtils.toPinyin(vo.getDisplayName()))
|
||||
.setNicknamePinyin(StrUtils.toPinyin(vo.getNickname()));
|
||||
return vo;
|
||||
});
|
||||
}
|
||||
|
||||
private ImFriendRespVO buildFriendRespVO(ImFriendDO friend) {
|
||||
if (friend == null) {
|
||||
return null;
|
||||
}
|
||||
return CollUtil.getFirst(buildFriendRespVOList(singleton(friend)));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
package cn.iocoder.yudao.module.im.controller.admin.friend;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.im.controller.admin.friend.vo.request.ImFriendRequestApplyReqVO;
|
||||
import cn.iocoder.yudao.module.im.controller.admin.friend.vo.request.ImFriendRequestRespVO;
|
||||
import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO;
|
||||
import cn.iocoder.yudao.module.im.service.friend.ImFriendRequestService;
|
||||
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
|
||||
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import javax.annotation.Resource;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.Max;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Size;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap;
|
||||
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
|
||||
|
||||
/**
|
||||
* IM 好友申请记录 Controller
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Tag(name = "管理后台 - IM 好友申请")
|
||||
@RestController
|
||||
@RequestMapping("/im/friend-request")
|
||||
@Validated
|
||||
public class ImFriendRequestController {
|
||||
|
||||
@Resource
|
||||
private ImFriendRequestService friendRequestService;
|
||||
|
||||
@Resource
|
||||
private AdminUserApi adminUserApi;
|
||||
|
||||
@PostMapping("/apply")
|
||||
@Operation(summary = "发起好友申请")
|
||||
public CommonResult<Long> applyFriend(@Valid @RequestBody ImFriendRequestApplyReqVO reqVO) {
|
||||
ImFriendRequestDO request = friendRequestService.applyFriend(getLoginUserId(), reqVO);
|
||||
return success(request != null ? request.getId() : null);
|
||||
}
|
||||
|
||||
@PutMapping("/agree")
|
||||
@Operation(summary = "同意好友申请")
|
||||
@Parameter(name = "id", description = "申请编号", required = true, example = "1024")
|
||||
public CommonResult<Boolean> agreeFriendRequest(
|
||||
@RequestParam("id") @NotNull(message = "申请编号不能为空") Long id) {
|
||||
friendRequestService.agreeFriendRequest(getLoginUserId(), id);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PutMapping("/refuse")
|
||||
@Operation(summary = "拒绝好友申请")
|
||||
public CommonResult<Boolean> refuseFriendRequest(
|
||||
@RequestParam("id") @NotNull(message = "申请编号不能为空") Long id,
|
||||
@RequestParam(value = "handleContent", required = false)
|
||||
@Size(max = 255, message = "处理理由最多 255 个字符") String handleContent) {
|
||||
friendRequestService.refuseFriendRequest(getLoginUserId(), id, handleContent);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
@Operation(summary = "查询「我相关」的好友申请列表(游标分页:传 maxId 加载更多)")
|
||||
public CommonResult<List<ImFriendRequestRespVO>> getMyFriendRequestList(
|
||||
@Parameter(description = "当前列表最旧记录的 id;首页不传")
|
||||
@RequestParam(value = "maxId", required = false) Long maxId,
|
||||
@Parameter(description = "单次拉取条数", required = true)
|
||||
@RequestParam("limit") @Min(1) @Max(200) Integer limit) {
|
||||
List<ImFriendRequestDO> list = friendRequestService.getMyFriendRequestList(getLoginUserId(), maxId, limit);
|
||||
return success(buildList(list));
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "按 id 单查「我相关」的申请记录(带越权过滤;WebSocket 通知到达后用)")
|
||||
@Parameter(name = "id", description = "申请记录编号", required = true)
|
||||
public CommonResult<ImFriendRequestRespVO> getMyFriendRequest(@RequestParam("id") Long id) {
|
||||
ImFriendRequestDO request = friendRequestService.getFriendRequest(id);
|
||||
// 越权过滤:fromUser / toUser 必有一方是当前用户,否则当不存在返回 null
|
||||
Long currentUserId = getLoginUserId();
|
||||
if (request == null || (ObjUtil.notEqual(request.getFromUserId(), currentUserId)
|
||||
&& ObjUtil.notEqual(request.getToUserId(), currentUserId))) {
|
||||
return success(null);
|
||||
}
|
||||
return success(CollUtil.getFirst(buildList(Collections.singletonList(request))));
|
||||
}
|
||||
|
||||
// ========== 私有方法:VO 组装 ==========
|
||||
|
||||
private List<ImFriendRequestRespVO> buildList(List<ImFriendRequestDO> list) {
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
// 双向 OR 列表,userIds 取 from + to 两组并集
|
||||
Set<Long> userIds = convertSetByFlatMap(list,
|
||||
request -> Stream.of(request.getFromUserId(), request.getToUserId()));
|
||||
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
|
||||
return convertList(list, request -> {
|
||||
ImFriendRequestRespVO vo = BeanUtils.toBean(request, ImFriendRequestRespVO.class);
|
||||
MapUtils.findAndThen(userMap, request.getFromUserId(), user ->
|
||||
vo.setFromNickname(user.getNickname()).setFromAvatar(user.getAvatar()));
|
||||
MapUtils.findAndThen(userMap, request.getToUserId(), user ->
|
||||
vo.setToNickname(user.getNickname()).setToAvatar(user.getAvatar()));
|
||||
return vo;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package cn.iocoder.yudao.module.im.controller.admin.friend.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* IM 好友 Response VO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Schema(description = "管理后台 - IM 好友 Response VO")
|
||||
@Data
|
||||
public class ImFriendRespVO {
|
||||
|
||||
@Schema(description = "关系记录编号", example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "好友的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
|
||||
private Long friendUserId;
|
||||
|
||||
@Schema(description = "是否免打扰", example = "false")
|
||||
private Boolean silent;
|
||||
|
||||
@Schema(description = "好友展示备注(仅自己可见)", example = "老张")
|
||||
private String displayName;
|
||||
|
||||
@Schema(description = "好友展示备注的拼音(小写无空格)", example = "laozhang")
|
||||
private String displayNamePinyin;
|
||||
|
||||
@Schema(description = "添加来源", example = "1")
|
||||
private Integer addSource; // 参见 ImFriendAddSourceEnum 枚举
|
||||
|
||||
@Schema(description = "是否置顶联系人", example = "false")
|
||||
private Boolean pinned;
|
||||
|
||||
@Schema(description = "是否拉黑(仅自己可见)", example = "false")
|
||||
private Boolean blocked;
|
||||
|
||||
@Schema(description = "好友状态", example = "0")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "添加好友时间")
|
||||
private LocalDateTime addTime;
|
||||
|
||||
@Schema(description = "删除好友时间")
|
||||
private LocalDateTime deleteTime;
|
||||
|
||||
// ========== 下面是聚合字段,方便前端显示 ==========
|
||||
|
||||
@Schema(description = "好友昵称(实时聚合自 AdminUser)", example = "芋道")
|
||||
private String nickname;
|
||||
|
||||
@Schema(description = "好友昵称的拼音(小写无空格)", example = "yudao")
|
||||
private String nicknamePinyin;
|
||||
|
||||
@Schema(description = "好友头像(实时聚合自 AdminUser)")
|
||||
private String avatar;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package cn.iocoder.yudao.module.im.controller.admin.friend.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "管理后台 - IM 好友更新 Request VO")
|
||||
@Data
|
||||
public class ImFriendUpdateReqVO {
|
||||
|
||||
@Schema(description = "好友的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
|
||||
@NotNull(message = "好友用户编号不能为空")
|
||||
private Long friendUserId;
|
||||
|
||||
@Schema(description = "是否免打扰;不传表示不修改", example = "true")
|
||||
private Boolean silent;
|
||||
|
||||
@Schema(description = "好友展示备注(仅自己可见);不传表示不修改,传空串表示清空", example = "老张")
|
||||
@Size(max = 16, message = "好友备注最多 16 个字符")
|
||||
private String displayName;
|
||||
|
||||
@Schema(description = "是否置顶联系人;不传表示不修改", example = "true")
|
||||
private Boolean pinned;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package cn.iocoder.yudao.module.im.controller.admin.friend.vo.request;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.validation.InEnum;
|
||||
import cn.iocoder.yudao.module.im.enums.friend.ImFriendAddSourceEnum;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* IM 好友申请 - 发起 Request VO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Schema(description = "管理后台 - IM 好友申请发起 Request VO")
|
||||
@Data
|
||||
public class ImFriendRequestApplyReqVO {
|
||||
|
||||
@Schema(description = "接收方用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
|
||||
@NotNull(message = "接收方用户编号不能为空")
|
||||
private Long toUserId;
|
||||
|
||||
@Schema(description = "申请理由", example = "我是芋艿(一种食材)")
|
||||
@Size(max = 255, message = "申请理由最多 255 个字符")
|
||||
private String applyContent;
|
||||
|
||||
@Schema(description = "对接收方的备注(仅自己可见)", example = "老张")
|
||||
@Size(max = 16, message = "好友备注最多 16 个字符")
|
||||
private String displayName;
|
||||
|
||||
@Schema(description = "添加来源", example = "1")
|
||||
@InEnum(ImFriendAddSourceEnum.class)
|
||||
private Integer addSource;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package cn.iocoder.yudao.module.im.controller.admin.friend.vo.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* IM 好友申请 Response VO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Schema(description = "管理后台 - IM 好友申请 Response VO")
|
||||
@Data
|
||||
public class ImFriendRequestRespVO {
|
||||
|
||||
@Schema(description = "申请编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "发起方用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
|
||||
private Long fromUserId;
|
||||
|
||||
@Schema(description = "接收方用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
|
||||
private Long toUserId;
|
||||
|
||||
@Schema(description = "处理结果", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||
private Integer handleResult; // 参见 ImFriendRequestHandleResultEnum 枚举
|
||||
|
||||
@Schema(description = "申请理由", example = "我是芋艿(一种食材)")
|
||||
private String applyContent;
|
||||
|
||||
@Schema(description = "处理理由(接收方拒绝时可选填)", example = "暂不通过")
|
||||
private String handleContent;
|
||||
|
||||
@Schema(description = "添加来源", example = "1")
|
||||
private Integer addSource; // 参见 ImFriendAddSourceEnum 枚举
|
||||
|
||||
@Schema(description = "处理时间")
|
||||
private LocalDateTime handleTime;
|
||||
|
||||
@Schema(description = "申请创建时间")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
// ========== 下面是聚合字段,方便前端显示 ==========
|
||||
|
||||
@Schema(description = "发起方昵称(实时聚合自 AdminUser)", example = "芋道")
|
||||
private String fromNickname;
|
||||
|
||||
@Schema(description = "发起方头像(实时聚合自 AdminUser)")
|
||||
private String fromAvatar;
|
||||
|
||||
@Schema(description = "接收方昵称(实时聚合自 AdminUser)", example = "老张")
|
||||
private String toNickname;
|
||||
|
||||
@Schema(description = "接收方头像(实时聚合自 AdminUser)")
|
||||
private String toAvatar;
|
||||
|
||||
}
|
||||