feat: 职位管理,增删改查功能,投递简历功能

This commit is contained in:
wangjx 2025-02-21 19:00:22 +08:00
commit 3aacff04ce
244 changed files with 30832 additions and 0 deletions

109
.cz-config.js Normal file
View File

@ -0,0 +1,109 @@
module.exports = {
// type 类型(定义之后,可通过上下键选择)
types: [
{ value: "improvement", name: "improvement: 功能优化" },
{ value: "fix", name: "fix: 修复 bug" },
{ value: "feat", name: "feat: 新增功能" },
{
value: "style",
name: "style: 代码格式(不影响功能,例如空格、分号等格式修正)",
},
{ value: "docs", name: "docs: 文档变更" },
{
value: "refactor",
name: "refactor: 代码重构(不包括 bug 修复、功能新增)",
},
{ value: "perf", name: "perf: 性能优化" },
{ value: "test", name: "test: 添加、修改测试用例" },
{
value: "ci",
name: "ci: 修改 CI 配置、脚本如打包部署脚本、dockerfile等",
},
{ value: "revert", name: "revert: 回滚 commit" },
{
value: "build",
name: "build: 构建流程、外部依赖变更(如升级 npm 包、修改 webpack 配置等)",
},
{
value: "chore",
name: "chore: 版本发布或对构建过程或辅助工具和库的更改(不影响源文件、测试用例)",
},
// { value: ':bug: fix', name: '🐛 fix: 修复 bug' },
// { value: ':sparkles: feat', name: '✨ feat: 新增功能' },
// {
// value: ':lipstick: style',
// name: '💄 style: 代码格式(不影响功能,例如空格、分号等格式修正)'
// },
// { value: ':memo: docs', name: '📝 docs: 文档变更' },
// { value: ':recycle: refactor', name: '♻️ refactor: 代码重构(不包括 bug 修复、功能新增)' },
// { value: ':zap: perf', name: '⚡️ perf: 性能优化' },
// { value: ':white_check_mark: test', name: '✅ test: 添加、修改测试用例' },
// {
// value: ':hammer: chore',
// name: '🔨 chore: 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)'
// },
// { value: ':wrench: ci', name: '🔧 ci: 修改 CI 配置、脚本' },
// { value: ':rocket: deps', name: '🚀 deps: 升级依赖' }
// {
// value: ':bookmark: release',
// name: '🔖 release: 版本发布'
// }
],
// scope 类型(定义之后,可通过上下键选择)
scopes: [
["custom", "自定义范围例如sys/user"],
["utils", "utils 相关"],
["router", "router 相关"],
].map(([value, description]) => {
return {
value,
name: `${value.padEnd(30)} (${description})`,
};
}),
// 是否允许自定义填写 scope在 scope 选择的时候,会有 empty 和 custom 可以选择。
// allowCustomScopes: true,
// allowTicketNumber: false,
// isTicketNumberRequired: false,
// ticketNumberPrefix: 'TICKET-',
// ticketNumberRegExp: '\\d{1,5}',
// 针对每一个 type 去定义对应的 scopes例如 fix
scopeOverrides: {
chore: [
{ value: "release", name: "release 版本发布" },
{ value: "custom", name: "custom 自定义scope" },
],
ci: [
{ value: "deploy", name: "deploy 部署脚本" },
{ value: "docker", name: "docker docker相关" },
{ value: "custom", name: "custom 自定义scope" },
],
build: [
{ value: "deps", name: "deps 依赖变更" },
{ value: "custom", name: "custom 自定义scope" },
],
},
// 交互提示信息
messages: {
type: "确保本次提交遵循 Angular 规范!\n选择你要提交的类型",
scope: "\n选择一个 scope可选",
customScope: "请输入自定义的 scope",
subject: "填写简短精炼的变更描述:\n",
body: '填写更加详细的变更描述(可选)。使用 "|" 换行:\n',
breaking: "列举非兼容性重大的变更(可选):\n",
footer: "列举出所有变更的 ISSUES CLOSED可选。 例如: #31, #34\n",
confirmCommit: "确认提交?",
},
// ['feat', 'fix']设置只有 type 选择了 feat 或 fix才询问 breaking message
allowBreakingChanges: [],
// 跳过要询问的步骤
skipQuestions: ["body"],
// subject 限制长度
subjectLimit: 100,
breaklineChar: "|", // 支持 body 和 footer
// footerPrefix : 'ISSUES CLOSED:'
// askForBreakingChangeFirst : true,
};

3
.dockerignore Normal file
View File

@ -0,0 +1,3 @@
.gradle/
build/
.idea/

16
.editorconfig Normal file
View File

