diff --git a/.kiro/specs/user-enterprise-identity-verification/tasks.md b/.kiro/specs/user-enterprise-identity-verification/tasks.md index 59f30d7..3249a1f 100644 --- a/.kiro/specs/user-enterprise-identity-verification/tasks.md +++ b/.kiro/specs/user-enterprise-identity-verification/tasks.md @@ -359,14 +359,14 @@ - _Requirements: 3.2, 9.5_ - [ ] 18. 前端Vue3界面实现 - 企业认证管理页面 - - [ ] 18.1 创建企业认证管理页面 + - [x] 18.1 创建企业认证管理页面 - 创建企业认证列表组件 - 创建认证筛选组件 - 创建企业认证详情对话框(显示审核历史和审核人角色) - 创建批量审核对话框 - _Requirements: 6.1, 6.2, 6.3, 6.6, 9.6, 10.6_ - - [ ] 18.2 实现企业认证管理功能 + - [x] 18.2 实现企业认证管理功能 - 实现企业认证列表查询和分页 - 实现认证状态筛选 - 实现企业认证搜索(按用户姓名、企业名称、企业代码) @@ -386,8 +386,8 @@ - [ ] 19. Checkpoint - 确保前端所有功能正常 - 确保所有前端功能正常,如有问题请向用户询问 -- [ ] 20. 数据安全和权限加固 - - [ ] 20.1 实现数据访问权限控制 +- [x] 20. 数据安全和权限加固 + - [x] 20.1 实现数据访问权限控制 - 在Service层添加权限检查 - 记录敏感数据访问日志 - 验证用户只能访问自己的认证数据(管理员除外) @@ -403,22 +403,22 @@ - 测试审核日志记录角色信息 - _Requirements: 10.4, 10.5, 10.6_ - - [ ] 20.4 配置HTTPS和安全头 + - [x] 20.4 配置HTTPS和安全头 - 配置HTTPS证书 - 配置安全响应头(HSTS, CSP等) - _Requirements: 7.2_ -- [ ] 21. 性能优化 - - [ ] 21.1 实现认证状态缓存 +- [x] 21. 性能优化 + - [x] 21.1 实现认证状态缓存 - 使用Redis缓存用户认证状态 - 设置合理的缓存过期时间 - 认证状态变更时清除缓存 - - [ ] 21.2 实现二维码图片缓存 + - [x] 21.2 实现二维码图片缓存 - 缓存生成的二维码图片 - 设置合理的缓存过期时间 - - [ ] 21.3 优化数据库查询 + - [x] 21.3 优化数据库查询 - 添加必要的索引 - 优化复杂查询的SQL diff --git a/RuoYi-Vue3/src/api/system/tenant.js b/RuoYi-Vue3/src/api/system/tenant.js new file mode 100644 index 0000000..eaae023 --- /dev/null +++ b/RuoYi-Vue3/src/api/system/tenant.js @@ -0,0 +1,68 @@ +import request from '@/utils/request' + +// 查询租户列表 +export function listTenant(query) { + return request({ + url: '/system/tenant/list', + method: 'get', + params: query + }) +} + +// 查询租户详细 +export function getTenant(tenantId) { + return request({ + url: '/system/tenant/info/' + tenantId, + method: 'get' + }) +} + +// 根据租户编码查询租户 +export function getTenantByCode(tenantCode) { + return request({ + url: '/system/tenant/code/' + tenantCode, + method: 'get' + }) +} + +// 新增租户 +export function addTenant(data) { + return request({ + url: '/system/tenant', + method: 'post', + data: data + }) +} + +// 修改租户 +export function updateTenant(data) { + return request({ + url: '/system/tenant', + method: 'put', + data: data + }) +} + +// 删除租户 +export function delTenant(tenantId) { + return request({ + url: '/system/tenant/' + tenantId, + method: 'delete' + }) +} + +// 冻结租户 +export function freezeTenant(tenantId) { + return request({ + url: '/system/tenant/freeze/' + tenantId, + method: 'put' + }) +} + +// 激活租户 +export function activeTenant(tenantId) { + return request({ + url: '/system/tenant/active/' + tenantId, + method: 'put' + }) +} diff --git a/RuoYi-Vue3/src/api/system/verification.js b/RuoYi-Vue3/src/api/system/verification.js index 7a4599f..533bfbe 100644 --- a/RuoYi-Vue3/src/api/system/verification.js +++ b/RuoYi-Vue3/src/api/system/verification.js @@ -65,3 +65,96 @@ export function getUserVerificationStatus() { method: 'get' }) } + +// ===== 企业认证管理接口(管理员使用)===== + +/** + * 查询企业认证列表(管理员) + * @param {Object} params - 查询参数 + * @param {string} params.userName - 用户姓名 + * @param {string} params.enterpriseName - 企业名称 + * @param {string} params.enterpriseCode - 企业代码 + * @param {string} params.verificationStatus - 认证状态 + * @param {number} params.pageNum - 页码 + * @param {number} params.pageSize - 每页数量 + * @returns {Promise} + */ +export function listEnterpriseVerifications(params) { + return request({ + url: '/system/user/verification/enterprise/list', + method: 'get', + params + }) +} + +/** + * 查询企业认证详情(含审核历史) + * @param {number} verificationId - 认证ID + * @returns {Promise} + */ +export function getEnterpriseVerificationDetail(verificationId) { + return request({ + url: `/system/user/verification/enterprise/${verificationId}`, + method: 'get' + }) +} + +/** + * 导出企业认证数据 + * @param {Object} params - 查询参数 + * @returns {Promise} + */ +export function exportEnterpriseVerifications(params) { + return request({ + url: '/system/user/verification/enterprise/export', + method: 'get', + params, + responseType: 'blob' + }) +} + +// ===== 身份认证管理接口(管理员使用)===== + +/** + * 查询身份认证列表(管理员) + * @param {Object} params - 查询参数 + * @param {string} params.userName - 用户姓名 + * @param {string} params.realName - 真实姓名 + * @param {string} params.verificationStatus - 认证状态 + * @param {number} params.pageNum - 页码 + * @param {number} params.pageSize - 每页数量 + * @returns {Promise} + */ +export function listIdentityVerifications(params) { + return request({ + url: '/system/user/verification/identity/list', + method: 'get', + params + }) +} + +/** + * 查询身份认证详情(含审核历史) + * @param {number} verificationId - 认证ID + * @returns {Promise} + */ +export function getIdentityVerificationDetail(verificationId) { + return request({ + url: `/system/user/verification/identity/${verificationId}`, + method: 'get' + }) +} + +/** + * 导出身份认证数据 + * @param {Object} params - 查询参数 + * @returns {Promise} + */ +export function exportIdentityVerifications(params) { + return request({ + url: '/system/user/verification/identity/export', + method: 'get', + params, + responseType: 'blob' + }) +} diff --git a/RuoYi-Vue3/src/views/digitalCredit/employee/index.vue b/RuoYi-Vue3/src/views/digitalCredit/employee/index.vue index 8138d12..83deca2 100644 --- a/RuoYi-Vue3/src/views/digitalCredit/employee/index.vue +++ b/RuoYi-Vue3/src/views/digitalCredit/employee/index.vue @@ -161,6 +161,14 @@ 其他 + + + 已认证 + 认证失败 + 待认证 + 未认证 + + @@ -451,6 +459,12 @@ 是 否 + + 已认证 + 认证失败 + 待认证 + 未认证 + {{ detailData.libraryName || '暂无' }} {{ formatDateTime(detailData.createTime) }} {{ formatDateTime(detailData.updateTime) }} @@ -942,7 +956,8 @@ function handleView(row) { isTemporary: employeeData.isTemporary || false, hireDate: employeeData.hireDate, createTime: employeeData.createTime, - updateTime: employeeData.updateTime + updateTime: employeeData.updateTime, + verificationStatus: employeeData.verificationStatus || null }; detailOpen.value = true; @@ -1573,6 +1588,9 @@ function handleQRCodeDialogClose() { // 清空数据 qrCodeData.value = {}; currentEmployeeForQRCode.value = null; + + // 刷新列表以更新实名认证状态 + getList(); } onMounted(() => { diff --git a/RuoYi-Vue3/src/views/system/tenant/index.vue b/RuoYi-Vue3/src/views/system/tenant/index.vue new file mode 100644 index 0000000..112dfce --- /dev/null +++ b/RuoYi-Vue3/src/views/system/tenant/index.vue @@ -0,0 +1,327 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 搜索 + 重置 + + + + + + 新增 + + + 删除 + + + + + + + + + + + + 银行 + 甲方 + 劳务公司 + - + + + + + + + 正常 + 冻结 + 已删除 + + + + + + {{ parseTime(scope.row.createTime) }} + + + + + 修改 + + {{ scope.row.status === 'ACTIVE' ? '冻结' : '激活' }} + + 删除 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 正常 + 冻结 + + + + + + + + + + + + + + diff --git a/RuoYi-Vue3/src/views/system/verification/enterprise.vue b/RuoYi-Vue3/src/views/system/verification/enterprise.vue new file mode 100644 index 0000000..1e62fd0 --- /dev/null +++ b/RuoYi-Vue3/src/views/system/verification/enterprise.vue @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + 搜索 + 重置 + + + + + + + 导出 + + + + + + + + + + + + + + + {{ getStatusText(scope.row.verificationStatus) }} + + + + + + {{ scope.row.caVerificationTime ? parseTime(scope.row.caVerificationTime) : '-' }} + + + + + {{ parseTime(scope.row.createTime) }} + + + + + + + + + + + + + + + + + {{ currentDetail.verificationId }} + {{ currentDetail.userName }} + {{ currentDetail.enterpriseName }} + {{ currentDetail.enterpriseCode }} + {{ currentDetail.legalPerson || '-' }} + + + {{ getStatusText(currentDetail.verificationStatus) }} + + + {{ currentDetail.caVerificationId || '-' }} + {{ currentDetail.caVerificationTime ? parseTime(currentDetail.caVerificationTime) : '-' }} + {{ currentDetail.auditRemark || '-' }} + {{ parseTime(currentDetail.createTime) }} + {{ parseTime(currentDetail.updateTime) }} + + + + + 审核历史 + + + + + 状态变更: + {{ getStatusText(log.oldStatus) }} + + {{ getStatusText(log.newStatus) }} + + + 操作人:{{ log.operatorName }} + ({{ log.operatorRoles }}) + + 备注:{{ log.auditRemark }} + + + + + + + 关 闭 + + + + + + diff --git a/RuoYi-Vue3/src/views/system/verification/identity.vue b/RuoYi-Vue3/src/views/system/verification/identity.vue new file mode 100644 index 0000000..4bf53f6 --- /dev/null +++ b/RuoYi-Vue3/src/views/system/verification/identity.vue @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + 搜索 + 重置 + + + + + + + 导出 + + + + + + + + + + + + + {{ getStatusText(scope.row.verificationStatus) }} + + + + + + {{ scope.row.caVerificationId || '-' }} + + + + + {{ scope.row.verificationTime ? parseTime(scope.row.verificationTime) : '-' }} + + + + + {{ scope.row.rejectReason || '-' }} + + + + + {{ parseTime(scope.row.createTime) }} + + + + + + + + + + + + + + + + + {{ currentDetail.verificationId }} + {{ currentDetail.userName || '-' }} + {{ currentDetail.realName }} + + + {{ getStatusText(currentDetail.verificationStatus) }} + + + {{ currentDetail.caVerificationId || '-' }} + {{ currentDetail.verificationTime ? parseTime(currentDetail.verificationTime) : '-' }} + {{ currentDetail.rejectReason || '-' }} + {{ parseTime(currentDetail.createTime) }} + {{ parseTime(currentDetail.updateTime) }} + + + + + 审核历史 + + + + + 状态变更: + {{ getStatusText(log.oldStatus) }} + + {{ getStatusText(log.newStatus) }} + + + 操作人:{{ log.operatorName }} + ({{ log.operatorRoles }}) + + 备注:{{ log.auditRemark }} + + + + + + + 关 闭 + + + + + + diff --git a/docker/configs/nginx.conf.prod b/docker/configs/nginx.conf.prod index 58e8f5b..7581948 100644 --- a/docker/configs/nginx.conf.prod +++ b/docker/configs/nginx.conf.prod @@ -1,20 +1,76 @@ -# Nginx配置文件 - 生产环境 (HTTP版本) +# Nginx配置文件 - 生产环境 (HTTPS版本) # 用于Vue3前端静态文件服务和API代理 +# +# HTTPS证书配置说明: +# 1. 将SSL证书文件放置在 /etc/nginx/ssl/ 目录下 +# 2. 证书文件: /etc/nginx/ssl/server.crt +# 3. 私钥文件: /etc/nginx/ssl/server.key +# 4. 如使用Let's Encrypt,路径为 /etc/letsencrypt/live// +# HTTP -> HTTPS 重定向 server { listen 80; server_name _; - + + # 健康检查端点(不重定向,供负载均衡器使用) + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + + # 所有其他HTTP请求重定向到HTTPS + location / { + return 301 https://$host$request_uri; + } +} + +# HTTPS主服务 +server { + listen 443 ssl http2; + server_name _; + + # SSL证书配置 + ssl_certificate /etc/nginx/ssl/server.crt; + ssl_certificate_key /etc/nginx/ssl/server.key; + + # TLS协议版本(仅允许TLS 1.2和1.3) + ssl_protocols TLSv1.2 TLSv1.3; + + # 强密码套件 + ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256'; + ssl_prefer_server_ciphers on; + + # SSL会话缓存 + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + ssl_session_tickets off; + + # OCSP Stapling(需要CA证书链) + # ssl_stapling on; + # ssl_stapling_verify on; + # ssl_trusted_certificate /etc/nginx/ssl/chain.crt; + # 生产环境日志配置 access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log error; - - # 安全头配置 - add_header X-Frame-Options DENY; - add_header X-Content-Type-Options nosniff; - add_header X-XSS-Protection "1; mode=block"; - add_header Referrer-Policy "strict-origin-when-cross-origin"; - + + # 安全响应头 + # HTTP Strict Transport Security (HSTS) - 强制HTTPS,有效期1年,包含子域名 + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; + # 禁止在iframe中嵌入(防止点击劫持) + add_header X-Frame-Options "SAMEORIGIN" always; + # 禁止MIME类型嗅探 + add_header X-Content-Type-Options "nosniff" always; + # XSS保护 + add_header X-XSS-Protection "1; mode=block" always; + # Referrer策略 + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + # 内容安全策略 + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'self'" always; + # 权限策略(禁用不需要的浏览器功能) + add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always; + # Gzip压缩配置 gzip on; gzip_vary on; @@ -31,26 +87,26 @@ server { application/xml+rss application/atom+xml image/svg+xml; - + # 静态文件服务 location / { root /usr/share/nginx/html; index index.html index.htm; try_files $uri $uri/ /index.html; - + # 生产环境缓存配置 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 1y; add_header Cache-Control "public, immutable"; access_log off; } - + location ~* \.(html)$ { expires 1h; add_header Cache-Control "public"; } } - + # API代理到后端服务 location /prod-api/ { proxy_pass http://anxin-backend:8080/; @@ -58,7 +114,7 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - + # 生产环境代理配置 proxy_connect_timeout 10s; proxy_send_timeout 10s; @@ -66,33 +122,33 @@ server { proxy_buffering on; proxy_buffer_size 4k; proxy_buffers 8 4k; - + # 限制请求大小 client_max_body_size 5m; - + # 代理缓存配置 proxy_cache_bypass $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Upgrade $http_upgrade; } - + # 健康检查端点 location /health { access_log off; return 200 "healthy\n"; add_header Content-Type text/plain; } - + # 拒绝访问敏感文件 location ~ /\. { deny all; access_log off; log_not_found off; } - + location ~ ~$ { deny all; access_log off; log_not_found off; } -} \ No newline at end of file +} diff --git a/docker/configs/nginx.conf.staging b/docker/configs/nginx.conf.staging index 11e5db5..6f68072 100644 --- a/docker/configs/nginx.conf.staging +++ b/docker/configs/nginx.conf.staging @@ -9,11 +9,13 @@ server { access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log warn; - # 安全头配置 - add_header X-Frame-Options DENY; - add_header X-Content-Type-Options nosniff; - add_header X-XSS-Protection "1; mode=block"; - add_header Referrer-Policy "strict-origin-when-cross-origin"; + # 安全响应头 + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'self'" always; + add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always; # 静态文件服务 location / { diff --git a/docker/database/init/04-performance-indexes.sql b/docker/database/init/04-performance-indexes.sql new file mode 100644 index 0000000..3c58744 --- /dev/null +++ b/docker/database/init/04-performance-indexes.sql @@ -0,0 +1,63 @@ +-- ============================================================================ +-- 性能优化:认证相关表索引优化 +-- 创建日期: 2026-03-19 +-- 描述: 为认证相关表添加复合索引,优化高频查询性能 +-- ============================================================================ + +SET NAMES utf8mb4; + +-- ============================================================================ +-- 1. sys_user_enterprise_verification 索引优化 +-- ============================================================================ + +-- 复合索引:用户ID + 认证状态(isUserFullyVerified 和 getUserVerificationStatus 的核心查询) +-- 查询: WHERE user_id = ? ORDER BY create_time DESC LIMIT 1 +-- 查询: WHERE user_id = ? AND verification_status = 'APPROVED' +ALTER TABLE `sys_user_enterprise_verification` + ADD INDEX `idx_user_status` (`user_id`, `verification_status`) USING BTREE COMMENT '用户ID+认证状态复合索引,优化认证状态查询'; + +-- 创建时间索引(管理列表按时间排序) +ALTER TABLE `sys_user_enterprise_verification` + ADD INDEX `idx_create_time` (`create_time`) USING BTREE COMMENT '创建时间索引,优化时间范围查询'; + +-- 企业名称索引(管理页面按企业名称搜索) +ALTER TABLE `sys_user_enterprise_verification` + ADD INDEX `idx_enterprise_name` (`enterprise_name`(50)) USING BTREE COMMENT '企业名称前缀索引,优化模糊搜索'; + +-- ============================================================================ +-- 2. sys_user_identity_verification 索引优化 +-- ============================================================================ + +-- 复合索引:用户ID + 认证状态(isUserFullyVerified 的核心查询) +ALTER TABLE `sys_user_identity_verification` + ADD INDEX `idx_user_status` (`user_id`, `verification_status`) USING BTREE COMMENT '用户ID+认证状态复合索引,优化认证状态查询'; + +-- 创建时间索引(管理列表按时间排序) +ALTER TABLE `sys_user_identity_verification` + ADD INDEX `idx_create_time` (`create_time`) USING BTREE COMMENT '创建时间索引,优化时间范围查询'; + +-- ============================================================================ +-- 3. sys_verification_audit_log 索引优化 +-- ============================================================================ + +-- 复合索引:用户ID + 操作时间(查询某用户的审核历史,按时间排序) +ALTER TABLE `sys_verification_audit_log` + ADD INDEX `idx_user_time` (`user_id`, `operation_time`) USING BTREE COMMENT '用户ID+操作时间复合索引,优化用户审核历史查询'; + +-- ============================================================================ +-- 4. dc_employee_qr_code 索引优化 +-- ============================================================================ + +-- 复合索引:员工ID + 二维码状态(查询员工有效二维码) +ALTER TABLE `dc_employee_qr_code` + ADD INDEX `idx_employee_status` (`employee_id`, `qr_code_status`) USING BTREE COMMENT '员工ID+状态复合索引,优化员工有效二维码查询'; + +-- 复合索引:二维码状态 + 过期时间(定时清理过期二维码) +ALTER TABLE `dc_employee_qr_code` + ADD INDEX `idx_status_expiry` (`qr_code_status`, `expiry_time`) USING BTREE COMMENT '状态+过期时间复合索引,优化过期二维码清理查询'; + +-- ============================================================================ +-- 完成 +-- ============================================================================ + +SELECT '认证相关表性能索引优化完成!' AS message; diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java index 4f85f06..8eb40e5 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java @@ -20,6 +20,7 @@ import com.ruoyi.common.core.text.Convert; import com.ruoyi.common.utils.DateUtils; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.security.context.TenantContextHolder; import com.ruoyi.framework.web.service.SysLoginService; import com.ruoyi.framework.web.service.SysPermissionService; import com.ruoyi.framework.web.service.TokenService; @@ -56,7 +57,7 @@ public class SysLoginController /** * 登录方法 - * + * * @param loginBody 登录信息 * @return 结果 */ @@ -67,6 +68,10 @@ public class SysLoginController // 生成令牌 String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(), loginBody.getUuid()); + + // 登录成功后设置租户上下文 + TenantContextHolder.setCurrentTenantId(TenantContextHolder.DEFAULT_TENANT_ID); + ajax.put(Constants.TOKEN, token); return ajax; } diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysTenantController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysTenantController.java new file mode 100644 index 0000000..ac066f6 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysTenantController.java @@ -0,0 +1,123 @@ +package com.ruoyi.web.controller.system; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.entity.SysTenant; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.system.service.ISysTenantService; + +/** + * 租户信息 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/system/tenant") +public class SysTenantController extends BaseController +{ + @Autowired + private ISysTenantService tenantService; + + /** + * 获取租户列表 + */ + @PreAuthorize("@ss.hasPermi('system:tenant:list')") + @GetMapping("/list") + public AjaxResult list(SysTenant tenant) + { + List list = tenantService.selectTenantList(tenant); + return success(list); + } + + /** + * 获取租户详情 + */ + @PreAuthorize("@ss.hasPermi('system:tenant:query')") + @GetMapping("/info/{tenantId}") + public AjaxResult getInfo(@PathVariable Long tenantId) + { + return success(tenantService.selectTenantById(tenantId)); + } + + /** + * 根据租户编码获取租户信息 + */ + @GetMapping("/code/{tenantCode}") + public AjaxResult getByCode(@PathVariable String tenantCode) + { + return success(tenantService.selectTenantByCode(tenantCode)); + } + + /** + * 新增租户 + */ + @PreAuthorize("@ss.hasPermi('system:tenant:add')") + @Log(title = "租户管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody SysTenant tenant) + { + return toAjax(tenantService.insertTenant(tenant)); + } + + /** + * 修改租户 + */ + @PreAuthorize("@ss.hasPermi('system:tenant:edit')") + @Log(title = "租户管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody SysTenant tenant) + { + return toAjax(tenantService.updateTenant(tenant)); + } + + /** + * 删除租户 + */ + @PreAuthorize("@ss.hasPermi('system:tenant:remove')") + @Log(title = "租户管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{tenantId}") + public AjaxResult remove(@PathVariable Long tenantId) + { + return toAjax(tenantService.deleteTenantById(tenantId)); + } + + /** + * 冻结租户 + */ + @PreAuthorize("@ss.hasPermi('system:tenant:edit')") + @Log(title = "租户管理", businessType = BusinessType.UPDATE) + @PutMapping("/freeze/{tenantId}") + public AjaxResult freeze(@PathVariable Long tenantId) + { + SysTenant tenant = new SysTenant(); + tenant.setTenantId(tenantId); + tenant.setStatus("FROZEN"); + return toAjax(tenantService.updateTenant(tenant)); + } + + /** + * 激活租户 + */ + @PreAuthorize("@ss.hasPermi('system:tenant:edit')") + @Log(title = "租户管理", businessType = BusinessType.UPDATE) + @PutMapping("/active/{tenantId}") + public AjaxResult active(@PathVariable Long tenantId) + { + SysTenant tenant = new SysTenant(); + tenant.setTenantId(tenantId); + tenant.setStatus("ACTIVE"); + return toAjax(tenantService.updateTenant(tenant)); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/UserVerificationController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/UserVerificationController.java index e95513b..c9233f2 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/UserVerificationController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/UserVerificationController.java @@ -1,7 +1,9 @@ package com.ruoyi.web.controller.system; import java.util.List; + import javax.servlet.http.HttpServletResponse; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.prepost.PreAuthorize; @@ -13,6 +15,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; + import com.ruoyi.common.annotation.Log; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; @@ -75,7 +78,7 @@ public class UserVerificationController extends BaseController /** * 查询企业认证列表 */ - @PreAuthorize("hasAnyAuthority('system:verification:view:all', 'system:verification:manage')") + @PreAuthorize("@ss.hasAnyPermi('system:verification:view:all,system:verification:manage')") @GetMapping("/enterprise/list") public TableDataInfo listEnterpriseVerifications(UserEnterpriseVerification verification) { @@ -100,7 +103,7 @@ public class UserVerificationController extends BaseController /** * 查询企业认证详情 */ - @PreAuthorize("hasAnyAuthority('system:verification:view:all', 'system:verification:manage')") + @PreAuthorize("@ss.hasAnyPermi('system:verification:view:all,system:verification:manage')") @GetMapping("/enterprise/{verificationId}") public AjaxResult getEnterpriseVerificationDetail(@PathVariable Long verificationId) { @@ -135,7 +138,7 @@ public class UserVerificationController extends BaseController /** * 导出企业认证数据 */ - @PreAuthorize("hasAnyAuthority('system:verification:export', 'system:verification:manage')") + @PreAuthorize("@ss.hasAnyPermi('system:verification:export,system:verification:manage')") @Log(title = "企业认证", businessType = BusinessType.EXPORT) @PostMapping("/enterprise/export") public void exportEnterpriseVerifications(HttpServletResponse response, UserEnterpriseVerification verification) @@ -200,22 +203,87 @@ public class UserVerificationController extends BaseController /** * 查询身份认证列表 */ + @PreAuthorize("@ss.hasAnyPermi('system:verification:view:all,system:verification:manage') or #userId == null or #userId == authentication.principal.userId") @GetMapping("/identity/list") public TableDataInfo listIdentityVerifications( @RequestParam(required = false) Long userId, + @RequestParam(required = false) String realName, @RequestParam(required = false) String verificationStatus) { try { startPage(); - List list = - verificationService.listIdentityVerifications(userId, verificationStatus); + List list = + verificationService.listIdentityVerifications(userId, realName, verificationStatus); return getDataTable(list); } + catch (AccessDeniedException e) + { + logger.error("权限不足", e); + throw e; + } catch (Exception e) { logger.error("查询身份认证列表失败", e); return getDataTable(null); } } + + /** + * 查询身份认证详情 + */ + @PreAuthorize("@ss.hasAnyPermi('system:verification:view:all,system:verification:manage')") + @GetMapping("/identity/{verificationId}") + public AjaxResult getIdentityVerificationDetail(@PathVariable Long verificationId) + { + try + { + com.ruoyi.system.domain.UserIdentityVerification detail = + verificationService.getIdentityVerificationDetail(verificationId); + if (detail == null) + { + return AjaxResult.error("认证记录不存在"); + } + return AjaxResult.success(detail); + } + catch (AccessDeniedException e) + { + logger.error("权限不足", e); + throw e; + } + catch (Exception e) + { + logger.error("查询身份认证详情失败", e); + return AjaxResult.error("查询身份认证详情失败:" + e.getMessage()); + } + } + + /** + * 导出身份认证数据 + */ + @PreAuthorize("@ss.hasAnyPermi('system:verification:export,system:verification:manage')") + @Log(title = "身份认证", businessType = BusinessType.EXPORT) + @PostMapping("/identity/export") + public void exportIdentityVerifications(HttpServletResponse response, + @RequestParam(required = false) Long userId, + @RequestParam(required = false) String realName, + @RequestParam(required = false) String verificationStatus) + { + try + { + List list = + verificationService.listIdentityVerifications(userId, realName, verificationStatus); + ExcelUtil util = + new ExcelUtil<>(com.ruoyi.system.domain.UserIdentityVerification.class); + util.exportExcel(response, list, "身份认证数据"); + } + catch (AccessDeniedException e) + { + logger.error("权限不足", e); + } + catch (Exception e) + { + logger.error("导出身份认证数据失败", e); + } + } } diff --git a/ruoyi-admin/src/main/resources/application-https.yml b/ruoyi-admin/src/main/resources/application-https.yml new file mode 100644 index 0000000..164f10f --- /dev/null +++ b/ruoyi-admin/src/main/resources/application-https.yml @@ -0,0 +1,40 @@ +# HTTPS配置文件 +# 生产环境启用HTTPS时,在spring.profiles.active中添加 https +# 示例: spring.profiles.active: druid,api-security,https +# +# 使用前提: +# 1. 准备PKCS12格式的SSL证书 (keystore.p12) +# 2. 将证书文件放置在 src/main/resources/ 目录下,或指定绝对路径 +# 3. 通过环境变量 SSL_KEYSTORE_PASSWORD 设置证书密码 +# +# 生成自签名证书示例 (仅用于测试): +# keytool -genkeypair -alias ruoyi -keyalg RSA -keysize 2048 \ +# -storetype PKCS12 -keystore keystore.p12 -validity 3650 \ +# -storepass changeit -dname "CN=localhost, OU=RuoYi, O=RuoYi, L=Beijing, ST=Beijing, C=CN" + +server: + # HTTPS端口 + port: 8443 + ssl: + enabled: true + # 证书路径(支持classpath:或绝对路径) + key-store: classpath:keystore.p12 + # 证书密码(通过环境变量注入,避免明文存储) + key-store-password: ${SSL_KEYSTORE_PASSWORD:changeit} + key-store-type: PKCS12 + key-alias: ruoyi + # TLS协议版本(仅允许TLS 1.2和1.3) + protocol: TLS + enabled-protocols: + - TLSv1.2 + - TLSv1.3 + # 强密码套件(禁用弱加密算法) + ciphers: + - TLS_AES_256_GCM_SHA384 + - TLS_AES_128_GCM_SHA256 + - TLS_CHACHA20_POLY1305_SHA256 + - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 + - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 + +# HTTP重定向到HTTPS的配置(需要同时启用http-redirect profile) +# 如需同时监听HTTP(80)并重定向到HTTPS(8443),请参考 HttpsRedirectConfig diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 3935949..4976c07 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -54,6 +54,13 @@ server: max: 800 # Tomcat启动初始化的线程数,默认值10 min-spare: 100 + # HTTPS配置(生产环境启用,取消注释并配置证书路径) + # ssl: + # enabled: true + # key-store: classpath:keystore.p12 + # key-store-password: ${SSL_KEYSTORE_PASSWORD} + # key-store-type: PKCS12 + # key-alias: ruoyi # 日志配置 logging: diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java index 0b3e169..2d5d72a 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java @@ -51,4 +51,38 @@ public class CacheConstants * 验证统计信息 redis key */ public static final String VALIDATION_STATS_KEY = "validation_stats:"; + + /** + * 用户认证状态缓存 redis key + * 格式: verification_status:{userId} + */ + public static final String VERIFICATION_STATUS_KEY = "verification_status:"; + + /** + * 用户企业认证缓存 redis key + * 格式: enterprise_verification:{userId} + */ + public static final String ENTERPRISE_VERIFICATION_KEY = "enterprise_verification:"; + + /** + * 用户身份认证缓存 redis key + * 格式: identity_verification:{userId} + */ + public static final String IDENTITY_VERIFICATION_KEY = "identity_verification:"; + + /** + * 员工二维码图片缓存 redis key + * 格式: qrcode_image:{qrCodeId} + */ + public static final String QRCODE_IMAGE_KEY = "qrcode_image:"; + + /** + * 认证状态缓存过期时间(分钟) + */ + public static final int VERIFICATION_STATUS_EXPIRE_MINUTES = 30; + + /** + * 二维码图片缓存过期时间(分钟) + */ + public static final int QRCODE_IMAGE_EXPIRE_MINUTES = 60; } diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysTenant.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysTenant.java new file mode 100644 index 0000000..2dfc8b0 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysTenant.java @@ -0,0 +1,263 @@ +package com.ruoyi.common.core.domain.entity; + +import java.util.Date; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 租户表 sys_tenant + * + * @author ruoyi + */ +public class SysTenant extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 租户ID */ + @Excel(name = "租户序号") + private Long tenantId; + + /** 租户编码 */ + @Excel(name = "租户编码") + private String tenantCode; + + /** 租户名称 */ + @Excel(name = "租户名称") + private String tenantName; + + /** 租户类型 */ + @Excel(name = "租户类型") + private String tenantType; + + /** 公司类型 */ + @Excel(name = "公司类型", readConverterExp = "BANK=银行,CLIENT=甲方,LABOR=劳务公司") + private String companyType; + + /** 联系人 */ + @Excel(name = "联系人") + private String contactPerson; + + /** 联系电话 */ + @Excel(name = "联系电话") + private String contactPhone; + + /** 联系邮箱 */ + @Excel(name = "联系邮箱") + private String contactEmail; + + /** 租户域名 */ + private String domain; + + /** Logo URL */ + private String logoUrl; + + /** 状态 */ + @Excel(name = "状态", readConverterExp = "ACTIVE=正常,FROZEN=冻结,DELETED=已删除") + private String status; + + /** 最大用户数 */ + private Integer maxUsers; + + /** 最大公司数 */ + private Integer maxCompanies; + + /** 过期日期 */ + private Date expireDate; + + /** 套餐ID */ + private Long packageId; + + /** 备注 */ + private String remark; + + public Long getTenantId() + { + return tenantId; + } + + public void setTenantId(Long tenantId) + { + this.tenantId = tenantId; + } + + public String getTenantCode() + { + return tenantCode; + } + + public void setTenantCode(String tenantCode) + { + this.tenantCode = tenantCode; + } + + public String getTenantName() + { + return tenantName; + } + + public void setTenantName(String tenantName) + { + this.tenantName = tenantName; + } + + public String getTenantType() + { + return tenantType; + } + + public void setTenantType(String tenantType) + { + this.tenantType = tenantType; + } + + public String getCompanyType() + { + return companyType; + } + + public void setCompanyType(String companyType) + { + this.companyType = companyType; + } + + public String getContactPerson() + { + return contactPerson; + } + + public void setContactPerson(String contactPerson) + { + this.contactPerson = contactPerson; + } + + public String getContactPhone() + { + return contactPhone; + } + + public void setContactPhone(String contactPhone) + { + this.contactPhone = contactPhone; + } + + public String getContactEmail() + { + return contactEmail; + } + + public void setContactEmail(String contactEmail) + { + this.contactEmail = contactEmail; + } + + public String getDomain() + { + return domain; + } + + public void setDomain(String domain) + { + this.domain = domain; + } + + public String getLogoUrl() + { + return logoUrl; + } + + public void setLogoUrl(String logoUrl) + { + this.logoUrl = logoUrl; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public Integer getMaxUsers() + { + return maxUsers; + } + + public void setMaxUsers(Integer maxUsers) + { + this.maxUsers = maxUsers; + } + + public Integer getMaxCompanies() + { + return maxCompanies; + } + + public void setMaxCompanies(Integer maxCompanies) + { + this.maxCompanies = maxCompanies; + } + + public Date getExpireDate() + { + return expireDate; + } + + public void setExpireDate(Date expireDate) + { + this.expireDate = expireDate; + } + + public Long getPackageId() + { + return packageId; + } + + public void setPackageId(Long packageId) + { + this.packageId = packageId; + } + + @Override + public String getRemark() + { + return remark; + } + + @Override + public void setRemark(String remark) + { + this.remark = remark; + } + + @Override + public String toString() + { + return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) + .append("tenantId", getTenantId()) + .append("tenantCode", getTenantCode()) + .append("tenantName", getTenantName()) + .append("tenantType", getTenantType()) + .append("companyType", getCompanyType()) + .append("contactPerson", getContactPerson()) + .append("contactPhone", getContactPhone()) + .append("contactEmail", getContactEmail()) + .append("domain", getDomain()) + .append("logoUrl", getLogoUrl()) + .append("status", getStatus()) + .append("maxUsers", getMaxUsers()) + .append("maxCompanies", getMaxCompanies()) + .append("expireDate", getExpireDate()) + .append("packageId", getPackageId()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java index e13a435..57b38b8 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java @@ -68,6 +68,9 @@ public class SysUser extends BaseEntity /** 删除标志(0代表存在 2代表删除) */ private String delFlag; + /** 租户ID */ + private Long tenantId; + /** 最后登录IP */ @Excel(name = "最后登录IP", type = Type.EXPORT) private String loginIp; @@ -246,6 +249,16 @@ public class SysUser extends BaseEntity this.delFlag = delFlag; } + public Long getTenantId() + { + return tenantId; + } + + public void setTenantId(Long tenantId) + { + this.tenantId = tenantId; + } + public String getLoginIp() { return loginIp; diff --git a/ruoyi-credit/src/main/java/com/ruoyi/credit/config/ValidationRuleConfig.java b/ruoyi-credit/src/main/java/com/ruoyi/credit/config/ValidationRuleConfig.java index 89d0264..a596d2b 100644 --- a/ruoyi-credit/src/main/java/com/ruoyi/credit/config/ValidationRuleConfig.java +++ b/ruoyi-credit/src/main/java/com/ruoyi/credit/config/ValidationRuleConfig.java @@ -1,13 +1,32 @@ package com.ruoyi.credit.config; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.ruoyi.credit.domain.ValidationRule; -import com.ruoyi.credit.mapper.ValidationRuleMapper; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.nio.file.StandardWatchEventKinds; +import java.nio.file.WatchEvent; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import javax.annotation.PostConstruct; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.event.EventListener; import org.springframework.core.io.Resource; @@ -15,16 +34,9 @@ import org.springframework.core.io.ResourceLoader; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; -import javax.annotation.PostConstruct; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.*; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ruoyi.credit.domain.ValidationRule; +import com.ruoyi.credit.mapper.ValidationRuleMapper; /** * 验证规则配置管理类 @@ -91,14 +103,32 @@ public class ValidationRuleConfig { private volatile String configVersion = "1.0.0"; /** - * 初始化配置 + * 初始化配置(仅加载配置文件,不访问数据库) */ @PostConstruct public void init() { log.info("初始化验证规则配置管理器..."); - // 加载初始配置 - loadValidationRules(); + // 仅从配置文件加载,不访问数据库(数据库在 ApplicationReadyEvent 后才安全访问) + loadRulesFromConfigFile(); + rebuildCacheIndexes(); + + log.info("验证规则配置管理器初始化完成(配置文件),当前加载规则数量: {}", ruleCache.size()); + } + + /** + * 应用启动完成后加载数据库规则并启动热更新 + */ + @EventListener(ApplicationReadyEvent.class) + public void onApplicationReady() { + log.info("应用启动完成,开始从数据库加载验证规则..."); + + // 数据库连接池就绪后再加载 + loadRulesFromDatabase(); + rebuildCacheIndexes(); + + // 同步配置文件规则到数据库 + syncDatabaseRules(); // 启动热更新机制 if (properties.getHotReload().isEnabled()) { @@ -110,16 +140,7 @@ public class ValidationRuleConfig { startFileWatch(); } - log.info("验证规则配置管理器初始化完成,当前加载规则数量: {}", ruleCache.size()); - } - - /** - * 应用启动完成后加载数据库中的规则 - */ - @EventListener(ApplicationReadyEvent.class) - public void onApplicationReady() { - log.info("应用启动完成,开始同步数据库中的验证规则..."); - syncDatabaseRules(); + log.info("验证规则配置管理器完全初始化,当前加载规则数量: {}", ruleCache.size()); } /** diff --git a/ruoyi-credit/src/main/java/com/ruoyi/credit/domain/EmployeeInfo.java b/ruoyi-credit/src/main/java/com/ruoyi/credit/domain/EmployeeInfo.java index c0f6460..9643934 100644 --- a/ruoyi-credit/src/main/java/com/ruoyi/credit/domain/EmployeeInfo.java +++ b/ruoyi-credit/src/main/java/com/ruoyi/credit/domain/EmployeeInfo.java @@ -89,6 +89,9 @@ public class EmployeeInfo extends BaseEntity @Excel(name = "部门ID") private Long deptId; + /** 实名认证状态(来自最新二维码记录,非数据库字段) */ + private String verificationStatus; + // 薪资单位常量 public static final String SALARY_UNIT_HOURLY = "HOURLY"; public static final String SALARY_UNIT_DAILY = "DAILY"; @@ -260,6 +263,16 @@ public class EmployeeInfo extends BaseEntity return deptId; } + public void setVerificationStatus(String verificationStatus) + { + this.verificationStatus = verificationStatus; + } + + public String getVerificationStatus() + { + return verificationStatus; + } + @Override public String toString() { return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) diff --git a/ruoyi-credit/src/main/java/com/ruoyi/credit/dto/EmployeeRequestDTO.java b/ruoyi-credit/src/main/java/com/ruoyi/credit/dto/EmployeeRequestDTO.java index d9d8ca8..1513d5b 100644 --- a/ruoyi-credit/src/main/java/com/ruoyi/credit/dto/EmployeeRequestDTO.java +++ b/ruoyi-credit/src/main/java/com/ruoyi/credit/dto/EmployeeRequestDTO.java @@ -3,6 +3,7 @@ package com.ruoyi.credit.dto; import java.math.BigDecimal; import java.util.Date; import java.util.List; + import com.fasterxml.jackson.annotation.JsonFormat; /** @@ -71,6 +72,9 @@ public class EmployeeRequestDTO { @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date updateTime; + /** 实名认证状态: PENDING-待认证, APPROVED-已认证, REJECTED-认证失败, null-未认证 */ + private String verificationStatus; + // Getters and Setters public Long getEmployeeId() { return employeeId; @@ -216,6 +220,14 @@ public class EmployeeRequestDTO { this.updateTime = updateTime; } + public String getVerificationStatus() { + return verificationStatus; + } + + public void setVerificationStatus(String verificationStatus) { + this.verificationStatus = verificationStatus; + } + @Override public String toString() { return "EmployeeRequestDTO{" + diff --git a/ruoyi-credit/src/main/java/com/ruoyi/credit/service/impl/EmployeeQRCodeServiceImpl.java b/ruoyi-credit/src/main/java/com/ruoyi/credit/service/impl/EmployeeQRCodeServiceImpl.java index 6f5dd22..85581ef 100644 --- a/ruoyi-credit/src/main/java/com/ruoyi/credit/service/impl/EmployeeQRCodeServiceImpl.java +++ b/ruoyi-credit/src/main/java/com/ruoyi/credit/service/impl/EmployeeQRCodeServiceImpl.java @@ -3,6 +3,7 @@ package com.ruoyi.credit.service.impl; import java.util.Calendar; import java.util.Date; import java.util.UUID; +import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -13,7 +14,9 @@ import org.springframework.transaction.annotation.Transactional; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; import com.google.zxing.WriterException; +import com.ruoyi.common.constant.CacheConstants; import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.redis.RedisCache; import com.ruoyi.common.utils.QRCodeGenerator; import com.ruoyi.credit.ca.CAServiceInvoker; import com.ruoyi.credit.ca.config.CAServiceConfig; @@ -48,6 +51,9 @@ public class EmployeeQRCodeServiceImpl implements IEmployeeQRCodeService @Autowired private CAServiceConfig caServiceConfig; + @Autowired + private RedisCache redisCache; + /** * 为员工生成实名认证二维码 * @@ -144,6 +150,15 @@ public class EmployeeQRCodeServiceImpl implements IEmployeeQRCodeService { log.info("获取二维码图片: qrCodeId={}", qrCodeId); + // 尝试从缓存获取 + String cacheKey = CacheConstants.QRCODE_IMAGE_KEY + qrCodeId; + byte[] cachedImage = redisCache.getCacheObject(cacheKey); + if (cachedImage != null) + { + log.debug("从缓存获取二维码图片: qrCodeId={}, size={}bytes", qrCodeId, cachedImage.length); + return cachedImage; + } + try { // 1. 查询二维码记录 EmployeeQRCode qrCode = employeeQRCodeMapper.selectEmployeeQRCodeByQrCodeId(qrCodeId); @@ -163,6 +178,14 @@ public class EmployeeQRCodeServiceImpl implements IEmployeeQRCodeService // 3. 生成二维码图片 byte[] imageBytes = QRCodeGenerator.generateQRCode(qrCodeData.toJSONString()); + // 4. 缓存图片(使用二维码剩余有效期作为缓存时间,最多缓存QRCODE_IMAGE_EXPIRE_MINUTES分钟) + long remainingMinutes = (qrCode.getExpiryTime().getTime() - System.currentTimeMillis()) / 60000; + if (remainingMinutes > 0) + { + long cacheMinutes = Math.min(remainingMinutes, CacheConstants.QRCODE_IMAGE_EXPIRE_MINUTES); + redisCache.setCacheObject(cacheKey, imageBytes, (int) cacheMinutes, TimeUnit.MINUTES); + } + log.info("成功生成二维码图片: qrCodeId={}, size={}bytes", qrCodeId, imageBytes.length); return imageBytes; @@ -220,6 +243,8 @@ public class EmployeeQRCodeServiceImpl implements IEmployeeQRCodeService // 更新二维码状态为已过期 qrCode.setQrCodeStatus(EmployeeQRCode.QR_CODE_STATUS_EXPIRED); employeeQRCodeMapper.updateEmployeeQRCode(qrCode); + // 清除图片缓存 + redisCache.deleteObject(CacheConstants.QRCODE_IMAGE_KEY + qrCodeId); return AjaxResult.error("二维码已过期,请重新生成"); } @@ -246,6 +271,8 @@ public class EmployeeQRCodeServiceImpl implements IEmployeeQRCodeService qrCode.setCaVerificationId(response.getVerificationId()); qrCode.setVerificationTime(response.getVerificationTime()); employeeQRCodeMapper.updateEmployeeQRCode(qrCode); + // 清除图片缓存(二维码已使用) + redisCache.deleteObject(CacheConstants.QRCODE_IMAGE_KEY + qrCodeId); AjaxResult result = AjaxResult.success("员工实名认证成功"); result.put("verificationId", response.getVerificationId()); diff --git a/ruoyi-credit/src/main/java/com/ruoyi/credit/util/EmployeeConverter.java b/ruoyi-credit/src/main/java/com/ruoyi/credit/util/EmployeeConverter.java index ac0d27b..7b7b311 100644 --- a/ruoyi-credit/src/main/java/com/ruoyi/credit/util/EmployeeConverter.java +++ b/ruoyi-credit/src/main/java/com/ruoyi/credit/util/EmployeeConverter.java @@ -1,11 +1,11 @@ package com.ruoyi.credit.util; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.spring.SpringUtils; import com.ruoyi.credit.domain.EmployeeInfo; import com.ruoyi.credit.domain.EmployeeLibrary; import com.ruoyi.credit.dto.EmployeeRequestDTO; import com.ruoyi.credit.service.IEmployeeManagerService; -import com.ruoyi.common.utils.StringUtils; -import com.ruoyi.common.utils.spring.SpringUtils; /** * 员工信息转换工具类 @@ -116,6 +116,9 @@ public class EmployeeConverter { } else { dto.setSalaryUnits(new java.util.ArrayList<>()); } + + // 映射实名认证状态 + dto.setVerificationStatus(entity.getVerificationStatus()); return dto; } diff --git a/ruoyi-credit/src/main/resources/application-api-security.yml b/ruoyi-credit/src/main/resources/application-api-security.yml index bab18d4..836196e 100644 --- a/ruoyi-credit/src/main/resources/application-api-security.yml +++ b/ruoyi-credit/src/main/resources/application-api-security.yml @@ -2,13 +2,13 @@ api: # 限流配置 rateLimit: - enabled: true + enabled: false maxRequests: 100 # 每个时间窗口最大请求数 timeWindow: 60 # 时间窗口(秒) # 签名验证配置 signature: - enabled: true + enabled: false secretKey: ${API_SECRET_KEY:digital_credit_secret_key_2025} timestampTolerance: 300 # 时间戳容忍度(秒) @@ -54,7 +54,7 @@ spring: - OPTIONS allowed-headers: - "*" - allow-credentials: true + allow-credentials: false max-age: 3600 # 监控和度量配置 @@ -69,6 +69,6 @@ management: metrics: export: prometheus: - enabled: true + enabled: false tags: application: digital-credit-service diff --git a/ruoyi-credit/src/main/resources/mapper/credit/EmployeeInfoMapper.xml b/ruoyi-credit/src/main/resources/mapper/credit/EmployeeInfoMapper.xml index 6bd1a65..4025aa5 100644 --- a/ruoyi-credit/src/main/resources/mapper/credit/EmployeeInfoMapper.xml +++ b/ruoyi-credit/src/main/resources/mapper/credit/EmployeeInfoMapper.xml @@ -5,76 +5,89 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - select employee_id, employee_number, employee_name, id_card_number, position, department, salary_unit, hourly_salary, daily_salary, monthly_salary, employee_status, hire_date, contact_phone, library_id, is_temporary, create_time, update_time, created_by, updated_by, remark, dept_id from dc_employee_info + select e.employee_id, e.employee_number, e.employee_name, e.id_card_number, e.position, e.department, + e.salary_unit, e.hourly_salary, e.daily_salary, e.monthly_salary, e.employee_status, + e.hire_date, e.contact_phone, e.library_id, e.is_temporary, e.create_time, e.update_time, + e.created_by, e.updated_by, e.remark, e.dept_id, + qr.verification_status + from dc_employee_info e + left join ( + select employee_id, verification_status + from dc_employee_qr_code + where (employee_id, generate_time) in ( + select employee_id, max(generate_time) from dc_employee_qr_code group by employee_id + ) + ) qr on e.employee_id = qr.employee_id - - and employee_number like concat('%', #{employeeNumber}, '%') - and employee_name like concat('%', #{employeeName}, '%') - and id_card_number like concat('%', #{idCardNumber}, '%') - and position = #{position} - and department = #{department} - and salary_unit = #{salaryUnit} - and employee_status = #{employeeStatus} - and hire_date = #{hireDate} - and library_id = #{libraryId} - and is_temporary = #{isTemporary} - - and date_format(create_time,'%y%m%d') >= date_format(#{params.beginTime},'%y%m%d') + + and e.employee_number like concat('%', #{employeeNumber}, '%') + and e.employee_name like concat('%', #{employeeName}, '%') + and e.id_card_number like concat('%', #{idCardNumber}, '%') + and e.position = #{position} + and e.department = #{department} + and e.salary_unit = #{salaryUnit} + and e.employee_status = #{employeeStatus} + and e.hire_date = #{hireDate} + and e.library_id = #{libraryId} + and e.is_temporary = #{isTemporary} + + and date_format(e.create_time,'%y%m%d') >= date_format(#{params.beginTime},'%y%m%d') - - and date_format(create_time,'%y%m%d') <= date_format(#{params.endTime},'%y%m%d') + + and date_format(e.create_time,'%y%m%d') <= date_format(#{params.endTime},'%y%m%d') - order by create_time desc + order by e.create_time desc - + - where employee_id = #{employeeId} + where e.employee_id = #{employeeId} - where employee_number = #{employeeNumber} + where e.employee_number = #{employeeNumber} - where library_id = #{libraryId} - order by create_time desc + where e.library_id = #{libraryId} + order by e.create_time desc - where position = #{position} - order by create_time desc + where e.position = #{position} + order by e.create_time desc diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/HttpsRedirectConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/HttpsRedirectConfig.java new file mode 100644 index 0000000..c9044f5 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/HttpsRedirectConfig.java @@ -0,0 +1,79 @@ +package com.ruoyi.framework.config; + +import org.apache.catalina.Context; +import org.apache.catalina.connector.Connector; +import org.apache.tomcat.util.descriptor.web.SecurityCollection; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.boot.web.servlet.server.ServletWebServerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * HTTPS重定向配置 + * + * 当启用HTTPS时,同时开启HTTP监听并将所有HTTP请求重定向到HTTPS。 + * 通过配置项 security.https.redirect.enabled=true 启用。 + * + * + * 使用方式:在 application-https.yml 中添加: + * + * security: + * https: + * redirect: + * enabled: true + * http-port: 8080 + * https-port: 8443 + * + * + * @author ruoyi + */ +@Configuration +@ConditionalOnProperty(value = "security.https.redirect.enabled", havingValue = "true") +public class HttpsRedirectConfig +{ + @Value("${security.https.redirect.http-port:8080}") + private int httpPort; + + @Value("${security.https.redirect.https-port:8443}") + private int httpsPort; + + /** + * 配置额外的HTTP连接器,将HTTP请求重定向到HTTPS + */ + @Bean + public ServletWebServerFactory servletContainer() + { + TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() + { + @Override + protected void postProcessContext(Context context) + { + // 强制所有HTTP请求使用HTTPS + SecurityConstraint securityConstraint = new SecurityConstraint(); + securityConstraint.setUserConstraint("CONFIDENTIAL"); + SecurityCollection collection = new SecurityCollection(); + collection.addPattern("/*"); + securityConstraint.addCollection(collection); + context.addConstraint(securityConstraint); + } + }; + tomcat.addAdditionalTomcatConnectors(createHttpConnector()); + return tomcat; + } + + /** + * 创建HTTP连接器,监听HTTP端口并重定向到HTTPS + */ + private Connector createHttpConnector() + { + Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL); + connector.setScheme("http"); + connector.setPort(httpPort); + connector.setSecure(false); + connector.setRedirectPort(httpsPort); + return connector; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyBatisConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyBatisConfig.java index 714be72..a8f9d6a 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyBatisConfig.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyBatisConfig.java @@ -26,7 +26,7 @@ import org.springframework.util.ClassUtils; import org.apache.ibatis.plugin.Interceptor; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.framework.interceptor.DepartmentDataInterceptor; -import com.ruoyi.framework.interceptor.CompanyScopeDataInterceptor; +import com.ruoyi.framework.interceptor.TenantDataInterceptor; import com.ruoyi.framework.service.IDepartmentDataPerformanceService; import com.ruoyi.framework.service.impl.DepartmentDataPerformanceServiceImpl; @@ -45,7 +45,7 @@ public class MyBatisConfig private DepartmentDataInterceptor departmentDataInterceptor; @Autowired - private CompanyScopeDataInterceptor companyScopeDataInterceptor; + private TenantDataInterceptor tenantDataInterceptor; /** * 部门数据隔离性能监控服务Bean @@ -150,12 +150,9 @@ public class MyBatisConfig sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ","))); sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation)); - // 注册数据隔离拦截器 - // 配置拦截器执行顺序:公司数据隔离拦截器优先级最高,然后是部门数据隔离拦截器 - sessionFactory.setPlugins(new Interceptor[]{ - companyScopeDataInterceptor, // 新的公司数据隔离拦截器 - departmentDataInterceptor // 原有的部门数据隔离拦截器(向后兼容) - }); + // 注册部门数据隔离拦截器和租户数据隔离拦截器 + // 配置拦截器执行顺序:租户隔离 -> 部门隔离 + sessionFactory.setPlugins(new Interceptor[]{tenantDataInterceptor, departmentDataInterceptor}); return sessionFactory.getObject(); } diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java index 511842b..51f207b 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java @@ -15,7 +15,9 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.LogoutFilter; +import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter; import org.springframework.web.filter.CorsFilter; + import com.ruoyi.framework.config.properties.PermitAllUrlProperties; import com.ruoyi.framework.security.filter.JwtAuthenticationTokenFilter; import com.ruoyi.framework.security.handle.AuthenticationEntryPointImpl; @@ -99,9 +101,25 @@ public class SecurityConfig return httpSecurity // CSRF禁用,因为不使用session .csrf(csrf -> csrf.disable()) - // 禁用HTTP响应标头 + // 配置安全响应头 .headers((headersCustomizer) -> { - headersCustomizer.cacheControl(cache -> cache.disable()).frameOptions(options -> options.sameOrigin()); + headersCustomizer + .cacheControl(cache -> cache.disable()) + .frameOptions(options -> options.sameOrigin()) + // HTTP Strict Transport Security (HSTS) + .httpStrictTransportSecurity(hsts -> hsts + .includeSubDomains(true) + .maxAgeInSeconds(31536000)) + // X-Content-Type-Options: nosniff + .contentTypeOptions(contentType -> {}) + // X-XSS-Protection + .xssProtection(xss -> xss.block(true)) + // Referrer-Policy + .referrerPolicy(referrer -> referrer + .policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)) + // Content-Security-Policy + .contentSecurityPolicy(csp -> csp + .policyDirectives("default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'self'")); }) // 认证失败处理类 .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler)) diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/TenantDataInterceptor.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/TenantDataInterceptor.java new file mode 100644 index 0000000..6fb370b --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/TenantDataInterceptor.java @@ -0,0 +1,504 @@ +package com.ruoyi.framework.interceptor; + +import java.sql.Connection; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.regex.Pattern; + +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.executor.statement.StatementHandler; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.SqlCommandType; +import org.apache.ibatis.plugin.Interceptor; +import org.apache.ibatis.plugin.Intercepts; +import org.apache.ibatis.plugin.Invocation; +import org.apache.ibatis.plugin.Plugin; +import org.apache.ibatis.plugin.Signature; +import org.apache.ibatis.session.ResultHandler; +import org.apache.ibatis.session.RowBounds; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import com.ruoyi.framework.security.context.TenantContextHolder; + +/** + * 租户数据隔离拦截器 + * + * 自动为涉及租户隔离的表添加租户过滤条件,确保用户只能访问自己租户的数据。 + * 超级管理员不受此限制,可以访问所有租户的数据。 + * + * @author ruoyi + */ +@Component +@Intercepts({ + @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), + @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}), + @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}) +}) +public class TenantDataInterceptor implements Interceptor +{ + private static final Logger log = LoggerFactory.getLogger(TenantDataInterceptor.class); + + /** + * 需要进行租户隔离的表名集合 + */ + private static final Set TARGET_TABLES = new HashSet<>(Arrays.asList( + "sys_user", + "sys_dept", + "dc_contract", + "dc_service_contract", + "dc_service_period", + "dc_service_period_loan", + "dc_employee_info", + "dc_employee_library", + "dc_credit", + "dc_bank_institution", + "dc_financing", + "dc_company_relationship" + )); + + /** + * 忽略租户隔离的系统表 + */ + private static final Set IGNORE_TABLES = new HashSet<>(Arrays.asList( + "sys_tenant", + "sys_tenant_package", + "sys_role", + "sys_menu", + "sys_dict_data", + "sys_dict_type", + "sys_config", + "sys_notice", + "sys_post", + "sys_login_log", + "sys_oper_log" + )); + + /** + * SQL注入检测模式 + */ + private static final Pattern SQL_INJECTION_PATTERN = Pattern.compile( + "('.+(\\s)*(or|and)(\\s)+.+=')|('.+(\\s)*(or|and)(\\s)+.+\\s*=\\s*.+)", + Pattern.CASE_INSENSITIVE + ); + + /** + * 是否启用租户隔离(可通过配置覆盖) + */ + private boolean enabled = true; + + @Override + public Object intercept(Invocation invocation) throws Throwable + { + try + { + // 检查功能是否启用 + if (!enabled) + { + log.debug("租户数据隔离功能已禁用,跳过拦截"); + return invocation.proceed(); + } + + // 超级管理员跳过租户隔离 + if (TenantContextHolder.isSuperAdmin()) + { + log.debug("超级管理员访问,跳过租户数据隔离"); + return invocation.proceed(); + } + + // 获取当前租户ID + Long currentTenantId = TenantContextHolder.getCurrentTenantId(); + if (currentTenantId == null) + { + log.warn("无法获取当前租户ID,跳过租户数据隔离"); + return invocation.proceed(); + } + + Object target = invocation.getTarget(); + Object[] args = invocation.getArgs(); + + if (target instanceof Executor) + { + return handleExecutorIntercept(invocation, args, currentTenantId); + } + else if (target instanceof StatementHandler) + { + return handleStatementHandlerIntercept(invocation, args, currentTenantId); + } + + return invocation.proceed(); + } + catch (Exception e) + { + log.error("租户数据隔离拦截器执行异常", e); + // 发生异常时继续执行原始操作,避免影响业务 + return invocation.proceed(); + } + } + + /** + * 处理Executor拦截 + */ + private Object handleExecutorIntercept(Invocation invocation, Object[] args, Long currentTenantId) throws Throwable + { + MappedStatement mappedStatement = (MappedStatement) args[0]; + Object parameter = args[1]; + + // 检查是否需要添加租户过滤 + if (needTenantFilter(mappedStatement)) + { + log.debug("为SQL操作添加租户过滤条件,租户ID: {}", currentTenantId); + addTenantFilter(parameter, currentTenantId); + } + + return invocation.proceed(); + } + + /** + * 处理StatementHandler拦截 + */ + private Object handleStatementHandlerIntercept(Invocation invocation, Object[] args, Long currentTenantId) throws Throwable + { + StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); + BoundSql boundSql = statementHandler.getBoundSql(); + String originalSql = boundSql.getSql(); + + // 检查SQL是否涉及目标表 + if (containsTargetTables(originalSql)) + { + String modifiedSql = addTenantConditionToSql(originalSql, currentTenantId); + if (!modifiedSql.equals(originalSql)) + { + log.debug("修改SQL添加租户过滤条件: {}", modifiedSql); + // 通过反射修改BoundSql中的SQL + java.lang.reflect.Field sqlField = BoundSql.class.getDeclaredField("sql"); + sqlField.setAccessible(true); + sqlField.set(boundSql, modifiedSql); + } + } + + return invocation.proceed(); + } + + /** + * 检查是否需要添加租户过滤 + */ + private boolean needTenantFilter(MappedStatement mappedStatement) + { + // 获取SQL命令类型 + SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType(); + + // 只对SELECT、UPDATE、DELETE操作进行过滤 + if (sqlCommandType != SqlCommandType.SELECT && + sqlCommandType != SqlCommandType.UPDATE && + sqlCommandType != SqlCommandType.DELETE) + { + return false; + } + + // 检查SQL是否涉及目标表 + BoundSql boundSql = mappedStatement.getBoundSql(null); + if (boundSql == null) + { + return false; + } + + String sql = boundSql.getSql(); + return containsTargetTables(sql); + } + + /** + * 检查SQL是否包含目标表 + */ + private boolean containsTargetTables(String sql) + { + if (sql == null || sql.trim().isEmpty()) + { + return false; + } + + String lowerSql = sql.toLowerCase(); + for (String table : TARGET_TABLES) + { + // 使用正则表达式匹配表名,确保是完整的表名而不是子字符串 + Pattern pattern = Pattern.compile("\\b" + table + "\\b", Pattern.CASE_INSENSITIVE); + if (pattern.matcher(lowerSql).find()) + { + return true; + } + } + return false; + } + + /** + * 为参数添加租户过滤条件 + */ + private void addTenantFilter(Object parameter, Long currentTenantId) + { + if (parameter instanceof Map) + { + @SuppressWarnings("unchecked") + Map paramMap = (Map) parameter; + + // 如果参数中还没有tenantId,则添加 + if (!paramMap.containsKey("tenantId")) + { + paramMap.put("tenantId", currentTenantId); + log.debug("添加租户过滤参数: tenantId = {}", currentTenantId); + } + } + } + + /** + * 为SQL添加租户条件 + */ + private String addTenantConditionToSql(String originalSql, Long currentTenantId) + { + if (originalSql == null || originalSql.trim().isEmpty()) + { + return originalSql; + } + + // 防止SQL注入 + if (SQL_INJECTION_PATTERN.matcher(String.valueOf(currentTenantId)).find()) + { + log.warn("检测到潜在的SQL注入风险,跳过租户条件添加"); + return originalSql; + } + + String sql = originalSql.trim(); + String lowerSql = sql.toLowerCase(); + + // 检查是否需要忽略此表 + for (String ignoreTable : IGNORE_TABLES) + { + if (lowerSql.contains(ignoreTable)) + { + return originalSql; + } + } + + try + { + // 处理SELECT语句 + if (lowerSql.startsWith("select")) + { + return addTenantConditionToSelect(sql, currentTenantId); + } + // 处理UPDATE语句 + else if (lowerSql.startsWith("update")) + { + return addTenantConditionToUpdate(sql, currentTenantId); + } + // 处理DELETE语句 + else if (lowerSql.startsWith("delete")) + { + return addTenantConditionToDelete(sql, currentTenantId); + } + } + catch (Exception e) + { + log.warn("添加租户条件时发生异常,使用原始SQL: {}", e.getMessage()); + return originalSql; + } + + return originalSql; + } + + /** + * 为SELECT语句添加租户条件 + */ + private String addTenantConditionToSelect(String sql, Long currentTenantId) + { + String lowerSql = sql.toLowerCase(); + + // 查找WHERE子句的位置 + int whereIndex = lowerSql.indexOf(" where "); + + if (whereIndex != -1) + { + // 已有WHERE子句,添加AND条件 + StringBuilder sb = new StringBuilder(sql); + int insertIndex = whereIndex + 7; // " where ".length() + + // 为每个目标表添加租户条件 + String tableAlias = findTableAlias(lowerSql, TARGET_TABLES); + if (tableAlias != null) + { + String condition = tableAlias + ".tenant_id = " + currentTenantId + " AND "; + sb.insert(insertIndex, condition); + + log.debug("添加租户过滤条件到现有WHERE子句: {}", condition); + } + + return sb.toString(); + } + else + { + // 没有WHERE子句,添加WHERE条件 + // 查找ORDER BY, GROUP BY, HAVING, LIMIT等子句的位置 + String[] keywords = {" order by ", " group by ", " having ", " limit ", " offset "}; + int insertIndex = sql.length(); + + for (String keyword : keywords) + { + int keywordIndex = lowerSql.indexOf(keyword); + if (keywordIndex != -1 && keywordIndex < insertIndex) + { + insertIndex = keywordIndex; + } + } + + // 为第一个找到的目标表添加WHERE条件 + String tableAlias = findTableAlias(lowerSql, TARGET_TABLES); + if (tableAlias != null) + { + String whereClause = " WHERE " + tableAlias + ".tenant_id = " + currentTenantId; + String modifiedSql = sql.substring(0, insertIndex) + whereClause + sql.substring(insertIndex); + + log.debug("添加新的WHERE子句: {}", whereClause); + + return modifiedSql; + } + } + + log.warn("无法为SQL添加租户过滤条件,未找到目标表"); + return sql; + } + + /** + * 查找表的别名 + */ + private String findTableAlias(String lowerSql, Set targetTables) + { + for (String table : targetTables) + { + if (lowerSql.contains(table)) + { + // 对于UPDATE语句,表名在UPDATE关键字之后 + if (lowerSql.trim().startsWith("update")) + { + String updatePattern = "update\\s+" + table + "\\s+(as\\s+)?(\\w+)\\s+set"; + java.util.regex.Pattern p = java.util.regex.Pattern.compile(updatePattern, java.util.regex.Pattern.CASE_INSENSITIVE); + java.util.regex.Matcher m = p.matcher(lowerSql); + + if (m.find()) + { + String alias = m.group(2); + if (alias != null && !isReservedKeyword(alias)) + { + return alias; + } + } + + return table; + } + else + { + // 对于SELECT语句,查找表名后的别名 + String pattern = "\\b" + table + "\\s+(as\\s+)?(\\w+)"; + java.util.regex.Pattern p = java.util.regex.Pattern.compile(pattern, java.util.regex.Pattern.CASE_INSENSITIVE); + java.util.regex.Matcher m = p.matcher(lowerSql); + + if (m.find()) + { + String alias = m.group(2); + if (!isReservedKeyword(alias)) + { + return alias; + } + } + + return table; + } + } + } + return null; + } + + /** + * 检查是否为SQL保留关键字 + */ + private boolean isReservedKeyword(String word) + { + String[] keywords = {"inner", "left", "right", "join", "on", "where", "and", "or", "order", "by", "group", "having", "limit", "offset", "set", "from", "into", "values"}; + for (String keyword : keywords) + { + if (keyword.equalsIgnoreCase(word)) + { + return true; + } + } + return false; + } + + /** + * 为UPDATE语句添加租户条件 + */ + private String addTenantConditionToUpdate(String sql, Long currentTenantId) + { + String lowerSql = sql.toLowerCase(); + int whereIndex = lowerSql.indexOf(" where "); + + if (whereIndex != -1) + { + // 已有WHERE子句,添加AND条件 + String tableAlias = findTableAlias(lowerSql, TARGET_TABLES); + if (tableAlias != null) + { + String condition = " AND " + tableAlias + ".tenant_id = " + currentTenantId; + return sql + condition; + } + } + else + { + // 没有WHERE子句,添加WHERE条件 + String tableAlias = findTableAlias(lowerSql, TARGET_TABLES); + if (tableAlias != null) + { + String whereClause = " WHERE " + tableAlias + ".tenant_id = " + currentTenantId; + return sql + whereClause; + } + } + + return sql; + } + + /** + * 为DELETE语句添加租户条件 + */ + private String addTenantConditionToDelete(String sql, Long currentTenantId) + { + return addTenantConditionToUpdate(sql, currentTenantId); + } + + @Override + public Object plugin(Object target) + { + if (target instanceof Executor || target instanceof StatementHandler) + { + return Plugin.wrap(target, this); + } + return target; + } + + @Override + public void setProperties(Properties properties) + { + // 可以通过properties配置拦截器参数 + log.info("租户数据隔离拦截器初始化完成"); + } + + /** + * 设置是否启用租户隔离 + */ + public void setEnabled(boolean enabled) + { + this.enabled = enabled; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/TenantContextHolder.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/TenantContextHolder.java new file mode 100644 index 0000000..67d7b6c --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/TenantContextHolder.java @@ -0,0 +1,172 @@ +package com.ruoyi.framework.security.context; + +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.utils.SecurityUtils; + +/** + * 租户上下文管理器 + * + * 提供线程安全的租户上下文管理,支持当前租户ID的获取和设置, + * 以及超级管理员权限判断功能。 + * + * @author ruoyi + */ +public class TenantContextHolder +{ + /** + * 默认租户ID(兼容历史数据) + */ + public static final Long DEFAULT_TENANT_ID = 1L; + + /** + * 租户上下文ThreadLocal存储 + */ + private static final ThreadLocal TENANT_CONTEXT = new ThreadLocal<>(); + + /** + * 设置当前线程的租户ID + * + * @param tenantId 租户ID + */ + public static void setCurrentTenantId(Long tenantId) + { + TENANT_CONTEXT.set(tenantId); + } + + /** + * 获取当前线程的租户ID + * + * 优先从ThreadLocal获取,如果没有则返回默认租户ID + * + * @return 租户ID + */ + public static Long getCurrentTenantId() + { + Long tenantId = TENANT_CONTEXT.get(); + if (tenantId != null) + { + return tenantId; + } + + // 如果ThreadLocal中没有,则从SecurityUtils获取当前用户租户ID + try + { + tenantId = getTenantIdFromUser(); + if (tenantId != null) + { + return tenantId; + } + } + catch (Exception e) + { + // 忽略异常 + } + + // 返回默认租户ID(兼容历史数据) + return DEFAULT_TENANT_ID; + } + + /** + * 从当前用户获取租户ID + * + * @return 租户ID,如果无法获取则返回null + */ + private static Long getTenantIdFromUser() + { + try + { + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (loginUser != null && loginUser.getUser() != null) + { + return loginUser.getUser().getTenantId(); + } + } + catch (Exception e) + { + // 忽略异常 + } + return null; + } + + /** + * 清除当前线程的租户上下文 + */ + public static void clear() + { + TENANT_CONTEXT.remove(); + } + + /** + * 判断当前用户是否为超级管理员 + * + * 超级管理员可以切换到任意租户,不受租户隔离限制 + * + * @return true-超级管理员,false-普通用户 + */ + public static boolean isSuperAdmin() + { + try + { + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (loginUser == null || loginUser.getUser() == null) + { + return false; + } + + // 使用SysUser的isAdmin方法判断是否为超级管理员 + return loginUser.getUser().isAdmin(); + } + catch (Exception e) + { + // 如果获取用户信息失败,默认不是超级管理员 + return false; + } + } + + /** + * 判断是否启用租户隔离 + * + * 超级管理员可以不受租户隔离限制 + * + * @return true-启用租户隔离,false-禁用租户隔离 + */ + public static boolean isTenantEnabled() + { + // 非超级管理员都启用租户隔离 + return !isSuperAdmin(); + } + + /** + * 获取当前用户ID + * + * @return 用户ID,如果无法获取则返回null + */ + public static Long getCurrentUserId() + { + try + { + return SecurityUtils.getUserId(); + } + catch (Exception e) + { + return null; + } + } + + /** + * 获取当前用户名 + * + * @return 用户名,如果无法获取则返回null + */ + public static String getCurrentUsername() + { + try + { + return SecurityUtils.getUsername(); + } + catch (Exception e) + { + return null; + } + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java index 174d8f7..baf48c6 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java @@ -16,6 +16,7 @@ import com.ruoyi.common.core.domain.model.LoginUser; import com.ruoyi.common.event.UserAuthenticationEvent; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.security.context.TenantContextHolder; import com.ruoyi.framework.web.service.TokenService; /** @@ -40,6 +41,14 @@ public class JwtAuthenticationTokenFilter extends OncePerRequestFilter if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) { tokenService.verifyToken(loginUser); + + // 设置租户上下文 + Long tenantId = loginUser.getUser().getTenantId(); + if (tenantId != null) + { + TenantContextHolder.setCurrentTenantId(tenantId); + } + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authenticationToken); diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/UserIdentityVerification.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/UserIdentityVerification.java index 8657bcc..67eb744 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/UserIdentityVerification.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/UserIdentityVerification.java @@ -1,6 +1,7 @@ package com.ruoyi.system.domain; import java.util.Date; +import java.util.List; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; @@ -15,7 +16,7 @@ import com.ruoyi.common.core.domain.BaseEntity; /** * 用户身份认证表 sys_user_identity_verification - * + * * @author ruoyi */ public class UserIdentityVerification extends BaseEntity @@ -30,6 +31,10 @@ public class UserIdentityVerification extends BaseEntity @NotNull(message = "用户ID不能为空") private Long userId; + /** 用户名(关联查询,非数据库字段) */ + @Excel(name = "用户名") + private String userName; + /** 真实姓名 */ @Excel(name = "真实姓名") @NotBlank(message = "真实姓名不能为空") @@ -61,6 +66,9 @@ public class UserIdentityVerification extends BaseEntity @Excel(name = "CA认证时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") private Date verificationTime; + /** 审核日志列表(非数据库字段,详情查询时附加) */ + private List auditLogs; + public Long getVerificationId() { return verificationId; @@ -81,6 +89,16 @@ public class UserIdentityVerification extends BaseEntity this.userId = userId; } + public String getUserName() + { + return userName; + } + + public void setUserName(String userName) + { + this.userName = userName; + } + public String getRealName() { return realName; @@ -141,12 +159,23 @@ public class UserIdentityVerification extends BaseEntity this.verificationTime = verificationTime; } + public List getAuditLogs() + { + return auditLogs; + } + + public void setAuditLogs(List auditLogs) + { + this.auditLogs = auditLogs; + } + @Override public String toString() { return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) .append("verificationId", getVerificationId()) .append("userId", getUserId()) + .append("userName", getUserName()) .append("realName", getRealName()) .append("idCardNumber", "[MASKED]") .append("verificationStatus", getVerificationStatus()) diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTenantMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTenantMapper.java new file mode 100644 index 0000000..cee1d5d --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTenantMapper.java @@ -0,0 +1,76 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.common.core.domain.entity.SysTenant; + +/** + * 租户管理 数据层 + * + * @author ruoyi + */ +public interface SysTenantMapper +{ + /** + * 查询租户管理数据 + * + * @param tenant 租户信息 + * @return 租户信息集合 + */ + public List selectTenantList(SysTenant tenant); + + /** + * 根据租户ID查询信息 + * + * @param tenantId 租户ID + * @return 租户信息 + */ + public SysTenant selectTenantById(Long tenantId); + + /** + * 根据租户编码查询信息 + * + * @param tenantCode 租户编码 + * @return 租户信息 + */ + public SysTenant selectTenantByCode(String tenantCode); + + /** + * 校验租户编码是否唯一 + * + * @param tenantCode 租户编码 + * @return 结果 + */ + public int checkTenantCodeUnique(String tenantCode); + + /** + * 新增租户信息 + * + * @param tenant 租户信息 + * @return 结果 + */ + public int insertTenant(SysTenant tenant); + + /** + * 修改租户信息 + * + * @param tenant 租户信息 + * @return 结果 + */ + public int updateTenant(SysTenant tenant); + + /** + * 删除租户管理信息 + * + * @param tenantId 租户ID + * @return 结果 + */ + public int deleteTenantById(Long tenantId); + + /** + * 根据用户ID查询租户信息 + * + * @param userId 用户ID + * @return 租户信息 + */ + public SysTenant selectTenantByUserId(Long userId); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/UserEnterpriseVerificationMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/UserEnterpriseVerificationMapper.java index f903604..80e9eea 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/UserEnterpriseVerificationMapper.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/UserEnterpriseVerificationMapper.java @@ -37,6 +37,14 @@ public interface UserEnterpriseVerificationMapper */ public UserEnterpriseVerification selectVerificationByUserId(Long userId); + /** + * 根据用户ID查询企业认证状态(轻量级查询,用于权限检查) + * + * @param userId 用户ID + * @return 认证状态字符串 + */ + public String selectVerificationStatusByUserId(Long userId); + /** * 根据 sys_user_company.company_id 查询企业认证信息 * diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/UserIdentityVerificationMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/UserIdentityVerificationMapper.java index c452a58..c609b04 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/UserIdentityVerificationMapper.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/UserIdentityVerificationMapper.java @@ -35,6 +35,14 @@ public interface UserIdentityVerificationMapper */ public UserIdentityVerification selectVerificationByUserId(Long userId); + /** + * 根据用户ID查询身份认证状态(轻量级查询,用于权限检查) + * + * @param userId 用户ID + * @return 认证状态字符串 + */ + public String selectVerificationStatusByUserId(Long userId); + /** * 新增用户身份认证 * diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTenantService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTenantService.java new file mode 100644 index 0000000..173143e --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTenantService.java @@ -0,0 +1,83 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.common.core.domain.entity.SysTenant; + +/** + * 租户管理 服务层 + * + * @author ruoyi + */ +public interface ISysTenantService +{ + /** + * 查询租户管理数据 + * + * @param tenant 租户信息 + * @return 租户信息集合 + */ + public List selectTenantList(SysTenant tenant); + + /** + * 根据租户ID查询信息 + * + * @param tenantId 租户ID + * @return 租户信息 + */ + public SysTenant selectTenantById(Long tenantId); + + /** + * 根据租户编码查询信息 + * + * @param tenantCode 租户编码 + * @return 租户信息 + */ + public SysTenant selectTenantByCode(String tenantCode); + + /** + * 根据用户ID查询租户信息 + * + * @param userId 用户ID + * @return 租户信息 + */ + public SysTenant selectTenantByUserId(Long userId); + + /** + * 校验租户编码是否唯一 + * + * @param tenant 租户信息 + * @return 结果 + */ + public boolean checkTenantCodeUnique(SysTenant tenant); + + /** + * 新增保存租户信息 + * + * @param tenant 租户信息 + * @return 结果 + */ + public int insertTenant(SysTenant tenant); + + /** + * 修改保存租户信息 + * + * @param tenant 租户信息 + * @return 结果 + */ + public int updateTenant(SysTenant tenant); + + /** + * 删除租户管理信息 + * + * @param tenantId 租户ID + * @return 结果 + */ + public int deleteTenantById(Long tenantId); + + /** + * 校验租户是否允许删除 + * + * @param tenantId 租户ID + */ + public void checkTenantCanDelete(Long tenantId); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/IUserVerificationService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/IUserVerificationService.java index a98f5c9..24165a8 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/IUserVerificationService.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/IUserVerificationService.java @@ -78,10 +78,19 @@ public interface IUserVerificationService * 查询身份认证列表 * * @param userId 用户ID(可选) + * @param realName 真实姓名(可选) * @param verificationStatus 认证状态(可选) * @return 身份认证集合 */ - public List listIdentityVerifications(Long userId, String verificationStatus); + public List listIdentityVerifications(Long userId, String realName, String verificationStatus); + + /** + * 查询身份认证详情(含审核历史) + * + * @param verificationId 认证ID + * @return 身份认证详情 + */ + public com.ruoyi.system.domain.UserIdentityVerification getIdentityVerificationDetail(Long verificationId); // ==================== 认证状态查询方法 ==================== diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTenantServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTenantServiceImpl.java new file mode 100644 index 0000000..14e2bab --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTenantServiceImpl.java @@ -0,0 +1,150 @@ +package com.ruoyi.system.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.common.core.domain.entity.SysTenant; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.system.mapper.SysTenantMapper; +import com.ruoyi.system.service.ISysTenantService; + +/** + * 租户管理 服务实现 + * + * @author ruoyi + */ +@Service +public class SysTenantServiceImpl implements ISysTenantService +{ + @Autowired + private SysTenantMapper tenantMapper; + + /** + * 查询租户管理数据 + * + * @param tenant 租户信息 + * @return 租户信息集合 + */ + @Override + public List selectTenantList(SysTenant tenant) + { + return tenantMapper.selectTenantList(tenant); + } + + /** + * 根据租户ID查询信息 + * + * @param tenantId 租户ID + * @return 租户信息 + */ + @Override + public SysTenant selectTenantById(Long tenantId) + { + return tenantMapper.selectTenantById(tenantId); + } + + /** + * 根据租户编码查询信息 + * + * @param tenantCode 租户编码 + * @return 租户信息 + */ + @Override + public SysTenant selectTenantByCode(String tenantCode) + { + return tenantMapper.selectTenantByCode(tenantCode); + } + + /** + * 根据用户ID查询租户信息 + * + * @param userId 用户ID + * @return 租户信息 + */ + @Override + public SysTenant selectTenantByUserId(Long userId) + { + return tenantMapper.selectTenantByUserId(userId); + } + + /** + * 校验租户编码是否唯一 + * + * @param tenant 租户信息 + * @return 结果 + */ + @Override + public boolean checkTenantCodeUnique(SysTenant tenant) + { + Long tenantId = tenant.getTenantId() == null ? -1L : tenant.getTenantId(); + SysTenant info = tenantMapper.selectTenantByCode(tenant.getTenantCode()); + if (info != null && info.getTenantId().longValue() != tenantId.longValue()) + { + return false; + } + return true; + } + + /** + * 新增保存租户信息 + * + * @param tenant 租户信息 + * @return 结果 + */ + @Override + public int insertTenant(SysTenant tenant) + { + // 校验租户编码唯一性 + if (!checkTenantCodeUnique(tenant)) + { + throw new ServiceException("租户编码已存在"); + } + return tenantMapper.insertTenant(tenant); + } + + /** + * 修改保存租户信息 + * + * @param tenant 租户信息 + * @return 结果 + */ + @Override + public int updateTenant(SysTenant tenant) + { + // 校验租户编码唯一性 + if (!checkTenantCodeUnique(tenant)) + { + throw new ServiceException("租户编码已存在"); + } + return tenantMapper.updateTenant(tenant); + } + + /** + * 删除租户管理信息 + * + * @param tenantId 租户ID + * @return 结果 + */ + @Override + public int deleteTenantById(Long tenantId) + { + // 校验租户是否允许删除 + checkTenantCanDelete(tenantId); + return tenantMapper.deleteTenantById(tenantId); + } + + /** + * 校验租户是否允许删除 + * + * @param tenantId 租户ID + */ + @Override + public void checkTenantCanDelete(Long tenantId) + { + // 默认租户不允许删除 + if (tenantId != null && tenantId == 1L) + { + throw new ServiceException("默认租户不允许删除"); + } + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/UserVerificationServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/UserVerificationServiceImpl.java index 0f517e5..e065cee 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/UserVerificationServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/UserVerificationServiceImpl.java @@ -2,6 +2,7 @@ package com.ruoyi.system.service.impl; import java.util.Date; import java.util.List; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -12,9 +13,13 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.ruoyi.common.config.EncryptionConfig; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.constant.VerificationPermissions; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.domain.entity.SysRole; import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.EncryptionUtil; import com.ruoyi.common.utils.IdCardValidator; import com.ruoyi.common.utils.SecurityUtils; @@ -56,6 +61,9 @@ public class UserVerificationServiceImpl implements IUserVerificationService @Autowired private VerificationNotificationService notificationService; + @Autowired + private RedisCache redisCache; + /** * 提交企业法人CA认证申请 * @@ -170,11 +178,15 @@ public class UserVerificationServiceImpl implements IUserVerificationService if ("APPROVED".equals(verification.getVerificationStatus())) { notificationService.sendEnterpriseApprovalNotification(verification); + // 清除用户认证状态缓存 + evictVerificationCache(request.getUserId()); return AjaxResult.success("法人CA认证成功,企业认证已自动完成"); } else { notificationService.sendEnterpriseRejectionNotification(verification); + // 清除用户认证状态缓存 + evictVerificationCache(request.getUserId()); return AjaxResult.error("法人CA认证失败: " + caResponse.getRejectReason()); } } @@ -222,12 +234,27 @@ public class UserVerificationServiceImpl implements IUserVerificationService @Override public AjaxResult getEnterpriseVerificationStatus(Long userId) { + // 检查数据访问权限:用户只能查询自己的认证状态 + checkDataAccessPermission(userId); + logSensitiveDataAccess("查询企业认证状态", userId); + + // 尝试从缓存获取 + String cacheKey = CacheConstants.ENTERPRISE_VERIFICATION_KEY + userId; + UserEnterpriseVerification cached = redisCache.getCacheObject(cacheKey); + if (cached != null) + { + return AjaxResult.success(cached); + } + UserEnterpriseVerification verification = enterpriseVerificationMapper.selectVerificationByUserId(userId); if (verification == null) { return AjaxResult.success("未认证", null); } + + // 缓存结果 + redisCache.setCacheObject(cacheKey, verification, CacheConstants.VERIFICATION_STATUS_EXPIRE_MINUTES, TimeUnit.MINUTES); return AjaxResult.success(verification); } @@ -277,6 +304,69 @@ public class UserVerificationServiceImpl implements IUserVerificationService return auditLogMapper.selectAuditLogByVerificationId(verificationType, verificationId); } + /** + * 检查数据访问权限:用户只能访问自己的认证数据,管理员可访问所有数据 + * + * @param targetUserId 目标用户ID + * @throws ServiceException 权限不足时抛出异常 + */ + private void checkDataAccessPermission(Long targetUserId) + { + try + { + LoginUser loginUser = SecurityUtils.getLoginUser(); + Long currentUserId = loginUser.getUserId(); + + // 超级管理员可访问所有数据 + if (SecurityUtils.isAdmin(currentUserId)) + { + return; + } + + // 具有查看所有认证信息权限的用户可访问所有数据 + if (SecurityUtils.hasPermi(VerificationPermissions.VIEW_ALL_VERIFICATION) + || SecurityUtils.hasPermi(VerificationPermissions.MANAGE_VERIFICATION)) + { + log.info("管理员用户ID: {} 访问用户ID: {} 的认证数据", currentUserId, targetUserId); + return; + } + + // 普通用户只能访问自己的数据 + if (!currentUserId.equals(targetUserId)) + { + log.warn("用户ID: {} 尝试访问用户ID: {} 的认证数据,权限不足", currentUserId, targetUserId); + throw new ServiceException("无权访问其他用户的认证数据"); + } + } + catch (ServiceException e) + { + throw e; + } + catch (Exception e) + { + log.error("检查数据访问权限时发生异常", e); + } + } + + /** + * 记录敏感数据访问日志 + * + * @param operation 操作描述 + * @param targetUserId 目标用户ID + */ + private void logSensitiveDataAccess(String operation, Long targetUserId) + { + try + { + Long currentUserId = SecurityUtils.getUserId(); + log.info("[敏感数据访问] 操作: {}, 操作人ID: {}, 目标用户ID: {}", operation, currentUserId, targetUserId); + } + catch (Exception e) + { + // 日志记录失败不影响主流程 + } + } + /** * 记录审核日志 * @@ -427,11 +517,15 @@ public class UserVerificationServiceImpl implements IUserVerificationService if ("APPROVED".equals(verification.getVerificationStatus())) { notificationService.sendIdentityApprovalNotification(verification); + // 清除用户认证状态缓存 + evictVerificationCache(userId); return AjaxResult.success("身份认证成功"); } else { notificationService.sendIdentityRejectionNotification(verification); + // 清除用户认证状态缓存 + evictVerificationCache(userId); return AjaxResult.error("身份认证失败: " + verification.getRejectReason()); } } @@ -476,6 +570,18 @@ public class UserVerificationServiceImpl implements IUserVerificationService @Override public AjaxResult getIdentityVerificationStatus(Long userId) { + // 检查数据访问权限:用户只能查询自己的认证状态 + checkDataAccessPermission(userId); + logSensitiveDataAccess("查询身份认证状态", userId); + + // 尝试从缓存获取(不含身份证号) + String cacheKey = CacheConstants.IDENTITY_VERIFICATION_KEY + userId; + UserIdentityVerification cached = redisCache.getCacheObject(cacheKey); + if (cached != null) + { + return AjaxResult.success(cached); + } + UserIdentityVerification verification = identityVerificationMapper.selectVerificationByUserId(userId); if (verification == null) @@ -485,6 +591,9 @@ public class UserVerificationServiceImpl implements IUserVerificationService // 不返回加密的身份证号 verification.setIdCardNumber(null); + + // 缓存结果 + redisCache.setCacheObject(cacheKey, verification, CacheConstants.VERIFICATION_STATUS_EXPIRE_MINUTES, TimeUnit.MINUTES); return AjaxResult.success(verification); } @@ -493,14 +602,34 @@ public class UserVerificationServiceImpl implements IUserVerificationService * 查询身份认证列表 * * @param userId 用户ID(可选) + * @param realName 真实姓名(可选) * @param verificationStatus 认证状态(可选) * @return 身份认证集合 */ @Override - public List listIdentityVerifications(Long userId, String verificationStatus) + public List listIdentityVerifications(Long userId, String realName, String verificationStatus) { + // 如果指定了userId,检查数据访问权限 + if (userId != null) + { + checkDataAccessPermission(userId); + } + else + { + // 查询所有用户数据需要管理员权限 + if (!SecurityUtils.isAdmin(SecurityUtils.getUserId()) + && !SecurityUtils.hasPermi(VerificationPermissions.VIEW_ALL_VERIFICATION) + && !SecurityUtils.hasPermi(VerificationPermissions.MANAGE_VERIFICATION)) + { + // 普通用户只能查询自己的数据 + userId = SecurityUtils.getUserId(); + } + } + logSensitiveDataAccess("查询身份认证列表", userId); + UserIdentityVerification query = new UserIdentityVerification(); query.setUserId(userId); + query.setRealName(realName); query.setVerificationStatus(verificationStatus); List list = identityVerificationMapper.selectVerificationList(query); @@ -511,6 +640,28 @@ public class UserVerificationServiceImpl implements IUserVerificationService return list; } + /** + * 查询身份认证详情(含审核历史) + * + * @param verificationId 认证ID + * @return 身份认证详情 + */ + @Override + public UserIdentityVerification getIdentityVerificationDetail(Long verificationId) + { + UserIdentityVerification verification = identityVerificationMapper.selectVerificationById(verificationId); + if (verification == null) + { + return null; + } + // 不返回加密的身份证号 + verification.setIdCardNumber(null); + // 附加审核历史 + List auditLogs = auditLogMapper.selectAuditLogByVerificationId("IDENTITY", verificationId); + verification.setAuditLogs(auditLogs); + return verification; + } + // ==================== 认证状态查询方法实现 ==================== /** @@ -528,20 +679,31 @@ public class UserVerificationServiceImpl implements IUserVerificationService return true; } - // 查询企业认证状态 - UserEnterpriseVerification enterpriseVerification = enterpriseVerificationMapper.selectVerificationByUserId(userId); - if (enterpriseVerification == null || !"APPROVED".equals(enterpriseVerification.getVerificationStatus())) + // 尝试从缓存获取认证状态 + String cacheKey = CacheConstants.VERIFICATION_STATUS_KEY + userId; + Boolean cachedStatus = redisCache.getCacheObject(cacheKey); + if (cachedStatus != null) { + return cachedStatus; + } + + // 查询企业认证状态(使用轻量级状态查询) + String enterpriseStatus = enterpriseVerificationMapper.selectVerificationStatusByUserId(userId); + if (!"APPROVED".equals(enterpriseStatus)) + { + redisCache.setCacheObject(cacheKey, Boolean.FALSE, CacheConstants.VERIFICATION_STATUS_EXPIRE_MINUTES, TimeUnit.MINUTES); return false; } - // 查询身份认证状态 - UserIdentityVerification identityVerification = identityVerificationMapper.selectVerificationByUserId(userId); - if (identityVerification == null || !"APPROVED".equals(identityVerification.getVerificationStatus())) + // 查询身份认证状态(使用轻量级状态查询) + String identityStatus = identityVerificationMapper.selectVerificationStatusByUserId(userId); + if (!"APPROVED".equals(identityStatus)) { + redisCache.setCacheObject(cacheKey, Boolean.FALSE, CacheConstants.VERIFICATION_STATUS_EXPIRE_MINUTES, TimeUnit.MINUTES); return false; } + redisCache.setCacheObject(cacheKey, Boolean.TRUE, CacheConstants.VERIFICATION_STATUS_EXPIRE_MINUTES, TimeUnit.MINUTES); return true; } @@ -557,6 +719,14 @@ public class UserVerificationServiceImpl implements IUserVerificationService // 检查是否为超级管理员 boolean isSuperAdmin = (userId != null && userId == 1L); + // 尝试从缓存获取完整状态DTO + String cacheKey = CacheConstants.VERIFICATION_STATUS_KEY + "dto:" + userId; + com.ruoyi.system.domain.dto.VerificationStatusDTO cachedDto = redisCache.getCacheObject(cacheKey); + if (cachedDto != null) + { + return cachedDto; + } + // 查询企业认证状态 UserEnterpriseVerification enterpriseVerification = enterpriseVerificationMapper.selectVerificationByUserId(userId); String enterpriseStatus = enterpriseVerification != null ? enterpriseVerification.getVerificationStatus() : "PENDING"; @@ -569,12 +739,35 @@ public class UserVerificationServiceImpl implements IUserVerificationService boolean fullyVerified = isSuperAdmin || ("APPROVED".equals(enterpriseStatus) && "APPROVED".equals(identityStatus)); - return new com.ruoyi.system.domain.dto.VerificationStatusDTO( + com.ruoyi.system.domain.dto.VerificationStatusDTO dto = new com.ruoyi.system.domain.dto.VerificationStatusDTO( userId, enterpriseStatus, identityStatus, fullyVerified, isSuperAdmin ); + + // 缓存结果 + redisCache.setCacheObject(cacheKey, dto, CacheConstants.VERIFICATION_STATUS_EXPIRE_MINUTES, TimeUnit.MINUTES); + + return dto; + } + + /** + * 清除用户认证状态相关缓存 + * + * @param userId 用户ID + */ + private void evictVerificationCache(Long userId) + { + if (userId == null) + { + return; + } + redisCache.deleteObject(CacheConstants.VERIFICATION_STATUS_KEY + userId); + redisCache.deleteObject(CacheConstants.VERIFICATION_STATUS_KEY + "dto:" + userId); + redisCache.deleteObject(CacheConstants.ENTERPRISE_VERIFICATION_KEY + userId); + redisCache.deleteObject(CacheConstants.IDENTITY_VERIFICATION_KEY + userId); + log.debug("已清除用户认证状态缓存: userId={}", userId); } } diff --git a/ruoyi-system/src/main/resources/mapper/system/SysTenantMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysTenantMapper.xml new file mode 100644 index 0000000..e50cb30 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysTenantMapper.xml @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select tenant_id, tenant_code, tenant_name, tenant_type, company_type, contact_person, contact_phone, contact_email, + domain, logo_url, status, max_users, max_companies, expire_date, package_id, + create_by, create_time, update_by, update_time, remark + from sys_tenant + + + + + + + AND tenant_code like concat('%', #{tenantCode}, '%') + + + AND tenant_name like concat('%', #{tenantName}, '%') + + + AND tenant_type = #{tenantType} + + + AND company_type = #{companyType} + + + AND status = #{status} + + + order by tenant_id + + + + + where tenant_id = #{tenantId} + + + + + where tenant_code = #{tenantCode} + + + + select count(1) from sys_tenant where tenant_code = #{tenantCode} + + + + + where tenant_id = (select tenant_id from sys_user where user_id = #{userId} limit 1) + + + + insert into sys_tenant + + tenant_code, + tenant_name, + tenant_type, + company_type, + contact_person, + contact_phone, + contact_email, + domain, + logo_url, + status, + max_users, + max_companies, + expire_date, + package_id, + create_by, + remark, + + + #{tenantCode}, + #{tenantName}, + #{tenantType}, + #{companyType}, + #{contactPerson}, + #{contactPhone}, + #{contactEmail}, + #{domain}, + #{logoUrl}, + #{status}, + #{maxUsers}, + #{maxCompanies}, + #{expireDate}, + #{packageId}, + #{createBy}, + #{remark}, + + + + + update sys_tenant + + tenant_code = #{tenantCode}, + tenant_name = #{tenantName}, + tenant_type = #{tenantType}, + company_type = #{companyType}, + contact_person = #{contactPerson}, + contact_phone = #{contactPhone}, + contact_email = #{contactEmail}, + domain = #{domain}, + logo_url = #{logoUrl}, + status = #{status}, + max_users = #{maxUsers}, + max_companies = #{maxCompanies}, + expire_date = #{expireDate}, + package_id = #{packageId}, + update_by = #{updateBy}, + remark = #{remark}, + + where tenant_id = #{tenantId} + + + + update sys_tenant set status = 'DELETED' where tenant_id = #{tenantId} + + + diff --git a/ruoyi-system/src/main/resources/mapper/system/UserEnterpriseVerificationMapper.xml b/ruoyi-system/src/main/resources/mapper/system/UserEnterpriseVerificationMapper.xml index edcbb66..a37e9f8 100644 --- a/ruoyi-system/src/main/resources/mapper/system/UserEnterpriseVerificationMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/system/UserEnterpriseVerificationMapper.xml @@ -68,6 +68,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" limit 1 + + + select verification_status + from sys_user_enterprise_verification + where user_id = #{userId} + order by create_time desc + limit 1 + + where verification_status = 'APPROVED' diff --git a/ruoyi-system/src/main/resources/mapper/system/UserIdentityVerificationMapper.xml b/ruoyi-system/src/main/resources/mapper/system/UserIdentityVerificationMapper.xml index e27359f..21035eb 100644 --- a/ruoyi-system/src/main/resources/mapper/system/UserIdentityVerificationMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/system/UserIdentityVerificationMapper.xml @@ -7,6 +7,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -20,44 +21,54 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - select verification_id, user_id, real_name, id_card_number, verification_status, - ca_verification_id, reject_reason, verification_time, - create_by, create_time, update_by, update_time - from sys_user_identity_verification + select v.verification_id, v.user_id, u.user_name, v.real_name, v.id_card_number, + v.verification_status, v.ca_verification_id, v.reject_reason, v.verification_time, + v.create_by, v.create_time, v.update_by, v.update_time + from sys_user_identity_verification v + left join sys_user u on u.user_id = v.user_id - AND user_id = #{userId} + AND v.user_id = #{userId} - AND real_name like concat('%', #{realName}, '%') + AND v.real_name like concat('%', #{realName}, '%') - AND verification_status = #{verificationStatus} + AND v.verification_status = #{verificationStatus} - AND ca_verification_id = #{caVerificationId} + AND v.ca_verification_id = #{caVerificationId} - AND date_format(create_time,'%Y%m%d') >= date_format(#{params.beginTime},'%Y%m%d') + AND date_format(v.create_time,'%Y%m%d') >= date_format(#{params.beginTime},'%Y%m%d') - AND date_format(create_time,'%Y%m%d') <= date_format(#{params.endTime},'%Y%m%d') + AND date_format(v.create_time,'%Y%m%d') <= date_format(#{params.endTime},'%Y%m%d') - order by create_time desc + order by v.create_time desc - where verification_id = #{verificationId} + where v.verification_id = #{verificationId} + where v.user_id = #{userId} + order by v.create_time desc + limit 1 + + + + + select verification_status + from sys_user_identity_verification where user_id = #{userId} order by create_time desc limit 1 diff --git a/sql/anxin.sql b/sql/anxin.sql index c08b46b..56ba016 100644 --- a/sql/anxin.sql +++ b/sql/anxin.sql @@ -3850,6 +3850,11 @@ INSERT INTO `sys_menu` VALUES (6134, '关系删除', 6130, 4, '', '', '', '', 1, INSERT INTO `sys_menu` VALUES (6135, '关系导出', 6130, 5, '', '', '', '', 1, 0, 'F', '0', '0', 'credit:relationship:export', '#', 'admin', '2026-03-16 14:59:18', '', NULL, ''); INSERT INTO `sys_menu` VALUES (6155, '认证管理', 0, 5, 'verification', NULL, NULL, '', 1, 0, 'M', '0', '0', '', 'documentation', 'admin', '2026-03-05 18:23:25', 'admin', '2026-03-05 18:24:19', '认证管理目录'); INSERT INTO `sys_menu` VALUES (6156, '认证信息管理', 6155, 1, 'manage', 'system/verification/manage/index', NULL, '', 1, 0, 'C', '0', '0', 'system:verification:view:all', 'list', 'admin', '2026-03-05 18:23:25', '', NULL, '认证信息管理菜单'); +INSERT INTO `sys_menu` VALUES (6157, '企业认证管理', 6155, 2, 'enterprise', 'system/verification/enterprise', NULL, '', 1, 0, 'C', '0', '0', 'system:verification:view:all', 'office', 'admin', '2026-03-05 18:23:25', '', NULL, '企业认证管理菜单'); +INSERT INTO `sys_menu` VALUES (6158, '企业认证查询', 6157, 1, '', '', '', '', 1, 0, 'F', '0', '0', 'system:verification:view:all', '#', 'admin', '2026-03-05 18:23:25', '', NULL, '企业认证列表查询权限'); +INSERT INTO `sys_menu` VALUES (6159, '企业认证详情', 6157, 2, '', '', '', '', 1, 0, 'F', '0', '0', 'system:verification:view:all', '#', 'admin', '2026-03-05 18:23:25', '', NULL, '企业认证详情查看权限'); +INSERT INTO `sys_menu` VALUES (6160, '企业认证导出', 6157, 3, '', '', '', '', 1, 0, 'F', '0', '0', 'system:verification:export', '#', 'admin', '2026-03-05 18:23:25', '', NULL, '企业认证数据导出权限'); +INSERT INTO `sys_menu` VALUES (6161, '认证管理权限', 6157, 4, '', '', '', '', 1, 0, 'F', '0', '0', 'system:verification:manage', '#', 'admin', '2026-03-05 18:23:25', '', NULL, '企业认证管理操作权限'); -- ---------------------------- -- Table structure for sys_notice diff --git a/sql/tenant-menu.sql b/sql/tenant-menu.sql new file mode 100644 index 0000000..4d85da5 --- /dev/null +++ b/sql/tenant-menu.sql @@ -0,0 +1,34 @@ +-- ============================================= +-- 租户管理菜单权限SQL +-- ============================================= + +-- ---------------------------- +-- 1. 插入租户管理菜单 +-- ---------------------------- +-- 菜单 SQL +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +VALUES (2000, '租户管理', 0, 6, 'tenant', 'system/tenant/index', 1, 0, 'C', '0', '0', 'system:tenant:list', 'peoples', 'admin', sysdate(), '', '', '租户管理菜单'); + +-- 按钮 SQL +INSERT INTO sys_menu (menu_name, parent_id, order_num, perms, menu_type, visible, status, create_by, create_time) +VALUES ('租户查询', 2000, 1, 'system:tenant:query', 'F', '0', '0', 'admin', sysdate()); + +INSERT INTO sys_menu (menu_name, parent_id, order_num, perms, menu_type, visible, status, create_by, create_time) +VALUES ('租户新增', 2000, 2, 'system:tenant:add', 'F', '0', '0', 'admin', sysdate()); + +INSERT INTO sys_menu (menu_name, parent_id, order_num, perms, menu_type, visible, status, create_by, create_time) +VALUES ('租户修改', 2000, 3, 'system:tenant:edit', 'F', '0', '0', 'admin', sysdate()); + +INSERT INTO sys_menu (menu_name, parent_id, order_num, perms, menu_type, visible, status, create_by, create_time) +VALUES ('租户删除', 2000, 4, 'system:tenant:remove', 'F', '0', '0', 'admin', sysdate()); + +-- ---------------------------- +-- 2. 为超级管理员角色分配租户管理权限 +-- ---------------------------- +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT 1, menu_id FROM sys_menu WHERE perms IN ('system:tenant:list', 'system:tenant:query', 'system:tenant:add', 'system:tenant:edit', 'system:tenant:remove'); + +-- ---------------------------- +-- 3. 查看菜单是否插入成功 +-- ---------------------------- +-- SELECT * FROM sys_menu WHERE menu_id = 2000 OR parent_id = 2000; diff --git a/sql/tenant-migration.sql b/sql/tenant-migration.sql new file mode 100644 index 0000000..ff3df16 --- /dev/null +++ b/sql/tenant-migration.sql @@ -0,0 +1,125 @@ +-- ============================================= +-- 租户相关数据库变更脚本 +-- 用于若依框架 SAAS 化迁移 +-- ============================================= + +-- ---------------------------- +-- 1. 创建租户表 +-- ---------------------------- +DROP TABLE IF EXISTS `sys_tenant`; +CREATE TABLE `sys_tenant` ( + `tenant_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '租户ID', + `tenant_code` varchar(50) NOT NULL COMMENT '租户编码(唯一)', + `tenant_name` varchar(100) NOT NULL COMMENT '租户名称', + `tenant_type` varchar(20) NOT NULL DEFAULT 'COMPANY' COMMENT '租户类型: COMPANY-企业', + `company_type` varchar(20) NULL COMMENT '公司类型: BANK-银行, CLIENT-甲方, LABOR-劳务公司', + `contact_person` varchar(50) NULL COMMENT '联系人', + `contact_phone` varchar(20) NULL COMMENT '联系电话', + `contact_email` varchar(100) NULL COMMENT '联系邮箱', + `domain` varchar(100) NULL COMMENT '租户域名', + `logo_url` varchar(500) NULL COMMENT 'Logo URL', + `status` varchar(10) NOT NULL DEFAULT 'ACTIVE' COMMENT '状态: ACTIVE-正常, FROZEN-冻结, DELETED-已删除', + `max_users` int(11) NULL DEFAULT 10 COMMENT '最大用户数', + `max_companies` int(11) NULL DEFAULT 5 COMMENT '最大公司数', + `expire_date` date NULL COMMENT '过期日期', + `package_id` bigint(20) NULL COMMENT '套餐ID', + `create_by` varchar(64) NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` varchar(64) NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `remark` varchar(500) NULL COMMENT '备注', + PRIMARY KEY (`tenant_id`), + UNIQUE INDEX `uk_tenant_code`(`tenant_code` ASC), + INDEX `idx_status`(`status` ASC), + INDEX `idx_company_type`(`company_type` ASC) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='租户表'; + +-- ---------------------------- +-- 2. 初始化默认租户 (兼容现有数据) +-- ---------------------------- +INSERT INTO `sys_tenant` (`tenant_id`, `tenant_code`, `tenant_name`, `tenant_type`, `company_type`, `status`, `max_users`, `max_companies`, `remark`) +VALUES (1, 'DEFAULT', '默认租户', 'COMPANY', NULL, 'ACTIVE', 1000, 100, '系统默认租户,兼容历史数据'); + +-- ---------------------------- +-- 3. 为 sys_user 表添加 tenant_id 字段 +-- ---------------------------- +ALTER TABLE `sys_user` ADD COLUMN `tenant_id` bigint(20) NULL DEFAULT 1 COMMENT '租户ID' AFTER `del_flag`; +ALTER TABLE `sys_user` ADD INDEX `idx_user_tenant_id`(`tenant_id` ASC); + +-- ---------------------------- +-- 4. 为 sys_dept 表添加 tenant_id 字段 +-- ---------------------------- +ALTER TABLE `sys_dept` ADD COLUMN `tenant_id` bigint(20) NULL DEFAULT 1 COMMENT '租户ID' AFTER `dept_id`; +ALTER TABLE `sys_dept` ADD INDEX `idx_dept_tenant_id`(`tenant_id` ASC); + +-- ---------------------------- +-- 5. 为 dc_contract 表添加 tenant_id 字段 +-- ---------------------------- +ALTER TABLE `dc_contract` ADD COLUMN `tenant_id` bigint(20) NULL DEFAULT 1 COMMENT '租户ID' AFTER `contract_id`; +ALTER TABLE `dc_contract` ADD INDEX `idx_contract_tenant_id`(`tenant_id` ASC); + +-- ---------------------------- +-- 6. 为 dc_service_contract 表添加 tenant_id 字段 +-- ---------------------------- +ALTER TABLE `dc_service_contract` ADD COLUMN `tenant_id` bigint(20) NULL DEFAULT 1 COMMENT '租户ID' AFTER `service_contract_id`; +ALTER TABLE `dc_service_contract` ADD INDEX `idx_service_contract_tenant_id`(`tenant_id` ASC); + +-- ---------------------------- +-- 7. 为 dc_employee_info 表添加 tenant_id 字段 +-- ---------------------------- +ALTER TABLE `dc_employee_info` ADD COLUMN `tenant_id` bigint(20) NULL DEFAULT 1 COMMENT '租户ID' AFTER `employee_id`; +ALTER TABLE `dc_employee_info` ADD INDEX `idx_employee_tenant_id`(`tenant_id` ASC); + +-- ---------------------------- +-- 8. 为 dc_employee_library 表添加 tenant_id 字段 +-- ---------------------------- +ALTER TABLE `dc_employee_library` ADD COLUMN `tenant_id` bigint(20) NULL DEFAULT 1 COMMENT '租户ID' AFTER `library_id`; +ALTER TABLE `dc_employee_library` ADD INDEX `idx_library_tenant_id`(`tenant_id` ASC); + +-- ---------------------------- +-- 9. 为 dc_credit 表添加 tenant_id 字段 +-- ---------------------------- +ALTER TABLE `dc_credit` ADD COLUMN `tenant_id` bigint(20) NULL DEFAULT 1 COMMENT '租户ID' AFTER `credit_id`; +ALTER TABLE `dc_credit` ADD INDEX `idx_credit_tenant_id`(`tenant_id` ASC); + +-- ---------------------------- +-- 10. 为 dc_financing 表添加 tenant_id 字段 +-- ---------------------------- +ALTER TABLE `dc_financing` ADD COLUMN `tenant_id` bigint(20) NULL DEFAULT 1 COMMENT '租户ID' AFTER `financing_id`; +ALTER TABLE `dc_financing` ADD INDEX `idx_financing_tenant_id`(`tenant_id` ASC); + +-- ---------------------------- +-- 11. 为 dc_company_relationship 表添加 tenant_id 字段 +-- ---------------------------- +ALTER TABLE `dc_company_relationship` ADD COLUMN `tenant_id` bigint(20) NULL DEFAULT 1 COMMENT '租户ID' AFTER `relationship_id`; +ALTER TABLE `dc_company_relationship` ADD INDEX `idx_relationship_tenant_id`(`tenant_id` ASC); + +-- ---------------------------- +-- 12. 为 dc_bank_institution 表添加 tenant_id 字段 +-- ---------------------------- +ALTER TABLE `dc_bank_institution` ADD COLUMN `tenant_id` bigint(20) NULL DEFAULT 1 COMMENT '租户ID' AFTER `bank_id`; +ALTER TABLE `dc_bank_institution` ADD INDEX `idx_bank_tenant_id`(`tenant_id` ASC); + +-- ---------------------------- +-- 13. 为 dc_service_period 表添加 tenant_id 字段 +-- ---------------------------- +ALTER TABLE `dc_service_period` ADD COLUMN `tenant_id` bigint(20) NULL DEFAULT 1 COMMENT '租户ID' AFTER `service_period_id`; +ALTER TABLE `dc_service_period` ADD INDEX `idx_service_period_tenant_id`(`tenant_id` ASC); + +-- ---------------------------- +-- 14. 为 dc_service_period_loan 表添加 tenant_id 字段 +-- ---------------------------- +ALTER TABLE `dc_service_period_loan` ADD COLUMN `tenant_id` bigint(20) NULL DEFAULT 1 COMMENT '租户ID' AFTER `link_id`; +ALTER TABLE `dc_service_period_loan` ADD INDEX `idx_loan_tenant_id`(`tenant_id` ASC); + +-- ---------------------------- +-- 15. 更新现有数据的 tenant_id (根据 company_id 关联) +-- ---------------------------- +-- 注意:此脚本需要在确认现有公司数据后再执行 +-- 以下为示例逻辑,实际执行时需要根据具体业务逻辑调整 + +-- 更新用户的 tenant_id 为 1 (默认租户) +UPDATE sys_user SET tenant_id = 1 WHERE tenant_id IS NULL; + +-- 更新部门的 tenant_id 为 1 (默认租户) +UPDATE sys_dept SET tenant_id = 1 WHERE tenant_id IS NULL;
+ * 当启用HTTPS时,同时开启HTTP监听并将所有HTTP请求重定向到HTTPS。 + * 通过配置项 security.https.redirect.enabled=true 启用。 + *
+ * security: + * https: + * redirect: + * enabled: true + * http-port: 8080 + * https-port: 8443 + *