瀏覽代碼

feat: initial commit

gin 6 天之前
當前提交
cdee21ee8e
共有 100 個文件被更改,包括 7196 次插入0 次删除
  1. 44 0
      .githooks/commit-msg
  2. 16 0
      .githooks/pre-commit
  3. 104 0
      README.md
  4. 36 0
      backend/.github/ISSUE_TEMPLATE/bug_report.md
  5. 20 0
      backend/.github/ISSUE_TEMPLATE/feature_request.md
  6. 116 0
      backend/.github/workflows/ci-cd.yml
  7. 50 0
      backend/.gitignore
  8. 二進制
      backend/.mvn/wrapper/maven-wrapper.jar
  9. 18 0
      backend/.mvn/wrapper/maven-wrapper.properties
  10. 567 0
      backend/GoogleStyle.xml
  11. 21 0
      backend/LICENSE
  12. 344 0
      backend/README.md
  13. 72 0
      backend/agileboot-admin/pom.xml
  14. 32 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/AgileBootAdminApplication.java
  15. 129 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/common/FileController.java
  16. 139 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/common/LoginController.java
  17. 82 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/MonitorController.java
  18. 88 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysConfigController.java
  19. 111 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysDeptController.java
  20. 120 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysLogsController.java
  21. 120 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysMenuController.java
  22. 122 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysNoticeController.java
  23. 122 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysPostController.java
  24. 97 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysProfileController.java
  25. 197 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysRoleController.java
  26. 169 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysUserController.java
  27. 45 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/aop/accessLog/AccessLog.java
  28. 59 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/aop/accessLog/AccessLogAspect.java
  29. 160 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/aop/accessLog/OperationLogModel.java
  30. 78 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/async/AsyncTaskFactory.java
  31. 53 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/config/JwtAuthenticationTokenFilter.java
  32. 164 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/config/SecurityConfig.java
  33. 222 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/login/LoginService.java
  34. 163 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/login/TokenService.java
  35. 117 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/login/UserDetailsServiceImpl.java
  36. 33 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/login/command/LoginCommand.java
  37. 15 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/login/dto/CaptchaDTO.java
  38. 18 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/login/dto/ConfigDTO.java
  39. 66 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/permission/DataPermissionCheckerFactory.java
  40. 70 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/permission/DataPermissionService.java
  41. 48 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/permission/MenuPermissionService.java
  42. 25 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/permission/model/AbstractDataPermissionChecker.java
  43. 21 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/permission/model/DataCondition.java
  44. 25 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/permission/model/checker/AllDataPermissionChecker.java
  45. 42 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/permission/model/checker/CustomDataPermissionChecker.java
  46. 25 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/permission/model/checker/DefaultDataPermissionChecker.java
  47. 44 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/permission/model/checker/DeptTreeDataPermissionChecker.java
  48. 40 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/permission/model/checker/OnlySelfDataPermissionChecker.java
  49. 42 0
      backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/permission/model/checker/SingleDeptDataPermissionChecker.java
  50. 105 0
      backend/agileboot-admin/src/main/resources/application-dev.yml
  51. 53 0
      backend/agileboot-admin/src/main/resources/application-test.yml
  52. 46 0
      backend/agileboot-admin/src/main/resources/application.yml
  53. 44 0
      backend/agileboot-admin/src/test/java/com/agileboot/admin/config/AgileBootConfigTest.java
  54. 83 0
      backend/agileboot-admin/src/test/java/com/agileboot/admin/customize/service/permission/CustomDataPermissionCheckerTest.java
  55. 92 0
      backend/agileboot-admin/src/test/java/com/agileboot/admin/customize/service/permission/DeptTreeDataPermissionCheckerTest.java
  56. 59 0
      backend/agileboot-admin/src/test/java/com/agileboot/admin/customize/service/permission/OnlySelfDataPermissionCheckerTest.java
  57. 72 0
      backend/agileboot-admin/src/test/java/com/agileboot/admin/customize/service/permission/SingleDeptDataPermissionCheckerTest.java
  58. 47 0
      backend/agileboot-api/pom.xml
  59. 32 0
      backend/agileboot-api/src/main/java/com/agileboot/api/AgileBooApiApplication.java
  60. 25 0
      backend/agileboot-api/src/main/java/com/agileboot/api/controller/OrderController.java
  61. 39 0
      backend/agileboot-api/src/main/java/com/agileboot/api/controller/app/AppController.java
  62. 38 0
      backend/agileboot-api/src/main/java/com/agileboot/api/controller/common/LoginController.java
  63. 52 0
      backend/agileboot-api/src/main/java/com/agileboot/api/customize/config/JwtAuthenticationFilter.java
  64. 85 0
      backend/agileboot-api/src/main/java/com/agileboot/api/customize/config/SecurityConfig.java
  65. 127 0
      backend/agileboot-api/src/main/java/com/agileboot/api/customize/service/JwtTokenService.java
  66. 12 0
      backend/agileboot-api/src/main/java/com/agileboot/api/customize/util/ApiEncryptor.java
  67. 28 0
      backend/agileboot-api/src/main/resources/application.yml
  68. 189 0
      backend/agileboot-common/pom.xml
  69. 19 0
      backend/agileboot-common/src/main/java/com/agileboot/common/annotation/ExcelColumn.java
  70. 20 0
      backend/agileboot-common/src/main/java/com/agileboot/common/annotation/ExcelSheet.java
  71. 109 0
      backend/agileboot-common/src/main/java/com/agileboot/common/config/AgileBootConfig.java
  72. 86 0
      backend/agileboot-common/src/main/java/com/agileboot/common/constant/Constants.java
  73. 40 0
      backend/agileboot-common/src/main/java/com/agileboot/common/core/base/BaseController.java
  74. 46 0
      backend/agileboot-common/src/main/java/com/agileboot/common/core/base/BaseEntity.java
  75. 59 0
      backend/agileboot-common/src/main/java/com/agileboot/common/core/dto/ResponseDTO.java
  76. 44 0
      backend/agileboot-common/src/main/java/com/agileboot/common/core/page/AbstractPageQuery.java
  77. 90 0
      backend/agileboot-common/src/main/java/com/agileboot/common/core/page/AbstractQuery.java
  78. 38 0
      backend/agileboot-common/src/main/java/com/agileboot/common/core/page/PageDTO.java
  79. 24 0
      backend/agileboot-common/src/main/java/com/agileboot/common/enums/BasicEnum.java
  80. 63 0
      backend/agileboot-common/src/main/java/com/agileboot/common/enums/BasicEnumUtil.java
  81. 15 0
      backend/agileboot-common/src/main/java/com/agileboot/common/enums/DictionaryEnum.java
  82. 54 0
      backend/agileboot-common/src/main/java/com/agileboot/common/enums/common/BusinessTypeEnum.java
  83. 40 0
      backend/agileboot-common/src/main/java/com/agileboot/common/enums/common/ConfigKeyEnum.java
  84. 47 0
      backend/agileboot-common/src/main/java/com/agileboot/common/enums/common/GenderEnum.java
  85. 46 0
      backend/agileboot-common/src/main/java/com/agileboot/common/enums/common/LoginStatusEnum.java
  86. 36 0
      backend/agileboot-common/src/main/java/com/agileboot/common/enums/common/MenuComponentEnum.java
  87. 38 0
      backend/agileboot-common/src/main/java/com/agileboot/common/enums/common/MenuTypeEnum.java
  88. 45 0
      backend/agileboot-common/src/main/java/com/agileboot/common/enums/common/NoticeStatusEnum.java
  89. 47 0
      backend/agileboot-common/src/main/java/com/agileboot/common/enums/common/NoticeTypeEnum.java
  90. 45 0
      backend/agileboot-common/src/main/java/com/agileboot/common/enums/common/OperationStatusEnum.java
  91. 39 0
      backend/agileboot-common/src/main/java/com/agileboot/common/enums/common/OperatorTypeEnum.java
  92. 39 0
      backend/agileboot-common/src/main/java/com/agileboot/common/enums/common/RequestMethodEnum.java
  93. 44 0
      backend/agileboot-common/src/main/java/com/agileboot/common/enums/common/StatusEnum.java
  94. 49 0
      backend/agileboot-common/src/main/java/com/agileboot/common/enums/common/UserStatusEnum.java
  95. 46 0
      backend/agileboot-common/src/main/java/com/agileboot/common/enums/common/VisibleStatusEnum.java
  96. 46 0
      backend/agileboot-common/src/main/java/com/agileboot/common/enums/common/YesOrNoEnum.java
  97. 17 0
      backend/agileboot-common/src/main/java/com/agileboot/common/enums/dictionary/CssTag.java
  98. 25 0
      backend/agileboot-common/src/main/java/com/agileboot/common/enums/dictionary/Dictionary.java
  99. 26 0
      backend/agileboot-common/src/main/java/com/agileboot/common/enums/dictionary/DictionaryData.java
  100. 75 0
      backend/agileboot-common/src/main/java/com/agileboot/common/exception/ApiException.java

