Compare commits

..

57 Commits

Author SHA1 Message Date
ayi 2c37cd2c75 测试ci
Sync All Branches to GitHub / sync (push) Successful in 1s
2026-02-10 16:53:19 +08:00
ayi aefb212285 修改gitlab-ci中的镜像标签 2026-02-10 16:45:30 +08:00
ayi 2e18a8a419 网管过滤器,用户id穿透到下游 2026-02-04 16:54:44 +08:00
ayi a6a5d42bea gateway全局异常处理 2026-02-03 16:25:17 +08:00
ayi eaf44e1b4f SaToken接口鉴权 2026-02-03 16:09:47 +08:00
ayi 6da20bdac4 用户登录加载角色到缓存;修改用户角色Key格式 2026-02-02 18:28:33 +08:00
ayi 02010bd9ae 自定义token风格 2026-02-02 17:47:11 +08:00
ayi 660a8430f1 网关整合Sa-Token鉴权 2026-02-02 17:27:09 +08:00
ayi da56185960 添加网关服务,更新包名 2026-02-02 17:01:48 +08:00
ayi 5d8816fb2f 添加网关服务,更新包名 2026-02-02 17:00:42 +08:00
ayi b271f0aa87 nacos服务注册 2026-02-02 16:38:01 +08:00
ayi 50ac15a8f7 nacos动态加载bean 2026-02-02 15:54:44 +08:00
ayi 872be51082 整合nacos配置 2026-02-02 15:33:19 +08:00
ayi da78005ad7 修改数据库连接 2026-02-01 21:12:14 +08:00
ayi f6b3369dc0 添加gitlab-ci.yml 2026-01-31 18:43:51 +08:00
ayi 155376ca6e 项目启动将角色权限写入缓存
Sync All Branches to GitHub / sync (push) Successful in 2s
2026-01-23 18:09:58 +08:00
ayi 865dfa31f5 登录接口验证码使用guava参数校验 2026-01-23 16:48:14 +08:00
ayi 25264daed8 修改数据库配置
Sync All Branches to GitHub / sync (push) Successful in 2s
2026-01-23 15:38:07 +08:00
ayi 37ba2a02ad 完善注册功能事务管理
Sync All Branches to GitHub / sync (push) Successful in 2s
2026-01-20 19:43:19 +08:00
ayi b87abe0e3e 完善验证码登录
Sync All Branches to GitHub / sync (push) Successful in 2s
2026-01-20 19:01:43 +08:00
ayi 31f5a31b9b 用户注册与登录功能(待完善)
Sync All Branches to GitHub / sync (push) Successful in 3s
2026-01-18 21:29:02 +08:00
ayi ffecb1f6d8 权限、角色、关联表建表SQL
Sync All Branches to GitHub / sync (push) Successful in 3s
2026-01-18 19:14:28 +08:00
ayi 160a5fecd9 手机号校验注解
Sync All Branches to GitHub / sync (push) Successful in 2s
2026-01-18 13:23:14 +08:00
hanfuye 1c6a5dc3be 添加jasypt加密配置
Sync All Branches to GitHub / sync (push) Successful in 3s
2026-01-17 15:06:32 +08:00
hanfuye afcf469488 接入阿里云短信服务
Sync All Branches to GitHub / sync (push) Successful in 2s
2026-01-17 13:53:14 +08:00
hanfuye fe2608f3ea 自定义线程池并简单测试
Sync All Branches to GitHub / sync (push) Successful in 2s
2026-01-16 22:43:06 +08:00
ayi e79df6fc1f 重构包名,解决JacksonAutoConfiguration被spring boot默认JacksonAutoConfiguration配置覆盖的问题
Sync All Branches to GitHub / sync (push) Successful in 2s
2026-01-16 09:40:16 +08:00
ayi 3cba48728d 编写短信验证码发送接口
Sync All Branches to GitHub / sync (push) Successful in 2s
2026-01-15 22:59:58 +08:00
ayi c32f06239e 更改ID生成器的开始时间戳
Sync All Branches to GitHub / sync (push) Successful in 3s
2026-01-15 15:35:08 +08:00
ayi f99f645cf7 添加ID生成器
Sync All Branches to GitHub / sync (push) Successful in 2s
2026-01-15 15:32:09 +08:00
ayi 0f069433bc 全局异常捕获;参数校验
Sync All Branches to GitHub / sync (push) Successful in 2s
2026-01-15 15:23:47 +08:00
ayi f9613d2e01 添加jansi,控制台彩色日志
Sync All Branches to GitHub / sync (push) Successful in 2s
2026-01-15 14:25:42 +08:00
ayi 42a209400a 修改代码格式化样式
Sync All Branches to GitHub / sync (push) Successful in 1s
2026-01-15 13:42:15 +08:00
ayi bc7ca98132 更换runner测试
Sync All Branches to GitHub / sync (push) Successful in 1s
2026-01-14 11:17:03 +08:00
ayi 4edbe3438c 修改.gitignore
Sync All Branches to GitHub / sync (push) Successful in 2s
2026-01-12 19:53:21 +08:00
ayi 8901d7401b 修改.gitignore,排除日志
Sync All Branches to GitHub / sync (push) Successful in 3s
2026-01-12 19:48:54 +08:00
hanfuye 1c1490271d 格式化代码
Sync All Branches to GitHub / sync (push) Successful in 3s
2026-01-12 19:39:33 +08:00
hanfuye 2f001c3e9e 修改代码片段 vscode
Sync All Branches to GitHub / sync (push) Successful in 3s
2026-01-12 19:22:41 +08:00
hanfuye 3466810ae8 整合redis并进行简单测试
Sync All Branches to GitHub / sync (push) Successful in 2s
2026-01-12 19:22:18 +08:00
hanfuye 1c4a093b46 添加Sa-Token配置并简单测试
Sync All Branches to GitHub / sync (push) Successful in 3s
2026-01-12 19:00:06 +08:00
hanfuye 2bca8d96d3 用户表建表SQL
Sync All Branches to GitHub / sync (push) Successful in 3s
2026-01-12 18:34:54 +08:00
hanfuye 8bcbb3c332 修改.gitignore,排除日志
Sync All Branches to GitHub / sync (push) Successful in 3s
2026-01-12 17:42:42 +08:00
hanfuye 31d970a3ef 整合flatten-maven-plugin,解决子模块单独打包失败的问题
Sync All Branches to GitHub / sync (push) Successful in 3s
2026-01-12 17:01:27 +08:00
hanfuye 88008a74fd 1、logback日志配置
Sync All Branches to GitHub / sync (push) Successful in 3s
2026-01-12 16:50:36 +08:00
hanfuye 4eb71c4d4d 1、修改包名,避免自动配置被SpringBoot隐性优先规则覆盖
Sync All Branches to GitHub / sync (push) Successful in 3s
2、自定义Jackson starter
2026-01-12 16:28:31 +08:00
hanfuye 5c18560351 自定义Jackson配置,解决LocalDateTime序列化问题
Sync All Branches to GitHub / sync (push) Successful in 3s
2026-01-06 17:28:59 +08:00
hanfuye 275e7dded9 修改数据库连接
Sync All Branches to GitHub / sync (push) Successful in 3s
2026-01-05 13:43:24 +08:00
ayi a836ae23bf 1、整合Druid连接池
Sync All Branches to GitHub / sync (push) Successful in 2s
2、配置日志格式
2025-12-01 17:27:11 +08:00
ayi 4a9860a25c 添加代码同步配置push2github.yml 2025-11-28 19:57:36 +08:00
ayi f921bad9fc 整合Mubatis-Plus 2025-11-27 19:28:07 +08:00
ayi 11b5f6fe65 修改vscode配置 2025-11-27 17:35:19 +08:00
ayi ebdcd3c78a 1、调整日志切面组件
2、添加vscode配置
2025-11-26 11:57:11 +08:00
ayi 149591eea8 修改.gitignore文件 2025-11-26 10:47:17 +08:00
ayi 3eddcf59ad 日志切面组件 2025-11-21 15:28:59 +08:00
ayi 26265bfd0e 添加基础异常接口、业务异常类和统一响应类 2025-11-20 18:25:24 +08:00
ayi f0ccb263d0 格式化配置 2025-11-20 16:43:38 +08:00
ayi ae8d28244b 修改认证模块主类名 2025-11-20 16:33:17 +08:00
102 changed files with 5611 additions and 201 deletions
+36
View File
@@ -0,0 +1,36 @@
name: Sync All Branches to GitHub
on:
push:
branches: [ '*' ] # 监听所有分支的推送事件
jobs:
sync:
runs-on: sync
steps:
- name: Checkout code (Git CLI)
run: |
# 克隆 Gitea 仓库
git clone ${{ github.server_url }}/${{ github.repository }}.git .
# 切换到当前推送的分支
git checkout ${{ github.ref }}
- name: Set up Git identity
run: |
# 配置 Git 用户信息
git config --global user.name "ayi"
git config --global user.email "2294931964@qq.com"
- name: Add GitHub remote repository
run: |
# 添加 GitHub 远程仓库,使用令牌进行身份验证
git remote add github https://${{ secrets.GIT_HUB_TOKEN }}@github.com/xing-jiayi/xiaoyishu.git
- name: Push current branch to GitHub
run: |
# 获取当前推送的分支名(兼容 refs/heads/ 前缀)
CURRENT_BRANCH="${GITHUB_REF#refs/heads/}"
# 拉取 GitHub 对应分支的最新更改,允许无关历史(防止冲突)
git pull github "${CURRENT_BRANCH}" --rebase --allow-unrelated-histories || true
# 推送当前分支到 GitHub 对应分支
git push github "${CURRENT_BRANCH}" --force-with-lease # 安全强制推送(避免覆盖他人提交)
+5 -5
View File
@@ -5,10 +5,7 @@ target/
.kotlin
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
.idea/
*.iws
*.iml
*.ipr
@@ -18,7 +15,6 @@ target/
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
@@ -35,3 +31,7 @@ build/
### Mac OS ###
.DS_Store
### 日志 ###
# 排除所有logs
logs/
+54
View File
@@ -0,0 +1,54 @@
stages:
- sync
sync-to-github:
stage: sync
tags:
- sync
image: ruby:3.3
rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_TAG == null'
when: on_success
- when: never
# 关闭浅克隆,拉取完整提交记录
variables:
GIT_DEPTH: 0
script:
# 1. 初始化Git配置,复用提交者信息
- echo "当前提交者信息:$CI_COMMIT_AUTHOR"
- GIT_AUTHOR_NAME=$(echo "$CI_COMMIT_AUTHOR" | cut -d'<' -f1 | xargs)
- GIT_AUTHOR_EMAIL=$(echo "$CI_COMMIT_AUTHOR" | cut -d'<' -f2 | cut -d'>' -f1 | xargs)
- git config --global user.name "$GIT_AUTHOR_NAME"
- git config --global user.email "$GIT_AUTHOR_EMAIL"
- git config --global http.sslVerify false
# 核心新增:禁用HTTP2,强制使用HTTP/1.1(解决HTTP2 framing layer帧层错误)
- git config --global http.version HTTP/1.1
- git config --global http.proxy $PROXY_ADDR
- git config --global https.proxy $PROXY_ADDR
# 2. 构造带PAT的GitHub推送地址
- GITHUB_PUSH_URL="https://$GITHUB_PAT@${GITHUB_REPO_URL#https://}"
- echo "同步目标:GitHub仓库 $GITHUB_REPO_URL"
# 3. 安全处理GitHub远程仓库
- git remote rm github || true
- git remote add github "$GITHUB_PUSH_URL"
# 4. 核心修复:基于分离HEAD创建本地同名分支(解决refspec不匹配问题)
- echo "基于分离HEAD创建本地分支 $CI_COMMIT_BRANCH"
- git checkout -b $CI_COMMIT_BRANCH
# 5. 拉取GitHub远程最新代码(避免冲突,新分支拉取失败不影响)
- git fetch github || true
# 6. 单分支精准推送(GitHub自动创建不存在的分支)
- echo "开始同步GitLab分支 $CI_COMMIT_BRANCH 到GitHub..."
- git push github $CI_COMMIT_BRANCH:$CI_COMMIT_BRANCH -f
retry:
max: 2
when:
- runner_system_failure
- stuck_or_timeout_failure
- api_failure
+603
View File
@@ -0,0 +1,603 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<profiles version="12">
<profile kind="CodeFormatterProfile" name="P3C-CodeStyle" version="13">
<!--可变参数的... Idea没有对应的配置项,强制insert-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_ellipsis" value="insert"/>
<!--枚举值之间 Idea没有对应的配置项,强制insert-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations" value="insert"/>
<!--org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=Java:SPACE_BEFORE_COMMA-->
<!--org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=Java:SPACE_BEFORE_COMMA
由于IDEA只有一个SPACE_BEFORE_COMMA选项,所以统一设置 insert_space_before_comma 为 do not insert
-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations"
value="do not insert"/>
<!--insert_space_before_comma end-->
<!--org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=Java:SPACE_AFTER_COMMA_IN_TYPE_ARGUMENTS-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments" value="insert"/>
<!--IDEA只有一个配置项SPACE_AFTER_COMMAinsert_space_after_comma*统一设置成insert-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters"
value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations"
value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments"
value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference"
value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations"
value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments"
value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws"
value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters"
value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws"
value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments"
value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation" value="insert"/>
<!--insert_space_after_comma end-->
<!--org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=Java:SPACE_BEFORE_COLON-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for" value="insert"/>
<!--org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=Java:SPACE_AFTER_COLON-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement" value="insert"/>
<!--IDEA不支持配置,默认do not insert-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case" value="do not insert"/>
<!--这个在Eclipse也没有找到配置的地方-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case" value="do not insert"/>
<!--org.eclipse.jdt.core.formatter.insert_space_before_semicolon=Java:SPACE_BEFORE_SEMICOLON
程序导入的时候强制将SPACE_BEFORE_SEMICOLON设置为false
-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for" value="do not insert"/>
<!--SPACE_AFTER_SEMICOLON=true-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources" value="insert"/>
<!--IDEA不支持配置,do not insert-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant"
value="do not insert"/>
<setting
id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration"
value="do not insert"/>
<!--IDEA不支持,使用默认-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator" value="do not insert"/>
<!--org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=Java:<Programmatic>-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_binary_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_binary_operator" value="insert"/>
<!--IDEA不支持配置,使用如下值,两者对应-->
<setting
id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters"
value="insert"/>
<setting
id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments"
value="do not insert"/>
<setting
id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters"
value="do not insert"/>
<!--Java:SPACE_BEFORE_OPENING_ANGLE_BRACKET_IN_TYPE_PARAMETER-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters"
value="do not insert"/>
<!--org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=Java:SPACE_AFTER_CLOSING_ANGLE_BRACKET_IN_TYPE_ARGUMENT-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments"
value="do not insert"/>
<!--org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=Java:<Programmatic>-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block" value="insert"/>
<!--IDEA使用了对应的配置:Java:SPACE_WITHIN_ARRAY_INITIALIZER_BRACES,但感觉不太好,IDEA默认不插入,Eclipse也使用不插入-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer"
value="do not insert"/>
<!--use default insert-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return"
value="insert"/>
<!--use default do not insert -->
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch" value="do not insert"/>
<!--use default insert-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration"
value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration"
value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration"
value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration"
value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw"
value="insert"/>
<!--org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=Java:SPACE_BEFORE_SWITCH_LBRACE-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch" value="insert"/>
<!--org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=Java:SPACE_BEFORE_CLASS_LBRACE-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration"
value="insert"/>
<!--org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=Java:<Programmatic>-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block" value="insert"/>
<!--org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=Java:SPACE_BEFORE_ARRAY_INITIALIZER_LBRACE-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer"
value="insert"/>
<!--org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=Java:SPACE_BEFORE_METHOD_LBRACE-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration"
value="insert"/>
<!--org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=Java:SPACE_AFTER_QUEST-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional" value="insert"/>
<!--org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=Java:SPACE_BEFORE_QUEST-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional" value="insert"/>
<!--org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=Java:SPACE_BEFORE_ANOTATION_PARAMETER_LIST-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation"
value="do not insert"/>
<!--use default do not insert-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_ellipsis" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast" value="do not insert"/>
<setting
id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference"
value="do not insert"/>
<!--下面两个对应IDEA中的一个配置Java:SPACE_AROUND_ASSIGNMENT_OPERATORS,使用insert-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator" value="insert"/>
<!--org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=Java:SPACE_BEFORE_CATCH_PARENTHESES-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch" value="insert"/>
<!--org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=Java:SPACE_BEFORE_METHOD_CALL_PARENTHESES-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation"
value="do not insert"/>
<!--org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=Java:SPACE_BEFORE_TRY_PARENTHESES-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try" value="insert"/>
<!--下面两个对应IDEA中的一个配置Java:SPACE_AROUND_UNARY_OPERATOR,使用do not insert-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_unary_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_unary_operator" value="do not insert"/>
<!--org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=Java:SPACE_BEFORE_IF_PARENTHESES-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if" value="insert"/>
<!--org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=Java:SPACE_BEFORE_WHILE_PARENTHESES-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while" value="insert"/>
<!--org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=Java:SPACE_AFTER_TYPE_CAST-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast" value="do not insert"/>
<!--org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=Java:SPACE_BEFORE_METHOD_PARENTHESES-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration"
value="do not insert"/>
<!--org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=Java:SPACE_BEFORE_FOR_PARENTHESES-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for" value="insert"/>
<!--org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=Java:SPACE_BEFORE_SYNCHRONIZED_PARENTHESES-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized" value="insert"/>
<!--org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=Java:SPACE_BEFORE_SWITCH_PARENTHESES-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch" value="insert"/>
<!--下面两个对应IDEA中的一个配置Java:SPACE_AROUND_LAMBDA_ARROW,使用insert-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow" value="insert"/>
<!--SPACE_WITHIN_EMPTY_ARRAY_INITIALIZER_BRACES-->
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer"
value="do not insert"/>
<!--Idea -> Wrapping And Braces -> Simple classes in one line -->
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration" value="do not insert"/>
<!--Idea -> Wrapping And Braces -> Simple method in one line -->
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body" value="do not insert"/>
<!--因为Idea不支持配置,所以设置为 Idea默认值-->
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_enum_constant"
value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_label" value="insert"/>
<!--Idea可以通过Wrap Always实现 TODO-->
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type" value="insert"/>
<!--Idea -> Wrapping And Braces -> Simple block in one line -> do not select -->
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block" value="insert"/>
<!--Idea -> Wrapping And Braces -> try statement -> catch.... (Java:CATCH_ON_NEW_LINE)-->
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement"
value="do not insert"/>
<!--org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=<Programmatic>-->
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing" value="do not insert"/>
<!--org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=Java:ARRAY_INITIALIZER_RBRACE_ON_NEXT_LINE-->
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer"
value="do not insert"/>
<!--#org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=Java:ARRAY_INITIALIZER_LBRACE_ON_NEXT_LINE-->
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer"
value="do not insert"/>
<!--org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=Java:ELSE_ON_NEW_LINE-->
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement" value="do not insert"/>
<!--org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=Java:WHILE_ON_NEW_LINE-->
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement"
value="do not insert"/>
<!--org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=Java:FINALLY_ON_NEW_LINE-->
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement"
value="do not insert"/>
<!--comment start-->
<setting id="org.eclipse.jdt.core.formatter.comment.line_length" value="120"/>
<!--ENABLE_JAVADOC_FORMATTING-->
<setting id="org.eclipse.jdt.core.formatter.comment.format_javadoc_comments" value="true"/>
<!--org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=<Programmatic>-->
<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment" value="false"/>
<!--IDEA无对应设置,所以关闭对block comment的格式化 -->
<setting id="org.eclipse.jdt.core.formatter.comment.format_block_comments" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries" value="true"/>
<!--org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=Java:KEEP_FIRST_COLUMN_COMMENT-->
<setting id="org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column" value="false"/>
<!--org.eclipse.jdt.core.formatter.use_on_off_tags=FORMATTER_TAGS_ENABLED-->
<setting id="org.eclipse.jdt.core.formatter.use_on_off_tags" value="true"/>
<!--org.eclipse.jdt.core.formatter.disabling_tag=FORMATTER_OFF_TAG-->
<setting id="org.eclipse.jdt.core.formatter.disabling_tag" value="@formatter:off"/>
<!--org.eclipse.jdt.core.formatter.enabling_tag=FORMATTER_ON_TAG-->
<setting id="org.eclipse.jdt.core.formatter.enabling_tag" value="@formatter:on"/>
<!--下面的没有IDEA对应项,在代码里面对IDEA中使用默认值即可,LINE_COMMENT_AT_FIRST_COLUMN BLOCK_COMMENT_AT_FIRST_COLUMN设置为false-->
<setting id="org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.indent_root_tags" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments"
value="false"/>
<setting id="org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_line_comments" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_header" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_source_code" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_html" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.join_lines_in_comments" value="true"/>
<!--和IDEA保持一致,注释换行-->
<setting id="org.eclipse.jdt.core.formatter.comment.indent_parameter_description" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter" value="do not insert"/>
<!--comment end-->
<!--org.eclipse.jdt.core.formatter.blank_lines_after_imports=Java:BLANK_LINES_AFTER_IMPORTS-->
<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_imports" value="1"/>
<!--org.eclipse.jdt.core.formatter.blank_lines_before_imports=Java:BLANK_LINES_BEFORE_IMPORTS-->
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_imports" value="1"/>
<!--org.eclipse.jdt.core.formatter.blank_lines_after_package=Java:BLANK_LINES_AFTER_PACKAGE-->
<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_package" value="1"/>
<!--org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=Java:BLANK_LINES_AROUND_CLASS-->
<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations" value="1"/>
<!--org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=Java:BLANK_LINES_BEFORE_METHOD_BODY-->
<setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body" value="0"/>
<!--org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=Java:<Programmatic>-->
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration" value="0"/>
<!--org.eclipse.jdt.core.formatter.blank_lines_before_field=Java:BLANK_LINES_AROUND_FIELD-->
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_field" value="0"/>
<!--org.eclipse.jdt.core.formatter.blank_lines_before_method=Java:BLANK_LINES_AROUND_METHOD-->
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_method" value="1"/>
<!--org.eclipse.jdt.core.formatter.blank_lines_before_package=Java:BLANK_LINES_BEFORE_PACKAGE-->
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_package" value="0"/>
<!--下面IDEA没有对应设置,使用对应值即可-->
<setting id="org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines" value="2147483647"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_member_type" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_import_groups" value="1"/>
<!--org.eclipse.jdt.core.formatter.indentation.size=Java:IndentOptions:INDENT_SIZE-->
<setting id="org.eclipse.jdt.core.formatter.indentation.size" value="4"/>
<!--org.eclipse.jdt.core.formatter.continuation_indentation=Java:IndentOptions:<Programmatic>-->
<setting id="org.eclipse.jdt.core.formatter.continuation_indentation" value="1"/>
<!--org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=Java:<Programmatic>-->
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header" value="true"/>
<!--org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=Java:IndentOptions:SMART_TABS-->
<setting id="org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations" value="false"/>
<!--org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=Java:INDENT_CASE_FROM_SWITCH-->
<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch" value="true"/>
<!--KEEP_INDENTS_ON_EMPTY_LINES-->
<setting id="org.eclipse.jdt.core.formatter.indent_empty_lines" value="false"/>
<!--org.eclipse.jdt.core.formatter.tabulation.size=Java:IndentOptions:<Programmatic>-->
<setting id="org.eclipse.jdt.core.formatter.tabulation.size" value="4"/>
<!--Java:IndentOptions:<Programmatic>-->
<setting id="org.eclipse.jdt.core.formatter.tabulation.char" value="space"/>
<!--下面IDEA没有对应设置,使用对应值即可-->
<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_block" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header"
value="true"/>
<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header"
value="true"/>
<setting id="org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_body" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header"
value="true"/>
<!--Java:<Programmatic>-->
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_assignment" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_binary_expression" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_resources_in_try" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_conditional_expression" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer" value="16"/>
<!--下面没有对应的IDEA设置,Eclipse先使用对应值-->
<setting id="org.eclipse.jdt.core.formatter.alignment_for_enum_constants" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameterized_type_references" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_type_parameters" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_method_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_compact_if" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_type_arguments" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression"
value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_multiple_fields" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_expressions_in_for_loop_header" value="16"/>
<!--IDEA默认配置在同一行,Eclipse使用对应值即可-->
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_for_statment" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_method_invocation" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_switch_statement" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_enum_constant_declaration"
value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_if_while_statement" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_catch_clause" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_annotation" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_try_clause" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_method_delcaration" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_lambda_declaration" value="common_lines"/>
<!--Java:BINARY_OPERATION_SIGN_ON_NEXT_LINE-->
<setting id="org.eclipse.jdt.core.formatter.wrap_before_binary_operator" value="true"/>
<!--ASSIGNMENT_WRAP 需要设置为 WRAP_AS_NEEDED WRAP_AS_NEEDED . Add in jdt.core-3.12it's not work in previous version -->
<setting id="org.eclipse.jdt.core.formatter.wrap_before_assignment_operator" value="false"/>
<!--IDEA无配置项,Eclipse使用对应值即可-->
<setting id="org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_conditional_operator" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line" value="false"/>
<!--org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=Java:KEEP_CONTROL_STATEMENT_IN_ONE_LINE-->
<setting id="org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line" value="false"/>
<!--org.eclipse.jdt.core.formatter.compact_else_if=Java:SPECIAL_ELSE_IF_TREATMENT-->
<setting id="org.eclipse.jdt.core.formatter.compact_else_if" value="true"/>
<!--Java:ALIGN_GROUP_FIELD_DECLARATIONS-->
<setting id="org.eclipse.jdt.core.formatter.align_type_members_on_columns" value="false"/>
<!--Java:<Programmatic>-->
<setting id="org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.join_wrapped_lines" value="true"/>
<!--统一为end_of_lintIDEA默认一致-->
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_method_declaration" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_lambda_body" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block_in_case" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration"
value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_switch" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_array_initializer" value="end_of_line"/>
<!-- <setting id="org.eclipse.jdt.core.compiler.source" value="1.8"/>
<setting id="org.eclipse.jdt.core.compiler.compliance" value="1.8"/>
<setting id="org.eclipse.jdt.core.compiler.codegen.targetPlatform" value="1.8"/>
<setting id="org.eclipse.jdt.core.compiler.problem.enumIdentifier" value="error"/>
<setting id="org.eclipse.jdt.core.compiler.problem.assertIdentifier" value="error"/>
<setting id="org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode" value="enabled"/>
-->
<!--Java:KEEP_SIMPLE_BLOCKS_IN_ONE_LINE-->
<setting id="org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line" value="false"/>
<!--Java:CLASS_BRACE_STYLE,统一使用end_of_line TODO-->
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_constant" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_type_declaration" value="end_of_line"/>
<!--org.eclipse.jdt.core.formatter.lineSplit=RIGHT_MARGIN-->
<setting id="org.eclipse.jdt.core.formatter.lineSplit" value="120"/>
</profile>
</profiles>
+80
View File
@@ -0,0 +1,80 @@
{
// Place your xiaoyishu 工作区 snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
// Placeholders with the same ids are connected.
// Example:
// "Print to console": {
// "scope": "javascript,typescript",
// "prefix": "log",
// "body": [
// "console.log('$1');",
// "$2"
// ],
// "description": "Log output to console"
// }
"类注释": {
"prefix": "ccj",
"body": [
"/**",
" *",
" * @author ${1:}",
" * @version V1.0",
" * @title ${TM_FILENAME_BASE}",
" * @date ${CURRENT_YEAR}/${CURRENT_MONTH}/${CURRENT_DATE} ${CURRENT_HOUR}:${CURRENT_MINUTE}",
" * @description ${2:}",
" */",
""
],
"description": "生成类注释"
},
"接口注释": {
"prefix": "icj",
"body": [
"/**",
" *",
" * @author ${1:}",
" * @version V1.0",
" * @title ${TM_FILENAME_BASE}",
" * @date ${CURRENT_YEAR}/${CURRENT_MONTH}/${CURRENT_DATE} ${CURRENT_HOUR}:${CURRENT_MINUTE}",
" * @description ${2:}",
" */",
""
],
"description": "生成接口注释"
},
"注解注释": {
"prefix": "acj",
"body": [
"/**",
" *",
" * @author ${1:}",
" * @version V1.0",
" * @title ${TM_FILENAME_BASE}",
" * @date ${CURRENT_YEAR}/${CURRENT_MONTH}/${CURRENT_DATE} ${CURRENT_HOUR}:${CURRENT_MINUTE}",
" * @description ${2:}",
" */",
""
],
"description": "生成注解注释"
},
"字符串常量": {
"prefix": "prfs",
"body": ["private final static String ${1:NAME} = \"${1:NAME}\";"],
"description": "生成字符串常量"
},
"Long常量": {
"prefix": "prfl",
"body": ["private final static Long ${1:NAME} = ${1:NAME}L;"],
"description": "生成Long常量"
},
"Generate serialVersionUID": {
"prefix": "serial", // 输入 "serial" 触发模板
"body": [
"private static final long serialVersionUID = ${:${generateSerialVersionUID}}L;"
],
"description": "生成 Serializable 类的 serialVersionUID"
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"configurations": [
{
"type": "java",
"name": "Spring Boot-XiaoyiAuthApplication<xiaoyi-auth>",
"request": "launch",
"cwd": "${workspaceFolder}",
"mainClass": "top.crushtj.xiaoyishu.auth.XiaoyiAuthApplication",
"projectName": "xiaoyi-auth",
"args": "",
"vmArgs": "-Djasypt.encryptor.password=GhaU7VjZd2b3M4Hbx4SelEXZc",
"envFile": "${workspaceFolder}/.env"
}
]
}
+8 -8
View File
@@ -1,11 +1,11 @@
{
"editor.fontSize": 14,
"files.autoSave": "onFocusChange",
"maven.executable.options": "-s D:\\Programs\\Dev\\maven\\conf\\settings-aliyun.xml",
"java.configuration.maven.userSettings": "D:\\Programs\\Dev\\maven\\conf\\settings-aliyun.xml",
"java.configuration.maven.globalSettings": "D:\\Programs\\Dev\\maven\\conf\\settings-aliyun.xml",
"editor.accessibilitySupport": "auto",
"editor.fontFamily": "JetBrains Mono",
"maven.executable.options": "-s D:\\Programs\\Dev\\maven\\conf\\settings-aliyun.xml -DskipTests",
"java.format.settings.url": ".vscode/java-formatter.xml",
"java.compile.nullAnalysis.mode": "automatic",
"java.configuration.updateBuildConfiguration": "interactive"
"java.configuration.updateBuildConfiguration": "automatic",
"[java]": {
"editor.tabSize": 4
},
"maven.executable.path": "D:\\Programs\\Dev\\maven\\bin\\mvn.cmd",
"maven.view": "hierarchical"
}
+153
View File
@@ -19,6 +19,7 @@
<modules>
<module>xiaoyi-auth</module>
<module>xiaoyi-framework</module>
<module>xiaoyi-gateway</module>
</modules>
<properties>
@@ -38,11 +39,42 @@
<spring-boot.version>3.0.2</spring-boot.version>
<spring-cloud-alibaba.version>2022.0.0.0</spring-cloud-alibaba.version>
<spring-cloud.version>2022.0.0</spring-cloud.version>
<jackson.version>2.16.1</jackson.version>
<mysql-connector-java.version>8.0.29</mysql-connector-java.version>
<mybatis-plus.version>3.5.5</mybatis-plus.version>
<druid.version>1.2.27</druid.version>
<flatten-maven-plugin.version>1.5.0</flatten-maven-plugin.version>
<jansi.version>1.18</jansi.version>
<sa-token.version>1.38.0</sa-token.version>
<guava.version>33.0.0-jre</guava.version>
<hutool.version>5.8.26</hutool.version>
<commons-lang3.version>3.12.0</commons-lang3.version>
<dypnsapi.version>2.0.0</dypnsapi.version>
<jasypt-starter.version>3.0.5</jasypt-starter.version>
<nacos-config.version>0.3.0-RC</nacos-config.version>
</properties>
<!-- 统一依赖管理 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>top.crushtj</groupId>
<artifactId>xiaoyi-common</artifactId>
<version>${revision}</version>
</dependency>
<!-- 业务接口日志组件 -->
<dependency>
<groupId>top.crushtj</groupId>
<artifactId>xiaoyi-spring-boot-starter-biz-operationlog</artifactId>
<version>${revision}</version>
</dependency>
<!-- 业务接口日志组件 -->
<dependency>
<groupId>top.crushtj</groupId>
<artifactId>xiaoyi-spring-boot-starter-jackson</artifactId>
<version>${revision}</version>
</dependency>
<!-- Spring Boot 官方依赖管理 -->
<dependency>
<groupId>org.springframework.boot</groupId>
@@ -77,6 +109,100 @@
<version>${lombok.version}</version>
</dependency>
<!-- Jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Mybatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector-java.version}</version>
</dependency>
<!-- Druid 数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<!-- sa-token 认证组件 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>${sa-token.version}</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-reactor-spring-boot3-starter</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>${sa-token.version}</version>
</dependency>
<dependency>
<groupId>org.fusesource.jansi</groupId>
<artifactId>jansi</artifactId>
<version>${jansi.version}</version>
</dependency>
<!-- 常用工具类 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<!--阿里云短信认证服务-->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dypnsapi20170525</artifactId>
<version>${dypnsapi.version}</version>
</dependency>
<!-- jasypt 加密工具 -->
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>${jasypt-starter.version}</version>
</dependency>
<!-- Nacos配置中心 -->
<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>nacos-config-spring-boot-starter</artifactId>
<version>${nacos-config.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
@@ -116,6 +242,33 @@
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>flatten-maven-plugin</artifactId>
<version>${flatten-maven-plugin.version}</version>
<configuration>
<flattenMode>resolveCiFriendliesOnly</flattenMode>
<updatePomFile>true</updatePomFile>
</configuration>
<executions>
<execution>
<id>flatten</id>
<phase>process-resources</phase>
<goals>
<goal>flatten</goal>
</goals>
</execution>
<execution>
<id>flatten.clean</id>
<phase>clean</phase>
<goals>
<goal>clean</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
+100
View File
@@ -18,6 +18,21 @@
<description>小壹书:认证服务(负责处理用户登录、注册、账号注销等)</description>
<dependencies>
<dependency>
<groupId>top.crushtj</groupId>
<artifactId>xiaoyi-common</artifactId>
</dependency>
<!-- 业务接口日志组件 -->
<dependency>
<groupId>top.crushtj</groupId>
<artifactId>xiaoyi-spring-boot-starter-biz-operationlog</artifactId>
</dependency>
<!--Jackson-->
<dependency>
<groupId>top.crushtj</groupId>
<artifactId>xiaoyi-spring-boot-starter-jackson</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
@@ -28,6 +43,79 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Mybatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- Druid 数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
</dependency>
<!-- sa-token 认证组件 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Redis 连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- 阿里云短信认证服务 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dypnsapi20170525</artifactId>
</dependency>
<!-- jasypt 加密工具 -->
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Nacos 配置中心 -->
<!-- <dependency> -->
<!-- <groupId>com.alibaba.boot</groupId> -->
<!-- <artifactId>nacos-config-spring-boot-starter</artifactId> -->
<!-- </dependency> -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!-- 服务注册发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
<build>
@@ -37,6 +125,18 @@
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
</build>
</project>
@@ -0,0 +1,21 @@
package top.crushtj.xiaoyi.auth;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
*
* @author ayi
* @title XiaoyiAuthApplication
* @description 启动类
* @date 2025/11/20
*/
@SpringBootApplication
public class XiaoyiAuthApplication {
public static void main(String[] args) {
SpringApplication.run(XiaoyiAuthApplication.class, args);
}
}
@@ -0,0 +1,39 @@
package top.crushtj.xiaoyi.auth.alarm;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import top.crushtj.xiaoyi.auth.alarm.impl.MailAlarmHelper;
import top.crushtj.xiaoyi.auth.alarm.impl.SmsAlarmHelper;
import top.crushtj.xiaoyi.auth.constant.XiaoyiAuthConstants;
/**
* @author ayi
* @version V1.0
* @title AlarmConfig
* @date 2026/2/2 15:37
* @description 告警配置类
*/
@Configuration
@RefreshScope
public class AlarmConfig {
@Value("${alarm.type}")
private String alarmType;
@Bean
@RefreshScope
public AlarmInterface alarmHelper() {
// 根据配置文件中的告警类型,初始化选择不同的告警实现类
if (StringUtils.equals(XiaoyiAuthConstants.ALARM_TYPE_SMS, alarmType)) {
return new SmsAlarmHelper();
} else if (StringUtils.equals(XiaoyiAuthConstants.ALARM_TYPE_EMAIL, alarmType)) {
return new MailAlarmHelper();
} else {
throw new IllegalArgumentException("错误的告警类型...");
}
}
}
@@ -0,0 +1,20 @@
package top.crushtj.xiaoyi.auth.alarm;
/**
* @author ayi
* @version V1.0
* @title AlarmInterface
* @date 2026/2/2 15:37
* @description 告警接口
*/
public interface AlarmInterface {
/**
* 发送告警信息
*
* @param message 告警信息
* @return 发送结果
*/
boolean send(String message);
}
@@ -0,0 +1,31 @@
package top.crushtj.xiaoyi.auth.alarm.impl;
import lombok.extern.slf4j.Slf4j;
import top.crushtj.xiaoyi.auth.alarm.AlarmInterface;
/**
* @author ayi
* @version V1.0
* @title MailAlarmHelper
* @date 2026/2/2 15:38
* @description 邮件告警通知类
*/
@Slf4j
public class MailAlarmHelper implements AlarmInterface {
/**
* 发送告警信息
*
* @param message 告警信息
* @return 发送结果
*/
@Override
public boolean send(String message) {
log.info("==> 【邮件告警】:{}", message);
// 业务逻辑...
return true;
}
}
@@ -0,0 +1,28 @@
package top.crushtj.xiaoyi.auth.alarm.impl;
import lombok.extern.slf4j.Slf4j;
import top.crushtj.xiaoyi.auth.alarm.AlarmInterface;
/**
* @author ayi
* @version V1.0
* @title SmsAlarmHelper
* @date 2026/2/2 15:39
* @description 短信告警通知类
*/
@Slf4j
public class SmsAlarmHelper implements AlarmInterface {
/**
* 发送告警信息
*
* @param message 告警信息
* @return 发送结果
*/
@Override
public boolean send(String message) {
log.info("==> 【短信告警】:{}", message);
return false;
}
}
@@ -0,0 +1,31 @@
package top.crushtj.xiaoyi.auth.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
*
* @author ayi
* @title MyBatisConfig
* @date 2025/11/27
* @description MybatisPlus配置类
*/
@Configuration
@MapperScan("top.crushtj.**.mappers")
public class MybatisPlusConfig {
/**
* 分页插件
*
* @return MybatisPlusInterceptor
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}
@@ -0,0 +1,41 @@
package top.crushtj.xiaoyi.auth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.lang.NonNull;
/**
*
* @author ayi
* @version V1.0
* @title RedisTemplateConfig
* @description Redis 配置类
* @date 2026/01/12 19:13
*/
@Configuration
public class RedisTemplateConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(@NonNull RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 设置 RedisTemplate 的连接工厂
redisTemplate.setConnectionFactory(connectionFactory);
// 使用 StringRedisSerializer 来序列化和反序列化 redis 的 key 值,确保 key 是可读的字符串
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// 使用 Jackson2JsonRedisSerializer 来序列化和反序列化 redis 的 value 值, 确保存储的是 JSON 格式
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
redisTemplate.setValueSerializer(serializer);
redisTemplate.setHashValueSerializer(serializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
@@ -0,0 +1,48 @@
package top.crushtj.xiaoyi.auth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import static top.crushtj.xiaoyi.auth.constant.ConfigConstants.*;
/**
* @author ayi
* @version V1.0
* @title ThreadPoolConfig
* @date 2026/01/16 22:26
* @description 线程池配置类
*/
@Configuration
public class ThreadPoolConfig {
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数
executor.setCorePoolSize(CORE_POOL_SIZE);
// 最大线程数
executor.setMaxPoolSize(MAX_POOL_SIZE);
// 队列容量
executor.setQueueCapacity(QUEUE_CAPACITY);
// 线程活跃时间(秒)
executor.setKeepAliveSeconds(KEEP_ALIVE_SECONDS);
// 线程名前缀
executor.setThreadNamePrefix(THREAD_NAME_PREFIX);
// 拒绝策略:由调用线程处理(一般为主线程)
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 等待所有任务结束后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
// 设置等待时间,如果超过这个时间还没有销毁就强制销毁,以确保应用最后能够被关闭,而不是被没有完成的任务阻塞
executor.setAwaitTerminationSeconds(AWAIT_TERMINATION_SECONDS);
executor.initialize();
return executor;
}
}
@@ -0,0 +1,35 @@
package top.crushtj.xiaoyi.auth.constant;
/**
* @author ayi
* @version V1.0
* @title ConfigConstants
* @date 2026/01/16
* @description 配置常量类
*/
public class ConfigConstants {
/**
* 核心线程数
*/
public static final int CORE_POOL_SIZE = 10;
/**
* 最大线程数
*/
public static final int MAX_POOL_SIZE = 50;
/**
* 队列容量
*/
public static final int QUEUE_CAPACITY = 200;
/**
* 线程活跃时间(秒)
*/
public static final int KEEP_ALIVE_SECONDS = 30;
/**
* 线程名前缀
*/
public static final String THREAD_NAME_PREFIX = "AuthExecutor-";
public static final int AWAIT_TERMINATION_SECONDS = 60;
}
@@ -0,0 +1,68 @@
package top.crushtj.xiaoyi.auth.constant;
/**
*
* @author ayi
* @version V1.0
* @title RedisKeyConstants
* @description Redis key 常量
* @date 2026/01/15 18:45
*/
public class RedisKeyConstants {
/**
* 验证码 KEY 前缀
*/
private static final String VERIFICATION_CODE_KEY_PREFIX = "verification_code:";
/**
* 验证码 KEY 过期时间 (分钟)
*/
public static final long VERIFICATION_CODE_EXPIRE_TIME = 3;
/**
* 用户角色数据 KEY 前缀
*/
private static final String USER_ROLES_KEY_PREFIX = "user:roles:";
/**
* 小壹书全局 ID 生成器 KEY
*/
public static final String XIAOYI_ID_GENERATOR_KEY = "xiaoyishu.id.generator";
/**
* 角色权限数据 KEY 前缀
*/
public static final String ROLE_PERMISSIONS_KEY_PREFIX = "role:permissions:";
/**
* 构建验证码 KEY
*
* @param phone 手机号
* @return 验证码 KEY
*/
public static String buildVerificationCodeKey(String phone) {
return VERIFICATION_CODE_KEY_PREFIX + phone;
}
/**
* 构建用户角色数据 KEY
*
* @param userId 用户手机号
* @return 用户角色数据 KEY
*/
public static String buildUserRolesKey(Long userId) {
return USER_ROLES_KEY_PREFIX + userId;
}
/**
* 构建角色权限数据 KEY
*
* @param roleKey 角色 ID
* @return 角色权限数据 KEY
*/
public static String buildRolePermissionsKey(String roleKey) {
return ROLE_PERMISSIONS_KEY_PREFIX + roleKey;
}
}
@@ -0,0 +1,16 @@
package top.crushtj.xiaoyi.auth.constant;
/**
* @author ayi
* @version V1.0
* @title RoleConstants
* @date 2026/01/18 21:24
* @description 角色常量
*/
public class RoleConstants {
/**
* 普通用户的角色 ID
*/
public static final Long COMMON_USER_ROLE_ID = 1L;
}
@@ -0,0 +1,17 @@
package top.crushtj.xiaoyi.auth.constant;
/**
* @author ayi
* @version V1.0
* @title XiaoyiAuthConstants
* @date 2026/01/19 19:40
* @description 小一书常量
*/
public class XiaoyiAuthConstants {
public static final String NICK_NAME_PREFIX = "咿呀_";
public static final Long XIAOYI_ID_INITIAL_VALUE = 1000000L;
public static final String ALARM_TYPE_SMS = "sms";
public static final String ALARM_TYPE_EMAIL = "email";
}
@@ -0,0 +1,30 @@
package top.crushtj.xiaoyi.auth.controller;
import com.alibaba.nacos.api.config.annotation.NacosValue;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import top.crushtj.xiaoyi.auth.alarm.AlarmInterface;
@Slf4j
@RestController
public class TestController {
@NacosValue(value = "${rate-limit.api.limit}",autoRefreshed = true)
private Integer limit;
@Resource
private AlarmInterface alarm;
@GetMapping("/test")
public String test() {
return "当前限流阈值为: " + limit;
}
@GetMapping("/alarm")
public String sendAlarm() {
alarm.send("系统出错啦,犬小哈这个月绩效没了,速度上线解决问题!");
return "alarm success";
}
}
@@ -0,0 +1,41 @@
package top.crushtj.xiaoyi.auth.controller;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import top.crushtj.framework.biz.operationlog.aspect.ApiOperationLog;
import top.crushtj.framework.common.response.Response;
import top.crushtj.xiaoyi.auth.model.vo.user.UserLoginReqVO;
import top.crushtj.xiaoyi.auth.service.UserService;
/**
* @author ayi
* @version V1.0
* @title UserController
* @date 2026-01-18 20:40:21
* @description 用户表(User)表控制层
*/
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private UserService userService;
@PostMapping("/login")
@ApiOperationLog(description = "用户登录或注册")
public Response<String> loginOrRegister(@RequestBody @Validated UserLoginReqVO userLoginReqVO) {
return userService.loginOrRegister(userLoginReqVO);
}
@PostMapping("/logout")
@ApiOperationLog(description = "用户登出")
public Response<Void> logout(@RequestHeader("userId") String userId) {
// todo 实现用户登出逻辑
return Response.success();
}
}
@@ -0,0 +1,33 @@
package top.crushtj.xiaoyi.auth.controller;
import top.crushtj.framework.biz.operationlog.aspect.ApiOperationLog;
import top.crushtj.framework.common.response.Response;
import top.crushtj.xiaoyi.auth.model.vo.verificationcode.SendVerificationCodeReqVO;
import top.crushtj.xiaoyi.auth.service.VerificationCodeService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* @author ayi
* @version V1.0
* @title VerificationCodeController
* @date 2026/01/15
* @description 验证码控制器
*/
@Slf4j
@RestController
public class VerificationCodeController {
@Resource
private VerificationCodeService verificationCodeService;
@PostMapping("/verification/code/send")
@ApiOperationLog(description = "发送短信验证码")
public Response<?> send(@Validated @RequestBody SendVerificationCodeReqVO sendVerificationCodeReqVO) {
return verificationCodeService.send(sendVerificationCodeReqVO);
}
}
@@ -0,0 +1,104 @@
package top.crushtj.xiaoyi.auth.domain.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* @author ayi
* @version V1.0
* @title PermissionEntity
* @date 2026-01-19 19:47:27
* @description 权限表(t_permission)表实体类
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName("t_permission")
public class PermissionEntity implements Serializable {
@Serial
private static final long serialVersionUID = -30618877661010662L;
/**
* 主键ID
*/
@TableId("ID")
private Long id;
/**
* 父ID
*/
@TableField("PARENT_ID")
private Long parentId;
/**
* 权限名称
*/
@TableField("NAME")
private String name;
/**
* 类型(1:目录 2:菜单 3:按钮)
*/
@TableField("TYPE")
private Integer type;
/**
* 菜单路由
*/
@TableField("MENU_URL")
private String menuUrl;
/**
* 菜单图标
*/
@TableField("MENU_ICON")
private String menuIcon;
/**
* 管理系统中的显示顺序
*/
@TableField("SORT")
private Integer sort;
/**
* 权限标识
*/
@TableField("PERMISSION_KEY")
private String permissionKey;
/**
* 状态(1:启用;0:禁用)
*/
@TableField("STATUS")
private Integer status;
/**
* 创建时间
*/
@TableField("CREATE_TIME")
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField("UPDATE_TIME")
private LocalDateTime updateTime;
/**
* 逻辑删除(0:未删除 1:已删除)
*/
@TableField("IS_DELETED")
private Boolean isDeleted;
}
@@ -0,0 +1,86 @@
package top.crushtj.xiaoyi.auth.domain.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* @author ayi
* @version V1.0
* @title RoleEntity
* @date 2026-01-19 19:48:23
* @description 角色表(t_role)表实体类
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName("t_role")
public class RoleEntity implements Serializable {
@Serial
private static final long serialVersionUID = -77681371692201000L;
/**
* 主键ID
*/
@TableId("ID")
private Long id;
/**
* 角色名
*/
@TableField("ROLE_NAME")
private String roleName;
/**
* 角色唯一标识
*/
@TableField("ROLE_KEY")
private String roleKey;
/**
* 状态(1:启用 0:禁用)
*/
@TableField("STATUS")
private Integer status;
/**
* 管理系统中的显示顺序
*/
@TableField("SORT")
private Integer sort;
/**
* 备注
*/
@TableField("REMARK")
private String remark;
/**
* 创建时间
*/
@TableField("CREATE_TIME")
private LocalDateTime createTime;
/**
* 最后一次更新时间
*/
@TableField("UPDATE_TIME")
private LocalDateTime updateTime;
/**
* 逻辑删除(0:未删除 1:已删除)
*/
@TableField("IS_DELETED")
private Boolean isDeleted;
}
@@ -0,0 +1,68 @@
package top.crushtj.xiaoyi.auth.domain.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* @author ayi
* @version V1.0
* @title RolePermissionRelEntity
* @date 2026-01-19 19:48:31
* @description 用户权限表(t_role_permission_rel)表实体类
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName("t_role_permission_rel")
public class RolePermissionRelEntity implements Serializable {
@Serial
private static final long serialVersionUID = 345004944667469434L;
/**
* 主键ID
*/
@TableId("ID")
private Long id;
/**
* 角色ID
*/
@TableField("ROLE_ID")
private Long roleId;
/**
* 权限ID
*/
@TableField("PERMISSION_ID")
private Long permissionId;
/**
* 创建时间
*/
@TableField("CREATE_TIME")
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField("UPDATE_TIME")
private LocalDateTime updateTime;
/**
* 逻辑删除(0:未删除 1:已删除)
*/
@TableField("IS_DELETED")
private Boolean isDeleted;
}
@@ -0,0 +1,117 @@
package top.crushtj.xiaoyi.auth.domain.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* @author ayi
* @version V1.0
* @title UserEntity
* @date 2026-01-19 19:49:50
* @description 用户表(t_user)表实体类
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName("t_user")
public class UserEntity implements Serializable {
@Serial
private static final long serialVersionUID = -47473970233354078L;
/**
* 主键ID
*/
@TableId("ID")
private Long id;
/**
* 小壹书号(唯一凭证)
*/
@TableField("XIAOYISHU_ID")
private String xiaoyishuId;
/**
* 密码
*/
@TableField("PASSWORD")
private String password;
/**
* 昵称
*/
@TableField("NICKNAME")
private String nickname;
/**
* 头像
*/
@TableField("AVATAR")
private String avatar;
/**
* 生日
*/
@TableField("BIRTHDAY")
private LocalDate birthday;
/**
* 背景图
*/
@TableField("BACKGROUND_IMG")
private String backgroundImg;
/**
* 手机号
*/
@TableField("PHONE")
private String phone;
/**
* 性别(1: 男 2: 女 3: 未知)
*/
@TableField("SEX")
private Integer sex;
/**
* 状态(1:启用 0:禁用)
*/
@TableField("STATUS")
private Integer status;
/**
* 个人简介
*/
@TableField("INTRODUCTION")
private String introduction;
/**
* 创建时间
*/
@TableField("CREATE_TIME")
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField("UPDATE_TIME")
private LocalDateTime updateTime;
/**
* 逻辑删除(0:未删除 1:已删除)
*/
@TableField("IS_DELETED")
private Boolean isDeleted;
}
@@ -0,0 +1,68 @@
package top.crushtj.xiaoyi.auth.domain.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* @author ayi
* @version V1.0
* @title UserRoleRelEntity
* @date 2026-01-19 19:49:59
* @description 用户角色表(t_user_role_rel)表实体类
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName("t_user_role_rel")
public class UserRoleRelEntity implements Serializable {
@Serial
private static final long serialVersionUID = -77498437697772085L;
/**
* 主键ID
*/
@TableId("ID")
private Long id;
/**
* 用户ID
*/
@TableField("USER_ID")
private Long userId;
/**
* 角色ID
*/
@TableField("ROLE_ID")
private Long roleId;
/**
* 创建时间
*/
@TableField("CREATE_TIME")
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField("UPDATE_TIME")
private LocalDateTime updateTime;
/**
* 逻辑删除(0:未删除 1:已删除)
*/
@TableField("IS_DELETED")
private Boolean isDeleted;
}
@@ -0,0 +1,25 @@
package top.crushtj.xiaoyi.auth.domain.mappers;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import top.crushtj.xiaoyi.auth.domain.entity.PermissionEntity;
import java.util.List;
/**
* @author ayi
* @version V1.0
* @title PermissionMapper
* @date 2026-01-19 19:47:28
* @description 权限表(t_permission)表数据库访问层
*/
public interface PermissionMapper extends BaseMapper<PermissionEntity> {
/**
* 查询所有已启用的权限列表
*
* @return 已启用的权限列表
*/
List<PermissionEntity> selectAppEnabledList();
}
@@ -0,0 +1,27 @@
package top.crushtj.xiaoyi.auth.domain.mappers;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import top.crushtj.xiaoyi.auth.domain.entity.RoleEntity;
import java.util.List;
/**
* @author ayi
* @version V1.0
* @title RoleMapper
* @date 2026-01-19 19:48:24
* @description 角色表(t_role)表数据库访问层
*/
public interface RoleMapper extends BaseMapper<RoleEntity> {
/**
* 查询所有启用的角色列表
*
* @return 角色列表
*/
List<RoleEntity> selectEnabledRoleList();
List<String> selectRoleKeyByUserId(Long userId);
}
@@ -0,0 +1,27 @@
package top.crushtj.xiaoyi.auth.domain.mappers;
import org.apache.ibatis.annotations.Param;
import top.crushtj.xiaoyi.auth.domain.entity.RolePermissionRelEntity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import java.util.List;
/**
* @author ayi
* @version V1.0
* @title RolePermissionRelMapper
* @date 2026-01-19 19:48:32
* @description 用户权限表(t_role_permission_rel)表数据库访问层
*/
public interface RolePermissionRelMapper extends BaseMapper<RolePermissionRelEntity> {
/**
* 根据角色ID查询权限
*
* @param roleIds 角色ID列表
* @return 权限列表
*/
List<RolePermissionRelEntity> selectByRoleIds(@Param("roleIds") List<Long> roleIds);
}
@@ -0,0 +1,23 @@
package top.crushtj.xiaoyi.auth.domain.mappers;
import org.apache.ibatis.annotations.Param;
import top.crushtj.xiaoyi.auth.domain.entity.UserEntity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* @author ayi
* @version V1.0
* @title UserMapper
* @date 2026-01-19 19:49:51
* @description 用户表(t_user)表数据库访问层
*/
public interface UserMapper extends BaseMapper<UserEntity> {
/**
* 根据手机号查询用户
* @param phone 手机号
* @return 用户信息
*/
UserEntity selectByPhone(@Param("phone") String phone);
}
@@ -0,0 +1,16 @@
package top.crushtj.xiaoyi.auth.domain.mappers;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import top.crushtj.xiaoyi.auth.domain.entity.UserRoleRelEntity;
/**
* @author ayi
* @version V1.0
* @title UserRoleRelMapper
* @date 2026-01-19 19:50:00
* @description 用户角色表(t_user_role_rel)表数据库访问层
*/
public interface UserRoleRelMapper extends BaseMapper<UserRoleRelEntity> {
}
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="top.crushtj.xiaoyi.auth.domain.mappers.PermissionMapper">
<resultMap type="top.crushtj.xiaoyi.auth.domain.entity.PermissionEntity" id="PermissionMap">
<result property="id" column="id" jdbcType="INTEGER"/>
<result property="parentId" column="parent_id" jdbcType="INTEGER"/>
<result property="name" column="name" jdbcType="VARCHAR"/>
<result property="type" column="type" jdbcType="INTEGER"/>
<result property="menuUrl" column="menu_url" jdbcType="VARCHAR"/>
<result property="menuIcon" column="menu_icon" jdbcType="VARCHAR"/>
<result property="sort" column="sort" jdbcType="INTEGER"/>
<result property="permissionKey" column="permission_key" jdbcType="VARCHAR"/>
<result property="status" column="status" jdbcType="INTEGER"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
<result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
<result property="isDeleted" column="is_deleted" jdbcType="BOOLEAN"/>
</resultMap>
<select id="selectAppEnabledList" resultType="top.crushtj.xiaoyi.auth.domain.entity.PermissionEntity">
SELECT id, name, permission_key
FROM t_permission p
WHERE p.status = 1
AND p.is_deleted = 0
AND type = 3
</select>
</mapper>
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="top.crushtj.xiaoyi.auth.domain.mappers.RoleMapper">
<resultMap type="top.crushtj.xiaoyi.auth.domain.entity.RoleEntity" id="RoleMap">
<result property="id" column="id" jdbcType="INTEGER"/>
<result property="roleName" column="role_name" jdbcType="VARCHAR"/>
<result property="roleKey" column="role_key" jdbcType="VARCHAR"/>
<result property="status" column="status" jdbcType="INTEGER"/>
<result property="sort" column="sort" jdbcType="INTEGER"/>
<result property="remark" column="remark" jdbcType="VARCHAR"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
<result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
<result property="isDeleted" column="is_deleted" jdbcType="BOOLEAN"/>
</resultMap>
<select id="selectEnabledRoleList" resultType="top.crushtj.xiaoyi.auth.domain.entity.RoleEntity">
SELECT id, role_name, role_key
FROM t_role
WHERE status = 1
AND is_deleted = 0
</select>
<select id="selectRoleKeyByUserId" resultType="java.lang.String">
SELECT r.role_key
FROM t_role r
LEFT JOIN t_user_role_rel urr ON r.id = urr.role_id
WHERE urr.user_id = #{userId}
</select>
</mapper>
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="top.crushtj.xiaoyi.auth.domain.mappers.RolePermissionRelMapper">
<resultMap type="top.crushtj.xiaoyi.auth.domain.entity.RolePermissionRelEntity" id="RolePermissionRelMap">
<result property="id" column="id" jdbcType="INTEGER"/>
<result property="roleId" column="role_id" jdbcType="INTEGER"/>
<result property="permissionId" column="permission_id" jdbcType="INTEGER"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
<result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
<result property="isDeleted" column="is_deleted" jdbcType="BOOLEAN"/>
</resultMap>
<select id="selectByRoleIds" resultType="top.crushtj.xiaoyi.auth.domain.entity.RolePermissionRelEntity">
SELECT role_id, permission_id FROM t_role_permission_rel
<where>
<if test="roleIds!=null and !roleIds.empty">
AND role_id IN
<foreach collection="roleIds" item="roleId" open="(" separator="," close=")">
#{roleId}
</foreach>
</if>
</where>
</select>
</mapper>
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="top.crushtj.xiaoyi.auth.domain.mappers.UserMapper">
<resultMap type="top.crushtj.xiaoyi.auth.domain.entity.UserEntity" id="UserMap">
<result property="id" column="id" jdbcType="INTEGER"/>
<result property="xiaoyishuId" column="xiaoyishu_id" jdbcType="VARCHAR"/>
<result property="password" column="password" jdbcType="VARCHAR"/>
<result property="nickname" column="nickname" jdbcType="VARCHAR"/>
<result property="avatar" column="avatar" jdbcType="VARCHAR"/>
<result property="birthday" column="birthday" jdbcType="TIMESTAMP"/>
<result property="backgroundImg" column="background_img" jdbcType="VARCHAR"/>
<result property="phone" column="phone" jdbcType="VARCHAR"/>
<result property="sex" column="sex" jdbcType="INTEGER"/>
<result property="status" column="status" jdbcType="INTEGER"/>
<result property="introduction" column="introduction" jdbcType="VARCHAR"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
<result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
<result property="isDeleted" column="is_deleted" jdbcType="BOOLEAN"/>
</resultMap>
<select id = "selectByPhone" resultType = "top.crushtj.xiaoyi.auth.domain.entity.UserEntity">
SELECT id, password, xiaoyishu_id
FROM t_user
WHERE phone = #{phone}
</select>
</mapper>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="top.crushtj.xiaoyi.auth.domain.mappers.UserRoleRelMapper">
<resultMap type="top.crushtj.xiaoyi.auth.domain.entity.UserRoleRelEntity" id="UserRoleRelMap">
<result property="id" column="id" jdbcType="INTEGER"/>
<result property="userId" column="user_id" jdbcType="INTEGER"/>
<result property="roleId" column="role_id" jdbcType="INTEGER"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
<result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
<result property="isDeleted" column="is_deleted" jdbcType="BOOLEAN"/>
</resultMap>
</mapper>
@@ -0,0 +1,34 @@
package top.crushtj.xiaoyi.auth.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Objects;
/**
* @author ayi
* @version V1.0
* @title LoginTypeEnum
* @date 2026/01/18 20:15
* @description 登录类型枚举
*/
@Getter
@AllArgsConstructor
public enum LoginTypeEnum {
// 验证码
VERIFICATION_CODE(1),
// 密码
PASSWORD(2);
private final Integer value;
public static LoginTypeEnum valueOf(Integer code) {
for (LoginTypeEnum loginTypeEnum : LoginTypeEnum.values()) {
if (Objects.equals(code, loginTypeEnum.getValue())) {
return loginTypeEnum;
}
}
return null;
}
}
@@ -0,0 +1,35 @@
package top.crushtj.xiaoyi.auth.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import top.crushtj.framework.common.exception.BaseExceptionInterface;
/**
*
* @author ayi
* @version V1.0
* @title ResponseCodeEnum
* @description 响应码枚举类
* @date 2026/01/15 14:37
*/
@Getter
@AllArgsConstructor
public enum ResponseCodeEnum implements BaseExceptionInterface {
// -------- 通用异常状态码 --------
SYSTEM_ERROR("AUTH-10000", "系统错误"),
PARAM_NOT_VALID("AUTH-10001", "参数错误"), // -------- 验证码异常状态码 --------
VERIFICATION_CODE_SEND_FREQUENTLY("AUTH-20000", "请求太频繁,请3分钟后再试"),
SMS_SEND_FAILED("AUTH-20001", "短信发送失败,请稍后再试"),
SMS_SEND_TIMEOUT("AUTH-20002", "短信发送超时,请稍后再试"),
VERIFICATION_CODE_ERROR("AUTH-20003", "验证码错误"),
;
/**
* 错误码
*/
private final String errorCode;
/**
* 错误信息
*/
private final String errorMessage;
}
@@ -0,0 +1,103 @@
package top.crushtj.xiaoyi.auth.exception;
import java.util.Optional;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import top.crushtj.framework.common.exception.BizException;
import top.crushtj.framework.common.response.Response;
import top.crushtj.xiaoyi.auth.enums.ResponseCodeEnum;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
/**
*
* @author ayi
* @version V1.0
* @title GlobalExceptionHandler
* @description 全局异常处理器
* @date 2026/01/15 18:40
*/
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* 捕获自定义业务异常
*
* @param request 请求
* @param e 异常
* @return 响应结果
*/
@ExceptionHandler({BizException.class})
@ResponseBody
public Response<Object> handleBizException(HttpServletRequest request, BizException e) {
log.warn("{} request failure, errorCode: {}, errorMessage: {}", request.getRequestURI(), e.getErrorCode(),
e.getErrorMessage());
return Response.failure(e);
}
/**
* 捕获参数校验异常
*
* @param request 请求
* @param e 异常
* @return 响应结果
*/
@ExceptionHandler({MethodArgumentNotValidException.class})
@ResponseBody
public Response<Object> handleMethodArgumentNotValidException(HttpServletRequest request,
MethodArgumentNotValidException e) {
// 参数错误异常码
String errorCode = ResponseCodeEnum.PARAM_NOT_VALID.getErrorCode();
// 获取 BindingResult
BindingResult bindingResult = e.getBindingResult();
StringBuilder sb = new StringBuilder();
// 获取校验不通过的字段,并组合错误信息,格式为: email 邮箱格式不正确, 当前值: '123124qq.com';
Optional.of(bindingResult.getFieldErrors()).ifPresent(errors -> {
errors.forEach(error -> sb.append(error.getField()).append(" ").append(error.getDefaultMessage())
.append(", 当前值: '").append(error.getRejectedValue()).append("'; ")
);
});
// 错误信息
String errorMessage = sb.toString();
log.warn("{} request error, errorCode: {}, errorMessage: {}", request.getRequestURI(), errorCode, errorMessage);
return Response.failure(errorCode, errorMessage);
}
/**
* 其他类型异常
*
* @param request 请求
* @param e 异常
* @return 响应结果
*/
@ExceptionHandler({Exception.class})
@ResponseBody
public Response<Object> handleOtherException(HttpServletRequest request, Exception e) {
log.error("{} request error, ", request.getRequestURI(), e);
return Response.failure(ResponseCodeEnum.SYSTEM_ERROR);
}
@ExceptionHandler({IllegalArgumentException.class})
@ResponseBody
public Response<Object> handleIllegalArgumentException(HttpServletRequest request, IllegalArgumentException e) {
log.error("{} request error, ", request.getRequestURI(), e);
String errorCode = ResponseCodeEnum.PARAM_NOT_VALID.getErrorCode();
String errorMessage = e.getMessage();
return Response.failure(errorCode,errorMessage);
}
}
@@ -0,0 +1,51 @@
package top.crushtj.xiaoyi.auth.model.vo.user;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import top.crushtj.framework.common.validator.PhoneNumber;
import java.io.Serial;
import java.io.Serializable;
/**
* @author ayi
* @version V1.0
* @title UserVo
* @date 2026/01/18 20:13
* @description 用户登录参数
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserLoginReqVO implements Serializable {
@Serial
private static final long serialVersionUID = -2349922278492431614L;
/**
* 手机号
*/
@NotBlank(message = "手机号不能为空")
@PhoneNumber
private String phone;
/**
* 验证码
*/
private String code;
/**
* 密码
*/
private String password;
/**
* 登录类型:手机号验证码,或者是账号密码
*/
@NotNull(message = "登录类型不能为空")
private Integer type;
}
@@ -0,0 +1,32 @@
package top.crushtj.xiaoyi.auth.model.vo.verificationcode;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import top.crushtj.framework.common.validator.PhoneNumber;
/**
*
* @author ayi
* @version V1.0
* @title SendVerificationCodeReqVO
* @description 发送验证码请求参数
* @date 2026/01/15 18:39
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class SendVerificationCodeReqVO {
/**
* 手机号
*/
@NotBlank(message = "手机号不能为空")
@PhoneNumber
private String phoneNumber;
}
@@ -0,0 +1,123 @@
package top.crushtj.xiaoyi.auth.runner;
import cn.hutool.core.collection.CollUtil;
import com.google.common.collect.Lists;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import top.crushtj.framework.common.utils.JsonUtils;
import top.crushtj.xiaoyi.auth.constant.RedisKeyConstants;
import top.crushtj.xiaoyi.auth.domain.entity.PermissionEntity;
import top.crushtj.xiaoyi.auth.domain.entity.RoleEntity;
import top.crushtj.xiaoyi.auth.domain.entity.RolePermissionRelEntity;
import top.crushtj.xiaoyi.auth.domain.mappers.PermissionMapper;
import top.crushtj.xiaoyi.auth.domain.mappers.RoleMapper;
import top.crushtj.xiaoyi.auth.domain.mappers.RolePermissionRelMapper;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
*
* @author ayi
* @version V1.0
* @title PushRolePermissions2RedisRunner
* @date 2026/01/23 16:51
* @description 推送角色权限到Redis
*/
@Slf4j
@Component
public class PushRolePermissions2RedisRunner implements ApplicationRunner {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private RoleMapper roleMapper;
@Resource
private RolePermissionRelMapper rolePermissionRelMapper;
@Resource
private PermissionMapper permissionMapper;
public static final String PUSH_PERMISSION_FLAG = "push.permission.flag";
@Override
public void run(ApplicationArguments args) throws Exception {
log.info("==> 开始同步角色权限数据到 Redis 中...");
try {
// 是否能够同步数据: 原子操作,只有在键 PUSH_PERMISSION_FLAG 不存在时,才会设置该键的值为 "1",并设置过期时间为 1 天
boolean canPushed = Boolean.TRUE.equals(redisTemplate.opsForValue()
.setIfAbsent(PUSH_PERMISSION_FLAG, "1", 1, TimeUnit.DAYS));
// 如果无法同步权限数据
if (!canPushed) {
log.warn("==> 角色权限数据已经同步至 Redis 中,不再同步...");
return;
}
// 查询所有已启用角色
List<RoleEntity> roleList = roleMapper.selectEnabledRoleList();
if (CollUtil.isNotEmpty(roleList)) {
// 获取角色ID集合
List<Long> roleIds = roleList.stream()
.map(RoleEntity::getId)
.toList();
//查询角色对应的权限
List<RolePermissionRelEntity> rolePermissionList = rolePermissionRelMapper.selectByRoleIds(roleIds);
// 按角色 ID 分组, 每个角色 ID 对应多个权限 ID
Map<Long, List<Long>> roleIdPermissionIdsMap = rolePermissionList.stream()
.collect(Collectors.groupingBy(RolePermissionRelEntity::getRoleId,
Collectors.mapping(RolePermissionRelEntity::getPermissionId, Collectors.toList())));
// 查询所有启用的权限
List<PermissionEntity> permissionList = permissionMapper.selectAppEnabledList();
// 构建权限 ID 和权限 DO 对象的映射关系
Map<Long, PermissionEntity> permissionIdEntityMap = permissionList.stream()
.collect(Collectors.toMap(PermissionEntity::getId, permissionEntity -> permissionEntity));
// 构建角色 ID 和权限 DO 列表的映射关系
Map<String, List<String>> roleIdPermissionMap = new HashMap<>();
roleList.forEach(role -> {
Long roleId = role.getId();
//获取角色对应的权限id列表
List<Long> permissionIds = roleIdPermissionIdsMap.get(roleId);
if (CollUtil.isNotEmpty(permissionIds)) {
List<String> permissionKeys = Lists.newArrayList();
permissionIds.forEach(permissionId -> {
// 根据权限 ID 获取具体的权限 DO 对象
PermissionEntity permissionDO = permissionIdEntityMap.get(permissionId);
if (Objects.nonNull(permissionDO)) {
permissionKeys.add(permissionDO.getPermissionKey());
}
});
roleIdPermissionMap.put(role.getRoleKey(), permissionKeys);
}
});
// 将角色 ID 和权限 DO 列表的映射关系写入 Redis
roleIdPermissionMap.forEach((roleKey, permission) -> {
String key = RedisKeyConstants.buildRolePermissionsKey(roleKey);
redisTemplate.opsForValue()
.set(key, JsonUtils.toJsonString(permission));
});
}
log.info("==> 角色权限数据同步到 Redis 完成...");
} catch (Exception e) {
log.error("==> 角色权限数据同步到 Redis 失败...", e);
}
}
}
@@ -0,0 +1,51 @@
package top.crushtj.xiaoyi.auth.runner.cache;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import top.crushtj.xiaoyi.auth.domain.entity.UserEntity;
import top.crushtj.xiaoyi.auth.domain.mappers.UserMapper;
import static top.crushtj.xiaoyi.auth.constant.RedisKeyConstants.XIAOYI_ID_GENERATOR_KEY;
import static top.crushtj.xiaoyi.auth.constant.XiaoyiAuthConstants.XIAOYI_ID_INITIAL_VALUE;
/**
* @author ayi
* @version V1.0
* @title CacheLoader
* @date 2026/01/20 18:25
* @description 初始化缓存
*/
@Slf4j
@Component
public class CacheLoader {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private UserMapper userMapper;
/**
* 加载用户自增ID缓存
*/
@PostConstruct
public void loadUserCache() {
log.info("==> 加载用户自增ID缓存开始...");
LambdaQueryWrapper<UserEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.orderByDesc(UserEntity::getXiaoyishuId);
queryWrapper.last("limit 1");
UserEntity user = userMapper.selectOne(queryWrapper);
if (user != null) {
redisTemplate.opsForValue()
.set(XIAOYI_ID_GENERATOR_KEY, Long.valueOf(user.getXiaoyishuId()));
} else {
redisTemplate.opsForValue()
.set(XIAOYI_ID_GENERATOR_KEY, XIAOYI_ID_INITIAL_VALUE);
}
log.info("==> 加载用户自增ID缓存结束...");
}
}
@@ -0,0 +1,26 @@
package top.crushtj.xiaoyi.auth.service;
import com.baomidou.mybatisplus.extension.service.IService;
import top.crushtj.framework.common.response.Response;
import top.crushtj.xiaoyi.auth.domain.entity.UserEntity;
import top.crushtj.xiaoyi.auth.model.vo.user.UserLoginReqVO;
/**
* @author ayi
* @version V1.0
* @title UserService
* @date 2026-01-18 20:09:57
* @description 用户表(t_user)表服务接口
*/
public interface UserService extends IService<UserEntity> {
/**
* 登录或注册
*
* @param userLoginReqVO 登录或注册参数
* @return 登录结果
*/
Response<String> loginOrRegister(UserLoginReqVO userLoginReqVO);
}
@@ -0,0 +1,25 @@
package top.crushtj.xiaoyi.auth.service;
import top.crushtj.framework.common.response.Response;
import top.crushtj.xiaoyi.auth.model.vo.verificationcode.SendVerificationCodeReqVO;
/**
*
* @author ayi
* @version V1.0
* @title VerificationCodeService
* @description 验证码服务
* @date 2026/01/15 18:48
*/
public interface VerificationCodeService {
/**
* 发送验证码
*
* @param sendVerificationCodeReqVO 发送验证码请求参数
* @return 响应结果
*/
Response<?> send(SendVerificationCodeReqVO sendVerificationCodeReqVO);
}
@@ -0,0 +1,203 @@
package top.crushtj.xiaoyi.auth.service.impl;
import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.base.Preconditions;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
import top.crushtj.framework.common.enums.DeleteEnum;
import top.crushtj.framework.common.enums.StatusEnum;
import top.crushtj.framework.common.exception.BizException;
import top.crushtj.framework.common.response.Response;
import top.crushtj.framework.common.utils.IdGenerator;
import top.crushtj.framework.common.utils.JsonUtils;
import top.crushtj.framework.common.utils.MaskUtils;
import top.crushtj.xiaoyi.auth.constant.RedisKeyConstants;
import top.crushtj.xiaoyi.auth.domain.entity.RoleEntity;
import top.crushtj.xiaoyi.auth.domain.entity.UserEntity;
import top.crushtj.xiaoyi.auth.domain.entity.UserRoleRelEntity;
import top.crushtj.xiaoyi.auth.domain.mappers.RoleMapper;
import top.crushtj.xiaoyi.auth.domain.mappers.UserMapper;
import top.crushtj.xiaoyi.auth.domain.mappers.UserRoleRelMapper;
import top.crushtj.xiaoyi.auth.enums.LoginTypeEnum;
import top.crushtj.xiaoyi.auth.enums.ResponseCodeEnum;
import top.crushtj.xiaoyi.auth.model.vo.user.UserLoginReqVO;
import top.crushtj.xiaoyi.auth.service.UserService;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static top.crushtj.xiaoyi.auth.constant.RedisKeyConstants.XIAOYI_ID_GENERATOR_KEY;
import static top.crushtj.xiaoyi.auth.constant.RoleConstants.COMMON_USER_ROLE_ID;
import static top.crushtj.xiaoyi.auth.constant.XiaoyiAuthConstants.NICK_NAME_PREFIX;
import static top.crushtj.xiaoyi.auth.constant.XiaoyiAuthConstants.XIAOYI_ID_INITIAL_VALUE;
/**
* @author ayi
* @version V1.0
* @title UserServiceImpl
* @date 2026-01-18 20:09:58
* @description 用户表(t_user)表服务实现类
*/
@Slf4j
@Service("userService")
public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> implements UserService {
@Resource
private UserMapper userMapper;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private UserRoleRelMapper userRoleRelMapper;
@Resource
private RoleMapper roleMapper;
@Resource
private TransactionTemplate transactionTemplate;
@Override
public Response<String> loginOrRegister(UserLoginReqVO userLoginReqVO) {
String phone = userLoginReqVO.getPhone();
Integer type = userLoginReqVO.getType();
Long userId = null;
LoginTypeEnum loginTypeEnum = LoginTypeEnum.valueOf(type);
switch (loginTypeEnum) {
case PASSWORD -> {
String password = userLoginReqVO.getPassword();
// 密码登录
}
case VERIFICATION_CODE -> {
String verificationCode = userLoginReqVO.getCode();
// 验证码登录
// 校验入参验证码是否为空
Preconditions.checkArgument(StringUtils.isNotBlank(verificationCode), "验证码不能为空");
// 构建验证码 Redis Key
String key = RedisKeyConstants.buildVerificationCodeKey(phone);
// 查询存储在 Redis 中该用户的登录验证码
String sentCode = (String)redisTemplate.opsForValue()
.get(key);
// 判断用户提交的验证码,与 Redis 中的验证码是否一致
if (!StringUtils.equals(verificationCode, sentCode)) {
throw new BizException(ResponseCodeEnum.VERIFICATION_CODE_ERROR);
}
// 通过手机号查询记录
UserEntity userEntity = userMapper.selectByPhone(phone);
log.info("==> 用户是否注册, phone: {}, userId: {}", MaskUtils.maskMobile(phone),
userEntity == null ? "未注册" : userEntity.getXiaoyishuId());
// 判断是否注册
if (Objects.isNull(userEntity)) {
// 若此用户还没有注册,系统自动注册该用户
// todo 自动注册用户逻辑
userId = registerUser(phone);
} else {
// 已注册,则获取其用户 ID
userId = userEntity.getId();
this.loadUserRole(userId);
}
}
}
StpUtil.login(userId);
// 获取 Token 令牌
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
// 返回 Token 令牌
return Response.success(tokenInfo.tokenValue);
}
/**
* 用户注册
*
* @param phone 手机号
* @return 用户ID
*/
private Long registerUser(String phone) {
return transactionTemplate.execute(status -> {
try {
long userId = IdGenerator.getInstance()
.nextId();
// 小壹书用户ID,非数据库主键ID
Long xiaoyishuId = redisTemplate.opsForValue()
.increment(XIAOYI_ID_GENERATOR_KEY);
UserEntity userEntity = UserEntity.builder()
.id(userId)
.phone(phone)
.xiaoyishuId(String.valueOf(xiaoyishuId))
.nickname(NICK_NAME_PREFIX + xiaoyishuId) // 自动注册的用户昵称默认为"咿呀_" + 用户ID
.status(StatusEnum.ENABLED.getValue())
.createTime(LocalDateTime.now())
.updateTime(LocalDateTime.now())
.isDeleted(DeleteEnum.NO.getValue())
.build();
save(userEntity);
UserRoleRelEntity userRoleRel = UserRoleRelEntity.builder()
.id(IdGenerator.getInstance()
.nextId())
.userId(userId)
.roleId(COMMON_USER_ROLE_ID) // 自动注册的用户角色为普通用户
.createTime(LocalDateTime.now())
.updateTime(LocalDateTime.now())
.isDeleted(DeleteEnum.NO.getValue())
.build();
int i = 1 / 0;
userRoleRelMapper.insert(userRoleRel);
// 将用户角色信息存储到 Redis 中
RoleEntity role = roleMapper.selectById(COMMON_USER_ROLE_ID);
List<String> roles = new ArrayList<>(1);
roles.add(role.getRoleKey());
String userRolesKey = RedisKeyConstants.buildUserRolesKey(userId);
redisTemplate.opsForValue()
.set(userRolesKey, JsonUtils.toJsonString(roles));
return userId;
} catch (Exception e) {
// 回滚事务
log.error("==> 用户注册异常,开始回滚事务: ", e);
status.setRollbackOnly();
// 回滚 redis 自增ID
Long decrement = Long.valueOf(Objects.requireNonNull(redisTemplate.opsForValue()
.get(XIAOYI_ID_GENERATOR_KEY))
.toString()
.trim());
if (decrement.compareTo(XIAOYI_ID_INITIAL_VALUE) > 0) {
log.error("==> 用户注册异常,回滚redis自增ID: {}", decrement);
redisTemplate.opsForValue()
.decrement(XIAOYI_ID_GENERATOR_KEY);
}
}
return null;
});
}
/**
* 加载用户角色信息到缓存
*
* @param userId 用户ID
*/
private void loadUserRole(Long userId) {
String userRolesKey = RedisKeyConstants.buildUserRolesKey(userId);
Boolean hasKey = redisTemplate.hasKey(userRolesKey);
if (!hasKey) {
List<String> roles = roleMapper.selectRoleKeyByUserId(userId);
redisTemplate.opsForValue()
.set(userRolesKey, JsonUtils.toJsonString(roles));
}
}
}
@@ -0,0 +1,102 @@
package top.crushtj.xiaoyi.auth.service.impl;
import cn.hutool.core.util.RandomUtil;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import top.crushtj.framework.common.exception.BizException;
import top.crushtj.framework.common.response.Response;
import top.crushtj.framework.common.utils.MaskUtils;
import top.crushtj.xiaoyi.auth.constant.RedisKeyConstants;
import top.crushtj.xiaoyi.auth.enums.ResponseCodeEnum;
import top.crushtj.xiaoyi.auth.model.vo.verificationcode.SendVerificationCodeReqVO;
import top.crushtj.xiaoyi.auth.service.VerificationCodeService;
import top.crushtj.xiaoyi.auth.sms.AliyunSmsHelper;
import java.util.concurrent.TimeUnit;
import static top.crushtj.xiaoyi.auth.constant.RedisKeyConstants.VERIFICATION_CODE_EXPIRE_TIME;
/**
* 验证码服务实现类
* 修复点:异步异常捕获、Redis过期时间、验证码日志脱敏、手机号校验、线程任务跟踪
*/
@Service
@Slf4j
public class VerificationCodeServiceImpl implements VerificationCodeService {
// 短信发送超时时间(秒)
private static final int SMS_SEND_TIMEOUT_SECONDS = 5;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource(name = "taskExecutor")
private ThreadPoolTaskExecutor taskExecutor;
@Resource
private AliyunSmsHelper aliyunSmsHelper;
@Override
public Response<?> send(SendVerificationCodeReqVO sendVerificationCodeReqVO) {
// 1. 获取并校验手机号格式
String phoneNumber = sendVerificationCodeReqVO.getPhoneNumber();
// 2. 构建Redis Key并检查发送频率
String key = RedisKeyConstants.buildVerificationCodeKey(phoneNumber);
boolean isSent = redisTemplate.hasKey(key);
if (isSent) {
throw new BizException(ResponseCodeEnum.VERIFICATION_CODE_SEND_FREQUENTLY);
}
// 3. 生成6位随机验证码
String verificationCode = RandomUtil.randomNumbers(6);
//=============== 开发环境不实际调用短信发送接口 ===============
// 4. 异步发送短信(用CompletableFuture跟踪任务状态,捕获异常)
//CompletableFuture<Boolean> smsSendFuture = CompletableFuture.supplyAsync(() -> {
// // 设置线程名称,便于日志排查
// Thread.currentThread().setName("sms-send-" + MaskUtils.maskMobile(phoneNumber));
// String signName = "速通互联验证码";
// String templateCode = "100001";
// String templateParam = String.format("{\"code\":\"%s\",\"min\":\"%d\"}", verificationCode, VERIFICATION_CODE_EXPIRE_TIME);
// try {
// return aliyunSmsHelper.sendMessage(signName, templateCode, phoneNumber, templateParam);
// } catch (Exception e) {
// log.error("==> 手机号: {}, 短信发送接口调用异常", MaskUtils.maskMobile(phoneNumber), e);
// return false;
// }
//}, taskExecutor);
//
// //5. 同步等待短信发送结果(超时控制,避免主线程阻塞过久)
//boolean smsSendSuccess;
//try {
// smsSendSuccess = smsSendFuture.get(SMS_SEND_TIMEOUT_SECONDS, TimeUnit.SECONDS);
//} catch (InterruptedException e) {
// log.error("==> 手机号: {}, 短信发送任务被中断", MaskUtils.maskMobile(phoneNumber), e);
// Thread.currentThread().interrupt(); // 恢复中断状态
// throw new BizException(ResponseCodeEnum.SMS_SEND_FAILED);
//} catch (ExecutionException e) {
// log.error("==> 手机号: {}, 短信发送任务执行异常", MaskUtils.maskMobile(phoneNumber), e);
// throw new BizException(ResponseCodeEnum.SMS_SEND_FAILED);
//} catch (TimeoutException e) {
// log.error("==> 手机号: {}, 短信发送任务超时({}秒)", MaskUtils.maskMobile(phoneNumber), SMS_SEND_TIMEOUT_SECONDS, e);
// throw new BizException(ResponseCodeEnum.SMS_SEND_TIMEOUT);
//}
//
//// 6. 短信发送失败则直接抛异常,不存储Redis
//if (!smsSendSuccess) {
// log.error("==> 手机号: {}, 发送验证码失败(第三方接口返回失败)", MaskUtils.maskMobile(phoneNumber));
// throw new BizException(ResponseCodeEnum.SMS_SEND_FAILED);
//}
//=============== 开发环境不实际调用短信发送接口 ===============
// 7. 短信发送成功后,记录日志(验证码脱敏,仅保留后2位)+ 存储Redis
log.info("==> 手机号: {}, 已发送验证码:【****{}】", MaskUtils.maskMobile(phoneNumber), verificationCode.substring(4));
redisTemplate.opsForValue().set(key, verificationCode, VERIFICATION_CODE_EXPIRE_TIME, TimeUnit.MINUTES);
return Response.success(verificationCode);
//return Response.success();
}
}
@@ -0,0 +1,21 @@
package top.crushtj.xiaoyi.auth.sms;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @author ayi
* @version V1.0
* @title AliyunAccessKeyProperties
* @date 2026/01/16 23:31
* @description 阿里云访问密钥配置
*/
@ConfigurationProperties(prefix = "aliyun")
@Component
@Data
public class AliyunAccessKeyProperties {
private String accessKeyId;
private String accessKeySecret;
}
@@ -0,0 +1,42 @@
package top.crushtj.xiaoyi.auth.sms;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author ayi
* @version V1.0
* @title AliyunSmsClientConfig
* @date 2026/01/16 23:48
* @description 阿里云短信配置
*/
@Slf4j
@Configuration
public class AliyunSmsClientConfig {
@Resource
private AliyunAccessKeyProperties aliyunAccessKeyProperties;
@Bean
public com.aliyun.dypnsapi20170525.Client smsClient() {
try {
com.aliyun.credentials.Client credential = new com.aliyun.credentials.Client();
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
.setCredential(credential);
// Endpoint 请参考 https://api.aliyun.com/product/Dypnsapi
config.endpoint = "dypnsapi.aliyuncs.com";
config.accessKeyId = aliyunAccessKeyProperties.getAccessKeyId();
config.accessKeySecret = aliyunAccessKeyProperties.getAccessKeySecret();
return new com.aliyun.dypnsapi20170525.Client(config);
} catch (Exception e) {
log.error("初始化阿里云短信发送客户端错误: ", e);
return null;
}
}
}
@@ -0,0 +1,49 @@
package top.crushtj.xiaoyi.auth.sms;
import com.aliyun.dypnsapi20170525.models.SendSmsVerifyCodeResponse;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import top.crushtj.framework.common.utils.JsonUtils;
import top.crushtj.framework.common.utils.MaskUtils;
/**
* @author ayi
* @version V1.0
* @title AliyunSmsHelper
* @date 2026/01/16 23:36
* @description 阿里云短信工具
*/
@Slf4j
@Component
public class AliyunSmsHelper {
@Resource
private com.aliyun.dypnsapi20170525.Client client;
public boolean sendMessage(String signName, String templateCode, String phoneNumber, String templateParam) {
com.aliyun.dypnsapi20170525.models.SendSmsVerifyCodeRequest sendSmsVerifyCodeRequest = new com.aliyun.dypnsapi20170525.models.SendSmsVerifyCodeRequest()
.setSignName(signName)
.setTemplateCode(templateCode)
.setPhoneNumber(phoneNumber)
.setTemplateParam(templateParam);
com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
try {
log.info("==> 开始短信发送, phone: {}, signName: {}, templateCode: {}, templateParam: {}", MaskUtils.maskMobile(phoneNumber), signName, templateCode, templateParam);
// 发送短信
SendSmsVerifyCodeResponse response = client.sendSmsVerifyCodeWithOptions(sendSmsVerifyCodeRequest, runtime);
boolean success = response.getBody().success;
log.info("==> 短信发送状态: {}, response: {}", success, JsonUtils.toJsonString(response));
return success;
} catch (Exception error) {
log.error("==> 短信发送错误: ", error);
return false;
}
}
}
@@ -1,13 +0,0 @@
package top.crushtj.xiaoyishu.auth;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class XiaoyishuAuthApplication {
public static void main(String[] args) {
SpringApplication.run(XiaoyishuAuthApplication.class, args);
}
}
@@ -1,6 +0,0 @@
spring:
application:
name: xiaoyishu-auth
output:
ansi:
enabled: always
+18
View File
@@ -0,0 +1,18 @@
/$$ /$$ /$$ /$$ /$$
| $$ / $$|__/ |__/ | $$
| $$/ $$/ /$$ /$$$$$$ /$$$$$$ /$$ /$$ /$$ /$$$$$$$| $$$$$$$ /$$ /$$
\ $$$$/ | $$ |____ $$ /$$__ $$| $$ | $$| $$ /$$_____/| $$__ $$| $$ | $$
>$$ $$ | $$ /$$$$$$$| $$ \ $$| $$ | $$| $$| $$$$$$ | $$ \ $$| $$ | $$
/$$/\ $$| $$ /$$__ $$| $$ | $$| $$ | $$| $$ \____ $$| $$ | $$| $$ | $$
| $$ \ $$| $$| $$$$$$$| $$$$$$/| $$$$$$$| $$ /$$$$$$$/| $$ | $$| $$$$$$/
|__/ |__/|__/ \_______/ \______/ \____ $$|__/|_______/ |__/ |__/ \______/
/$$ | $$
| $$$$$$/
\______/
One-stop focus on the whole network hotspot.
:::::::::::::::::::::::::::::::::::::::::::::::::::
author: 刑加一
email: 2294931964@qq.com
about me: https://blog.crushtj.top
:::::::::::::::::::::::::::::::::::::::::::::::::::
@@ -0,0 +1,97 @@
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
# 数据库连接信息
url: ENC(oyIjmfAXMQ/z3uHl88N68RYJ96HJhdLkXMBBpE1wq9kSbYuTlxwZV/F7Qxa6tDAu+MD6I9swGqmYl2ormW/bzxPQdk+rrQANSQVA+ekyPGd97I5RGPfLLGQCw6FExrbhh9uzKGvekSmsIC8h91dTL2sMD8dYHbuRpW9J5knEyHSCZ90Bep1ZOlu2mWNQfdbaqmbafs6Bhjj9Iu4m9SblAMomAbDVT1oREPzjtMX640ro+Ou/YGVYPuLj3LlwVooauGCoswmHK0cA2TCD0RqaIg==)
username: ENC(Hf/tJoMig8FWFHopB7CMlQFuBhu61JbpA64JQWvqrmL+fTtGi6QNuI+sHBNY43nY)
password: ENC(l2k707lyMR9ACwqEHl/d3VOojwmvIyRsH7leB9fa9ppOtbKBae8YnVVuEMcRhEqMTTvcqZ4Otd/TotqZMerL/w==)
druid: # Druid 连接池
initial-size: 5 # 初始化连接池大小
min-idle: 5 # 最小连接池数量
max-active: 100 # 最大连接池数量
max-wait: 60000 # 连接时最大等待时间(单位:毫秒)
test-while-idle: true
time-between-eviction-runs-millis: 60000 # 配置多久进行一次检测,检测需要关闭的连接(单位:毫秒)
min-evictable-idle-time-millis: 300000 # 配置一个连接在连接池中最小生存的时间(单位:毫秒)
max-evictable-idle-time-millis: 900000 # 配置一个连接在连接池中最大生存的时间(单位:毫秒)
validation-query: SELECT 1 FROM DUAL # 配置测试连接是否可用的查询 sql
test-on-borrow: false
test-on-return: false
pool-prepared-statements: false
web-stat-filter:
enabled: true
stat-view-servlet:
enabled: true
url-pattern: /druid/* # 配置监控后台访问路径
login-username: admin # 配置监控后台登录的用户名、密码
login-password: admin
filter:
config:
enabled: true
stat:
enabled: true
log-slow-sql: true # 开启慢 sql 记录
slow-sql-millis: 2000 # 若执行耗时大于 2s,则视为慢 sql
merge-sql: true
wall: # 防火墙
config:
multi-statement-allow: true
data:
redis:
database: 0 # Redis 数据库索引(默认为 0)
host: ENC(C1TWXF+/HzWQBF25uXCdy/0fHoRDXdCW72+NKCIJURg4l3IDnJzl278KmFhfsusX) # Redis 服务器地址
port: 6379 # Redis 服务器连接端口
password: ENC(iK/k0IGPflACqYMUwX4N/sGvCVuysYywLcAO+Ikeqk326V8hCr8dgEGzkiEIwWOo) # Redis 服务器连接密码(默认为空)
timeout: 5s # 读超时时间
connect-timeout: 5s # 链接超时时间
lettuce:
pool:
max-active: 200 # 连接池最大连接数
max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)
min-idle: 0 # 连接池中的最小空闲连接
max-idle: 10 # 连接池中的最大空闲连接
output:
ansi:
enabled: always
application:
name: xiaoyishu-auth
mybatis-plus:
mapper-locations:
- classpath*:top/crushtj/**/*.xml # 匹配所有模块中的 Mapper XML 文件
type-aliases-package: top.crushtj.xiaoyishu.auth.domain.entity # 实体类包路径
configuration:
map-underscore-to-camel-case: true # 开启驼峰命名转换
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 日志输出(调试用)
logging:
level:
top.crushtj.xiaoyishu.auth.domain.mappers: debug
aliyun:
accessKeyId: ENC(0GWRL+sq6nY4HvFY8tGqAJz/21NQm4Ga3Qbek+yiR3dwgk4TZXLpcoflD1pyXI/nrkzgcbZsiXXBd6FXW00GJA==)
accessKeySecret: ENC(VUngnZ/ERJDRHmimA0CB7tla6LKIXB7K6jzRJjOg2kqc3KwtXwin+E3cnpJSe92daR5yLCcMa2kPYwy9h6lPqg==)
jasypt:
encryptor:
password:
algorithm: PBEWithHMACSHA512AndAES_256
key-obtention-iterations: 1000
string-output-type: base64
provider-name: SunJCE
iv-generator-classname: org.jasypt.iv.RandomIvGenerator
#nacos:
# config: # Nacos 配置中心
# access-key: # 身份验证
# secret-key: # 身份验证
# data-id: xiaoyishu-auth # 指定要加载的配置数据的 Data Id
# group: DEFAULT_GROUP # 指定配置数据所属的组
# type: yaml # 指定配置数据的格式
# server-addr: http://127.0.0.1:8848/ # 指定 Nacos 配置中心的服务器地址
# auto-refresh: true # 是否自动刷新配置
# remote-first: true # 是否优先使用远程配置
# bootstrap:
# enable: true # 启动时,预热配置
rate-limit:
api:
limit: 100 # 接口限流阈值
@@ -0,0 +1,60 @@
server:
servlet:
context-path: /xiaoyishu-auth
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
# 数据库连接信息
url: jdbc:mysql://127.0.0.1:3306/xiaoyishu?useUnicode=true&characterEncoding=utf-8&autoReconnect=true&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: ayi
password: Os0TpcErSh26nT4Nqqjgo2vwi3IaEglzj+brT2b7q0P4Dlhnv3OEQVUNpG/dYqvJZUCR2/IyfxQ4LnQIB7FcfQ==
druid: # Druid 连接池
initial-size: 5 # 初始化连接池大小
min-idle: 5 # 最小连接池数量
max-active: 100 # 最大连接池数量
max-wait: 60000 # 连接时最大等待时间(单位:毫秒)
test-while-idle: true
time-between-eviction-runs-millis: 60000 # 配置多久进行一次检测,检测需要关闭的连接(单位:毫秒)
min-evictable-idle-time-millis: 300000 # 配置一个连接在连接池中最小生存的时间(单位:毫秒)
max-evictable-idle-time-millis: 900000 # 配置一个连接在连接池中最大生存的时间(单位:毫秒)
validation-query: SELECT 1 FROM DUAL # 配置测试连接是否可用的查询 sql
test-on-borrow: false
test-on-return: false
pool-prepared-statements: false
web-stat-filter:
enabled: true
stat-view-servlet:
enabled: true
url-pattern: /druid/* # 配置监控后台访问路径
login-username: admin # 配置监控后台登录的用户名、密码
login-password: admin
filter:
config:
enabled: true
stat:
enabled: true
log-slow-sql: true # 开启慢 sql 记录
slow-sql-millis: 2000 # 若执行耗时大于 2s,则视为慢 sql
merge-sql: true
wall: # 防火墙
config:
multi-statement-allow: true
connection-properties: config.decrypt=true;config.decrypt.key=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAK6C4nQHNuYSebx/5vOdvDqP/o8AH+p73s1LWCFs915RiwVHvtEd+ropmXkCO3Agc9Zuo8pyMvccIgPL9F0I8YkCAwEAAQ==
output:
ansi:
enabled: always
application:
name: xiaoyishu-auth
mybatis-plus:
mapper-locations:
- classpath*:top/crushtj/**/*.xml # 匹配所有模块中的 Mapper XML 文件
type-aliases-package: top.crushtj.xiaoyishu.auth.domain.entity # 实体类包路径
configuration:
map-underscore-to-camel-case: true # 开启驼峰命名转换
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 日志输出(调试用)
logging:
level:
top.crushtj.xiaoyishu.auth.domain.mappers: debug
@@ -0,0 +1,30 @@
spring:
profiles:
active: dev
server:
port: 18881
logging:
config: classpath:config/logback-spring.xml
############## Sa-Token 配置 ##############
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: Authorization
token-prefix: Bearer
# token 有效期(单位:秒) 默认30天,-1 代表永久有效
timeout: 2592000
# timeout: 60
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
active-timeout: -1
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
is-share: true
# token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik
token-style: random-128
# 是否输出操作日志
is-log: true
alarm:
type: sms # 告警方式,sms-短信,email-邮件
@@ -0,0 +1,19 @@
spring:
application:
name: xiaoyishu-auth # 应用名称
profiles:
active: dev # 默认激活 dev 本地开发环境
cloud:
nacos:
config:
server-addr: http://127.0.0.1:8848 # 指定 Nacos 配置中心的服务器地址
prefix: ${spring.application.name} # 配置 Data Id 前缀,这里使用应用名称作为前缀
group: DEFAULT_GROUP # 所属组
namespace: xiaoyishu # 命名空间
file-extension: yaml # 配置文件格式
refresh-enabled: true # 是否开启动态刷新
discovery:
enabled: true # 启用服务发现
group: DEFAULT_GROUP # 所属组
namespace: xiaoyishu # 命名空间
server-addr: 127.0.0.1:8848 # 指定 Nacos 配置中心的服务器地址
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<jmxConfigurator/>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 应用名称 -->
<property scope="context" name="appName" value="xiaoyi-auth"/>
<property name="LOG_FILE" value="./logs/${appName}.%d{yyyy-MM-dd}"/>
<property name="CONSOLE_LOG_PATTERN"
value="[%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){cyan}] [%thread] [%clr(%-5level){highlight}] %clr(%logger{50}){magenta} - %msg%n"/>
<property name="FILE_LOG_PATTERN" value="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] [%-5level] %logger{50} - %msg%n"/>
<!-- 文件输出 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 日志文件的命名格式 -->
<fileNamePattern>${LOG_FILE}-%i.log</fileNamePattern>
<!-- 保留 30 天的日志文件 -->
<maxHistory>30</maxHistory>
<!-- 单个日志文件最大大小 -->
<maxFileSize>10MB</maxFileSize>
<!-- 日志文件的总大小,0 表示不限制 -->
<totalSizeCap>0</totalSizeCap>
<!-- 重启服务时,是否清除历史日志,不推荐清理 -->
<cleanHistoryOnStart>false</cleanHistoryOnStart>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 异步写入日志,提升性能 -->
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<!-- 是否丢弃日志, 0 表示不丢弃。默认情况下,如果队列满 80%, 会丢弃 TRACE、DEBUG、INFO 级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 队列大小。默认值为 256 -->
<queueSize>256</queueSize>
<appender-ref ref="FILE"/>
</appender>
<springProfile name="dev">
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<withJansi>true</withJansi>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_FILE"/>
</root>
</springProfile>
<springProfile name="prod">
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<root level="INFO">
<appender-ref ref="ASYNC_FILE"/> <!-- 生产环境下,仅打印日志到文件中 -->
</root>
</springProfile>
</configuration>
@@ -0,0 +1,85 @@
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE IF NOT EXISTS `t_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`xiaoyishu_id` varchar(15) COLLATE UTF8MB4_UNICODE_CI NOT NULL COMMENT '小壹书号(唯一凭证)',
`password` varchar(64) COLLATE UTF8MB4_UNICODE_CI DEFAULT NULL COMMENT '密码',
`nickname` varchar(24) COLLATE UTF8MB4_UNICODE_CI NOT NULL COMMENT '昵称',
`avatar` varchar(120) COLLATE UTF8MB4_UNICODE_CI DEFAULT NULL COMMENT '头像',
`birthday` date DEFAULT NULL COMMENT '生日',
`background_img` varchar(120) COLLATE UTF8MB4_UNICODE_CI DEFAULT NULL COMMENT '背景图',
`phone` varchar(11) COLLATE UTF8MB4_UNICODE_CI NOT NULL COMMENT '手机号',
`sex` tinyint DEFAULT '3' COMMENT '性别(1: 男 2: 女 3: 未知)',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态(1:启用 0:禁用)',
`introduction` varchar(100) COLLATE UTF8MB4_UNICODE_CI DEFAULT NULL COMMENT '个人简介',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
`is_deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '逻辑删除(0:未删除 1:已删除)',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_xiaoyishu_id` (`xiaoyishu_id`),
UNIQUE KEY `uk_phone` (`phone`)
) ENGINE = InnoDB
DEFAULT CHARSET = UTF8MB4
COLLATE = UTF8MB4_UNICODE_CI COMMENT = '用户表';
DROP TABLE IF EXISTS `t_role`;
CREATE TABLE IF NOT EXISTS `t_role` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`role_name` varchar(32) COLLATE UTF8MB4_UNICODE_CI NOT NULL COMMENT '角色名',
`role_key` varchar(32) COLLATE UTF8MB4_UNICODE_CI NOT NULL COMMENT '角色唯一标识',
`status` tinyint unsigned NOT NULL DEFAULT '1' COMMENT '状态(1:启用 0:禁用)',
`sort` int unsigned NOT NULL DEFAULT 0 COMMENT '管理系统中的显示顺序',
`remark` varchar(255) COLLATE UTF8MB4_UNICODE_CI DEFAULT NULL COMMENT '备注',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最后一次更新时间',
`is_deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '逻辑删除(0:未删除 1:已删除)',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_role_key` (`role_key`)
) ENGINE = InnoDB
DEFAULT CHARSET = UTF8MB4
COLLATE = UTF8MB4_UNICODE_CI COMMENT ='角色表';
DROP TABLE IF EXISTS `t_permission`;
CREATE TABLE IF NOT EXISTS `t_permission` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`parent_id` bigint NOT NULL DEFAULT '0' COMMENT '父ID',
`name` varchar(16) COLLATE UTF8MB4_UNICODE_CI NOT NULL COMMENT '权限名称',
`type` tinyint unsigned NOT NULL COMMENT '类型(1:目录 2:菜单 3:按钮)',
`menu_url` varchar(32) COLLATE UTF8MB4_UNICODE_CI NOT NULL DEFAULT '' COMMENT '菜单路由',
`menu_icon` varchar(255) COLLATE UTF8MB4_UNICODE_CI NOT NULL DEFAULT '' COMMENT '菜单图标',
`sort` int unsigned NOT NULL DEFAULT 0 COMMENT '管理系统中的显示顺序',
`permission_key` varchar(64) COLLATE UTF8MB4_UNICODE_CI NOT NULL COMMENT '权限标识',
`status` tinyint unsigned NOT NULL DEFAULT '1' COMMENT '状态(1:启用;0:禁用)',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
`is_deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '逻辑删除(0:未删除 1:已删除)',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB
DEFAULT CHARSET = UTF8MB4
COLLATE = UTF8MB4_UNICODE_CI COMMENT ='权限表';
DROP TABLE IF EXISTS `t_user_role_rel`;
CREATE TABLE IF NOT EXISTS `t_user_role_rel` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` bigint NOT NULL COMMENT '用户ID',
`role_id` bigint NOT NULL COMMENT '角色ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
`is_deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '逻辑删除(0:未删除 1:已删除)',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB
DEFAULT CHARSET = UTF8MB4
COLLATE = UTF8MB4_UNICODE_CI COMMENT ='用户角色表';
DROP TABLE IF EXISTS `t_role_permission_rel`;
CREATE TABLE IF NOT EXISTS `t_role_permission_rel` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`role_id` bigint NOT NULL COMMENT '角色ID',
`permission_id` bigint NOT NULL COMMENT '权限ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
`is_deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '逻辑删除(0:未删除 1:已删除)',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB
DEFAULT CHARSET = UTF8MB4
COLLATE = UTF8MB4_UNICODE_CI COMMENT ='用户权限表';
@@ -0,0 +1,17 @@
-- Description: 初始化权限数据-权限管理未开发,用于测试
INSERT INTO `t_permission` (`id`, `parent_id`, `name`, `type`, `menu_url`, `menu_icon`, `sort`,
`permission_key`, `status`, `create_time`, `update_time`, `is_deleted`)
VALUES (1, 0, '发布笔记', 3, '', '', 1, 'app:note:publish', 1, NOW(), NOW(), b'0');
INSERT INTO `t_permission` (`id`, `parent_id`, `name`, `type`, `menu_url`, `menu_icon`, `sort`,
`permission_key`, `status`, `create_time`, `update_time`, `is_deleted`)
VALUES (2, 0, '发布评论', 3, '', '', 2, 'app:comment:publish', 1, NOW(), NOW(), b'0');
INSERT INTO `t_role` (`id`, `role_name`, `role_key`, `status`, `sort`, `remark`, `create_time`, `update_time`,
`is_deleted`)
VALUES (1, '普通用户', 'common_user', 1, 1, '', NOW(), NOW(), b'0');
INSERT INTO `t_role_permission_rel` (`id`, `role_id`, `permission_id`, `create_time`, `update_time`, `is_deleted`)
VALUES (1, 1, 1, NOW(), NOW(), b'0');
INSERT INTO `t_role_permission_rel` (`id`, `role_id`, `permission_id`, `create_time`, `update_time`, `is_deleted`)
VALUES (2, 1, 2, NOW(), NOW(), b'0');
@@ -0,0 +1,109 @@
package top.crushtj.xiaoyi.auth;
import com.alibaba.druid.filter.config.ConfigTools;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.jasypt.encryption.pbe.StandardPBEStringEncryptor;
import org.jasypt.iv.RandomIvGenerator;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
/**
* @author ayi
* @version V1.0
* @title DruidTest
* @description
* @date 2025/12/01
*/
@SpringBootTest
@Slf4j
@TestPropertySource(properties = {"jasypt.encryptor.password="})
public class EncryptTest {
//@Autowired
//private StringEncryptor defaultLazyEncryptor;
//private StringEncryptor pooledPbeStringEncryptor;
@Value("${jasypt.encryptor.password}")
private String encryptorPassword;
/**
* Druid 密码加密
*/
@Test
@SneakyThrows
void testEncodePassword() {
// 你的密码
String password = "";//密码
String[] arr = ConfigTools.genKeyPair(512);
// 私钥
log.info("privateKey: {}", arr[0]);
// 公钥
log.info("publicKey: {}", arr[1]);
// 通过私钥加密密码
String encodePassword = ConfigTools.encrypt(arr[0], password);
log.info("password: {}", encodePassword);
}
@Test
void encrypt() {
StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor();
// JDK 17适配的核心配置(关键参数必须完整)
String password = ""; // 加密密码
encryptor.setPassword(password); // AES-256要求密钥至少32位
encryptor.setAlgorithm("PBEWithHMACSHA512AndAES_256"); // JDK17原生支持的算法
encryptor.setKeyObtentionIterations(1000); // 迭代次数(固定值)
encryptor.setStringOutputType("base64"); // 输出格式(固定)
encryptor.setProviderName("SunJCE"); // 加密提供者(JDK17默认)
encryptor.setIvGenerator(new RandomIvGenerator()); // AES必须的IV生成器
// 待加密的原始值
String encry1 = ""; // 待加密的原始值1
String encry2 = ""; // 待加密的原始值2
String encry3 = ""; // 待加密的原始值3
try {
String cipherText1 = encryptor.encrypt(encry1);
System.out.println("字段1加密成功,密文:" + cipherText1);
String cipherText2 = encryptor.encrypt(encry2);
System.out.println("字段2加密成功,密文:" + cipherText2);
String cipherText3 = encryptor.encrypt(encry3);
System.out.println("字段2加密成功,密文:" + cipherText3);
} catch (Exception e) {
e.printStackTrace();
System.out.println("加密失败原因:" + e.getMessage());
}
}
@Test
public void dencrypt(){
StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor();
String cipherText1 = "";//密文
String cipherText2 = "";//密文
String cipherText3 = "";//密文
String password = "";//加密密码
encryptor.setPassword(password); // AES-256要求密钥至少32位
encryptor.setAlgorithm("PBEWithHMACSHA512AndAES_256"); // JDK17原生支持的算法
encryptor.setKeyObtentionIterations(1000); // 迭代次数(固定值)
encryptor.setStringOutputType("base64"); // 输出格式(固定)
encryptor.setProviderName("SunJCE"); // 加密提供者(JDK17默认)
encryptor.setIvGenerator(new RandomIvGenerator());
try {
String text1 = encryptor.decrypt(cipherText1);
System.out.println("字段1解密成功,明文:" + text1);
String text2 = encryptor.decrypt(cipherText2);
System.out.println("字段2解密成功,明文:" + text2);
String text3 = encryptor.decrypt(cipherText3);
System.out.println("字段2解密成功,明文:" + text3);
} catch (Exception e) {
e.printStackTrace();
System.out.println("加密失败原因:" + e.getMessage());
}// AES必须的IV生成器
}
}
@@ -0,0 +1,59 @@
package top.crushtj.xiaoyi.auth;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
/**
*
* @author ayi
* @version V1.0
* @title RedisTests
* @description Redis 测试类
* @date 2026/01/12 19:15
*/
@SpringBootTest
@Slf4j
public class RedisTests {
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* set key value
*/
@Test
void testSetKeyValue() {
// 添加一个 key 为 name, value 值为 刑加一
redisTemplate.opsForValue()
.set("name", "刑加一");
}
/**
* 判断某个 key 是否存在
*/
@Test
void testHasKey() {
log.info("key 是否存在:{}", Boolean.TRUE.equals(redisTemplate.hasKey("name")));
}
/**
* 获取某个 key 的 value
*/
@Test
void testGetValue() {
log.info("value 值:{}", redisTemplate.opsForValue()
.get("name"));
}
/**
* 删除某个 key
*/
@Test
void testDelete() {
redisTemplate.delete("name");
}
}
@@ -0,0 +1,36 @@
package top.crushtj.xiaoyi.auth;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.test.context.TestPropertySource;
/**
* @author ayi
* @version V1.0
* @title ThreadPoolTaskExecutorTests
* @date 2026/01/16 22:34
* @description 线程池测试类
*/
@Slf4j
@SpringBootTest
@TestPropertySource(properties = {"jasypt.encryptor.password="})
public class ThreadPoolTaskExecutorTests {
@Resource(name = "taskExecutor")
private ThreadPoolTaskExecutor taskExecutor;
/**
* 测试线程池
*/
@Test
void testSubmit() {
int count = 300;
while (count-- > 0) {
int finalCount = count;
taskExecutor.submit(() -> log.info("异步线程: 这是{}号线程", finalCount));
}
}
}
@@ -1,13 +0,0 @@
package top.crushtj.xiaoyishu.auth;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class XiaoyishuAuthApplicationTests {
@Test
void contextLoads() {
}
}
+5
View File
@@ -16,7 +16,12 @@
<description>平台基础设施层:封装一些常用功能,供各个业务线拿来即用</description>
<modules>
<!--通用-->
<module>xiaoyi-common</module>
<!--日志切面-->
<module>xiaoyi-spring-boot-starter-biz-operationlog</module>
<!--Jackson-->
<module>xiaoyi-spring-boot-starter-jackson</module>
</modules>
</project>
+53
View File
@@ -20,6 +20,59 @@
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- Jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<!-- 解决 Jackson Java 8 新日期 API 的序列化问题 -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<!-- AOP 切面 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- jansi -->
<dependency>
<groupId>org.fusesource.jansi</groupId>
<artifactId>jansi</artifactId>
</dependency>
<!-- 参数校验 -->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,31 @@
package top.crushtj.framework.common.constant;
import java.time.format.DateTimeFormatter;
/**
* @author ayi
* @title DateConstants
* @description 日期常量
* @date 2025/11/26
*/
public interface DateConstants {
/**
* DateTimeFormatter:年-月-日 时:分:秒
*/
DateTimeFormatter DATE_FORMAT_Y_M_D_H_M_S = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/**
* DateTimeFormatter:年-月-日
*/
DateTimeFormatter DATE_FORMAT_Y_M_D = DateTimeFormatter.ofPattern("yyyy-MM-dd");
/**
* DateTimeFormatter:时:分:秒
*/
DateTimeFormatter DATE_FORMAT_H_M_S = DateTimeFormatter.ofPattern("HH:mm:ss");
/**
* DateTimeFormatter:年-月
*/
DateTimeFormatter DATE_FORMAT_Y_M = DateTimeFormatter.ofPattern("yyyy-MM");
}
@@ -0,0 +1,21 @@
package top.crushtj.framework.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author ayi
* @version V1.0
* @title DeleteEnum
* @date 2026/01/18 21:15
* @description 删除标记
*/
@Getter
@AllArgsConstructor
public enum DeleteEnum {
YES(true),
NO(false);
public final Boolean value;
}
@@ -0,0 +1,21 @@
package top.crushtj.framework.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author ayi
* @version V1.0
* @title StatusEnum
* @date 2026/01/18
* @description 状态
*/
@Getter
@AllArgsConstructor
public enum StatusEnum {
ENABLED(1),
DISABLED(0);
public final Integer value;
}
@@ -0,0 +1,26 @@
package top.crushtj.framework.common.exception;
/**
*
* @author ayi
* @title BaseExceptionInterface
* @description 基础异常接口
* @date 2025/11/20
*/
public interface BaseExceptionInterface {
/**
* 获取异常码
*
* @return 异常码
*/
String getErrorCode();
/**
* 获取异常信息
*
* @return 异常信息
*/
String getErrorMessage();
}
@@ -0,0 +1,38 @@
package top.crushtj.framework.common.exception;
import lombok.Getter;
import lombok.Setter;
/**
*
* @author ayi
* @title BusiException
* @description 业务异常
* @date 2025/11/20
*/
@Getter
@Setter
public class BizException extends RuntimeException {
/**
* 异常码
*/
private String errorCode;
/**
* 异常信息
*/
private String errorMessage;
/**
* 构造函数
*
* @param baseExceptionInterface 基础异常接口
*/
public BizException(BaseExceptionInterface baseExceptionInterface) {
this.errorCode = baseExceptionInterface.getErrorCode();
this.errorMessage = baseExceptionInterface.getErrorMessage();
}
}
@@ -0,0 +1,138 @@
package top.crushtj.framework.common.response;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
import lombok.Data;
import top.crushtj.framework.common.exception.BaseExceptionInterface;
import top.crushtj.framework.common.exception.BizException;
/**
*
* @author ayi
* @title Response
* @description 响应体
* @date 2025/11/20
*/
@Data
public class Response<T> implements Serializable {
@Serial
private static final long serialVersionUID = -6624218097474846897L;
/**
* 响应码
*/
private String code;
/**
* 响应信息
*/
private String message;
/**
* 响应时间
*/
private LocalDateTime time = LocalDateTime.now();
/**
* 是否成功
*/
private boolean success = true;
/**
* 响应数据
*/
private T data;
/**
* 成功响应
*
* @return 成功响应
*/
public static <T> Response<T> success() {
return new Response<>();
}
/**
* 成功响应
*
* @param data 响应数据
* @return 成功响应
*/
public static <T> Response<T> success(T data) {
Response<T> response = new Response<>();
response.setData(data);
return response;
}
/**
* 失败响应
*
* @return 失败响应
*/
public static <T> Response<T> failure() {
Response<T> response = new Response<>();
response.setSuccess(false);
return response;
}
/**
* 失败响应
*
* @param message 异常信息
* @return 失败响应
*/
public static <T> Response<T> failure(String message) {
Response<T> response = new Response<>();
response.setSuccess(false);
response.setMessage(message);
return response;
}
/**
* 失败响应
*
* @param code 异常码
* @param message 异常信息
* @return 失败响应
*/
public static <T> Response<T> failure(String code, String message) {
Response<T> response = new Response<>();
response.setSuccess(false);
response.setCode(code);
response.setMessage(message);
return response;
}
/**
* 失败响应
*
* @param bizException 业务异常
* @return 失败响应
*/
public static <T> Response<T> failure(BizException bizException) {
Response<T> response = new Response<>();
response.setSuccess(false);
response.setCode(bizException.getErrorCode());
response.setMessage(bizException.getErrorMessage());
return response;
}
/**
* 失败响应
*
* @param <T> 响应数据类型
* @param baseExceptionInterface 基础异常
* @return 失败响应
*/
public static <T> Response<T> failure(BaseExceptionInterface baseExceptionInterface) {
Response<T> response = new Response<>();
response.setSuccess(false);
response.setCode(baseExceptionInterface.getErrorCode());
response.setMessage(baseExceptionInterface.getErrorMessage());
return response;
}
}
@@ -0,0 +1,180 @@
package top.crushtj.framework.common.utils;
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.NetworkInterface;
public class IdGenerator {
// ====================== 配置参数 ======================
/** 开始时间戳 (2024-01-01 00:00:00),可自定义,减少ID长度 */
private static final long START_TIMESTAMP = 1767196800000L;
/** 机器ID所占的位数 (最多10位,支持1024个节点) */
private static final long WORKER_ID_BITS = 10L;
/** 序列号所占的位数 (最多12位,支持4096个/毫秒) */
private static final long SEQUENCE_BITS = 12L;
// ====================== 位移计算 ======================
/** 机器ID左移位数 (12位) */
private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
/** 时间戳左移位数 (10+12=22位) */
private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
/** 序列号的最大值 (4095) */
private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
// ====================== 全局变量 ======================
/** 机器ID (0-1023) */
private final long workerId;
/** 序列号 (0-4095) */
private long sequence = 0L;
/** 上一次生成ID的时间戳 */
private long lastTimestamp = -1L;
// ====================== 单例实例 ======================
private static volatile IdGenerator INSTANCE;
/**
* 私有构造器,初始化机器ID(自动计算,也可手动指定)
*
* @param workerId 机器ID (0-1023)
* @throws IllegalArgumentException 机器ID超出范围时抛出
*/
private IdGenerator(long workerId) {
// 校验机器ID范围
long maxWorkerId = ~(-1L << WORKER_ID_BITS);
if (workerId < 0 || workerId > maxWorkerId) {
throw new IllegalArgumentException(String.format("Worker ID 必须在 0 到 %d 之间", maxWorkerId));
}
this.workerId = workerId;
}
/**
* 获取单例实例(自动计算机器ID)
*
* @return 雪花算法生成器实例
*/
public static IdGenerator getInstance() {
if (INSTANCE == null) {
synchronized (IdGenerator.class) {
if (INSTANCE == null) {
INSTANCE = new IdGenerator(calculateWorkerId());
}
}
}
return INSTANCE;
}
/**
* 手动指定机器ID获取实例(适合分布式部署时手动分配节点ID)
*
* @param workerId 机器ID (0-1023)
* @return 雪花算法生成器实例
*/
public static IdGenerator getInstance(long workerId) {
if (INSTANCE == null) {
synchronized (IdGenerator.class) {
if (INSTANCE == null) {
INSTANCE = new IdGenerator(workerId);
}
}
}
return INSTANCE;
}
/**
* 生成下一个ID(核心方法,线程安全)
*
* @return 有序、唯一的Long类型ID
* @throws RuntimeException 时钟回拨时抛出(避免ID重复)
*/
public synchronized long nextId() {
long currentTimestamp = System.currentTimeMillis();
// 检查时钟回拨(关键:避免ID重复)
if (currentTimestamp < lastTimestamp) {
throw new RuntimeException(
String.format("时钟回拨检测!拒绝生成ID,上一次时间:%d,当前时间:%d", lastTimestamp, currentTimestamp));
}
// 同一毫秒内,序列号递增
if (currentTimestamp == lastTimestamp) {
sequence = (sequence + 1) & SEQUENCE_MASK;
// 同一毫秒内序列号用尽,等待下一毫秒
if (sequence == 0) {
currentTimestamp = waitNextMillis(lastTimestamp);
}
} else {
// 不同毫秒,序列号重置为0
sequence = 0L;
}
// 更新最后生成ID的时间戳
lastTimestamp = currentTimestamp;
// 组合ID:时间戳 + 机器ID + 序列号
return ((currentTimestamp - START_TIMESTAMP) << TIMESTAMP_LEFT_SHIFT) | (workerId << WORKER_ID_SHIFT)
| sequence;
}
/**
* 等待下一毫秒,直到获取新的时间戳
*
* @param lastTimestamp 上一次生成ID的时间戳
* @return 新的时间戳
*/
private long waitNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
/**
* 自动计算机器ID(基于网卡+进程ID,避免手动配置)
*
* @return 0-1023之间的机器ID
*/
private static long calculateWorkerId() {
try {
// 第一步:获取网卡MAC地址的哈希值
InetAddress address = InetAddress.getLocalHost();
long macHash = NetworkInterface.getByInetAddress(address).getHardwareAddress()[0] & 0xFF;
// 第二步:获取进程ID(避免同一机器多进程冲突)
String processName = ManagementFactory.getRuntimeMXBean().getName();
long pid = Long.parseLong(processName.split("@")[0]);
// 组合并取模,确保在0-1023之间
return (macHash + pid) % (~(-1L << WORKER_ID_BITS));
} catch (Exception e) {
// 异常时返回随机数(生产环境建议手动指定机器ID)
return (long)(Math.random() * (~(-1L << WORKER_ID_BITS)));
}
}
// ====================== 测试用例 ======================
public static void main(String[] args) {
// 测试生成10个ID,验证有序性和唯一性
IdGenerator generator = IdGenerator.getInstance();
for (int i = 0; i < 10; i++) {
long id = generator.nextId();
System.out.println("生成的ID" + id);
}
// 多线程测试(验证线程安全)
Runnable task = () -> {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " -> " + generator.nextId());
}
};
new Thread(task, "线程1").start();
new Thread(task, "线程2").start();
}
}
@@ -0,0 +1,44 @@
package top.crushtj.framework.common.utils;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.SneakyThrows;
/**
*
* @author ayi
* @title JsonUtils
* @description JSON 工具类
* @date 2025/11/21
*/
public class JsonUtils {
private static ObjectMapper OBJECT_MAPPER = new ObjectMapper();
static {
OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
OBJECT_MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
OBJECT_MAPPER.registerModules(new JavaTimeModule());
}
/**
* 初始化:统一使用 Spring Boot 个性化配置的 ObjectMapper
*
* @param objectMapper
*/
public static void init(ObjectMapper objectMapper) {
OBJECT_MAPPER = objectMapper;
}
/**
* 将对象转换为 JSON 字符串
*
* @param obj 要转换的对象
* @return JSON 字符串
*/
@SneakyThrows
public static String toJsonString(Object obj) {
return OBJECT_MAPPER.writeValueAsString(obj);
}
}
@@ -0,0 +1,143 @@
package top.crushtj.framework.common.utils;
import org.apache.commons.lang3.StringUtils;
/**
* @author ayi
* @version V1.0
* @title MaskUtils
* @date 2026/01/17 13:22
* @description 数据脱敏工具类
*/
public class MaskUtils {
// ===================== 通用脱敏方法(基础) =====================
/**
* 通用脱敏方法:保留前prefixLen位,后suffixLen位,中间用*填充
* @param content 原始字符串
* @param prefixLen 保留前缀长度
* @param suffixLen 保留后缀长度
* @return 脱敏后的字符串
*/
public static String maskGeneral(String content, int prefixLen, int suffixLen) {
// 空值处理:null/空串直接返回,避免NPE
if (StringUtils.isBlank(content)) {
return content;
}
int contentLen = content.length();
// 若长度小于等于前缀+后缀,直接返回原字符串(避免过度脱敏)
if (contentLen <= prefixLen + suffixLen) {
return content;
}
// 拼接前缀 + 中间* + 后缀
StringBuilder sb = new StringBuilder();
// 添加前缀
sb.append(content.substring(0, prefixLen));
// 添加中间掩码(长度=总长度-前缀-后缀)
sb.append("*".repeat(Math.max(0, contentLen - prefixLen - suffixLen)));
// 添加后缀
sb.append(content.substring(contentLen - suffixLen));
return sb.toString();
}
// ===================== 常用敏感字段脱敏方法(业务封装) =====================
/**
* 手机号脱敏:保留前3位,后4位,中间4位掩码(如:138****1234
* @param mobile 手机号(支持11位常规手机号)
* @return 脱敏后的手机号
*/
public static String maskMobile(String mobile) {
// 先校验手机号格式(简单校验,可根据业务调整)
if (StringUtils.isBlank(mobile) || !mobile.matches("^1[3-9]\\d{9}$")) {
return mobile;
}
return maskGeneral(mobile, 3, 4);
}
/**
* 邮箱脱敏:保留前缀前3位,@及域名完整,中间掩码(如:123****@qq.com
* @param email 邮箱地址
* @return 脱敏后的邮箱
*/
public static String maskEmail(String email) {
if (StringUtils.isBlank(email) || !email.contains("@")) {
return email;
}
String[] parts = email.split("@");
String prefix = parts[0];
String domain = parts[1];
// 前缀长度<=3时不脱敏,否则保留前3位
String maskedPrefix = prefix.length() <= 3 ? prefix : maskGeneral(prefix, 3, 0);
return maskedPrefix + "@" + domain;
}
/**
* 姓名脱敏:
* - 单字名:直接返回(如:李 → 李)
* - 两字名:保留姓,名掩码(如:李白 → 李*)
* - 三字及以上:保留姓和最后一个字,中间掩码(如:李世民 → 李*民)
* @param name 姓名
* @return 脱敏后的姓名
*/
public static String maskName(String name) {
if (StringUtils.isBlank(name)) {
return name;
}
int nameLen = name.length();
if (nameLen == 1) {
return name;
} else if (nameLen == 2) {
return name.substring(0, 1) + "*";
} else {
// 姓 + 中间* + 最后一个字
return name.substring(0, 1) +
"*".repeat(nameLen - 2) +
name.substring(nameLen - 1);
}
}
/**
* 身份证号脱敏:保留前3位,后4位,中间掩码(如:110***********1234
* @param idCard 身份证号(支持15位/18位)
* @return 脱敏后的身份证号
*/
public static String maskIdCard(String idCard) {
if (StringUtils.isBlank(idCard) || (idCard.length() != 15 && idCard.length() != 18)) {
return idCard;
}
return maskGeneral(idCard, 3, 4);
}
/**
* 银行卡号脱敏:保留前6位,后4位,中间掩码(如:622260**********1234
* @param bankCard 银行卡号(常规16/19位)
* @return 脱敏后的银行卡号
*/
public static String maskBankCard(String bankCard) {
if (StringUtils.isBlank(bankCard) || bankCard.length() < 10) {
return bankCard;
}
return maskGeneral(bankCard, 6, 4);
}
// ===================== 测试用例(可直接运行验证) =====================
public static void main(String[] args) {
// 测试手机号脱敏
System.out.println("手机号脱敏:" + maskMobile("13812345678")); // 输出:138****5678
// 测试邮箱脱敏
System.out.println("邮箱脱敏:" + maskEmail("1234567890@qq.com")); // 输出:123****@qq.com
// 测试姓名脱敏
System.out.println("单字名:" + maskName("")); // 输出:李
System.out.println("两字名:" + maskName("李白")); // 输出:李*
System.out.println("三字名:" + maskName("李世民")); // 输出:李*民
// 测试身份证脱敏
System.out.println("身份证脱敏:" + maskIdCard("110101199001011234")); // 输出:110***********1234
// 测试银行卡脱敏
System.out.println("银行卡脱敏:" + maskBankCard("6222600000000001234")); // 输出:622260**********1234
// 测试空值/异常值
System.out.println("空手机号:" + maskMobile(null)); // 输出:null
System.out.println("无效邮箱:" + maskEmail("123456")); // 输出:123456
}
}
@@ -0,0 +1,28 @@
package top.crushtj.framework.common.validator;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author ayi
* @version V1.0
* @title PhoneNumber
* @date 2026/01/18
* @description 手机号验证
*/
@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneNumberValidator.class)
public @interface PhoneNumber {
String message() default "手机号格式不正确, 需为 11 位数字";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
@@ -0,0 +1,25 @@
package top.crushtj.framework.common.validator;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
/**
* @author ayi
* @version V1.0
* @title PhoneNumberValidator
* @date 2026/01/18 13:17
* @description 手机号验证器
*/
public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber,String> {
@Override
public boolean isValid(String phoneNumber, ConstraintValidatorContext context) {
// 校验逻辑:正则表达式判断手机号是否为 11 位数字
return phoneNumber != null && phoneNumber.matches("\\d{11}");
}
@Override
public void initialize(PhoneNumber constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
}
}
@@ -0,0 +1,124 @@
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
/**
* 时间戳工具类 - 用于获取指定时间的时间戳(毫秒级)
* 基于Java 8+ java.time包实现,线程安全、支持时区、异常友好
*/
public class TimestampUtils {
// ====================== 常量定义 ======================
/** 默认时间格式:yyyy-MM-dd HH:mm:ss */
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
/** 系统默认时区(可根据需求修改,如ZoneId.of("UTC")、ZoneId.of("Asia/Shanghai") */
public static final ZoneId DEFAULT_ZONE_ID = ZoneId.systemDefault();
// ====================== 方法1:通过年月日时分秒获取时间戳 ======================
/**
* 获取指定年月日时分秒的时间戳(毫秒级),使用系统默认时区
* @param year 年(如2024
* @param month 月(1-12
* @param day 日(1-31
* @param hour 时(0-23
* @param minute 分(0-59
* @param second 秒(0-59
* @return 对应时间的毫秒级时间戳
* @throws IllegalArgumentException 时间参数不合法时抛出
*/
public static long getTimestamp(int year, int month, int day, int hour, int minute, int second) {
return getTimestamp(year, month, day, hour, minute, second, DEFAULT_ZONE_ID);
}
/**
* 获取指定年月日时分秒的时间戳(毫秒级),支持指定时区
* @param year 年(如2024
* @param month 月(1-12
* @param day 日(1-31
* @param hour 时(0-23
* @param minute 分(0-59
* @param second 秒(0-59
* @param zoneId 时区(如ZoneId.of("Asia/Shanghai")
* @return 对应时间的毫秒级时间戳
* @throws IllegalArgumentException 时间参数不合法时抛出
*/
public static long getTimestamp(int year, int month, int day, int hour, int minute, int second, ZoneId zoneId) {
try {
// 构建指定时间的LocalDateTime
LocalDateTime localDateTime = LocalDateTime.of(year, month, day, hour, minute, second);
// 转换为指定时区的ZonedDateTime,再获取时间戳
ZonedDateTime zonedDateTime = localDateTime.atZone(zoneId);
return zonedDateTime.toInstant().toEpochMilli();
} catch (Exception e) {
throw new IllegalArgumentException("时间参数不合法:" + year + "-" + month + "-" + day + " " + hour + ":" + minute + ":" + second, e);
}
}
// ====================== 方法2:解析时间字符串获取时间戳 ======================
/**
* 解析指定格式的时间字符串为时间戳(毫秒级),使用系统默认时区
* @param timeStr 时间字符串(如"2024-01-01 12:00:00"
* @param formatter 时间格式化器(如DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
* @return 对应时间的毫秒级时间戳
* @throws DateTimeParseException 时间字符串格式不匹配时抛出
*/
public static long getTimestamp(String timeStr, DateTimeFormatter formatter) {
return getTimestamp(timeStr, formatter, DEFAULT_ZONE_ID);
}
/**
* 解析默认格式(yyyy-MM-dd HH:mm:ss)的时间字符串为时间戳(毫秒级)
* @param timeStr 时间字符串(如"2024-01-01 12:00:00"
* @return 对应时间的毫秒级时间戳
* @throws DateTimeParseException 时间字符串格式不匹配时抛出
*/
public static long getTimestamp(String timeStr) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT);
return getTimestamp(timeStr, formatter, DEFAULT_ZONE_ID);
}
/**
* 解析指定格式的时间字符串为时间戳(毫秒级),支持指定时区
* @param timeStr 时间字符串(如"2024-01-01 12:00:00"
* @param formatter 时间格式化器
* @param zoneId 时区
* @return 对应时间的毫秒级时间戳
* @throws DateTimeParseException 时间字符串格式不匹配时抛出
*/
public static long getTimestamp(String timeStr, DateTimeFormatter formatter, ZoneId zoneId) {
try {
// 解析时间字符串为LocalDateTime
LocalDateTime localDateTime = LocalDateTime.parse(timeStr, formatter);
// 转换为指定时区的ZonedDateTime,再获取时间戳
ZonedDateTime zonedDateTime = localDateTime.atZone(zoneId);
return zonedDateTime.toInstant().toEpochMilli();
} catch (DateTimeParseException e) {
throw new DateTimeParseException("时间字符串格式解析失败:" + timeStr + ",期望格式:" + formatter.toString(),
e.getParsedString(), e.getErrorIndex(), e);
}
}
// ====================== 测试用例 ======================
public static void main(String[] args) {
// 测试方法1:指定年月日时分秒获取时间戳
long timestamp1 = TimestampUtils.getTimestamp(2024, 1, 1, 0, 0, 0);
System.out.println("2024-01-01 00:00:00 的时间戳:" + timestamp1); // 输出:1704067200000
// 测试方法2:指定时区(UTC)获取时间戳
long timestamp2 = TimestampUtils.getTimestamp(2024, 1, 1, 0, 0, 0, ZoneId.of("UTC"));
System.out.println("2024-01-01 00:00:00 (UTC) 的时间戳:" + timestamp2); // 输出:1704038400000
// 测试方法3:解析默认格式的时间字符串
long timestamp3 = TimestampUtils.getTimestamp("2026-01-01 00:00:00");
System.out.println("2026-01-01 00:00:00 的时间戳:" + timestamp3);
// 测试方法4:解析自定义格式的时间字符串
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm");
long timestamp4 = TimestampUtils.getTimestamp("2024/06/01 12:30", formatter);
System.out.println("2024/06/01 12:30 的时间戳:" + timestamp4);
}
}
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>top.crushtj</groupId>
<artifactId>xiaoyi-framework</artifactId>
<version>${revision}</version>
</parent>
<packaging>jar</packaging>
<artifactId>xiaoyi-spring-boot-starter-biz-operationlog</artifactId>
<name>${project.artifactId}</name>
<description>平台基础设施层:操作日志记录器</description>
<dependencies>
<dependency>
<groupId>top.crushtj</groupId>
<artifactId>xiaoyi-common</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,103 @@
package top.crushtj.framework.biz.operationlog;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import lombok.extern.slf4j.Slf4j;
import top.crushtj.framework.biz.operationlog.aspect.ApiOperationLog;
import top.crushtj.framework.common.utils.JsonUtils;
/**
*
* @author ayi
* @title ApiOperationLog
* @description 自定义注解,用于标记需要记录操作日志的方法
* @date 2025/11/21
*/
@Slf4j
@Aspect
public class ApiOperationLogAspect {
/** 以自定义 @ApiOperationLog 注解为切点,凡是添加 @ApiOperationLog 的方法,都会执行环绕中的代码 */
@Pointcut("@annotation(top.crushtj.framework.biz.operationlog.aspect.ApiOperationLog)")
public void apiOperationLog() {
}
/**
* 环绕
*
* @param joinPoint 连接点
* @return 方法执行结果
* @throws Throwable 方法执行过程中抛出的异常
*/
@Around("apiOperationLog()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
// 请求开始时间
long startTime = System.currentTimeMillis();
// 获取被请求的类和方法
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
// 请求入参
Object[] args = joinPoint.getArgs();
// 入参转 JSON 字符串
String argsJsonStr = Arrays.stream(args).map(toJsonStr()).collect(Collectors.joining(", "));
// 功能描述信息
String description = getApiOperationLogDescription(joinPoint);
// 打印请求相关参数
log.info("\n\n请求开始: [{}], 请求参数: {}, 请求类: {}, 请求方法: {}\n", description, argsJsonStr, className,
methodName);
// 执行切点方法
Object result = joinPoint.proceed();
// 执行耗时
long executionTime = System.currentTimeMillis() - startTime;
// 打印出参等相关信息
log.info("\n\n请求结束: [{}], 耗时: {}ms, 响应参数: {}\n", description, executionTime,
JsonUtils.toJsonString(result));
return result;
}
/**
* 获取注解的描述信息
*
* @param joinPoint 连接点
* @return 注解的描述信息
*/
private String getApiOperationLogDescription(ProceedingJoinPoint joinPoint) {
// 1. 从 ProceedingJoinPoint 获取 MethodSignature
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
// 2. 使用 MethodSignature 获取当前被注解的 Method
Method method = signature.getMethod();
// 3. 从 Method 中提取 LogExecution 注解
ApiOperationLog apiOperationLog = method.getAnnotation(ApiOperationLog.class);
// 4. 从 LogExecution 注解中获取 description 属性
return apiOperationLog.description();
}
/**
* 转 JSON 字符串
*
* @return 入参的 JSON 字符串
*/
private Function<Object, String> toJsonStr() {
return JsonUtils::toJsonString;
}
}
@@ -0,0 +1,26 @@
package top.crushtj.framework.biz.operationlog.aspect;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
*
* @author ayi
* @title ApiOperationLog
* @description 自定义注解,用于标记需要记录操作日志的方法
* @date 2025/11/21
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface ApiOperationLog {
/**
* API 功能描述
*
* @return 功能描述
*/
String description() default "";
}
@@ -0,0 +1,23 @@
package top.crushtj.framework.biz.operationlog.config;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import top.crushtj.framework.biz.operationlog.ApiOperationLogAspect;
/**
*
* @author ayi
* @title ApiOperationLog
* @description 自定义注解,用于标记需要记录操作日志的方法
* @date 2025/11/21
*/
@AutoConfiguration
public class ApiOperationLogAutoConfiguration {
@Bean
public ApiOperationLogAspect apiOperationLogAspect() {
return new ApiOperationLogAspect();
}
}
@@ -0,0 +1 @@
top.crushtj.framework.biz.operationlog.config.ApiOperationLogAutoConfiguration
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>top.crushtj</groupId>
<artifactId>xiaoyi-framework</artifactId>
<version>${revision}</version>
</parent>
<packaging>jar</packaging>
<artifactId>xiaoyi-spring-boot-starter-jackson</artifactId>
<name>${project.artifactId}</name>
<description>平台基础设施层:Jackson 配置</description>
<dependencies>
<dependency>
<groupId>top.crushtj</groupId>
<artifactId>xiaoyi-common</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,76 @@
package top.crushtj.framework.jackson;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.YearMonthDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.YearMonthSerializer;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.context.annotation.Bean;
import top.crushtj.framework.common.constant.DateConstants;
import top.crushtj.framework.common.utils.JsonUtils;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.YearMonth;
import java.util.TimeZone;
/**
* @author ayi
* @version V1.0
* @title JacksonConfig
* @description jackson 配置类
* @date 2026/01/06
*/
@AutoConfiguration
@AutoConfigureBefore(org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration.class)
public class JacksonAutoConfiguration {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
// 忽略未知属性
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
// 设置凡是为 null 的字段,返参中均不返回,请根据项目组约定是否开启
// objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
// 设置时区
objectMapper.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
// JavaTimeModule 用于指定序列化和反序列化规则
JavaTimeModule javaTimeModule = new JavaTimeModule();
// 支持 LocalDateTime、LocalDate、LocalTime
// 支持 LocalDateTime、LocalDate、LocalTime
javaTimeModule.addSerializer(LocalDateTime.class,
new LocalDateTimeSerializer(DateConstants.DATE_FORMAT_Y_M_D_H_M_S));
javaTimeModule.addDeserializer(LocalDateTime.class,
new LocalDateTimeDeserializer(DateConstants.DATE_FORMAT_Y_M_D_H_M_S));
javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateConstants.DATE_FORMAT_Y_M_D));
javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateConstants.DATE_FORMAT_Y_M_D));
javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateConstants.DATE_FORMAT_H_M_S));
javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateConstants.DATE_FORMAT_H_M_S));
// 支持 YearMonth
javaTimeModule.addSerializer(YearMonth.class, new YearMonthSerializer(DateConstants.DATE_FORMAT_Y_M));
javaTimeModule.addDeserializer(YearMonth.class, new YearMonthDeserializer(DateConstants.DATE_FORMAT_Y_M));
objectMapper.registerModule(javaTimeModule);
JsonUtils.init(objectMapper);
return objectMapper;
}
}
+93
View File
@@ -0,0 +1,93 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>top.crushtj</groupId>
<artifactId>xiaoyishu</artifactId>
<version>${revision}</version>
</parent>
<artifactId>xiaoyi-gateway</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>网关服务</description>
<dependencies>
<dependency>
<groupId>top.crushtj</groupId>
<artifactId>xiaoyi-common</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!-- 服务发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 网关 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- 负载均衡 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-reactor-spring-boot3-starter</artifactId>
</dependency>
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
</dependency>
<!-- 提供Redis连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- jasypt 加密工具 -->
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Jackson 组件 -->
<dependency>
<groupId>top.crushtj</groupId>
<artifactId>xiaoyi-spring-boot-starter-jackson</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,20 @@
package top.crushtj.xiaoyi.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author ayi
* @version V1.0
* @title XiaoyiGatewayApplication
* @date 2026/2/2 16:46
* @description 网关启动类
*/
@SpringBootApplication
public class XiaoyiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(XiaoyiGatewayApplication.class, args);
}
}
@@ -0,0 +1,55 @@
package top.crushtj.xiaoyi.gateway.auth;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.NotRoleException;
import cn.dev33.satoken.reactor.filter.SaReactorFilter;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author ayi
* @version V1.0
* @title SaTokenConfigure
* @date 2026/2/2 17:16
* @description SaToken配置类
*/
@Configuration
public class SaTokenConfigure {
// 注册 Sa-Token全局过滤器
@Bean
public SaReactorFilter getSaReactorFilter() {
return new SaReactorFilter()
// 拦截地址
.addInclude("/**") /* 拦截全部path */
// 鉴权方法:每次访问进入
.setAuth(obj -> {
// 登录校验
SaRouter.match("/**") // 拦截所有路由
.notMatch("/auth/user/login") // 排除登录接口
.notMatch("/auth/verification/code/send") // 排除验证码发送接口
.check(r -> StpUtil.checkLogin()) // 校验是否登录
;
// SaRouter.match("/auth/user/logout", r -> StpUtil.checkPermission("user"));
// 权限认证 -- 不同模块, 校验不同权限
// SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
// SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
// SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
// SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));
// 更多匹配 ... */
})
.setError(e -> {
if (e instanceof NotLoginException) { // 未登录异常
throw new NotLoginException(e.getMessage(), null, null);
} else if (e instanceof NotPermissionException || e instanceof NotRoleException) { // 权限不足,或不具备角色,统一抛出权限不足异常
throw new NotPermissionException(e.getMessage());
} else { // 其他异常,则抛出一个运行时异常
throw new RuntimeException(e.getMessage());
}
});
}
}
@@ -0,0 +1,107 @@
package top.crushtj.xiaoyi.gateway.auth;
import cn.dev33.satoken.stp.StpInterface;
import cn.hutool.core.collection.CollUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import jakarta.annotation.Resource;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import top.crushtj.xiaoyi.gateway.constant.RedisKeyConstants;
import java.util.List;
import java.util.Objects;
/**
* @author ayi
* @version V1.0
* @title StpInterfaceImpl
* @date 2026/2/2 17:14
* @description 自定义权限验证接口
*/
@Slf4j
@Component
public class StpInterfaceImpl implements StpInterface {
@Resource
private RedisTemplate<String, String> redisTemplate;
@Resource
private ObjectMapper objectMapper;
@SneakyThrows
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
log.info("## 获取用户权限列表, loginId: {}", loginId);
// 构建 用户-角色 Redis Key
String userRolesKey = RedisKeyConstants.buildUserRoleKey(Long.valueOf(loginId.toString()));
// 根据用户 ID ,从 Redis 中获取该用户的角色集合
String useRolesValue = redisTemplate.opsForValue()
.get(userRolesKey);
if (StringUtils.isBlank(useRolesValue)) {
return null;
}
// 将 JSON 字符串转换为 List<String> 角色集合
List<String> userRoleKeys = objectMapper.readValue(useRolesValue, new TypeReference<>() {
});
if (CollUtil.isNotEmpty(userRoleKeys)) {
// 查询这些角色对应的权限
// 构建 角色-权限 Redis Key 集合
List<String> rolePermissionsKeys = userRoleKeys.stream()
.map(RedisKeyConstants::buildRolePermissionsKey)
.toList();
// 通过 key 集合批量查询权限,提升查询性能。
List<String> rolePermissionsValues = Objects.requireNonNull(redisTemplate.opsForValue()
.multiGet(rolePermissionsKeys))
.stream()
.filter(Objects::nonNull)
.toList();
if (CollUtil.isNotEmpty(rolePermissionsValues)) {
List<String> permissions = Lists.newArrayList();
// 遍历所有角色的权限集合,统一添加到 permissions 集合中
rolePermissionsValues.forEach(jsonValue -> {
try {
// 将 JSON 字符串转换为 List<String> 权限集合
List<String> rolePermissions = objectMapper.readValue(jsonValue, new TypeReference<>() {
});
permissions.addAll(rolePermissions);
} catch (JsonProcessingException e) {
log.error("==> JSON 解析错误: ", e);
}
});
// 返回此用户所拥有的权限
return permissions;
}
}
return null;
}
@SneakyThrows
@Override
public List<String> getRoleList(Object loginId, String loginType) {
log.info("## 获取用户角色列表, loginId: {}", loginId);
String userRolesKey = RedisKeyConstants.buildUserRoleKey(Long.valueOf(loginId.toString()));
String useRolesValue = redisTemplate.opsForValue()
.get(userRolesKey);
if (StringUtils.isBlank(useRolesValue)) {
return null;
}
// 将 JSON 字符串转换为 List<String> 集合
return objectMapper.readValue(useRolesValue, new TypeReference<>() {
});
}
}
@@ -0,0 +1,41 @@
package top.crushtj.xiaoyi.gateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.lang.NonNull;
/**
*
* @author ayi
* @version V1.0
* @title RedisTemplateConfig
* @description Redis 配置类
* @date 2026/01/12 19:13
*/
@Configuration
public class RedisTemplateConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(@NonNull RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 设置 RedisTemplate 的连接工厂
redisTemplate.setConnectionFactory(connectionFactory);
// 使用 StringRedisSerializer 来序列化和反序列化 redis 的 key 值,确保 key 是可读的字符串
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// 使用 Jackson2JsonRedisSerializer 来序列化和反序列化 redis 的 value 值, 确保存储的是 JSON 格式
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
redisTemplate.setValueSerializer(serializer);
redisTemplate.setHashValueSerializer(serializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
@@ -0,0 +1,42 @@
package top.crushtj.xiaoyi.gateway.constant;
/**
* @author ayi
* @version V1.0
* @title RedisKeyConstants
* @date 2026/2/3 15:24
* @description Redis key 常量
*/
public class RedisKeyConstants {
/**
* 用户对应角色集合 KEY 前缀
*/
private static final String USER_ROLES_KEY_PREFIX = "user:roles:";
/**
* 角色对应的权限集合 KEY 前缀
*/
private static final String ROLE_PERMISSIONS_KEY_PREFIX = "role:permissions:";
/**
* 构建角色对应的权限集合 KEY
*
* @param roleKey 角色 key
* @return 角色对应的权限集合 KEY
*/
public static String buildRolePermissionsKey(String roleKey) {
return ROLE_PERMISSIONS_KEY_PREFIX + roleKey;
}
/**
* 构建用户-角色 KEY
*
* @param userId 用户 ID
* @return 用户-角色 KEY
*/
public static String buildUserRoleKey(Long userId) {
return USER_ROLES_KEY_PREFIX + userId;
}
}
@@ -0,0 +1,29 @@
package top.crushtj.xiaoyi.gateway.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import top.crushtj.framework.common.exception.BaseExceptionInterface;
/**
* @author ayi
* @version V1.0
* @title ResponseCodeEnum
* @date 2026/2/3 16:15
* @description 异常状态吗枚举
*/
@Getter
@AllArgsConstructor
public enum ResponseCodeEnum implements BaseExceptionInterface {
// ----------- 通用异常状态码 -----------
SYSTEM_ERROR("500", "系统繁忙,请稍后再试"),
UNAUTHORIZED("401", "没有权限"),
// ----------- 业务异常状态码 -----------
;
// 异常码
private final String errorCode;
// 错误信息
private final String errorMessage;
}
@@ -0,0 +1,78 @@
package top.crushtj.xiaoyi.gateway.exception;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.SaTokenException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import top.crushtj.framework.common.response.Response;
import top.crushtj.xiaoyi.gateway.enums.ResponseCodeEnum;
/**
* @author ayi
* @version V1.0
* @title GlobalExceptionHandler
* @date 2026/2/3 16:17
* @description 全局异常处理
*/
@Slf4j
@Component
public class GlobalExceptionHandler implements ErrorWebExceptionHandler {
@Resource
private ObjectMapper objectMapper;
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
// 获取响应对象
ServerHttpResponse response = exchange.getResponse();
log.error("==> 全局异常捕获: ", ex);
// 响参
Response<?> result;
// 根据捕获的异常类型,设置不同的响应状态码和响应消息
if (ex instanceof SaTokenException) { // Sa-Token 异常
// 权限认证失败时,设置 401 状态码
response.setStatusCode(HttpStatus.UNAUTHORIZED);
if (ex instanceof NotLoginException){
// 构建响应结果
result = Response.failure(ResponseCodeEnum.UNAUTHORIZED.getErrorCode(),
ex.getMessage());
}else if (ex instanceof NotPermissionException) {
result = Response.failure(ResponseCodeEnum.UNAUTHORIZED.getErrorCode(),
ResponseCodeEnum.UNAUTHORIZED.getErrorMessage());
} else {
result = Response.failure(ResponseCodeEnum.SYSTEM_ERROR);
}
} else { // 其他异常,则统一提示 “系统繁忙” 错误
result = Response.failure(ResponseCodeEnum.SYSTEM_ERROR);
}
// 设置响应头的内容类型为 application/json;charset=UTF-8,表示响应体为 JSON 格式
response.getHeaders()
.setContentType(MediaType.APPLICATION_JSON_UTF8);
// 设置 body 响应体
return response.writeWith(Mono.fromSupplier(() -> { // 使用 Mono.fromSupplier 创建响应体
DataBufferFactory bufferFactory = response.bufferFactory();
try {
// 使用 ObjectMapper 将 result 对象转换为 JSON 字节数组
return bufferFactory.wrap(objectMapper.writeValueAsBytes(result));
} catch (Exception e) {
// 如果转换过程中出现异常,则返回空字节数组
return bufferFactory.wrap(new byte[0]);
}
}));
}
}
@@ -0,0 +1,40 @@
package top.crushtj.xiaoyi.gateway.filter;
import cn.dev33.satoken.stp.StpUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* @author ayi
* @version V1.0
* @title AddUserId2HeaderFilter
* @date 2026/2/4 15:28
* @description 添加用户ID到请求头过滤器
*/
@Slf4j
@Component
public class AddUserId2HeaderFilter implements GlobalFilter {
private static final String HEADER_USER_ID = "userId";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
Long userId = null;
try {
userId = StpUtil.getLoginIdAsLong();
} catch (Exception e) {
return chain.filter(exchange);
}
Long finalUserId = userId;
ServerWebExchange newExchange = exchange.mutate()
.request(builder -> builder.header(HEADER_USER_ID, String.valueOf(finalUserId)))
.build();
return chain.filter(newExchange);
}
}
@@ -0,0 +1,54 @@
server:
port: 8000 # 指定启动端口
spring:
cloud:
gateway:
routes:
- id: auth
uri: lb://xiaoyishu-auth
predicates:
- Path=/auth/**
filters:
- StripPrefix=1
data:
redis:
database: 0 # Redis 数据库索引(默认为 0)
host: ENC(C1TWXF+/HzWQBF25uXCdy/0fHoRDXdCW72+NKCIJURg4l3IDnJzl278KmFhfsusX) # Redis 服务器地址
port: 6379 # Redis 服务器连接端口
password: ENC(iK/k0IGPflACqYMUwX4N/sGvCVuysYywLcAO+Ikeqk326V8hCr8dgEGzkiEIwWOo) # Redis 服务器连接密码(默认为空)
timeout: 5s # 读超时时间
connect-timeout: 5s # 链接超时时间
lettuce:
pool:
max-active: 200 # 连接池最大连接数
max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)
min-idle: 0 # 连接池中的最小空闲连接
max-idle: 10 # 连接池中的最大空闲连接
############## Sa-Token 配置 ##############
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: Authorization
token-prefix: Bearer
# token 有效期(单位:秒) 默认30天,-1 代表永久有效
timeout: 2592000
# timeout: 60
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
active-timeout: -1
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
is-share: true
# token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik
token-style: random-128
# 是否输出操作日志
is-log: true
jasypt:
encryptor:
password:
algorithm: PBEWithHMACSHA512AndAES_256
key-obtention-iterations: 1000
string-output-type: base64
provider-name: SunJCE
iv-generator-classname: org.jasypt.iv.RandomIvGenerator

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