first commit.

pull/1/head
ZhouXY108 2022-12-07 18:14:38 +08:00
commit e916d067f3
183 changed files with 9649 additions and 0 deletions

36
.editorconfig 100644
View File

@ -0,0 +1,36 @@
root = true
[*]
charset = utf-8
indent_style = space
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[{*.ng,*.sht,*.html,*.shtm,*.shtml,*.htm}]
indent_size=4
[{*.jhm,*.xslt,*.xul,*.rng,*.xsl,*.xsd,*.ant,*.tld,*.fxml,*.jrxml,*.xml,*.jnlp,*.wsdl}]
indent_size=4
[{.babelrc,.stylelintrc,jest.config,.eslintrc,.prettierrc,*.json,*.jsb3,*.jsb2,*.bowerrc}]
indent_size=2
[*.svg]
indent_size=2
[*.js.map]
indent_size=2
[*.less]
indent_size=2
[*.vue]
indent_size=4
[{.analysis_options,*.yml,*.yaml}]
indent_size=2
[*.java]
indent_size = 4

34
.gitignore vendored 100644
View File

@ -0,0 +1,34 @@
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
*/**/http/*.http
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/

21
README.md 100644
View File

@ -0,0 +1,21 @@
# PlusoneAdmin
后台管理系统 +1
本仓库是后端部分,使用 Spring Boot 2.X 进行开发,对前端提供接口。
简单落地 DDD。
- JDK 17
- 主要使用 Spring JDBC 的 NamedParameterJdbcTemplate 进行数据的查询和持久化,因为感觉有更多的自由度。可以根据 ResultSet任意按照需要的方式实例化 Entity。MyBatis 在某些地方用于查询。
- 数据库使用的是 PostgreSQL
- 权限管理使用的是 Sa-Token使用 Redis 共享 Session。
- 短信服务使用的是腾讯云的 SMS 服务。后续将其解耦,使实现等各方面可替换为阿里云或其它 SMS 服务。
目前项目还没完成,开发中……
相关的文档和介绍完善中……

774
err_code.json 100644
View File

@ -0,0 +1,774 @@
[
{
"code": 2000000,
"description": "正常"
},
{
"code": 4000000,
"description": "用户端错误",
"children": [
{
"code": 4010000,
"description": "用户注册错误",
"children": [
{
"code": 4010100,
"description": "用户未同意隐私协议"
},
{
"code": 4010200,
"description": "注册国家或地区受限"
},
{
"code": 4010300,
"description": "邮箱和手机号应至少绑定一个"
},
{
"code": 4010400,
"description": "用户名已存在"
},
{
"code": 4010500,
"description": "邮箱已存在"
},
{
"code": 4010600,
"description": "手机号已存在"
},
{
"code": 4010700,
"description": "校验码异常",
"children": [
{
"code": 4010701,
"description": "校验码输入错误"
},
{
"code": 4010702,
"description": "校验码不存在或过期"
}
]
},
{
"code": 4010800,
"description": "用户证件异常",
"children": [
{
"code": 4010801,
"description": "用户证件类型未选择"
},
{
"code": 4010802,
"description": "大陆身份证编号校验非法"
},
{
"code": 4010803,
"description": "护照编号校验非法"
},
{
"code": 4010804,
"description": "军官证编号校验非法"
}
]
}
]
},
{
"code": 4020000,
"description": "用户登录异常",
"children": [
{
"code": 4020100,
"description": "用户账户异常",
"children": [
{
"code": 4020101,
"description": "用户账户不存在"
},
{
"code": 4020102,
"description": "用户账户被冻结"
},
{
"code": 4020103,
"description": "用户账户已作废"
}
]
},
{
"code": 4020200,
"description": "用户密码错误",
"children": [
{
"code": 4020201,
"description": "用户输入密码错误次数超限"
}
]
},
{
"code": 4020300,
"description": "用户身份校验失败",
"children": [
{
"code": 4020301,
"description": "用户指纹识别失败"
},
{
"code": 4020302,
"description": "用户面容识别失败"
},
{
"code": 4020303,
"description": "用户未获得第三方登录授权"
}
]
},
{
"code": 4020400,
"description": "用户登录已过期"
},
{
"code": 4020500,
"description": "用户验证码错误",
"children": [
{
"code": 4020501,
"description": "用户验证码输入错误"
},
{
"code": 4020502,
"description": "用户验证码不存在或已过期"
}
]
}
]
},
{
"code": 4030000,
"description": "访问权限异常",
"children": [
{
"code": 4030100,
"description": "访问未授权",
"children": [
{
"code": 4030101,
"description": "因访问对象隐私设置被拦截"
},
{
"code": 4030102,
"description": "授权已过期"
},
{
"code": 4030103,
"description": "无权限使用 API"
}
]
},
{
"code": 4030200,
"description": "用户访问被拦截",
"children": [
{
"code": 4030201,
"description": "黑名单用户"
},
{
"code": 4030202,
"description": "账号被冻结"
},
{
"code": 4030203,
"description": "非法 IP 地址"
},
{
"code": 4030204,
"description": "网关访问受限"
},
{
"code": 4030205,
"description": "地域黑名单"
}
]
},
{
"code": 4030300,
"description": "服务已欠费"
},
{
"code": 4030400,
"description": "用户签名异常",
"children": [
{
"code": 4030401,
"description": "RSA 签名错误"
}
]
}
]
},
{
"code": 4040000,
"description": "用户请求参数错误",
"children": [
{
"code": 4040100,
"description": "包含非法恶意跳转链接"
},
{
"code": 4040200,
"description": "无效的用户输入",
"children": [
{
"code": 4040201,
"description": "不支持的 PrincipalType"
}
]
},
{
"code": 4040300,
"description": "请求必填参数为空",
"children": [
{
"code": 4040301,
"description": "用户订单号为空"
},
{
"code": 4040302,
"description": "订购数量为空"
},
{
"code": 4040303,
"description": "缺少时间戳参数"
},
{
"code": 4040304,
"description": "非法的时间戳参数"
}
]
},
{
"code": 4040400,
"description": "请求参数值超出允许的范围",
"children": [
{
"code": 4040401,
"description": "参数格式不匹配"
},
{
"code": 4040402,
"description": "地址不在服务范围"
},
{
"code": 4040403,
"description": "时间不在服务范围"
},
{
"code": 4040404,
"description": "金额超出限制"
},
{
"code": 4040405,
"description": "数量超出限制"
},
{
"code": 4040406,
"description": "请求批量处理总个数超出限制"
},
{
"code": 4040407,
"description": "请求 JSON 解析失败"
}
]
},
{
"code": 4040500,
"description": "用户输入内容非法",
"children": [
{
"code": 4040501,
"description": "包含违禁敏感词"
},
{
"code": 4040502,
"description": "图片包含违禁信息"
},
{
"code": 4040503,
"description": "文件侵犯版权"
}
]
},
{
"code": 4040600,
"description": "用户操作异常",
"children": [
{
"code": 4040601,
"description": "用户支付超时"
},
{
"code": 4040602,
"description": "确认订单超时"
},
{
"code": 4040603,
"description": "订单已关闭"
}
]
}
]
},
{
"code": 4050000,
"description": "用户请求服务异常",
"children": [
{
"code": 4050100,
"description": "请求次数超出限制"
},
{
"code": 4050200,
"description": "请求并发数超出限制"
},
{
"code": 4050300,
"description": "用户操作请等待"
},
{
"code": 4050400,
"description": "WebSocket 连接异常"
},
{
"code": 4050500,
"description": "WebSocket 连接断开"
},
{
"code": 4050600,
"description": "用户重复请求"
}
]
},
{
"code": 4060000,
"description": "用户资源异常",
"children": [
{
"code": 4060100,
"description": "账户余额不足"
},
{
"code": 4060200,
"description": "用户磁盘空间不足"
},
{
"code": 4060300,
"description": "用户内存空间不足"
},
{
"code": 4060400,
"description": "用户 OSS 容量不足"
},
{
"code": 4060500,
"description": "用户配额已用光"
}
]
},
{
"code": 4070000,
"description": "用户上传文件异常",
"children": [
{
"code": 4070100,
"description": "用户上传文件类型不匹配"
},
{
"code": 4070200,
"description": "用户上传文件太大"
},
{
"code": 4070300,
"description": "用户上传图片太大"
},
{
"code": 4070400,
"description": "用户上传视频太大"
},
{
"code": 4070500,
"description": "用户上传压缩文件太大"
}
]
},
{
"code": 4080000,
"description": "用户当前版本异常",
"children": [
{
"code": 4080100,
"description": "用户安装版本与系统不匹配",
"children": [
{
"code": 4080101,
"description": "用户安装版本过低"
},
{
"code": 4080102,
"description": "用户安装版本过高"
},
{
"code": 4080103,
"description": "用户安装版本已过期"
}
]
},
{
"code": 4080200,
"description": "用户 API 请求版本不匹配",
"children": [
{
"code": 4080201,
"description": "用户 API 请求版本过高"
},
{
"code": 4080202,
"description": "用户 API 请求版本过低"
}
]
}
]
},
{
"code": 4090000,
"description": "用户隐私未授权",
"children": [
{
"code": 4090100,
"description": "用户隐私未签署"
},
{
"code": 4090200,
"description": "用户摄像头未授权"
},
{
"code": 4090300,
"description": "用户相机未授权"
},
{
"code": 4090400,
"description": "用户图片库未授权"
},
{
"code": 4090500,
"description": "用户文件未授权"
},
{
"code": 4090600,
"description": "用户位置信息未授权"
},
{
"code": 4090700,
"description": "用户通讯录未授权"
}
]
},
{
"code": 4100000,
"description": "用户设备异常",
"children": [
{
"code": 4100100,
"description": "用户相机异常"
},
{
"code": 4100200,
"description": "用户麦克风异常"
},
{
"code": 4100300,
"description": "用户听筒异常"
},
{
"code": 4100400,
"description": "用户扬声器异常"
},
{
"code": 4100500,
"description": "用户 GPS 定位异常"
}
]
},
{
"code": 4110000,
"description": "数据资源异常",
"children": [
{
"code": 4110100,
"description": "数据资源不存在"
},
{
"code": 4110200,
"description": "数据操作的行数不符合预期"
}
]
}
]
},
{
"code": 5000000,
"description": "系统执行出错",
"children": [
{
"code": 5010000,
"description": "系统执行超时",
"children": [
{
"code": 5010100,
"description": "系统订单处理超时"
}
]
},
{
"code": 5020000,
"description": "系统容灾功能被触发",
"children": [
{
"code": 5020100,
"description": "系统限流"
},
{
"code": 5020200,
"description": "系统功能降级"
}
]
},
{
"code": 5030000,
"description": "系统资源异常",
"children": [
{
"code": 5030100,
"description": "系统资源耗尽",
"children": [
{
"code": 5030101,
"description": "系统磁盘空间耗尽"
},
{
"code": 5030102,
"description": "系统内存耗尽"
},
{
"code": 5030103,
"description": "文件句柄耗尽"
},
{
"code": 5030104,
"description": "系统连接池耗尽"
},
{
"code": 5030105,
"description": "系统线程池耗尽"
}
]
},
{
"code": 5030200,
"description": "系统资源访问异常",
"children": [
{
"code": 5030201,
"description": "系统读取磁盘文件失败"
}
]
}
]
}
]
},
{
"code": 6000000,
"description": "调用第三方服务出错",
"children": [
{
"code": 6010000,
"description": "中间件服务出错",
"children": [
{
"code": 6010100,
"description": "RPC 服务出错",
"children": [
{
"code": 6010101,
"description": "RPC 服务未找到"
},
{
"code": 6010102,
"description": "RPC 服务未注册"
},
{
"code": 6010103,
"description": "接口不存在"
}
]
},
{
"code": 6010200,
"description": "消息服务出错",
"children": [
{
"code": 6010201,
"description": "消息投递出错"
},
{
"code": 6010202,
"description": "消息消费出错"
},
{
"code": 6010203,
"description": "消息订阅出错"
},
{
"code": 6010204,
"description": "消息分组未查到"
}
]
},
{
"code": 6010300,
"description": "缓存服务出错",
"children": [
{
"code": 6010301,
"description": "key 长度超过限制"
},
{
"code": 6010302,
"description": "value 长度超过限制"
},
{
"code": 6010303,
"description": "存储容量已满"
},
{
"code": 6010304,
"description": "不支持的数据格式"
}
]
},
{
"code": 6010400,
"description": "配置服务出错"
},
{
"code": 6010500,
"description": "网络资源服务出错",
"children": [
{
"code": 6010501,
"description": "VPN 服务出错"
},
{
"code": 6010502,
"description": "CDN 服务出错"
},
{
"code": 6010503,
"description": "域名解析服务出错"
},
{
"code": 6010504,
"description": "网关服务出错"
}
]
}
]
},
{
"code": 6020000,
"description": "第三方系统执行超时",
"children": [
{
"code": 6020100,
"description": "RPC 执行超时"
},
{
"code": 6020200,
"description": "消息投递超时"
},
{
"code": 6020300,
"description": "缓存服务超时"
},
{
"code": 6020400,
"description": "配置服务超时"
},
{
"code": 6020500,
"description": "数据库服务超时"
}
]
},
{
"code": 6030000,
"description": "数据库服务出错",
"children": [
{
"code": 6030100,
"description": "表不存在"
},
{
"code": 6030200,
"description": "列不存在"
},
{
"code": 6030300,
"description": "多表关联中存在多个相同名称的列"
},
{
"code": 6030400,
"description": "数据库死锁"
},
{
"code": 6030500,
"description": "主键冲突"
}
]
},
{
"code": 6040000,
"description": "第三方容灾系统被触发",
"children": [
{
"code": 6040100,
"description": "第三方系统限流"
},
{
"code": 6040200,
"description": "第三方功能降级"
}
]
},
{
"code": 6050000,
"description": "通知服务出错",
"children": [
{
"code": 6050100,
"description": "短信提醒服务失败"
},
{
"code": 6050200,
"description": "语音提醒服务失败"
},
{
"code": 6050300,
"description": "邮件提醒服务失败"
}
]
}
]
}
]

View File

@ -0,0 +1,34 @@
<?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>
<artifactId>plusone-basic</artifactId>
<groupId>xyz.zhouxy</groupId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>plusone-basic-application</artifactId>
<dependencies>
<dependency>
<groupId>xyz.zhouxy</groupId>
<artifactId>plusone-basic-common</artifactId>
</dependency>
<dependency>
<groupId>xyz.zhouxy</groupId>
<artifactId>plusone-basic-domain</artifactId>
</dependency>
<dependency>
<groupId>xyz.zhouxy</groupId>
<artifactId>plusone-basic-infrastructure</artifactId>
</dependency>
<!-- 防止XSS攻击的过滤插件 -->
<dependency>
<groupId>net.dreamlu</groupId>
<artifactId>mica-xss</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,17 @@
package xyz.zhouxy.plusone.exception.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import xyz.zhouxy.plusone.exception.handler.BaseExceptionHandler.ExceptionInfoHolder;
@Configuration
public class PlusoneExceptionHandlerConfig {
@Bean
@ConditionalOnMissingBean
ExceptionInfoHolder exceptionInfoHolder() {
return new ExceptionInfoHolder();
}
}

View File

@ -0,0 +1,29 @@
package xyz.zhouxy.plusone.exception.handler;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import lombok.extern.slf4j.Slf4j;
import xyz.zhouxy.plusone.util.RestfulResult;
/**
*
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@ConditionalOnProperty(prefix = "plusone.exception", name = "handle-all-exception", havingValue = "true")
@RestControllerAdvice
@Slf4j
public class AllExceptionHandler extends BaseExceptionHandler {
protected AllExceptionHandler(ExceptionInfoHolder exceptionInfoHolder) {
super(exceptionInfoHolder);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<RestfulResult> handleException(Throwable e) {
log.error(e.getMessage(), e);
return this.buildExceptionResponse(e);
}
}

View File

@ -0,0 +1,63 @@
package xyz.zhouxy.plusone.exception.handler;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import lombok.extern.slf4j.Slf4j;
import xyz.zhouxy.plusone.util.RestfulResult;
/**
*
*
* <p>
* {@link IllegalArgumentException}
* </p>
*
* <p>
* SQL
* </p>
*
* <p>
* {@link AllExceptionHandler}
* {@code Order} {@value Ordered.LOWEST_PRECEDENCE - 1}
* Spring Bean Order Ordered.LOWEST_PRECEDENCE
*
* {@link Order} order {@code Ordered.LOWEST_PRECEDENCE - 1}
*
* 0 1
* </p>
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@RestControllerAdvice
@Order(Ordered.LOWEST_PRECEDENCE - 1)
@Slf4j
public class DefaultExceptionHandler extends BaseExceptionHandler {
public DefaultExceptionHandler(ExceptionInfoHolder exceptionInfoHolder) {
super(exceptionInfoHolder);
set(IllegalArgumentException.class, 4010000, "格式错误", HttpStatus.FORBIDDEN);
set(DataAccessException.class, 6030000, "数据库错误", HttpStatus.INTERNAL_SERVER_ERROR, true);
set(MethodArgumentNotValidException.class,
4040401,
e -> ((MethodArgumentNotValidException) e).getAllErrors()
.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.toList()
.toString(),
HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<RestfulResult> handleException(Exception e) {
log.error(e.getMessage(), e);
return buildExceptionResponse(e);
}
}

View File

@ -0,0 +1,46 @@
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>plusone-basic</artifactId>
<groupId>xyz.zhouxy</groupId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>plusone-basic-common</artifactId>
<dependencies>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-core</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
<dependency>
<groupId>xyz.zhouxy.plusone</groupId>
<artifactId>plusone-commons</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>xyz.zhouxy.plusone</groupId>
<artifactId>plusone-exception-handler</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,31 @@
package xyz.zhouxy.plusone.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
/**
*
*
* <p>
*
* </p>
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @see xyz.zhouxy.plusone.util.AssertResult
*/
@ResponseStatus(HttpStatus.NOT_FOUND)
public class DataNotExistException extends PlusoneException {
@java.io.Serial
private static final long serialVersionUID = 6536955800679703111L;
public static final int ERROR_CODE = 4110100;
public DataNotExistException() {
super(ERROR_CODE, "数据不存在");
}
public DataNotExistException(String message) {
super(ERROR_CODE, message);
}
}

View File

@ -0,0 +1,31 @@
package xyz.zhouxy.plusone.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
/**
*
*
* <p>
*
* </p>
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @see xyz.zhouxy.plusone.util.AssertResult
*/
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public class DataOperationNumberException extends PlusoneException {
@java.io.Serial
private static final long serialVersionUID = -9220765735990318186L;
public static final int ERROR_CODE = 4110200;
public DataOperationNumberException() {
super(ERROR_CODE, "数据操作的行数不符合预期");
}
public DataOperationNumberException(String message) {
super(ERROR_CODE, message);
}
}

View File

@ -0,0 +1,56 @@
package xyz.zhouxy.plusone.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
/**
* 4040200 -
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class InvalidInputException extends PlusoneException {
@java.io.Serial
private static final long serialVersionUID = 7956661913360059670L;
public static final int ERROR_CODE = 4040200;
private InvalidInputException(int code, String msg) {
super(code, msg);
}
private InvalidInputException(int code, Throwable cause) {
super(code, cause);
}
private InvalidInputException(int code, String msg, Throwable cause) {
super(code, msg, cause);
}
public InvalidInputException(String msg) {
this(ERROR_CODE, msg);
}
public InvalidInputException(Throwable cause) {
this(ERROR_CODE, cause);
}
public InvalidInputException(String msg, Throwable cause) {
this(ERROR_CODE, msg, cause);
}
/**
* Principal
*/
public static InvalidInputException unsupportedPrincipalTypeException() {
return unsupportedPrincipalTypeException("不支持的 PrincipalType");
}
/**
* Principal
*/
public static InvalidInputException unsupportedPrincipalTypeException(String message) {
return new InvalidInputException(4040201, message);
}
}

View File

@ -0,0 +1,58 @@
package xyz.zhouxy.plusone.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
/**
* 4040600 -
*
* @author ZhouXY
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class UserOperationException extends PlusoneException {
@java.io.Serial
private static final long serialVersionUID = 4371055414421991940L;
public static final int DEFAULT_ERROR_CODE = 4040600;
public UserOperationException(String msg) {
super(DEFAULT_ERROR_CODE, msg);
}
public UserOperationException(String msg, Throwable cause) {
super(DEFAULT_ERROR_CODE, msg, cause);
}
public UserOperationException(int code, String msg) {
super(code, msg);
}
public UserOperationException(int code, Throwable cause) {
super(code, cause);
}
public UserOperationException(int code, String msg, Throwable cause) {
super(code, msg, cause);
}
/**
*
*
* @return
*/
public static UserOperationException invalidOperation() {
return invalidOperation("无效的操作");
}
/**
*
*
* @param msg
* @return
*/
public static UserOperationException invalidOperation(String msg) {
return new UserOperationException(4040604, msg);
}
}

View File

@ -0,0 +1,67 @@
package xyz.zhouxy.plusone.util;
import xyz.zhouxy.plusone.exception.DataNotExistException;
import xyz.zhouxy.plusone.exception.DataOperationNumberException;
import java.util.Objects;
/**
*
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public final class AssertResult {
private AssertResult() {
throw new IllegalStateException("Utility class");
}
public static void update(boolean expression) {
if (!expression) {
throw new DataOperationNumberException();
}
}
public static void update(boolean expression, String message) {
if (!expression) {
throw new DataOperationNumberException(message);
}
}
public static void update(Object i, int expectedValue) {
if (!Objects.equals(i, expectedValue)) {
throw new DataOperationNumberException();
}
}
public static void update(Object i, int expectedValue, String format) {
if (!Objects.equals(i, expectedValue)) {
throw new DataOperationNumberException(String.format(format, i));
}
}
public static void exist(boolean expression) {
if (!expression) {
throw new DataNotExistException();
}
}
public static void exist(boolean expression, String message) {
if (!expression) {
throw new DataNotExistException(message);
}
}
public static void nonNull(Object obj) {
if (Objects.isNull(obj)) {
throw new DataNotExistException();
}
}
public static void nonNull(Object obj, String message) {
if (Objects.isNull(obj)) {
throw new DataNotExistException(message);
}
}
}

View File

@ -0,0 +1,43 @@
package xyz.zhouxy.plusone.util;
import java.util.Objects;
import java.util.regex.Pattern;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
*
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public class PlusoneStrUtil {
private PlusoneStrUtil() {
throw new IllegalStateException("Utility class");
}
public static final String EMPTY_STR = "";
public static String getTextOrEmpty(String value) {
return StringUtils.hasText(value) ? value : EMPTY_STR;
}
public static String getStrOrEmpty(String value) {
return StringUtils.hasLength(value) ? value : EMPTY_STR;
}
public static String checkString(String value, String regex, String message) {
Assert.notNull(value, message);
Assert.isTrue(Pattern.matches(regex, value), message);
return value;
}
public static String checkStringNullable(String value, String regex, String message) {
return Objects.nonNull(value) ? checkString(value, regex, message) : null;
}
public static String checkStringOrDefault(String value, String regex, String message) {
return StringUtils.hasText(value) ? checkString(value, regex, message) : "";
}
}

View File

@ -0,0 +1,39 @@
<?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>xyz.zhouxy</groupId>
<artifactId>plusone-basic</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>plusone-basic-domain</artifactId>
<dependencies>
<dependency>
<artifactId>plusone-basic-common</artifactId>
<groupId>xyz.zhouxy</groupId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.13.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.13.4</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,32 @@
package xyz.zhouxy.plusone.constant;
import xyz.zhouxy.plusone.util.Enumeration;
import xyz.zhouxy.plusone.util.EnumerationValuesHolder;
/**
*
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public class EntityStatus extends Enumeration<EntityStatus> {
private EntityStatus(int value, String name) {
super(value, name);
}
// 常量
public static final EntityStatus AVAILABLE = new EntityStatus(0, "正常");
public static final EntityStatus DISABLED = new EntityStatus(1, "禁用");
private static final EnumerationValuesHolder<EntityStatus> ENUMERATION_VALUES = new EnumerationValuesHolder<>(
new EntityStatus[] { AVAILABLE, DISABLED });
public static EntityStatus of(int value) {
return ENUMERATION_VALUES.get(value);
}
@Override
public String toString() {
return "EntityStatus" + super.toString();
}
}

View File

@ -0,0 +1,27 @@
package xyz.zhouxy.plusone.constant;
/**
*
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public final class RegexConsts {
public static final String DATE = "^\\d{4}-\\d{2}-\\d{2}";
public static final String PASSWORD = "^(?=.*\\d)(?=.*[a-z])(?=.*[A-Z])[\\w\\\\!#$%&'*\\+\\-/=?^`{|}~@\\(\\)\\[\\]\",\\.;':><]{8,32}$";
public static final String CAPTCHA = "^[0-9A-Za-z]{4,6}$";
public static final String EMAIL = "^\\w+([-+.]\\w+)*@[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})*(\\.(?![0-9]+$)[a-zA-Z0-9][-0-9A-Za-z]{0,62})$";
public static final String MOBILE_PHONE = "^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}$";
public static final String USERNAME = "^[\\da-zA-Z_.@\\\\]{4,36}$";
public static final String NICKNAME = "^[\\da-zA-Z_.@\\\\]{4,36}$";
private RegexConsts() {
throw new IllegalStateException("Utility class");
}
}

View File

@ -0,0 +1,17 @@
package xyz.zhouxy.plusone.domain;
import java.io.Serializable;
/**
*
*
* <p>
* Repository
* </p>
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @see Entity
* @see IRepository
*/
public abstract class AggregateRoot<ID extends Serializable> extends Entity<ID> {
}

View File

@ -0,0 +1,26 @@
package xyz.zhouxy.plusone.domain;
import cn.hutool.core.lang.UUID;
import lombok.Getter;
/**
*
*
* <p>
* <b>使 {@link java.io.Serializable} </b>
* </p>
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @see Entity
* @see IEventHandler
*/
@Getter
public abstract class DomainEvent {
private String identifier;
private long happenedAt;
protected DomainEvent() {
this.identifier = UUID.randomUUID().toString(true);
this.happenedAt = System.currentTimeMillis();
}
}

View File

@ -0,0 +1,32 @@
package xyz.zhouxy.plusone.domain;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
*
*
* <p>
* DDD ID
* </p>
*
* <p>
* {@link DomainEvent}
* </p>
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @see IValueObject
* @see AggregateRoot
*/
public abstract class Entity<ID extends Serializable> {
private final List<DomainEvent> domainEvents = new ArrayList<>();
public abstract Optional<ID> getId();
protected void addDomainEvent(DomainEvent domainEvent) {
domainEvents.add(domainEvent);
}
}

View File

@ -0,0 +1,10 @@
package xyz.zhouxy.plusone.domain;
/**
*
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @see ICommandHandler
*/
public interface ICommand {
}

View File

@ -0,0 +1,12 @@
package xyz.zhouxy.plusone.domain;
/**
*
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @see ICommand
*/
@FunctionalInterface
public interface ICommandHandler<T extends ICommand> {
void handle(T command);
}

View File

@ -0,0 +1,12 @@
package xyz.zhouxy.plusone.domain;
/**
*
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @see DomainEvent
*/
@FunctionalInterface
public interface IEventHandler<E extends DomainEvent> {
void handle(E command);
}

View File

@ -0,0 +1,21 @@
package xyz.zhouxy.plusone.domain;
import java.io.Serializable;
/**
* Repository
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @see AggregateRoot
*/
public interface IRepository<T extends AggregateRoot<ID>, ID extends Serializable> {
T find(ID id);
T save(T entity);
void delete(T entity);
boolean exists(ID id);
}

View File

@ -0,0 +1,9 @@
package xyz.zhouxy.plusone.domain;
/**
*
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public interface IValueObject {
}

View File

@ -0,0 +1,10 @@
package xyz.zhouxy.plusone.domain;
/**
* label
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public interface IWithLabel {
String getLabel();
}

View File

@ -0,0 +1,33 @@
package xyz.zhouxy.plusone.domain;
import java.util.Comparator;
import java.util.Objects;
/**
* orderNumber
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public interface IWithOrderNumber extends Comparable<IWithOrderNumber> {
int getOrderNumber();
@Override
default int compareTo(IWithOrderNumber that) {
return new OrderNumberComparator<IWithOrderNumber>().compare(this, that);
}
class OrderNumberComparator<T extends Comparable<T>>
implements Comparator<IWithOrderNumber> {
@Override
public int compare(IWithOrderNumber a, IWithOrderNumber b) {
if (Objects.equals(a, b)) {
return 0;
}
if (a == null) {
return -1;
}
return (b == null) ? 1 : Integer.compare(a.getOrderNumber(), b.getOrderNumber());
}
}
}

View File

@ -0,0 +1,10 @@
package xyz.zhouxy.plusone.domain;
/**
*
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public interface IWithVersion {
long getVersion();
}

View File

@ -0,0 +1,34 @@
package xyz.zhouxy.plusone.domain;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonValue;
/**
*
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public abstract class ValidatableStringRecord implements IValueObject {
protected String value;
protected final String format;
protected ValidatableStringRecord(String format) {
this.format = format;
}
@JsonIgnore
protected boolean isValid() {
return value.matches(format);
}
@JsonValue
public String value() {
return value;
}
@Override
public String toString() {
return value;
}
}

View File

@ -0,0 +1,36 @@
package xyz.zhouxy.plusone.util;
import java.util.regex.Pattern;
public class RegexUtil {
private RegexUtil() {
throw new IllegalStateException("Utility class");
}
public static boolean matches(CharSequence input, String regex) {
return Pattern.matches(regex, input);
}
public static boolean matchesOr(CharSequence input, String... regexs) {
boolean isMatched;
for (var regex : regexs) {
isMatched = Pattern.matches(regex, input);
if (isMatched) {
return true;
}
}
return false;
}
public static boolean matchesAnd(CharSequence input, String... regexs) {
boolean isMatched;
for (var regex : regexs) {
isMatched = Pattern.matches(regex, input);
if (!isMatched) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,95 @@
<?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>xyz.zhouxy</groupId>
<artifactId>plusone-basic</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>plusone-basic-infrastructure</artifactId>
<dependencies>
<dependency>
<groupId>xyz.zhouxy</groupId>
<artifactId>plusone-basic-common</artifactId>
</dependency>
<dependency>
<groupId>xyz.zhouxy</groupId>
<artifactId>plusone-basic-domain</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java-sms</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,14 @@
package xyz.zhouxy.plusone.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
/**
* Plusone plusone.properties
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Configuration
@PropertySource(value = {"classpath:conf/plusone.properties"}, encoding = "utf-8")
public class PlusoneConfig {
}

View File

@ -0,0 +1,41 @@
package xyz.zhouxy.plusone.infrastructure.pojo;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/**
*
*
* <p>
* <i> Repository
* Entity使 PO</i>
* </p>
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Getter
@Setter
@ToString
public abstract class AbstractEntityPO {
protected Long createdBy;
protected LocalDateTime createTime;
protected Long updatedBy;
protected LocalDateTime updateTime;
public abstract Long getId();
public void auditFields(Long updatedBy, LocalDateTime updateTime) {
this.updatedBy = updatedBy;
this.updateTime = updateTime;
}
public void auditFields(Long createdBy, LocalDateTime createTime, Long updatedBy, LocalDateTime updateTime) {
this.createdBy = createdBy;
this.createTime = createTime;
this.updatedBy = updatedBy;
this.updateTime = updateTime;
}
}

View File

@ -0,0 +1,33 @@
package xyz.zhouxy.plusone.jdbc;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.lang.Nullable;
/**
* {@link BeanPropertySqlParameterSource} POJO
* {@link org.springframework.jdbc.core.namedparam.SqlParameterSource}
* 使 {@link Enum#ordinal()}
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*
* @see SqlParameterSource
* @see BeanPropertySqlParameterSource
* @see org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
* @see Enum
*/
public class BeanPropertyParamSource extends BeanPropertySqlParameterSource {
public BeanPropertyParamSource(Object object) {
super(object);
}
@Override
@Nullable
public Object getValue(String paramName) throws IllegalArgumentException {
Object value = super.getValue(paramName);
if (value instanceof Enum) {
return ((Enum<?>) value).ordinal();
}
return value;
}
}

View File

@ -0,0 +1,51 @@
package xyz.zhouxy.plusone.jdbc;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jdbc.core.convert.JdbcCustomConversions;
import org.springframework.data.jdbc.repository.QueryMappingConfiguration;
import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration;
import org.springframework.data.jdbc.repository.config.DefaultQueryMappingConfiguration;
import xyz.zhouxy.plusone.jdbc.converter.EnumToOrdinalConverter;
/**
* JDBC
*
* <p>
*
* </p>
*
* <p>
* {@link DefaultQueryMappingConfiguration} Spring bean
* bean
* {@link org.springframework.jdbc.core.RowMapper}
* Spring Data JDBC 使
*
* <pre>
* {@code @Configuration}
* class CustomConfig {
* public CustomConfig(DefaultQueryMappingConfiguration rowMappers) {
* rowMappers.registerRowMapper(Person.class, new PersonRowMapper());
* }
* }
* </pre>
* </p>
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Configuration
public class JdbcConfig extends AbstractJdbcConfiguration {
@Override
public JdbcCustomConversions jdbcCustomConversions() {
return new JdbcCustomConversions(List.of(EnumToOrdinalConverter.INSTANCE));
}
@Bean
QueryMappingConfiguration rowMappers() {
return new DefaultQueryMappingConfiguration();
}
}

View File

@ -0,0 +1,68 @@
package xyz.zhouxy.plusone.jdbc;
import java.io.Serializable;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.stream.Stream;
import javax.annotation.Nonnull;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import xyz.zhouxy.plusone.domain.Entity;
public abstract class JdbcEntityDaoSupport<T extends Entity<ID>, ID extends Serializable> {
protected final NamedParameterJdbcTemplate jdbc;
protected RowMapper<T> rowMapper;
protected ResultSetExtractor<T> resultSetExtractor;
protected JdbcEntityDaoSupport(@Nonnull NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
this.jdbc = namedParameterJdbcTemplate;
this.rowMapper = (ResultSet rs, int rowNum) -> mapRow(rs);
this.resultSetExtractor = (ResultSet rs) -> rs.next() ? mapRow(rs) : null;
}
protected final T queryForObject(String sql) {
return this.jdbc.query(sql, this.resultSetExtractor);
}
protected final T queryForObject(String sql, SqlParameterSource paramSource) {
return this.jdbc.query(sql, paramSource, this.resultSetExtractor);
}
protected final List<T> queryForList(String sql) {
return this.jdbc.query(sql, this.rowMapper);
}
protected final List<T> queryForList(String sql, SqlParameterSource parameterSource) {
return this.jdbc.query(sql, parameterSource, this.rowMapper);
}
protected final Stream<T> queryForStream(String sql, SqlParameterSource parameterSource) {
return this.jdbc.queryForStream(sql, parameterSource, this.rowMapper);
}
protected final <E> Stream<E> queryForStream(String sql, SqlParameterSource parameterSource, Class<E> elementType) {
return this.jdbc.queryForList(sql, parameterSource, elementType).stream();
}
protected final boolean queryExists(String sql, SqlParameterSource parameterSource) {
Boolean isExists = this.jdbc.query(sql, parameterSource, ResultSet::next);
return Boolean.TRUE.equals(isExists);
}
protected abstract T mapRow(ResultSet rs) throws SQLException;
protected void setRowMapper(@Nonnull RowMapper<T> rowMapper) {
this.rowMapper = rowMapper;
}
protected void setResultSetExtractor(@Nonnull ResultSetExtractor<T> resultSetExtractor) {
this.resultSetExtractor = resultSetExtractor;
}
}

View File

@ -0,0 +1,30 @@
package xyz.zhouxy.plusone.jdbc;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import xyz.zhouxy.plusone.spring.SpringContextHolder;
/**
* Spring
* Spring {@link JdbcTemplate}
* {@link NamedParameterJdbcTemplate}
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @see JdbcTemplate
* @see NamedParameterJdbcTemplate
*/
public final class JdbcFactory {
private JdbcFactory() {
throw new IllegalStateException("Utility class");
}
public static JdbcTemplate getJdbcTemplate() {
return SpringContextHolder.getContext().getBean(JdbcTemplate.class);
}
public static NamedParameterJdbcTemplate getNamedParameterJdbcTemplate() {
return SpringContextHolder.getContext().getBean(NamedParameterJdbcTemplate.class);
}
}

View File

@ -0,0 +1,58 @@
package xyz.zhouxy.plusone.jdbc;
import java.io.Serializable;
import javax.annotation.Nonnull;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import xyz.zhouxy.plusone.domain.AggregateRoot;
import xyz.zhouxy.plusone.domain.IRepository;
public abstract class JdbcRepositorySupport<T extends AggregateRoot<ID>, ID extends Serializable>
extends JdbcEntityDaoSupport<T, ID>
implements IRepository<T, ID> {
protected JdbcRepositorySupport(@Nonnull NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
super(namedParameterJdbcTemplate);
}
protected abstract void doDelete(@Nonnull T entity);
protected abstract T doFindById(@Nonnull ID id);
protected abstract T doInsert(@Nonnull T entity);
protected abstract T doUpdate(@Nonnull T entity);
@Override
public final void delete(T entity) {
if (entity == null) {
throw new IllegalArgumentException("Cannot delete null.");
}
doDelete(entity);
}
@Override
public final T find(ID id) {
if (id == null) {
throw new IllegalArgumentException("Id cannot be null.");
}
return doFindById(id);
}
@Override
public final T save(T entity) {
if (entity == null) {
throw new IllegalArgumentException("Cannot save null.");
}
return entity.getId().isPresent() ? doUpdate(entity) : doInsert(entity);
}
protected abstract SqlParameterSource generateParamSource(ID id, @Nonnull T entity);
protected final SqlParameterSource generateParamSource(@Nonnull T entity) {
return generateParamSource(entity.getId().orElseThrow(), entity);
}
}

