Compare commits

..

9 Commits

92 changed files with 3716 additions and 82 deletions

View File

@@ -1,3 +1,12 @@
## [v2.1.0](https://github.com/Charles7c/continew-starter/compare/v2.0.2...v2.1.0) (2024-06-05)
### ✨ 新特性
- 【messaging/mail】新增动态邮箱配置 ([Gitee#19](https://gitee.com/continew/continew-starter/pulls/19)) ([ee30e86](https://github.com/Charles7c/continew-starter/commit/ee30e861ff536ee3ed6f14ff5ded5af7a513941d)) ([7feda79](https://github.com/Charles7c/continew-starter/commit/7feda79359ea40331eee1e3d4d5fd12000f027c5))
- 【data/mybatis-flex】新增 continew-starter-data-mybatis-flex 数据访问模块Mybatis Flex 自动配置) ([Gitee#18](https://gitee.com/continew/continew-starter/pulls/18)) ([124c7ff](https://github.com/Charles7c/continew-starter/commit/124c7ffe11a0e6563d9b513036c53ff66edbb9b3))
- 【extension/crud】新增查询字典列表方法 ([3d2a427](https://github.com/Charles7c/continew-starter/commit/3d2a4271d5eed676f16f4728b461dc3b298a65a9))
- 【messaging/websocket】新增 continew-starter-messaging-websocket 消息模块 ([cc079e8](https://github.com/Charles7c/continew-starter/commit/cc079e8bf422825bf9a96ddbd4329fc77d3cbf2c))
## [v2.0.2](https://github.com/Charles7c/continew-starter/compare/v2.0.1...v2.0.2) (2024-05-20)
### ✨ 新特性

View File

@@ -7,7 +7,7 @@
<img src="https://img.shields.io/maven-central/v/top.continew/continew-starter.svg?label=Maven%20Central&logo=sonatype&logoColor=FFF" alt="Release" />
</a>
<a href="https://github.com/Charles7c/continew-starter" target="_blank">
<img src="https://img.shields.io/badge/RELEASE-v2.0.2-%23ff3f59.svg" alt="Release" />
<img src="https://img.shields.io/badge/RELEASE-v2.1.0-%23ff3f59.svg" alt="Release" />
</a>
<a href="https://app.codacy.com/gh/Charles7c/continew-starter/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade" target="_blank">
<img src="https://app.codacy.com/project/badge/Grade/90ed633957a9410aa8745f0654827c01" alt="Codacy Badge" />
@@ -207,10 +207,11 @@ continew-starter.web:
### 数据访问模块
| 模块名称 | 模块说明 | 依赖版本 |
| ---------------------------------- | --------------------- | ------------------------------------------------------------ |
| continew-starter-data-core | 数据访问核心模块 | |
| 模块名称 | 模块说明 | 依赖版本 |
|------------------------------------|-------------------| ----------------------------------------------------------- |
| continew-starter-data-core | 数据访问核心模块 | |
| continew-starter-data-mybatis-plus | MyBatis Plus 自动配置 | <a href="https://baomidou.com/" target="_blank">MyBatis Plus</a>3.5.5<br /><a href="https://www.kancloud.cn/tracy5546/dynamic-datasource/2264611" target="_blank">dynamic-datasource-spring-boot-starter</a>4.3.0<br /><a href="https://github.com/p6spy/p6spy" target="_blank">P6Spy</a>3.9.1 |
| continew-starter-data-mybatis-flex | MyBatis Flex 自动配置 | |
### 认证模块
@@ -221,9 +222,10 @@ continew-starter.web:
### 消息模块
| 模块名称 | 模块说明 | 依赖版本 |
| ------------------------------- | -------- | ------------------------------------------------------------ |
| continew-starter-messaging-mail | 邮件 | Jakarta Mail1.1.0 |
| 模块名称 | 模块说明 | 依赖版本 |
|--------------------------------------|-----------| ------------------------------------------------------------ |
| continew-starter-messaging-mail | 邮件 | Jakarta Mail1.1.0 |
| continew-starter-messaging-websocket | WebSocket | |
### 扩展模块

View File

@@ -54,7 +54,7 @@ import java.util.concurrent.TimeUnit;
*/
@EnableWebMvc
@AutoConfiguration
@ConditionalOnProperty(prefix = PropertiesConstants.SPRINGDOC_SWAGGER_UI, name = PropertiesConstants.ENABLED, havingValue = "true")
@ConditionalOnProperty(prefix = PropertiesConstants.SPRINGDOC_SWAGGER_UI, name = PropertiesConstants.ENABLED, matchIfMissing = true)
@EnableConfigurationProperties(SpringDocExtensionProperties.class)
@PropertySource(value = "classpath:default-api-doc.yml", factory = GeneralPropertySourceFactory.class)
public class SpringDocAutoConfiguration implements WebMvcConfigurer {

View File

@@ -35,7 +35,7 @@ import top.continew.starter.core.constant.PropertiesConstants;
* @since 1.0.0
*/
@AutoConfiguration(before = com.xkcoding.justauth.autoconfigure.JustAuthAutoConfiguration.class)
@ConditionalOnProperty(prefix = "justauth", name = PropertiesConstants.ENABLED, havingValue = "true", matchIfMissing = true)
@ConditionalOnProperty(prefix = "justauth", name = PropertiesConstants.ENABLED, matchIfMissing = true)
public class JustAuthAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(JustAuthAutoConfiguration.class);

View File

@@ -46,7 +46,7 @@ import java.util.List;
* @since 1.0.0
*/
@AutoConfiguration
@ConditionalOnProperty(prefix = "spring.data.redisson", name = PropertiesConstants.ENABLED, havingValue = "true")
@ConditionalOnProperty(prefix = "spring.data.redisson", name = PropertiesConstants.ENABLED, matchIfMissing = true)
@EnableConfigurationProperties(RedissonProperties.class)
public class RedissonAutoConfiguration {

View File

@@ -34,7 +34,7 @@ public class RedissonProperties {
/**
* 是否启用 Redisson
*/
private boolean enabled = false;
private boolean enabled = true;
/**
* Redis 模式

View File

@@ -35,7 +35,7 @@ import top.continew.starter.core.constant.PropertiesConstants;
*/
@AutoConfiguration
@EnableConfigurationProperties(GraphicCaptchaProperties.class)
@ConditionalOnProperty(prefix = PropertiesConstants.CAPTCHA_GRAPHIC, name = PropertiesConstants.ENABLED, havingValue = "true", matchIfMissing = true)
@ConditionalOnProperty(prefix = PropertiesConstants.CAPTCHA_GRAPHIC, name = PropertiesConstants.ENABLED, matchIfMissing = true)
public class GraphicCaptchaAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(GraphicCaptchaAutoConfiguration.class);

View File

@@ -114,6 +114,16 @@ public class PropertiesConstants {
*/
public static final String CAPTCHA_BEHAVIOR = CAPTCHA + StringConstants.DOT + "behavior";
/**
* 消息配置
*/
public static final String MESSAGING = CONTINEW_STARTER + StringConstants.DOT + "messaging";
/**
* WebSocket 配置
*/
public static final String MESSAGING_WEBSOCKET = MESSAGING + StringConstants.DOT + "websocket";
private PropertiesConstants() {
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.core.util;
import java.util.Map;
import java.util.Properties;
/**
* Map 工具类
*
* @author Charles7c
* @since 2.1.0
*/
public class MapUtils {
private MapUtils() {
}
/**
* 转换为 Properties 对象
*
* @param source 数据源
* @return Properties 对象
*/
public static Properties toProperties(Map<String, String> source) {
Properties properties = new Properties();
properties.putAll(source);
return properties;
}
}

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>top.continew</groupId>
<artifactId>continew-starter-data</artifactId>
<version>${revision}</version>
</parent>
<artifactId>continew-starter-data-mybatis-flex</artifactId>
<description>ContiNew Starter 数据访问模块 - MyBatis Flex</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- MyBatis PlusMyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,简化开发、提高效率) -->
<dependency>
<groupId>com.mybatis-flex</groupId>
<artifactId>mybatis-flex-spring-boot3-starter</artifactId>
</dependency>
<dependency>
<groupId>com.mybatis-flex</groupId>
<artifactId>mybatis-flex-processor</artifactId>
<scope>provided</scope>
</dependency>
<!-- 数据访问模块 - 核心模块 -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-data-core</artifactId>
</dependency>
<!-- CosId通用、灵活、高性能的分布式 ID 生成器) -->
<dependency>
<groupId>me.ahoo.cosid</groupId>
<artifactId>cosid-spring-boot-starter</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,35 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.data.mybatis.flex.autoconfigure;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import top.continew.starter.core.constant.PropertiesConstants;
import java.lang.annotation.*;
/**
* 是否启用数据权限注解
*
* @author hellokaton
* @since 2.0.2
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@ConditionalOnProperty(prefix = "mybatis-flex.extension.data-permission", name = PropertiesConstants.ENABLED, havingValue = "true")
public @interface ConditionalOnEnabledDataPermission {
}

View File

@@ -0,0 +1,162 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.data.mybatis.flex.autoconfigure;
import com.mybatisflex.core.dialect.DbType;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* MyBatis Plus 扩展配置属性
*
* @author Charles7c
* @since 1.0.0
*/
@ConfigurationProperties(prefix = "mybatis-flex.extension")
public class MyBatisFlexExtensionProperties {
/**
* 是否启用扩展
*/
private boolean enabled = false;
/**
* Mapper 接口扫描包配置时必须使用mapper-package 键名)
* <p>
* e.g. com.example.**.mapper
* </p>
*/
private String mapperPackage;
/**
* 数据权限插件配置
*/
private DataPermissionProperties dataPermission;
/**
* 分页插件配置
*/
private PaginationProperties pagination;
/**
* 数据权限插件配置属性
*/
public static class DataPermissionProperties {
/**
* 是否启用数据权限插件
*/
private boolean enabled = false;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
/**
* 分页插件配置属性
*/
public static class PaginationProperties {
/**
* 是否启用分页插件
*/
private boolean enabled = true;
/**
* 数据库类型
*/
private DbType dbType;
/**
* 是否溢出处理
*/
private boolean overflow = false;
/**
* 单页分页条数限制(默认:-1 表示无限制)
*/
private Long maxLimit = -1L;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public DbType getDbType() {
return dbType;
}
public void setDbType(DbType dbType) {
this.dbType = dbType;
}
public boolean isOverflow() {
return overflow;
}
public void setOverflow(boolean overflow) {
this.overflow = overflow;
}
public Long getMaxLimit() {
return maxLimit;
}
public void setMaxLimit(Long maxLimit) {
this.maxLimit = maxLimit;
}
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getMapperPackage() {
return mapperPackage;
}
public void setMapperPackage(String mapperPackage) {
this.mapperPackage = mapperPackage;
}
public DataPermissionProperties getDataPermission() {
return dataPermission;
}
public void setDataPermission(DataPermissionProperties dataPermission) {
this.dataPermission = dataPermission;
}
public PaginationProperties getPagination() {
return pagination;
}
public void setPagination(PaginationProperties pagination) {
this.pagination = pagination;
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.data.mybatis.flex.autoconfigure;
import com.mybatisflex.core.dialect.DbType;
import com.mybatisflex.core.dialect.DialectFactory;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import org.mybatis.spring.annotation.MapperScan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.PropertySource;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.core.util.GeneralPropertySourceFactory;
import top.continew.starter.data.mybatis.flex.datapermission.DataPermissionDialect;
import top.continew.starter.data.mybatis.flex.datapermission.DataPermissionFilter;
/**
* MyBatis Flex 自动配置
*
* @author hellokaton
* @since 2.0.2
*/
@AutoConfiguration
@MapperScan("${mybatis-flex.extension.mapper-package}")
@EnableTransactionManagement(proxyTargetClass = true)
@EnableConfigurationProperties(MyBatisFlexExtensionProperties.class)
@ConditionalOnProperty(prefix = "mybatis-flex.extension", name = PropertiesConstants.ENABLED, havingValue = "true")
@PropertySource(value = "classpath:default-data-mybatis-flex.yml", factory = GeneralPropertySourceFactory.class)
public class MybatisFlexAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(MybatisFlexAutoConfiguration.class);
@Resource
private DataPermissionFilter dataPermissionFilter;
@PostConstruct
public void postConstruct() {
log.debug("[ContiNew Starter] - Auto Configuration 'MyBatis Flex' completed initialization.");
DialectFactory.registerDialect(DbType.MYSQL, new DataPermissionDialect(dataPermissionFilter));
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.data.mybatis.flex.base;
import cn.hutool.core.util.ClassUtil;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.core.row.Db;
import java.util.Collection;
/**
* Mapper 基类
*
* @param <T> 实体类
* @author hellokaton
* @since 1.0.0
*/
public interface BaseMapper<T> extends com.mybatisflex.core.BaseMapper<T> {
/**
* 批量更新记录
*
* @param entityList 实体列表
* @return 是否成功
*/
default boolean updateBatchById(Collection<T> entityList) {
return Db.updateEntitiesBatch(entityList) > 0;
}
/**
* 链式查询
*
* @return QueryWrapper 的包装类
*/
default QueryWrapper query() {
return QueryWrapper.create();
}
/**
* 链式查询
*
* @return QueryWrapper 的包装类
*/
default QueryWrapper query(T entity) {
return QueryWrapper.create(entity);
}
/**
* 获取实体类 Class 对象
*
* @return 实体类 Class 对象
*/
default Class<T> currentEntityClass() {
return (Class<T>)ClassUtil.getTypeArgument(this.getClass(), 0);
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.data.mybatis.flex.base;
import java.io.Serializable;
/**
* 枚举接口
*
* @param <T> value 类型
* @author hellokaton
* @since 1.0.0
*/
public interface IBaseEnum<T extends Serializable> {
/**
* 枚举描述
*
* @return 枚举描述
*/
String getDescription();
/**
* 枚举数据库存储值
*/
T getValue();
/**
* 颜色
*
* @return 颜色
*/
default String getColor() {
return null;
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.data.mybatis.flex.datapermission;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;
/**
* 数据权限注解
*
* @author hellokaton
* @since 2.0.2
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataPermission {
/**
* Alias for the {@link #tableAlias()} attribute.
*/
@AliasFor("tableAlias")
String value() default "";
/**
* 表别名
*/
@AliasFor("value")
String tableAlias() default "";
/**
* ID
*/
String id() default "id";
/**
* 部门 ID
*/
String deptId() default "dept_id";
/**
* 用户 ID
*/
String userId() default "create_user";
/**
* 角色 ID角色和部门关联表
*/
String roleId() default "role_id";
/**
* 部门表别名
*/
String deptTableAlias() default "sys_dept";
/**
* 角色和部门关联表别名
*/
String roleDeptTableAlias() default "sys_role_dept";
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.data.mybatis.flex.datapermission;
import org.aspectj.lang.annotation.*;
@Aspect
public class DataPermissionAspect {
// ThreadLocal用于存储注解信息
private static final ThreadLocal<DataPermission> THREAD_LOCAL = new ThreadLocal<>();
@Pointcut("@annotation(dataPermission)")
public void dataPermissionPointcut(DataPermission dataPermission) {
}
@Before("dataPermissionPointcut(dataPermission)")
public void beforeMethod(DataPermission dataPermission) {
THREAD_LOCAL.set(dataPermission);
}
@AfterThrowing(pointcut = "dataPermissionPointcut(dataPermission)")
public void afterThrowingMethod(DataPermission dataPermission) {
THREAD_LOCAL.remove();
}
@After("dataPermissionPointcut(dataPermission)")
public void afterMethod(DataPermission dataPermission) {
THREAD_LOCAL.remove();
}
public static DataPermission currentDataPermission() {
return THREAD_LOCAL.get();
}
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.data.mybatis.flex.datapermission;
import java.util.Set;
/**
* 当前用户信息
*
* @author hellokaton
* @since 2.0.2
*/
public class DataPermissionCurrentUser {
/**
* 用户 ID
*/
private String userId;
/**
* 角色列表
*/
private Set<CurrentUserRole> roles;
/**
* 部门 ID
*/
private String deptId;
/**
* 当前用户角色信息
*/
public static class CurrentUserRole {
/**
* 角色 ID
*/
private String roleId;
/**
* 数据权限
*/
private DataScope dataScope;
public CurrentUserRole() {
}
public CurrentUserRole(String roleId, DataScope dataScope) {
this.roleId = roleId;
this.dataScope = dataScope;
}
public String getRoleId() {
return roleId;
}
public void setRoleId(String roleId) {
this.roleId = roleId;
}
public DataScope getDataScope() {
return dataScope;
}
public void setDataScope(DataScope dataScope) {
this.dataScope = dataScope;
}
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public Set<CurrentUserRole> getRoles() {
return roles;
}
public void setRoles(Set<CurrentUserRole> roles) {
this.roles = roles;
}
public String getDeptId() {
return deptId;
}
public void setDeptId(String deptId) {
this.deptId = deptId;
}
}

View File

@@ -0,0 +1,161 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.data.mybatis.flex.datapermission;
import cn.hutool.core.util.StrUtil;
import com.mybatisflex.core.dialect.impl.CommonsDialectImpl;
import com.mybatisflex.core.query.QueryWrapper;
import java.util.Set;
/**
* 数据权限处理器实现类
*
* @author <a href="https://mybatis-flex.com/zh/core/data-permission.html">数据权限</a>
* @author hellokaton
* @since 2.0.2
*/
public class DataPermissionDialect extends CommonsDialectImpl {
private final DataPermissionFilter dataPermissionFilter;
public DataPermissionDialect(DataPermissionFilter dataPermissionFilter) {
this.dataPermissionFilter = dataPermissionFilter;
}
@Override
public String forSelectByQuery(QueryWrapper queryWrapper) {
if (!dataPermissionFilter.isFilter()) {
return super.buildSelectSql(queryWrapper);
}
DataPermission dataPermission = DataPermissionAspect.currentDataPermission();
if (null == dataPermission) {
return super.buildSelectSql(queryWrapper);
}
DataPermissionCurrentUser currentUser = dataPermissionFilter.getCurrentUser();
Set<DataPermissionCurrentUser.CurrentUserRole> roles = currentUser.getRoles();
for (DataPermissionCurrentUser.CurrentUserRole role : roles) {
DataScope dataScope = role.getDataScope();
if (DataScope.ALL.equals(dataScope)) {
return super.buildSelectSql(queryWrapper);
}
switch (dataScope) {
case DEPT_AND_CHILD -> this.buildDeptAndChildExpression(dataPermission, currentUser, queryWrapper);
case DEPT -> this.buildDeptExpression(dataPermission, currentUser, queryWrapper);
case SELF -> this.buildSelfExpression(dataPermission, currentUser, queryWrapper);
case CUSTOM -> this.buildCustomExpression(dataPermission, role, queryWrapper);
default -> throw new IllegalArgumentException("暂不支持 [%s] 数据权限".formatted(dataScope));
}
}
return super.buildSelectSql(queryWrapper);
}
/**
* 构建自定义数据权限表达式
*
* <p>
* 处理完后的 SQL 示例:<br /> select t1.* from table as t1 where t1.dept_id in (select dept_id from sys_role_dept
* where role_id = xxx);
* </p>
*
* @param dataPermission 数据权限
* @param role 当前用户角色
* @param queryWrapper 查询条件
* @return 处理完后的表达式
*/
private void buildCustomExpression(DataPermission dataPermission,
DataPermissionCurrentUser.CurrentUserRole role,
QueryWrapper queryWrapper) {
QueryWrapper subQueryWrapper = QueryWrapper.create();
subQueryWrapper.select(dataPermission.deptId()).from(dataPermission.roleDeptTableAlias());
subQueryWrapper.eq(dataPermission.roleId(), role.getRoleId());
queryWrapper.in(buildColumn(dataPermission.tableAlias(), dataPermission.deptId()), subQueryWrapper);
}
/**
* 构建仅本人数据权限表达式
*
* <p>
* 处理完后的 SQL 示例:<br /> select t1.* from table as t1 where t1.create_user = xxx;
* </p>
*
* @param dataPermission 数据权限
* @param currentUser 当前用户
* @param queryWrapper 处理前的表达式
*/
private void buildSelfExpression(DataPermission dataPermission,
DataPermissionCurrentUser currentUser,
QueryWrapper queryWrapper) {
queryWrapper.eq(buildColumn(dataPermission.tableAlias(), dataPermission.userId()), currentUser.getUserId());
}
/**
* 构建本部门数据权限表达式
*
* <p>
* 处理完后的 SQL 示例:<br /> select t1.* from table as t1 where t1.dept_id = xxx;
* </p>
*
* @param dataPermission 数据权限
* @param currentUser 当前用户
* @param queryWrapper 查询条件
*/
private void buildDeptExpression(DataPermission dataPermission,
DataPermissionCurrentUser currentUser,
QueryWrapper queryWrapper) {
queryWrapper.eq(buildColumn(dataPermission.tableAlias(), dataPermission.deptId()), currentUser.getDeptId());
}
/**
* 构建本部门及以下数据权限表达式
*
* <p>
* 处理完后的 SQL 示例:<br /> select t1.* from table as t1 where t1.dept_id in (select id from sys_dept where id =
* xxx or find_in_set(xxx, ancestors));
* </p>
*
* @param dataPermission 数据权限
* @param currentUser 当前用户
* @param queryWrapper 查询条件
*/
private void buildDeptAndChildExpression(DataPermission dataPermission,
DataPermissionCurrentUser currentUser,
QueryWrapper queryWrapper) {
QueryWrapper subQueryWrapper = QueryWrapper.create();
subQueryWrapper.select(dataPermission.id()).from(dataPermission.deptTableAlias());
subQueryWrapper.and(qw -> {
qw.eq(dataPermission.id(), currentUser.getDeptId())
.or("find_in_set(" + currentUser.getDeptId() + ",ancestors)");
});
queryWrapper.in(buildColumn(dataPermission.tableAlias(), dataPermission.deptId()), subQueryWrapper);
}
/**
* 构建 Column
*
* @param tableAlias 表别名
* @param columnName 字段名称
* @return 带表别名字段
*/
private String buildColumn(String tableAlias, String columnName) {
if (StrUtil.isNotEmpty(tableAlias)) {
return "%s.%s".formatted(tableAlias, columnName);
}
return columnName;
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.data.mybatis.flex.datapermission;
/**
* 数据权限过滤器接口
*
* @author hellokaton
* @since 2.0.2
*/
public interface DataPermissionFilter {
/**
* 是否过滤
*
* @return true过滤false不过滤
*/
boolean isFilter();
/**
* 获取当前用户信息
*
* @return 当前用户信息
*/
DataPermissionCurrentUser getCurrentUser();
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.data.mybatis.flex.datapermission;
/**
* 数据权限枚举
*
* @author hellokaton
* @since 2.0.2
*/
public enum DataScope {
/**
* 全部数据权限
*/
ALL,
/**
* 本部门及以下数据权限
*/
DEPT_AND_CHILD,
/**
* 本部门数据权限
*/
DEPT,
/**
* 仅本人数据权限
*/
SELF,
/**
* 自定义数据权限
*/
CUSTOM,
}

View File

@@ -0,0 +1,193 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.data.mybatis.flex.query;
import com.mybatisflex.core.query.QueryWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.text.CharSequenceUtil;
import top.continew.starter.core.exception.BadRequestException;
import top.continew.starter.core.util.ReflectUtils;
import top.continew.starter.core.util.validate.ValidationUtils;
import top.continew.starter.data.core.annotation.Query;
import top.continew.starter.data.core.annotation.QueryIgnore;
import top.continew.starter.data.core.enums.QueryType;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
/**
* QueryWrapper 助手
*
* @author hellokaton
* @author Jasmine
* @since 1.0.0
*/
public class QueryWrapperHelper {
private static final Logger log = LoggerFactory.getLogger(QueryWrapperHelper.class);
private QueryWrapperHelper() {
}
/**
* 构建 QueryWrapper
*
* @param query 查询条件
* @param <Q> 查询条件数据类型
* @param <R> 查询数据类型
* @return QueryWrapper
*/
public static <Q, R> QueryWrapper build(Q query) {
QueryWrapper queryWrapper = QueryWrapper.create();
// 没有查询条件,直接返回
if (null == query) {
return queryWrapper;
}
// 获取查询条件中所有的字段
List<Field> fieldList = ReflectUtils.getNonStaticFields(query.getClass());
return build(query, fieldList, queryWrapper);
}
/**
* 构建 QueryWrapper
*
* @param query 查询条件
* @param fields 查询条件字段列表
* @param queryWrapper QueryWrapper
* @param <Q> 查询条件数据类型
* @return QueryWrapper
*/
public static <Q> QueryWrapper build(Q query, List<Field> fields, QueryWrapper queryWrapper) {
// 没有查询条件,直接返回
if (null == query) {
return queryWrapper;
}
// 解析并拼接查询条件
for (Field field : fields) {
List<Consumer<QueryWrapper>> consumers = buildWrapperConsumer(query, field);
if (CollUtil.isNotEmpty(consumers)) {
consumers.forEach(queryWrapper::and);
}
}
return queryWrapper;
}
/**
* 构建 QueryWrapper Consumer
*
* @param query 查询条件
* @param field 查询条件字段
* @param <Q> 查询条件数据类型
* @param <R> 查询数据类型
* @return QueryWrapper Consumer
*/
private static <Q, R> List<Consumer<QueryWrapper>> buildWrapperConsumer(Q query, Field field) {
boolean accessible = field.canAccess(query);
try {
field.setAccessible(true);
// 如果字段值为空,直接返回
Object fieldValue = field.get(query);
if (ObjectUtil.isEmpty(fieldValue)) {
return Collections.emptyList();
}
// 设置了 @QueryIgnore 注解,直接忽略
QueryIgnore queryIgnoreAnnotation = field.getAnnotation(QueryIgnore.class);
if (null != queryIgnoreAnnotation && queryIgnoreAnnotation.value()) {
return Collections.emptyList();
}
// 建议:数据库表列建议采用下划线连接法命名,程序变量建议采用驼峰法命名
String fieldName = field.getName();
// 没有 @Query 注解,默认等值查询
Query queryAnnotation = field.getAnnotation(Query.class);
if (null == queryAnnotation) {
return Collections.singletonList(q -> q.eq(CharSequenceUtil.toUnderlineCase(fieldName), fieldValue));
}
// 解析单列查询
QueryType queryType = queryAnnotation.type();
String[] columns = queryAnnotation.columns();
final int columnLength = ArrayUtil.length(columns);
List<Consumer<QueryWrapper>> consumers = new ArrayList<>(columnLength);
if (columnLength <= 1) {
String columnName = columnLength == 1 ? columns[0] : CharSequenceUtil.toUnderlineCase(fieldName);
parse(queryType, columnName, fieldValue, consumers);
return consumers;
}
// 解析多列查询
for (String column : columns) {
parse(queryType, column, fieldValue, consumers);
}
return consumers;
} catch (BadRequestException e) {
throw e;
} catch (Exception e) {
log.error("Build query wrapper occurred an error: {}. Query: {}, Field: {}.", e
.getMessage(), query, field, e);
} finally {
field.setAccessible(accessible);
}
return Collections.emptyList();
}
/**
* 解析查询条件
*
* @param queryType 查询类型
* @param columnName 列名
* @param fieldValue 字段值
* @param <R> 查询数据类型
*/
private static <R> void parse(QueryType queryType,
String columnName,
Object fieldValue,
List<Consumer<QueryWrapper>> consumers) {
switch (queryType) {
case EQ -> consumers.add(q -> q.eq(columnName, fieldValue));
case NE -> consumers.add(q -> q.ne(columnName, fieldValue));
case GT -> consumers.add(q -> q.gt(columnName, fieldValue));
case GE -> consumers.add(q -> q.ge(columnName, fieldValue));
case LT -> consumers.add(q -> q.lt(columnName, fieldValue));
case LE -> consumers.add(q -> q.le(columnName, fieldValue));
case BETWEEN -> {
List<Object> between = new ArrayList<>((List<Object>)fieldValue);
ValidationUtils.throwIf(between.size() != 2, "[{}] 必须是一个范围", columnName);
consumers.add(q -> q.between(columnName, between.get(0), between.get(1)));
}
case LIKE -> consumers.add(q -> q.like(columnName, fieldValue));
case LIKE_LEFT -> consumers.add(q -> q.likeLeft(columnName, fieldValue));
case LIKE_RIGHT -> consumers.add(q -> q.likeRight(columnName, fieldValue));
case IN -> {
ValidationUtils.throwIfEmpty(fieldValue, "[{}] 不能为空", columnName);
consumers.add(q -> q.in(columnName, (Collection<Object>)fieldValue));
}
case NOT_IN -> {
ValidationUtils.throwIfEmpty(fieldValue, "[{}] 不能为空", columnName);
consumers.add(q -> q.notIn(columnName, (Collection<Object>)fieldValue));
}
case IS_NULL -> consumers.add(q -> q.isNull(columnName));
case IS_NOT_NULL -> consumers.add(q -> q.isNotNull(columnName));
default -> throw new IllegalArgumentException("暂不支持 [%s] 查询类型".formatted(queryType));
}
}
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.data.mybatis.flex.service;
/**
* 通用业务接口
*
* @param <T> 实体类型
* @author hellokaton
* @since 1.2.0
*/
public interface IService<T> extends com.mybatisflex.core.service.IService<T> {
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.data.mybatis.flex.service.impl;
import top.continew.starter.core.util.ClassUtils;
import top.continew.starter.data.mybatis.flex.base.BaseMapper;
import top.continew.starter.data.mybatis.flex.service.IService;
/**
* 通用业务实现类
*
* @param <M> Mapper 接口
* @param <T> 实体类型
* @author hellokaton
* @since 1.5.0
*/
public class ServiceImpl<M extends BaseMapper<T>, T> extends com.mybatisflex.spring.service.impl.ServiceImpl<M, T> implements IService<T> {
protected final Class<?>[] typeArguments = ClassUtils.getTypeArguments(this.getClass());
protected final Class<T> entityClass = currentModelClass();
protected Class<T> currentModelClass() {
return (Class<T>)this.typeArguments[1];
}
}

View File

@@ -0,0 +1 @@
top.continew.starter.data.mybatis.flex.autoconfigure.MybatisFlexAutoConfiguration

View File

@@ -0,0 +1,22 @@
--- ### MyBatis Flex 配置https://mybatis-flex.com/zh/base/configuration.html
mybatis-flex:
# 启动时是否检查 MyBatis XML 文件的存在默认false 不检查)
check-config-location: true
## MyBatis 原生支持配置
configuration:
# 是否开启自动驼峰命名规则camel case映射即从经典数据库列名 A_COLUMN下划线命名到经典 Java 属性名 aColumn驼峰命名的类似映射
# 此属性在 MyBatis 中原默认值为 false在 MyBatis-Plus 中,此属性也将用于生成最终的 SQL 的 select body如果您的数据库命名符合规则无需使用 @TableField 注解指定数据库字段名
map-underscore-to-camel-case: true
# MyBatis 自动映射时未知列或未知属性处理策略,通过该配置可指定 MyBatis 在自动映射过程中遇到未知列或者未知属性时如何处理
# NONE不做任何处理 (默认值)WARNING以日志的形式打印相关警告信息FAILING当作映射失败处理并抛出异常和详细信息
auto-mapping-unknown-column-behavior: NONE
# 日志配置
# 默认org.apache.ibatis.logging.slf4j.Slf4jImpl
# 更详细会有性能损耗org.apache.ibatis.logging.stdout.StdOutImpl
# 关闭(可单纯使用 p6spy 分析org.apache.ibatis.logging.nologging.NoLoggingImpl
log-impl: org.apache.ibatis.logging.nologging.NoLoggingImpl
global-config:
key-config:
key-type: generator
# flexId 主键生成器 com.mybatisflex.core.keygen.impl.FlexIDKeyGenerator
value: flexId

View File

@@ -16,6 +16,7 @@
<modules>
<module>continew-starter-data-core</module>
<module>continew-starter-data-mybatis-plus</module>
<module>continew-starter-data-mybatis-flex</module>
</modules>
<dependencies>

View File

@@ -43,10 +43,11 @@
<properties>
<!-- 项目版本号 -->
<revision>2.0.2</revision>
<revision>2.1.0</revision>
<sa-token.version>1.38.0</sa-token.version>
<just-auth.version>1.16.6</just-auth.version>
<mybatis-plus.version>3.5.5</mybatis-plus.version>
<mybatis-flex.version>1.8.9</mybatis-flex.version>
<dynamic-datasource.version>4.3.0</dynamic-datasource.version>
<p6spy.version>3.9.1</p6spy.version>
<jetcache.version>2.7.5</jetcache.version>
@@ -119,6 +120,18 @@
<version>${mybatis-plus.version}</version>
</dependency>
<!-- MyBatis FlexMyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,简化开发、提高效率) -->
<dependency>
<groupId>com.mybatis-flex</groupId>
<artifactId>mybatis-flex-spring-boot3-starter</artifactId>
<version>${mybatis-flex.version}</version>
</dependency>
<dependency>
<groupId>com.mybatis-flex</groupId>
<artifactId>mybatis-flex-processor</artifactId>
<version>${mybatis-flex.version}</version>
</dependency>
<!-- Dynamic Datasource基于 Spring Boot 的快速集成多数据源的启动器) -->
<dependency>
<groupId>com.baomidou</groupId>
@@ -299,10 +312,24 @@
</dependency>
<!-- ContiNew Starter 依赖 -->
<!-- 扩展模块 - CRUD -->
<!-- 扩展模块 - CRUD - 核心模块 -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-extension-crud</artifactId>
<artifactId>continew-starter-extension-crud-core</artifactId>
<version>${revision}</version>
</dependency>
<!-- 扩展模块 - CRUD - MyBatis Plus ORM 模块 -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-extension-crud-mp</artifactId>
<version>${revision}</version>
</dependency>
<!-- 扩展模块 - CRUD - MyBatis Flex ORM 模块 -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-extension-crud-mf</artifactId>
<version>${revision}</version>
</dependency>
@@ -327,6 +354,13 @@
<version>${revision}</version>
</dependency>
<!-- 数据访问模块 - MyBatis Flex -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-data-mybatis-flex</artifactId>
<version>${revision}</version>
</dependency>
<!-- 数据访问模块 - 核心模块 -->
<dependency>
<groupId>top.continew</groupId>
@@ -355,6 +389,13 @@
<version>${revision}</version>
</dependency>
<!-- 消息模块 - WebSocket -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-messaging-websocket</artifactId>
<version>${revision}</version>
</dependency>
<!-- 消息模块 - 邮件 -->
<dependency>
<groupId>top.continew</groupId>

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>top.continew</groupId>
<artifactId>continew-starter-extension-crud</artifactId>
<version>${revision}</version>
</parent>
<artifactId>continew-starter-extension-crud-core</artifactId>
<description>ContiNew Starter 扩展模块 - CRUD增删改查 - 核心模块</description>
<dependencies>
<!-- Crane4j一个基于注解的用于完成一切 “根据 A 的 key 值拿到 B再把 B 的属性映射到 A” 这类需求的字段填充框架) -->
<dependency>
<groupId>cn.crane4j</groupId>
<artifactId>crane4j-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
</dependency>
<!-- 认证模块 - SaToken -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-auth-satoken</artifactId>
</dependency>
<!-- 文件处理模块 - Excel -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-file-excel</artifactId>
</dependency>
<!-- Web 模块 -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-web</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,45 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.extension.crud.annotation;
import java.lang.annotation.*;
/**
* 字典结构字段
*
* @author Charles7c
* @since 2.1.0
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DictField {
/**
* 标签字段名
*
* @return 标签字段名
*/
String labelKey() default "name";
/**
* 值字段名
*
* @return 值字段名
*/
String valueKey() default "id";
}

View File

@@ -16,8 +16,8 @@
package top.continew.starter.extension.crud.model.query;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ArrayUtil;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.data.domain.Sort;
import top.continew.starter.core.constant.StringConstants;

View File

@@ -0,0 +1,114 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.extension.crud.model.resp;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
/**
* 键值对信息
*
* @param <T>
* @author Charles7c
* @since 2.1.0
*/
@Schema(description = "键值对信息")
public class LabelValueResp<T> implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 标签
*/
@Schema(description = "标签", example = "")
private String label;
/**
* 值
*/
@Schema(description = "", example = "1")
private T value;
/**
* 是否禁用
*/
@Schema(description = "是否禁用", example = "false")
private Boolean disabled;
/**
* 扩展
*/
@Schema(description = "扩展")
@JsonInclude(JsonInclude.Include.NON_NULL)
private Object extend;
public LabelValueResp() {
}
public LabelValueResp(String label, T value) {
this.label = label;
this.value = value;
}
public LabelValueResp(String label, T value, Object extend) {
this.label = label;
this.value = value;
this.extend = extend;
}
public LabelValueResp(String label, T value, Boolean disabled) {
this.label = label;
this.value = value;
this.disabled = disabled;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
public Boolean getDisabled() {
return disabled;
}
public void setDisabled(Boolean disabled) {
this.disabled = disabled;
}
public Object getExtend() {
return extend;
}
public void setExtend(Object extend) {
this.extend = extend;
}
}

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>top.continew</groupId>
<artifactId>continew-starter-extension-crud</artifactId>
<version>${revision}</version>
</parent>
<artifactId>continew-starter-extension-crud-mf</artifactId>
<description>ContiNew Starter 扩展模块 - CRUD增删改查 - MyBatis Flex ORM 模块</description>
<dependencies>
<!-- 核心模块 -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-extension-crud-core</artifactId>
</dependency>
<!-- 数据访问模块 - MyBatis Flex -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-data-mybatis-flex</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -28,13 +28,13 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.extension.crud.annotation.CrudRequestMapping;
import top.continew.starter.extension.crud.util.ValidateGroup;
import top.continew.starter.extension.crud.enums.Api;
import top.continew.starter.extension.crud.model.query.PageQuery;
import top.continew.starter.extension.crud.model.query.SortQuery;
import top.continew.starter.extension.crud.model.req.BaseReq;
import top.continew.starter.extension.crud.model.resp.PageResp;
import top.continew.starter.extension.crud.service.BaseService;
import top.continew.starter.extension.crud.util.ValidateGroup;
import top.continew.starter.extension.crud.model.query.PageQuery;
import top.continew.starter.extension.crud.model.resp.PageResp;
import top.continew.starter.web.model.R;
import java.util.List;

View File

@@ -0,0 +1,91 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.extension.crud.converter;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ClassUtil;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.data.mybatis.flex.base.IBaseEnum;
/**
* Easy Excel 枚举接口转换器
*
* @see IBaseEnum
* @author Charles7c
* @since 1.2.0
*/
public class ExcelBaseEnumConverter implements Converter<IBaseEnum<Integer>> {
@Override
public Class<IBaseEnum> supportJavaTypeKey() {
return IBaseEnum.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
/**
* 转换为 Java 数据(读取 Excel
*/
@Override
public IBaseEnum convertToJavaData(ReadCellData<?> cellData,
ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
return this.getEnum(IBaseEnum.class, Convert.toStr(cellData.getData()));
}
/**
* 转换为 Excel 数据(写入 Excel
*/
@Override
public WriteCellData<String> convertToExcelData(IBaseEnum<Integer> value,
ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
if (null == value) {
return new WriteCellData<>(StringConstants.EMPTY);
}
return new WriteCellData<>(value.getDescription());
}
/**
* 通过 value 获取枚举对象,获取不到时为 {@code null}
*
* @param enumType 枚举类型
* @param description 描述
* @return 对应枚举 ,获取不到时为 {@code null}
*/
private IBaseEnum<Integer> getEnum(Class<?> enumType, String description) {
Object[] enumConstants = enumType.getEnumConstants();
for (Object enumConstant : enumConstants) {
if (ClassUtil.isAssignable(IBaseEnum.class, enumType)) {
IBaseEnum<Integer> baseEnum = (IBaseEnum<Integer>)enumConstant;
if (baseEnum.getDescription().equals(description)) {
return baseEnum;
}
}
}
return null;
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.extension.crud.model.entity;
import java.io.Serial;
import java.time.LocalDateTime;
/**
* 实体类基类
*
* <p>
* 通用字段:创建人、创建时间
* </p>
*
* @author Charles7c
* @since 2.0.1
*/
public class BaseCreateDO extends BaseIdDO {
@Serial
private static final long serialVersionUID = 1L;
/**
* 创建人
*/
private Long createUser;
/**
* 创建时间
*/
private LocalDateTime createTime;
public Long getCreateUser() {
return createUser;
}
public void setCreateUser(Long createUser) {
this.createUser = createUser;
}
public LocalDateTime getCreateTime() {
return createTime;
}
public void setCreateTime(LocalDateTime createTime) {
this.createTime = createTime;
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.extension.crud.model.entity;
import java.io.Serial;
import java.time.LocalDateTime;
/**
* 实体类基类
*
* @author Charles7c
* @since 1.0.0
*/
public class BaseDO extends BaseIdDO {
@Serial
private static final long serialVersionUID = 1L;
/**
* 创建人
*/
private Long createUser;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 修改人
*/
private Long updateUser;
/**
* 修改时间
*/
private LocalDateTime updateTime;
public Long getCreateUser() {
return createUser;
}
public void setCreateUser(Long createUser) {
this.createUser = createUser;
}
public LocalDateTime getCreateTime() {
return createTime;
}
public void setCreateTime(LocalDateTime createTime) {
this.createTime = createTime;
}
public Long getUpdateUser() {
return updateUser;
}
public void setUpdateUser(Long updateUser) {
this.updateUser = updateUser;
}
public LocalDateTime getUpdateTime() {
return updateTime;
}
public void setUpdateTime(LocalDateTime updateTime) {
this.updateTime = updateTime;
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.extension.crud.model.entity;
import com.mybatisflex.annotation.Id;
import java.io.Serial;
import java.io.Serializable;
/**
* 实体类基类
*
* <p>
* 通用字段ID 主键
* </p>
*
* @author Charles7c
* @since 2.0.1
*/
public class BaseIdDO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
@Id
private Long id;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.extension.crud.model.entity;
import java.io.Serial;
import java.time.LocalDateTime;
/**
* 实体类基类
*
* <p>
* 通用字段:创建人、创建时间
* </p>
*
* @author Charles7c
* @since 2.0.1
*/
public class BaseUpdateDO extends BaseIdDO {
@Serial
private static final long serialVersionUID = 1L;
/**
* 修改人
*/
private Long updateUser;
/**
* 修改时间
*/
private LocalDateTime updateTime;
public Long getUpdateUser() {
return updateUser;
}
public void setUpdateUser(Long updateUser) {
this.updateUser = updateUser;
}
public LocalDateTime getUpdateTime() {
return updateTime;
}
public void setUpdateTime(LocalDateTime updateTime) {
this.updateTime = updateTime;
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.extension.crud.model.query;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import org.hibernate.validator.constraints.Range;
import org.springdoc.core.annotations.ParameterObject;
import java.io.Serial;
/**
* 分页查询条件
*
* @author Charles7c
* @since 1.0.0
*/
@ParameterObject
@Schema(description = "分页查询条件")
public class PageQuery extends SortQuery {
@Serial
private static final long serialVersionUID = 1L;
/**
* 默认页码1
*/
private static final int DEFAULT_PAGE = 1;
/**
* 默认每页条数10
*/
private static final int DEFAULT_SIZE = 10;
/**
* 页码
*/
@Schema(description = "页码", example = "1")
@Min(value = 1, message = "页码最小值为 {value}")
private Integer page = DEFAULT_PAGE;
/**
* 每页条数
*/
@Schema(description = "每页条数", example = "10")
@Range(min = 1, max = 1000, message = "每页条数(取值范围 {min}-{max}")
private Integer size = DEFAULT_SIZE;
public Integer getPage() {
return page;
}
public void setPage(Integer page) {
this.page = page;
}
public Integer getSize() {
return size;
}
public void setSize(Integer size) {
this.size = size;
}
}

View File

@@ -0,0 +1,146 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.extension.crud.model.resp;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import com.mybatisflex.core.paginate.Page;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 分页信息
*
* @param <L> 列表数据类型
* @author Charles7c
* @since 1.0.0
*/
@Schema(description = "分页信息")
public class PageResp<L> implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 列表数据
*/
@Schema(description = "列表数据")
private List<L> list;
/**
* 总记录数
*/
@Schema(description = "总记录数", example = "10")
private long total;
/**
* 基于 MyBatis Plus 分页数据构建分页信息,并将源数据转换为指定类型数据
*
* @param page MyBatis Plus 分页数据
* @param targetClass 目标类型 Class 对象
* @param <T> 源列表数据类型
* @param <L> 目标列表数据类型
* @return 分页信息
*/
public static <T, L> PageResp<L> build(Page<T> page, Class<L> targetClass) {
if (null == page) {
return empty();
}
PageResp<L> pageResp = new PageResp<>();
pageResp.setList(BeanUtil.copyToList(page.getRecords(), targetClass));
pageResp.setTotal(page.getTotalRow());
return pageResp;
}
/**
* 基于 MyBatis Plus 分页数据构建分页信息
*
* @param page MyBatis Plus 分页数据
* @param <L> 列表数据类型
* @return 分页信息
*/
public static <L> PageResp<L> build(Page<L> page) {
if (null == page) {
return empty();
}
PageResp<L> pageResp = new PageResp<>();
pageResp.setList(page.getRecords());
pageResp.setTotal(page.getTotalRow());
return pageResp;
}
/**
* 基于列表数据构建分页信息
*
* @param page 页码
* @param size 每页条数
* @param list 列表数据
* @param <L> 列表数据类型
* @return 分页信息
*/
public static <L> PageResp<L> build(int page, int size, List<L> list) {
if (CollUtil.isEmpty(list)) {
return empty();
}
PageResp<L> pageResp = new PageResp<>();
pageResp.setTotal(list.size());
// 对列表数据进行分页
int fromIndex = (page - 1) * size;
int toIndex = page * size + fromIndex;
if (fromIndex > list.size()) {
pageResp.setList(new ArrayList<>(0));
} else if (toIndex >= list.size()) {
pageResp.setList(list.subList(fromIndex, list.size()));
} else {
pageResp.setList(list.subList(fromIndex, toIndex));
}
return pageResp;
}
/**
* 空分页信息
*
* @param <L> 列表数据类型
* @return 分页信息
*/
private static <L> PageResp<L> empty() {
PageResp<L> pageResp = new PageResp<>();
pageResp.setList(Collections.emptyList());
return pageResp;
}
public List<L> getList() {
return list;
}
public void setList(List<L> list) {
this.list = list;
}
public long getTotal() {
return total;
}
public void setTotal(long total) {
this.total = total;
}
}

View File

@@ -18,8 +18,9 @@ package top.continew.starter.extension.crud.service;
import cn.hutool.core.lang.tree.Tree;
import jakarta.servlet.http.HttpServletResponse;
import top.continew.starter.extension.crud.model.query.PageQuery;
import top.continew.starter.extension.crud.model.query.SortQuery;
import top.continew.starter.extension.crud.model.query.PageQuery;
import top.continew.starter.extension.crud.model.resp.LabelValueResp;
import top.continew.starter.extension.crud.model.resp.PageResp;
import java.util.List;
@@ -72,6 +73,15 @@ public interface BaseService<L, D, Q, C> {
*/
D get(Long id);
/**
* 查询字典列表
*
* @param query 查询条件
* @param sortQuery 排序查询条件
* @return 字典列表信息
*/
List<LabelValueResp> listDict(Q query, SortQuery sortQuery);
/**
* 新增
*

View File

@@ -0,0 +1,319 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.extension.crud.service.impl;
import cn.crane4j.core.support.OperateTemplate;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Opt;
import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.lang.tree.TreeNodeConfig;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.data.domain.Sort;
import org.springframework.transaction.annotation.Transactional;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.util.ReflectUtils;
import top.continew.starter.core.util.validate.ValidationUtils;
import top.continew.starter.data.mybatis.flex.base.BaseMapper;
import top.continew.starter.data.mybatis.flex.query.QueryWrapperHelper;
import top.continew.starter.data.mybatis.flex.service.impl.ServiceImpl;
import top.continew.starter.extension.crud.annotation.TreeField;
import top.continew.starter.extension.crud.model.query.PageQuery;
import top.continew.starter.extension.crud.model.query.SortQuery;
import top.continew.starter.extension.crud.model.resp.PageResp;
import top.continew.starter.extension.crud.service.BaseService;
import top.continew.starter.extension.crud.util.TreeUtils;
import top.continew.starter.extension.crud.model.entity.BaseIdDO;
import top.continew.starter.file.excel.util.ExcelUtils;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* 业务实现基类
*
* @param <M> Mapper 接口
* @param <T> 实体类型
* @param <L> 列表类型
* @param <D> 详情类型
* @param <Q> 查询条件
* @param <C> 创建或修改类型
* @author Charles7c
* @since 1.0.0
*/
public abstract class BaseServiceImpl<M extends BaseMapper<T>, T extends BaseIdDO, L, D, Q, C> extends ServiceImpl<M, T> implements BaseService<L, D, Q, C> {
protected final Class<L> listClass = this.currentListClass();
protected final Class<D> detailClass = this.currentDetailClass();
protected final Class<Q> queryClass = this.currentQueryClass();
private final List<Field> queryFields = ReflectUtils.getNonStaticFields(this.queryClass);
@Override
public PageResp<L> page(Q query, PageQuery pageQuery) {
QueryWrapper queryWrapper = this.buildQueryWrapper(query);
Page<T> page = mapper.paginate(pageQuery.getPage(), pageQuery.getSize(), queryWrapper);
PageResp<L> pageResp = PageResp.build(page, listClass);
pageResp.getList().forEach(this::fill);
return pageResp;
}
@Override
public List<Tree<Long>> tree(Q query, SortQuery sortQuery, boolean isSimple) {
List<L> list = this.list(query, sortQuery);
if (CollUtil.isEmpty(list)) {
return new ArrayList<>(0);
}
// 如果构建简单树结构,则不包含基本树结构之外的扩展字段
TreeNodeConfig treeNodeConfig = TreeUtils.DEFAULT_CONFIG;
TreeField treeField = listClass.getDeclaredAnnotation(TreeField.class);
if (!isSimple) {
// 根据 @TreeField 配置生成树结构配置
treeNodeConfig = TreeUtils.genTreeNodeConfig(treeField);
}
// 构建树
return TreeUtils.build(list, treeNodeConfig, (node, tree) -> {
// 转换器
tree.setId(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.value())));
tree.setParentId(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.parentIdKey())));
tree.setName(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.nameKey())));
tree.setWeight(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.weightKey())));
if (!isSimple) {
List<Field> fieldList = ReflectUtils.getNonStaticFields(listClass);
fieldList.removeIf(f -> CharSequenceUtil.equalsAnyIgnoreCase(f.getName(), treeField.value(), treeField
.parentIdKey(), treeField.nameKey(), treeField.weightKey(), treeField.childrenKey()));
fieldList.forEach(f -> tree.putExtra(f.getName(), ReflectUtil.invoke(node, CharSequenceUtil.genGetter(f
.getName()))));
}
});
}
@Override
public List<L> list(Q query, SortQuery sortQuery) {
List<L> list = this.list(query, sortQuery, listClass);
list.forEach(this::fill);
return list;
}
@Override
public D get(Long id) {
T entity = super.getById(id);
D detail = BeanUtil.toBean(entity, detailClass);
this.fill(detail);
return detail;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long add(C req) {
this.beforeAdd(req);
T entity = BeanUtil.copyProperties(req, this.entityClass);
mapper.insert(entity);
this.afterAdd(req, entity);
return entity.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void update(C req, Long id) {
this.beforeUpdate(req, id);
T entity = this.getById(id);
BeanUtil.copyProperties(req, entity, CopyOptions.create().ignoreNullValue());
mapper.update(entity);
this.afterUpdate(req, entity);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void delete(List<Long> ids) {
this.beforeDelete(ids);
mapper.deleteBatchByIds(ids);
this.afterDelete(ids);
}
@Override
public void export(Q query, SortQuery sortQuery, HttpServletResponse response) {
List<D> list = this.list(query, sortQuery, detailClass);
list.forEach(this::fill);
ExcelUtils.export(list, "导出数据", detailClass, response);
}
/**
* 查询列表
*
* @param query 查询条件
* @param sortQuery 排序查询条件
* @param targetClass 指定类型
* @return 列表信息
*/
protected <E> List<E> list(Q query, SortQuery sortQuery, Class<E> targetClass) {
QueryWrapper queryWrapper = this.buildQueryWrapper(query);
// 设置排序
this.sort(queryWrapper, sortQuery);
List<T> entityList = mapper.selectListByQuery(queryWrapper);
if (this.entityClass == targetClass) {
return (List<E>)entityList;
}
return BeanUtil.copyToList(entityList, targetClass);
}
/**
* 设置排序
*
* @param queryWrapper 查询条件封装对象
* @param sortQuery 排序查询条件
*/
protected void sort(QueryWrapper queryWrapper, SortQuery sortQuery) {
Sort sort = Opt.ofNullable(sortQuery).orElseGet(SortQuery::new).getSort();
List<Field> entityFields = ReflectUtils.getNonStaticFields(this.entityClass);
for (Sort.Order order : sort) {
if (null == order) {
continue;
}
String property = order.getProperty();
String checkProperty;
// 携带表别名则获取 . 后面的字段名
if (property.contains(StringConstants.DOT)) {
checkProperty = CollUtil.getLast(CharSequenceUtil.split(property, StringConstants.DOT));
} else {
checkProperty = property;
}
Optional<Field> optional = entityFields.stream()
.filter(field -> checkProperty.equals(field.getName()))
.findFirst();
ValidationUtils.throwIf(optional.isEmpty(), "无效的排序字段 [{}]", property);
queryWrapper.orderBy(CharSequenceUtil.toUnderlineCase(property), order.isAscending());
}
}
/**
* 填充数据
*
* @param obj 待填充信息
*/
protected void fill(Object obj) {
if (null == obj) {
return;
}
OperateTemplate operateTemplate = SpringUtil.getBean(OperateTemplate.class);
operateTemplate.execute(obj);
}
/**
* 构建 QueryWrapper
*
* @param query 查询条件
* @return QueryWrapper
*/
protected QueryWrapper buildQueryWrapper(Q query) {
QueryWrapper queryWrapper = QueryWrapper.create();
// 解析并拼接查询条件
return QueryWrapperHelper.build(query, queryFields, queryWrapper);
}
/**
* 新增前置处理
*
* @param req 创建信息
*/
protected void beforeAdd(C req) {
/* 新增前置处理 */
}
/**
* 修改前置处理
*
* @param req 修改信息
* @param id ID
*/
protected void beforeUpdate(C req, Long id) {
/* 修改前置处理 */
}
/**
* 删除前置处理
*
* @param ids ID 列表
*/
protected void beforeDelete(List<Long> ids) {
/* 删除前置处理 */
}
/**
* 新增后置处理
*
* @param req 创建信息
* @param entity 实体信息
*/
protected void afterAdd(C req, T entity) {
/* 新增后置处理 */
}
/**
* 修改后置处理
*
* @param req 修改信息
* @param entity 实体信息
*/
protected void afterUpdate(C req, T entity) {
/* 修改后置处理 */
}
/**
* 删除后置处理
*
* @param ids ID 列表
*/
protected void afterDelete(List<Long> ids) {
/* 删除后置处理 */
}
/**
* 获取当前列表信息类型
*
* @return 当前列表信息类型
*/
protected Class<L> currentListClass() {
return (Class<L>)this.typeArguments[2];
}
/**
* 获取当前详情信息类型
*
* @return 当前详情信息类型
*/
protected Class<D> currentDetailClass() {
return (Class<D>)this.typeArguments[3];
}
/**
* 获取当前查询条件类型
*
* @return 当前查询条件类型
*/
protected Class<Q> currentQueryClass() {
return (Class<Q>)this.typeArguments[4];
}
}

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>top.continew</groupId>
<artifactId>continew-starter-extension-crud</artifactId>
<version>${revision}</version>
</parent>
<artifactId>continew-starter-extension-crud-mp</artifactId>
<description>ContiNew Starter 扩展模块 - CRUD增删改查 - MyBatis Plus ORM 模块</description>
<dependencies>
<!-- 核心模块 -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-extension-crud-core</artifactId>
</dependency>
<!-- 数据访问模块 - MyBatis Plus -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-data-mybatis-plus</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,191 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.extension.crud.controller;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.text.CharSequenceUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.extension.crud.annotation.CrudRequestMapping;
import top.continew.starter.extension.crud.enums.Api;
import top.continew.starter.extension.crud.model.query.SortQuery;
import top.continew.starter.extension.crud.model.req.BaseReq;
import top.continew.starter.extension.crud.util.ValidateGroup;
import top.continew.starter.extension.crud.model.query.PageQuery;
import top.continew.starter.extension.crud.model.resp.PageResp;
import top.continew.starter.extension.crud.service.BaseService;
import top.continew.starter.web.model.R;
import java.util.List;
/**
* 控制器基类
*
* @param <S> 业务接口
* @param <L> 列表类型
* @param <D> 详情类型
* @param <Q> 查询条件
* @param <C> 创建或修改类型
* @author Charles7c
* @since 1.0.0
*/
public abstract class BaseController<S extends BaseService<L, D, Q, C>, L, D, Q, C extends BaseReq> {
@Autowired
protected S baseService;
/**
* 分页查询列表
*
* @param query 查询条件
* @param pageQuery 分页查询条件
* @return 分页信息
*/
@Operation(summary = "分页查询列表", description = "分页查询列表")
@ResponseBody
@GetMapping
public R<PageResp<L>> page(Q query, @Validated PageQuery pageQuery) {
this.checkPermission(Api.LIST);
return R.ok(baseService.page(query, pageQuery));
}
/**
* 查询树列表
*
* @param query 查询条件
* @param sortQuery 排序查询条件
* @return 树列表信息
*/
@Operation(summary = "查询树列表", description = "查询树列表")
@ResponseBody
@GetMapping("/tree")
public R<List<Tree<Long>>> tree(Q query, SortQuery sortQuery) {
this.checkPermission(Api.LIST);
return R.ok(baseService.tree(query, sortQuery, false));
}
/**
* 查询列表
*
* @param query 查询条件
* @param sortQuery 排序查询条件
* @return 列表信息
*/
@Operation(summary = "查询列表", description = "查询列表")
@ResponseBody
@GetMapping("/list")
public R<List<L>> list(Q query, SortQuery sortQuery) {
this.checkPermission(Api.LIST);
return R.ok(baseService.list(query, sortQuery));
}
/**
* 查询详情
*
* @param id ID
* @return 详情信息
*/
@Operation(summary = "查询详情", description = "查询详情")
@Parameter(name = "id", description = "ID", example = "1", in = ParameterIn.PATH)
@ResponseBody
@GetMapping("/{id}")
public R<D> get(@PathVariable Long id) {
this.checkPermission(Api.LIST);
return R.ok(baseService.get(id));
}
/**
* 新增
*
* @param req 创建信息
* @return 自增 ID
*/
@Operation(summary = "新增数据", description = "新增数据")
@ResponseBody
@PostMapping
public R<Long> add(@Validated(ValidateGroup.Crud.Add.class) @RequestBody C req) {
this.checkPermission(Api.ADD);
return R.ok("新增成功", baseService.add(req));
}
/**
* 修改
*
* @param req 修改信息
* @param id ID
* @return /
*/
@Operation(summary = "修改数据", description = "修改数据")
@Parameter(name = "id", description = "ID", example = "1", in = ParameterIn.PATH)
@ResponseBody
@PutMapping("/{id}")
public R<Void> update(@Validated(ValidateGroup.Crud.Update.class) @RequestBody C req, @PathVariable Long id) {
this.checkPermission(Api.UPDATE);
baseService.update(req, id);
return R.ok("修改成功");
}
/**
* 删除
*
* @param ids ID 列表
* @return /
*/
@Operation(summary = "删除数据", description = "删除数据")
@Parameter(name = "ids", description = "ID 列表", example = "1,2", in = ParameterIn.PATH)
@ResponseBody
@DeleteMapping("/{ids}")
public R<Void> delete(@PathVariable List<Long> ids) {
this.checkPermission(Api.DELETE);
baseService.delete(ids);
return R.ok("删除成功");
}
/**
* 导出
*
* @param query 查询条件
* @param sortQuery 排序查询条件
* @param response 响应对象
*/
@Operation(summary = "导出数据", description = "导出数据")
@GetMapping("/export")
public void export(Q query, SortQuery sortQuery, HttpServletResponse response) {
this.checkPermission(Api.EXPORT);
baseService.export(query, sortQuery, response);
}
/**
* 根据 API 类型进行权限验证
*
* @param api API 类型
*/
protected void checkPermission(Api api) {
CrudRequestMapping crudRequestMapping = this.getClass().getDeclaredAnnotation(CrudRequestMapping.class);
String path = crudRequestMapping.value();
String permissionPrefix = String.join(StringConstants.COLON, CharSequenceUtil
.splitTrim(path, StringConstants.SLASH));
StpUtil.checkPermission("%s:%s".formatted(permissionPrefix, api.name().toLowerCase()));
}
}

View File

@@ -0,0 +1,117 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.extension.crud.service;
import cn.hutool.core.lang.tree.Tree;
import jakarta.servlet.http.HttpServletResponse;
import top.continew.starter.extension.crud.model.query.PageQuery;
import top.continew.starter.extension.crud.model.query.SortQuery;
import top.continew.starter.extension.crud.model.resp.LabelValueResp;
import top.continew.starter.extension.crud.model.resp.PageResp;
import java.util.List;
/**
* 业务接口基类
*
* @param <L> 列表类型
* @param <D> 详情类型
* @param <Q> 查询条件
* @param <C> 创建或修改类型
* @author Charles7c
* @since 1.0.0
*/
public interface BaseService<L, D, Q, C> {
/**
* 分页查询列表
*
* @param query 查询条件
* @param pageQuery 分页查询条件
* @return 分页列表信息
*/
PageResp<L> page(Q query, PageQuery pageQuery);
/**
* 查询树列表
*
* @param query 查询条件
* @param sortQuery 排序查询条件
* @param isSimple 是否为简单树结构(不包含基本树结构之外的扩展字段)
* @return 树列表信息
*/
List<Tree<Long>> tree(Q query, SortQuery sortQuery, boolean isSimple);
/**
* 查询列表
*
* @param query 查询条件
* @param sortQuery 排序查询条件
* @return 列表信息
*/
List<L> list(Q query, SortQuery sortQuery);
/**
* 查询详情
*
* @param id ID
* @return 详情信息
*/
D get(Long id);
/**
* 查询字典列表
*
* @param query 查询条件
* @param sortQuery 排序查询条件
* @return 字典列表信息
* @since 2.1.0
*/
List<LabelValueResp> listDict(Q query, SortQuery sortQuery);
/**
* 新增
*
* @param req 创建信息
* @return 自增 ID
*/
Long add(C req);
/**
* 修改
*
* @param req 修改信息
* @param id ID
*/
void update(C req, Long id);
/**
* 删除
*
* @param ids ID 列表
*/
void delete(List<Long> ids);
/**
* 导出
*
* @param query 查询条件
* @param sortQuery 排序查询条件
* @param response 响应对象
*/
void export(Q query, SortQuery sortQuery, HttpServletResponse response);
}

View File

@@ -23,6 +23,7 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Opt;
import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.lang.tree.TreeNodeConfig;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.extra.spring.SpringUtil;
@@ -34,23 +35,24 @@ import org.springframework.transaction.annotation.Transactional;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.util.ClassUtils;
import top.continew.starter.core.util.ReflectUtils;
import top.continew.starter.core.util.validate.CheckUtils;
import top.continew.starter.core.util.validate.ValidationUtils;
import top.continew.starter.data.mybatis.plus.base.BaseMapper;
import top.continew.starter.data.mybatis.plus.query.QueryWrapperHelper;
import top.continew.starter.data.mybatis.plus.service.impl.ServiceImpl;
import top.continew.starter.extension.crud.annotation.DictField;
import top.continew.starter.extension.crud.annotation.TreeField;
import top.continew.starter.extension.crud.model.entity.BaseIdDO;
import top.continew.starter.extension.crud.model.query.PageQuery;
import top.continew.starter.extension.crud.model.query.SortQuery;
import top.continew.starter.extension.crud.model.resp.LabelValueResp;
import top.continew.starter.extension.crud.model.resp.PageResp;
import top.continew.starter.extension.crud.service.BaseService;
import top.continew.starter.extension.crud.util.TreeUtils;
import top.continew.starter.extension.crud.model.query.PageQuery;
import top.continew.starter.file.excel.util.ExcelUtils;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.*;
/**
* 业务实现基类
@@ -126,6 +128,23 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
return detail;
}
@Override
public List<LabelValueResp> listDict(Q query, SortQuery sortQuery) {
QueryWrapper<T> queryWrapper = this.buildQueryWrapper(query);
this.sort(queryWrapper, sortQuery);
DictField dictField = entityClass.getDeclaredAnnotation(DictField.class);
CheckUtils.throwIfNull(dictField, "请添加并配置 @DictField 字典结构信息");
// 指定查询字典字段
queryWrapper.select(dictField.labelKey(), dictField.valueKey());
List<T> entityList = baseMapper.selectList(queryWrapper);
// 解析映射
Map<String, String> fieldMapping = MapUtil.newHashMap(2);
fieldMapping.put(dictField.labelKey(), "label");
fieldMapping.put(dictField.valueKey(), "value");
return BeanUtil.copyToList(entityList, LabelValueResp.class, CopyOptions.create()
.setFieldMapping(fieldMapping));
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long add(C req) {

View File

@@ -10,42 +10,12 @@
</parent>
<artifactId>continew-starter-extension-crud</artifactId>
<packaging>pom</packaging>
<description>ContiNew Starter 扩展模块 - CRUD增删改查</description>
<dependencies>
<!-- Crane4j一个基于注解的用于完成一切 “根据 A 的 key 值拿到 B再把 B 的属性映射到 A” 这类需求的字段填充框架) -->
<dependency>
<groupId>cn.crane4j</groupId>
<artifactId>crane4j-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
</dependency>
<!-- 认证模块 - SaToken -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-auth-satoken</artifactId>
</dependency>
<!-- 数据访问模块 - MyBatis Plus -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-data-mybatis-plus</artifactId>
</dependency>
<!-- 文件处理模块 - Excel -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-file-excel</artifactId>
</dependency>
<!-- Web 模块 -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-web</artifactId>
</dependency>
</dependencies>
<modules>
<module>continew-starter-extension-crud-core</module>
<module>continew-starter-extension-crud-mp</module>
<module>continew-starter-extension-crud-mf</module>
</modules>
</project>

View File

@@ -30,6 +30,6 @@ import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@ConditionalOnProperty(prefix = PropertiesConstants.LOG, name = PropertiesConstants.ENABLED, havingValue = "true", matchIfMissing = true)
@ConditionalOnProperty(prefix = PropertiesConstants.LOG, name = PropertiesConstants.ENABLED, matchIfMissing = true)
public @interface ConditionalOnEnabledLog {
}

View File

@@ -0,0 +1,173 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.messaging.mail.core;
import cn.hutool.core.map.MapUtil;
import top.continew.starter.core.util.validate.ValidationUtils;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Properties;
/**
* 邮件配置
*
* @author KAI
* @author Charles7c
* @since 2.1.0
*/
public class MailConfig {
private static final Charset DEFAULT_CHARSET;
public static final String DEFAULT_PROTOCOL = "smtp";
/**
* 协议
*/
private String protocol = DEFAULT_PROTOCOL;
/**
* 服务器地址
*/
private String host;
/**
* 服务器端口
*/
private Integer port;
/**
* 用户名
*/
private String username;
/**
* 密码(授权码)
*/
private String password;
/**
* 是否启用 SSL 连接
*/
private boolean sslEnabled = false;
/**
* SSL 端口
*/
private Integer sslPort;
private Charset defaultEncoding;
private final Map<String, String> properties;
public MailConfig() {
this.defaultEncoding = DEFAULT_CHARSET;
this.properties = MapUtil.newHashMap();
}
public String getProtocol() {
return protocol;
}
public void setProtocol(String protocol) {
this.protocol = protocol;
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public Integer getPort() {
return port;
}
public void setPort(Integer port) {
this.port = port;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public boolean isSslEnabled() {
return sslEnabled;
}
public void setSslEnabled(boolean sslEnabled) {
this.sslEnabled = sslEnabled;
}
public Integer getSslPort() {
return sslPort;
}
public void setSslPort(Integer sslPort) {
this.sslPort = sslPort;
}
public Charset getDefaultEncoding() {
return defaultEncoding;
}
public void setDefaultEncoding(Charset defaultEncoding) {
this.defaultEncoding = defaultEncoding;
}
public Map<String, String> getProperties() {
return properties;
}
/**
* 将当前配置转换为 JavaMail 的 Properties 对象
*
* @return Properties 对象
*/
public Properties toJavaMailProperties() {
Properties javaMailProperties = new Properties();
javaMailProperties.putAll(this.getProperties());
javaMailProperties.put("mail.smtp.auth", true);
javaMailProperties.put("mail.smtp.ssl.enable", this.isSslEnabled());
if (this.isSslEnabled()) {
ValidationUtils.throwIfNull(this.getSslPort(), "邮件配置错误SSL端口不能为空");
javaMailProperties.put("mail.smtp.socketFactory.port", this.sslPort);
javaMailProperties.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
}
return javaMailProperties;
}
static {
DEFAULT_CHARSET = StandardCharsets.UTF_8;
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.messaging.mail.core;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import top.continew.starter.core.util.validate.ValidationUtils;
/**
* 邮件配置服务
*
* @author Charles7c
* @since 2.1.0
*/
public interface MailConfigService {
/**
* 获取邮件配置
*
* @return 邮件配置
*/
MailConfig getMailConfig();
/**
* 应用配置
*
* @param mailConfig 邮件配置
* @param sender 邮件 Sender
*/
default void apply(MailConfig mailConfig, JavaMailSenderImpl sender) {
String protocolLowerCase = mailConfig.getProtocol().toLowerCase();
ValidationUtils.throwIfNotEqual(MailConfig.DEFAULT_PROTOCOL, protocolLowerCase, "邮件配置错误:不支持的邮件发送协议: %s"
.formatted(mailConfig.getProtocol()));
sender.setProtocol(mailConfig.getProtocol());
ValidationUtils.throwIfBlank(mailConfig.getHost(), "邮件配置错误:服务器地址不能为空");
sender.setHost(mailConfig.getHost());
ValidationUtils.throwIfNull(mailConfig.getPort(), "邮件配置错误:服务器端口不能为空");
sender.setPort(mailConfig.getPort());
ValidationUtils.throwIfBlank(mailConfig.getUsername(), "邮件配置错误:用户名不能为空");
sender.setUsername(mailConfig.getUsername());
ValidationUtils.throwIfBlank(mailConfig.getPassword(), "邮件配置错误:密码不能为空");
sender.setPassword(mailConfig.getPassword());
if (mailConfig.getDefaultEncoding() != null) {
sender.setDefaultEncoding(mailConfig.getDefaultEncoding().name());
}
if (!mailConfig.getProperties().isEmpty()) {
sender.setJavaMailProperties(mailConfig.toJavaMailProperties());
}
}
}

View File

@@ -18,14 +18,16 @@ package top.continew.starter.messaging.mail.util;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.extra.spring.SpringUtil;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.util.ExceptionUtils;
import top.continew.starter.messaging.mail.core.MailConfigService;
import java.io.File;
import java.nio.charset.StandardCharsets;
@@ -39,9 +41,8 @@ import java.util.List;
* @author Charles7c
* @since 1.0.0
*/
public class MailUtils {
private static final JavaMailSender MAIL_SENDER = SpringUtil.getBean(JavaMailSender.class);
public class MailUtils {
private MailUtils() {
}
@@ -157,11 +158,13 @@ public class MailUtils {
boolean isHtml,
File... files) throws MessagingException {
Assert.isFalse(CollUtil.isEmpty(tos), "请至少指定一名收件人");
MimeMessage mimeMessage = MAIL_SENDER.createMimeMessage();
JavaMailSenderImpl mailSender = getMailSender();
MimeMessage mimeMessage = mailSender.createMimeMessage();
// 创建邮件发送器
MimeMessageHelper messageHelper = new MimeMessageHelper(mimeMessage, true, StandardCharsets.UTF_8
.displayName());
// 设置基本信息
messageHelper.setFrom(SpringUtil.getProperty("spring.mail.username"));
messageHelper.setFrom(mailSender.getUsername());
messageHelper.setSubject(subject);
messageHelper.setText(content, isHtml);
// 设置收信人
@@ -182,7 +185,7 @@ public class MailUtils {
}
}
// 发送邮件
MAIL_SENDER.send(mimeMessage);
mailSender.send(mimeMessage);
}
/**
@@ -206,4 +209,18 @@ public class MailUtils {
return result;
}
/**
* 获取邮件 Sender
*
* @return 邮件 Sender
*/
public static JavaMailSenderImpl getMailSender() {
JavaMailSenderImpl mailSender = SpringUtil.getBean(JavaMailSenderImpl.class);
MailConfigService mailConfigService = ExceptionUtils.exToNull(() -> SpringUtil
.getBean(MailConfigService.class));
if (mailConfigService != null && mailConfigService.getMailConfig() != null) {
mailConfigService.apply(mailConfigService.getMailConfig(), mailSender);
}
return mailSender;
}
}

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>top.continew</groupId>
<artifactId>continew-starter-messaging</artifactId>
<version>${revision}</version>
</parent>
<artifactId>continew-starter-messaging-websocket</artifactId>
<description>ContiNew Starter 消息模块 - WebSocket</description>
<dependencies>
<!-- WebSocket 支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,101 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.messaging.websocket.autoconfigure;
import cn.hutool.extra.spring.SpringUtil;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
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.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;
import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.messaging.websocket.core.CurrentUserProvider;
import top.continew.starter.messaging.websocket.core.WebSocketInterceptor;
import top.continew.starter.messaging.websocket.dao.WebSocketSessionDao;
import top.continew.starter.messaging.websocket.dao.WebSocketSessionDaoDefaultImpl;
/**
* WebSocket 自动配置
*
* @author WeiRan
* @author Charles7c
* @since 2.1.0
*/
@AutoConfiguration
@EnableWebSocket
@EnableConfigurationProperties(WebSocketProperties.class)
@ConditionalOnProperty(prefix = PropertiesConstants.MESSAGING_WEBSOCKET, name = PropertiesConstants.ENABLED, matchIfMissing = true)
public class WebSocketAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(WebSocketAutoConfiguration.class);
private final WebSocketProperties properties;
public WebSocketAutoConfiguration(WebSocketProperties properties) {
this.properties = properties;
}
@Bean
public WebSocketConfigurer webSocketConfigurer(WebSocketHandler handler, HandshakeInterceptor interceptor) {
return registry -> registry.addHandler(handler, properties.getPath())
.addInterceptors(interceptor)
.setAllowedOrigins(properties.getAllowedOrigins().toArray(String[]::new));
}
@Bean
@ConditionalOnMissingBean
public WebSocketHandler webSocketHandler() {
return new top.continew.starter.messaging.websocket.core.WebSocketHandler(properties, SpringUtil
.getBean(WebSocketSessionDao.class));
}
@Bean
@ConditionalOnMissingBean
public HandshakeInterceptor handshakeInterceptor() {
return new WebSocketInterceptor(properties, SpringUtil.getBean(CurrentUserProvider.class));
}
/**
* WebSocket 会话 DAO
*/
@Bean
@ConditionalOnMissingBean
public WebSocketSessionDao webSocketSessionDao() {
return new WebSocketSessionDaoDefaultImpl();
}
/**
* 当前用户 Provider如不提供则报错
*/
@Bean
@ConditionalOnMissingBean
public CurrentUserProvider currentUserProvider() {
throw new NoSuchBeanDefinitionException(CurrentUserProvider.class);
}
@PostConstruct
public void postConstruct() {
log.debug("[ContiNew Starter] - Auto Configuration 'Messaging-WebSocket' completed initialization.");
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.messaging.websocket.autoconfigure;
import org.springframework.boot.context.properties.ConfigurationProperties;
import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.core.constant.StringConstants;
import java.awt.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* WebSocket 配置属性
*
* @author Charles7c
* @since 2.1.0
*/
@ConfigurationProperties(PropertiesConstants.MESSAGING_WEBSOCKET)
public class WebSocketProperties {
private static final List<String> ALL = Collections.singletonList(StringConstants.ASTERISK);
/**
* 是否启用 WebSocket
*/
private boolean enabled = true;
/**
* 路径
*/
private String path = StringConstants.SLASH + "websocket";
/**
* 允许跨域的域名
*/
private List<String> allowedOrigins = new ArrayList<>(ALL);
/**
* 当前登录用户 Key
*/
private String currentUserKey = "CURRENT_USER";
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public List<String> getAllowedOrigins() {
return allowedOrigins;
}
public void setAllowedOrigins(List<String> allowedOrigins) {
this.allowedOrigins = allowedOrigins;
}
public String getCurrentUserKey() {
return currentUserKey;
}
public void setCurrentUserKey(String currentUserKey) {
this.currentUserKey = currentUserKey;
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.messaging.websocket.core;
import org.springframework.http.server.ServletServerHttpRequest;
import top.continew.starter.messaging.websocket.model.CurrentUser;
/**
* 当前登录用户 Provider
*
* @author Charles7c
* @since 2.1.0
*/
public interface CurrentUserProvider {
/**
* 获取当前登录用户
*
* @param request 请求对象
* @return 当前登录用户
*/
CurrentUser getCurrentUser(ServletServerHttpRequest request);
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.messaging.websocket.core;
import cn.hutool.core.convert.Convert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.AbstractWebSocketHandler;
import top.continew.starter.messaging.websocket.autoconfigure.WebSocketProperties;
import top.continew.starter.messaging.websocket.dao.WebSocketSessionDao;
/**
* WebSocket 处理器
*
* @author WeiRan
* @author Charles7c
* @since 2.1.0
*/
public class WebSocketHandler extends AbstractWebSocketHandler {
private static final Logger log = LoggerFactory.getLogger(WebSocketHandler.class);
private final WebSocketProperties webSocketProperties;
private final WebSocketSessionDao webSocketSessionDao;
public WebSocketHandler(WebSocketProperties webSocketProperties, WebSocketSessionDao webSocketSessionDao) {
this.webSocketProperties = webSocketProperties;
this.webSocketSessionDao = webSocketSessionDao;
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
log.info("WebSocket receive message. sessionId: {}, message: {}.", session.getId(), message.getPayload());
super.handleTextMessage(session, message);
}
@Override
public void afterConnectionEstablished(WebSocketSession session) {
String sessionKey = Convert.toStr(session.getAttributes().get(webSocketProperties.getCurrentUserKey()));
webSocketSessionDao.add(sessionKey, session);
log.info("WebSocket connect successfully. sessionKey: {}.", sessionKey);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
String sessionKey = Convert.toStr(session.getAttributes().get(webSocketProperties.getCurrentUserKey()));
webSocketSessionDao.delete(sessionKey);
log.info("WebSocket connect closed. sessionKey: {}.", sessionKey);
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) {
log.error("WebSocket transport error. sessionId: {}.", session.getId(), exception);
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.messaging.websocket.core;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
import top.continew.starter.messaging.websocket.autoconfigure.WebSocketProperties;
import top.continew.starter.messaging.websocket.model.CurrentUser;
import java.util.Map;
/**
* WebSocket 拦截器
*
* @author WeiRan
* @author Charles7c
* @since 2.1.0
*/
public class WebSocketInterceptor extends HttpSessionHandshakeInterceptor {
private final WebSocketProperties webSocketProperties;
private final CurrentUserProvider currentUserProvider;
public WebSocketInterceptor(WebSocketProperties webSocketProperties, CurrentUserProvider currentUserProvider) {
this.webSocketProperties = webSocketProperties;
this.currentUserProvider = currentUserProvider;
}
@Override
public boolean beforeHandshake(ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler,
Map<String, Object> attributes) {
CurrentUser currentUser = currentUserProvider.getCurrentUser((ServletServerHttpRequest)request);
attributes.put(webSocketProperties.getCurrentUserKey(), currentUser.getUserId());
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler,
Exception exception) {
super.afterHandshake(request, response, wsHandler, exception);
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.messaging.websocket.dao;
import org.springframework.web.socket.WebSocketSession;
/**
* WebSocket 会话 DAO
*
* @author Charles7c
* @since 2.1.0
*/
public interface WebSocketSessionDao {
/**
* 添加会话
*
* @param key 会话 Key
* @param session 会话信息
*/
void add(String key, WebSocketSession session);
/**
* 删除会话
*
* @param key 会话 Key
*/
void delete(String key);
/**
* 获取会话
*
* @param key 会话 Key
* @return 会话信息
*/
WebSocketSession get(String key);
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.messaging.websocket.dao;
import org.springframework.web.socket.WebSocketSession;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* WebSocket 会话 DAO 默认实现
*
* @author Charles7c
* @since 2.1.0
*/
public class WebSocketSessionDaoDefaultImpl implements WebSocketSessionDao {
private static final Map<String, WebSocketSession> SESSION_MAP = new ConcurrentHashMap<>();
@Override
public void add(String key, WebSocketSession session) {
SESSION_MAP.put(key, session);
}
@Override
public void delete(String key) {
SESSION_MAP.remove(key);
}
@Override
public WebSocketSession get(String key) {
return SESSION_MAP.get(key);
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.messaging.websocket.model;
import java.io.Serial;
import java.io.Serializable;
/**
* 当前登录用户信息
*
* @author Charles7c
* @since 2.1.0
*/
public class CurrentUser implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 用户 ID
*/
private String userId;
/**
* 扩展字段
*/
private Object extend;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public Object getExtend() {
return extend;
}
public void setExtend(Object extend) {
this.extend = extend;
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.messaging.websocket.util;
import cn.hutool.extra.spring.SpringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import top.continew.starter.messaging.websocket.dao.WebSocketSessionDao;
import java.io.IOException;
/**
* WebSocket 工具类
*
* @author WeiRan
* @author Charles7c
* @since 2.1.0
*/
public class WebSocketUtils {
private static final Logger log = LoggerFactory.getLogger(WebSocketUtils.class);
private static final WebSocketSessionDao SESSION_DAO = SpringUtil.getBean(WebSocketSessionDao.class);
private WebSocketUtils() {
}
/**
* 发送消息
*
* @param sessionKey 会话 Key
* @param message 消息内容
*/
public static void sendMessage(String sessionKey, String message) {
WebSocketSession session = SESSION_DAO.get(sessionKey);
sendMessage(session, message);
}
/**
* 发送消息
*
* @param session 会话
* @param message 消息内容
*/
public static void sendMessage(WebSocketSession session, String message) {
sendMessage(session, new TextMessage(message));
}
/**
* 发送消息
*
* @param session 会话
* @param message 消息内容
*/
public static void sendMessage(WebSocketSession session, WebSocketMessage<?> message) {
if (session == null || !session.isOpen()) {
log.warn("WebSocket session closed.");
return;
}
try {
session.sendMessage(message);
} catch (IOException e) {
log.error("WebSocket send message failed. sessionId: {}.", session.getId(), e);
}
}
}

View File

@@ -0,0 +1 @@
top.continew.starter.messaging.websocket.autoconfigure.WebSocketAutoConfiguration

View File

@@ -15,6 +15,7 @@
<modules>
<module>continew-starter-messaging-mail</module>
<module>continew-starter-messaging-websocket</module>
</modules>
<dependencies>

View File

@@ -36,7 +36,7 @@ import top.continew.starter.security.crypto.core.MyBatisEncryptInterceptor;
*/
@AutoConfiguration
@EnableConfigurationProperties(CryptoProperties.class)
@ConditionalOnProperty(prefix = PropertiesConstants.CRYPTO, name = PropertiesConstants.ENABLED, havingValue = "true", matchIfMissing = true)
@ConditionalOnProperty(prefix = PropertiesConstants.CRYPTO, name = PropertiesConstants.ENABLED, matchIfMissing = true)
public class CryptoAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(CryptoAutoConfiguration.class);

View File

@@ -53,7 +53,7 @@ import java.util.Map;
*/
@AutoConfiguration
@EnableConfigurationProperties(PasswordEncoderProperties.class)
@ConditionalOnProperty(prefix = PropertiesConstants.PASSWORD, name = PropertiesConstants.ENABLED, havingValue = "true")
@ConditionalOnProperty(prefix = PropertiesConstants.PASSWORD, name = PropertiesConstants.ENABLED, matchIfMissing = true)
public class PasswordEncoderAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(PasswordEncoderAutoConfiguration.class);

View File

@@ -31,7 +31,7 @@ public class PasswordEncoderProperties {
/**
* 是否启用密码编解码配置
*/
private boolean enabled = false;
private boolean enabled = true;
/**
* 默认启用的编码器 ID默认BCryptPasswordEncoder

View File

@@ -40,7 +40,7 @@ import java.util.Map;
@EnableWebMvc
@AutoConfiguration
@EnableConfigurationProperties(LocalStorageProperties.class)
@ConditionalOnProperty(prefix = PropertiesConstants.STORAGE_LOCAL, name = PropertiesConstants.ENABLED, havingValue = "true")
@ConditionalOnProperty(prefix = PropertiesConstants.STORAGE_LOCAL, name = PropertiesConstants.ENABLED, matchIfMissing = true)
public class LocalStorageAutoConfiguration implements WebMvcConfigurer {
private static final Logger log = LoggerFactory.getLogger(LocalStorageAutoConfiguration.class);

View File

@@ -35,7 +35,7 @@ public class LocalStorageProperties {
/**
* 是否启用本地存储
*/
private boolean enabled = false;
private boolean enabled = true;
/**
* 存储映射

View File

@@ -30,13 +30,6 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
<!-- 移除 websocket 依赖,后续使用 websocket 可考虑由 Netty 提供。另可解决日志警告UT026010: Buffer pool was not set on WebSocketDeploymentInfo, the default pool will be used -->
<exclusions>
<exclusion>
<groupId>io.undertow</groupId>
<artifactId>undertow-websockets-jsr</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Hibernate Validator -->

View File

@@ -33,6 +33,8 @@ import java.util.List;
@ConfigurationProperties(PropertiesConstants.CORS)
public class CorsProperties {
private static final List<String> ALL = Collections.singletonList(StringConstants.ASTERISK);
/**
* 是否启用跨域配置
*/
@@ -58,8 +60,6 @@ public class CorsProperties {
*/
private List<String> exposedHeaders = new ArrayList<>();
private static final List<String> ALL = Collections.singletonList(StringConstants.ASTERISK);
public boolean isEnabled() {
return enabled;
}