+ 44 - 0
.githooks/commit-msg

@@ -0,0 +1,44 @@
+#!/bin/sh
+
+commit_msg_file="$1"
+
+if [ ! -f "$commit_msg_file" ]; then
+  echo "commit-msg hook error: commit message file not found"
+  exit 1
+fi
+
+header="$(sed -n '/^[[:space:]]*#/!{/^[[:space:]]*$/!{p;q;}}' "$commit_msg_file")"
+
+if [ -z "$header" ]; then
+  echo "Invalid commit message: empty message"
+  exit 1
+fi
+
+case "$header" in
+  Merge\ *|Revert\ *|fixup!\ *|squash!\ *)
+    exit 0
+    ;;
+esac
+
+type_pattern="feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|wip|workflow|types|release"
+commit_pattern="^($type_pattern)(\([a-z0-9][a-z0-9._/-]*\))?!?: .{1,}$"
+
+if ! printf '%s\n' "$header" | grep -Eq "$commit_pattern"; then
+  echo "Invalid commit message format."
+  echo ""
+  echo "Expected Conventional Commits format:"
+  echo "  <type>[optional scope][optional !]: <description>"
+  echo ""
+  echo "Allowed types:"
+  echo "  feat, fix, docs, style, refactor, perf, test, build, ci, chore,"
+  echo "  revert, wip, workflow, types, release"
+  echo ""
+  echo "Examples:"
+  echo "  feat: add user search"
+  echo "  fix(backend): handle token expiry"
+  echo "  docs(readme): update setup guide"
+  echo "  refactor(web)!: remove legacy auth flow"
+  exit 1
+fi
+
+exit 0

+ 16 - 0
.githooks/pre-commit

@@ -0,0 +1,16 @@
+#!/bin/sh
+
+if [ -n "$CI" ]; then
+  exit 0
+fi
+
+staged_files="$(git diff --cached --name-only --diff-filter=ACMR)"
+
+if printf '%s\n' "$staged_files" | grep -q '^frontend/'; then
+  if ! command -v pnpm >/dev/null 2>&1; then
+    echo "pre-commit hook error: pnpm is required for frontend checks"
+    exit 1
+  fi
+
+  pnpm --dir frontend lint
+fi

+ 104 - 0
README.md

@@ -0,0 +1,104 @@
+# Simple Template
+
+## Quick Start
+
+This project contains:
+
+- `backend`: Spring Boot admin API.
+- `frontend/web`: Vite + Vue 3 admin frontend.
+- `frontend/app`: Taro + Vue 3 app frontend.
+
+The local development flow starts MySQL and Redis with Docker, then runs the
+backend and web frontend locally.
+
+### Requirements
+
+- JDK 8+
+- Docker Desktop or Docker Engine with Compose
+- Node.js
+- pnpm
+
+### Start Infrastructure
+
+The development backend is configured to use the same defaults as
+`backend/docker-compose.yml`:
+
+- MySQL: `localhost:3306`, user `root`, password `root123`
+- Redis: `localhost:6379`, password `redis123`
+- Database name: `agileboot_pure`
+
+You can start MySQL and Redis manually:
+
+```bash
+cd backend
+docker compose up -d
+```
+
+On a fresh Docker volume, Compose creates the MySQL database `agileboot_pure`.
+Import the SQL files under `backend/sql/` before starting the backend.
+
+### Install Frontend Dependencies
+
+```bash
+cd frontend
+pnpm install
+```
+
+### Start Backend and Web Frontend
+
+Start the backend:
+
+```bash
+cd backend
+./mvnw -pl agileboot-admin -am spring-boot:run
+```
+
+Start the web frontend:
+
+```bash
+cd frontend
+pnpm dev:web
+```
+
+- Backend: `http://localhost:8080`
+- Frontend: Vite output, usually `http://localhost:80/`
+
+The frontend development proxy maps `/dev-api` to `http://localhost:8080`.
+
+Optional app commands:
+
+```bash
+cd frontend
+pnpm dev:app:weapp
+pnpm dev:app:h5
+```
+
+## Git Hooks
+
+This repository uses `.githooks` as the single Git hooks entrypoint.
+
+```bash
+git config core.hooksPath .githooks
+```
+
+The frontend workspace uses pnpm:
+
+```bash
+cd frontend
+pnpm install
+pnpm dev:web
+pnpm build:web
+pnpm dev:app:weapp
+pnpm build:app:weapp
+pnpm lint
+pnpm typecheck
+```
+
+## Frontend
+
+`frontend` is a pnpm workspace:
+
+- `web`: Vite + Vue 3 admin frontend.
+- `app`: Taro + Vue 3 app frontend.
+
+Shared engineering configuration lives in `frontend` root. Subprojects should extend the shared TypeScript, ESLint, Stylelint, Prettier, commitlint, and lint-staged configuration instead of duplicating it.