View File

@ -0,0 +1,45 @@
package xyz.zhouxy.plusone.jdbc.common;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
*
*
* <p>
* {@link #map(ResultSet)} {@link ResultSet}
* {@link #rowMapper(ResultSet, int)}
* {@link org.springframework.jdbc.core.RowMapper}
* {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate}
*
* Spring Data JDBC 使
* </p>
*
* <p>
*
* {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate}
* query(String, java.util.Map, ResultSetExtractor)
* query(String, SqlParameterSource, ResultSetExtractor)
* ResultSetExtractor {@link ResultSet#next()}
*
* {@link #resultSetExtractor(ResultSet)}
* {@link #rowMapper(ResultSet, int)}
* {@link org.springframework.jdbc.core.ResultSetExtractor}
* </p>
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @see org.springframework.data.jdbc.repository.config.DefaultQueryMappingConfiguration
*/
@FunctionalInterface
public interface ResultMapper<T> {
T map(ResultSet resultSet) throws SQLException;
default T rowMapper(ResultSet resultSet, int rowNum) throws SQLException {
return map(resultSet);
}
default T resultSetExtractor(ResultSet resultSet) throws SQLException {
return resultSet.next() ? map(resultSet) : null;
}
}

View File

@ -0,0 +1,30 @@
package xyz.zhouxy.plusone.jdbc.common;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.RowMapper;
public class SimpleResultMapper<T> implements ResultMapper<T> {
private final RowMapper<T> rowMapper;
public SimpleResultMapper(RowMapper<T> rowMapper) {
this.rowMapper = rowMapper;
}
@Override
public T map(ResultSet resultSet) throws SQLException {
return rowMapper.mapRow(resultSet, 1);
}
@Override
public T rowMapper(ResultSet resultSet, int rowNum) throws SQLException {
return this.rowMapper.mapRow(resultSet, rowNum);
}
public static <T> SimpleResultMapper<T> of(Class<T> clazz) {
return new SimpleResultMapper<>(new BeanPropertyRowMapper<>(clazz));
}
}

View File

@ -0,0 +1,20 @@
package xyz.zhouxy.plusone.jdbc.converter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.WritingConverter;
/**
*
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@WritingConverter
public enum EnumToOrdinalConverter implements Converter<Enum<?>, Integer> {
INSTANCE
;
@Override
public Integer convert(Enum<?> source) {
return source.ordinal();
}
}

View File

@ -0,0 +1,44 @@
package xyz.zhouxy.plusone.jdbc.converter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.ReadingConverter;
/**
*
* <p>
* {@link org.springframework.data.jdbc.core.convert.JdbcCustomConversions}
*
*
*
* <pre>
* return new JdbcCustomConversions(List.of(new OrdinalToEnumConverter<SystemStatus>()));
* </pre>
* </p>
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*
* @see Converter
* @see org.springframework.data.jdbc.core.convert.JdbcCustomConversions
*/
@ReadingConverter
public class OrdinalToEnumConverter<E extends Enum<E>> implements Converter<Integer, Enum<E>> {
private final Class<E> type;
private final E[] constants;
public OrdinalToEnumConverter(Class<E> type) {
this.type = type;
this.constants = type.getEnumConstants();
}
@Override
public Enum<E> convert(Integer ordinal) {
try {
return constants[ordinal];
} catch (ArrayIndexOutOfBoundsException exception) {
throw new IllegalArgumentException(
String.format("Cannot convert %d to %s by ordinal value.", ordinal, type.getSimpleName()),
exception);
}
}
}

View File

@ -0,0 +1,31 @@
package xyz.zhouxy.plusone.mail;
import org.springframework.mail.SimpleMailMessage;
/**
* {@link SimpleMailMessage}
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public class MailMessageFactory {
private final PlusoneMailProperties mailProperties;
public MailMessageFactory(PlusoneMailProperties mailProperties) {
this.mailProperties = mailProperties;
}
public SimpleMailMessage getCodeMailMessage(String code, String to) {
String subject = mailProperties.getSubject().get("code");
String content = String.format(mailProperties.getTemplate().get("code"), code);
return getSimpleMailMessage(to, subject, content);
}
public SimpleMailMessage getSimpleMailMessage(String to, String subject, String content) {
SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
simpleMailMessage.setFrom(mailProperties.getFrom());
simpleMailMessage.setTo(to);
simpleMailMessage.setSubject(subject);
simpleMailMessage.setText(content);
return simpleMailMessage;
}
}

View File

@ -0,0 +1,17 @@
package xyz.zhouxy.plusone.mail;
import javax.mail.MessagingException;
/**
*
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public interface MailService {
void sendSimpleMail(String to, String subject, String content);
void sendHtmlMail(String to, String subject, String content) throws MessagingException;
void sendCodeMail(String code, String to);
}

View File

@ -0,0 +1,26 @@
package xyz.zhouxy.plusone.mail;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mail.javamail.JavaMailSender;
/**
* {@link MailService} Spring
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Configuration
@EnableConfigurationProperties(PlusoneMailProperties.class)
@ConditionalOnClass(MailService.class)
@EnableAutoConfiguration
public class PlusoneMailAutoConfiguration {
@Bean
public MailService mailService(JavaMailSender mailSender, PlusoneMailProperties mailProperties) {
MailMessageFactory mailMessageFactory = new MailMessageFactory(mailProperties);
return new SimpleMailService(mailSender, mailMessageFactory);
}
}

View File

@ -0,0 +1,16 @@
package xyz.zhouxy.plusone.mail;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.Map;
@Getter
@Setter
@ConfigurationProperties("plusone.mail")
public class PlusoneMailProperties {
private String from;
private Map<String, String> subject;
private Map<String, String> template;
}

View File

@ -0,0 +1,57 @@
package xyz.zhouxy.plusone.mail;
import lombok.extern.slf4j.Slf4j;
import org.springframework.mail.MailException;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import javax.mail.MessagingException;
/**
* <b> HTML </b>
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Slf4j
public class SimpleMailService implements MailService {
private final JavaMailSender mailSender;
private final MailMessageFactory mailMessageFactory;
public SimpleMailService(JavaMailSender mailSender, MailMessageFactory mailMessageFactory) {
this.mailSender = mailSender;
this.mailMessageFactory = mailMessageFactory;
}
/**
*
*/
@Override
public void sendSimpleMail(String to, String subject, String content) {
SimpleMailMessage message = mailMessageFactory.getSimpleMailMessage(to, subject, content);
try {
mailSender.send(message);
log.debug("简单邮件已经发送。");
} catch (MailException e) {
log.error("发送简单邮件时发生异常!", e);
throw e;
}
}
@Override
public void sendHtmlMail(String to, String subject, String content) throws MessagingException {
throw new UnsupportedOperationException("暂不支持");
}
@Override
public void sendCodeMail(String code, String to) {
SimpleMailMessage message = mailMessageFactory.getCodeMailMessage(code, to);
try {
mailSender.send(message);
log.debug("简单邮件已经发送。");
} catch (MailException e) {
log.error("发送简单邮件时发生异常!", e);
throw e;
}
}
}

View File

@ -0,0 +1,17 @@
package xyz.zhouxy.plusone.mybatis;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableAutoConfiguration
public class MyBatisAutoConfiguration {
@Bean
MybatisUtil mybatisUtil(SqlSessionFactory sqlSessionFactory) {
return MybatisUtil.getInstance()
.setSqlSessionFactory(sqlSessionFactory);
}
}

View File

@ -0,0 +1,19 @@
package xyz.zhouxy.plusone.mybatis;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}

View File

@ -0,0 +1,28 @@
package xyz.zhouxy.plusone.mybatis;
import org.apache.ibatis.session.SqlSessionFactory;
public final class MybatisUtil {
private SqlSessionFactory sqlSessionFactory;
private MybatisUtil() {
}
MybatisUtil setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
return this;
}
private static final class Holder {
private static final MybatisUtil INSTANCE = new MybatisUtil();
}
public static MybatisUtil getInstance() {
return Holder.INSTANCE;
}
public static SqlSessionFactory getSqlSessionFactory() {
return MybatisUtil.getInstance().sqlSessionFactory;
}
}

View File

@ -0,0 +1,42 @@
package xyz.zhouxy.plusone.redis;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Repository;
import java.util.concurrent.TimeUnit;
/**
* 使 Redis
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Repository
public class RedisStrCacheDAO implements StrCacheDAO {
private final StringRedisTemplate template;
public RedisStrCacheDAO(StringRedisTemplate template) {
this.template = template;
}
@Override
public void setValue(String key, String value, long timeout, TimeUnit unit) {
ValueOperations<String, String> ops = template.opsForValue();
ops.set(key, value, timeout, unit);
}
@Override
public String getValue(String key) {
ValueOperations<String, String> ops = this.template.opsForValue();
return ops.get(key);
}
@Override
public String getValueAndDelete(String key) {
ValueOperations<String, String> ops = this.template.opsForValue();
String value = ops.get(key);
template.delete(key);
return value;
}
}

View File

@ -0,0 +1,17 @@
package xyz.zhouxy.plusone.redis;
import java.util.concurrent.TimeUnit;
/**
*
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public interface StrCacheDAO {
void setValue(String key, String value, long timeout, TimeUnit unit);
String getValue(String key);
String getValueAndDelete(String key);
}

View File

@ -0,0 +1,27 @@
package xyz.zhouxy.plusone.sms;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* SMS
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Configuration
@EnableConfigurationProperties(value = {
SmsProperties.class,
SmsCredentialProperties.class,
SmsClientProperties.class,
SmsHttpProperties.class,
SmsProxyProperties.class})
@ConditionalOnClass(SmsService.class)
public class PlusoneSmsAutoConfiguration {
@Bean
public SmsService smsService(SmsProperties smsProperties) {
return new TencentSmsServiceImpl(smsProperties);
}
}

View File

@ -0,0 +1,51 @@
package xyz.zhouxy.plusone.sms;
import java.util.Map;
import org.springframework.boot.context.properties.ConfigurationProperties;
import lombok.Data;
/**
* SMS
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Data
@ConfigurationProperties("plusone.sms")
public class SmsProperties {
private String region;
private SmsCredentialProperties credential;
private SmsClientProperties client;
private String appId;
private Map<String, String> templates;
}
@Data
@ConfigurationProperties("plusone.sms.credential")
class SmsCredentialProperties {
private String secretId;
private String secretKey;
}
@Data
@ConfigurationProperties("plusone.sms.client")
class SmsClientProperties {
private String signMethod;
private SmsHttpProperties http;
}
@Data
@ConfigurationProperties("plusone.sms.client.http")
class SmsHttpProperties {
private SmsProxyProperties proxy;
private String reqMethod;
private Integer connTimeout;
}
@Data
@ConfigurationProperties("plusone.sms.client.http.proxy")
class SmsProxyProperties {
private String host;
private Integer port;
}

View File

@ -0,0 +1,13 @@
package xyz.zhouxy.plusone.sms;
/**
* SMS
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public interface SmsService {
void sendCodeMessage(String code, String phoneNumber);
void sendCodeMessage(String signName, String code, String phoneNumber);
}

View File

@ -0,0 +1,145 @@
package xyz.zhouxy.plusone.sms;
import com.tencentcloudapi.common.Credential;
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
import com.tencentcloudapi.common.profile.ClientProfile;
import com.tencentcloudapi.common.profile.HttpProfile;
import com.tencentcloudapi.sms.v20210111.SmsClient;
import com.tencentcloudapi.sms.v20210111.models.SendSmsRequest;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
/**
* 使 SMS
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Service
@Slf4j
public class TencentSmsServiceImpl implements SmsService {
private final SmsProperties properties;
private final Credential credential;
public TencentSmsServiceImpl(SmsProperties properties) {
this.properties = properties;
SmsCredentialProperties smsCredential = properties.getCredential();
this.credential = new Credential(smsCredential.getSecretId(), smsCredential.getSecretKey());
}
@Override
public void sendCodeMessage(String code, String phoneNumber) {
sendCodeMessage("ZhouXY", code, phoneNumber);
}
@Override
public void sendCodeMessage(String signName, String code, String phoneNumber) {
String[] phoneNums = {"+86" + phoneNumber};
sendMessage(signName, "code", phoneNums, code, "10");
}
public void sendMessage(String signName, String action, String[] phoneNumberSet, String... templateParamSet) {
try {
SmsClient client = getClient();
SendSmsRequest req = getSendSmsRequest(signName,
properties.getTemplates().get(action),
phoneNumberSet,
templateParamSet);
/*
* client SendSms res
* SendSmsResponse
*/
// var res = client.SendSms(req);
client.SendSms(req);
// 输出json格式的字符串回包
// System.out.println(SendSmsResponse.toJsonString(res));
// 也可以取出单个值你可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义
// System.out.println(res.getRequestId());
} catch (TencentCloudSDKException e) {
log.error(e.getMessage(), e);
}
}
private SendSmsRequest getSendSmsRequest(String signName,
String templateId,
String[] phoneNumberSet,
String... templateParamSet) {
SendSmsRequest req = new SendSmsRequest();
String sdkAppId = properties.getAppId();
req.setSmsSdkAppId(sdkAppId);
req.setSignName(signName);
/* 国际/港澳台短信 SenderId: 国内短信填空,默认未开通,如需开通请联系 [sms helper] */
String senderId = "";
req.setSenderId(senderId);
/* 用户的 session 内容: 可以携带用户侧 ID 等上下文信息server 会原样返回 */
// String sessionContext = "xxx";
// req.setSessionContext(sessionContext);
/* 短信号码扩展号: 默认未开通,如需开通请联系 [sms helper] */
String extendCode = "";
req.setExtendCode(extendCode);
/* 模板 ID: 必须填写已审核通过的模板 ID。模板ID可登录 [短信控制台] 查看 */
req.setTemplateId(templateId);
/*
* E.164 +[][] +8613711112222 +
* 8613711112222200
*/
req.setPhoneNumberSet(phoneNumberSet);
/* 模板参数: 若无模板参数,则设置为空 */
req.setTemplateParamSet(templateParamSet);
return req;
}
private SmsClient getClient() {
String region = properties.getRegion();
ClientProfile clientProfile = getClientProfile();
return new SmsClient(credential, region, clientProfile);
}
private ClientProfile getClientProfile() {
HttpProfile httpProfile = getHttpProfile();
/*
* :
*/
ClientProfile clientProfile = new ClientProfile();
String signMethod = properties.getClient().getSignMethod();
if (signMethod != null) {
clientProfile.setSignMethod(signMethod);
} else {
clientProfile.setSignMethod("HmacSHA256");
}
clientProfile.setHttpProfile(httpProfile);
return clientProfile;
}
private HttpProfile getHttpProfile() {
HttpProfile httpProfile = new HttpProfile();
SmsHttpProperties smsHttp = properties.getClient().getHttp();
if (smsHttp != null) {
if (smsHttp.getReqMethod() != null) {
httpProfile.setReqMethod(smsHttp.getReqMethod());
}
if (smsHttp.getConnTimeout() != null) {
httpProfile.setConnTimeout(60);
}
SmsProxyProperties proxy = smsHttp.getProxy();
if (proxy != null && proxy.getHost() != null && proxy.getPort() != null) {// 设置代理
httpProfile.setProxyHost(proxy.getHost());
httpProfile.setProxyPort(proxy.getPort());
}
}
return httpProfile;
}
}

