1
0

feat: initial commit

This commit is contained in:
gin
2026-05-07 18:39:00 +08:00
commit cdee21ee8e
653 changed files with 63946 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
---
name: Bug 报告
about: 创建BUG报告以改进项目
title: ''
labels: ''
assignees: ''
---
**BUG描述**
关于BUG清晰简洁的描述。
**复现步骤**
详细的复现步骤。
**正确的行为**
你认为这个修复这个BUG后,正确的行为应该是什么。
**详细截图**
如果可以的话,请添加截图以帮助调查BUG.
**桌面端:**
- 操作系统: [例如. iOS]
- 浏览器及版本 [例如. chrome 11]
- 项目版本 [例如. 1.6.0]
**手机端:**
- 设备: [例如. iPhone6]
- 操作系统: [例如. iOS8.1]
- 浏览器及版本 [例如.safari 8]
- 项目版本 [例如. 1.6.0]
**Additional context**
任何其他你认为有助于排查错误的信息,或者你的猜测。
+20
View File
@@ -0,0 +1,20 @@
---
name: 功能建议
about: 关于该项目的建议
title: ''
labels: ''
assignees: ''
---
**您的功能请求是否与问题相关? 请描述。**
清楚简明地描述问题所在。
**描述您想要的解决方案**
对您所设想的问题的清晰简洁的描述。
**描述您考虑过的替代方案**
对您考虑过的任何替代解决方案或功能的清晰简洁的描述。
**附加上下文**
在此处添加有关功能请求的任何其他上下文或屏幕截图。
+116
View File
@@ -0,0 +1,116 @@
# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven
# 权限声明,确保 workflow 有权限写 checks 和 security-events
permissions:
contents: read
checks: write
security-events: write
name: Java CI with Maven
on:
push:
branches: [ "main" ]
paths-ignore:
- 'README.md'
- 'LICENSE'
- '.gitignore'
- '.gitattributes'
- 'picture'
pull_request:
branches: [ "main" ]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
matrix:
java-version: ['8', '17', '21']
fail-fast: false
name: Build with Java ${{ matrix.java-version }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up JDK ${{ matrix.java-version }}
uses: actions/setup-java@v3
with:
java-version: ${{ matrix.java-version }}
distribution: 'temurin'
cache: 'maven'
# 优化Maven本地仓库缓存策略
- name: Cache Maven packages
uses: actions/cache@v3
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}-${{ matrix.java-version }}
restore-keys: |
${{ runner.os }}-m2-
# 编译和测试:去掉failOnWarning,避免因为警告导致失败
- name: Build and Test with Maven
run: |
mvn -B verify --file pom.xml -Dmaven.test.failure.ignore=false -Dgpg.skip -Dmaven.javadoc.skip=false
env:
MAVEN_OPTS: -Xmx4g -XX:MaxMetaspaceSize=1g
MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version"
- name: Publish Test Report
uses: mikepenz/action-junit-report@v4
if: success() || failure()
with:
report_paths: '**/target/surefire-reports/TEST-*.xml'
detailed_summary: true
include_passed: true
fail_on_failure: true
- name: Run SonarQube Analysis
if: matrix.java-version == '17' && github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
continue-on-error: true
run: |
if [[ ! -z "${{ secrets.SONAR_TOKEN }}" ]]; then
mvn sonar:sonar \
-Dsonar.projectKey=agileboot \
-Dsonar.organization=${{ secrets.SONAR_ORGANIZATION || 'default' }} \
-Dsonar.host.url=${{ secrets.SONAR_HOST_URL || 'https://sonarcloud.io' }} \
-Dsonar.login=${{ secrets.SONAR_TOKEN }} \
-Dsonar.java.source=${{ matrix.java-version }}
else
echo "Skipping SonarQube analysis - SONAR_TOKEN not configured"
fi
# 上传构建产物,if-no-files-found 改为 warn
- name: Upload Build Artifacts
uses: actions/upload-artifact@v4
with:
name: agileboot-artifacts-java-${{ matrix.java-version }}
path: |
**/target/*.jar
!**/target/original-*.jar
retention-days: 5
if-no-files-found: warn
# # 只在 Java 17 版本上更新依赖图(权限和token已修复)
# - name: Update dependency graph
# uses: advanced-security/maven-dependency-submission-action@v4
# if: matrix.java-version == '17' && success()
# with:
# token: ${{ secrets.GITHUB_TOKEN }}
# # 发送构建状态通知
# - name: Notify Build Status
# if: always()
# uses: rtCamp/action-slack-notify@v2.2.1
# env:
# SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK || '' }}
# SLACK_CHANNEL: build-notifications
# SLACK_COLOR: ${{ job.status }}
# SLACK_TITLE: Build Status for Java ${{ matrix.java-version }}
# SLACK_MESSAGE: 'Build ${{ job.status }} on Java ${{ matrix.java-version }}'
+50
View File
@@ -0,0 +1,50 @@
######################################################################
# Build Tools
.gradle
/build/
!gradle/wrapper/gradle-wrapper.jar
target/
!.mvn/wrapper/maven-wrapper.jar
######################################################################
# IDE
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### JRebel ###
rebel.xml
### NetBeans ###
nbproject/private/
build/*
nbbuild/
dist/
nbdist/
.nb-gradle/
######################################################################
# Others
*.log
*.xml.versionsBackup
*.swp
!*/build/*.java
!*/build/*.html
!*/build/*.xml
/agileboot-admin/src/main/resources/application-prod.yml
Binary file not shown.
+18
View File
@@ -0,0 +1,18 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.3/apache-maven-3.9.3-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
+567
View File
@@ -0,0 +1,567 @@
<code_scheme name="GoogleStyle" version="173">
<option name="OTHER_INDENT_OPTIONS">
<value>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</value>
</option>
<option name="RIGHT_MARGIN" value="100" />
<AndroidXmlCodeStyleSettings>
<option name="USE_CUSTOM_SETTINGS" value="true" />
<option name="LAYOUT_SETTINGS">
<value>
<option name="INSERT_BLANK_LINE_BEFORE_TAG" value="false" />
</value>
</option>
</AndroidXmlCodeStyleSettings>
<JSCodeStyleSettings version="0">
<option name="INDENT_CHAINED_CALLS" value="false" />
</JSCodeStyleSettings>
<JavaCodeStyleSettings>
<option name="INSERT_INNER_CLASS_IMPORTS" value="true" />
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
<option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
<value />
</option>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="" withSubpackages="true" static="true" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
</value>
</option>
<option name="JD_ALIGN_PARAM_COMMENTS" value="false" />
<option name="JD_ALIGN_EXCEPTION_COMMENTS" value="false" />
<option name="JD_P_AT_EMPTY_LINES" value="false" />
<option name="JD_KEEP_EMPTY_PARAMETER" value="false" />
<option name="JD_KEEP_EMPTY_EXCEPTION" value="false" />
<option name="JD_KEEP_EMPTY_RETURN" value="false" />
</JavaCodeStyleSettings>
<Objective-C>
<option name="INDENT_NAMESPACE_MEMBERS" value="0" />
<option name="INDENT_C_STRUCT_MEMBERS" value="2" />
<option name="INDENT_CLASS_MEMBERS" value="2" />
<option name="INDENT_VISIBILITY_KEYWORDS" value="1" />
<option name="INDENT_INSIDE_CODE_BLOCK" value="2" />
<option name="KEEP_STRUCTURES_IN_ONE_LINE" value="true" />
<option name="FUNCTION_PARAMETERS_WRAP" value="5" />
<option name="FUNCTION_CALL_ARGUMENTS_WRAP" value="5" />
<option name="TEMPLATE_CALL_ARGUMENTS_WRAP" value="5" />
<option name="TEMPLATE_CALL_ARGUMENTS_ALIGN_MULTILINE" value="true" />
<option name="ALIGN_INIT_LIST_IN_COLUMNS" value="false" />
<option name="SPACE_BEFORE_SUPERCLASS_COLON" value="false" />
</Objective-C>
<Objective-C-extensions>
<option name="GENERATE_INSTANCE_VARIABLES_FOR_PROPERTIES" value="ASK" />
<option name="RELEASE_STYLE" value="IVAR" />
<option name="TYPE_QUALIFIERS_PLACEMENT" value="BEFORE" />
<file>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Import" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Macro" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Typedef" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Enum" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Constant" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Global" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Struct" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="FunctionPredecl" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Function" />
</file>
<class>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Property" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Synthesize" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InitMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="StaticMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InstanceMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="DeallocMethod" />
</class>
<extensions>
<pair source="cc" header="h" />
<pair source="c" header="h" />
</extensions>
</Objective-C-extensions>
<Python>
<option name="USE_CONTINUATION_INDENT_FOR_ARGUMENTS" value="true" />
</Python>
<TypeScriptCodeStyleSettings version="0">
<option name="INDENT_CHAINED_CALLS" value="false" />
</TypeScriptCodeStyleSettings>
<XML>
<option name="XML_ALIGN_ATTRIBUTES" value="false" />
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
</XML>
<codeStyleSettings language="CSS">
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="ECMA Script Level 4">
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="ALIGN_MULTILINE_FOR" value="false" />
<option name="CALL_PARAMETERS_WRAP" value="1" />
<option name="METHOD_PARAMETERS_WRAP" value="1" />
<option name="EXTENDS_LIST_WRAP" value="1" />
<option name="BINARY_OPERATION_WRAP" value="1" />
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
<option name="TERNARY_OPERATION_WRAP" value="1" />
<option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
<option name="FOR_STATEMENT_WRAP" value="1" />
<option name="ARRAY_INITIALIZER_WRAP" value="1" />
<option name="IF_BRACE_FORCE" value="3" />
<option name="DOWHILE_BRACE_FORCE" value="3" />
<option name="WHILE_BRACE_FORCE" value="3" />
<option name="FOR_BRACE_FORCE" value="3" />
</codeStyleSettings>
<codeStyleSettings language="HTML">
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JAVA">
<option name="RIGHT_MARGIN" value="120" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="BLANK_LINES_AFTER_CLASS_HEADER" value="1" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="ALIGN_MULTILINE_RESOURCES" value="false" />
<option name="ALIGN_MULTILINE_FOR" value="false" />
<option name="CALL_PARAMETERS_WRAP" value="1" />
<option name="METHOD_PARAMETERS_WRAP" value="1" />
<option name="EXTENDS_LIST_WRAP" value="1" />
<option name="THROWS_KEYWORD_WRAP" value="1" />
<option name="METHOD_CALL_CHAIN_WRAP" value="1" />
<option name="BINARY_OPERATION_WRAP" value="1" />
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
<option name="TERNARY_OPERATION_WRAP" value="1" />
<option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
<option name="FOR_STATEMENT_WRAP" value="1" />
<option name="ARRAY_INITIALIZER_WRAP" value="1" />
<option name="WRAP_COMMENTS" value="true" />
<option name="IF_BRACE_FORCE" value="3" />
<option name="DOWHILE_BRACE_FORCE" value="3" />
<option name="WHILE_BRACE_FORCE" value="3" />
<option name="FOR_BRACE_FORCE" value="3" />
<option name="WRAP_ON_TYPING" value="0" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JSON">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="RIGHT_MARGIN" value="80" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="ALIGN_MULTILINE_FOR" value="false" />
<option name="CALL_PARAMETERS_WRAP" value="1" />
<option name="METHOD_PARAMETERS_WRAP" value="1" />
<option name="BINARY_OPERATION_WRAP" value="1" />
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
<option name="TERNARY_OPERATION_WRAP" value="1" />
<option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
<option name="FOR_STATEMENT_WRAP" value="1" />
<option name="ARRAY_INITIALIZER_WRAP" value="1" />
<option name="IF_BRACE_FORCE" value="3" />
<option name="DOWHILE_BRACE_FORCE" value="3" />
<option name="WHILE_BRACE_FORCE" value="3" />
<option name="FOR_BRACE_FORCE" value="3" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="ObjectiveC">
<option name="RIGHT_MARGIN" value="80" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="1" />
<option name="BLANK_LINES_BEFORE_IMPORTS" value="0" />
<option name="BLANK_LINES_AFTER_IMPORTS" value="0" />
<option name="BLANK_LINES_AROUND_CLASS" value="0" />
<option name="BLANK_LINES_AROUND_METHOD" value="0" />
<option name="BLANK_LINES_AROUND_METHOD_IN_INTERFACE" value="0" />
<option name="ALIGN_MULTILINE_BINARY_OPERATION" value="false" />
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
<option name="FOR_STATEMENT_WRAP" value="1" />
<option name="ASSIGNMENT_WRAP" value="1" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="PROTO">
<option name="RIGHT_MARGIN" value="80" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Python">
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="RIGHT_MARGIN" value="80" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="PARENT_SETTINGS_INSTALLED" value="true" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="SASS">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="SCSS">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:.*Style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_width</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_height</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_weight</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_margin</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_marginTop</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_marginBottom</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_marginStart</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_marginEnd</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_marginLeft</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_marginRight</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:padding</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:paddingTop</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:paddingBottom</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:paddingStart</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:paddingEnd</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:paddingLeft</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:paddingRight</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res-auto</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>http://schemas.android.com/tools</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="protobuf">
<option name="RIGHT_MARGIN" value="80" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 valarchie
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+344
View File
@@ -0,0 +1,344 @@
<p align="center">
<img src="https://img.shields.io/badge/Release-V1.8.0-green.svg" alt="Downloads">
<img src="https://img.shields.io/badge/JDK-1.8+-green.svg" alt="Build Status">
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="Build Status">
<img src="https://img.shields.io/badge/Spring%20Boot-2.7.1-blue.svg" alt="Downloads">
<a target="_blank" href="https://bladex.vip">
<img src="https://img.shields.io/badge/Author-valarchie-ff69b4.svg" alt="Downloads">
</a>
<a target="_blank" href="https://bladex.vip">
<img src="https://img.shields.io/badge/Copyright%20-@Agileboot-%23ff3f59.svg" alt="Downloads">
</a>
</p>
<p align="center">
<img alt="logo" height="200" src="https://oscimg.oschina.net/oscnet/up-eda2a402cc061f1f5f40d9ac4c084f4c98c.png">
</p>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">AgileBoot v2.0.0 </h1>
<h4 align="center">基于SpringBoot+Vue3前后端分离的Java快速开发脚手架</h4>
<p align="center">
</p>
## ⚡平台简介⚡
AgileBoot是一套开源的全栈精简快速开发平台,毫无保留给个人及企业免费使用。本项目的目标是做一款精简可靠,代码风格优良,项目规范的小型开发脚手架。
适合个人开发者的小型项目或者公司内部项目使用。也可作为供初学者学习使用的案例。
* 前端是基于优秀的开源项目[Pure-Admin](https://github.com/pure-admin/vue-pure-admin)开发而成。在此感谢Pure-Admin作者。
* 前端采用Vue3、Element Plus、TypeScript、Pinia。对应前端仓库 [AgileBoot-Front-End](https://github.com/valarchie/AgileBoot-Front-End) ,保持同步更新。
* 后端采用Spring Boot、Spring Security & Jwt、Redis & MySql、Mybatis Plus、Hutool工具包。
* 权限认证使用Jwt,支持多终端认证系统。
* 支持注解式主从数据库切换,注解式请求限流,注解式重复请求拦截。
* 支持注解式菜单权限拦截,注解式数据权限拦截。
* 支持加载动态权限菜单,实时权限控制。
* ***有大量的单元测试,集成测试覆盖确保业务逻辑正确***。
***V1.0.0版本使用JS开发,V2.0.0版本使用TS开发***。
***V1.0.0地址:[后端(AgileBoot-Back-End-Basic)](https://github.com/valarchie/AgileBoot-Back-End-Basic) - [前端(AgileBoot-Front-End-Basic)](https://github.com/valarchie/AgileBoot-Front-End-Basic)***
> 有任何问题或者建议,可以在 _Issues_ 中提给作者。
>
> 您的Issue比Star更重要
>
> 如果觉得项目对您有帮助,可以来个Star ⭐
## 💥 在线体验 💥
演示地址:
- www.agileboot.vip
- www.agileboot.cc
> 账号密码:admin/admin123
## 🌴 项目背景 🌴
业余时间想做一些个人小项目,一开始找了很多开源项目比如Ruoyi / Jeecg / ElAdmin / RenRen-Fast / Guns / EAdmin
最后本项目选择基于Ruoyi项目进行完全重构改造。
首先非常感谢Ruoyi作者。但是Ruoyi项目存在太多缺陷。
- 命名比较乱七八糟(很多很糟糕的命名,包括机翻英语乱用)
- 项目分包以及模块比较乱
- 比较原始的Controller > Service > DAO的开发模式。过于面向过程。
- 一大堆自己造的轮子,并且没有UT覆盖。
- 大量逻辑嵌套在if else块当中
- 值的前后不统一,比如有的地方1代表是,有的地方1代表否
- 很多很奇怪的代码写法(比如return result > 0 ? true:false.. 一言难尽)
- 业务逻辑不集中,代码可读性较差。
于是我做了大量的重构工作。
### 重构内容
- 规范:
- 切分不同环境的启动文件
- 统一设计异常类
- 统一设计错误码并集中处理异常
- 统一系统内的变量并集中管理
- 统一返回模型
- 引入Google代码格式化模板
- 后端代码的命名基本都整改OK
- 前端代码的命名也非常混乱,进行了整改
- 规范系统内的常量
- 整改:
- 引入hutool包以及guava包去掉大量自己造的轮子,尽可能使用现成的轮子
- 去除代码中大量的warning
- 引入lombok去除大量getter setter代码
- 调整日志级别
- 字典类型数据完全用Enum进行代替
- 移除SQL注入的Filter,因为迁移到Mybatis Plus就不会有这个注入的问题
- XSS直接通过JSON序列化进行转义。
- 替换掉很多Deprecated的类以及配置
- 替换fastJson为Jackson
- 数据库的整体重构设计,缩减至10张表。
- 重新设计异步代码
- 前后端密码加密传输(更严谨的话,还是需要HTTPS)
- 重构权限校验和数据权限校验(直接都通过注解的形式)
- 优化:
- 优化异步服务
- 优化Redis缓存类,封装各个业务缓存,提供多级缓存实现(Redis+Guava
- 提供三个层级的缓存供使用者调用(Map,Guava,Redis使用者可依情况选择使用哪个缓存类)
- 权限判断使用多级缓存
- IP地址查询引入离线包
- 前端优化字典数据缓存
- 启动优化
- i18n支持
- 优化excel工具类,代码更加简洁
- 将所有逻辑集中于Domain模块中
- 切面记录修改者和创建者
- 统一设置事务
## ✨ 使用 ✨
### 开发环境
- JDK
- Mysql
- Redis
- Node.js
### 技术栈
| 技术 | 说明 | 版本 |
|----------------|-----------------|-------------------|
| `springboot` | Java项目必备框架 | 2.7 |
| `druid` | alibaba数据库连接池 | 1.2.8 |
| `springdoc` | 文档生成 | 3.0.0 |
| `mybatis-plus` | 数据库框架 | 3.5.2 |
| `hutool` | 国产工具包(简单易用) | 3.5.2 |
| `mockito` | 单元测试模拟 | 1.10.19 |
| `guava` | 谷歌工具包(提供简易缓存实现) | 31.0.1-jre |
| `junit` | 单元测试 | 1.10.19 |
| `h2` | 内存数据库 | 1.10.19 |
| `jackson` | 比较安全的Json框架 | follow springboot |
| `knife4j` | 接口文档框架 | 3.0.3 |
| `Spring Task` | 定时任务框架(适合小型项目) | follow springboot |
### 启动说明
#### 前置准备: 下载前后端代码
```
git clone https://github.com/valarchie/AgileBoot-Back-End
git clone https://github.com/valarchie/AgileBoot-Front-End
```
#### 安装好Mysql和Redis
#### 后端启动
```
1. 生成所需的数据库表
找到后端项目根目录下的sql目录中的agileboot_xxxxx.sql脚本文件(取最新的sql文件)。 导入到你新建的数据库中。
2. 在admin模块底下,找到resource目录下的application-dev.yml文件
配置数据库以及Redis的 地址、端口、账号密码
3. 在根目录执行mvn install
4. 找到agileboot-admin模块中的AgileBootAdminApplication启动类,直接启动即可
5. 当出现以下字样即为启动成功
____ _ _ __ _ _
/ ___| | |_ __ _ _ __ | |_ _ _ _ __ ___ _ _ ___ ___ ___ ___ ___ / _| _ _ | || |
\___ \ | __|/ _` || '__|| __| | | | || '_ \ / __|| | | | / __|/ __|/ _ \/ __|/ __|| |_ | | | || || |
___) || |_| (_| || | | |_ | |_| || |_) | \__ \| |_| || (__| (__| __/\__ \\__ \| _|| |_| || ||_|
|____/ \__|\__,_||_| \__| \__,_|| .__/ |___/ \__,_| \___|\___|\___||___/|___/|_| \__,_||_|(_)
|_|
```
#### 前端启动
详细步骤请查看对应前端部分
```
1. pnpm install
2. pnpm run dev
3. 当出现以下字样时即为启动成功
vite v2.6.14 dev server running at:
> Local: http://127.0.0.1:80/
ready in 4376ms.
```
详细过程在这个文章中:[AgileBoot - 手把手一步一步带你Run起全栈项目(SpringBoot+Vue3)](https://juejin.cn/post/7153812187834744845)
> 对于想要尝试全栈项目的前端人员,这边提供更简便的后端启动方式,无需配置Mysql和Redis直接启动
#### 无Mysql/Redis 后端启动
```
1. 找到agilboot-admin模块下的resource文件中的application.yml文件
2. 配置以下两个值
spring.profiles.active: basic,dev
改为
spring.profiles.active: basic,test
agileboot.embedded.mysql: false
agileboot.embedded.redis: false
改为
agileboot.embedded.mysql: true
agileboot.embedded.redis: true
请注意:高版本的MacOS系统,无法启动内置的Redis
3. 找到agileboot-admin模块中的AgileBootAdminApplication启动类,直接启动即可
```
## 🙊 系统内置功能 🙊
🙂 大部分功能,均有通过 **单元测试** **集成测试** 保证质量。
| | 功能 | 描述 |
|-----|-------|---------------------------------|
| | 用户管理 | 用户是系统操作者,该功能主要完成系统用户配置 |
| ⭐ | 部门管理 | 配置系统组织机构(公司、部门、小组),树结构展现支持数据权限 |
| ⭐ | 岗位管理 | 配置系统用户所属担任职务 |
| | 菜单管理 | 配置系统菜单、操作权限、按钮权限标识等,本地缓存提供性能 |
| ⭐ | 角色管理 | 角色菜单权限分配、设置角色按机构进行数据范围权限划分 |
| | 参数管理 | 对系统动态配置常用参数 |
| | 通知公告 | 系统通知公告信息发布维护 |
| 🚀 | 操作日志 | 系统正常操作日志记录和查询;系统异常信息日志记录和查询 |
| | 登录日志 | 系统登录日志记录查询包含登录异常 |
| | 在线用户 | 当前系统中活跃用户状态监控 |
| | 系统接口 | 根据业务代码自动生成相关的api接口文档 |
| | 服务监控 | 监视当前系统CPU、内存、磁盘、堆栈等相关信息 |
| | 缓存监控 | 对系统的缓存信息查询,命令统计等 |
| | 连接池监视 | 监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈 |
## 🐯 工程结构 🐯
```
agileboot
├── agileboot-admin -- 管理后台接口模块(供后台调用)
├── agileboot-api -- 开放接口模块(供客户端调用)
├── agileboot-common -- 精简基础工具模块
├── agileboot-infrastructure -- 基础设施模块(主要是配置和集成,不包含业务逻辑)
├── agileboot-domain -- 业务模块
├ ├── user -- 用户模块(举例)
├ ├── command -- 命令参数接收模型(命令)
├ ├── dto -- 返回数据类
├ ├── db -- DB操作类
├ ├── entity -- 实体类
├ ├── service -- DB Service
├ ├── mapper -- DB Dao
├ ├── model -- 领域模型类
├ ├── query -- 查询参数模型(查询)
│ ├────── UserApplicationService -- 应用服务(事务层,操作领域模型类完成业务逻辑)
```
### 代码流转
请求分为两类:一类是查询,一类是操作(即对数据有进行更新)。
**查询**Controller > xxxQuery > xxxApplicationService > xxxService(Db) > xxxMapper
**操作**Controller > xxxCommand > xxxApplicationService > xxxModel(处理逻辑) > save 或者 update (本项目直接采用JPA的方式进行插入已经更新数据)
这是借鉴CQRS的开发理念,将查询和操作分开处理。操作类的业务实现借鉴了DDD战术设计的理念,使用领域类,工厂类更面向对象的实现逻辑。
如果你不太适应这样的开发模式的话。可以在domain模块中按照你之前从Controller->Service->DAO的模式进行开发。it is up to you.
### 二次开发指南
假设你要新增一个会员member业务,可以在以下三个模块新增对应的包来实现你的业务
```
agileboot
├── agileboot-admin --
│ ├── member -- 会员模块
├── agileboot-domain --
├ ├── member -- 会员模块(举例)
├ ├── command -- 命令参数接收模型(命令)
├ ├── dto -- 返回数据类
├ ├── db -- DB操作类
├ ├── entity -- 实体类
├ ├── service -- DB Service
├ ├── mapper -- DB Dao
├ ├── model -- 领域模型类
├ ├── query -- 查询参数模型(查询)
│ ├────── MemberApplicationService -- 应用服务(事务层,操作领域模型类完成业务逻辑)
└─
```
---
## 🎅 技术文档 🎅
* [AgileBoot - 基于SpringBoot + Vue3的前后端快速开发脚手架](https://juejin.cn/post/7152871067151777829)
* [AgileBoot - 手把手一步一步带你Run起全栈项目(SpringBoot+Vue3)](https://juejin.cn/post/7153812187834744845)
* [AgileBoot - 项目内统一的错误码设计](https://juejin.cn/post/7156062116712022023)
* [AgileBoot - 如何集成内置数据库H2和内置Redis](https://juejin.cn/post/7158793441198112781)
* [AgileBoot - Mybatis Plus 框架项目落地实践总结](https://juejin.cn/post/7202573260659195963)
* [AgileBoot - SpringBoot项目多层级多环境yml设计](https://juejin.cn/post/7205171975647215676)
* [AgileBoot - 项目中多级缓存设计实践总结](https://juejin.cn/post/7208112485764857914)
* 持续输出中
## 🌻 注意事项 🌻
- IDEA会自动将.properties文件的编码设置为ISO-8859-1,请在Settings > Editor > File Encodings > Properties Files > 设置为UTF-8
- 请导入统一的代码格式化模板(Google: Settings > Editor > Code Style > Java > 设置按钮 > import schema > 选择项目根目录下的GoogleStyle.xml文件
- 如需要生成新的表,请使用CodeGenerator类进行生成。
- 填入数据库地址,账号密码,库名。然后填入所需的表名执行代码即可。(大概看一下代码就知道怎么填啦)
- 生成的类在infrastructure模块下的target/classes目录下
- 不同的数据库keywordsHandler方法请填入对应不同数据库handler。(搜索keywordsHandler关键字)
- 项目基础环境搭建,请参考docker目录下的指南搭建。保姆级启动说明:
- [AgileBoot - 手把手一步一步带你Run起全栈项目(SpringBoot+Vue3)](https://juejin.cn/post/7153812187834744845)
- 注意:管理后台的后端启动类是AgileBoot**Admin**Application
- Swagger的API地址为 http://localhost:8080/v3/api-docs
## 🎬 AgileBoot全栈交流群 🎬
QQ群: [![加入QQ群](https://img.shields.io/badge/1398880-blue.svg)](https://qm.qq.com/cgi-bin/qm/qr?k=TR5guoXS0HssErVWefmdFRirJvfpEvp1&jump_from=webapi&authKey=VkWMmVhp/pNdWuRD8sqgM+Sv2+Vy2qCJQSeLmeXlLtfER2RJBi6zL56PdcRlCmTs) 点击按钮入群。
如果觉得该项目对您有帮助,可以小额捐赠支持本项目演示网站服务器等费用~
<img alt="logo" height="200" src="https://oscimg.oschina.net/oscnet/up-28b63fdd7b3ce003bd30c25883f2276212b.png">
## 💕 特别鸣谢
- <a href="https://github.com/FerryboatSeranade" target="_blank">@pokr</a> 感谢提供ChatGpt账号助力本项目开发
## 💒 相关框架
- 基于node.js开发的后端 <a href="https://gitee.com/TsMask/mask_api_midwayjs" target="_blank">Midwayjs</a>
+72
View File
@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>agileboot</artifactId>
<groupId>com.agileboot</groupId>
<version>1.0.0</version>
</parent>
<packaging>jar</packaging>
<modelVersion>4.0.0</modelVersion>
<artifactId>agileboot-admin</artifactId>
<description>
web服务入口
</description>
<dependencies>
<!-- 业务领域 -->
<dependency>
<groupId>com.agileboot</groupId>
<artifactId>agileboot-domain</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven.surefire.plugin.version}</version>
<!-- 想跑test的话 设置成false -->
<configuration>
<skipTests>false</skipTests>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,32 @@
package com.agileboot.admin;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
/**
* 启动程序
* 定制banner.txt的网站
* http://patorjk.com/software/taag
* http://www.network-science.de/ascii/
* http://www.degraeve.com/img2txt.php
* http://life.chacuo.net/convertfont2char
* @author valarchie
*/
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@ComponentScan(basePackages = "com.agileboot.*")
public class AgileBootAdminApplication {
public static void main(String[] args) {
SpringApplication.run(AgileBootAdminApplication.class, args);
String successMsg = " ____ _ _ __ _ _ \n"
+ " / ___| | |_ __ _ _ __ | |_ _ _ _ __ ___ _ _ ___ ___ ___ ___ ___ / _| _ _ | || |\n"
+ " \\___ \\ | __|/ _` || '__|| __| | | | || '_ \\ / __|| | | | / __|/ __|/ _ \\/ __|/ __|| |_ | | | || || |\n"
+ " ___) || |_| (_| || | | |_ | |_| || |_) | \\__ \\| |_| || (__| (__| __/\\__ \\\\__ \\| _|| |_| || ||_|\n"
+ " |____/ \\__|\\__,_||_| \\__| \\__,_|| .__/ |___/ \\__,_| \\___|\\___|\\___||___/|___/|_| \\__,_||_|(_)\n"
+ " |_| ";
System.out.println(successMsg);
}
}
@@ -0,0 +1,129 @@
package com.agileboot.admin.controller.common;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.file.FileNameUtil;
import com.agileboot.common.constant.Constants.UploadSubDir;
import com.agileboot.common.core.dto.ResponseDTO;
import com.agileboot.common.exception.ApiException;
import com.agileboot.common.exception.error.ErrorCode;
import com.agileboot.common.exception.error.ErrorCode.Business;
import com.agileboot.common.utils.ServletHolderUtil;
import com.agileboot.common.utils.file.FileUploadUtils;
import com.agileboot.common.utils.jackson.JacksonUtil;
import com.agileboot.domain.common.dto.UploadDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
/**
* 通用请求处理
* TODO 需要重构
* @author valarchie
*/
@Tag(name = "上传API", description = "上传相关接口")
@RestController
@RequestMapping("/file")
@Slf4j
public class FileController {
/**
* 通用下载请求
* download接口 其实不是很有必要
* @param fileName 文件名称
*/
@Operation(summary = "下载文件")
@GetMapping("/download")
public ResponseEntity<byte[]> fileDownload(String fileName, HttpServletResponse response) {
try {
if (!FileUploadUtils.isAllowDownload(fileName)) {
// 返回类型是ResponseEntity 不能捕获异常, 需要手动将错误填到 ResponseEntity
ResponseDTO<Object> fail = ResponseDTO.fail(
new ApiException(Business.COMMON_FILE_NOT_ALLOWED_TO_DOWNLOAD, fileName));
return new ResponseEntity<>(JacksonUtil.to(fail).getBytes(), null, HttpStatus.OK);
}
String filePath = FileUploadUtils.getFileAbsolutePath(UploadSubDir.DOWNLOAD_PATH, fileName);
HttpHeaders downloadHeader = FileUploadUtils.getDownloadHeader(fileName);
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
return new ResponseEntity<>(FileUtil.readBytes(filePath), downloadHeader, HttpStatus.OK);
} catch (Exception e) {
log.error("下载文件失败", e);
return null;
}
}
/**
* 通用上传请求(单个)
*/
@Operation(summary = "单个上传文件")
@PostMapping("/upload")
public ResponseDTO<UploadDTO> uploadFile(MultipartFile file) {
if (file == null) {
throw new ApiException(ErrorCode.Business.UPLOAD_FILE_IS_EMPTY);
}
// 上传并返回新文件名称
String fileName = FileUploadUtils.upload(UploadSubDir.UPLOAD_PATH, file);
String url = ServletHolderUtil.getContextUrl() + fileName;
UploadDTO uploadDTO = UploadDTO.builder()
// 全路径
.url(url)
// 相对路径
.fileName(fileName)
// 新生成的文件名
.newFileName(FileNameUtil.getName(fileName))
// 原始的文件名
.originalFilename(file.getOriginalFilename()).build();
return ResponseDTO.ok(uploadDTO);
}
/**
* 通用上传请求(多个)
*/
@Operation(summary = "多个上传文件")
@PostMapping("/uploads")
public ResponseDTO<List<UploadDTO>> uploadFiles(List<MultipartFile> files) {
if (CollUtil.isEmpty(files)) {
throw new ApiException(ErrorCode.Business.UPLOAD_FILE_IS_EMPTY);
}
List<UploadDTO> uploads = new ArrayList<>();
for (MultipartFile file : files) {
if (file != null) {
// 上传并返回新文件名称
String fileName = FileUploadUtils.upload(UploadSubDir.UPLOAD_PATH, file);
String url = ServletHolderUtil.getContextUrl() + fileName;
UploadDTO uploadDTO = UploadDTO.builder()
.url(url)
.fileName(fileName)
.newFileName(FileNameUtil.getName(fileName))
.originalFilename(file.getOriginalFilename()).build();
uploads.add(uploadDTO);
}
}
return ResponseDTO.ok(uploads);
}
}
@@ -0,0 +1,139 @@
package com.agileboot.admin.controller.common;
import cn.hutool.core.util.StrUtil;
import com.agileboot.common.config.AgileBootConfig;
import com.agileboot.common.core.dto.ResponseDTO;
import com.agileboot.common.exception.ApiException;
import com.agileboot.common.exception.error.ErrorCode.Business;
import com.agileboot.domain.common.dto.CurrentLoginUserDTO;
import com.agileboot.domain.common.dto.TokenDTO;
import com.agileboot.domain.system.menu.MenuApplicationService;
import com.agileboot.domain.system.menu.dto.RouterDTO;
import com.agileboot.domain.system.user.UserApplicationService;
import com.agileboot.domain.system.user.command.AddUserCommand;
import com.agileboot.infrastructure.annotations.ratelimit.RateLimit;
import com.agileboot.infrastructure.annotations.ratelimit.RateLimit.CacheType;
import com.agileboot.infrastructure.annotations.ratelimit.RateLimit.LimitType;
import com.agileboot.infrastructure.user.AuthenticationUtils;
import com.agileboot.admin.customize.service.login.dto.CaptchaDTO;
import com.agileboot.admin.customize.service.login.dto.ConfigDTO;
import com.agileboot.admin.customize.service.login.command.LoginCommand;
import com.agileboot.infrastructure.user.web.SystemLoginUser;
import com.agileboot.infrastructure.annotations.ratelimit.RateLimitKey;
import com.agileboot.admin.customize.service.login.LoginService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* 首页
*
* @author valarchie
*/
@Tag(name = "登录API", description = "登录相关接口")
@RestController
@RequiredArgsConstructor
public class LoginController {
private final LoginService loginService;
private final MenuApplicationService menuApplicationService;
private final UserApplicationService userApplicationService;
private final AgileBootConfig agileBootConfig;
/**
* 访问首页,提示语
*/
@Operation(summary = "首页")
@GetMapping("/")
@RateLimit(key = RateLimitKey.TEST_KEY, time = 10, maxCount = 5, cacheType = CacheType.Map,
limitType = LimitType.GLOBAL)
public String index() {
return StrUtil.format("欢迎使用{}后台管理框架,当前版本:v{},请通过前端地址访问。",
agileBootConfig.getName(), agileBootConfig.getVersion());
}
/**
* 获取系统的内置配置
*
* @return 配置信息
*/
@GetMapping("/getConfig")
public ResponseDTO<ConfigDTO> getConfig() {
ConfigDTO configDTO = loginService.getConfig();
return ResponseDTO.ok(configDTO);
}
/**
* 生成验证码
*/
@Operation(summary = "验证码")
@RateLimit(key = RateLimitKey.LOGIN_CAPTCHA_KEY, time = 10, maxCount = 10, cacheType = CacheType.REDIS,
limitType = LimitType.IP)
@GetMapping("/captchaImage")
public ResponseDTO<CaptchaDTO> getCaptchaImg() {
CaptchaDTO captchaImg = loginService.generateCaptchaImg();
return ResponseDTO.ok(captchaImg);
}
/**
* 登录方法
*
* @param loginCommand 登录信息
* @return 结果
*/
@Operation(summary = "登录")
@PostMapping("/login")
public ResponseDTO<TokenDTO> login(@RequestBody LoginCommand loginCommand) {
// 生成令牌
String token = loginService.login(loginCommand);
SystemLoginUser loginUser = AuthenticationUtils.getSystemLoginUser();
CurrentLoginUserDTO currentUserDTO = userApplicationService.getLoginUserInfo(loginUser);
return ResponseDTO.ok(new TokenDTO(token, currentUserDTO));
}
/**
* 获取用户信息
*
* @return 用户信息
*/
@Operation(summary = "获取当前登录用户信息")
@GetMapping("/getLoginUserInfo")
public ResponseDTO<CurrentLoginUserDTO> getLoginUserInfo() {
SystemLoginUser loginUser = AuthenticationUtils.getSystemLoginUser();
CurrentLoginUserDTO currentUserDTO = userApplicationService.getLoginUserInfo(loginUser);
return ResponseDTO.ok(currentUserDTO);
}
/**
* 获取路由信息
* TODO 如果要在前端开启路由缓存的话 需要在ServerConfig.json 中 设置CachingAsyncRoutes=true 避免一直重复请求路由接口
* @return 路由信息
*/
@Operation(summary = "获取用户对应的菜单路由", description = "用于动态生成路由")
@GetMapping("/getRouters")
public ResponseDTO<List<RouterDTO>> getRouters() {
SystemLoginUser loginUser = AuthenticationUtils.getSystemLoginUser();
List<RouterDTO> routerTree = menuApplicationService.getRouterTree(loginUser);
return ResponseDTO.ok(routerTree);
}
@Operation(summary = "注册接口", description = "暂未实现")
@PostMapping("/register")
public ResponseDTO<Void> register(@RequestBody AddUserCommand command) {
return ResponseDTO.fail(new ApiException(Business.COMMON_UNSUPPORTED_OPERATION));
}
}
@@ -0,0 +1,82 @@
package com.agileboot.admin.controller.system;
import com.agileboot.common.core.base.BaseController;
import com.agileboot.common.core.dto.ResponseDTO;
import com.agileboot.common.core.page.PageDTO;
import com.agileboot.domain.common.cache.CacheCenter;
import com.agileboot.domain.system.monitor.MonitorApplicationService;
import com.agileboot.domain.system.monitor.dto.OnlineUserDTO;
import com.agileboot.domain.system.monitor.dto.RedisCacheInfoDTO;
import com.agileboot.domain.system.monitor.dto.ServerInfo;
import com.agileboot.admin.customize.aop.accessLog.AccessLog;
import com.agileboot.common.enums.common.BusinessTypeEnum;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 缓存监控
*
* @author valarchie
*/
@Tag(name = "监控API", description = "监控相关信息")
@RestController
@RequestMapping("/monitor")
@RequiredArgsConstructor
public class MonitorController extends BaseController {
private final MonitorApplicationService monitorApplicationService;
@Operation(summary = "Redis信息")
@PreAuthorize("@permission.has('monitor:cache:list')")
@GetMapping("/cacheInfo")
public ResponseDTO<RedisCacheInfoDTO> getRedisCacheInfo() {
RedisCacheInfoDTO redisCacheInfo = monitorApplicationService.getRedisCacheInfo();
return ResponseDTO.ok(redisCacheInfo);
}
@Operation(summary = "服务器信息")
@PreAuthorize("@permission.has('monitor:server:list')")
@GetMapping("/serverInfo")
public ResponseDTO<ServerInfo> getServerInfo() {
ServerInfo serverInfo = monitorApplicationService.getServerInfo();
return ResponseDTO.ok(serverInfo);
}
/**
* 获取在线用户列表
*
* @param ipAddress ip地址
* @param username 用户名
* @return 分页处理后的在线用户信息
*/
@Operation(summary = "在线用户列表")
@PreAuthorize("@permission.has('monitor:online:list')")
@GetMapping("/onlineUsers")
public ResponseDTO<PageDTO<OnlineUserDTO>> onlineUsers(String ipAddress, String username) {
List<OnlineUserDTO> onlineUserList = monitorApplicationService.getOnlineUserList(username, ipAddress);
return ResponseDTO.ok(new PageDTO<>(onlineUserList));
}
/**
* 强退用户
*/
@Operation(summary = "强退用户")
@PreAuthorize("@permission.has('monitor:online:forceLogout')")
@AccessLog(title = "在线用户", businessType = BusinessTypeEnum.FORCE_LOGOUT)
@DeleteMapping("/onlineUser/{tokenId}")
public ResponseDTO<Void> logoutOnlineUser(@PathVariable String tokenId) {
CacheCenter.loginUserCache.delete(tokenId);
return ResponseDTO.ok();
}
}
@@ -0,0 +1,88 @@
package com.agileboot.admin.controller.system;
import com.agileboot.common.core.base.BaseController;
import com.agileboot.common.core.dto.ResponseDTO;
import com.agileboot.common.core.page.PageDTO;
import com.agileboot.domain.common.cache.CacheCenter;
import com.agileboot.domain.system.config.ConfigApplicationService;
import com.agileboot.domain.system.config.command.ConfigUpdateCommand;
import com.agileboot.domain.system.config.dto.ConfigDTO;
import com.agileboot.domain.system.config.query.ConfigQuery;
import com.agileboot.admin.customize.aop.accessLog.AccessLog;
import com.agileboot.common.enums.common.BusinessTypeEnum;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 参数配置 信息操作处理
* @author valarchie
*/
@RestController
@RequestMapping("/system")
@Validated
@RequiredArgsConstructor
@Tag(name = "配置API", description = "配置相关的增删查改")
public class SysConfigController extends BaseController {
private final ConfigApplicationService configApplicationService;
/**
* 获取参数配置列表
*/
@Operation(summary = "参数列表", description = "分页获取配置参数列表")
@PreAuthorize("@permission.has('system:config:list')")
@GetMapping("/configs")
public ResponseDTO<PageDTO<ConfigDTO>> list(ConfigQuery query) {
PageDTO<ConfigDTO> page = configApplicationService.getConfigList(query);
return ResponseDTO.ok(page);
}
/**
* 根据参数编号获取详细信息
*/
@PreAuthorize("@permission.has('system:config:query')")
@GetMapping(value = "/config/{configId}")
@Operation(summary = "配置信息", description = "配置的详细信息")
public ResponseDTO<ConfigDTO> getInfo(@NotNull @Positive @PathVariable Long configId) {
ConfigDTO config = configApplicationService.getConfigInfo(configId);
return ResponseDTO.ok(config);
}
/**
* 修改参数配置
*/
@PreAuthorize("@permission.has('system:config:edit')")
@AccessLog(title = "参数管理", businessType = BusinessTypeEnum.MODIFY)
@Operation(summary = "配置修改", description = "配置修改")
@PutMapping(value = "/config/{configId}")
public ResponseDTO<Void> edit(@NotNull @Positive @PathVariable Long configId, @RequestBody ConfigUpdateCommand config) {
config.setConfigId(configId);
configApplicationService.updateConfig(config);
return ResponseDTO.ok();
}
/**
* 刷新参数缓存
*/
@Operation(summary = "刷新配置缓存")
@PreAuthorize("@permission.has('system:config:remove')")
@AccessLog(title = "参数管理", businessType = BusinessTypeEnum.CLEAN)
@DeleteMapping("/configs/cache")
public ResponseDTO<Void> refreshCache() {
CacheCenter.configCache.invalidateAll();
return ResponseDTO.ok();
}
}
@@ -0,0 +1,111 @@
package com.agileboot.admin.controller.system;
import cn.hutool.core.lang.tree.Tree;
import com.agileboot.common.core.base.BaseController;
import com.agileboot.common.core.dto.ResponseDTO;
import com.agileboot.domain.system.dept.DeptApplicationService;
import com.agileboot.domain.system.dept.command.AddDeptCommand;
import com.agileboot.domain.system.dept.command.UpdateDeptCommand;
import com.agileboot.domain.system.dept.dto.DeptDTO;
import com.agileboot.domain.system.dept.query.DeptQuery;
import com.agileboot.admin.customize.aop.accessLog.AccessLog;
import com.agileboot.common.enums.common.BusinessTypeEnum;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import javax.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 部门信息
*
* @author valarchie
*/
@RestController
@RequestMapping("/system")
@Validated
@RequiredArgsConstructor
@Tag(name = "部门API", description = "部门相关的增删查改")
public class SysDeptController extends BaseController {
private final DeptApplicationService deptApplicationService;
/**
* 获取部门列表
*/
@Operation(summary = "部门列表")
@PreAuthorize("@permission.has('system:dept:list')")
@GetMapping("/depts")
public ResponseDTO<List<DeptDTO>> list(DeptQuery query) {
List<DeptDTO> deptList = deptApplicationService.getDeptList(query);
return ResponseDTO.ok(deptList);
}
/**
* 根据部门编号获取详细信息
*/
@Operation(summary = "部门详情")
@PreAuthorize("@permission.has('system:dept:query')")
@GetMapping(value = "/dept/{deptId}")
public ResponseDTO<DeptDTO> getInfo(@PathVariable Long deptId) {
DeptDTO dept = deptApplicationService.getDeptInfo(deptId);
return ResponseDTO.ok(dept);
}
/**
* 获取部门下拉树列表
*/
@Operation(summary = "获取部门树级结构")
@GetMapping("/depts/dropdown")
public ResponseDTO<List<Tree<Long>>> dropdownList() {
List<Tree<Long>> deptTree = deptApplicationService.getDeptTree();
return ResponseDTO.ok(deptTree);
}
/**
* 新增部门
*/
@Operation(summary = "新增部门")
@PreAuthorize("@permission.has('system:dept:add')")
@AccessLog(title = "部门管理", businessType = BusinessTypeEnum.ADD)
@PostMapping("/dept")
public ResponseDTO<Void> add(@RequestBody AddDeptCommand addCommand) {
deptApplicationService.addDept(addCommand);
return ResponseDTO.ok();
}
/**
* 修改部门
*/
@Operation(summary = "修改部门")
@PreAuthorize("@permission.has('system:dept:edit') AND @dataScope.checkDeptId(#updateCommand.deptId)")
@AccessLog(title = "部门管理", businessType = BusinessTypeEnum.MODIFY)
@PutMapping("/dept/{deptId}")
public ResponseDTO<Void> edit(@PathVariable("deptId")Long deptId, @RequestBody UpdateDeptCommand updateCommand) {
updateCommand.setDeptId(deptId);
deptApplicationService.updateDept(updateCommand);
return ResponseDTO.ok();
}
/**
* 删除部门
*/
@Operation(summary = "删除部门")
@PreAuthorize("@permission.has('system:dept:remove') AND @dataScope.checkDeptId(#deptId)")
@AccessLog(title = "部门管理", businessType = BusinessTypeEnum.DELETE)
@DeleteMapping("/dept/{deptId}")
public ResponseDTO<Void> remove(@PathVariable @NotNull Long deptId) {
deptApplicationService.removeDept(deptId);
return ResponseDTO.ok();
}
}
@@ -0,0 +1,120 @@
package com.agileboot.admin.controller.system;
import com.agileboot.common.core.base.BaseController;
import com.agileboot.common.core.dto.ResponseDTO;
import com.agileboot.common.core.page.PageDTO;
import com.agileboot.common.utils.poi.CustomExcelUtil;
import com.agileboot.domain.common.command.BulkOperationCommand;
import com.agileboot.domain.system.log.LogApplicationService;
import com.agileboot.domain.system.log.dto.LoginLogDTO;
import com.agileboot.domain.system.log.query.LoginLogQuery;
import com.agileboot.domain.system.log.dto.OperationLogDTO;
import com.agileboot.domain.system.log.query.OperationLogQuery;
import com.agileboot.admin.customize.aop.accessLog.AccessLog;
import com.agileboot.common.enums.common.BusinessTypeEnum;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 系统访问记录
*
* @author valarchie
*/
@Tag(name = "日志API", description = "日志相关API")
@RestController
@RequestMapping("/logs")
@Validated
@RequiredArgsConstructor
public class SysLogsController extends BaseController {
private final LogApplicationService logApplicationService;
@Operation(summary = "登录日志列表")
@PreAuthorize("@permission.has('monitor:logininfor:list')")
@GetMapping("/loginLogs")
public ResponseDTO<PageDTO<LoginLogDTO>> loginInfoList(LoginLogQuery query) {
PageDTO<LoginLogDTO> pageDTO = logApplicationService.getLoginInfoList(query);
return ResponseDTO.ok(pageDTO);
}
@Operation(summary = "登录日志导出", description = "将登录日志导出到excel")
@AccessLog(title = "登录日志", businessType = BusinessTypeEnum.EXPORT)
@PreAuthorize("@permission.has('monitor:logininfor:export')")
@GetMapping("/loginLogs/excel")
public void loginInfosExcel(HttpServletResponse response, LoginLogQuery query) {
PageDTO<LoginLogDTO> pageDTO = logApplicationService.getLoginInfoList(query);
CustomExcelUtil.writeToResponse(pageDTO.getRows(), LoginLogDTO.class, response);
}
@Operation(summary = "删除登录日志")
@PreAuthorize("@permission.has('monitor:logininfor:remove')")
@AccessLog(title = "登录日志", businessType = BusinessTypeEnum.DELETE)
@DeleteMapping("/loginLogs")
public ResponseDTO<Void> removeLoginInfos(@RequestParam @NotNull @NotEmpty List<Long> ids) {
logApplicationService.deleteLoginInfo(new BulkOperationCommand<>(ids));
return ResponseDTO.ok();
}
@Operation(summary = "操作日志列表")
@PreAuthorize("@permission.has('monitor:operlog:list')")
@GetMapping("/operationLogs")
public ResponseDTO<PageDTO<OperationLogDTO>> operationLogs(OperationLogQuery query) {
PageDTO<OperationLogDTO> pageDTO = logApplicationService.getOperationLogList(query);
return ResponseDTO.ok(pageDTO);
}
// @GetMapping("/download")
// public ResponseEntity<InputStreamResource> downloadFile() throws IOException {
// // 从文件系统或其他位置获取文件输入流
// File file = new File("path/to/file");
// InputStream inputStream = new FileInputStream(file);
// CustomExcelUtil.wri
//
// // 创建一个 InputStreamResource 对象,将文件输入流包装在其中
// InputStreamResource resource = new InputStreamResource(inputStream);
//
// // 返回 ResponseEntity 对象,其中包含 InputStreamResource 对象和文件名
// return ResponseEntity.ok()
// .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + file.getName())
// .contentType(MediaType.APPLICATION_OCTET_STREAM)
// .contentLength(file.length())
// .body(resource);
// }
/**
* 可否改成以上的形式 TODO
* @param response
* @param query
*/
@Operation(summary = "操作日志导出")
@AccessLog(title = "操作日志", businessType = BusinessTypeEnum.EXPORT)
@PreAuthorize("@permission.has('monitor:operlog:export')")
@GetMapping("/operationLogs/excel")
public void operationLogsExcel(HttpServletResponse response, OperationLogQuery query) {
PageDTO<OperationLogDTO> pageDTO = logApplicationService.getOperationLogList(query);
CustomExcelUtil.writeToResponse(pageDTO.getRows(), OperationLogDTO.class, response);
}
@Operation(summary = "删除操作日志")
@AccessLog(title = "操作日志", businessType = BusinessTypeEnum.DELETE)
@PreAuthorize("@permission.has('monitor:operlog:remove')")
@DeleteMapping("/operationLogs")
public ResponseDTO<Void> removeOperationLogs(@RequestParam List<Long> operationIds) {
logApplicationService.deleteOperationLog(new BulkOperationCommand<>(operationIds));
return ResponseDTO.ok();
}
}
@@ -0,0 +1,120 @@
package com.agileboot.admin.controller.system;
import cn.hutool.core.lang.tree.Tree;
import com.agileboot.common.core.base.BaseController;
import com.agileboot.common.core.dto.ResponseDTO;
import com.agileboot.domain.system.menu.MenuApplicationService;
import com.agileboot.domain.system.menu.command.AddMenuCommand;
import com.agileboot.domain.system.menu.command.UpdateMenuCommand;
import com.agileboot.domain.system.menu.dto.MenuDTO;
import com.agileboot.domain.system.menu.dto.MenuDetailDTO;
import com.agileboot.domain.system.menu.query.MenuQuery;
import com.agileboot.admin.customize.aop.accessLog.AccessLog;
import com.agileboot.infrastructure.user.AuthenticationUtils;
import com.agileboot.infrastructure.user.web.SystemLoginUser;
import com.agileboot.common.enums.common.BusinessTypeEnum;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.PositiveOrZero;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 菜单信息
*
* @author valarchie
*/
@Tag(name = "菜单API", description = "菜单相关的增删查改")
@RestController
@RequestMapping("/system/menus")
@Validated
@RequiredArgsConstructor
public class SysMenuController extends BaseController {
private final MenuApplicationService menuApplicationService;
/**
* 获取菜单列表
*/
@Operation(summary = "菜单列表")
@PreAuthorize("@permission.has('system:menu:list')")
@GetMapping
public ResponseDTO<List<MenuDTO>> menuList(MenuQuery menuQuery) {
List<MenuDTO> menuList = menuApplicationService.getMenuList(menuQuery);
return ResponseDTO.ok(menuList);
}
/**
* 根据菜单编号获取详细信息
*/
@Operation(summary = "菜单详情")
@PreAuthorize("@permission.has('system:menu:query')")
@GetMapping(value = "/{menuId}")
public ResponseDTO<MenuDetailDTO> menuInfo(@PathVariable @NotNull @PositiveOrZero Long menuId) {
MenuDetailDTO menu = menuApplicationService.getMenuInfo(menuId);
return ResponseDTO.ok(menu);
}
/**
* 获取菜单下拉树列表
*/
@Operation(summary = "菜单列表(树级)", description = "菜单树级下拉框")
@GetMapping("/dropdown")
public ResponseDTO<List<Tree<Long>>> dropdownList() {
SystemLoginUser loginUser = AuthenticationUtils.getSystemLoginUser();
List<Tree<Long>> dropdownList = menuApplicationService.getDropdownList(loginUser);
return ResponseDTO.ok(dropdownList);
}
/**
* 新增菜单
* 需支持一级菜单以及 多级菜单 子菜单为一个 或者 多个的情况
* 隐藏菜单不显示 以及rank排序
* 内链 和 外链
*/
@Operation(summary = "添加菜单")
@PreAuthorize("@permission.has('system:menu:add')")
@AccessLog(title = "菜单管理", businessType = BusinessTypeEnum.ADD)
@PostMapping
public ResponseDTO<Void> add(@RequestBody AddMenuCommand addCommand) {
menuApplicationService.addMenu(addCommand);
return ResponseDTO.ok();
}
/**
* 修改菜单
*/
@Operation(summary = "编辑菜单")
@PreAuthorize("@permission.has('system:menu:edit')")
@AccessLog(title = "菜单管理", businessType = BusinessTypeEnum.MODIFY)
@PutMapping("/{menuId}")
public ResponseDTO<Void> edit(@PathVariable("menuId") Long menuId, @RequestBody UpdateMenuCommand updateCommand) {
updateCommand.setMenuId(menuId);
menuApplicationService.updateMenu(updateCommand);
return ResponseDTO.ok();
}
/**
* 删除菜单
*/
@Operation(summary = "删除菜单")
@PreAuthorize("@permission.has('system:menu:remove')")
@AccessLog(title = "菜单管理", businessType = BusinessTypeEnum.DELETE)
@DeleteMapping("/{menuId}")
public ResponseDTO<Void> remove(@PathVariable("menuId") Long menuId) {
menuApplicationService.remove(menuId);
return ResponseDTO.ok();
}
}
@@ -0,0 +1,122 @@
package com.agileboot.admin.controller.system;
import com.agileboot.common.core.base.BaseController;
import com.agileboot.common.core.dto.ResponseDTO;
import com.agileboot.common.core.page.PageDTO;
import com.agileboot.domain.common.command.BulkOperationCommand;
import com.agileboot.domain.system.notice.NoticeApplicationService;
import com.agileboot.domain.system.notice.command.NoticeAddCommand;
import com.agileboot.domain.system.notice.command.NoticeUpdateCommand;
import com.agileboot.domain.system.notice.dto.NoticeDTO;
import com.agileboot.domain.system.notice.query.NoticeQuery;
import com.agileboot.admin.customize.aop.accessLog.AccessLog;
import com.agileboot.infrastructure.annotations.unrepeatable.Unrepeatable;
import com.agileboot.infrastructure.annotations.unrepeatable.Unrepeatable.CheckType;
import com.agileboot.common.enums.common.BusinessTypeEnum;
import com.baomidou.dynamic.datasource.annotation.DS;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 公告 信息操作处理
*
* @author valarchie
*/
@Tag(name = "公告API", description = "公告相关的增删查改")
@RestController
@RequestMapping("/system/notices")
@Validated
@RequiredArgsConstructor
public class SysNoticeController extends BaseController {
private final NoticeApplicationService noticeApplicationService;
/**
* 获取通知公告列表
*/
@Operation(summary = "公告列表")
@PreAuthorize("@permission.has('system:notice:list')")
@GetMapping
public ResponseDTO<PageDTO<NoticeDTO>> list(NoticeQuery query) {
PageDTO<NoticeDTO> pageDTO = noticeApplicationService.getNoticeList(query);
return ResponseDTO.ok(pageDTO);
}
/**
* 获取通知公告列表
* 从从库获取数据 例子 仅供参考
*/
@Operation(summary = "公告列表(从数据库从库获取)", description = "演示主从库的例子")
@DS("slave")
@PreAuthorize("@permission.has('system:notice:list')")
@GetMapping("/database/slave")
public ResponseDTO<PageDTO<NoticeDTO>> listFromSlave(NoticeQuery query) {
PageDTO<NoticeDTO> pageDTO = noticeApplicationService.getNoticeList(query);
return ResponseDTO.ok(pageDTO);
}
/**
* 根据通知公告编号获取详细信息
*/
@Operation(summary = "公告详情")
@PreAuthorize("@permission.has('system:notice:query')")
@GetMapping(value = "/{noticeId}")
public ResponseDTO<NoticeDTO> getInfo(@PathVariable @NotNull @Positive Long noticeId) {
return ResponseDTO.ok(noticeApplicationService.getNoticeInfo(noticeId));
}
/**
* 新增通知公告
*/
@Operation(summary = "添加公告")
@Unrepeatable(interval = 60, checkType = CheckType.SYSTEM_USER)
@PreAuthorize("@permission.has('system:notice:add')")
@AccessLog(title = "通知公告", businessType = BusinessTypeEnum.ADD)
@PostMapping
public ResponseDTO<Void> add(@RequestBody NoticeAddCommand addCommand) {
noticeApplicationService.addNotice(addCommand);
return ResponseDTO.ok();
}
/**
* 修改通知公告
*/
@Operation(summary = "修改公告")
@PreAuthorize("@permission.has('system:notice:edit')")
@AccessLog(title = "通知公告", businessType = BusinessTypeEnum.MODIFY)
@PutMapping("/{noticeId}")
public ResponseDTO<Void> edit(@PathVariable Long noticeId, @RequestBody NoticeUpdateCommand updateCommand) {
updateCommand.setNoticeId(noticeId);
noticeApplicationService.updateNotice(updateCommand);
return ResponseDTO.ok();
}
/**
* 删除通知公告
*/
@Operation(summary = "删除公告")
@PreAuthorize("@permission.has('system:notice:remove')")
@AccessLog(title = "通知公告", businessType = BusinessTypeEnum.DELETE)
@DeleteMapping
public ResponseDTO<Void> remove(@RequestParam List<Integer> noticeIds) {
noticeApplicationService.deleteNotice(new BulkOperationCommand<>(noticeIds));
return ResponseDTO.ok();
}
}
@@ -0,0 +1,122 @@
package com.agileboot.admin.controller.system;
import com.agileboot.admin.customize.aop.accessLog.AccessLog;
import com.agileboot.common.core.base.BaseController;
import com.agileboot.common.core.dto.ResponseDTO;
import com.agileboot.common.core.page.PageDTO;
import com.agileboot.common.enums.common.BusinessTypeEnum;
import com.agileboot.common.utils.poi.CustomExcelUtil;
import com.agileboot.domain.common.command.BulkOperationCommand;
import com.agileboot.domain.system.post.PostApplicationService;
import com.agileboot.domain.system.post.command.AddPostCommand;
import com.agileboot.domain.system.post.command.UpdatePostCommand;
import com.agileboot.domain.system.post.dto.PostDTO;
import com.agileboot.domain.system.post.query.PostQuery;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 岗位信息操作处理
*
* @author ruoyi
*/
@Tag(name = "职位API", description = "职位相关的增删查改")
@RestController
@RequestMapping("/system/post")
@Validated
@RequiredArgsConstructor
public class SysPostController extends BaseController {
private final PostApplicationService postApplicationService;
/**
* 获取岗位列表
*/
@Operation(summary = "职位列表")
@PreAuthorize("@permission.has('system:post:list')")
@GetMapping("/list")
public ResponseDTO<PageDTO<PostDTO>> list(PostQuery query) {
PageDTO<PostDTO> pageDTO = postApplicationService.getPostList(query);
return ResponseDTO.ok(pageDTO);
}
/**
* 导出查询到的所有岗位信息到excel文件
* @param response http响应
* @param query 查询参数
* @author Kevin Zhang
* @date 2023-10-02
*/
@Operation(summary = "职位列表导出")
@AccessLog(title = "岗位管理", businessType = BusinessTypeEnum.EXPORT)
@PreAuthorize("@permission.has('system:post:export')")
@GetMapping("/excel")
public void export(HttpServletResponse response, PostQuery query) {
List<PostDTO> all = postApplicationService.getPostListAll(query);
CustomExcelUtil.writeToResponse(all, PostDTO.class, response);
}
/**
* 根据岗位编号获取详细信息
*/
@Operation(summary = "职位详情")
@PreAuthorize("@permission.has('system:post:query')")
@GetMapping(value = "/{postId}")
public ResponseDTO<PostDTO> getInfo(@PathVariable Long postId) {
PostDTO post = postApplicationService.getPostInfo(postId);
return ResponseDTO.ok(post);
}
/**
* 新增岗位
*/
@Operation(summary = "添加职位")
@PreAuthorize("@permission.has('system:post:add')")
@AccessLog(title = "岗位管理", businessType = BusinessTypeEnum.ADD)
@PostMapping
public ResponseDTO<Void> add(@RequestBody AddPostCommand addCommand) {
postApplicationService.addPost(addCommand);
return ResponseDTO.ok();
}
/**
* 修改岗位
*/
@Operation(summary = "修改职位")
@PreAuthorize("@permission.has('system:post:edit')")
@AccessLog(title = "岗位管理", businessType = BusinessTypeEnum.MODIFY)
@PutMapping
public ResponseDTO<Void> edit(@RequestBody UpdatePostCommand updateCommand) {
postApplicationService.updatePost(updateCommand);
return ResponseDTO.ok();
}
/**
* 删除岗位
*/
@Operation(summary = "删除职位")
@PreAuthorize("@permission.has('system:post:remove')")
@AccessLog(title = "岗位管理", businessType = BusinessTypeEnum.DELETE)
@DeleteMapping
public ResponseDTO<Void> remove(@RequestParam @NotNull @NotEmpty List<Long> ids) {
postApplicationService.deletePost(new BulkOperationCommand<>(ids));
return ResponseDTO.ok();
}
}
@@ -0,0 +1,97 @@
package com.agileboot.admin.controller.system;
import com.agileboot.common.constant.Constants.UploadSubDir;
import com.agileboot.common.core.base.BaseController;
import com.agileboot.common.core.dto.ResponseDTO;
import com.agileboot.common.exception.ApiException;
import com.agileboot.common.exception.error.ErrorCode;
import com.agileboot.common.utils.file.FileUploadUtils;
import com.agileboot.domain.common.dto.UploadFileDTO;
import com.agileboot.domain.system.user.UserApplicationService;
import com.agileboot.domain.system.user.command.UpdateProfileCommand;
import com.agileboot.domain.system.user.command.UpdateUserAvatarCommand;
import com.agileboot.domain.system.user.command.UpdateUserPasswordCommand;
import com.agileboot.domain.system.user.dto.UserProfileDTO;
import com.agileboot.admin.customize.aop.accessLog.AccessLog;
import com.agileboot.infrastructure.user.AuthenticationUtils;
import com.agileboot.infrastructure.user.web.SystemLoginUser;
import com.agileboot.common.enums.common.BusinessTypeEnum;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
/**
* 个人信息 业务处理
*
* @author ruoyi
*/
@Tag(name = "个人信息API", description = "个人信息相关接口")
@RestController
@RequestMapping("/system/user/profile")
@RequiredArgsConstructor
public class SysProfileController extends BaseController {
private final UserApplicationService userApplicationService;
/**
* 个人信息
*/
@Operation(summary = "获取个人信息")
@GetMapping
public ResponseDTO<UserProfileDTO> profile() {
SystemLoginUser user = AuthenticationUtils.getSystemLoginUser();
UserProfileDTO userProfile = userApplicationService.getUserProfile(user.getUserId());
return ResponseDTO.ok(userProfile);
}
/**
* 修改用户
*/
@Operation(summary = "修改个人信息")
@AccessLog(title = "个人信息", businessType = BusinessTypeEnum.MODIFY)
@PutMapping
public ResponseDTO<Void> updateProfile(@RequestBody UpdateProfileCommand command) {
SystemLoginUser loginUser = AuthenticationUtils.getSystemLoginUser();
command.setUserId(loginUser.getUserId());
userApplicationService.updateUserProfile(command);
return ResponseDTO.ok();
}
/**
* 重置密码
*/
@Operation(summary = "重置个人密码")
@AccessLog(title = "个人信息", businessType = BusinessTypeEnum.MODIFY)
@PutMapping("/password")
public ResponseDTO<Void> updatePassword(@RequestBody UpdateUserPasswordCommand command) {
SystemLoginUser loginUser = AuthenticationUtils.getSystemLoginUser();
command.setUserId(loginUser.getUserId());
userApplicationService.updatePasswordBySelf(loginUser, command);
return ResponseDTO.ok();
}
/**
* 头像上传
*/
@Operation(summary = "修改个人头像")
@AccessLog(title = "用户头像", businessType = BusinessTypeEnum.MODIFY)
@PostMapping("/avatar")
public ResponseDTO<UploadFileDTO> avatar(@RequestParam("avatarfile") MultipartFile file) {
if (file.isEmpty()) {
throw new ApiException(ErrorCode.Business.USER_UPLOAD_FILE_FAILED);
}
SystemLoginUser loginUser = AuthenticationUtils.getSystemLoginUser();
String avatarUrl = FileUploadUtils.upload(UploadSubDir.AVATAR_PATH, file);
userApplicationService.updateUserAvatar(new UpdateUserAvatarCommand(loginUser.getUserId(), avatarUrl));
return ResponseDTO.ok(new UploadFileDTO(avatarUrl));
}
}
@@ -0,0 +1,197 @@
package com.agileboot.admin.controller.system;
import com.agileboot.common.core.base.BaseController;
import com.agileboot.common.core.dto.ResponseDTO;
import com.agileboot.common.core.page.PageDTO;
import com.agileboot.common.utils.poi.CustomExcelUtil;
import com.agileboot.domain.system.role.RoleApplicationService;
import com.agileboot.domain.system.role.command.AddRoleCommand;
import com.agileboot.domain.system.role.command.UpdateDataScopeCommand;
import com.agileboot.domain.system.role.command.UpdateRoleCommand;
import com.agileboot.domain.system.role.command.UpdateStatusCommand;
import com.agileboot.domain.system.role.dto.RoleDTO;
import com.agileboot.domain.system.role.query.AllocatedRoleQuery;
import com.agileboot.domain.system.role.query.RoleQuery;
import com.agileboot.domain.system.role.query.UnallocatedRoleQuery;
import com.agileboot.domain.system.user.dto.UserDTO;
import com.agileboot.admin.customize.aop.accessLog.AccessLog;
import com.agileboot.common.enums.common.BusinessTypeEnum;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 角色信息
*
* @author valarchie
*/
@Tag(name = "角色API", description = "角色相关的增删查改")
@RestController
@RequestMapping("/system/role")
@Validated
@RequiredArgsConstructor
public class SysRoleController extends BaseController {
private final RoleApplicationService roleApplicationService;
@Operation(summary = "角色列表")
@PreAuthorize("@permission.has('system:role:list')")
@GetMapping("/list")
public ResponseDTO<PageDTO<RoleDTO>> list(RoleQuery query) {
PageDTO<RoleDTO> pageDTO = roleApplicationService.getRoleList(query);
return ResponseDTO.ok(pageDTO);
}
@Operation(summary = "角色列表导出")
@AccessLog(title = "角色管理", businessType = BusinessTypeEnum.EXPORT)
@PreAuthorize("@permission.has('system:role:export')")
@PostMapping("/export")
public void export(HttpServletResponse response, RoleQuery query) {
PageDTO<RoleDTO> pageDTO = roleApplicationService.getRoleList(query);
CustomExcelUtil.writeToResponse(pageDTO.getRows(), RoleDTO.class, response);
}
/**
* 根据角色编号获取详细信息
*/
@Operation(summary = "角色详情")
@PreAuthorize("@permission.has('system:role:query')")
@GetMapping(value = "/{roleId}")
public ResponseDTO<RoleDTO> getInfo(@PathVariable @NotNull Long roleId) {
RoleDTO roleInfo = roleApplicationService.getRoleInfo(roleId);
return ResponseDTO.ok(roleInfo);
}
/**
* 新增角色
*/
@Operation(summary = "添加角色")
@PreAuthorize("@permission.has('system:role:add')")
@AccessLog(title = "角色管理", businessType = BusinessTypeEnum.ADD)
@PostMapping
public ResponseDTO<Void> add(@RequestBody AddRoleCommand addCommand) {
roleApplicationService.addRole(addCommand);
return ResponseDTO.ok();
}
/**
* 移除角色
*/
@Operation(summary = "删除角色")
@PreAuthorize("@permission.has('system:role:remove')")
@AccessLog(title = "角色管理", businessType = BusinessTypeEnum.DELETE)
@DeleteMapping(value = "/{roleId}")
public ResponseDTO<Void> remove(@PathVariable("roleId") List<Long> roleIds) {
roleApplicationService.deleteRoleByBulk(roleIds);
return ResponseDTO.ok();
}
/**
* 修改保存角色
*/
@Operation(summary = "修改角色")
@PreAuthorize("@permission.has('system:role:edit')")
@AccessLog(title = "角色管理", businessType = BusinessTypeEnum.MODIFY)
@PutMapping
public ResponseDTO<Void> edit(@Validated @RequestBody UpdateRoleCommand updateCommand) {
roleApplicationService.updateRole(updateCommand);
return ResponseDTO.ok();
}
/**
* 修改保存数据权限
*/
@Operation(summary = "修改角色数据权限")
@PreAuthorize("@permission.has('system:role:edit')")
@AccessLog(title = "角色管理", businessType = BusinessTypeEnum.MODIFY)
@PutMapping("/{roleId}/dataScope")
public ResponseDTO<Void> dataScope(@PathVariable("roleId") Long roleId,
@RequestBody UpdateDataScopeCommand command) {
command.setRoleId(roleId);
roleApplicationService.updateDataScope(command);
return ResponseDTO.ok();
}
/**
* 角色状态修改
*/
@Operation(summary = "修改角色状态")
@PreAuthorize("@permission.has('system:role:edit')")
@AccessLog(title = "角色管理", businessType = BusinessTypeEnum.MODIFY)
@PutMapping("/{roleId}/status")
public ResponseDTO<Void> changeStatus(@PathVariable("roleId") Long roleId,
@RequestBody UpdateStatusCommand command) {
command.setRoleId(roleId);
roleApplicationService.updateStatus(command);
return ResponseDTO.ok();
}
/**
* 查询已分配用户角色列表
*/
@Operation(summary = "已关联该角色的用户列表")
@PreAuthorize("@permission.has('system:role:list')")
@GetMapping("/{roleId}/allocated/list")
public ResponseDTO<PageDTO<UserDTO>> allocatedUserList(@PathVariable("roleId") Long roleId,
AllocatedRoleQuery query) {
query.setRoleId(roleId);
PageDTO<UserDTO> page = roleApplicationService.getAllocatedUserList(query);
return ResponseDTO.ok(page);
}
/**
* 查询未分配用户角色列表
*/
@Operation(summary = "未关联该角色的用户列表")
@PreAuthorize("@permission.has('system:role:list')")
@GetMapping("/{roleId}/unallocated/list")
public ResponseDTO<PageDTO<UserDTO>> unallocatedUserList(@PathVariable("roleId") Long roleId,
UnallocatedRoleQuery query) {
query.setRoleId(roleId);
PageDTO<UserDTO> page = roleApplicationService.getUnallocatedUserList(query);
return ResponseDTO.ok(page);
}
/**
* 批量取消授权用户
*/
@Operation(summary = "批量解除角色和用户的关联")
@PreAuthorize("@permission.has('system:role:edit')")
@AccessLog(title = "角色管理", businessType = BusinessTypeEnum.GRANT)
@DeleteMapping("/users/{userIds}/grant/bulk")
public ResponseDTO<Void> deleteRoleOfUserByBulk(@PathVariable("userIds") List<Long> userIds) {
roleApplicationService.deleteRoleOfUserByBulk(userIds);
return ResponseDTO.ok();
}
/**
* 批量选择用户授权
*/
@Operation(summary = "批量添加用户和角色关联")
@PreAuthorize("@permission.has('system:role:edit')")
@AccessLog(title = "角色管理", businessType = BusinessTypeEnum.GRANT)
@PostMapping("/{roleId}/users/{userIds}/grant/bulk")
public ResponseDTO<Void> addRoleForUserByBulk(@PathVariable("roleId") Long roleId,
@PathVariable("userIds") List<Long> userIds) {
roleApplicationService.addRoleOfUserByBulk(roleId, userIds);
return ResponseDTO.ok();
}
}
@@ -0,0 +1,169 @@
package com.agileboot.admin.controller.system;
import cn.hutool.core.collection.ListUtil;
import com.agileboot.common.core.base.BaseController;
import com.agileboot.common.core.dto.ResponseDTO;
import com.agileboot.common.core.page.PageDTO;
import com.agileboot.common.utils.poi.CustomExcelUtil;
import com.agileboot.domain.common.command.BulkOperationCommand;
import com.agileboot.domain.system.user.UserApplicationService;
import com.agileboot.domain.system.user.command.AddUserCommand;
import com.agileboot.domain.system.user.command.ChangeStatusCommand;
import com.agileboot.domain.system.user.command.ResetPasswordCommand;
import com.agileboot.domain.system.user.command.UpdateUserCommand;
import com.agileboot.domain.system.user.dto.UserDTO;
import com.agileboot.domain.system.user.dto.UserDetailDTO;
import com.agileboot.domain.system.user.query.SearchUserQuery;
import com.agileboot.admin.customize.aop.accessLog.AccessLog;
import com.agileboot.infrastructure.user.AuthenticationUtils;
import com.agileboot.infrastructure.user.web.SystemLoginUser;
import com.agileboot.common.enums.common.BusinessTypeEnum;
import com.agileboot.domain.system.user.db.SearchUserDO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
/**
* 用户信息
* @author valarchie
*/
@Tag(name = "用户API", description = "用户相关的增删查改")
@RestController
@RequestMapping("/system/users")
@RequiredArgsConstructor
public class SysUserController extends BaseController {
private final UserApplicationService userApplicationService;
/**
* 获取用户列表
*/
@Operation(summary = "用户列表")
@PreAuthorize("@permission.has('system:user:list') AND @dataScope.checkDeptId(#query.deptId)")
@GetMapping
public ResponseDTO<PageDTO<UserDTO>> userList(SearchUserQuery<SearchUserDO> query) {
PageDTO<UserDTO> page = userApplicationService.getUserList(query);
return ResponseDTO.ok(page);
}
@Operation(summary = "用户列表导出")
@AccessLog(title = "用户管理", businessType = BusinessTypeEnum.EXPORT)
@PreAuthorize("@permission.has('system:user:export')")
@GetMapping("/excel")
public void exportUserByExcel(HttpServletResponse response, SearchUserQuery<SearchUserDO> query) {
PageDTO<UserDTO> userList = userApplicationService.getUserList(query);
CustomExcelUtil.writeToResponse(userList.getRows(), UserDTO.class, response);
}
@Operation(summary = "用户列表导入")
@AccessLog(title = "用户管理", businessType = BusinessTypeEnum.IMPORT)
@PreAuthorize("@permission.has('system:user:import')")
@PostMapping("/excel")
public ResponseDTO<Void> importUserByExcel(MultipartFile file) {
List<AddUserCommand> commands = CustomExcelUtil.readFromRequest(AddUserCommand.class, file);
for (AddUserCommand command : commands) {
userApplicationService.addUser(command);
}
return ResponseDTO.ok();
}
/**
* 下载批量导入模板
*/
@Operation(summary = "用户导入excel下载")
@GetMapping("/excelTemplate")
public void downloadExcelTemplate(HttpServletResponse response) {
CustomExcelUtil.writeToResponse(ListUtil.toList(new AddUserCommand()), AddUserCommand.class, response);
}
/**
* 根据用户编号获取详细信息
*/
@Operation(summary = "用户详情")
@PreAuthorize("@permission.has('system:user:query')")
@GetMapping("/{userId}")
public ResponseDTO<UserDetailDTO> getUserDetailInfo(@PathVariable(value = "userId", required = false) Long userId) {
UserDetailDTO userDetailInfo = userApplicationService.getUserDetailInfo(userId);
return ResponseDTO.ok(userDetailInfo);
}
/**
* 新增用户
*/
@Operation(summary = "新增用户")
@PreAuthorize("@permission.has('system:user:add') AND @dataScope.checkDeptId(#command.deptId)")
@AccessLog(title = "用户管理", businessType = BusinessTypeEnum.ADD)
@PostMapping
public ResponseDTO<Void> add(@Validated @RequestBody AddUserCommand command) {
userApplicationService.addUser(command);
return ResponseDTO.ok();
}
/**
* 修改用户
*/
@Operation(summary = "修改用户")
@PreAuthorize("@permission.has('system:user:edit') AND @dataScope.checkUserId(#command.userId)")
@AccessLog(title = "用户管理", businessType = BusinessTypeEnum.MODIFY)
@PutMapping("/{userId}")
public ResponseDTO<Void> edit(@Validated @RequestBody UpdateUserCommand command) {
userApplicationService.updateUser(command);
return ResponseDTO.ok();
}
/**
* 删除用户
*/
@Operation(summary = "删除用户")
@PreAuthorize("@permission.has('system:user:remove') AND @dataScope.checkUserIds(#userIds)")
@AccessLog(title = "用户管理", businessType = BusinessTypeEnum.DELETE)
@DeleteMapping("/{userIds}")
public ResponseDTO<Void> remove(@PathVariable List<Long> userIds) {
BulkOperationCommand<Long> bulkDeleteCommand = new BulkOperationCommand<>(userIds);
SystemLoginUser loginUser = AuthenticationUtils.getSystemLoginUser();
userApplicationService.deleteUsers(loginUser, bulkDeleteCommand);
return ResponseDTO.ok();
}
/**
* 重置密码
*/
@Operation(summary = "重置用户密码")
@PreAuthorize("@permission.has('system:user:resetPwd') AND @dataScope.checkUserId(#userId)")
@AccessLog(title = "用户管理", businessType = BusinessTypeEnum.MODIFY)
@PutMapping("/{userId}/password")
public ResponseDTO<Void> resetPassword(@PathVariable Long userId, @RequestBody ResetPasswordCommand command) {
command.setUserId(userId);
userApplicationService.resetUserPassword(command);
return ResponseDTO.ok();
}
/**
* 状态修改
*/
@Operation(summary = "修改用户状态")
@PreAuthorize("@permission.has('system:user:edit') AND @dataScope.checkUserId(#command.userId)")
@AccessLog(title = "用户管理", businessType = BusinessTypeEnum.MODIFY)
@PutMapping("/{userId}/status")
public ResponseDTO<Void> changeStatus(@PathVariable Long userId, @RequestBody ChangeStatusCommand command) {
command.setUserId(userId);
userApplicationService.changeUserStatus(command);
return ResponseDTO.ok();
}
}
@@ -0,0 +1,45 @@
package com.agileboot.admin.customize.aop.accessLog;
import com.agileboot.common.enums.common.BusinessTypeEnum;
import com.agileboot.common.enums.common.OperatorTypeEnum;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义操作日志记录注解
*
* @author ruoyi
*/
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AccessLog {
/**
* 模块
*/
String title() default "";
/**
* 功能
*/
BusinessTypeEnum businessType() default BusinessTypeEnum.OTHER;
/**
* 操作人类别
*/
OperatorTypeEnum operatorType() default OperatorTypeEnum.WEB;
/**
* 是否保存请求的参数
*/
boolean isSaveRequestData() default true;
/**
* 是否保存响应的参数
*/
boolean isSaveResponseData() default false;
}
@@ -0,0 +1,59 @@
package com.agileboot.admin.customize.aop.accessLog;
import com.agileboot.admin.customize.async.AsyncTaskFactory;
import com.agileboot.infrastructure.thread.ThreadPoolManager;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/**
* 操作日志记录处理
*
* @author valarchie
*/
@Aspect
@Component
@Slf4j
public class AccessLogAspect {
/**
* 处理完请求后执行
*
* @param joinPoint 切点
*/
@AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, AccessLog controllerLog, Object jsonResult) {
handleLog(joinPoint, controllerLog, null, jsonResult);
}
/**
* 拦截异常操作
*
* @param joinPoint 切点
* @param e 异常
*/
@AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, AccessLog controllerLog, Exception e) {
handleLog(joinPoint, controllerLog, e, null);
}
protected void handleLog(final JoinPoint joinPoint, AccessLog accessLog, final Exception e, Object jsonResult) {
try {
OperationLogModel operationLog = new OperationLogModel();
operationLog.fillOperatorInfo();
operationLog.fillRequestInfo(joinPoint, accessLog, jsonResult);
operationLog.fillStatus(e);
operationLog.fillAccessLogInfo(accessLog);
// 保存数据库
ThreadPoolManager.execute(AsyncTaskFactory.recordOperationLog(operationLog));
} catch (Exception exp) {
log.error("写入操作日式失败", exp);
}
}
}
@@ -0,0 +1,160 @@
package com.agileboot.admin.customize.aop.accessLog;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.EnumUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.json.JSONUtil;
import com.agileboot.common.utils.ServletHolderUtil;
import com.agileboot.infrastructure.user.AuthenticationUtils;
import com.agileboot.infrastructure.user.web.SystemLoginUser;
import com.agileboot.common.enums.common.OperationStatusEnum;
import com.agileboot.common.enums.common.RequestMethodEnum;
import com.agileboot.common.enums.BasicEnumUtil;
import com.agileboot.domain.system.log.db.SysOperationLogEntity;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.HandlerMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Collection;
import java.util.Map;
/**
* @author valarchie
*/
@Slf4j
public class OperationLogModel extends SysOperationLogEntity {
public static final int MAX_DATA_LENGTH = 512;
HttpServletRequest request = ServletHolderUtil.getRequest();
public void fillOperatorInfo() {
// 获取当前的用户
String ip = ServletUtil.getClientIP(request);
setOperatorIp(ip);
SystemLoginUser loginUser = AuthenticationUtils.getSystemLoginUser();
if (loginUser != null) {
this.setUsername(loginUser.getUsername());
}
this.setOperationTime(DateUtil.date());
}
public void fillRequestInfo(final JoinPoint joinPoint, AccessLog accessLog, Object jsonResult) {
this.setRequestUrl(request.getRequestURI());
// 设置方法名称
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
String methodFormat = StrUtil.format("{}.{}()", className, methodName);
this.setCalledMethod(methodFormat);
// 设置请求方式
RequestMethodEnum requestMethodEnum = EnumUtil.fromString(RequestMethodEnum.class,
request.getMethod());
this.setRequestMethod(requestMethodEnum != null ? requestMethodEnum.getValue() : RequestMethodEnum.UNKNOWN.getValue());
// 是否需要保存request,参数和值
if (accessLog.isSaveRequestData()) {
// 获取参数的信息,传入到数据库中。
recordRequestData(joinPoint);
}
// 是否需要保存response,参数和值
if (accessLog.isSaveResponseData() && jsonResult != null) {
this.setOperationResult(StrUtil.sub(JSONUtil.toJsonStr(jsonResult), 0, MAX_DATA_LENGTH));
}
}
public void fillAccessLogInfo(AccessLog log) {
// 设置action动作
this.setBusinessType(log.businessType().ordinal());
// 设置标题
this.setRequestModule(log.title());
// 设置操作人类别
this.setOperatorType(log.operatorType().ordinal());
}
public void fillStatus(Exception e) {
if (e != null) {
this.setStatus(OperationStatusEnum.FAIL.getValue());
this.setErrorStack(StrUtil.sub(e.getMessage(), 0, MAX_DATA_LENGTH));
} else {
this.setStatus(OperationStatusEnum.SUCCESS.getValue());
}
}
/**
* 获取请求的参数,放到log中
*
* @param joinPoint 方法切面
*/
private void recordRequestData(JoinPoint joinPoint) {
RequestMethodEnum requestMethodEnum = BasicEnumUtil.fromValue(RequestMethodEnum.class,
this.getRequestMethod());
if (requestMethodEnum == RequestMethodEnum.GET || requestMethodEnum == RequestMethodEnum.POST) {
String params = argsArrayToString(joinPoint.getArgs());
this.setOperationParam(StrUtil.sub(params, 0, MAX_DATA_LENGTH));
} else {
Map<?, ?> paramsMap = (Map<?, ?>) request
.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
this.setOperationParam(StrUtil.sub(paramsMap.toString(), 0, MAX_DATA_LENGTH));
}
}
/**
* 参数拼装
*/
private String argsArrayToString(Object[] paramsArray) {
StringBuilder params = new StringBuilder();
if (paramsArray != null) {
for (Object o : paramsArray) {
if (o != null && !isCanNotBeParseToJson(o)) {
try {
Object jsonObj = JSONUtil.parseObj(o);
params.append(jsonObj).append(",");
} catch (Exception e) {
log.info("参数拼接错误", e);
}
}
}
}
return params.toString().trim();
}
/**
* 判断是否需要过滤的对象。
*
* @param o 对象信息。
* @return 如果是需要过滤的对象,则返回true;否则返回false。
*/
@SuppressWarnings("rawtypes")
public boolean isCanNotBeParseToJson(final Object o) {
Class<?> clazz = o.getClass();
if (clazz.isArray()) {
return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
} else if (Collection.class.isAssignableFrom(clazz)) {
Collection collection = (Collection) o;
for (Object value : collection) {
return value instanceof MultipartFile;
}
} else if (Map.class.isAssignableFrom(clazz)) {
Map map = (Map) o;
for (Object value : map.entrySet()) {
Map.Entry entry = (Map.Entry) value;
return entry.getValue() instanceof MultipartFile;
}
}
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
|| o instanceof BindingResult;
}
}
@@ -0,0 +1,78 @@
package com.agileboot.admin.customize.async;
import cn.hutool.core.date.DateUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.agileboot.common.utils.ServletHolderUtil;
import com.agileboot.common.utils.ip.IpRegionUtil;
import com.agileboot.common.enums.common.LoginStatusEnum;
import com.agileboot.domain.system.log.db.SysLoginInfoEntity;
import com.agileboot.domain.system.log.db.SysOperationLogEntity;
import com.agileboot.domain.system.log.db.SysLoginInfoService;
import com.agileboot.domain.system.log.db.SysOperationLogService;
import eu.bitwalker.useragentutils.UserAgent;
import lombok.extern.slf4j.Slf4j;
/**
* 异步工厂(产生任务用)
*
* @author ruoyi
*/
@Slf4j
public class AsyncTaskFactory {
private AsyncTaskFactory() {
}
/**
* 记录登录信息
*
* @param username 用户名
* @param loginStatusEnum 状态
* @param message 消息
* @return 任务task
*/
public static Runnable loginInfoTask(final String username, final LoginStatusEnum loginStatusEnum, final String message) {
// 优化一下这个类
final UserAgent userAgent = UserAgent.parseUserAgentString(
ServletHolderUtil.getRequest().getHeader("User-Agent"));
// 获取客户端浏览器
final String browser = userAgent.getBrowser() != null ? userAgent.getBrowser().getName() : "";
final String ip = ServletUtil.getClientIP(ServletHolderUtil.getRequest());
final String address = IpRegionUtil.getBriefLocationByIp(ip);
// 获取客户端操作系统
final String os = userAgent.getOperatingSystem() != null ? userAgent.getOperatingSystem().getName() : "";
log.info("ip: {}, address: {}, username: {}, loginStatusEnum: {}, message: {}", ip, address, username,
loginStatusEnum, message);
return () -> {
// 封装对象
SysLoginInfoEntity loginInfo = new SysLoginInfoEntity();
loginInfo.setUsername(username);
loginInfo.setIpAddress(ip);
loginInfo.setLoginLocation(address);
loginInfo.setBrowser(browser);
loginInfo.setOperationSystem(os);
loginInfo.setMsg(message);
loginInfo.setLoginTime(DateUtil.date());
loginInfo.setStatus(loginStatusEnum.getValue());
// 插入数据
SpringUtil.getBean(SysLoginInfoService.class).save(loginInfo);
};
}
/**
* 操作日志记录
*
* @param operationLog 操作日志信息
* @return 任务task
*/
public static Runnable recordOperationLog(final SysOperationLogEntity operationLog) {
return () -> {
// 远程查询操作地点
operationLog.setOperatorLocation(IpRegionUtil.getBriefLocationByIp(operationLog.getOperatorIp()));
SpringUtil.getBean(SysOperationLogService.class).save(operationLog);
};
}
}
@@ -0,0 +1,53 @@
package com.agileboot.admin.customize.config;
import com.agileboot.infrastructure.user.AuthenticationUtils;
import com.agileboot.infrastructure.user.web.SystemLoginUser;
import com.agileboot.admin.customize.service.login.TokenService;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* token过滤器 验证token有效性
* 继承OncePerRequestFilter类的话 可以确保只执行filter一次, 避免执行多次
* @author valarchie
*/
@Component
@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private final TokenService tokenService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
SystemLoginUser loginUser = tokenService.getLoginUser(request);
if (loginUser != null && AuthenticationUtils.getAuthentication() == null) {
tokenService.refreshToken(loginUser);
// 如果没有将当前登录用户放入到上下文中的话,会认定用户未授权,返回用户未登陆的错误
putCurrentLoginUserIntoContext(request, loginUser);
log.debug("request process in jwt token filter. get login user id: {}", loginUser.getUserId());
}
chain.doFilter(request, response);
}
private void putCurrentLoginUserIntoContext(HttpServletRequest request, SystemLoginUser loginUser) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(loginUser,
null, loginUser.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
@@ -0,0 +1,164 @@
package com.agileboot.admin.customize.config;
import cn.hutool.json.JSONUtil;
import com.agileboot.admin.customize.service.login.LoginService;
import com.agileboot.common.core.dto.ResponseDTO;
import com.agileboot.common.exception.ApiException;
import com.agileboot.common.exception.error.ErrorCode.Client;
import com.agileboot.common.utils.ServletHolderUtil;
import com.agileboot.domain.common.cache.RedisCacheService;
import com.agileboot.admin.customize.async.AsyncTaskFactory;
import com.agileboot.infrastructure.thread.ThreadPoolManager;
import com.agileboot.infrastructure.user.web.SystemLoginUser;
import com.agileboot.admin.customize.service.login.TokenService;
import com.agileboot.common.enums.common.LoginStatusEnum;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.web.filter.CorsFilter;
/**
* 主要配置登录流程逻辑涉及以下几个类
* @see UserDetailsServiceImpl#loadUserByUsername 用于登录流程通过用户名加载用户
* @see this#unauthorizedHandler() 用于用户未授权或登录失败处理
* @see this#logOutSuccessHandler 用于退出登录成功后的逻辑
* @see JwtAuthenticationTokenFilter#doFilter token的校验和刷新
* @see LoginService#login 登录逻辑
* @author valarchie
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig {
private final TokenService tokenService;
private final RedisCacheService redisCache;
/**
* token认证过滤器
*/
private final JwtAuthenticationTokenFilter jwtTokenFilter;
private final UserDetailsService userDetailsService;
/**
* 跨域过滤器
*/
private final CorsFilter corsFilter;
/**
* 登录异常处理类
* 用户未登陆的话 在这个Bean中处理
*/
@Bean
public AuthenticationEntryPoint unauthorizedHandler() {
return (request, response, exception) -> {
ResponseDTO<Object> responseDTO = ResponseDTO.fail(
new ApiException(Client.COMMON_NO_AUTHORIZATION, request.getRequestURI())
);
ServletHolderUtil.renderString(response, JSONUtil.toJsonStr(responseDTO));
};
}
/**
* 退出成功处理类 返回成功
* 在SecurityConfig类当中 定义了/logout 路径对应处理逻辑
*/
@Bean
public LogoutSuccessHandler logOutSuccessHandler() {
return (request, response, authentication) -> {
SystemLoginUser loginUser = tokenService.getLoginUser(request);
if (loginUser != null) {
String userName = loginUser.getUsername();
// 删除用户缓存记录
redisCache.loginUserCache.delete(loginUser.getCachedKey());
// 记录用户退出日志
ThreadPoolManager.execute(AsyncTaskFactory.loginInfoTask(
userName, LoginStatusEnum.LOGOUT, LoginStatusEnum.LOGOUT.description()));
}
ServletHolderUtil.renderString(response, JSONUtil.toJsonStr(ResponseDTO.ok()));
};
}
/**
* 强散列哈希加密实现
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 鉴权管理类
* @see UserDetailsServiceImpl#loadUserByUsername
*/
@Bean
public AuthenticationManager authManager(HttpSecurity http) throws Exception {
return http.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(userDetailsService)
.passwordEncoder(bCryptPasswordEncoder())
.and()
.build();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// CSRF禁用,因为不使用session
.csrf().disable()
// 认证失败处理类
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler()).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests()
// 对于登录login 注册register 验证码captchaImage 以及公共Api的请求允许匿名访问
// 注意: 当携带token请求以下这几个接口时 会返回403的错误
.antMatchers("/login", "/register", "/getConfig", "/captchaImage", "/api/**").anonymous()
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js",
"/profile/**").permitAll()
// TODO this is danger.
.antMatchers("/swagger-ui.html").anonymous()
.antMatchers("/swagger-resources/**").anonymous()
.antMatchers("/webjars/**").anonymous()
.antMatchers("/*/api-docs","/*/api-docs/swagger-config").anonymous()
.antMatchers("/**/api-docs.yaml" ).anonymous()
.antMatchers("/druid/**").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
.and()
// 禁用 X-Frame-Options 响应头。下面是具体解释:
// X-Frame-Options 是一个 HTTP 响应头,用于防止网页被嵌入到其他网页的 <frame>、<iframe> 或 <object> 标签中,从而可以减少点击劫持攻击的风险
.headers().frameOptions().disable();
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logOutSuccessHandler());
// 添加JWT filter 需要一开始就通过token识别出登录用户 并放到上下文中 所以jwtFilter需要放前面
httpSecurity.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 添加CORS filter
httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
return httpSecurity.build();
}
}
@@ -0,0 +1,222 @@
package com.agileboot.admin.customize.service.login;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.img.ImgUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.extra.servlet.ServletUtil;
import com.agileboot.common.config.AgileBootConfig;
import com.agileboot.common.constant.Constants.Captcha;
import com.agileboot.common.exception.ApiException;
import com.agileboot.common.exception.error.ErrorCode;
import com.agileboot.common.exception.error.ErrorCode.Business;
import com.agileboot.common.utils.ServletHolderUtil;
import com.agileboot.common.utils.i18n.MessageUtils;
import com.agileboot.domain.common.cache.GuavaCacheService;
import com.agileboot.domain.common.cache.MapCache;
import com.agileboot.domain.common.cache.RedisCacheService;
import com.agileboot.admin.customize.async.AsyncTaskFactory;
import com.agileboot.infrastructure.thread.ThreadPoolManager;
import com.agileboot.admin.customize.service.login.dto.CaptchaDTO;
import com.agileboot.admin.customize.service.login.dto.ConfigDTO;
import com.agileboot.admin.customize.service.login.command.LoginCommand;
import com.agileboot.infrastructure.user.web.SystemLoginUser;
import com.agileboot.common.enums.common.ConfigKeyEnum;
import com.agileboot.common.enums.common.LoginStatusEnum;
import com.agileboot.domain.system.user.db.SysUserEntity;
import com.google.code.kaptcha.Producer;
import java.awt.image.BufferedImage;
import javax.annotation.Resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.FastByteArrayOutputStream;
/**
* 登录校验方法
*
* @author ruoyi
*/
@Component
@Slf4j
@RequiredArgsConstructor
public class LoginService {
private final TokenService tokenService;
private final RedisCacheService redisCache;
private final GuavaCacheService guavaCache;
private final AuthenticationManager authenticationManager;
@Resource(name = "captchaProducer")
private Producer captchaProducer;
@Resource(name = "captchaProducerMath")
private Producer captchaProducerMath;
/**
* 登录验证
*
* @param loginCommand 登录参数
* @return 结果
*/
public String login(LoginCommand loginCommand) {
// 验证码开关
if (isCaptchaOn()) {
validateCaptcha(loginCommand.getUsername(), loginCommand.getCaptchaCode(), loginCommand.getCaptchaCodeKey());
}
// 用户验证
Authentication authentication;
String decryptPassword = decryptPassword(loginCommand.getPassword());
try {
// 该方法会去调用UserDetailsServiceImpl#loadUserByUsername 校验用户名和密码 认证鉴权
authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
loginCommand.getUsername(), decryptPassword));
} catch (BadCredentialsException e) {
ThreadPoolManager.execute(AsyncTaskFactory.loginInfoTask(loginCommand.getUsername(), LoginStatusEnum.LOGIN_FAIL,
MessageUtils.message("Business.LOGIN_WRONG_USER_PASSWORD")));
throw new ApiException(e, ErrorCode.Business.LOGIN_WRONG_USER_PASSWORD);
} catch (AuthenticationException e) {
ThreadPoolManager.execute(AsyncTaskFactory.loginInfoTask(loginCommand.getUsername(), LoginStatusEnum.LOGIN_FAIL, e.getMessage()));
throw new ApiException(e, ErrorCode.Business.LOGIN_ERROR, e.getMessage());
} catch (Exception e) {
ThreadPoolManager.execute(AsyncTaskFactory.loginInfoTask(loginCommand.getUsername(), LoginStatusEnum.LOGIN_FAIL, e.getMessage()));
throw new ApiException(e, Business.LOGIN_ERROR, e.getMessage());
}
// 把当前登录用户 放入上下文中
SecurityContextHolder.getContext().setAuthentication(authentication);
// 这里获取的loginUser是UserDetailsServiceImpl#loadUserByUsername方法返回的LoginUser
SystemLoginUser loginUser = (SystemLoginUser) authentication.getPrincipal();
recordLoginInfo(loginUser);
// 生成token
return tokenService.createTokenAndPutUserInCache(loginUser);
}
/**
* 获取验证码 data
*
* @return {@link ConfigDTO}
*/
public ConfigDTO getConfig() {
ConfigDTO configDTO = new ConfigDTO();
boolean isCaptchaOn = isCaptchaOn();
configDTO.setIsCaptchaOn(isCaptchaOn);
configDTO.setDictionary(MapCache.dictionaryCache());
return configDTO;
}
/**
* 获取验证码 data
*
* @return 验证码
*/
public CaptchaDTO generateCaptchaImg() {
CaptchaDTO captchaDTO = new CaptchaDTO();
boolean isCaptchaOn = isCaptchaOn();
captchaDTO.setIsCaptchaOn(isCaptchaOn);
if (isCaptchaOn) {
String expression;
String answer = null;
BufferedImage image = null;
// 生成验证码
String captchaType = AgileBootConfig.getCaptchaType();
if (Captcha.MATH_TYPE.equals(captchaType)) {
String capText = captchaProducerMath.createText();
String[] expressionAndAnswer = capText.split("@");
expression = expressionAndAnswer[0];
answer = expressionAndAnswer[1];
image = captchaProducerMath.createImage(expression);
}
if (Captcha.CHAR_TYPE.equals(captchaType)) {
expression = answer = captchaProducer.createText();
image = captchaProducer.createImage(expression);
}
if (image == null) {
throw new ApiException(ErrorCode.Internal.LOGIN_CAPTCHA_GENERATE_FAIL);
}
// 保存验证码信息
String imgKey = IdUtil.simpleUUID();
redisCache.captchaCache.set(imgKey, answer);
// 转换流信息写出
FastByteArrayOutputStream os = new FastByteArrayOutputStream();
ImgUtil.writeJpg(image, os);
captchaDTO.setCaptchaCodeKey(imgKey);
captchaDTO.setCaptchaCodeImg(Base64.encode(os.toByteArray()));
}
return captchaDTO;
}
/**
* 校验验证码
*
* @param username 用户名
* @param captchaCode 验证码
* @param captchaCodeKey 验证码对应的缓存key
*/
public void validateCaptcha(String username, String captchaCode, String captchaCodeKey) {
String captcha = redisCache.captchaCache.getObjectById(captchaCodeKey);
redisCache.captchaCache.delete(captchaCodeKey);
if (captcha == null) {
ThreadPoolManager.execute(AsyncTaskFactory.loginInfoTask(username, LoginStatusEnum.LOGIN_FAIL,
ErrorCode.Business.LOGIN_CAPTCHA_CODE_EXPIRE.message()));
throw new ApiException(ErrorCode.Business.LOGIN_CAPTCHA_CODE_EXPIRE);
}
if (!captchaCode.equalsIgnoreCase(captcha)) {
ThreadPoolManager.execute(AsyncTaskFactory.loginInfoTask(username, LoginStatusEnum.LOGIN_FAIL,
ErrorCode.Business.LOGIN_CAPTCHA_CODE_WRONG.message()));
throw new ApiException(ErrorCode.Business.LOGIN_CAPTCHA_CODE_WRONG);
}
}
/**
* 记录登录信息
* @param loginUser 登录用户
*/
public void recordLoginInfo(SystemLoginUser loginUser) {
ThreadPoolManager.execute(AsyncTaskFactory.loginInfoTask(loginUser.getUsername(), LoginStatusEnum.LOGIN_SUCCESS,
LoginStatusEnum.LOGIN_SUCCESS.description()));
SysUserEntity entity = redisCache.userCache.getObjectById(loginUser.getUserId());
entity.setLoginIp(ServletUtil.getClientIP(ServletHolderUtil.getRequest()));
entity.setLoginDate(DateUtil.date());
entity.updateById();
}
public String decryptPassword(String originalPassword) {
byte[] decryptBytes = SecureUtil.rsa(AgileBootConfig.getRsaPrivateKey(), null)
.decrypt(Base64.decode(originalPassword), KeyType.PrivateKey);
return StrUtil.str(decryptBytes, CharsetUtil.CHARSET_UTF_8);
}
private boolean isCaptchaOn() {
return Convert.toBool(guavaCache.configCache.get(ConfigKeyEnum.CAPTCHA.getValue()));
}
}
@@ -0,0 +1,163 @@
package com.agileboot.admin.customize.service.login;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.agileboot.common.constant.Constants.Token;
import com.agileboot.common.exception.ApiException;
import com.agileboot.common.exception.error.ErrorCode;
import com.agileboot.domain.common.cache.RedisCacheService;
import com.agileboot.infrastructure.user.web.SystemLoginUser;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* token验证处理
*
* @author valarchie
*/
@Component
@Slf4j
@Data
@RequiredArgsConstructor
public class TokenService {
/**
* 自定义令牌标识
*/
@Value("${token.header}")
private String header;
/**
* 令牌秘钥
*/
@Value("${token.secret}")
private String secret;
/**
* 自动刷新token的时间,当过期时间不足autoRefreshTime的值的时候,会触发刷新用户登录缓存的时间
* 比如这个值是20, 用户是8点登录的, 8点半缓存会过期, 当过8.10分的时候,就少于20分钟了,便触发
* 刷新登录用户的缓存时间
*/
@Value("${token.autoRefreshTime}")
private long autoRefreshTime;
private final RedisCacheService redisCache;
/**
* 获取用户身份信息
*
* @return 用户信息
*/
public SystemLoginUser getLoginUser(HttpServletRequest request) {
// 获取请求携带的令牌
String token = getTokenFromRequest(request);
if (StrUtil.isNotEmpty(token)) {
try {
Claims claims = parseToken(token);
// 解析对应的权限以及用户信息
String uuid = (String) claims.get(Token.LOGIN_USER_KEY);
return redisCache.loginUserCache.getObjectOnlyInCacheById(uuid);
} catch (SignatureException | MalformedJwtException | UnsupportedJwtException | IllegalArgumentException jwtException) {
log.error("parse token failed.", jwtException);
throw new ApiException(jwtException, ErrorCode.Client.INVALID_TOKEN);
} catch (Exception e) {
log.error("fail to get cached user from redis", e);
throw new ApiException(e, ErrorCode.Client.TOKEN_PROCESS_FAILED, e.getMessage());
}
}
return null;
}
/**
* 创建令牌
*
* @param loginUser 用户信息
* @return 令牌
*/
public String createTokenAndPutUserInCache(SystemLoginUser loginUser) {
loginUser.setCachedKey(IdUtil.fastUUID());
redisCache.loginUserCache.set(loginUser.getCachedKey(), loginUser);
return generateToken(MapUtil.of(Token.LOGIN_USER_KEY, loginUser.getCachedKey()));
}
/**
* 当超过20分钟,自动刷新token
* @param loginUser 登录用户
*/
public void refreshToken(SystemLoginUser loginUser) {
long currentTime = System.currentTimeMillis();
if (currentTime > loginUser.getAutoRefreshCacheTime()) {
loginUser.setAutoRefreshCacheTime(currentTime + TimeUnit.MINUTES.toMillis(autoRefreshTime));
// 根据uuid将loginUser存入缓存
redisCache.loginUserCache.set(loginUser.getCachedKey(), loginUser);
}
}
/**
* 从数据声明生成令牌
*
* @param claims 数据声明
* @return 令牌
*/
private String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret).compact();
}
/**
* 从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/
private Claims parseToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
/**
* 从令牌中获取用户名
*
* @param token 令牌
* @return 用户名
*/
private String getUsernameFromToken(String token) {
Claims claims = parseToken(token);
return claims.getSubject();
}
/**
* 获取请求token
*
* @return token
*/
private String getTokenFromRequest(HttpServletRequest request) {
String token = request.getHeader(header);
if (StrUtil.isNotEmpty(token) && token.startsWith(Token.PREFIX)) {
token = StrUtil.stripIgnoreCase(token, Token.PREFIX, null);
}
return token;
}
}
@@ -0,0 +1,117 @@
package com.agileboot.admin.customize.service.login;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import com.agileboot.common.exception.ApiException;
import com.agileboot.common.exception.error.ErrorCode;
import com.agileboot.infrastructure.user.web.SystemLoginUser;
import com.agileboot.infrastructure.user.web.RoleInfo;
import com.agileboot.infrastructure.user.web.DataScopeEnum;
import com.agileboot.common.enums.common.UserStatusEnum;
import com.agileboot.common.enums.BasicEnumUtil;
import com.agileboot.domain.system.menu.db.SysMenuEntity;
import com.agileboot.domain.system.role.db.SysRoleEntity;
import com.agileboot.domain.system.user.db.SysUserEntity;
import com.agileboot.domain.system.menu.db.SysMenuService;
import com.agileboot.domain.system.role.db.SysRoleService;
import com.agileboot.domain.system.user.db.SysUserService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.SetUtils;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
/**
* 自定义加载用户信息通过用户名
* 用于SpringSecurity 登录流程
* 没有办法把这个类 放进loginService中 会在SecurityConfig中造成循环依赖
* @see com.agileboot.infrastructure.config.SecurityConfig#filterChain(HttpSecurity)
* @author valarchie
*/
@Service
@Slf4j
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final SysUserService userService;
private final SysMenuService menuService;
private final SysRoleService roleService;
private final TokenService tokenService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUserEntity userEntity = userService.getUserByUserName(username);
if (userEntity == null) {
log.info("登录用户:{} 不存在.", username);
throw new ApiException(ErrorCode.Business.USER_NON_EXIST, username);
}
if (!Objects.equals(UserStatusEnum.NORMAL.getValue(), userEntity.getStatus())) {
log.info("登录用户:{} 已被停用.", username);
throw new ApiException(ErrorCode.Business.USER_IS_DISABLE, username);
}
RoleInfo roleInfo = getRoleInfo(userEntity.getRoleId(), userEntity.getIsAdmin());
SystemLoginUser loginUser = new SystemLoginUser(userEntity.getUserId(), userEntity.getIsAdmin(), userEntity.getUsername(),
userEntity.getPassword(), roleInfo, userEntity.getDeptId());
loginUser.fillLoginInfo();
loginUser.setAutoRefreshCacheTime(loginUser.getLoginInfo().getLoginTime()
+ TimeUnit.MINUTES.toMillis(tokenService.getAutoRefreshTime()));
return loginUser;
}
public RoleInfo getRoleInfo(Long roleId, boolean isAdmin) {
if (roleId == null) {
return RoleInfo.EMPTY_ROLE;
}
if (isAdmin) {
LambdaQueryWrapper<SysMenuEntity> menuQuery = Wrappers.lambdaQuery();
menuQuery.select(SysMenuEntity::getMenuId);
List<SysMenuEntity> allMenus = menuService.list(menuQuery);
Set<Long> allMenuIds = allMenus.stream().map(SysMenuEntity::getMenuId).collect(Collectors.toSet());
return new RoleInfo(RoleInfo.ADMIN_ROLE_ID, RoleInfo.ADMIN_ROLE_KEY, DataScopeEnum.ALL, SetUtils.emptySet(),
RoleInfo.ADMIN_PERMISSIONS, allMenuIds);
}
SysRoleEntity roleEntity = roleService.getById(roleId);
if (roleEntity == null) {
return RoleInfo.EMPTY_ROLE;
}
List<SysMenuEntity> menuList = roleService.getMenuListByRoleId(roleId);
Set<Long> menuIds = menuList.stream().map(SysMenuEntity::getMenuId).collect(Collectors.toSet());
Set<String> permissions = menuList.stream().map(SysMenuEntity::getPermission).collect(Collectors.toSet());
DataScopeEnum dataScopeEnum = BasicEnumUtil.fromValue(DataScopeEnum.class, roleEntity.getDataScope());
Set<Long> deptIdSet = SetUtils.emptySet();
if (StrUtil.isNotEmpty(roleEntity.getDeptIdSet())) {
deptIdSet = StrUtil.split(roleEntity.getDeptIdSet(), ",").stream()
.map(Convert::toLong).collect(Collectors.toSet());
}
return new RoleInfo(roleId, roleEntity.getRoleKey(), dataScopeEnum, deptIdSet, permissions, menuIds);
}
}
@@ -0,0 +1,33 @@
package com.agileboot.admin.customize.service.login.command;
import lombok.Data;
/**
* 用户登录对象
*
* @author valarchie
*/
@Data
public class LoginCommand {
/**
* 用户名
*/
private String username;
/**
* 用户密码
*/
private String password;
/**
* 验证码
*/
private String captchaCode;
/**
* 唯一标识
*/
private String captchaCodeKey;
}
@@ -0,0 +1,15 @@
package com.agileboot.admin.customize.service.login.dto;
import lombok.Data;
/**
* @author valarchie
*/
@Data
public class CaptchaDTO {
private Boolean isCaptchaOn;
private String captchaCodeKey;
private String captchaCodeImg;
}
@@ -0,0 +1,18 @@
package com.agileboot.admin.customize.service.login.dto;
import com.agileboot.common.enums.dictionary.DictionaryData;
import java.util.List;
import java.util.Map;
import lombok.Data;
/**
* @author valarchie
*/
@Data
public class ConfigDTO {
private Boolean isCaptchaOn;
private Map<String, List<DictionaryData>> dictionary;
}
@@ -0,0 +1,66 @@
package com.agileboot.admin.customize.service.permission;
import cn.hutool.extra.spring.SpringUtil;
import com.agileboot.admin.customize.service.permission.model.AbstractDataPermissionChecker;
import com.agileboot.infrastructure.user.web.SystemLoginUser;
import com.agileboot.admin.customize.service.permission.model.checker.AllDataPermissionChecker;
import com.agileboot.admin.customize.service.permission.model.checker.CustomDataPermissionChecker;
import com.agileboot.admin.customize.service.permission.model.checker.DefaultDataPermissionChecker;
import com.agileboot.admin.customize.service.permission.model.checker.DeptTreeDataPermissionChecker;
import com.agileboot.admin.customize.service.permission.model.checker.OnlySelfDataPermissionChecker;
import com.agileboot.admin.customize.service.permission.model.checker.SingleDeptDataPermissionChecker;
import com.agileboot.infrastructure.user.web.DataScopeEnum;
import com.agileboot.domain.system.dept.db.SysDeptService;
import javax.annotation.PostConstruct;
import org.springframework.stereotype.Component;
/**
* 数据权限检测器工厂
* @author valarchie
*/
@Component
public class DataPermissionCheckerFactory {
private static AbstractDataPermissionChecker allChecker;
private static AbstractDataPermissionChecker customChecker;
private static AbstractDataPermissionChecker singleDeptChecker;
private static AbstractDataPermissionChecker deptTreeChecker;
private static AbstractDataPermissionChecker onlySelfChecker;
private static AbstractDataPermissionChecker defaultSelfChecker;
@PostConstruct
public void initAllChecker() {
SysDeptService deptService = SpringUtil.getBean(SysDeptService.class);
allChecker = new AllDataPermissionChecker();
customChecker = new CustomDataPermissionChecker(deptService);
singleDeptChecker = new SingleDeptDataPermissionChecker(deptService);
deptTreeChecker = new DeptTreeDataPermissionChecker(deptService);
onlySelfChecker = new OnlySelfDataPermissionChecker(deptService);
defaultSelfChecker = new DefaultDataPermissionChecker();
}
public static AbstractDataPermissionChecker getChecker(SystemLoginUser loginUser) {
if (loginUser == null) {
return deptTreeChecker;
}
DataScopeEnum dataScope = loginUser.getRoleInfo().getDataScope();
switch (dataScope) {
case ALL:
return allChecker;
case CUSTOM_DEFINE:
return customChecker;
case SINGLE_DEPT:
return singleDeptChecker;
case DEPT_TREE:
return deptTreeChecker;
case ONLY_SELF:
return onlySelfChecker;
default:
return defaultSelfChecker;
}
}
}
@@ -0,0 +1,70 @@
package com.agileboot.admin.customize.service.permission;
import cn.hutool.core.collection.CollUtil;
import com.agileboot.admin.customize.service.permission.model.AbstractDataPermissionChecker;
import com.agileboot.admin.customize.service.permission.model.DataCondition;
import com.agileboot.infrastructure.user.AuthenticationUtils;
import com.agileboot.infrastructure.user.web.SystemLoginUser;
import com.agileboot.domain.system.user.db.SysUserEntity;
import com.agileboot.domain.system.user.db.SysUserService;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* 数据权限校验服务
* @author valarchie
*/
@Service("dataScope")
@RequiredArgsConstructor
public class DataPermissionService {
private final SysUserService userService;
/**
* 通过userId 校验当前用户 对 目标用户是否有操作权限
*
* @param userId 用户id
* @return 检验结果
*/
public boolean checkUserId(Long userId) {
SystemLoginUser loginUser = AuthenticationUtils.getSystemLoginUser();
SysUserEntity targetUser = userService.getById(userId);
if (targetUser == null) {
return true;
}
return checkDataScope(loginUser, targetUser.getDeptId(), userId);
}
/**
* 通过userId 校验当前用户 对 目标用户是否有操作权限
* @param userIds 用户id列表
* @return 校验结果
*/
public boolean checkUserIds(List<Long> userIds) {
if (CollUtil.isNotEmpty(userIds)) {
for (Long userId : userIds) {
boolean checkResult = checkUserId(userId);
if (!checkResult) {
return false;
}
}
}
return true;
}
public boolean checkDeptId(Long deptId) {
SystemLoginUser loginUser = AuthenticationUtils.getSystemLoginUser();
return checkDataScope(loginUser, deptId, null);
}
public boolean checkDataScope(SystemLoginUser loginUser, Long targetDeptId, Long targetUserId) {
DataCondition dataCondition = DataCondition.builder().targetDeptId(targetDeptId).targetUserId(targetUserId).build();
AbstractDataPermissionChecker checker = DataPermissionCheckerFactory.getChecker(loginUser);
return checker.check(loginUser, dataCondition);
}
}
@@ -0,0 +1,48 @@
package com.agileboot.admin.customize.service.permission;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.agileboot.infrastructure.user.AuthenticationUtils;
import com.agileboot.infrastructure.user.web.SystemLoginUser;
import com.agileboot.infrastructure.user.web.RoleInfo;
import java.util.Set;
import org.springframework.stereotype.Service;
/**
*
* @author valarchie
*/
@Service("permission")
public class MenuPermissionService {
/**
* 验证用户是否具备某权限
*
* @param permission 权限字符串
* @return 用户是否具备某权限
*/
public boolean has(String permission) {
if (StrUtil.isEmpty(permission)) {
return false;
}
SystemLoginUser loginUser = AuthenticationUtils.getSystemLoginUser();
if (loginUser == null || CollUtil.isEmpty(loginUser.getRoleInfo().getMenuPermissions())) {
return false;
}
return has(loginUser.getRoleInfo().getMenuPermissions(), permission);
}
/**
* 判断是否包含权限
*
* @param permissions 权限列表
* @param permission 权限字符串
* @return 用户是否具备某权限
*/
private boolean has(Set<String> permissions, String permission) {
return permissions.contains(RoleInfo.ALL_PERMISSIONS) || permissions.contains(StrUtil.trim(permission));
}
}
@@ -0,0 +1,25 @@
package com.agileboot.admin.customize.service.permission.model;
import com.agileboot.infrastructure.user.web.SystemLoginUser;
import com.agileboot.domain.system.dept.db.SysDeptService;
import lombok.Data;
/**
* 数据权限测试接口
* @author valarchie
*/
@Data
public abstract class AbstractDataPermissionChecker {
private SysDeptService deptService;
/**
* 检测当前用户对于 给定条件的数据 是否有权限
*
* @param loginUser 登录用户
* @param condition 条件
* @return 校验结果
*/
public abstract boolean check(SystemLoginUser loginUser, DataCondition condition);
}
@@ -0,0 +1,21 @@
package com.agileboot.admin.customize.service.permission.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author valarchie
* 供 DataPermissionChecker使用的 数据条件
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DataCondition {
private Long targetDeptId;
private Long targetUserId;
}
@@ -0,0 +1,25 @@
package com.agileboot.admin.customize.service.permission.model.checker;
import com.agileboot.infrastructure.user.web.SystemLoginUser;
import com.agileboot.admin.customize.service.permission.model.AbstractDataPermissionChecker;
import com.agileboot.admin.customize.service.permission.model.DataCondition;
import com.agileboot.domain.system.dept.db.SysDeptService;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 数据权限测试接口
* @author valarchie
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class AllDataPermissionChecker extends AbstractDataPermissionChecker {
private SysDeptService deptService;
@Override
public boolean check(SystemLoginUser loginUser, DataCondition condition) {
return true;
}
}
@@ -0,0 +1,42 @@
package com.agileboot.admin.customize.service.permission.model.checker;
import cn.hutool.core.collection.CollUtil;
import com.agileboot.infrastructure.user.web.SystemLoginUser;
import com.agileboot.admin.customize.service.permission.model.AbstractDataPermissionChecker;
import com.agileboot.admin.customize.service.permission.model.DataCondition;
import com.agileboot.domain.system.dept.db.SysDeptService;
import java.util.Set;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* 数据权限测试接口
* @author valarchie
*/
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CustomDataPermissionChecker extends AbstractDataPermissionChecker {
private SysDeptService deptService;
@Override
public boolean check(SystemLoginUser loginUser, DataCondition condition) {
if (condition == null || loginUser == null) {
return false;
}
if (loginUser.getRoleInfo() == null) {
return false;
}
Set<Long> deptIdSet = loginUser.getRoleInfo().getDeptIdSet();
Long targetDeptId = condition.getTargetDeptId();
return condition.getTargetDeptId() != null && CollUtil.safeContains(deptIdSet, targetDeptId);
}
}
@@ -0,0 +1,25 @@
package com.agileboot.admin.customize.service.permission.model.checker;
import com.agileboot.infrastructure.user.web.SystemLoginUser;
import com.agileboot.admin.customize.service.permission.model.AbstractDataPermissionChecker;
import com.agileboot.admin.customize.service.permission.model.DataCondition;
import com.agileboot.domain.system.dept.db.SysDeptService;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 数据权限测试接口
* @author valarchie
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class DefaultDataPermissionChecker extends AbstractDataPermissionChecker {
private SysDeptService deptService;
@Override
public boolean check(SystemLoginUser loginUser, DataCondition condition) {
return false;
}
}
@@ -0,0 +1,44 @@
package com.agileboot.admin.customize.service.permission.model.checker;
import com.agileboot.infrastructure.user.web.SystemLoginUser;
import com.agileboot.admin.customize.service.permission.model.AbstractDataPermissionChecker;
import com.agileboot.admin.customize.service.permission.model.DataCondition;
import com.agileboot.domain.system.dept.db.SysDeptService;
import java.util.Objects;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* 数据权限测试接口
* @author valarchie
*/
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DeptTreeDataPermissionChecker extends AbstractDataPermissionChecker {
private SysDeptService deptService;
@Override
public boolean check(SystemLoginUser loginUser, DataCondition condition) {
if (condition == null || loginUser == null) {
return false;
}
if (loginUser.getDeptId() == null || condition.getTargetDeptId() == null) {
return false;
}
Long currentDeptId = loginUser.getDeptId();
Long targetDeptId = condition.getTargetDeptId();
boolean isContainsTargetDept = deptService.isChildOfTheDept(loginUser.getDeptId(), targetDeptId);
boolean isSameDept = Objects.equals(currentDeptId, targetDeptId);
return isContainsTargetDept || isSameDept;
}
}
@@ -0,0 +1,40 @@
package com.agileboot.admin.customize.service.permission.model.checker;
import com.agileboot.infrastructure.user.web.SystemLoginUser;
import com.agileboot.admin.customize.service.permission.model.AbstractDataPermissionChecker;
import com.agileboot.admin.customize.service.permission.model.DataCondition;
import com.agileboot.domain.system.dept.db.SysDeptService;
import java.util.Objects;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* 数据权限测试接口
* @author valarchie
*/
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OnlySelfDataPermissionChecker extends AbstractDataPermissionChecker {
private SysDeptService deptService;
@Override
public boolean check(SystemLoginUser loginUser, DataCondition condition) {
if (condition == null || loginUser == null) {
return false;
}
if (loginUser.getUserId() == null || condition.getTargetUserId() == null) {
return false;
}
Long currentUserId = loginUser.getUserId();
Long targetUserId = condition.getTargetUserId();
return Objects.equals(currentUserId, targetUserId);
}
}
@@ -0,0 +1,42 @@
package com.agileboot.admin.customize.service.permission.model.checker;
import com.agileboot.infrastructure.user.web.SystemLoginUser;
import com.agileboot.admin.customize.service.permission.model.AbstractDataPermissionChecker;
import com.agileboot.admin.customize.service.permission.model.DataCondition;
import com.agileboot.domain.system.dept.db.SysDeptService;
import java.util.Objects;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* 数据权限测试接口
* @author valarchie
*/
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SingleDeptDataPermissionChecker extends AbstractDataPermissionChecker {
private SysDeptService deptService;
@Override
public boolean check(SystemLoginUser loginUser, DataCondition condition) {
if (condition == null || loginUser == null) {
return false;
}
if (loginUser.getDeptId() == null || condition.getTargetDeptId() == null) {
return false;
}
Long currentDeptId = loginUser.getDeptId();
Long targetDeptId = condition.getTargetDeptId();
return Objects.equals(currentDeptId, targetDeptId);
}
}
@@ -0,0 +1,105 @@
# 数据源配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
druid:
webStatFilter:
enabled: true
statViewServlet:
enabled: true
# 设置白名单,不填则允许所有访问
allow:
url-pattern: /druid/*
# 控制台管理用户名和密码
login-username: agileboot
login-password: 123456
filter:
stat:
enabled: true
# 慢SQL记录
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: true
wall:
config:
multi-statement-allow: true
dynamic:
primary: master
strict: false
druid:
# 初始连接数
initialSize: 5
# 最小连接池数量
minIdle: 10
# 最大连接池数量
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
# 配置一个连接在池中最大生存的时间,单位是毫秒
maxEvictableIdleTimeMillis: 900000
# 配置检测连接是否有效
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
datasource:
# 主库数据源
master:
url: jdbc:mysql://localhost:3306/agileboot_pure?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: root123
# 从库数据源
# slave:
# url: jdbc:mysql://localhost:3306/agileboot2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
# username: root
# password: root123
# redis 配置
redis:
# 地址
host: localhost
# 端口,默认为6379
port: 6379
# 数据库索引
database: 0
# 密码
password: redis123
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 8
# 连接池的最大数据库连接数
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
logging:
file:
path: D:/logs/agileboot-dev
springdoc:
swagger-ui:
# ***注意*** 开启Swagger UI界面 **安全考虑的话生产环境需要关掉**
# 因为knife4j的一些配置不灵活 所以重新改回springdoc+swagger的组合 真实开发的时候 使用apifox这种工具效率更高
enabled: true
url: ${agileboot.api-prefix}/v3/api-docs
config-url: ${agileboot.api-prefix}/v3/api-docs/swagger-config
# 项目相关配置
agileboot:
# 文件基路径 示例( Windows配置D:\agilebootLinux配置 /home/agileboot
file-base-dir: D:\agileboot
# 前端url请求转发前缀
api-prefix: /dev-api
demo-enabled: false
@@ -0,0 +1,53 @@
# 数据源配置
spring:
datasource:
# 驱动
driver-class-name: org.h2.Driver
dynamic:
primary: master
strict: false
datasource:
master:
# h2 内存数据库 内存模式连接配置 库名: agileboot
url: jdbc:h2:mem:agileboot;DB_CLOSE_DELAY=-1;MODE=MySQL
h2:
# 开启console 访问 默认false
console:
enabled: true
settings:
# 开启h2 console 跟踪 方便调试 默认 false
trace: true
# 允许console 远程访问 默认false
web-allow-others: true
# h2 访问路径上下文
path: /h2-console
sql:
init:
platform: mysql
# 初始化数据
schema-locations: classpath:h2sql/agileboot_schema.sql
data-locations: classpath:h2sql/agileboot_data.sql
# redis 配置
redis:
# 地址
host: localhost
# 端口,默认为6379
port: 36379
# 数据库索引
database: 0
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 8
# 连接池的最大数据库连接数
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
@@ -0,0 +1,46 @@
# 开发环境配置
server:
# 服务器的HTTP端口,默认为8080
port: 8080
servlet:
# 应用的访问路径
context-path: /
tomcat:
# tomcat的URI编码
uri-encoding: UTF-8
# 连接数满后的排队数,默认为100
accept-count: 1000
threads:
# tomcat最大线程数,默认为200
max: 800
# Tomcat启动初始化的线程数,默认值10
min-spare: 100
# Spring配置 如果需要无Mysql 无Redis直接启动的话 dev改为test
# 生产环境把dev改为prod
spring:
profiles:
active: basic,dev
# 如果需要无Mysql 无Redis直接启动的话 可以将这两个参数置为true, 并且spring.profile.active: dev换成test
# redis的端口可能会被占用,如果被占用请自己修改一下端口号
agileboot:
embedded:
mysql: false
redis: false
springdoc:
api-docs:
enabled: true
groups:
enabled: true
group-configs:
- group: '公共API'
packages-to-scan: com.agileboot.admin.controller.common
- group: '内置系统API'
packages-to-scan: com.agileboot.admin.controller.system
@@ -0,0 +1,44 @@
package com.agileboot.admin.config;
import com.agileboot.admin.AgileBootAdminApplication;
import com.agileboot.common.config.AgileBootConfig;
import com.agileboot.common.constant.Constants.UploadSubDir;
import java.io.File;
import javax.annotation.Resource;
import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@SpringBootTest(classes = AgileBootAdminApplication.class)
@RunWith(SpringRunner.class)
public class AgileBootConfigTest {
@Resource
private AgileBootConfig config;
@Test
public void testConfig() {
String fileBaseDir = "D:\\agileboot\\profile";
Assertions.assertEquals("AgileBoot", config.getName());
Assertions.assertEquals("1.8.0", config.getVersion());
Assertions.assertEquals("2022", config.getCopyrightYear());
Assertions.assertFalse(config.isDemoEnabled());
Assertions.assertEquals(fileBaseDir, AgileBootConfig.getFileBaseDir());
Assertions.assertFalse(AgileBootConfig.isAddressEnabled());
Assertions.assertEquals("math", AgileBootConfig.getCaptchaType());
Assertions.assertEquals("math", AgileBootConfig.getCaptchaType());
Assertions.assertEquals(fileBaseDir + "\\import",
AgileBootConfig.getFileBaseDir() + File.separator + UploadSubDir.IMPORT_PATH);
Assertions.assertEquals(fileBaseDir + "\\avatar",
AgileBootConfig.getFileBaseDir() + File.separator + UploadSubDir.AVATAR_PATH);
Assertions.assertEquals(fileBaseDir + "\\download",
AgileBootConfig.getFileBaseDir() + File.separator + UploadSubDir.DOWNLOAD_PATH);
Assertions.assertEquals(fileBaseDir + "\\upload",
AgileBootConfig.getFileBaseDir() + File.separator + UploadSubDir.UPLOAD_PATH);
}
}
@@ -0,0 +1,83 @@
package com.agileboot.admin.customize.service.permission;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.agileboot.admin.customize.service.permission.model.checker.CustomDataPermissionChecker;
import com.agileboot.infrastructure.user.web.SystemLoginUser;
import com.agileboot.infrastructure.user.web.RoleInfo;
import com.agileboot.admin.customize.service.permission.model.DataCondition;
import com.agileboot.domain.system.dept.db.SysDeptService;
import org.apache.commons.collections4.SetUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class CustomDataPermissionCheckerTest {
private final SysDeptService deptService = mock(SysDeptService.class);
public SystemLoginUser loginUser = mock(SystemLoginUser.class);
@BeforeEach
public void mockBefore() {
when(loginUser.getRoleInfo()).thenReturn(RoleInfo.EMPTY_ROLE);
}
@Test
void testCheckWhenParameterNull() {
CustomDataPermissionChecker customChecker = new CustomDataPermissionChecker(deptService);
boolean check1 = customChecker.check(null, null);
boolean check2 = customChecker.check(loginUser, null);
boolean check3 = customChecker.check(null, new DataCondition());
assertFalse(check1);
assertFalse(check2);
assertFalse(check3);
}
@Test
void testCheckWhenTargetDeptIdNull() {
CustomDataPermissionChecker customChecker = new CustomDataPermissionChecker(deptService);
boolean check = customChecker.check(loginUser, new DataCondition(null, 1L));
assertFalse(check);
}
@Test
void testCheckWhenRoleIsNull() {
CustomDataPermissionChecker customChecker = new CustomDataPermissionChecker(deptService);
when(loginUser.getRoleInfo()).thenReturn(null);
boolean check = customChecker.check(loginUser, new DataCondition(1L, 1L));
assertFalse(check);
}
@Test
void testCheckWhenNotContainTargetDeptId() {
CustomDataPermissionChecker customChecker = new CustomDataPermissionChecker(deptService);
loginUser.getRoleInfo().setDeptIdSet(SetUtils.hashSet(2L));
boolean check = customChecker.check(loginUser, new DataCondition(1L, 1L));
assertFalse(check);
}
@Test
void testCheckWhenContainTargetDeptId() {
CustomDataPermissionChecker customChecker = new CustomDataPermissionChecker(deptService);
loginUser.getRoleInfo().setDeptIdSet(SetUtils.hashSet(1L));
boolean check = customChecker.check(loginUser, new DataCondition(1L, 1L));
assertTrue(check);
}
}
@@ -0,0 +1,92 @@
package com.agileboot.admin.customize.service.permission;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.agileboot.admin.customize.service.permission.model.checker.DeptTreeDataPermissionChecker;
import com.agileboot.infrastructure.user.web.SystemLoginUser;
import com.agileboot.infrastructure.user.web.RoleInfo;
import com.agileboot.admin.customize.service.permission.model.DataCondition;
import com.agileboot.domain.system.dept.db.SysDeptService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class DeptTreeDataPermissionCheckerTest {
private final SysDeptService deptService = mock(SysDeptService.class);
public SystemLoginUser loginUser = mock(SystemLoginUser.class);
@BeforeEach
public void mockBefore() {
when(loginUser.getRoleInfo()).thenReturn(RoleInfo.EMPTY_ROLE);
}
@Test
void testCheckWhenParameterNull() {
DeptTreeDataPermissionChecker checker = new DeptTreeDataPermissionChecker(deptService);
boolean check1 = checker.check(null, null);
boolean check2 = checker.check(new SystemLoginUser(), null);
boolean check3 = checker.check(null, new DataCondition());
boolean check4 = checker.check(loginUser, new DataCondition());
assertFalse(check1);
assertFalse(check2);
assertFalse(check3);
assertFalse(check4);
}
@Test
void testCheckWhenIsChildOfDept() {
DeptTreeDataPermissionChecker checker = new DeptTreeDataPermissionChecker(deptService);
when(deptService.isChildOfTheDept(any(), any())).thenReturn(true);
when(loginUser.getDeptId()).thenReturn(1L);
DataCondition dataCondition = new DataCondition();
dataCondition.setTargetDeptId(2L);
boolean check = checker.check(loginUser, dataCondition);
assertTrue(check);
}
@Test
void testCheckWhenIsSameDept() {
DeptTreeDataPermissionChecker checker = new DeptTreeDataPermissionChecker(deptService);
when(deptService.isChildOfTheDept(any(), any())).thenReturn(false);
when(loginUser.getDeptId()).thenReturn(1L);
DataCondition dataCondition = new DataCondition();
dataCondition.setTargetDeptId(1L);
boolean check = checker.check(loginUser, dataCondition);
assertTrue(check);
}
@Test
void testCheckWhenFailed() {
DeptTreeDataPermissionChecker checker = new DeptTreeDataPermissionChecker(deptService);
when(deptService.isChildOfTheDept(any(), any())).thenReturn(false);
when(loginUser.getDeptId()).thenReturn(1L);
DataCondition dataCondition = new DataCondition();
dataCondition.setTargetDeptId(2L);
boolean check = checker.check(loginUser, dataCondition);
assertFalse(check);
}
}
@@ -0,0 +1,59 @@
package com.agileboot.admin.customize.service.permission;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import com.agileboot.admin.customize.service.permission.model.checker.OnlySelfDataPermissionChecker;
import com.agileboot.infrastructure.user.web.SystemLoginUser;
import com.agileboot.admin.customize.service.permission.model.DataCondition;
import com.agileboot.domain.system.dept.db.SysDeptService;
import org.junit.jupiter.api.Test;
class OnlySelfDataPermissionCheckerTest {
private final SysDeptService deptService = mock(SysDeptService.class);
@Test
void testCheckWhenParameterNull() {
OnlySelfDataPermissionChecker checker = new OnlySelfDataPermissionChecker(deptService);
boolean check1 = checker.check(null, null);
boolean check2 = checker.check(new SystemLoginUser(), null);
boolean check3 = checker.check(null, new DataCondition());
boolean check4 = checker.check(new SystemLoginUser(), new DataCondition());
assertFalse(check1);
assertFalse(check2);
assertFalse(check3);
assertFalse(check4);
}
@Test
void testCheckWhenSameUserId() {
OnlySelfDataPermissionChecker checker = new OnlySelfDataPermissionChecker(deptService);
SystemLoginUser loginUser = new SystemLoginUser();
loginUser.setUserId(1L);
DataCondition dataCondition = new DataCondition();
dataCondition.setTargetUserId(1L);
boolean check = checker.check(loginUser, dataCondition);
assertTrue(check);
}
@Test
void testCheckWhenDifferentUserId() {
OnlySelfDataPermissionChecker checker = new OnlySelfDataPermissionChecker(deptService);
SystemLoginUser loginUser = new SystemLoginUser();
loginUser.setUserId(1L);
DataCondition dataCondition = new DataCondition();
dataCondition.setTargetDeptId(2L);
boolean check = checker.check(loginUser, dataCondition);
assertFalse(check);
}
}
@@ -0,0 +1,72 @@
package com.agileboot.admin.customize.service.permission;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.agileboot.admin.customize.service.permission.model.checker.SingleDeptDataPermissionChecker;
import com.agileboot.infrastructure.user.web.SystemLoginUser;
import com.agileboot.infrastructure.user.web.RoleInfo;
import com.agileboot.admin.customize.service.permission.model.DataCondition;
import com.agileboot.domain.system.dept.db.SysDeptService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class SingleDeptDataPermissionCheckerTest {
private final SysDeptService deptService = mock(SysDeptService.class);
public SystemLoginUser loginUser = mock(SystemLoginUser.class);
@BeforeEach
public void mockBefore() {
when(loginUser.getRoleInfo()).thenReturn(RoleInfo.EMPTY_ROLE);
}
@Test
void testCheckWhenParameterNull() {
SingleDeptDataPermissionChecker checker = new SingleDeptDataPermissionChecker(deptService);
boolean check1 = checker.check(null, null);
boolean check2 = checker.check(new SystemLoginUser(), null);
boolean check3 = checker.check(null, new DataCondition());
boolean check4 = checker.check(loginUser, new DataCondition());
assertFalse(check1);
assertFalse(check2);
assertFalse(check3);
assertFalse(check4);
}
@Test
void testCheckWhenSameDeptId() {
SingleDeptDataPermissionChecker checker = new SingleDeptDataPermissionChecker(deptService);
when(loginUser.getDeptId()).thenReturn(1L);
DataCondition dataCondition = new DataCondition();
dataCondition.setTargetDeptId(1L);
boolean check = checker.check(loginUser, dataCondition);
assertTrue(check);
}
@Test
void testCheckWhenDifferentDeptId() {
SingleDeptDataPermissionChecker checker = new SingleDeptDataPermissionChecker(deptService);
when(loginUser.getDeptId()).thenReturn(1L);
DataCondition dataCondition = new DataCondition();
dataCondition.setTargetUserId(2L);
boolean check = checker.check(loginUser, dataCondition);
assertFalse(check);
}
}
+47
View File
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>agileboot</artifactId>
<groupId>com.agileboot</groupId>
<version>1.0.0</version>
</parent>
<packaging>jar</packaging>
<modelVersion>4.0.0</modelVersion>
<artifactId>agileboot-api</artifactId>
<description>
外部API
</description>
<dependencies>
<!-- 核心模块-->
<dependency>
<groupId>com.agileboot</groupId>
<artifactId>agileboot-infrastructure</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--使用undertow依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<!-- 业务领域 -->
<dependency>
<groupId>com.agileboot</groupId>
<artifactId>agileboot-domain</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,32 @@
package com.agileboot.api;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
/**
* 启动程序 定制banner.txt的网站
* <a href="http://patorjk.com/software/taag">http://patorjk.com/software/taag</a>
* <a href="http://www.network-science.de/ascii/">http://www.network-science.de/ascii/</a>
* <a href="http://www.degraeve.com/img2txt.php">http://www.degraeve.com/img2txt.php</a>
* <a href="http://life.chacuo.net/convertfont2char">http://life.chacuo.net/convertfont2char</a>
*
* @author valarchie
*/
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@ComponentScan(basePackages = "com.agileboot.*")
public class AgileBooApiApplication {
public static void main(String[] args) {
SpringApplication.run(AgileBooApiApplication.class, args);
String successMsg = " ____ _ _ __ _ _ \n"
+ " / ___| | |_ __ _ _ __ | |_ _ _ _ __ ___ _ _ ___ ___ ___ ___ ___ / _| _ _ | || |\n"
+ " \\___ \\ | __|/ _` || '__|| __| | | | || '_ \\ / __|| | | | / __|/ __|/ _ \\/ __|/ __|| |_ | | | || || |\n"
+ " ___) || |_| (_| || | | |_ | |_| || |_) | \\__ \\| |_| || (__| (__| __/\\__ \\\\__ \\| _|| |_| || ||_|\n"
+ " |____/ \\__|\\__,_||_| \\__| \\__,_|| .__/ |___/ \\__,_| \\___|\\___|\\___||___/|___/|_| \\__,_||_|(_)\n"
+ " |_| ";
System.out.println(successMsg);
}
}
@@ -0,0 +1,25 @@
package com.agileboot.api.controller;
import com.agileboot.common.core.base.BaseController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 调度日志操作处理
*
* @author valarchie
*/
@RestController
@RequestMapping("/api/order")
public class OrderController extends BaseController {
/**
* 访问首页,提示语
*/
@RequestMapping("/")
public String index() {
return "暂无订单";
}
}
@@ -0,0 +1,39 @@
package com.agileboot.api.controller.app;
import com.agileboot.api.customize.service.JwtTokenService;
import com.agileboot.common.core.base.BaseController;
import com.agileboot.common.core.dto.ResponseDTO;
import lombok.AllArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 调度日志操作处理
*
* @author ruoyi
*/
@RestController
@RequestMapping("/app")
@AllArgsConstructor
public class AppController extends BaseController {
private final JwtTokenService jwtTokenService;
/**
* 访问首页,提示语
*/
@PreAuthorize("hasAuthority('annie')")
@GetMapping("/list")
public ResponseDTO<?> appLogin() {
return ResponseDTO.ok();
}
}
@@ -0,0 +1,38 @@
package com.agileboot.api.controller.common;
import cn.hutool.core.map.MapUtil;
import com.agileboot.api.customize.service.JwtTokenService;
import com.agileboot.common.core.base.BaseController;
import com.agileboot.common.core.dto.ResponseDTO;
import java.util.Map;
import lombok.AllArgsConstructor;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 调度日志操作处理
*
* @author ruoyi
*/
@RestController
@RequestMapping("/common")
@AllArgsConstructor
public class LoginController extends BaseController {
private final JwtTokenService jwtTokenService;
/**
* 访问首页,提示语
*/
@PostMapping("/app/{appId}/login")
public ResponseDTO<String> appLogin() {
String token = jwtTokenService.generateToken(MapUtil.of("token", "user1"));
return ResponseDTO.ok(token);
}
}
@@ -0,0 +1,52 @@
package com.agileboot.api.customize.config;
import com.agileboot.api.customize.service.JwtTokenService;
import com.agileboot.infrastructure.user.app.AppLoginUser;
import io.jsonwebtoken.Claims;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* token过滤器 验证token有效性
* 继承OncePerRequestFilter类的话 可以确保只执行filter一次, 避免执行多次
* @author valarchie
*/
@Component
@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenService jwtTokenService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String tokenFromRequest = jwtTokenService.getTokenFromRequest(request);
if (tokenFromRequest != null) {
Claims claims = jwtTokenService.parseToken(tokenFromRequest);
String token = (String) claims.get("token");
// 根据token去查缓存里面 有没有对应的App用户
// 没有的话 再去数据库中查询
if (token != null && token.equals("user1")) {
AppLoginUser loginUser = new AppLoginUser(23232323L, false, "dasdsadsds");
loginUser.grantAppPermission("annie");
UsernamePasswordAuthenticationToken suer1 = new UsernamePasswordAuthenticationToken(loginUser, null,
loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(suer1);
}
}
filterChain.doFilter(request, response);
}
}
@@ -0,0 +1,85 @@
package com.agileboot.api.customize.config;
import com.agileboot.common.core.dto.ResponseDTO;
import com.agileboot.common.exception.ApiException;
import com.agileboot.common.exception.error.ErrorCode.Client;
import com.agileboot.common.utils.ServletHolderUtil;
import com.agileboot.common.utils.jackson.JacksonUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.filter.CorsFilter;
/**
* 主要配置登录流程逻辑涉及以下几个类
* @author valarchie
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig {
/**
* token认证过滤器
*/
private final JwtAuthenticationFilter jwtTokenFilter;
/**
* 跨域过滤器
*/
private final CorsFilter corsFilter;
/**
* 登录异常处理类
* 用户未登陆的话 在这个Bean中处理
*/
@Bean
public AuthenticationEntryPoint customAuthenticationEntryPoint() {
return (request, response, exception) -> {
ResponseDTO<Void> responseDTO = ResponseDTO.fail(
new ApiException(Client.COMMON_NO_AUTHORIZATION, request.getRequestURI())
);
ServletHolderUtil.renderString(response, JacksonUtil.to(responseDTO));
};
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf().disable()
// 不配这个错误处理的话 会直接返回403
.exceptionHandling().authenticationEntryPoint(customAuthenticationEntryPoint())
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 禁用 session
.and()
.authorizeRequests()
.antMatchers("/common/**").permitAll()
.anyRequest().authenticated()
.and()
// 禁用 X-Frame-Options 响应头。下面是具体解释:
// X-Frame-Options 是一个 HTTP 响应头,用于防止网页被嵌入到其他网页的 <frame>、<iframe> 或 <object> 标签中,从而可以减少点击劫持攻击的风险
.headers().frameOptions().disable()
.and()
.formLogin().disable();
httpSecurity.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 添加CORS filter
httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationFilter.class);
return httpSecurity.build();
}
}
@@ -0,0 +1,127 @@
package com.agileboot.api.customize.service;
import cn.hutool.core.util.StrUtil;
import com.agileboot.common.constant.Constants.Token;
import com.agileboot.common.exception.ApiException;
import com.agileboot.common.exception.error.ErrorCode;
import com.agileboot.domain.common.cache.RedisCacheService;
import com.agileboot.infrastructure.user.web.SystemLoginUser;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* token验证处理
*
* @author valarchie
*/
@Component
@Slf4j
@Data
@RequiredArgsConstructor
public class JwtTokenService {
/**
* 自定义令牌标识
*/
@Value("${token.header}")
private String header;
/**
* 令牌秘钥
*/
@Value("${token.secret}")
private String secret;
private final RedisCacheService redisCache;
/**
* 获取用户身份信息
*
* @return 用户信息
*/
public SystemLoginUser getLoginUser(HttpServletRequest request) {
// 获取请求携带的令牌
String token = getTokenFromRequest(request);
if (StrUtil.isNotEmpty(token)) {
try {
Claims claims = parseToken(token);
// 解析对应的权限以及用户信息
String uuid = (String) claims.get(Token.LOGIN_USER_KEY);
return redisCache.loginUserCache.getObjectOnlyInCacheById(uuid);
} catch (SignatureException | MalformedJwtException | UnsupportedJwtException | IllegalArgumentException jwtException) {
log.error("parse token failed.", jwtException);
throw new ApiException(jwtException, ErrorCode.Client.INVALID_TOKEN);
} catch (Exception e) {
log.error("fail to get cached user from redis", e);
throw new ApiException(e, ErrorCode.Client.TOKEN_PROCESS_FAILED, e.getMessage());
}
}
return null;
}
/**
* 从数据声明生成令牌
*
* @param claims 数据声明
* @return 令牌
*/
public String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret).compact();
}
/**
* 从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/
public Claims parseToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
/**
* 从令牌中获取用户名
*
* @param token 令牌
* @return 用户名
*/
private String getUsernameFromToken(String token) {
Claims claims = parseToken(token);
return claims.getSubject();
}
/**
* 获取请求token
*
* @return token
*/
public String getTokenFromRequest(HttpServletRequest request) {
String token = request.getHeader(header);
if (StrUtil.isNotEmpty(token) && token.startsWith(Token.PREFIX)) {
token = StrUtil.stripIgnoreCase(token, Token.PREFIX, null);
}
return token;
}
}
@@ -0,0 +1,12 @@
package com.agileboot.api.customize.util;
public class ApiEncryptor {
public static void main(String[] args) {
}
}
@@ -0,0 +1,28 @@
# 开发环境配置
server:
# 服务器的HTTP端口,默认为8080
port: 8090
servlet:
# 应用的访问路径
context-path: /
tomcat:
# tomcat的URI编码
uri-encoding: UTF-8
# 连接数满后的排队数,默认为100
accept-count: 1000
threads:
# tomcat最大线程数,默认为200
max: 800
# Tomcat启动初始化的线程数,默认值10
min-spare: 100
# Spring配置
spring:
profiles:
active: basic,dev
+189
View File
@@ -0,0 +1,189 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>agileboot</artifactId>
<groupId>com.agileboot</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>agileboot-common</artifactId>
<description>
common通用工具
</description>
<dependencies>
<!-- Spring框架基本的核心工具 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<!-- SpringWeb模块 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<!-- spring security 安全认证 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 自定义验证注解 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!--常用工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- JSON工具类 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- io常用工具类 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<!-- 文件上传工具类 -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
</dependency>
<!-- excel工具 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
</dependency>
<!-- yml解析器 -->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</dependency>
<!-- Token生成与解析-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
<!-- Jaxb -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</dependency>
<!-- redis 缓存操作 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- pool 对象池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- 解析客户端操作系统、浏览器等 -->
<dependency>
<groupId>eu.bitwalker</groupId>
<artifactId>UserAgentUtils</artifactId>
</dependency>
<!-- servlet包 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-parameter-names</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
</dependency>
<dependency>
<groupId>it.ozimov</groupId>
<artifactId>embedded-redis</artifactId>
<!-- 不排除掉slf4j的话 会冲突-->
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</exclusion>
<!-- 排除掉guava依赖,以本项目的guava依赖为准 -->
<exclusion>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 多数据源 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
</dependency>
<!-- swagger注解 -->
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,19 @@
package com.agileboot.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义导出Excel数据注解
*
* @author valarchie
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ExcelColumn {
String name() default "";
}
@@ -0,0 +1,20 @@
package com.agileboot.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author valarchie
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ExcelSheet {
/**
* sheet名称
*/
String name() default "";
}
@@ -0,0 +1,109 @@
package com.agileboot.common.config;
import com.agileboot.common.constant.Constants;
import java.io.File;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 读取项目相关配置
* TODO 移走 不合适放在这里common包底下
* @author valarchie
*/
@Component
@ConfigurationProperties(prefix = "agileboot")
@Data
public class AgileBootConfig {
/**
* 项目名称
*/
private String name;
/**
* 版本
*/
private String version;
/**
* 版权年份
*/
private String copyrightYear;
/**
* 实例演示开关
*/
private static boolean demoEnabled;
/**
* 上传路径
*/
private static String fileBaseDir;
/**
* 获取地址开关
*/
private static boolean addressEnabled;
/**
* 验证码类型
*/
private static String captchaType;
/**
* rsa private key 静态属性的注入!! set方法一定不能是static 方法
*/
private static String rsaPrivateKey;
private static String apiPrefix;
public static String getFileBaseDir() {
return fileBaseDir;
}
public void setFileBaseDir(String fileBaseDir) {
AgileBootConfig.fileBaseDir = fileBaseDir + File.separator + Constants.RESOURCE_PREFIX;
}
public static String getApiPrefix() {
return apiPrefix;
}
public void setApiPrefix(String apiDocsPathPrefix) {
AgileBootConfig.apiPrefix = apiDocsPathPrefix;
}
public static boolean isAddressEnabled() {
return addressEnabled;
}
public void setAddressEnabled(boolean addressEnabled) {
AgileBootConfig.addressEnabled = addressEnabled;
}
public static String getCaptchaType() {
return captchaType;
}
public void setCaptchaType(String captchaType) {
AgileBootConfig.captchaType = captchaType;
}
public static String getRsaPrivateKey() {
return rsaPrivateKey;
}
public void setRsaPrivateKey(String rsaPrivateKey) {
AgileBootConfig.rsaPrivateKey = rsaPrivateKey;
}
public static boolean isDemoEnabled() {
return demoEnabled;
}
public void setDemoEnabled(boolean demoEnabled) {
AgileBootConfig.demoEnabled = demoEnabled;
}
}
@@ -0,0 +1,86 @@
package com.agileboot.common.constant;
/**
* 通用常量信息
*
* @author valarchie
*/
public class Constants {
private Constants() {
}
public static final int KB = 1024;
public static final int MB = KB * 1024;
public static final int GB = MB * 1024;
/**
* http请求
*/
public static final String HTTP = "http://";
/**
* https请求
*/
public static final String HTTPS = "https://";
public static class Token {
private Token() {
}
/**
* 令牌前缀
*/
public static final String PREFIX = "Bearer ";
/**
* 令牌前缀
*/
public static final String LOGIN_USER_KEY = "login_user_key";
}
public static class Captcha {
private Captcha() {
}
/**
* 令牌
*/
public static final String MATH_TYPE = "math";
/**
* 令牌前缀
*/
public static final String CHAR_TYPE = "char";
}
/**
* 资源映射路径 前缀
*/
public static final String RESOURCE_PREFIX = "profile";
public static class UploadSubDir {
private UploadSubDir() {
}
public static final String IMPORT_PATH = "import";
public static final String AVATAR_PATH = "avatar";
public static final String DOWNLOAD_PATH = "download";
public static final String UPLOAD_PATH = "upload";
}
}
@@ -0,0 +1,40 @@
package com.agileboot.common.core.base;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import java.beans.PropertyEditorSupport;
import java.util.Date;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
/**
* @author valarchie
*/
@Slf4j
public class BaseController {
/**
*
* 将前台传递过来的日期格式的字符串,自动转化为Date类型
*/
@InitBinder
public void initBinder(WebDataBinder binder) {
// Date 类型转换
binder.registerCustomEditor(Date.class, new PropertyEditorSupport() {
@Override
public void setAsText(String text) {
setValue(DateUtil.parseDate(text));
}
});
}
/**
* 页面跳转
*/
public String redirect(String url) {
return StrUtil.format("redirect:{}", url);
}
}
@@ -0,0 +1,46 @@
package com.agileboot.common.core.base;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import io.swagger.annotations.ApiModelProperty;
import java.util.Date;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* Entity基类
*
* @author valarchie
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class BaseEntity<T extends Model<?>> extends Model<T> {
@ApiModelProperty("创建者ID")
@TableField(value = "creator_id", fill = FieldFill.INSERT)
private Long creatorId;
@ApiModelProperty("创建时间")
@TableField(value = "create_time", fill = FieldFill.INSERT)
private Date createTime;
@ApiModelProperty("更新者ID")
@TableField(value = "updater_id", fill = FieldFill.UPDATE, updateStrategy = FieldStrategy.NOT_NULL)
private Long updaterId;
@ApiModelProperty("更新时间")
@TableField(value = "update_time", fill = FieldFill.UPDATE)
private Date updateTime;
/**
* deleted字段请在数据库中 设置为tinyInt 并且非null 默认值为0
*/
@ApiModelProperty("删除标志(0代表存在 1代表删除)")
@TableField("deleted")
@TableLogic
private Boolean deleted;
}
@@ -0,0 +1,59 @@
package com.agileboot.common.core.dto;
import com.agileboot.common.exception.ApiException;
import com.agileboot.common.exception.error.ErrorCode;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* 响应信息主体
*
* @author valarchie
*/
@Data
@AllArgsConstructor
public class ResponseDTO<T> {
private Integer code;
private String msg;
@JsonInclude
private T data;
public static <T> ResponseDTO<T> ok() {
return build(null, ErrorCode.SUCCESS.code(), ErrorCode.SUCCESS.message());
}
public static <T> ResponseDTO<T> ok(T data) {
return build(data, ErrorCode.SUCCESS.code(), ErrorCode.SUCCESS.message());
}
public static <T> ResponseDTO<T> fail() {
return build(null, ErrorCode.FAILED.code(), ErrorCode.FAILED.message());
}
public static <T> ResponseDTO<T> fail(T data) {
return build(data, ErrorCode.FAILED.code(), ErrorCode.FAILED.message());
}
public static <T> ResponseDTO<T> fail(ApiException exception) {
return build(null, exception.getErrorCode().code(), exception.getMessage());
}
public static <T> ResponseDTO<T> fail(ApiException exception, T data) {
return build(data, exception.getErrorCode().code(), exception.getMessage());
}
public static <T> ResponseDTO<T> build(T data, Integer code, String msg) {
return new ResponseDTO<>(code, msg, data);
}
// 去掉直接填充错误码的方式, 这种方式不能拿到i18n的错误消息 统一通过ApiException来构造错误消息
// public static <T> ResponseDTO<T> fail(ErrorCodeInterface code, Object... args) {
// return build(null, code, args);
// }
}
@@ -0,0 +1,44 @@
package com.agileboot.common.core.page;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import javax.validation.constraints.Max;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author valarchie
*/
@EqualsAndHashCode(callSuper = true)
@Data
public abstract class AbstractPageQuery<T> extends AbstractQuery<T> {
/**
* 最大分页页数
*/
public static final int MAX_PAGE_NUM = 200;
/**
* 单页最大大小
*/
public static final int MAX_PAGE_SIZE = 500;
/**
* 默认分页页数
*/
public static final int DEFAULT_PAGE_NUM = 1;
/**
* 默认分页大小
*/
public static final int DEFAULT_PAGE_SIZE = 10;
@Max(MAX_PAGE_NUM)
protected Integer pageNum;
@Max(MAX_PAGE_SIZE)
protected Integer pageSize;
public Page<T> toPage() {
pageNum = ObjectUtil.defaultIfNull(pageNum, DEFAULT_PAGE_NUM);
pageSize = ObjectUtil.defaultIfNull(pageSize, DEFAULT_PAGE_SIZE);
return new Page<>(pageNum, pageSize);
}
}
@@ -0,0 +1,90 @@
package com.agileboot.common.core.page;
import cn.hutool.core.util.StrUtil;
import com.agileboot.common.utils.time.DatePickUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonFormat.Shape;
import java.util.Date;
import lombok.Data;
/**
* 如果是简单的排序 和 时间范围筛选 可以使用内置的这几个字段
* @author valarchie
*/
@Data
public abstract class AbstractQuery<T> {
protected String orderColumn;
protected String orderDirection;
protected String timeRangeColumn;
@JsonFormat(shape = Shape.STRING, pattern = "yyyy-MM-dd")
private Date beginTime;
@JsonFormat(shape = Shape.STRING, pattern = "yyyy-MM-dd")
private Date endTime;
private static final String ASC = "ascending";
private static final String DESC = "descending";
/**
* 生成query conditions
*
* @return 添加条件后的QueryWrapper
*/
public QueryWrapper<T> toQueryWrapper() {
QueryWrapper<T> queryWrapper = addQueryCondition();
addSortCondition(queryWrapper);
addTimeCondition(queryWrapper);
return queryWrapper;
}
public abstract QueryWrapper<T> addQueryCondition();
public void addSortCondition(QueryWrapper<T> queryWrapper) {
if (queryWrapper == null || StrUtil.isEmpty(orderColumn)) {
return;
}
Boolean sortDirection = convertSortDirection();
if (sortDirection != null) {
queryWrapper.orderBy(StrUtil.isNotEmpty(orderColumn), sortDirection,
StrUtil.toUnderlineCase(orderColumn));
}
}
public void addTimeCondition(QueryWrapper<T> queryWrapper) {
if (queryWrapper != null
&& StrUtil.isNotEmpty(this.timeRangeColumn)) {
queryWrapper
.ge(beginTime != null, StrUtil.toUnderlineCase(timeRangeColumn),
DatePickUtil.getBeginOfTheDay(beginTime))
.le(endTime != null, StrUtil.toUnderlineCase(timeRangeColumn), DatePickUtil.getEndOfTheDay(endTime));
}
}
/**
* 获取前端传来的排序方向 转换成MyBatisPlus所需的排序参数 boolean=isAsc
* @return 排序顺序, null为无排序
*/
public Boolean convertSortDirection() {
Boolean isAsc = null;
if (StrUtil.isEmpty(this.orderDirection)) {
return isAsc;
}
if (ASC.equals(this.orderDirection)) {
isAsc = true;
}
if (DESC.equals(this.orderDirection)) {
isAsc = false;
}
return isAsc;
}
}
@@ -0,0 +1,38 @@
package com.agileboot.common.core.page;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import java.util.List;
import lombok.Data;
/**
* 分页模型类
* @author valarchie
*/
@Data
public class PageDTO<T> {
/**
* 总记录数
*/
private Long total;
/**
* 列表数据
*/
private List<T> rows;
public PageDTO(List<T> list) {
this.rows = list;
this.total = (long) list.size();
}
public PageDTO(Page<T> page) {
this.rows = page.getRecords();
this.total = page.getTotal();
}
public PageDTO(List<T> list, Long count) {
this.rows = list;
this.total = count;
}
}
@@ -0,0 +1,24 @@
package com.agileboot.common.enums;
/**
* @author valarchie
* 普通的枚举 接口
* @param <T>
*/
public interface BasicEnum<T>{
/**
* 获取枚举的值
* @return 枚举值
*/
T getValue();
/**
* 获取枚举的描述
* @return 描述
*/
String description();
}
@@ -0,0 +1,63 @@
package com.agileboot.common.enums;
import cn.hutool.core.convert.Convert;
import com.agileboot.common.exception.ApiException;
import com.agileboot.common.exception.error.ErrorCode;
import com.agileboot.common.enums.BasicEnum;
import java.util.Objects;
/**
* @author valarchie
*/
public class BasicEnumUtil {
private BasicEnumUtil() {
}
public static final String UNKNOWN = "未知";
public static <E extends Enum<E>> E fromValueSafely(Class<E> enumClass, Object value) {
E target = null;
for (E enumConstant : enumClass.getEnumConstants()) {
BasicEnum<?> basicEnum = (BasicEnum<?>) enumConstant;
if (Objects.equals(basicEnum.getValue(), value)) {
target = (E) basicEnum;
}
}
return target;
}
public static <E extends Enum<E>> E fromValue(Class<E> enumClass, Object value) {
E target = null;
for (E enumConstant : enumClass.getEnumConstants()) {
BasicEnum basicEnum = (BasicEnum) enumConstant;
if (Objects.equals(basicEnum.getValue(), value)) {
target = (E) basicEnum;
}
}
if (target == null) {
throw new ApiException(ErrorCode.Internal.GET_ENUM_FAILED, enumClass.getSimpleName());
}
return target;
}
public static <E extends Enum<E>> String getDescriptionByBool(Class<E> enumClass, Boolean bool) {
Integer value = Convert.toInt(bool, 0);
return getDescriptionByValue(enumClass, value);
}
public static <E extends Enum<E>> String getDescriptionByValue(Class<E> enumClass, Object value) {
E basicEnum = fromValueSafely(enumClass, value);
if (basicEnum != null) {
return ((BasicEnum<?>) basicEnum).description();
}
return UNKNOWN;
}
}
@@ -0,0 +1,15 @@
package com.agileboot.common.enums;
/**
* 字典类型 接口
* @author valarchie
*/
public interface DictionaryEnum<T> extends BasicEnum<T> {
/**
* 获取css标签
* @return css标签
*/
String cssTag();
}
@@ -0,0 +1,54 @@
package com.agileboot.common.enums.common;
import com.agileboot.common.enums.dictionary.CssTag;
import com.agileboot.common.enums.dictionary.Dictionary;
import com.agileboot.common.enums.DictionaryEnum;
/**
* 对应sys_operation_log的business_type
*
* @author valarchie
*/
@Dictionary(name = "sysOperationLog.businessType")
public enum BusinessTypeEnum implements DictionaryEnum<Integer> {
/**
* 操作类型
*/
OTHER(0, "其他操作", CssTag.INFO),
ADD(1, "添加", CssTag.PRIMARY),
MODIFY(2, "修改", CssTag.PRIMARY),
DELETE(3, "删除", CssTag.DANGER),
GRANT(4, "授权", CssTag.PRIMARY),
EXPORT(5, "导出", CssTag.WARNING),
IMPORT(6, "导入", CssTag.WARNING),
FORCE_LOGOUT(7, "强退", CssTag.DANGER),
CLEAN(8, "清空", CssTag.DANGER),
;
private final int value;
private final String description;
private final String cssTag;
BusinessTypeEnum(int value, String description, String cssTag) {
this.value = value;
this.description = description;
this.cssTag = cssTag;
}
@Override
public Integer getValue() {
return value;
}
@Override
public String description() {
return description;
}
@Override
public String cssTag() {
return cssTag;
}
}
@@ -0,0 +1,40 @@
package com.agileboot.common.enums.common;
import com.agileboot.common.enums.BasicEnum;
/**
* 系统配置
* @author valarchie
* 对应 sys_config表的config_key字段
*/
public enum ConfigKeyEnum implements BasicEnum<String> {
/**
* 菜单类型
*/
SKIN_THEME("sys.index.skinName", "系统皮肤主题"),
INIT_PASSWORD("sys.user.initPassword", "初始密码"),
SIDE_BAR_THEME("sys.index.sideTheme", "侧边栏开关"),
CAPTCHA("sys.account.captchaOnOff", "验证码开关"),
REGISTER("sys.account.registerUser", "注册开放功能");
private final String value;
private final String description;
ConfigKeyEnum(String value, String description) {
this.value = value;
this.description = description;
}
@Override
public String getValue() {
return value;
}
@Override
public String description() {
return description;
}
}
@@ -0,0 +1,47 @@
package com.agileboot.common.enums.common;
import com.agileboot.common.enums.dictionary.CssTag;
import com.agileboot.common.enums.dictionary.Dictionary;
import com.agileboot.common.enums.DictionaryEnum;
/**
* 对应sys_user的sex字段
*
* @author valarchie
*/
@Dictionary(name = "sysUser.sex")
public enum GenderEnum implements DictionaryEnum<Integer> {
/**
* 用户性别
*/
MALE(1, "", CssTag.PRIMARY),
FEMALE(2, "", CssTag.PRIMARY),
UNKNOWN(0, "未知", CssTag.PRIMARY);
private final int value;
private final String description;
private final String cssTag;
GenderEnum(int value, String description, String cssTag) {
this.value = value;
this.description = description;
this.cssTag = cssTag;
}
@Override
public Integer getValue() {
return value;
}
@Override
public String description() {
return description;
}
@Override
public String cssTag() {
return cssTag;
}
}
@@ -0,0 +1,46 @@
package com.agileboot.common.enums.common;
import com.agileboot.common.enums.dictionary.CssTag;
import com.agileboot.common.enums.dictionary.Dictionary;
import com.agileboot.common.enums.DictionaryEnum;
/**
* 用户状态
* @author valarchie
*/
// TODO 表记得改成LoginLog
@Dictionary(name = "sysLoginLog.status")
public enum LoginStatusEnum implements DictionaryEnum<Integer> {
/**
* status of user
*/
LOGIN_SUCCESS(1, "登录成功", CssTag.SUCCESS),
LOGOUT(2, "退出成功", CssTag.INFO),
REGISTER(3, "注册", CssTag.PRIMARY),
LOGIN_FAIL(0, "登录失败", CssTag.DANGER);
private final int value;
private final String msg;
private final String cssTag;
LoginStatusEnum(int status, String msg, String cssTag) {
this.value = status;
this.msg = msg;
this.cssTag = cssTag;
}
@Override
public Integer getValue() {
return value;
}
@Override
public String description() {
return msg;
}
@Override
public String cssTag() {
return cssTag;
}
}
@@ -0,0 +1,36 @@
package com.agileboot.common.enums.common;
import com.agileboot.common.enums.BasicEnum;
/**
*
* @author valarchie
*/
@Deprecated
public enum MenuComponentEnum implements BasicEnum<Integer> {
/**
* 菜单组件类型
*/
LAYOUT(1,"Layout"),
PARENT_VIEW(2,"ParentView"),
INNER_LINK(3,"InnerLink");
private final int value;
private final String description;
MenuComponentEnum(int value, String description) {
this.value = value;
this.description = description;
}
@Override
public Integer getValue() {
return value;
}
@Override
public String description() {
return description;
}
}
@@ -0,0 +1,38 @@
package com.agileboot.common.enums.common;
import com.agileboot.common.enums.BasicEnum;
/**
* @author valarchie
* 对应 sys_menu表的menu_type字段
*/
public enum MenuTypeEnum implements BasicEnum<Integer> {
/**
* 菜单类型
*/
MENU(1, "页面"),
CATALOG(2, "目录"),
IFRAME(3, "内嵌Iframe"),
OUTSIDE_LINK_REDIRECT(4, "外链跳转");
private final int value;
private final String description;
MenuTypeEnum(int value, String description) {
this.value = value;
this.description = description;
}
@Override
public Integer getValue() {
return value;
}
@Override
public String description() {
return description;
}
}
@@ -0,0 +1,45 @@
package com.agileboot.common.enums.common;
import com.agileboot.common.enums.dictionary.CssTag;
import com.agileboot.common.enums.dictionary.Dictionary;
import com.agileboot.common.enums.DictionaryEnum;
/**
* 对应sys_notice的 status字段
* @author valarchie
*/
@Dictionary(name = "sysNotice.status")
public enum NoticeStatusEnum implements DictionaryEnum<Integer> {
/**
* 通知状态
*/
OPEN(1, "正常", CssTag.PRIMARY),
CLOSE(0, "关闭", CssTag.DANGER);
private final int value;
private final String description;
private final String cssTag;
NoticeStatusEnum(int value, String description, String cssTag) {
this.value = value;
this.description = description;
this.cssTag = cssTag;
}
@Override
public Integer getValue() {
return value;
}
@Override
public String description() {
return description;
}
@Override
public String cssTag() {
return cssTag;
}
}
@@ -0,0 +1,47 @@
package com.agileboot.common.enums.common;
import com.agileboot.common.enums.dictionary.CssTag;
import com.agileboot.common.enums.dictionary.Dictionary;
import com.agileboot.common.enums.DictionaryEnum;
/**
* 对应sys_notice的 notice_type字段
* 名称一般由对应的表名.字段构成
* 全局的话使用common作为表名
* @author valarchie
*/
@Dictionary(name = "sysNotice.noticeType")
public enum NoticeTypeEnum implements DictionaryEnum<Integer> {
/**
* 通知类型
*/
NOTIFICATION(1, "通知", CssTag.WARNING),
ANNOUNCEMENT(2, "公告", CssTag.SUCCESS);
private final int value;
private final String description;
private final String cssTag;
NoticeTypeEnum(int value, String description, String cssTag) {
this.value = value;
this.description = description;
this.cssTag = cssTag;
}
@Override
public Integer getValue() {
return value;
}
@Override
public String description() {
return description;
}
@Override
public String cssTag() {
return cssTag;
}
}
@@ -0,0 +1,45 @@
package com.agileboot.common.enums.common;
import com.agileboot.common.enums.dictionary.CssTag;
import com.agileboot.common.enums.dictionary.Dictionary;
import com.agileboot.common.enums.DictionaryEnum;
/**
* 对应sys_operation_log的status字段
* @author valarchie
*/
@Dictionary(name = "sysOperationLog.status")
public enum OperationStatusEnum implements DictionaryEnum<Integer> {
/**
* 操作状态
*/
SUCCESS(1, "成功", CssTag.PRIMARY),
FAIL(0, "失败", CssTag.DANGER);
private final int value;
private final String description;
private final String cssTag;
OperationStatusEnum(int value, String description, String cssTag) {
this.value = value;
this.description = description;
this.cssTag = cssTag;
}
@Override
public Integer getValue() {
return value;
}
@Override
public String description() {
return description;
}
@Override
public String cssTag() {
return cssTag;
}
}
@@ -0,0 +1,39 @@
package com.agileboot.common.enums.common;
import com.agileboot.common.enums.dictionary.Dictionary;
import com.agileboot.common.enums.BasicEnum;
/**
* 操作者类型
* @author valarchie
*/
@Dictionary(name = "sysOperationLog.operatorType")
public enum OperatorTypeEnum implements BasicEnum<Integer> {
/**
* 菜单类型
*/
OTHER(1, "其他"),
WEB(2, "Web用户"),
MOBILE(3, "手机端用户");
private final int value;
private final String description;
OperatorTypeEnum(int value, String description) {
this.value = value;
this.description = description;
}
@Override
public Integer getValue() {
return value;
}
@Override
public String description() {
return description;
}
}
@@ -0,0 +1,39 @@
package com.agileboot.common.enums.common;
import com.agileboot.common.enums.BasicEnum;
/**
* Http Method
* @author valarchie
*/
public enum RequestMethodEnum implements BasicEnum<Integer> {
/**
* 菜单类型
*/
GET(1, "GET"),
POST(2, "POST"),
PUT(3, "PUT"),
DELETE(4, "DELETE"),
UNKNOWN(-1, "UNKNOWN");
private final int value;
private final String description;
RequestMethodEnum(int value, String description) {
this.value = value;
this.description = description;
}
@Override
public Integer getValue() {
return value;
}
@Override
public String description() {
return description;
}
}
@@ -0,0 +1,44 @@
package com.agileboot.common.enums.common;
import com.agileboot.common.enums.dictionary.CssTag;
import com.agileboot.common.enums.dictionary.Dictionary;
import com.agileboot.common.enums.DictionaryEnum;
/**
* 除非表有特殊指明的话,一般用这个枚举代表 status字段
* @author valarchie
*/
@Dictionary(name = "common.status")
public enum StatusEnum implements DictionaryEnum<Integer> {
/**
* 开关状态
*/
ENABLE(1, "正常", CssTag.PRIMARY),
DISABLE(0, "停用", CssTag.DANGER);
private final int value;
private final String description;
private final String cssTag;
StatusEnum(int value, String description, String cssTag) {
this.value = value;
this.description = description;
this.cssTag = cssTag;
}
@Override
public Integer getValue() {
return value;
}
@Override
public String description() {
return description;
}
@Override
public String cssTag() {
return cssTag;
}
}
@@ -0,0 +1,49 @@
package com.agileboot.common.enums.common;
import com.agileboot.common.enums.dictionary.CssTag;
import com.agileboot.common.enums.dictionary.Dictionary;
import com.agileboot.common.enums.DictionaryEnum;
/**
* 对应sys_user的status字段
* @author valarchie
*/
@Dictionary(name = "sysUser.status")
public enum UserStatusEnum implements DictionaryEnum<Integer> {
/**
* 用户账户状态
*/
NORMAL(1, "正常", CssTag.PRIMARY),
DISABLED(2, "禁用", CssTag.DANGER),
FROZEN(3, "冻结", CssTag.WARNING);
private final int value;
private final String description;
private final String cssTag;
UserStatusEnum(int value, String description, String cssTag) {
this.value = value;
this.description = description;
this.cssTag = cssTag;
}
public Integer getValue() {
return value;
}
@Override
public String description() {
return this.description;
}
public String getDescription() {
return description;
}
@Override
public String cssTag() {
return null;
}
}
@@ -0,0 +1,46 @@
package com.agileboot.common.enums.common;
import com.agileboot.common.enums.dictionary.CssTag;
import com.agileboot.common.enums.dictionary.Dictionary;
import com.agileboot.common.enums.DictionaryEnum;
/**
* 对应sys_menu表的is_visible字段
* @author valarchie
*/
@Deprecated
@Dictionary(name = "sysMenu.isVisible")
public enum VisibleStatusEnum implements DictionaryEnum<Integer> {
/**
* 显示与否
*/
SHOW(1, "显示", CssTag.PRIMARY),
HIDE(0, "隐藏", CssTag.DANGER);
private final int value;
private final String description;
private final String cssTag;
VisibleStatusEnum(int value, String description, String cssTag) {
this.value = value;
this.description = description;
this.cssTag = cssTag;
}
@Override
public Integer getValue() {
return value;
}
@Override
public String description() {
return description;
}
@Override
public String cssTag() {
return cssTag;
}
}
@@ -0,0 +1,46 @@
package com.agileboot.common.enums.common;
import com.agileboot.common.enums.DictionaryEnum;
import com.agileboot.common.enums.dictionary.CssTag;
import com.agileboot.common.enums.dictionary.Dictionary;
/**
* 系统内代表是与否的枚举
* @author valarchie
*/
@Dictionary(name = "common.yesOrNo")
public enum YesOrNoEnum implements DictionaryEnum<Integer> {
/**
* 是与否
*/
YES(1, "", CssTag.PRIMARY),
NO(0, "", CssTag.DANGER);
private final int value;
private final String description;
private final String cssTag;
YesOrNoEnum(int value, String description, String cssTag) {
this.value = value;
this.description = description;
this.cssTag = cssTag;
}
@Override
public Integer getValue() {
return value;
}
@Override
public String description() {
return description;
}
@Override
public String cssTag() {
return cssTag;
}
}
@@ -0,0 +1,17 @@
package com.agileboot.common.enums.dictionary;
/**
* Css 样式
* @author valarchie
*/
public class CssTag {
public static final String PRIMARY = "";
public static final String DANGER = "danger";
public static final String WARNING = "warning";
public static final String SUCCESS = "success";
public static final String INFO = "info";
private CssTag() {
}
}
@@ -0,0 +1,25 @@
package com.agileboot.common.enums.dictionary;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 字典类型注解
*
* @author valarchie
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Dictionary {
/**
* 字典类型名称
*/
String name() default "";
}
@@ -0,0 +1,26 @@
package com.agileboot.common.enums.dictionary;
import com.agileboot.common.enums.DictionaryEnum;
import lombok.Data;
/**
* 字典模型类
* @author valarchie
*/
@Data
public class DictionaryData {
private String label;
private Integer value;
private String cssTag;
@SuppressWarnings("rawtypes")
public DictionaryData(DictionaryEnum enumType) {
if (enumType != null) {
this.label = enumType.description();
this.value = (Integer) enumType.getValue();
this.cssTag = enumType.cssTag();
}
}
}
@@ -0,0 +1,75 @@
package com.agileboot.common.exception;
import cn.hutool.core.util.StrUtil;
import com.agileboot.common.exception.error.ErrorCodeInterface;
import com.agileboot.common.utils.i18n.MessageUtils;
import java.util.HashMap;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
/**
* 统一异常类
*
* @author valarchie
*/
@EqualsAndHashCode(callSuper = true)
@Slf4j
@Data
public class ApiException extends RuntimeException {
protected ErrorCodeInterface errorCode;
protected String message;
protected String i18nMessage;
/**
* 如果有一些特殊的数据 可以放在这个payload里面
* 有时候错误的返回信息太少 不便前端处理的话 可以放在这个payload字段当中
* 比如你做了一个大批量操作,操作ID为1~10的实体, 其中1~5成功 6~10失败
* 你可以将这些相关信息放在这个payload中
*/
protected HashMap<String, Object> payload;
public ApiException(ErrorCodeInterface errorCode) {
fillErrorCode(errorCode);
}
public ApiException(ErrorCodeInterface errorCode, Object... args) {
fillErrorCode(errorCode, args);
}
/**
* 注意 如果是try catch的情况下捕获异常 并转为ApiException的话 一定要填入Throwable e
* @param e 捕获到的原始异常
* @param errorCode 错误码
* @param args 错误详细信息参数
*/
public ApiException(Throwable e, ErrorCodeInterface errorCode, Object... args) {
super(e);
fillErrorCode(errorCode, args);
}
private void fillErrorCode(ErrorCodeInterface errorCode, Object... args) {
this.errorCode = errorCode;
this.message = StrUtil.format(errorCode.message(), args);
try {
this.i18nMessage = MessageUtils.message(errorCode.i18nKey(), args);
} catch (Exception e) {
log.error("could not found i18nMessage entry for key: " + errorCode.i18nKey());
}
}
@Override
public String getMessage() {
return i18nMessage != null ? i18nMessage : message;
}
@Override
public String getLocalizedMessage() {
return i18nMessage != null ? i18nMessage : message;
}
}
@@ -0,0 +1,410 @@
package com.agileboot.common.exception.error;
import org.springframework.util.Assert;
/**
* 错误码集合
*
* @author valarchie
*/
public enum ErrorCode implements ErrorCodeInterface {
/**
* 错误码集合
* ******以下是旧的设计****
* 1~9999 为保留错误码 或者 常用错误码
* 10000~19999 为内部错误码
* 20000~29999 客户端错误码 (客户端异常调用之类的错误)
* 30000~39999 为第三方错误码 (代码正常,但是第三方异常)
* 40000~49999 为业务逻辑 错误码 (无异常,代码正常流转,并返回提示给用户)
* 由于系统内的错误码都是独一无二的,所以错误码应该放在common包集中管理
* ---------------------------
* 旧的设计的缺陷,比如内部错误码其实并不会很多 但是占用了1~9999的序列,其实是不必要的。
* 而且错误码不一定位数一定要相同。比如腾讯的微信接口错误码的位数就并不相同。按照常理错误码的数量大小应该是:
* 内部错误码< 客户端错误码< 第三方错误码< 业务错误码
* 所以我们应该尽可能的把错误码的数量留给业务错误码
* ---------------------------
* *******新的设计**********
* 1~99 为内部错误码(框架本身的错误)
* 100~999 客户端错误码 (客户端异常调用之类的错误)
* 1000~9999为第三方错误码 (代码正常,但是第三方异常)
* 10000~99999 为业务逻辑 错误码 (无异常,代码正常流转,并返回提示给用户)
* 由于系统内的错误码都是独一无二的,所以错误码应该放在common包集中管理
* ---------------------------
* 总体设计就是值越小 错误严重性越高
* 目前10000~19999是初始系统内嵌功能使用的错误码,后续开发者可以直接使用20000以上的错误码作为业务错误码
*/
SUCCESS(0, "操作成功", "SUCCESS"),
FAILED(99999, "操作失败", "FAILED");
private final int code;
private final String msg;
private final String i18nKey;
ErrorCode(int code, String msg, String i18nKey) {
this.code = code;
this.msg = msg;
this.i18nKey = i18nKey;
}
@Override
public int code() {
return this.code;
}
@Override
public String message() {
return this.msg;
}
@Override
public String i18nKey() {
return this.i18nKey;
}
/**
* 10000~99999 为业务逻辑 错误码 (无代码异常,代码正常流转,并返回提示给用户)
* 1XX01 XX是代表模块的意思 比如10101 01是Permission模块
* 错误码的命名最好以模块为开头 比如 NOT_ALLOWED_TO_OPERATE前面加上PERMISSION = PERMISSION_NOT_ALLOWED_TO_OPERATE
*/
public enum Business implements ErrorCodeInterface {
// ----------------------------- COMMON --------------------------------------
COMMON_OBJECT_NOT_FOUND(10001, "找不到ID为 {} 的 {}", "Business.OBJECT_NOT_FOUND"),
COMMON_UNSUPPORTED_OPERATION(10002, "不支持的操作", "Business.UNSUPPORTED_OPERATION"),
COMMON_BULK_DELETE_IDS_IS_INVALID(10003, "批量参数ID列表为空", "Business.BULK_DELETE_IDS_IS_INVALID"),
COMMON_FILE_NOT_ALLOWED_TO_DOWNLOAD(10004, "文件名称({})非法,不允许下载", "Business.FILE_NOT_ALLOWED_TO_DOWNLOAD"),
// ----------------------------- PERMISSION -----------------------------------
PERMISSION_FORBIDDEN_TO_MODIFY_ADMIN(10101, "不允许修改管理员的信息", "Business.FORBIDDEN_TO_MODIFY_ADMIN"),
PERMISSION_NOT_ALLOWED_TO_OPERATE(10202, "没有权限进行此操作,请联系管理员", "Business.NO_PERMISSION_TO_OPERATE"),
// ----------------------------- LOGIN -----------------------------------------
LOGIN_WRONG_USER_PASSWORD(10201, "用户密码错误,请重输", "Business.LOGIN_WRONG_USER_PASSWORD"),
LOGIN_ERROR(10202, "登录失败:{}", "Business.LOGIN_ERROR"),
LOGIN_CAPTCHA_CODE_WRONG(10203, "验证码错误", "Business.LOGIN_CAPTCHA_CODE_WRONG"),
LOGIN_CAPTCHA_CODE_EXPIRE(10204, "验证码过期", "Business.LOGIN_CAPTCHA_CODE_EXPIRE"),
LOGIN_CAPTCHA_CODE_NULL(10205, "验证码为空", "Business.LOGIN_CAPTCHA_CODE_NULL"),
// ----------------------------- UPLOAD -----------------------------------------
UPLOAD_FILE_TYPE_NOT_ALLOWED(10401, "不允许上传的文件类型,仅允许:{}", "Business.UPLOAD_FILE_TYPE_NOT_ALLOWED"),
UPLOAD_FILE_NAME_EXCEED_MAX_LENGTH(10402, "文件名长度超过:{} ", "Business.UPLOAD_FILE_NAME_EXCEED_MAX_LENGTH"),
UPLOAD_FILE_SIZE_EXCEED_MAX_SIZE(10403, "文件名大小超过:{} MB", "Business.UPLOAD_FILE_SIZE_EXCEED_MAX_SIZE"),
UPLOAD_IMPORT_EXCEL_FAILED(10404, "导入excel失败:{}", "Business.UPLOAD_IMPORT_EXCEL_FAILED"),
UPLOAD_FILE_IS_EMPTY(10405, "上传文件为空", "Business.UPLOAD_FILE_IS_EMPTY"),
UPLOAD_FILE_FAILED(10406, "上传文件失败:{}", "Business.UPLOAD_FILE_FAILED"),
// ----------------------------- CONFIG -----------------------------------------
CONFIG_VALUE_IS_NOT_ALLOW_TO_EMPTY(10601, "参数键值不允许为空", "Business.CONFIG_VALUE_IS_NOT_ALLOW_TO_EMPTY"),
CONFIG_VALUE_IS_NOT_IN_OPTIONS(10602, "参数键值不存在列表中", "Business.CONFIG_VALUE_IS_NOT_IN_OPTIONS"),
// ------------------------------- POST --------------------------------------------
POST_NAME_IS_NOT_UNIQUE(10701, "岗位名称:{}, 已存在", "Business.POST_NAME_IS_NOT_UNIQUE"),
POST_CODE_IS_NOT_UNIQUE(10702, "岗位编号:{}, 已存在", "Business.POST_CODE_IS_NOT_UNIQUE"),
POST_ALREADY_ASSIGNED_TO_USER_CAN_NOT_BE_DELETED(10703, "职位已分配给用户,请先取消分配再删除", "Business.POST_ALREADY_ASSIGNED_TO_USER_CAN_NOT_BE_DELETED"),
// ------------------------------- DEPT ---------------------------------------------
DEPT_NAME_IS_NOT_UNIQUE(10801, "部门名称:{}, 已存在", "Business.DEPT_NAME_IS_NOT_UNIQUE"),
DEPT_PARENT_ID_IS_NOT_ALLOWED_SELF(10802, "父级部门不能选择自己", "Business.DEPT_PARENT_ID_IS_NOT_ALLOWED_SELF"),
DEPT_STATUS_ID_IS_NOT_ALLOWED_CHANGE(10803, "子部门还有正在启用的部门,暂时不能停用该部门", "Business.DEPT_STATUS_ID_IS_NOT_ALLOWED_CHANGE"),
DEPT_EXIST_CHILD_DEPT_NOT_ALLOW_DELETE(10804, "该部门存在下级部门不允许删除", "Business.DEPT_EXIST_CHILD_DEPT_NOT_ALLOW_DELETE"),
DEPT_EXIST_LINK_USER_NOT_ALLOW_DELETE(10805, "该部门存在关联的用户不允许删除", "Business.DEPT_EXIST_LINK_USER_NOT_ALLOW_DELETE"),
DEPT_PARENT_DEPT_NO_EXIST_OR_DISABLED(10806, "该父级部门不存在或已停用", "Business.DEPT_PARENT_DEPT_NO_EXIST_OR_DISABLED"),
// ------------------------------- MENU -------------------------------------------------
MENU_NAME_IS_NOT_UNIQUE(10901, "新增菜单:{} 失败,菜单名称已存在", "Business.MENU_NAME_IS_NOT_UNIQUE"),
MENU_EXTERNAL_LINK_MUST_BE_HTTP(10902, "菜单外链必须以 http(s)://开头", "Business.MENU_EXTERNAL_LINK_MUST_BE_HTTP"),
MENU_PARENT_ID_NOT_ALLOW_SELF(10903, "父级菜单不能选择自身", "Business.MENU_PARENT_ID_NOT_ALLOW_SELF"),
MENU_EXIST_CHILD_MENU_NOT_ALLOW_DELETE(10904, "存在子菜单不允许删除", "Business.MENU_EXIST_CHILD_MENU_NOT_ALLOW_DELETE"),
MENU_ALREADY_ASSIGN_TO_ROLE_NOT_ALLOW_DELETE(10905, "菜单已分配给角色,不允许", "Business.MENU_ALREADY_ASSIGN_TO_ROLE_NOT_ALLOW_DELETE"),
MENU_NOT_ALLOWED_TO_CREATE_BUTTON_ON_IFRAME_OR_OUT_LINK(10906, "不允许在Iframe和外链跳转类型下创建按钮", "Business.MENU_ONLY_ALLOWED_TO_CREATE_BUTTON_ON_PAGE"),
MENU_ONLY_ALLOWED_TO_CREATE_SUB_MENU_IN_CATALOG(10907, "只允许在目录类型底下创建子菜单", "Business.MENU_ONLY_ALLOWED_TO_CREATE_SUB_MENU_IN_CATALOG"),
MENU_CAN_NOT_CHANGE_MENU_TYPE(10908, "不允许更改菜单的类型", "Business.MENU_CAN_NOT_CHANGE_MENU_TYPE"),
// -------------------------------- ROLE -------------------------------------------------
ROLE_NAME_IS_NOT_UNIQUE(11001, "角色名称:{}, 已存在", "Business.ROLE_NAME_IS_NOT_UNIQUE"),
ROLE_KEY_IS_NOT_UNIQUE(11002, "角色标识:{}, 已存在", "Business.ROLE_KEY_IS_NOT_UNIQUE"),
ROLE_DATA_SCOPE_DUPLICATED_DEPT(11003, "重复的部门id", "Business.ROLE_DATA_SCOPE_DUPLICATED_DEPT"),
ROLE_ALREADY_ASSIGN_TO_USER(11004, "角色已分配给用户,请先取消分配,再删除角色", "Business.ROLE_ALREADY_ASSIGN_TO_USER"),
ROLE_IS_NOT_AVAILABLE(11005, "角色:{} 已禁用,无法分配给用户", "Business.ROLE_IS_NOT_AVAILABLE"),
// ---------------------------------- USER -----------------------------------------------
USER_NON_EXIST(10501, "登录用户:{} 不存在", "Business.USER_NON_EXIST"),
USER_IS_DISABLE(10502, "对不起, 您的账号:{} 已停用", "Business.USER_IS_DISABLE"),
USER_CACHE_IS_EXPIRE(11003, "用户缓存信息已经过期", "Business.USER_CACHE_IS_EXPIRE"),
USER_FAIL_TO_GET_USER_ID(11004, "获取用户ID失败", "Business.USER_FAIL_TO_GET_USER_ID"),
USER_FAIL_TO_GET_DEPT_ID(10504, "获取用户部门ID失败", "Business.USER_FAIL_TO_GET_DEPT_ID"),
USER_FAIL_TO_GET_ACCOUNT(10505, "获取用户账户失败", "Business.USER_FAIL_TO_GET_ACCOUNT"),
USER_FAIL_TO_GET_USER_INFO(10506, "获取用户信息失败", "Business.USER_FAIL_TO_GET_USER_INFO"),
USER_IMPORT_DATA_IS_NULL(10507, "导入的用户为空", "Business.USER_IMPORT_DATA_IS_NULL"),
USER_PHONE_NUMBER_IS_NOT_UNIQUE(10508, "该电话号码已被其他用户占用", "Business.USER_PHONE_NUMBER_IS_NOT_UNIQUE"),
USER_EMAIL_IS_NOT_UNIQUE(10509, "该邮件地址已被其他用户占用", "Business.USER_EMAIL_IS_NOT_UNIQUE"),
USER_PASSWORD_IS_NOT_CORRECT(10510, "用户密码错误", "Business.USER_PASSWORD_IS_NOT_CORRECT"),
USER_NEW_PASSWORD_IS_THE_SAME_AS_OLD(10511, "用户新密码与旧密码相同", "Business.USER_NEW_PASSWORD_IS_THE_SAME_AS_OLD"),
USER_UPLOAD_FILE_FAILED(10512, "用户上传文件失败", "Business.USER_UPLOAD_FILE_FAILED"),
USER_NAME_IS_NOT_UNIQUE(10513, "用户名已被其他用户占用", "Business.USER_NAME_IS_NOT_UNIQUE"),
USER_CURRENT_USER_CAN_NOT_BE_DELETE(10514, "当前用户不允许被删除", "Business.USER_CURRENT_USER_CAN_NOT_BE_DELETE"),
USER_ADMIN_CAN_NOT_BE_MODIFY(10515, "管理员不允许做任何修改", "Business.USER_ADMIN_CAN_NOT_BE_MODIFY"),
;
private final int code;
private final String msg;
private final String i18nKey;
Business(int code, String msg, String i18nKey) {
Assert.isTrue(code > 10000 && code < 99999,
"错误码code值定义失败,Business错误码code值范围在10000~99099之间,请查看ErrorCode.Business类,当前错误码码为" + name());
String errorTypeName = this.getClass().getSimpleName();
Assert.isTrue(i18nKey != null && i18nKey.startsWith(errorTypeName),
String.format("错误码i18nKey值定义失败,%s错误码i18nKey值必须以%s开头,当前错误码为%s", errorTypeName, errorTypeName, name()));
this.code = code;
this.msg = msg;
this.i18nKey = i18nKey;
}
@Override
public int code() {
return this.code;
}
@Override
public String message() {
return this.msg;
}
@Override
public String i18nKey() {
return i18nKey;
}
}
/**
* 1000~9999是外部错误码 比如调用支付失败
*/
public enum External implements ErrorCodeInterface {
/**
* 支付宝调用失败
*/
FAIL_TO_PAY_ON_ALIPAY(1001, "支付宝调用失败", "External.FAIL_TO_PAY_ON_ALIPAY");
private final int code;
private final String msg;
private final String i18nKey;
External(int code, String msg, String i18nKey) {
Assert.isTrue(code > 1000 && code < 9999,
"错误码code值定义失败,External错误码code值范围在1000~9999之间,请查看ErrorCode.External类,当前错误码码为" + name());
String errorTypeName = this.getClass().getSimpleName();
Assert.isTrue(i18nKey != null && i18nKey.startsWith(errorTypeName),
String.format("错误码i18nKey值定义失败,%s错误码i18nKey值必须以%s开头,当前错误码为%s", errorTypeName, errorTypeName, name()));
this.code = code;
this.msg = msg;
this.i18nKey = i18nKey;
}
@Override
public int code() {
return this.code;
}
@Override
public String message() {
return this.msg;
}
@Override
public String i18nKey() {
return this.i18nKey;
}
}
/**
* 100~999是客户端错误码
* 客户端如 Web+小程序+手机端 调用出错
* 可能由于参数问题或者授权问题或者调用过去频繁
*/
public enum Client implements ErrorCodeInterface {
COMMON_FORBIDDEN_TO_CALL(101, "禁止调用", "Client.COMMON_FORBIDDEN_TO_CALL"),
COMMON_REQUEST_TOO_OFTEN(102, "调用太过频繁", "Client.COMMON_REQUEST_TOO_OFTEN"),
COMMON_REQUEST_PARAMETERS_INVALID(103, "请求参数异常,{}", "Client.COMMON_REQUEST_PARAMETERS_INVALID"),
COMMON_REQUEST_METHOD_INVALID(104, "请求方式: {} 不支持", "Client.COMMON_REQUEST_METHOD_INVALID"),
COMMON_REQUEST_RESUBMIT(105, "请求重复提交", "Client.COMMON_REQUEST_RESUBMIT"),
COMMON_NO_AUTHORIZATION(106, "请求接口:{} 失败,用户未授权", "Client.COMMON_NO_AUTHORIZATION"),
INVALID_TOKEN(107, "token异常", "Client.INVALID_TOKEN"),
TOKEN_PROCESS_FAILED(108, "token处理失败:{}", "Client.TOKEN_PROCESS_FAILED"),
;
private final int code;
private final String msg;
private final String i18nKey;
Client(int code, String msg, String i18nKey) {
Assert.isTrue(code > 100 && code < 999,
"错误码code值定义失败,Client错误码code值范围在100~999之间,请查看ErrorCode.Client类,当前错误码码为" + name());
String errorTypeName = this.getClass().getSimpleName();
Assert.isTrue(i18nKey != null && i18nKey.startsWith(errorTypeName),
String.format("错误码i18nKey值定义失败,%s错误码i18nKey值必须以%s开头,当前错误码为%s", errorTypeName, errorTypeName, name()));
this.code = code;
this.msg = msg;
this.i18nKey = i18nKey;
}
@Override
public int code() {
return this.code;
}
@Override
public String message() {
return this.msg;
}
@Override
public String i18nKey() {
return this.i18nKey;
}
}
/**
* 0~99是内部错误码 例如 框架内部问题之类的
*/
public enum Internal implements ErrorCodeInterface {
/**
* 内部错误码
*/
INVALID_PARAMETER(1, "参数异常:{}", "Internal.INVALID_PARAMETER"),
/**
* 该错误主要用于返回 未知的异常(大部分是RuntimeException) 程序未能捕获 未能预料的错误
*/
INTERNAL_ERROR(2, "系统内部错误:{}", "Internal.INTERNAL_ERROR"),
GET_ENUM_FAILED(3, "获取枚举类型失败, 枚举类:{}", "Internal.GET_ENUM_FAILED"),
GET_CACHE_FAILED(4, "获取缓存失败:{}", "Internal.GET_CACHE_FAILED"),
DB_INTERNAL_ERROR(5, "数据库异常", "Internal.DB_INTERNAL_ERROR"),
LOGIN_CAPTCHA_GENERATE_FAIL(7, "验证码生成失败", "Internal.LOGIN_CAPTCHA_GENERATE_FAIL"),
EXCEL_PROCESS_ERROR(8, "excel处理失败:{}", "Internal.EXCEL_PROCESS_ERROR"),
;
private final int code;
private final String msg;
private final String i18nKey;
Internal(int code, String msg, String i18nKey) {
Assert.isTrue(code < 100,
"错误码code值定义失败,Internal错误码code值范围在100~999之间,请查看ErrorCode.Internal类,当前错误码码为" + name());
String errorTypeName = this.getClass().getSimpleName();
Assert.isTrue(i18nKey != null && i18nKey.startsWith(errorTypeName),
String.format("错误码i18nKey值定义失败,%s错误码i18nKey值必须以%s开头,当前错误码为%s", errorTypeName, errorTypeName, name()));
this.code = code;
this.msg = msg;
this.i18nKey = i18nKey;
}
@Override
public int code() {
return this.code;
}
@Override
public String message() {
return this.msg;
}
@Override
public String i18nKey() {
return this.i18nKey;
}
}
}
@@ -0,0 +1,26 @@
package com.agileboot.common.exception.error;
/**
* @author valarchie
*/
public interface ErrorCodeInterface {
/**
* 返回错误码
* @return 错误码
*/
int code();
/**
* 返回具体的详细错误描述
* @return 错误描述
*/
String message();
/**
* i18n资源文件的key, 详见messages.properties文件
* @return key
*/
String i18nKey();
}
@@ -0,0 +1,77 @@
package com.agileboot.common.utils;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 客户端工具类
*
* @author ruoyi
*/
@Slf4j
public class ServletHolderUtil {
private ServletHolderUtil() {
}
/**
* 获取request
*/
public static HttpServletRequest getRequest() {
return getRequestAttributes().getRequest();
}
/**
* 获取response
*/
public static HttpServletResponse getResponse() {
return getRequestAttributes().getResponse();
}
public static ServletRequestAttributes getRequestAttributes() {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
return (ServletRequestAttributes) attributes;
}
/**
* 将字符串渲染到客户端
*
* @param response 渲染对象
* @param string 待渲染的字符串
*/
public static void renderString(HttpServletResponse response, String string) {
try {
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(string);
} catch (IOException e) {
log.error("返回response失败", e);
}
}
/**
* 获取仅含有项目根路径的url
* 比如 localhost:8080/agileboot/user/list
* 返回 localhost:8080/agileboot
* @return localhost:8080/agileboot
*/
public static String getContextUrl() {
HttpServletRequest request = getRequest();
StringBuffer url = request.getRequestURL();
String contextPath = request.getServletContext().getContextPath();
String strip = StrUtil.strip(url, null, request.getRequestURI());
return strip + contextPath;
}
}

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