feat: EtherPMS 物业管理系统初始提交
- 后端:8个微服务(gateway/auth/base/operation/charge/notify/file/audit)+ pms-common公共模块 - 前端:Vue3 + TypeScript + Ant Design Vue 4.2 管理端(40个页面) - 数据库:7个独立库 + Flyway迁移脚本(12个SQL) - 基础设施:Docker Compose(MySQL/Redis/RabbitMQ/Nacos/Elasticsearch) - 安全:JWT RS256 + BCrypt + AES-256-GCM + Redis分布式限流 - 文档:详细设计文档 + 评审报告 + CI/CD规划方案
This commit is contained in:
commit
a471b497a5
|
|
@ -0,0 +1,44 @@
|
|||
# ─── Backend (Gradle) ───
|
||||
backend/**/build/
|
||||
backend/.gradle/
|
||||
backend/local.properties
|
||||
backend/*.iml
|
||||
backend/.idea/
|
||||
|
||||
# ─── Frontend (Node/Vite) ───
|
||||
frontend/node_modules/
|
||||
frontend/dist/
|
||||
frontend/.vite/
|
||||
frontend/*.local
|
||||
|
||||
# ─── IDE ───
|
||||
.idea/
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# ─── OS ───
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# ─── Logs ───
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# ─── Environment ───
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# ─── E2E test screenshots ───
|
||||
e2e_*.png
|
||||
|
||||
# ─── Qoder cache ───
|
||||
.qoder/
|
||||
|
||||
# ─── Docker volumes ───
|
||||
docker/data/
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
# Gradle
|
||||
.gradle/
|
||||
build/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
# IDE - IntelliJ IDEA
|
||||
.idea/
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
|
||||
# IDE - Eclipse / Spring STS
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
# IDE - VS Code
|
||||
.vscode/
|
||||
*.code-workspace
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# 日志
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# 编译输出
|
||||
target/
|
||||
*.class
|
||||
|
||||
# 包文件
|
||||
*.jar
|
||||
*.war
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
# 环境配置(敏感信息)
|
||||
application-local.yml
|
||||
application-local.properties
|
||||
application-prod.yml
|
||||
*.env
|
||||
|
||||
# 密钥文件
|
||||
*.pem
|
||||
*.key
|
||||
*.p12
|
||||
*.jks
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
// ========================================
|
||||
// 根构建文件:管理公共依赖版本与插件
|
||||
// ========================================
|
||||
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'java-library'
|
||||
id 'io.spring.dependency-management' version '1.1.5' apply false
|
||||
id 'org.springframework.boot' version "${springBootVersion}" apply false
|
||||
id 'org.flywaydb.flyway' version "${flywayVersion}" apply false
|
||||
}
|
||||
|
||||
// 所有子模块通用配置
|
||||
subprojects {
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'java-library'
|
||||
apply plugin: 'io.spring.dependency-management'
|
||||
|
||||
// 编译目标版本
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
// 仓库
|
||||
repositories {
|
||||
mavenLocal()
|
||||
maven { url 'https://maven.aliyun.com/repository/public' }
|
||||
maven { url 'https://maven.aliyun.com/repository/spring' }
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
// 统一依赖版本管理(不引入实际依赖,仅做版本约束)
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom "org.springframework.boot:spring-boot-dependencies:${springBootVersion}"
|
||||
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
|
||||
mavenBom "com.alibaba.cloud:spring-cloud-alibaba-dependencies:${springCloudAlibabaVersion}"
|
||||
}
|
||||
dependencies {
|
||||
// MyBatis-Plus
|
||||
dependency "com.baomidou:mybatis-plus-spring-boot3-starter:${mybatisPlusVersion}"
|
||||
dependency "com.baomidou:mybatis-plus-jsqlparser:${mybatisPlusVersion}"
|
||||
|
||||
// Flyway
|
||||
dependency "org.flywaydb:flyway-core:${flywayVersion}"
|
||||
dependency "org.flywaydb:flyway-mysql:${flywayVersion}"
|
||||
|
||||
// MySQL 驱动
|
||||
dependency "com.mysql:mysql-connector-j:${mysqlConnectorVersion}"
|
||||
|
||||
// JWT (jjwt)
|
||||
dependency "io.jsonwebtoken:jjwt-api:${jjwtVersion}"
|
||||
dependency "io.jsonwebtoken:jjwt-impl:${jjwtVersion}"
|
||||
dependency "io.jsonwebtoken:jjwt-jackson:${jjwtVersion}"
|
||||
|
||||
// MapStruct
|
||||
dependency "org.mapstruct:mapstruct:${mapstructVersion}"
|
||||
dependency "org.mapstruct:mapstruct-processor:${mapstructVersion}"
|
||||
|
||||
// Hutool
|
||||
dependency "cn.hutool:hutool-all:${hutoolVersion}"
|
||||
|
||||
// Guava
|
||||
dependency "com.google.guava:guava:${guavaVersion}"
|
||||
}
|
||||
}
|
||||
|
||||
// 全局公共依赖(所有模块共享)
|
||||
dependencies {
|
||||
// Lombok
|
||||
compileOnly "org.projectlombok:lombok:${lombokVersion}"
|
||||
annotationProcessor "org.projectlombok:lombok:${lombokVersion}"
|
||||
testCompileOnly "org.projectlombok:lombok:${lombokVersion}"
|
||||
testAnnotationProcessor "org.projectlombok:lombok:${lombokVersion}"
|
||||
|
||||
// MapStruct 注解处理器(与 Lombok 绑定)
|
||||
annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
|
||||
annotationProcessor "org.projectlombok:lombok-mapstruct-binding:${lombokMapstructBindingVersion}"
|
||||
|
||||
// Hutool 工具包
|
||||
implementation "cn.hutool:hutool-all:${hutoolVersion}"
|
||||
|
||||
// 测试
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
}
|
||||
|
||||
// 编译选项:保留参数名
|
||||
compileJava {
|
||||
options.encoding = 'UTF-8'
|
||||
options.compilerArgs << '-parameters'
|
||||
}
|
||||
compileTestJava {
|
||||
options.encoding = 'UTF-8'
|
||||
}
|
||||
|
||||
// 测试配置:使用 JUnit 5 Platform
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
testLogging {
|
||||
events 'passed', 'skipped', 'failed'
|
||||
showStandardStreams = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
# ========================================
|
||||
# 物业管理系统 - 开发环境中间件编排
|
||||
# 使用:docker-compose up -d
|
||||
# 停止:docker-compose down
|
||||
# ========================================
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# ====== MySQL 8.0 ======
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
container_name: pms-mysql
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3306:3306"
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root}
|
||||
TZ: Asia/Shanghai
|
||||
# 自动创建各服务数据库
|
||||
MYSQL_DATABASE: auth_db
|
||||
command: >
|
||||
--character-set-server=utf8mb4
|
||||
--collation-server=utf8mb4_unicode_ci
|
||||
--default-authentication-plugin=mysql_native_password
|
||||
--lower_case_table_names=1
|
||||
volumes:
|
||||
- mysql-data:/var/lib/mysql
|
||||
- ./docker/mysql/init:/docker-entrypoint-initdb.d
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p${MYSQL_ROOT_PASSWORD:-root}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
- pms-net
|
||||
|
||||
# ====== Redis 7 ======
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: pms-redis
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "6379:6379"
|
||||
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
- pms-net
|
||||
|
||||
# ====== RabbitMQ 3.12 ======
|
||||
rabbitmq:
|
||||
image: rabbitmq:3.12-management
|
||||
container_name: pms-rabbitmq
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "5672:5672" # AMQP 端口
|
||||
- "15672:15672" # 管理界面
|
||||
environment:
|
||||
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:-guest}
|
||||
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASS:-guest}
|
||||
volumes:
|
||||
- rabbitmq-data:/var/lib/rabbitmq
|
||||
healthcheck:
|
||||
test: ["CMD", "rabbitmq-diagnostics", "check_port_connectivity"]
|
||||
interval: 15s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
networks:
|
||||
- pms-net
|
||||
|
||||
# ====== Nacos 2.3.0 ======
|
||||
nacos:
|
||||
image: nacos/nacos-server:v2.3.0
|
||||
container_name: pms-nacos
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8848:8848" # Nacos 主端口
|
||||
- "9848:9848" # gRPC 端口
|
||||
- "9849:9849" # gRPC 端口
|
||||
environment:
|
||||
MODE: standalone
|
||||
PREFER_HOST_MODE: hostname
|
||||
SPRING_DATASOURCE_PLATFORM: ""
|
||||
NACOS_AUTH_ENABLE: "false"
|
||||
JVM_XMS: 256m
|
||||
JVM_XMX: 512m
|
||||
volumes:
|
||||
- nacos-data:/home/nacos/data
|
||||
- nacos-logs:/home/nacos/logs
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8848/nacos/v1/console/health/readiness"]
|
||||
interval: 15s
|
||||
timeout: 10s
|
||||
retries: 10
|
||||
networks:
|
||||
- pms-net
|
||||
|
||||
# ====== Elasticsearch 8.x ======
|
||||
elasticsearch:
|
||||
image: elasticsearch:8.11.0
|
||||
container_name: pms-elasticsearch
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "9200:9200"
|
||||
- "9300:9300"
|
||||
environment:
|
||||
discovery.type: single-node
|
||||
ES_JAVA_OPTS: "-Xms256m -Xmx256m"
|
||||
xpack.security.enabled: "false"
|
||||
bootstrap.memory_lock: "true"
|
||||
ulimits:
|
||||
memlock:
|
||||
soft: -1
|
||||
hard: -1
|
||||
volumes:
|
||||
- es-data:/usr/share/elasticsearch/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:9200/_cluster/health || exit 1"]
|
||||
interval: 15s
|
||||
timeout: 10s
|
||||
retries: 10
|
||||
networks:
|
||||
- pms-net
|
||||
|
||||
# ====== 数据卷 ======
|
||||
volumes:
|
||||
mysql-data:
|
||||
redis-data:
|
||||
rabbitmq-data:
|
||||
nacos-data:
|
||||
nacos-logs:
|
||||
es-data:
|
||||
|
||||
# ====== 网络 ======
|
||||
networks:
|
||||
pms-net:
|
||||
driver: bridge
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
-- ========================================
|
||||
-- 物业管理系统 - 数据库初始化脚本
|
||||
-- 创建7个微服务独立数据库
|
||||
-- ========================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS auth_db DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
CREATE DATABASE IF NOT EXISTS base_db DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
CREATE DATABASE IF NOT EXISTS operation_db DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
CREATE DATABASE IF NOT EXISTS charge_db DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
CREATE DATABASE IF NOT EXISTS notify_db DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
CREATE DATABASE IF NOT EXISTS file_db DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
CREATE DATABASE IF NOT EXISTS audit_db DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# ========================================
|
||||
# 版本属性集中管理
|
||||
# ========================================
|
||||
|
||||
# JDK 版本
|
||||
javaVersion=17
|
||||
|
||||
# Spring 家族
|
||||
springBootVersion=3.2.5
|
||||
springCloudVersion=2023.0.1
|
||||
springCloudAlibabaVersion=2023.0.1.0
|
||||
|
||||
# 持久层
|
||||
mybatisPlusVersion=3.5.5
|
||||
flywayVersion=9.22.3
|
||||
mysqlConnectorVersion=8.0.33
|
||||
|
||||
# 中间件客户端
|
||||
springAmqpVersion=3.1.5
|
||||
springDataElasticsearchVersion=5.2.5
|
||||
|
||||
# 工具库
|
||||
lombokVersion=1.18.32
|
||||
mapstructVersion=1.5.5.Final
|
||||
lombokMapstructBindingVersion=0.2.0
|
||||
hutoolVersion=5.8.27
|
||||
jjwtVersion=0.12.5
|
||||
guavaVersion=33.1.0-jre
|
||||
|
||||
# Gradle 构建
|
||||
gradleWrapperVersion=8.7
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||
networkTimeout=10000
|
||||
retries=0
|
||||
retryBackOffMs=500
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed 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
|
||||
#
|
||||
# https://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.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# gradlew start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh gradlew
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem gradlew startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables, and ensure extensions are enabled
|
||||
setlocal EnableExtensions
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
"%COMSPEC%" /c exit 1
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
"%COMSPEC%" /c exit 1
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
|
||||
|
||||
@rem Execute gradlew
|
||||
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
|
||||
@rem which allows us to clear the local environment before executing the java command
|
||||
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
|
||||
|
||||
:exitWithErrorLevel
|
||||
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
|
||||
"%COMSPEC%" /c exit %ERRORLEVEL%
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
// ========================================
|
||||
// pms-audit 审计服务模块
|
||||
// 操作审计日志、登录日志记录与查询
|
||||
// ========================================
|
||||
|
||||
apply plugin: 'org.springframework.boot'
|
||||
|
||||
dependencies {
|
||||
implementation project(':pms-common')
|
||||
|
||||
// Web(Servlet)
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||
|
||||
// 服务间调用
|
||||
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
|
||||
implementation 'org.springframework.cloud:spring-cloud-starter-loadbalancer'
|
||||
|
||||
// Nacos
|
||||
implementation 'com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-discovery'
|
||||
implementation 'com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-config'
|
||||
implementation 'org.springframework.cloud:spring-cloud-starter-bootstrap'
|
||||
|
||||
// MyBatis-Plus
|
||||
implementation "com.baomidou:mybatis-plus-spring-boot3-starter:${mybatisPlusVersion}"
|
||||
|
||||
// Flyway
|
||||
implementation 'org.flywaydb:flyway-core'
|
||||
implementation 'org.flywaydb:flyway-mysql'
|
||||
|
||||
// MySQL
|
||||
runtimeOnly "com.mysql:mysql-connector-j:${mysqlConnectorVersion}"
|
||||
|
||||
// Redis
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
|
||||
|
||||
// RabbitMQ(异步接收审计消息)
|
||||
implementation 'org.springframework.boot:spring-boot-starter-amqp'
|
||||
|
||||
// MapStruct
|
||||
implementation "org.mapstruct:mapstruct:${mapstructVersion}"
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.pms.audit;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
|
||||
/**
|
||||
* 审计服务启动类
|
||||
* 操作审计日志、登录日志记录与查询
|
||||
*/
|
||||
@SpringBootApplication(scanBasePackages = "com.pms")
|
||||
@EnableDiscoveryClient
|
||||
@EnableFeignClients(basePackages = "com.pms")
|
||||
@MapperScan({"com.pms.audit.**.mapper", "com.pms.common.mapper"})
|
||||
public class AuditServiceApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AuditServiceApplication.class, args);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.pms.audit.config;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
|
||||
import com.pms.common.config.PmsTenantLineHandler;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* MyBatis-Plus 配置
|
||||
* 乐观锁 + 多租户 + 分页插件
|
||||
* 拦截器顺序:OptimisticLocker → TenantLine → Pagination
|
||||
* 审计日志表不使用逻辑删除,实体中不声明 deleted 字段即可
|
||||
*/
|
||||
@Configuration
|
||||
public class MyBatisPlusConfig {
|
||||
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
|
||||
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new PmsTenantLineHandler()));
|
||||
PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
|
||||
pageInterceptor.setMaxLimit(100L);
|
||||
pageInterceptor.setOverflow(false);
|
||||
interceptor.addInnerInterceptor(pageInterceptor);
|
||||
return interceptor;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.pms.audit.config;
|
||||
|
||||
import com.pms.audit.constant.AuditConstants;
|
||||
import org.springframework.amqp.core.*;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* RabbitMQ 配置
|
||||
* 声明审计服务消费所需的交换机、队列及绑定关系
|
||||
*/
|
||||
@Configuration
|
||||
public class RabbitMQConfig {
|
||||
|
||||
/**
|
||||
* 审计交换机(topic 类型)
|
||||
*/
|
||||
@Bean
|
||||
public TopicExchange auditExchange() {
|
||||
return ExchangeBuilder.topicExchange(AuditConstants.EXCHANGE_AUDIT)
|
||||
.durable(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作审计日志队列
|
||||
*/
|
||||
@Bean
|
||||
public Queue auditLogQueue() {
|
||||
return QueueBuilder.durable(AuditConstants.QUEUE_AUDIT_OPERATION)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Binding auditLogBinding() {
|
||||
return BindingBuilder.bind(auditLogQueue())
|
||||
.to(auditExchange())
|
||||
.with(AuditConstants.ROUTING_KEY_AUDIT_OPERATION);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.pms.audit.config;
|
||||
|
||||
import com.pms.common.interceptor.UserContextInterceptor;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* Web MVC 配置
|
||||
* 注册用户上下文拦截器
|
||||
*/
|
||||
@Configuration
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
private final UserContextInterceptor userContextInterceptor;
|
||||
|
||||
public WebMvcConfig(UserContextInterceptor userContextInterceptor) {
|
||||
this.userContextInterceptor = userContextInterceptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(userContextInterceptor)
|
||||
.addPathPatterns("/api/v1/**")
|
||||
.excludePathPatterns("/actuator/**", "/error");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.pms.audit.constant;
|
||||
|
||||
/**
|
||||
* 审计服务常量
|
||||
*/
|
||||
public final class AuditConstants {
|
||||
|
||||
private AuditConstants() {
|
||||
}
|
||||
|
||||
// ===== 交换机 =====
|
||||
public static final String EXCHANGE_AUDIT = "exchange.audit";
|
||||
|
||||
// ===== 队列 =====
|
||||
public static final String QUEUE_AUDIT_OPERATION = "queue.audit.operation";
|
||||
|
||||
// ===== 路由键 =====
|
||||
public static final String ROUTING_KEY_AUDIT_OPERATION = "audit.operation";
|
||||
|
||||
// ===== 操作类型 =====
|
||||
public static final String OP_CREATE = "CREATE";
|
||||
public static final String OP_UPDATE = "UPDATE";
|
||||
public static final String OP_DELETE = "DELETE";
|
||||
public static final String OP_QUERY = "QUERY";
|
||||
public static final String OP_EXPORT = "EXPORT";
|
||||
public static final String OP_IMPORT = "IMPORT";
|
||||
public static final String OP_LOGIN = "LOGIN";
|
||||
public static final String OP_LOGOUT = "LOGOUT";
|
||||
public static final String OP_OTHER = "OTHER";
|
||||
|
||||
// ===== 操作结果 =====
|
||||
public static final String RESULT_SUCCESS = "SUCCESS";
|
||||
public static final String RESULT_FAIL = "FAIL";
|
||||
public static final String RESULT_ERROR = "ERROR";
|
||||
|
||||
// ===== 登录结果 =====
|
||||
public static final String LOGIN_SUCCESS = "LOGIN_SUCCESS";
|
||||
public static final String LOGIN_FAIL = "LOGIN_FAIL";
|
||||
public static final String LOGOUT = "LOGOUT";
|
||||
public static final String KICK_OUT = "KICK_OUT";
|
||||
|
||||
// ===== 幂等去重 Redis Key 前缀 =====
|
||||
public static final String IDEMPOTENT_KEY_PREFIX = "audit:idempotent:";
|
||||
|
||||
// ===== 敏感字段(脱敏) =====
|
||||
public static final String[] SENSITIVE_FIELDS = {"password", "oldPassword", "newPassword", "token", "secretKey", "bankAccount", "idCard"};
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package com.pms.audit.consumer;
|
||||
|
||||
import com.pms.audit.constant.AuditConstants;
|
||||
import com.pms.audit.dto.AuditEventMessage;
|
||||
import com.pms.audit.service.AuditLogService;
|
||||
import com.pms.common.entity.MqConsumeLog;
|
||||
import com.pms.common.mapper.MqConsumeLogMapper;
|
||||
import com.pms.common.util.JsonUtils;
|
||||
import com.rabbitmq.client.Channel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.amqp.rabbit.annotation.RabbitListener;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 审计日志消费者
|
||||
* 消费各服务通过AOP切面采集并发送的操作日志事件,异步写入数据库
|
||||
* <p>
|
||||
* 消费幂等:Redis 快速去重 + mq_consume_log 数据库持久化保障
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class AuditConsumer {
|
||||
|
||||
private static final String CONSUMER_GROUP = "audit-consumer";
|
||||
|
||||
private final AuditLogService auditLogService;
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
private final MqConsumeLogMapper mqConsumeLogMapper;
|
||||
|
||||
/**
|
||||
* 消费操作审计日志
|
||||
*/
|
||||
@RabbitListener(queues = AuditConstants.QUEUE_AUDIT_OPERATION)
|
||||
public void onAuditOperation(String message, Channel channel,
|
||||
org.springframework.amqp.core.Message amqpMessage) throws java.io.IOException {
|
||||
long deliveryTag = amqpMessage.getMessageProperties().getDeliveryTag();
|
||||
try {
|
||||
AuditEventMessage event = JsonUtils.fromJson(message, AuditEventMessage.class);
|
||||
if (event == null) {
|
||||
log.warn("审计消息解析失败,丢弃: message={}", message);
|
||||
channel.basicAck(deliveryTag, false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 幂等校验:基于 eventId 去重
|
||||
String eventId = event.getEventId();
|
||||
if (eventId == null || eventId.isBlank()) {
|
||||
eventId = "audit:" + event.getOperatorId() + ":" + event.getOperationTime();
|
||||
}
|
||||
|
||||
// 1. Redis 快速检查(快速路径)
|
||||
String idempotentKey = AuditConstants.IDEMPOTENT_KEY_PREFIX + eventId;
|
||||
Boolean isNew = stringRedisTemplate.opsForValue().setIfAbsent(idempotentKey, "1", 24, TimeUnit.HOURS);
|
||||
if (Boolean.FALSE.equals(isNew)) {
|
||||
log.debug("审计消息已消费过(Redis),跳过: eventId={}", eventId);
|
||||
channel.basicAck(deliveryTag, false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 数据库持久化检查(防止 Redis 失效后重复消费)
|
||||
if (mqConsumeLogMapper.countByEventIdAndGroup(eventId, CONSUMER_GROUP) > 0) {
|
||||
log.debug("审计消息已消费过(DB),跳过: eventId={}", eventId);
|
||||
channel.basicAck(deliveryTag, false);
|
||||
return;
|
||||
}
|
||||
|
||||
auditLogService.save(event);
|
||||
|
||||
// 3. 消费成功后记录到 mq_consume_log(持久化保障)
|
||||
MqConsumeLog consumeLog = new MqConsumeLog();
|
||||
consumeLog.setEventId(eventId);
|
||||
consumeLog.setConsumerGroup(CONSUMER_GROUP);
|
||||
consumeLog.setQueueName(AuditConstants.QUEUE_AUDIT_OPERATION);
|
||||
consumeLog.setConsumeTime(LocalDateTime.now());
|
||||
consumeLog.setStatus("SUCCESS");
|
||||
mqConsumeLogMapper.insert(consumeLog);
|
||||
|
||||
channel.basicAck(deliveryTag, false);
|
||||
log.debug("审计日志消费成功: eventId={}, module={}", eventId, event.getModule());
|
||||
} catch (Exception e) {
|
||||
log.error("审计日志消费失败: message={}", message, e);
|
||||
// 拒绝消息,不重新入队
|
||||
channel.basicNack(deliveryTag, false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.pms.audit.controller;
|
||||
|
||||
import com.pms.audit.dto.AuditLogDTO;
|
||||
import com.pms.audit.dto.AuditLogQueryRequest;
|
||||
import com.pms.audit.dto.AuditLogStatisticsDTO;
|
||||
import com.pms.audit.service.AuditLogService;
|
||||
import com.pms.common.response.PageResult;
|
||||
import com.pms.common.response.Result;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 审计日志控制器
|
||||
* 路径前缀: /api/v1/audit/logs
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/audit/logs")
|
||||
@RequiredArgsConstructor
|
||||
public class AuditLogController {
|
||||
|
||||
private final AuditLogService auditLogService;
|
||||
|
||||
/**
|
||||
* 审计日志列表(分页,多条件筛选)
|
||||
*/
|
||||
@GetMapping
|
||||
public Result<PageResult<AuditLogDTO>> list(AuditLogQueryRequest request) {
|
||||
return Result.success(auditLogService.page(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* 审计日志详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public Result<AuditLogDTO> getById(@PathVariable Long id) {
|
||||
return Result.success(auditLogService.getById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出审计日志
|
||||
*/
|
||||
@GetMapping("/export")
|
||||
public Result<List<AuditLogDTO>> export(AuditLogQueryRequest request) {
|
||||
return Result.success(auditLogService.export(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作统计
|
||||
*/
|
||||
@GetMapping("/statistics")
|
||||
public Result<AuditLogStatisticsDTO> statistics() {
|
||||
return Result.success(auditLogService.statistics());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.pms.audit.controller;
|
||||
|
||||
import com.pms.audit.dto.LoginLogDTO;
|
||||
import com.pms.audit.dto.LoginLogQueryRequest;
|
||||
import com.pms.audit.service.LoginLogService;
|
||||
import com.pms.common.response.PageResult;
|
||||
import com.pms.common.response.Result;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 登录日志控制器
|
||||
* 路径前缀: /api/v1/audit/login-logs
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/audit/login-logs")
|
||||
@RequiredArgsConstructor
|
||||
public class LoginLogController {
|
||||
|
||||
private final LoginLogService loginLogService;
|
||||
|
||||
/**
|
||||
* 登录日志列表
|
||||
*/
|
||||
@GetMapping
|
||||
public Result<PageResult<LoginLogDTO>> list(LoginLogQueryRequest request) {
|
||||
return Result.success(loginLogService.page(request));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
package com.pms.audit.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 审计事件消息(RabbitMQ 消息体)
|
||||
* 各服务通过AOP切面采集操作日志后发送到此消息结构
|
||||
*/
|
||||
@Data
|
||||
public class AuditEventMessage implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 事件唯一标识(用于消费幂等去重) */
|
||||
private String eventId;
|
||||
|
||||
/** 物业项目ID */
|
||||
private Long projectId;
|
||||
|
||||
/** 操作人ID */
|
||||
private Long operatorId;
|
||||
|
||||
/** 操作人姓名 */
|
||||
private String operatorName;
|
||||
|
||||
/** 操作人账号 */
|
||||
private String operatorAccount;
|
||||
|
||||
/** 操作人类型 */
|
||||
private Integer operatorType;
|
||||
|
||||
/** 操作类型 */
|
||||
private String operationType;
|
||||
|
||||
/** 操作模块 */
|
||||
private String module;
|
||||
|
||||
/** 操作描述 */
|
||||
private String operationDesc;
|
||||
|
||||
/** 请求方法 */
|
||||
private String method;
|
||||
|
||||
/** 请求URL */
|
||||
private String requestUrl;
|
||||
|
||||
/** HTTP方法 */
|
||||
private String requestMethod;
|
||||
|
||||
/** 请求参数(JSON) */
|
||||
private String requestParams;
|
||||
|
||||
/** 响应结果(JSON) */
|
||||
private String responseResult;
|
||||
|
||||
/** 操作结果 */
|
||||
private String operationResult;
|
||||
|
||||
/** 错误信息 */
|
||||
private String errorMsg;
|
||||
|
||||
/** 操作IP */
|
||||
private String ip;
|
||||
|
||||
/** User-Agent */
|
||||
private String userAgent;
|
||||
|
||||
/** 设备类型 */
|
||||
private String deviceType;
|
||||
|
||||
/** 接口耗时(毫秒) */
|
||||
private Long costTime;
|
||||
|
||||
/** 业务ID */
|
||||
private Long bizId;
|
||||
|
||||
/** 业务类型 */
|
||||
private String bizType;
|
||||
|
||||
/** 链路追踪ID */
|
||||
private String traceId;
|
||||
|
||||
/** 操作时间(时间戳) */
|
||||
private Long operationTime;
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package com.pms.audit.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 审计日志响应DTO
|
||||
*/
|
||||
@Data
|
||||
public class AuditLogDTO {
|
||||
|
||||
private Long id;
|
||||
|
||||
private String logNo;
|
||||
|
||||
private Long projectId;
|
||||
|
||||
private Long operatorId;
|
||||
|
||||
private String operatorName;
|
||||
|
||||
private String operatorAccount;
|
||||
|
||||
private Integer operatorType;
|
||||
|
||||
private String operationType;
|
||||
|
||||
private String module;
|
||||
|
||||
private String operationDesc;
|
||||
|
||||
private String method;
|
||||
|
||||
private String requestUrl;
|
||||
|
||||
private String requestMethod;
|
||||
|
||||
private String requestParams;
|
||||
|
||||
private String responseResult;
|
||||
|
||||
private String operationResult;
|
||||
|
||||
private String errorMsg;
|
||||
|
||||
private String ip;
|
||||
|
||||
private String ipLocation;
|
||||
|
||||
private String userAgent;
|
||||
|
||||
private String deviceType;
|
||||
|
||||
private Long costTime;
|
||||
|
||||
private Long bizId;
|
||||
|
||||
private String bizType;
|
||||
|
||||
private String traceId;
|
||||
|
||||
private Long operationTime;
|
||||
|
||||
private Long createdAt;
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.pms.audit.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 审计日志查询请求
|
||||
*/
|
||||
@Data
|
||||
public class AuditLogQueryRequest {
|
||||
|
||||
/** 页码 */
|
||||
private Integer page;
|
||||
|
||||
/** 每页大小 */
|
||||
private Integer size;
|
||||
|
||||
/** 操作人ID */
|
||||
private Long operatorId;
|
||||
|
||||
/** 操作类型 */
|
||||
private String operationType;
|
||||
|
||||
/** 操作模块 */
|
||||
private String module;
|
||||
|
||||
/** 操作结果 */
|
||||
private String operationResult;
|
||||
|
||||
/** 业务类型 */
|
||||
private String bizType;
|
||||
|
||||
/** 链路追踪ID */
|
||||
private String traceId;
|
||||
|
||||
/** 起始时间(时间戳) */
|
||||
private Long startTime;
|
||||
|
||||
/** 结束时间(时间戳) */
|
||||
private Long endTime;
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.pms.audit.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 审计日志统计DTO
|
||||
*/
|
||||
@Data
|
||||
public class AuditLogStatisticsDTO {
|
||||
|
||||
/** 总操作数 */
|
||||
private long totalCount;
|
||||
|
||||
/** 成功数 */
|
||||
private long successCount;
|
||||
|
||||
/** 失败数 */
|
||||
private long failCount;
|
||||
|
||||
/** 异常数 */
|
||||
private long errorCount;
|
||||
|
||||
/** 各模块操作统计 */
|
||||
private List<ModuleStat> moduleStats;
|
||||
|
||||
@Data
|
||||
public static class ModuleStat {
|
||||
private String module;
|
||||
private long count;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package com.pms.audit.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 登录日志响应DTO
|
||||
*/
|
||||
@Data
|
||||
public class LoginLogDTO {
|
||||
|
||||
private Long id;
|
||||
|
||||
private String logNo;
|
||||
|
||||
private Long projectId;
|
||||
|
||||
private Long userId;
|
||||
|
||||
private String userName;
|
||||
|
||||
private String userAccount;
|
||||
|
||||
private Integer userType;
|
||||
|
||||
private String loginType;
|
||||
|
||||
private String loginResult;
|
||||
|
||||
private String failReason;
|
||||
|
||||
private String ip;
|
||||
|
||||
private String ipLocation;
|
||||
|
||||
private String userAgent;
|
||||
|
||||
private String deviceType;
|
||||
|
||||
private String osName;
|
||||
|
||||
private String browser;
|
||||
|
||||
private String sessionId;
|
||||
|
||||
private Long loginTime;
|
||||
|
||||
private Long logoutTime;
|
||||
|
||||
private Long onlineDuration;
|
||||
|
||||
private Long createdAt;
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.pms.audit.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 登录日志查询请求
|
||||
*/
|
||||
@Data
|
||||
public class LoginLogQueryRequest {
|
||||
|
||||
/** 页码 */
|
||||
private Integer page;
|
||||
|
||||
/** 每页大小 */
|
||||
private Integer size;
|
||||
|
||||
/** 用户ID */
|
||||
private Long userId;
|
||||
|
||||
/** 登录账号 */
|
||||
private String userAccount;
|
||||
|
||||
/** 登录结果 */
|
||||
private String loginResult;
|
||||
|
||||
/** 登录方式 */
|
||||
private String loginType;
|
||||
|
||||
/** 起始时间(时间戳) */
|
||||
private Long startTime;
|
||||
|
||||
/** 结束时间(时间戳) */
|
||||
private Long endTime;
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
package com.pms.audit.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 审计日志实体
|
||||
* 注意:审计日志表不使用逻辑删除(无 deleted 字段),日志一旦写入不可修改
|
||||
*/
|
||||
@Data
|
||||
@TableName("t_audit_log")
|
||||
public class AuditLog implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 主键ID */
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
|
||||
/** 日志编号,全局唯一 */
|
||||
private String logNo;
|
||||
|
||||
/** 物业项目ID(0表示全平台) */
|
||||
private Long projectId;
|
||||
|
||||
/** 操作人ID */
|
||||
private Long operatorId;
|
||||
|
||||
/** 操作人姓名 */
|
||||
private String operatorName;
|
||||
|
||||
/** 操作人账号 */
|
||||
private String operatorAccount;
|
||||
|
||||
/** 操作人类型:1-业主 2-物业员工 3-系统管理员 */
|
||||
private Integer operatorType;
|
||||
|
||||
/** 操作类型(CREATE/UPDATE/DELETE/QUERY/EXPORT/IMPORT/LOGIN/LOGOUT/OTHER) */
|
||||
private String operationType;
|
||||
|
||||
/** 操作模块 */
|
||||
private String module;
|
||||
|
||||
/** 操作描述 */
|
||||
private String operationDesc;
|
||||
|
||||
/** 请求方法(如ChargeController.create) */
|
||||
private String method;
|
||||
|
||||
/** 请求URL */
|
||||
private String requestUrl;
|
||||
|
||||
/** HTTP方法(GET/POST/PUT/DELETE) */
|
||||
private String requestMethod;
|
||||
|
||||
/** 请求参数(JSON,敏感字段已脱敏) */
|
||||
private String requestParams;
|
||||
|
||||
/** 响应结果(JSON,超长截断) */
|
||||
private String responseResult;
|
||||
|
||||
/** 操作结果(SUCCESS/FAIL/ERROR) */
|
||||
private String operationResult;
|
||||
|
||||
/** 错误信息 */
|
||||
private String errorMsg;
|
||||
|
||||
/** 操作IP地址 */
|
||||
private String ip;
|
||||
|
||||
/** IP归属地 */
|
||||
private String ipLocation;
|
||||
|
||||
/** User-Agent */
|
||||
private String userAgent;
|
||||
|
||||
/** 设备类型(PC/APP/WEB) */
|
||||
private String deviceType;
|
||||
|
||||
/** 接口耗时(毫秒) */
|
||||
private Long costTime;
|
||||
|
||||
/** 业务ID */
|
||||
private Long bizId;
|
||||
|
||||
/** 业务类型 */
|
||||
private String bizType;
|
||||
|
||||
/** 链路追踪ID */
|
||||
private String traceId;
|
||||
|
||||
/** 操作时间(时间戳) */
|
||||
private Long operationTime;
|
||||
|
||||
/** 创建时间(时间戳) */
|
||||
private Long createdAt;
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
package com.pms.audit.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 登录日志实体
|
||||
* 注意:登录日志表不使用逻辑删除
|
||||
*/
|
||||
@Data
|
||||
@TableName("t_login_log")
|
||||
public class LoginLog implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 主键ID */
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
|
||||
/** 日志编号,全局唯一 */
|
||||
private String logNo;
|
||||
|
||||
/** 物业项目ID(0表示全平台) */
|
||||
private Long projectId;
|
||||
|
||||
/** 用户ID */
|
||||
private Long userId;
|
||||
|
||||
/** 用户姓名 */
|
||||
private String userName;
|
||||
|
||||
/** 登录账号 */
|
||||
private String userAccount;
|
||||
|
||||
/** 用户类型:1-业主 2-物业员工 3-系统管理员 */
|
||||
private Integer userType;
|
||||
|
||||
/** 登录方式(PASSWORD/SMS_CODE/WECHAT/ALIPAY) */
|
||||
private String loginType;
|
||||
|
||||
/** 登录结果(LOGIN_SUCCESS/LOGIN_FAIL/LOGOUT/KICK_OUT) */
|
||||
private String loginResult;
|
||||
|
||||
/** 失败原因 */
|
||||
private String failReason;
|
||||
|
||||
/** 登录IP地址 */
|
||||
private String ip;
|
||||
|
||||
/** IP归属地 */
|
||||
private String ipLocation;
|
||||
|
||||
/** User-Agent */
|
||||
private String userAgent;
|
||||
|
||||
/** 设备类型 */
|
||||
private String deviceType;
|
||||
|
||||
/** 设备唯一标识 */
|
||||
private String deviceId;
|
||||
|
||||
/** 操作系统 */
|
||||
private String osName;
|
||||
|
||||
/** 浏览器 */
|
||||
private String browser;
|
||||
|
||||
/** 会话ID */
|
||||
private String sessionId;
|
||||
|
||||
/** 登录令牌(脱敏) */
|
||||
private String token;
|
||||
|
||||
/** 登录时间(时间戳) */
|
||||
private Long loginTime;
|
||||
|
||||
/** 登出时间(时间戳) */
|
||||
private Long logoutTime;
|
||||
|
||||
/** 在线时长(秒) */
|
||||
private Long onlineDuration;
|
||||
|
||||
/** 创建时间(时间戳) */
|
||||
private Long createdAt;
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.pms.audit.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.pms.audit.entity.AuditLog;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 审计日志 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface AuditLogMapper extends BaseMapper<AuditLog> {
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.pms.audit.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.pms.audit.entity.LoginLog;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 登录日志 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface LoginLogMapper extends BaseMapper<LoginLog> {
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.pms.audit.service;
|
||||
|
||||
import com.pms.audit.dto.AuditEventMessage;
|
||||
import com.pms.audit.dto.AuditLogDTO;
|
||||
import com.pms.audit.dto.AuditLogQueryRequest;
|
||||
import com.pms.audit.dto.AuditLogStatisticsDTO;
|
||||
import com.pms.common.response.PageResult;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 审计日志服务接口
|
||||
*/
|
||||
public interface AuditLogService {
|
||||
|
||||
/**
|
||||
* 分页查询审计日志
|
||||
*/
|
||||
PageResult<AuditLogDTO> page(AuditLogQueryRequest request);
|
||||
|
||||
/**
|
||||
* 审计日志详情
|
||||
*/
|
||||
AuditLogDTO getById(Long id);
|
||||
|
||||
/**
|
||||
* 导出审计日志(返回日志列表)
|
||||
*/
|
||||
List<AuditLogDTO> export(AuditLogQueryRequest request);
|
||||
|
||||
/**
|
||||
* 操作统计
|
||||
*/
|
||||
AuditLogStatisticsDTO statistics();
|
||||
|
||||
/**
|
||||
* 保存审计日志(从MQ消息转换)
|
||||
*/
|
||||
void save(AuditEventMessage message);
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.pms.audit.service;
|
||||
|
||||
import com.pms.audit.dto.LoginLogDTO;
|
||||
import com.pms.audit.dto.LoginLogQueryRequest;
|
||||
import com.pms.common.response.PageResult;
|
||||
|
||||
/**
|
||||
* 登录日志服务接口
|
||||
*/
|
||||
public interface LoginLogService {
|
||||
|
||||
/**
|
||||
* 分页查询登录日志
|
||||
*/
|
||||
PageResult<LoginLogDTO> page(LoginLogQueryRequest request);
|
||||
}
|
||||
|
|
@ -0,0 +1,266 @@
|
|||
package com.pms.audit.service.impl;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.pms.audit.constant.AuditConstants;
|
||||
import com.pms.audit.dto.AuditEventMessage;
|
||||
import com.pms.audit.dto.AuditLogDTO;
|
||||
import com.pms.audit.dto.AuditLogQueryRequest;
|
||||
import com.pms.audit.dto.AuditLogStatisticsDTO;
|
||||
import com.pms.audit.entity.AuditLog;
|
||||
import com.pms.audit.mapper.AuditLogMapper;
|
||||
import com.pms.audit.service.AuditLogService;
|
||||
import com.pms.common.constant.CommonConstants;
|
||||
import com.pms.common.exception.BusinessException;
|
||||
import com.pms.common.exception.ErrorCode;
|
||||
import com.pms.common.response.PageResult;
|
||||
import com.pms.common.security.UserContext;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 审计日志服务实现
|
||||
* <p>
|
||||
* 关键特性:
|
||||
* - 敏感数据脱敏:request_params中的password等字段自动脱敏
|
||||
* - 审计日志不使用逻辑删除
|
||||
* - 按月分表建议(在代码注释中说明,实际分表后续实现)
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AuditLogServiceImpl implements AuditLogService {
|
||||
|
||||
private final AuditLogMapper auditLogMapper;
|
||||
|
||||
/** 敏感字段值脱敏正则:"fieldName":"value" → "fieldName":"***" */
|
||||
private static final Pattern SENSITIVE_PATTERN =
|
||||
Pattern.compile("\"(password|oldPassword|newPassword|token|secretKey|bankAccount|idCard)\"\\s*:\\s*\"[^\"]*\"",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
|
||||
@Override
|
||||
public PageResult<AuditLogDTO> page(AuditLogQueryRequest request) {
|
||||
int page = request.getPage() != null && request.getPage() > 0 ? request.getPage() : CommonConstants.DEFAULT_PAGE;
|
||||
int size = request.getSize() != null && request.getSize() > 0 ? request.getSize() : CommonConstants.DEFAULT_SIZE;
|
||||
size = Math.min(size, CommonConstants.MAX_SIZE);
|
||||
|
||||
LambdaQueryWrapper<AuditLog> wrapper = buildQueryWrapper(request);
|
||||
wrapper.orderByDesc(AuditLog::getOperationTime);
|
||||
|
||||
Page<AuditLog> pageObj = new Page<>(page, size);
|
||||
IPage<AuditLog> result = auditLogMapper.selectPage(pageObj, wrapper);
|
||||
|
||||
return new PageResult<>(
|
||||
result.getRecords().stream().map(this::toDTO).toList(),
|
||||
result.getTotal(), page, size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuditLogDTO getById(Long id) {
|
||||
AuditLog auditLog = auditLogMapper.selectById(id);
|
||||
if (auditLog == null) {
|
||||
throw new BusinessException(ErrorCode.AUDIT_LOG_NOT_FOUND);
|
||||
}
|
||||
return toDTO(auditLog);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AuditLogDTO> export(AuditLogQueryRequest request) {
|
||||
// 导出最多 10000 条
|
||||
LambdaQueryWrapper<AuditLog> wrapper = buildQueryWrapper(request);
|
||||
wrapper.orderByDesc(AuditLog::getOperationTime);
|
||||
wrapper.last("LIMIT 10000");
|
||||
|
||||
List<AuditLog> logs = auditLogMapper.selectList(wrapper);
|
||||
return logs.stream().map(this::toDTO).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuditLogStatisticsDTO statistics() {
|
||||
Long projectId = UserContext.getProjectId();
|
||||
|
||||
LambdaQueryWrapper<AuditLog> baseWrapper = new LambdaQueryWrapper<>();
|
||||
if (projectId != null) {
|
||||
baseWrapper.eq(AuditLog::getProjectId, projectId);
|
||||
}
|
||||
long totalCount = auditLogMapper.selectCount(baseWrapper);
|
||||
|
||||
LambdaQueryWrapper<AuditLog> successWrapper = new LambdaQueryWrapper<>();
|
||||
if (projectId != null) {
|
||||
successWrapper.eq(AuditLog::getProjectId, projectId);
|
||||
}
|
||||
successWrapper.eq(AuditLog::getOperationResult, AuditConstants.RESULT_SUCCESS);
|
||||
long successCount = auditLogMapper.selectCount(successWrapper);
|
||||
|
||||
LambdaQueryWrapper<AuditLog> failWrapper = new LambdaQueryWrapper<>();
|
||||
if (projectId != null) {
|
||||
failWrapper.eq(AuditLog::getProjectId, projectId);
|
||||
}
|
||||
failWrapper.eq(AuditLog::getOperationResult, AuditConstants.RESULT_FAIL);
|
||||
long failCount = auditLogMapper.selectCount(failWrapper);
|
||||
|
||||
LambdaQueryWrapper<AuditLog> errorWrapper = new LambdaQueryWrapper<>();
|
||||
if (projectId != null) {
|
||||
errorWrapper.eq(AuditLog::getProjectId, projectId);
|
||||
}
|
||||
errorWrapper.eq(AuditLog::getOperationResult, AuditConstants.RESULT_ERROR);
|
||||
long errorCount = auditLogMapper.selectCount(errorWrapper);
|
||||
|
||||
AuditLogStatisticsDTO dto = new AuditLogStatisticsDTO();
|
||||
dto.setTotalCount(totalCount);
|
||||
dto.setSuccessCount(successCount);
|
||||
dto.setFailCount(failCount);
|
||||
dto.setErrorCount(errorCount);
|
||||
// 模块统计暂为空列表(实际可通过 GROUP BY 查询实现)
|
||||
dto.setModuleStats(new ArrayList<>());
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(AuditEventMessage message) {
|
||||
AuditLog auditLog = new AuditLog();
|
||||
auditLog.setLogNo(generateLogNo());
|
||||
auditLog.setProjectId(message.getProjectId() != null ? message.getProjectId() : 0L);
|
||||
auditLog.setOperatorId(message.getOperatorId());
|
||||
auditLog.setOperatorName(message.getOperatorName());
|
||||
auditLog.setOperatorAccount(message.getOperatorAccount());
|
||||
auditLog.setOperatorType(message.getOperatorType() != null ? message.getOperatorType() : 2);
|
||||
auditLog.setOperationType(message.getOperationType());
|
||||
auditLog.setModule(message.getModule());
|
||||
auditLog.setOperationDesc(message.getOperationDesc());
|
||||
auditLog.setMethod(message.getMethod());
|
||||
auditLog.setRequestUrl(message.getRequestUrl());
|
||||
auditLog.setRequestMethod(message.getRequestMethod());
|
||||
// 敏感数据脱敏
|
||||
auditLog.setRequestParams(desensitizeParams(message.getRequestParams()));
|
||||
// 响应结果超长截断(最大 5000 字符)
|
||||
auditLog.setResponseResult(truncate(message.getResponseResult(), 5000));
|
||||
auditLog.setOperationResult(message.getOperationResult());
|
||||
auditLog.setErrorMsg(truncate(message.getErrorMsg(), 2000));
|
||||
auditLog.setIp(message.getIp());
|
||||
auditLog.setUserAgent(message.getUserAgent());
|
||||
auditLog.setDeviceType(message.getDeviceType());
|
||||
auditLog.setCostTime(message.getCostTime() != null ? message.getCostTime() : 0L);
|
||||
auditLog.setBizId(message.getBizId());
|
||||
auditLog.setBizType(message.getBizType());
|
||||
auditLog.setTraceId(message.getTraceId());
|
||||
long now = System.currentTimeMillis();
|
||||
auditLog.setOperationTime(message.getOperationTime() != null ? message.getOperationTime() : now);
|
||||
auditLog.setCreatedAt(now);
|
||||
|
||||
auditLogMapper.insert(auditLog);
|
||||
log.debug("审计日志保存成功: logNo={}, module={}, operationType={}",
|
||||
auditLog.getLogNo(), auditLog.getModule(), auditLog.getOperationType());
|
||||
}
|
||||
|
||||
// ====== 私有方法 ======
|
||||
|
||||
private LambdaQueryWrapper<AuditLog> buildQueryWrapper(AuditLogQueryRequest request) {
|
||||
Long projectId = UserContext.getProjectId();
|
||||
LambdaQueryWrapper<AuditLog> wrapper = new LambdaQueryWrapper<>();
|
||||
if (projectId != null) {
|
||||
wrapper.eq(AuditLog::getProjectId, projectId);
|
||||
}
|
||||
if (request.getOperatorId() != null) {
|
||||
wrapper.eq(AuditLog::getOperatorId, request.getOperatorId());
|
||||
}
|
||||
if (StringUtils.hasText(request.getOperationType())) {
|
||||
wrapper.eq(AuditLog::getOperationType, request.getOperationType());
|
||||
}
|
||||
if (StringUtils.hasText(request.getModule())) {
|
||||
wrapper.eq(AuditLog::getModule, request.getModule());
|
||||
}
|
||||
if (StringUtils.hasText(request.getOperationResult())) {
|
||||
wrapper.eq(AuditLog::getOperationResult, request.getOperationResult());
|
||||
}
|
||||
if (StringUtils.hasText(request.getBizType())) {
|
||||
wrapper.eq(AuditLog::getBizType, request.getBizType());
|
||||
}
|
||||
if (StringUtils.hasText(request.getTraceId())) {
|
||||
wrapper.eq(AuditLog::getTraceId, request.getTraceId());
|
||||
}
|
||||
if (request.getStartTime() != null) {
|
||||
wrapper.ge(AuditLog::getOperationTime, request.getStartTime());
|
||||
}
|
||||
if (request.getEndTime() != null) {
|
||||
wrapper.le(AuditLog::getOperationTime, request.getEndTime());
|
||||
}
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 敏感数据脱敏
|
||||
* 将 requestParams JSON 中的 password、token 等字段值替换为 ***
|
||||
*/
|
||||
private String desensitizeParams(String params) {
|
||||
if (params == null || params.isBlank()) {
|
||||
return params;
|
||||
}
|
||||
Matcher matcher = SENSITIVE_PATTERN.matcher(params);
|
||||
return matcher.replaceAll(m -> {
|
||||
// 保留字段名,替换值为 ***
|
||||
String matched = m.group();
|
||||
int colonIdx = matched.indexOf(':');
|
||||
return matched.substring(0, colonIdx + 1) + "\"***\"";
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 字符串截断
|
||||
*/
|
||||
private String truncate(String str, int maxLength) {
|
||||
if (str == null) {
|
||||
return null;
|
||||
}
|
||||
if (str.length() <= maxLength) {
|
||||
return str;
|
||||
}
|
||||
return str.substring(0, maxLength) + "...[truncated]";
|
||||
}
|
||||
|
||||
private String generateLogNo() {
|
||||
return "AUD" + System.currentTimeMillis() + IdUtil.fastSimpleUUID().substring(0, 6).toUpperCase();
|
||||
}
|
||||
|
||||
private AuditLogDTO toDTO(AuditLog auditLog) {
|
||||
AuditLogDTO dto = new AuditLogDTO();
|
||||
dto.setId(auditLog.getId());
|
||||
dto.setLogNo(auditLog.getLogNo());
|
||||
dto.setProjectId(auditLog.getProjectId());
|
||||
dto.setOperatorId(auditLog.getOperatorId());
|
||||
dto.setOperatorName(auditLog.getOperatorName());
|
||||
dto.setOperatorAccount(auditLog.getOperatorAccount());
|
||||
dto.setOperatorType(auditLog.getOperatorType());
|
||||
dto.setOperationType(auditLog.getOperationType());
|
||||
dto.setModule(auditLog.getModule());
|
||||
dto.setOperationDesc(auditLog.getOperationDesc());
|
||||
dto.setMethod(auditLog.getMethod());
|
||||
dto.setRequestUrl(auditLog.getRequestUrl());
|
||||
dto.setRequestMethod(auditLog.getRequestMethod());
|
||||
dto.setRequestParams(auditLog.getRequestParams());
|
||||
dto.setResponseResult(auditLog.getResponseResult());
|
||||
dto.setOperationResult(auditLog.getOperationResult());
|
||||
dto.setErrorMsg(auditLog.getErrorMsg());
|
||||
dto.setIp(auditLog.getIp());
|
||||
dto.setIpLocation(auditLog.getIpLocation());
|
||||
dto.setUserAgent(auditLog.getUserAgent());
|
||||
dto.setDeviceType(auditLog.getDeviceType());
|
||||
dto.setCostTime(auditLog.getCostTime());
|
||||
dto.setBizId(auditLog.getBizId());
|
||||
dto.setBizType(auditLog.getBizType());
|
||||
dto.setTraceId(auditLog.getTraceId());
|
||||
dto.setOperationTime(auditLog.getOperationTime());
|
||||
dto.setCreatedAt(auditLog.getCreatedAt());
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
package com.pms.audit.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.pms.audit.dto.LoginLogDTO;
|
||||
import com.pms.audit.dto.LoginLogQueryRequest;
|
||||
import com.pms.audit.entity.LoginLog;
|
||||
import com.pms.audit.mapper.LoginLogMapper;
|
||||
import com.pms.audit.service.LoginLogService;
|
||||
import com.pms.common.constant.CommonConstants;
|
||||
import com.pms.common.response.PageResult;
|
||||
import com.pms.common.security.UserContext;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* 登录日志服务实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LoginLogServiceImpl implements LoginLogService {
|
||||
|
||||
private final LoginLogMapper loginLogMapper;
|
||||
|
||||
@Override
|
||||
public PageResult<LoginLogDTO> page(LoginLogQueryRequest request) {
|
||||
int page = request.getPage() != null && request.getPage() > 0 ? request.getPage() : CommonConstants.DEFAULT_PAGE;
|
||||
int size = request.getSize() != null && request.getSize() > 0 ? request.getSize() : CommonConstants.DEFAULT_SIZE;
|
||||
size = Math.min(size, CommonConstants.MAX_SIZE);
|
||||
|
||||
Long projectId = UserContext.getProjectId();
|
||||
LambdaQueryWrapper<LoginLog> wrapper = new LambdaQueryWrapper<>();
|
||||
if (projectId != null) {
|
||||
wrapper.eq(LoginLog::getProjectId, projectId);
|
||||
}
|
||||
if (request.getUserId() != null) {
|
||||
wrapper.eq(LoginLog::getUserId, request.getUserId());
|
||||
}
|
||||
if (StringUtils.hasText(request.getUserAccount())) {
|
||||
wrapper.eq(LoginLog::getUserAccount, request.getUserAccount());
|
||||
}
|
||||
if (StringUtils.hasText(request.getLoginResult())) {
|
||||
wrapper.eq(LoginLog::getLoginResult, request.getLoginResult());
|
||||
}
|
||||
if (StringUtils.hasText(request.getLoginType())) {
|
||||
wrapper.eq(LoginLog::getLoginType, request.getLoginType());
|
||||
}
|
||||
if (request.getStartTime() != null) {
|
||||
wrapper.ge(LoginLog::getLoginTime, request.getStartTime());
|
||||
}
|
||||
if (request.getEndTime() != null) {
|
||||
wrapper.le(LoginLog::getLoginTime, request.getEndTime());
|
||||
}
|
||||
wrapper.orderByDesc(LoginLog::getLoginTime);
|
||||
|
||||
Page<LoginLog> pageObj = new Page<>(page, size);
|
||||
IPage<LoginLog> result = loginLogMapper.selectPage(pageObj, wrapper);
|
||||
|
||||
return new PageResult<>(
|
||||
result.getRecords().stream().map(this::toDTO).toList(),
|
||||
result.getTotal(), page, size);
|
||||
}
|
||||
|
||||
private LoginLogDTO toDTO(LoginLog loginLog) {
|
||||
LoginLogDTO dto = new LoginLogDTO();
|
||||
dto.setId(loginLog.getId());
|
||||
dto.setLogNo(loginLog.getLogNo());
|
||||
dto.setProjectId(loginLog.getProjectId());
|
||||
dto.setUserId(loginLog.getUserId());
|
||||
dto.setUserName(loginLog.getUserName());
|
||||
dto.setUserAccount(loginLog.getUserAccount());
|
||||
dto.setUserType(loginLog.getUserType());
|
||||
dto.setLoginType(loginLog.getLoginType());
|
||||
dto.setLoginResult(loginLog.getLoginResult());
|
||||
dto.setFailReason(loginLog.getFailReason());
|
||||
dto.setIp(loginLog.getIp());
|
||||
dto.setIpLocation(loginLog.getIpLocation());
|
||||
dto.setUserAgent(loginLog.getUserAgent());
|
||||
dto.setDeviceType(loginLog.getDeviceType());
|
||||
dto.setOsName(loginLog.getOsName());
|
||||
dto.setBrowser(loginLog.getBrowser());
|
||||
dto.setSessionId(loginLog.getSessionId());
|
||||
dto.setLoginTime(loginLog.getLoginTime());
|
||||
dto.setLogoutTime(loginLog.getLogoutTime());
|
||||
dto.setOnlineDuration(loginLog.getOnlineDuration());
|
||||
dto.setCreatedAt(loginLog.getCreatedAt());
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
server:
|
||||
port: 8087
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: audit-service
|
||||
profiles:
|
||||
active: dev
|
||||
datasource:
|
||||
url: jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/audit_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
username: ${MYSQL_USERNAME:root}
|
||||
password: ${MYSQL_PASSWORD:root}
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
hikari:
|
||||
maximum-pool-size: 10
|
||||
minimum-idle: 2
|
||||
connection-timeout: 30000
|
||||
idle-timeout: 600000
|
||||
flyway:
|
||||
enabled: true
|
||||
locations: classpath:db/migration
|
||||
baseline-on-migrate: true
|
||||
baseline-version: 0
|
||||
url: jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/audit_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
user: ${MYSQL_USERNAME:root}
|
||||
password: ${MYSQL_PASSWORD:root}
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:}
|
||||
database: 7
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 16
|
||||
max-idle: 8
|
||||
min-idle: 2
|
||||
rabbitmq:
|
||||
host: ${RABBITMQ_HOST:localhost}
|
||||
port: ${RABBITMQ_PORT:5672}
|
||||
username: ${RABBITMQ_USERNAME:guest}
|
||||
password: ${RABBITMQ_PASSWORD:guest}
|
||||
virtual-host: /
|
||||
publisher-confirm-type: correlated
|
||||
publisher-returns: true
|
||||
listener:
|
||||
simple:
|
||||
acknowledge-mode: manual
|
||||
prefetch: 50
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: ${NACOS_ADDR:localhost:8848}
|
||||
namespace: ${NACOS_NAMESPACE:public}
|
||||
config:
|
||||
server-addr: ${NACOS_ADDR:localhost:8848}
|
||||
namespace: ${NACOS_NAMESPACE:public}
|
||||
file-extension: yaml
|
||||
shared-configs:
|
||||
- data-id: pms-common.yaml
|
||||
refresh: true
|
||||
openfeign:
|
||||
client:
|
||||
config:
|
||||
default:
|
||||
connect-timeout: 3000
|
||||
read-timeout: 5000
|
||||
|
||||
mybatis-plus:
|
||||
mapper-locations: classpath*:mapper/**/*.xml
|
||||
type-aliases-package: com.pms.audit.**.entity
|
||||
global-config:
|
||||
db-config:
|
||||
id-type: ASSIGN_ID
|
||||
logic-delete-field: deleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
table-prefix: t_
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
cache-enabled: false
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.pms: debug
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
-- ========================================
|
||||
-- audit_db 数据库初始化脚本(2张表)
|
||||
-- 审计服务:操作审计日志、登录日志
|
||||
-- 技术约定:
|
||||
-- 主键 BIGINT UNSIGNED(雪花算法)
|
||||
-- 时间戳 BIGINT(毫秒级时间戳)
|
||||
-- 审计日志表不使用逻辑删除(日志一旦写入不可修改)
|
||||
-- ========================================
|
||||
|
||||
-- ====================
|
||||
-- 1. 审计日志表
|
||||
-- ====================
|
||||
CREATE TABLE IF NOT EXISTS `t_audit_log` (
|
||||
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
|
||||
`log_no` VARCHAR(32) NOT NULL COMMENT '日志编号,全局唯一',
|
||||
`project_id` BIGINT NOT NULL DEFAULT 0 COMMENT '物业项目ID(0表示全平台)',
|
||||
`operator_id` BIGINT NOT NULL COMMENT '操作人ID',
|
||||
`operator_name` VARCHAR(50) NOT NULL COMMENT '操作人姓名',
|
||||
`operator_account` VARCHAR(50) NOT NULL COMMENT '操作人账号',
|
||||
`operator_type` TINYINT NOT NULL COMMENT '操作人类型:1-业主 2-物业员工 3-系统管理员',
|
||||
`operation_type` VARCHAR(20) NOT NULL COMMENT '操作类型(CREATE/UPDATE/DELETE/QUERY/EXPORT/IMPORT/LOGIN/LOGOUT/OTHER)',
|
||||
`module` VARCHAR(50) NOT NULL COMMENT '操作模块',
|
||||
`operation_desc` VARCHAR(500) NOT NULL COMMENT '操作描述',
|
||||
`method` VARCHAR(200) DEFAULT NULL COMMENT '请求方法(如ChargeController.create)',
|
||||
`request_url` VARCHAR(500) DEFAULT NULL COMMENT '请求URL',
|
||||
`request_method` VARCHAR(10) DEFAULT NULL COMMENT 'HTTP方法(GET/POST/PUT/DELETE)',
|
||||
`request_params` TEXT COMMENT '请求参数(JSON,敏感字段已脱敏)',
|
||||
`response_result` TEXT COMMENT '响应结果(JSON,超长截断)',
|
||||
`operation_result` VARCHAR(20) NOT NULL COMMENT '操作结果(SUCCESS/FAIL/ERROR)',
|
||||
`error_msg` TEXT COMMENT '错误信息',
|
||||
`ip` VARCHAR(50) NOT NULL COMMENT '操作IP地址',
|
||||
`ip_location` VARCHAR(200) DEFAULT NULL COMMENT 'IP归属地',
|
||||
`user_agent` VARCHAR(500) DEFAULT NULL COMMENT 'User-Agent',
|
||||
`device_type` VARCHAR(20) DEFAULT NULL COMMENT '设备类型(PC/APP/WEB)',
|
||||
`cost_time` BIGINT NOT NULL DEFAULT 0 COMMENT '接口耗时(毫秒)',
|
||||
`biz_id` BIGINT DEFAULT NULL COMMENT '业务ID',
|
||||
`biz_type` VARCHAR(50) DEFAULT NULL COMMENT '业务类型',
|
||||
`trace_id` VARCHAR(64) DEFAULT NULL COMMENT '链路追踪ID',
|
||||
`operation_time` BIGINT NOT NULL COMMENT '操作时间(时间戳)',
|
||||
`created_at` BIGINT NOT NULL COMMENT '创建时间(时间戳)',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_log_no` (`log_no`),
|
||||
KEY `idx_project_id` (`project_id`),
|
||||
KEY `idx_operator_id` (`operator_id`),
|
||||
KEY `idx_operation_type` (`operation_type`),
|
||||
KEY `idx_module` (`module`),
|
||||
KEY `idx_operation_result` (`operation_result`),
|
||||
KEY `idx_trace_id` (`trace_id`),
|
||||
KEY `idx_operation_time` (`operation_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审计日志表';
|
||||
|
||||
-- 说明:审计日志表不使用逻辑删除字段(deleted),日志一旦写入不可修改或删除。
|
||||
-- 建议后续按月分表(如 t_audit_log_202406),并设置数据保留策略(如保留2年)。
|
||||
|
||||
-- ====================
|
||||
-- 2. 登录日志表
|
||||
-- ====================
|
||||
CREATE TABLE IF NOT EXISTS `t_login_log` (
|
||||
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
|
||||
`log_no` VARCHAR(32) NOT NULL COMMENT '日志编号,全局唯一',
|
||||
`project_id` BIGINT NOT NULL DEFAULT 0 COMMENT '物业项目ID(0表示全平台)',
|
||||
`user_id` BIGINT NOT NULL COMMENT '用户ID',
|
||||
`user_name` VARCHAR(50) NOT NULL COMMENT '用户姓名',
|
||||
`user_account` VARCHAR(50) NOT NULL COMMENT '登录账号',
|
||||
`user_type` TINYINT NOT NULL COMMENT '用户类型:1-业主 2-物业员工 3-系统管理员',
|
||||
`login_type` VARCHAR(20) NOT NULL COMMENT '登录方式(PASSWORD/SMS_CODE/WECHAT/ALIPAY)',
|
||||
`login_result` VARCHAR(20) NOT NULL COMMENT '登录结果(LOGIN_SUCCESS/LOGIN_FAIL/LOGOUT/KICK_OUT)',
|
||||
`fail_reason` VARCHAR(500) DEFAULT NULL COMMENT '失败原因',
|
||||
`ip` VARCHAR(50) NOT NULL COMMENT '登录IP地址',
|
||||
`ip_location` VARCHAR(200) DEFAULT NULL COMMENT 'IP归属地',
|
||||
`user_agent` VARCHAR(500) DEFAULT NULL COMMENT 'User-Agent',
|
||||
`device_type` VARCHAR(20) DEFAULT NULL COMMENT '设备类型',
|
||||
`device_id` VARCHAR(200) DEFAULT NULL COMMENT '设备唯一标识',
|
||||
`os_name` VARCHAR(50) DEFAULT NULL COMMENT '操作系统',
|
||||
`browser` VARCHAR(100) DEFAULT NULL COMMENT '浏览器',
|
||||
`session_id` VARCHAR(128) DEFAULT NULL COMMENT '会话ID',
|
||||
`token` VARCHAR(500) DEFAULT NULL COMMENT '登录令牌(脱敏)',
|
||||
`login_time` BIGINT NOT NULL COMMENT '登录时间(时间戳)',
|
||||
`logout_time` BIGINT DEFAULT NULL COMMENT '登出时间(时间戳)',
|
||||
`online_duration` BIGINT NOT NULL DEFAULT 0 COMMENT '在线时长(秒)',
|
||||
`created_at` BIGINT NOT NULL COMMENT '创建时间(时间戳)',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_log_no` (`log_no`),
|
||||
KEY `idx_project_id` (`project_id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_user_account` (`user_account`),
|
||||
KEY `idx_login_result` (`login_result`),
|
||||
KEY `idx_session_id` (`session_id`),
|
||||
KEY `idx_login_time` (`login_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='登录日志表';
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
-- 消息消费幂等日志表
|
||||
-- 用于持久化保障消息消费幂等性,防止 Redis 失效后重复消费
|
||||
CREATE TABLE IF NOT EXISTS mq_consume_log (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
event_id VARCHAR(128) NOT NULL COMMENT '事件唯一ID',
|
||||
consumer_group VARCHAR(64) NOT NULL COMMENT '消费组名称',
|
||||
queue_name VARCHAR(128) NOT NULL COMMENT '队列名称',
|
||||
consume_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '消费时间',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'SUCCESS' COMMENT '消费状态',
|
||||
error_msg TEXT NULL COMMENT '错误信息',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_event_consumer (event_id, consumer_group)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息消费幂等日志表';
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
// ========================================
|
||||
// pms-auth 认证服务模块
|
||||
// ========================================
|
||||
|
||||
apply plugin: 'org.springframework.boot'
|
||||
|
||||
dependencies {
|
||||
implementation project(':pms-common')
|
||||
|
||||
// Web(Servlet)
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
// 参数校验
|
||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||
// Actuator
|
||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||
|
||||
// Spring Security(密码加密 BCrypt)
|
||||
implementation 'org.springframework.boot:spring-boot-starter-security'
|
||||
|
||||
// 服务间调用
|
||||
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
|
||||
implementation 'org.springframework.cloud:spring-cloud-starter-loadbalancer'
|
||||
|
||||
// Nacos
|
||||
implementation 'com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-discovery'
|
||||
implementation 'com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-config'
|
||||
implementation 'org.springframework.cloud:spring-cloud-starter-bootstrap'
|
||||
|
||||
// MyBatis-Plus
|
||||
implementation "com.baomidou:mybatis-plus-spring-boot3-starter:${mybatisPlusVersion}"
|
||||
|
||||
// Flyway
|
||||
implementation 'org.flywaydb:flyway-core'
|
||||
implementation 'org.flywaydb:flyway-mysql'
|
||||
|
||||
// MySQL
|
||||
runtimeOnly "com.mysql:mysql-connector-j:${mysqlConnectorVersion}"
|
||||
|
||||
// Redis
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
|
||||
|
||||
// RabbitMQ
|
||||
implementation 'org.springframework.boot:spring-boot-starter-amqp'
|
||||
|
||||
// MapStruct
|
||||
implementation "org.mapstruct:mapstruct:${mapstructVersion}"
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.pms.auth;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
|
||||
import com.pms.common.security.JwtProperties;
|
||||
|
||||
/**
|
||||
* 认证服务启动类
|
||||
* 负责用户登录、令牌签发(RS256)、刷新、权限管理
|
||||
*/
|
||||
@SpringBootApplication(scanBasePackages = "com.pms")
|
||||
@EnableDiscoveryClient
|
||||
@EnableFeignClients(basePackages = "com.pms")
|
||||
@EnableConfigurationProperties(JwtProperties.class)
|
||||
@MapperScan("com.pms.auth.**.mapper")
|
||||
public class AuthServiceApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AuthServiceApplication.class, args);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.pms.auth.config;
|
||||
|
||||
import com.pms.common.security.JwtProperties;
|
||||
import com.pms.common.security.JwtUtils;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* JWT Bean 配置
|
||||
* 将 pms-common 中的 JwtUtils 注册为 Spring Bean
|
||||
*/
|
||||
@Configuration
|
||||
public class JwtBeanConfig {
|
||||
|
||||
/**
|
||||
* JwtUtils Bean
|
||||
* 持有 RSA 私钥用于签发令牌
|
||||
*/
|
||||
@Bean
|
||||
public JwtUtils jwtUtils(JwtProperties jwtProperties) {
|
||||
return new JwtUtils(jwtProperties);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.pms.auth.config;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
|
||||
import com.pms.common.config.PmsTenantLineHandler;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* MyBatis-Plus 配置
|
||||
* 乐观锁 + 多租户 + 分页插件
|
||||
* 拦截器顺序:OptimisticLocker → TenantLine → Pagination
|
||||
*/
|
||||
@Configuration
|
||||
public class MyBatisPlusConfig {
|
||||
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
// 乐观锁插件
|
||||
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
|
||||
// 多租户插件(基于 project_id 行级隔离)
|
||||
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new PmsTenantLineHandler()));
|
||||
// 分页插件
|
||||
PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
|
||||
pageInterceptor.setMaxLimit(100L);
|
||||
pageInterceptor.setOverflow(false);
|
||||
interceptor.addInnerInterceptor(pageInterceptor);
|
||||
return interceptor;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package com.pms.auth.config;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
|
||||
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
/**
|
||||
* Redis 配置
|
||||
* 自定义序列化方式:Key 使用 String,Value 使用 JSON
|
||||
* <p>
|
||||
* 安全修复:使用 BasicPolymorphicTypeValidator 白名单校验器替代 LaissezFaireSubTypeValidator,
|
||||
* 仅允许 com.pms.* 包下及标准 Java 类型反序列化,防止远程代码执行(RCE)漏洞。
|
||||
*/
|
||||
@Configuration
|
||||
public class RedisConfig {
|
||||
|
||||
/**
|
||||
* RedisTemplate 配置(通用)
|
||||
*/
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(factory);
|
||||
|
||||
// Key 使用 String 序列化
|
||||
StringRedisSerializer stringSerializer = new StringRedisSerializer();
|
||||
template.setKeySerializer(stringSerializer);
|
||||
template.setHashKeySerializer(stringSerializer);
|
||||
|
||||
// Value 使用 Jackson JSON 序列化
|
||||
Jackson2JsonRedisSerializer<Object> jsonSerializer = jackson2JsonRedisSerializer();
|
||||
template.setValueSerializer(jsonSerializer);
|
||||
template.setHashValueSerializer(jsonSerializer);
|
||||
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* StringRedisTemplate(用于存储简单字符串,如验证码、Token黑名单)
|
||||
*/
|
||||
@Bean
|
||||
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
|
||||
return new StringRedisTemplate(factory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Jackson JSON 序列化器
|
||||
* <p>
|
||||
* 安全修复:使用白名单 PolymorphicTypeValidator 替代 LaissezFaireSubTypeValidator.instance。
|
||||
* 白名单仅允许以下包/类进行多态反序列化:
|
||||
* - com.pms.common.entity, com.pms.auth.entity 等业务实体
|
||||
* - java.util.*, java.lang.*, java.time.* 等标准 Java 类型
|
||||
* 防止攻击者通过构造恶意 JSON 触发任意类实例化导致 RCE。
|
||||
*/
|
||||
private Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer() {
|
||||
// 构建白名单类型校验器,仅允许安全的包前缀进行多态反序列化
|
||||
PolymorphicTypeValidator typeValidator = BasicPolymorphicTypeValidator.builder()
|
||||
.allowIfBaseType(Object.class)
|
||||
.allowIfSubType("com.pms.")
|
||||
.allowIfSubType("java.util.")
|
||||
.allowIfSubType("java.lang.")
|
||||
.allowIfSubType("java.time.")
|
||||
.allowIfSubType("java.math.")
|
||||
.build();
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
|
||||
// 使用白名单校验器 + As.PROPERTY 类型信息嵌入方式
|
||||
mapper.activateDefaultTyping(typeValidator,
|
||||
ObjectMapper.DefaultTyping.NON_FINAL,
|
||||
JsonTypeInfo.As.PROPERTY);
|
||||
return new Jackson2JsonRedisSerializer<>(mapper, Object.class);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.pms.auth.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
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.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
/**
|
||||
* Spring Security 配置
|
||||
* 认证服务为无状态 API,JWT 鉴权由网关统一处理
|
||||
* 此处主要提供 BCrypt 密码编码器,并放行登录相关接口
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
// 关闭 CSRF(无状态 API)
|
||||
.csrf(csrf -> csrf.disable())
|
||||
// 关闭表单登录与默认认证
|
||||
.formLogin(form -> form.disable())
|
||||
.httpBasic(basic -> basic.disable())
|
||||
// 无状态会话
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
// 放行所有请求:JWT 鉴权由网关统一处理,下游服务无需再次认证
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.anyRequest().permitAll()
|
||||
);
|
||||
return http.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* BCrypt 密码编码器
|
||||
*/
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.pms.auth.config;
|
||||
|
||||
import com.pms.common.interceptor.UserContextInterceptor;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* Web MVC 配置
|
||||
* 注册拦截器
|
||||
*/
|
||||
@Configuration
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
private final UserContextInterceptor userContextInterceptor;
|
||||
|
||||
public WebMvcConfig(UserContextInterceptor userContextInterceptor) {
|
||||
this.userContextInterceptor = userContextInterceptor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 鉴权白名单(无需解析用户上下文的路径)
|
||||
*/
|
||||
private static final String[] WHITELIST = {
|
||||
"/api/v1/auth/login",
|
||||
"/api/v1/auth/refresh",
|
||||
"/api/v1/auth/captcha",
|
||||
"/actuator/**",
|
||||
"/error"
|
||||
};
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(userContextInterceptor)
|
||||
.addPathPatterns("/api/v1/**")
|
||||
.excludePathPatterns(WHITELIST);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.pms.auth.constant;
|
||||
|
||||
/**
|
||||
* 认证服务常量
|
||||
*/
|
||||
public final class AuthConstants {
|
||||
|
||||
private AuthConstants() {
|
||||
}
|
||||
|
||||
// ====== Redis Key 前缀 ======
|
||||
|
||||
/** JWT 黑名单前缀 */
|
||||
public static final String JWT_BLACKLIST_ACCESS = "jwt:blacklist:access:";
|
||||
public static final String JWT_BLACKLIST_REFRESH = "jwt:blacklist:refresh:";
|
||||
|
||||
/** 验证码 Redis Key 前缀 */
|
||||
public static final String CAPTCHA_KEY = "captcha:";
|
||||
|
||||
/** 用户权限缓存前缀 */
|
||||
public static final String USER_PERMISSIONS_KEY = "user:permissions:";
|
||||
|
||||
/** 用户角色缓存前缀 */
|
||||
public static final String USER_ROLES_KEY = "user:roles:";
|
||||
|
||||
/** 用户信息缓存前缀 */
|
||||
public static final String USER_INFO_CACHE_KEY = "user:info:";
|
||||
|
||||
/** 登录失败计数 Redis Key 前缀 */
|
||||
public static final String LOGIN_FAIL_KEY = "login:fail:";
|
||||
|
||||
/** 登录锁定 Redis Key 前缀 */
|
||||
public static final String LOGIN_LOCK_KEY = "login:lock:";
|
||||
|
||||
// ====== 验证码配置 ======
|
||||
|
||||
/** 验证码有效期(秒) */
|
||||
public static final long CAPTCHA_EXPIRE = 300;
|
||||
|
||||
// ====== 登录失败锁定配置 ======
|
||||
|
||||
/** 最大连续登录失败次数 */
|
||||
public static final int MAX_FAIL_COUNT = 5;
|
||||
|
||||
/** 锁定时长(秒) */
|
||||
public static final long LOCK_DURATION = 15 * 60;
|
||||
|
||||
/** 用户信息缓存 TTL 基准值(秒) */
|
||||
public static final int USER_CACHE_TTL = 1800;
|
||||
|
||||
/** 用户信息缓存 TTL 随机偏移上限(秒),防雪崩 */
|
||||
public static final int USER_CACHE_TTL_JITTER = 300;
|
||||
|
||||
/** 空值缓存 TTL(秒),防穿透 */
|
||||
public static final int NULL_CACHE_TTL = 60;
|
||||
|
||||
// ====== 默认密码 ======
|
||||
|
||||
/** 系统默认密码 */
|
||||
public static final String DEFAULT_PASSWORD = "Init@123456";
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
package com.pms.auth.controller;
|
||||
|
||||
import com.pms.auth.dto.*;
|
||||
import com.pms.auth.service.AuthService;
|
||||
import com.pms.common.constant.CommonConstants;
|
||||
import com.pms.common.response.Result;
|
||||
import com.pms.common.security.UserContext;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 认证控制器
|
||||
* 路径前缀: /api/v1/auth
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/auth")
|
||||
@RequiredArgsConstructor
|
||||
public class AuthController {
|
||||
|
||||
private final AuthService authService;
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
@PostMapping("/login")
|
||||
public Result<LoginResponse> login(@Valid @RequestBody LoginRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
String clientIp = getClientIp(httpRequest);
|
||||
return Result.success(authService.login(request, clientIp));
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
@PostMapping("/logout")
|
||||
public Result<Void> logout(@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||
@RequestBody(required = false) RefreshTokenRequest request) {
|
||||
String accessToken = null;
|
||||
if (authHeader != null && authHeader.startsWith(CommonConstants.BEARER_PREFIX)) {
|
||||
accessToken = authHeader.substring(CommonConstants.BEARER_PREFIX.length());
|
||||
}
|
||||
String refreshToken = request != null ? request.getRefreshToken() : null;
|
||||
authService.logout(accessToken, refreshToken);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新令牌
|
||||
*/
|
||||
@PostMapping("/refresh")
|
||||
public Result<TokenRefreshResponse> refresh(@Valid @RequestBody RefreshTokenRequest request) {
|
||||
return Result.success(authService.refreshToken(request.getRefreshToken()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取验证码
|
||||
*/
|
||||
@GetMapping("/captcha")
|
||||
public Result<CaptchaResponse> captcha() {
|
||||
return Result.success(authService.getCaptcha());
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*/
|
||||
@PutMapping("/password")
|
||||
public Result<Void> changePassword(@Valid @RequestBody ChangePasswordRequest request) {
|
||||
authService.changePassword(UserContext.getUserId(), request);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置密码(管理员)
|
||||
*/
|
||||
@PutMapping("/password/reset")
|
||||
public Result<Void> resetPassword(@Valid @RequestBody ResetPasswordRequest request) {
|
||||
authService.resetPassword(request);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*/
|
||||
@GetMapping("/userinfo")
|
||||
public Result<UserInfoDTO> userInfo() {
|
||||
return Result.success(authService.getCurrentUserInfo());
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限检查
|
||||
*/
|
||||
@PostMapping("/check-permission")
|
||||
public Result<Boolean> checkPermission(@RequestParam Long userId,
|
||||
@RequestParam String permissionCode,
|
||||
@RequestParam(required = false) Long projectId) {
|
||||
return Result.success(authService.checkPermission(userId, permissionCode, projectId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端 IP
|
||||
*/
|
||||
private String getClientIp(HttpServletRequest request) {
|
||||
String ip = request.getHeader("X-Forwarded-For");
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("X-Real-IP");
|
||||
}
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getRemoteAddr();
|
||||
}
|
||||
// 多级代理时取第一个
|
||||
if (ip != null && ip.contains(",")) {
|
||||
ip = ip.split(",")[0].trim();
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
package com.pms.auth.controller;
|
||||
|
||||
import com.pms.auth.dto.UserDTO;
|
||||
import com.pms.auth.mapper.UserMapper;
|
||||
import com.pms.common.dto.internal.InternalServiceDTOs;
|
||||
import com.pms.common.dto.internal.InternalServiceDTOs.PermissionResultDTO;
|
||||
import com.pms.common.dto.internal.InternalServiceDTOs.VerifyPermissionRequest;
|
||||
import com.pms.common.response.Result;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* auth-service 内部接口控制器
|
||||
* <p>
|
||||
* 供其他微服务通过 Feign 调用验证权限、查询用户信息。
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
public class InternalController {
|
||||
|
||||
private final UserMapper userMapper;
|
||||
|
||||
/**
|
||||
* 验证用户权限
|
||||
*/
|
||||
@PostMapping("/api/internal/auth/verify-permission")
|
||||
public Result<PermissionResultDTO> verifyPermission(@Valid @RequestBody VerifyPermissionRequest request) {
|
||||
log.info("内部接口-验证权限: userId={}, permission={}",
|
||||
request.getUserId(), request.getPermissionCode());
|
||||
|
||||
List<String> permissions = userMapper.selectPermissionCodesByUserId(request.getUserId());
|
||||
boolean hasPermission = permissions != null && permissions.contains(request.getPermissionCode());
|
||||
|
||||
PermissionResultDTO result = new PermissionResultDTO();
|
||||
result.setHasPermission(hasPermission);
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户信息
|
||||
*/
|
||||
@GetMapping("/api/internal/auth/users/{userId}")
|
||||
public Result<InternalServiceDTOs.UserDTO> getUser(@PathVariable Long userId) {
|
||||
log.info("内部接口-查询用户: userId={}", userId);
|
||||
|
||||
UserDTO user = userMapper.selectUserWithRolesById(userId);
|
||||
if (user == null) {
|
||||
return Result.success(null);
|
||||
}
|
||||
|
||||
InternalServiceDTOs.UserDTO dto = new InternalServiceDTOs.UserDTO();
|
||||
dto.setId(user.getId());
|
||||
dto.setUsername(user.getUsername());
|
||||
dto.setRealName(user.getRealName());
|
||||
dto.setPhone(user.getPhone());
|
||||
dto.setOrgId(user.getOrgId());
|
||||
dto.setOrgName(user.getOrgName());
|
||||
|
||||
// 角色编码列表
|
||||
if (user.getRoles() != null) {
|
||||
dto.setRoles(user.getRoles().stream()
|
||||
.map(role -> role.getRoleCode())
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
// 权限编码列表
|
||||
List<String> permissions = userMapper.selectPermissionCodesByUserId(userId);
|
||||
dto.setPermissions(permissions);
|
||||
|
||||
return Result.success(dto);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.pms.auth.controller;
|
||||
|
||||
import com.pms.auth.dto.OrgDTO;
|
||||
import com.pms.auth.dto.OrgSaveRequest;
|
||||
import com.pms.auth.service.OrgService;
|
||||
import com.pms.common.response.Result;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 组织管理控制器
|
||||
* 路径前缀: /api/v1/auth/orgs
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/auth/orgs")
|
||||
@RequiredArgsConstructor
|
||||
public class OrgController {
|
||||
|
||||
private final OrgService orgService;
|
||||
|
||||
/**
|
||||
* 组织架构树
|
||||
*/
|
||||
@GetMapping
|
||||
public Result<List<OrgDTO>> list(@RequestParam(required = false) Long parentId,
|
||||
@RequestParam(required = false) Integer status,
|
||||
@RequestParam(required = false) Long projectId) {
|
||||
return Result.success(orgService.list(parentId, status, projectId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建组织节点
|
||||
*/
|
||||
@PostMapping
|
||||
public Result<Map<String, Long>> create(@Valid @RequestBody OrgSaveRequest request) {
|
||||
Long id = orgService.create(request);
|
||||
Map<String, Long> data = new HashMap<>();
|
||||
data.put("id", id);
|
||||
return Result.success(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新组织节点
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody OrgSaveRequest request) {
|
||||
orgService.update(id, request);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除组织节点
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
orgService.delete(id);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.pms.auth.controller;
|
||||
|
||||
import com.pms.auth.dto.PermissionTreeResult;
|
||||
import com.pms.auth.service.PermissionService;
|
||||
import com.pms.common.response.Result;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 权限管理控制器
|
||||
* 路径前缀: /api/v1/auth/permissions
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/auth/permissions")
|
||||
@RequiredArgsConstructor
|
||||
public class PermissionController {
|
||||
|
||||
private final PermissionService permissionService;
|
||||
|
||||
/**
|
||||
* 权限树查询
|
||||
*/
|
||||
@GetMapping
|
||||
public Result<PermissionTreeResult> tree(@RequestParam(required = false) Long roleId,
|
||||
@RequestParam(required = false) Integer permType) {
|
||||
return Result.success(permissionService.getPermissionTree(roleId, permType));
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限树查询(/tree 别名)
|
||||
*/
|
||||
@GetMapping("/tree")
|
||||
public Result<PermissionTreeResult> treeAlias(@RequestParam(required = false) Long roleId,
|
||||
@RequestParam(required = false) Integer permType) {
|
||||
return Result.success(permissionService.getPermissionTree(roleId, permType));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package com.pms.auth.controller;
|
||||
|
||||
import com.pms.auth.dto.*;
|
||||
import com.pms.auth.service.RoleService;
|
||||
import com.pms.common.response.PageResult;
|
||||
import com.pms.common.response.Result;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 角色管理控制器
|
||||
* 路径前缀: /api/v1/auth/roles
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/auth/roles")
|
||||
@RequiredArgsConstructor
|
||||
public class RoleController {
|
||||
|
||||
private final RoleService roleService;
|
||||
|
||||
/**
|
||||
* 角色列表
|
||||
*/
|
||||
@GetMapping
|
||||
public Result<List<RoleDTO>> list(@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false) Integer status,
|
||||
@RequestParam(required = false) Long projectId,
|
||||
@RequestParam(required = false, defaultValue = "false") boolean all) {
|
||||
return Result.success(roleService.list(keyword, status, projectId, all));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建角色
|
||||
*/
|
||||
@PostMapping
|
||||
public Result<Map<String, Long>> create(@Valid @RequestBody RoleSaveRequest request) {
|
||||
Long id = roleService.create(request);
|
||||
Map<String, Long> data = new HashMap<>();
|
||||
data.put("id", id);
|
||||
return Result.success(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public Result<RoleDTO> getById(@PathVariable Long id) {
|
||||
return Result.success(roleService.getById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新角色
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody RoleSaveRequest request) {
|
||||
roleService.update(id, request);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除角色
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
roleService.delete(id);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 分配权限
|
||||
*/
|
||||
@PostMapping("/{id}/permissions")
|
||||
public Result<Void> assignPermissions(@PathVariable Long id,
|
||||
@Valid @RequestBody AssignPermissionsRequest request) {
|
||||
roleService.assignPermissions(id, request);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
package com.pms.auth.controller;
|
||||
|
||||
import com.pms.auth.dto.*;
|
||||
import com.pms.auth.service.UserService;
|
||||
import com.pms.common.response.PageResult;
|
||||
import com.pms.common.response.Result;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 用户管理控制器
|
||||
* 路径前缀: /api/v1/auth/users
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/auth/users")
|
||||
@RequiredArgsConstructor
|
||||
public class UserController {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
/**
|
||||
* 用户列表查询(分页)
|
||||
*/
|
||||
@GetMapping
|
||||
public Result<PageResult<UserDTO>> list(UserQueryRequest request) {
|
||||
return Result.success(userService.page(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建用户
|
||||
*/
|
||||
@PostMapping
|
||||
public Result<Map<String, Long>> create(@Valid @RequestBody UserSaveRequest request) {
|
||||
Long id = userService.create(request);
|
||||
Map<String, Long> data = new HashMap<>();
|
||||
data.put("id", id);
|
||||
return Result.success(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public Result<UserDTO> getById(@PathVariable Long id) {
|
||||
return Result.success(userService.getById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody UserUpdateRequest request) {
|
||||
userService.update(id, request);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
userService.delete(id);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户状态
|
||||
*/
|
||||
@PutMapping("/{id}/status")
|
||||
public Result<Void> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
|
||||
userService.updateStatus(id, status);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 分配角色
|
||||
*/
|
||||
@PostMapping("/{id}/roles")
|
||||
public Result<Void> assignRoles(@PathVariable Long id, @Valid @RequestBody AssignRolesRequest request) {
|
||||
userService.assignRoles(id, request);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.pms.auth.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 分配权限请求
|
||||
*/
|
||||
@Data
|
||||
public class AssignPermissionsRequest implements Serializable {
|
||||
|
||||
/** 权限 ID 列表 */
|
||||
@NotNull(message = "permissionIds不能为null")
|
||||
private List<Long> permissionIds;
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.pms.auth.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 分配角色请求
|
||||
*/
|
||||
@Data
|
||||
public class AssignRolesRequest implements Serializable {
|
||||
|
||||
/** 角色 ID 列表(可空数组表示清空角色) */
|
||||
@NotNull(message = "roleIds不能为null")
|
||||
private List<Long> roleIds;
|
||||
|
||||
/** 项目ID,不传则为全局角色 */
|
||||
private Long projectId;
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.pms.auth.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 验证码响应
|
||||
*/
|
||||
@Data
|
||||
public class CaptchaResponse implements Serializable {
|
||||
|
||||
/** 验证码 UUID,登录时回传 */
|
||||
private String captchaKey;
|
||||
|
||||
/** Base64 编码的 PNG 图片,不含前缀 */
|
||||
private String captchaImg;
|
||||
|
||||
/** 有效期秒数 */
|
||||
private long expireSeconds;
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.pms.auth.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 修改密码请求
|
||||
*/
|
||||
@Data
|
||||
public class ChangePasswordRequest implements Serializable {
|
||||
|
||||
@NotBlank(message = "原密码不能为空")
|
||||
private String oldPassword;
|
||||
|
||||
@NotBlank(message = "新密码不能为空")
|
||||
@Size(min = 8, max = 20, message = "新密码长度8-20位")
|
||||
private String newPassword;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.pms.auth.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 登录请求
|
||||
*/
|
||||
@Data
|
||||
public class LoginRequest implements Serializable {
|
||||
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
private String username;
|
||||
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String password;
|
||||
|
||||
private String captchaKey;
|
||||
|
||||
private String captchaCode;
|
||||
|
||||
/** 指定登录的项目ID,不传则使用默认项目 */
|
||||
private Long projectId;
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.pms.auth.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 登录响应
|
||||
*/
|
||||
@Data
|
||||
public class LoginResponse implements Serializable {
|
||||
|
||||
/** 访问令牌 */
|
||||
private String accessToken;
|
||||
|
||||
/** 刷新令牌 */
|
||||
private String refreshToken;
|
||||
|
||||
/** 令牌类型 */
|
||||
private String tokenType = "bearer";
|
||||
|
||||
/** accessToken 剩余秒数 */
|
||||
private long expiresIn;
|
||||
|
||||
/** 用户信息 */
|
||||
private UserInfoDTO userInfo;
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.pms.auth.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 组织DTO(树形结构)
|
||||
*/
|
||||
@Data
|
||||
public class OrgDTO implements Serializable {
|
||||
|
||||
private Long id;
|
||||
|
||||
private Long parentId;
|
||||
|
||||
private String orgCode;
|
||||
|
||||
private String orgName;
|
||||
|
||||
/** 类型:1公司 2部门 3小组 4项目部 */
|
||||
private Integer orgType;
|
||||
|
||||
private Long leaderId;
|
||||
|
||||
/** 负责人姓名 */
|
||||
private String leaderName;
|
||||
|
||||
private String phone;
|
||||
|
||||
private Integer sort;
|
||||
|
||||
private Integer status;
|
||||
|
||||
/** 子组织列表 */
|
||||
private List<OrgDTO> children;
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.pms.auth.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 创建/更新组织请求
|
||||
*/
|
||||
@Data
|
||||
public class OrgSaveRequest implements Serializable {
|
||||
|
||||
private Long parentId;
|
||||
|
||||
@NotBlank(message = "组织编码不能为空")
|
||||
@Size(max = 64, message = "组织编码最长64位")
|
||||
private String orgCode;
|
||||
|
||||
@NotBlank(message = "组织名称不能为空")
|
||||
@Size(max = 128, message = "组织名称最长128位")
|
||||
private String orgName;
|
||||
|
||||
@NotNull(message = "组织类型不能为空")
|
||||
private Integer orgType;
|
||||
|
||||
private Long leaderId;
|
||||
|
||||
@Size(max = 20, message = "联系电话最长20位")
|
||||
private String phone;
|
||||
|
||||
private Integer sort;
|
||||
|
||||
private Integer status;
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.pms.auth.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 权限DTO(树形结构)
|
||||
*/
|
||||
@Data
|
||||
public class PermissionDTO implements Serializable {
|
||||
|
||||
private Long id;
|
||||
|
||||
private Long parentId;
|
||||
|
||||
private String permCode;
|
||||
|
||||
private String permName;
|
||||
|
||||
/** 类型:1菜单 2按钮 3接口 */
|
||||
private Integer permType;
|
||||
|
||||
private String path;
|
||||
|
||||
private String component;
|
||||
|
||||
private String icon;
|
||||
|
||||
private String apiMethod;
|
||||
|
||||
private String apiPath;
|
||||
|
||||
private Integer sort;
|
||||
|
||||
private Integer isHidden;
|
||||
|
||||
private Integer status;
|
||||
|
||||
/** 子权限列表 */
|
||||
private List<PermissionDTO> children;
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.pms.auth.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 权限树查询结果
|
||||
*/
|
||||
@Data
|
||||
public class PermissionTreeResult implements Serializable {
|
||||
|
||||
/** 权限树 */
|
||||
private List<PermissionDTO> tree;
|
||||
|
||||
/** 已勾选权限 ID(roleId 不为空时返回) */
|
||||
private List<Long> checkedKeys;
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.pms.auth.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 项目信息DTO(用于用户详情中的项目列表)
|
||||
*/
|
||||
@Data
|
||||
public class ProjectInfoDTO implements Serializable {
|
||||
|
||||
private Long id;
|
||||
|
||||
private String projectName;
|
||||
|
||||
/** 是否默认项目:0否 1是 */
|
||||
private Integer isDefault;
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.pms.auth.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 刷新令牌请求
|
||||
*/
|
||||
@Data
|
||||
public class RefreshTokenRequest implements Serializable {
|
||||
|
||||
@NotBlank(message = "refreshToken不能为空")
|
||||
private String refreshToken;
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.pms.auth.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 重置密码请求
|
||||
*/
|
||||
@Data
|
||||
public class ResetPasswordRequest implements Serializable {
|
||||
|
||||
@NotNull(message = "用户ID不能为空")
|
||||
private Long userId;
|
||||
|
||||
/** 新密码,不传则使用系统默认密码 */
|
||||
private String newPassword;
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.pms.auth.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 角色DTO
|
||||
*/
|
||||
@Data
|
||||
public class RoleDTO implements Serializable {
|
||||
|
||||
private Long id;
|
||||
|
||||
private String roleCode;
|
||||
|
||||
private String roleName;
|
||||
|
||||
/** 数据范围:1全部 2本组织 3本组织及下级 4本人 5自定义 */
|
||||
private Integer dataScope;
|
||||
|
||||
private Integer sort;
|
||||
|
||||
private Integer status;
|
||||
|
||||
private String remark;
|
||||
|
||||
private Long projectId;
|
||||
|
||||
private Long createdAt;
|
||||
|
||||
private Long updatedAt;
|
||||
|
||||
/** 关联用户数 */
|
||||
private Integer userCount;
|
||||
|
||||
/** 权限ID列表(角色详情时返回) */
|
||||
private List<Long> permissionIds;
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.pms.auth.dto;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 创建/更新角色请求
|
||||
*/
|
||||
@Data
|
||||
public class RoleSaveRequest implements Serializable {
|
||||
|
||||
@NotBlank(message = "角色编码不能为空")
|
||||
@Size(max = 64, message = "角色编码最长64位")
|
||||
private String roleCode;
|
||||
|
||||
@NotBlank(message = "角色名称不能为空")
|
||||
@Size(max = 64, message = "角色名称最长64位")
|
||||
private String roleName;
|
||||
|
||||
/** 数据范围,默认1 */
|
||||
private Integer dataScope = 1;
|
||||
|
||||
private Long projectId;
|
||||
|
||||
private Integer sort = 0;
|
||||
|
||||
private Integer status = 1;
|
||||
|
||||
@Size(max = 255, message = "备注最长255位")
|
||||
private String remark;
|
||||
|
||||
/** 权限 ID 列表 */
|
||||
private List<Long> permissionIds;
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.pms.auth.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 令牌刷新响应
|
||||
*/
|
||||
@Data
|
||||
public class TokenRefreshResponse implements Serializable {
|
||||
|
||||
private String accessToken;
|
||||
|
||||
private String refreshToken;
|
||||
|
||||
private String tokenType = "bearer";
|
||||
|
||||
private long expiresIn;
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package com.pms.auth.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 用户信息DTO(含角色、组织等关联信息)
|
||||
*/
|
||||
@Data
|
||||
public class UserDTO implements Serializable {
|
||||
|
||||
private Long id;
|
||||
|
||||
private String username;
|
||||
|
||||
/** 密码(仅内部使用,不返回前端) */
|
||||
private transient String password;
|
||||
|
||||
private String realName;
|
||||
|
||||
private String nickname;
|
||||
|
||||
private Integer gender;
|
||||
|
||||
private String phone;
|
||||
|
||||
private String email;
|
||||
|
||||
private String avatar;
|
||||
|
||||
private Long orgId;
|
||||
|
||||
/** 组织名称 */
|
||||
private String orgName;
|
||||
|
||||
private Integer status;
|
||||
|
||||
private Integer accountType;
|
||||
|
||||
private Long lastLoginAt;
|
||||
|
||||
private String lastLoginIp;
|
||||
|
||||
private Integer failCount;
|
||||
|
||||
private Long lockUntil;
|
||||
|
||||
private String remark;
|
||||
|
||||
private Long projectId;
|
||||
|
||||
private Long createdAt;
|
||||
|
||||
private Long updatedAt;
|
||||
|
||||
/** 角色列表 */
|
||||
private java.util.List<RoleDTO> roles;
|
||||
|
||||
/** 可访问项目列表 */
|
||||
private java.util.List<ProjectInfoDTO> projects;
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.pms.auth.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 当前用户信息(含角色和权限)
|
||||
*/
|
||||
@Data
|
||||
public class UserInfoDTO implements Serializable {
|
||||
|
||||
private Long id;
|
||||
|
||||
private String username;
|
||||
|
||||
private String realName;
|
||||
|
||||
private String nickname;
|
||||
|
||||
private Integer gender;
|
||||
|
||||
private String phone;
|
||||
|
||||
private String email;
|
||||
|
||||
private String avatar;
|
||||
|
||||
private Long orgId;
|
||||
|
||||
private String orgName;
|
||||
|
||||
private Long projectId;
|
||||
|
||||
private String projectName;
|
||||
|
||||
/** 角色列表 */
|
||||
private List<RoleDTO> roles;
|
||||
|
||||
/** 权限编码列表 */
|
||||
private List<String> permissions;
|
||||
|
||||
/** 菜单树 */
|
||||
private List<PermissionDTO> menus;
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.pms.auth.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 用户列表查询参数
|
||||
*/
|
||||
@Data
|
||||
public class UserQueryRequest implements Serializable {
|
||||
|
||||
/** 页码,默认1 */
|
||||
private Integer page = 1;
|
||||
|
||||
/** 每页条数,默认20 */
|
||||
private Integer size = 20;
|
||||
|
||||
/** 关键字(用户名/姓名/手机号模糊匹配) */
|
||||
private String keyword;
|
||||
|
||||
/** 组织ID */
|
||||
private Long orgId;
|
||||
|
||||
/** 状态:0禁用 1启用 */
|
||||
private Integer status;
|
||||
|
||||
/** 角色 ID(筛选拥有该角色的用户) */
|
||||
private Long roleId;
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.pms.auth.dto;
|
||||
|
||||
import jakarta.validation.constraints.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 创建用户请求
|
||||
*/
|
||||
@Data
|
||||
public class UserSaveRequest implements Serializable {
|
||||
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
@Size(min = 4, max = 64, message = "用户名长度4-64位")
|
||||
private String username;
|
||||
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String password;
|
||||
|
||||
@NotBlank(message = "真实姓名不能为空")
|
||||
@Size(max = 64, message = "真实姓名最长64位")
|
||||
private String realName;
|
||||
|
||||
private String nickname;
|
||||
|
||||
private Integer gender;
|
||||
|
||||
@Size(max = 20, message = "手机号最长20位")
|
||||
private String phone;
|
||||
|
||||
@Size(max = 128, message = "邮箱最长128位")
|
||||
private String email;
|
||||
|
||||
private Long orgId;
|
||||
|
||||
@Size(max = 255, message = "备注最长255位")
|
||||
private String remark;
|
||||
|
||||
/** 角色 ID 列表 */
|
||||
private List<Long> roleIds;
|
||||
|
||||
/** 可访问项目 ID 列表 */
|
||||
private List<Long> projectIds;
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.pms.auth.dto;
|
||||
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 用户更新请求
|
||||
*/
|
||||
@Data
|
||||
public class UserUpdateRequest implements Serializable {
|
||||
|
||||
@Size(max = 64, message = "真实姓名最长64位")
|
||||
private String realName;
|
||||
|
||||
@Size(max = 64, message = "昵称最长64位")
|
||||
private String nickname;
|
||||
|
||||
private Integer gender;
|
||||
|
||||
@Size(max = 20, message = "手机号最长20位")
|
||||
private String phone;
|
||||
|
||||
@Size(max = 128, message = "邮箱最长128位")
|
||||
private String email;
|
||||
|
||||
private Long orgId;
|
||||
|
||||
private Integer status;
|
||||
|
||||
@Size(max = 255, message = "备注最长255位")
|
||||
private String remark;
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.pms.auth.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.pms.common.entity.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 组织架构实体
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("t_org")
|
||||
public class Org extends BaseEntity {
|
||||
|
||||
/** 父组织ID,0表示根节点 */
|
||||
private Long parentId;
|
||||
|
||||
/** 组织编码 */
|
||||
private String orgCode;
|
||||
|
||||
/** 组织名称 */
|
||||
private String orgName;
|
||||
|
||||
/** 类型:1公司 2部门 3小组 4项目部 */
|
||||
private Integer orgType;
|
||||
|
||||
/** 负责人用户ID */
|
||||
private Long leaderId;
|
||||
|
||||
/** 联系电话 */
|
||||
private String phone;
|
||||
|
||||
/** 排序号 */
|
||||
private Integer sort;
|
||||
|
||||
/** 状态:0禁用 1启用 */
|
||||
private Integer status;
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package com.pms.auth.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.pms.common.entity.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 权限实体
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("t_permission")
|
||||
public class Permission extends BaseEntity {
|
||||
|
||||
/** 父权限ID,0表示根节点 */
|
||||
private Long parentId;
|
||||
|
||||
/** 权限编码 */
|
||||
private String permCode;
|
||||
|
||||
/** 权限名称 */
|
||||
private String permName;
|
||||
|
||||
/** 类型:1菜单 2按钮 3接口 */
|
||||
private Integer permType;
|
||||
|
||||
/** 前端路由路径 */
|
||||
private String path;
|
||||
|
||||
/** 前端组件路径 */
|
||||
private String component;
|
||||
|
||||
/** 菜单图标 */
|
||||
private String icon;
|
||||
|
||||
/** 接口方法:GET/POST/PUT/DELETE */
|
||||
private String apiMethod;
|
||||
|
||||
/** 接口路径 */
|
||||
private String apiPath;
|
||||
|
||||
/** 排序号 */
|
||||
private Integer sort;
|
||||
|
||||
/** 是否隐藏:0否 1是 */
|
||||
private Integer isHidden;
|
||||
|
||||
/** 状态:0禁用 1启用 */
|
||||
private Integer status;
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.pms.auth.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.pms.common.entity.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 项目用户关联实体
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("t_project_user")
|
||||
public class ProjectUser extends BaseEntity {
|
||||
|
||||
/** 用户ID */
|
||||
private Long userId;
|
||||
|
||||
/** 组织ID */
|
||||
private Long orgId;
|
||||
|
||||
/** 是否默认项目:0否 1是 */
|
||||
private Integer isDefault;
|
||||
|
||||
/** 状态:0禁用 1启用 */
|
||||
private Integer status;
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.pms.auth.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.pms.common.entity.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 角色实体
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("t_role")
|
||||
public class Role extends BaseEntity {
|
||||
|
||||
/** 角色编码 */
|
||||
private String roleCode;
|
||||
|
||||
/** 角色名称 */
|
||||
private String roleName;
|
||||
|
||||
/** 数据范围:1全部 2本组织 3本组织及下级 4本人 5自定义 */
|
||||
private Integer dataScope;
|
||||
|
||||
/** 排序号 */
|
||||
private Integer sort;
|
||||
|
||||
/** 状态:0禁用 1启用 */
|
||||
private Integer status;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.pms.auth.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.pms.common.entity.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 角色权限关联实体
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("t_role_permission")
|
||||
public class RolePermission extends BaseEntity {
|
||||
|
||||
/** 角色ID */
|
||||
private Long roleId;
|
||||
|
||||
/** 权限ID */
|
||||
private Long permissionId;
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package com.pms.auth.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.pms.common.entity.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 用户实体
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("t_user")
|
||||
public class User extends BaseEntity {
|
||||
|
||||
/** 登录账号 */
|
||||
private String username;
|
||||
|
||||
/** BCrypt加密密码 */
|
||||
private String password;
|
||||
|
||||
/** 真实姓名 */
|
||||
private String realName;
|
||||
|
||||
/** 昵称 */
|
||||
private String nickname;
|
||||
|
||||
/** 性别:0未知 1男 2女 */
|
||||
private Integer gender;
|
||||
|
||||
/** 手机号 */
|
||||
private String phone;
|
||||
|
||||
/** 邮箱 */
|
||||
private String email;
|
||||
|
||||
/** 头像URL */
|
||||
private String avatar;
|
||||
|
||||
/** 所属组织ID */
|
||||
private Long orgId;
|
||||
|
||||
/** 状态:0禁用 1启用 */
|
||||
private Integer status;
|
||||
|
||||
/** 账号类型:1普通 2系统管理员 */
|
||||
private Integer accountType;
|
||||
|
||||
/** 最后登录时间(时间戳) */
|
||||
private Long lastLoginAt;
|
||||
|
||||
/** 最后登录IP */
|
||||
private String lastLoginIp;
|
||||
|
||||
/** 连续登录失败次数 */
|
||||
private Integer failCount;
|
||||
|
||||
/** 锁定截止时间(时间戳) */
|
||||
private Long lockUntil;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.pms.auth.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.pms.common.entity.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 用户角色关联实体
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("t_user_role")
|
||||
public class UserRole extends BaseEntity {
|
||||
|
||||
/** 用户ID */
|
||||
private Long userId;
|
||||
|
||||
/** 角色ID */
|
||||
private Long roleId;
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.pms.auth.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.pms.auth.entity.Org;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 组织架构Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface OrgMapper extends BaseMapper<Org> {
|
||||
|
||||
/**
|
||||
* 查询全部组织(按排序号排序)
|
||||
*
|
||||
* @param projectId 项目ID
|
||||
* @return 组织列表
|
||||
*/
|
||||
List<Org> selectAllOrgs(@Param("projectId") Long projectId);
|
||||
|
||||
/**
|
||||
* 统计子节点数量
|
||||
*
|
||||
* @param parentId 父节点ID
|
||||
* @return 子节点数量
|
||||
*/
|
||||
int countChildren(@Param("parentId") Long parentId);
|
||||
|
||||
/**
|
||||
* 统计组织下的用户数
|
||||
*
|
||||
* @param orgId 组织ID
|
||||
* @return 用户数
|
||||
*/
|
||||
int countUsersByOrgId(@Param("orgId") Long orgId);
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.pms.auth.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.pms.auth.entity.Permission;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 权限Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface PermissionMapper extends BaseMapper<Permission> {
|
||||
|
||||
/**
|
||||
* 根据用户ID查询权限列表
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 权限列表
|
||||
*/
|
||||
List<Permission> selectPermissionsByUserId(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 根据角色ID查询权限列表
|
||||
*
|
||||
* @param roleId 角色 ID
|
||||
* @return 权限列表
|
||||
*/
|
||||
List<Permission> selectPermissionsByRoleId(@Param("roleId") Long roleId);
|
||||
|
||||
/**
|
||||
* 根据角色ID查询权限编码列表
|
||||
*
|
||||
* @param roleId 角色 ID
|
||||
* @return 权限编码列表
|
||||
*/
|
||||
List<String> selectPermissionCodesByRoleId(@Param("roleId") Long roleId);
|
||||
|
||||
/**
|
||||
* 根据角色ID查询权限ID列表
|
||||
*
|
||||
* @param roleId 角色 ID
|
||||
* @return 权限ID列表
|
||||
*/
|
||||
List<Long> selectPermissionIdsByRoleId(@Param("roleId") Long roleId);
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.pms.auth.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.pms.auth.entity.ProjectUser;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 项目用户关联Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface ProjectUserMapper extends BaseMapper<ProjectUser> {
|
||||
|
||||
/**
|
||||
* 根据用户ID查询关联项目列表
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 项目用户关联列表
|
||||
*/
|
||||
List<ProjectUser> selectByUserId(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 根据用户ID删除所有项目关联
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 删除条数
|
||||
*/
|
||||
int deleteByUserId(@Param("userId") Long userId);
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package com.pms.auth.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.pms.auth.dto.RoleDTO;
|
||||
import com.pms.auth.entity.Role;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 角色Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface RoleMapper extends BaseMapper<Role> {
|
||||
|
||||
/**
|
||||
* 查询角色列表(含关联用户数)
|
||||
*
|
||||
* @param projectId 项目ID
|
||||
* @return 角色列表
|
||||
*/
|
||||
List<RoleDTO> selectRoleListWithUserCount(@Param("projectId") Long projectId);
|
||||
|
||||
/**
|
||||
* 根据用户ID查询角色列表
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 角色列表
|
||||
*/
|
||||
List<Role> selectRolesByUserId(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 根据用户ID查询角色编码列表
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 角色编码列表
|
||||
*/
|
||||
List<String> selectRoleCodesByUserId(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 统计角色关联的用户数
|
||||
*
|
||||
* @param roleId 角色 ID
|
||||
* @return 用户数
|
||||
*/
|
||||
int countUsersByRoleId(@Param("roleId") Long roleId);
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.pms.auth.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.pms.auth.entity.RolePermission;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* 角色权限关联Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface RolePermissionMapper extends BaseMapper<RolePermission> {
|
||||
|
||||
/**
|
||||
* 根据角色ID删除所有权限关联(物理删除,用于覆盖式分配)
|
||||
*
|
||||
* @param roleId 角色 ID
|
||||
* @return 删除条数
|
||||
*/
|
||||
int deleteByRoleId(@Param("roleId") Long roleId);
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package com.pms.auth.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.pms.auth.dto.UserDTO;
|
||||
import com.pms.auth.entity.User;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface UserMapper extends BaseMapper<User> {
|
||||
|
||||
/**
|
||||
* 根据用户名查询用户(含角色信息)
|
||||
*
|
||||
* @param username 用户名
|
||||
* @return 用户信息(含角色列表)
|
||||
*/
|
||||
UserDTO selectUserWithRoles(@Param("username") String username);
|
||||
|
||||
/**
|
||||
* 根据用户ID查询用户(含角色信息)
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 用户信息(含角色列表)
|
||||
*/
|
||||
UserDTO selectUserWithRolesById(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 分页查询用户列表(含角色、组织名称)
|
||||
*
|
||||
* @param page 分页参数
|
||||
* @param keyword 关键字(用户名/姓名/手机号)
|
||||
* @param orgId 组织ID
|
||||
* @param status 状态
|
||||
* @param roleId 角色ID
|
||||
* @param projectId 项目ID
|
||||
* @return 分页结果
|
||||
*/
|
||||
IPage<UserDTO> selectUserPage(IPage<UserDTO> page,
|
||||
@Param("keyword") String keyword,
|
||||
@Param("orgId") Long orgId,
|
||||
@Param("status") Integer status,
|
||||
@Param("roleId") Long roleId,
|
||||
@Param("projectId") Long projectId);
|
||||
|
||||
/**
|
||||
* 获取用户权限编码列表
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 权限编码列表
|
||||
*/
|
||||
List<String> selectPermissionCodesByUserId(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 获取用户角色编码列表
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 角色编码列表
|
||||
*/
|
||||
List<String> selectRoleCodesByUserId(@Param("userId") Long userId);
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.pms.auth.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.pms.auth.entity.UserRole;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户角色关联Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface UserRoleMapper extends BaseMapper<UserRole> {
|
||||
|
||||
/**
|
||||
* 根据用户ID删除所有角色关联(物理删除,用于覆盖式分配)
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 删除条数
|
||||
*/
|
||||
int deleteByUserId(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 根据角色ID查询关联的用户ID列表
|
||||
*
|
||||
* @param roleId 角色 ID
|
||||
* @return 用户ID列表
|
||||
*/
|
||||
List<Long> selectUserIdsByRoleId(@Param("roleId") Long roleId);
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package com.pms.auth.service;
|
||||
|
||||
import com.pms.auth.dto.*;
|
||||
|
||||
/**
|
||||
* 认证服务接口
|
||||
*/
|
||||
public interface AuthService {
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*/
|
||||
LoginResponse login(LoginRequest request, String clientIp);
|
||||
|
||||
/**
|
||||
* 注销
|
||||
*/
|
||||
void logout(String accessToken, String refreshToken);
|
||||
|
||||
/**
|
||||
* 刷新令牌
|
||||
*/
|
||||
TokenRefreshResponse refreshToken(String refreshToken);
|
||||
|
||||
/**
|
||||
* 获取验证码
|
||||
*/
|
||||
CaptchaResponse getCaptcha();
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*/
|
||||
void changePassword(Long userId, ChangePasswordRequest request);
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
*/
|
||||
void resetPassword(ResetPasswordRequest request);
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*/
|
||||
UserInfoDTO getCurrentUserInfo();
|
||||
|
||||
/**
|
||||
* 权限检查
|
||||
*/
|
||||
boolean checkPermission(Long userId, String permissionCode, Long projectId);
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.pms.auth.service;
|
||||
|
||||
import com.pms.auth.dto.OrgDTO;
|
||||
import com.pms.auth.dto.OrgSaveRequest;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 组织管理服务接口
|
||||
*/
|
||||
public interface OrgService {
|
||||
|
||||
/**
|
||||
* 组织列表(树形)
|
||||
*/
|
||||
List<OrgDTO> list(Long parentId, Integer status, Long projectId);
|
||||
|
||||
/**
|
||||
* 创建组织
|
||||
*/
|
||||
Long create(OrgSaveRequest request);
|
||||
|
||||
/**
|
||||
* 更新组织
|
||||
*/
|
||||
void update(Long id, OrgSaveRequest request);
|
||||
|
||||
/**
|
||||
* 删除组织
|
||||
*/
|
||||
void delete(Long id);
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.pms.auth.service;
|
||||
|
||||
import com.pms.auth.dto.PermissionDTO;
|
||||
import com.pms.auth.dto.PermissionTreeResult;
|
||||
|
||||
/**
|
||||
* 权限管理服务接口
|
||||
*/
|
||||
public interface PermissionService {
|
||||
|
||||
/**
|
||||
* 获取权限树
|
||||
*
|
||||
* @param roleId 角色 ID(可选,传入则返回已勾选权限ID列表)
|
||||
* @param permType 权限类型过滤(可选)
|
||||
* @return 权限树结果
|
||||
*/
|
||||
PermissionTreeResult getPermissionTree(Long roleId, Integer permType);
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.pms.auth.service;
|
||||
|
||||
import com.pms.auth.dto.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 角色管理服务接口
|
||||
*/
|
||||
public interface RoleService {
|
||||
|
||||
/**
|
||||
* 角色列表
|
||||
*/
|
||||
List<RoleDTO> list(String keyword, Integer status, Long projectId, boolean all);
|
||||
|
||||
/**
|
||||
* 角色详情(含权限ID列表)
|
||||
*/
|
||||
RoleDTO getById(Long id);
|
||||
|
||||
/**
|
||||
* 创建角色
|
||||
*/
|
||||
Long create(RoleSaveRequest request);
|
||||
|
||||
/**
|
||||
* 更新角色
|
||||
*/
|
||||
void update(Long id, RoleSaveRequest request);
|
||||
|
||||
/**
|
||||
* 删除角色
|
||||
*/
|
||||
void delete(Long id);
|
||||
|
||||
/**
|
||||
* 分配权限
|
||||
*/
|
||||
void assignPermissions(Long roleId, AssignPermissionsRequest request);
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.pms.auth.service;
|
||||
|
||||
import com.pms.auth.dto.*;
|
||||
import com.pms.common.response.PageResult;
|
||||
|
||||
/**
|
||||
* 用户管理服务接口
|
||||
*/
|
||||
public interface UserService {
|
||||
|
||||
/**
|
||||
* 分页查询用户列表
|
||||
*/
|
||||
PageResult<UserDTO> page(UserQueryRequest request);
|
||||
|
||||
/**
|
||||
* 用户详情
|
||||
*/
|
||||
UserDTO getById(Long id);
|
||||
|
||||
/**
|
||||
* 创建用户
|
||||
*/
|
||||
Long create(UserSaveRequest request);
|
||||
|
||||
/**
|
||||
* 更新用户
|
||||
*/
|
||||
void update(Long id, UserUpdateRequest request);
|
||||
|
||||
/**
|
||||
* 删除用户(逻辑删除)
|
||||
*/
|
||||
void delete(Long id);
|
||||
|
||||
/**
|
||||
* 更新用户状态
|
||||
*/
|
||||
void updateStatus(Long id, Integer status);
|
||||
|
||||
/**
|
||||
* 分配角色
|
||||
*/
|
||||
void assignRoles(Long userId, AssignRolesRequest request);
|
||||
}
|
||||
|
|
@ -0,0 +1,617 @@
|
|||
package com.pms.auth.service.impl;
|
||||
|
||||
import cn.hutool.captcha.CaptchaUtil;
|
||||
import cn.hutool.captcha.LineCaptcha;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.pms.auth.constant.AuthConstants;
|
||||
import com.pms.auth.dto.*;
|
||||
import com.pms.auth.entity.Org;
|
||||
import com.pms.auth.entity.Permission;
|
||||
import com.pms.auth.entity.Role;
|
||||
import com.pms.auth.entity.User;
|
||||
import com.pms.auth.mapper.*;
|
||||
import com.pms.auth.service.AuthService;
|
||||
import com.pms.common.exception.BusinessException;
|
||||
import com.pms.common.exception.ErrorCode;
|
||||
import com.pms.common.security.JwtUtils;
|
||||
import com.pms.common.security.UserContext;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.*;
|
||||
import java.util.Base64;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 认证服务实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AuthServiceImpl implements AuthService {
|
||||
|
||||
private final UserMapper userMapper;
|
||||
private final RoleMapper roleMapper;
|
||||
private final PermissionMapper permissionMapper;
|
||||
private final OrgMapper orgMapper;
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
private final JwtUtils jwtUtils;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
@Value("${captcha.debug:false}")
|
||||
private boolean captchaDebug;
|
||||
|
||||
private static final int CAPTCHA_WIDTH = 120;
|
||||
private static final int CAPTCHA_HEIGHT = 40;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public LoginResponse login(LoginRequest request, String clientIp) {
|
||||
// 1. 验证码校验
|
||||
validateCaptcha(request.getCaptchaKey(), request.getCaptchaCode());
|
||||
|
||||
// 2. 查询用户
|
||||
UserDTO userDTO = userMapper.selectUserWithRoles(request.getUsername());
|
||||
if (userDTO == null) {
|
||||
throw new BusinessException(ErrorCode.USERNAME_OR_PASSWORD_ERROR);
|
||||
}
|
||||
|
||||
User user = getUserEntity(userDTO.getId());
|
||||
|
||||
// 3. 账号状态检查
|
||||
if (user.getStatus() == 0) {
|
||||
throw new BusinessException(ErrorCode.ACCOUNT_DISABLED);
|
||||
}
|
||||
|
||||
// 4. 检查锁定
|
||||
checkAccountLock(user);
|
||||
|
||||
// 5. 密码验证
|
||||
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
|
||||
// 记录失败次数
|
||||
incrementFailCount(user);
|
||||
throw new BusinessException(ErrorCode.USERNAME_OR_PASSWORD_ERROR);
|
||||
}
|
||||
|
||||
// 6. 登录成功,重置失败次数
|
||||
resetFailCount(user, clientIp);
|
||||
|
||||
// 7. 收集角色和权限
|
||||
List<String> roleCodes = collectRoleCodes(userDTO);
|
||||
List<String> permissionCodes = collectPermissionCodes(user.getId());
|
||||
|
||||
// 8. 确定 projectId
|
||||
Long projectId = request.getProjectId() != null ? request.getProjectId() : user.getProjectId();
|
||||
|
||||
// 9. 生成 JWT 令牌对
|
||||
Map<String, Object> accessClaims = buildAccessClaims(user, roleCodes, permissionCodes, projectId);
|
||||
Map<String, Object> refreshClaims = buildRefreshClaims(user);
|
||||
|
||||
String accessToken = jwtUtils.signAccessToken(accessClaims);
|
||||
String refreshToken = jwtUtils.signRefreshToken(refreshClaims);
|
||||
|
||||
// 10. 构建响应
|
||||
LoginResponse response = new LoginResponse();
|
||||
response.setAccessToken(accessToken);
|
||||
response.setRefreshToken(refreshToken);
|
||||
response.setExpiresIn(jwtUtils.getAccessTokenExpire());
|
||||
|
||||
UserInfoDTO userInfo = new UserInfoDTO();
|
||||
userInfo.setId(user.getId());
|
||||
userInfo.setUsername(user.getUsername());
|
||||
userInfo.setRealName(user.getRealName());
|
||||
userInfo.setNickname(user.getNickname());
|
||||
userInfo.setAvatar(user.getAvatar());
|
||||
userInfo.setProjectId(projectId);
|
||||
response.setUserInfo(userInfo);
|
||||
|
||||
log.info("用户登录成功: userId={}, username={}", user.getId(), user.getUsername());
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logout(String accessToken, String refreshToken) {
|
||||
// 将 accessToken 加入黑名单
|
||||
if (accessToken != null && !accessToken.isBlank()) {
|
||||
blacklistToken(accessToken, AuthConstants.JWT_BLACKLIST_ACCESS);
|
||||
}
|
||||
// 将 refreshToken 加入黑名单
|
||||
if (refreshToken != null && !refreshToken.isBlank()) {
|
||||
blacklistToken(refreshToken, AuthConstants.JWT_BLACKLIST_REFRESH);
|
||||
}
|
||||
log.info("用户注销成功");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TokenRefreshResponse refreshToken(String refreshToken) {
|
||||
// 1. 验签
|
||||
Claims claims;
|
||||
try {
|
||||
claims = jwtUtils.verify(refreshToken);
|
||||
} catch (Exception e) {
|
||||
throw new BusinessException(ErrorCode.REFRESH_TOKEN_INVALID);
|
||||
}
|
||||
|
||||
// 2. 检查令牌类型
|
||||
String tokenType = claims.get("type", String.class);
|
||||
if (!JwtUtils.TOKEN_TYPE_REFRESH.equals(tokenType)) {
|
||||
throw new BusinessException(ErrorCode.REFRESH_TOKEN_INVALID);
|
||||
}
|
||||
|
||||
// 3. 检查黑名单
|
||||
String jti = claims.getId();
|
||||
if (jti != null) {
|
||||
Boolean isBlacklisted = stringRedisTemplate.hasKey(AuthConstants.JWT_BLACKLIST_REFRESH + jti);
|
||||
if (Boolean.TRUE.equals(isBlacklisted)) {
|
||||
throw new BusinessException(ErrorCode.REFRESH_TOKEN_INVALID);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 获取用户ID
|
||||
Long userId = Long.parseLong(claims.getSubject());
|
||||
UserDTO userDTO = userMapper.selectUserWithRolesById(userId);
|
||||
if (userDTO == null) {
|
||||
throw new BusinessException(ErrorCode.ACCOUNT_NOT_FOUND);
|
||||
}
|
||||
|
||||
User user = getUserEntity(userId);
|
||||
if (user.getStatus() == 0) {
|
||||
throw new BusinessException(ErrorCode.ACCOUNT_DISABLED);
|
||||
}
|
||||
|
||||
// 5. 重新签发令牌对
|
||||
List<String> roleCodes = collectRoleCodes(userDTO);
|
||||
List<String> permissionCodes = collectPermissionCodes(userId);
|
||||
Long projectId = user.getProjectId();
|
||||
|
||||
Map<String, Object> accessClaims = buildAccessClaims(user, roleCodes, permissionCodes, projectId);
|
||||
Map<String, Object> refreshClaims = buildRefreshClaims(user);
|
||||
|
||||
String newAccessToken = jwtUtils.signAccessToken(accessClaims);
|
||||
String newRefreshToken = jwtUtils.signRefreshToken(refreshClaims);
|
||||
|
||||
// 6. 旧 refreshToken 加入黑名单
|
||||
blacklistToken(refreshToken, AuthConstants.JWT_BLACKLIST_REFRESH);
|
||||
|
||||
TokenRefreshResponse response = new TokenRefreshResponse();
|
||||
response.setAccessToken(newAccessToken);
|
||||
response.setRefreshToken(newRefreshToken);
|
||||
response.setExpiresIn(jwtUtils.getAccessTokenExpire());
|
||||
|
||||
log.info("令牌刷新成功: userId={}", userId);
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CaptchaResponse getCaptcha() {
|
||||
// 生成验证码
|
||||
LineCaptcha captcha = CaptchaUtil.createLineCaptcha(CAPTCHA_WIDTH, CAPTCHA_HEIGHT, 4, 30);
|
||||
String code = captcha.getCode();
|
||||
|
||||
// UUID 作为 key
|
||||
String key = IdUtil.fastSimpleUUID();
|
||||
|
||||
// 存入 Redis(5分钟过期)
|
||||
stringRedisTemplate.opsForValue().set(
|
||||
AuthConstants.CAPTCHA_KEY + key,
|
||||
code,
|
||||
AuthConstants.CAPTCHA_EXPIRE,
|
||||
TimeUnit.SECONDS);
|
||||
|
||||
// 转为 Base64
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
captcha.write(bos);
|
||||
String base64Img = Base64.getEncoder().encodeToString(bos.toByteArray());
|
||||
|
||||
CaptchaResponse response = new CaptchaResponse();
|
||||
response.setCaptchaKey(key);
|
||||
response.setCaptchaImg(base64Img);
|
||||
response.setExpireSeconds(AuthConstants.CAPTCHA_EXPIRE);
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void changePassword(Long userId, ChangePasswordRequest request) {
|
||||
User user = getUserEntity(userId);
|
||||
|
||||
// 验证原密码
|
||||
if (!passwordEncoder.matches(request.getOldPassword(), user.getPassword())) {
|
||||
throw new BusinessException(ErrorCode.USERNAME_OR_PASSWORD_ERROR, "原密码错误");
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
User update = new User();
|
||||
update.setId(userId);
|
||||
update.setPassword(passwordEncoder.encode(request.getNewPassword()));
|
||||
update.setUpdatedAt(System.currentTimeMillis());
|
||||
update.setUpdatedBy(userId);
|
||||
userMapper.updateById(update);
|
||||
|
||||
// 清除用户缓存
|
||||
evictUserCache(userId);
|
||||
|
||||
log.info("用户修改密码成功: userId={}", userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void resetPassword(ResetPasswordRequest request) {
|
||||
// 校验当前用户是否有重置密码权限
|
||||
UserContext.CurrentUser currentUser = UserContext.get();
|
||||
String roles = currentUser != null ? currentUser.getRoles() : null;
|
||||
if (roles == null || (!roles.contains("ROLE_ADMIN") && !roles.contains("ROLE_PROPERTY"))) {
|
||||
throw new BusinessException(ErrorCode.NO_PERMISSION, "无权限重置密码");
|
||||
}
|
||||
|
||||
User user = getUserEntity(request.getUserId());
|
||||
|
||||
// 系统管理员账号不可重置
|
||||
if (user.getAccountType() != null && user.getAccountType() == 2) {
|
||||
throw new BusinessException(ErrorCode.OPERATION_FAILED, "系统管理员账号不可重置密码");
|
||||
}
|
||||
|
||||
// 使用新密码或默认密码
|
||||
String newPassword = request.getNewPassword();
|
||||
if (newPassword == null || newPassword.isBlank()) {
|
||||
newPassword = AuthConstants.DEFAULT_PASSWORD;
|
||||
}
|
||||
|
||||
User update = new User();
|
||||
update.setId(request.getUserId());
|
||||
update.setPassword(passwordEncoder.encode(newPassword));
|
||||
update.setUpdatedAt(System.currentTimeMillis());
|
||||
update.setUpdatedBy(UserContext.getUserId());
|
||||
userMapper.updateById(update);
|
||||
|
||||
// 清除用户缓存
|
||||
evictUserCache(request.getUserId());
|
||||
|
||||
log.info("管理员重置用户密码: userId={}, operatorId={}", request.getUserId(), UserContext.getUserId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserInfoDTO getCurrentUserInfo() {
|
||||
Long userId = UserContext.getUserId();
|
||||
if (userId == null) {
|
||||
throw new BusinessException(com.pms.common.response.ResultCode.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
return buildUserInfo(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkPermission(Long userId, String permissionCode, Long projectId) {
|
||||
List<String> permissions = userMapper.selectPermissionCodesByUserId(userId);
|
||||
if (permissions == null || permissions.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
// 超级管理员拥有全部权限
|
||||
if (permissions.contains("*")) {
|
||||
return true;
|
||||
}
|
||||
return permissions.contains(permissionCode);
|
||||
}
|
||||
|
||||
// ====== 私有方法 ======
|
||||
|
||||
/**
|
||||
* 验证验证码
|
||||
*/
|
||||
private void validateCaptcha(String captchaKey, String captchaCode) {
|
||||
// 调试模式:跳过验证码校验
|
||||
if (captchaDebug) {
|
||||
log.warn("验证码调试模式已开启,跳过校验");
|
||||
return;
|
||||
}
|
||||
if (captchaKey == null || captchaCode == null) {
|
||||
throw new BusinessException(ErrorCode.CAPTCHA_ERROR, "验证码不能为空");
|
||||
}
|
||||
String key = AuthConstants.CAPTCHA_KEY + captchaKey;
|
||||
String cachedCode = stringRedisTemplate.opsForValue().get(key);
|
||||
if (cachedCode == null) {
|
||||
throw new BusinessException(ErrorCode.CAPTCHA_ERROR, "验证码已过期");
|
||||
}
|
||||
if (!cachedCode.equalsIgnoreCase(captchaCode)) {
|
||||
throw new BusinessException(ErrorCode.CAPTCHA_ERROR);
|
||||
}
|
||||
// 验证成功后删除验证码
|
||||
stringRedisTemplate.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查账号是否被锁定
|
||||
* 先检查 Redis 锁定状态,再检查数据库 lockUntil
|
||||
*/
|
||||
private void checkAccountLock(User user) {
|
||||
// 1. 检查 Redis 锁定状态
|
||||
String lockKey = AuthConstants.LOGIN_LOCK_KEY + user.getUsername();
|
||||
if (Boolean.TRUE.equals(stringRedisTemplate.hasKey(lockKey))) {
|
||||
throw new BusinessException(ErrorCode.ACCOUNT_LOCKED, "账号已锁定,请15分钟后重试");
|
||||
}
|
||||
// 2. 检查数据库 lockUntil
|
||||
if (user.getLockUntil() != null && user.getLockUntil() > System.currentTimeMillis()) {
|
||||
throw new BusinessException(ErrorCode.ACCOUNT_LOCKED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递增登录失败次数(Redis INCR 原子操作)
|
||||
*/
|
||||
private void incrementFailCount(User user) {
|
||||
String failKey = AuthConstants.LOGIN_FAIL_KEY + user.getUsername();
|
||||
Long count = stringRedisTemplate.opsForValue().increment(failKey);
|
||||
|
||||
// 首次失败时设置过期时间
|
||||
if (count != null && count == 1) {
|
||||
stringRedisTemplate.expire(failKey, AuthConstants.LOCK_DURATION, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
User update = new User();
|
||||
update.setId(user.getId());
|
||||
update.setFailCount(count != null ? count.intValue() : user.getFailCount() + 1);
|
||||
update.setUpdatedAt(System.currentTimeMillis());
|
||||
|
||||
// 达到最大失败次数时锁定账号
|
||||
if (count != null && count >= AuthConstants.MAX_FAIL_COUNT) {
|
||||
// 设置 Redis 锁定状态
|
||||
String lockKey = AuthConstants.LOGIN_LOCK_KEY + user.getUsername();
|
||||
stringRedisTemplate.opsForValue().set(lockKey, "1", AuthConstants.LOCK_DURATION, TimeUnit.SECONDS);
|
||||
// 更新数据库锁定状态
|
||||
update.setLockUntil(System.currentTimeMillis() + AuthConstants.LOCK_DURATION * 1000);
|
||||
}
|
||||
userMapper.updateById(update);
|
||||
|
||||
log.warn("登录失败: username={}, failCount={}", user.getUsername(), count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置失败次数并记录登录信息
|
||||
* 登录成功后清除 Redis 失败计数和锁定状态
|
||||
*/
|
||||
private void resetFailCount(User user, String clientIp) {
|
||||
// 清除 Redis 失败计数和锁定状态
|
||||
stringRedisTemplate.delete(AuthConstants.LOGIN_FAIL_KEY + user.getUsername());
|
||||
stringRedisTemplate.delete(AuthConstants.LOGIN_LOCK_KEY + user.getUsername());
|
||||
|
||||
User update = new User();
|
||||
update.setId(user.getId());
|
||||
update.setFailCount(0);
|
||||
update.setLockUntil(null);
|
||||
update.setLastLoginAt(System.currentTimeMillis());
|
||||
update.setLastLoginIp(clientIp);
|
||||
update.setUpdatedAt(System.currentTimeMillis());
|
||||
userMapper.updateById(update);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户实体(带 Redis 缓存)
|
||||
* <p>
|
||||
* 缓存策略:
|
||||
* - 命中缓存直接返回
|
||||
* - 未命中查数据库,空值缓存60秒防穿透
|
||||
* - 写入缓存时 TTL 随机偏移防雪崩
|
||||
*/
|
||||
private User getUserEntity(Long userId) {
|
||||
String cacheKey = AuthConstants.USER_INFO_CACHE_KEY + userId;
|
||||
|
||||
// 1. 查缓存
|
||||
Object cached = redisTemplate.opsForValue().get(cacheKey);
|
||||
if (cached != null) {
|
||||
if ("NULL".equals(cached)) {
|
||||
throw new BusinessException(ErrorCode.ACCOUNT_NOT_FOUND);
|
||||
}
|
||||
return (User) cached;
|
||||
}
|
||||
|
||||
// 2. 查数据库
|
||||
User user = userMapper.selectById(userId);
|
||||
if (user == null) {
|
||||
// 防穿透:缓存空值
|
||||
redisTemplate.opsForValue().set(cacheKey, "NULL",
|
||||
AuthConstants.NULL_CACHE_TTL, TimeUnit.SECONDS);
|
||||
throw new BusinessException(ErrorCode.ACCOUNT_NOT_FOUND);
|
||||
}
|
||||
|
||||
// 3. 写缓存(TTL 随机偏移防雪崩)
|
||||
int ttl = AuthConstants.USER_CACHE_TTL + ThreadLocalRandom.current().nextInt(AuthConstants.USER_CACHE_TTL_JITTER);
|
||||
redisTemplate.opsForValue().set(cacheKey, user, ttl, TimeUnit.SECONDS);
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除用户信息缓存
|
||||
*/
|
||||
private void evictUserCache(Long userId) {
|
||||
redisTemplate.delete(AuthConstants.USER_INFO_CACHE_KEY + userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集角色编码
|
||||
*/
|
||||
private List<String> collectRoleCodes(UserDTO userDTO) {
|
||||
if (userDTO.getRoles() == null || userDTO.getRoles().isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return userDTO.getRoles().stream()
|
||||
.map(RoleDTO::getRoleCode)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集权限编码
|
||||
*/
|
||||
private List<String> collectPermissionCodes(Long userId) {
|
||||
List<String> codes = userMapper.selectPermissionCodesByUserId(userId);
|
||||
return codes != null ? codes : Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 AccessToken 的 claims
|
||||
*/
|
||||
private Map<String, Object> buildAccessClaims(User user, List<String> roleCodes,
|
||||
List<String> permissionCodes, Long projectId) {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put("userId", user.getId());
|
||||
claims.put("username", user.getUsername());
|
||||
claims.put("projectId", projectId);
|
||||
claims.put("orgId", user.getOrgId());
|
||||
claims.put("roles", roleCodes);
|
||||
claims.put("permissions", permissionCodes);
|
||||
claims.put("jti", IdUtil.fastSimpleUUID());
|
||||
return claims;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 RefreshToken 的 claims
|
||||
*/
|
||||
private Map<String, Object> buildRefreshClaims(User user) {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put("userId", user.getId());
|
||||
claims.put("username", user.getUsername());
|
||||
claims.put("jti", IdUtil.fastSimpleUUID());
|
||||
return claims;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将令牌加入黑名单
|
||||
*/
|
||||
private void blacklistToken(String token, String keyPrefix) {
|
||||
try {
|
||||
Claims claims = jwtUtils.verify(token);
|
||||
String jti = claims.getId();
|
||||
if (jti == null) {
|
||||
jti = claims.get("jti", String.class);
|
||||
}
|
||||
if (jti != null) {
|
||||
long expiration = claims.getExpiration().getTime();
|
||||
long ttl = expiration - System.currentTimeMillis();
|
||||
if (ttl > 0) {
|
||||
stringRedisTemplate.opsForValue().set(
|
||||
keyPrefix + jti,
|
||||
"1",
|
||||
ttl,
|
||||
TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("令牌加入黑名单失败(可能已过期): {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建用户信息 DTO
|
||||
*/
|
||||
private UserInfoDTO buildUserInfo(Long userId) {
|
||||
UserDTO userDTO = userMapper.selectUserWithRolesById(userId);
|
||||
if (userDTO == null) {
|
||||
throw new BusinessException(ErrorCode.ACCOUNT_NOT_FOUND);
|
||||
}
|
||||
|
||||
UserInfoDTO userInfo = new UserInfoDTO();
|
||||
userInfo.setId(userDTO.getId());
|
||||
userInfo.setUsername(userDTO.getUsername());
|
||||
userInfo.setRealName(userDTO.getRealName());
|
||||
userInfo.setNickname(userDTO.getNickname());
|
||||
userInfo.setGender(userDTO.getGender());
|
||||
userInfo.setPhone(userDTO.getPhone());
|
||||
userInfo.setEmail(userDTO.getEmail());
|
||||
userInfo.setAvatar(userDTO.getAvatar());
|
||||
userInfo.setOrgId(userDTO.getOrgId());
|
||||
userInfo.setOrgName(userDTO.getOrgName());
|
||||
userInfo.setProjectId(UserContext.getProjectId() != null ? UserContext.getProjectId() : userDTO.getProjectId());
|
||||
|
||||
// 角色列表
|
||||
if (userDTO.getRoles() != null) {
|
||||
userInfo.setRoles(userDTO.getRoles());
|
||||
}
|
||||
|
||||
// 权限编码列表
|
||||
List<String> permissionCodes = collectPermissionCodes(userId);
|
||||
userInfo.setPermissions(permissionCodes);
|
||||
|
||||
// 菜单树
|
||||
List<Permission> permissions = permissionMapper.selectPermissionsByUserId(userId);
|
||||
List<PermissionDTO> menuTree = buildPermissionTree(permissions, 1);
|
||||
userInfo.setMenus(menuTree);
|
||||
|
||||
return userInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建权限树(仅菜单类型)
|
||||
*/
|
||||
private List<PermissionDTO> buildPermissionTree(List<Permission> permissions, int permType) {
|
||||
if (permissions == null || permissions.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 过滤指定类型
|
||||
List<Permission> filtered = permissions.stream()
|
||||
.filter(p -> permType == 0 || (p.getPermType() != null && p.getPermType() == permType))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 按 parentId 分组
|
||||
Map<Long, List<PermissionDTO>> parentMap = new LinkedHashMap<>();
|
||||
List<PermissionDTO> roots = new ArrayList<>();
|
||||
|
||||
for (Permission p : filtered) {
|
||||
PermissionDTO dto = toPermissionDTO(p);
|
||||
Long parentId = p.getParentId() != null ? p.getParentId() : 0L;
|
||||
parentMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(dto);
|
||||
}
|
||||
|
||||
// 递归构建子节点
|
||||
for (PermissionDTO root : parentMap.getOrDefault(0L, Collections.emptyList())) {
|
||||
fillChildren(root, parentMap);
|
||||
roots.add(root);
|
||||
}
|
||||
|
||||
return roots;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归填充子节点
|
||||
*/
|
||||
private void fillChildren(PermissionDTO parent, Map<Long, List<PermissionDTO>> parentMap) {
|
||||
List<PermissionDTO> children = parentMap.getOrDefault(parent.getId(), Collections.emptyList());
|
||||
parent.setChildren(children);
|
||||
for (PermissionDTO child : children) {
|
||||
fillChildren(child, parentMap);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission 转 PermissionDTO
|
||||
*/
|
||||
private PermissionDTO toPermissionDTO(Permission p) {
|
||||
PermissionDTO dto = new PermissionDTO();
|
||||
dto.setId(p.getId());
|
||||
dto.setParentId(p.getParentId());
|
||||
dto.setPermCode(p.getPermCode());
|
||||
dto.setPermName(p.getPermName());
|
||||
dto.setPermType(p.getPermType());
|
||||
dto.setPath(p.getPath());
|
||||
dto.setComponent(p.getComponent());
|
||||
dto.setIcon(p.getIcon());
|
||||
dto.setApiMethod(p.getApiMethod());
|
||||
dto.setApiPath(p.getApiPath());
|
||||
dto.setSort(p.getSort());
|
||||
dto.setIsHidden(p.getIsHidden());
|
||||
dto.setStatus(p.getStatus());
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,228 @@
|
|||
package com.pms.auth.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.pms.auth.dto.OrgDTO;
|
||||
import com.pms.auth.dto.OrgSaveRequest;
|
||||
import com.pms.auth.entity.Org;
|
||||
import com.pms.auth.mapper.OrgMapper;
|
||||
import com.pms.auth.service.OrgService;
|
||||
import com.pms.common.constant.CommonConstants;
|
||||
import com.pms.common.exception.BusinessException;
|
||||
import com.pms.common.exception.ErrorCode;
|
||||
import com.pms.common.security.UserContext;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 组织管理服务实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class OrgServiceImpl implements OrgService {
|
||||
|
||||
private final OrgMapper orgMapper;
|
||||
|
||||
@Override
|
||||
public List<OrgDTO> list(Long parentId, Integer status, Long projectId) {
|
||||
Long currentProjectId = projectId != null ? projectId : UserContext.getProjectId();
|
||||
|
||||
LambdaQueryWrapper<Org> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Org::getDeleted, 0);
|
||||
if (status != null) {
|
||||
wrapper.eq(Org::getStatus, status);
|
||||
}
|
||||
if (currentProjectId != null) {
|
||||
wrapper.and(w -> w.eq(Org::getProjectId, currentProjectId).or().isNull(Org::getProjectId));
|
||||
}
|
||||
wrapper.orderByAsc(Org::getSort);
|
||||
|
||||
List<Org> orgs = orgMapper.selectList(wrapper);
|
||||
|
||||
// 构建树
|
||||
List<OrgDTO> tree = buildTree(orgs);
|
||||
|
||||
// 如果指定了 parentId,返回该节点的子树
|
||||
if (parentId != null) {
|
||||
tree = findSubTree(tree, parentId);
|
||||
}
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long create(OrgSaveRequest request) {
|
||||
// 检查组织编码是否已存在
|
||||
LambdaQueryWrapper<Org> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Org::getOrgCode, request.getOrgCode());
|
||||
Long existCount = orgMapper.selectCount(wrapper);
|
||||
if (existCount > 0) {
|
||||
throw new BusinessException(ErrorCode.RESOURCE_EXISTS, "组织编码已存在");
|
||||
}
|
||||
|
||||
// 检查父节点是否存在
|
||||
if (request.getParentId() != null && request.getParentId() != 0) {
|
||||
Org parent = orgMapper.selectById(request.getParentId());
|
||||
if (parent == null) {
|
||||
throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "父节点不存在");
|
||||
}
|
||||
}
|
||||
|
||||
Org org = new Org();
|
||||
org.setParentId(request.getParentId() != null ? request.getParentId() : 0L);
|
||||
org.setOrgCode(request.getOrgCode());
|
||||
org.setOrgName(request.getOrgName());
|
||||
org.setOrgType(request.getOrgType() != null ? request.getOrgType() : 1);
|
||||
org.setLeaderId(request.getLeaderId());
|
||||
org.setPhone(request.getPhone());
|
||||
org.setSort(request.getSort() != null ? request.getSort() : 0);
|
||||
org.setStatus(request.getStatus() != null ? request.getStatus() : CommonConstants.STATUS_ENABLED);
|
||||
org.setProjectId(UserContext.getProjectId());
|
||||
long now = System.currentTimeMillis();
|
||||
org.setCreatedAt(now);
|
||||
org.setUpdatedAt(now);
|
||||
org.setCreatedBy(UserContext.getUserId());
|
||||
org.setUpdatedBy(UserContext.getUserId());
|
||||
|
||||
orgMapper.insert(org);
|
||||
log.info("创建组织成功: orgId={}, orgCode={}", org.getId(), org.getOrgCode());
|
||||
return org.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void update(Long id, OrgSaveRequest request) {
|
||||
Org org = getOrgEntity(id);
|
||||
|
||||
Org update = new Org();
|
||||
update.setId(id);
|
||||
if (StringUtils.hasText(request.getOrgName())) {
|
||||
update.setOrgName(request.getOrgName());
|
||||
}
|
||||
if (request.getOrgType() != null) {
|
||||
update.setOrgType(request.getOrgType());
|
||||
}
|
||||
if (request.getLeaderId() != null) {
|
||||
update.setLeaderId(request.getLeaderId());
|
||||
}
|
||||
update.setPhone(request.getPhone());
|
||||
if (request.getSort() != null) {
|
||||
update.setSort(request.getSort());
|
||||
}
|
||||
if (request.getStatus() != null) {
|
||||
update.setStatus(request.getStatus());
|
||||
}
|
||||
update.setUpdatedAt(System.currentTimeMillis());
|
||||
update.setUpdatedBy(UserContext.getUserId());
|
||||
|
||||
orgMapper.updateById(update);
|
||||
log.info("更新组织成功: orgId={}", id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void delete(Long id) {
|
||||
Org org = getOrgEntity(id);
|
||||
|
||||
// 检查子节点
|
||||
int childCount = orgMapper.countChildren(id);
|
||||
if (childCount > 0) {
|
||||
throw new BusinessException(ErrorCode.OPERATION_FAILED, "存在子节点,禁止删除");
|
||||
}
|
||||
|
||||
// 检查关联用户
|
||||
int userCount = orgMapper.countUsersByOrgId(id);
|
||||
if (userCount > 0) {
|
||||
throw new BusinessException(ErrorCode.OPERATION_FAILED, "存在关联用户,禁止删除");
|
||||
}
|
||||
|
||||
orgMapper.deleteById(id);
|
||||
log.info("删除组织成功: orgId={}", id);
|
||||
}
|
||||
|
||||
// ====== 私有方法 ======
|
||||
|
||||
private Org getOrgEntity(Long id) {
|
||||
Org org = orgMapper.selectById(id);
|
||||
if (org == null) {
|
||||
throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "组织不存在");
|
||||
}
|
||||
return org;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建组织树
|
||||
*/
|
||||
private List<OrgDTO> buildTree(List<Org> orgs) {
|
||||
if (orgs == null || orgs.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Map<Long, List<OrgDTO>> parentMap = new LinkedHashMap<>();
|
||||
for (Org org : orgs) {
|
||||
OrgDTO dto = toDTO(org);
|
||||
Long parentId = org.getParentId() != null ? org.getParentId() : 0L;
|
||||
parentMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(dto);
|
||||
}
|
||||
|
||||
List<OrgDTO> roots = parentMap.getOrDefault(0L, new ArrayList<>());
|
||||
for (OrgDTO root : roots) {
|
||||
fillChildren(root, parentMap);
|
||||
}
|
||||
|
||||
return roots;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归填充子节点
|
||||
*/
|
||||
private void fillChildren(OrgDTO parent, Map<Long, List<OrgDTO>> parentMap) {
|
||||
List<OrgDTO> children = parentMap.getOrDefault(parent.getId(), new ArrayList<>());
|
||||
parent.setChildren(children);
|
||||
for (OrgDTO child : children) {
|
||||
fillChildren(child, parentMap);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在树中查找指定节点的子树
|
||||
*/
|
||||
private List<OrgDTO> findSubTree(List<OrgDTO> tree, Long parentId) {
|
||||
for (OrgDTO node : tree) {
|
||||
if (node.getId().equals(parentId)) {
|
||||
return node.getChildren() != null ? node.getChildren() : Collections.emptyList();
|
||||
}
|
||||
if (node.getChildren() != null) {
|
||||
List<OrgDTO> found = findSubTree(node.getChildren(), parentId);
|
||||
if (!found.isEmpty()) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Org 转 OrgDTO
|
||||
*/
|
||||
private OrgDTO toDTO(Org org) {
|
||||
OrgDTO dto = new OrgDTO();
|
||||
dto.setId(org.getId());
|
||||
dto.setParentId(org.getParentId());
|
||||
dto.setOrgCode(org.getOrgCode());
|
||||
dto.setOrgName(org.getOrgName());
|
||||
dto.setOrgType(org.getOrgType());
|
||||
dto.setLeaderId(org.getLeaderId());
|
||||
dto.setPhone(org.getPhone());
|
||||
dto.setSort(org.getSort());
|
||||
dto.setStatus(org.getStatus());
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
package com.pms.auth.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.pms.auth.dto.PermissionDTO;
|
||||
import com.pms.auth.dto.PermissionTreeResult;
|
||||
import com.pms.auth.entity.Permission;
|
||||
import com.pms.auth.mapper.PermissionMapper;
|
||||
import com.pms.auth.service.PermissionService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 权限管理服务实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PermissionServiceImpl implements PermissionService {
|
||||
|
||||
private final PermissionMapper permissionMapper;
|
||||
|
||||
@Override
|
||||
public PermissionTreeResult getPermissionTree(Long roleId, Integer permType) {
|
||||
// 查询全部权限
|
||||
LambdaQueryWrapper<Permission> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Permission::getStatus, 1);
|
||||
wrapper.orderByAsc(Permission::getSort);
|
||||
List<Permission> allPermissions = permissionMapper.selectList(wrapper);
|
||||
|
||||
// 类型过滤
|
||||
if (permType != null) {
|
||||
allPermissions = allPermissions.stream()
|
||||
.filter(p -> permType.equals(p.getPermType()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// 构建树
|
||||
List<PermissionDTO> tree = buildTree(allPermissions);
|
||||
|
||||
PermissionTreeResult result = new PermissionTreeResult();
|
||||
result.setTree(tree);
|
||||
|
||||
// 如果传了 roleId,返回已勾选的权限ID列表
|
||||
if (roleId != null) {
|
||||
List<Long> checkedKeys = permissionMapper.selectPermissionIdsByRoleId(roleId);
|
||||
result.setCheckedKeys(checkedKeys);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建权限树
|
||||
*/
|
||||
private List<PermissionDTO> buildTree(List<Permission> permissions) {
|
||||
if (permissions == null || permissions.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 按 parentId 分组
|
||||
Map<Long, List<PermissionDTO>> parentMap = new LinkedHashMap<>();
|
||||
for (Permission p : permissions) {
|
||||
PermissionDTO dto = toDTO(p);
|
||||
Long parentId = p.getParentId() != null ? p.getParentId() : 0L;
|
||||
parentMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(dto);
|
||||
}
|
||||
|
||||
// 构建根节点列表
|
||||
List<PermissionDTO> roots = parentMap.getOrDefault(0L, new ArrayList<>());
|
||||
for (PermissionDTO root : roots) {
|
||||
fillChildren(root, parentMap);
|
||||
}
|
||||
|
||||
return roots;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归填充子节点
|
||||
*/
|
||||
private void fillChildren(PermissionDTO parent, Map<Long, List<PermissionDTO>> parentMap) {
|
||||
List<PermissionDTO> children = parentMap.getOrDefault(parent.getId(), new ArrayList<>());
|
||||
parent.setChildren(children);
|
||||
for (PermissionDTO child : children) {
|
||||
fillChildren(child, parentMap);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission 转 PermissionDTO
|
||||
*/
|
||||
private PermissionDTO toDTO(Permission p) {
|
||||
PermissionDTO dto = new PermissionDTO();
|
||||
dto.setId(p.getId());
|
||||
dto.setParentId(p.getParentId());
|
||||
dto.setPermCode(p.getPermCode());
|
||||
dto.setPermName(p.getPermName());
|
||||
dto.setPermType(p.getPermType());
|
||||
dto.setPath(p.getPath());
|
||||
dto.setComponent(p.getComponent());
|
||||
dto.setIcon(p.getIcon());
|
||||
dto.setApiMethod(p.getApiMethod());
|
||||
dto.setApiPath(p.getApiPath());
|
||||
dto.setSort(p.getSort());
|
||||
dto.setIsHidden(p.getIsHidden());
|
||||
dto.setStatus(p.getStatus());
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,214 @@
|
|||
package com.pms.auth.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.pms.auth.dto.*;
|
||||
import com.pms.auth.entity.Role;
|
||||
import com.pms.auth.entity.RolePermission;
|
||||
import com.pms.auth.mapper.PermissionMapper;
|
||||
import com.pms.auth.mapper.RoleMapper;
|
||||
import com.pms.auth.mapper.RolePermissionMapper;
|
||||
import com.pms.auth.mapper.UserRoleMapper;
|
||||
import com.pms.auth.service.RoleService;
|
||||
import com.pms.common.constant.CommonConstants;
|
||||
import com.pms.common.exception.BusinessException;
|
||||
import com.pms.common.exception.ErrorCode;
|
||||
import com.pms.common.security.UserContext;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 角色管理服务实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RoleServiceImpl implements RoleService {
|
||||
|
||||
private final RoleMapper roleMapper;
|
||||
private final RolePermissionMapper rolePermissionMapper;
|
||||
private final PermissionMapper permissionMapper;
|
||||
private final UserRoleMapper userRoleMapper;
|
||||
|
||||
@Override
|
||||
public List<RoleDTO> list(String keyword, Integer status, Long projectId, boolean all) {
|
||||
Long currentProjectId = projectId != null ? projectId : UserContext.getProjectId();
|
||||
List<RoleDTO> roles = roleMapper.selectRoleListWithUserCount(currentProjectId);
|
||||
|
||||
// 关键字过滤
|
||||
if (StringUtils.hasText(keyword)) {
|
||||
roles = roles.stream()
|
||||
.filter(r -> (r.getRoleCode() != null && r.getRoleCode().contains(keyword))
|
||||
|| (r.getRoleName() != null && r.getRoleName().contains(keyword)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// 状态过滤
|
||||
if (status != null) {
|
||||
roles = roles.stream()
|
||||
.filter(r -> status.equals(r.getStatus()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RoleDTO getById(Long id) {
|
||||
Role role = roleMapper.selectById(id);
|
||||
if (role == null) {
|
||||
throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "角色不存在");
|
||||
}
|
||||
|
||||
RoleDTO dto = new RoleDTO();
|
||||
dto.setId(role.getId());
|
||||
dto.setRoleCode(role.getRoleCode());
|
||||
dto.setRoleName(role.getRoleName());
|
||||
dto.setDataScope(role.getDataScope());
|
||||
dto.setSort(role.getSort());
|
||||
dto.setStatus(role.getStatus());
|
||||
dto.setRemark(role.getRemark());
|
||||
dto.setProjectId(role.getProjectId());
|
||||
dto.setCreatedAt(role.getCreatedAt());
|
||||
dto.setUpdatedAt(role.getUpdatedAt());
|
||||
|
||||
// 查询权限ID列表
|
||||
List<Long> permissionIds = permissionMapper.selectPermissionIdsByRoleId(id);
|
||||
dto.setPermissionIds(permissionIds);
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long create(RoleSaveRequest request) {
|
||||
// 检查角色编码是否已存在
|
||||
LambdaQueryWrapper<Role> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Role::getRoleCode, request.getRoleCode());
|
||||
Long existCount = roleMapper.selectCount(wrapper);
|
||||
if (existCount > 0) {
|
||||
throw new BusinessException(ErrorCode.RESOURCE_EXISTS, "角色编码已存在");
|
||||
}
|
||||
|
||||
Role role = new Role();
|
||||
role.setRoleCode(request.getRoleCode());
|
||||
role.setRoleName(request.getRoleName());
|
||||
role.setDataScope(request.getDataScope() != null ? request.getDataScope() : 1);
|
||||
role.setProjectId(request.getProjectId() != null ? request.getProjectId() : UserContext.getProjectId());
|
||||
role.setSort(request.getSort() != null ? request.getSort() : 0);
|
||||
role.setStatus(request.getStatus() != null ? request.getStatus() : CommonConstants.STATUS_ENABLED);
|
||||
role.setRemark(request.getRemark());
|
||||
long now = System.currentTimeMillis();
|
||||
role.setCreatedAt(now);
|
||||
role.setUpdatedAt(now);
|
||||
role.setCreatedBy(UserContext.getUserId());
|
||||
role.setUpdatedBy(UserContext.getUserId());
|
||||
|
||||
roleMapper.insert(role);
|
||||
|
||||
// 分配权限
|
||||
if (request.getPermissionIds() != null && !request.getPermissionIds().isEmpty()) {
|
||||
assignRolePermissions(role.getId(), request.getPermissionIds());
|
||||
}
|
||||
|
||||
log.info("创建角色成功: roleId={}, roleCode={}", role.getId(), role.getRoleCode());
|
||||
return role.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void update(Long id, RoleSaveRequest request) {
|
||||
Role role = getRoleEntity(id);
|
||||
|
||||
Role update = new Role();
|
||||
update.setId(id);
|
||||
if (StringUtils.hasText(request.getRoleName())) {
|
||||
update.setRoleName(request.getRoleName());
|
||||
}
|
||||
if (request.getDataScope() != null) {
|
||||
update.setDataScope(request.getDataScope());
|
||||
}
|
||||
if (request.getSort() != null) {
|
||||
update.setSort(request.getSort());
|
||||
}
|
||||
if (request.getStatus() != null) {
|
||||
update.setStatus(request.getStatus());
|
||||
}
|
||||
update.setRemark(request.getRemark());
|
||||
update.setUpdatedAt(System.currentTimeMillis());
|
||||
update.setUpdatedBy(UserContext.getUserId());
|
||||
|
||||
roleMapper.updateById(update);
|
||||
log.info("更新角色成功: roleId={}", id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void delete(Long id) {
|
||||
Role role = getRoleEntity(id);
|
||||
|
||||
// 检查是否存在关联用户
|
||||
int userCount = roleMapper.countUsersByRoleId(id);
|
||||
if (userCount > 0) {
|
||||
throw new BusinessException(ErrorCode.OPERATION_FAILED, "角色下存在关联用户,禁止删除");
|
||||
}
|
||||
|
||||
// 逻辑删除角色
|
||||
roleMapper.deleteById(id);
|
||||
|
||||
// 删除权限关联
|
||||
rolePermissionMapper.deleteByRoleId(id);
|
||||
|
||||
log.info("删除角色成功: roleId={}", id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void assignPermissions(Long roleId, AssignPermissionsRequest request) {
|
||||
// 检查角色存在
|
||||
getRoleEntity(roleId);
|
||||
|
||||
// 先删除旧的权限关联
|
||||
rolePermissionMapper.deleteByRoleId(roleId);
|
||||
|
||||
// 再分配新权限
|
||||
if (request.getPermissionIds() != null && !request.getPermissionIds().isEmpty()) {
|
||||
assignRolePermissions(roleId, request.getPermissionIds());
|
||||
}
|
||||
|
||||
log.info("分配权限成功: roleId={}, permissionIds={}", roleId, request.getPermissionIds());
|
||||
}
|
||||
|
||||
// ====== 私有方法 ======
|
||||
|
||||
private Role getRoleEntity(Long id) {
|
||||
Role role = roleMapper.selectById(id);
|
||||
if (role == null) {
|
||||
throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "角色不存在");
|
||||
}
|
||||
return role;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分配角色权限
|
||||
*/
|
||||
private void assignRolePermissions(Long roleId, List<Long> permissionIds) {
|
||||
long now = System.currentTimeMillis();
|
||||
for (Long permissionId : permissionIds) {
|
||||
RolePermission rp = new RolePermission();
|
||||
rp.setRoleId(roleId);
|
||||
rp.setPermissionId(permissionId);
|
||||
rp.setProjectId(UserContext.getProjectId());
|
||||
rp.setCreatedAt(now);
|
||||
rp.setUpdatedAt(now);
|
||||
rp.setCreatedBy(UserContext.getUserId());
|
||||
rp.setUpdatedBy(UserContext.getUserId());
|
||||
rolePermissionMapper.insert(rp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,285 @@
|
|||
package com.pms.auth.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.pms.auth.dto.*;
|
||||
import com.pms.auth.entity.ProjectUser;
|
||||
import com.pms.auth.entity.User;
|
||||
import com.pms.auth.entity.UserRole;
|
||||
import com.pms.auth.mapper.*;
|
||||
import com.pms.auth.service.UserService;
|
||||
import com.pms.common.constant.CommonConstants;
|
||||
import com.pms.common.exception.BusinessException;
|
||||
import com.pms.common.exception.ErrorCode;
|
||||
import com.pms.common.response.PageResult;
|
||||
import com.pms.common.security.UserContext;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户管理服务实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class UserServiceImpl implements UserService {
|
||||
|
||||
private final UserMapper userMapper;
|
||||
private final RoleMapper roleMapper;
|
||||
private final UserRoleMapper userRoleMapper;
|
||||
private final ProjectUserMapper projectUserMapper;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
@Override
|
||||
public PageResult<UserDTO> page(UserQueryRequest request) {
|
||||
int page = request.getPage() != null && request.getPage() > 0 ? request.getPage() : CommonConstants.DEFAULT_PAGE;
|
||||
int size = request.getSize() != null && request.getSize() > 0 ? request.getSize() : CommonConstants.DEFAULT_SIZE;
|
||||
size = Math.min(size, CommonConstants.MAX_SIZE);
|
||||
|
||||
Page<UserDTO> pageObj = new Page<>(page, size);
|
||||
IPage<UserDTO> result = userMapper.selectUserPage(
|
||||
pageObj,
|
||||
request.getKeyword(),
|
||||
request.getOrgId(),
|
||||
request.getStatus(),
|
||||
request.getRoleId(),
|
||||
UserContext.getProjectId());
|
||||
|
||||
// 为每个用户填充角色信息
|
||||
for (UserDTO dto : result.getRecords()) {
|
||||
fillUserRoles(dto);
|
||||
}
|
||||
|
||||
return new PageResult<>(result.getRecords(), result.getTotal(), page, size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserDTO getById(Long id) {
|
||||
UserDTO userDTO = userMapper.selectUserWithRolesById(id);
|
||||
if (userDTO == null) {
|
||||
throw new BusinessException(ErrorCode.ACCOUNT_NOT_FOUND);
|
||||
}
|
||||
// 清除密码
|
||||
userDTO.setPassword(null);
|
||||
|
||||
// 填充项目列表
|
||||
fillUserProjects(userDTO);
|
||||
|
||||
return userDTO;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long create(UserSaveRequest request) {
|
||||
// 检查用户名是否已存在
|
||||
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(User::getUsername, request.getUsername());
|
||||
Long existCount = userMapper.selectCount(wrapper);
|
||||
if (existCount > 0) {
|
||||
throw new BusinessException(ErrorCode.RESOURCE_EXISTS, "用户名已存在");
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
User user = new User();
|
||||
user.setUsername(request.getUsername());
|
||||
user.setPassword(passwordEncoder.encode(request.getPassword()));
|
||||
user.setRealName(request.getRealName());
|
||||
user.setNickname(request.getNickname());
|
||||
user.setGender(request.getGender() != null ? request.getGender() : 0);
|
||||
user.setPhone(request.getPhone());
|
||||
user.setEmail(request.getEmail());
|
||||
user.setOrgId(request.getOrgId());
|
||||
user.setStatus(CommonConstants.STATUS_ENABLED);
|
||||
user.setAccountType(1);
|
||||
user.setFailCount(0);
|
||||
user.setRemark(request.getRemark());
|
||||
user.setProjectId(UserContext.getProjectId());
|
||||
long now = System.currentTimeMillis();
|
||||
user.setCreatedAt(now);
|
||||
user.setUpdatedAt(now);
|
||||
user.setCreatedBy(UserContext.getUserId());
|
||||
user.setUpdatedBy(UserContext.getUserId());
|
||||
|
||||
userMapper.insert(user);
|
||||
|
||||
// 分配角色
|
||||
if (request.getRoleIds() != null && !request.getRoleIds().isEmpty()) {
|
||||
assignUserRoles(user.getId(), request.getRoleIds());
|
||||
}
|
||||
|
||||
// 分配项目
|
||||
if (request.getProjectIds() != null && !request.getProjectIds().isEmpty()) {
|
||||
assignUserProjects(user.getId(), request.getProjectIds());
|
||||
}
|
||||
|
||||
log.info("创建用户成功: userId={}, username={}", user.getId(), user.getUsername());
|
||||
return user.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void update(Long id, UserUpdateRequest request) {
|
||||
User user = getUserEntity(id);
|
||||
|
||||
User update = new User();
|
||||
update.setId(id);
|
||||
if (StringUtils.hasText(request.getRealName())) {
|
||||
update.setRealName(request.getRealName());
|
||||
}
|
||||
update.setNickname(request.getNickname());
|
||||
update.setGender(request.getGender());
|
||||
update.setPhone(request.getPhone());
|
||||
update.setEmail(request.getEmail());
|
||||
if (request.getOrgId() != null) {
|
||||
update.setOrgId(request.getOrgId());
|
||||
}
|
||||
if (request.getStatus() != null) {
|
||||
update.setStatus(request.getStatus());
|
||||
}
|
||||
update.setRemark(request.getRemark());
|
||||
update.setUpdatedAt(System.currentTimeMillis());
|
||||
update.setUpdatedBy(UserContext.getUserId());
|
||||
|
||||
userMapper.updateById(update);
|
||||
log.info("更新用户成功: userId={}", id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void delete(Long id) {
|
||||
User user = getUserEntity(id);
|
||||
|
||||
// 系统管理员账号不可删除
|
||||
if (user.getAccountType() != null && user.getAccountType() == 2) {
|
||||
throw new BusinessException(ErrorCode.OPERATION_FAILED, "系统管理员账号不可删除");
|
||||
}
|
||||
|
||||
// 逻辑删除用户
|
||||
userMapper.deleteById(id);
|
||||
|
||||
// 删除角色关联
|
||||
userRoleMapper.deleteByUserId(id);
|
||||
|
||||
// 删除项目关联
|
||||
projectUserMapper.deleteByUserId(id);
|
||||
|
||||
log.info("删除用户成功: userId={}", id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateStatus(Long id, Integer status) {
|
||||
User user = getUserEntity(id);
|
||||
|
||||
User update = new User();
|
||||
update.setId(id);
|
||||
update.setStatus(status);
|
||||
update.setUpdatedAt(System.currentTimeMillis());
|
||||
update.setUpdatedBy(UserContext.getUserId());
|
||||
userMapper.updateById(update);
|
||||
log.info("更新用户状态: userId={}, status={}", id, status);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void assignRoles(Long userId, AssignRolesRequest request) {
|
||||
// 检查用户存在
|
||||
getUserEntity(userId);
|
||||
|
||||
// 先删除旧的角色关联
|
||||
userRoleMapper.deleteByUserId(userId);
|
||||
|
||||
// 再分配新角色
|
||||
if (request.getRoleIds() != null && !request.getRoleIds().isEmpty()) {
|
||||
assignUserRoles(userId, request.getRoleIds());
|
||||
}
|
||||
log.info("分配角色成功: userId={}, roleIds={}", userId, request.getRoleIds());
|
||||
}
|
||||
|
||||
// ====== 私有方法 ======
|
||||
|
||||
private User getUserEntity(Long id) {
|
||||
User user = userMapper.selectById(id);
|
||||
if (user == null) {
|
||||
throw new BusinessException(ErrorCode.ACCOUNT_NOT_FOUND);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分配用户角色
|
||||
*/
|
||||
private void assignUserRoles(Long userId, List<Long> roleIds) {
|
||||
long now = System.currentTimeMillis();
|
||||
for (Long roleId : roleIds) {
|
||||
UserRole ur = new UserRole();
|
||||
ur.setUserId(userId);
|
||||
ur.setRoleId(roleId);
|
||||
ur.setProjectId(UserContext.getProjectId());
|
||||
ur.setCreatedAt(now);
|
||||
ur.setUpdatedAt(now);
|
||||
ur.setCreatedBy(UserContext.getUserId());
|
||||
ur.setUpdatedBy(UserContext.getUserId());
|
||||
userRoleMapper.insert(ur);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分配用户项目
|
||||
*/
|
||||
private void assignUserProjects(Long userId, List<Long> projectIds) {
|
||||
long now = System.currentTimeMillis();
|
||||
for (int i = 0; i < projectIds.size(); i++) {
|
||||
ProjectUser pu = new ProjectUser();
|
||||
pu.setProjectId(projectIds.get(i));
|
||||
pu.setUserId(userId);
|
||||
pu.setIsDefault(i == 0 ? 1 : 0);
|
||||
pu.setStatus(CommonConstants.STATUS_ENABLED);
|
||||
pu.setCreatedAt(now);
|
||||
pu.setUpdatedAt(now);
|
||||
pu.setCreatedBy(UserContext.getUserId());
|
||||
pu.setUpdatedBy(UserContext.getUserId());
|
||||
projectUserMapper.insert(pu);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充用户角色
|
||||
*/
|
||||
private void fillUserRoles(UserDTO dto) {
|
||||
List<com.pms.auth.entity.Role> roles = roleMapper.selectRolesByUserId(dto.getId());
|
||||
List<RoleDTO> roleDTOs = new ArrayList<>();
|
||||
for (com.pms.auth.entity.Role role : roles) {
|
||||
RoleDTO r = new RoleDTO();
|
||||
r.setId(role.getId());
|
||||
r.setRoleCode(role.getRoleCode());
|
||||
r.setRoleName(role.getRoleName());
|
||||
r.setDataScope(role.getDataScope());
|
||||
r.setStatus(role.getStatus());
|
||||
roleDTOs.add(r);
|
||||
}
|
||||
dto.setRoles(roleDTOs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充用户项目列表
|
||||
*/
|
||||
private void fillUserProjects(UserDTO dto) {
|
||||
List<ProjectUser> projectUsers = projectUserMapper.selectByUserId(dto.getId());
|
||||
List<ProjectInfoDTO> projects = new ArrayList<>();
|
||||
for (ProjectUser pu : projectUsers) {
|
||||
ProjectInfoDTO p = new ProjectInfoDTO();
|
||||
p.setId(pu.getProjectId());
|
||||
p.setIsDefault(pu.getIsDefault());
|
||||
projects.add(p);
|
||||
}
|
||||
dto.setProjects(projects);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
server:
|
||||
port: 8081
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: auth-service
|
||||
profiles:
|
||||
active: dev
|
||||
# 数据源
|
||||
datasource:
|
||||
url: jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/auth_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
username: ${MYSQL_USERNAME:root}
|
||||
password: ${MYSQL_PASSWORD:root}
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
hikari:
|
||||
maximum-pool-size: 20
|
||||
minimum-idle: 5
|
||||
connection-timeout: 30000
|
||||
idle-timeout: 600000
|
||||
# Flyway
|
||||
flyway:
|
||||
enabled: true
|
||||
locations: classpath:db/migration
|
||||
baseline-on-migrate: true
|
||||
baseline-version: 0
|
||||
url: jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/auth_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
user: ${MYSQL_USERNAME:root}
|
||||
password: ${MYSQL_PASSWORD:root}
|
||||
# Redis(与 gateway 共用 database=0,确保 JWT 黑名单可跨服务读取)
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:}
|
||||
database: 0
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 16
|
||||
max-idle: 8
|
||||
min-idle: 2
|
||||
# RabbitMQ
|
||||
rabbitmq:
|
||||
host: ${RABBITMQ_HOST:localhost}
|
||||
port: ${RABBITMQ_PORT:5672}
|
||||
username: ${RABBITMQ_USERNAME:guest}
|
||||
password: ${RABBITMQ_PASSWORD:guest}
|
||||
virtual-host: /
|
||||
publisher-confirm-type: correlated
|
||||
publisher-returns: true
|
||||
listener:
|
||||
simple:
|
||||
acknowledge-mode: manual
|
||||
prefetch: 10
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: ${NACOS_ADDR:localhost:8848}
|
||||
namespace: ${NACOS_NAMESPACE:public}
|
||||
config:
|
||||
server-addr: ${NACOS_ADDR:localhost:8848}
|
||||
namespace: ${NACOS_NAMESPACE:public}
|
||||
file-extension: yaml
|
||||
shared-configs:
|
||||
- data-id: pms-common.yaml
|
||||
refresh: true
|
||||
openfeign:
|
||||
client:
|
||||
config:
|
||||
default:
|
||||
connect-timeout: 3000
|
||||
read-timeout: 5000
|
||||
|
||||
# MyBatis-Plus
|
||||
mybatis-plus:
|
||||
mapper-locations: classpath*:mapper/**/*.xml
|
||||
type-aliases-package: com.pms.auth.**.entity
|
||||
global-config:
|
||||
db-config:
|
||||
id-type: ASSIGN_ID
|
||||
logic-delete-field: deleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
table-prefix: t_
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
cache-enabled: false
|
||||
|
||||
# 验证码调试模式(E2E测试时设为true,生产环境必须为false)
|
||||
captcha:
|
||||
debug: false
|
||||
|
||||
# JWT 配置(认证服务持有私钥用于签发)
|
||||
jwt:
|
||||
algorithm: RS256
|
||||
private-key: ${JWT_PRIVATE_KEY:}
|
||||
public-key: ${JWT_PUBLIC_KEY:}
|
||||
access-token-expire: 7200
|
||||
refresh-token-expire: 604800
|
||||
issuer: ether-pms
|
||||
audience: pms-client
|
||||
|
||||
# 日志
|
||||
logging:
|
||||
level:
|
||||
com.pms: debug
|
||||
|
||||
# 端点暴露
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
|
|
@ -0,0 +1,250 @@
|
|||
-- ========================================
|
||||
-- auth_db 数据库初始化脚本(7张表 + 种子数据)
|
||||
-- 认证服务:用户、角色、权限、组织及关联关系
|
||||
-- 技术约定:
|
||||
-- 主键 BIGINT UNSIGNED(雪花算法)
|
||||
-- 时间戳 BIGINT(毫秒级时间戳)
|
||||
-- 唯一约束包含 project_id(多租户安全)
|
||||
-- ========================================
|
||||
|
||||
-- ====================
|
||||
-- 1. 用户表
|
||||
-- ====================
|
||||
CREATE TABLE IF NOT EXISTS `t_user` (
|
||||
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键,雪花算法',
|
||||
`project_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '所属项目ID',
|
||||
`username` VARCHAR(64) NOT NULL COMMENT '登录账号,唯一',
|
||||
`password` VARCHAR(128) NOT NULL COMMENT 'BCrypt加密密码',
|
||||
`real_name` VARCHAR(64) NOT NULL COMMENT '真实姓名',
|
||||
`nickname` VARCHAR(64) DEFAULT NULL COMMENT '昵称',
|
||||
`gender` TINYINT DEFAULT 0 COMMENT '性别:0未知 1男 2女',
|
||||
`phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号',
|
||||
`email` VARCHAR(128) DEFAULT NULL COMMENT '邮箱',
|
||||
`avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像URL',
|
||||
`org_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '所属组织ID',
|
||||
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0禁用 1启用',
|
||||
`account_type` TINYINT NOT NULL DEFAULT 1 COMMENT '账号类型:1普通 2系统管理员',
|
||||
`last_login_at` BIGINT DEFAULT NULL COMMENT '最后登录时间(时间戳)',
|
||||
`last_login_ip` VARCHAR(64) DEFAULT NULL COMMENT '最后登录IP',
|
||||
`fail_count` INT NOT NULL DEFAULT 0 COMMENT '连续登录失败次数',
|
||||
`lock_until` BIGINT DEFAULT NULL COMMENT '锁定截止时间(时间戳)',
|
||||
`remark` VARCHAR(255) DEFAULT NULL COMMENT '备注',
|
||||
`created_at` BIGINT NOT NULL COMMENT '创建时间(时间戳)',
|
||||
`updated_at` BIGINT NOT NULL COMMENT '更新时间(时间戳)',
|
||||
`created_by` BIGINT UNSIGNED DEFAULT NULL COMMENT '创建人ID',
|
||||
`updated_by` BIGINT UNSIGNED DEFAULT NULL COMMENT '更新人ID',
|
||||
`deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除:0未删 1已删',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_username` (`username`, `deleted`) COMMENT '登录账号唯一索引',
|
||||
KEY `idx_project_id` (`project_id`),
|
||||
KEY `idx_phone` (`phone`),
|
||||
KEY `idx_org_id` (`org_id`),
|
||||
KEY `idx_status` (`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';
|
||||
|
||||
-- ====================
|
||||
-- 2. 角色表
|
||||
-- ====================
|
||||
CREATE TABLE IF NOT EXISTS `t_role` (
|
||||
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键,雪花算法',
|
||||
`project_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '所属项目ID,NULL表示全局角色',
|
||||
`role_code` VARCHAR(64) NOT NULL COMMENT '角色编码,唯一',
|
||||
`role_name` VARCHAR(64) NOT NULL COMMENT '角色名称',
|
||||
`data_scope` TINYINT NOT NULL DEFAULT 1 COMMENT '数据范围:1全部 2本组织 3本组织及下级 4本人 5自定义',
|
||||
`sort` INT NOT NULL DEFAULT 0 COMMENT '排序号',
|
||||
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0禁用 1启用',
|
||||
`remark` VARCHAR(255) DEFAULT NULL COMMENT '备注',
|
||||
`created_at` BIGINT NOT NULL COMMENT '创建时间(时间戳)',
|
||||
`updated_at` BIGINT NOT NULL COMMENT '更新时间(时间戳)',
|
||||
`created_by` BIGINT UNSIGNED DEFAULT NULL COMMENT '创建人ID',
|
||||
`updated_by` BIGINT UNSIGNED DEFAULT NULL COMMENT '更新人ID',
|
||||
`deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_role_code` (`project_id`, `role_code`, `deleted`) COMMENT '角色编码唯一索引(含项目ID)',
|
||||
KEY `idx_project_id` (`project_id`),
|
||||
KEY `idx_status` (`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色表';
|
||||
|
||||
-- ====================
|
||||
-- 3. 权限表
|
||||
-- ====================
|
||||
CREATE TABLE IF NOT EXISTS `t_permission` (
|
||||
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键,雪花算法',
|
||||
`parent_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '父权限ID,0表示根节点',
|
||||
`perm_code` VARCHAR(128) NOT NULL COMMENT '权限编码,唯一',
|
||||
`perm_name` VARCHAR(64) NOT NULL COMMENT '权限名称',
|
||||
`perm_type` TINYINT NOT NULL DEFAULT 1 COMMENT '类型:1菜单 2按钮 3接口',
|
||||
`path` VARCHAR(255) DEFAULT NULL COMMENT '前端路由路径',
|
||||
`component` VARCHAR(255) DEFAULT NULL COMMENT '前端组件路径',
|
||||
`icon` VARCHAR(64) DEFAULT NULL COMMENT '菜单图标',
|
||||
`api_method` VARCHAR(10) DEFAULT NULL COMMENT '接口方法:GET/POST/PUT/DELETE',
|
||||
`api_path` VARCHAR(255) DEFAULT NULL COMMENT '接口路径',
|
||||
`sort` INT NOT NULL DEFAULT 0 COMMENT '排序号',
|
||||
`is_hidden` TINYINT NOT NULL DEFAULT 0 COMMENT '是否隐藏:0否 1是',
|
||||
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0禁用 1启用',
|
||||
`created_at` BIGINT NOT NULL COMMENT '创建时间(时间戳)',
|
||||
`updated_at` BIGINT NOT NULL COMMENT '更新时间(时间戳)',
|
||||
`created_by` BIGINT UNSIGNED DEFAULT NULL COMMENT '创建人ID',
|
||||
`updated_by` BIGINT UNSIGNED DEFAULT NULL COMMENT '更新人ID',
|
||||
`deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_perm_code` (`perm_code`, `deleted`) COMMENT '权限编码唯一索引',
|
||||
KEY `idx_parent_id` (`parent_id`),
|
||||
KEY `idx_perm_type` (`perm_type`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='权限表';
|
||||
|
||||
-- ====================
|
||||
-- 4. 用户角色关联表
|
||||
-- ====================
|
||||
CREATE TABLE IF NOT EXISTS `t_user_role` (
|
||||
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键,雪花算法',
|
||||
`project_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '项目ID,NULL表示全局授权',
|
||||
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
|
||||
`role_id` BIGINT UNSIGNED NOT NULL COMMENT '角色ID',
|
||||
`created_at` BIGINT NOT NULL COMMENT '创建时间(时间戳)',
|
||||
`updated_at` BIGINT NOT NULL COMMENT '更新时间(时间戳)',
|
||||
`created_by` BIGINT UNSIGNED DEFAULT NULL COMMENT '创建人ID',
|
||||
`updated_by` BIGINT UNSIGNED DEFAULT NULL COMMENT '更新人ID',
|
||||
`deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_user_role` (`project_id`, `user_id`, `role_id`, `deleted`) COMMENT '防重复分配',
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_role_id` (`role_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户角色关联表';
|
||||
|
||||
-- ====================
|
||||
-- 5. 角色权限关联表
|
||||
-- ====================
|
||||
CREATE TABLE IF NOT EXISTS `t_role_permission` (
|
||||
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键,雪花算法',
|
||||
`project_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '项目ID',
|
||||
`role_id` BIGINT UNSIGNED NOT NULL COMMENT '角色ID',
|
||||
`permission_id` BIGINT UNSIGNED NOT NULL COMMENT '权限ID',
|
||||
`created_at` BIGINT NOT NULL COMMENT '创建时间(时间戳)',
|
||||
`updated_at` BIGINT NOT NULL COMMENT '更新时间(时间戳)',
|
||||
`created_by` BIGINT UNSIGNED DEFAULT NULL COMMENT '创建人ID',
|
||||
`updated_by` BIGINT UNSIGNED DEFAULT NULL COMMENT '更新人ID',
|
||||
`deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_role_perm` (`project_id`, `role_id`, `permission_id`, `deleted`) COMMENT '防重复分配',
|
||||
KEY `idx_role_id` (`role_id`),
|
||||
KEY `idx_permission_id` (`permission_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色权限关联表';
|
||||
|
||||
-- ====================
|
||||
-- 6. 组织架构表
|
||||
-- ====================
|
||||
CREATE TABLE IF NOT EXISTS `t_org` (
|
||||
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键,雪花算法',
|
||||
`project_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '所属项目ID',
|
||||
`parent_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '父组织ID,0表示根节点',
|
||||
`org_code` VARCHAR(64) NOT NULL COMMENT '组织编码,唯一',
|
||||
`org_name` VARCHAR(128) NOT NULL COMMENT '组织名称',
|
||||
`org_type` TINYINT NOT NULL DEFAULT 1 COMMENT '类型:1公司 2部门 3小组 4项目部',
|
||||
`leader_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '负责人用户ID',
|
||||
`phone` VARCHAR(20) DEFAULT NULL COMMENT '联系电话',
|
||||
`sort` INT NOT NULL DEFAULT 0 COMMENT '排序号',
|
||||
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0禁用 1启用',
|
||||
`created_at` BIGINT NOT NULL COMMENT '创建时间(时间戳)',
|
||||
`updated_at` BIGINT NOT NULL COMMENT '更新时间(时间戳)',
|
||||
`created_by` BIGINT UNSIGNED DEFAULT NULL COMMENT '创建人ID',
|
||||
`updated_by` BIGINT UNSIGNED DEFAULT NULL COMMENT '更新人ID',
|
||||
`deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_org_code` (`project_id`, `org_code`, `deleted`) COMMENT '组织编码唯一索引(含项目ID)',
|
||||
KEY `idx_parent_id` (`parent_id`),
|
||||
KEY `idx_leader_id` (`leader_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='组织架构表';
|
||||
|
||||
-- ====================
|
||||
-- 7. 项目用户关联表
|
||||
-- ====================
|
||||
CREATE TABLE IF NOT EXISTS `t_project_user` (
|
||||
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键,雪花算法',
|
||||
`project_id` BIGINT UNSIGNED NOT NULL COMMENT '物业项目ID',
|
||||
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
|
||||
`org_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '组织ID',
|
||||
`is_default` TINYINT NOT NULL DEFAULT 0 COMMENT '是否默认项目:0否 1是',
|
||||
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0禁用 1启用',
|
||||
`created_at` BIGINT NOT NULL COMMENT '创建时间(时间戳)',
|
||||
`updated_at` BIGINT NOT NULL COMMENT '更新时间(时间戳)',
|
||||
`created_by` BIGINT UNSIGNED DEFAULT NULL COMMENT '创建人ID',
|
||||
`updated_by` BIGINT UNSIGNED DEFAULT NULL COMMENT '更新人ID',
|
||||
`deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_project_user` (`project_id`, `user_id`, `deleted`) COMMENT '一个用户在一个项目仅一条记录',
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_is_default` (`user_id`, `is_default`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='项目用户关联表';
|
||||
|
||||
-- ========================================
|
||||
-- 种子数据
|
||||
-- ========================================
|
||||
|
||||
-- 默认管理员用户(密码: Admin@123456 的 BCrypt 哈希)
|
||||
INSERT INTO `t_user` (`id`, `project_id`, `username`, `password`, `real_name`, `nickname`, `gender`, `phone`, `email`, `avatar`, `org_id`, `status`, `account_type`, `remark`, `created_at`, `updated_at`, `created_by`, `updated_by`, `deleted`)
|
||||
VALUES (1, NULL, 'admin', '$2a$10$82EB3SvbuxsvKRAtu1g3O.NpnU75LMcDppvBy26mJN7zH7X34Qq3.', '系统管理员', '管理员', 1, '13800000001', 'admin@etherpms.com', NULL, 1, 1, 2, '系统内置管理员账号', UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0);
|
||||
|
||||
-- 默认角色
|
||||
INSERT INTO `t_role` (`id`, `project_id`, `role_code`, `role_name`, `data_scope`, `sort`, `status`, `remark`, `created_at`, `updated_at`, `created_by`, `updated_by`, `deleted`) VALUES
|
||||
(1, NULL, 'ROLE_ADMIN', '系统管理员', 1, 1, 1, '拥有全部权限', UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(2, NULL, 'ROLE_PROPERTY', '物业管理员', 2, 2, 1, '物业管理操作权限', UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(3, NULL, 'ROLE_FINANCE', '财务专员', 4, 3, 1, '收费与对账权限', UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(4, NULL, 'ROLE_VIEWER', '只读用户', 4, 4, 1, '仅查看权限', UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0);
|
||||
|
||||
-- 权限数据(菜单+按钮+接口)
|
||||
-- 一级菜单:系统管理
|
||||
INSERT INTO `t_permission` (`id`, `parent_id`, `perm_code`, `perm_name`, `perm_type`, `path`, `component`, `icon`, `api_method`, `api_path`, `sort`, `is_hidden`, `status`, `created_at`, `updated_at`, `deleted`) VALUES
|
||||
(101, 0, 'system', '系统管理', 1, '/system', 'Layout', 'setting', NULL, NULL, 1, 0, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(102, 101, 'system:user', '用户管理', 1, '/system/user', 'system/user/index', 'user', NULL, NULL, 1, 0, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(103, 102, 'system:user:list', '用户列表', 2, NULL, NULL, NULL, 'GET', '/api/v1/users', 1, 0, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(104, 102, 'system:user:add', '新增用户', 2, NULL, NULL, NULL, 'POST', '/api/v1/users', 2, 0, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(105, 102, 'system:user:edit', '编辑用户', 2, NULL, NULL, NULL, 'PUT', '/api/v1/users', 3, 0, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(106, 102, 'system:user:delete', '删除用户', 2, NULL, NULL, NULL, 'DELETE', '/api/v1/users', 4, 0, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(107, 102, 'system:user:assign', '分配角色', 2, NULL, NULL, NULL, 'POST', '/api/v1/users/*/roles', 5, 0, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(108, 102, 'system:user:reset', '重置密码', 2, NULL, NULL, NULL, 'POST', '/api/v1/auth/reset-password', 6, 0, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(111, 101, 'system:role', '角色管理', 1, '/system/role', 'system/role/index', 'peoples', NULL, NULL, 2, 0, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(112, 111, 'system:role:list', '角色列表', 2, NULL, NULL, NULL, 'GET', '/api/v1/roles', 1, 0, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(113, 111, 'system:role:add', '新增角色', 2, NULL, NULL, NULL, 'POST', '/api/v1/roles', 2, 0, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(114, 111, 'system:role:edit', '编辑角色', 2, NULL, NULL, NULL, 'PUT', '/api/v1/roles', 3, 0, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(115, 111, 'system:role:delete', '删除角色', 2, NULL, NULL, NULL, 'DELETE', '/api/v1/roles', 4, 0, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(116, 111, 'system:role:assign', '分配权限', 2, NULL, NULL, NULL, 'POST', '/api/v1/roles/*/permissions', 5, 0, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(121, 101, 'system:permission', '权限管理', 1, '/system/permission', 'system/permission/index', 'tree-table', NULL, NULL, 3, 0, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(122, 121, 'system:permission:list', '权限树查询', 2, NULL, NULL, NULL, 'GET', '/api/v1/permissions', 1, 0, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(131, 101, 'system:org', '组织管理', 1, '/system/org', 'system/org/index', 'organization', NULL, NULL, 4, 0, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(132, 131, 'system:org:list', '组织树', 2, NULL, NULL, NULL, 'GET', '/api/v1/orgs', 1, 0, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(133, 131, 'system:org:add', '新增组织', 2, NULL, NULL, NULL, 'POST', '/api/v1/orgs', 2, 0, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(134, 131, 'system:org:edit', '编辑组织', 2, NULL, NULL, NULL, 'PUT', '/api/v1/orgs', 3, 0, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(135, 131, 'system:org:delete', '删除组织', 2, NULL, NULL, NULL, 'DELETE', '/api/v1/orgs', 4, 0, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0);
|
||||
|
||||
-- 管理员角色关联全部权限
|
||||
INSERT INTO `t_role_permission` (`id`, `project_id`, `role_id`, `permission_id`, `created_at`, `updated_at`, `created_by`, `updated_by`, `deleted`) VALUES
|
||||
(1, NULL, 1, 101, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(2, NULL, 1, 102, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(3, NULL, 1, 103, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(4, NULL, 1, 104, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(5, NULL, 1, 105, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(6, NULL, 1, 106, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(7, NULL, 1, 107, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(8, NULL, 1, 108, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(9, NULL, 1, 111, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(10, NULL, 1, 112, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(11, NULL, 1, 113, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(12, NULL, 1, 114, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(13, NULL, 1, 115, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(14, NULL, 1, 116, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(15, NULL, 1, 121, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(16, NULL, 1, 122, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(17, NULL, 1, 131, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(18, NULL, 1, 132, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(19, NULL, 1, 133, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(20, NULL, 1, 134, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(21, NULL, 1, 135, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0);
|
||||
|
||||
-- 管理员用户关联管理员角色
|
||||
INSERT INTO `t_user_role` (`id`, `project_id`, `user_id`, `role_id`, `created_at`, `updated_at`, `created_by`, `updated_by`, `deleted`) VALUES
|
||||
(1, NULL, 1, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0);
|
||||
|
||||
-- 默认组织
|
||||
INSERT INTO `t_org` (`id`, `project_id`, `parent_id`, `org_code`, `org_name`, `org_type`, `leader_id`, `phone`, `sort`, `status`, `created_at`, `updated_at`, `created_by`, `updated_by`, `deleted`) VALUES
|
||||
(1, NULL, 0, 'HQ', '物业公司总部', 1, 1, '021-88888888', 1, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0);
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.pms.auth.mapper.OrgMapper">
|
||||
|
||||
<!-- 查询全部组织 -->
|
||||
<select id="selectAllOrgs" resultType="com.pms.auth.entity.Org">
|
||||
SELECT id, parent_id, org_code, org_name, org_type, leader_id, phone, sort, status,
|
||||
project_id, created_at, updated_at, created_by, updated_by, deleted
|
||||
FROM t_org
|
||||
WHERE deleted = 0
|
||||
<if test="projectId != null">
|
||||
AND (project_id = #{projectId} OR project_id IS NULL)
|
||||
</if>
|
||||
ORDER BY sort ASC, created_at ASC
|
||||
</select>
|
||||
|
||||
<!-- 统计子节点数量 -->
|
||||
<select id="countChildren" resultType="int">
|
||||
SELECT COUNT(1) FROM t_org WHERE parent_id = #{parentId} AND deleted = 0
|
||||
</select>
|
||||
|
||||
<!-- 统计组织下的用户数 -->
|
||||
<select id="countUsersByOrgId" resultType="int">
|
||||
SELECT COUNT(1) FROM t_user WHERE org_id = #{orgId} AND deleted = 0
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.pms.auth.mapper.PermissionMapper">
|
||||
|
||||
<!-- 根据用户ID查询权限列表 -->
|
||||
<select id="selectPermissionsByUserId" resultType="com.pms.auth.entity.Permission">
|
||||
SELECT DISTINCT p.id, p.parent_id, p.perm_code, p.perm_name, p.perm_type,
|
||||
p.path, p.component, p.icon, p.api_method, p.api_path,
|
||||
p.sort, p.is_hidden, p.status,
|
||||
p.created_at, p.updated_at, p.created_by, p.updated_by, p.deleted
|
||||
FROM t_user_role ur
|
||||
INNER JOIN t_role_permission rp ON ur.role_id = rp.role_id AND rp.deleted = 0
|
||||
INNER JOIN t_permission p ON rp.permission_id = p.id AND p.deleted = 0
|
||||
WHERE ur.user_id = #{userId} AND ur.deleted = 0 AND p.status = 1
|
||||
ORDER BY p.sort ASC
|
||||
</select>
|
||||
|
||||
<!-- 根据角色ID查询权限列表 -->
|
||||
<select id="selectPermissionsByRoleId" resultType="com.pms.auth.entity.Permission">
|
||||
SELECT DISTINCT p.id, p.parent_id, p.perm_code, p.perm_name, p.perm_type,
|
||||
p.path, p.component, p.icon, p.api_method, p.api_path,
|
||||
p.sort, p.is_hidden, p.status,
|
||||
p.created_at, p.updated_at, p.created_by, p.updated_by, p.deleted
|
||||
FROM t_role_permission rp
|
||||
INNER JOIN t_permission p ON rp.permission_id = p.id AND p.deleted = 0
|
||||
WHERE rp.role_id = #{roleId} AND rp.deleted = 0
|
||||
ORDER BY p.sort ASC
|
||||
</select>
|
||||
|
||||
<!-- 根据角色ID查询权限编码列表 -->
|
||||
<select id="selectPermissionCodesByRoleId" resultType="java.lang.String">
|
||||
SELECT DISTINCT p.perm_code
|
||||
FROM t_role_permission rp
|
||||
INNER JOIN t_permission p ON rp.permission_id = p.id AND p.deleted = 0
|
||||
WHERE rp.role_id = #{roleId} AND rp.deleted = 0
|
||||
</select>
|
||||
|
||||
<!-- 根据角色ID查询权限ID列表 -->
|
||||
<select id="selectPermissionIdsByRoleId" resultType="java.lang.Long">
|
||||
SELECT DISTINCT rp.permission_id
|
||||
FROM t_role_permission rp
|
||||
WHERE rp.role_id = #{roleId} AND rp.deleted = 0
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.pms.auth.mapper.ProjectUserMapper">
|
||||
|
||||
<!-- 根据用户ID查询关联项目列表 -->
|
||||
<select id="selectByUserId" resultType="com.pms.auth.entity.ProjectUser">
|
||||
SELECT id, project_id, user_id, org_id, is_default, status,
|
||||
created_at, updated_at, created_by, updated_by, deleted
|
||||
FROM t_project_user
|
||||
WHERE user_id = #{userId} AND deleted = 0
|
||||
</select>
|
||||
|
||||
<!-- 根据用户ID删除所有项目关联 -->
|
||||
<delete id="deleteByUserId">
|
||||
DELETE FROM t_project_user WHERE user_id = #{userId}
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.pms.auth.mapper.RoleMapper">
|
||||
|
||||
<!-- 查询角色列表(含关联用户数) -->
|
||||
<select id="selectRoleListWithUserCount" resultType="com.pms.auth.dto.RoleDTO">
|
||||
SELECT r.id, r.role_code, r.role_name, r.data_scope, r.sort, r.status, r.remark,
|
||||
r.project_id, r.created_at, r.updated_at,
|
||||
(SELECT COUNT(1) FROM t_user_role ur WHERE ur.role_id = r.id AND ur.deleted = 0) AS userCount
|
||||
FROM t_role r
|
||||
WHERE r.deleted = 0
|
||||
<if test="projectId != null">
|
||||
AND (r.project_id = #{projectId} OR r.project_id IS NULL)
|
||||
</if>
|
||||
ORDER BY r.sort ASC, r.created_at ASC
|
||||
</select>
|
||||
|
||||
<!-- 根据用户ID查询角色列表 -->
|
||||
<select id="selectRolesByUserId" resultType="com.pms.auth.entity.Role">
|
||||
SELECT r.id, r.role_code, r.role_name, r.data_scope, r.sort, r.status, r.remark,
|
||||
r.project_id, r.created_at, r.updated_at, r.created_by, r.updated_by, r.deleted
|
||||
FROM t_user_role ur
|
||||
INNER JOIN t_role r ON ur.role_id = r.id AND r.deleted = 0
|
||||
WHERE ur.user_id = #{userId} AND ur.deleted = 0
|
||||
</select>
|
||||
|
||||
<!-- 根据用户ID查询角色编码列表 -->
|
||||
<select id="selectRoleCodesByUserId" resultType="java.lang.String">
|
||||
SELECT DISTINCT r.role_code
|
||||
FROM t_user_role ur
|
||||
INNER JOIN t_role r ON ur.role_id = r.id AND r.deleted = 0
|
||||
WHERE ur.user_id = #{userId} AND ur.deleted = 0
|
||||
</select>
|
||||
|
||||
<!-- 统计角色关联的用户数 -->
|
||||
<select id="countUsersByRoleId" resultType="int">
|
||||
SELECT COUNT(1) FROM t_user_role WHERE role_id = #{roleId} AND deleted = 0
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue