release: 第一版工程提交
This commit is contained in:
commit
9a5057e47d
109
.cz-config.js
Normal file
109
.cz-config.js
Normal 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
3
.dockerignore
Normal file
@ -0,0 +1,3 @@
|
||||
.gradle/
|
||||
build/
|
||||
.idea/
|
16
.editorconfig
Normal file
16
.editorconfig
Normal 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
36
.gitattributes
vendored
Normal 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
6
.gitea/ISSUE_TEMPLATE.md
Normal file
@ -0,0 +1,6 @@
|
||||
## 截图
|
||||
<!--操作截图、日志截图、相关代码截图-->
|
||||
|
||||
|
||||
## 描述
|
||||
<!--回显步骤、问题描述、预期结果-->
|
40
.gitea/issue_template/bug.yml
Normal file
40
.gitea/issue_template/bug.yml
Normal 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: 例如:张三
|
40
.gitea/issue_template/feat.yml
Normal file
40
.gitea/issue_template/feat.yml
Normal 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: 例如:张三
|
40
.gitea/issue_template/improvement.yml
Normal file
40
.gitea/issue_template/improvement.yml
Normal 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: 例如:张三
|
64
.gitea/workflows/build.yaml
Normal file
64
.gitea/workflows/build.yaml
Normal 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 }}"
|
20
.gitea/workflows/check-commitlint.yml
Normal file
20
.gitea/workflows/check-commitlint.yml
Normal 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
|
32
.gitea/workflows/check-package.yaml
Normal file
32
.gitea/workflows/check-package.yaml
Normal 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
85
.gitignore
vendored
Normal 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
4
.husky/commit-msg
Normal file
@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx --no-install commitlint --edit
|
23
Dockerfile
Normal file
23
Dockerfile
Normal 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
331
README.md
Normal file
@ -0,0 +1,331 @@
|
||||
# FxBoot
|
||||
|
||||
## 一、开发准备
|
||||
|
||||
### 1.开发工具:IDEA
|
||||
|
||||
### 2.安装插件
|
||||
|
||||
#### google-java-format(谷歌代码规范插件)
|
||||
|
||||
Preferences->Other Settings->google-java-format Settings
|
||||
|
||||

|
||||
|
||||
#### save actions(保存代码自动格式化插件)
|
||||
|
||||
Preferences->Other Settings->Save Actions
|
||||
|
||||

|
||||
|
||||
#### 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
|
||||
邮箱:随意
|
||||
```
|
||||
|
||||
而后可使用该插件代替原有的启动按钮,修改代码后无需重启即可生效
|
||||
|
||||

|
||||
|
||||
## 二、快速入门
|
||||
|
||||
### 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 开启暂存区
|
||||
|
||||

|
||||
|
||||
#### 6.2 暂存想要提交的文件
|
||||
|
||||
每次暂存前,请审查一遍自己变动的,降低审查被驳回的概率
|
||||
|
||||

|
||||
|
||||
#### 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
21
app/build.gradle
Normal 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
|
||||
}
|
33
app/src/main/java/cn/zgfxrc/boot/FxBootApplication.java
Normal file
33
app/src/main/java/cn/zgfxrc/boot/FxBootApplication.java
Normal 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";
|
||||
}
|
||||
}
|
10
app/src/main/resources/application-dev.yml
Normal file
10
app/src/main/resources/application-dev.yml
Normal 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
|
3
app/src/main/resources/application-prod.yml
Normal file
3
app/src/main/resources/application-prod.yml
Normal file
@ -0,0 +1,3 @@
|
||||
spring:
|
||||
redis:
|
||||
password: ${FXBOOT_REDIS_PASSWORD:???}
|
3
app/src/main/resources/application-test.yml
Normal file
3
app/src/main/resources/application-test.yml
Normal file
@ -0,0 +1,3 @@
|
||||
spring:
|
||||
redis:
|
||||
password: ${FXBOOT_REDIS_PASSWORD:???}
|
52
app/src/main/resources/application.yml
Normal file
52
app/src/main/resources/application.yml
Normal 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
|
11
app/src/test/java/cn/zgfxrc/boot/FxBootApplicationTests.java
Normal file
11
app/src/test/java/cn/zgfxrc/boot/FxBootApplicationTests.java
Normal 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
40
build.gradle
Normal 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
23
commitlint.config.js
Normal 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",
|
||||
],
|
||||
],
|
||||
},
|
||||
};
|
24
common-config/build.gradle
Normal file
24
common-config/build.gradle
Normal 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
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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("删除失败");
|
||||
// }
|
||||
// }
|
||||
// }
|
@ -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);
|
||||
}
|
||||
}
|
@ -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分钟后再访问");
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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());
|
||||
// // }
|
||||
// }
|
||||
//}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
// }
|
||||
//}
|
@ -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();
|
||||
// }
|
||||
//}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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
18
common-core/build.gradle
Normal 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
|
||||
}
|
@ -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";
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package cn.zgfxrc.boot.common.core.entity.interfaces;
|
||||
|
||||
/**
|
||||
* 作用于新增校验,例如 @NotBlank(message = "名称不能为空",groups=AddGroup.class)
|
||||
*
|
||||
* @author seepine
|
||||
*/
|
||||
public interface AddGroup {}
|
@ -0,0 +1,8 @@
|
||||
package cn.zgfxrc.boot.common.core.entity.interfaces;
|
||||
|
||||
/**
|
||||
* 作用于删除校验
|
||||
*
|
||||
* @author seepine
|
||||
*/
|
||||
public interface DelGroup {}
|
@ -0,0 +1,8 @@
|
||||
package cn.zgfxrc.boot.common.core.entity.interfaces;
|
||||
|
||||
/**
|
||||
* 作用于拖拽校验,例如 @NotNull(message = "主键不能为空",groups=DragGroup.class)
|
||||
*
|
||||
* @author seepine
|
||||
*/
|
||||
public interface DragGroup {}
|
@ -0,0 +1,8 @@
|
||||
package cn.zgfxrc.boot.common.core.entity.interfaces;
|
||||
|
||||
/**
|
||||
* 作用于编辑校验,例如 @NotNull(message = "主键id不能为空",groups=EditGroup.class)
|
||||
*
|
||||
* @author seepine
|
||||
*/
|
||||
public interface EditGroup {}
|
@ -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);
|
||||
}
|
||||
}
|
||||
```
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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
19
common-feign/build.gradle
Normal 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
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
18
common-mybatis/build.gradle
Normal file
18
common-mybatis/build.gradle
Normal 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
|
||||
}
|
@ -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 {}
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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), '_');
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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';
|
||||
}
|
||||
}
|
@ -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
Loading…
x
Reference in New Issue
Block a user