+ 36 - 0
backend/.github/ISSUE_TEMPLATE/bug_report.md

@@ -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 - 0
backend/.github/ISSUE_TEMPLATE/feature_request.md

@@ -0,0 +1,20 @@
+---
+name: 功能建议
+about: 关于该项目的建议
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**您的功能请求是否与问题相关? 请描述。**
+清楚简明地描述问题所在。 
+
+**描述您想要的解决方案**
+对您所设想的问题的清晰简洁的描述。
+
+**描述您考虑过的替代方案**
+对您考虑过的任何替代解决方案或功能的清晰简洁的描述。
+
+**附加上下文**
+在此处添加有关功能请求的任何其他上下文或屏幕截图。

+ 116 - 0
backend/.github/workflows/ci-cd.yml

@@ -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 - 0
backend/.gitignore

@@ -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
+

二進制
backend/.mvn/wrapper/maven-wrapper.jar


+ 18 - 0
backend/.mvn/wrapper/maven-wrapper.properties

@@ -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 - 0
backend/GoogleStyle.xml

@@ -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 - 0
backend/LICENSE

@@ -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 - 0
backend/README.md

@@ -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 - 0
backend/agileboot-admin/pom.xml

@@ -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>

+ 32 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/AgileBootAdminApplication.java

@@ -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);
+    }
+}

+ 129 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/common/FileController.java

@@ -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);
+    }
+
+}

+ 139 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/common/LoginController.java

@@ -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));
+    }
+
+}

+ 82 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/MonitorController.java

@@ -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();
+    }
+
+
+}

+ 88 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysConfigController.java

@@ -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();
+    }
+}

+ 111 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysDeptController.java

@@ -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();
+    }
+}

+ 120 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysLogsController.java

@@ -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();
+    }
+
+
+}

+ 120 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysMenuController.java

@@ -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();
+    }
+
+}

+ 122 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysNoticeController.java

