diff --git a/.gitignore b/.gitignore index 220a6961d..8365d1baf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +ruoyi-fastapi-backend/.env.dev # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -85,7 +86,7 @@ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: -# .python-version +.python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. diff --git a/README.md b/README.md index 705778548..36ccaa609 100644 --- a/README.md +++ b/README.md @@ -246,7 +246,7 @@ pip3 install -r requirements.txt pip3 install -r requirements-pg.txt # 配置环境 -在.env.dev文件中配置开发环境的数据库和redis +把.env.dev.example修改为.env.dev,并在.env.dev文件中配置开发环境的数据库和redis # 运行sql文件 1.新建数据库ruoyi-fastapi(默认,可修改) diff --git a/ruoyi-fastapi-backend/.env.dev b/ruoyi-fastapi-backend/.env.dev index f5feade42..fe9823854 100644 --- a/ruoyi-fastapi-backend/.env.dev +++ b/ruoyi-fastapi-backend/.env.dev @@ -25,6 +25,8 @@ APP_DEMO_MODE = false APP_DISABLE_SWAGGER = false # 应用是否禁用ReDoc文档 APP_DISABLE_REDOC = false +# 启用用户信息缓存 +APP_ENABLE_USER_CACHE = true # -------- Jwt配置 -------- # Jwt秘钥 diff --git a/ruoyi-fastapi-backend/.env.dev.example b/ruoyi-fastapi-backend/.env.dev.example new file mode 100644 index 000000000..fe9823854 --- /dev/null +++ b/ruoyi-fastapi-backend/.env.dev.example @@ -0,0 +1,122 @@ +# -------- 应用配置 -------- +# 应用运行环境 +APP_ENV = 'dev' +# 应用名称 +APP_NAME = 'RuoYi-FastAPI' +# 应用代理路径 +APP_ROOT_PATH = '/dev-api' +# 应用主机 +APP_HOST = '0.0.0.0' +# 应用端口 +APP_PORT = 9099 +# 应用版本 +APP_VERSION= '1.9.0' +# 应用是否开启热重载 +APP_RELOAD = true +# 应用工作进程数 +APP_WORKERS = 1 +# 应用是否开启IP归属区域查询 +APP_IP_LOCATION_QUERY = true +# 应用是否允许账号同时登录 +APP_SAME_TIME_LOGIN = true +# 应用是否为演示模式 +APP_DEMO_MODE = false +# 应用是否禁用Swagger文档 +APP_DISABLE_SWAGGER = false +# 应用是否禁用ReDoc文档 +APP_DISABLE_REDOC = false +# 启用用户信息缓存 +APP_ENABLE_USER_CACHE = true + +# -------- Jwt配置 -------- +# Jwt秘钥 +JWT_SECRET_KEY = 'b01c66dc2c58dc6a0aabfe2144256be36226de378bf87f72c0c795dda67f4d55' +# Jwt算法 +JWT_ALGORITHM = 'HS256' +# 令牌过期时间 +JWT_EXPIRE_MINUTES = 1440 +# redis中令牌过期时间 +JWT_REDIS_EXPIRE_MINUTES = 30 + + +# -------- 数据库配置 -------- +# 数据库类型,可选的有'mysql'、'postgresql',默认为'mysql' +DB_TYPE = 'mysql' +# 数据库主机 +DB_HOST = '127.0.0.1' +# 数据库端口 +DB_PORT = 3306 +# 数据库用户名 +DB_USERNAME = 'root' +# 数据库密码 +DB_PASSWORD = 'mysqlroot' +# 数据库名称 +DB_DATABASE = 'ruoyi-fastapi' +# 是否开启sqlalchemy日志 +DB_ECHO = true +# 允许溢出连接池大小的最大连接数 +DB_MAX_OVERFLOW = 10 +# 连接池大小,0表示连接数无限制 +DB_POOL_SIZE = 50 +# 连接回收时间(单位:秒) +DB_POOL_RECYCLE = 3600 +# 连接池中没有线程可用时,最多等待的时间(单位:秒) +DB_POOL_TIMEOUT = 30 + +# -------- Redis配置 -------- +# Redis主机 +REDIS_HOST = '127.0.0.1' +# Redis端口 +REDIS_PORT = 6379 +# Redis用户名 +REDIS_USERNAME = '' +# Redis密码 +REDIS_PASSWORD = '' +# Redis数据库 +REDIS_DATABASE = 2 + +# -------- 日志配置 -------- +# Redis Stream Key +LOG_STREAM_KEY = 'log:stream' +# Redis Stream 消费组名称 +LOG_STREAM_GROUP = 'log_aggregator' +# Redis Stream 消费者名称前缀 +LOG_STREAM_CONSUMER_PREFIX = 'worker' +# 每次读取的最大消息数量 +LOG_STREAM_BATCH_SIZE = 100 +# 阻塞读取等待时间(毫秒) +LOG_STREAM_BLOCK_MS = 2000 +# Stream 最大长度(近似裁剪) +LOG_STREAM_MAXLEN = 100000 +# Pending 回收最小空闲时间(毫秒) +LOG_STREAM_CLAIM_IDLE_MS = 60000 +# Pending 回收检查间隔(毫秒) +LOG_STREAM_CLAIM_INTERVAL_MS = 5000 +# 每次回收的最大消息数量 +LOG_STREAM_CLAIM_BATCH_SIZE = 100 +# 去重 Key 过期时间(秒) +LOG_STREAM_DEDUP_TTL = 3600 +# 去重 Key 前缀 +LOG_STREAM_DEDUP_PREFIX = 'log:dedup' +# stdout 输出是否为 JSON +LOGURU_JSON = false +# Loguru 最低输出级别 +LOGURU_LEVEL = 'INFO' +# 是否输出到 stdout +LOGURU_STDOUT = true +# 是否启用文件日志 +LOG_FILE_ENABLED = true +# 文件日志根目录 +LOG_FILE_BASE_DIR = 'logs' +# 文件滚动策略 +LOGURU_ROTATION = '50MB' +# 文件保留策略 +LOGURU_RETENTION = '30 days' +# 文件压缩格式 +LOGURU_COMPRESSION = 'zip' +# 实例标识(用于区分实例) +LOG_INSTANCE_ID = 'dev' +# 服务名称(用于统一标识服务) +LOG_SERVICE_NAME = 'ruoyi-fastapi-backend' +# Worker 标识(auto 自动生成) +LOG_WORKER_ID = 'auto' diff --git a/ruoyi-fastapi-backend/.env.dockermy b/ruoyi-fastapi-backend/.env.dockermy index a4716822b..59543eaf4 100644 --- a/ruoyi-fastapi-backend/.env.dockermy +++ b/ruoyi-fastapi-backend/.env.dockermy @@ -25,6 +25,8 @@ APP_DEMO_MODE = false APP_DISABLE_SWAGGER = true # 应用是否禁用ReDoc文档 APP_DISABLE_REDOC = true +# 启用用户信息缓存 +APP_ENABLE_USER_CACHE = true # -------- Jwt配置 -------- # Jwt秘钥 diff --git a/ruoyi-fastapi-backend/.env.dockerpg b/ruoyi-fastapi-backend/.env.dockerpg index 5b8619d79..691378f87 100644 --- a/ruoyi-fastapi-backend/.env.dockerpg +++ b/ruoyi-fastapi-backend/.env.dockerpg @@ -25,6 +25,8 @@ APP_SAME_TIME_LOGIN = true APP_DISABLE_SWAGGER = true # 应用是否禁用ReDoc文档 APP_DISABLE_REDOC = true +# 启用用户信息缓存 +APP_ENABLE_USER_CACHE = true # -------- Jwt配置 -------- # Jwt秘钥 diff --git a/ruoyi-fastapi-backend/.env.prod b/ruoyi-fastapi-backend/.env.prod index be624c86c..a6b20074d 100644 --- a/ruoyi-fastapi-backend/.env.prod +++ b/ruoyi-fastapi-backend/.env.prod @@ -25,6 +25,8 @@ APP_DEMO_MODE = false APP_DISABLE_SWAGGER = true # 应用是否禁用ReDoc文档 APP_DISABLE_REDOC = true +# 启用用户信息缓存 +APP_ENABLE_USER_CACHE = true # -------- Jwt配置 -------- # Jwt秘钥 diff --git a/ruoyi-fastapi-backend/common/enums.py b/ruoyi-fastapi-backend/common/enums.py index 3ce6e320a..d6983c207 100644 --- a/ruoyi-fastapi-backend/common/enums.py +++ b/ruoyi-fastapi-backend/common/enums.py @@ -49,3 +49,4 @@ def remark(self) -> str | None: ACCOUNT_LOCK = {'key': 'account_lock', 'remark': '用户锁定'} PASSWORD_ERROR_COUNT = {'key': 'password_error_count', 'remark': '密码错误次数'} SMS_CODE = {'key': 'sms_code', 'remark': '短信验证码'} + USER_INFO = {'key': 'user', 'remark': '用户信息缓存'} diff --git a/ruoyi-fastapi-backend/config/env.py b/ruoyi-fastapi-backend/config/env.py index 89baec580..8fd344212 100644 --- a/ruoyi-fastapi-backend/config/env.py +++ b/ruoyi-fastapi-backend/config/env.py @@ -27,6 +27,7 @@ class AppSettings(BaseSettings): app_demo_mode: bool = False app_disable_swagger: bool = False app_disable_redoc: bool = False + app_enable_user_cache: bool = True class JwtSettings(BaseSettings): diff --git a/ruoyi-fastapi-backend/module_admin/controller/dept_controller.py b/ruoyi-fastapi-backend/module_admin/controller/dept_controller.py index cbae112ad..c5c9dd301 100644 --- a/ruoyi-fastapi-backend/module_admin/controller/dept_controller.py +++ b/ruoyi-fastapi-backend/module_admin/controller/dept_controller.py @@ -110,7 +110,7 @@ async def edit_system_dept( await DeptService.check_dept_data_scope_services(query_db, edit_dept.dept_id, data_scope_sql) edit_dept.update_by = current_user.user.user_name edit_dept.update_time = datetime.now() - edit_dept_result = await DeptService.edit_dept_services(query_db, edit_dept) + edit_dept_result = await DeptService.edit_dept_services(request, query_db, edit_dept) logger.info(edit_dept_result.message) return ResponseUtil.success(msg=edit_dept_result.message) @@ -139,7 +139,7 @@ async def delete_system_dept( delete_dept = DeleteDeptModel(deptIds=dept_ids) delete_dept.update_by = current_user.user.user_name delete_dept.update_time = datetime.now() - delete_dept_result = await DeptService.delete_dept_services(query_db, delete_dept) + delete_dept_result = await DeptService.delete_dept_services(request, query_db, delete_dept) logger.info(delete_dept_result.message) return ResponseUtil.success(msg=delete_dept_result.message) diff --git a/ruoyi-fastapi-backend/module_admin/controller/login_controller.py b/ruoyi-fastapi-backend/module_admin/controller/login_controller.py index 2143895fb..93741006f 100644 --- a/ruoyi-fastapi-backend/module_admin/controller/login_controller.py +++ b/ruoyi-fastapi-backend/module_admin/controller/login_controller.py @@ -73,7 +73,7 @@ async def login( ex=timedelta(minutes=JwtConfig.jwt_redis_expire_minutes), ) await UserService.edit_user_services( - query_db, EditUserModel(userId=result[0].user_id, loginDate=datetime.now(), type='status') + request, query_db, EditUserModel(userId=result[0].user_id, loginDate=datetime.now(), type='status') ) logger.info('登录成功') # 判断请求是否来自于api文档,如果是返回指定格式的结果,用于修复api文档认证成功后token显示undefined的bug @@ -169,14 +169,19 @@ async def register_user( response_model=ResponseBaseModel, ) async def logout(request: Request, token: Annotated[str | None, Depends(oauth2_scheme)]) -> Response: - payload = jwt.decode( - token, JwtConfig.jwt_secret_key, algorithms=[JwtConfig.jwt_algorithm], options={'verify_exp': False} - ) + try: + payload = jwt.decode( + token, JwtConfig.jwt_secret_key, algorithms=[JwtConfig.jwt_algorithm], options={'verify_exp': False} + ) + except jwt.InvalidSignatureError: + logger.info('Token已过期,无法解析用户信息') + return ResponseUtil.success(msg='退出成功') + if AppConfig.app_same_time_login: token_id: str = payload.get('session_id') else: token_id: str = payload.get('user_id') - await LoginService.logout_services(request, token_id) + await LoginService.logout_services(request, token_id, payload.get('user_id')) logger.info('退出成功') return ResponseUtil.success(msg='退出成功') diff --git a/ruoyi-fastapi-backend/module_admin/controller/menu_controller.py b/ruoyi-fastapi-backend/module_admin/controller/menu_controller.py index d5d8ffced..3219b16d6 100644 --- a/ruoyi-fastapi-backend/module_admin/controller/menu_controller.py +++ b/ruoyi-fastapi-backend/module_admin/controller/menu_controller.py @@ -120,7 +120,7 @@ async def edit_system_menu( ) -> Response: edit_menu.update_by = current_user.user.user_name edit_menu.update_time = datetime.now() - edit_menu_result = await MenuService.edit_menu_services(query_db, edit_menu) + edit_menu_result = await MenuService.edit_menu_services(request, query_db, edit_menu) logger.info(edit_menu_result.message) return ResponseUtil.success(msg=edit_menu_result.message) @@ -140,7 +140,7 @@ async def delete_system_menu( query_db: Annotated[AsyncSession, DBSessionDependency()], ) -> Response: delete_menu = DeleteMenuModel(menuIds=menu_ids) - delete_menu_result = await MenuService.delete_menu_services(query_db, delete_menu) + delete_menu_result = await MenuService.delete_menu_services(request, query_db, delete_menu) logger.info(delete_menu_result.message) return ResponseUtil.success(msg=delete_menu_result.message) diff --git a/ruoyi-fastapi-backend/module_admin/controller/post_controller.py b/ruoyi-fastapi-backend/module_admin/controller/post_controller.py index 6a6312113..845dde3f2 100644 --- a/ruoyi-fastapi-backend/module_admin/controller/post_controller.py +++ b/ruoyi-fastapi-backend/module_admin/controller/post_controller.py @@ -86,7 +86,7 @@ async def edit_system_post( ) -> Response: edit_post.update_by = current_user.user.user_name edit_post.update_time = datetime.now() - edit_post_result = await PostService.edit_post_services(query_db, edit_post) + edit_post_result = await PostService.edit_post_services(request, query_db, edit_post) logger.info(edit_post_result.message) return ResponseUtil.success(msg=edit_post_result.message) @@ -106,7 +106,7 @@ async def delete_system_post( query_db: Annotated[AsyncSession, DBSessionDependency()], ) -> Response: delete_post = DeletePostModel(postIds=post_ids) - delete_post_result = await PostService.delete_post_services(query_db, delete_post) + delete_post_result = await PostService.delete_post_services(request, query_db, delete_post) logger.info(delete_post_result.message) return ResponseUtil.success(msg=delete_post_result.message) diff --git a/ruoyi-fastapi-backend/module_admin/controller/role_controller.py b/ruoyi-fastapi-backend/module_admin/controller/role_controller.py index faefde992..372f2e587 100644 --- a/ruoyi-fastapi-backend/module_admin/controller/role_controller.py +++ b/ruoyi-fastapi-backend/module_admin/controller/role_controller.py @@ -126,7 +126,7 @@ async def edit_system_role( await RoleService.check_role_data_scope_services(query_db, str(edit_role.role_id), data_scope_sql) edit_role.update_by = current_user.user.user_name edit_role.update_time = datetime.now() - edit_role_result = await RoleService.edit_role_services(query_db, edit_role) + edit_role_result = await RoleService.edit_role_services(request, query_db, edit_role) logger.info(edit_role_result.message) return ResponseUtil.success(msg=edit_role_result.message) @@ -158,7 +158,7 @@ async def edit_system_role_datascope( updateBy=current_user.user.user_name, updateTime=datetime.now(), ) - role_data_scope_result = await RoleService.role_datascope_services(query_db, edit_role) + role_data_scope_result = await RoleService.role_datascope_services(request, query_db, edit_role) logger.info(role_data_scope_result.message) return ResponseUtil.success(msg=role_data_scope_result.message) @@ -186,7 +186,7 @@ async def delete_system_role( if not current_user.user.admin: await RoleService.check_role_data_scope_services(query_db, role_id, data_scope_sql) delete_role = DeleteRoleModel(roleIds=role_ids, updateBy=current_user.user.user_name, updateTime=datetime.now()) - delete_role_result = await RoleService.delete_role_services(query_db, delete_role) + delete_role_result = await RoleService.delete_role_services(request, query_db, delete_role) logger.info(delete_role_result.message) return ResponseUtil.success(msg=delete_role_result.message) @@ -271,7 +271,7 @@ async def reset_system_role_status( updateTime=datetime.now(), type='status', ) - edit_role_result = await RoleService.edit_role_services(query_db, edit_role) + edit_role_result = await RoleService.edit_role_services(request, query_db, edit_role) logger.info(edit_role_result.message) return ResponseUtil.success(msg=edit_role_result.message) @@ -336,7 +336,7 @@ async def add_system_role_user( ) -> Response: if not current_user.user.admin: await RoleService.check_role_data_scope_services(query_db, str(add_role_user.role_id), data_scope_sql) - add_role_user_result = await UserService.add_user_role_services(query_db, add_role_user) + add_role_user_result = await UserService.add_user_role_services(request, query_db, add_role_user) logger.info(add_role_user_result.message) return ResponseUtil.success(msg=add_role_user_result.message) @@ -355,7 +355,7 @@ async def cancel_system_role_user( cancel_user_role: CrudUserRoleModel, query_db: Annotated[AsyncSession, DBSessionDependency()], ) -> Response: - cancel_user_role_result = await UserService.delete_user_role_services(query_db, cancel_user_role) + cancel_user_role_result = await UserService.delete_user_role_services(request, query_db, cancel_user_role) logger.info(cancel_user_role_result.message) return ResponseUtil.success(msg=cancel_user_role_result.message) @@ -374,7 +374,9 @@ async def batch_cancel_system_role_user( batch_cancel_user_role: Annotated[CrudUserRoleModel, Query()], query_db: Annotated[AsyncSession, DBSessionDependency()], ) -> Response: - batch_cancel_user_role_result = await UserService.delete_user_role_services(query_db, batch_cancel_user_role) + batch_cancel_user_role_result = await UserService.delete_user_role_services( + request, query_db, batch_cancel_user_role + ) logger.info(batch_cancel_user_role_result.message) return ResponseUtil.success(msg=batch_cancel_user_role_result.message) diff --git a/ruoyi-fastapi-backend/module_admin/controller/user_controller.py b/ruoyi-fastapi-backend/module_admin/controller/user_controller.py index 31fc8dbcc..5133e382e 100644 --- a/ruoyi-fastapi-backend/module_admin/controller/user_controller.py +++ b/ruoyi-fastapi-backend/module_admin/controller/user_controller.py @@ -153,7 +153,7 @@ async def edit_system_user( ) edit_user.update_by = current_user.user.user_name edit_user.update_time = datetime.now() - edit_user_result = await UserService.edit_user_services(query_db, edit_user) + edit_user_result = await UserService.edit_user_services(request, query_db, edit_user) logger.info(edit_user_result.message) return ResponseUtil.success(msg=edit_user_result.message) @@ -185,7 +185,7 @@ async def delete_system_user( if not current_user.user.admin: await UserService.check_user_data_scope_services(query_db, int(user_id), data_scope_sql) delete_user = DeleteUserModel(userIds=user_ids, updateBy=current_user.user.user_name, updateTime=datetime.now()) - delete_user_result = await UserService.delete_user_services(query_db, delete_user) + delete_user_result = await UserService.delete_user_services(request, query_db, delete_user) logger.info(delete_user_result.message) return ResponseUtil.success(msg=delete_user_result.message) @@ -217,7 +217,7 @@ async def reset_system_user_pwd( updateTime=datetime.now(), type='pwd', ) - edit_user_result = await UserService.edit_user_services(query_db, edit_user) + edit_user_result = await UserService.edit_user_services(request, query_db, edit_user) logger.info(edit_user_result.message) return ResponseUtil.success(msg=edit_user_result.message) @@ -248,7 +248,7 @@ async def change_system_user_status( updateTime=datetime.now(), type='status', ) - edit_user_result = await UserService.edit_user_services(query_db, edit_user) + edit_user_result = await UserService.edit_user_services(request, query_db, edit_user) logger.info(edit_user_result.message) return ResponseUtil.success(msg=edit_user_result.message) @@ -333,7 +333,7 @@ async def change_system_user_profile_avatar( updateTime=datetime.now(), type='avatar', ) - edit_user_result = await UserService.edit_user_services(query_db, edit_user) + edit_user_result = await UserService.edit_user_services(request, query_db, edit_user) logger.info(edit_user_result.message) return ResponseUtil.success(model_content=AvatarModel(imgUrl=edit_user.avatar), msg=edit_user_result.message) @@ -363,7 +363,7 @@ async def change_system_user_profile_info( postIds=current_user.user.post_ids.split(',') if current_user.user.post_ids else [], role=current_user.user.role, ) - edit_user_result = await UserService.edit_user_services(query_db, edit_user) + edit_user_result = await UserService.edit_user_services(request, query_db, edit_user) logger.info(edit_user_result.message) return ResponseUtil.success(msg=edit_user_result.message) @@ -390,7 +390,7 @@ async def reset_system_user_password( updateBy=current_user.user.user_name, updateTime=datetime.now(), ) - reset_user_result = await UserService.reset_user_services(query_db, reset_user) + reset_user_result = await UserService.reset_user_services(request, query_db, reset_user) logger.info(reset_user_result.message) return ResponseUtil.success(msg=reset_user_result.message) @@ -519,7 +519,7 @@ async def update_system_role_user( await UserService.check_user_data_scope_services(query_db, user_id, user_data_scope_sql) await RoleService.check_role_data_scope_services(query_db, role_ids, role_data_scope_sql) add_user_role_result = await UserService.add_user_role_services( - query_db, CrudUserRoleModel(userId=user_id, roleIds=role_ids) + request, query_db, CrudUserRoleModel(userId=user_id, roleIds=role_ids) ) logger.info(add_user_role_result.message) diff --git a/ruoyi-fastapi-backend/module_admin/service/cache_service.py b/ruoyi-fastapi-backend/module_admin/service/cache_service.py index 410f6b896..28960ac06 100644 --- a/ruoyi-fastapi-backend/module_admin/service/cache_service.py +++ b/ruoyi-fastapi-backend/module_admin/service/cache_service.py @@ -2,6 +2,7 @@ from common.enums import RedisInitKeyConfig from common.vo import CrudResponseModel +from config.env import AppConfig from config.get_redis import RedisUtil from module_admin.entity.vo.cache_vo import CacheInfoModel, CacheMonitorModel @@ -124,3 +125,53 @@ async def clear_cache_monitor_all_services(cls, request: Request) -> CrudRespons await RedisUtil.init_sys_config(request.app.state.redis) return CrudResponseModel(is_success=True, message='所有缓存清除成功') + + @classmethod + async def clear_usercache_by_id(cls, request: Request, user_id: int) -> bool: + """ + 根据用户ID清除用户信息缓存service + + :param request: Request对象 + :param user_id: 用户ID + :return: 操作缓存响应信息 + """ + if not AppConfig.app_enable_user_cache: + return False + + cache_key = f'{RedisInitKeyConfig.USER_INFO.key}:{user_id}' + await request.app.state.redis.delete(cache_key) + + return True + + @classmethod + async def clear_usercache_all(cls, request: Request) -> bool: + """ + 清除所有用户信息缓存service + + :param request: Request对象 + :return: 操作缓存响应信息 + """ + if not AppConfig.app_enable_user_cache: + return False + + pattern = f'{RedisInitKeyConfig.USER_INFO.key}:*' + batch_size = 1000 + + try: + cursor = 0 + deleted_count = 0 + while True: + cursor, keys = await request.app.state.redis.scan(cursor=cursor, match=pattern, count=batch_size) + if keys: + await request.app.state.redis.unlink(*keys) + deleted_count += len(keys) + # 游标为 0 表示迭代完成 + if cursor == 0: + break + + print(f'✅ 已删除 {deleted_count} 个用户缓存键') + except Exception as e: + print(f'❌ 删除失败: {e}') + return False + + return True diff --git a/ruoyi-fastapi-backend/module_admin/service/dept_service.py b/ruoyi-fastapi-backend/module_admin/service/dept_service.py index da61a2b08..29fb87a58 100644 --- a/ruoyi-fastapi-backend/module_admin/service/dept_service.py +++ b/ruoyi-fastapi-backend/module_admin/service/dept_service.py @@ -1,6 +1,7 @@ from collections.abc import Sequence from typing import Any +from fastapi import Request from sqlalchemy import ColumnElement from sqlalchemy.ext.asyncio import AsyncSession @@ -10,6 +11,7 @@ from module_admin.dao.dept_dao import DeptDao from module_admin.entity.do.dept_do import SysDept from module_admin.entity.vo.dept_vo import DeleteDeptModel, DeptModel, DeptTreeModel +from module_admin.service.cache_service import CacheService from utils.common_util import CamelCaseUtil @@ -126,7 +128,9 @@ async def add_dept_services(cls, query_db: AsyncSession, page_object: DeptModel) raise e @classmethod - async def edit_dept_services(cls, query_db: AsyncSession, page_object: DeptModel) -> CrudResponseModel: + async def edit_dept_services( + cls, request: Request, query_db: AsyncSession, page_object: DeptModel + ) -> CrudResponseModel: """ 编辑部门信息service @@ -160,13 +164,16 @@ async def edit_dept_services(cls, query_db: AsyncSession, page_object: DeptModel ): await cls.update_parent_dept_status_normal(query_db, page_object) await query_db.commit() + await CacheService.clear_usercache_all(request) return CrudResponseModel(is_success=True, message='更新成功') except Exception as e: await query_db.rollback() raise e @classmethod - async def delete_dept_services(cls, query_db: AsyncSession, page_object: DeleteDeptModel) -> CrudResponseModel: + async def delete_dept_services( + cls, request: Request, query_db: AsyncSession, page_object: DeleteDeptModel + ) -> CrudResponseModel: """ 删除部门信息service @@ -185,6 +192,7 @@ async def delete_dept_services(cls, query_db: AsyncSession, page_object: DeleteD await DeptDao.delete_dept_dao(query_db, DeptModel(deptId=dept_id)) await query_db.commit() + await CacheService.clear_usercache_all(request) return CrudResponseModel(is_success=True, message='删除成功') except Exception as e: await query_db.rollback() diff --git a/ruoyi-fastapi-backend/module_admin/service/login_service.py b/ruoyi-fastapi-backend/module_admin/service/login_service.py index e7bc1bfd9..48230551e 100644 --- a/ruoyi-fastapi-backend/module_admin/service/login_service.py +++ b/ruoyi-fastapi-backend/module_admin/service/login_service.py @@ -1,3 +1,4 @@ +import json import random import uuid from datetime import datetime, timedelta, timezone @@ -24,8 +25,10 @@ from module_admin.entity.do.user_do import SysUser from module_admin.entity.vo.login_vo import MenuTreeModel, MetaModel, RouterModel, SmsCode, UserLogin, UserRegister from module_admin.entity.vo.user_vo import AddUserModel, CurrentUserModel, ResetUserModel, TokenData, UserInfoModel +from module_admin.service.cache_service import CacheService from module_admin.service.user_service import UserService from utils.common_util import CamelCaseUtil +from utils.json_util import ComplexEncoder from utils.log_util import logger from utils.message_util import message_service from utils.pwd_util import PwdUtil @@ -168,6 +171,9 @@ async def __check_login_captcha(cls, request: Request, login_user: UserLogin) -> if login_user.code != str(captcha_value): logger.warning('验证码错误') raise LoginException(data='', message='验证码错误') + + # 验证成功/失败后删除验证码缓存 + await request.app.state.redis.delete(f'{RedisInitKeyConfig.CAPTCHA_CODES.key}:{login_user.uuid}') return True @classmethod @@ -188,6 +194,61 @@ async def create_access_token(cls, data: dict, expires_delta: timedelta | None = encoded_jwt = jwt.encode(to_encode, JwtConfig.jwt_secret_key, algorithm=JwtConfig.jwt_algorithm) return encoded_jwt + @classmethod + async def __get_user_by_id_cache(cls, request: Request, db: AsyncSession, user_id: int) -> dict[str, Any]: + """ + 根据user_id获取用户信息(启用缓存) + + :param request: 当前请求对象 + :param db: orm对象 + :param user_id: 用户id + :return: 当前user_id的用户信息对象 + """ + if AppConfig.app_enable_user_cache: + cached_user_info = await request.app.state.redis.get(f'{RedisInitKeyConfig.USER_INFO.key}:{user_id}') + if cached_user_info: + await request.app.state.redis.expire( + f'{RedisInitKeyConfig.USER_INFO.key}:{user_id}', + timedelta(minutes=JwtConfig.jwt_redis_expire_minutes), + ) + user_data = json.loads(cached_user_info) + return user_data + + query_user = await UserDao.get_user_by_id(db, user_id=user_id) + if query_user is None: + return None + + role_id_list = [item.role_id for item in query_user.get('user_role_info')] + if 1 in role_id_list: + permissions = ['*:*:*'] + else: + permissions = [row.perms for row in query_user.get('user_menu_info') if row.perms] + + post_ids = ','.join([str(row.post_id) for row in query_user.get('user_post_info')]) + role_ids = ','.join([str(row.role_id) for row in query_user.get('user_role_info')]) + roles = [row.role_key for row in query_user.get('user_role_info')] + + query_user['permissions'] = permissions + query_user['post_ids'] = post_ids + query_user['role_ids'] = role_ids + query_user['roles'] = roles + + # 不输出部门信息 + if 'user_menu_info' in query_user: + del query_user['user_menu_info'] + + user_data_info = json.dumps(query_user, cls=ComplexEncoder, ensure_ascii=False, indent=2) + user_data = json.loads(user_data_info) + + if AppConfig.app_enable_user_cache: + await request.app.state.redis.set( + f'{RedisInitKeyConfig.USER_INFO.key}:{user_id}', + user_data_info, + ex=timedelta(minutes=JwtConfig.jwt_redis_expire_minutes), + ) + + return user_data + @classmethod async def get_current_user( cls, request: Request = Request, token: str = Depends(oauth2_scheme), query_db: AsyncSession = Depends(get_db) @@ -217,7 +278,7 @@ async def get_current_user( except InvalidTokenError as e: logger.warning('用户token已失效,请重新登录') raise AuthException(data='', message='用户token已失效,请重新登录') from e - query_user = await UserDao.get_user_by_id(query_db, user_id=token_data.user_id) + query_user = await cls.__get_user_by_id_cache(request, query_db, user_id=token_data.user_id) if query_user.get('user_basic_info') is None: logger.warning('用户token不合法') raise AuthException(data='', message='用户token不合法') @@ -242,28 +303,20 @@ async def get_current_user( ex=timedelta(minutes=JwtConfig.jwt_redis_expire_minutes), ) - role_id_list = [item.role_id for item in query_user.get('user_role_info')] - if 1 in role_id_list: # noqa: SIM108 - permissions = ['*:*:*'] - else: - permissions = [row.perms for row in query_user.get('user_menu_info')] - post_ids = ','.join([str(row.post_id) for row in query_user.get('user_post_info')]) - role_ids = ','.join([str(row.role_id) for row in query_user.get('user_role_info')]) - roles = [row.role_key for row in query_user.get('user_role_info')] is_default_modify_pwd = await cls.__init_password_is_modify( - request, query_user.get('user_basic_info').pwd_update_date + request, query_user.get('user_basic_info').get('pwd_update_date') ) is_password_expired = await cls.__password_is_expired( - request, query_user.get('user_basic_info').pwd_update_date + request, query_user.get('user_basic_info').get('pwd_update_date') ) current_user = CurrentUserModel( - permissions=permissions, - roles=roles, + permissions=query_user.get('permissions'), + roles=query_user.get('roles'), user=UserInfoModel( **CamelCaseUtil.transform_result(query_user.get('user_basic_info')), - postIds=post_ids, - roleIds=role_ids, + postIds=query_user.get('post_ids'), + roleIds=query_user.get('roleIds'), dept=CamelCaseUtil.transform_result(query_user.get('user_dept_info')), role=CamelCaseUtil.transform_result(query_user.get('user_role_info')), ), @@ -504,7 +557,7 @@ async def forget_user_services( if forget_user.sms_code == redis_sms_result: forget_user.password = PwdUtil.get_password_hash(forget_user.password) forget_user.user_id = (await UserDao.get_user_by_name(query_db, forget_user.user_name)).user_id - edit_result = await UserService.reset_user_services(query_db, forget_user) + edit_result = await UserService.reset_user_services(request, query_db, forget_user) result = edit_result.dict() elif not redis_sms_result: result = {'is_success': False, 'message': '短信验证码已过期'} @@ -515,7 +568,7 @@ async def forget_user_services( return CrudResponseModel(**result) @classmethod - async def logout_services(cls, request: Request, token_id: str) -> bool: + async def logout_services(cls, request: Request, token_id: str, user_id: str) -> bool: """ 退出登录services @@ -526,6 +579,7 @@ async def logout_services(cls, request: Request, token_id: str) -> bool: await request.app.state.redis.delete(f'{RedisInitKeyConfig.ACCESS_TOKEN.key}:{token_id}') # await request.app.state.redis.delete(f'{current_user.user.user_id}_access_token') # await request.app.state.redis.delete(f'{current_user.user.user_id}_session_id') + await CacheService.clear_usercache_by_id(request, user_id) return True diff --git a/ruoyi-fastapi-backend/module_admin/service/menu_service.py b/ruoyi-fastapi-backend/module_admin/service/menu_service.py index 734bfe61a..1f992df30 100644 --- a/ruoyi-fastapi-backend/module_admin/service/menu_service.py +++ b/ruoyi-fastapi-backend/module_admin/service/menu_service.py @@ -1,6 +1,7 @@ from collections.abc import Sequence from typing import Any +from fastapi import Request from sqlalchemy.ext.asyncio import AsyncSession from common.constant import CommonConstant, MenuConstant @@ -12,6 +13,7 @@ from module_admin.entity.vo.menu_vo import DeleteMenuModel, MenuModel, MenuQueryModel, MenuTreeModel from module_admin.entity.vo.role_vo import RoleMenuQueryModel from module_admin.entity.vo.user_vo import CurrentUserModel +from module_admin.service.cache_service import CacheService from utils.common_util import CamelCaseUtil from utils.string_util import StringUtil @@ -118,7 +120,9 @@ async def add_menu_services(cls, query_db: AsyncSession, page_object: MenuModel) raise e @classmethod - async def edit_menu_services(cls, query_db: AsyncSession, page_object: MenuModel) -> CrudResponseModel: + async def edit_menu_services( + cls, request: Request, query_db: AsyncSession, page_object: MenuModel + ) -> CrudResponseModel: """ 编辑菜单信息service @@ -138,6 +142,7 @@ async def edit_menu_services(cls, query_db: AsyncSession, page_object: MenuModel try: await MenuDao.edit_menu_dao(query_db, edit_menu) await query_db.commit() + await CacheService.clear_usercache_all(request) return CrudResponseModel(is_success=True, message='更新成功') except Exception as e: await query_db.rollback() @@ -146,7 +151,9 @@ async def edit_menu_services(cls, query_db: AsyncSession, page_object: MenuModel raise ServiceException(message='菜单不存在') @classmethod - async def delete_menu_services(cls, query_db: AsyncSession, page_object: DeleteMenuModel) -> CrudResponseModel: + async def delete_menu_services( + cls, request: Request, query_db: AsyncSession, page_object: DeleteMenuModel + ) -> CrudResponseModel: """ 删除菜单信息service @@ -164,6 +171,7 @@ async def delete_menu_services(cls, query_db: AsyncSession, page_object: DeleteM raise ServiceWarning(message='菜单已分配,不允许删除') await MenuDao.delete_menu_dao(query_db, MenuModel(menuId=menu_id)) await query_db.commit() + await CacheService.clear_usercache_all(request) return CrudResponseModel(is_success=True, message='删除成功') except Exception as e: await query_db.rollback() diff --git a/ruoyi-fastapi-backend/module_admin/service/post_service.py b/ruoyi-fastapi-backend/module_admin/service/post_service.py index 9a11f23c2..01aaba1ac 100644 --- a/ruoyi-fastapi-backend/module_admin/service/post_service.py +++ b/ruoyi-fastapi-backend/module_admin/service/post_service.py @@ -1,5 +1,6 @@ from typing import Any +from fastapi import Request from sqlalchemy.ext.asyncio import AsyncSession from common.constant import CommonConstant @@ -7,6 +8,7 @@ from exceptions.exception import ServiceException from module_admin.dao.post_dao import PostDao from module_admin.entity.vo.post_vo import DeletePostModel, PostModel, PostPageQueryModel +from module_admin.service.cache_service import CacheService from utils.common_util import CamelCaseUtil from utils.excel_util import ExcelUtil @@ -84,7 +86,9 @@ async def add_post_services(cls, query_db: AsyncSession, page_object: PostModel) raise e @classmethod - async def edit_post_services(cls, query_db: AsyncSession, page_object: PostModel) -> CrudResponseModel: + async def edit_post_services( + cls, request: Request, query_db: AsyncSession, page_object: PostModel + ) -> CrudResponseModel: """ 编辑岗位信息service @@ -102,6 +106,7 @@ async def edit_post_services(cls, query_db: AsyncSession, page_object: PostModel try: await PostDao.edit_post_dao(query_db, edit_post) await query_db.commit() + await CacheService.clear_usercache_all(request) return CrudResponseModel(is_success=True, message='更新成功') except Exception as e: await query_db.rollback() @@ -110,7 +115,9 @@ async def edit_post_services(cls, query_db: AsyncSession, page_object: PostModel raise ServiceException(message='岗位不存在') @classmethod - async def delete_post_services(cls, query_db: AsyncSession, page_object: DeletePostModel) -> CrudResponseModel: + async def delete_post_services( + cls, request: Request, query_db: AsyncSession, page_object: DeletePostModel + ) -> CrudResponseModel: """ 删除岗位信息service @@ -127,6 +134,7 @@ async def delete_post_services(cls, query_db: AsyncSession, page_object: DeleteP raise ServiceException(message=f'{post.post_name}已分配,不能删除') await PostDao.delete_post_dao(query_db, PostModel(postId=post_id)) await query_db.commit() + await CacheService.clear_usercache_all(request) return CrudResponseModel(is_success=True, message='删除成功') except Exception as e: await query_db.rollback() diff --git a/ruoyi-fastapi-backend/module_admin/service/role_service.py b/ruoyi-fastapi-backend/module_admin/service/role_service.py index b7cd0a208..ccbd8556e 100644 --- a/ruoyi-fastapi-backend/module_admin/service/role_service.py +++ b/ruoyi-fastapi-backend/module_admin/service/role_service.py @@ -1,5 +1,6 @@ from typing import Any +from fastapi import Request from sqlalchemy import ColumnElement from sqlalchemy.ext.asyncio import AsyncSession @@ -18,6 +19,7 @@ RolePageQueryModel, ) from module_admin.entity.vo.user_vo import UserInfoModel, UserRolePageQueryModel +from module_admin.service.cache_service import CacheService from utils.common_util import CamelCaseUtil from utils.excel_util import ExcelUtil @@ -167,7 +169,9 @@ async def add_role_services(cls, query_db: AsyncSession, page_object: AddRoleMod raise e @classmethod - async def edit_role_services(cls, query_db: AsyncSession, page_object: AddRoleModel) -> CrudResponseModel: + async def edit_role_services( + cls, request: Request, query_db: AsyncSession, page_object: AddRoleModel + ) -> CrudResponseModel: """ 编辑角色信息service @@ -197,6 +201,7 @@ async def edit_role_services(cls, query_db: AsyncSession, page_object: AddRoleMo query_db, RoleMenuModel(roleId=page_object.role_id, menuId=menu) ) await query_db.commit() + await CacheService.clear_usercache_all(request) return CrudResponseModel(is_success=True, message='更新成功') except Exception as e: await query_db.rollback() @@ -205,7 +210,9 @@ async def edit_role_services(cls, query_db: AsyncSession, page_object: AddRoleMo raise ServiceException(message='角色不存在') @classmethod - async def role_datascope_services(cls, query_db: AsyncSession, page_object: AddRoleModel) -> CrudResponseModel: + async def role_datascope_services( + cls, request: Request, query_db: AsyncSession, page_object: AddRoleModel + ) -> CrudResponseModel: """ 分配角色数据权限service @@ -225,6 +232,7 @@ async def role_datascope_services(cls, query_db: AsyncSession, page_object: AddR query_db, RoleDeptModel(roleId=page_object.role_id, deptId=dept) ) await query_db.commit() + await CacheService.clear_usercache_all(request) return CrudResponseModel(is_success=True, message='分配成功') except Exception as e: await query_db.rollback() @@ -233,7 +241,9 @@ async def role_datascope_services(cls, query_db: AsyncSession, page_object: AddR raise ServiceException(message='角色不存在') @classmethod - async def delete_role_services(cls, query_db: AsyncSession, page_object: DeleteRoleModel) -> CrudResponseModel: + async def delete_role_services( + cls, request: Request, query_db: AsyncSession, page_object: DeleteRoleModel + ) -> CrudResponseModel: """ 删除角色信息service @@ -257,6 +267,7 @@ async def delete_role_services(cls, query_db: AsyncSession, page_object: DeleteR await RoleDao.delete_role_dept_dao(query_db, RoleDeptModel(**role_id_dict)) await RoleDao.delete_role_dao(query_db, RoleModel(**role_id_dict)) await query_db.commit() + await CacheService.clear_usercache_all(request) return CrudResponseModel(is_success=True, message='删除成功') except Exception as e: await query_db.rollback() diff --git a/ruoyi-fastapi-backend/module_admin/service/user_service.py b/ruoyi-fastapi-backend/module_admin/service/user_service.py index 5d35780c3..91462dcdd 100644 --- a/ruoyi-fastapi-backend/module_admin/service/user_service.py +++ b/ruoyi-fastapi-backend/module_admin/service/user_service.py @@ -32,6 +32,7 @@ UserRoleResponseModel, UserRowModel, ) +from module_admin.service.cache_service import CacheService from module_admin.service.config_service import ConfigService from module_admin.service.dept_service import DeptService from module_admin.service.post_service import PostService @@ -200,7 +201,9 @@ def _deal_edit_user(cls, page_object: EditUserModel, edit_user: dict[str, Any]) del edit_user['type'] @classmethod - async def edit_user_services(cls, query_db: AsyncSession, page_object: EditUserModel) -> CrudResponseModel: + async def edit_user_services( + cls, request: Request, query_db: AsyncSession, page_object: EditUserModel + ) -> CrudResponseModel: """ 编辑用户信息service @@ -235,6 +238,7 @@ async def edit_user_services(cls, query_db: AsyncSession, page_object: EditUserM query_db, UserPostModel(userId=page_object.user_id, postId=post) ) await query_db.commit() + await CacheService.clear_usercache_by_id(request, page_object.user_id) return CrudResponseModel(is_success=True, message='更新成功') except Exception as e: await query_db.rollback() @@ -243,7 +247,9 @@ async def edit_user_services(cls, query_db: AsyncSession, page_object: EditUserM raise ServiceException(message='用户不存在') @classmethod - async def delete_user_services(cls, query_db: AsyncSession, page_object: DeleteUserModel) -> CrudResponseModel: + async def delete_user_services( + cls, request: Request, query_db: AsyncSession, page_object: DeleteUserModel + ) -> CrudResponseModel: """ 删除用户信息service @@ -263,6 +269,7 @@ async def delete_user_services(cls, query_db: AsyncSession, page_object: DeleteU await UserDao.delete_user_role_dao(query_db, UserRoleModel(**user_id_dict)) await UserDao.delete_user_post_dao(query_db, UserPostModel(**user_id_dict)) await UserDao.delete_user_dao(query_db, UserModel(**user_id_dict)) + await CacheService.clear_usercache_by_id(request, user_id) await query_db.commit() return CrudResponseModel(is_success=True, message='删除成功') except Exception as e: @@ -333,7 +340,9 @@ async def user_profile_services(cls, query_db: AsyncSession, user_id: int) -> Us ) @classmethod - async def reset_user_services(cls, query_db: AsyncSession, page_object: ResetUserModel) -> CrudResponseModel: + async def reset_user_services( + cls, request: Request, query_db: AsyncSession, page_object: ResetUserModel + ) -> CrudResponseModel: """ 重置用户密码service @@ -356,6 +365,7 @@ async def reset_user_services(cls, query_db: AsyncSession, page_object: ResetUse reset_user['password'] = PwdUtil.get_password_hash(page_object.password) await UserDao.edit_user_dao(query_db, reset_user) await query_db.commit() + await CacheService.clear_usercache_by_id(request, page_object.user_id) return CrudResponseModel(is_success=True, message='重置成功') except Exception as e: await query_db.rollback() @@ -581,7 +591,9 @@ async def get_user_role_allocated_list_services( return result @classmethod - async def add_user_role_services(cls, query_db: AsyncSession, page_object: CrudUserRoleModel) -> CrudResponseModel: + async def add_user_role_services( + cls, request: Request, query_db: AsyncSession, page_object: CrudUserRoleModel + ) -> CrudResponseModel: """ 新增用户关联角色信息service @@ -596,6 +608,7 @@ async def add_user_role_services(cls, query_db: AsyncSession, page_object: CrudU for role_id in role_id_list: await UserDao.add_user_role_dao(query_db, UserRoleModel(userId=page_object.user_id, roleId=role_id)) await query_db.commit() + await CacheService.clear_usercache_by_id(request, page_object.user_id) return CrudResponseModel(is_success=True, message='分配成功') except Exception as e: await query_db.rollback() @@ -604,6 +617,7 @@ async def add_user_role_services(cls, query_db: AsyncSession, page_object: CrudU try: await UserDao.delete_user_role_by_user_and_role_dao(query_db, UserRoleModel(userId=page_object.user_id)) await query_db.commit() + await CacheService.clear_usercache_by_id(request, page_object.user_id) return CrudResponseModel(is_success=True, message='分配成功') except Exception as e: await query_db.rollback() @@ -618,6 +632,7 @@ async def add_user_role_services(cls, query_db: AsyncSession, page_object: CrudU if user_role: continue await UserDao.add_user_role_dao(query_db, UserRoleModel(userId=user_id, roleId=page_object.role_id)) + await CacheService.clear_usercache_by_id(request, user_id) await query_db.commit() return CrudResponseModel(is_success=True, message='新增成功') except Exception as e: @@ -628,7 +643,7 @@ async def add_user_role_services(cls, query_db: AsyncSession, page_object: CrudU @classmethod async def delete_user_role_services( - cls, query_db: AsyncSession, page_object: CrudUserRoleModel + cls, request: Request, query_db: AsyncSession, page_object: CrudUserRoleModel ) -> CrudResponseModel: """ 删除用户关联角色信息service @@ -644,6 +659,7 @@ async def delete_user_role_services( query_db, UserRoleModel(userId=page_object.user_id, roleId=page_object.role_id) ) await query_db.commit() + await CacheService.clear_usercache_by_id(request, page_object.user_id) return CrudResponseModel(is_success=True, message='删除成功') except Exception as e: await query_db.rollback() @@ -655,6 +671,7 @@ async def delete_user_role_services( await UserDao.delete_user_role_by_user_and_role_dao( query_db, UserRoleModel(userId=user_id, roleId=page_object.role_id) ) + await CacheService.clear_usercache_by_id(request, user_id) await query_db.commit() return CrudResponseModel(is_success=True, message='删除成功') except Exception as e: diff --git a/ruoyi-fastapi-backend/utils/json_util.py b/ruoyi-fastapi-backend/utils/json_util.py new file mode 100644 index 000000000..a4672187e --- /dev/null +++ b/ruoyi-fastapi-backend/utils/json_util.py @@ -0,0 +1,32 @@ +import json +from datetime import datetime +from typing import Any + + +# 1. 自定义 JSONEncoder +class ComplexEncoder(json.JSONEncoder): + def default(self, obj: Any) -> Any: + # 处理 datetime 对象 + if isinstance(obj, datetime): + return obj.isoformat() + + # 处理自定义类对象 + if hasattr(obj, '__dict__'): + # 获取对象的字典表示,递归处理嵌套对象 + result = {} + for key, value in obj.__dict__.items(): + # 过滤掉以 _ 开头的私有属性 + if not key.startswith('_'): + # 递归调用 default 处理嵌套对象 + if isinstance(value, (datetime,)): + result[key] = self.default(value) + else: + result[key] = value + return result + + # 处理 dataclass + if hasattr(obj, '__dataclass_fields__'): + return {field: self.default(getattr(obj, field)) for field in obj.__dataclass_fields__} + + # 让基类处理其他类型 + return super().default(obj)