@ -0,0 +1,16 @@
# Editor configuration, see http://editorconfig.org
# 表示是最顶层的 EditorConfig 配置文件
root = true
[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符集为 utf-8
indent_style = space # 缩进风格tab | space
indent_size = 2 # 缩进大小
end_of_line = lf # 控制换行类型(lf | cr | crlf)
trim_trailing_whitespace = true # 去除行首的任意空白字符
insert_final_newline = true # 始终在文件末尾插入一个新行
[*.md] # 表示仅 md 文件适用以下规则
max_line_length = off
trim_trailing_whitespace = false

36
.gitattributes vendored Normal file
View File

@ -0,0 +1,36 @@
* text=auto
# Force the following filetypes to have unix eols, so Windows does not break them
*.* text eol=lf
# Separate configuration for files without suffix
LICENSE text eol=lf
Dockerfile text eol=lf
pre-commit text eol=lf
commit-msg text eol=lf
gradlew text eol=lf
# These files are binary and should be left untouched
# (binary is a macro for -text -diff)
*.ico binary
*.jpg binary
*.jpeg binary
*.png binary
*.pdf binary
*.doc binary
*.docx binary
*.ppt binary
*.pptx binary
*.xls binary
*.xlsx binary
*.exe binary
*.jar binary
*.ttf binary
*.woff binary
*.woff2 binary
*.eot binary
*.otf binary
# Add more binary...

6
.gitea/ISSUE_TEMPLATE.md Normal file
View File

@ -0,0 +1,6 @@
## 截图
<!--操作截图、日志截图、相关代码截图-->
## 描述
<!--回显步骤、问题描述、预期结果-->

View File

@ -0,0 +1,40 @@
name: Bug
about: 创建一个报告Bug的工单。
title: "应用: "
labels:
- kind/bug
body:
- type: markdown
attributes:
value: |
标题前方的 "应用" 请改为涉及的应用名称,且标题内容用一句话概括所描述情况,例如:"跟进: 修复跟进内容加密后emoji图标无法正常显示"
- type: dropdown
attributes:
label: 影响范围
multiple: true
options:
- 微信小程序
- H5
- Web
- 服务端
- type: textarea
attributes:
label: 描述
description: 请尽可能描述清楚例如问题描述、回显步骤、预期结果等,以免日后不能理解当初此工单内容表达的含义
placeholder: 请输入...
validations:
required: true
- type: textarea
attributes:
label: 截图
description: 操作截图、日志截图、相关代码截图等
placeholder: 使用快捷键 ctrl+v 可粘贴图片
- type: input
attributes:
label: 提出人
description: 若问题非创建者提出,而是例如客户或用户提出,此时应记录真实提出人员
placeholder: 例如:张三

View File

@ -0,0 +1,40 @@
name: Feat
about: 创建一个新增功能的工单。
title: "应用: "
labels:
- kind/feat
body:
- type: markdown
attributes:
value: |
标题前方的 "应用" 请改为涉及的应用名称,且标题内容用一句话概括所描述情况,例如:"跟进: 新增分组拖拽排序功能"
- type: dropdown
attributes:
label: 影响范围
multiple: true
options:
- 微信小程序
- H5
- Web
- 服务端
- type: textarea
attributes:
label: 描述
description: 请尽可能描述清楚新功能作用、操作逻辑、预期结果等,以免日后不能理解当初此工单内容表达的含义
placeholder: 请输入...
validations:
required: true
- type: textarea
attributes:
label: 截图
description: 需求截图、设计截图等
placeholder: 使用快捷键 ctrl+v 可粘贴图片
- type: input
attributes:
label: 提出人
description: 若新功能非创建者提出,而是例如客户或用户提出,此时应记录真实提出人员
placeholder: 例如:张三

View File

@ -0,0 +1,40 @@
name: Improvement
about: 创建一个功能优化的工单。
title: "应用: "
labels:
- kind/improvement
body:
- type: markdown
attributes:
value: |
标题前方的 "应用" 请改为涉及的应用名称,且标题内容用一句话概括所描述情况,例如:"跟进: 优化时间轴内容展示图标边距"
- type: dropdown
attributes:
label: 影响范围
multiple: true
options:
- 微信小程序
- H5
- Web
- 服务端
- type: textarea
attributes:
label: 描述
description: 请尽可能描述清楚例如问题描述、回显步骤、预期结果等,以免日后不能理解当初此工单内容表达的含义
placeholder: 请输入...
validations:
required: true
- type: textarea
attributes:
label: 截图
description: 操作截图、日志截图、相关代码截图等
placeholder: 使用快捷键 ctrl+v 可粘贴图片
- type: input
attributes:
label: 提出人
description: 若问题非创建者提出,而是例如客户或用户提出,此时应记录真实提出人员
placeholder: 例如:张三

View File

@ -0,0 +1,64 @@
name: CI
# 打标签时触发构建另外标签需v开头例如v1.0.0需要配置DOCKER_PASSWORD的secrets
# 构建后镜像为 ${docker_registry}/${docker_username}/${repo_name}:1.0.0
on:
push:
tags:
- v*
env:
DOCKER_REGISTRY: registry.cn-hangzhou.aliyuncs.com
DOCKER_USERNAME: rsjst
jobs:
build-image:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 1
- name: Cache Runner ToolCache
uses: actions/cache@v3
with:
path: ${{ env.RUNNER_TOOL_CACHE }}
key: ${{ runner.os }}-runner-tool-cache
- name: Setup Java
uses: actions/setup-java@v3
with:
distribution: zulu
java-version: 17
cache: gradle
- name: Gradle Build
run: |
chmod a+x ./gradlew
./gradlew bootJar -x test --no-daemon
- name: Get Meta
id: meta
run: |
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
echo REPO_VERSION=$(echo ${{ github.ref }} | awk -F"/" '{print $3}' | awk -F"v" '{print $2}') >> $GITHUB_OUTPUT
- name: Docker build push
uses: seepine/action-docker-build-push@v1
with:
registry: ${{ env.DOCKER_REGISTRY }}
username: ${{ env.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
- name: WeChat Work notification
uses: seepine/action-wechat-work@master
env:
WECHAT_WORK_BOT_WEBHOOK: ${{ secrets.WECHAT_WORK_BOT_WEBHOOK }}
if: ${{ env.WECHAT_WORK_BOT_WEBHOOK != '' }}
with:
msgtype: markdown
content: "${{ steps.meta.outputs.REPO_NAME }} build docker image success.\n
> Tag: ${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_USERNAME }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}"

View File

@ -0,0 +1,20 @@
name: Checks
on:
- pull_request
jobs:
test:
name: commit lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 10
- name: Setup Pnpm and Install
uses: seepine/action-setup-pnpm@v1
- name: Commit lint
run: npx commitlint --to HEAD --verbose

View File

@ -0,0 +1,32 @@
name: Checks
on:
- pull_request
jobs:
test:
name: gradle package
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 1
- name: Cache Runner ToolCache
uses: actions/cache@v3
with:
path: ${{ env.RUNNER_TOOL_CACHE }}
key: ${{ runner.os }}-runner-tool-cache
- name: Setup Java
uses: actions/setup-java@v3
with:
distribution: zulu
java-version: 17
cache: gradle
- name: Gradle Build
run: |
chmod a+x ./gradlew
./gradlew bootJar -x test --no-daemon

85
.gitignore vendored Normal file
View File

@ -0,0 +1,85 @@
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### temp ignore ###
*.cache
*.diff
*.patch
*.tmp
*.java~
*.properties~
*.xml~
### system ignore ###
.DS_Store
Thumbs.db
Servers
.metadata
upload
gen_code
### logs ####
/logs/
*.log
### node ###
node_modules
dist
dist-ssr
*.local
# local env files
.env.local
.env.*.local
# log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# editor directories and files
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# lock file
yarn.lock
package-lock.json

4
.husky/commit-msg Normal file
View File

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx --no-install commitlint --edit

1
.npmrc Normal file
View File

@ -0,0 +1 @@
registry=https://registry.npmmirror.com

23
Dockerfile Normal file
View File

@ -0,0 +1,23 @@
FROM git.zgfxrc.cn/registry/alpine:17-jre as builder
COPY app/build/libs/*.jar /work/application.jar
WORKDIR /work
RUN java -Djarmode=layertools -jar application.jar extract
FROM git.zgfxrc.cn/registry/alpine:17-jre
WORKDIR work
COPY --from=builder /work/spring-boot-loader/ ./
COPY --from=builder /work/snapshot-dependencies/ ./
COPY --from=builder /work/dependencies/ ./
COPY --from=builder /work/application/ ./
ENV JAVA_OPTS="-server -Xms512m -Xmx512m" \
SPRING_PROFILES_ACTIVE="prod"
HEALTHCHECK --interval=5s --timeout=2s --retries=3 --start-period=15s\
CMD curl -fs -X POST http://127.0.0.1:8020/health || exit 1
ENTRYPOINT crond && wait.sh && java ${JAVA_OPTS} -Ddruid.mysql.usePingMethod=false \
-Djava.security.egd=file:/dev/./urandom \
-Dspring.profiles.active=${SPRING_PROFILES_ACTIVE} \
org.springframework.boot.loader.JarLauncher

331
README.md Normal file
View File

@ -0,0 +1,331 @@
# FxBoot
## 一、开发准备
### 1.开发工具IDEA
### 2.安装插件
#### google-java-format谷歌代码规范插件
Preferences->Other Settings->google-java-format Settings
![image.png](https://s1.ax1x.com/2022/08/29/vf9aKP.png)
#### save actions保存代码自动格式化插件
Preferences->Other Settings->Save Actions
![image.png](https://s1.ax1x.com/2022/08/29/vf9Nvt.png)
#### Alibaba Java Coding Guidelines阿里编码规范插件
#### JRebel and XRebel热加载插件
下载旧版本手动安装[JRebel and XRebel 2022.4.1](https://plugins.jetbrains.com/plugin/4441-jrebel-and-xrebel/versions)
跟随JRebel setup guide导航一步步设置激活插件可前往[jrebel.qekang.com](http://jrebel.qekang.com/)获取一键地址,例如
```
地址https://jrebel.qekang.com/f9754b17-23dd-4915-a0c6-4fafef7e46e0
邮箱:随意
```
而后可使用该插件代替原有的启动按钮,修改代码后无需重启即可生效
![zUHjzT.png](https://s1.ax1x.com/2022/11/28/zUHjzT.png)
## 二、快速入门
### 1.修改项目
配置根目录的`gradle.properties`,修改项目名、版本号(有必要的话)。
```shell
projectName=fx-boot
projectVersion=1.0.0
```
### 2.导入数据库
新建fxboot数据库字符集选择`utf8mb4`,排序规则选择`utf8mb4_general_ci`
将根目录下的`db/fxboot.sql`导入到新建的数据库中
### 3.修改配置
> 具体可查阅 `app/resources/application.yml`
若数据库为`127.0.0.1:3306`且账号密码都为`root`,则可跳过此步骤
否则可配置环境变量(如何配置请自行搜索),支持环境变量如下
#### mysql
- FXBOOT_MYSQL_HOST
- FXBOOT_MYSQL_PORT
- FXBOOT_MYSQL_USER
- FXBOOT_MYSQL_PASSWORD
#### redis
- FXBOOT_REDIS_HOST
- FXBOOT_REDIS_PORT
- FXBOOT_REDIS_DATABASE
### 4.运行项目
在app模块中的FxBootApplication启动类上右键运行或者在IDE右上角找到启动按钮点击启动
### 5.初始化提交环境
> 需要安装npm且安装pnpm未安装可执行`npm i -g pnpm`
```shell
pnpm i
```
### 6.提交代码
#### 6.1 开启暂存区
![img.png](https://s1.ax1x.com/2022/12/08/z2NDdx.png)
#### 6.2 暂存想要提交的文件
每次暂存前,请审查一遍自己变动的,降低审查被驳回的概率
![img.png](https://s1.ax1x.com/2022/12/08/z2NfOA.png)
#### 6.3 提交已暂存的文件
具体提交规范查看相关文档
```shell
pnpm cz
```
## 三、开发规范
### 1.结构分层
分controller/entity/mapper/service四个层次复杂模块需再次分级
- controller/system
- controller/base
### 2.注释
- 类上需`author``date`
```java
/**
* @author huanghs@zgfxrc.cn
* @date 2021.11.22
*/
```
- 方法上需通过/**回车自动生成注释,并补齐入参和返回的说明。
```java
/**
* 不为empty则执行方法empty包含null、数组队列空等情况
*
* @param obj 对象
* @param apply 方法(传入对象)
*/
```
- 若方法添加时间与类不一致,需要增加如`@date 2021.11.22`说明。
```java
/**
* 不为empty则执行方法empty包含null、数组队列空等情况
*
* @param obj 对象
* @param apply 方法(传入对象)
* @date 2021.11.22
*/
```
- 若且此类为他人创建,而你创建了方法,则需要增加如`@date 2021.11.22 your@email.com`说明
```java
/**
* 不为empty则执行方法empty包含null、数组队列空等情况
*
* @param obj 对象
* @param apply 方法(传入对象)
* @date 2021.11.22 your@email.com
*/
```
### 3.entity规范
注释注解
```java
/**
* 用户表
*
* @author huanghs@zgfxrc.cn
* @date 2021.11.22
*/
@Data
@ApiModel("用户表")
@EqualsAndHashCode(callSuper = true)
public class User extends Model<User> {
}
```
关联表无主键
```java
public class User extends Model<User> {
}
```
有主键
```java
public class User extends BaseModel<User> {
}
```
有主键且需要租户过滤
```java
public class User extends BaseModelWithTenant<User> {
}
```
### 4.controller规范
需要使用`@Api`说明业务, 其内方法需`@ApiOperation`说明具体功能, 通过`@Permission`指定访问接口所需权限
控制层不写业务逻辑所有代码需封装在Service中控制层只需要`service.runYourFunc()`调用封装的业务方法。
```java
/**
* @author huanghs@zgfxrc.cn
* @date 2021.11.22
*/
@RestController
@Api(tags = "用户管理")
@RequestMapping("sys/user")
public class SysUserController {
final SysUserService sysUserService;
// 最终会需要 sys_user_view 权限
@Permission("sys_user_view")
@ApiOperation("查询")
@PostMapping
public void get() {
sysUserService.runYourFunc();
}
}
```
### 5.RequestMapper
(1).遵循小写单词,多单词由/隔开,如`@RequestMapping("sys/user")`
(2).遵循POST规范所有接口使用`PostMapping`定义
(3).传参只能通过params传递`void func(Entity entity)`或者body传递`void func(@RequestBody Entity entity)`
(4).路由传参通过`@PostMapping("/get/one/{id}")`,通过`@PathVariable Long id`接收
### 6.Service规范
- 在Service中不能调用本类的其他方法避免aop失效
- 参数校验使用`Validate.nonNull(req.userId,"用户id不能为空")`等校验值和给出错误提示
## 四、部署
### 1.配置镜像
配置根目录`deploy.sh`的镜像仓库地址,并且在每次发布版本时,只需要修改版本号 `VERSION` 即可。
```properties
HUB=registry.cn-hangzhou.aliyuncs.com/rsjst
VERSION=1.0.0
```
### 2.打包
执行脚本,将会自动构建镜像`镜像仓库地址/项目名:版本号`并推送到仓库中,
例如 `registry.cn-hangzhou.aliyuncs.com/rsjst/fx-boot:1.0.0`
```shell
sh deploy.sh
```
### 3.部署
配置dockerSwarm的stack中的image为打包的镜像即可例如
```yml
version: '3.7'
services:
fx-boot:
image: registry.cn-hangzhou.aliyuncs.com/rsjst/fx-boot:1.0.0
ports:
- '8020:8020'
environment:
# 数据库配置
FXBOOT_MYSQL_HOST: 192.168.1.1
FXBOOT_MYSQL_PORT: 3306
FXBOOT_MYSQL_USER: root
FXBOOT_MYSQL_PASSWORD: 123456
FXBOOT_MYSQL_TABLE: fxboot
# Redis配置
FXBOOT_REDIS_HOST: 127.0.0.1
FXBOOT_REDIS_PORT: 6379
FXBOOT_REDIS_DATABASE: 1
FXBOOT_REDIS_PASSWORD: 123456
# *配置等待数据库启动完毕再启动应用
WAIT_FOR: 192.168.1.1:3306
WAIT_TIMEOUT: 60
# *配置active值若增加模块仍需要往后添加避免无法读取到子模块的配置例如application-upms.yml
SPRING_PROFILES_ACTIVE: prod,upms,recruit
# *存储配置,若不知道填什么向上级要
MINIO_ACCESS_KEY: xxx
MINIO_SECRET_KEY: xxx
MINIO_BUCKET_NAME: xxx
# 下述配置实现热更新,不需要可去除
deploy:
mode: replicated
replicas: 1
resources:
limits:
cpus: '0.75'
reservations:
cpus: '0.50'
update_config:
delay: 5s
order: start-first
```
## 五、相关文档
### 1.鉴权功能
[secret](https://github.com/seepine/secret/)
### 2.统一返回体包装
[wrap-spring-boot-starter](https://seepine.com/springboot/wrap/)
### 3.工具类
[tool](https://github.com/seepine/tool/)
### 4.ORM
[mybatis-plus](https://baomidou.com/)
### 5.部署相关
[构建多平台Docker镜像](https://seepine.com/docker/build-multi-platform-image/)
[Docker nginx conf 使用环境变量](https://seepine.com/docker/nginx/)
### 6.协同开发
[Git多分支开发](https://seepine.com/dev/git/multi-branch/)

21
app/build.gradle Normal file
View File

@ -0,0 +1,21 @@
plugins {
id 'org.springframework.boot'
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation("org.springframework.cloud:spring-cloud-starter-openfeign:3.1.3")
implementation("org.mybatis:mybatis-spring:2.0.7")
implementation("cn.xuyanwu:spring-file-storage:0.7.0")
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation project(":recruit-biz")
implementation project(":upms-biz")
}
test {
useJUnitPlatform()
}
bootJar {
enabled = true
}

View File

@ -0,0 +1,33 @@
package cn.zgfxrc.boot;
import cn.xuyanwu.spring.file.storage.EnableFileStorage;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author huanghs
* @date 2022.8.22
*/
@EnableAsync
@EnableFileStorage
@EnableScheduling
@EnableFeignClients(basePackages = {"cn.zgfxrc", "com.rsjst"})
@MapperScan({"cn.zgfxrc.**.mapper", "com.rsjst.**.mapper"})
@SpringBootApplication(scanBasePackages = {"cn.zgfxrc", "com.rsjst"})
@RestController
public class FxBootApplication {
public static void main(String[] args) {
SpringApplication.run(FxBootApplication.class, args);
}
@PostMapping("health")
public String health() {
return "UP";
}
}

View File

@ -0,0 +1,10 @@
spring:
mvc:
pathmatch:
# swagger3 需要
matching-strategy: ant_path_matcher
logging:
level:
com.seepine.auth: debug
com.seepine.mybatis: debug
cn.zgfxrc.boot.upms.biz.*.mapper: debug

View File

@ -0,0 +1,3 @@
spring:
redis:
password: ${FXBOOT_REDIS_PASSWORD:???}

View File

@ -0,0 +1,3 @@
spring:
redis:
password: ${FXBOOT_REDIS_PASSWORD:???}

View File

@ -0,0 +1,52 @@
server:
port: 8020
spring:
profiles:
active: dev,upms,recruit
jackson:
time-zone: GMT+8
default-property-inclusion: non_null
cache:
type: redis
redis:
host: ${FXBOOT_REDIS_HOST:127.0.0.1}
port: ${FXBOOT_REDIS_PORT:6379}
database: ${FXBOOT_REDIS_DATABASE:1}
servlet:
multipart:
max-file-size: 50MB
max-request-size: 50MB
mvc:
async:
request-timeout: 20000
datasource:
url: jdbc:mysql://${FXBOOT_MYSQL_HOST:58.22.60.29}:${FXBOOT_MYSQL_PORT:3306}/${FXBOOT_MYSQL_TABLE:wangjx_fxboot}?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&allowMultiQueries=true&allowPublicKeyRetrieval=true
username: ${FXBOOT_MYSQL_USER:root}
password: ${FXBOOT_MYSQL_PASSWORD:fxrc123456}
hikari:
idle-timeout: 120000 # 120s 连接会话没使用则关闭
minimum-idle: 0 # 连接池最小连接会话数非serverless数据库建议设置2及以上
maximum-pool-size: 20 # 连接池最大连接会话数
max-lifetime: 300000 # 300s 连接会话最大生命时长,超时而且没被使用则被释放
mybatis-plus:
type-handlers-package: cn.zgfxrc.boot.common.mybatis.handler
mybatis:
tenant:
scan-packages:
- cn.zgfxrc
- com.rsjst
wrap:
status: 500
scan-packages: cn.zgfxrc,com.rsjst
feign:
httpclient:
enabled: false
okhttp:
enabled: true
secret:
# 过期时间
expires-second: 43200
# 缓存前缀
cache-prefix: fx_boot_auth
# 加密密钥
secret: fxboot!!!!fxboot

View File

@ -0,0 +1,11 @@
package cn.zgfxrc.boot;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class FxBootApplicationTests {
@Test
void contextLoads() {}
}

40
build.gradle Normal file
View File

@ -0,0 +1,40 @@
plugins {
id 'org.springframework.boot'
id 'io.spring.dependency-management'
}
allprojects {
repositories {
maven { url = uri("https://mirrors.cloud.tencent.com/nexus/repository/maven-public/") }
mavenCentral()
gradlePluginPortal()
mavenLocal()
}
group = "${projectGroup}" as Object
version = "${projectVersion}" as Object
}
subprojects {
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
all*.exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat"
}
dependencies {
implementation platform("com.seepine:spring-boot-dependencies:3.1.0")
implementation platform("cn.hutool:hutool-bom:5.8.16")
compileOnly "org.projectlombok:lombok:${lombokVersion}"
annotationProcessor "org.projectlombok:lombok:${lombokVersion}"
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
compileJava {
options.encoding = 'UTF-8'
options.compilerArgs << '-parameters'
}
}

23
commitlint.config.js Normal file
View File

@ -0,0 +1,23 @@
module.exports = {
extends: ["@commitlint/config-conventional"],
rules: {
"type-enum": [
2,
"always",
[
"build",
"chore",
"ci",
"docs",
"feat",
"fix",
"perf",
"refactor",
"revert",
"style",
"test",
"improvement",
],
],
},
};

View File

@ -0,0 +1,24 @@
plugins {
id 'java-library'
}
dependencies {
api project(":common-core")
api project(":common-feign")
api project(":common-mybatis")
api project(":upms-api")
api("org.springframework.boot:spring-boot-starter-web")
api("org.springframework.boot:spring-boot-starter-undertow")
api("org.springframework.boot:spring-boot-starter-aop")
api("com.seepine:secret-spring-boot-starter-2:${secretVersion}")
api("com.seepine:wrap-spring-boot-starter:0.5.2")
api "io.springfox:springfox-boot-starter:3.0.0"
api("com.github.yitter:yitter-idgenerator:1.0.6")
api 'org.redisson:redisson-spring-boot-starter:3.23.2'
}
bootJar {
enabled = false
}
jar {
enabled = true
}

View File

@ -0,0 +1,28 @@
package cn.zgfxrc.boot.common.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class CorsConfig {
private CorsConfiguration corsConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOriginPattern("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.setAllowCredentials(true);
corsConfiguration.setMaxAge(3600L);
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfig());
return new CorsFilter(source);
}
}

View File

@ -0,0 +1,75 @@
package cn.zgfxrc.boot.common.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.PackageVersion;
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.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.TimeZone;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author seepine
*/
@Configuration
@ConditionalOnClass(ObjectMapper.class)
@AutoConfigureBefore(JacksonAutoConfiguration.class)
public class JacksonConfig {
public static final String NORM_DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
public static final String NORM_DATE_PATTERN = "yyyy-MM-dd";
public static final String NORM_TIME_PATTERN = "HH:mm:ss";
@Bean
public Jackson2ObjectMapperBuilderCustomizer customizer() {
return builder -> {
builder.locale(Locale.CHINA);
builder.timeZone(TimeZone.getTimeZone(ZoneId.systemDefault()));
builder.simpleDateFormat(NORM_DATETIME_PATTERN);
// 日期序列号
builder.modules(new JavaTimeModule());
// Long序列号解决前端精度丢失
builder.serializerByType(Long.class, ToStringSerializer.instance);
// MybatisPlus 序列化枚举值为数据库存储值
builder.featuresToEnable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);
};
}
static class JavaTimeModule extends SimpleModule {
public JavaTimeModule() {
super(PackageVersion.VERSION);
this.addSerializer(
LocalDateTime.class,
new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(NORM_DATETIME_PATTERN)));
this.addSerializer(
LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(NORM_DATE_PATTERN)));
this.addSerializer(
LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(NORM_TIME_PATTERN)));
this.addDeserializer(
LocalDateTime.class,
new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(NORM_DATETIME_PATTERN)));
this.addDeserializer(
LocalDate.class,
new LocalDateDeserializer(DateTimeFormatter.ofPattern(NORM_DATE_PATTERN)));
this.addDeserializer(
LocalTime.class,
new LocalTimeDeserializer(DateTimeFormatter.ofPattern(NORM_TIME_PATTERN)));
}
}
}

View File

@ -0,0 +1,96 @@
package cn.zgfxrc.boot.common.config;
import io.swagger.annotations.Api;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Objects;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.NonNull;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.oas.annotations.EnableOpenApi;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider;
/**
* Swagger2Config
*
* <p><a href="http://127.0.0.1:port/swagger-ui/index.html">访问地址</a>
*
* @author huanghs
*/
@EnableOpenApi
@Configuration
public class Swagger2Config {
@Value("${spring.profiles.active}")
private String profilesActive;
@Bean
public static BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() {
return new BeanPostProcessor() {
@Override
public Object postProcessAfterInitialization(@NonNull Object bean, @NonNull String beanName)
throws BeansException {
if (bean instanceof WebMvcRequestHandlerProvider) {
customizeSpringfoxHandlerMappings(getHandlerMappings(bean));
}
return bean;
}
private <T extends RequestMappingInfoHandlerMapping> void customizeSpringfoxHandlerMappings(
List<T> mappings) {
List<T> copy =
mappings.stream().filter(mapping -> mapping.getPatternParser() == null).toList();
mappings.clear();
mappings.addAll(copy);
}
@SuppressWarnings("unchecked")
private List<RequestMappingInfoHandlerMapping> getHandlerMappings(Object bean) {
try {
Field field = ReflectionUtils.findField(bean.getClass(), "handlerMappings");
Objects.requireNonNull(field).setAccessible(true);
return (List<RequestMappingInfoHandlerMapping>) field.get(bean);
} catch (IllegalArgumentException | IllegalAccessException | NullPointerException e) {
throw new IllegalStateException(e);
}
}
};
}
// 生成接口信息包括标题联系人联系方式等
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Swagger3接口文档")
.description("如有疑问,请联系开发工程师")
.contact(new Contact("huanghs", "https://seepine.com/", "huanghs@zgfxrc.cn"))
.version("1.0")
.build();
}
@Bean
public Docket defaultDocket() {
// swagger设置基本信息要解析的接口及路径等
return new Docket(DocumentationType.OAS_30)
.enable(profilesActive.contains("dev"))
.apiInfo(apiInfo())
.groupName("defaultDocket")
.select()
// 设置通过什么方式定位需要自动生成文档的接口这里定位方法上的@Api注解
.apis(RequestHandlerSelectors.withClassAnnotation(Api.class))
// 接口URI路径设置any是全路径也可以通过PathSelectors.regex()正则匹配
.paths(PathSelectors.any())
.build();
}
}

View File

@ -0,0 +1,44 @@
package cn.zgfxrc.boot.common.config.auth;
import cn.zgfxrc.boot.upms.api.feign.RemoteUserService;
import com.seepine.secret.entity.AuthUser;
import com.seepine.secret.exception.SecretException;
import com.seepine.secret.interfaces.PermissionService;
import com.seepine.tool.cache.Cache;
import com.seepine.tool.lock.Lock;
import com.seepine.tool.secure.digest.MD5;
import java.time.Duration;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import javax.annotation.Nonnull;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@AllArgsConstructor
public class PermissionServiceImpl implements PermissionService {
private final RemoteUserService remoteUserService;
@Nonnull
@Override
public Set<String> query(@NotNull AuthUser authUser) throws SecretException {
try {
return Objects.requireNonNullElse(
Cache.getIfPresent(
"secret_permissions:" + MD5.digestHex(authUser.getToken()),
() -> {
var permissions =
Lock.sync(authUser.getId(), () -> remoteUserService.getPermissions(authUser));
return permissions.isEmpty() ? null : permissions;
},
Duration.ofMinutes(10).toMillis()),
new HashSet<>());
} catch (Exception e) {
throw new SecretException(e);
}
}
}

View File

@ -0,0 +1,109 @@
// package cn.zgfxrc.boot.common.config.base;
//
// import cn.zgfxrc.boot.common.core.entity.base.BaseModel;
// import cn.zgfxrc.boot.common.core.entity.interfaces.AddGroup;
// import cn.zgfxrc.boot.common.core.entity.interfaces.DelGroup;
// import cn.zgfxrc.boot.common.core.entity.interfaces.EditGroup;
// import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
// import com.baomidou.mybatisplus.core.mapper.BaseMapper;
// import com.baomidou.mybatisplus.core.metadata.IPage;
// import com.baomidou.mybatisplus.core.toolkit.Wrappers;
// import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
// import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
// import com.seepine.secret.annotation.Log;
// import com.seepine.secret.annotation.Permission;
// import com.seepine.wrap.WrapException;
// import io.swagger.annotations.ApiOperation;
// import io.swagger.annotations.ApiParam;
// import org.springframework.beans.factory.annotation.Autowired;
// import org.springframework.lang.Nullable;
// import org.springframework.validation.annotation.Validated;
// import org.springframework.web.bind.annotation.PostMapping;
// import org.springframework.web.bind.annotation.RequestBody;
//
// import java.util.List;
//
/// **
// * 包含基础crud接口 若有权限需要一定重写方法并加上鉴权
// *
// * @author huanghs
// */
// public class BaseController<
// S extends ServiceImpl<M, T>, M extends BaseMapper<T>, T extends BaseModel<T>> {
// @Autowired(required = false)
// protected S service;
//
// /**
// * 分页查询
// *
// * @param page 分页对象
// * @param entity 实体类
// * @return IPage
// */
// @ApiOperation("获取分页")
// @PostMapping("page")
// @Permission("view")
// public IPage<T> page(Page<T> page, @RequestBody T entity) {
// LambdaQueryWrapper<T> wrapper = Wrappers.lambdaQuery(entity);
// return service.page(page, wrapper);
// }
//
// /**
// * 分页查询
// *
// * @param entity 实体类
// * @return List
// */
// @ApiOperation("获取数组")
// @PostMapping("list")
// @Permission("view")
// public List<T> list(@RequestBody @Nullable T entity) {
// LambdaQueryWrapper<T> wrapper = Wrappers.lambdaQuery(entity);
// return service.list(wrapper);
// }
//
// /**
// * 新增
// *
// * @param entity 实体类
// */
// @Log("新增")
// @ApiOperation("新增")
// @PostMapping("add")
// @Permission("add")
// public void save(@RequestBody @Validated(AddGroup.class) T entity) {
// if (!service.save(entity)) {
// throw new WrapException("保存失败");
// }
// }
//
// /**
// * 修改
// *
// * @param entity 实体类
// */
// @Log("修改")
// @ApiOperation("修改")
// @PostMapping("edit")
// @Permission("edit")
// public void updateById(@RequestBody @Validated(EditGroup.class) T entity) {
// if (!service.updateById(entity)) {
// throw new WrapException("修改失败");
// }
// }
//
// /**
// * 通过id删除
// *
// * @param entity 实体类
// */
// @Log("删除")
// @ApiOperation("根据id删除")
// @PostMapping("del")
// @Permission("del")
// public void removeById(@ApiParam("主键") @RequestBody @Validated(DelGroup.class) T entity) {
// if (!service.removeById(entity.getId())) {
// throw new WrapException("删除失败");
// }
// }
// }

View File

@ -0,0 +1,41 @@
package cn.zgfxrc.boot.common.config.exception;
import com.seepine.tool.R;
import java.util.Map;
import java.util.Objects;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable;
import org.springframework.boot.autoconfigure.web.servlet.error.AbstractErrorController;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 解决如404等内部错误
*
* @author seepine
*/
@Slf4j
@RestController
public class GlobalExceptionController extends AbstractErrorController {
private static final String ERROR_PATH = "/error";
public GlobalExceptionController(@Nullable ErrorAttributes errorAttributes) {
super(Objects.requireNonNull(errorAttributes));
}
@RequestMapping(value = ERROR_PATH)
public R<?> error(HttpServletRequest request, HttpServletResponse response) {
Map<String, Object> attributes = getErrorAttributes(request, ErrorAttributeOptions.defaults());
Object status = attributes.get("status");
String error = attributes.get("error").toString();
if (status.equals(HttpStatus.NOT_FOUND.value())) {
error = "Not Found Api Path";
}
return R.fail(error);
}
}

View File

@ -0,0 +1,104 @@
package cn.zgfxrc.boot.common.config.exception;
import cn.zgfxrc.boot.common.core.util.TenantUtil;
import cn.zgfxrc.boot.common.feign.FeignWrapException;
import com.seepine.secret.exception.ExpiresSecretException;
import com.seepine.secret.exception.ForbiddenSecretException;
import com.seepine.secret.exception.SecretException;
import com.seepine.secret.exception.UnauthorizedSecretException;
import com.seepine.tool.R;
import com.seepine.tool.exception.RunException;
import java.net.UnknownHostException;
import java.sql.SQLSyntaxErrorException;
import javax.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.jdbc.BadSqlGrammarException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* @author Seepine
*/
@Slf4j
@RestControllerAdvice
@AllArgsConstructor
public class GlobalExceptionHandler {
@ExceptionHandler(UnknownHostException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Object unknownHostException(UnknownHostException e) {
return R.fail("服务器卡住啦,请五秒后再试试");
}
@ExceptionHandler(SecretException.class)
public R<?> authException(SecretException e, HttpServletResponse response) {
if (e instanceof ForbiddenSecretException) {
response.setStatus(HttpStatus.FORBIDDEN.value());
return R.build(HttpStatus.FORBIDDEN.value(), e.getMessage(), "权限不足");
} else if (e instanceof UnauthorizedSecretException) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return R.build(HttpStatus.UNAUTHORIZED.value(), e.getMessage(), "请先登录");
} else if (e instanceof ExpiresSecretException) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return R.build(HttpStatus.UNAUTHORIZED.value(), e.getMessage(), "登录过期,请重新登录");
}
return R.fail(e.getMessage());
}
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Object illegalArgumentException(IllegalArgumentException e) {
return R.fail(e.getMessage());
}
@ExceptionHandler(TenantUtil.TenantBrokerException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Object tenantBrokerException(TenantUtil.TenantBrokerException e) {
return R.fail(e.getMessage());
}
@ExceptionHandler(RunException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Object runException(RunException e) {
return R.fail(e.getMessage());
}
@ExceptionHandler(FeignWrapException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Object feignWrapException(FeignWrapException e) {
return R.fail(e.getMessage());
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Object exception(Exception e) {
e.printStackTrace();
return R.fail("系统内部错误,请联系管理员");
}
@ExceptionHandler(DataIntegrityViolationException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Object dataIntegrityViolationException(DataIntegrityViolationException e) {
log.error("数据库错误!!!!");
e.printStackTrace();
return R.fail("系统发生错误!请联系管理员!");
}
@ExceptionHandler(BadSqlGrammarException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Object badSqlGrammarException(BadSqlGrammarException e) {
log.error("数据库缺失字段!!!!{}", e.getMessage());
return R.fail("该模块升级中请10分钟后再访问");
}
@ExceptionHandler(SQLSyntaxErrorException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Object sqlSyntaxErrorException(SQLSyntaxErrorException e) {
log.error("数据库缺失表!!!!{}", e.getMessage());
return R.fail("该模块升级中请10分钟后再访问");
}
}

View File

@ -0,0 +1,48 @@
package cn.zgfxrc.boot.common.config.exception;
import com.seepine.tool.R;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
/**
* 处理错误
*
* @author seepine
*/
@Slf4j
@AllArgsConstructor
@RestControllerAdvice
public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
/**
* 用来处理如上错误 @RequestMapping public void test(@RequestParam String a)
*
* @param e 异常
* @param body body
* @param headers headers
* @param status status
* @param request request
* @return ResponseEntity
*/
@NotNull
@Override
protected ResponseEntity<Object> handleExceptionInternal(
@NotNull Exception e,
@Nullable Object body,
@NotNull HttpHeaders headers,
@NotNull HttpStatus status,
@NotNull WebRequest request) {
if (HttpStatus.INTERNAL_SERVER_ERROR.equals(status)) {
request.setAttribute("javax.servlet.error.exception", e, 0);
}
return new ResponseEntity<>(R.fail(e.getMessage()), headers, HttpStatus.BAD_REQUEST);
}
}

View File

@ -0,0 +1,22 @@
//package cn.zgfxrc.boot.common.config.log;
//
//import com.seepine.secret.entity.LogEvent;
//import com.seepine.secret.interfaces.AuthLogService;
//import lombok.extern.slf4j.Slf4j;
//import org.springframework.stereotype.Component;
//
///**
// * @author seepine
// */
//@Slf4j
//@Component
//public class AuthLogServiceImpl implements AuthLogService {
// @Override
// public void save(LogEvent logEvent) {
// log.info(
// "log : {}, {}, {}", logEvent.getRequestUri(), logEvent.getModule(), logEvent.getTitle());
// // if (StringUtils.hasText(logEvent.getExceptionStackTrace())) {
// // log.error(logEvent.getExceptionStackTrace());
// // }
// }
//}

View File

@ -0,0 +1,22 @@
package cn.zgfxrc.boot.common.config.log;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* @author seepine
*/
public class HttpServletUtil {
/**
* 获取request
*
* @return HttpServletRequest | null
*/
public static HttpServletRequest getHttpRequest() {
if (RequestContextHolder.getRequestAttributes() == null) {
return null;
}
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
}
}

View File

@ -0,0 +1,64 @@
//package cn.zgfxrc.boot.common.config.log;
//
//
//import com.seepine.secret.annotation.Log;
//import com.seepine.secret.entity.LogEvent;
//import com.seepine.secret.interfaces.AuthLogService;
//import com.seepine.secret.spring.util.LogUtil;
//import com.seepine.tool.time.CurrentTimeMillis;
//import org.aspectj.lang.ProceedingJoinPoint;
//import org.aspectj.lang.Signature;
//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 org.springframework.beans.factory.annotation.Autowired;
//import org.springframework.stereotype.Component;
//
///**
// * 日志aop
// *
// * @author seepine
// */
//@Aspect
//@Component
//public class LogAspect {
//
// @Autowired(required = false)
// private AuthLogService authLogService;
//
// /** 频率限制切入点(注解类的路径) */
// @Pointcut(value = "@annotation(com.seepine.secret.annotation.Log)")
// public void logPointCut() {}
//
// /**
// * 切面请求频率限制
// *
// * @param joinPoint joinPoint
// * @return obj
// * @throws Throwable e
// */
// @Around("logPointCut()")
// public Object doAfter(ProceedingJoinPoint joinPoint) throws Throwable {
// if (authLogService == null) {
// return joinPoint.proceed();
// }
// Signature signature = joinPoint.getSignature();
// MethodSignature methodSignature = (MethodSignature) signature;
// Object result = null;
// Throwable exception = null;
// long startTime = CurrentTimeMillis.now();
// try {
// result = joinPoint.proceed();
// } catch (Throwable e) {
// exception = e;
// }
// Log log = methodSignature.getMethod().getAnnotation(Log.class);
// LogEvent logEvent = LogUtil.gen(log, CurrentTimeMillis.now() - startTime, exception);
// authLogService.save(logEvent);
// if (exception != null) {
// throw exception;
// }
// return result;
// }
//}

View File

@ -0,0 +1,152 @@
//package cn.zgfxrc.boot.common.config.log;
//
//
//import com.seepine.secret.AuthUtil;
//import com.seepine.secret.annotation.Log;
//import com.seepine.secret.entity.LogEvent;
//import com.seepine.secret.spring.util.IpUtil;
//import com.seepine.tool.Run;
//import com.seepine.tool.util.Objects;
//import com.seepine.tool.util.Strings;
//
//import java.util.Arrays;
//import java.util.Enumeration;
//import java.util.Map;
//import javax.servlet.http.HttpServletRequest;
//
///**
// * @author seepine
// */
//public class LogUtil {
// public static final String LF = "\n";
// public static final String EQUAL = "=";
// public static final String AND = "&";
// public static final String SPACE = " ";
// /**
// * 生成日志
// *
// * @param log 日志
// * @param executionTime 执行时间
// * @param exception 异常
// * @return 日志
// */
// public static LogEvent gen(Log log, Long executionTime, Throwable exception) {
// return gen(
// log.module(),
// Objects.isBlank(log.title()) ? log.value() : log.title(),
// log.content(),
// executionTime,
// exception);
// }
//
// /**
// * 生成日志
// *
// * @param module 模块
// * @param title 标题
// * @param content 内容
// * @return 日志
// */
// public static LogEvent gen(String module, String title, String content) {
// return gen(module, title, content, null, null);
// }
//
// /**
// * 生成日志
// *
// * @param module 模块
// * @param title 标题
// * @param content 内容
// * @param exception 异常
// * @return 日志
// */
// public static LogEvent gen(String module, String title, String content, Throwable exception) {
// return gen(module, title, content, null, exception);
// }
//
// /**
// * 生成日志
// *
// * @param module 模块
// * @param title 标题
// * @param content 内容
// * @param executionTime 执行时间
// * @param exception 异常
// * @return 日志
// */
// public static LogEvent gen(
// String module, String title, String content, Long executionTime, Throwable exception) {
// HttpServletRequest request = HttpServletUtil.getHttpRequest();
// LogEvent logEvent = new LogEvent();
// logEvent.setExecutionTime(executionTime);
// logEvent.setModule(module);
// logEvent.setTitle(title);
// logEvent.setContent(content);
// if (request != null) {
// logEvent.setClientIp(IpUtil.getIp(request.getHeader("x-forwarded-for"), request.getHeader("Proxy-Client-IP"), request.getHeader("WL-Proxy-Client-IP"), request.getRemoteAddr()));
// logEvent.setRequestUri(request.getRequestURI());
// logEvent.setMethod(request.getMethod());
// logEvent.setUserAgent(request.getHeader("User-Agent"));
// logEvent.setParams(printMap(request.getParameterMap()));
// logEvent.setContentType(request.getContentType());
// logEvent.setHeaders(printHeader(request, request.getHeaderNames()));
// }
// if (exception != null) {
// logEvent.setException(exception.toString());
// logEvent.setExceptionStackTrace(printStackTrace(exception));
// }
// try {
// // 避免未登录日志报错
// logEvent.setUser(AuthUtil.getUser());
// } catch (Exception ignored) {
// }
// return logEvent;
// }
//
// private static String printMap(Map<String, String[]> params) {
// StringBuilder str = new StringBuilder();
// for (Map.Entry<String, String[]> stringEntry : params.entrySet()) {
// String key = stringEntry.getKey();
// String[] value = stringEntry.getValue();
// str.append(key).append(EQUAL);
// if (value != null) {
// if (value.length == 1) {
// str.append(value[0]);
// } else {
// str.append(Arrays.toString(value));
// }
// }
// str.append(key).append(AND);
// }
// return str.length() > 0 ? str.substring(0, str.length() - 1) : str.toString();
// }
//
// private static String printStackTrace(Throwable e) {
// StringBuilder str = new StringBuilder();
// if (e.getStackTrace() != null) {
// for (StackTraceElement stackTraceElement : e.getStackTrace()) {
// str.append(stackTraceElement.toString()).append(LF);
// }
// }
// return str.length() > 0 ? str.substring(0, str.length() - 1) : str.toString();
// }
//
// private static String printHeader(HttpServletRequest request, Enumeration<String> headers) {
// StringBuilder str = new StringBuilder();
// while (headers.hasMoreElements()) {
// String header = headers.nextElement();
// if (Objects.isBlank(header)) {
// continue;
// }
// Run.nonBlank(
// request.getHeader(header),
// headerValue ->
// str.append(header)
// .append(Strings.COLON)
// .append(SPACE)
// .append(headerValue)
// .append(LF));
// }
// return str.length() > 0 ? str.substring(0, str.length() - 1) : str.toString();
// }
//}

View File

@ -0,0 +1,65 @@
package cn.zgfxrc.boot.common.config.mybatis;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.seepine.secret.AuthUtil;
import com.seepine.secret.entity.AuthUser;
import com.seepine.secret.exception.SecretException;
import java.time.LocalDateTime;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
/**
* @author huanghs@zgfxrc.cn
* @date 2021.11.1
*/
@Component
public class BaseModalMetaObjectHandler implements MetaObjectHandler {
private static final String CREATE_ID = "createId";
private static final String UPDATE_ID = "updateId";
/** 用来保存姓名 */
private static final String CREATE_NAME = "createName";
private static final String UPDATE_NAME = "updateName";
private static final String CREATE_TIME = "createTime";
private static final String UPDATE_TIME = "updateTime";
private static final String IS_DELETE = "isDelete";
private static final String IS_SYSTEM = "isSystem";
private static final String IS_LOCK = "isLock";
@Override
public void insertFill(MetaObject metaObject) {
// 初始化是否删除
setField(metaObject, IS_DELETE, false);
// 初始化是否系统内置
setField(metaObject, IS_SYSTEM, false);
// 初始化是否锁定
setField(metaObject, IS_LOCK, false);
// 设置创建时间
setField(metaObject, CREATE_TIME, LocalDateTime.now());
try {
AuthUser user = AuthUtil.getUser();
setField(metaObject, CREATE_ID, user.getIdAsLong());
setField(metaObject, CREATE_NAME, user.getFullName());
} catch (SecretException ignored) {
}
}
@Override
public void updateFill(MetaObject metaObject) {
// 设置更新时间
setField(metaObject, UPDATE_TIME, LocalDateTime.now());
try {
AuthUser user = AuthUtil.getUser();
setField(metaObject, UPDATE_ID, user.getIdAsLong());
setField(metaObject, UPDATE_NAME, user.getFullName());
} catch (SecretException ignored) {
}
}
void setField(MetaObject metaObject, String fieldName, Object fieldVal) {
if (this.getFieldValByName(fieldName, metaObject) == null && fieldVal != null) {
this.setFieldValByName(fieldName, fieldVal, metaObject);
}
}
}

View File

@ -0,0 +1,13 @@
package cn.zgfxrc.boot.common.config.mybatis;
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import com.github.yitter.idgen.YitIdHelper;
import org.springframework.stereotype.Component;
@Component
public class SnowflakeIdGenerator implements IdentifierGenerator {
@Override
public Number nextId(Object entity) {
return YitIdHelper.nextId();
}
}

View File

@ -0,0 +1,46 @@
package cn.zgfxrc.boot.common.config.mybatis;
import cn.zgfxrc.boot.common.mybatis.tenant.interceptor.TenantFilterService;
import com.seepine.secret.AuthUtil;
import com.seepine.secret.entity.AuthUser;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
/**
* 默认租户id获取
*
* @author huanghs
*/
@Component
public class TenantFilterServiceImpl implements TenantFilterService {
private static final long DEFAULT_TENANT_ID = -1L;
private static final String INNER_KEY = "inner";
private static final String INNER_VALUE = "true";
private static final String TENANT_KEY = "tenantId";
@Override
@NonNull
public Object fill(
@NonNull ServletRequest servletRequest, @NonNull ServletResponse servletResponse) {
try {
try {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String inner = request.getHeader(INNER_KEY);
if (INNER_VALUE.equals(inner)) {
String tenantId = request.getHeader(TENANT_KEY);
if (tenantId != null) {
return tenantId;
}
}
} catch (Exception ignored) {
}
AuthUser user = AuthUtil.getUser();
return user.getTenantId() == null ? DEFAULT_TENANT_ID : user.getTenantId();
} catch (Exception e) {
return DEFAULT_TENANT_ID;
}
}
}

View File

@ -0,0 +1,72 @@
package cn.zgfxrc.boot.common.config.runner;
import com.seepine.tool.cache.Cache;
import com.seepine.tool.cache.CacheService;
import com.seepine.tool.lock.Lock;
import com.seepine.tool.lock.LockService;
import java.time.Duration;
import java.util.function.Supplier;
import javax.annotation.Nonnull;
import lombok.AllArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Order(Integer.MIN_VALUE)
@Component
@AllArgsConstructor
public class RedissonInitRunner implements ApplicationRunner {
RedissonClient redissonClient;
@Override
public void run(ApplicationArguments args) throws Exception {
Cache.enhance(
new CacheService() {
@Override
public void set(@Nonnull String key, @Nonnull Object value, long delayMillisecond) {
if (delayMillisecond > 0) {
redissonClient.getBucket(key).set(value, Duration.ofMillis(delayMillisecond));
} else {
redissonClient.getBucket(key).set(value);
}
}
@Nullable
@Override
public Object get(@Nonnull String key) {
return redissonClient.getBucket(key).get();
}
@Nullable
@Override
public Object remove(@Nonnull String key) {
return redissonClient.getBucket(key).getAndDelete();
}
@Override
public long removeByPattern(@NotNull String pattern) {
return redissonClient.getKeys().deleteByPattern(pattern);
}
});
Lock.enhance(
new LockService() {
@Nullable
@Override
public <T> T lock(@Nonnull String key, @Nonnull Supplier<T> supplier) {
RLock lock = redissonClient.getLock("redisson_lock:" + key);
try {
lock.lock();
return supplier.get();
} finally {
lock.unlock();
}
}
});
}
}

View File

@ -0,0 +1,27 @@
package cn.zgfxrc.boot.common.config.runner;
import com.github.yitter.contract.IdGeneratorOptions;
import com.github.yitter.idgen.YitIdHelper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Order(Integer.MIN_VALUE)
public class SnowflakeIdGeneratorInitRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
// 创建 IdGeneratorOptions 对象当分布式时一定手动指定workerId避免重复例如使用redis方案
IdGeneratorOptions options = new IdGeneratorOptions((short) 0);
options.WorkerIdBitLength = 10; // 默认值6限定 WorkerId 最大值为2^6-1即默认最多支持64个节点
options.SeqBitLength = 10; // 默认值6限制每毫秒生成的ID个数若生成速度超过5万个/建议加大 SeqBitLength 10
// 保存参数务必调用否则参数设置不生效
YitIdHelper.setIdGenerator(options);
// 以上过程只需全局一次且应在生成ID之前完成
// 而后在需要的地方调用 YitIdHelper.nextId() 即可
log.info("Id generator init, try gen: {}", YitIdHelper.nextId());
}
}

View File

@ -0,0 +1,25 @@
package cn.zgfxrc.boot.common.config.thread;
import com.alibaba.ttl.threadpool.TtlExecutors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ForkJoinPool;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author huanghs
* @date 2022.4.12
*/
@Configuration
public class ExecutorConfigurer {
@Bean
public ExecutorService executorService() {
return TtlExecutors.getTtlExecutorService(
new ForkJoinPool(
Math.max(Runtime.getRuntime().availableProcessors() - 4, 4),
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null,
true));
}
}

View File

@ -0,0 +1,25 @@
package cn.zgfxrc.boot.common.config.thread;
import cn.hutool.core.thread.ThreadUtil;
import com.alibaba.ttl.threadpool.TtlExecutors;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
/**
* @author huanghs
* @date 2022.4.12
*/
@Configuration
public class MySchedulingConfigurer implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
ScheduledThreadPoolExecutor executorService =
new ScheduledThreadPoolExecutor(
Math.max(Runtime.getRuntime().availableProcessors() - 4, 4),
ThreadUtil.newNamedThreadFactory("schedule-", true));
taskRegistrar.setScheduler(TtlExecutors.getTtlScheduledExecutorService(executorService));
}
}

18
common-core/build.gradle Normal file
View File

@ -0,0 +1,18 @@
plugins {
id 'java-library'
}
dependencies {
api("io.swagger:swagger-annotations:1.5.20")
api("com.baomidou:mybatis-plus-extension")
api("jakarta.validation:jakarta.validation-api:2.0.2")
api("cn.hutool:hutool-core")
api("com.seepine:json:${jsonVersion}")
api("com.seepine:tool:${toolVersion}")
}
bootJar {
enabled = false
}
jar {
enabled = true
}

View File

@ -0,0 +1,20 @@
package cn.zgfxrc.boot.common.core.constant;
/**
* 常量
*
* @author huanghs@zgfxrc.cn
*/
public interface Constant {
String isSystem = "isSystem";
String status = "status";
String platformType = "platformType";
String tenantLogo = "tenantLogo";
String tenantShortName = "tenantShortName";
String tenantParentId = "tenantParentId";
/** 平台id */
String sysId = "sysId";
/** 企业端id */
String enterpriseId = "enterpriseId";
}

View File

@ -0,0 +1,60 @@
package cn.zgfxrc.boot.common.core.entity.base;
import cn.zgfxrc.boot.common.core.entity.interfaces.DelGroup;
import cn.zgfxrc.boot.common.core.entity.interfaces.EditGroup;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
/**
* BaseModel
*
* @author huanghs@zgfxrc.cn
* @date 2021.11.1
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class BaseModel<T extends Model<?>> extends Model<T> {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.ASSIGN_ID)
@ApiModelProperty("主键")
@NotNull(
message = "主键id不能为空",
groups = {EditGroup.class, DelGroup.class})
private Long id;
@TableLogic
@TableField(fill = FieldFill.INSERT)
@ApiModelProperty("是否删除")
private Boolean isDelete;
@TableField(fill = FieldFill.INSERT)
@ApiModelProperty("创建时间")
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT)
@ApiModelProperty("创建者id")
private Long createId;
@TableField(fill = FieldFill.INSERT)
@ApiModelProperty("创建者名称")
private String createName;
@TableField(fill = FieldFill.UPDATE)
@ApiModelProperty("更新时间")
private LocalDateTime updateTime;
@TableField(fill = FieldFill.UPDATE)
@ApiModelProperty("更新者id")
private Long updateId;
@TableField(fill = FieldFill.UPDATE)
@ApiModelProperty("更新者名称")
private String updateName;
}

View File

@ -0,0 +1,19 @@
package cn.zgfxrc.boot.common.core.entity.base;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author huanghs@zgfxrc.cn
* @date 2021.11.1
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class BaseModelWithTenant<T extends Model<?>> extends BaseModel<T> {
private static final long serialVersionUID = 1L;
@ApiModelProperty("租户id")
private Long tenantId;
}

View File

@ -0,0 +1,32 @@
package cn.zgfxrc.boot.common.core.entity.base;
import cn.zgfxrc.boot.common.core.entity.interfaces.DragGroup;
import cn.zgfxrc.boot.common.core.enums.DropPosition;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
/**
* 拖拽用
*
* @author huanghs
* @date 2022.12.2
*/
@Data
public class DragModel implements Serializable {
private static final long serialVersionUID = 1L;
@NotNull(message = "拖拽对象id不能为空", groups = DragGroup.class)
@ApiModelProperty("拖拽对象id")
Long dragId;
@NotNull(message = "释放对象id不能为空", groups = DragGroup.class)
@ApiModelProperty("释放对象id")
Long dropId;
@NotNull(message = "释放位置不能为空", groups = DragGroup.class)
@ApiModelProperty("释放位置")
DropPosition dropPosition;
}

View File

@ -0,0 +1,19 @@
package cn.zgfxrc.boot.common.core.entity.base;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author huanghs
* @date 2022.2.21
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class ModelWithTenant<T extends Model<?>> extends Model<T> {
private static final long serialVersionUID = 1L;
@ApiModelProperty("租户id")
private Long tenantId;
}

View File

@ -0,0 +1,45 @@
package cn.zgfxrc.boot.common.core.entity.base;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serial;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
/**
* @author huanghs
* @date 2021.4.8
*/
@Data
@ApiModel(value = "树形节点")
public class TreeNode<T> implements Serializable {
@Serial private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "当前节点id")
protected Long id;
@ApiModelProperty(value = "名称")
protected String name;
@ApiModelProperty(value = "父节点id")
protected Long parentId;
@ApiModelProperty(value = "是否可拖拽")
protected Boolean draggable;
@ApiModelProperty("是否显示复选框")
protected Boolean checkable;
@ApiModelProperty("排序值")
protected Integer sort;
@ApiModelProperty(value = "子节点列表")
protected List<T> children = new ArrayList<>();
public void add(T node) {
children.add(node);
}
}

View File

@ -0,0 +1,8 @@
package cn.zgfxrc.boot.common.core.entity.interfaces;
/**
* 作用于新增校验例如 @NotBlank(message = "名称不能为空",groups=AddGroup.class)
*
* @author seepine
*/
public interface AddGroup {}

View File

@ -0,0 +1,8 @@
package cn.zgfxrc.boot.common.core.entity.interfaces;
/**
* 作用于删除校验
*
* @author seepine
*/
public interface DelGroup {}

View File

@ -0,0 +1,8 @@
package cn.zgfxrc.boot.common.core.entity.interfaces;
/**
* 作用于拖拽校验例如 @NotNull(message = "主键不能为空",groups=DragGroup.class)
*
* @author seepine
*/
public interface DragGroup {}

View File

@ -0,0 +1,8 @@
package cn.zgfxrc.boot.common.core.entity.interfaces;
/**
* 作用于编辑校验例如 @NotNull(message = "主键id不能为空",groups=EditGroup.class)
*
* @author seepine
*/
public interface EditGroup {}

View File

@ -0,0 +1,39 @@
## 说明
此包定义了常见接口需要校验的情况例如新增时需要校验名称不能为空编辑时名称可为空但主键id不能为空 此时就需要通过分组校验实现
### 1. 实体类添加校验规则
```java
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
class User {
@NotNull(message = "主键id不能为空", groups = EditGroup.class)
Long id;
@NotBlank(message = "名称不能为空", groups = AddGroup.class)
String name;
}
```
### 2. 接口添加校验注解
```java
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import org.springframework.validation.annotation.Validated;
class Controller {
@RequestMapping("add")
public void add(@RequestBody @Validated(AddGroup.class) User entity) {
// service.add(entity);
}
@RequestMapping("edit")
public void edit(@RequestBody @Validated(EditGroup.class) User entity) {
// service.edit(entity);
}
}
```

View File

@ -0,0 +1,43 @@
package cn.zgfxrc.boot.common.core.enums;
import com.baomidou.mybatisplus.annotation.IEnum;
import com.fasterxml.jackson.annotation.JsonCreator;
import lombok.AllArgsConstructor;
/**
* 活动状态(账号或租户)
*
* @author huanghs@zgfxrc.cn
* @date 2022.12.13
*/
@AllArgsConstructor
public enum ActiveStatus implements IEnum<String> {
/** 拖拽类型 */
NOT_ACTIVE("not_active", "未激活"),
NORMAL("normal", "正常"),
SUSPEND("suspend", "已停用");
private final String value;
private final String label;
@Override
public String getValue() {
return this.value;
}
@Override
public String toString() {
return value;
}
@JsonCreator
static ActiveStatus jsonCreator(Object data) {
String str = data.toString();
for (ActiveStatus item : ActiveStatus.values()) {
if (item.getValue().equals(str) || item.name().equals(str)) {
return item;
}
}
return null;
}
}

View File

@ -0,0 +1,38 @@
package cn.zgfxrc.boot.common.core.enums;
import com.baomidou.mybatisplus.annotation.IEnum;
import com.fasterxml.jackson.annotation.JsonCreator;
import lombok.AllArgsConstructor;
@AllArgsConstructor
public enum DistrictType implements IEnum<String> {
/** 省份 */
PROVINCE("province"),
/** 市区 */
CITY("city"),
/** 区县 */
DISTRICT("district");
private final String value;
@Override
public String getValue() {
return this.value;
}
@Override
public String toString() {
return value;
}
@JsonCreator
static DistrictType jsonCreator(Object data) {
String str = data.toString();
for (DistrictType item : DistrictType.values()) {
if (item.getValue().equals(str) || item.name().equals(str)) {
return item;
}
}
return null;
}
}

View File

@ -0,0 +1,43 @@
package cn.zgfxrc.boot.common.core.enums;
import com.baomidou.mybatisplus.annotation.IEnum;
import com.fasterxml.jackson.annotation.JsonCreator;
import lombok.AllArgsConstructor;
/**
* 拖拽类型
*
* @author huanghs@zgfxrc.cn
* @date 2022.12.2
*/
@AllArgsConstructor
public enum DropPosition implements IEnum<Integer> {
/** 拖拽类型 */
UPPER(-1, "dropNode的上方"),
INNER(0, "drop内"),
BELOW(1, "dropNode的下方");
private final Integer value;
private final String label;
@Override
public Integer getValue() {
return this.value;
}
@Override
public String toString() {
return value.toString();
}
@JsonCreator
static DropPosition jsonCreator(Object data) {
String str = data.toString();
for (DropPosition item : DropPosition.values()) {
if (item.getValue().toString().equals(str) || item.name().equals(str)) {
return item;
}
}
return null;
}
}

View File

@ -0,0 +1,41 @@
package cn.zgfxrc.boot.common.core.enums;
import com.baomidou.mybatisplus.annotation.IEnum;
import com.fasterxml.jackson.annotation.JsonCreator;
import lombok.AllArgsConstructor;
/**
* @author huanghs@zgfxrc.cn
* @date 2021.11.1
*/
@AllArgsConstructor
public enum Gender implements IEnum<String> {
/** 性别 */
UNKNOWN("unknown", "未知"),
MALE("male", ""),
FEMALE("female", "");
private final String value;
private final String label;
@Override
public String getValue() {
return this.value;
}
@Override
public String toString() {
return value;
}
@JsonCreator
static Gender jsonCreator(Object data) {
String str = data.toString();
for (Gender item : Gender.values()) {
if (item.getValue().equals(str) || item.name().equals(str)) {
return item;
}
}
return null;
}
}

View File

@ -0,0 +1,34 @@
package cn.zgfxrc.boot.common.core.enums;
import com.baomidou.mybatisplus.annotation.IEnum;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
/**
* @author huanghs@zgfxrc.cn
* @date 2021.11.2
*/
@AllArgsConstructor
public enum MenuType implements IEnum<String> {
/** 菜单类型 */
MENU("menu"),
BUTTON("button");
@JsonValue public final String value;
@Override
public String getValue() {
return this.value;
}
@JsonCreator
static MenuType jsonCreator(Object data) {
for (MenuType item : MenuType.values()) {
if (item.getValue().equals(data.toString())) {
return item;
}
}
return null;
}
}

View File

@ -0,0 +1,65 @@
package cn.zgfxrc.boot.common.core.enums;
import com.baomidou.mybatisplus.annotation.IEnum;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
/**
* @author huanghs@zgfxrc.cn
* @date 2022.3.24
*/
@AllArgsConstructor
public enum PlatformType implements IEnum<String> {
/** 平台类型:平台端/园区端/企业端 */
SYS("sys", "平台端", 0L),
ENTERPRISE("enterprise", "企业端", 1L),
MEMBER("member", "会员端", 2L);
@JsonValue public final String value;
public final String label;
public final Long id;
@Override
public String getValue() {
return this.value;
}
@Override
public String toString() {
return this.name();
}
@JsonCreator
static PlatformType jsonCreator(Object data) {
return from(data);
}
public static PlatformType from(Object data) {
for (PlatformType item : PlatformType.values()) {
if (data instanceof PlatformType) {
if (item.equals(data)) {
return item;
}
} else if (data instanceof Long) {
if (item.id.equals(data)) {
return item;
}
} else {
if (item.name().equals(data) || item.getValue().equals(data)) {
return item;
}
}
}
return null;
}
/**
* 是否相等
*
* @param data 可以是value字符串也可以是label也可以是name
* @return 是否相等
*/
public boolean eq(Object data) {
return this.equals(from(data));
}
}

View File

@ -0,0 +1,57 @@
package cn.zgfxrc.boot.common.core.util;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
/**
* @author huanghs
* @date 2022.7.11
*/
public class LocalDateUtil {
private static final DateTimeFormatter DF = DateTimeFormatter.ofPattern("yyyy-MM-dd");
/**
* 判断a是否小于b
*
* @param a 2022-7-11
* @param b 2022-7-12
* @return bool
*/
public static boolean isLt(LocalDate a, LocalDate b) {
return a.isBefore(b);
}
/**
* 判断a是否小于b
*
* @param a 2022-7-11
* @param b 2022-7-12
* @return bool
*/
public static boolean isLe(LocalDate a, LocalDate b) {
return isLt(a, b) || a.equals(b);
}
/**
* 判断a是否大于b
*
* @param a 2022-7-11
* @param b 2022-7-12
* @return bool
*/
public static boolean isGt(LocalDate a, LocalDate b) {
return a.isAfter(b);
}
/**
* 判断a是否大于b
*
* @param a 2022-7-11
* @param b 2022-7-12
* @return bool
*/
public static boolean isGe(LocalDate a, LocalDate b) {
return isGt(a, b) || a.equals(b);
}
public static String format(LocalDate date) {
return date.format(DF);
}
}

View File

@ -0,0 +1,52 @@
package cn.zgfxrc.boot.common.core.util;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.seepine.tool.util.ListUtil;
import com.seepine.tool.util.Objects;
import java.util.function.Function;
/**
* @author huanghs
* @date 2022.6.20
*/
public class PageUtil {
/**
* 分页转换
*
* @param originPage 原分页数据
* @param mapper 转换逻辑
* @param <T> 原类
* @param <R> 新类
* @return 新分页数据
*/
public static <T, R> Page<R> convertPage(
IPage<T> originPage, Function<? super T, ? extends R> mapper) {
if (Objects.isEmpty(originPage)) {
return new Page<>();
}
Page<R> pageRes = new Page<>();
BeanUtil.copyProperties(originPage, pageRes, "records");
pageRes.setRecords(ListUtil.map(originPage.getRecords(), mapper));
return pageRes;
}
/**
* 将IPage转为Page
*
* @param originPage iPage
* @param <T> T
* @return page
*/
public static <T> Page<T> convertPage(IPage<T> originPage) {
if (Objects.isEmpty(originPage)) {
return new Page<>();
}
Page<T> pageRes = new Page<>();
BeanUtil.copyProperties(originPage, pageRes, "records");
pageRes.setRecords(originPage.getRecords());
return pageRes;
}
}

View File

@ -0,0 +1,206 @@
package cn.zgfxrc.boot.common.core.util;
import java.util.ArrayList;
import java.util.List;
import lombok.experimental.UtilityClass;
/**
* 租户工具类
*
* @author seepine
*/
@UtilityClass
public class TenantUtil {
private static final List<String> TENANT_TABLE = new ArrayList<>();
private final ThreadLocal<Object> THREAD_LOCAL_TENANT = new ThreadLocal<>();
private static final String EMPTY = "";
/**
* 添加多租户表
*
* @param tableName 表名对应数据库例如sys_user
*/
public void addIgnoreTable(String tableName) {
if (tableName != null && !EMPTY.equals(tableName)) {
TENANT_TABLE.add(tableName);
}
}
/**
* 获取多租户表名
*
* @return List<String>
*/
public List<String> getTables() {
return TENANT_TABLE;
}
/**
* 获取TTL中的租户ID
*
* @return 租户id
*/
public String getTenantId() {
Object tenantId = THREAD_LOCAL_TENANT.get();
return tenantId == null ? null : tenantId.toString();
}
/**
* TTL 设置租户ID<br>
* <b>谨慎使用此方法,避免嵌套调用尽量使用 {@code TenantBroker} </b>
*
* @param tenantId 租户id
*/
public void setTenantId(Object tenantId) {
if (tenantId != null) {
THREAD_LOCAL_TENANT.set(tenantId);
}
}
/** 清理 */
public void clear() {
THREAD_LOCAL_TENANT.remove();
}
/**
* 以某个租户的身份运行
*
* @param tenant 租户id
* @param func func
*/
public void run(String tenant, Run func) {
final String pre = getTenantId();
try {
setTenantId(tenant);
func.run();
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new TenantBrokerException(e.getMessage(), e);
} finally {
setTenantId(pre);
}
}
/**
* 以某个租户的身份运行
*
* @param tenant 租户id
* @param func func
*/
public void run(Long tenant, Run func) {
run(tenant.toString(), func);
}
/**
* 以某个租户的身份运行
*
* @param tenant 租户id
* @param func func
*/
public void run(Integer tenant, Run func) {
run(tenant.toString(), func);
}
/**
* 以某个租户的身份运行
*
* @param tenant 租户id
* @param func func
* @param <T> T
*/
public <T> void runAs(T tenant, RunAs<T> func) {
final String pre = getTenantId();
try {
setTenantId(tenant);
func.run(tenant);
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
e.printStackTrace();
throw new TenantBrokerException(e.getMessage(), e);
} finally {
setTenantId(pre);
}
}
/**
* 以某个租户的身份运行
*
* @param tenant 租户ID
* @param func func
* @param <R> R
* @return R
*/
public <T, R> R apply(T tenant, Apply<R> func) {
return applyAs(tenant, (val) -> func.apply());
}
/**
* 以某个租户的身份运行
*
* @param tenant 租户ID
* @param func func
* @param <T> T
* @return T
*/
public <T, R> R applyAs(T tenant, ApplyAs<T, R> func) {
final String pre = getTenantId();
try {
setTenantId(tenant);
return func.apply(tenant);
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
e.printStackTrace();
throw new TenantBrokerException(e.getMessage(), e);
} finally {
setTenantId(pre);
}
}
@FunctionalInterface
public interface Run {
/**
* 执行业务逻辑
*
* @throws Exception 异常
*/
void run() throws Exception;
}
@FunctionalInterface
public interface RunAs<T> {
/**
* 执行业务逻辑
*
* @param tenantId 租户id
* @throws Exception 异常
*/
void run(T tenantId) throws Exception;
}
@FunctionalInterface
public interface Apply<R> {
/**
* 执行业务逻辑,返回一个值
*
* @return R
* @throws Exception 异常
*/
R apply() throws Exception;
}
@FunctionalInterface
public interface ApplyAs<T, R> {
/**
* 执行业务逻辑,返回一个值
*
* @param tenantId 租户id
* @return R
* @throws Exception 异常
*/
R apply(T tenantId) throws Exception;
}
public static class TenantBrokerException extends RuntimeException {
public TenantBrokerException(String message, Throwable cause) {
super(message, cause);
}
}
}

View File

@ -0,0 +1,26 @@
package cn.zgfxrc.boot.common.core.util;
import cn.zgfxrc.boot.common.core.entity.base.TreeNode;
import com.seepine.tool.util.Trees;
import java.util.List;
import lombok.experimental.UtilityClass;
/**
* @author huanghs
* @date 2021.4.8
*/
@UtilityClass
public class TreeUtil {
public <T extends TreeNode<T>> List<T> build(List<T> treeNodes) {
return build(treeNodes, 0L);
}
/**
* 两层循环实现建树
*
* @param treeNodes 传入的树节点列表
* @return List
*/
public <T extends TreeNode<T>> List<T> build(List<T> treeNodes, Object rootId) {
return Trees.build(treeNodes, T::getId, T::getParentId, rootId, T::getSort, T::setChildren);
}
}

19
common-feign/build.gradle Normal file
View File

@ -0,0 +1,19 @@
plugins {
id 'java-library'
}
dependencies {
implementation project(":common-core")
api("org.springframework.cloud:spring-cloud-starter-openfeign:3.1.3")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-undertow")
implementation("com.seepine:secret-core:${secretVersion}")
api("com.seepine:http:${httpVersion}")
}
bootJar {
enabled = false
}
jar {
enabled = true
}

View File

@ -0,0 +1,70 @@
package cn.zgfxrc.boot.common.feign;
import cn.zgfxrc.boot.common.core.util.TenantUtil;
import cn.zgfxrc.boot.common.feign.decoder.ResultStatusDecoder;
import com.seepine.secret.AuthUtil;
import com.seepine.secret.entity.AuthUser;
import com.seepine.tool.util.Strings;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import feign.codec.Decoder;
import feign.optionals.OptionalDecoder;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.support.ResponseEntityDecoder;
import org.springframework.cloud.openfeign.support.SpringDecoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* 传递请求头和加密
*
* @author seepine
*/
@Configuration
public class FeignConfiguration implements RequestInterceptor {
@Autowired private ObjectFactory<HttpMessageConverters> messageConverters;
@Bean
public Decoder feignDecoder() {
return new ResultStatusDecoder(
new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(messageConverters))));
}
@Override
public void apply(RequestTemplate requestTemplate) {
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String authorization = null;
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
// 传递重要请求头
requestTemplate.header("token", request.getHeader("token"));
requestTemplate.header("secret", request.getHeader("secret"));
authorization = request.getHeader("Authorization");
}
if (Strings.isBlank(authorization)) {
AuthUser user = null;
try {
user = AuthUtil.getUser();
} catch (Exception ignore) {
}
if (user != null) {
if (Strings.nonBlank(user.getToken())) {
authorization = "Bearer " + user.getToken();
} else {
authorization = "Bearer " + AuthUtil.login(user).getToken();
}
}
}
requestTemplate.header("Authorization", authorization);
// 添加内部请求头确保只内部调用
requestTemplate.header("inner", "true");
requestTemplate.header("tenantId", TenantUtil.getTenantId());
}
}

View File

@ -0,0 +1,44 @@
package cn.zgfxrc.boot.common.feign;
import feign.Response;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* @author huanghs
* @date 2022.7.1
*/
public class FeignUtil {
/**
* feign获取byteOutputStream
*
* @param response feignResponse
* @return ByteArrayOutputStream
* @throws IOException e
*/
public static ByteArrayOutputStream getOutputStream(Response response) throws IOException {
ByteArrayOutputStream os = new ByteArrayOutputStream();
getOutputStream(response, os);
return os;
}
/**
* feign获取byteOutputStream
*
* @param response feignResponse
* @param os 输出流
* @throws IOException e
*/
public static void getOutputStream(Response response, OutputStream os) throws IOException {
Response.Body body = response.body();
InputStream inputStream = body.asInputStream();
byte[] c = new byte[8096];
int length;
while ((length = inputStream.read(c)) > 0) {
os.write(c, 0, length);
}
}
}

View File

@ -0,0 +1,29 @@
package cn.zgfxrc.boot.common.feign;
import feign.FeignException;
import feign.Request;
import java.util.Collection;
import java.util.Map;
/**
* @author huanghs
* @date 2022.7.27
*/
public class FeignWrapException extends FeignException {
Integer code;
public FeignWrapException(
int status,
int code,
String message,
Request request,
Map<String, Collection<String>> headers) {
super(status, message, request, null, headers);
this.code = code;
}
public Integer getCode() {
return code;
}
}

View File

@ -0,0 +1,73 @@
package cn.zgfxrc.boot.common.feign;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.commons.CommonsMultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* @author huanghs
* @date 2022.6.28
*/
public class MultipartFileFactory {
private static final int BLOCK_SIZE = 8192;
/**
* 获取传输的multipartFile将输入流+文件名转成multipartFile文件去调用feignClient
*
* @param inputStream inputStream
* @param originalFileName 文件名
* @param fileParamName 传递给后端的文件param字段
* @return MultipartFile
*/
public static MultipartFile getMulFile(
InputStream inputStream, String originalFileName, String fileParamName) {
FileItem fileItem = createFileItem(inputStream, originalFileName, fileParamName);
// CommonsMultipartFile是feign对multipartFile的封装但是要FileItem类对象
return new CommonsMultipartFile(fileItem);
}
/**
* 获取传输的multipartFile将输入流+文件名转成multipartFile文件去调用feignClient
*
* @param inputStream inputStream
* @param originalFileName 文件
* @return MultipartFile
*/
public static MultipartFile getMulFile(InputStream inputStream, String originalFileName) {
FileItem fileItem = createFileItem(inputStream, originalFileName, "file");
// CommonsMultipartFile是feign对multipartFile的封装但是要FileItem类对象
return new CommonsMultipartFile(fileItem);
}
/**
* FileItem类对象创建
*
* @param fis 文件流
* @param originalFileName 文件名
* @param fileParamName 传递param
* @return FileItem
*/
private static FileItem createFileItem(
InputStream fis, String originalFileName, String fileParamName) {
FileItemFactory factory = new DiskFileItemFactory(16, null);
FileItem item =
factory.createItem(fileParamName, "multipart/form-data", true, originalFileName);
int bytesRead;
byte[] buffer = new byte[BLOCK_SIZE];
try {
OutputStream os = item.getOutputStream();
while ((bytesRead = fis.read(buffer, 0, BLOCK_SIZE)) != -1) {
os.write(buffer, 0, bytesRead);
}
os.close();
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
return item;
}
}

View File

@ -0,0 +1,26 @@
package cn.zgfxrc.boot.common.feign;
import feign.codec.Encoder;
import feign.form.spring.SpringFormEncoder;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.support.SpringEncoder;
import org.springframework.context.annotation.Bean;
/**
* 关键内部类此处制定了注入的feignFormEncoder可以覆盖掉原本的encoder 该方式可以让发送multipartFile成为可能
*
* @author huanghs
*/
public class MultipartSupportConfig {
private final ObjectFactory<HttpMessageConverters> messageConverters;
public MultipartSupportConfig(ObjectFactory<HttpMessageConverters> messageConverters) {
this.messageConverters = messageConverters;
}
@Bean
public Encoder feignFormEncoder() {
return new SpringFormEncoder(new SpringEncoder(messageConverters));
}
}

View File

@ -0,0 +1,60 @@
package cn.zgfxrc.boot.common.feign.decoder;
import cn.zgfxrc.boot.common.feign.FeignWrapException;
import com.fasterxml.jackson.databind.JsonNode;
import com.seepine.json.Json;
import feign.Response;
import feign.codec.ErrorDecoder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StreamUtils;
import java.nio.charset.Charset;
/**
* 处理接口异常逻辑
*
* @author huanghs
* @date 2022.7.27
*/
@Slf4j
@Configuration
public class ErrorFallback implements ErrorDecoder {
@Override
public Exception decode(String s, Response response) {
String resultStr;
try {
if (response.body() == null) {
return new FeignWrapException(
response.status(),
1,
response.status() == 401 ? "请先登录" : "",
response.request(),
response.headers());
} else {
resultStr =
StreamUtils.copyToString(response.body().asInputStream(), Charset.defaultCharset());
}
} catch (Exception e) {
return new FeignWrapException(
response.status(), 1, e.getMessage(), response.request(), response.headers());
}
JsonNode jsonNode = null;
try {
jsonNode = Json.parse(resultStr);
} catch (Exception ignored) {
}
if (jsonNode != null && jsonNode.has("code")) {
if (jsonNode.get("code").asInt(1) != 0) {
return new FeignWrapException(
response.status(),
jsonNode.get("code").asInt(1),
jsonNode.get("msg").asText(),
response.request(),
response.headers());
}
}
return new FeignWrapException(
response.status(), 1, resultStr, response.request(), response.headers());
}
}

View File

@ -0,0 +1,76 @@
package cn.zgfxrc.boot.common.feign.decoder;
import cn.zgfxrc.boot.common.feign.FeignWrapException;
import com.fasterxml.jackson.databind.JsonNode;
import com.seepine.json.Json;
import feign.Response;
import feign.codec.Decoder;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.Optional;
/**
* @author huanghs
*/
@Slf4j
public final class ResultStatusDecoder implements Decoder {
final Decoder delegate;
public ResultStatusDecoder(Decoder delegate) {
Objects.requireNonNull(delegate, "Decoder must not be null. ");
this.delegate = delegate;
}
@Override
public Object decode(Response response, Type type) throws IOException {
String contentLength =
Optional.ofNullable(response.headers().get("content-length"))
.map(Objects::toString)
.orElse("");
if (contentLength.equals("[0]")) {
return delegate.decode(response, type);
}
String contentType =
Optional.ofNullable(response.headers().get("content-type"))
.map(Objects::toString)
.orElse("");
if (!contentType.contains("json")) {
return delegate.decode(response, type);
}
String resultStr =
IOUtils.toString(response.body().asInputStream(), StandardCharsets.UTF_8.name());
JsonNode jsonNode = null;
try {
jsonNode = Json.parse(resultStr);
} catch (Exception ignored) {
}
if (jsonNode != null && jsonNode.has("code")) {
if (jsonNode.get("code").asInt(-1) == 0) {
if (jsonNode.hasNonNull("data")) {
resultStr =
jsonNode.get("data").isTextual()
? jsonNode.get("data").textValue()
: jsonNode.get("data").toString();
} else {
return null;
}
} else {
resultStr = jsonNode.get("msg").asText();
throw new FeignWrapException(
response.status(),
jsonNode.get("code").asInt(1),
resultStr,
response.request(),
response.headers());
}
}
// 回写body,因为response的流数据只能读一次这里回写后重新生成response
return delegate.decode(
response.toBuilder().body(resultStr, StandardCharsets.UTF_8).build(), type);
}
}

View File

@ -0,0 +1,18 @@
plugins {
id 'java-library'
}
dependencies {
api project(":common-core")
api("com.baomidou:mybatis-plus-boot-starter:3.5.3.1")
api("com.baomidou:mybatis-plus-extension:3.5.3.1")
api('org.springframework.boot:spring-boot-starter-web')
api("org.springframework.boot:spring-boot-starter-undertow")
api("mysql:mysql-connector-java:8.0.33")
}
bootJar {
enabled = false
}
jar {
enabled = true
}

View File

@ -0,0 +1,12 @@
package cn.zgfxrc.boot.common.mybatis;
import cn.zgfxrc.boot.common.mybatis.properties.TenantProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* @author seepine
*/
@Configuration
@EnableConfigurationProperties({TenantProperties.class})
public class AutoConfiguration {}

View File

@ -0,0 +1,59 @@
package cn.zgfxrc.boot.common.mybatis;
import cn.zgfxrc.boot.common.mybatis.properties.TenantProperties;
import cn.zgfxrc.boot.common.mybatis.tenant.TenantInnerInterceptor;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.extension.injector.methods.InsertBatchSomeColumn;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import java.util.List;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author seepine
*/
@Configuration
@AllArgsConstructor
public class MyBatisConfig {
private final TenantProperties tenantProperties;
/**
* 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false
* 避免缓存出现问题(该属性会在旧插件移除后一同移除)
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 多租户
if (tenantProperties.getEnabled()) {
interceptor.addInnerInterceptor(new TenantInnerInterceptor(tenantProperties));
}
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 防全表更新与删除插件 https://baomidou.com/pages/333106/#blockattackinnerinterceptor
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
return interceptor;
}
/**
* 添加批量插入仅支持mysql
*
* @return defaultSqlInjector
*/
@Bean
public DefaultSqlInjector dataScopeSqlInjector() {
return new DefaultSqlInjector() {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
List<AbstractMethod> methodList = super.getMethodList(mapperClass, tableInfo);
methodList.add(new InsertBatchSomeColumn());
return methodList;
}
};
}
}

View File

@ -0,0 +1,38 @@
package cn.zgfxrc.boot.common.mybatis.base;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper;
import com.baomidou.mybatisplus.extension.toolkit.ChainWrappers;
import java.util.List;
/**
* @author seepine
* @param <T> T
*/
public interface MpBaseMapper<T> extends BaseMapper<T> {
/**
* 批量插入 仅适用于 mysql
*
* @param entityList 实体列表
* @return 影响行数
*/
Integer insertBatchSomeColumn(List<T> entityList);
/**
* 丰富接口支持自定义lambda查询
*
* @return LambdaQueryChainWrapper
*/
default LambdaQueryChainWrapper<T> lambdaQuery() {
return ChainWrappers.lambdaQueryChain(this);
}
/**
* 丰富接口支持自定义lambda更新
*
* @return LambdaQueryChainWrapper
*/
default LambdaUpdateChainWrapper<T> lambdaUpdate() {
return ChainWrappers.lambdaUpdateChain(this);
}
}

View File

@ -0,0 +1,19 @@
package cn.zgfxrc.boot.common.mybatis.converter;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.lang.NonNull;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author seepine
*/
@Configuration
public class EnumWebMvcConfig implements WebMvcConfigurer {
@Override
public void addFormatters(@NonNull FormatterRegistry registry) {
registry.addConverterFactory(new StringToIEnumConverterFactory());
registry.addConverter(new LocalDateTimeConverter.StringToLocalDateTimeConverter());
registry.addConverter(new LocalDateTimeConverter.StringToLocalDateConverter());
}
}

View File

@ -0,0 +1,34 @@
package cn.zgfxrc.boot.common.mybatis.converter;
import com.baomidou.mybatisplus.annotation.IEnum;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.convert.converter.Converter;
import org.springframework.lang.NonNull;
/**
* @author seepine
*/
@Slf4j
public class IEnumConverter<T extends Serializable, E extends IEnum<T>> implements Converter<T, E> {
private final Map<String, E> enumMap = new HashMap<>();
public IEnumConverter(Class<E> enumType) {
E[] enums = enumType.getEnumConstants();
for (E e : enums) {
enumMap.put(e.getValue().toString(), e);
}
}
@Override
public E convert(@NonNull T source) {
E t = enumMap.get(source.toString());
// 允许找不到对应的枚举
if (t == null) {
log.error("IEnumConverter:找不到对应的枚举:[{}]", source);
}
return t;
}
}

View File

@ -0,0 +1,33 @@
package cn.zgfxrc.boot.common.mybatis.converter;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import org.springframework.core.convert.converter.Converter;
/**
* @author seepins
*/
public class LocalDateTimeConverter {
public static final String NORM_DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
public static final String NORM_DATE_PATTERN = "yyyy-MM-dd";
public static final String NORM_TIME_PATTERN = "HH:mm:ss";
public static class StringToLocalDateTimeConverter implements Converter<String, LocalDateTime> {
@Override
public LocalDateTime convert(String s) {
DateTimeFormatter formatter =
DateTimeFormatter.ofPattern(NORM_DATETIME_PATTERN, Locale.CHINESE);
return LocalDateTime.parse(s, formatter);
}
}
public static class StringToLocalDateConverter implements Converter<String, LocalDate> {
@Override
public LocalDate convert(String s) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(NORM_DATE_PATTERN, Locale.CHINESE);
return LocalDate.parse(s, formatter);
}
}
}

View File

@ -0,0 +1,27 @@
package cn.zgfxrc.boot.common.mybatis.converter;
import com.baomidou.mybatisplus.annotation.IEnum;
import java.util.HashMap;
import java.util.Map;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import org.springframework.lang.NonNull;
/**
* @author seepine
*/
public class StringToIEnumConverterFactory implements ConverterFactory<String, IEnum<String>> {
private static final Map<Class<?>, Converter> CONVERTERS = new HashMap<>();
@Override
@NonNull
@SuppressWarnings("unchecked")
public <E extends IEnum<String>> Converter<String, E> getConverter(@NonNull Class<E> targetType) {
Converter<String, E> converter = CONVERTERS.get(targetType);
if (converter == null) {
converter = new IEnumConverter<>(targetType);
CONVERTERS.put(targetType, converter);
}
return converter;
}
}

View File

@ -0,0 +1,48 @@
package cn.zgfxrc.boot.common.mybatis.handler;
import cn.zgfxrc.boot.common.mybatis.util.ArrayUtil;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import lombok.SneakyThrows;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
/**
* Mybatis数组,符串互转
*
* <p>MappedJdbcTypes 数据库中的数据类型 MappedTypes java中的的数据类型
*
* @author seepine
*/
@MappedTypes(value = {Integer[].class})
@MappedJdbcTypes(value = JdbcType.VARCHAR)
public class IntegerArrayTypeHandler extends BaseTypeHandler<Integer[]> {
@Override
public void setNonNullParameter(
PreparedStatement ps, int i, Integer[] parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, ArrayUtil.join(parameter));
}
@Override
@SneakyThrows
public Integer[] getNullableResult(ResultSet rs, String columnName) {
return ArrayUtil.toIntArray(rs.getString(columnName));
}
@Override
@SneakyThrows
public Integer[] getNullableResult(ResultSet rs, int columnIndex) {
return ArrayUtil.toIntArray(rs.getString(columnIndex));
}
@Override
@SneakyThrows
public Integer[] getNullableResult(CallableStatement cs, int columnIndex) {
return ArrayUtil.toIntArray(cs.getString(columnIndex));
}
}

View File

@ -0,0 +1,48 @@
package cn.zgfxrc.boot.common.mybatis.handler;
import cn.zgfxrc.boot.common.mybatis.util.ArrayUtil;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import lombok.SneakyThrows;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
/**
* Mybatis数组,符串互转
*
* <p>MappedJdbcTypes 数据库中的数据类型 MappedTypes java中的的数据类型
*
* @author seepine
*/
@MappedTypes(value = {Long[].class})
@MappedJdbcTypes(value = JdbcType.VARCHAR)
public class LongArrayTypeHandler extends BaseTypeHandler<Long[]> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Long[] parameter, JdbcType jdbcType)
throws SQLException {
ps.setString(i, ArrayUtil.join(parameter));
}
@Override
@SneakyThrows
public Long[] getNullableResult(ResultSet rs, String columnName) {
return ArrayUtil.toLongArray(rs.getString(columnName));
}
@Override
@SneakyThrows
public Long[] getNullableResult(ResultSet rs, int columnIndex) {
return ArrayUtil.toLongArray(rs.getString(columnIndex));
}
@Override
@SneakyThrows
public Long[] getNullableResult(CallableStatement cs, int columnIndex) {
return ArrayUtil.toLongArray(cs.getString(columnIndex));
}
}

View File

@ -0,0 +1,48 @@
package cn.zgfxrc.boot.common.mybatis.handler;
import cn.zgfxrc.boot.common.mybatis.util.ArrayUtil;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import lombok.SneakyThrows;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
/**
* Mybatis数组,符串互转
*
* <p>MappedJdbcTypes 数据库中的数据类型 MappedTypes java中的的数据类型
*
* @author seepine
*/
@MappedTypes(value = {List.class})
@MappedJdbcTypes(value = JdbcType.VARCHAR)
public class LongListTypeHandler extends BaseTypeHandler<List<Long>> {
@Override
public void setNonNullParameter(
PreparedStatement ps, int i, List<Long> parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, ArrayUtil.joinOfLong(parameter));
}
@Override
@SneakyThrows
public List<Long> getNullableResult(ResultSet rs, String columnName) {
return ArrayUtil.convertToLong(rs.getString(columnName));
}
@Override
@SneakyThrows
public List<Long> getNullableResult(ResultSet rs, int columnIndex) {
return ArrayUtil.convertToLong(rs.getString(columnIndex));
}
@Override
@SneakyThrows
public List<Long> getNullableResult(CallableStatement cs, int columnIndex) {
return ArrayUtil.convertToLong(cs.getString(columnIndex));
}
}

View File

@ -0,0 +1,48 @@
package cn.zgfxrc.boot.common.mybatis.handler;
import cn.zgfxrc.boot.common.mybatis.util.ArrayUtil;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import lombok.SneakyThrows;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
/**
* Mybatis数组,符串互转
*
* <p>MappedJdbcTypes 数据库中的数据类型 MappedTypes java中的的数据类型
*
* @author seepine
*/
@MappedTypes(value = {String[].class})
@MappedJdbcTypes(value = JdbcType.VARCHAR)
public class StringArrayTypeHandler extends BaseTypeHandler<String[]> {
@Override
public void setNonNullParameter(
PreparedStatement ps, int i, String[] parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, ArrayUtil.join(parameter));
}
@Override
@SneakyThrows
public String[] getNullableResult(ResultSet rs, String columnName) {
return ArrayUtil.toStrArray(rs.getString(columnName));
}
@Override
@SneakyThrows
public String[] getNullableResult(ResultSet rs, int columnIndex) {
return ArrayUtil.toStrArray(rs.getString(columnIndex));
}
@Override
@SneakyThrows
public String[] getNullableResult(CallableStatement cs, int columnIndex) {
return ArrayUtil.toStrArray(cs.getString(columnIndex));
}
}

View File

@ -0,0 +1,48 @@
package cn.zgfxrc.boot.common.mybatis.handler;
import cn.zgfxrc.boot.common.mybatis.util.ArrayUtil;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import lombok.SneakyThrows;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
/**
* Mybatis数组,符串互转
*
* <p>MappedJdbcTypes 数据库中的数据类型 MappedTypes java中的的数据类型
*
* @author seepine
*/
@MappedTypes(value = {List.class})
@MappedJdbcTypes(value = JdbcType.VARCHAR)
public class StringListTypeHandler extends BaseTypeHandler<List<String>> {
@Override
public void setNonNullParameter(
PreparedStatement ps, int i, List<String> parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, ArrayUtil.join(parameter));
}
@Override
@SneakyThrows
public List<String> getNullableResult(ResultSet rs, String columnName) {
return ArrayUtil.convert(rs.getString(columnName));
}
@Override
@SneakyThrows
public List<String> getNullableResult(ResultSet rs, int columnIndex) {
return ArrayUtil.convert(rs.getString(columnIndex));
}
@Override
@SneakyThrows
public List<String> getNullableResult(CallableStatement cs, int columnIndex) {
return ArrayUtil.convert(cs.getString(columnIndex));
}
}

View File

@ -0,0 +1,37 @@
package cn.zgfxrc.boot.common.mybatis.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.Ordered;
/**
* @author Seepine
*/
@Data
@ConfigurationProperties(prefix = "mybatis.tenant")
public class TenantProperties {
/** 扫描多租户表实体类包路径 */
private String[] scanPackages;
/** 请求头,未实现TenantFilterService时租户id从请求头获取 */
private String header = "Tenant-Id";
/** 数据库字段 */
private String dbField = "tenant_id";
/** 实体类字段 */
private String entityField = "tenantId";
/** 默认租户id */
private String defaultId = "0";
/** 扫描多租户表实体类规则 */
private String resourcePattern = "/**/*.class";
/** 租户过滤器order值一般要在权限order之后、其他Component之前 */
private Integer tenantInterceptorOrder = Ordered.HIGHEST_PRECEDENCE + 10000;
/**
* 配置了扫描包路径则说明开启多租户
*
* @return 是否开启多租户
*/
public boolean getEnabled() {
return scanPackages != null && scanPackages.length > 0;
}
}

View File

@ -0,0 +1,89 @@
package cn.zgfxrc.boot.common.mybatis.tenant;
import cn.zgfxrc.boot.common.core.util.TenantUtil;
import cn.zgfxrc.boot.common.mybatis.properties.TenantProperties;
import cn.zgfxrc.boot.common.mybatis.util.StringUtil;
import java.io.IOException;
import java.lang.reflect.Field;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.ClassUtils;
/**
* @author seepine
*/
@Order(Integer.MIN_VALUE)
@Slf4j
@Component
@AllArgsConstructor
public class TenantApplicationRunner implements ApplicationRunner {
private final TenantProperties tenantProperties;
@Override
public void run(ApplicationArguments applicationArguments) {
if (Boolean.TRUE.equals(tenantProperties.getEnabled())) {
log.info("=== start scan tenant table name ===");
// spring工具类可以获取指定路径下的全部类
for (String scanPackage : tenantProperties.getScanPackages()) {
addIgnoredTables(scanPackage);
}
log.info("{}", TenantUtil.getTables().toString());
log.info("=== tenant table name finish ===");
}
}
private void addIgnoredTables(String scanPackage) {
ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
try {
String pattern =
ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
+ ClassUtils.convertClassNameToResourcePath(scanPackage)
+ tenantProperties.getResourcePattern();
Resource[] resources = resourcePatternResolver.getResources(pattern);
// MetadataReader 的工厂类
MetadataReaderFactory readerFactory =
new CachingMetadataReaderFactory(resourcePatternResolver);
for (Resource resource : resources) {
// 用于读取类信息
MetadataReader reader = readerFactory.getMetadataReader(resource);
// 扫描到的class
String classname = reader.getClassMetadata().getClassName();
Class<?> clazz = Class.forName(classname);
// 判断
if (hasField(clazz.getSuperclass())) {
TenantUtil.addIgnoreTable(rebuildClassName(classname));
} else if (hasField(clazz)) {
TenantUtil.addIgnoreTable(rebuildClassName(classname));
}
}
} catch (IOException | ClassNotFoundException ignored) {
}
}
boolean hasField(Class<?> clazz) {
if (clazz == null) {
return false;
}
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (tenantProperties.getEntityField().equals(field.getName())) {
return true;
}
}
return false;
}
String rebuildClassName(String className) {
return StringUtil.toSymbolCase(className.substring(className.lastIndexOf(".") + 1), '_');
}
}

View File

@ -0,0 +1,37 @@
package cn.zgfxrc.boot.common.mybatis.tenant;
import cn.zgfxrc.boot.common.core.util.TenantUtil;
import cn.zgfxrc.boot.common.mybatis.properties.TenantProperties;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.StringValue;
/**
* @author seepine
*/
public class TenantHandler implements TenantLineHandler {
TenantProperties tenantProperties;
public TenantHandler(TenantProperties tenantProperties) {
this.tenantProperties = tenantProperties;
}
@Override
public Expression getTenantId() {
String tenantId = TenantUtil.getTenantId();
if (tenantId == null) {
throw new IllegalArgumentException("租户id为空");
}
return new StringValue(tenantId);
}
@Override
public boolean ignoreTable(String tableName) {
return !TenantUtil.getTables().contains(tableName);
}
@Override
public String getTenantIdColumn() {
return tenantProperties.getDbField();
}
}

View File

@ -0,0 +1,13 @@
package cn.zgfxrc.boot.common.mybatis.tenant;
import cn.zgfxrc.boot.common.mybatis.properties.TenantProperties;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
/**
* @author seepine
*/
public class TenantInnerInterceptor extends TenantLineInnerInterceptor {
public TenantInnerInterceptor(TenantProperties tenantProperties) {
super(new TenantHandler(tenantProperties));
}
}

View File

@ -0,0 +1,22 @@
package cn.zgfxrc.boot.common.mybatis.tenant.interceptor;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.springframework.lang.NonNull;
/**
* secret 验证接口
*
* @author seepine
*/
public interface TenantFilterService {
/**
* 填充租户id
*
* @param servletRequest request
* @param servletResponse response
* @return 租户id一般为String或Long或Integer
*/
@NonNull
Object fill(@NonNull ServletRequest servletRequest, @NonNull ServletResponse servletResponse);
}

View File

@ -0,0 +1,48 @@
package cn.zgfxrc.boot.common.mybatis.tenant.interceptor;
import cn.zgfxrc.boot.common.core.util.TenantUtil;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.NonNull;
import org.springframework.web.servlet.HandlerInterceptor;
/**
* @author seepine
* @since 0.1.0
*/
@Slf4j
public class TenantHandlerInterceptor implements HandlerInterceptor {
private final TenantFilterService tenantFilterService;
public TenantHandlerInterceptor(TenantFilterService tenantFilterService) {
this.tenantFilterService = tenantFilterService;
}
@Override
public boolean preHandle(
@NonNull HttpServletRequest httpServletRequest,
@NonNull HttpServletResponse httpServletResponse,
@NonNull Object handler) {
Object tenantId = tenantFilterService.fill(httpServletRequest, httpServletResponse);
log.debug("获取header中的租户ID为:{}", tenantId);
TenantUtil.setTenantId(tenantId);
return true;
}
/**
* clear ThreadLocal
*
* @param httpServletRequest httpServletRequest
* @param httpServletResponse httpServletResponse
* @param o o
* @param e e
*/
@Override
public void afterCompletion(
@NonNull HttpServletRequest httpServletRequest,
@NonNull HttpServletResponse httpServletResponse,
@NonNull Object o,
Exception e) {
TenantUtil.clear();
}
}

View File

@ -0,0 +1,33 @@
package cn.zgfxrc.boot.common.mybatis.tenant.interceptor.config;
import cn.zgfxrc.boot.common.mybatis.properties.TenantProperties;
import cn.zgfxrc.boot.common.mybatis.tenant.interceptor.TenantFilterService;
import cn.zgfxrc.boot.common.mybatis.tenant.interceptor.TenantHandlerInterceptor;
import javax.annotation.Resource;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.NonNull;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author seepine
* @since 0.1.0
*/
@Configuration
@AutoConfigureAfter(TenantServiceConfiguration.class)
public class TenantInterceptorConfiguration implements WebMvcConfigurer {
@Resource private TenantProperties tenantProperties;
@Resource private TenantFilterService tenantFilterService;
@Override
public void addInterceptors(@NonNull InterceptorRegistry registry) {
if (tenantProperties.getEnabled()) {
registry
.addInterceptor(new TenantHandlerInterceptor(tenantFilterService))
.addPathPatterns("/**")
.excludePathPatterns("/error")
.order(tenantProperties.getTenantInterceptorOrder());
}
}
}

View File

@ -0,0 +1,30 @@
package cn.zgfxrc.boot.common.mybatis.tenant.interceptor.config;
import cn.zgfxrc.boot.common.mybatis.AutoConfiguration;
import cn.zgfxrc.boot.common.mybatis.properties.TenantProperties;
import cn.zgfxrc.boot.common.mybatis.tenant.interceptor.TenantFilterService;
import cn.zgfxrc.boot.common.mybatis.tenant.interceptor.impl.MybatisTenantServiceImpl;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author seepine
* @since 0.1.0
*/
@Slf4j
@Configuration
@AutoConfigureAfter(AutoConfiguration.class)
public class TenantServiceConfiguration {
@Resource private TenantProperties tenantProperties;
@Bean
@ConditionalOnMissingBean(TenantFilterService.class)
public TenantFilterService tenantFilterService() {
return new MybatisTenantServiceImpl(tenantProperties);
}
}

View File

@ -0,0 +1,39 @@
package cn.zgfxrc.boot.common.mybatis.tenant.interceptor.impl;
import cn.zgfxrc.boot.common.mybatis.properties.TenantProperties;
import cn.zgfxrc.boot.common.mybatis.tenant.interceptor.TenantFilterService;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.springframework.lang.NonNull;
/**
* 默认租户id填充实现类从请求头获取租户id
*
* <p>生产环境建议从token对应的登录用户信息获取避免被人恶意传递请求头拿到其他租户数据
*
* @author seepine
* @since 0.1.0
*/
public class MybatisTenantServiceImpl implements TenantFilterService {
private final TenantProperties tenantProperties;
public MybatisTenantServiceImpl(TenantProperties tenantProperties) {
this.tenantProperties = tenantProperties;
}
@NonNull
@Override
public Object fill(
@NonNull ServletRequest servletRequest, @NonNull ServletResponse servletResponse) {
try {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String tenantId = request.getHeader(tenantProperties.getHeader());
if (tenantId != null && !"".equals(tenantId)) {
return tenantId;
}
} catch (Exception ignored) {
}
return tenantProperties.getDefaultId();
}
}

View File

@ -0,0 +1,83 @@
package cn.zgfxrc.boot.common.mybatis.util;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author seepine
*/
public class ArrayUtil {
private static final String COMMA = ",";
public static String joinOfLong(List<Long> list) {
if (list == null || list.isEmpty()) {
return null;
}
StringBuilder sb = new StringBuilder();
for (Object item : list) {
sb.append(item).append(COMMA);
}
return sb.substring(0, sb.length() - 1);
}
public static String join(List<String> list) {
if (list == null || list.isEmpty()) {
return null;
}
StringBuilder sb = new StringBuilder();
for (Object item : list) {
sb.append(item).append(COMMA);
}
return sb.substring(0, sb.length() - 1);
}
public static String join(String[] arr) {
return arr == null ? null : join(Arrays.asList(arr));
}
public static String join(Long[] arr) {
return arr == null
? null
: join(Arrays.stream(arr).map(Object::toString).collect(Collectors.toList()));
}
public static String join(Integer[] arr) {
return arr == null
? null
: join(Arrays.stream(arr).map(Object::toString).collect(Collectors.toList()));
}
public static String[] toStrArray(String str) {
return convert(str).toArray(new String[0]);
}
public static Long[] toLongArray(String str) {
return convert(str).stream().map(Long::valueOf).toArray(Long[]::new);
}
public static Integer[] toIntArray(String str) {
return convert(str).stream().map(Integer::valueOf).toArray(Integer[]::new);
}
public static List<String> convert(String str) {
if (isBlank(str)) {
return new ArrayList<>();
}
String[] arr = str.split(COMMA);
return Arrays.asList(arr);
}
public static List<Long> convertToLong(String str) {
if (isBlank(str)) {
return new ArrayList<>();
}
String[] arr = str.split(COMMA);
return Arrays.stream(arr).map(Long::valueOf).collect(Collectors.toList());
}
public static boolean isBlank(String str) {
return str == null || "".equals(str);
}
}

View File

@ -0,0 +1,71 @@
package cn.zgfxrc.boot.common.mybatis.util;
/**
* @author seepine
*/
public class StringUtil {
/**
* 将驼峰式命名的字符串转换为使用符号连接方式如果转换前的驼峰式命名的字符串为空则返回空字符串
*
* @param str 转换前的驼峰式命名的字符串也可以为符号连接形式
* @param symbol 连接符
* @return 转换后符号连接方式命名的字符串
* @since 4.0.10
*/
public static String toSymbolCase(CharSequence str, char symbol) {
if (str == null) {
return null;
}
final int length = str.length();
final StringBuilder sb = new StringBuilder();
char c;
for (int i = 0; i < length; i++) {
c = str.charAt(i);
if (Character.isUpperCase(c)) {
final Character preChar = (i > 0) ? str.charAt(i - 1) : null;
final Character nextChar = (i < str.length() - 1) ? str.charAt(i + 1) : null;
if (null != preChar) {
if (symbol == preChar) {
// 前一个为分隔符
if (null == nextChar || Character.isLowerCase(nextChar)) {
// 普通首字母大写如_Abb -> _abb
c = Character.toLowerCase(c);
}
// 后一个为大写按照专有名词对待如_AB -> _AB
} else if (Character.isLowerCase(preChar)) {
// 前一个为小写
sb.append(symbol);
if (null == nextChar || Character.isLowerCase(nextChar) || isNumber(nextChar)) {
// 普通首字母大写如aBcc -> a_bcc
c = Character.toLowerCase(c);
}
// 后一个为大写按照专有名词对待如aBC -> a_BC
} else {
// 前一个为大写
if (null == nextChar || Character.isLowerCase(nextChar)) {
// 普通首字母大写如ABcc -> A_bcc
sb.append(symbol);
c = Character.toLowerCase(c);
}
// 后一个为大写按照专有名词对待如ABC -> ABC
}
} else {
// 首字母需要根据后一个判断是否转为小写
if (null == nextChar || Character.isLowerCase(nextChar)) {
// 普通首字母大写如Abc -> abc
c = Character.toLowerCase(c);
}
// 后一个为大写按照专有名词对待如ABC -> ABC
}
}
sb.append(c);
}
return sb.toString();
}
public static boolean isNumber(char ch) {
return ch >= '0' && ch <= '9';
}
}

View File

@ -0,0 +1,8 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.zgfxrc.boot.common.config.JacksonConfig,\
cn.zgfxrc.boot.common.mybatis.AutoConfiguration,\
cn.zgfxrc.boot.common.mybatis.tenant.TenantApplicationRunner,\
cn.zgfxrc.boot.common.mybatis.tenant.interceptor.config.TenantServiceConfiguration,\
cn.zgfxrc.boot.common.mybatis.tenant.interceptor.config.TenantInterceptorConfiguration,\
cn.zgfxrc.boot.common.mybatis.converter.EnumWebMvcConfig,\
cn.zgfxrc.boot.common.mybatis.MyBatisConfig

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