@@ -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();
+    }
+
+
+}

+ 122 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysPostController.java

@@ -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();
+    }
+
+}

+ 97 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysProfileController.java

@@ -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));
+    }
+}

+ 197 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysRoleController.java

@@ -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();
+    }
+
+}

+ 169 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysUserController.java

@@ -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();
+    }
+
+
+}

+ 45 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/aop/accessLog/AccessLog.java

@@ -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;
+}

+ 59 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/aop/accessLog/AccessLogAspect.java

@@ -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);
+        }
+    }
+
+
+}

+ 160 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/aop/accessLog/OperationLogModel.java

@@ -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;
+    }
+
+}

+ 78 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/async/AsyncTaskFactory.java

@@ -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);
+        };
+    }
+
+}

+ 53 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/config/JwtAuthenticationTokenFilter.java

@@ -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);
+    }
+
+}

+ 164 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/config/SecurityConfig.java

@@ -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();
+    }
+
+
+}

+ 222 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/login/LoginService.java

@@ -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()));
+    }
+
+}

+ 163 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/login/TokenService.java

@@ -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;
+    }
+
+}

+ 117 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/login/UserDetailsServiceImpl.java

@@ -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);
+    }
+
+
+}

+ 33 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/login/command/LoginCommand.java

@@ -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;
+
+}

+ 15 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/login/dto/CaptchaDTO.java

@@ -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;
+
+}

+ 18 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/login/dto/ConfigDTO.java

@@ -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;
+
+}

+ 66 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/permission/DataPermissionCheckerFactory.java

@@ -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;
+        }
+    }
+
+}

+ 70 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/permission/DataPermissionService.java

@@ -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);
+    }
+
+
+
+}

+ 48 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/permission/MenuPermissionService.java

@@ -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));
+    }
+
+}

+ 25 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/permission/model/AbstractDataPermissionChecker.java

@@ -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);
+
+}

+ 21 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/permission/model/DataCondition.java

@@ -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;
+
+}

+ 25 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/permission/model/checker/AllDataPermissionChecker.java

@@ -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;
+    }
+}

+ 42 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/permission/model/checker/CustomDataPermissionChecker.java

@@ -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);
+    }
+}

+ 25 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/permission/model/checker/DefaultDataPermissionChecker.java

@@ -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;
+    }
+
+}

+ 44 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/permission/model/checker/DeptTreeDataPermissionChecker.java

@@ -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;
+    }
+
+}

+ 40 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/permission/model/checker/OnlySelfDataPermissionChecker.java

@@ -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);
+    }
+
+}

+ 42 - 0
backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/permission/model/checker/SingleDeptDataPermissionChecker.java

@@ -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);
+    }
+
+
+}

+ 105 - 0
backend/agileboot-admin/src/main/resources/application-dev.yml

@@ -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:\agileboot,Linux配置 /home/agileboot)
+    file-base-dir: D:\agileboot
+    # 前端url请求转发前缀
+    api-prefix: /dev-api
+    demo-enabled: false

+ 53 - 0
backend/agileboot-admin/src/main/resources/application-test.yml

@@ -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
+

+ 46 - 0
backend/agileboot-admin/src/main/resources/application.yml

@@ -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
+
+

+ 44 - 0
backend/agileboot-admin/src/test/java/com/agileboot/admin/config/AgileBootConfigTest.java

@@ -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);
+    }
+
+}

+ 83 - 0
backend/agileboot-admin/src/test/java/com/agileboot/admin/customize/service/permission/CustomDataPermissionCheckerTest.java

@@ -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);
+    }
+
+
+}

+ 92 - 0
backend/agileboot-admin/src/test/java/com/agileboot/admin/customize/service/permission/DeptTreeDataPermissionCheckerTest.java

@@ -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);
+    }
+
+
+
+
+}

+ 59 - 0
backend/agileboot-admin/src/test/java/com/agileboot/admin/customize/service/permission/OnlySelfDataPermissionCheckerTest.java

