diff --git a/pom.xml b/pom.xml
index b4016c70ce..32938eecc2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -16,7 +16,7 @@
yudao-module-system
yudao-module-infra
-
+ yudao-module-bpm
diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml b/yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml
index 3272059d85..519b3de7e5 100644
--- a/yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml
+++ b/yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml
@@ -51,7 +51,6 @@
com.dameng
DmJdbcDriver18
- true
cn.com.kingbase
diff --git a/yudao-module-bpm/pom.xml b/yudao-module-bpm/pom.xml
index d44b5a515a..83651c6524 100644
--- a/yudao-module-bpm/pom.xml
+++ b/yudao-module-bpm/pom.xml
@@ -75,5 +75,11 @@
org.flowable
flowable-spring-boot-starter-actuator
+
+
+ org.liquibase
+ liquibase-core
+ provided
+
diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/config/BpmFlowableConfiguration.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/config/BpmFlowableConfiguration.java
index 8297dbce17..790d6f6070 100644
--- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/config/BpmFlowableConfiguration.java
+++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/config/BpmFlowableConfiguration.java
@@ -6,12 +6,15 @@ import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCand
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.event.BpmProcessInstanceEventPublisher;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import org.apache.ibatis.mapping.DatabaseIdProvider;
+import org.apache.ibatis.mapping.VendorDatabaseIdProvider;
import org.flowable.common.engine.api.delegate.FlowableFunctionDelegate;
import org.flowable.common.engine.api.delegate.event.FlowableEventListener;
import org.flowable.spring.SpringProcessEngineConfiguration;
import org.flowable.spring.boot.EngineConfigurationConfigurer;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -19,6 +22,7 @@ import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.List;
+import java.util.Properties;
/**
* BPM 模块的 Flowable 配置类
@@ -29,7 +33,8 @@ import java.util.List;
public class BpmFlowableConfiguration {
/**
- * 参考 {@link org.flowable.spring.boot.FlowableJobConfiguration} 类,创建对应的 AsyncListenableTaskExecutor Bean
+ * 参考 {@link org.flowable.spring.boot.FlowableJobConfiguration} 类,创建对应的
+ * AsyncListenableTaskExecutor Bean
*
* 如果不创建,会导致项目启动时,Flowable 报错的问题
*/
@@ -65,7 +70,8 @@ public class BpmFlowableConfiguration {
// 设置 ActivityBehaviorFactory 实现类,用于流程任务的审核人的自定义
configuration.setActivityBehaviorFactory(bpmActivityBehaviorFactory);
// 设置自定义的函数
- configuration.setCustomFlowableFunctionDelegates(ListUtil.toList(customFlowableFunctionDelegates.stream().iterator()));
+ configuration.setCustomFlowableFunctionDelegates(
+ ListUtil.toList(customFlowableFunctionDelegates.stream().iterator()));
};
}
@@ -81,7 +87,7 @@ public class BpmFlowableConfiguration {
@Bean
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") // adminUserApi 可以注入成功
public BpmTaskCandidateInvoker bpmTaskCandidateInvoker(List strategyList,
- AdminUserApi adminUserApi) {
+ AdminUserApi adminUserApi) {
return new BpmTaskCandidateInvoker(strategyList, adminUserApi);
}
@@ -92,4 +98,42 @@ public class BpmFlowableConfiguration {
return new BpmProcessInstanceEventPublisher(publisher);
}
+ // =========== 达梦数据库适配(通过 yudao.dameng.enabled=true 开启)==========
+
+ /**
+ * 达梦数据库适配的内部配置类。
+ *
+ * 仅当配置 yudao.dameng.enabled=true 时才生效,避免影响 MySQL、PostgreSQL 等其他数据库。
+ * 适配内容:
+ * 1. 强制 Flowable 引擎 databaseType 为 oracle,复用 Oracle 方言
+ * 2. 自定义 MyBatis DatabaseIdProvider,将 "DM DBMS" 映射为 "oracle"
+ */
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnProperty(prefix = "yudao.dameng", name = "enabled", havingValue = "true")
+ static class DamengAdaptConfiguration {
+
+ /**
+ * 达梦数据库适配:预设 Flowable 的 databaseType 为 oracle,
+ * initDataSource() 中检测到 databaseType 已有值时会跳过自动探测,
+ * 从而避免因找不到 "DM DBMS" 映射而报错。
+ */
+ @Bean
+ public EngineConfigurationConfigurer damengEngineConfigurationConfigurer() {
+ return configuration -> configuration.setDatabaseType("oracle");
+ }
+
+ /**
+ * 自定义 DatabaseIdProvider,将达梦数据库驱动返回的 "DM DBMS" 映射为 "oracle",
+ * 让 MyBatis 层自动复用 Flowable 的 Oracle SQL 映射文件。
+ */
+ @Bean
+ public DatabaseIdProvider databaseIdProvider() {
+ DatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider();
+ Properties properties = new Properties();
+ properties.setProperty("DM DBMS", "oracle");
+ databaseIdProvider.setProperties(properties);
+ return databaseIdProvider;
+ }
+ }
+
}
\ No newline at end of file
diff --git a/yudao-module-bpm/src/main/java/liquibase/database/core/DmDatabase.java b/yudao-module-bpm/src/main/java/liquibase/database/core/DmDatabase.java
new file mode 100644
index 0000000000..a53007f2d8
--- /dev/null
+++ b/yudao-module-bpm/src/main/java/liquibase/database/core/DmDatabase.java
@@ -0,0 +1,605 @@
+package liquibase.database.core;
+
+import liquibase.CatalogAndSchema;
+import liquibase.Scope;
+import liquibase.database.AbstractJdbcDatabase;
+import liquibase.database.DatabaseConnection;
+import liquibase.database.OfflineConnection;
+import liquibase.database.jvm.JdbcConnection;
+import liquibase.exception.DatabaseException;
+import liquibase.exception.UnexpectedLiquibaseException;
+import liquibase.exception.ValidationErrors;
+import liquibase.executor.ExecutorService;
+import liquibase.statement.DatabaseFunction;
+import liquibase.statement.SequenceCurrentValueFunction;
+import liquibase.statement.SequenceNextValueFunction;
+import liquibase.statement.core.RawCallStatement;
+import liquibase.statement.core.RawSqlStatement;
+import liquibase.structure.DatabaseObject;
+import liquibase.structure.core.Catalog;
+import liquibase.structure.core.Index;
+import liquibase.structure.core.PrimaryKey;
+import liquibase.structure.core.Schema;
+import liquibase.util.JdbcUtils;
+import liquibase.util.StringUtil;
+
+import java.lang.reflect.Method;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.Statement;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * 达梦数据库(DM DBMS)的 Liquibase 适配实现。
+ *
+ * 参考 {@link OracleDatabase} 实现,达梦 DM8 的 SQL 语法与 Oracle 高度兼容。
+ * 主要修改点:
+ *
+ * - PRODUCT_NAME 常量设为 "DM DBMS"
+ * - 默认端口设为 5236
+ * - shortName 设为 "dm"
+ * - 默认驱动设为 dm.jdbc.driver.DmDriver
+ * - getDatabaseMinorVersion() 增加异常保护,防止部分 DM8 驱动返回非数字版本号
+ *
+ */
+public class DmDatabase extends AbstractJdbcDatabase {
+ private static final String PRODUCT_NAME = "DM DBMS";
+
+ @Override
+ protected String getDefaultDatabaseProductName() {
+ return PRODUCT_NAME;
+ }
+
+ /**
+ * 判断当前连接是否为达梦数据库
+ */
+ @Override
+ public boolean isCorrectDatabaseImplementation(DatabaseConnection conn) throws DatabaseException {
+ return PRODUCT_NAME.equalsIgnoreCase(conn.getDatabaseProductName());
+ }
+
+ /**
+ * 根据 JDBC URL 返回达梦数据库的默认驱动类名
+ */
+ @Override
+ public String getDefaultDriver(String url) {
+ if (url.startsWith("jdbc:dm")) {
+ return "dm.jdbc.driver.DmDriver";
+ }
+
+ return null;
+ }
+
+ /**
+ * 返回数据库类型的短名称 "dm",用于 Liquibase 的 dbms 条件判断
+ */
+ @Override
+ public String getShortName() {
+ return "dm";
+ }
+
+ @Override
+ public Integer getDefaultPort() {
+ return 5236;
+ }
+
+ /**
+ * 达梦数据库支持延迟约束
+ */
+ @Override
+ public boolean supportsInitiallyDeferrableColumns() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsTablespaces() {
+ return true;
+ }
+
+ @Override
+ public int getPriority() {
+ return PRIORITY_DEFAULT;
+ }
+
+ private static final Pattern PROXY_USER = Pattern.compile(".*(?:thin|oci)\\:(.+)/@.*");
+
+ protected final int SHORT_IDENTIFIERS_LENGTH = 30;
+ protected final int LONG_IDENTIFIERS_LEGNTH = 128;
+ public static final int ORACLE_12C_MAJOR_VERSION = 12;
+
+ private Set reservedWords = new HashSet<>();
+ private Set userDefinedTypes;
+ private Map savedSessionNlsSettings;
+
+ private Boolean canAccessDbaRecycleBin;
+ private Integer databaseMajorVersion;
+ private Integer databaseMinorVersion;
+
+ /**
+ * 达梦数据库默认构造函数,设置日期函数和序列语法(兼容 Oracle 语法)
+ */
+ public DmDatabase() {
+ super.unquotedObjectsAreUppercased = true;
+ //noinspection HardCodedStringLiteral
+ super.setCurrentDateTimeFunction("SYSTIMESTAMP");
+ // 设置兼容 Oracle 的日期函数
+ //noinspection HardCodedStringLiteral
+ dateFunctions.add(new DatabaseFunction("SYSDATE"));
+ //noinspection HardCodedStringLiteral
+ dateFunctions.add(new DatabaseFunction("SYSTIMESTAMP"));
+ //noinspection HardCodedStringLiteral
+ dateFunctions.add(new DatabaseFunction("CURRENT_TIMESTAMP"));
+ //noinspection HardCodedStringLiteral
+ super.sequenceNextValueFunction = "%s.nextval";
+ //noinspection HardCodedStringLiteral
+ super.sequenceCurrentValueFunction = "%s.currval";
+ }
+
+ private void tryProxySession(final String url, final Connection con) {
+ Matcher m = PROXY_USER.matcher(url);
+ if (m.matches()) {
+ Properties props = new Properties();
+ props.put("PROXY_USER_NAME", m.group(1));
+ try {
+ Method method = con.getClass().getMethod("openProxySession", int.class, Properties.class);
+ method.setAccessible(true);
+ method.invoke(con, 1, props);
+ } catch (Exception e) {
+ Scope.getCurrentScope().getLog(getClass()).info("Could not open proxy session on DmDatabase: " + e.getCause().getMessage());
+ }
+ }
+ }
+
+ @Override
+ public int getDatabaseMajorVersion() throws DatabaseException {
+ if (databaseMajorVersion == null) {
+ return super.getDatabaseMajorVersion();
+ } else {
+ return databaseMajorVersion;
+ }
+ }
+
+ /**
+ * 获取数据库次版本号。
+ *
+ * 注意:部分 DM8 驱动版本返回的微版本号可能包含非数字字符(如 "8.1.2.xxx"),
+ * 导致 {@link NumberFormatException}。此处增加异常保护,返回默认值 0。
+ */
+ @Override
+ public int getDatabaseMinorVersion() throws DatabaseException {
+ if (databaseMinorVersion == null) {
+ try {
+ return super.getDatabaseMinorVersion();
+ } catch (Exception e) {
+ // 达梦驱动某些版本返回的版本号可能导致解析异常,返回默认值 0
+ Scope.getCurrentScope().getLog(getClass()).info(
+ "无法解析达梦数据库次版本号,使用默认值 0", e);
+ return 0;
+ }
+ } else {
+ return databaseMinorVersion;
+ }
+ }
+
+ @Override
+ public String getJdbcCatalogName(CatalogAndSchema schema) {
+ return null;
+ }
+
+ @Override
+ public String getJdbcSchemaName(CatalogAndSchema schema) {
+ return correctObjectName((schema.getCatalogName() == null) ? schema.getSchemaName() : schema.getCatalogName(), Schema.class);
+ }
+
+ @Override
+ protected String getAutoIncrementClause(final String generationType, final Boolean defaultOnNull) {
+ if (StringUtil.isEmpty(generationType)) {
+ return super.getAutoIncrementClause();
+ }
+
+ String autoIncrementClause = "GENERATED %s AS IDENTITY"; // %s -- [ ALWAYS | BY DEFAULT [ ON NULL ] ]
+ String generationStrategy = generationType;
+ if (Boolean.TRUE.equals(defaultOnNull) && generationType.toUpperCase().equals("BY DEFAULT")) {
+ generationStrategy += " ON NULL";
+ }
+ return String.format(autoIncrementClause, generationStrategy);
+ }
+
+ @Override
+ public String generatePrimaryKeyName(String tableName) {
+ if (tableName.length() > 27) {
+ //noinspection HardCodedStringLiteral
+ return "PK_" + tableName.toUpperCase(Locale.US).substring(0, 27);
+ } else {
+ //noinspection HardCodedStringLiteral
+ return "PK_" + tableName.toUpperCase(Locale.US);
+ }
+ }
+
+ @Override
+ public boolean isReservedWord(String objectName) {
+ return reservedWords.contains(objectName.toUpperCase());
+ }
+
+ @Override
+ public boolean supportsSequences() {
+ return true;
+ }
+
+ /**
+ * 达梦数据库在 Liquibase 层面不使用 schema 概念(与 Oracle 行为一致)
+ */
+ @Override
+ public boolean supportsSchemas() {
+ return false;
+ }
+
+ @Override
+ protected String getConnectionCatalogName() throws DatabaseException {
+ if (getConnection() instanceof OfflineConnection) {
+ return getConnection().getCatalog();
+ }
+ try {
+ //noinspection HardCodedStringLiteral
+ return Scope.getCurrentScope().getSingleton(ExecutorService.class).getExecutor("jdbc", this).queryForObject(new RawCallStatement("select sys_context( 'userenv', 'current_schema' ) from dual"), String.class);
+ } catch (Exception e) {
+ //noinspection HardCodedStringLiteral
+ Scope.getCurrentScope().getLog(getClass()).info("Error getting default schema", e);
+ }
+ return null;
+ }
+
+ @Override
+ public String getDefaultCatalogName() {//NOPMD
+ return (super.getDefaultCatalogName() == null) ? null : super.getDefaultCatalogName().toUpperCase(Locale.US);
+ }
+
+ /**
+ *
Returns an Oracle date literal with the same value as a string formatted using ISO 8601.
+ *
+ * Convert an ISO8601 date string to one of the following results:
+ * to_date('1995-05-23', 'YYYY-MM-DD')
+ * to_date('1995-05-23 09:23:59', 'YYYY-MM-DD HH24:MI:SS')
+ *
+ * Implementation restriction:
+ * Currently, only the following subsets of ISO8601 are supported:
+ *
+ * - YYYY-MM-DD
+ * - YYYY-MM-DDThh:mm:ss
+ *
+ */
+ @Override
+ public String getDateLiteral(String isoDate) {
+ String normalLiteral = super.getDateLiteral(isoDate);
+
+ if (isDateOnly(isoDate)) {
+ return "TO_DATE(" + normalLiteral + ", 'YYYY-MM-DD')";
+ } else if (isTimeOnly(isoDate)) {
+ return "TO_DATE(" + normalLiteral + ", 'HH24:MI:SS')";
+ } else if (isTimestamp(isoDate)) {
+ return "TO_TIMESTAMP(" + normalLiteral + ", 'YYYY-MM-DD HH24:MI:SS.FF')";
+ } else if (isDateTime(isoDate)) {
+ int seppos = normalLiteral.lastIndexOf('.');
+ if (seppos != -1) {
+ normalLiteral = normalLiteral.substring(0, seppos) + "'";
+ }
+ return "TO_DATE(" + normalLiteral + ", 'YYYY-MM-DD HH24:MI:SS')";
+ }
+ return "UNSUPPORTED:" + isoDate;
+ }
+
+ @Override
+ public boolean isSystemObject(DatabaseObject example) {
+ if (example == null) {
+ return false;
+ }
+
+ if (this.isLiquibaseObject(example)) {
+ return false;
+ }
+
+ if (example instanceof Schema) {
+ //noinspection HardCodedStringLiteral,HardCodedStringLiteral,HardCodedStringLiteral,HardCodedStringLiteral
+ if ("SYSTEM".equals(example.getName()) || "SYS".equals(example.getName()) || "CTXSYS".equals(example.getName()) || "XDB".equals(example.getName())) {
+ return true;
+ }
+ //noinspection HardCodedStringLiteral,HardCodedStringLiteral,HardCodedStringLiteral,HardCodedStringLiteral
+ if ("SYSTEM".equals(example.getSchema().getCatalogName()) || "SYS".equals(example.getSchema().getCatalogName()) || "CTXSYS".equals(example.getSchema().getCatalogName()) || "XDB".equals(example.getSchema().getCatalogName())) {
+ return true;
+ }
+ } else if (isSystemObject(example.getSchema())) {
+ return true;
+ }
+ if (example instanceof Catalog) {
+ //noinspection HardCodedStringLiteral,HardCodedStringLiteral,HardCodedStringLiteral,HardCodedStringLiteral
+ if (("SYSTEM".equals(example.getName()) || "SYS".equals(example.getName()) || "CTXSYS".equals(example.getName()) || "XDB".equals(example.getName()))) {
+ return true;
+ }
+ } else if (example.getName() != null) {
+ //noinspection HardCodedStringLiteral
+ if (example.getName().startsWith("BIN$")) { //oracle deleted table
+ boolean filteredInOriginalQuery = this.canAccessDbaRecycleBin();
+ if (!filteredInOriginalQuery) {
+ filteredInOriginalQuery = StringUtil.trimToEmpty(example.getSchema().getName()).equalsIgnoreCase(this.getConnection().getConnectionUserName());
+ }
+
+ if (filteredInOriginalQuery) {
+ return !((example instanceof PrimaryKey) || (example instanceof Index) || (example instanceof
+ liquibase.statement.UniqueConstraint));
+ } else {
+ return true;
+ }
+ } else //noinspection HardCodedStringLiteral
+ if (example.getName().startsWith("AQ$")) { //oracle AQ tables
+ return true;
+ } else //noinspection HardCodedStringLiteral
+ if (example.getName().startsWith("DR$")) { //oracle index tables
+ return true;
+ } else //noinspection HardCodedStringLiteral
+ if (example.getName().startsWith("SYS_IOT_OVER")) { //oracle system table
+ return true;
+ } else //noinspection HardCodedStringLiteral,HardCodedStringLiteral
+ if ((example.getName().startsWith("MDRT_") || example.getName().startsWith("MDRS_")) && example.getName().endsWith("$")) {
+ // CORE-1768 - Oracle creates these for spatial indices and will remove them when the index is removed.
+ return true;
+ } else //noinspection HardCodedStringLiteral
+ if (example.getName().startsWith("MLOG$_")) { //Created by materliaized view logs for every table that is part of a materialized view. Not available for DDL operations.
+ return true;
+ } else //noinspection HardCodedStringLiteral
+ if (example.getName().startsWith("RUPD$_")) { //Created by materialized view log tables using primary keys. Not available for DDL operations.
+ return true;
+ } else //noinspection HardCodedStringLiteral
+ if (example.getName().startsWith("WM$_")) { //Workspace Manager backup tables.
+ return true;
+ } else //noinspection HardCodedStringLiteral
+ if ("CREATE$JAVA$LOB$TABLE".equals(example.getName())) { //This table contains the name of the Java object, the date it was loaded, and has a BLOB column to store the Java object.
+ return true;
+ } else //noinspection HardCodedStringLiteral
+ if ("JAVA$CLASS$MD5$TABLE".equals(example.getName())) { //This is a hash table that tracks the loading of Java objects into a schema.
+ return true;
+ } else //noinspection HardCodedStringLiteral
+ if (example.getName().startsWith("ISEQ$$_")) { //System-generated sequence
+ return true;
+ } else //noinspection HardCodedStringLiteral
+ if (example.getName().startsWith("USLOG$")) { //for update materialized view
+ return true;
+ } else if (example.getName().startsWith("SYS_FBA")) { //for Flashback tables
+ return true;
+ }
+ }
+
+ return super.isSystemObject(example);
+ }
+
+ @Override
+ public boolean supportsAutoIncrement() {
+ // Oracle supports Identity beginning with version 12c
+ boolean isAutoIncrementSupported = false;
+
+ try {
+ if (getDatabaseMajorVersion() >= 12) {
+ isAutoIncrementSupported = true;
+ }
+
+ // Returning true will generate create table command with 'IDENTITY' clause, example:
+ // CREATE TABLE AutoIncTest (IDPrimaryKey NUMBER(19) GENERATED BY DEFAULT AS IDENTITY NOT NULL, TypeID NUMBER(3) NOT NULL, Description NVARCHAR2(50), CONSTRAINT PK_AutoIncTest PRIMARY KEY (IDPrimaryKey));
+
+ // While returning false will continue to generate create table command without 'IDENTITY' clause, example:
+ // CREATE TABLE AutoIncTest (IDPrimaryKey NUMBER(19) NOT NULL, TypeID NUMBER(3) NOT NULL, Description NVARCHAR2(50), CONSTRAINT PK_AutoIncTest PRIMARY KEY (IDPrimaryKey));
+
+ } catch (DatabaseException ex) {
+ isAutoIncrementSupported = false;
+ }
+
+ return isAutoIncrementSupported;
+ }
+
+
+// public Set findUniqueConstraints(String schema) throws DatabaseException {
+// Set returnSet = new HashSet();
+//
+// List