View File

@ -0,0 +1,28 @@
package xyz.zhouxy.plusone.spring;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
public class SpringContextHolder {
private ApplicationContext context;
private static final SpringContextHolder INSTANCE = new SpringContextHolder();
private SpringContextHolder() {
}
public static ApplicationContext getContext() {
return INSTANCE.context;
}
@Configuration
static class SpringContextHolderConfig {
@Bean(name = "springContextHolder")
SpringContextHolder getSpringContextHolder(ApplicationContext context) {
SpringContextHolder.INSTANCE.context = context;
return SpringContextHolder.INSTANCE;
}
}
}

View File

@ -0,0 +1,34 @@
package xyz.zhouxy.plusone.sql;
import java.util.Objects;
import org.apache.ibatis.jdbc.AbstractSQL;
/**
* MyBatis SQL
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public class SQL extends AbstractSQL<SQL> {
public SQL SET_IF(boolean condition, String sets) {
return condition ? SET(sets) : getSelf();
}
public SQL SET_IF_NOT_NULL(Object param, String sets) {
return Objects.nonNull(param) ? SET(sets) : getSelf();
}
public SQL WHERE_IF(boolean condition, String sqlCondition) {
return condition ? WHERE(sqlCondition) : getSelf();
}
public SQL WHERE_IF_NOT_NULL(Object param, String sqlCondition) {
return Objects.nonNull(param) ? WHERE(sqlCondition) : getSelf();
}
@Override
public SQL getSelf() {
return this;
}
}

View File

@ -0,0 +1,72 @@
package xyz.zhouxy.plusone.validator;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import xyz.zhouxy.plusone.exception.InvalidInputException;
/**
*
*
* <p>使</p>
*
* <pre>
* BaseValidator&lt;Integer&gt; validator = new BaseValidator&lt;&gt;() {
* {
* ruleFor(value -> Objects.nonNull(value), "value 不能为空");
* ruleFor(value -> (value >= 0 && value <= 500), "value 应在 [0, 500] 内");
* }
* };
* </pre>
*
* <p>使</p>
*
* <p>
* {@link #validate}
* {@link ValidateUtil#validate(Object, Validator)}
* </p>
*
* <pre>
* ValidateUtil.validate(255, validator);
* </pre>
*
* <pre>
* validator.validate(666);
* </pre>
* </p>
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @see IValidateRequired
* @see ValidateUtil
* @see Validator
*/
public abstract class BaseValidator<T> {
private final List<RuleInfo<T>> rules = new ArrayList<>();
protected BaseValidator() {
}
protected final void ruleFor(Predicate<T> rule, String errorMessage) {
this.rules.add(new RuleInfo<>(rule, errorMessage));
}
public void validate(T obj) {
this.rules.forEach((RuleInfo<T> ruleInfo) -> {
if (!ruleInfo.rule.test(obj)) {
throw new InvalidInputException(ruleInfo.message);
}
});
}
protected static class RuleInfo<T> {
Predicate<T> rule;
String message;
public RuleInfo(Predicate<T> rule, String message) {
this.rule = rule;
this.message = message;
}
}
}

View File

@ -0,0 +1,129 @@
package xyz.zhouxy.plusone.validator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Predicate;
import cn.hutool.core.exceptions.ValidateException;
import lombok.AllArgsConstructor;
import xyz.zhouxy.plusone.constant.RegexConsts;
import xyz.zhouxy.plusone.util.RegexUtil;
public abstract class BaseValidator2<T> {
private List<ValidValueHolder<T, ?>> hs = new ArrayList<>();
protected final <R> ValidValueHolder<T, R> ruleFor(Function<T, R> getter) {
ValidValueHolder<T, R> validValueHolder = new ValidValueHolder<>(getter);
hs.add(validValueHolder);
return validValueHolder;
}
public void validate(T obj) {
for (var holder : hs) {
var value = holder.getter.apply(obj);
for (var rule : holder.rules) {
if (!rule.condition.test(value)) {
throw new ValidateException(rule.errMsg);
}
}
}
}
}
class ValidValueHolder<T, R> {
Function<T, R> getter;
List<RuleInfo<Object>> rules = new ArrayList<>();
public ValidValueHolder(Function<T, R> getter) {
this.getter = getter;
}
private void addRule(Predicate<Object> condition, String errMsg) {
this.rules.add(new RuleInfo<>(condition, errMsg));
}
public ValidValueHolder<T, R> nonNull(String errMsg) {
addRule(Objects::nonNull, errMsg);
return this;
}
public ValidValueHolder<T, R> nonEmpty(String errMsg) {
addRule(value -> {
if (value == null) {
return false;
}
if (value instanceof Collection) {
return ((Collection<?>) value).isEmpty();
}
if (value instanceof String) {
return ((String) value).isEmpty();
}
return false;
}, errMsg);
return this;
}
public ValidValueHolder<T, R> size(int min, int max, String errMsg) {
addRule(value -> {
if (value == null) {
return false;
}
if (value instanceof Collection) {
int size = ((Collection<?>) value).size();
return size >= min && size <= max;
}
return true;
}, errMsg);
return this;
}
public ValidValueHolder<T, R> length(int min, int max, String errMsg) {
addRule(value -> {
if (value == null) {
return false;
}
if (value instanceof String) {
int length = ((String) value).length();
return length >= min && length <= max;
}
return true;
}, errMsg);
return this;
}
public ValidValueHolder<T, R> matches(String regex, String errMsg) {
addRule(input -> RegexUtil.matches(regex, (String) input), errMsg);
return this;
}
public ValidValueHolder<T, R> matchesOr(String[] regexs, String errMsg) {
addRule(input -> RegexUtil.matchesOr((String) input, regexs), errMsg);
return this;
}
public ValidValueHolder<T, R> matchesAnd(String[] regexs, String errMsg) {
addRule(input -> RegexUtil.matchesAnd((String) input, regexs), errMsg);
return this;
}
public ValidValueHolder<T, R> email(String errMsg) {
return matches(RegexConsts.EMAIL, errMsg);
}
@SuppressWarnings("unchecked")
public ValidValueHolder<T, R> and(Predicate<R> condition, String errMsg) {
addRule((Predicate<Object>) condition, errMsg);
return this;
}
@AllArgsConstructor
static final class RuleInfo<R> {
Predicate<R> condition;
String errMsg;
}
}

View File

@ -0,0 +1,12 @@
package xyz.zhouxy.plusone.validator;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DtoValidator {
Class<?> value();
}

View File

@ -0,0 +1,13 @@
package xyz.zhouxy.plusone.validator;
/**
*
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*
* @see ValidateUtil
* @see BaseValidator
*/
public interface IValidateRequired {
void validate();
}

View File

@ -0,0 +1,42 @@
package xyz.zhouxy.plusone.validator;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
@Aspect
@Component
class ValidateDtosConfig {
final Map<Class<?>, BaseValidator<?>> validatorMap = new ConcurrentHashMap<>();
ValidateDtosConfig(ApplicationContext context) {
Map<String, Object> beans = context.getBeansWithAnnotation(DtoValidator.class);
for (var validator : beans.values()) {
Class<?> targetClass = validator.getClass().getAnnotation(DtoValidator.class).value();
this.validatorMap.put(targetClass, (BaseValidator<?>) validator);
}
}
@Before("@annotation(ValidateDto) && args(dto)")
@SuppressWarnings("unchecked")
public <T> void doValidateDto(T dto) {
BaseValidator<T> validator = (BaseValidator<T>) this.validatorMap.get(dto.getClass());
if (validator != null) {
validator.validate(dto);
}
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidateDto {
}

View File

@ -0,0 +1,29 @@
package xyz.zhouxy.plusone.validator;
/**
*
* <p>
* {@link IValidateRequired}
* </p>
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*
* @see BaseValidator
* @see Validator
* @see IValidateRequired
*/
public class ValidateUtil {
private ValidateUtil() {
throw new IllegalStateException("Utility class");
}
public static void validate(Object obj) {
if (obj instanceof IValidateRequired) {
((IValidateRequired) obj).validate();
}
}
public static <T> void validate(T obj, BaseValidator<T> validator) {
validator.validate(obj);
}
}

View File

@ -0,0 +1,42 @@
package xyz.zhouxy.plusone.validator;
import java.util.function.Predicate;
/**
*
*
* <p>
* 使
* </p>
*
* <pre>
* var validator = new Validator&lt;Integer&gt;()
* .addRule(value -> Objects.nonNull(value), "value 不能为空")
* .addRule(value -> (value >= 0 && value <= 500), "value 应在 [0, 500] 内");
* </pre>
*
* <p>
* {@link #validate}
* {@link ValidateUtil#validate(Object, Validator)}
* </p>
*
* <pre>
* validator.validate(666);
* </pre>
*
* <pre>
* ValidateUtil.validate(255, validator);
* </pre>
* </p>
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @see IValidateRequired
* @see ValidateUtil
* @see BaseValidator
*/
public final class Validator<T> extends BaseValidator<T> {
public final Validator<T> addRule(final Predicate<T> rule, final String errorMessage) {
ruleFor(rule, errorMessage);
return this;
}
}

View File

@ -0,0 +1,23 @@
package xyz.zhouxy.plusone.web.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
*
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Configuration
public class WebCorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "OPTIONS", "PUT", "DELETE", "PATCH")
.maxAge(3600);
}
}

View File

@ -0,0 +1,55 @@
package xyz.zhouxy.plusone.web.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Spring MVC
*
* <p>
*
* </p>
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Configuration
public class WebEnumMapConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverterFactory(new IntToEnumConverterFactory());
registry.addConverterFactory(new StringToEnumConverterFactory());
}
}
class IntToEnumConverterFactory implements ConverterFactory<Integer, Enum<?>> {
@Override
public <T extends Enum<?>> Converter<Integer, T> getConverter(Class<T> targetType) {
return (Integer source) -> {
try {
T[] values = targetType.getEnumConstants();
return values[source];
} catch (IndexOutOfBoundsException e) {
throw new EnumConstantNotPresentException(targetType, Integer.toString(source));
}
};
}
}
class StringToEnumConverterFactory implements ConverterFactory<String, Enum<?>> {
@Override
public <T extends Enum<?>> Converter<String, T> getConverter(Class<T> targetType) {
return (String source) -> {
int index = Integer.parseInt(source);
try {
T[] values = targetType.getEnumConstants();
return values[index];
} catch (IndexOutOfBoundsException e) {
throw new EnumConstantNotPresentException(targetType, Integer.toString(index));
}
};
}
}