@@ -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);
+    }
+
+}

+ 72 - 0
backend/agileboot-admin/src/test/java/com/agileboot/admin/customize/service/permission/SingleDeptDataPermissionCheckerTest.java

@@ -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 - 0
backend/agileboot-api/pom.xml

@@ -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>

+ 32 - 0
backend/agileboot-api/src/main/java/com/agileboot/api/AgileBooApiApplication.java

@@ -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);
+    }
+}

+ 25 - 0
backend/agileboot-api/src/main/java/com/agileboot/api/controller/OrderController.java

@@ -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 "暂无订单";
+    }
+
+
+}

+ 39 - 0
backend/agileboot-api/src/main/java/com/agileboot/api/controller/app/AppController.java

@@ -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();
+    }
+
+
+
+
+
+
+
+}

+ 38 - 0
backend/agileboot-api/src/main/java/com/agileboot/api/controller/common/LoginController.java

@@ -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);
+    }
+
+
+
+
+}

+ 52 - 0
backend/agileboot-api/src/main/java/com/agileboot/api/customize/config/JwtAuthenticationFilter.java

@@ -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);
+    }
+}

+ 85 - 0
backend/agileboot-api/src/main/java/com/agileboot/api/customize/config/SecurityConfig.java

@@ -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();
+    }
+
+
+}

+ 127 - 0
backend/agileboot-api/src/main/java/com/agileboot/api/customize/service/JwtTokenService.java

@@ -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;
+    }
+
+}

+ 12 - 0
backend/agileboot-api/src/main/java/com/agileboot/api/customize/util/ApiEncryptor.java

@@ -0,0 +1,12 @@
+package com.agileboot.api.customize.util;
+
+public class ApiEncryptor {
+
+    public static void main(String[] args) {
+
+
+
+
+    }
+
+}

+ 28 - 0
backend/agileboot-api/src/main/resources/application.yml

@@ -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 - 0
backend/agileboot-common/pom.xml

@@ -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>

+ 19 - 0
backend/agileboot-common/src/main/java/com/agileboot/common/annotation/ExcelColumn.java

@@ -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 "";
+
+}

+ 20 - 0
backend/agileboot-common/src/main/java/com/agileboot/common/annotation/ExcelSheet.java

@@ -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 "";
+
+}

+ 109 - 0
backend/agileboot-common/src/main/java/com/agileboot/common/config/AgileBootConfig.java

@@ -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;
+    }
+
+}

+ 86 - 0
backend/agileboot-common/src/main/java/com/agileboot/common/constant/Constants.java

@@ -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";
+
+    }
+
+
+
+}

+ 40 - 0
backend/agileboot-common/src/main/java/com/agileboot/common/core/base/BaseController.java

@@ -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);
+    }
+
+
+}

+ 46 - 0
backend/agileboot-common/src/main/java/com/agileboot/common/core/base/BaseEntity.java

@@ -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;
+
+}

+ 59 - 0
backend/agileboot-common/src/main/java/com/agileboot/common/core/dto/ResponseDTO.java

@@ -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);
+//    }
+
+}
+

+ 44 - 0
backend/agileboot-common/src/main/java/com/agileboot/common/core/page/AbstractPageQuery.java

@@ -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);
+    }
+
+}

+ 90 - 0
backend/agileboot-common/src/main/java/com/agileboot/common/core/page/AbstractQuery.java

@@ -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;
+    }
+
+}

+ 38 - 0
backend/agileboot-common/src/main/java/com/agileboot/common/core/page/PageDTO.java

@@ -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;
+    }
+
+}

+ 24 - 0
backend/agileboot-common/src/main/java/com/agileboot/common/enums/BasicEnum.java

@@ -0,0 +1,24 @@
+package com.agileboot.common.enums;
+
+/**
+ * @author valarchie
+ * 普通的枚举 接口
+ * @param <T>
+ */
+public interface BasicEnum<T>{
+
+
+    /**
+     * 获取枚举的值
+     * @return 枚举值
+     */
+    T getValue();
+
+    /**
+     * 获取枚举的描述
+     * @return 描述
+     */
+    String description();
+
+
+}

