eventRegistryEventConsumers = new HashMap<>();
+
+ // MYBATIS SQL SESSION FACTORY /////////////////////////////////////
+
+ protected boolean isDbHistoryUsed = true;
+ protected DbSqlSessionFactory dbSqlSessionFactory;
+ protected SqlSessionFactory sqlSessionFactory;
+ protected TransactionFactory transactionFactory;
+ protected TransactionContextFactory transactionContextFactory;
+
+ /**
+ * If set to true, enables bulk insert (grouping sql inserts together). Default true.
+ * For some databases (eg DB2+z/OS) needs to be set to false.
+ */
+ protected boolean isBulkInsertEnabled = true;
+
+ /**
+ * Some databases have a limit of how many parameters one sql insert can have (eg SQL Server, 2000 params (!= insert statements) ). Tweak this parameter in case of exceptions indicating too much
+ * is being put into one bulk insert, or make it higher if your database can cope with it and there are inserts with a huge amount of data.
+ *
+ * By default: 100 (55 for mssql server as it has a hard limit of 2000 parameters in a statement)
+ */
+ protected int maxNrOfStatementsInBulkInsert = 100;
+
+ public int DEFAULT_MAX_NR_OF_STATEMENTS_BULK_INSERT_SQL_SERVER = 55; // currently Execution has most params (35). 2000 / 35 = 57.
+
+ protected String mybatisMappingFile;
+ protected Set> customMybatisMappers;
+ protected Set customMybatisXMLMappers;
+ protected List customMybatisInterceptors;
+
+ protected Set dependentEngineMyBatisXmlMappers;
+ protected List dependentEngineMybatisTypeAliasConfigs;
+ protected List dependentEngineMybatisTypeHandlerConfigs;
+
+ // SESSION FACTORIES ///////////////////////////////////////////////
+ protected List customSessionFactories;
+ protected Map, SessionFactory> sessionFactories;
+
+ protected boolean enableEventDispatcher = true;
+ protected FlowableEventDispatcher eventDispatcher;
+ protected List eventListeners;
+ protected Map> typedEventListeners;
+ protected List additionalEventDispatchActions;
+
+ protected LoggingListener loggingListener;
+
+ protected boolean transactionsExternallyManaged;
+
+ /**
+ * Flag that can be set to configure or not a relational database is used. This is useful for custom implementations that do not use relational databases at all.
+ *
+ * If true (default), the {@link AbstractEngineConfiguration#getDatabaseSchemaUpdate()} value will be used to determine what needs to happen wrt the database schema.
+ *
+ * If false, no validation or schema creation will be done. That means that the database schema must have been created 'manually' before but the engine does not validate whether the schema is
+ * correct. The {@link AbstractEngineConfiguration#getDatabaseSchemaUpdate()} value will not be used.
+ */
+ protected boolean usingRelationalDatabase = true;
+
+ /**
+ * Flag that can be set to configure whether or not a schema is used. This is useful for custom implementations that do not use relational databases at all.
+ * Setting {@link #usingRelationalDatabase} to true will automatically imply using a schema.
+ */
+ protected boolean usingSchemaMgmt = true;
+
+ /**
+ * Allows configuring a database table prefix which is used for all runtime operations of the process engine. For example, if you specify a prefix named 'PRE1.', Flowable will query for executions
+ * in a table named 'PRE1.ACT_RU_EXECUTION_'.
+ *
+ *
+ * NOTE: the prefix is not respected by automatic database schema management. If you use {@link AbstractEngineConfiguration#DB_SCHEMA_UPDATE_CREATE_DROP} or
+ * {@link AbstractEngineConfiguration#DB_SCHEMA_UPDATE_TRUE}, Flowable will create the database tables using the default names, regardless of the prefix configured here.
+ */
+ protected String databaseTablePrefix = "";
+
+ /**
+ * Escape character for doing wildcard searches.
+ *
+ * This will be added at then end of queries that include for example a LIKE clause. For example: SELECT * FROM table WHERE column LIKE '%\%%' ESCAPE '\';
+ */
+ protected String databaseWildcardEscapeCharacter;
+
+ /**
+ * database catalog to use
+ */
+ protected String databaseCatalog = "";
+
+ /**
+ * In some situations you want to set the schema to use for table checks / generation if the database metadata doesn't return that correctly, see https://jira.codehaus.org/browse/ACT-1220,
+ * https://jira.codehaus.org/browse/ACT-1062
+ */
+ protected String databaseSchema;
+
+ /**
+ * Set to true in case the defined databaseTablePrefix is a schema-name, instead of an actual table name prefix. This is relevant for checking if Flowable-tables exist, the databaseTablePrefix
+ * will not be used here - since the schema is taken into account already, adding a prefix for the table-check will result in wrong table-names.
+ */
+ protected boolean tablePrefixIsSchema;
+
+ /**
+ * Set to true if the latest version of a definition should be retrieved, ignoring a possible parent deployment id value
+ */
+ protected boolean alwaysLookupLatestDefinitionVersion;
+
+ /**
+ * Set to true if by default lookups should fallback to the default tenant (an empty string by default or a defined tenant value)
+ */
+ protected boolean fallbackToDefaultTenant;
+
+ /**
+ * Default tenant provider that is executed when looking up definitions, in case the global or local fallback to default tenant value is true
+ */
+ protected DefaultTenantProvider defaultTenantProvider = (tenantId, scope, scopeKey) -> NO_TENANT_ID;
+
+ /**
+ * Enables the MyBatis plugin that logs the execution time of sql statements.
+ */
+ protected boolean enableLogSqlExecutionTime;
+
+ protected Properties databaseTypeMappings = getDefaultDatabaseTypeMappings();
+
+ /**
+ * Duration between the checks when acquiring a lock.
+ */
+ protected Duration lockPollRate = Duration.ofSeconds(10);
+
+ /**
+ * Duration to wait for the DB Schema lock before giving up.
+ */
+ protected Duration schemaLockWaitTime = Duration.ofMinutes(5);
+
+ // DATA MANAGERS //////////////////////////////////////////////////////////////////
+
+ protected PropertyDataManager propertyDataManager;
+ protected ByteArrayDataManager byteArrayDataManager;
+ protected TableDataManager tableDataManager;
+
+ // ENTITY MANAGERS ////////////////////////////////////////////////////////////////
+
+ protected PropertyEntityManager propertyEntityManager;
+ protected ByteArrayEntityManager byteArrayEntityManager;
+
+ protected List customPreDeployers;
+ protected List customPostDeployers;
+ protected List deployers;
+
+ // CONFIGURATORS ////////////////////////////////////////////////////////////
+
+ protected boolean enableConfiguratorServiceLoader = true; // Enabled by default. In certain environments this should be set to false (eg osgi)
+ protected List configurators; // The injected configurators
+ protected List allConfigurators; // Including auto-discovered configurators
+ protected EngineConfigurator idmEngineConfigurator;
+ protected EngineConfigurator eventRegistryConfigurator;
+
+ public static final String PRODUCT_NAME_POSTGRES = "PostgreSQL";
+ public static final String PRODUCT_NAME_CRDB = "CockroachDB";
+
+ public static final String DATABASE_TYPE_H2 = "h2";
+ public static final String DATABASE_TYPE_HSQL = "hsql";
+ public static final String DATABASE_TYPE_MYSQL = "mysql";
+ public static final String DATABASE_TYPE_ORACLE = "oracle";
+ public static final String DATABASE_TYPE_POSTGRES = "postgres";
+ public static final String DATABASE_TYPE_MSSQL = "mssql";
+ public static final String DATABASE_TYPE_DB2 = "db2";
+ public static final String DATABASE_TYPE_COCKROACHDB = "cockroachdb";
+
+ public static Properties getDefaultDatabaseTypeMappings() {
+ Properties databaseTypeMappings = new Properties();
+ databaseTypeMappings.setProperty("H2", DATABASE_TYPE_H2);
+ databaseTypeMappings.setProperty("HSQL Database Engine", DATABASE_TYPE_HSQL);
+ databaseTypeMappings.setProperty("MySQL", DATABASE_TYPE_MYSQL);
+ databaseTypeMappings.setProperty("MariaDB", DATABASE_TYPE_MYSQL);
+ databaseTypeMappings.setProperty("Oracle", DATABASE_TYPE_ORACLE);
+ databaseTypeMappings.setProperty(PRODUCT_NAME_POSTGRES, DATABASE_TYPE_POSTGRES);
+ databaseTypeMappings.setProperty("Microsoft SQL Server", DATABASE_TYPE_MSSQL);
+ databaseTypeMappings.setProperty(DATABASE_TYPE_DB2, DATABASE_TYPE_DB2);
+ databaseTypeMappings.setProperty("DB2", DATABASE_TYPE_DB2);
+ databaseTypeMappings.setProperty("DB2/NT", DATABASE_TYPE_DB2);
+ databaseTypeMappings.setProperty("DB2/NT64", DATABASE_TYPE_DB2);
+ databaseTypeMappings.setProperty("DB2 UDP", DATABASE_TYPE_DB2);
+ databaseTypeMappings.setProperty("DB2/LINUX", DATABASE_TYPE_DB2);
+ databaseTypeMappings.setProperty("DB2/LINUX390", DATABASE_TYPE_DB2);
+ databaseTypeMappings.setProperty("DB2/LINUXX8664", DATABASE_TYPE_DB2);
+ databaseTypeMappings.setProperty("DB2/LINUXZ64", DATABASE_TYPE_DB2);
+ databaseTypeMappings.setProperty("DB2/LINUXPPC64", DATABASE_TYPE_DB2);
+ databaseTypeMappings.setProperty("DB2/LINUXPPC64LE", DATABASE_TYPE_DB2);
+ databaseTypeMappings.setProperty("DB2/400 SQL", DATABASE_TYPE_DB2);
+ databaseTypeMappings.setProperty("DB2/6000", DATABASE_TYPE_DB2);
+ databaseTypeMappings.setProperty("DB2 UDB iSeries", DATABASE_TYPE_DB2);
+ databaseTypeMappings.setProperty("DB2/AIX64", DATABASE_TYPE_DB2);
+ databaseTypeMappings.setProperty("DB2/HPUX", DATABASE_TYPE_DB2);
+ databaseTypeMappings.setProperty("DB2/HP64", DATABASE_TYPE_DB2);
+ databaseTypeMappings.setProperty("DB2/SUN", DATABASE_TYPE_DB2);
+ databaseTypeMappings.setProperty("DB2/SUN64", DATABASE_TYPE_DB2);
+ databaseTypeMappings.setProperty("DB2/PTX", DATABASE_TYPE_DB2);
+ databaseTypeMappings.setProperty("DB2/2", DATABASE_TYPE_DB2);
+ databaseTypeMappings.setProperty("DB2 UDB AS400", DATABASE_TYPE_DB2);
+ databaseTypeMappings.setProperty(PRODUCT_NAME_CRDB, DATABASE_TYPE_COCKROACHDB);
+ databaseTypeMappings.setProperty("DM DBMS", DATABASE_TYPE_ORACLE); // dhb52: DM support
+ return databaseTypeMappings;
+ }
+
+ protected Map beans;
+
+ protected IdGenerator idGenerator;
+ protected boolean usePrefixId;
+
+ protected Clock clock;
+ protected ObjectMapper objectMapper;
+
+ // Variables
+
+ public static final int DEFAULT_GENERIC_MAX_LENGTH_STRING = 4000;
+ public static final int DEFAULT_ORACLE_MAX_LENGTH_STRING = 2000;
+
+ /**
+ * Define a max length for storing String variable types in the database. Mainly used for the Oracle NVARCHAR2 limit of 2000 characters
+ */
+ protected int maxLengthStringVariableType = -1;
+
+ protected void initEngineConfigurations() {
+ addEngineConfiguration(getEngineCfgKey(), getEngineScopeType(), this);
+ }
+
+ // DataSource
+ // ///////////////////////////////////////////////////////////////
+
+ protected void initDataSource() {
+ if (dataSource == null) {
+ if (dataSourceJndiName != null) {
+ try {
+ dataSource = (DataSource) new InitialContext().lookup(dataSourceJndiName);
+ } catch (Exception e) {
+ throw new FlowableException("couldn't lookup datasource from " + dataSourceJndiName + ": " + e.getMessage(), e);
+ }
+
+ } else if (jdbcUrl != null) {
+ if ((jdbcDriver == null) || (jdbcUsername == null)) {
+ throw new FlowableException("DataSource or JDBC properties have to be specified in a process engine configuration");
+ }
+
+ logger.debug("initializing datasource to db: {}", jdbcUrl);
+
+ if (logger.isInfoEnabled()) {
+ logger.info("Configuring Datasource with following properties (omitted password for security)");
+ logger.info("datasource driver : {}", jdbcDriver);
+ logger.info("datasource url : {}", jdbcUrl);
+ logger.info("datasource user name : {}", jdbcUsername);
+ }
+
+ PooledDataSource pooledDataSource = new PooledDataSource(this.getClass().getClassLoader(), jdbcDriver, jdbcUrl, jdbcUsername, jdbcPassword);
+
+ if (jdbcMaxActiveConnections > 0) {
+ pooledDataSource.setPoolMaximumActiveConnections(jdbcMaxActiveConnections);
+ }
+ if (jdbcMaxIdleConnections > 0) {
+ pooledDataSource.setPoolMaximumIdleConnections(jdbcMaxIdleConnections);
+ }
+ if (jdbcMaxCheckoutTime > 0) {
+ pooledDataSource.setPoolMaximumCheckoutTime(jdbcMaxCheckoutTime);
+ }
+ if (jdbcMaxWaitTime > 0) {
+ pooledDataSource.setPoolTimeToWait(jdbcMaxWaitTime);
+ }
+ if (jdbcPingEnabled) {
+ pooledDataSource.setPoolPingEnabled(true);
+ if (jdbcPingQuery != null) {
+ pooledDataSource.setPoolPingQuery(jdbcPingQuery);
+ }
+ pooledDataSource.setPoolPingConnectionsNotUsedFor(jdbcPingConnectionNotUsedFor);
+ }
+ if (jdbcDefaultTransactionIsolationLevel > 0) {
+ pooledDataSource.setDefaultTransactionIsolationLevel(jdbcDefaultTransactionIsolationLevel);
+ }
+ dataSource = pooledDataSource;
+ }
+ }
+
+ if (databaseType == null) {
+ initDatabaseType();
+ }
+ }
+
+ public void initDatabaseType() {
+ Connection connection = null;
+ try {
+ connection = dataSource.getConnection();
+ DatabaseMetaData databaseMetaData = connection.getMetaData();
+ String databaseProductName = databaseMetaData.getDatabaseProductName();
+ logger.debug("database product name: '{}'", databaseProductName);
+
+ // CRDB does not expose the version through the jdbc driver, so we need to fetch it through version().
+ if (PRODUCT_NAME_POSTGRES.equalsIgnoreCase(databaseProductName)) {
+ try (PreparedStatement preparedStatement = connection.prepareStatement("select version() as version;");
+ ResultSet resultSet = preparedStatement.executeQuery()) {
+ String version = null;
+ if (resultSet.next()) {
+ version = resultSet.getString("version");
+ }
+
+ if (StringUtils.isNotEmpty(version) && version.toLowerCase().startsWith(PRODUCT_NAME_CRDB.toLowerCase())) {
+ databaseProductName = PRODUCT_NAME_CRDB;
+ logger.info("CockroachDB version '{}' detected", version);
+ }
+ }
+ }
+
+ databaseType = databaseTypeMappings.getProperty(databaseProductName);
+ if (databaseType == null) {
+ throw new FlowableException("couldn't deduct database type from database product name '" + databaseProductName + "'");
+ }
+ logger.debug("using database type: {}", databaseType);
+
+ } catch (SQLException e) {
+ throw new RuntimeException("Exception while initializing Database connection", e);
+ } finally {
+ try {
+ if (connection != null) {
+ connection.close();
+ }
+ } catch (SQLException e) {
+ logger.error("Exception while closing the Database connection", e);
+ }
+ }
+
+ // Special care for MSSQL, as it has a hard limit of 2000 params per statement (incl bulk statement).
+ // Especially with executions, with 100 as default, this limit is passed.
+ if (DATABASE_TYPE_MSSQL.equals(databaseType)) {
+ maxNrOfStatementsInBulkInsert = DEFAULT_MAX_NR_OF_STATEMENTS_BULK_INSERT_SQL_SERVER;
+ }
+ }
+
+ public void initSchemaManager() {
+ if (this.commonSchemaManager == null) {
+ this.commonSchemaManager = new CommonDbSchemaManager();
+ }
+ }
+
+ // session factories ////////////////////////////////////////////////////////
+
+ public void addSessionFactory(SessionFactory sessionFactory) {
+ sessionFactories.put(sessionFactory.getSessionType(), sessionFactory);
+ }
+
+ public void initCommandContextFactory() {
+ if (commandContextFactory == null) {
+ commandContextFactory = new CommandContextFactory();
+ }
+ }
+
+ public void initTransactionContextFactory() {
+ if (transactionContextFactory == null) {
+ transactionContextFactory = new StandaloneMybatisTransactionContextFactory();
+ }
+ }
+
+ public void initCommandExecutors() {
+ initDefaultCommandConfig();
+ initSchemaCommandConfig();
+ initCommandInvoker();
+ initCommandInterceptors();
+ initCommandExecutor();
+ }
+
+
+ public void initDefaultCommandConfig() {
+ if (defaultCommandConfig == null) {
+ defaultCommandConfig = new CommandConfig();
+ }
+ }
+
+ public void initSchemaCommandConfig() {
+ if (schemaCommandConfig == null) {
+ schemaCommandConfig = new CommandConfig();
+ }
+ }
+
+ public void initCommandInvoker() {
+ if (commandInvoker == null) {
+ commandInvoker = new DefaultCommandInvoker();
+ }
+ }
+
+ public void initCommandInterceptors() {
+ if (commandInterceptors == null) {
+ commandInterceptors = new ArrayList<>();
+ if (customPreCommandInterceptors != null) {
+ commandInterceptors.addAll(customPreCommandInterceptors);
+ }
+ commandInterceptors.addAll(getDefaultCommandInterceptors());
+ if (customPostCommandInterceptors != null) {
+ commandInterceptors.addAll(customPostCommandInterceptors);
+ }
+ commandInterceptors.add(commandInvoker);
+ }
+ }
+
+ public Collection extends CommandInterceptor> getDefaultCommandInterceptors() {
+ if (defaultCommandInterceptors == null) {
+ List interceptors = new ArrayList<>();
+ interceptors.add(new LogInterceptor());
+
+ if (DATABASE_TYPE_COCKROACHDB.equals(databaseType)) {
+ interceptors.add(new CrDbRetryInterceptor());
+ }
+
+ CommandInterceptor transactionInterceptor = createTransactionInterceptor();
+ if (transactionInterceptor != null) {
+ interceptors.add(transactionInterceptor);
+ }
+
+ if (commandContextFactory != null) {
+ String engineCfgKey = getEngineCfgKey();
+ CommandContextInterceptor commandContextInterceptor = new CommandContextInterceptor(commandContextFactory,
+ classLoader, useClassForNameClassLoading, clock, objectMapper);
+ engineConfigurations.put(engineCfgKey, this);
+ commandContextInterceptor.setEngineCfgKey(engineCfgKey);
+ commandContextInterceptor.setEngineConfigurations(engineConfigurations);
+ interceptors.add(commandContextInterceptor);
+ }
+
+ if (transactionContextFactory != null) {
+ interceptors.add(new TransactionContextInterceptor(transactionContextFactory));
+ }
+
+ List additionalCommandInterceptors = getAdditionalDefaultCommandInterceptors();
+ if (additionalCommandInterceptors != null) {
+ interceptors.addAll(additionalCommandInterceptors);
+ }
+
+ defaultCommandInterceptors = interceptors;
+ }
+ return defaultCommandInterceptors;
+ }
+
+ public abstract String getEngineCfgKey();
+
+ public abstract String getEngineScopeType();
+
+ public List getAdditionalDefaultCommandInterceptors() {
+ return null;
+ }
+
+ public void initCommandExecutor() {
+ if (commandExecutor == null) {
+ CommandInterceptor first = initInterceptorChain(commandInterceptors);
+ commandExecutor = new CommandExecutorImpl(getDefaultCommandConfig(), first);
+ }
+ }
+
+ public CommandInterceptor initInterceptorChain(List chain) {
+ if (chain == null || chain.isEmpty()) {
+ throw new FlowableException("invalid command interceptor chain configuration: " + chain);
+ }
+ for (int i = 0; i < chain.size() - 1; i++) {
+ chain.get(i).setNext(chain.get(i + 1));
+ }
+ return chain.get(0);
+ }
+
+ public abstract CommandInterceptor createTransactionInterceptor();
+
+
+ public void initBeans() {
+ if (beans == null) {
+ beans = new HashMap<>();
+ }
+ }
+
+ // id generator
+ // /////////////////////////////////////////////////////////////
+
+ public void initIdGenerator() {
+ if (idGenerator == null) {
+ idGenerator = new StrongUuidGenerator();
+ }
+ }
+
+ public void initObjectMapper() {
+ if (objectMapper == null) {
+ objectMapper = new ObjectMapper();
+ objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
+ }
+ }
+
+ public void initClock() {
+ if (clock == null) {
+ clock = new DefaultClockImpl();
+ }
+ }
+
+ // Data managers ///////////////////////////////////////////////////////////
+
+ public void initDataManagers() {
+ if (propertyDataManager == null) {
+ propertyDataManager = new MybatisPropertyDataManager(idGenerator);
+ }
+
+ if (byteArrayDataManager == null) {
+ byteArrayDataManager = new MybatisByteArrayDataManager(idGenerator);
+ }
+ }
+
+ // Entity managers //////////////////////////////////////////////////////////
+
+ public void initEntityManagers() {
+ if (propertyEntityManager == null) {
+ propertyEntityManager = new PropertyEntityManagerImpl(this, propertyDataManager);
+ }
+
+ if (byteArrayEntityManager == null) {
+ byteArrayEntityManager = new ByteArrayEntityManagerImpl(byteArrayDataManager, getEngineCfgKey(), this::getEventDispatcher);
+ }
+
+ if (tableDataManager == null) {
+ tableDataManager = new TableDataManagerImpl(this);
+ }
+ }
+
+ // services
+ // /////////////////////////////////////////////////////////////////
+
+ protected void initService(Object service) {
+ if (service instanceof CommonEngineServiceImpl) {
+ ((CommonEngineServiceImpl) service).setCommandExecutor(commandExecutor);
+ }
+ }
+
+ // myBatis SqlSessionFactory
+ // ////////////////////////////////////////////////
+
+ public void initSessionFactories() {
+ if (sessionFactories == null) {
+ sessionFactories = new HashMap<>();
+
+ if (usingRelationalDatabase) {
+ initDbSqlSessionFactory();
+ }
+
+ addSessionFactory(new GenericManagerFactory(EntityCache.class, EntityCacheImpl.class));
+
+ if (isLoggingSessionEnabled()) {
+ if (!sessionFactories.containsKey(LoggingSession.class)) {
+ LoggingSessionFactory loggingSessionFactory = new LoggingSessionFactory();
+ loggingSessionFactory.setLoggingListener(loggingListener);
+ loggingSessionFactory.setObjectMapper(objectMapper);
+ sessionFactories.put(LoggingSession.class, loggingSessionFactory);
+ }
+ }
+
+ commandContextFactory.setSessionFactories(sessionFactories);
+
+ } else {
+ if (usingRelationalDatabase) {
+ initDbSqlSessionFactoryEntitySettings();
+ }
+ }
+
+ if (customSessionFactories != null) {
+ for (SessionFactory sessionFactory : customSessionFactories) {
+ addSessionFactory(sessionFactory);
+ }
+ }
+ }
+
+ public void initDbSqlSessionFactory() {
+ if (dbSqlSessionFactory == null) {
+ dbSqlSessionFactory = createDbSqlSessionFactory();
+ }
+ dbSqlSessionFactory.setDatabaseType(databaseType);
+ dbSqlSessionFactory.setSqlSessionFactory(sqlSessionFactory);
+ dbSqlSessionFactory.setDbHistoryUsed(isDbHistoryUsed);
+ dbSqlSessionFactory.setDatabaseTablePrefix(databaseTablePrefix);
+ dbSqlSessionFactory.setTablePrefixIsSchema(tablePrefixIsSchema);
+ dbSqlSessionFactory.setDatabaseCatalog(databaseCatalog);
+ dbSqlSessionFactory.setDatabaseSchema(databaseSchema);
+ dbSqlSessionFactory.setMaxNrOfStatementsInBulkInsert(maxNrOfStatementsInBulkInsert);
+
+ initDbSqlSessionFactoryEntitySettings();
+
+ addSessionFactory(dbSqlSessionFactory);
+ }
+
+ public DbSqlSessionFactory createDbSqlSessionFactory() {
+ return new DbSqlSessionFactory(usePrefixId);
+ }
+
+ protected abstract void initDbSqlSessionFactoryEntitySettings();
+
+ protected void defaultInitDbSqlSessionFactoryEntitySettings(List> insertOrder, List> deleteOrder) {
+ if (insertOrder != null) {
+ for (Class extends Entity> clazz : insertOrder) {
+ dbSqlSessionFactory.getInsertionOrder().add(clazz);
+
+ if (isBulkInsertEnabled) {
+ dbSqlSessionFactory.getBulkInserteableEntityClasses().add(clazz);
+ }
+ }
+ }
+
+ if (deleteOrder != null) {
+ for (Class extends Entity> clazz : deleteOrder) {
+ dbSqlSessionFactory.getDeletionOrder().add(clazz);
+ }
+ }
+ }
+
+ public void initTransactionFactory() {
+ if (transactionFactory == null) {
+ if (transactionsExternallyManaged) {
+ transactionFactory = new ManagedTransactionFactory();
+ Properties properties = new Properties();
+ properties.put("closeConnection", "false");
+ this.transactionFactory.setProperties(properties);
+ } else {
+ transactionFactory = new JdbcTransactionFactory();
+ }
+ }
+ }
+
+ public void initSqlSessionFactory() {
+ if (sqlSessionFactory == null) {
+ InputStream inputStream = null;
+ try {
+ inputStream = getMyBatisXmlConfigurationStream();
+
+ Environment environment = new Environment("default", transactionFactory, dataSource);
+ Reader reader = new InputStreamReader(inputStream);
+ Properties properties = new Properties();
+ properties.put("prefix", databaseTablePrefix);
+
+ String wildcardEscapeClause = "";
+ if ((databaseWildcardEscapeCharacter != null) && (databaseWildcardEscapeCharacter.length() != 0)) {
+ wildcardEscapeClause = " escape '" + databaseWildcardEscapeCharacter + "'";
+ }
+ properties.put("wildcardEscapeClause", wildcardEscapeClause);
+
+ // set default properties
+ properties.put("limitBefore", "");
+ properties.put("limitAfter", "");
+ properties.put("limitBetween", "");
+ properties.put("limitBeforeNativeQuery", "");
+ properties.put("limitAfterNativeQuery", "");
+ properties.put("blobType", "BLOB");
+ properties.put("boolValue", "TRUE");
+
+ if (databaseType != null) {
+ properties.load(getResourceAsStream(pathToEngineDbProperties()));
+ }
+
+ Configuration configuration = initMybatisConfiguration(environment, reader, properties);
+ sqlSessionFactory = new DefaultSqlSessionFactory(configuration);
+
+ } catch (Exception e) {
+ throw new FlowableException("Error while building ibatis SqlSessionFactory: " + e.getMessage(), e);
+ } finally {
+ IoUtil.closeSilently(inputStream);
+ }
+ } else {
+ // This is needed when the SQL Session Factory is created by another engine.
+ // When custom XML Mappers are registered with this engine they need to be loaded in the configuration as well
+ applyCustomMybatisCustomizations(sqlSessionFactory.getConfiguration());
+ }
+ }
+
+ public String pathToEngineDbProperties() {
+ return "org/flowable/common/db/properties/" + databaseType + ".properties";
+ }
+
+ public Configuration initMybatisConfiguration(Environment environment, Reader reader, Properties properties) {
+ XMLConfigBuilder parser = new XMLConfigBuilder(reader, "", properties);
+ Configuration configuration = parser.getConfiguration();
+
+ if (databaseType != null) {
+ configuration.setDatabaseId(databaseType);
+ }
+
+ configuration.setEnvironment(environment);
+
+ initMybatisTypeHandlers(configuration);
+ initCustomMybatisInterceptors(configuration);
+ if (isEnableLogSqlExecutionTime()) {
+ initMyBatisLogSqlExecutionTimePlugin(configuration);
+ }
+
+ configuration = parseMybatisConfiguration(parser);
+ return configuration;
+ }
+
+ public void initCustomMybatisMappers(Configuration configuration) {
+ if (getCustomMybatisMappers() != null) {
+ for (Class> clazz : getCustomMybatisMappers()) {
+ if (!configuration.hasMapper(clazz)) {
+ configuration.addMapper(clazz);
+ }
+ }
+ }
+ }
+
+ public void initMybatisTypeHandlers(Configuration configuration) {
+ // When mapping into Map there is currently a problem with MyBatis.
+ // It will return objects which are driver specific.
+ // Therefore we are registering the mappings between Object.class and the specific jdbc type here.
+ // see https://github.com/mybatis/mybatis-3/issues/2216 for more info
+ TypeHandlerRegistry handlerRegistry = configuration.getTypeHandlerRegistry();
+
+ handlerRegistry.register(Object.class, JdbcType.BOOLEAN, new BooleanTypeHandler());
+ handlerRegistry.register(Object.class, JdbcType.BIT, new BooleanTypeHandler());
+
+ handlerRegistry.register(Object.class, JdbcType.TINYINT, new ByteTypeHandler());
+
+ handlerRegistry.register(Object.class, JdbcType.SMALLINT, new ShortTypeHandler());
+
+ handlerRegistry.register(Object.class, JdbcType.INTEGER, new IntegerTypeHandler());
+
+ handlerRegistry.register(Object.class, JdbcType.FLOAT, new FloatTypeHandler());
+
+ handlerRegistry.register(Object.class, JdbcType.DOUBLE, new DoubleTypeHandler());
+
+ handlerRegistry.register(Object.class, JdbcType.CHAR, new StringTypeHandler());
+ handlerRegistry.register(Object.class, JdbcType.CLOB, new ClobTypeHandler());
+ handlerRegistry.register(Object.class, JdbcType.VARCHAR, new StringTypeHandler());
+ handlerRegistry.register(Object.class, JdbcType.LONGVARCHAR, new StringTypeHandler());
+ handlerRegistry.register(Object.class, JdbcType.NVARCHAR, new NStringTypeHandler());
+ handlerRegistry.register(Object.class, JdbcType.NCHAR, new NStringTypeHandler());
+ handlerRegistry.register(Object.class, JdbcType.NCLOB, new NClobTypeHandler());
+
+ handlerRegistry.register(Object.class, JdbcType.ARRAY, new ArrayTypeHandler());
+
+ handlerRegistry.register(Object.class, JdbcType.BIGINT, new LongTypeHandler());
+
+ handlerRegistry.register(Object.class, JdbcType.REAL, new BigDecimalTypeHandler());
+ handlerRegistry.register(Object.class, JdbcType.DECIMAL, new BigDecimalTypeHandler());
+ handlerRegistry.register(Object.class, JdbcType.NUMERIC, new BigDecimalTypeHandler());
+
+ handlerRegistry.register(Object.class, JdbcType.BLOB, new BlobInputStreamTypeHandler());
+ handlerRegistry.register(Object.class, JdbcType.LONGVARBINARY, new BlobTypeHandler());
+
+ handlerRegistry.register(Object.class, JdbcType.DATE, new DateOnlyTypeHandler());
+ handlerRegistry.register(Object.class, JdbcType.TIME, new TimeOnlyTypeHandler());
+ handlerRegistry.register(Object.class, JdbcType.TIMESTAMP, new DateTypeHandler());
+
+ handlerRegistry.register(Object.class, JdbcType.SQLXML, new SqlxmlTypeHandler());
+ }
+
+ public void initCustomMybatisInterceptors(Configuration configuration) {
+ if (customMybatisInterceptors!=null){
+ for (Interceptor interceptor :customMybatisInterceptors){
+ configuration.addInterceptor(interceptor);
+ }
+ }
+ }
+
+ public void initMyBatisLogSqlExecutionTimePlugin(Configuration configuration) {
+ configuration.addInterceptor(new LogSqlExecutionTimePlugin());
+ }
+
+ public Configuration parseMybatisConfiguration(XMLConfigBuilder parser) {
+ Configuration configuration = parser.parse();
+
+ applyCustomMybatisCustomizations(configuration);
+ return configuration;
+ }
+
+ protected void applyCustomMybatisCustomizations(Configuration configuration) {
+ initCustomMybatisMappers(configuration);
+
+ if (dependentEngineMybatisTypeAliasConfigs != null) {
+ for (MybatisTypeAliasConfigurator typeAliasConfig : dependentEngineMybatisTypeAliasConfigs) {
+ typeAliasConfig.configure(configuration.getTypeAliasRegistry());
+ }
+ }
+ if (dependentEngineMybatisTypeHandlerConfigs != null) {
+ for (MybatisTypeHandlerConfigurator typeHandlerConfig : dependentEngineMybatisTypeHandlerConfigs) {
+ typeHandlerConfig.configure(configuration.getTypeHandlerRegistry());
+ }
+ }
+
+ parseDependentEngineMybatisXMLMappers(configuration);
+ parseCustomMybatisXMLMappers(configuration);
+ }
+
+ public void parseCustomMybatisXMLMappers(Configuration configuration) {
+ if (getCustomMybatisXMLMappers() != null) {
+ for (String resource : getCustomMybatisXMLMappers()) {
+ parseMybatisXmlMapping(configuration, resource);
+ }
+ }
+ }
+
+ public void parseDependentEngineMybatisXMLMappers(Configuration configuration) {
+ if (getDependentEngineMyBatisXmlMappers() != null) {
+ for (String resource : getDependentEngineMyBatisXmlMappers()) {
+ parseMybatisXmlMapping(configuration, resource);
+ }
+ }
+ }
+
+ protected void parseMybatisXmlMapping(Configuration configuration, String resource) {
+ // see XMLConfigBuilder.mapperElement()
+ XMLMapperBuilder mapperParser = new XMLMapperBuilder(getResourceAsStream(resource), configuration, resource, configuration.getSqlFragments());
+ mapperParser.parse();
+ }
+
+ protected InputStream getResourceAsStream(String resource) {
+ ClassLoader classLoader = getClassLoader();
+ if (classLoader != null) {
+ return getClassLoader().getResourceAsStream(resource);
+ } else {
+ return this.getClass().getClassLoader().getResourceAsStream(resource);
+ }
+ }
+
+ public void setMybatisMappingFile(String file) {
+ this.mybatisMappingFile = file;
+ }
+
+ public String getMybatisMappingFile() {
+ return mybatisMappingFile;
+ }
+
+ public abstract InputStream getMyBatisXmlConfigurationStream();
+
+ public void initConfigurators() {
+
+ allConfigurators = new ArrayList<>();
+ allConfigurators.addAll(getEngineSpecificEngineConfigurators());
+
+ // Configurators that are explicitly added to the config
+ if (configurators != null) {
+ allConfigurators.addAll(configurators);
+ }
+
+ // Auto discovery through ServiceLoader
+ if (enableConfiguratorServiceLoader) {
+ ClassLoader classLoader = getClassLoader();
+ if (classLoader == null) {
+ classLoader = ReflectUtil.getClassLoader();
+ }
+
+ ServiceLoader configuratorServiceLoader = ServiceLoader.load(EngineConfigurator.class, classLoader);
+ int nrOfServiceLoadedConfigurators = 0;
+ for (EngineConfigurator configurator : configuratorServiceLoader) {
+ allConfigurators.add(configurator);
+ nrOfServiceLoadedConfigurators++;
+ }
+
+ if (nrOfServiceLoadedConfigurators > 0) {
+ logger.info("Found {} auto-discoverable Process Engine Configurator{}", nrOfServiceLoadedConfigurators, nrOfServiceLoadedConfigurators > 1 ? "s" : "");
+ }
+
+ if (!allConfigurators.isEmpty()) {
+
+ // Order them according to the priorities (useful for dependent
+ // configurator)
+ allConfigurators.sort(new Comparator() {
+
+ @Override
+ public int compare(EngineConfigurator configurator1, EngineConfigurator configurator2) {
+ int priority1 = configurator1.getPriority();
+ int priority2 = configurator2.getPriority();
+
+ if (priority1 < priority2) {
+ return -1;
+ } else if (priority1 > priority2) {
+ return 1;
+ }
+ return 0;
+ }
+ });
+
+ // Execute the configurators
+ logger.info("Found {} Engine Configurators in total:", allConfigurators.size());
+ for (EngineConfigurator configurator : allConfigurators) {
+ logger.info("{} (priority:{})", configurator.getClass(), configurator.getPriority());
+ }
+
+ }
+
+ }
+ }
+
+ public void close() {
+ if (forceCloseMybatisConnectionPool && dataSource instanceof PooledDataSource) {
+ /*
+ * When the datasource is created by a Flowable engine (i.e. it's an instance of PooledDataSource),
+ * the connection pool needs to be closed when closing the engine.
+ * Note that calling forceCloseAll() multiple times (as is the case when running with multiple engine) is ok.
+ */
+ ((PooledDataSource) dataSource).forceCloseAll();
+ }
+ }
+
+ protected List getEngineSpecificEngineConfigurators() {
+ // meant to be overridden if needed
+ return Collections.emptyList();
+ }
+
+ public void configuratorsBeforeInit() {
+ for (EngineConfigurator configurator : allConfigurators) {
+ logger.info("Executing beforeInit() of {} (priority:{})", configurator.getClass(), configurator.getPriority());
+ configurator.beforeInit(this);
+ }
+ }
+
+ public void configuratorsAfterInit() {
+ for (EngineConfigurator configurator : allConfigurators) {
+ logger.info("Executing configure() of {} (priority:{})", configurator.getClass(), configurator.getPriority());
+ configurator.configure(this);
+ }
+ }
+
+ public LockManager getLockManager(String lockName) {
+ return new LockManagerImpl(commandExecutor, lockName, getLockPollRate(), getEngineCfgKey());
+ }
+
+ // getters and setters
+ // //////////////////////////////////////////////////////
+
+ public abstract String getEngineName();
+
+ public ClassLoader getClassLoader() {
+ return classLoader;
+ }
+
+ public AbstractEngineConfiguration setClassLoader(ClassLoader classLoader) {
+ this.classLoader = classLoader;
+ return this;
+ }
+
+ public boolean isUseClassForNameClassLoading() {
+ return useClassForNameClassLoading;
+ }
+
+ public AbstractEngineConfiguration setUseClassForNameClassLoading(boolean useClassForNameClassLoading) {
+ this.useClassForNameClassLoading = useClassForNameClassLoading;
+ return this;
+ }
+
+ public void addEngineLifecycleListener(EngineLifecycleListener engineLifecycleListener) {
+ if (this.engineLifecycleListeners == null) {
+ this.engineLifecycleListeners = new ArrayList<>();
+ }
+ this.engineLifecycleListeners.add(engineLifecycleListener);
+ }
+
+ public List getEngineLifecycleListeners() {
+ return engineLifecycleListeners;
+ }
+
+ public AbstractEngineConfiguration setEngineLifecycleListeners(List engineLifecycleListeners) {
+ this.engineLifecycleListeners = engineLifecycleListeners;
+ return this;
+ }
+
+ public String getDatabaseType() {
+ return databaseType;
+ }
+
+ public AbstractEngineConfiguration setDatabaseType(String databaseType) {
+ this.databaseType = databaseType;
+ return this;
+ }
+
+ public DataSource getDataSource() {
+ return dataSource;
+ }
+
+ public AbstractEngineConfiguration setDataSource(DataSource dataSource) {
+ this.dataSource = dataSource;
+ return this;
+ }
+
+ public SchemaManager getSchemaManager() {
+ return schemaManager;
+ }
+
+ public AbstractEngineConfiguration setSchemaManager(SchemaManager schemaManager) {
+ this.schemaManager = schemaManager;
+ return this;
+ }
+
+ public SchemaManager getCommonSchemaManager() {
+ return commonSchemaManager;
+ }
+
+ public AbstractEngineConfiguration setCommonSchemaManager(SchemaManager commonSchemaManager) {
+ this.commonSchemaManager = commonSchemaManager;
+ return this;
+ }
+
+ public Command getSchemaManagementCmd() {
+ return schemaManagementCmd;
+ }
+
+ public AbstractEngineConfiguration setSchemaManagementCmd(Command schemaManagementCmd) {
+ this.schemaManagementCmd = schemaManagementCmd;
+ return this;
+ }
+
+ public String getJdbcDriver() {
+ return jdbcDriver;
+ }
+
+ public AbstractEngineConfiguration setJdbcDriver(String jdbcDriver) {
+ this.jdbcDriver = jdbcDriver;
+ return this;
+ }
+
+ public String getJdbcUrl() {
+ return jdbcUrl;
+ }
+
+ public AbstractEngineConfiguration setJdbcUrl(String jdbcUrl) {
+ this.jdbcUrl = jdbcUrl;
+ return this;
+ }
+
+ public String getJdbcUsername() {
+ return jdbcUsername;
+ }
+
+ public AbstractEngineConfiguration setJdbcUsername(String jdbcUsername) {
+ this.jdbcUsername = jdbcUsername;
+ return this;
+ }
+
+ public String getJdbcPassword() {
+ return jdbcPassword;
+ }
+
+ public AbstractEngineConfiguration setJdbcPassword(String jdbcPassword) {
+ this.jdbcPassword = jdbcPassword;
+ return this;
+ }
+
+ public int getJdbcMaxActiveConnections() {
+ return jdbcMaxActiveConnections;
+ }
+
+ public AbstractEngineConfiguration setJdbcMaxActiveConnections(int jdbcMaxActiveConnections) {
+ this.jdbcMaxActiveConnections = jdbcMaxActiveConnections;
+ return this;
+ }
+
+ public int getJdbcMaxIdleConnections() {
+ return jdbcMaxIdleConnections;
+ }
+
+ public AbstractEngineConfiguration setJdbcMaxIdleConnections(int jdbcMaxIdleConnections) {
+ this.jdbcMaxIdleConnections = jdbcMaxIdleConnections;
+ return this;
+ }
+
+ public int getJdbcMaxCheckoutTime() {
+ return jdbcMaxCheckoutTime;
+ }
+
+ public AbstractEngineConfiguration setJdbcMaxCheckoutTime(int jdbcMaxCheckoutTime) {
+ this.jdbcMaxCheckoutTime = jdbcMaxCheckoutTime;
+ return this;
+ }
+
+ public int getJdbcMaxWaitTime() {
+ return jdbcMaxWaitTime;
+ }
+
+ public AbstractEngineConfiguration setJdbcMaxWaitTime(int jdbcMaxWaitTime) {
+ this.jdbcMaxWaitTime = jdbcMaxWaitTime;
+ return this;
+ }
+
+ public boolean isJdbcPingEnabled() {
+ return jdbcPingEnabled;
+ }
+
+ public AbstractEngineConfiguration setJdbcPingEnabled(boolean jdbcPingEnabled) {
+ this.jdbcPingEnabled = jdbcPingEnabled;
+ return this;
+ }
+
+ public int getJdbcPingConnectionNotUsedFor() {
+ return jdbcPingConnectionNotUsedFor;
+ }
+
+ public AbstractEngineConfiguration setJdbcPingConnectionNotUsedFor(int jdbcPingConnectionNotUsedFor) {
+ this.jdbcPingConnectionNotUsedFor = jdbcPingConnectionNotUsedFor;
+ return this;
+ }
+
+ public int getJdbcDefaultTransactionIsolationLevel() {
+ return jdbcDefaultTransactionIsolationLevel;
+ }
+
+ public AbstractEngineConfiguration setJdbcDefaultTransactionIsolationLevel(int jdbcDefaultTransactionIsolationLevel) {
+ this.jdbcDefaultTransactionIsolationLevel = jdbcDefaultTransactionIsolationLevel;
+ return this;
+ }
+
+ public String getJdbcPingQuery() {
+ return jdbcPingQuery;
+ }
+
+ public AbstractEngineConfiguration setJdbcPingQuery(String jdbcPingQuery) {
+ this.jdbcPingQuery = jdbcPingQuery;
+ return this;
+ }
+
+ public String getDataSourceJndiName() {
+ return dataSourceJndiName;
+ }
+
+ public AbstractEngineConfiguration setDataSourceJndiName(String dataSourceJndiName) {
+ this.dataSourceJndiName = dataSourceJndiName;
+ return this;
+ }
+
+ public CommandConfig getSchemaCommandConfig() {
+ return schemaCommandConfig;
+ }
+
+ public AbstractEngineConfiguration setSchemaCommandConfig(CommandConfig schemaCommandConfig) {
+ this.schemaCommandConfig = schemaCommandConfig;
+ return this;
+ }
+
+ public boolean isTransactionsExternallyManaged() {
+ return transactionsExternallyManaged;
+ }
+
+ public AbstractEngineConfiguration setTransactionsExternallyManaged(boolean transactionsExternallyManaged) {
+ this.transactionsExternallyManaged = transactionsExternallyManaged;
+ return this;
+ }
+
+ public Map getBeans() {
+ return beans;
+ }
+
+ public AbstractEngineConfiguration setBeans(Map beans) {
+ this.beans = beans;
+ return this;
+ }
+
+ public IdGenerator getIdGenerator() {
+ return idGenerator;
+ }
+
+ public AbstractEngineConfiguration setIdGenerator(IdGenerator idGenerator) {
+ this.idGenerator = idGenerator;
+ return this;
+ }
+
+ public boolean isUsePrefixId() {
+ return usePrefixId;
+ }
+
+ public AbstractEngineConfiguration setUsePrefixId(boolean usePrefixId) {
+ this.usePrefixId = usePrefixId;
+ return this;
+ }
+
+ public String getXmlEncoding() {
+ return xmlEncoding;
+ }
+
+ public AbstractEngineConfiguration setXmlEncoding(String xmlEncoding) {
+ this.xmlEncoding = xmlEncoding;
+ return this;
+ }
+
+ public CommandConfig getDefaultCommandConfig() {
+ return defaultCommandConfig;
+ }
+
+ public AbstractEngineConfiguration setDefaultCommandConfig(CommandConfig defaultCommandConfig) {
+ this.defaultCommandConfig = defaultCommandConfig;
+ return this;
+ }
+
+ public CommandExecutor getCommandExecutor() {
+ return commandExecutor;
+ }
+
+ public AbstractEngineConfiguration setCommandExecutor(CommandExecutor commandExecutor) {
+ this.commandExecutor = commandExecutor;
+ return this;
+ }
+
+ public CommandContextFactory getCommandContextFactory() {
+ return commandContextFactory;
+ }
+
+ public AbstractEngineConfiguration setCommandContextFactory(CommandContextFactory commandContextFactory) {
+ this.commandContextFactory = commandContextFactory;
+ return this;
+ }
+
+ public CommandInterceptor getCommandInvoker() {
+ return commandInvoker;
+ }
+
+ public AbstractEngineConfiguration setCommandInvoker(CommandInterceptor commandInvoker) {
+ this.commandInvoker = commandInvoker;
+ return this;
+ }
+
+ public AgendaOperationRunner getAgendaOperationRunner() {
+ return agendaOperationRunner;
+ }
+
+ public AbstractEngineConfiguration setAgendaOperationRunner(AgendaOperationRunner agendaOperationRunner) {
+ this.agendaOperationRunner = agendaOperationRunner;
+ return this;
+ }
+
+ public List getCustomPreCommandInterceptors() {
+ return customPreCommandInterceptors;
+ }
+
+ public AbstractEngineConfiguration setCustomPreCommandInterceptors(List customPreCommandInterceptors) {
+ this.customPreCommandInterceptors = customPreCommandInterceptors;
+ return this;
+ }
+
+ public List getCustomPostCommandInterceptors() {
+ return customPostCommandInterceptors;
+ }
+
+ public AbstractEngineConfiguration setCustomPostCommandInterceptors(List customPostCommandInterceptors) {
+ this.customPostCommandInterceptors = customPostCommandInterceptors;
+ return this;
+ }
+
+ public List getCommandInterceptors() {
+ return commandInterceptors;
+ }
+
+ public AbstractEngineConfiguration setCommandInterceptors(List commandInterceptors) {
+ this.commandInterceptors = commandInterceptors;
+ return this;
+ }
+
+ public Map getEngineConfigurations() {
+ return engineConfigurations;
+ }
+
+ public AbstractEngineConfiguration setEngineConfigurations(Map engineConfigurations) {
+ this.engineConfigurations = engineConfigurations;
+ return this;
+ }
+
+ public void addEngineConfiguration(String key, String scopeType, AbstractEngineConfiguration engineConfiguration) {
+ if (engineConfigurations == null) {
+ engineConfigurations = new HashMap<>();
+ }
+ engineConfigurations.put(key, engineConfiguration);
+ engineConfigurations.put(scopeType, engineConfiguration);
+ }
+
+ public Map getServiceConfigurations() {
+ return serviceConfigurations;
+ }
+
+ public AbstractEngineConfiguration setServiceConfigurations(Map serviceConfigurations) {
+ this.serviceConfigurations = serviceConfigurations;
+ return this;
+ }
+
+ public void addServiceConfiguration(String key, AbstractServiceConfiguration serviceConfiguration) {
+ if (serviceConfigurations == null) {
+ serviceConfigurations = new HashMap<>();
+ }
+ serviceConfigurations.put(key, serviceConfiguration);
+ }
+
+ public Map getEventRegistryEventConsumers() {
+ return eventRegistryEventConsumers;
+ }
+
+ public AbstractEngineConfiguration setEventRegistryEventConsumers(Map eventRegistryEventConsumers) {
+ this.eventRegistryEventConsumers = eventRegistryEventConsumers;
+ return this;
+ }
+
+ public void addEventRegistryEventConsumer(String key, EventRegistryEventConsumer eventRegistryEventConsumer) {
+ if (eventRegistryEventConsumers == null) {
+ eventRegistryEventConsumers = new HashMap<>();
+ }
+ eventRegistryEventConsumers.put(key, eventRegistryEventConsumer);
+ }
+
+ public AbstractEngineConfiguration setDefaultCommandInterceptors(Collection extends CommandInterceptor> defaultCommandInterceptors) {
+ this.defaultCommandInterceptors = defaultCommandInterceptors;
+ return this;
+ }
+
+ public SqlSessionFactory getSqlSessionFactory() {
+ return sqlSessionFactory;
+ }
+
+ public AbstractEngineConfiguration setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
+ this.sqlSessionFactory = sqlSessionFactory;
+ return this;
+ }
+
+ public boolean isDbHistoryUsed() {
+ return isDbHistoryUsed;
+ }
+
+ public AbstractEngineConfiguration setDbHistoryUsed(boolean isDbHistoryUsed) {
+ this.isDbHistoryUsed = isDbHistoryUsed;
+ return this;
+ }
+
+ public DbSqlSessionFactory getDbSqlSessionFactory() {
+ return dbSqlSessionFactory;
+ }
+
+ public AbstractEngineConfiguration setDbSqlSessionFactory(DbSqlSessionFactory dbSqlSessionFactory) {
+ this.dbSqlSessionFactory = dbSqlSessionFactory;
+ return this;
+ }
+
+ public TransactionFactory getTransactionFactory() {
+ return transactionFactory;
+ }
+
+ public AbstractEngineConfiguration setTransactionFactory(TransactionFactory transactionFactory) {
+ this.transactionFactory = transactionFactory;
+ return this;
+ }
+
+ public TransactionContextFactory getTransactionContextFactory() {
+ return transactionContextFactory;
+ }
+
+ public AbstractEngineConfiguration setTransactionContextFactory(TransactionContextFactory transactionContextFactory) {
+ this.transactionContextFactory = transactionContextFactory;
+ return this;
+ }
+
+ public int getMaxNrOfStatementsInBulkInsert() {
+ return maxNrOfStatementsInBulkInsert;
+ }
+
+ public AbstractEngineConfiguration setMaxNrOfStatementsInBulkInsert(int maxNrOfStatementsInBulkInsert) {
+ this.maxNrOfStatementsInBulkInsert = maxNrOfStatementsInBulkInsert;
+ return this;
+ }
+
+ public boolean isBulkInsertEnabled() {
+ return isBulkInsertEnabled;
+ }
+
+ public AbstractEngineConfiguration setBulkInsertEnabled(boolean isBulkInsertEnabled) {
+ this.isBulkInsertEnabled = isBulkInsertEnabled;
+ return this;
+ }
+
+ public Set> getCustomMybatisMappers() {
+ return customMybatisMappers;
+ }
+
+ public AbstractEngineConfiguration setCustomMybatisMappers(Set> customMybatisMappers) {
+ this.customMybatisMappers = customMybatisMappers;
+ return this;
+ }
+
+ public Set getCustomMybatisXMLMappers() {
+ return customMybatisXMLMappers;
+ }
+
+ public AbstractEngineConfiguration setCustomMybatisXMLMappers(Set customMybatisXMLMappers) {
+ this.customMybatisXMLMappers = customMybatisXMLMappers;
+ return this;
+ }
+
+ public Set getDependentEngineMyBatisXmlMappers() {
+ return dependentEngineMyBatisXmlMappers;
+ }
+
+ public AbstractEngineConfiguration setCustomMybatisInterceptors(List customMybatisInterceptors) {
+ this.customMybatisInterceptors = customMybatisInterceptors;
+ return this;
+ }
+
+ public List getCustomMybatisInterceptors() {
+ return customMybatisInterceptors;
+ }
+
+ public AbstractEngineConfiguration setDependentEngineMyBatisXmlMappers(Set dependentEngineMyBatisXmlMappers) {
+ this.dependentEngineMyBatisXmlMappers = dependentEngineMyBatisXmlMappers;
+ return this;
+ }
+
+ public List getDependentEngineMybatisTypeAliasConfigs() {
+ return dependentEngineMybatisTypeAliasConfigs;
+ }
+
+ public AbstractEngineConfiguration setDependentEngineMybatisTypeAliasConfigs(List dependentEngineMybatisTypeAliasConfigs) {
+ this.dependentEngineMybatisTypeAliasConfigs = dependentEngineMybatisTypeAliasConfigs;
+ return this;
+ }
+
+ public List getDependentEngineMybatisTypeHandlerConfigs() {
+ return dependentEngineMybatisTypeHandlerConfigs;
+ }
+
+ public AbstractEngineConfiguration setDependentEngineMybatisTypeHandlerConfigs(List dependentEngineMybatisTypeHandlerConfigs) {
+ this.dependentEngineMybatisTypeHandlerConfigs = dependentEngineMybatisTypeHandlerConfigs;
+ return this;
+ }
+
+ public List getCustomSessionFactories() {
+ return customSessionFactories;
+ }
+
+ public AbstractEngineConfiguration addCustomSessionFactory(SessionFactory sessionFactory) {
+ if (customSessionFactories == null) {
+ customSessionFactories = new ArrayList<>();
+ }
+ customSessionFactories.add(sessionFactory);
+ return this;
+ }
+
+ public AbstractEngineConfiguration setCustomSessionFactories(List customSessionFactories) {
+ this.customSessionFactories = customSessionFactories;
+ return this;
+ }
+
+ public boolean isUsingRelationalDatabase() {
+ return usingRelationalDatabase;
+ }
+
+ public AbstractEngineConfiguration setUsingRelationalDatabase(boolean usingRelationalDatabase) {
+ this.usingRelationalDatabase = usingRelationalDatabase;
+ return this;
+ }
+
+ public boolean isUsingSchemaMgmt() {
+ return usingSchemaMgmt;
+ }
+
+ public AbstractEngineConfiguration setUsingSchemaMgmt(boolean usingSchema) {
+ this.usingSchemaMgmt = usingSchema;
+ return this;
+ }
+
+ public String getDatabaseTablePrefix() {
+ return databaseTablePrefix;
+ }
+
+ public AbstractEngineConfiguration setDatabaseTablePrefix(String databaseTablePrefix) {
+ this.databaseTablePrefix = databaseTablePrefix;
+ return this;
+ }
+
+ public String getDatabaseWildcardEscapeCharacter() {
+ return databaseWildcardEscapeCharacter;
+ }
+
+ public AbstractEngineConfiguration setDatabaseWildcardEscapeCharacter(String databaseWildcardEscapeCharacter) {
+ this.databaseWildcardEscapeCharacter = databaseWildcardEscapeCharacter;
+ return this;
+ }
+
+ public String getDatabaseCatalog() {
+ return databaseCatalog;
+ }
+
+ public AbstractEngineConfiguration setDatabaseCatalog(String databaseCatalog) {
+ this.databaseCatalog = databaseCatalog;
+ return this;
+ }
+
+ public String getDatabaseSchema() {
+ return databaseSchema;
+ }
+
+ public AbstractEngineConfiguration setDatabaseSchema(String databaseSchema) {
+ this.databaseSchema = databaseSchema;
+ return this;
+ }
+
+ public boolean isTablePrefixIsSchema() {
+ return tablePrefixIsSchema;
+ }
+
+ public AbstractEngineConfiguration setTablePrefixIsSchema(boolean tablePrefixIsSchema) {
+ this.tablePrefixIsSchema = tablePrefixIsSchema;
+ return this;
+ }
+
+ public boolean isAlwaysLookupLatestDefinitionVersion() {
+ return alwaysLookupLatestDefinitionVersion;
+ }
+
+ public AbstractEngineConfiguration setAlwaysLookupLatestDefinitionVersion(boolean alwaysLookupLatestDefinitionVersion) {
+ this.alwaysLookupLatestDefinitionVersion = alwaysLookupLatestDefinitionVersion;
+ return this;
+ }
+
+ public boolean isFallbackToDefaultTenant() {
+ return fallbackToDefaultTenant;
+ }
+
+ public AbstractEngineConfiguration setFallbackToDefaultTenant(boolean fallbackToDefaultTenant) {
+ this.fallbackToDefaultTenant = fallbackToDefaultTenant;
+ return this;
+ }
+
+ /**
+ * @return name of the default tenant
+ * @deprecated use {@link AbstractEngineConfiguration#getDefaultTenantProvider()} instead
+ */
+ @Deprecated
+ public String getDefaultTenantValue() {
+ return getDefaultTenantProvider().getDefaultTenant(null, null, null);
+ }
+
+ public AbstractEngineConfiguration setDefaultTenantValue(String defaultTenantValue) {
+ this.defaultTenantProvider = (tenantId, scope, scopeKey) -> defaultTenantValue;
+ return this;
+ }
+
+ public DefaultTenantProvider getDefaultTenantProvider() {
+ return defaultTenantProvider;
+ }
+
+ public AbstractEngineConfiguration setDefaultTenantProvider(DefaultTenantProvider defaultTenantProvider) {
+ this.defaultTenantProvider = defaultTenantProvider;
+ return this;
+ }
+
+ public boolean isEnableLogSqlExecutionTime() {
+ return enableLogSqlExecutionTime;
+ }
+
+ public void setEnableLogSqlExecutionTime(boolean enableLogSqlExecutionTime) {
+ this.enableLogSqlExecutionTime = enableLogSqlExecutionTime;
+ }
+
+ public Map, SessionFactory> getSessionFactories() {
+ return sessionFactories;
+ }
+
+ public AbstractEngineConfiguration setSessionFactories(Map, SessionFactory> sessionFactories) {
+ this.sessionFactories = sessionFactories;
+ return this;
+ }
+
+ public String getDatabaseSchemaUpdate() {
+ return databaseSchemaUpdate;
+ }
+
+ public AbstractEngineConfiguration setDatabaseSchemaUpdate(String databaseSchemaUpdate) {
+ this.databaseSchemaUpdate = databaseSchemaUpdate;
+ return this;
+ }
+
+ public boolean isUseLockForDatabaseSchemaUpdate() {
+ return useLockForDatabaseSchemaUpdate;
+ }
+
+ public AbstractEngineConfiguration setUseLockForDatabaseSchemaUpdate(boolean useLockForDatabaseSchemaUpdate) {
+ this.useLockForDatabaseSchemaUpdate = useLockForDatabaseSchemaUpdate;
+ return this;
+ }
+
+ public boolean isEnableEventDispatcher() {
+ return enableEventDispatcher;
+ }
+
+ public AbstractEngineConfiguration setEnableEventDispatcher(boolean enableEventDispatcher) {
+ this.enableEventDispatcher = enableEventDispatcher;
+ return this;
+ }
+
+ public FlowableEventDispatcher getEventDispatcher() {
+ return eventDispatcher;
+ }
+
+ public AbstractEngineConfiguration setEventDispatcher(FlowableEventDispatcher eventDispatcher) {
+ this.eventDispatcher = eventDispatcher;
+ return this;
+ }
+
+ public List getEventListeners() {
+ return eventListeners;
+ }
+
+ public AbstractEngineConfiguration setEventListeners(List eventListeners) {
+ this.eventListeners = eventListeners;
+ return this;
+ }
+
+ public Map> getTypedEventListeners() {
+ return typedEventListeners;
+ }
+
+ public AbstractEngineConfiguration setTypedEventListeners(Map> typedEventListeners) {
+ this.typedEventListeners = typedEventListeners;
+ return this;
+ }
+
+ public List getAdditionalEventDispatchActions() {
+ return additionalEventDispatchActions;
+ }
+
+ public AbstractEngineConfiguration setAdditionalEventDispatchActions(List additionalEventDispatchActions) {
+ this.additionalEventDispatchActions = additionalEventDispatchActions;
+ return this;
+ }
+
+ public void initEventDispatcher() {
+ if (this.eventDispatcher == null) {
+ this.eventDispatcher = new FlowableEventDispatcherImpl();
+ }
+
+ initAdditionalEventDispatchActions();
+
+ this.eventDispatcher.setEnabled(enableEventDispatcher);
+
+ initEventListeners();
+ initTypedEventListeners();
+ }
+
+ protected void initEventListeners() {
+ if (eventListeners != null) {
+ for (FlowableEventListener listenerToAdd : eventListeners) {
+ this.eventDispatcher.addEventListener(listenerToAdd);
+ }
+ }
+ }
+
+ protected void initAdditionalEventDispatchActions() {
+ if (this.additionalEventDispatchActions == null) {
+ this.additionalEventDispatchActions = new ArrayList<>();
+ }
+ }
+
+ protected void initTypedEventListeners() {
+ if (typedEventListeners != null) {
+ for (Map.Entry> listenersToAdd : typedEventListeners.entrySet()) {
+ // Extract types from the given string
+ FlowableEngineEventType[] types = FlowableEngineEventType.getTypesFromString(listenersToAdd.getKey());
+
+ for (FlowableEventListener listenerToAdd : listenersToAdd.getValue()) {
+ this.eventDispatcher.addEventListener(listenerToAdd, types);
+ }
+ }
+ }
+ }
+
+ public boolean isLoggingSessionEnabled() {
+ return loggingListener != null;
+ }
+
+ public LoggingListener getLoggingListener() {
+ return loggingListener;
+ }
+
+ public void setLoggingListener(LoggingListener loggingListener) {
+ this.loggingListener = loggingListener;
+ }
+
+ public Clock getClock() {
+ return clock;
+ }
+
+ public AbstractEngineConfiguration setClock(Clock clock) {
+ this.clock = clock;
+ return this;
+ }
+
+ public ObjectMapper getObjectMapper() {
+ return objectMapper;
+ }
+
+ public AbstractEngineConfiguration setObjectMapper(ObjectMapper objectMapper) {
+ this.objectMapper = objectMapper;
+ return this;
+ }
+
+ public int getMaxLengthString() {
+ if (maxLengthStringVariableType == -1) {
+ if ("oracle".equalsIgnoreCase(databaseType)) {
+ return DEFAULT_ORACLE_MAX_LENGTH_STRING;
+ } else {
+ return DEFAULT_GENERIC_MAX_LENGTH_STRING;
+ }
+ } else {
+ return maxLengthStringVariableType;
+ }
+ }
+
+ public int getMaxLengthStringVariableType() {
+ return maxLengthStringVariableType;
+ }
+
+ public AbstractEngineConfiguration setMaxLengthStringVariableType(int maxLengthStringVariableType) {
+ this.maxLengthStringVariableType = maxLengthStringVariableType;
+ return this;
+ }
+
+ public PropertyDataManager getPropertyDataManager() {
+ return propertyDataManager;
+ }
+
+ public Duration getLockPollRate() {
+ return lockPollRate;
+ }
+
+ public AbstractEngineConfiguration setLockPollRate(Duration lockPollRate) {
+ this.lockPollRate = lockPollRate;
+ return this;
+ }
+
+ public Duration getSchemaLockWaitTime() {
+ return schemaLockWaitTime;
+ }
+
+ public void setSchemaLockWaitTime(Duration schemaLockWaitTime) {
+ this.schemaLockWaitTime = schemaLockWaitTime;
+ }
+
+ public AbstractEngineConfiguration setPropertyDataManager(PropertyDataManager propertyDataManager) {
+ this.propertyDataManager = propertyDataManager;
+ return this;
+ }
+
+ public PropertyEntityManager getPropertyEntityManager() {
+ return propertyEntityManager;
+ }
+
+ public AbstractEngineConfiguration setPropertyEntityManager(PropertyEntityManager propertyEntityManager) {
+ this.propertyEntityManager = propertyEntityManager;
+ return this;
+ }
+
+ public ByteArrayDataManager getByteArrayDataManager() {
+ return byteArrayDataManager;
+ }
+
+ public AbstractEngineConfiguration setByteArrayDataManager(ByteArrayDataManager byteArrayDataManager) {
+ this.byteArrayDataManager = byteArrayDataManager;
+ return this;
+ }
+
+ public ByteArrayEntityManager getByteArrayEntityManager() {
+ return byteArrayEntityManager;
+ }
+
+ public AbstractEngineConfiguration setByteArrayEntityManager(ByteArrayEntityManager byteArrayEntityManager) {
+ this.byteArrayEntityManager = byteArrayEntityManager;
+ return this;
+ }
+
+ public TableDataManager getTableDataManager() {
+ return tableDataManager;
+ }
+
+ public AbstractEngineConfiguration setTableDataManager(TableDataManager tableDataManager) {
+ this.tableDataManager = tableDataManager;
+ return this;
+ }
+
+ public List getDeployers() {
+ return deployers;
+ }
+
+ public AbstractEngineConfiguration setDeployers(List deployers) {
+ this.deployers = deployers;
+ return this;
+ }
+
+ public List getCustomPreDeployers() {
+ return customPreDeployers;
+ }
+
+ public AbstractEngineConfiguration setCustomPreDeployers(List customPreDeployers) {
+ this.customPreDeployers = customPreDeployers;
+ return this;
+ }
+
+ public List getCustomPostDeployers() {
+ return customPostDeployers;
+ }
+
+ public AbstractEngineConfiguration setCustomPostDeployers(List customPostDeployers) {
+ this.customPostDeployers = customPostDeployers;
+ return this;
+ }
+
+ public boolean isEnableConfiguratorServiceLoader() {
+ return enableConfiguratorServiceLoader;
+ }
+
+ public AbstractEngineConfiguration setEnableConfiguratorServiceLoader(boolean enableConfiguratorServiceLoader) {
+ this.enableConfiguratorServiceLoader = enableConfiguratorServiceLoader;
+ return this;
+ }
+
+ public List getConfigurators() {
+ return configurators;
+ }
+
+ public AbstractEngineConfiguration addConfigurator(EngineConfigurator configurator) {
+ if (configurators == null) {
+ configurators = new ArrayList<>();
+ }
+ configurators.add(configurator);
+ return this;
+ }
+
+ /**
+ * @return All {@link EngineConfigurator} instances. Will only contain values after init of the engine.
+ * Use the {@link #getConfigurators()} or {@link #addConfigurator(EngineConfigurator)} methods otherwise.
+ */
+ public List getAllConfigurators() {
+ return allConfigurators;
+ }
+
+ public AbstractEngineConfiguration setConfigurators(List configurators) {
+ this.configurators = configurators;
+ return this;
+ }
+
+ public EngineConfigurator getIdmEngineConfigurator() {
+ return idmEngineConfigurator;
+ }
+
+ public AbstractEngineConfiguration setIdmEngineConfigurator(EngineConfigurator idmEngineConfigurator) {
+ this.idmEngineConfigurator = idmEngineConfigurator;
+ return this;
+ }
+
+ public EngineConfigurator getEventRegistryConfigurator() {
+ return eventRegistryConfigurator;
+ }
+
+ public AbstractEngineConfiguration setEventRegistryConfigurator(EngineConfigurator eventRegistryConfigurator) {
+ this.eventRegistryConfigurator = eventRegistryConfigurator;
+ return this;
+ }
+
+ public AbstractEngineConfiguration setForceCloseMybatisConnectionPool(boolean forceCloseMybatisConnectionPool) {
+ this.forceCloseMybatisConnectionPool = forceCloseMybatisConnectionPool;
+ return this;
+ }
+
+ public boolean isForceCloseMybatisConnectionPool() {
+ return forceCloseMybatisConnectionPool;
+ }
+}
diff --git a/sql/dm/flowable-patch/src/main/resources/META-INF/package-info.md b/sql/dm/flowable-patch/src/main/resources/META-INF/package-info.md
new file mode 100644
index 000000000..1932c7a3f
--- /dev/null
+++ b/sql/dm/flowable-patch/src/main/resources/META-INF/package-info.md
@@ -0,0 +1 @@
+防止IDEA将`.`和`/`混为一谈
\ No newline at end of file
diff --git a/sql/dm/flowable-patch/src/main/resources/META-INF/services/liquibase.database.Database b/sql/dm/flowable-patch/src/main/resources/META-INF/services/liquibase.database.Database
new file mode 100644
index 000000000..efbcfcca2
--- /dev/null
+++ b/sql/dm/flowable-patch/src/main/resources/META-INF/services/liquibase.database.Database
@@ -0,0 +1,21 @@
+liquibase.database.core.CockroachDatabase
+liquibase.database.core.DB2Database
+liquibase.database.core.Db2zDatabase
+liquibase.database.core.DerbyDatabase
+liquibase.database.core.Firebird3Database
+liquibase.database.core.FirebirdDatabase
+liquibase.database.core.H2Database
+liquibase.database.core.HsqlDatabase
+liquibase.database.core.InformixDatabase
+liquibase.database.core.Ingres9Database
+liquibase.database.core.MSSQLDatabase
+liquibase.database.core.MariaDBDatabase
+liquibase.database.core.MockDatabase
+liquibase.database.core.MySQLDatabase
+liquibase.database.core.OracleDatabase
+liquibase.database.core.PostgresDatabase
+liquibase.database.core.SQLiteDatabase
+liquibase.database.core.SybaseASADatabase
+liquibase.database.core.SybaseDatabase
+liquibase.database.core.DmDatabase
+liquibase.database.core.UnsupportedDatabase
diff --git a/sql/mysql/crm.sql b/sql/mysql/optinal/crm.sql
similarity index 100%
rename from sql/mysql/crm.sql
rename to sql/mysql/optinal/crm.sql
diff --git a/sql/mysql/crm_data.sql b/sql/mysql/optinal/crm_data.sql
similarity index 100%
rename from sql/mysql/crm_data.sql
rename to sql/mysql/optinal/crm_data.sql
diff --git a/sql/mysql/crm_menu.sql b/sql/mysql/optinal/crm_menu.sql
similarity index 100%
rename from sql/mysql/crm_menu.sql
rename to sql/mysql/optinal/crm_menu.sql
diff --git a/sql/mysql/mall.sql b/sql/mysql/optinal/mall.sql
similarity index 100%
rename from sql/mysql/mall.sql
rename to sql/mysql/optinal/mall.sql
diff --git a/sql/mysql/pay_wallet.sql b/sql/mysql/optinal/pay_wallet.sql
similarity index 96%
rename from sql/mysql/pay_wallet.sql
rename to sql/mysql/optinal/pay_wallet.sql
index 7c6c04003..1e9f1d255 100644
--- a/sql/mysql/pay_wallet.sql
+++ b/sql/mysql/optinal/pay_wallet.sql
@@ -246,3 +246,11 @@ VALUES (
'转账订单', '', 2, 3, 1117,
'transfer', 'ep:credit-card', 'pay/transfer/index', 0, 'PayTransfer'
);
+
+-- 转账通知脚本
+
+ALTER TABLE `pay_app`
+ ADD COLUMN `transfer_notify_url` varchar(1024) NOT NULL COMMENT '转账结果的回调地址' AFTER `refund_notify_url`;
+ALTER TABLE `pay_notify_task`
+ MODIFY COLUMN `merchant_order_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '商户订单编号' AFTER `status`,
+ ADD COLUMN `merchant_transfer_id` varchar(64) COMMENT '商户转账单编号' AFTER `merchant_order_id`;
\ No newline at end of file
diff --git a/sql/mysql/ruoyi-vue-pro.sql b/sql/mysql/ruoyi-vue-pro.sql
index 025045872..98acf8be3 100644
--- a/sql/mysql/ruoyi-vue-pro.sql
+++ b/sql/mysql/ruoyi-vue-pro.sql
@@ -3,15 +3,15 @@
Source Server : 127.0.0.1 MySQL
Source Server Type : MySQL
- Source Server Version : 80034
- Source Host : localhost:3306
+ Source Server Version : 80200 (8.2.0)
+ Source Host : 127.0.0.1:3306
Source Schema : ruoyi-vue-pro
Target Server Type : MySQL
- Target Server Version : 80034
+ Target Server Version : 80200 (8.2.0)
File Encoding : 65001
- Date: 18/11/2023 17:48:18
+ Date: 30/11/2023 21:13:06
*/
SET NAMES utf8mb4;
@@ -385,7 +385,7 @@ CREATE TABLE `infra_api_error_log` (
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1964 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志';
+) ENGINE = InnoDB AUTO_INCREMENT = 2018 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志';
-- ----------------------------
-- Records of infra_api_error_log
@@ -423,7 +423,7 @@ CREATE TABLE `infra_codegen_column` (
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1905 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表字段定义';
+) ENGINE = InnoDB AUTO_INCREMENT = 2000 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表字段定义';
-- ----------------------------
-- Records of infra_codegen_column
@@ -461,7 +461,7 @@ CREATE TABLE `infra_codegen_table` (
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 146 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表定义';
+) ENGINE = InnoDB AUTO_INCREMENT = 155 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表定义';
-- ----------------------------
-- Records of infra_codegen_table
@@ -535,8 +535,8 @@ CREATE TABLE `infra_demo01_contact` (
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '名字',
`sex` tinyint(1) NOT NULL COMMENT '性别',
`birthday` datetime NOT NULL COMMENT '出生年',
- `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '简介',
- `avatar` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '头像',
+ `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '简介',
+ `avatar` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '头像',
`creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
@@ -568,7 +568,7 @@ CREATE TABLE `infra_demo02_category` (
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '示例分类表';
+) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '示例分类表';
-- ----------------------------
-- Records of infra_demo02_category
@@ -579,6 +579,7 @@ INSERT INTO `infra_demo02_category` (`id`, `name`, `parent_id`, `creator`, `crea
INSERT INTO `infra_demo02_category` (`id`, `name`, `parent_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3, '怪怪', 0, '1', '2023-11-16 20:24:32', '1', '2023-11-16 20:24:32', b'0', 1);
INSERT INTO `infra_demo02_category` (`id`, `name`, `parent_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4, '小番茄', 2, '1', '2023-11-16 20:24:39', '1', '2023-11-16 20:24:39', b'0', 1);
INSERT INTO `infra_demo02_category` (`id`, `name`, `parent_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (5, '大番茄', 2, '1', '2023-11-16 20:24:46', '1', '2023-11-16 20:24:46', b'0', 1);
+INSERT INTO `infra_demo02_category` (`id`, `name`, `parent_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6, '11', 3, '1', '2023-11-24 19:29:34', '1', '2023-11-24 19:29:34', b'0', 1);
COMMIT;
-- ----------------------------
@@ -651,7 +652,7 @@ CREATE TABLE `infra_demo03_student` (
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '名字',
`sex` tinyint NOT NULL COMMENT '性别',
`birthday` datetime NOT NULL COMMENT '出生日期',
- `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '简介',
+ `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '简介',
`creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
@@ -765,7 +766,7 @@ CREATE TABLE `infra_job` (
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 27 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '定时任务表';
+) ENGINE = InnoDB AUTO_INCREMENT = 28 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '定时任务表';
-- ----------------------------
-- Records of infra_job
@@ -806,7 +807,7 @@ CREATE TABLE `infra_job_log` (
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 232 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '定时任务日志表';
+) ENGINE = InnoDB AUTO_INCREMENT = 233 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '定时任务日志表';
-- ----------------------------
-- Records of infra_job_log
@@ -834,7 +835,7 @@ CREATE TABLE `member_address` (
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_userId`(`user_id` ASC) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 24 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '用户收件地址';
+) ENGINE = InnoDB AUTO_INCREMENT = 25 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '用户收件地址';
-- ----------------------------
-- Records of member_address
@@ -893,7 +894,7 @@ CREATE TABLE `member_experience_record` (
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_user_id`(`user_id` ASC) USING BTREE COMMENT '会员经验记录-用户编号',
INDEX `idx_user_biz_type`(`user_id` ASC, `biz_type` ASC) USING BTREE COMMENT '会员经验记录-用户业务类型'
-) ENGINE = InnoDB AUTO_INCREMENT = 41 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '会员经验记录';
+) ENGINE = InnoDB AUTO_INCREMENT = 42 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '会员经验记录';
-- ----------------------------
-- Records of member_experience_record
@@ -951,7 +952,7 @@ CREATE TABLE `member_group` (
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户分组';
+) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户分组';
-- ----------------------------
-- Records of member_group
@@ -979,7 +980,7 @@ CREATE TABLE `member_level` (
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '会员等级';
+) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '会员等级';
-- ----------------------------
-- Records of member_level
@@ -1009,7 +1010,7 @@ CREATE TABLE `member_level_record` (
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_user_id`(`user_id` ASC) USING BTREE COMMENT '会员等级记录-用户编号'
-) ENGINE = InnoDB AUTO_INCREMENT = 20 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '会员等级记录';
+) ENGINE = InnoDB AUTO_INCREMENT = 21 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '会员等级记录';
-- ----------------------------
-- Records of member_level_record
@@ -1039,7 +1040,7 @@ CREATE TABLE `member_point_record` (
PRIMARY KEY (`id`) USING BTREE,
INDEX `index_userId`(`user_id` ASC) USING BTREE,
INDEX `index_title`(`title` ASC) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 60 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户积分记录';
+) ENGINE = InnoDB AUTO_INCREMENT = 61 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户积分记录';
-- ----------------------------
-- Records of member_point_record
@@ -1179,7 +1180,7 @@ CREATE TABLE `member_tag` (
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '会员标签';
+) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '会员标签';
-- ----------------------------
-- Records of member_tag
@@ -1221,7 +1222,7 @@ CREATE TABLE `member_user` (
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 249 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '会员用户';
+) ENGINE = InnoDB AUTO_INCREMENT = 250 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '会员用户';
-- ----------------------------
-- Records of member_user
@@ -1289,7 +1290,7 @@ CREATE TABLE `system_dict_data` (
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1447 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表';
+) ENGINE = InnoDB AUTO_INCREMENT = 1455 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表';
-- ----------------------------
-- Records of system_dict_data
@@ -1604,6 +1605,14 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1444, 10, '主表(标准模式)', '10', 'infra_codegen_template_type', 0, 'default', '', '', '1', '2023-11-14 12:32:49', '1', '2023-11-14 12:32:49', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1445, 11, '主表(ERP 模式)', '11', 'infra_codegen_template_type', 0, 'default', '', '', '1', '2023-11-14 12:33:05', '1', '2023-11-14 12:33:05', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1446, 12, '主表(内嵌模式)', '12', 'infra_codegen_template_type', 0, '', '', '', '1', '2023-11-14 12:33:31', '1', '2023-11-14 12:33:31', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1447, 1, '负责人', '1', 'crm_permission_level', 0, 'default', '', '', '1', '2023-11-30 09:53:12', '1', '2023-11-30 09:53:12', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1448, 2, '只读', '2', 'crm_permission_level', 0, '', '', '', '1', '2023-11-30 09:53:29', '1', '2023-11-30 09:53:29', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1449, 3, '读写', '3', 'crm_permission_level', 0, '', '', '', '1', '2023-11-30 09:53:36', '1', '2023-11-30 09:53:36', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1450, 0, '未提交', '0', 'crm_audit_status', 0, '', '', '', '1', '2023-11-30 18:56:59', '1', '2023-11-30 18:56:59', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1451, 10, '审批中', '10', 'crm_audit_status', 0, '', '', '', '1', '2023-11-30 18:57:10', '1', '2023-11-30 18:57:10', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1452, 20, '审核通过', '20', 'crm_audit_status', 0, '', '', '', '1', '2023-11-30 18:57:24', '1', '2023-11-30 18:57:24', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1453, 30, '审核不通过', '30', 'crm_audit_status', 0, '', '', '', '1', '2023-11-30 18:57:32', '1', '2023-11-30 18:57:32', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1454, 40, '已取消', '40', 'crm_audit_status', 0, '', '', '', '1', '2023-11-30 18:57:42', '1', '2023-11-30 18:57:42', b'0');
COMMIT;
-- ----------------------------
@@ -1624,7 +1633,7 @@ CREATE TABLE `system_dict_type` (
`deleted_time` datetime NULL DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `dict_type`(`type` ASC) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 605 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表';
+) ENGINE = InnoDB AUTO_INCREMENT = 607 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表';
-- ----------------------------
-- Records of system_dict_type
@@ -1704,6 +1713,8 @@ INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creat
INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (600, 'Banner 位置', 'promotion_banner_position', 0, '', '1', '2023-10-08 07:24:25', '1', '2023-11-04 13:04:02', b'0', '1970-01-01 00:00:00');
INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (601, '社交类型', 'system_social_type', 0, '', '1', '2023-11-04 13:03:54', '1', '2023-11-04 13:03:54', b'0', '1970-01-01 00:00:00');
INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (604, '产品状态', 'crm_product_status', 0, '', '1', '2023-10-30 21:47:59', '1', '2023-10-30 21:48:45', b'0', '1970-01-01 00:00:00');
+INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (605, 'CRM 数据权限的级别', 'crm_permission_level', 0, '', '1', '2023-11-30 09:51:59', '1', '2023-11-30 09:51:59', b'0', '1970-01-01 00:00:00');
+INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (606, 'CRM 审批状态', 'crm_audit_status', 0, '', '1', '2023-11-30 18:56:23', '1', '2023-11-30 18:56:23', b'0', '1970-01-01 00:00:00');
COMMIT;
-- ----------------------------
@@ -1723,7 +1734,7 @@ CREATE TABLE `system_error_code` (
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 5932 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '错误码表';
+) ENGINE = InnoDB AUTO_INCREMENT = 6039 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '错误码表';
-- ----------------------------
-- Records of system_error_code
@@ -1752,7 +1763,7 @@ CREATE TABLE `system_login_log` (
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 2647 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录';
+) ENGINE = InnoDB AUTO_INCREMENT = 2667 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录';
-- ----------------------------
-- Records of system_login_log
@@ -1817,7 +1828,7 @@ CREATE TABLE `system_mail_log` (
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 355 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '邮件日志表';
+) ENGINE = InnoDB AUTO_INCREMENT = 356 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '邮件日志表';
-- ----------------------------
-- Records of system_mail_log
@@ -1882,7 +1893,7 @@ CREATE TABLE `system_menu` (
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 2504 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '菜单权限表';
+) ENGINE = InnoDB AUTO_INCREMENT = 2526 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '菜单权限表';
-- ----------------------------
-- Records of system_menu
@@ -2292,7 +2303,7 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2302, '支付通知查询', 'pay:notify:query', 3, 1, 2301, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-07-20 04:41:32', '', '2023-07-20 04:41:32', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2303, '拼团活动', '', 2, 3, 2030, 'combination', 'fa:group', '', '', 0, b'1', b'1', b'1', '1', '2023-08-12 17:19:54', '1', '2023-08-12 17:20:05', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2304, '拼团商品', '', 2, 1, 2303, 'acitivity', 'ep:apple', 'mall/promotion/combination/activity/index', 'PromotionCombinationActivity', 0, b'1', b'1', b'1', '1', '2023-08-12 17:22:03', '1', '2023-08-12 17:22:29', b'0');
-INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2305, '拼团活动查询', 'promotion:combination-activity:query ', 3, 1, 2304, '', '', '', '', 0, b'1', b'1', b'1', '1', '2023-08-12 17:54:32', '1', '2023-08-12 17:54:32', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2305, '拼团活动查询', 'promotion:combination-activity:query', 3, 1, 2304, '', '', '', '', 0, b'1', b'1', b'1', '1', '2023-08-12 17:54:32', '1', '2023-11-24 11:57:40', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2306, '拼团活动创建', 'promotion:combination-activity:create', 3, 2, 2304, '', '', '', '', 0, b'1', b'1', b'1', '1', '2023-08-12 17:54:49', '1', '2023-08-12 17:54:49', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2307, '拼团活动更新', 'promotion:combination-activity:update', 3, 3, 2304, '', '', '', '', 0, b'1', b'1', b'1', '1', '2023-08-12 17:55:04', '1', '2023-08-12 17:55:04', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2308, '拼团活动删除', 'promotion:combination-activity:delete', 3, 4, 2304, '', '', '', '', 0, b'1', b'1', b'1', '1', '2023-08-12 17:55:23', '1', '2023-08-12 17:55:23', b'0');
@@ -2457,6 +2468,16 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2494, '学生删除', 'infra:demo03-student:delete', 3, 4, 2490, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-16 12:53:37', '', '2023-11-16 12:53:37', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2495, '学生导出', 'infra:demo03-student:export', 3, 5, 2490, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-16 12:53:37', '', '2023-11-16 12:53:37', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2497, '主子表(ERP)', '', 2, 11, 1070, 'demo03-erp', 'ep:calendar', 'infra/demo/demo03/erp/index', 'Demo03StudentERP', 0, b'1', b'1', b'1', '', '2023-11-16 15:50:59', '1', '2023-11-17 13:19:56', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2516, '客户公海配置', '', 2, 0, 2524, 'customer-pool-config', 'ep:data-analysis', 'crm/config/customerPoolConfig/index', 'CrmCustomerPoolConfig', 0, b'1', b'1', b'1', '', '2023-11-18 13:33:31', '1', '2023-11-26 20:08:14', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2517, '客户公海配置保存', 'crm:customer-pool-config:update', 3, 1, 2516, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-18 13:33:31', '', '2023-11-18 13:33:31', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2518, '客户限制配置', '', 2, 0, 2524, 'customer-limit-config', 'ep:avatar', 'crm/config/customerLimitConfig/index', 'CrmCustomerLimitConfig', 0, b'1', b'1', b'1', '', '2023-11-18 13:33:53', '1', '2023-11-26 20:07:04', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2519, '客户限制配置查询', 'crm:customer-limit-config:query', 3, 1, 2518, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-18 13:33:53', '', '2023-11-18 13:33:53', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2520, '客户限制配置创建', 'crm:customer-limit-config:create', 3, 2, 2518, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-18 13:33:53', '', '2023-11-18 13:33:53', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2521, '客户限制配置更新', 'crm:customer-limit-config:update', 3, 3, 2518, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-18 13:33:53', '', '2023-11-18 13:33:53', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2522, '客户限制配置删除', 'crm:customer-limit-config:delete', 3, 4, 2518, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-18 13:33:53', '', '2023-11-18 13:33:53', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2523, '客户限制配置导出', 'crm:customer-limit-config:export', 3, 5, 2518, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-18 13:33:53', '', '2023-11-18 13:33:53', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2524, '系统配置', '', 1, 99, 2397, 'config', 'ep:connection', '', '', 0, b'1', b'1', b'1', '1', '2023-11-18 21:58:00', '1', '2023-11-18 21:58:00', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2525, 'WebSocket 测试', '', 2, 7, 2, 'websocket', 'ep:connection', 'infra/webSocket/index', 'InfraWebSocket', 0, b'1', b'1', b'1', '1', '2023-11-23 19:41:55', '1', '2023-11-24 19:22:30', b'0');
COMMIT;
-- ----------------------------
@@ -2483,7 +2504,7 @@ CREATE TABLE `system_notice` (
-- ----------------------------
BEGIN;
INSERT INTO `system_notice` (`id`, `title`, `content`, `type`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, '芋道的公众', '新版本内容133
', 1, 0, 'admin', '2021-01-05 17:03:48', '1', '2022-05-04 21:00:20', b'0', 1);
-INSERT INTO `system_notice` (`id`, `title`, `content`, `type`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2, '维护通知:2018-07-01 若依系统凌晨维护', ' 1111
', 2, 1, 'admin', '2021-01-05 17:03:48', '1', '2023-11-11 12:51:11', b'0', 1);
+INSERT INTO `system_notice` (`id`, `title`, `content`, `type`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2, '维护通知:2018-07-01 系统凌晨维护', ' 1111
', 2, 1, 'admin', '2021-01-05 17:03:48', '1', '2023-11-23 23:37:41', b'0', 1);
INSERT INTO `system_notice` (`id`, `title`, `content`, `type`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4, '我是测试标题', '哈哈哈哈123
', 1, 0, '110', '2022-02-22 01:01:25', '110', '2022-02-22 01:01:46', b'0', 121);
COMMIT;
@@ -2547,7 +2568,7 @@ CREATE TABLE `system_notify_template` (
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '站内信模板表';
+) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '站内信模板表';
-- ----------------------------
-- Records of system_notify_template
@@ -2577,7 +2598,7 @@ CREATE TABLE `system_oauth2_access_token` (
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_access_token`(`access_token` ASC) USING BTREE,
INDEX `idx_refresh_token`(`refresh_token` ASC) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 3467 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌';
+) ENGINE = InnoDB AUTO_INCREMENT = 3587 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌';
-- ----------------------------
-- Records of system_oauth2_access_token
@@ -2699,7 +2720,7 @@ CREATE TABLE `system_oauth2_refresh_token` (
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1115 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌';
+) ENGINE = InnoDB AUTO_INCREMENT = 1132 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌';
-- ----------------------------
-- Records of system_oauth2_refresh_token
@@ -2739,7 +2760,7 @@ CREATE TABLE `system_operate_log` (
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 9090 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录';
+) ENGINE = InnoDB AUTO_INCREMENT = 9175 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录';
-- ----------------------------
-- Records of system_operate_log
@@ -2765,7 +2786,7 @@ CREATE TABLE `system_post` (
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '岗位信息表';
+) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '岗位信息表';
-- ----------------------------
-- Records of system_post
@@ -3739,7 +3760,7 @@ CREATE TABLE `system_sms_code` (
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_mobile`(`mobile` ASC) USING BTREE COMMENT '手机号'
-) ENGINE = InnoDB AUTO_INCREMENT = 535 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '手机验证码';
+) ENGINE = InnoDB AUTO_INCREMENT = 536 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '手机验证码';
-- ----------------------------
-- Records of system_sms_code
@@ -3766,8 +3787,6 @@ CREATE TABLE `system_sms_log` (
`user_type` tinyint NULL DEFAULT NULL COMMENT '用户类型',
`send_status` tinyint NOT NULL DEFAULT 0 COMMENT '发送状态',
`send_time` datetime NULL DEFAULT NULL COMMENT '发送时间',
- `send_code` int NULL DEFAULT NULL COMMENT '发送结果的编码',
- `send_msg` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '发送结果的提示',
`api_send_code` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '短信 API 发送结果的编码',
`api_send_msg` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '短信 API 发送失败的提示',
`api_request_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '短信 API 发送返回的唯一请求 ID',
@@ -3782,7 +3801,7 @@ CREATE TABLE `system_sms_log` (
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 502 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信日志';
+) ENGINE = InnoDB AUTO_INCREMENT = 503 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信日志';
-- ----------------------------
-- Records of system_sms_log
@@ -3812,7 +3831,7 @@ CREATE TABLE `system_sms_template` (
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 16 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信模板';
+) ENGINE = InnoDB AUTO_INCREMENT = 17 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信模板';
-- ----------------------------
-- Records of system_sms_template
@@ -3882,7 +3901,7 @@ CREATE TABLE `system_social_user` (
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 24 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '社交用户表';
+) ENGINE = InnoDB AUTO_INCREMENT = 25 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '社交用户表';
-- ----------------------------
-- Records of system_social_user
@@ -3907,7 +3926,7 @@ CREATE TABLE `system_social_user_bind` (
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 80 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '社交绑定表';
+) ENGINE = InnoDB AUTO_INCREMENT = 81 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '社交绑定表';
-- ----------------------------
-- Records of system_social_user_bind
@@ -4077,13 +4096,13 @@ CREATE TABLE `system_users` (
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `idx_username`(`username` ASC, `update_time` ASC, `tenant_id` ASC) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 127 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户信息表';
+) ENGINE = InnoDB AUTO_INCREMENT = 126 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户信息表';
-- ----------------------------
-- Records of system_users
-- ----------------------------
BEGIN;
-INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$10$mRMIYLDtRHlf6.9ipiqH1.Z.bh/R9dO9d5iHiGYPigi6r5KOoR2Wm', '芋道源码', '管理员', 103, '[1]', 'aoteman@126.com', '15612345678', 1, 'http://127.0.0.1:48080/admin-api/infra/file/4/get/37e56010ecbee472cdd821ac4b608e151e62a74d9633f15d085aee026eedeb60.png', 0, '0:0:0:0:0:0:0:1', '2023-11-18 17:19:30', 'admin', '2021-01-05 17:03:47', NULL, '2023-11-18 17:19:30', b'0', 1);
+INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$10$mRMIYLDtRHlf6.9ipiqH1.Z.bh/R9dO9d5iHiGYPigi6r5KOoR2Wm', '芋道源码', '管理员', 103, '[1]', 'aoteman@126.com', '15612345678', 1, 'http://127.0.0.1:48080/admin-api/infra/file/4/get/37e56010ecbee472cdd821ac4b608e151e62a74d9633f15d085aee026eedeb60.png', 0, '127.0.0.1', '2023-11-30 09:16:00', 'admin', '2021-01-05 17:03:47', NULL, '2023-11-30 09:16:00', b'0', 1);
INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (100, 'yudao', '$2a$10$11U48RhyJ5pSBYWSn12AD./ld671.ycSzJHbyrtpeoMeYiw31eo8a', '芋道', '不要吓我', 104, '[1]', 'yudao@iocoder.cn', '15601691300', 1, '', 1, '127.0.0.1', '2022-07-09 23:03:33', '', '2021-01-07 09:07:17', NULL, '2022-07-09 23:03:33', b'0', 1);
INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (103, 'yuanma', '$2a$10$YMpimV4T6BtDhIaA8jSW.u8UTGBeGhc/qwXP4oxoMr4mOw9.qttt6', '源码', NULL, 106, NULL, 'yuanma@iocoder.cn', '15601701300', 0, '', 0, '127.0.0.1', '2022-07-08 01:26:27', '', '2021-01-13 23:50:35', NULL, '2022-07-08 01:26:27', b'0', 1);
INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (104, 'test', '$2a$10$GP8zvqHB//TekuzYZSBYAuBQJiNq1.fxQVDYJ.uBCOnWCtDVKE4H6', '测试号', NULL, 107, '[1,2]', '111@qq.com', '15601691200', 1, '', 0, '0:0:0:0:0:0:0:1', '2023-09-24 18:21:19', '', '2021-01-21 02:13:53', NULL, '2023-09-24 18:21:19', b'0', 1);
@@ -4098,7 +4117,7 @@ INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`,
INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (115, 'aotemane', '$2a$10$/WCwGHu1eq0wOVDd/u8HweJ0gJCHyLS6T7ndCqI8UXZAQom1etk2e', '1', '11', 101, '[]', '', '', 1, '', 0, '', NULL, '1', '2022-04-30 02:55:43', '1', '2022-06-22 13:34:58', b'0', 1);
INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (116, '15601691302', '$2a$10$L5C4S0U6adBWMvFv1Wwl4.DI/NwYS3WIfLj5Q.Naqr5II8CmqsDZ6', '小豆', NULL, NULL, NULL, '', '', 0, '', 0, '', NULL, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124);
INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (117, 'admin123', '$2a$10$WI8Gg/lpZQIrOEZMHqka7OdFaD4Nx.B/qY8ZGTTUKrOJwaHFqibaC', '测试号', '1111', 100, '[2]', '', '15601691234', 1, '', 0, '', NULL, '1', '2022-07-09 17:40:26', '1', '2022-07-09 17:40:26', b'0', 1);
-INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (118, 'goudan', '$2a$10$Lrb71muL.s5/AFjQ2IHkzOFlAFwUToH.zQL7bnghvTDt/QptjGgF6', '狗蛋', NULL, 103, '[1]', '', '', 2, '', 0, '', NULL, '1', '2022-07-09 17:44:43', '1', '2022-12-31 17:29:13', b'0', 1);
+INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (118, 'goudan', '$2a$10$Lrb71muL.s5/AFjQ2IHkzOFlAFwUToH.zQL7bnghvTDt/QptjGgF6', '狗蛋', NULL, 103, '[1]', '', '', 2, '', 0, '', NULL, '1', '2022-07-09 17:44:43', '1', '2023-11-18 19:02:13', b'0', 1);
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
diff --git a/yudao-dependencies/pom.xml b/yudao-dependencies/pom.xml
index dbd6e2f3c..b71244f71 100644
--- a/yudao-dependencies/pom.xml
+++ b/yudao-dependencies/pom.xml
@@ -14,7 +14,7 @@
https://github.com/YunaiV/ruoyi-vue-pro
- 1.8.3-snapshot
+ 1.9.0-snapshot
1.5.0
2.7.17
@@ -70,10 +70,10 @@
4.6.4
2.2.1
3.1.880
- 1.0.7
+ 1.0.8
1.6.1
2.12.2
- 4.5.0
+ 4.5.7.B
@@ -113,11 +113,6 @@
yudao-spring-boot-starter-biz-pay
${revision}
-
- cn.iocoder.boot
- yudao-spring-boot-starter-biz-weixin
- ${revision}
-
cn.iocoder.boot
yudao-spring-boot-starter-biz-tenant
@@ -175,6 +170,12 @@
${revision}
+
+ cn.iocoder.boot
+ yudao-spring-boot-starter-websocket
+ ${revision}
+
+
com.github.xiaoymin
knife4j-openapi3-spring-boot-starter
@@ -605,6 +606,12 @@
com.xingyuv
spring-boot-starter-justauth
${justauth.version}
+
+
+ cn.hutool
+ hutool-core
+
+
@@ -612,11 +619,6 @@
weixin-java-pay
${weixin-java.version}
-
- com.github.binarywang
- weixin-java-mp
- ${weixin-java.version}
-
com.github.binarywang
wx-java-mp-spring-boot-starter
diff --git a/yudao-framework/pom.xml b/yudao-framework/pom.xml
index 12244f5ce..6a8212314 100644
--- a/yudao-framework/pom.xml
+++ b/yudao-framework/pom.xml
@@ -31,8 +31,6 @@
yudao-spring-boot-starter-biz-sms
yudao-spring-boot-starter-biz-pay
- yudao-spring-boot-starter-biz-weixin
- yudao-spring-boot-starter-biz-social
yudao-spring-boot-starter-biz-tenant
yudao-spring-boot-starter-biz-data-permission
yudao-spring-boot-starter-biz-error-code
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-error-code/src/main/java/cn/iocoder/yudao/framework/errorcode/core/generator/ErrorCodeAutoGeneratorImpl.java b/yudao-framework/yudao-spring-boot-starter-biz-error-code/src/main/java/cn/iocoder/yudao/framework/errorcode/core/generator/ErrorCodeAutoGeneratorImpl.java
index d670c1829..00fbc03ca 100644
--- a/yudao-framework/yudao-spring-boot-starter-biz-error-code/src/main/java/cn/iocoder/yudao/framework/errorcode/core/generator/ErrorCodeAutoGeneratorImpl.java
+++ b/yudao-framework/yudao-spring-boot-starter-biz-error-code/src/main/java/cn/iocoder/yudao/framework/errorcode/core/generator/ErrorCodeAutoGeneratorImpl.java
@@ -49,8 +49,12 @@ public class ErrorCodeAutoGeneratorImpl implements ErrorCodeAutoGenerator {
log.info("[execute][解析到错误码数量为 ({}) 个]", autoGenerateDTOs.size());
// 第二步,写入到 system 服务
- errorCodeApi.autoGenerateErrorCodeList(autoGenerateDTOs);
- log.info("[execute][写入到 system 组件完成]");
+ try {
+ errorCodeApi.autoGenerateErrorCodeList(autoGenerateDTOs);
+ log.info("[execute][写入到 system 组件完成]");
+ } catch (Exception ex) {
+ log.error("[execute][写入到 system 组件失败({})]", ExceptionUtil.getRootCauseMessage(ex));
+ }
}
/**
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-error-code/src/main/java/cn/iocoder/yudao/framework/errorcode/core/loader/ErrorCodeLoader.java b/yudao-framework/yudao-spring-boot-starter-biz-error-code/src/main/java/cn/iocoder/yudao/framework/errorcode/core/loader/ErrorCodeLoader.java
index 26fb060d2..0bf70211f 100644
--- a/yudao-framework/yudao-spring-boot-starter-biz-error-code/src/main/java/cn/iocoder/yudao/framework/errorcode/core/loader/ErrorCodeLoader.java
+++ b/yudao-framework/yudao-spring-boot-starter-biz-error-code/src/main/java/cn/iocoder/yudao/framework/errorcode/core/loader/ErrorCodeLoader.java
@@ -21,4 +21,14 @@ public interface ErrorCodeLoader {
ServiceExceptionUtil.put(code, msg);
}
+ /**
+ * 刷新错误码
+ */
+ void refreshErrorCodes();
+
+ /**
+ * 加载错误码
+ */
+ void loadErrorCodes();
+
}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-error-code/src/main/java/cn/iocoder/yudao/framework/errorcode/core/loader/ErrorCodeLoaderImpl.java b/yudao-framework/yudao-spring-boot-starter-biz-error-code/src/main/java/cn/iocoder/yudao/framework/errorcode/core/loader/ErrorCodeLoaderImpl.java
index c1e8b3ef2..4d1febfd7 100644
--- a/yudao-framework/yudao-spring-boot-starter-biz-error-code/src/main/java/cn/iocoder/yudao/framework/errorcode/core/loader/ErrorCodeLoaderImpl.java
+++ b/yudao-framework/yudao-spring-boot-starter-biz-error-code/src/main/java/cn/iocoder/yudao/framework/errorcode/core/loader/ErrorCodeLoaderImpl.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.framework.errorcode.core.loader;
import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.exceptions.ExceptionUtil;
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.module.system.api.errorcode.ErrorCodeApi;
import cn.iocoder.yudao.module.system.api.errorcode.dto.ErrorCodeRespDTO;
@@ -8,6 +9,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import java.time.LocalDateTime;
@@ -43,31 +45,38 @@ public class ErrorCodeLoaderImpl implements ErrorCodeLoader {
*/
private LocalDateTime maxUpdateTime;
+ @Override
@EventListener(ApplicationReadyEvent.class)
+ @Async // 异步,保证项目的启动过程,毕竟非关键流程
public void loadErrorCodes() {
- this.loadErrorCodes0();
+ loadErrorCodes0();
}
+ @Override
@Scheduled(fixedDelay = REFRESH_ERROR_CODE_PERIOD, initialDelay = REFRESH_ERROR_CODE_PERIOD)
public void refreshErrorCodes() {
- this.loadErrorCodes0();
+ loadErrorCodes0();
}
private void loadErrorCodes0() {
- // 加载错误码
- List errorCodeRespDTOs = errorCodeApi.getErrorCodeList(applicationName, maxUpdateTime);
- if (CollUtil.isEmpty(errorCodeRespDTOs)) {
- return;
- }
- log.info("[loadErrorCodes0][加载到 ({}) 个错误码]", errorCodeRespDTOs.size());
+ try {
+ // 加载错误码
+ List errorCodeRespDTOs = errorCodeApi.getErrorCodeList(applicationName, maxUpdateTime);
+ if (CollUtil.isEmpty(errorCodeRespDTOs)) {
+ return;
+ }
+ log.info("[loadErrorCodes0][加载到 ({}) 个错误码]", errorCodeRespDTOs.size());
- // 刷新错误码的缓存
- errorCodeRespDTOs.forEach(errorCodeRespDTO -> {
- // 写入到错误码的缓存
- putErrorCode(errorCodeRespDTO.getCode(), errorCodeRespDTO.getMessage());
- // 记录下更新时间,方便增量更新
- maxUpdateTime = DateUtils.max(maxUpdateTime, errorCodeRespDTO.getUpdateTime());
- });
+ // 刷新错误码的缓存
+ errorCodeRespDTOs.forEach(errorCodeRespDTO -> {
+ // 写入到错误码的缓存
+ putErrorCode(errorCodeRespDTO.getCode(), errorCodeRespDTO.getMessage());
+ // 记录下更新时间,方便增量更新
+ maxUpdateTime = DateUtils.max(maxUpdateTime, errorCodeRespDTO.getUpdateTime());
+ });
+ } catch (Exception ex) {
+ log.error("[loadErrorCodes0][加载错误码失败({})]", ExceptionUtil.getRootCauseMessage(ex));
+ }
}
}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/OperateLogFrameworkServiceImpl.java b/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/OperateLogFrameworkServiceImpl.java
index a636c4bdf..495193f7c 100644
--- a/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/OperateLogFrameworkServiceImpl.java
+++ b/yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/OperateLogFrameworkServiceImpl.java
@@ -21,7 +21,7 @@ public class OperateLogFrameworkServiceImpl implements OperateLogFrameworkServic
@Override
@Async
public void createOperateLog(OperateLog operateLog) {
- OperateLogCreateReqDTO reqDTO = BeanUtil.copyProperties(operateLog, OperateLogCreateReqDTO.class);
+ OperateLogCreateReqDTO reqDTO = BeanUtil.toBean(operateLog, OperateLogCreateReqDTO.class);
operateLogApi.createOperateLog(reqDTO);
}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java
index 18ae017d1..86e3566b2 100644
--- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java
+++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java
@@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
+import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
import java.util.Map;
@@ -86,4 +87,12 @@ public interface PayClient {
*/
PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO);
+ /**
+ * 获得转账订单信息
+ *
+ * @param outTradeNo 外部订单号
+ * @param type 转账类型
+ * @return 转账信息
+ */
+ PayTransferRespDTO getTransfer(String outTradeNo, PayTransferTypeEnum type);
}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java
index da6f22774..0f9b48240 100644
--- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java
+++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java
@@ -53,11 +53,24 @@ public class PayTransferRespDTO {
/**
* 创建【WAITING】状态的转账返回
*/
- public static PayTransferRespDTO waitingOf(String channelOrderNo,
+ public static PayTransferRespDTO waitingOf(String channelTransferNo,
String outTransferNo, Object rawData) {
PayTransferRespDTO respDTO = new PayTransferRespDTO();
respDTO.status = PayTransferStatusRespEnum.WAITING.getStatus();
- respDTO.channelTransferNo = channelOrderNo;
+ respDTO.channelTransferNo = channelTransferNo;
+ respDTO.outTransferNo = outTransferNo;
+ respDTO.rawData = rawData;
+ return respDTO;
+ }
+
+ /**
+ * 创建【IN_PROGRESS】状态的转账返回
+ */
+ public static PayTransferRespDTO dealingOf(String channelTransferNo,
+ String outTransferNo, Object rawData) {
+ PayTransferRespDTO respDTO = new PayTransferRespDTO();
+ respDTO.status = PayTransferStatusRespEnum.IN_PROGRESS.getStatus();
+ respDTO.channelTransferNo = channelTransferNo;
respDTO.outTransferNo = outTransferNo;
respDTO.rawData = rawData;
return respDTO;
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java
index f06dab22e..82d68b58f 100644
--- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java
+++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java
@@ -188,11 +188,11 @@ public abstract class AbstractPayClient implemen
@Override
public final PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
+ validatePayTransferReqDTO(reqDTO);
PayTransferRespDTO resp;
- try{
- validatePayTransferReqDTO(reqDTO);
+ try {
resp = doUnifiedTransfer(reqDTO);
- }catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
+ } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
throw ex;
} catch (Throwable ex) {
// 系统异常,则包装成 PayException 异常抛出
@@ -219,9 +219,25 @@ public abstract class AbstractPayClient implemen
}
}
+ @Override
+ public final PayTransferRespDTO getTransfer(String outTradeNo, PayTransferTypeEnum type) {
+ try {
+ return doGetTransfer(outTradeNo, type);
+ } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
+ throw ex;
+ } catch (Throwable ex) {
+ log.error("[getTransfer][客户端({}) outTradeNo({}) type({}) 查询转账单异常]",
+ getId(), outTradeNo, type, ex);
+ throw buildPayException(ex);
+ }
+ }
+
protected abstract PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO)
throws Throwable;
+ protected abstract PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type)
+ throws Throwable;
+
// ========== 各种工具方法 ==========
private PayException buildPayException(Throwable ex) {
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java
index fc9d658ac..4dcf23675 100644
--- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java
+++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java
@@ -23,14 +23,8 @@ import com.alipay.api.AlipayResponse;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.domain.*;
import com.alipay.api.internal.util.AlipaySignature;
-import com.alipay.api.request.AlipayFundTransUniTransferRequest;
-import com.alipay.api.request.AlipayTradeFastpayRefundQueryRequest;
-import com.alipay.api.request.AlipayTradeQueryRequest;
-import com.alipay.api.request.AlipayTradeRefundRequest;
-import com.alipay.api.response.AlipayFundTransUniTransferResponse;
-import com.alipay.api.response.AlipayTradeFastpayRefundQueryResponse;
-import com.alipay.api.response.AlipayTradeQueryResponse;
-import com.alipay.api.response.AlipayTradeRefundResponse;
+import com.alipay.api.request.*;
+import com.alipay.api.response.*;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
@@ -126,7 +120,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient {
+ Assert.notNull(status, () -> {
throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", response.getBody()));
});
return PayOrderRespDTO.of(status, response.getTradeNo(), response.getBuyerUserId(), LocalDateTimeUtil.of(response.getSendPayDate()),
@@ -228,7 +222,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient {
throw new UnsupportedOperationException("待实现");
}
+ @Override
+ protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) {
+ throw new UnsupportedOperationException("待实现");
+ }
+
}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java
index f4f326a65..ada1f42e6 100644
--- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java
+++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java
@@ -16,8 +16,9 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDT
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
+import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
+import com.github.binarywang.wxpay.bean.notify.WxPayNotifyV3Result;
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
-import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result;
import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult;
import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyV3Result;
import com.github.binarywang.wxpay.bean.request.*;
@@ -175,8 +176,8 @@ public abstract class AbstractWxPayClient extends AbstractPayClient sendSms(Long logId, String mobile, String apiTemplateId,
- List> templateParams);
+ SmsSendRespDTO sendSms(Long logId, String mobile, String apiTemplateId,
+ List> templateParams) throws Throwable;
/**
* 解析接收短信的接收结果
@@ -49,6 +49,6 @@ public interface SmsClient {
* @param apiTemplateId 短信 API 的模板编号
* @return 短信模板
*/
- SmsCommonResult getSmsTemplate(String apiTemplateId);
+ SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable;
}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsCodeMapping.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsCodeMapping.java
deleted file mode 100644
index 826306fde..000000000
--- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsCodeMapping.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package cn.iocoder.yudao.framework.sms.core.client;
-
-import cn.iocoder.yudao.framework.common.exception.ErrorCode;
-import cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
-
-import java.util.function.Function;
-
-/**
- * 将 API 的错误码,转换为通用的错误码
- *
- * @see SmsCommonResult
- * @see SmsFrameworkErrorCodeConstants
- *
- * @author 芋道源码
- */
-public interface SmsCodeMapping extends Function {
-}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsCommonResult.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsCommonResult.java
deleted file mode 100644
index d314de2cd..000000000
--- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsCommonResult.java
+++ /dev/null
@@ -1,68 +0,0 @@
-package cn.iocoder.yudao.framework.sms.core.client;
-
-import cn.hutool.core.exceptions.ExceptionUtil;
-import cn.hutool.core.lang.Assert;
-import cn.iocoder.yudao.framework.common.exception.ErrorCode;
-import cn.iocoder.yudao.framework.common.pojo.CommonResult;
-import cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-import lombok.ToString;
-
-/**
- * 短信的 CommonResult 拓展类
- *
- * 考虑到不同的平台,返回的 code 和 msg 是不同的,所以统一额外返回 {@link #apiCode} 和 {@link #apiMsg} 字段
- *
- * 另外,一些短信平台(例如说阿里云、腾讯云)会返回一个请求编号,用于排查请求失败的问题,我们设置到 {@link #apiRequestId} 字段
- *
- * @author 芋道源码
- */
-@Data
-@EqualsAndHashCode(callSuper = true)
-@ToString(callSuper = true)
-public class SmsCommonResult extends CommonResult {
-
- /**
- * API 返回错误码
- *
- * 由于第三方的错误码可能是字符串,所以使用 String 类型
- */
- private String apiCode;
- /**
- * API 返回提示
- */
- private String apiMsg;
-
- /**
- * API 请求编号
- */
- private String apiRequestId;
-
- private SmsCommonResult() {
- }
-
- public static SmsCommonResult build(String apiCode, String apiMsg, String apiRequestId,
- T data, SmsCodeMapping codeMapping) {
- Assert.notNull(codeMapping, "参数 codeMapping 不能为空");
- SmsCommonResult result = new SmsCommonResult().setApiCode(apiCode).setApiMsg(apiMsg).setApiRequestId(apiRequestId);
- result.setData(data);
- // 翻译错误码
- if (codeMapping != null) {
- ErrorCode errorCode = codeMapping.apply(apiCode);
- if (errorCode == null) {
- errorCode = SmsFrameworkErrorCodeConstants.SMS_UNKNOWN;
- }
- result.setCode(errorCode.getCode()).setMsg(errorCode.getMsg());
- }
- return result;
- }
-
- public static SmsCommonResult error(Throwable ex) {
- SmsCommonResult result = new SmsCommonResult<>();
- result.setCode(SmsFrameworkErrorCodeConstants.EXCEPTION.getCode());
- result.setMsg(ExceptionUtil.getRootCauseMessage(ex));
- return result;
- }
-
-}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/dto/SmsSendRespDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/dto/SmsSendRespDTO.java
index 9d320d57b..ec517508a 100644
--- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/dto/SmsSendRespDTO.java
+++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/dto/SmsSendRespDTO.java
@@ -10,9 +10,34 @@ import lombok.Data;
@Data
public class SmsSendRespDTO {
+ /**
+ * 是否成功
+ */
+ private Boolean success;
+
+ /**
+ * API 请求编号
+ */
+ private String apiRequestId;
+
+ // ==================== 成功时字段 ====================
+
/**
* 短信 API 发送返回的序号
*/
private String serialNo;
+ // ==================== 失败时字段 ====================
+
+ /**
+ * API 返回错误码
+ *
+ * 由于第三方的错误码可能是字符串,所以使用 String 类型
+ */
+ private String apiCode;
+ /**
+ * API 返回提示
+ */
+ private String apiMsg;
+
}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/AbstractSmsClient.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/AbstractSmsClient.java
index f91083ddd..42f00f968 100644
--- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/AbstractSmsClient.java
+++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/AbstractSmsClient.java
@@ -1,17 +1,9 @@
package cn.iocoder.yudao.framework.sms.core.client.impl;
-import cn.iocoder.yudao.framework.common.core.KeyValue;
import cn.iocoder.yudao.framework.sms.core.client.SmsClient;
-import cn.iocoder.yudao.framework.sms.core.client.SmsCodeMapping;
-import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult;
-import cn.iocoder.yudao.framework.sms.core.client.dto.SmsReceiveRespDTO;
-import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO;
-import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO;
import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties;
import lombok.extern.slf4j.Slf4j;
-import java.util.List;
-
/**
* 短信客户端的抽象类,提供模板方法,减少子类的冗余代码
*
@@ -25,14 +17,9 @@ public abstract class AbstractSmsClient implements SmsClient {
* 短信渠道配置
*/
protected volatile SmsChannelProperties properties;
- /**
- * 错误码枚举类
- */
- protected final SmsCodeMapping codeMapping;
- public AbstractSmsClient(SmsChannelProperties properties, SmsCodeMapping codeMapping) {
- this.properties = prepareProperties(properties);
- this.codeMapping = codeMapping;
+ public AbstractSmsClient(SmsChannelProperties properties) {
+ this.properties = properties;
}
/**
@@ -54,74 +41,13 @@ public abstract class AbstractSmsClient implements SmsClient {
return;
}
log.info("[refresh][配置({})发生变化,重新初始化]", properties);
- this.properties = prepareProperties(properties);
// 初始化
this.init();
}
- /**
- * 在赋值给{@link this#properties}前,子类可根据需要预处理短信渠道配置
- *
- * @param properties 数据库中存储的短信渠道配置
- * @return 满足子类实现的短信渠道配置
- */
- protected SmsChannelProperties prepareProperties(SmsChannelProperties properties) {
- return properties;
- }
-
@Override
public Long getId() {
return properties.getId();
}
- @Override
- public final SmsCommonResult sendSms(Long logId, String mobile,
- String apiTemplateId, List> templateParams) {
- // 执行短信发送
- SmsCommonResult result;
- try {
- result = doSendSms(logId, mobile, apiTemplateId, templateParams);
- } catch (Throwable ex) {
- // 打印异常日志
- log.error("[sendSms][发送短信异常,sendLogId({}) mobile({}) apiTemplateId({}) templateParams({})]",
- logId, mobile, apiTemplateId, templateParams, ex);
- // 封装返回
- return SmsCommonResult.error(ex);
- }
- return result;
- }
-
- protected abstract SmsCommonResult doSendSms(Long sendLogId, String mobile,
- String apiTemplateId, List> templateParams)
- throws Throwable;
-
- @Override
- public List parseSmsReceiveStatus(String text) throws Throwable {
- try {
- return doParseSmsReceiveStatus(text);
- } catch (Throwable ex) {
- log.error("[parseSmsReceiveStatus][text({}) 解析发生异常]", text, ex);
- throw ex;
- }
- }
-
- protected abstract List doParseSmsReceiveStatus(String text) throws Throwable;
-
- @Override
- public SmsCommonResult getSmsTemplate(String apiTemplateId) {
- // 执行短信发送
- SmsCommonResult result;
- try {
- result = doGetSmsTemplate(apiTemplateId);
- } catch (Throwable ex) {
- // 打印异常日志
- log.error("[getSmsTemplate][获得短信模板({}) 发生异常]", apiTemplateId, ex);
- // 封装返回
- return SmsCommonResult.error(ex);
- }
- return result;
- }
-
- protected abstract SmsCommonResult doGetSmsTemplate(String apiTemplateId) throws Throwable;
-
}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java
index 62cd6bcd5..d32a5bb11 100644
--- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java
+++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java
@@ -1,25 +1,21 @@
package cn.iocoder.yudao.framework.sms.core.client.impl.aliyun;
import cn.hutool.core.lang.Assert;
-import cn.hutool.core.util.ReflectUtil;
-import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.core.KeyValue;
-import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.sms.core.client.dto.SmsReceiveRespDTO;
import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO;
import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO;
import cn.iocoder.yudao.framework.sms.core.client.impl.AbstractSmsClient;
import cn.iocoder.yudao.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties;
-import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
-import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
-import com.aliyuncs.AcsRequest;
-import com.aliyuncs.AcsResponse;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateRequest;
+import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateResponse;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
-import com.aliyuncs.exceptions.ClientException;
+import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import com.fasterxml.jackson.annotation.JsonFormat;
@@ -31,9 +27,8 @@ import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
-import java.util.function.Function;
-import java.util.stream.Collectors;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
@@ -46,6 +41,11 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DE
@Slf4j
public class AliyunSmsClient extends AbstractSmsClient {
+ /**
+ * 调用成功 code
+ */
+ public static final String API_CODE_SUCCESS = "OK";
+
/**
* REGION, 使用杭州
*/
@@ -57,7 +57,7 @@ public class AliyunSmsClient extends AbstractSmsClient {
private volatile IAcsClient client;
public AliyunSmsClient(SmsChannelProperties properties) {
- super(properties, new AliyunSmsCodeMapping());
+ super(properties);
Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空");
Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
}
@@ -69,9 +69,9 @@ public class AliyunSmsClient extends AbstractSmsClient {
}
@Override
- protected SmsCommonResult doSendSms(Long sendLogId, String mobile,
- String apiTemplateId, List> templateParams) {
- // 构建参数
+ public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
+ List> templateParams) throws Throwable {
+ // 构建请求
SendSmsRequest request = new SendSmsRequest();
request.setPhoneNumbers(mobile);
request.setSignName(properties.getSignature());
@@ -79,34 +79,32 @@ public class AliyunSmsClient extends AbstractSmsClient {
request.setTemplateParam(JsonUtils.toJsonString(MapUtils.convertMap(templateParams)));
request.setOutId(String.valueOf(sendLogId));
// 执行请求
- return invoke(request, response -> new SmsSendRespDTO().setSerialNo(response.getBizId()));
+ SendSmsResponse response = client.getAcsResponse(request);
+ return new SmsSendRespDTO().setSuccess(Objects.equals(response.getCode(), API_CODE_SUCCESS)).setSerialNo(response.getBizId())
+ .setApiRequestId(response.getRequestId()).setApiCode(response.getCode()).setApiMsg(response.getMessage());
}
@Override
- protected List doParseSmsReceiveStatus(String text) throws Throwable {
+ public List parseSmsReceiveStatus(String text) {
List statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class);
- return statuses.stream().map(status -> {
- SmsReceiveRespDTO resp = new SmsReceiveRespDTO();
- resp.setSuccess(status.getSuccess());
- resp.setErrorCode(status.getErrCode()).setErrorMsg(status.getErrMsg());
- resp.setMobile(status.getPhoneNumber()).setReceiveTime(status.getReportTime());
- resp.setSerialNo(status.getBizId()).setLogId(Long.valueOf(status.getOutId()));
- return resp;
- }).collect(Collectors.toList());
+ return convertList(statuses, status -> new SmsReceiveRespDTO().setSuccess(status.getSuccess())
+ .setErrorCode(status.getErrCode()).setErrorMsg(status.getErrMsg())
+ .setMobile(status.getPhoneNumber()).setReceiveTime(status.getReportTime())
+ .setSerialNo(status.getBizId()).setLogId(Long.valueOf(status.getOutId())));
}
@Override
- protected SmsCommonResult doGetSmsTemplate(String apiTemplateId) {
- // 构建参数
+ public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
+ // 构建请求
QuerySmsTemplateRequest request = new QuerySmsTemplateRequest();
request.setTemplateCode(apiTemplateId);
// 执行请求
- return invoke(request, response -> {
- SmsTemplateRespDTO data = new SmsTemplateRespDTO();
- data.setId(response.getTemplateCode()).setContent(response.getTemplateContent());
- data.setAuditStatus(convertSmsTemplateAuditStatus(response.getTemplateStatus())).setAuditReason(response.getReason());
- return data;
- });
+ QuerySmsTemplateResponse response = client.getAcsResponse(request);
+ if (response.getTemplateStatus() == null) {
+ return null;
+ }
+ return new SmsTemplateRespDTO().setId(response.getTemplateCode()).setContent(response.getTemplateContent())
+ .setAuditStatus(convertSmsTemplateAuditStatus(response.getTemplateStatus())).setAuditReason(response.getReason());
}
@VisibleForTesting
@@ -119,37 +117,10 @@ public class AliyunSmsClient extends AbstractSmsClient {
}
}
- @VisibleForTesting
- SmsCommonResult invoke(AcsRequest request, Function responseConsumer) {
- try {
- // 执行发送. 由于阿里云 sms 短信没有统一的 Response,但是有统一的 code、message、requestId 属性,所以只好反射
- T sendResult = client.getAcsResponse(request);
- String code = (String) ReflectUtil.getFieldValue(sendResult, "code");
- String message = (String) ReflectUtil.getFieldValue(sendResult, "message");
- String requestId = (String) ReflectUtil.getFieldValue(sendResult, "requestId");
- // 解析结果
- R data = null;
- if (Objects.equals(code, "OK")) { // 请求成功的情况下
- data = responseConsumer.apply(sendResult);
- }
- // 拼接结果
- return SmsCommonResult.build(code, message, requestId, data, codeMapping);
- } catch (ClientException ex) {
- return SmsCommonResult.build(ex.getErrCode(), formatResultMsg(ex), ex.getRequestId(), null, codeMapping);
- }
- }
-
- private static String formatResultMsg(ClientException ex) {
- if (StrUtil.isEmpty(ex.getErrorDescription())) {
- return ex.getErrMsg();
- }
- return ex.getErrMsg() + " => " + ex.getErrorDescription();
- }
-
/**
* 短信接收状态
*
- * 参见 https://help.aliyun.com/document_detail/101867.html 文档
+ * 参见 文档
*
* @author 芋道源码
*/
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java
deleted file mode 100644
index fd188d9b2..000000000
--- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package cn.iocoder.yudao.framework.sms.core.client.impl.aliyun;
-
-import cn.iocoder.yudao.framework.common.exception.ErrorCode;
-import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
-import cn.iocoder.yudao.framework.sms.core.client.SmsCodeMapping;
-import cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
-
-/**
- * 阿里云的 SmsCodeMapping 实现类
- *
- * 参见 https://help.aliyun.com/document_detail/101346.htm 文档
- *
- * @author 芋道源码
- */
-public class AliyunSmsCodeMapping implements SmsCodeMapping {
-
- @Override
- public ErrorCode apply(String apiCode) {
- switch (apiCode) {
- case "OK": return GlobalErrorCodeConstants.SUCCESS;
- case "isv.ACCOUNT_NOT_EXISTS":
- case "isv.ACCOUNT_ABNORMAL":
- case "MissingAccessKeyId": return SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID;
- case "isp.RAM_PERMISSION_DENY": return SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY;
- case "isv.INVALID_JSON_PARAM":
- case "isv.INVALID_PARAMETERS": return SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR;
- case "isv.BUSINESS_LIMIT_CONTROL": return SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL;
- case "isv.DAY_LIMIT_CONTROL": return SmsFrameworkErrorCodeConstants.SMS_SEND_DAY_LIMIT_CONTROL;
- case "isv.SMS_CONTENT_ILLEGAL": return SmsFrameworkErrorCodeConstants.SMS_SEND_CONTENT_INVALID;
- case "isv.SMS_TEMPLATE_ILLEGAL": return SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID;
- case "isv.SMS_SIGNATURE_ILLEGAL":
- case "isv.SIGN_NAME_ILLEGAL":
- case "isv.SMS_SIGN_ILLEGAL": return SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID;
- case "isv.AMOUNT_NOT_ENOUGH":
- case "isv.OUT_OF_SERVICE": return SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH;
- case "isv.MOBILE_NUMBER_ILLEGAL": return SmsFrameworkErrorCodeConstants.SMS_MOBILE_INVALID;
- case "isv.TEMPLATE_MISSING_PARAMETERS": return SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR;
- default: return SmsFrameworkErrorCodeConstants.SMS_UNKNOWN;
- }
- }
-
-}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/debug/DebugDingTalkCodeMapping.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/debug/DebugDingTalkCodeMapping.java
deleted file mode 100644
index 0a7e48bfc..000000000
--- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/debug/DebugDingTalkCodeMapping.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package cn.iocoder.yudao.framework.sms.core.client.impl.debug;
-
-import cn.iocoder.yudao.framework.common.exception.ErrorCode;
-import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
-import cn.iocoder.yudao.framework.sms.core.client.SmsCodeMapping;
-import cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
-
-import java.util.Objects;
-
-/**
- * 钉钉的 SmsCodeMapping 实现类
- *
- * @author 芋道源码
- */
-public class DebugDingTalkCodeMapping implements SmsCodeMapping {
-
- @Override
- public ErrorCode apply(String apiCode) {
- return Objects.equals(apiCode, "0") ? GlobalErrorCodeConstants.SUCCESS : SmsFrameworkErrorCodeConstants.SMS_UNKNOWN;
- }
-
-}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java
index cedcfb6dc..315edf942 100644
--- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java
+++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java
@@ -8,19 +8,19 @@ import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.crypto.digest.HmacAlgorithm;
import cn.hutool.http.HttpUtil;
import cn.iocoder.yudao.framework.common.core.KeyValue;
-import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.sms.core.client.dto.SmsReceiveRespDTO;
import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO;
import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO;
import cn.iocoder.yudao.framework.sms.core.client.impl.AbstractSmsClient;
import cn.iocoder.yudao.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties;
-import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
-import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
/**
* 基于钉钉 WebHook 实现的调试的短信客户端实现类
@@ -32,7 +32,7 @@ import java.util.Map;
public class DebugDingTalkSmsClient extends AbstractSmsClient {
public DebugDingTalkSmsClient(SmsChannelProperties properties) {
- super(properties, new DebugDingTalkCodeMapping());
+ super(properties);
Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空");
Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
}
@@ -42,8 +42,8 @@ public class DebugDingTalkSmsClient extends AbstractSmsClient {
}
@Override
- protected SmsCommonResult doSendSms(Long sendLogId, String mobile,
- String apiTemplateId, List> templateParams) throws Throwable {
+ public SmsSendRespDTO sendSms(Long sendLogId, String mobile,
+ String apiTemplateId, List> templateParams) throws Throwable {
// 构建请求
String url = buildUrl("robot/send");
Map params = new HashMap<>();
@@ -55,14 +55,15 @@ public class DebugDingTalkSmsClient extends AbstractSmsClient {
String responseText = HttpUtil.post(url, JsonUtils.toJsonString(params));
// 解析结果
Map, ?> responseObj = JsonUtils.parseObject(responseText, Map.class);
- return SmsCommonResult.build(MapUtil.getStr(responseObj, "errcode"), MapUtil.getStr(responseObj, "errorMsg"),
- null, new SmsSendRespDTO().setSerialNo(StrUtil.uuid()), codeMapping);
+ String errorCode = MapUtil.getStr(responseObj, "errcode");
+ return new SmsSendRespDTO().setSuccess(Objects.equals(errorCode, "0")).setSerialNo(StrUtil.uuid())
+ .setApiCode(errorCode).setApiMsg(MapUtil.getStr(responseObj, "errorMsg"));
}
/**
* 构建请求地址
*
- * 参见 https://developers.dingtalk.com/document/app/custom-robot-access/title-nfv-794-g71 文档
+ * 参见 文档
*
* @param path 请求路径
* @return 请求地址
@@ -82,15 +83,14 @@ public class DebugDingTalkSmsClient extends AbstractSmsClient {
}
@Override
- protected List doParseSmsReceiveStatus(String text) throws Throwable {
+ public List parseSmsReceiveStatus(String text) {
throw new UnsupportedOperationException("模拟短信客户端,暂时无需解析回调");
}
@Override
- protected SmsCommonResult doGetSmsTemplate(String apiTemplateId) {
- SmsTemplateRespDTO data = new SmsTemplateRespDTO().setId(apiTemplateId).setContent("")
+ public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) {
+ return new SmsTemplateRespDTO().setId(apiTemplateId).setContent("")
.setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason("");
- return SmsCommonResult.build("0", "success", null, data, codeMapping);
}
}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsChannelProperties.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsChannelProperties.java
deleted file mode 100644
index 6b539c602..000000000
--- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsChannelProperties.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package cn.iocoder.yudao.framework.sms.core.client.impl.tencent;
-
-import cn.hutool.core.bean.BeanUtil;
-import cn.hutool.core.lang.Assert;
-import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties;
-import lombok.Data;
-
-/**
- * 腾讯云短信配置实现类
- * 腾讯云发送短信时,需要额外的参数 sdkAppId,
- *
- * @author shiwp
- */
-@Data
-public class TencentSmsChannelProperties extends SmsChannelProperties {
-
- /**
- * 应用 id
- */
- private String sdkAppId;
-
- /**
- * 考虑到不破坏原有的 apiKey + apiSecret 的结构,
- * 所以腾讯云短信存储时,将 secretId 拼接到 apiKey 字段中,格式为 "secretId sdkAppId"。
- * 因此在使用时,需要将 secretId 和 sdkAppId 解析出来,分别存储到对应字段中。
- */
- public static TencentSmsChannelProperties build(SmsChannelProperties properties) {
- if (properties instanceof TencentSmsChannelProperties) {
- return (TencentSmsChannelProperties) properties;
- }
- TencentSmsChannelProperties result = BeanUtil.toBean(properties, TencentSmsChannelProperties.class);
- String combineKey = properties.getApiKey();
- Assert.notEmpty(combineKey, "apiKey 不能为空");
- String[] keys = combineKey.trim().split(" ");
- Assert.isTrue(keys.length == 2, "腾讯云短信 apiKey 配置格式错误,请配置 为[secretId sdkAppId]");
- Assert.notBlank(keys[0], "腾讯云短信 secretId 不能为空");
- Assert.notBlank(keys[1], "腾讯云短信 sdkAppId 不能为空");
- result.setSdkAppId(keys[1]).setApiKey(keys[0]);
- return result;
- }
-}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsClient.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsClient.java
index 743c1aed8..eeb287ffe 100644
--- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsClient.java
+++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsClient.java
@@ -4,9 +4,7 @@ import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.core.KeyValue;
import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils;
-import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
-import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult;
import cn.iocoder.yudao.framework.sms.core.client.dto.SmsReceiveRespDTO;
import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO;
import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO;
@@ -17,23 +15,22 @@ import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import com.tencentcloudapi.common.Credential;
-import com.tencentcloudapi.common.exception.TencentCloudSDKException;
import com.tencentcloudapi.sms.v20210111.SmsClient;
import com.tencentcloudapi.sms.v20210111.models.*;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
-import java.util.function.Function;
-import java.util.function.Supplier;
+import java.util.Objects;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
/**
* 腾讯云短信功能实现
- *
- * 参见 https://cloud.tencent.com/document/product/382/52077
+ *
+ * 参见 文档
*
* @author shiwp
*/
@@ -42,7 +39,7 @@ public class TencentSmsClient extends AbstractSmsClient {
/**
* 调用成功 code
*/
- public static final String API_SUCCESS_CODE = "Ok";
+ public static final String API_CODE_SUCCESS = "Ok";
/**
* REGION,使用南京
@@ -51,180 +48,103 @@ public class TencentSmsClient extends AbstractSmsClient {
/**
* 是否国际/港澳台短信:
+ *
* 0:表示国内短信。
* 1:表示国际/港澳台短信。
*/
- private static final long INTERNATIONAL = 0L;
+ private static final long INTERNATIONAL_CHINA = 0L;
private SmsClient client;
public TencentSmsClient(SmsChannelProperties properties) {
- super(properties, new TencentSmsCodeMapping());
+ super(properties);
Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
+ validateSdkAppId(properties);
}
@Override
protected void doInit() {
// 实例化一个认证对象,入参需要传入腾讯云账户密钥对 secretId,secretKey
- Credential credential = new Credential(properties.getApiKey(), properties.getApiSecret());
+ Credential credential = new Credential(getApiKey(), properties.getApiSecret());
client = new SmsClient(credential, ENDPOINT);
}
+ /**
+ * 参数校验腾讯云的 SDK AppId
+ *
+ * 原因是:腾讯云发放短信的时候,需要额外的参数 sdkAppId
+ *
+ * 解决方案:考虑到不破坏原有的 apiKey + apiSecret 的结构,所以将 secretId 拼接到 apiKey 字段中,格式为 "secretId sdkAppId"。
+ *
+ * @param properties 配置
+ */
+ private static void validateSdkAppId(SmsChannelProperties properties) {
+ String combineKey = properties.getApiKey();
+ Assert.notEmpty(combineKey, "apiKey 不能为空");
+ String[] keys = combineKey.trim().split(" ");
+ Assert.isTrue(keys.length == 2, "腾讯云短信 apiKey 配置格式错误,请配置 为[secretId sdkAppId]");
+ }
+
+ private String getSdkAppId() {
+ return StrUtil.subAfter(properties.getApiKey(), " ", true);
+ }
+
+ private String getApiKey() {
+ return StrUtil.subBefore(properties.getApiKey(), " ", true);
+ }
+
@Override
- protected SmsCommonResult doSendSms(Long sendLogId,
- String mobile,
- String apiTemplateId,
- List> templateParams) throws Throwable {
- return invoke(() -> buildSendSmsRequest(sendLogId, mobile, apiTemplateId, templateParams),
- this::doSendSms0,
- response -> {
- SendStatus sendStatus = response.getSendStatusSet()[0];
- return SmsCommonResult.build(sendStatus.getCode(), sendStatus.getMessage(), response.getRequestId(),
- new SmsSendRespDTO().setSerialNo(sendStatus.getSerialNo()), codeMapping);
- });
- }
-
-
- /**
- * 腾讯云发放短信的时候,需要额外的参数 sdkAppId。
- * 考虑到不破坏原有的 apiKey + apiSecret 的结构,所以将 secretId 拼接到 apiKey 字段中,格式为 "secretId sdkAppId"。
- * 因此,这边需要使用 TencentSmsChannelProperties 做拆分,重新封装到 properties 内。
- *
- * @param properties 数据库中存储的短信渠道配置
- * @return TencentSmsChannelProperties
- */
- @Override
- protected SmsChannelProperties prepareProperties(SmsChannelProperties properties) {
- return TencentSmsChannelProperties.build(properties);
- }
-
- /**
- * 调用腾讯云 SDK 发送短信
- *
- * @param request 发送短信请求
- * @return 发送短信响应
- * @throws TencentCloudSDKException SDK 用来封装发送短信失败
- */
- private SendSmsResponse doSendSms0(SendSmsRequest request) throws TencentCloudSDKException {
- return client.SendSms(request);
- }
-
- /**
- * 封装腾讯云发送短信请求
- *
- * @param sendLogId 日志编号
- * @param mobile 手机号
- * @param apiTemplateId 短信 API 的模板编号
- * @param templateParams 短信模板参数。通过 List 数组,保证参数的顺序
- * @return 腾讯云发送短信请求
- */
- private SendSmsRequest buildSendSmsRequest(Long sendLogId,
- String mobile,
- String apiTemplateId,
- List> templateParams) {
+ public SmsSendRespDTO sendSms(Long sendLogId, String mobile,
+ String apiTemplateId, List> templateParams) throws Throwable {
+ // 构建请求
SendSmsRequest request = new SendSmsRequest();
- request.setSmsSdkAppId(((TencentSmsChannelProperties) properties).getSdkAppId());
+ request.setSmsSdkAppId(getSdkAppId());
request.setPhoneNumberSet(new String[]{mobile});
request.setSignName(properties.getSignature());
request.setTemplateId(apiTemplateId);
request.setTemplateParamSet(ArrayUtils.toArray(templateParams, e -> String.valueOf(e.getValue())));
request.setSessionContext(JsonUtils.toJsonString(new SessionContext().setLogId(sendLogId)));
- return request;
+ // 执行请求
+ SendSmsResponse response = client.SendSms(request);
+ SendStatus status = response.getSendStatusSet()[0];
+ return new SmsSendRespDTO().setSuccess(Objects.equals(status.getCode(), API_CODE_SUCCESS)).setSerialNo(status.getSerialNo())
+ .setApiRequestId(response.getRequestId()).setApiCode(status.getCode()).setApiMsg(status.getMessage());
}
@Override
- protected List doParseSmsReceiveStatus(String text) throws Throwable {
+ public List parseSmsReceiveStatus(String text) {
List callback = JsonUtils.parseArray(text, SmsReceiveStatus.class);
- return CollectionUtils.convertList(callback, status -> {
- SmsReceiveRespDTO data = new SmsReceiveRespDTO();
- data.setErrorCode(status.getErrCode()).setErrorMsg(status.getDescription());
- data.setReceiveTime(status.getReceiveTime()).setSuccess(SmsReceiveStatus.SUCCESS_CODE.equalsIgnoreCase(status.getStatus()));
- data.setMobile(status.getMobile()).setSerialNo(status.getSerialNo());
- SessionContext context;
- Long logId;
- Assert.notNull(context = status.getSessionContext(), "回执信息中未解析出 context,请联系腾讯云小助手");
- Assert.notNull(logId = context.getLogId(), "回执信息中未解析出 logId,请联系腾讯云小助手");
- data.setLogId(logId);
- return data;
- });
+ return convertList(callback, status -> new SmsReceiveRespDTO()
+ .setSuccess(SmsReceiveStatus.SUCCESS_CODE.equalsIgnoreCase(status.getStatus()))
+ .setErrorCode(status.getErrCode()).setErrorMsg(status.getDescription())
+ .setMobile(status.getMobile()).setReceiveTime(status.getReceiveTime())
+ .setSerialNo(status.getSerialNo()).setLogId(status.getSessionContext().getLogId()));
}
@Override
- protected SmsCommonResult doGetSmsTemplate(String apiTemplateId) throws Throwable {
- return invoke(() -> this.buildSmsTemplateStatusRequest(apiTemplateId),
- this::doGetSmsTemplate0,
- response -> {
- SmsTemplateRespDTO data = convertTemplateStatusDTO(response.getDescribeTemplateStatusSet()[0]);
- return SmsCommonResult.build(API_SUCCESS_CODE, null, response.getRequestId(), data, codeMapping);
- });
+ public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
+ // 构建请求
+ DescribeSmsTemplateListRequest request = new DescribeSmsTemplateListRequest();
+ request.setTemplateIdSet(new Long[]{Long.parseLong(apiTemplateId)});
+ request.setInternational(INTERNATIONAL_CHINA);
+ // 执行请求
+ DescribeSmsTemplateListResponse response = client.DescribeSmsTemplateList(request);
+ DescribeTemplateListStatus status = response.getDescribeTemplateStatusSet()[0];
+ if (status == null || status.getStatusCode() == null) {
+ return null;
+ }
+ return new SmsTemplateRespDTO().setId(status.getTemplateId().toString()).setContent(status.getTemplateContent())
+ .setAuditStatus(convertSmsTemplateAuditStatus(status.getStatusCode().intValue())).setAuditReason(status.getReviewReply());
}
@VisibleForTesting
- SmsTemplateRespDTO convertTemplateStatusDTO(DescribeTemplateListStatus templateStatus) {
- if (templateStatus == null) {
- return null;
+ Integer convertSmsTemplateAuditStatus(int templateStatus) {
+ switch (templateStatus) {
+ case 1: return SmsTemplateAuditStatusEnum.CHECKING.getStatus();
+ case 0: return SmsTemplateAuditStatusEnum.SUCCESS.getStatus();
+ case -1: return SmsTemplateAuditStatusEnum.FAIL.getStatus();
+ default: throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus));
}
- SmsTemplateAuditStatusEnum auditStatus;
- Assert.notNull(templateStatus.getStatusCode(),
- StrUtil.format("短信模版审核状态为 null,模版 id{}", templateStatus.getTemplateId()));
- switch (templateStatus.getStatusCode().intValue()) {
- case -1:
- auditStatus = SmsTemplateAuditStatusEnum.FAIL;
- break;
- case 0:
- auditStatus = SmsTemplateAuditStatusEnum.SUCCESS;
- break;
- case 1:
- auditStatus = SmsTemplateAuditStatusEnum.CHECKING;
- break;
- default:
- throw new IllegalStateException(StrUtil.format("不能解析短信模版审核状态{},模版 id{}",
- templateStatus.getStatusCode(), templateStatus.getTemplateId()));
- }
- SmsTemplateRespDTO data = new SmsTemplateRespDTO();
- data.setId(String.valueOf(templateStatus.getTemplateId())).setContent(templateStatus.getTemplateContent());
- data.setAuditStatus(auditStatus.getStatus()).setAuditReason(templateStatus.getReviewReply());
- return data;
- }
-
- /**
- * 封装查询模版审核状态请求
- * @param apiTemplateId api 的模版 id
- * @return 查询模版审核状态请求
- */
- private DescribeSmsTemplateListRequest buildSmsTemplateStatusRequest(String apiTemplateId) {
- DescribeSmsTemplateListRequest request = new DescribeSmsTemplateListRequest();
- request.setTemplateIdSet(new Long[]{Long.parseLong(apiTemplateId)});
- // 地区 0:表示国内短信。1:表示国际/港澳台短信。
- request.setInternational(INTERNATIONAL);
- return request;
- }
-
- /**
- * 调用腾讯云 SDK 查询短信模版状态
- *
- * @param request 查询短信模版状态请求
- * @return 查询短信模版状态响应
- * @throws TencentCloudSDKException SDK 用来封装查询短信模版状态失败
- */
- private DescribeSmsTemplateListResponse doGetSmsTemplate0(DescribeSmsTemplateListRequest request) throws TencentCloudSDKException {
- return client.DescribeSmsTemplateList(request);
- }
-
- SmsCommonResult invoke(Supplier requestSupplier,
- SdkFunction responseSupplier,
- Function> resultGen) {
- // 构建请求body
- Q request = requestSupplier.get();
- P response;
- // 调用腾讯云发送短信
- try {
- response = responseSupplier.apply(request);
- } catch (TencentCloudSDKException e) {
- // 调用异常,封装结果
- return SmsCommonResult.build(e.getErrorCode(), e.getMessage(), e.getRequestId(), null, codeMapping);
- }
- return resultGen.apply(response);
}
@Data
@@ -278,7 +198,7 @@ public class TencentSmsClient extends AbstractSmsClient {
private String serialNo;
/**
- * 用户的 session 内容(与发送接口的请求参数SessionContext一致)
+ * 用户的 session 内容(与发送接口的请求参数 SessionContext 一致)
*/
@JsonProperty("ext")
private SessionContext sessionContext;
@@ -293,10 +213,7 @@ public class TencentSmsClient extends AbstractSmsClient {
* 发送短信记录id
*/
private Long logId;
- }
- private interface SdkFunction {
- R apply(T t) throws TencentCloudSDKException;
}
}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsCodeMapping.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsCodeMapping.java
deleted file mode 100644
index 05ad355ef..000000000
--- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsCodeMapping.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package cn.iocoder.yudao.framework.sms.core.client.impl.tencent;
-
-import cn.iocoder.yudao.framework.common.exception.ErrorCode;
-import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
-import cn.iocoder.yudao.framework.sms.core.client.SmsCodeMapping;
-import cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
-
-import static cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants.*;
-
-/**
- * 腾讯云的 SmsCodeMapping 实现类
- *
- * 参见 https://cloud.tencent.com/document/api/382/52075#.E5.85.AC.E5.85.B1.E9.94.99.E8.AF.AF.E7.A0.81
- *
- * @author : shiwp
- */
-public class TencentSmsCodeMapping implements SmsCodeMapping {
-
- @Override
- public ErrorCode apply(String apiCode) {
- switch (apiCode) {
- case TencentSmsClient.API_SUCCESS_CODE: return GlobalErrorCodeConstants.SUCCESS;
- case "FailedOperation.ContainSensitiveWord": return SMS_SEND_CONTENT_INVALID;
- case "FailedOperation.JsonParseFail":
- case "MissingParameter.EmptyPhoneNumberSet":
- case "LimitExceeded.PhoneNumberCountLimit":
- case "FailedOperation.FailResolvePacket": return GlobalErrorCodeConstants.BAD_REQUEST;
- case "FailedOperation.InsufficientBalanceInSmsPackage": return SMS_ACCOUNT_MONEY_NOT_ENOUGH;
- case "FailedOperation.MarketingSendTimeConstraint": return SMS_SEND_MARKET_LIMIT_CONTROL;
- case "FailedOperation.PhoneNumberInBlacklist": return SMS_MOBILE_BLACK;
- case "FailedOperation.SignatureIncorrectOrUnapproved": return SMS_SIGN_INVALID;
- case "FailedOperation.MissingTemplateToModify":
- case "FailedOperation.TemplateIncorrectOrUnapproved": return SMS_TEMPLATE_INVALID;
- case "InvalidParameterValue.IncorrectPhoneNumber": return SMS_MOBILE_INVALID;
- case "InvalidParameterValue.SdkAppIdNotExist": return SMS_APP_ID_INVALID;
- case "InvalidParameterValue.TemplateParameterLengthLimit":
- case "InvalidParameterValue.TemplateParameterFormatError": return SMS_TEMPLATE_PARAM_ERROR;
- case "LimitExceeded.PhoneNumberDailyLimit": return SMS_SEND_DAY_LIMIT_CONTROL;
- case "LimitExceeded.PhoneNumberThirtySecondLimit":
- case "LimitExceeded.PhoneNumberOneHourLimit": return SMS_SEND_BUSINESS_LIMIT_CONTROL;
- case "UnauthorizedOperation.RequestPermissionDeny":
- case "FailedOperation.ForbidAddMarketingTemplates":
- case "FailedOperation.NotEnterpriseCertification":
- case "UnauthorizedOperation.IndividualUserMarketingSmsPermissionDeny": return SMS_PERMISSION_DENY;
- case "UnauthorizedOperation.RequestIpNotInWhitelist": return SMS_IP_DENY;
- case "AuthFailure.SecretIdNotFound": return SMS_ACCOUNT_INVALID;
- }
- return SmsFrameworkErrorCodeConstants.SMS_UNKNOWN;
- }
-}
\ No newline at end of file
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test-integration/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClientIntegrationTest.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test-integration/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClientIntegrationTest.java
deleted file mode 100644
index 36f9dec82..000000000
--- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test-integration/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClientIntegrationTest.java
+++ /dev/null
@@ -1,55 +0,0 @@
-package cn.iocoder.yudao.framework.sms.core.client.impl.aliyun;
-
-import cn.iocoder.yudao.framework.common.core.KeyValue;
-import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult;
-import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO;
-import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO;
-import cn.iocoder.yudao.framework.sms.core.client.impl.aliyun.AliyunSmsClient;
-import cn.iocoder.yudao.framework.sms.core.enums.SmsChannelEnum;
-import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties;
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Test;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * {@link AliyunSmsClient} 的集成测试
- */
-public class AliyunSmsClientIntegrationTest {
-
- private static AliyunSmsClient smsClient;
-
- @BeforeAll
- public static void before() {
- // 创建配置类
- SmsChannelProperties properties = new SmsChannelProperties();
- properties.setId(1L);
- properties.setSignature("Ballcat");
- properties.setCode(SmsChannelEnum.ALIYUN.getCode());
- properties.setApiKey(System.getenv("ALIYUN_ACCESS_KEY"));
- properties.setApiSecret(System.getenv("ALIYUN_SECRET_KEY"));
- // 创建客户端
- smsClient = new AliyunSmsClient(properties);
- smsClient.init();
- }
-
- @Test
- public void testSendSms() {
- List> templateParams = new ArrayList<>();
- templateParams.add(new KeyValue<>("code", "1024"));
-// templateParams.put("operation", "嘿嘿");
-// SmsResult result = smsClient.send(1L, "15601691399", "4372216", templateParams);
- SmsCommonResult result = smsClient.sendSms(1L, "15601691399",
- "SMS_207945135", templateParams);
- System.out.println(result);
- }
-
- @Test
- public void testGetSmsTemplate() {
- String apiTemplateId = "SMS_2079451351";
- SmsCommonResult result = smsClient.getSmsTemplate(apiTemplateId);
- System.out.println(result);
- }
-
-}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test-integration/java/cn/iocoder/yudao/framework/sms/core/client/impl/debug/DebugDingTalkSmsClientIntegrationTest.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test-integration/java/cn/iocoder/yudao/framework/sms/core/client/impl/debug/DebugDingTalkSmsClientIntegrationTest.java
deleted file mode 100644
index 47de977cb..000000000
--- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test-integration/java/cn/iocoder/yudao/framework/sms/core/client/impl/debug/DebugDingTalkSmsClientIntegrationTest.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package cn.iocoder.yudao.framework.sms.core.client.impl.debug;
-
-import cn.iocoder.yudao.framework.common.core.KeyValue;
-import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult;
-import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO;
-import cn.iocoder.yudao.framework.sms.core.client.impl.debug.DebugDingTalkSmsClient;
-import cn.iocoder.yudao.framework.sms.core.enums.SmsChannelEnum;
-import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties;
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Test;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * {@link DebugDingTalkSmsClient} 的集成测试
- */
-public class DebugDingTalkSmsClientIntegrationTest {
-
- private static DebugDingTalkSmsClient smsClient;
-
- @BeforeAll
- public static void init() {
- // 创建配置类
- SmsChannelProperties properties = new SmsChannelProperties();
- properties.setId(1L);
- properties.setSignature("芋道");
- properties.setCode(SmsChannelEnum.DEBUG_DING_TALK.getCode());
- properties.setApiKey("696b5d8ead48071237e4aa5861ff08dbadb2b4ded1c688a7b7c9afc615579859");
- properties.setApiSecret("SEC5c4e5ff888bc8a9923ae47f59e7ccd30af1f14d93c55b4e2c9cb094e35aeed67");
- // 创建客户端
- smsClient = new DebugDingTalkSmsClient(properties);
- smsClient.init();
- }
-
- @Test
- public void testSendSms() {
- List> templateParams = new ArrayList<>();
- templateParams.add(new KeyValue<>("code", "1024"));
- templateParams.add(new KeyValue<>("operation", "嘿嘿"));
-// SmsResult result = smsClient.send(1L, "15601691399", "4372216", templateParams);
- SmsCommonResult result = smsClient.sendSms(1L, "15601691399", "4383920", templateParams);
- System.out.println(result);
- }
-
-}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java
index 4b02c5121..c60cd26a1 100644
--- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java
+++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java
@@ -1,26 +1,20 @@
package cn.iocoder.yudao.framework.sms.core.client.impl.aliyun;
import cn.hutool.core.util.ReflectUtil;
-import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.framework.common.core.KeyValue;
-import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
-import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.sms.core.client.dto.SmsReceiveRespDTO;
import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO;
import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO;
import cn.iocoder.yudao.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties;
-import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
-import cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
-import com.aliyuncs.AcsRequest;
+import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateRequest;
import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateResponse;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
-import com.aliyuncs.exceptions.ClientException;
import com.google.common.collect.Lists;
-import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentMatcher;
import org.mockito.InjectMocks;
@@ -28,12 +22,10 @@ import org.mockito.Mock;
import java.time.LocalDateTime;
import java.util.List;
-import java.util.function.Function;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.when;
@@ -67,8 +59,7 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest {
}
@Test
- @SuppressWarnings("unchecked")
- public void testDoSendSms() throws ClientException {
+ public void tesSendSms_success() throws Throwable {
// 准备参数
Long sendLogId = randomLongId();
String mobile = randomString();
@@ -87,20 +78,47 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest {
}))).thenReturn(response);
// 调用
- SmsCommonResult result = smsClient.doSendSms(sendLogId, mobile,
+ SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
apiTemplateId, templateParams);
// 断言
+ assertTrue(result.getSuccess());
+ assertEquals(response.getRequestId(), result.getApiRequestId());
assertEquals(response.getCode(), result.getApiCode());
assertEquals(response.getMessage(), result.getApiMsg());
- assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode());
- assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg());
- assertEquals(response.getRequestId(), result.getApiRequestId());
- // 断言结果
- assertEquals(response.getBizId(), result.getData().getSerialNo());
+ assertEquals(response.getBizId(), result.getSerialNo());
}
@Test
- public void testDoTParseSmsReceiveStatus() throws Throwable {
+ public void tesSendSms_fail() throws Throwable {
+ // 准备参数
+ Long sendLogId = randomLongId();
+ String mobile = randomString();
+ String apiTemplateId = randomString();
+ List> templateParams = Lists.newArrayList(
+ new KeyValue<>("code", 1234), new KeyValue<>("op", "login"));
+ // mock 方法
+ SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> o.setCode("ERROR"));
+ when(client.getAcsResponse(argThat((ArgumentMatcher) acsRequest -> {
+ assertEquals(mobile, acsRequest.getPhoneNumbers());
+ assertEquals(properties.getSignature(), acsRequest.getSignName());
+ assertEquals(apiTemplateId, acsRequest.getTemplateCode());
+ assertEquals(toJsonString(MapUtils.convertMap(templateParams)), acsRequest.getTemplateParam());
+ assertEquals(sendLogId.toString(), acsRequest.getOutId());
+ return true;
+ }))).thenReturn(response);
+
+ // 调用
+ SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
+ // 断言
+ assertFalse(result.getSuccess());
+ assertEquals(response.getRequestId(), result.getApiRequestId());
+ assertEquals(response.getCode(), result.getApiCode());
+ assertEquals(response.getMessage(), result.getApiMsg());
+ assertEquals(response.getBizId(), result.getSerialNo());
+ }
+
+ @Test
+ public void testParseSmsReceiveStatus() {
// 准备参数
String text = "[\n" +
" {\n" +
@@ -118,20 +136,21 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest {
// mock 方法
// 调用
- List statuses = smsClient.doParseSmsReceiveStatus(text);
+ List statuses = smsClient.parseSmsReceiveStatus(text);
// 断言
assertEquals(1, statuses.size());
assertTrue(statuses.get(0).getSuccess());
assertEquals("DELIVERED", statuses.get(0).getErrorCode());
assertEquals("用户接收成功", statuses.get(0).getErrorMsg());
assertEquals("13900000001", statuses.get(0).getMobile());
- assertEquals(LocalDateTime.of(2017, 2, 2, 22, 23, 24), statuses.get(0).getReceiveTime());
+ assertEquals(LocalDateTime.of(2017, 2, 2, 22, 23, 24),
+ statuses.get(0).getReceiveTime());
assertEquals("12345", statuses.get(0).getSerialNo());
assertEquals(67890L, statuses.get(0).getLogId());
}
@Test
- public void testDoGetSmsTemplate() throws ClientException {
+ public void testGetSmsTemplate() throws Throwable {
// 准备参数
String apiTemplateId = randomString();
// mock 方法
@@ -145,18 +164,12 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest {
}))).thenReturn(response);
// 调用
- SmsCommonResult result = smsClient.doGetSmsTemplate(apiTemplateId);
+ SmsTemplateRespDTO result = smsClient.getSmsTemplate(apiTemplateId);
// 断言
- assertEquals(response.getCode(), result.getApiCode());
- assertEquals(response.getMessage(), result.getApiMsg());
- assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode());
- assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg());
- assertEquals(response.getRequestId(), result.getApiRequestId());
- // 断言结果
- assertEquals(response.getTemplateCode(), result.getData().getId());
- assertEquals(response.getTemplateContent(), result.getData().getContent());
- assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getData().getAuditStatus());
- assertEquals(response.getReason(), result.getData().getAuditReason());
+ assertEquals(response.getTemplateCode(), result.getId());
+ assertEquals(response.getTemplateContent(), result.getContent());
+ assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getAuditStatus());
+ assertEquals(response.getReason(), result.getAuditReason());
}
@Test
@@ -171,55 +184,4 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest {
"未知审核状态(3)");
}
- @Test
- @SuppressWarnings("unchecked")
- public void testInvoke_throwable() throws ClientException {
- // 准备参数
- QuerySmsTemplateRequest request = new QuerySmsTemplateRequest();
- // mock 方法
- ClientException ex = new ClientException("isv.INVALID_PARAMETERS", "参数不正确", randomString());
- when(client.getAcsResponse(any(AcsRequest.class))).thenThrow(ex);
-
- // 调用,并断言异常
- SmsCommonResult> result = smsClient.invoke(request, null);
- // 断言
- assertEquals(ex.getErrCode(), result.getApiCode());
- assertEquals(ex.getErrMsg(), result.getApiMsg());
- Assertions.assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR.getCode(), result.getCode());
- Assertions.assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR.getMsg(), result.getMsg());
- assertEquals(ex.getRequestId(), result.getApiRequestId());
- }
-
- @Test
- public void testInvoke_success() throws ClientException {
- // 准备参数
- QuerySmsTemplateRequest request = new QuerySmsTemplateRequest();
- Function responseConsumer = response -> {
- SmsTemplateRespDTO data = new SmsTemplateRespDTO();
- data.setId(response.getTemplateCode()).setContent(response.getTemplateContent());
- data.setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(response.getReason());
- return data;
- };
- // mock 方法
- QuerySmsTemplateResponse response = randomPojo(QuerySmsTemplateResponse.class, o -> {
- o.setCode("OK");
- o.setTemplateStatus(1); // 设置模板通过
- });
- when(client.getAcsResponse(any(AcsRequest.class))).thenReturn(response);
-
- // 调用
- SmsCommonResult result = smsClient.invoke(request, responseConsumer);
- // 断言
- assertEquals(response.getCode(), result.getApiCode());
- assertEquals(response.getMessage(), result.getApiMsg());
- assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode());
- assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg());
- assertEquals(response.getRequestId(), result.getApiRequestId());
- // 断言结果
- assertEquals(response.getTemplateCode(), result.getData().getId());
- assertEquals(response.getTemplateContent(), result.getData().getContent());
- assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getData().getAuditStatus());
- assertEquals(response.getReason(), result.getData().getAuditReason());
- }
-
}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMappingTest.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMappingTest.java
deleted file mode 100644
index ef8901daf..000000000
--- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMappingTest.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package cn.iocoder.yudao.framework.sms.core.client.impl.aliyun;
-
-import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
-import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
-import cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
-import org.junit.jupiter.api.Test;
-import org.mockito.InjectMocks;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * {@link AliyunSmsCodeMapping} 的单元测试
- *
- * @author 芋道源码
- */
-public class AliyunSmsCodeMappingTest extends BaseMockitoUnitTest {
-
- @InjectMocks
- private AliyunSmsCodeMapping codeMapping;
-
- @Test
- public void testApply() {
- assertEquals(GlobalErrorCodeConstants.SUCCESS, codeMapping.apply("OK"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("MissingAccessKeyId"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("isv.ACCOUNT_NOT_EXISTS"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("isv.ACCOUNT_ABNORMAL"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_DAY_LIMIT_CONTROL, codeMapping.apply("isv.DAY_LIMIT_CONTROL"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_CONTENT_INVALID, codeMapping.apply("isv.SMS_CONTENT_ILLEGAL"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("isv.SMS_SIGN_ILLEGAL"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("isv.SIGN_NAME_ILLEGAL"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("isp.RAM_PERMISSION_DENY"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH, codeMapping.apply("isv.OUT_OF_SERVICE"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH, codeMapping.apply("isv.AMOUNT_NOT_ENOUGH"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply("isv.SMS_TEMPLATE_ILLEGAL"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("isv.SMS_SIGNATURE_ILLEGAL"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR, codeMapping.apply("isv.INVALID_PARAMETERS"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR, codeMapping.apply("isv.INVALID_JSON_PARAM"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_MOBILE_INVALID, codeMapping.apply("isv.MOBILE_NUMBER_ILLEGAL"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR, codeMapping.apply("isv.TEMPLATE_MISSING_PARAMETERS"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply("isv.BUSINESS_LIMIT_CONTROL"));
- }
-
-}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsClientTest.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsClientTest.java
index 9269de724..d62eed1e1 100644
--- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsClientTest.java
+++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsClientTest.java
@@ -1,13 +1,10 @@
package cn.iocoder.yudao.framework.sms.core.client.impl.tencent;
import cn.hutool.core.util.ReflectUtil;
-import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.core.KeyValue;
-import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
-import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult;
import cn.iocoder.yudao.framework.sms.core.client.dto.SmsReceiveRespDTO;
import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO;
import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO;
@@ -31,7 +28,6 @@ import java.util.List;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
import static org.junit.jupiter.api.Assertions.*;
-import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.when;
@@ -78,7 +74,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
}
@Test
- public void testDoSendSms() throws Throwable {
+ public void testDoSendSms_success() throws Throwable {
// 准备参数
Long sendLogId = randomLongId();
String mobile = randomString();
@@ -94,7 +90,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
o.setSendStatusSet(sendStatuses);
SendStatus sendStatus = new SendStatus();
sendStatuses[0] = sendStatus;
- sendStatus.setCode(TencentSmsClient.API_SUCCESS_CODE);
+ sendStatus.setCode(TencentSmsClient.API_CODE_SUCCESS);
sendStatus.setMessage("send success");
sendStatus.setSerialNo(serialNo);
});
@@ -109,20 +105,58 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
}))).thenReturn(response);
// 调用
- SmsCommonResult result = smsClient.doSendSms(sendLogId, mobile,
- apiTemplateId, templateParams);
+ SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
// 断言
+ assertTrue(result.getSuccess());
+ assertEquals(response.getRequestId(), result.getApiRequestId());
assertEquals(response.getSendStatusSet()[0].getCode(), result.getApiCode());
assertEquals(response.getSendStatusSet()[0].getMessage(), result.getApiMsg());
- assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode());
- assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg());
- assertEquals(response.getRequestId(), result.getApiRequestId());
- // 断言结果
- assertEquals(response.getSendStatusSet()[0].getSerialNo(), result.getData().getSerialNo());
+ assertEquals(response.getSendStatusSet()[0].getSerialNo(), result.getSerialNo());
}
@Test
- public void testDoTParseSmsReceiveStatus() throws Throwable {
+ public void testDoSendSms_fail() throws Throwable {
+ // 准备参数
+ Long sendLogId = randomLongId();
+ String mobile = randomString();
+ String apiTemplateId = randomString();
+ List> templateParams = Lists.newArrayList(
+ new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
+ String requestId = randomString();
+ String serialNo = randomString();
+ // mock 方法
+ SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> {
+ o.setRequestId(requestId);
+ SendStatus[] sendStatuses = new SendStatus[1];
+ o.setSendStatusSet(sendStatuses);
+ SendStatus sendStatus = new SendStatus();
+ sendStatuses[0] = sendStatus;
+ sendStatus.setCode("ERROR");
+ sendStatus.setMessage("send success");
+ sendStatus.setSerialNo(serialNo);
+ });
+ when(client.SendSms(argThat(request -> {
+ assertEquals(mobile, request.getPhoneNumberSet()[0]);
+ assertEquals(properties.getSignature(), request.getSignName());
+ assertEquals(apiTemplateId, request.getTemplateId());
+ assertEquals(toJsonString(ArrayUtils.toArray(new ArrayList<>(MapUtils.convertMap(templateParams).values()), String::valueOf)),
+ toJsonString(request.getTemplateParamSet()));
+ assertEquals(sendLogId, ReflectUtil.getFieldValue(JsonUtils.parseObject(request.getSessionContext(), TencentSmsClient.SessionContext.class), "logId"));
+ return true;
+ }))).thenReturn(response);
+
+ // 调用
+ SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
+ // 断言
+ assertFalse(result.getSuccess());
+ assertEquals(response.getRequestId(), result.getApiRequestId());
+ assertEquals(response.getSendStatusSet()[0].getCode(), result.getApiCode());
+ assertEquals(response.getSendStatusSet()[0].getMessage(), result.getApiMsg());
+ assertEquals(response.getSendStatusSet()[0].getSerialNo(), result.getSerialNo());
+ }
+
+ @Test
+ public void testParseSmsReceiveStatus() {
// 准备参数
String text = "[\n" +
" {\n" +
@@ -139,7 +173,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
// mock 方法
// 调用
- List statuses = smsClient.doParseSmsReceiveStatus(text);
+ List statuses = smsClient.parseSmsReceiveStatus(text);
// 断言
assertEquals(1, statuses.size());
assertTrue(statuses.get(0).getSuccess());
@@ -152,7 +186,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
}
@Test
- public void testDoGetSmsTemplate() throws Throwable {
+ public void testGetSmsTemplate() throws Throwable {
// 准备参数
Long apiTemplateId = randomLongId();
String requestId = randomString();
@@ -173,50 +207,24 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
}))).thenReturn(response);
// 调用
- SmsCommonResult result = smsClient.doGetSmsTemplate(apiTemplateId.toString());
+ SmsTemplateRespDTO result = smsClient.getSmsTemplate(apiTemplateId.toString());
// 断言
- assertEquals(TencentSmsClient.API_SUCCESS_CODE, result.getApiCode());
- assertNull(result.getApiMsg());
- assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode());
- assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg());
- assertEquals(response.getRequestId(), result.getApiRequestId());
- // 断言结果
- assertEquals(response.getDescribeTemplateStatusSet()[0].getTemplateId().toString(), result.getData().getId());
- assertEquals(response.getDescribeTemplateStatusSet()[0].getTemplateContent(), result.getData().getContent());
- assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getData().getAuditStatus());
- assertEquals(response.getDescribeTemplateStatusSet()[0].getReviewReply(), result.getData().getAuditReason());
+ assertEquals(response.getDescribeTemplateStatusSet()[0].getTemplateId().toString(), result.getId());
+ assertEquals(response.getDescribeTemplateStatusSet()[0].getTemplateContent(), result.getContent());
+ assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getAuditStatus());
+ assertEquals(response.getDescribeTemplateStatusSet()[0].getReviewReply(), result.getAuditReason());
}
@Test
- public void testConvertSuccessTemplateStatus() {
- testTemplateStatus(SmsTemplateAuditStatusEnum.SUCCESS, 0L);
- }
-
- @Test
- public void testConvertCheckingTemplateStatus() {
- testTemplateStatus(SmsTemplateAuditStatusEnum.CHECKING, 1L);
- }
-
- @Test
- public void testConvertFailTemplateStatus() {
- testTemplateStatus(SmsTemplateAuditStatusEnum.FAIL, -1L);
- }
-
- @Test
- public void testConvertUnknownTemplateStatus() {
- DescribeTemplateListStatus templateStatus = new DescribeTemplateListStatus();
- templateStatus.setStatusCode(3L);
- Long templateId = randomLongId();
- // 调用,并断言结果
- assertThrows(IllegalStateException.class, () -> smsClient.convertTemplateStatusDTO(templateStatus),
- StrUtil.format("不能解析短信模版审核状态[3],模版id[{}]", templateId));
- }
-
- private void testTemplateStatus(SmsTemplateAuditStatusEnum expected, Long value) {
- DescribeTemplateListStatus templateStatus = new DescribeTemplateListStatus();
- templateStatus.setStatusCode(value);
- SmsTemplateRespDTO result = smsClient.convertTemplateStatusDTO(templateStatus);
- assertEquals(expected.getStatus(), result.getAuditStatus());
+ public void testConvertSmsTemplateAuditStatus() {
+ assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(),
+ smsClient.convertSmsTemplateAuditStatus(0));
+ assertEquals(SmsTemplateAuditStatusEnum.CHECKING.getStatus(),
+ smsClient.convertSmsTemplateAuditStatus(1));
+ assertEquals(SmsTemplateAuditStatusEnum.FAIL.getStatus(),
+ smsClient.convertSmsTemplateAuditStatus(-1));
+ assertThrows(IllegalArgumentException.class, () -> smsClient.convertSmsTemplateAuditStatus(3),
+ "未知审核状态(3)");
}
}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsCodeMappingTest.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsCodeMappingTest.java
deleted file mode 100644
index ebcdaf18a..000000000
--- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsCodeMappingTest.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package cn.iocoder.yudao.framework.sms.core.client.impl.tencent;
-
-import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
-import cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
-import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
-import org.junit.jupiter.api.Test;
-import org.mockito.InjectMocks;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * {@link TencentSmsCodeMapping} 的单元测试
- *
- * @author : shiwp
- */
-public class TencentSmsCodeMappingTest extends BaseMockitoUnitTest {
-
- @InjectMocks
- private TencentSmsCodeMapping codeMapping;
-
- @Test
- public void testApply() {
- assertEquals(GlobalErrorCodeConstants.SUCCESS, codeMapping.apply(TencentSmsClient.API_SUCCESS_CODE));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_CONTENT_INVALID, codeMapping.apply("FailedOperation.ContainSensitiveWord"));
- assertEquals(GlobalErrorCodeConstants.BAD_REQUEST, codeMapping.apply("FailedOperation.JsonParseFail"));
- assertEquals(GlobalErrorCodeConstants.BAD_REQUEST, codeMapping.apply("MissingParameter.EmptyPhoneNumberSet"));
- assertEquals(GlobalErrorCodeConstants.BAD_REQUEST, codeMapping.apply("LimitExceeded.PhoneNumberCountLimit"));
- assertEquals(GlobalErrorCodeConstants.BAD_REQUEST, codeMapping.apply("FailedOperation.FailResolvePacket"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH, codeMapping.apply("FailedOperation.InsufficientBalanceInSmsPackage"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_MARKET_LIMIT_CONTROL, codeMapping.apply("FailedOperation.MarketingSendTimeConstraint"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_MOBILE_BLACK, codeMapping.apply("FailedOperation.PhoneNumberInBlacklist"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("FailedOperation.SignatureIncorrectOrUnapproved"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply("FailedOperation.MissingTemplateToModify"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply("FailedOperation.TemplateIncorrectOrUnapproved"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_MOBILE_INVALID, codeMapping.apply("InvalidParameterValue.IncorrectPhoneNumber"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_APP_ID_INVALID, codeMapping.apply("InvalidParameterValue.SdkAppIdNotExist"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR, codeMapping.apply("InvalidParameterValue.TemplateParameterLengthLimit"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR, codeMapping.apply("InvalidParameterValue.TemplateParameterFormatError"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_DAY_LIMIT_CONTROL, codeMapping.apply("LimitExceeded.PhoneNumberDailyLimit"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply("LimitExceeded.PhoneNumberThirtySecondLimit"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply("LimitExceeded.PhoneNumberOneHourLimit"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("UnauthorizedOperation.RequestPermissionDeny"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("FailedOperation.ForbidAddMarketingTemplates"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("FailedOperation.NotEnterpriseCertification"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("UnauthorizedOperation.IndividualUserMarketingSmsPermissionDeny"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_IP_DENY, codeMapping.apply("UnauthorizedOperation.RequestIpNotInWhitelist"));
- assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("AuthFailure.SecretIdNotFound"));
- }
-
-}
\ No newline at end of file
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-social/pom.xml b/yudao-framework/yudao-spring-boot-starter-biz-social/pom.xml
deleted file mode 100644
index e39107918..000000000
--- a/yudao-framework/yudao-spring-boot-starter-biz-social/pom.xml
+++ /dev/null
@@ -1,56 +0,0 @@
-
-
-
- cn.iocoder.boot
- yudao-framework
- ${revision}
-
- jar
- 4.0.0
-
- yudao-spring-boot-starter-biz-social
- ${project.artifactId}
-
-
-
- cn.iocoder.boot
- yudao-common
-
-
-
- org.springframework.boot
- spring-boot-starter-aop
-
-
-
- cn.iocoder.boot
- yudao-spring-boot-starter-web
-
-
-
- org.springframework.boot
- spring-boot-configuration-processor
- true
-
-
-
- com.xingyuv
- spring-boot-starter-justauth
-
-
- cn.hutool
- hutool-core
-
-
-
-
- cn.iocoder.boot
- yudao-spring-boot-starter-redis
-
-
-
-
-
-
\ No newline at end of file
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/config/YudaoSocialAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/config/YudaoSocialAutoConfiguration.java
deleted file mode 100644
index 57accd20d..000000000
--- a/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/config/YudaoSocialAutoConfiguration.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package cn.iocoder.yudao.framework.social.config;
-
-import cn.iocoder.yudao.framework.social.core.YudaoAuthRequestFactory;
-import com.xingyuv.http.HttpUtil;
-import com.xingyuv.http.support.hutool.HutoolImpl;
-import com.xingyuv.jushauth.cache.AuthStateCache;
-import com.xingyuv.justauth.autoconfigure.JustAuthProperties;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.boot.autoconfigure.AutoConfiguration;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Primary;
-
-/**
- * 社交自动装配类
- *
- * @author timfruit
- * @date 2021-10-30
- */
-@Slf4j
-@AutoConfiguration
-@EnableConfigurationProperties(JustAuthProperties.class)
-public class YudaoSocialAutoConfiguration {
-
- @Bean
- @Primary
- @ConditionalOnProperty(prefix = "justauth", value = "enabled", havingValue = "true", matchIfMissing = true)
- public YudaoAuthRequestFactory yudaoAuthRequestFactory(JustAuthProperties properties, AuthStateCache authStateCache) {
- // 需要修改 HttpUtil 使用的实现,避免类报错
- HttpUtil.setHttp(new HutoolImpl());
- // 创建 YudaoAuthRequestFactory
- return new YudaoAuthRequestFactory(properties, authStateCache);
- }
-
-}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/YudaoAuthRequestFactory.java b/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/YudaoAuthRequestFactory.java
deleted file mode 100644
index 6cabb0ccd..000000000
--- a/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/YudaoAuthRequestFactory.java
+++ /dev/null
@@ -1,94 +0,0 @@
-package cn.iocoder.yudao.framework.social.core;
-
-import cn.hutool.core.util.EnumUtil;
-import cn.hutool.core.util.ReflectUtil;
-import cn.iocoder.yudao.framework.social.core.enums.AuthExtendSource;
-import cn.iocoder.yudao.framework.social.core.request.AuthWeChatMiniAppRequest;
-import cn.iocoder.yudao.framework.social.core.request.AuthWeChatMpRequest;
-import com.xingyuv.jushauth.cache.AuthStateCache;
-import com.xingyuv.jushauth.config.AuthConfig;
-import com.xingyuv.jushauth.config.AuthSource;
-import com.xingyuv.jushauth.request.AuthRequest;
-import com.xingyuv.justauth.AuthRequestFactory;
-import com.xingyuv.justauth.autoconfigure.JustAuthProperties;
-
-import java.lang.reflect.Method;
-
-import static com.xingyuv.jushauth.config.AuthDefaultSource.WECHAT_MP;
-
-/**
- * 第三方授权拓展 request 工厂类
- * 为使得拓展配置 {@link AuthConfig} 和默认配置齐平,所以自定义本工厂类
- *
- * @author timfruit
- * @date 2021-10-31
- */
-public class YudaoAuthRequestFactory extends AuthRequestFactory {
-
- protected JustAuthProperties properties;
- protected AuthStateCache authStateCache;
-
- /**
- * 由于父类 configureHttpConfig 方法是 private 修饰,所以获取后,进行反射调用
- */
- private final Method configureHttpConfigMethod = ReflectUtil.getMethod(AuthRequestFactory.class,
- "configureHttpConfig", String.class, AuthConfig.class, JustAuthProperties.JustAuthHttpConfig.class);
-
- public YudaoAuthRequestFactory(JustAuthProperties properties, AuthStateCache authStateCache) {
- super(properties, authStateCache);
- this.properties = properties;
- this.authStateCache = authStateCache;
- }
-
- /**
- * 返回 AuthRequest 对象
- *
- * @param source {@link AuthSource}
- * @return {@link AuthRequest}
- */
- @Override
- public AuthRequest get(String source) {
- // 先尝试获取自定义扩展的
- AuthRequest authRequest = getExtendRequest(source);
- // 找不到,使用默认拓展
- if (authRequest == null) {
- authRequest = super.get(source);
- }
- return authRequest;
- }
-
- protected AuthRequest getExtendRequest(String source) {
- // TODO 芋艿:临时兼容 justauth 迁移的类型不对问题;
- if (WECHAT_MP.name().equalsIgnoreCase(source)) {
- AuthConfig config = properties.getType().get(WECHAT_MP.name());
- return new AuthWeChatMpRequest(config, authStateCache);
- }
-
- AuthExtendSource authExtendSource;
- try {
- authExtendSource = EnumUtil.fromString(AuthExtendSource.class, source.toUpperCase());
- } catch (IllegalArgumentException e) {
- // 无自定义匹配
- return null;
- }
-
- // 拓展配置和默认配置齐平,properties 放在一起
- AuthConfig config = properties.getType().get(authExtendSource.name());
- // 找不到对应关系,直接返回空
- if (config == null) {
- return null;
- }
- // 反射调用,配置 http config
- ReflectUtil.invoke(this, configureHttpConfigMethod, authExtendSource.name(), config, properties.getHttpConfig());
-
- // 获得拓展的 Request
- // noinspection SwitchStatementWithTooFewBranches
- switch (authExtendSource) {
- case WECHAT_MINI_APP:
- return new AuthWeChatMiniAppRequest(config, authStateCache);
- default:
- return null;
- }
- }
-
-}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/enums/AuthExtendSource.java b/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/enums/AuthExtendSource.java
deleted file mode 100644
index fd0fff709..000000000
--- a/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/enums/AuthExtendSource.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package cn.iocoder.yudao.framework.social.core.enums;
-
-import com.xingyuv.jushauth.config.AuthSource;
-import com.xingyuv.jushauth.request.AuthDefaultRequest;
-
-/**
- * 拓展 JustAuth 各 api 需要的 url, 用枚举类分平台类型管理
- *
- * 默认配置 {@link com.xingyuv.jushauth.config.AuthDefaultSource}
- *
- * @author timfruit
- */
-public enum AuthExtendSource implements AuthSource {
-
- /**
- * 微信小程序授权登录
- */
- WECHAT_MINI_APP {
-
- @Override
- public String authorize() {
- // 参见 https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html 文档
- throw new UnsupportedOperationException("不支持获取授权 url,请使用小程序内置函数 wx.login() 登录获取 code");
- }
-
- @Override
- public String accessToken() {
- // 参见 https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html 文档
- // 获取 openid, unionId , session_key 等字段
- return "https://api.weixin.qq.com/sns/jscode2session";
- }
-
- @Override
- public String userInfo() {
- // 参见 https://developers.weixin.qq.com/miniprogram/dev/api/open-api/user-info/wx.getUserProfile.html 文档
- throw new UnsupportedOperationException("不支持获取用户信息 url,请使用小程序内置函数 wx.getUserProfile() 获取用户信息");
- }
-
- @Override
- public Class extends AuthDefaultRequest> getTargetClass() {
- return null;
- }
- }
-
-}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/request/AuthWeChatMiniAppRequest.java b/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/request/AuthWeChatMiniAppRequest.java
deleted file mode 100644
index 964eb31cd..000000000
--- a/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/request/AuthWeChatMiniAppRequest.java
+++ /dev/null
@@ -1,97 +0,0 @@
-package cn.iocoder.yudao.framework.social.core.request;
-
-import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
-import cn.iocoder.yudao.framework.social.core.enums.AuthExtendSource;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.xingyuv.jushauth.cache.AuthStateCache;
-import com.xingyuv.jushauth.config.AuthConfig;
-import com.xingyuv.jushauth.exception.AuthException;
-import com.xingyuv.jushauth.model.AuthCallback;
-import com.xingyuv.jushauth.model.AuthToken;
-import com.xingyuv.jushauth.model.AuthUser;
-import com.xingyuv.jushauth.request.AuthDefaultRequest;
-import com.xingyuv.jushauth.utils.HttpUtils;
-import com.xingyuv.jushauth.utils.UrlBuilder;
-import lombok.Data;
-
-/**
- * 微信小程序登陆 Request 请求
- *
- * 由于 JustAuth 定位是面向 Web 为主的三方登录,所以微信小程序只能自己封装
- *
- * @author timfruit
- * @date 2021-10-29
- */
-public class AuthWeChatMiniAppRequest extends AuthDefaultRequest {
-
- public AuthWeChatMiniAppRequest(AuthConfig config, AuthStateCache authStateCache) {
- super(config, AuthExtendSource.WECHAT_MINI_APP, authStateCache);
- }
-
- @Override
- protected AuthToken getAccessToken(AuthCallback authCallback) {
- // 参见 https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html 文档
- // 使用 code 获取对应的 openId、unionId 等字段
- String response = new HttpUtils(config.getHttpConfig()).get(accessTokenUrl(authCallback.getCode())).getBody();
- JSCode2SessionResponse accessTokenObject = JsonUtils.parseObject(response, JSCode2SessionResponse.class);
- assert accessTokenObject != null;
- checkResponse(accessTokenObject);
- // 拼装结果
- return AuthToken.builder()
- .openId(accessTokenObject.getOpenid())
- .unionId(accessTokenObject.getUnionId())
- .build();
- }
-
- @Override
- protected AuthUser getUserInfo(AuthToken authToken) {
- // 参见 https://developers.weixin.qq.com/miniprogram/dev/api/open-api/user-info/wx.getUserProfile.html 文档
- // 如果需要用户信息,需要在小程序调用函数后传给后端
- return AuthUser.builder()
- .username("")
- .nickname("")
- .avatar("")
- .uuid(authToken.getOpenId())
- .token(authToken)
- .source(source.toString())
- .build();
- }
-
- /**
- * 检查响应内容是否正确
- *
- * @param response 请求响应内容
- */
- private void checkResponse(JSCode2SessionResponse response) {
- if (response.getErrorCode() != 0) {
- throw new AuthException(response.getErrorCode(), response.getErrorMsg());
- }
- }
-
- @Override
- protected String accessTokenUrl(String code) {
- return UrlBuilder.fromBaseUrl(source.accessToken())
- .queryParam("appid", config.getClientId())
- .queryParam("secret", config.getClientSecret())
- .queryParam("js_code", code)
- .queryParam("grant_type", "authorization_code")
- .build();
- }
-
- @Data
- @SuppressWarnings("SpellCheckingInspection")
- private static class JSCode2SessionResponse {
-
- @JsonProperty("errcode")
- private int errorCode;
- @JsonProperty("errmsg")
- private String errorMsg;
- @JsonProperty("session_key")
- private String sessionKey;
- private String openid;
- @JsonProperty("unionid")
- private String unionId;
-
- }
-
-}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/request/AuthWeChatMpRequest.java b/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/request/AuthWeChatMpRequest.java
deleted file mode 100644
index 13b77cd35..000000000
--- a/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/request/AuthWeChatMpRequest.java
+++ /dev/null
@@ -1,178 +0,0 @@
-package cn.iocoder.yudao.framework.social.core.request;
-
-import com.alibaba.fastjson.JSONObject;
-import com.xingyuv.jushauth.cache.AuthStateCache;
-import com.xingyuv.jushauth.config.AuthConfig;
-import com.xingyuv.jushauth.config.AuthDefaultSource;
-import com.xingyuv.jushauth.enums.AuthResponseStatus;
-import com.xingyuv.jushauth.enums.AuthUserGender;
-import com.xingyuv.jushauth.enums.scope.AuthWechatMpScope;
-import com.xingyuv.jushauth.exception.AuthException;
-import com.xingyuv.jushauth.model.AuthCallback;
-import com.xingyuv.jushauth.model.AuthResponse;
-import com.xingyuv.jushauth.model.AuthToken;
-import com.xingyuv.jushauth.model.AuthUser;
-import com.xingyuv.jushauth.request.AuthDefaultRequest;
-import com.xingyuv.jushauth.utils.AuthScopeUtils;
-import com.xingyuv.jushauth.utils.GlobalAuthUtils;
-import com.xingyuv.jushauth.utils.HttpUtils;
-import com.xingyuv.jushauth.utils.UrlBuilder;
-
-/**
- * 微信公众平台登录
- *
- * @author yangkai.shen (https://xkcoding.com)
- * @since 1.1.0
- */
-public class AuthWeChatMpRequest extends AuthDefaultRequest {
- public AuthWeChatMpRequest(AuthConfig config) {
- super(config, AuthDefaultSource.WECHAT_MP);
- }
-
- public AuthWeChatMpRequest(AuthConfig config, AuthStateCache authStateCache) {
- super(config, AuthDefaultSource.WECHAT_MP, authStateCache);
- }
-
- /**
- * 微信的特殊性,此时返回的信息同时包含 openid 和 access_token
- *
- * @param authCallback 回调返回的参数
- * @return 所有信息
- */
- @Override
- protected AuthToken getAccessToken(AuthCallback authCallback) {
- return this.getToken(accessTokenUrl(authCallback.getCode()));
- }
-
- @Override
- protected AuthUser getUserInfo(AuthToken authToken) {
- String openId = authToken.getOpenId();
-
- String response = doGetUserInfo(authToken);
- JSONObject object = JSONObject.parseObject(response);
-
- this.checkResponse(object);
-
- String location = String.format("%s-%s-%s", object.getString("country"), object.getString("province"), object.getString("city"));
-
- if (object.containsKey("unionid")) {
- authToken.setUnionId(object.getString("unionid"));
- }
-
- return AuthUser.builder()
- .rawUserInfo(object)
- .username(object.getString("nickname"))
- .nickname(object.getString("nickname"))
- .avatar(object.getString("headimgurl"))
- .location(location)
- .uuid(openId)
- .gender(AuthUserGender.getWechatRealGender(object.getString("sex")))
- .token(authToken)
- .source(source.toString())
- .build();
- }
-
- @Override
- public AuthResponse refresh(AuthToken oldToken) {
- return AuthResponse.builder()
- .code(AuthResponseStatus.SUCCESS.getCode())
- .data(this.getToken(refreshTokenUrl(oldToken.getRefreshToken())))
- .build();
- }
-
- /**
- * 检查响应内容是否正确
- *
- * @param object 请求响应内容
- */
- private void checkResponse(JSONObject object) {
- if (object.containsKey("errcode")) {
- throw new AuthException(object.getIntValue("errcode"), object.getString("errmsg"));
- }
- }
-
- /**
- * 获取token,适用于获取access_token和刷新token
- *
- * @param accessTokenUrl 实际请求token的地址
- * @return token对象
- */
- private AuthToken getToken(String accessTokenUrl) {
- String response = new HttpUtils(config.getHttpConfig()).get(accessTokenUrl).getBody();
- JSONObject accessTokenObject = JSONObject.parseObject(response);
-
- this.checkResponse(accessTokenObject);
-
- return AuthToken.builder()
- .accessToken(accessTokenObject.getString("access_token"))
- .refreshToken(accessTokenObject.getString("refresh_token"))
- .expireIn(accessTokenObject.getIntValue("expires_in"))
- .openId(accessTokenObject.getString("openid"))
- .scope(accessTokenObject.getString("scope"))
- .build();
- }
-
- /**
- * 返回带{@code state}参数的授权url,授权回调时会带上这个{@code state}
- *
- * @param state state 验证授权流程的参数,可以防止csrf
- * @return 返回授权地址
- * @since 1.9.3
- */
- @Override
- public String authorize(String state) {
- return UrlBuilder.fromBaseUrl(source.authorize())
- .queryParam("appid", config.getClientId())
- .queryParam("redirect_uri", GlobalAuthUtils.urlEncode(config.getRedirectUri()))
- .queryParam("response_type", "code")
- .queryParam("scope", this.getScopes(",", false, AuthScopeUtils.getDefaultScopes(AuthWechatMpScope.values())))
- .queryParam("state", getRealState(state).concat("#wechat_redirect"))
- .build();
- }
-
- /**
- * 返回获取accessToken的url
- *
- * @param code 授权码
- * @return 返回获取accessToken的url
- */
- @Override
- protected String accessTokenUrl(String code) {
- return UrlBuilder.fromBaseUrl(source.accessToken())
- .queryParam("appid", config.getClientId())
- .queryParam("secret", config.getClientSecret())
- .queryParam("code", code)
- .queryParam("grant_type", "authorization_code")
- .build();
- }
-
- /**
- * 返回获取userInfo的url
- *
- * @param authToken 用户授权后的token
- * @return 返回获取userInfo的url
- */
- @Override
- protected String userInfoUrl(AuthToken authToken) {
- return UrlBuilder.fromBaseUrl(source.userInfo())
- .queryParam("access_token", authToken.getAccessToken())
- .queryParam("openid", authToken.getOpenId())
- .queryParam("lang", "zh_CN")
- .build();
- }
-
- /**
- * 返回获取userInfo的url
- *
- * @param refreshToken getAccessToken方法返回的refreshToken
- * @return 返回获取userInfo的url
- */
- @Override
- protected String refreshTokenUrl(String refreshToken) {
- return UrlBuilder.fromBaseUrl(source.refresh())
- .queryParam("appid", config.getClientId())
- .queryParam("grant_type", "refresh_token")
- .queryParam("refresh_token", refreshToken)
- .build();
- }
-}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
deleted file mode 100644
index 5d9b35599..000000000
--- a/yudao-framework/yudao-spring-boot-starter-biz-social/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ /dev/null
@@ -1 +0,0 @@
-cn.iocoder.yudao.framework.social.config.YudaoSocialAutoConfiguration
\ No newline at end of file
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-weixin/pom.xml b/yudao-framework/yudao-spring-boot-starter-biz-weixin/pom.xml
deleted file mode 100644
index b67e542cf..000000000
--- a/yudao-framework/yudao-spring-boot-starter-biz-weixin/pom.xml
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
-
- cn.iocoder.boot
- yudao-framework
- ${revision}
-
- 4.0.0
- yudao-spring-boot-starter-biz-weixin
- jar
-
- ${project.artifactId}
- 微信拓展
- 1. 基于 weixin-java-mp 库,对接微信公众号平台。目前主要解决微信公众号的支付场景。
- 2. 基于 weixin-java-miniapp 库,对接微信小程序。目前主要解决微信小程序的一键登录场景。
-
- https://github.com/YunaiV/ruoyi-vue-pro
-
-
-
- cn.iocoder.boot
- yudao-common
-
-
-
-
- cn.iocoder.boot
- yudao-spring-boot-starter-test
- test
-
-
-
-
- com.github.binarywang
- wx-java-mp-spring-boot-starter
-
-
- com.github.binarywang
- wx-java-miniapp-spring-boot-starter
-
-
-
-
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-weixin/src/main/java/cn/iocoder/yudao/framework/weixin/package-info.java b/yudao-framework/yudao-spring-boot-starter-biz-weixin/src/main/java/cn/iocoder/yudao/framework/weixin/package-info.java
deleted file mode 100644
index cf15f39bf..000000000
--- a/yudao-framework/yudao-spring-boot-starter-biz-weixin/src/main/java/cn/iocoder/yudao/framework/weixin/package-info.java
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * 微信拓展
- * 1. 基于 weixin-java-mp 库,对接微信公众号平台。目前主要解决微信公众号的支付场景。
- * 2. 基于 weixin-java-miniapp 库,对接微信小程序。目前主要解决微信小程序的一键登录场景。
- */
-package cn.iocoder.yudao.framework.weixin;
-
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-weixin/src/test-integration/java/cn/iocoder/yudao/framework/weixin/WxMpServiceTest.java b/yudao-framework/yudao-spring-boot-starter-biz-weixin/src/test-integration/java/cn/iocoder/yudao/framework/weixin/WxMpServiceTest.java
deleted file mode 100644
index 392e651c5..000000000
--- a/yudao-framework/yudao-spring-boot-starter-biz-weixin/src/test-integration/java/cn/iocoder/yudao/framework/weixin/WxMpServiceTest.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package cn.iocoder.yudao.framework.weixin;
-
-import me.chanjar.weixin.common.error.WxErrorException;
-import me.chanjar.weixin.mp.api.WxMpService;
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.boot.test.context.SpringBootTest;
-
-import javax.annotation.Resource;
-
-@SpringBootTest(classes = WxMpServiceTest.Application.class)
-public class WxMpServiceTest {
-
- @Resource
- private WxMpService wxMpService;
-
- @Test
- public void testGetAccessToken() throws WxErrorException {
- String accessToken = wxMpService.getAccessToken();
- System.out.println(accessToken);
- }
-
- @Test
- public void testGet() throws WxErrorException {
- String jsapiTicket = wxMpService.getJsapiTicket();
- System.out.println(jsapiTicket);
- }
-
- @SpringBootApplication
- public static class Application {
-
- }
-
-}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-weixin/src/test-integration/resources/application.yml b/yudao-framework/yudao-spring-boot-starter-biz-weixin/src/test-integration/resources/application.yml
deleted file mode 100644
index 9b30060a8..000000000
--- a/yudao-framework/yudao-spring-boot-starter-biz-weixin/src/test-integration/resources/application.yml
+++ /dev/null
@@ -1,11 +0,0 @@
---- #################### 微信公众号相关配置 ####################
-wx: # 参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md 文档
- mp:
- # 公众号配置(必填)
- app-id: wx041349c6f39b268b
- secret: 5abee519483bc9f8cb37ce280e814bd0
- # 存储配置,解决 AccessToken 的跨节点的共享
-# config-storage:
-# type: RedisTemplate # 采用 RedisTemplate 操作 Redis,会自动从 Spring 中获取
-# key-prefix: wx # Redis Key 的前缀 TODO 芋艿:解决下 Redis key 管理的配置
-# http-client-type: HttpClient # 采用 HttpClient 请求微信公众号平台
diff --git a/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/context/FlowableContextHolder.java b/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/context/FlowableContextHolder.java
new file mode 100644
index 000000000..efc6d5340
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/context/FlowableContextHolder.java
@@ -0,0 +1,40 @@
+package cn.iocoder.yudao.framework.flowable.core.context;
+
+import cn.hutool.core.collection.CollUtil;
+import com.alibaba.ttl.TransmittableThreadLocal;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 工作流--用户用到的上下文相关信息
+ */
+public class FlowableContextHolder {
+
+ private static final ThreadLocal>> ASSIGNEE = new TransmittableThreadLocal<>();
+
+ /**
+ * 通过流程任务的定义 key ,拿到提前选好的审批人
+ * 此方法目的:首次创建流程实例时,数据库中还查询不到 assignee 字段,所以存入上下文中获取
+ *
+ * @param taskDefinitionKey 流程任务 key
+ * @return 审批人 ID 集合
+ */
+ public static List getAssigneeByTaskDefinitionKey(String taskDefinitionKey) {
+ if (CollUtil.isNotEmpty(ASSIGNEE.get())) {
+ return ASSIGNEE.get().get(taskDefinitionKey);
+ }
+ return Collections.emptyList();
+ }
+
+ /**
+ * 存入提前选好的审批人到上下文线程变量中
+ *
+ * @param assignee 流程任务 key -> 审批人 ID 炅和
+ */
+ public static void setAssignee(Map> assignee) {
+ ASSIGNEE.set(assignee);
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQConsumerAutoConfiguration.java
similarity index 92%
rename from yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQAutoConfiguration.java
rename to yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQConsumerAutoConfiguration.java
index bbc63b719..d02e84b14 100644
--- a/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQAutoConfiguration.java
+++ b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQConsumerAutoConfiguration.java
@@ -5,7 +5,6 @@ import cn.hutool.core.util.StrUtil;
import cn.hutool.system.SystemUtil;
import cn.iocoder.yudao.framework.common.enums.DocumentEnum;
import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
-import cn.iocoder.yudao.framework.mq.redis.core.interceptor.RedisMessageInterceptor;
import cn.iocoder.yudao.framework.mq.redis.core.job.RedisPendingMessageResendJob;
import cn.iocoder.yudao.framework.mq.redis.core.pubsub.AbstractRedisChannelMessageListener;
import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener;
@@ -23,7 +22,6 @@ import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.stream.StreamMessageListenerContainer;
@@ -33,30 +31,19 @@ import java.util.List;
import java.util.Properties;
/**
- * 消息队列配置类
+ * Redis 消息队列 Consumer 配置类
*
* @author 芋道源码
*/
@Slf4j
@EnableScheduling // 启用定时任务,用于 RedisPendingMessageResendJob 重发消息
@AutoConfiguration(after = YudaoRedisAutoConfiguration.class)
-public class YudaoRedisMQAutoConfiguration {
-
- @Bean
- public RedisMQTemplate redisMQTemplate(StringRedisTemplate redisTemplate,
- List interceptors) {
- RedisMQTemplate redisMQTemplate = new RedisMQTemplate(redisTemplate);
- // 添加拦截器
- interceptors.forEach(redisMQTemplate::addInterceptor);
- return redisMQTemplate;
- }
-
- // ========== 消费者相关 ==========
+public class YudaoRedisMQConsumerAutoConfiguration {
/**
* 创建 Redis Pub/Sub 广播消费的容器
*/
- @Bean(initMethod = "start", destroyMethod = "stop")
+ @Bean
@ConditionalOnBean(AbstractRedisChannelMessageListener.class) // 只有 AbstractChannelMessageListener 存在的时候,才需要注册 Redis pubsub 监听
public RedisMessageListenerContainer redisMessageListenerContainer(
RedisMQTemplate redisMQTemplate, List> listeners) {
diff --git a/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQProducerAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQProducerAutoConfiguration.java
new file mode 100644
index 000000000..c1950c489
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQProducerAutoConfiguration.java
@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.framework.mq.redis.config;
+
+import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
+import cn.iocoder.yudao.framework.mq.redis.core.interceptor.RedisMessageInterceptor;
+import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+import java.util.List;
+
+/**
+ * Redis 消息队列 Producer 配置类
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+@AutoConfiguration(after = YudaoRedisAutoConfiguration.class)
+public class YudaoRedisMQProducerAutoConfiguration {
+
+ @Bean
+ public RedisMQTemplate redisMQTemplate(StringRedisTemplate redisTemplate,
+ List interceptors) {
+ RedisMQTemplate redisMQTemplate = new RedisMQTemplate(redisTemplate);
+ // 添加拦截器
+ interceptors.forEach(redisMQTemplate::addInterceptor);
+ return redisMQTemplate;
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-mq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yudao-framework/yudao-spring-boot-starter-mq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index 660865453..213850b94 100644
--- a/yudao-framework/yudao-spring-boot-starter-mq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/yudao-framework/yudao-spring-boot-starter-mq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -1,2 +1,3 @@
-cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQAutoConfiguration
+cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQProducerAutoConfiguration
+cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQConsumerAutoConfiguration
cn.iocoder.yudao.framework.mq.rabbitmq.config.YudaoRabbitMQAutoConfiguration
\ No newline at end of file
diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
index 0bc54d532..023611bbc 100644
--- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
+++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
@@ -12,6 +12,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.baomidou.mybatisplus.extension.toolkit.Db;
import com.github.yulichang.base.MPJBaseMapper;
+import com.github.yulichang.interfaces.MPJBaseJoin;
import org.apache.ibatis.annotations.Param;
import java.util.Collection;
@@ -27,7 +28,7 @@ public interface BaseMapperX extends MPJBaseMapper {
default PageResult selectPage(PageParam pageParam, @Param("ew") Wrapper queryWrapper) {
// 特殊:不分页,直接查询全部
- if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageNo())) {
+ if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) {
List list = selectList(queryWrapper);
return new PageResult<>(list, (long) list.size());
}
@@ -39,6 +40,13 @@ public interface BaseMapperX extends MPJBaseMapper {
return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
}
+ default PageResult selectJoinPage(PageParam pageParam, Class resultTypeClass, MPJBaseJoin joinQueryWrapper) {
+ IPage mpPage = MyBatisUtils.buildPage(pageParam);
+ selectJoinPage(mpPage, resultTypeClass, joinQueryWrapper);
+ // 转换返回
+ return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
+ }
+
default T selectOne(String field, Object value) {
return selectOne(new QueryWrapper().eq(field, value));
}
diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/SecurityProperties.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/SecurityProperties.java
index d95dd5ff5..3d19f32a6 100644
--- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/SecurityProperties.java
+++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/SecurityProperties.java
@@ -19,6 +19,13 @@ public class SecurityProperties {
*/
@NotEmpty(message = "Token Header 不能为空")
private String tokenHeader = "Authorization";
+ /**
+ * HTTP 请求时,访问令牌的请求参数
+ *
+ * 初始目的:解决 WebSocket 无法通过 header 传参,只能通过 token 参数拼接
+ */
+ @NotEmpty(message = "Token Parameter 不能为空")
+ private String tokenParameter = "token";
/**
* mock 模式的开关
diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java
index 78aa328ad..fd09097dd 100644
--- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java
+++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.framework.security.config;
+import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.security.core.filter.TokenAuthenticationFilter;
import cn.iocoder.yudao.framework.web.config.WebProperties;
import com.google.common.collect.HashMultimap;
@@ -17,6 +18,7 @@ import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
@@ -129,8 +131,6 @@ public class YudaoWebSecurityConfigurerAdapter {
.antMatchers(buildAppApi("/**")).permitAll()
// 1.5 验证码captcha 允许匿名访问
.antMatchers("/captcha/get", "/captcha/check").permitAll()
- // 1.6 webSocket 允许匿名访问
- .antMatchers("/websocket/message").permitAll()
// ②:每个项目的自定义规则
.and().authorizeRequests(registry -> // 下面,循环设置自定义规则
authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(registry)))
@@ -164,6 +164,15 @@ public class YudaoWebSecurityConfigurerAdapter {
continue;
}
Set urls = entry.getKey().getPatternsCondition().getPatterns();
+ // 特殊:使用 @RequestMapping 注解,并且未写 method 属性,此时认为都需要免登录
+ Set methods = entry.getKey().getMethodsCondition().getMethods();
+ if (CollUtil.isEmpty(methods)) { //
+ result.putAll(HttpMethod.GET, urls);
+ result.putAll(HttpMethod.POST, urls);
+ result.putAll(HttpMethod.PUT, urls);
+ result.putAll(HttpMethod.DELETE, urls);
+ continue;
+ }
// 根据请求方法,添加到 result 结果
entry.getKey().getMethodsCondition().getMethods().forEach(requestMethod -> {
switch (requestMethod) {
diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/TokenAuthenticationFilter.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/TokenAuthenticationFilter.java
index e87f5bc44..544082d05 100644
--- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/TokenAuthenticationFilter.java
+++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/TokenAuthenticationFilter.java
@@ -41,7 +41,8 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
@SuppressWarnings("NullableProblems")
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
- String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader());
+ String token = SecurityFrameworkUtils.obtainAuthorization(request,
+ securityProperties.getTokenHeader(), securityProperties.getTokenParameter());
if (StrUtil.isNotEmpty(token)) {
Integer userType = WebFrameworkUtils.getLoginUserType(request);
try {
@@ -74,7 +75,10 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
return null;
}
// 用户类型不匹配,无权限
- if (ObjectUtil.notEqual(accessToken.getUserType(), userType)) {
+ // 注意:只有 /admin-api/* 和 /app-api/* 有 userType,才需要比对用户类型
+ // 类似 WebSocket 的 /ws/* 连接地址,是不需要比对用户类型的
+ if (userType != null
+ && ObjectUtil.notEqual(accessToken.getUserType(), userType)) {
throw new AccessDeniedException("错误的用户类型");
}
// 构建登录用户
diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java
index 5dc17b626..3caa7f98b 100644
--- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java
+++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.framework.security.core.util;
+import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import org.springframework.lang.Nullable;
@@ -20,6 +21,9 @@ import java.util.Collections;
*/
public class SecurityFrameworkUtils {
+ /**
+ * HEADER 认证头 value 的前缀
+ */
public static final String AUTHORIZATION_BEARER = "Bearer";
private SecurityFrameworkUtils() {}
@@ -28,19 +32,23 @@ public class SecurityFrameworkUtils {
* 从请求中,获得认证 Token
*
* @param request 请求
- * @param header 认证 Token 对应的 Header 名字
+ * @param headerName 认证 Token 对应的 Header 名字
+ * @param parameterName 认证 Token 对应的 Parameter 名字
* @return 认证 Token
*/
- public static String obtainAuthorization(HttpServletRequest request, String header) {
- String authorization = request.getHeader(header);
- if (!StringUtils.hasText(authorization)) {
+ public static String obtainAuthorization(HttpServletRequest request,
+ String headerName, String parameterName) {
+ // 1. 获得 Token。优先级:Header > Parameter
+ String token = request.getHeader(headerName);
+ if (StrUtil.isEmpty(token)) {
+ token = request.getParameter(parameterName);
+ }
+ if (!StringUtils.hasText(token)) {
return null;
}
- int index = authorization.indexOf(AUTHORIZATION_BEARER + " ");
- if (index == -1) { // 未找到
- return null;
- }
- return authorization.substring(index + 7).trim();
+ // 2. 去除 Token 中带的 Bearer
+ int index = token.indexOf(AUTHORIZATION_BEARER + " ");
+ return index >= 0 ? token.substring(index + 7).trim() : token;
}
/**
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/pom.xml b/yudao-framework/yudao-spring-boot-starter-websocket/pom.xml
index 320e52c48..b18ee4783 100644
--- a/yudao-framework/yudao-spring-boot-starter-websocket/pom.xml
+++ b/yudao-framework/yudao-spring-boot-starter-websocket/pom.xml
@@ -12,26 +12,73 @@
jar
${project.artifactId}
- WebSocket
+ WebSocket 框架,支持多节点的广播
https://github.com/YunaiV/ruoyi-vue-pro
-
cn.iocoder.boot
yudao-common
+
+
cn.iocoder.boot
yudao-spring-boot-starter-security
+ provided
org.springframework.boot
spring-boot-starter-websocket
+
+
+
+
+ cn.iocoder.boot
+ yudao-spring-boot-starter-security
+ provided
+
+
+
+
+ cn.iocoder.boot
+ yudao-spring-boot-starter-mq
+
+
+ org.springframework.kafka
+ spring-kafka
+ true
+
+
+ org.springframework.amqp
+ spring-rabbit
+ true
+
+
+ org.apache.rocketmq
+ rocketmq-spring-boot-starter
+ true
+
+
+
+
+
+ cn.iocoder.boot
+ yudao-spring-boot-starter-biz-tenant
+ provided
+
\ No newline at end of file
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketHandlerConfig.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketHandlerConfig.java
deleted file mode 100644
index 02c3415d5..000000000
--- a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketHandlerConfig.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package cn.iocoder.yudao.framework.websocket.config;
-
-import cn.iocoder.yudao.framework.websocket.core.UserHandshakeInterceptor;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.context.annotation.Bean;
-import org.springframework.web.socket.server.HandshakeInterceptor;
-
-@EnableConfigurationProperties(WebSocketProperties.class)
-public class WebSocketHandlerConfig {
- @Bean
- public HandshakeInterceptor handshakeInterceptor() {
- return new UserHandshakeInterceptor();
- }
-}
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketProperties.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketProperties.java
index 0ab1b498f..aa618fb04 100644
--- a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketProperties.java
+++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketProperties.java
@@ -4,6 +4,9 @@ import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+
/**
* WebSocket 配置项
*
@@ -15,15 +18,17 @@ import org.springframework.validation.annotation.Validated;
public class WebSocketProperties {
/**
- * 路径
+ * WebSocket 的连接路径
*/
- private String path = "";
+ @NotEmpty(message = "WebSocket 的连接路径不能为空")
+ private String path = "/ws";
+
/**
- * 默认最多允许同时在线用户数
+ * 消息发送器的类型
+ *
+ * 可选值:local、redis、rocketmq、kafka、rabbitmq
*/
- private int maxOnlineCount = 0;
- /**
- * 是否保存session
- */
- private boolean sessionMap = true;
+ @NotNull(message = "WebSocket 的消息发送者不能为空")
+ private String senderType = "local";
+
}
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/YudaoWebSocketAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/YudaoWebSocketAutoConfiguration.java
index f8c50ae6a..0f08b7cf5 100644
--- a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/YudaoWebSocketAutoConfiguration.java
+++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/YudaoWebSocketAutoConfiguration.java
@@ -1,11 +1,34 @@
package cn.iocoder.yudao.framework.websocket.config;
+import cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQConsumerAutoConfiguration;
+import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
+import cn.iocoder.yudao.framework.websocket.core.handler.JsonWebSocketMessageHandler;
+import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener;
+import cn.iocoder.yudao.framework.websocket.core.security.LoginUserHandshakeInterceptor;
+import cn.iocoder.yudao.framework.websocket.core.sender.kafka.KafkaWebSocketMessageConsumer;
+import cn.iocoder.yudao.framework.websocket.core.sender.kafka.KafkaWebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.sender.local.LocalWebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.sender.rabbitmq.RabbitMQWebSocketMessageConsumer;
+import cn.iocoder.yudao.framework.websocket.core.sender.rabbitmq.RabbitMQWebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.sender.redis.RedisWebSocketMessageConsumer;
+import cn.iocoder.yudao.framework.websocket.core.sender.redis.RedisWebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.sender.rocketmq.RocketMQWebSocketMessageConsumer;
+import cn.iocoder.yudao.framework.websocket.core.sender.rocketmq.RocketMQWebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionHandlerDecorator;
+import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager;
+import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManagerImpl;
+import org.apache.rocketmq.spring.core.RocketMQTemplate;
+import org.springframework.amqp.core.TopicExchange;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfiguration;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.server.HandshakeInterceptor;
@@ -16,19 +39,139 @@ import java.util.List;
*
* @author xingyu4j
*/
-@AutoConfiguration
-// 允许使用 yudao.websocket.enable=false 禁用websocket
-@ConditionalOnProperty(prefix = "yudao.websocket", value = "enable", matchIfMissing = true)
+@AutoConfiguration(before = YudaoRedisMQConsumerAutoConfiguration.class) // before YudaoRedisMQConsumerAutoConfiguration 的原因是,需要保证 RedisWebSocketMessageConsumer 先创建,才能创建 RedisMessageListenerContainer
+@EnableWebSocket // 开启 websocket
+@ConditionalOnProperty(prefix = "yudao.websocket", value = "enable", matchIfMissing = true) // 允许使用 yudao.websocket.enable=false 禁用 websocket
@EnableConfigurationProperties(WebSocketProperties.class)
public class YudaoWebSocketAutoConfiguration {
+
@Bean
- @ConditionalOnMissingBean
- public WebSocketConfigurer webSocketConfigurer(List handshakeInterceptor,
+ public WebSocketConfigurer webSocketConfigurer(HandshakeInterceptor[] handshakeInterceptors,
WebSocketHandler webSocketHandler,
WebSocketProperties webSocketProperties) {
-
return registry -> registry
+ // 添加 WebSocketHandler
.addHandler(webSocketHandler, webSocketProperties.getPath())
- .addInterceptors(handshakeInterceptor.toArray(new HandshakeInterceptor[0]));
+ .addInterceptors(handshakeInterceptors)
+ // 允许跨域,否则前端连接会直接断开
+ .setAllowedOriginPatterns("*");
}
-}
+
+ @Bean
+ public HandshakeInterceptor handshakeInterceptor() {
+ return new LoginUserHandshakeInterceptor();
+ }
+
+ @Bean
+ public WebSocketHandler webSocketHandler(WebSocketSessionManager sessionManager,
+ List extends WebSocketMessageListener>> messageListeners) {
+ // 1. 创建 JsonWebSocketMessageHandler 对象,处理消息
+ JsonWebSocketMessageHandler messageHandler = new JsonWebSocketMessageHandler(messageListeners);
+ // 2. 创建 WebSocketSessionHandlerDecorator 对象,处理连接
+ return new WebSocketSessionHandlerDecorator(messageHandler, sessionManager);
+ }
+
+ @Bean
+ public WebSocketSessionManager webSocketSessionManager() {
+ return new WebSocketSessionManagerImpl();
+ }
+
+ // ==================== Sender 相关 ====================
+
+ @Configuration
+ @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "local", matchIfMissing = true)
+ public class LocalWebSocketMessageSenderConfiguration {
+
+ @Bean
+ public LocalWebSocketMessageSender localWebSocketMessageSender(WebSocketSessionManager sessionManager) {
+ return new LocalWebSocketMessageSender(sessionManager);
+ }
+
+ }
+
+ @Configuration
+ @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "redis", matchIfMissing = true)
+ public class RedisWebSocketMessageSenderConfiguration {
+
+ @Bean
+ public RedisWebSocketMessageSender redisWebSocketMessageSender(WebSocketSessionManager sessionManager,
+ RedisMQTemplate redisMQTemplate) {
+ return new RedisWebSocketMessageSender(sessionManager, redisMQTemplate);
+ }
+
+ @Bean
+ public RedisWebSocketMessageConsumer redisWebSocketMessageConsumer(
+ RedisWebSocketMessageSender redisWebSocketMessageSender) {
+ return new RedisWebSocketMessageConsumer(redisWebSocketMessageSender);
+ }
+
+ }
+
+ @Configuration
+ @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "rocketmq", matchIfMissing = true)
+ public class RocketMQWebSocketMessageSenderConfiguration {
+
+ @Bean
+ public RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender(
+ WebSocketSessionManager sessionManager, RocketMQTemplate rocketMQTemplate,
+ @Value("${yudao.websocket.sender-rocketmq.topic}") String topic) {
+ return new RocketMQWebSocketMessageSender(sessionManager, rocketMQTemplate, topic);
+ }
+
+ @Bean
+ public RocketMQWebSocketMessageConsumer rocketMQWebSocketMessageConsumer(
+ RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender) {
+ return new RocketMQWebSocketMessageConsumer(rocketMQWebSocketMessageSender);
+ }
+
+ }
+
+ @Configuration
+ @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "rabbitmq", matchIfMissing = true)
+ public class RabbitMQWebSocketMessageSenderConfiguration {
+
+ @Bean
+ public RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender(
+ WebSocketSessionManager sessionManager, RabbitTemplate rabbitTemplate,
+ TopicExchange websocketTopicExchange) {
+ return new RabbitMQWebSocketMessageSender(sessionManager, rabbitTemplate, websocketTopicExchange);
+ }
+
+ @Bean
+ public RabbitMQWebSocketMessageConsumer rabbitMQWebSocketMessageConsumer(
+ RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender) {
+ return new RabbitMQWebSocketMessageConsumer(rabbitMQWebSocketMessageSender);
+ }
+
+ /**
+ * 创建 Topic Exchange
+ */
+ @Bean
+ public TopicExchange websocketTopicExchange(@Value("${yudao.websocket.sender-rabbitmq.exchange}") String exchange) {
+ return new TopicExchange(exchange,
+ true, // durable: 是否持久化
+ false); // exclusive: 是否排它
+ }
+
+ }
+
+ @Configuration
+ @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "kafka", matchIfMissing = true)
+ public class KafkaWebSocketMessageSenderConfiguration {
+
+ @Bean
+ public KafkaWebSocketMessageSender kafkaWebSocketMessageSender(
+ WebSocketSessionManager sessionManager, KafkaTemplate kafkaTemplate,
+ @Value("${yudao.websocket.sender-kafka.topic}") String topic) {
+ return new KafkaWebSocketMessageSender(sessionManager, kafkaTemplate, topic);
+ }
+
+ @Bean
+ public KafkaWebSocketMessageConsumer kafkaWebSocketMessageConsumer(
+ KafkaWebSocketMessageSender kafkaWebSocketMessageSender) {
+ return new KafkaWebSocketMessageConsumer(kafkaWebSocketMessageSender);
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/UserHandshakeInterceptor.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/UserHandshakeInterceptor.java
deleted file mode 100644
index 3f2fa4ec3..000000000
--- a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/UserHandshakeInterceptor.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package cn.iocoder.yudao.framework.websocket.core;
-
-import cn.iocoder.yudao.framework.security.core.LoginUser;
-import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
-import org.springframework.http.server.ServerHttpRequest;
-import org.springframework.http.server.ServerHttpResponse;
-import org.springframework.web.socket.WebSocketHandler;
-import org.springframework.web.socket.server.HandshakeInterceptor;
-
-import java.util.Map;
-
-public class UserHandshakeInterceptor implements HandshakeInterceptor {
- @Override
- public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) throws Exception {
- LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
- attributes.put(WebSocketKeyDefine.LOGIN_USER, loginUser);
- return true;
- }
-
- @Override
- public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
-
- }
-}
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketKeyDefine.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketKeyDefine.java
deleted file mode 100644
index f75ebc41c..000000000
--- a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketKeyDefine.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package cn.iocoder.yudao.framework.websocket.core;
-
-
-import lombok.Data;
-
-@Data
-public class WebSocketKeyDefine {
- public static final String LOGIN_USER ="LOGIN_USER";
-}
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketMessageDO.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketMessageDO.java
deleted file mode 100644
index 7bb348e99..000000000
--- a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketMessageDO.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package cn.iocoder.yudao.framework.websocket.core;
-
-import lombok.Data;
-import lombok.experimental.Accessors;
-
-import java.util.List;
-
-@Data
-@Accessors(chain = true)
-public class WebSocketMessageDO {
- /**
- * 接收消息的seesion
- */
- private List seesionKeyList;
- /**
- * 发送消息
- */
- private String msgText;
-
- public static WebSocketMessageDO build(List seesionKeyList, String msgText) {
- return new WebSocketMessageDO().setMsgText(msgText).setSeesionKeyList(seesionKeyList);
- }
-
-}
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketSessionHandler.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketSessionHandler.java
deleted file mode 100644
index 2747f8192..000000000
--- a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketSessionHandler.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package cn.iocoder.yudao.framework.websocket.core;
-
-import org.springframework.web.socket.WebSocketSession;
-
-import java.util.Collection;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-
-public final class WebSocketSessionHandler {
- private WebSocketSessionHandler() {
- }
-
- private static final Map SESSION_MAP = new ConcurrentHashMap<>();
-
- public static void addSession(Object sessionKey, WebSocketSession session) {
- SESSION_MAP.put(sessionKey.toString(), session);
- }
-
- public static void removeSession(Object sessionKey) {
- SESSION_MAP.remove(sessionKey.toString());
- }
-
- public static WebSocketSession getSession(Object sessionKey) {
- return SESSION_MAP.get(sessionKey.toString());
- }
-
- public static Collection getSessions() {
- return SESSION_MAP.values();
- }
-
- public static Set getSessionKeys() {
- return SESSION_MAP.keySet();
- }
-
-}
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketUtils.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketUtils.java
deleted file mode 100644
index 816e664cc..000000000
--- a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketUtils.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package cn.iocoder.yudao.framework.websocket.core;
-
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.web.socket.TextMessage;
-import org.springframework.web.socket.WebSocketSession;
-
-import java.io.IOException;
-
-@Slf4j
-public class WebSocketUtils {
- public static boolean sendMessage(WebSocketSession seesion, String message) {
- if (seesion == null) {
- log.error("seesion 不存在");
- return false;
- }
- if (seesion.isOpen()) {
- try {
- seesion.sendMessage(new TextMessage(message));
- } catch (IOException e) {
- log.error("WebSocket 消息发送异常 Session={} | msg= {} | exception={}", seesion, message, e);
- return false;
- }
- }
- return true;
- }
-
- public static boolean sendMessage(Object sessionKey, String message) {
- WebSocketSession session = WebSocketSessionHandler.getSession(sessionKey);
- return sendMessage(session, message);
- }
-}
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/YudaoWebSocketHandlerDecorator.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/YudaoWebSocketHandlerDecorator.java
deleted file mode 100644
index dd8dc602e..000000000
--- a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/YudaoWebSocketHandlerDecorator.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package cn.iocoder.yudao.framework.websocket.core;
-
-import cn.iocoder.yudao.framework.security.core.LoginUser;
-import org.springframework.web.socket.CloseStatus;
-import org.springframework.web.socket.WebSocketHandler;
-import org.springframework.web.socket.WebSocketSession;
-import org.springframework.web.socket.handler.WebSocketHandlerDecorator;
-
-public class YudaoWebSocketHandlerDecorator extends WebSocketHandlerDecorator {
- public YudaoWebSocketHandlerDecorator(WebSocketHandler delegate) {
- super(delegate);
- }
-
- /**
- * websocket 连接时执行的动作
- * @param session websocket session 对象
- * @throws Exception 异常对象
- */
- @Override
- public void afterConnectionEstablished(final WebSocketSession session) throws Exception {
- Object sessionKey = sessionKeyGen(session);
- WebSocketSessionHandler.addSession(sessionKey, session);
- }
-
- /**
- * websocket 关闭连接时执行的动作
- * @param session websocket session 对象
- * @param closeStatus 关闭状态对象
- * @throws Exception 异常对象
- */
- @Override
- public void afterConnectionClosed(final WebSocketSession session, CloseStatus closeStatus) throws Exception {
- Object sessionKey = sessionKeyGen(session);
- WebSocketSessionHandler.removeSession(sessionKey);
- }
-
- public Object sessionKeyGen(WebSocketSession webSocketSession) {
-
- Object obj = webSocketSession.getAttributes().get(WebSocketKeyDefine.LOGIN_USER);
-
- if (obj instanceof LoginUser) {
- LoginUser loginUser = (LoginUser) obj;
- // userId 作为唯一区分
- return String.valueOf(loginUser.getId());
- }
-
- return null;
- }
-}
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/handler/JsonWebSocketMessageHandler.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/handler/JsonWebSocketMessageHandler.java
new file mode 100644
index 000000000..120f529c2
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/handler/JsonWebSocketMessageHandler.java
@@ -0,0 +1,83 @@
+package cn.iocoder.yudao.framework.websocket.core.handler;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.core.util.TypeUtil;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
+import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener;
+import cn.iocoder.yudao.framework.websocket.core.message.JsonWebSocketMessage;
+import cn.iocoder.yudao.framework.websocket.core.util.WebSocketFrameworkUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.WebSocketSession;
+import org.springframework.web.socket.handler.TextWebSocketHandler;
+
+import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/**
+ * JSON 格式 {@link WebSocketHandler} 实现类
+ *
+ * 基于 {@link JsonWebSocketMessage#getType()} 消息类型,调度到对应的 {@link WebSocketMessageListener} 监听器。
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class JsonWebSocketMessageHandler extends TextWebSocketHandler {
+
+ /**
+ * type 与 WebSocketMessageListener 的映射
+ */
+ private final Map> listeners = new HashMap<>();
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ public JsonWebSocketMessageHandler(List extends WebSocketMessageListener> listenersList) {
+ listenersList.forEach((Consumer)
+ listener -> listeners.put(listener.getType(), listener));
+ }
+
+ @Override
+ protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
+ // 1.1 空消息,跳过
+ if (message.getPayloadLength() == 0) {
+ return;
+ }
+ // 1.2 ping 心跳消息,直接返回 pong 消息。
+ if (message.getPayloadLength() == 4 && Objects.equals(message.getPayload(), "ping")) {
+ session.sendMessage(new TextMessage("pong"));
+ return;
+ }
+
+ // 2.1 解析消息
+ try {
+ JsonWebSocketMessage jsonMessage = JsonUtils.parseObject(message.getPayload(), JsonWebSocketMessage.class);
+ if (jsonMessage == null) {
+ log.error("[handleTextMessage][session({}) message({}) 解析为空]", session.getId(), message.getPayload());
+ return;
+ }
+ if (StrUtil.isEmpty(jsonMessage.getType())) {
+ log.error("[handleTextMessage][session({}) message({}) 类型为空]", session.getId(), message.getPayload());
+ return;
+ }
+ // 2.2 获得对应的 WebSocketMessageListener
+ WebSocketMessageListener messageListener = listeners.get(jsonMessage.getType());
+ if (messageListener == null) {
+ log.error("[handleTextMessage][session({}) message({}) 监听器为空]", session.getId(), message.getPayload());
+ return;
+ }
+ // 2.3 处理消息
+ Type type = TypeUtil.getTypeArgument(messageListener.getClass(), 0);
+ Object messageObj = JsonUtils.parseObject(jsonMessage.getContent(), type);
+ Long tenantId = WebSocketFrameworkUtils.getTenantId(session);
+ TenantUtils.execute(tenantId, () -> messageListener.onMessage(session, messageObj));
+ } catch (Throwable ex) {
+ log.error("[handleTextMessage][session({}) message({}) 处理异常]", session.getId(), message.getPayload());
+ }
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/listener/WebSocketMessageListener.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/listener/WebSocketMessageListener.java
new file mode 100644
index 000000000..f3a62cc39
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/listener/WebSocketMessageListener.java
@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.framework.websocket.core.listener;
+
+import cn.iocoder.yudao.framework.websocket.core.message.JsonWebSocketMessage;
+import org.springframework.web.socket.WebSocketSession;
+
+/**
+ * WebSocket 消息监听器接口
+ *
+ * 目的:前端发送消息给后端后,处理对应 {@link #getType()} 类型的消息
+ *
+ * @param 泛型,消息类型
+ */
+public interface WebSocketMessageListener {
+
+ /**
+ * 处理消息
+ *
+ * @param session Session
+ * @param message 消息
+ */
+ void onMessage(WebSocketSession session, T message);
+
+ /**
+ * 获得消息类型
+ *
+ * @see JsonWebSocketMessage#getType()
+ * @return 消息类型
+ */
+ String getType();
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/message/JsonWebSocketMessage.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/message/JsonWebSocketMessage.java
new file mode 100644
index 000000000..0a55cd691
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/message/JsonWebSocketMessage.java
@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.framework.websocket.core.message;
+
+import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * JSON 格式的 WebSocket 消息帧
+ *
+ * @author 芋道源码
+ */
+@Data
+public class JsonWebSocketMessage implements Serializable {
+
+ /**
+ * 消息类型
+ *
+ * 目的:用于分发到对应的 {@link WebSocketMessageListener} 实现类
+ */
+ private String type;
+ /**
+ * 消息内容
+ *
+ * 要求 JSON 对象
+ */
+ private String content;
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/security/LoginUserHandshakeInterceptor.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/security/LoginUserHandshakeInterceptor.java
new file mode 100644
index 000000000..3a31825f5
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/security/LoginUserHandshakeInterceptor.java
@@ -0,0 +1,42 @@
+package cn.iocoder.yudao.framework.websocket.core.security;
+
+import cn.iocoder.yudao.framework.security.core.LoginUser;
+import cn.iocoder.yudao.framework.security.core.filter.TokenAuthenticationFilter;
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
+import cn.iocoder.yudao.framework.websocket.core.util.WebSocketFrameworkUtils;
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.http.server.ServerHttpResponse;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.WebSocketSession;
+import org.springframework.web.socket.server.HandshakeInterceptor;
+
+import java.util.Map;
+
+/**
+ * 登录用户的 {@link HandshakeInterceptor} 实现类
+ *
+ * 流程如下:
+ * 1. 前端连接 websocket 时,会通过拼接 ?token={token} 到 ws:// 连接后,这样它可以被 {@link TokenAuthenticationFilter} 所认证通过
+ * 2. {@link LoginUserHandshakeInterceptor} 负责把 {@link LoginUser} 添加到 {@link WebSocketSession} 中
+ *
+ * @author 芋道源码
+ */
+public class LoginUserHandshakeInterceptor implements HandshakeInterceptor {
+
+ @Override
+ public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
+ WebSocketHandler wsHandler, Map attributes) {
+ LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
+ if (loginUser != null) {
+ WebSocketFrameworkUtils.setLoginUser(loginUser, attributes);
+ }
+ return true;
+ }
+
+ @Override
+ public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
+ WebSocketHandler wsHandler, Exception exception) {
+ // do nothing
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/security/WebSocketAuthorizeRequestsCustomizer.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/security/WebSocketAuthorizeRequestsCustomizer.java
new file mode 100644
index 000000000..5614f05ce
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/security/WebSocketAuthorizeRequestsCustomizer.java
@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.framework.websocket.core.security;
+
+import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer;
+import cn.iocoder.yudao.framework.websocket.config.WebSocketProperties;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
+
+/**
+ * WebSocket 的权限自定义
+ *
+ * @author 芋道源码
+ */
+@RequiredArgsConstructor
+public class WebSocketAuthorizeRequestsCustomizer extends AuthorizeRequestsCustomizer {
+
+ private final WebSocketProperties webSocketProperties;
+
+ @Override
+ public void customize(ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry) {
+ registry.antMatchers(webSocketProperties.getPath()).permitAll();
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/AbstractWebSocketMessageSender.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/AbstractWebSocketMessageSender.java
new file mode 100644
index 000000000..4e0db44c9
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/AbstractWebSocketMessageSender.java
@@ -0,0 +1,104 @@
+package cn.iocoder.yudao.framework.websocket.core.sender;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.framework.websocket.core.message.JsonWebSocketMessage;
+import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * WebSocketMessageSender 实现类
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+@RequiredArgsConstructor
+public abstract class AbstractWebSocketMessageSender implements WebSocketMessageSender {
+
+ private final WebSocketSessionManager sessionManager;
+
+ @Override
+ public void send(Integer userType, Long userId, String messageType, String messageContent) {
+ send(null, userType, userId, messageType, messageContent);
+ }
+
+ @Override
+ public void send(Integer userType, String messageType, String messageContent) {
+ send(null, userType, null, messageType, messageContent);
+ }
+
+ @Override
+ public void send(String sessionId, String messageType, String messageContent) {
+ send(sessionId, null, null, messageType, messageContent);
+ }
+
+ /**
+ * 发送消息
+ *
+ * @param sessionId Session 编号
+ * @param userType 用户类型
+ * @param userId 用户编号
+ * @param messageType 消息类型
+ * @param messageContent 消息内容
+ */
+ public void send(String sessionId, Integer userType, Long userId, String messageType, String messageContent) {
+ // 1. 获得 Session 列表
+ List sessions = Collections.emptyList();
+ if (StrUtil.isNotEmpty(sessionId)) {
+ WebSocketSession session = sessionManager.getSession(sessionId);
+ if (session != null) {
+ sessions = Collections.singletonList(session);
+ }
+ } else if (userType != null && userId != null) {
+ sessions = (List) sessionManager.getSessionList(userType, userId);
+ } else if (userType != null) {
+ sessions = (List) sessionManager.getSessionList(userType);
+ }
+ if (CollUtil.isEmpty(sessions)) {
+ log.info("[send][sessionId({}) userType({}) userId({}) messageType({}) messageContent({}) 未匹配到会话]",
+ sessionId, userType, userId, messageType, messageContent);
+ }
+ // 2. 执行发送
+ doSend(sessions, messageType, messageContent);
+ }
+
+ /**
+ * 发送消息的具体实现
+ *
+ * @param sessions Session 列表
+ * @param messageType 消息类型
+ * @param messageContent 消息内容
+ */
+ public void doSend(Collection sessions, String messageType, String messageContent) {
+ JsonWebSocketMessage message = new JsonWebSocketMessage().setType(messageType).setContent(messageContent);
+ String payload = JsonUtils.toJsonString(message); // 关键,使用 JSON 序列化
+ sessions.forEach(session -> {
+ // 1. 各种校验,保证 Session 可以被发送
+ if (session == null) {
+ log.error("[doSend][session 为空, message({})]", message);
+ return;
+ }
+ if (!session.isOpen()) {
+ log.error("[doSend][session({}) 已关闭, message({})]", session.getId(), message);
+ return;
+ }
+ // 2. 执行发送
+ try {
+ session.sendMessage(new TextMessage(payload));
+ log.info("[doSend][session({}) 发送消息成功,message({})]", session.getId(), message);
+ } catch (IOException ex) {
+ log.error("[doSend][session({}) 发送消息失败,message({})]", session.getId(), message, ex);
+ }
+ });
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/WebSocketMessageSender.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/WebSocketMessageSender.java
new file mode 100644
index 000000000..9f75ad52d
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/WebSocketMessageSender.java
@@ -0,0 +1,52 @@
+package cn.iocoder.yudao.framework.websocket.core.sender;
+
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+
+/**
+ * WebSocket 消息的发送器接口
+ *
+ * @author 芋道源码
+ */
+public interface WebSocketMessageSender {
+
+ /**
+ * 发送消息给指定用户
+ *
+ * @param userType 用户类型
+ * @param userId 用户编号
+ * @param messageType 消息类型
+ * @param messageContent 消息内容,JSON 格式
+ */
+ void send(Integer userType, Long userId, String messageType, String messageContent);
+
+ /**
+ * 发送消息给指定用户类型
+ *
+ * @param userType 用户类型
+ * @param messageType 消息类型
+ * @param messageContent 消息内容,JSON 格式
+ */
+ void send(Integer userType, String messageType, String messageContent);
+
+ /**
+ * 发送消息给指定 Session
+ *
+ * @param sessionId Session 编号
+ * @param messageType 消息类型
+ * @param messageContent 消息内容,JSON 格式
+ */
+ void send(String sessionId, String messageType, String messageContent);
+
+ default void sendObject(Integer userType, Long userId, String messageType, Object messageContent) {
+ send(userType, userId, messageType, JsonUtils.toJsonString(messageContent));
+ }
+
+ default void sendObject(Integer userType, String messageType, Object messageContent) {
+ send(userType, messageType, JsonUtils.toJsonString(messageContent));
+ }
+
+ default void sendObject(String sessionId, String messageType, Object messageContent) {
+ send(sessionId, messageType, JsonUtils.toJsonString(messageContent));
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/kafka/KafkaWebSocketMessage.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/kafka/KafkaWebSocketMessage.java
new file mode 100644
index 000000000..5a4cf5311
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/kafka/KafkaWebSocketMessage.java
@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.framework.websocket.core.sender.kafka;
+
+import lombok.Data;
+
+/**
+ * Kafka 广播 WebSocket 的消息
+ *
+ * @author 芋道源码
+ */
+@Data
+public class KafkaWebSocketMessage {
+
+ /**
+ * Session 编号
+ */
+ private String sessionId;
+ /**
+ * 用户类型
+ */
+ private Integer userType;
+ /**
+ * 用户编号
+ */
+ private Long userId;
+
+ /**
+ * 消息类型
+ */
+ private String messageType;
+ /**
+ * 消息内容
+ */
+ private String messageContent;
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/kafka/KafkaWebSocketMessageConsumer.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/kafka/KafkaWebSocketMessageConsumer.java
new file mode 100644
index 000000000..201e65d81
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/kafka/KafkaWebSocketMessageConsumer.java
@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.framework.websocket.core.sender.kafka;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.amqp.rabbit.annotation.RabbitHandler;
+import org.springframework.kafka.annotation.KafkaListener;
+
+/**
+ * {@link KafkaWebSocketMessage} 广播消息的消费者,真正把消息发送出去
+ *
+ * @author 芋道源码
+ */
+@RequiredArgsConstructor
+public class KafkaWebSocketMessageConsumer {
+
+ private final KafkaWebSocketMessageSender rabbitMQWebSocketMessageSender;
+
+ @RabbitHandler
+ @KafkaListener(
+ topics = "${yudao.websocket.sender-kafka.topic}",
+ // 在 Group 上,使用 UUID 生成其后缀。这样,启动的 Consumer 的 Group 不同,以达到广播消费的目的
+ groupId = "${yudao.websocket.sender-kafka.consumer-group}" + "-" + "#{T(java.util.UUID).randomUUID()}")
+ public void onMessage(KafkaWebSocketMessage message) {
+ rabbitMQWebSocketMessageSender.send(message.getSessionId(),
+ message.getUserType(), message.getUserId(),
+ message.getMessageType(), message.getMessageContent());
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/kafka/KafkaWebSocketMessageSender.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/kafka/KafkaWebSocketMessageSender.java
new file mode 100644
index 000000000..47bb598ad
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/kafka/KafkaWebSocketMessageSender.java
@@ -0,0 +1,67 @@
+package cn.iocoder.yudao.framework.websocket.core.sender.kafka;
+
+import cn.iocoder.yudao.framework.websocket.core.sender.AbstractWebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.kafka.core.KafkaTemplate;
+
+import java.util.concurrent.ExecutionException;
+
+/**
+ * 基于 Kafka 的 {@link WebSocketMessageSender} 实现类
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class KafkaWebSocketMessageSender extends AbstractWebSocketMessageSender {
+
+ private final KafkaTemplate kafkaTemplate;
+
+ private final String topic;
+
+ public KafkaWebSocketMessageSender(WebSocketSessionManager sessionManager,
+ KafkaTemplate kafkaTemplate,
+ String topic) {
+ super(sessionManager);
+ this.kafkaTemplate = kafkaTemplate;
+ this.topic = topic;
+ }
+
+ @Override
+ public void send(Integer userType, Long userId, String messageType, String messageContent) {
+ sendKafkaMessage(null, userId, userType, messageType, messageContent);
+ }
+
+ @Override
+ public void send(Integer userType, String messageType, String messageContent) {
+ sendKafkaMessage(null, null, userType, messageType, messageContent);
+ }
+
+ @Override
+ public void send(String sessionId, String messageType, String messageContent) {
+ sendKafkaMessage(sessionId, null, null, messageType, messageContent);
+ }
+
+ /**
+ * 通过 Kafka 广播消息
+ *
+ * @param sessionId Session 编号
+ * @param userId 用户编号
+ * @param userType 用户类型
+ * @param messageType 消息类型
+ * @param messageContent 消息内容
+ */
+ private void sendKafkaMessage(String sessionId, Long userId, Integer userType,
+ String messageType, String messageContent) {
+ KafkaWebSocketMessage mqMessage = new KafkaWebSocketMessage()
+ .setSessionId(sessionId).setUserId(userId).setUserType(userType)
+ .setMessageType(messageType).setMessageContent(messageContent);
+ try {
+ kafkaTemplate.send(topic, mqMessage).get();
+ } catch (InterruptedException | ExecutionException e) {
+ log.error("[sendKafkaMessage][发送消息({}) 到 Kafka 失败]", mqMessage, e);
+ }
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/local/LocalWebSocketMessageSender.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/local/LocalWebSocketMessageSender.java
new file mode 100644
index 000000000..66640ef34
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/local/LocalWebSocketMessageSender.java
@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.framework.websocket.core.sender.local;
+
+import cn.iocoder.yudao.framework.websocket.core.sender.AbstractWebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager;
+
+/**
+ * 本地的 {@link WebSocketMessageSender} 实现类
+ *
+ * 注意:仅仅适合单机场景!!!
+ *
+ * @author 芋道源码
+ */
+public class LocalWebSocketMessageSender extends AbstractWebSocketMessageSender {
+
+ public LocalWebSocketMessageSender(WebSocketSessionManager sessionManager) {
+ super(sessionManager);
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessage.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessage.java
new file mode 100644
index 000000000..80a4bc176
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessage.java
@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.framework.websocket.core.sender.rabbitmq;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * RabbitMQ 广播 WebSocket 的消息
+ *
+ * @author 芋道源码
+ */
+@Data
+public class RabbitMQWebSocketMessage implements Serializable {
+
+ /**
+ * Session 编号
+ */
+ private String sessionId;
+ /**
+ * 用户类型
+ */
+ private Integer userType;
+ /**
+ * 用户编号
+ */
+ private Long userId;
+
+ /**
+ * 消息类型
+ */
+ private String messageType;
+ /**
+ * 消息内容
+ */
+ private String messageContent;
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageConsumer.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageConsumer.java
new file mode 100644
index 000000000..59e382421
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageConsumer.java
@@ -0,0 +1,39 @@
+package cn.iocoder.yudao.framework.websocket.core.sender.rabbitmq;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.amqp.core.ExchangeTypes;
+import org.springframework.amqp.rabbit.annotation.*;
+
+/**
+ * {@link RabbitMQWebSocketMessage} 广播消息的消费者,真正把消息发送出去
+ *
+ * @author 芋道源码
+ */
+@RabbitListener(
+ bindings = @QueueBinding(
+ value = @Queue(
+ // 在 Queue 的名字上,使用 UUID 生成其后缀。这样,启动的 Consumer 的 Queue 不同,以达到广播消费的目的
+ name = "${yudao.websocket.sender-rabbitmq.queue}" + "-" + "#{T(java.util.UUID).randomUUID()}",
+ // Consumer 关闭时,该队列就可以被自动删除了
+ autoDelete = "true"
+ ),
+ exchange = @Exchange(
+ name = "${yudao.websocket.sender-rabbitmq.exchange}",
+ type = ExchangeTypes.TOPIC,
+ declare = "false"
+ )
+ )
+)
+@RequiredArgsConstructor
+public class RabbitMQWebSocketMessageConsumer {
+
+ private final RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender;
+
+ @RabbitHandler
+ public void onMessage(RabbitMQWebSocketMessage message) {
+ rabbitMQWebSocketMessageSender.send(message.getSessionId(),
+ message.getUserType(), message.getUserId(),
+ message.getMessageType(), message.getMessageContent());
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageSender.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageSender.java
new file mode 100644
index 000000000..065a5d6bf
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageSender.java
@@ -0,0 +1,62 @@
+package cn.iocoder.yudao.framework.websocket.core.sender.rabbitmq;
+
+import cn.iocoder.yudao.framework.websocket.core.sender.AbstractWebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.amqp.core.TopicExchange;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+
+/**
+ * 基于 RabbitMQ 的 {@link WebSocketMessageSender} 实现类
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class RabbitMQWebSocketMessageSender extends AbstractWebSocketMessageSender {
+
+ private final RabbitTemplate rabbitTemplate;
+
+ private final TopicExchange topicExchange;
+
+ public RabbitMQWebSocketMessageSender(WebSocketSessionManager sessionManager,
+ RabbitTemplate rabbitTemplate,
+ TopicExchange topicExchange) {
+ super(sessionManager);
+ this.rabbitTemplate = rabbitTemplate;
+ this.topicExchange = topicExchange;
+ }
+
+ @Override
+ public void send(Integer userType, Long userId, String messageType, String messageContent) {
+ sendRabbitMQMessage(null, userId, userType, messageType, messageContent);
+ }
+
+ @Override
+ public void send(Integer userType, String messageType, String messageContent) {
+ sendRabbitMQMessage(null, null, userType, messageType, messageContent);
+ }
+
+ @Override
+ public void send(String sessionId, String messageType, String messageContent) {
+ sendRabbitMQMessage(sessionId, null, null, messageType, messageContent);
+ }
+
+ /**
+ * 通过 RabbitMQ 广播消息
+ *
+ * @param sessionId Session 编号
+ * @param userId 用户编号
+ * @param userType 用户类型
+ * @param messageType 消息类型
+ * @param messageContent 消息内容
+ */
+ private void sendRabbitMQMessage(String sessionId, Long userId, Integer userType,
+ String messageType, String messageContent) {
+ RabbitMQWebSocketMessage mqMessage = new RabbitMQWebSocketMessage()
+ .setSessionId(sessionId).setUserId(userId).setUserType(userType)
+ .setMessageType(messageType).setMessageContent(messageContent);
+ rabbitTemplate.convertAndSend(topicExchange.getName(), null, mqMessage);
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/redis/RedisWebSocketMessage.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/redis/RedisWebSocketMessage.java
new file mode 100644
index 000000000..fb9ea0ca0
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/redis/RedisWebSocketMessage.java
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.framework.websocket.core.sender.redis;
+
+import cn.iocoder.yudao.framework.mq.redis.core.pubsub.AbstractRedisChannelMessage;
+import lombok.Data;
+
+/**
+ * Redis 广播 WebSocket 的消息
+ */
+@Data
+public class RedisWebSocketMessage extends AbstractRedisChannelMessage {
+
+ /**
+ * Session 编号
+ */
+ private String sessionId;
+ /**
+ * 用户类型
+ */
+ private Integer userType;
+ /**
+ * 用户编号
+ */
+ private Long userId;
+
+ /**
+ * 消息类型
+ */
+ private String messageType;
+ /**
+ * 消息内容
+ */
+ private String messageContent;
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/redis/RedisWebSocketMessageConsumer.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/redis/RedisWebSocketMessageConsumer.java
new file mode 100644
index 000000000..abce00695
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/redis/RedisWebSocketMessageConsumer.java
@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.framework.websocket.core.sender.redis;
+
+import cn.iocoder.yudao.framework.mq.redis.core.pubsub.AbstractRedisChannelMessageListener;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * {@link RedisWebSocketMessage} 广播消息的消费者,真正把消息发送出去
+ *
+ * @author 芋道源码
+ */
+@RequiredArgsConstructor
+public class RedisWebSocketMessageConsumer extends AbstractRedisChannelMessageListener {
+
+ private final RedisWebSocketMessageSender redisWebSocketMessageSender;
+
+ @Override
+ public void onMessage(RedisWebSocketMessage message) {
+ redisWebSocketMessageSender.send(message.getSessionId(),
+ message.getUserType(), message.getUserId(),
+ message.getMessageType(), message.getMessageContent());
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/redis/RedisWebSocketMessageSender.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/redis/RedisWebSocketMessageSender.java
new file mode 100644
index 000000000..d6004ac6d
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/redis/RedisWebSocketMessageSender.java
@@ -0,0 +1,57 @@
+package cn.iocoder.yudao.framework.websocket.core.sender.redis;
+
+import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
+import cn.iocoder.yudao.framework.websocket.core.sender.AbstractWebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 基于 Redis 的 {@link WebSocketMessageSender} 实现类
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class RedisWebSocketMessageSender extends AbstractWebSocketMessageSender {
+
+ private final RedisMQTemplate redisMQTemplate;
+
+ public RedisWebSocketMessageSender(WebSocketSessionManager sessionManager,
+ RedisMQTemplate redisMQTemplate) {
+ super(sessionManager);
+ this.redisMQTemplate = redisMQTemplate;
+ }
+
+ @Override
+ public void send(Integer userType, Long userId, String messageType, String messageContent) {
+ sendRedisMessage(null, userId, userType, messageType, messageContent);
+ }
+
+ @Override
+ public void send(Integer userType, String messageType, String messageContent) {
+ sendRedisMessage(null, null, userType, messageType, messageContent);
+ }
+
+ @Override
+ public void send(String sessionId, String messageType, String messageContent) {
+ sendRedisMessage(sessionId, null, null, messageType, messageContent);
+ }
+
+ /**
+ * 通过 Redis 广播消息
+ *
+ * @param sessionId Session 编号
+ * @param userId 用户编号
+ * @param userType 用户类型
+ * @param messageType 消息类型
+ * @param messageContent 消息内容
+ */
+ private void sendRedisMessage(String sessionId, Long userId, Integer userType,
+ String messageType, String messageContent) {
+ RedisWebSocketMessage mqMessage = new RedisWebSocketMessage()
+ .setSessionId(sessionId).setUserId(userId).setUserType(userType)
+ .setMessageType(messageType).setMessageContent(messageContent);
+ redisMQTemplate.send(mqMessage);
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessage.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessage.java
new file mode 100644
index 000000000..91570e3e3
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessage.java
@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.framework.websocket.core.sender.rocketmq;
+
+import lombok.Data;
+
+/**
+ * RocketMQ 广播 WebSocket 的消息
+ *
+ * @author 芋道源码
+ */
+@Data
+public class RocketMQWebSocketMessage {
+
+ /**
+ * Session 编号
+ */
+ private String sessionId;
+ /**
+ * 用户类型
+ */
+ private Integer userType;
+ /**
+ * 用户编号
+ */
+ private Long userId;
+
+ /**
+ * 消息类型
+ */
+ private String messageType;
+ /**
+ * 消息内容
+ */
+ private String messageContent;
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageConsumer.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageConsumer.java
new file mode 100644
index 000000000..ab2e2c4dc
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageConsumer.java
@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.framework.websocket.core.sender.rocketmq;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.rocketmq.spring.annotation.MessageModel;
+import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
+import org.apache.rocketmq.spring.core.RocketMQListener;
+
+/**
+ * {@link RocketMQWebSocketMessage} 广播消息的消费者,真正把消息发送出去
+ *
+ * @author 芋道源码
+ */
+@RocketMQMessageListener( // 重点:添加 @RocketMQMessageListener 注解,声明消费的 topic
+ topic = "${yudao.websocket.sender-rocketmq.topic}",
+ consumerGroup = "${yudao.websocket.sender-rocketmq.consumer-group}",
+ messageModel = MessageModel.BROADCASTING // 设置为广播模式,保证每个实例都能收到消息
+)
+@RequiredArgsConstructor
+public class RocketMQWebSocketMessageConsumer implements RocketMQListener {
+
+ private final RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender;
+
+ @Override
+ public void onMessage(RocketMQWebSocketMessage message) {
+ rocketMQWebSocketMessageSender.send(message.getSessionId(),
+ message.getUserType(), message.getUserId(),
+ message.getMessageType(), message.getMessageContent());
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageSender.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageSender.java
new file mode 100644
index 000000000..ed059bac4
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageSender.java
@@ -0,0 +1,61 @@
+package cn.iocoder.yudao.framework.websocket.core.sender.rocketmq;
+
+import cn.iocoder.yudao.framework.websocket.core.sender.AbstractWebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.spring.core.RocketMQTemplate;
+
+/**
+ * 基于 RocketMQ 的 {@link WebSocketMessageSender} 实现类
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class RocketMQWebSocketMessageSender extends AbstractWebSocketMessageSender {
+
+ private final RocketMQTemplate rocketMQTemplate;
+
+ private final String topic;
+
+ public RocketMQWebSocketMessageSender(WebSocketSessionManager sessionManager,
+ RocketMQTemplate rocketMQTemplate,
+ String topic) {
+ super(sessionManager);
+ this.rocketMQTemplate = rocketMQTemplate;
+ this.topic = topic;
+ }
+
+ @Override
+ public void send(Integer userType, Long userId, String messageType, String messageContent) {
+ sendRocketMQMessage(null, userId, userType, messageType, messageContent);
+ }
+
+ @Override
+ public void send(Integer userType, String messageType, String messageContent) {
+ sendRocketMQMessage(null, null, userType, messageType, messageContent);
+ }
+
+ @Override
+ public void send(String sessionId, String messageType, String messageContent) {
+ sendRocketMQMessage(sessionId, null, null, messageType, messageContent);
+ }
+
+ /**
+ * 通过 RocketMQ 广播消息
+ *
+ * @param sessionId Session 编号
+ * @param userId 用户编号
+ * @param userType 用户类型
+ * @param messageType 消息类型
+ * @param messageContent 消息内容
+ */
+ private void sendRocketMQMessage(String sessionId, Long userId, Integer userType,
+ String messageType, String messageContent) {
+ RocketMQWebSocketMessage mqMessage = new RocketMQWebSocketMessage()
+ .setSessionId(sessionId).setUserId(userId).setUserType(userType)
+ .setMessageType(messageType).setMessageContent(messageContent);
+ rocketMQTemplate.syncSend(topic, mqMessage);
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/session/WebSocketSessionHandlerDecorator.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/session/WebSocketSessionHandlerDecorator.java
new file mode 100644
index 000000000..600a4dd96
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/session/WebSocketSessionHandlerDecorator.java
@@ -0,0 +1,49 @@
+package cn.iocoder.yudao.framework.websocket.core.session;
+
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.WebSocketSession;
+import org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator;
+import org.springframework.web.socket.handler.WebSocketHandlerDecorator;
+
+/**
+ * {@link WebSocketHandler} 的装饰类,实现了以下功能:
+ *
+ * 1. {@link WebSocketSession} 连接或关闭时,使用 {@link #sessionManager} 进行管理
+ * 2. 封装 {@link WebSocketSession} 支持并发操作
+ *
+ * @author 芋道源码
+ */
+public class WebSocketSessionHandlerDecorator extends WebSocketHandlerDecorator {
+
+ /**
+ * 发送时间的限制,单位:毫秒
+ */
+ private static final Integer SEND_TIME_LIMIT = 1000 * 5;
+ /**
+ * 发送消息缓冲上线,单位:bytes
+ */
+ private static final Integer BUFFER_SIZE_LIMIT = 1024 * 100;
+
+ private final WebSocketSessionManager sessionManager;
+
+ public WebSocketSessionHandlerDecorator(WebSocketHandler delegate,
+ WebSocketSessionManager sessionManager) {
+ super(delegate);
+ this.sessionManager = sessionManager;
+ }
+
+ @Override
+ public void afterConnectionEstablished(WebSocketSession session) {
+ // 实现 session 支持并发,可参考 https://blog.csdn.net/abu935009066/article/details/131218149
+ session = new ConcurrentWebSocketSessionDecorator(session, SEND_TIME_LIMIT, BUFFER_SIZE_LIMIT);
+ // 添加到 WebSocketSessionManager 中
+ sessionManager.addSession(session);
+ }
+
+ @Override
+ public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) {
+ sessionManager.removeSession(session);
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/session/WebSocketSessionManager.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/session/WebSocketSessionManager.java
new file mode 100644
index 000000000..ad1de23c2
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/session/WebSocketSessionManager.java
@@ -0,0 +1,53 @@
+package cn.iocoder.yudao.framework.websocket.core.session;
+
+import org.springframework.web.socket.WebSocketSession;
+
+import java.util.Collection;
+
+/**
+ * {@link WebSocketSession} 管理器的接口
+ *
+ * @author 芋道源码
+ */
+public interface WebSocketSessionManager {
+
+ /**
+ * 添加 Session
+ *
+ * @param session Session
+ */
+ void addSession(WebSocketSession session);
+
+ /**
+ * 移除 Session
+ *
+ * @param session Session
+ */
+ void removeSession(WebSocketSession session);
+
+ /**
+ * 获得指定编号的 Session
+ *
+ * @param id Session 编号
+ * @return Session
+ */
+ WebSocketSession getSession(String id);
+
+ /**
+ * 获得指定用户类型的 Session 列表
+ *
+ * @param userType 用户类型
+ * @return Session 列表
+ */
+ Collection getSessionList(Integer userType);
+
+ /**
+ * 获得指定用户编号的 Session 列表
+ *
+ * @param userType 用户类型
+ * @param userId 用户编号
+ * @return Session 列表
+ */
+ Collection getSessionList(Integer userType, Long userId);
+
+}
\ No newline at end of file
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/session/WebSocketSessionManagerImpl.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/session/WebSocketSessionManagerImpl.java
new file mode 100644
index 000000000..aca572f90
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/session/WebSocketSessionManagerImpl.java
@@ -0,0 +1,125 @@
+package cn.iocoder.yudao.framework.websocket.core.session;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.security.core.LoginUser;
+import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
+import cn.iocoder.yudao.framework.websocket.core.util.WebSocketFrameworkUtils;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * 默认的 {@link WebSocketSessionManager} 实现类
+ *
+ * @author 芋道源码
+ */
+public class WebSocketSessionManagerImpl implements WebSocketSessionManager {
+
+ /**
+ * id 与 WebSocketSession 映射
+ *
+ * key:Session 编号
+ */
+ private final ConcurrentMap idSessions = new ConcurrentHashMap<>();
+
+ /**
+ * user 与 WebSocketSession 映射
+ *
+ * key1:用户类型
+ * key2:用户编号
+ */
+ private final ConcurrentMap>> userSessions
+ = new ConcurrentHashMap<>();
+
+ @Override
+ public void addSession(WebSocketSession session) {
+ // 添加到 idSessions 中
+ idSessions.put(session.getId(), session);
+ // 添加到 userSessions 中
+ LoginUser user = WebSocketFrameworkUtils.getLoginUser(session);
+ if (user == null) {
+ return;
+ }
+ ConcurrentMap> userSessionsMap = userSessions.get(user.getUserType());
+ if (userSessionsMap == null) {
+ userSessionsMap = new ConcurrentHashMap<>();
+ if (userSessions.putIfAbsent(user.getUserType(), userSessionsMap) != null) {
+ userSessionsMap = userSessions.get(user.getUserType());
+ }
+ }
+ CopyOnWriteArrayList sessions = userSessionsMap.get(user.getId());
+ if (sessions == null) {
+ sessions = new CopyOnWriteArrayList<>();
+ if (userSessionsMap.putIfAbsent(user.getId(), sessions) != null) {
+ sessions = userSessionsMap.get(user.getId());
+ }
+ }
+ sessions.add(session);
+ }
+
+ @Override
+ public void removeSession(WebSocketSession session) {
+ // 移除从 idSessions 中
+ idSessions.remove(session.getId(), session);
+ // 移除从 idSessions 中
+ LoginUser user = WebSocketFrameworkUtils.getLoginUser(session);
+ if (user == null) {
+ return;
+ }
+ ConcurrentMap> userSessionsMap = userSessions.get(user.getUserType());
+ if (userSessionsMap == null) {
+ return;
+ }
+ CopyOnWriteArrayList sessions = userSessionsMap.get(user.getId());
+ sessions.removeIf(session0 -> session0.getId().equals(session.getId()));
+ if (CollUtil.isEmpty(sessions)) {
+ userSessionsMap.remove(user.getId(), sessions);
+ }
+ }
+
+ @Override
+ public WebSocketSession getSession(String id) {
+ return idSessions.get(id);
+ }
+
+ @Override
+ public Collection getSessionList(Integer userType) {
+ ConcurrentMap> userSessionsMap = userSessions.get(userType);
+ if (CollUtil.isEmpty(userSessionsMap)) {
+ return new ArrayList<>();
+ }
+ LinkedList result = new LinkedList<>(); // 避免扩容
+ Long contextTenantId = TenantContextHolder.getTenantId();
+ for (List sessions : userSessionsMap.values()) {
+ if (CollUtil.isEmpty(sessions)) {
+ continue;
+ }
+ // 特殊:如果租户不匹配,则直接排除
+ if (contextTenantId != null) {
+ Long userTenantId = WebSocketFrameworkUtils.getTenantId(sessions.get(0));
+ if (!contextTenantId.equals(userTenantId)) {
+ continue;
+ }
+ }
+ result.addAll(sessions);
+ }
+ return result;
+ }
+
+ @Override
+ public Collection getSessionList(Integer userType, Long userId) {
+ ConcurrentMap> userSessionsMap = userSessions.get(userType);
+ if (CollUtil.isEmpty(userSessionsMap)) {
+ return new ArrayList<>();
+ }
+ CopyOnWriteArrayList sessions = userSessionsMap.get(userId);
+ return CollUtil.isNotEmpty(sessions) ? new ArrayList<>(sessions) : new ArrayList<>();
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/util/WebSocketFrameworkUtils.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/util/WebSocketFrameworkUtils.java
new file mode 100644
index 000000000..58cdedc29
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/util/WebSocketFrameworkUtils.java
@@ -0,0 +1,67 @@
+package cn.iocoder.yudao.framework.websocket.core.util;
+
+import cn.iocoder.yudao.framework.security.core.LoginUser;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.util.Map;
+
+/**
+ * 专属于 web 包的工具类
+ *
+ * @author 芋道源码
+ */
+public class WebSocketFrameworkUtils {
+
+ public static final String ATTRIBUTE_LOGIN_USER = "LOGIN_USER";
+
+ /**
+ * 设置当前用户
+ *
+ * @param loginUser 登录用户
+ * @param attributes Session
+ */
+ public static void setLoginUser(LoginUser loginUser, Map attributes) {
+ attributes.put(ATTRIBUTE_LOGIN_USER, loginUser);
+ }
+
+ /**
+ * 获取当前用户
+ *
+ * @return 当前用户
+ */
+ public static LoginUser getLoginUser(WebSocketSession session) {
+ return (LoginUser) session.getAttributes().get(ATTRIBUTE_LOGIN_USER);
+ }
+
+ /**
+ * 获得当前用户的编号
+ *
+ * @return 用户编号
+ */
+ public static Long getLoginUserId(WebSocketSession session) {
+ LoginUser loginUser = getLoginUser(session);
+ return loginUser != null ? loginUser.getId() : null;
+ }
+
+ /**
+ * 获得当前用户的类型
+ *
+ * @return 用户编号
+ */
+ public static Integer getLoginUserType(WebSocketSession session) {
+ LoginUser loginUser = getLoginUser(session);
+ return loginUser != null ? loginUser.getUserType() : null;
+ }
+
+ /**
+ * 获得当前用户的租户编号
+ *
+ * @param session Session
+ * @return 租户编号
+ */
+ public static Long getTenantId(WebSocketSession session) {
+ LoginUser loginUser = getLoginUser(session);
+ return loginUser != null ? loginUser.getTenantId() : null;
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/package-info.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/package-info.java
index c771dfaac..97bc5f951 100644
--- a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/package-info.java
+++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/package-info.java
@@ -1 +1,4 @@
+/**
+ * WebSocket 框架,支持多节点的广播
+ */
package cn.iocoder.yudao.framework.websocket;
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/《芋道 Spring Boot WebSocket 入门》.md b/yudao-framework/yudao-spring-boot-starter-websocket/《芋道 Spring Boot WebSocket 入门》.md
new file mode 100644
index 000000000..8df5a7758
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-websocket/《芋道 Spring Boot WebSocket 入门》.md
@@ -0,0 +1 @@
+
diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/dto/BpmProcessInstanceCreateReqDTO.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/dto/BpmProcessInstanceCreateReqDTO.java
index 5d7edbe80..4403d3e68 100644
--- a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/dto/BpmProcessInstanceCreateReqDTO.java
+++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/dto/BpmProcessInstanceCreateReqDTO.java
@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.bpm.api.task.dto;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
+import java.util.List;
import java.util.Map;
/**
@@ -30,4 +31,15 @@ public class BpmProcessInstanceCreateReqDTO {
*/
@NotEmpty(message = "业务的唯一标识")
private String businessKey;
+
+ // TODO @hai:assignees 复数
+ /**
+ * 提前指派的审批人
+ *
+ * key:taskKey 任务编码
+ * value:审批人的数组
+ * 例如: { taskKey1 :[1, 2] },则表示 taskKey1 这个任务,提前设定了,由 userId 为 1,2 的用户进行审批
+ */
+ private Map> assignee;
+
}
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceCreateReqVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceCreateReqVO.java
index 8fc544ef4..93cf541bb 100644
--- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceCreateReqVO.java
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceCreateReqVO.java
@@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
+import java.util.List;
import java.util.Map;
@Schema(description = "管理后台 - 流程实例的创建 Request VO")
@@ -17,4 +18,8 @@ public class BpmProcessInstanceCreateReqVO {
@Schema(description = "变量实例")
private Map variables;
+ // TODO @hai:assignees 复数
+ @Schema(description = "提前指派的审批人", requiredMode = Schema.RequiredMode.REQUIRED, example = "{taskKey1: [1, 2]}")
+ private Map> assignee;
+
}
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/oa/BpmOALeaveDO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/oa/BpmOALeaveDO.java
index e4fbc1dea..4e4e68d6f 100644
--- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/oa/BpmOALeaveDO.java
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/oa/BpmOALeaveDO.java
@@ -1,10 +1,12 @@
package cn.iocoder.yudao.module.bpm.dal.dataobject.oa;
-import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceResultEnum;
-import lombok.*;
-import java.time.LocalDateTime;
-import com.baomidou.mybatisplus.annotation.*;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceResultEnum;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.time.LocalDateTime;
/**
* OA 请假申请 DO
@@ -37,7 +39,6 @@ public class BpmOALeaveDO extends BaseDO {
/**
* 请假类型
*/
- @TableField("`type`")
private String type;
/**
* 原因
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/task/BpmProcessInstanceExtDO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/task/BpmProcessInstanceExtDO.java
index 293cc2dd7..5a481fff0 100644
--- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/task/BpmProcessInstanceExtDO.java
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/task/BpmProcessInstanceExtDO.java
@@ -12,6 +12,7 @@ import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.time.LocalDateTime;
+import java.util.List;
import java.util.Map;
/**
@@ -87,4 +88,11 @@ public class BpmProcessInstanceExtDO extends BaseDO {
@TableField(typeHandler = JacksonTypeHandler.class)
private Map formVariables;
+ // TODO @hai:assignees 复数
+ /**
+ * 提前设定好的审批人
+ */
+ @TableField(typeHandler = JacksonTypeHandler.class, exist = false) // TODO 芋艿:临时 exist = false,避免 db 报错;
+ private Map> assignee;
+
}
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmTaskAssignRuleServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmTaskAssignRuleServiceImpl.java
index c6200b2dd..9ef936376 100644
--- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmTaskAssignRuleServiceImpl.java
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmTaskAssignRuleServiceImpl.java
@@ -18,6 +18,7 @@ import cn.iocoder.yudao.module.bpm.dal.mysql.definition.BpmTaskAssignRuleMapper;
import cn.iocoder.yudao.module.bpm.enums.DictTypeConstants;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmTaskAssignRuleTypeEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior.script.BpmTaskAssignScript;
+import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.PostApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
@@ -39,6 +40,7 @@ import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.*;
+import java.util.function.Function;
import static cn.hutool.core.text.CharSequenceUtil.format;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -77,6 +79,9 @@ public class BpmTaskAssignRuleServiceImpl implements BpmTaskAssignRuleService {
private DictDataApi dictDataApi;
@Resource
private PermissionApi permissionApi;
+ @Resource
+ @Lazy // 解决循环依赖
+ private BpmProcessInstanceService processInstanceService;
/**
* 任务分配脚本
*/
@@ -234,6 +239,14 @@ public class BpmTaskAssignRuleServiceImpl implements BpmTaskAssignRuleService {
@Override
@DataPermission(enable = false) // 忽略数据权限,不然分配会存在问题
public Set calculateTaskCandidateUsers(DelegateExecution execution) {
+ // 1. 先从提前选好的审批人中获取
+ List assignee = processInstanceService.getAssigneeByProcessInstanceIdAndTaskDefinitionKey(
+ execution.getProcessInstanceId(), execution.getCurrentActivityId());
+ if (CollUtil.isNotEmpty(assignee)) {
+ // TODO @hai:new HashSet 即可
+ return convertSet(assignee, Function.identity());
+ }
+ // 2. 通过分配规则,计算审批人
BpmTaskAssignRuleDO rule = getTaskRule(execution);
return calculateTaskCandidateUsers(execution, rule);
}
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java
index 23340ad19..cff0ec976 100644
--- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java
@@ -49,16 +49,17 @@ public interface BpmProcessInstanceService {
/**
* 获得流程实例的分页
*
- * @param userId 用户编号
+ * @param userId 用户编号
* @param pageReqVO 分页请求
* @return 流程实例的分页
*/
PageResult getMyProcessInstancePage(Long userId,
@Valid BpmProcessInstanceMyPageReqVO pageReqVO);
+
/**
* 创建流程实例(提供给前端)
*
- * @param userId 用户编号
+ * @param userId 用户编号
* @param createReqVO 创建信息
* @return 实例的编号
*/
@@ -67,7 +68,7 @@ public interface BpmProcessInstanceService {
/**
* 创建流程实例(提供给内部)
*
- * @param userId 用户编号
+ * @param userId 用户编号
* @param createReqDTO 创建信息
* @return 实例的编号
*/
@@ -84,7 +85,7 @@ public interface BpmProcessInstanceService {
/**
* 取消流程实例
*
- * @param userId 用户编号
+ * @param userId 用户编号
* @param cancelReqVO 取消信息
*/
void cancelProcessInstance(Long userId, @Valid BpmProcessInstanceCancelReqVO cancelReqVO);
@@ -139,9 +140,19 @@ public interface BpmProcessInstanceService {
/**
* 更新 ProcessInstance 拓展记录为不通过
*
- * @param id 流程编号
+ * @param id 流程编号
* @param reason 理由。例如说,审批不通过时,需要传递该值
*/
void updateProcessInstanceExtReject(String id, String reason);
+ // TODO @hai:改成 getProcessInstanceAssigneesByTaskDefinitionKey(String id, String taskDefinitionKey)
+ /**
+ * 获取流程实例中,取出指定流程任务提前指定的审批人
+ *
+ * @param processInstanceId 流程实例的编号
+ * @param taskDefinitionKey 流程任务定义的 key
+ * @return 审批人集合
+ */
+ List getAssigneeByProcessInstanceIdAndTaskDefinitionKey(String processInstanceId, String taskDefinitionKey);
+
}
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
index 56115d79b..415913a9a 100644
--- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
@@ -1 +1 @@
-package cn.iocoder.yudao.module.bpm.service.task;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*;
import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionExtDO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceExtDO;
import cn.iocoder.yudao.module.bpm.dal.mysql.task.BpmProcessInstanceExtMapper;
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceDeleteReasonEnum;
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceResultEnum;
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
import cn.iocoder.yudao.module.bpm.framework.bpm.core.event.BpmProcessInstanceResultEventPublisher;
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.bpm.service.message.BpmMessageService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.HistoryService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.delegate.event.FlowableCancelledEvent;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.Task;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.time.LocalDateTime;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
/**
* 流程实例 Service 实现类
*
* ProcessDefinition & ProcessInstance & Execution & Task 的关系:
* 1.
*
* HistoricProcessInstance & ProcessInstance 的关系:
* 1.
*
* 简单来说,前者 = 历史 + 运行中的流程实例,后者仅是运行中的流程实例
*
* @author 芋道源码
*/
@Service
@Validated
@Slf4j
public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService {
@Resource
private RuntimeService runtimeService;
@Resource
private BpmProcessInstanceExtMapper processInstanceExtMapper;
@Resource
@Lazy // 解决循环依赖
private BpmTaskService taskService;
@Resource
private BpmProcessDefinitionService processDefinitionService;
@Resource
private HistoryService historyService;
@Resource
private AdminUserApi adminUserApi;
@Resource
private DeptApi deptApi;
@Resource
private BpmProcessInstanceResultEventPublisher processInstanceResultEventPublisher;
@Resource
private BpmMessageService messageService;
@Override
public ProcessInstance getProcessInstance(String id) {
return runtimeService.createProcessInstanceQuery().processInstanceId(id).singleResult();
}
@Override
public List getProcessInstances(Set ids) {
return runtimeService.createProcessInstanceQuery().processInstanceIds(ids).list();
}
@Override
public PageResult getMyProcessInstancePage(Long userId,
BpmProcessInstanceMyPageReqVO pageReqVO) {
// 通过 BpmProcessInstanceExtDO 表,先查询到对应的分页
PageResult