commit cdee21ee8e920f09add4727aa6150525066ad44e Author: gin Date: Thu May 7 18:39:00 2026 +0800 feat: initial commit diff --git a/.githooks/commit-msg b/.githooks/commit-msg new file mode 100755 index 0000000..31f3e97 --- /dev/null +++ b/.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 " [optional scope][optional !]: " + 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 diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100644 index 0000000..0f19083 --- /dev/null +++ b/.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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..cae1036 --- /dev/null +++ b/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. diff --git a/backend/.github/ISSUE_TEMPLATE/bug_report.md b/backend/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..ab0e815 --- /dev/null +++ b/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** +任何其他你认为有助于排查错误的信息,或者你的猜测。 diff --git a/backend/.github/ISSUE_TEMPLATE/feature_request.md b/backend/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..913a980 --- /dev/null +++ b/backend/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: 功能建议 +about: 关于该项目的建议 +title: '' +labels: '' +assignees: '' + +--- + +**您的功能请求是否与问题相关? 请描述。** +清楚简明地描述问题所在。 + +**描述您想要的解决方案** +对您所设想的问题的清晰简洁的描述。 + +**描述您考虑过的替代方案** +对您考虑过的任何替代解决方案或功能的清晰简洁的描述。 + +**附加上下文** +在此处添加有关功能请求的任何其他上下文或屏幕截图。 diff --git a/backend/.github/workflows/ci-cd.yml b/backend/.github/workflows/ci-cd.yml new file mode 100644 index 0000000..4a7a5a7 --- /dev/null +++ b/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 }}' \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..f6e0209 --- /dev/null +++ b/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 + diff --git a/backend/.mvn/wrapper/maven-wrapper.jar b/backend/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..cb28b0e Binary files /dev/null and b/backend/.mvn/wrapper/maven-wrapper.jar differ diff --git a/backend/.mvn/wrapper/maven-wrapper.properties b/backend/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..d36117e --- /dev/null +++ b/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 diff --git a/backend/GoogleStyle.xml b/backend/GoogleStyle.xml new file mode 100644 index 0000000..c2e1d68 --- /dev/null +++ b/backend/GoogleStyle.xml @@ -0,0 +1,567 @@ + + + \ No newline at end of file diff --git a/backend/LICENSE b/backend/LICENSE new file mode 100644 index 0000000..50fca70 --- /dev/null +++ b/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. diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..9ca2de4 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,344 @@ + +

+ Downloads + Build Status + Build Status + Downloads + + Downloads + + + Downloads + +

+

+ +logo +

+

AgileBoot v2.0.0

+

基于SpringBoot+Vue3前后端分离的Java快速开发脚手架

+

+