View File

@ -0,0 +1,31 @@
spring.application.name=${plusone.application.name}
spring.mail.host=${plusone.mail.host}
spring.mail.username=${plusone.mail.from}
spring.mail.password=${plusone.mail.password}
spring.mail.default-encoding=UTF-8
spring.mail.properties.mail.debug=${plusone.debug}
server.port=${plusone.server.port}
mybatis-plus.global-config.banner=false
# 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
mybatis-plus.global-config.db-config.logic-delete-field=deleted
# 逻辑已删除值(默认为 1)
mybatis-plus.global-config.db-config.logic-delete-value=id
# 逻辑未删除值(默认为 0)
mybatis-plus.global-config.db-config.logic-not-delete-value=0
mybatis-plus.configuration.default-enum-type-handler=org.apache.ibatis.type.EnumOrdinalTypeHandler
# Sa-Token 配置
sa-token.is-print=false
# token名称 (同时也是cookie名称)
sa-token.token-name=${plusone.application.name}
# 是否允许同一账号并发登录 (为true时允许一起登录, 为 false 时新登录挤掉旧登录)
sa-token.is-concurrent=true
# 在多人登录同一账号时是否共用一个token (为 true 时所有登录共用一个 token, 为false时每次登录新建一个 token)
sa-token.is-share=true
sa-token.token-style=simple-uuid
sa-token.activity-timeout=1800
sa-token.is-read-cookie=false
sa-token.is-log=${plusone.debug}

View File