+ 63 - 0
backend/agileboot-common/src/main/java/com/agileboot/common/enums/BasicEnumUtil.java

@@ -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;
+    }
+
+}

+ 15 - 0
backend/agileboot-common/src/main/java/com/agileboot/common/enums/DictionaryEnum.java

@@ -0,0 +1,15 @@
+package com.agileboot.common.enums;
+
+/**
+ * 字典类型 接口
+ * @author valarchie
+ */
+public interface DictionaryEnum<T> extends BasicEnum<T> {
+
+    /**
+     * 获取css标签
+     * @return css标签
+     */
+    String cssTag();
+
+}

+ 54 - 0
backend/agileboot-common/src/main/java/com/agileboot/common/enums/common/BusinessTypeEnum.java

@@ -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;
+    }
+
+}

+ 40 - 0
backend/agileboot-common/src/main/java/com/agileboot/common/enums/common/ConfigKeyEnum.java

@@ -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;
+    }
+
+
+}

+ 47 - 0
backend/agileboot-common/src/main/java/com/agileboot/common/enums/common/GenderEnum.java

@@ -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;
+    }
+
+}

+ 46 - 0
backend/agileboot-common/src/main/java/com/agileboot/common/enums/common/LoginStatusEnum.java

@@ -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;
+    }
+}

+ 36 - 0
backend/agileboot-common/src/main/java/com/agileboot/common/enums/common/MenuComponentEnum.java

@@ -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;
+    }
+}

+ 38 - 0
backend/agileboot-common/src/main/java/com/agileboot/common/enums/common/MenuTypeEnum.java

@@ -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;
+    }
+
+
+}

+ 45 - 0
backend/agileboot-common/src/main/java/com/agileboot/common/enums/common/NoticeStatusEnum.java

@@ -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;
+    }
+
+}

+ 47 - 0
backend/agileboot-common/src/main/java/com/agileboot/common/enums/common/NoticeTypeEnum.java

@@ -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;
+    }
+
+}

+ 45 - 0
backend/agileboot-common/src/main/java/com/agileboot/common/enums/common/OperationStatusEnum.java

@@ -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;
+    }
+
+}

+ 39 - 0
backend/agileboot-common/src/main/java/com/agileboot/common/enums/common/OperatorTypeEnum.java

@@ -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;
+    }
+
+
+}

+ 39 - 0
backend/agileboot-common/src/main/java/com/agileboot/common/enums/common/RequestMethodEnum.java

@@ -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;
+    }
+
+
+}

+ 44 - 0
backend/agileboot-common/src/main/java/com/agileboot/common/enums/common/StatusEnum.java

@@ -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;
+    }
+
+}

+ 49 - 0
backend/agileboot-common/src/main/java/com/agileboot/common/enums/common/UserStatusEnum.java

@@ -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;
+    }
+}

+ 46 - 0
backend/agileboot-common/src/main/java/com/agileboot/common/enums/common/VisibleStatusEnum.java

@@ -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;
+    }
+
+}

+ 46 - 0
backend/agileboot-common/src/main/java/com/agileboot/common/enums/common/YesOrNoEnum.java

@@ -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;
+    }
+
+}

+ 17 - 0
backend/agileboot-common/src/main/java/com/agileboot/common/enums/dictionary/CssTag.java

@@ -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() {
+    }
+}

+ 25 - 0
backend/agileboot-common/src/main/java/com/agileboot/common/enums/dictionary/Dictionary.java

@@ -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 "";
+
+
+}

+ 26 - 0
backend/agileboot-common/src/main/java/com/agileboot/common/enums/dictionary/DictionaryData.java

@@ -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();
+        }
+    }
+
+}

+ 75 - 0
backend/agileboot-common/src/main/java/com/agileboot/common/exception/ApiException.java

@@ -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;
+    }
+
+}

部分文件因文件數量過多而無法顯示