+ +## ⚡平台简介⚡ + +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) 点击按钮入群。 + + +如果觉得该项目对您有帮助,可以小额捐赠支持本项目演示网站服务器等费用~ + + +logo + +## 💕 特别鸣谢 + + +- @pokr 感谢提供ChatGpt账号助力本项目开发 + +## 💒 相关框架 +- 基于node.js开发的后端 Midwayjs diff --git a/backend/agileboot-admin/pom.xml b/backend/agileboot-admin/pom.xml new file mode 100644 index 0000000..d53826a --- /dev/null +++ b/backend/agileboot-admin/pom.xml @@ -0,0 +1,72 @@ + + + + agileboot + com.agileboot + 1.0.0 + + jar + 4.0.0 + + agileboot-admin + + + web服务入口 + + + + + + + com.agileboot + agileboot-domain + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven.surefire.plugin.version} + + + false + + + + + + + + diff --git a/backend/agileboot-admin/src/main/java/com/agileboot/admin/AgileBootAdminApplication.java b/backend/agileboot-admin/src/main/java/com/agileboot/admin/AgileBootAdminApplication.java new file mode 100644 index 0000000..7fdfd4c --- /dev/null +++ b/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); + } +} diff --git a/backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/common/FileController.java b/backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/common/FileController.java new file mode 100644 index 0000000..7290371 --- /dev/null +++ b/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 fileDownload(String fileName, HttpServletResponse response) { + try { + if (!FileUploadUtils.isAllowDownload(fileName)) { + // 返回类型是ResponseEntity 不能捕获异常, 需要手动将错误填到 ResponseEntity + ResponseDTO 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 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> uploadFiles(List files) { + if (CollUtil.isEmpty(files)) { + throw new ApiException(ErrorCode.Business.UPLOAD_FILE_IS_EMPTY); + } + + List 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); + } + +} diff --git a/backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/common/LoginController.java b/backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/common/LoginController.java new file mode 100644 index 0000000..6c365ca --- /dev/null +++ b/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 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 getCaptchaImg() { + CaptchaDTO captchaImg = loginService.generateCaptchaImg(); + return ResponseDTO.ok(captchaImg); + } + + /** + * 登录方法 + * + * @param loginCommand 登录信息 + * @return 结果 + */ + @Operation(summary = "登录") + @PostMapping("/login") + public ResponseDTO 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 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> getRouters() { + SystemLoginUser loginUser = AuthenticationUtils.getSystemLoginUser(); + List routerTree = menuApplicationService.getRouterTree(loginUser); + return ResponseDTO.ok(routerTree); + } + + + @Operation(summary = "注册接口", description = "暂未实现") + @PostMapping("/register") + public ResponseDTO register(@RequestBody AddUserCommand command) { + return ResponseDTO.fail(new ApiException(Business.COMMON_UNSUPPORTED_OPERATION)); + } + +} diff --git a/backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/MonitorController.java b/backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/MonitorController.java new file mode 100644 index 0000000..a0d29cf --- /dev/null +++ b/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 getRedisCacheInfo() { + RedisCacheInfoDTO redisCacheInfo = monitorApplicationService.getRedisCacheInfo(); + return ResponseDTO.ok(redisCacheInfo); + } + + + @Operation(summary = "服务器信息") + @PreAuthorize("@permission.has('monitor:server:list')") + @GetMapping("/serverInfo") + public ResponseDTO 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> onlineUsers(String ipAddress, String username) { + List 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 logoutOnlineUser(@PathVariable String tokenId) { + CacheCenter.loginUserCache.delete(tokenId); + return ResponseDTO.ok(); + } + + +} diff --git a/backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysConfigController.java b/backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysConfigController.java new file mode 100644 index 0000000..38072ab --- /dev/null +++ b/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> list(ConfigQuery query) { + PageDTO page = configApplicationService.getConfigList(query); + return ResponseDTO.ok(page); + } + + /** + * 根据参数编号获取详细信息 + */ + @PreAuthorize("@permission.has('system:config:query')") + @GetMapping(value = "/config/{configId}") + @Operation(summary = "配置信息", description = "配置的详细信息") + public ResponseDTO 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 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 refreshCache() { + CacheCenter.configCache.invalidateAll(); + return ResponseDTO.ok(); + } +} diff --git a/backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysDeptController.java b/backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysDeptController.java new file mode 100644 index 0000000..21f7860 --- /dev/null +++ b/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(DeptQuery query) { + List deptList = deptApplicationService.getDeptList(query); + return ResponseDTO.ok(deptList); + } + + /** + * 根据部门编号获取详细信息 + */ + @Operation(summary = "部门详情") + @PreAuthorize("@permission.has('system:dept:query')") + @GetMapping(value = "/dept/{deptId}") + public ResponseDTO getInfo(@PathVariable Long deptId) { + DeptDTO dept = deptApplicationService.getDeptInfo(deptId); + return ResponseDTO.ok(dept); + } + + /** + * 获取部门下拉树列表 + */ + @Operation(summary = "获取部门树级结构") + @GetMapping("/depts/dropdown") + public ResponseDTO>> dropdownList() { + List> deptTree = deptApplicationService.getDeptTree(); + return ResponseDTO.ok(deptTree); + } + + /** + * 新增部门 + */ + @Operation(summary = "新增部门") + @PreAuthorize("@permission.has('system:dept:add')") + @AccessLog(title = "部门管理", businessType = BusinessTypeEnum.ADD) + @PostMapping("/dept") + public ResponseDTO 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 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 remove(@PathVariable @NotNull Long deptId) { + deptApplicationService.removeDept(deptId); + return ResponseDTO.ok(); + } +} diff --git a/backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysLogsController.java b/backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysLogsController.java new file mode 100644 index 0000000..741cc09 --- /dev/null +++ b/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> loginInfoList(LoginLogQuery query) { + PageDTO 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 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 removeLoginInfos(@RequestParam @NotNull @NotEmpty List ids) { + logApplicationService.deleteLoginInfo(new BulkOperationCommand<>(ids)); + return ResponseDTO.ok(); + } + + @Operation(summary = "操作日志列表") + @PreAuthorize("@permission.has('monitor:operlog:list')") + @GetMapping("/operationLogs") + public ResponseDTO> operationLogs(OperationLogQuery query) { + PageDTO pageDTO = logApplicationService.getOperationLogList(query); + return ResponseDTO.ok(pageDTO); + } + +// @GetMapping("/download") +// public ResponseEntity 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 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 removeOperationLogs(@RequestParam List operationIds) { + logApplicationService.deleteOperationLog(new BulkOperationCommand<>(operationIds)); + return ResponseDTO.ok(); + } + + +} diff --git a/backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysMenuController.java b/backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysMenuController.java new file mode 100644 index 0000000..9469f7e --- /dev/null +++ b/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> menuList(MenuQuery menuQuery) { + List menuList = menuApplicationService.getMenuList(menuQuery); + return ResponseDTO.ok(menuList); + } + + /** + * 根据菜单编号获取详细信息 + */ + @Operation(summary = "菜单详情") + @PreAuthorize("@permission.has('system:menu:query')") + @GetMapping(value = "/{menuId}") + public ResponseDTO menuInfo(@PathVariable @NotNull @PositiveOrZero Long menuId) { + MenuDetailDTO menu = menuApplicationService.getMenuInfo(menuId); + return ResponseDTO.ok(menu); + } + + /** + * 获取菜单下拉树列表 + */ + @Operation(summary = "菜单列表(树级)", description = "菜单树级下拉框") + @GetMapping("/dropdown") + public ResponseDTO>> dropdownList() { + SystemLoginUser loginUser = AuthenticationUtils.getSystemLoginUser(); + List> 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 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 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 remove(@PathVariable("menuId") Long menuId) { + menuApplicationService.remove(menuId); + return ResponseDTO.ok(); + } + +} diff --git a/backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysNoticeController.java b/backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysNoticeController.java new file mode 100644 index 0000000..2e149c0 --- /dev/null +++ b/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> list(NoticeQuery query) { + PageDTO pageDTO = noticeApplicationService.getNoticeList(query); + return ResponseDTO.ok(pageDTO); + } + + /** + * 获取通知公告列表 + * 从从库获取数据 例子 仅供参考 + */ + @Operation(summary = "公告列表(从数据库从库获取)", description = "演示主从库的例子") + @DS("slave") + @PreAuthorize("@permission.has('system:notice:list')") + @GetMapping("/database/slave") + public ResponseDTO> listFromSlave(NoticeQuery query) { + PageDTO pageDTO = noticeApplicationService.getNoticeList(query); + return ResponseDTO.ok(pageDTO); + } + + /** + * 根据通知公告编号获取详细信息 + */ + @Operation(summary = "公告详情") + @PreAuthorize("@permission.has('system:notice:query')") + @GetMapping(value = "/{noticeId}") + public ResponseDTO 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 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 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 remove(@RequestParam List noticeIds) { + noticeApplicationService.deleteNotice(new BulkOperationCommand<>(noticeIds)); + return ResponseDTO.ok(); + } + + +} diff --git a/backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysPostController.java b/backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysPostController.java new file mode 100644 index 0000000..a3fb248 --- /dev/null +++ b/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> list(PostQuery query) { + PageDTO 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 all = postApplicationService.getPostListAll(query); + CustomExcelUtil.writeToResponse(all, PostDTO.class, response); + } + + /** + * 根据岗位编号获取详细信息 + */ + @Operation(summary = "职位详情") + @PreAuthorize("@permission.has('system:post:query')") + @GetMapping(value = "/{postId}") + public ResponseDTO 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 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 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 remove(@RequestParam @NotNull @NotEmpty List ids) { + postApplicationService.deletePost(new BulkOperationCommand<>(ids)); + return ResponseDTO.ok(); + } + +} diff --git a/backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysProfileController.java b/backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysProfileController.java new file mode 100644 index 0000000..36905c8 --- /dev/null +++ b/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 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 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 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 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)); + } +} diff --git a/backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysRoleController.java b/backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysRoleController.java new file mode 100644 index 0000000..75c12d9 --- /dev/null +++ b/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> list(RoleQuery query) { + PageDTO 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 pageDTO = roleApplicationService.getRoleList(query); + CustomExcelUtil.writeToResponse(pageDTO.getRows(), RoleDTO.class, response); + } + + /** + * 根据角色编号获取详细信息 + */ + @Operation(summary = "角色详情") + @PreAuthorize("@permission.has('system:role:query')") + @GetMapping(value = "/{roleId}") + public ResponseDTO 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 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 remove(@PathVariable("roleId") List roleIds) { + roleApplicationService.deleteRoleByBulk(roleIds); + return ResponseDTO.ok(); + } + + /** + * 修改保存角色 + */ + @Operation(summary = "修改角色") + @PreAuthorize("@permission.has('system:role:edit')") + @AccessLog(title = "角色管理", businessType = BusinessTypeEnum.MODIFY) + @PutMapping + public ResponseDTO 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 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 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> allocatedUserList(@PathVariable("roleId") Long roleId, + AllocatedRoleQuery query) { + query.setRoleId(roleId); + PageDTO page = roleApplicationService.getAllocatedUserList(query); + return ResponseDTO.ok(page); + } + + /** + * 查询未分配用户角色列表 + */ + @Operation(summary = "未关联该角色的用户列表") + @PreAuthorize("@permission.has('system:role:list')") + @GetMapping("/{roleId}/unallocated/list") + public ResponseDTO> unallocatedUserList(@PathVariable("roleId") Long roleId, + UnallocatedRoleQuery query) { + query.setRoleId(roleId); + PageDTO 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 deleteRoleOfUserByBulk(@PathVariable("userIds") List 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 addRoleForUserByBulk(@PathVariable("roleId") Long roleId, + @PathVariable("userIds") List userIds) { + roleApplicationService.addRoleOfUserByBulk(roleId, userIds); + return ResponseDTO.ok(); + } + +} diff --git a/backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysUserController.java b/backend/agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysUserController.java new file mode 100644 index 0000000..8863560 --- /dev/null +++ b/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> userList(SearchUserQuery query) { + PageDTO 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 query) { + PageDTO 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 importUserByExcel(MultipartFile file) { + List 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 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 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 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 remove(@PathVariable List userIds) { + BulkOperationCommand 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 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 changeStatus(@PathVariable Long userId, @RequestBody ChangeStatusCommand command) { + command.setUserId(userId); + userApplicationService.changeUserStatus(command); + return ResponseDTO.ok(); + } + + +} diff --git a/backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/aop/accessLog/AccessLog.java b/backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/aop/accessLog/AccessLog.java new file mode 100644 index 0000000..a00cd7c --- /dev/null +++ b/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; +} diff --git a/backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/aop/accessLog/AccessLogAspect.java b/backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/aop/accessLog/AccessLogAspect.java new file mode 100644 index 0000000..75145de --- /dev/null +++ b/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); + } + } + + +} diff --git a/backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/aop/accessLog/OperationLogModel.java b/backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/aop/accessLog/OperationLogModel.java new file mode 100644 index 0000000..b1d3da2 --- /dev/null +++ b/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; + } + +} diff --git a/backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/async/AsyncTaskFactory.java b/backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/async/AsyncTaskFactory.java new file mode 100644 index 0000000..a5f66c9 --- /dev/null +++ b/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); + }; + } + +} diff --git a/backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/config/JwtAuthenticationTokenFilter.java b/backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/config/JwtAuthenticationTokenFilter.java new file mode 100644 index 0000000..6fa0d86 --- /dev/null +++ b/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); + } + +} diff --git a/backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/config/SecurityConfig.java b/backend/agileboot-admin/src/main/java/com/agileboot/admin/customize/config/SecurityConfig.java new file mode 100644 index 0000000..e8a6cc4 --- /dev/null +++ b/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 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 响应头,用于防止网页被嵌入到其他网页的 、