@ -0,0 +1,41 @@
package xyz.zhouxy.plusone.validator;
import org.junit.jupiter.api.Test;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import xyz.zhouxy.plusone.constant.RegexConsts;
class BaseValidator2Test {
@Test
void testValid() {
LoginCommand loginCommand = new LoginCommand("13169053215@qq.com", "8GouTDE53a", false);
LoginCommandValidator.INSTANCE.validate(loginCommand);
System.err.println(loginCommand);
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
class LoginCommand {
private String account;
private String pwd;
private boolean rememberMe;
}
class LoginCommandValidator extends BaseValidator2<LoginCommand> {
public static final LoginCommandValidator INSTANCE = new LoginCommandValidator();
private LoginCommandValidator() {
ruleFor(loginCommand -> loginCommand.getAccount())
.nonNull("邮箱地址不能为空")
.matchesOr(new String[] { RegexConsts.EMAIL, RegexConsts.MOBILE_PHONE }, "请输入邮箱地址或手机号");
ruleFor(loginCommand -> loginCommand.getPwd())
.nonNull("密码不能为空")
.matches(RegexConsts.PASSWORD, "密码格式错误");
}
}

View File

@ -0,0 +1,57 @@
<?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>
<artifactId>plusone</artifactId>
<groupId>xyz.zhouxy</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<groupId>xyz.zhouxy</groupId>
<artifactId>plusone-basic</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>plusone-basic</name>
<modules>
<module>plusone-basic-common</module>
<module>plusone-basic-domain</module>
<module>plusone-basic-infrastructure</module>
<module>plusone-basic-application</module>
</modules>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>xyz.zhouxy</groupId>
<artifactId>plusone-basic-common</artifactId>
<version>${plusone-basic.version}</version>
</dependency>
<dependency>
<groupId>xyz.zhouxy</groupId>
<artifactId>plusone-basic-domain</artifactId>
<version>${plusone-basic.version}</version>
</dependency>
<dependency>
<groupId>xyz.zhouxy</groupId>
<artifactId>plusone-basic-infrastructure</artifactId>
<version>${plusone-basic.version}</version>
</dependency>
<dependency>
<groupId>xyz.zhouxy</groupId>
<artifactId>plusone-basic-application</artifactId>
<version>${plusone-basic.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,85 @@
<?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>
<artifactId>plusone</artifactId>
<groupId>xyz.zhouxy</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<groupId>xyz.zhouxy</groupId>
<artifactId>plusone-start</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>plusone-start</name>
<description>参考 DDD 落地的脚手架</description>
<properties>
<spring-boot.version>2.7.6</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>xyz.zhouxy</groupId>
<artifactId>plusone-system-application</artifactId>
<version>${plusone-system.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<mainClass>xyz.zhouxy.plusone.PlusoneApplication</mainClass>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,17 @@
package xyz.zhouxy.plusone;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import lombok.extern.slf4j.Slf4j;
@SpringBootApplication
@Slf4j
public class PlusoneApplication {
public static void main(String[] args) {
log.debug("Plusone started!");
SpringApplication.run(PlusoneApplication.class, args);
}
}

View File

@ -0,0 +1,34 @@
{
"properties": [
{
"name": "plusone.application.name",
"type": "java.lang.String",
"description": "A description for 'plusone.application.name'"
},
{
"name": "plusone.server.port",
"type": "java.lang.Integer",
"description": "A description for 'plusone.server.port'"
},
{
"name": "plusone.debug",
"type": "java.lang.Boolean",
"description": "A description for 'plusone.debug'"
},
{
"name": "plusone.mail.host",
"type": "java.lang.String",
"description": "A description for 'plusone.mail.host'"
},
{
"name": "plusone.mail.password",
"type": "java.lang.String",
"description": "A description for 'plusone.mail.password'"
},
{
"name": "plusone.exception.handle-all-exception",
"type": "java.lang.Boolean",
"description": "A description for 'plusone.exception.handle-all-exception'"
}
]
}

View File

@ -0,0 +1,79 @@
plusone:
application:
name: plusone
server:
port: 8108
debug: true
# 短信发送相关参数
sms:
region: ap-guangzhou
credential:
secret-id: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
secret-key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
client:
# SDK 默认用 TC3-HMAC-SHA256 进行签名,非必要请不要修改这个字段
sign-method: HmacSHA256
http:
# proxy:
# host: xxx
# port: xxx
req-method: POST
conn-timeout: 60
app-id: 1111111111
templates:
code: 0000000
# 邮件发送相关参数
mail:
host: smtp.163.com
#设置邮件发送者
from: example@163.com
password: XXXXXXXXXXXXXXXX
subject:
code: Plusone
template:
code: 【Plusone】验证码%s10分钟内有效请勿泄露。
# 异常拦截机制是否拦截所有异常
exception:
handle-all-exception: false
spring:
# redis配置
redis:
# Redis数据库索引默认为0
database: 1
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码默认为空
# password:
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池最大连接数
max-active: 200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 连接池中的最大空闲连接
max-idle: 10
# 连接池中的最小空闲连接
min-idle: 0
# 数据库
datasource:
url: jdbc:postgresql://localhost:5432/plusone
username: plusone
password: XXXXXXXXXXXXXXXX
# 日志配置
logging:
file:
name: ${user.home}/logs/${plusone.application.name}.log
level:
root: INFO
xyz.zhouxy.plusone: DEBUG
org.springframework.jdbc.core: DEBUG
xyz.zhouxy.plusone.system.application.query: DEBUG

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<include resource="org/springframework/boot/logging/logback/console-appender.xml" />
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%green(%d{yyyy-MM-dd HH:mm:ss.SSS}) %highlight(%5level) ${PID:- } - [%15.15t] %cyan(%-40.108logger) : %msg%n</pattern>
</encoder>
</appender>
<!-- <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %5level ${PID:- } - [%15.15t] %-50.50logger : %msg%n</pattern>
<charset>utf-8</charset>
</encoder>
<file>log/output.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<fileNamePattern>log/output.log.%i</fileNamePattern>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>20MB</MaxFileSize>
</triggeringPolicy>
</appender> -->
<root level="INFO">
<appender-ref ref="CONSOLE" />
<!-- <appender-ref ref="FILE" /> -->
</root>
</configuration>

View File

@ -0,0 +1,22 @@
package xyz.zhouxy.plusone;
import java.io.ObjectStreamClass;
import org.junit.jupiter.api.Test;
import lombok.extern.slf4j.Slf4j;
import xyz.zhouxy.plusone.exception.PlusoneException;
@Slf4j
class SerialTests {
@Test
void testSerialVersionUID() {
var cl = PlusoneException.class;
var c = ObjectStreamClass.lookup(cl);
var uid = c.getSerialVersionUID();
log.info("\n @java.io.Serial" +
"\n private static final long serialVersionUID = {}L;", uid);
}
}

View File

@ -0,0 +1,31 @@
package xyz.zhouxy.plusone;
import javax.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import xyz.zhouxy.plusone.system.application.query.result.LoginInfoViewObject;
import xyz.zhouxy.plusone.system.application.service.AdminLoginService;
import xyz.zhouxy.plusone.system.application.service.command.LoginByPasswordCommand;
import xyz.zhouxy.plusone.system.domain.model.account.AccountRepository;
@SpringBootTest(classes = PlusoneApplication.class)
class TestAop {
@Resource
AccountRepository repository;
@Resource
AdminLoginService service;
@Test
void testAop() {
LoginByPasswordCommand command = new LoginByPasswordCommand();
command.setPrincipal("Code108");
command.setPassword("2333");
command.setRememberMe(false);
LoginInfoViewObject loginInfo = service.loginByPassword(command);
System.err.println(loginInfo);
}
}

View File

@ -0,0 +1,27 @@
<?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>
<artifactId>plusone-system</artifactId>
<groupId>xyz.zhouxy</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>plusone-system-application</artifactId>
<dependencies>
<dependency>
<groupId>xyz.zhouxy</groupId>
<artifactId>plusone-system-domain</artifactId>
</dependency>
<dependency>
<groupId>xyz.zhouxy</groupId>
<artifactId>plusone-system-infrastructure</artifactId>
</dependency>
<dependency>
<groupId>xyz.zhouxy</groupId>
<artifactId>plusone-basic-application</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,61 @@
package xyz.zhouxy.plusone.system.application.common.util;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import xyz.zhouxy.plusone.domain.IWithOrderNumber;
import xyz.zhouxy.plusone.system.application.query.result.MenuViewObject;
import xyz.zhouxy.plusone.system.domain.model.menu.Menu.MenuType;
/**
*
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public class MenuUtil {
private MenuUtil() {
throw new IllegalStateException("Utility class");
}
/**
*
*
* @param allMenus
* @return
*/
public static List<MenuViewObject> buildMenuTree(Collection<MenuViewObject> allMenus) {
// 先排序,保证添加到 rootMenus 中的顺序,以及 addChild 添加的子菜单的顺序
allMenus = allMenus.stream()
.sorted(Comparator.comparing(IWithOrderNumber::getOrderNumber))
.toList();
// 一级菜单
List<MenuViewObject> rootMenus = new ArrayList<>();
// key: 菜单 id; value: 菜单对象. 方便根据 id 查找相应对象。
Map<Long, MenuViewObject> menuListMap = new HashMap<>();
for (var menu : allMenus) {
// 添加 MENU_LIST 到 map 中,方便后面调用对象的方法
if (menu.getType() == MenuType.MENU_LIST.ordinal()) {
menuListMap.put(menu.getId(), menu);
}
// 一级菜单
if (menu.getParentId() == 0) {
rootMenus.add(menu);
}
}
for (var menu : allMenus) {
var parent = menuListMap.getOrDefault(menu.getParentId(), null);
// 父菜单存在于 map 中,调用父菜单的 addChild 方法将当前菜单添加为父菜单的子菜单。
if (parent != null) {
parent.addChild(menu);
}
}
return rootMenus;
}
}

View File

@ -0,0 +1,18 @@
package xyz.zhouxy.plusone.system.application.common.util;
import lombok.Getter;
import xyz.zhouxy.plusone.constant.RegexConsts;
public enum PrincipalType {
EMAIL(RegexConsts.EMAIL),
MOBILE_PHONE(RegexConsts.MOBILE_PHONE),
USERNAME(RegexConsts.USERNAME)
;
@Getter
private final String regex;
PrincipalType(String regex) {
this.regex = regex;
}
}

View File

@ -0,0 +1,64 @@
package xyz.zhouxy.plusone.system.application.common.util;
import javax.annotation.Nullable;
import xyz.zhouxy.plusone.exception.InvalidInputException;
import xyz.zhouxy.plusone.system.domain.model.account.Email;
import xyz.zhouxy.plusone.system.domain.model.account.MobilePhone;
import xyz.zhouxy.plusone.system.domain.model.account.Principal;
import xyz.zhouxy.plusone.system.domain.model.account.Username;
/**
* {@link Principal}
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @see Principal
* @see Username
* @see Email
* @see MobilePhone
* @see InvalidInputException
*/
public class PrincipalUtil {
private PrincipalUtil() {
throw new IllegalStateException("Utility class");
}
public static PrincipalType getPrincipalType(@Nullable String principal) {
if (principal == null) {
throw new IllegalArgumentException("principal 不能为空");
}
PrincipalType[] principalTypes = PrincipalType.values();
for (var principalType : principalTypes) {
if (principal.matches(principalType.getRegex())) {
return principalType;
}
}
throw InvalidInputException.unsupportedPrincipalTypeException();
}
public static Principal getPrincipal(@Nullable String principal) {
PrincipalType principalType = getPrincipalType(principal);
if (principalType == PrincipalType.EMAIL) {
return Email.of(principal);
}
if (principalType == PrincipalType.MOBILE_PHONE) {
return MobilePhone.of(principal);
}
if (principalType == PrincipalType.USERNAME) {
return Username.of(principal);
}
throw InvalidInputException.unsupportedPrincipalTypeException();
}
public static Principal getEmailOrMobilePhone(@Nullable String principal) {
PrincipalType principalType = getPrincipalType(principal);
if (principalType == PrincipalType.EMAIL) {
return Email.of(principal);
}
if (principalType == PrincipalType.MOBILE_PHONE) {
return MobilePhone.of(principal);
}
throw InvalidInputException.unsupportedPrincipalTypeException("输入邮箱地址或手机号");
}
}

View File

@ -0,0 +1,40 @@
package xyz.zhouxy.plusone.system.application.controller;
import static xyz.zhouxy.plusone.system.constant.AuthLogic.adminAuthLogic;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import xyz.zhouxy.plusone.system.application.service.AccountContextService;
import xyz.zhouxy.plusone.util.RestfulResult;
/**
*
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@RestController
@RequestMapping("account")
public class AccountContextController {
private final AccountContextService service;
public AccountContextController(AccountContextService service) {
this.service = service;
}
@GetMapping("info")
public RestfulResult getAccountInfo() {
adminAuthLogic.checkLogin();
var result = service.getAccountInfo();
return RestfulResult.success("查询成功", result);
}
@GetMapping("menus")
public RestfulResult getMenuTree() {
adminAuthLogic.checkLogin();
var result = service.getMenuTree();
return RestfulResult.success("查询成功", result);
}
}

View File

@ -0,0 +1,83 @@
package xyz.zhouxy.plusone.system.application.controller;
import static xyz.zhouxy.plusone.system.constant.AuthLogic.adminAuthLogic;
import static xyz.zhouxy.plusone.util.RestfulResult.success;
import java.util.List;
import javax.validation.Valid;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import xyz.zhouxy.plusone.system.application.query.params.AccountQueryParams;
import xyz.zhouxy.plusone.system.application.service.AccountManagementService;
import xyz.zhouxy.plusone.system.application.service.command.CreateAccountCommand;
import xyz.zhouxy.plusone.system.application.service.command.UpdateAccountCommand;
import xyz.zhouxy.plusone.util.AssertResult;
import xyz.zhouxy.plusone.util.RestfulResult;
/**
*
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@RestController
@RequestMapping("sys/account")
public class AccountManagementController {
private final AccountManagementService service;
public AccountManagementController(AccountManagementService service) {
this.service = service;
}
@PostMapping
public RestfulResult createAccount(@RequestBody @Valid CreateAccountCommand command) {
adminAuthLogic.checkLogin();
adminAuthLogic.checkPermission("sys-account-create");
service.createAccount(command);
return success();
}
@DeleteMapping
public RestfulResult deleteAccounts(@RequestBody List<Long> ids) {
adminAuthLogic.checkLogin();
adminAuthLogic.checkPermission("sys-account-delete");
service.deleteAccounts(ids);
return success();
}
@PatchMapping("{id}")
public RestfulResult updateAccountInfo(
@PathVariable("id") Long id,
@RequestBody @Valid UpdateAccountCommand command) {
adminAuthLogic.checkLogin();
adminAuthLogic.checkPermission("sys-account-update");
service.updateAccountInfo(id, command);
return success();
}
@GetMapping("query")
public RestfulResult queryAccountOverviewList(AccountQueryParams queryParams) {
adminAuthLogic.checkLogin();
adminAuthLogic.checkPermission("sys-account-list");
var accountOverviewList = service.queryAccountOverviewList(queryParams);
return success("查询成功", accountOverviewList);
}
@GetMapping("{accountId}")
public RestfulResult queryAccountDetails(@PathVariable("accountId") Long accountId) {
adminAuthLogic.checkLogin();
adminAuthLogic.checkPermission("sys-account-details");
var accountDetails = service.queryAccountDetails(accountId);
AssertResult.nonNull(accountDetails);
return success("查询成功", accountDetails);
}
}

View File

@ -0,0 +1,49 @@
package xyz.zhouxy.plusone.system.application.controller;
import static xyz.zhouxy.plusone.util.RestfulResult.success;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import xyz.zhouxy.plusone.system.application.service.AdminLoginService;
import xyz.zhouxy.plusone.system.application.service.command.LoginByOtpCommand;
import xyz.zhouxy.plusone.system.application.service.command.LoginByPasswordCommand;
import xyz.zhouxy.plusone.util.RestfulResult;
/**
* Admin
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@RestController
@RequestMapping("login")
public class AdminLoginController {
private final AdminLoginService service;
public AdminLoginController(AdminLoginService service) {
this.service = service;
}
@PostMapping("byPassword")
public RestfulResult loginByPassword(@RequestBody LoginByPasswordCommand command) {
var loginInfo = service.loginByPassword(command);
return success("登录成功", loginInfo);
}
@PostMapping("byOtp")
public RestfulResult loginByOtp(@RequestBody LoginByOtpCommand command) {
var loginInfo = service.loginByOtp(command);
return success("登录成功", loginInfo);
}
@GetMapping("sendOtp")
public RestfulResult sendOtp(@RequestParam String principal) {
service.sendOtp(principal);
return success("发送成功");
}
}

View File

@ -0,0 +1,30 @@
package xyz.zhouxy.plusone.system.application.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import xyz.zhouxy.plusone.system.application.service.AdminLogoutService;
import xyz.zhouxy.plusone.util.RestfulResult;
/**
* Admin
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@RestController
@RequestMapping("logout")
public class AdminLogoutController {
private final AdminLogoutService service;
public AdminLogoutController(AdminLogoutService service) {
this.service = service;
}
@GetMapping
public RestfulResult execute() {
service.execute();
return RestfulResult.success("注销成功");
}
}

View File

@ -0,0 +1,83 @@
package xyz.zhouxy.plusone.system.application.controller;
import static xyz.zhouxy.plusone.system.constant.AuthLogic.adminAuthLogic;
import static xyz.zhouxy.plusone.util.RestfulResult.success;
import java.util.List;
import javax.validation.Valid;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import xyz.zhouxy.plusone.system.application.query.params.DictQueryParams;
import xyz.zhouxy.plusone.system.application.service.DictManagementService;
import xyz.zhouxy.plusone.system.application.service.command.CreateDictCommand;
import xyz.zhouxy.plusone.system.application.service.command.UpdateDictCommand;
import xyz.zhouxy.plusone.util.RestfulResult;
/**
*
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@RestController
@RequestMapping("sys/dict")
public class DictManagementController {
private final DictManagementService service;
public DictManagementController(DictManagementService service) {
this.service = service;
}
@PostMapping
public RestfulResult createDict(@RequestBody @Valid CreateDictCommand command) {
adminAuthLogic.checkPermission("sys-dict-create");
service.createDict(command);
return success();
}
@DeleteMapping
public RestfulResult deleteDicts(@RequestBody List<Long> ids) {
adminAuthLogic.checkPermission("sys-dict-delete");
service.deleteDicts(ids);
return success();
}
@PatchMapping("{id}")
public RestfulResult updateDict(
@PathVariable("id") Long id,
@RequestBody @Valid UpdateDictCommand command) {
adminAuthLogic.checkPermission("sys-dict-update");
service.updateDict(id, command);
return success();
}
@GetMapping("{dictId}")
public RestfulResult findDictDetails(@PathVariable("dictId") Long dictId) {
adminAuthLogic.checkPermission("sys-dict-details");
var dictDetails = service.findDictDetails(dictId);
return success("查询成功", dictDetails);
}
@GetMapping("all")
public RestfulResult loadAllDicts() {
adminAuthLogic.checkPermissionAnd("sys-dict-list", "sys-dict-details");
var dicts = service.loadAllDicts();
return success("查询成功", dicts);
}
@GetMapping("query")
public RestfulResult queryDictOverviewList(@Valid DictQueryParams queryParams) {
adminAuthLogic.checkPermission("sys-dict-list");
var dicts = service.queryDictOverviewList(queryParams);
return success("查询成功", dicts);
}
}

View File

@ -0,0 +1,85 @@
package xyz.zhouxy.plusone.system.application.controller;
import static xyz.zhouxy.plusone.system.constant.AuthLogic.adminAuthLogic;
import static xyz.zhouxy.plusone.util.RestfulResult.success;
import javax.validation.Valid;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import xyz.zhouxy.plusone.system.application.service.MenuManagementService;
import xyz.zhouxy.plusone.system.application.service.command.CreateMenuCommand;
import xyz.zhouxy.plusone.system.application.service.command.UpdateMenuCommand;
import xyz.zhouxy.plusone.util.RestfulResult;
/**
*
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@RestController
@RequestMapping("sys/menu")
public class MenuManagementController {
private final MenuManagementService service;
public MenuManagementController(MenuManagementService service) {
this.service = service;
}
// ==================== create ====================
@PostMapping
public RestfulResult createMenu(@RequestBody @Valid CreateMenuCommand command) {
adminAuthLogic.checkPermission("sys-menu-create");
service.createMenu(command);
return success();
}
// ==================== delete ====================
@DeleteMapping("{id}")
public RestfulResult deleteMenu(@PathVariable("id") Long id) {
adminAuthLogic.checkPermission("sys-menu-delete");
service.deleteMenu(id);
return success();
}
// ==================== update ====================
@PatchMapping("{id}")
public RestfulResult updateMenu(
@PathVariable("id") Long id,
@RequestBody @Valid UpdateMenuCommand command) {
adminAuthLogic.checkPermission("sys-menu-update");
service.updateMenu(id, command);
return success();
}
// ==================== query ====================
@GetMapping("{id}")
public RestfulResult findById(@PathVariable("id") Long id) {
adminAuthLogic.checkPermission("sys-menu-details");
var result = service.findById(id);
return RestfulResult.success("查询成功", result);
}
@GetMapping("queryByAccountId")
public RestfulResult queryByAccountId(@RequestParam Long accountId) {
adminAuthLogic.checkPermission("sys-menu-details");
var result = service.queryByAccountId(accountId);
return success("查询成功", result);
}
@GetMapping("queryByRoleId")
public RestfulResult queryByRoleId(@RequestParam Long roleId) {
adminAuthLogic.checkPermission("sys-menu-details");
var result = service.queryByRoleId(roleId);
return success("查询成功", result);
}
}

View File

@ -0,0 +1,42 @@
package xyz.zhouxy.plusone.system.application.controller;
import static xyz.zhouxy.plusone.util.RestfulResult.success;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import xyz.zhouxy.plusone.system.application.service.RegisterAccountService;
import xyz.zhouxy.plusone.system.application.service.command.RegisterAccountCommand;
import xyz.zhouxy.plusone.util.RestfulResult;
/**
*
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@RestController
@RequestMapping("register")
public class RegisterAccountController {
private final RegisterAccountService service;
public RegisterAccountController(RegisterAccountService service) {
this.service = service;
}
@PostMapping
public RestfulResult registerAccount(@RequestBody RegisterAccountCommand command) {
service.registerAccount(command);
return success("注册成功");
}
@GetMapping("sendCode")
public RestfulResult sendCode(@RequestParam String principal) {
service.sendCode(principal);
return success("发送成功");
}
}

View File

@ -0,0 +1,78 @@
package xyz.zhouxy.plusone.system.application.controller;
import javax.validation.Valid;
import static xyz.zhouxy.plusone.system.constant.AuthLogic.adminAuthLogic;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import xyz.zhouxy.plusone.system.application.query.params.RoleQueryParams;
import xyz.zhouxy.plusone.system.application.service.RoleManagementService;
import xyz.zhouxy.plusone.system.application.service.command.CreateRoleCommand;
import xyz.zhouxy.plusone.system.application.service.command.UpdateRoleCommand;
import xyz.zhouxy.plusone.util.RestfulResult;
/**
*
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@RestController
@RequestMapping("sys/role")
public class RoleManagementController {
private final RoleManagementService service;
public RoleManagementController(RoleManagementService service) {
this.service = service;
}
@PostMapping
public RestfulResult createRole(@RequestBody @Valid CreateRoleCommand command) {
adminAuthLogic.checkPermission("sys-role-create");
service.createRole(command);
return RestfulResult.success();
}
@PatchMapping
public RestfulResult updateRole(@RequestBody @Valid UpdateRoleCommand command) {
adminAuthLogic.checkPermission("sys-role-update");
service.updateRole(command);
return RestfulResult.success();
}
@DeleteMapping("{id}")
public RestfulResult delete(@PathVariable("id") Long id) {
adminAuthLogic.checkPermission("sys-role-delete");
service.delete(id);
return RestfulResult.success();
}
@GetMapping("exists")
public RestfulResult exists(@RequestParam("id") Long id) {
adminAuthLogic.checkPermissionOr("sys-role-list", "sys-role-details");
var isExists = service.exists(id);
return RestfulResult.success(isExists ? "存在" : "不存在", isExists);
}
@GetMapping("{id}")
public RestfulResult findById(@PathVariable("id") Long id) {
adminAuthLogic.checkPermission("sys-role-details");
var result = service.findById(id);
return RestfulResult.success("查询成功", result);
}
@GetMapping("query")
public RestfulResult query(RoleQueryParams params) {
adminAuthLogic.checkPermission("sys-role-list");
var result = service.query(params);
return RestfulResult.success("查询成功", result);
}
}

View File

@ -0,0 +1,38 @@
package xyz.zhouxy.plusone.system.application.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
import xyz.zhouxy.plusone.exception.PlusoneException;
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class AccountLoginException extends PlusoneException {
@java.io.Serial
private static final long serialVersionUID = -3040996790739138556L;
private static final int DEFAULT_ERR_CODE = 4030000;
private AccountLoginException() {
super(DEFAULT_ERR_CODE, "用户登录异常");
}
private AccountLoginException(int code, String message) {
super(code, message);
}
public static AccountLoginException accountNotExistException() {
return new AccountLoginException(4030101, "用户账户不存在");
}
public static AccountLoginException otpErrorException() {
return new AccountLoginException(4030501, "验证码错误");
}
public static AccountLoginException otpNotExistsException() {
return new AccountLoginException(4030502, "验证码不存在或已过期");
}
public static AccountLoginException passwordErrorException() {
return new AccountLoginException(4030200, "用户密码错误");
}
}

View File

@ -0,0 +1,57 @@
package xyz.zhouxy.plusone.system.application.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
import xyz.zhouxy.plusone.exception.PlusoneException;
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class AccountRegisterException extends PlusoneException {
@java.io.Serial
private static final long serialVersionUID = 7580245181633370195L;
public AccountRegisterException() {
this(4020000, "用户注册错误");
}
public AccountRegisterException(String message) {
this(4020000, message);
}
public AccountRegisterException(Throwable cause) {
super(4020000, cause);
}
public AccountRegisterException(int code, String message) {
super(code, message);
}
public AccountRegisterException(int code, Throwable cause) {
super(code, cause);
}
public static AccountRegisterException emailOrMobilePhoneRequiredException() {
return new AccountRegisterException(4020300, "邮箱和手机号应至少绑定一个");
}
public static AccountRegisterException usernameAlreadyExists(String username) {
return new AccountRegisterException(4020400, String.format("用户名 %s 已存在", username));
}
public static AccountRegisterException emailAlreadyExists(String value) {
return new AccountRegisterException(4020500, String.format("邮箱 %s 已存在", value));
}
public static AccountRegisterException mobilePhoneAlreadyExists(String value) {
return new AccountRegisterException(4020600, String.format("手机号 %s 已存在", value));
}
public static AccountRegisterException codeErrorException() {
return new AccountRegisterException(4020701, "校验码错误");
}
public static AccountRegisterException codeNotExistsException() {
return new AccountRegisterException(4020702, "校验码不存在或已过期");
}
}

View File

@ -0,0 +1,17 @@
package xyz.zhouxy.plusone.system.application.exception;
import xyz.zhouxy.plusone.exception.InvalidInputException;
public class UnsupportedMenuTypeException extends InvalidInputException {
@java.io.Serial
private static final long serialVersionUID = -769169844015637730L;
public UnsupportedMenuTypeException() {
this("不支持的菜单类型");
}
public UnsupportedMenuTypeException(String message) {
super(message);
}
}

View File

@ -0,0 +1,56 @@
package xyz.zhouxy.plusone.system.application.exception.handler;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import cn.dev33.satoken.exception.DisableServiceException;
import cn.dev33.satoken.exception.SameTokenInvalidException;
import cn.dev33.satoken.exception.NotBasicAuthException;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.NotRoleException;
import cn.dev33.satoken.exception.NotSafeException;
import cn.dev33.satoken.exception.SaTokenException;
import lombok.extern.slf4j.Slf4j;
import xyz.zhouxy.plusone.exception.handler.BaseExceptionHandler;
import xyz.zhouxy.plusone.util.RestfulResult;
/**
* Sa-Token
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@RestControllerAdvice
@Slf4j
public class SaTokenExceptionHandler extends BaseExceptionHandler {
public SaTokenExceptionHandler(ExceptionInfoHolder exceptionInfoHolder) {
super(exceptionInfoHolder);
set(NotPermissionException.class, 4030103, "会话未能通过权限认证", HttpStatus.FORBIDDEN);
set(NotRoleException.class, 4030103, "会话未能通过角色认证", HttpStatus.FORBIDDEN);
set(DisableServiceException.class, 4030202, "账号指定服务已被封禁", HttpStatus.FORBIDDEN);
set(SameTokenInvalidException.class, 4030400, "提供的 Same-Token 无效", HttpStatus.UNAUTHORIZED);
set(NotBasicAuthException.class, 4030000, "会话未能通过 Http Basic 认证", HttpStatus.UNAUTHORIZED);
set(NotSafeException.class, 4020300, "会话未能通过二级认证", HttpStatus.UNAUTHORIZED);
set(NotLoginException.class,
4020400,
e -> switch (((NotLoginException) e).getType()) {
case NotLoginException.NOT_TOKEN -> "未提供 Token";
case NotLoginException.INVALID_TOKEN -> "Token 无效";
case NotLoginException.TOKEN_TIMEOUT -> "Token 已过期";
case NotLoginException.BE_REPLACED -> "Token 已被顶下线";
case NotLoginException.KICK_OUT -> "Token 已被踢下线";
default -> "当前会话未登录";
},
HttpStatus.UNAUTHORIZED);
set(SaTokenException.class, 4020300, "未通过身份认证或权限认证", HttpStatus.FORBIDDEN);
}
@ExceptionHandler(SaTokenException.class)
public ResponseEntity<RestfulResult> handleSaTokenException(SaTokenException e) {
log.error(e.getMessage(), e);
return buildExceptionResponse(e);
}
}

View File

@ -0,0 +1,32 @@
package xyz.zhouxy.plusone.system.application.query;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import xyz.zhouxy.plusone.system.application.query.params.AccountQueryParams;
import xyz.zhouxy.plusone.system.application.query.result.AccountDetails;
import xyz.zhouxy.plusone.system.application.query.result.AccountOverview;
import xyz.zhouxy.plusone.util.PageDTO;
/**
*
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Mapper
public interface AccountQueries {
default PageDTO<AccountOverview> queryAccountOverviewPage(AccountQueryParams queryParams) {
List<AccountOverview> content = queryAccountOverview(queryParams);
long total = count(queryParams);
return PageDTO.of(content, total);
}
List<AccountOverview> queryAccountOverview(AccountQueryParams queryParams);
long count(AccountQueryParams queryParams);
AccountDetails queryAccountDetails(Long accountId);
}

View File

@ -0,0 +1,39 @@
package xyz.zhouxy.plusone.system.application.query;
import java.util.List;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Component;
import xyz.zhouxy.plusone.sql.SQL;
import xyz.zhouxy.plusone.system.application.query.params.DictQueryParams;
import xyz.zhouxy.plusone.system.application.query.result.DictOverview;
/**
*
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Component
public class DictQueries {
private final NamedParameterJdbcTemplate jdbcTemplate;
public DictQueries(NamedParameterJdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public List<DictOverview> queryDictOverviewList(DictQueryParams queryParams) {
String sql = new SQL()
.SELECT("id", "dict_type", "dict_label",
"created_by", "create_time", "updated_by", "update_time", "count")
.FROM("view_sys_dict_overview")
.WHERE_IF(queryParams.getDictType() != null, "dict_type LIKE '%:dictType%'")
.WHERE_IF(queryParams.getDictLabel() != null, "dict_label LIKE '%:dictLabel%'")
.toString();
return this.jdbcTemplate
.query(sql, new BeanPropertySqlParameterSource(queryParams), new BeanPropertyRowMapper<>(DictOverview.class));
}
}

View File

@ -0,0 +1,10 @@
package xyz.zhouxy.plusone.system.application.query;
/**
*
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public interface PermissionQueries {
// TODO【添加】 权限信息查询器
}

View File

@ -0,0 +1,64 @@
package xyz.zhouxy.plusone.system.application.query;
import java.util.List;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;
import xyz.zhouxy.plusone.sql.SQL;
import xyz.zhouxy.plusone.system.application.query.params.RoleQueryParams;
import xyz.zhouxy.plusone.system.application.query.result.RoleOverview;
/**
*
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Repository
public class RoleQueries {
private final NamedParameterJdbcTemplate jdbcTemplate;
public RoleQueries(NamedParameterJdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
/**
* Role
* <p>
* <b> 使</b>
* </p>
*
* @param params
* @return
*/
public List<RoleOverview> query(RoleQueryParams params) {
String b = new SQL()
.SELECT("id")
.FROM("sys_role")
.WHERE_IF_NOT_NULL(params.getId(), "id = :id")
.WHERE_IF_NOT_NULL(params.getName(), "name = :name")
.WHERE_IF_NOT_NULL(params.getIdentifier(), "identifier = :identifier")
.WHERE_IF_NOT_NULL(params.getStatus(), "status = :status")
.WHERE_IF_NOT_NULL(params.getCreateTimeStart(), "create_time >= :createTimeStart")
.WHERE_IF_NOT_NULL(params.getCreateTimeEnd(), "create_time < :createTimeEnd")
.WHERE_IF_NOT_NULL(params.getUpdateTimeStart(), "update_time >= :updateTimeStart")
.WHERE_IF_NOT_NULL(params.getUpdateTimeEnd(), "update_time < :updateTimeEnd")
.LIMIT(params.getSize())
.OFFSET(params.getOffset())
.toString();
var sql = """
SELECT a.id AS id, a.name AS name, a.identifier AS identifier, a.status AS status
FROM sys_role AS a, (
""" + b + """
) AS b
WHERE a.id = b.id
""";
return this.jdbcTemplate
.query(sql, new BeanPropertySqlParameterSource(params),
new BeanPropertyRowMapper<>(RoleOverview.class));
}
}

View File

@ -0,0 +1,55 @@
package xyz.zhouxy.plusone.system.application.query.params;
import java.time.LocalDate;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import xyz.zhouxy.plusone.util.PagingAndSortingQueryParams;
/**
*
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@ToString
public class AccountQueryParams extends PagingAndSortingQueryParams {
public AccountQueryParams() {
super("id",
"username",
"email",
"mobile_phone",
"status",
"nickname",
"sex",
"created_by",
"create_time",
"updated_by",
"update_time");
}
// TODO【添加】 注解参数校验
private @Getter @Setter Long id;
private @Getter @Setter String username;
private @Getter @Setter String email;
private @Getter @Setter String mobilePhone;
private @Getter @Setter Integer status;
private @Getter @Setter String nickname;
private @Getter @Setter Integer sex;
private @Getter @Setter Long createdBy;
private @Getter @Setter LocalDate createTimeStart;
private @Getter LocalDate createTimeEnd;
private @Getter @Setter Long updatedBy;
private @Getter @Setter LocalDate updateTimeStart;
private @Getter LocalDate updateTimeEnd;
private @Getter @Setter Long roleId;
public void setCreateTimeEnd(LocalDate createTimeEnd) {
this.createTimeEnd = createTimeEnd.plusDays(1);
}
public void setUpdateTimeEnd(LocalDate updateTimeEnd) {
this.updateTimeEnd = updateTimeEnd.plusDays(1);
}
}

View File

@ -0,0 +1,21 @@
package xyz.zhouxy.plusone.system.application.query.params;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;
import xyz.zhouxy.plusone.util.PagingAndSortingQueryParams;
/**
*
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
@Getter
@Setter
@Accessors(chain = true)
@ToString(callSuper = true)
public class DictQueryParams extends PagingAndSortingQueryParams {
String dictType;
String dictLabel;
}

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