mirror of
https://github.com/fastapi-practices/fastapi-best-architecture.git
synced 2026-09-21 21:15:13 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eeb709c6aa | ||
|
|
a6bbf2971d | ||
|
|
cd48bb4210 | ||
|
|
4500dd0128 | ||
|
|
2b6d8222ad | ||
|
|
1b47ab7e83 | ||
|
|
bd804e0a38 | ||
|
|
8e8af2032a | ||
|
|
e09062eb39 | ||
|
|
8c00492e44 | ||
|
|
0237d4c7b1 | ||
|
|
fe3a3b4e86 | ||
|
|
65ec721a1c | ||
|
|
4eb76ad6ea | ||
|
|
1f8687155a | ||
|
|
8591d4e592 | ||
|
|
dedf4e7bae | ||
|
|
1e4aa88487 | ||
|
|
0dd745b7a7 | ||
|
|
6b2402f212 | ||
|
|
24a487eeea | ||
|
|
4f574189c7 | ||
|
|
b559a74cea | ||
|
|
83dcdbe59d | ||
|
|
d64f7c2911 | ||
|
|
53e64bce37 | ||
|
|
00a781357b | ||
|
|
d7f87ed0ad | ||
|
|
bda9b1d463 | ||
|
|
e0a106ec51 | ||
|
|
016361bd68 |
@@ -1,3 +1,38 @@
|
|||||||
|
<a id="v1.7.0"></a>
|
||||||
|
# [v1.7.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.7.0) - 2025-07-16
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update the changelog for v1.6.0 by [@wu-clan](https://github.com/wu-clan) in [#703](https://github.com/fastapi-practices/fastapi_best_architecture/pull/703)
|
||||||
|
* Update the CLI to be executed async by [@wu-clan](https://github.com/wu-clan) in [#704](https://github.com/fastapi-practices/fastapi_best_architecture/pull/704)
|
||||||
|
* Fix the code generation schema template by [@wu-clan](https://github.com/wu-clan) in [#706](https://github.com/fastapi-practices/fastapi_best_architecture/pull/706)
|
||||||
|
* Replace gunicorn deployment to granian by [@wu-clan](https://github.com/wu-clan) in [#705](https://github.com/fastapi-practices/fastapi_best_architecture/pull/705)
|
||||||
|
* Fix the code generation delete schema template by [@wu-clan](https://github.com/wu-clan) in [#708](https://github.com/fastapi-practices/fastapi_best_architecture/pull/708)
|
||||||
|
* Update the refresh token verify mechanism by [@wu-clan](https://github.com/wu-clan) in [#710](https://github.com/fastapi-practices/fastapi_best_architecture/pull/710)
|
||||||
|
* Update the reload excludes for CLI run by [@wu-clan](https://github.com/wu-clan) in [#709](https://github.com/fastapi-practices/fastapi_best_architecture/pull/709)
|
||||||
|
* Add CLI support for execute sql scripts by [@wu-clan](https://github.com/wu-clan) in [#711](https://github.com/fastapi-practices/fastapi_best_architecture/pull/711)
|
||||||
|
* Update the granian env to command params by [@wu-clan](https://github.com/wu-clan) in [#712](https://github.com/fastapi-practices/fastapi_best_architecture/pull/712)
|
||||||
|
* Update the middleware logging accuracy by [@wu-clan](https://github.com/wu-clan) in [#713](https://github.com/fastapi-practices/fastapi_best_architecture/pull/713)
|
||||||
|
* Update the log output default style by [@wu-clan](https://github.com/wu-clan) in [#714](https://github.com/fastapi-practices/fastapi_best_architecture/pull/714)
|
||||||
|
* Optimize the analysis of get plugins by [@wu-clan](https://github.com/wu-clan) in [#716](https://github.com/fastapi-practices/fastapi_best_architecture/pull/716)
|
||||||
|
* Simplify user permission database queries by [@wu-clan](https://github.com/wu-clan) in [#717](https://github.com/fastapi-practices/fastapi_best_architecture/pull/717)
|
||||||
|
* Update the CLI startup service mode by [@wu-clan](https://github.com/wu-clan) in [#718](https://github.com/fastapi-practices/fastapi_best_architecture/pull/718)
|
||||||
|
* Add support for celery dynamic tasks by [@wu-clan](https://github.com/wu-clan) in [#715](https://github.com/fastapi-practices/fastapi_best_architecture/pull/715)
|
||||||
|
* Fix the celery task scheduler query by [@wu-clan](https://github.com/wu-clan) in [#719](https://github.com/fastapi-practices/fastapi_best_architecture/pull/719)
|
||||||
|
* Update the celery task comment and name by [@wu-clan](https://github.com/wu-clan) in [#720](https://github.com/fastapi-practices/fastapi_best_architecture/pull/720)
|
||||||
|
* Optimize celery integrations and events by [@wu-clan](https://github.com/wu-clan) in [#721](https://github.com/fastapi-practices/fastapi_best_architecture/pull/721)
|
||||||
|
* Simplify celery task crontab config by [@wu-clan](https://github.com/wu-clan) in [#722](https://github.com/fastapi-practices/fastapi_best_architecture/pull/722)
|
||||||
|
* Delete the default value of schema enum data by [@wu-clan](https://github.com/wu-clan) in [#723](https://github.com/fastapi-practices/fastapi_best_architecture/pull/723)
|
||||||
|
* Fix the parsing of execution task params by [@wu-clan](https://github.com/wu-clan) in [#725](https://github.com/fastapi-practices/fastapi_best_architecture/pull/725)
|
||||||
|
* Bump granian from 2.4.0 to 2.4.2 by [@wu-clan](https://github.com/wu-clan) in [#727](https://github.com/fastapi-practices/fastapi_best_architecture/pull/727)
|
||||||
|
* Add CLI support for startup celery services by [@wu-clan](https://github.com/wu-clan) in [#724](https://github.com/fastapi-practices/fastapi_best_architecture/pull/724)
|
||||||
|
* Fix login and operation log clearing by [@wu-clan](https://github.com/wu-clan) in [#728](https://github.com/fastapi-practices/fastapi_best_architecture/pull/728)
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.6.0...v1.7.0
|
||||||
|
|
||||||
|
[Changes][v1.7.0]
|
||||||
|
|
||||||
|
|
||||||
<a id="v1.6.0"></a>
|
<a id="v1.6.0"></a>
|
||||||
# [v1.6.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.6.0) - 2025-06-30
|
# [v1.6.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.6.0) - 2025-06-30
|
||||||
|
|
||||||
@@ -679,6 +714,7 @@
|
|||||||
[Changes][v1.0.0]
|
[Changes][v1.0.0]
|
||||||
|
|
||||||
|
|
||||||
|
[v1.7.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.6.0...v1.7.0
|
||||||
[v1.6.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.5.2...v1.6.0
|
[v1.6.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.5.2...v1.6.0
|
||||||
[v1.5.2]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.5.1...v1.5.2
|
[v1.5.2]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.5.1...v1.5.2
|
||||||
[v1.5.1]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.5.0...v1.5.1
|
[v1.5.1]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.5.0...v1.5.1
|
||||||
|
|||||||
@@ -15,13 +15,7 @@ REDIS_DATABASE=0
|
|||||||
TOKEN_SECRET_KEY='1VkVF75nsNABBjK_7-qz7GtzNy3AMvktc9TCPwKczCk'
|
TOKEN_SECRET_KEY='1VkVF75nsNABBjK_7-qz7GtzNy3AMvktc9TCPwKczCk'
|
||||||
# Opera Log
|
# Opera Log
|
||||||
OPERA_LOG_ENCRYPT_SECRET_KEY='d77b25790a804c2b4a339dd0207941e4cefa5751935a33735bc73bb7071a005b'
|
OPERA_LOG_ENCRYPT_SECRET_KEY='d77b25790a804c2b4a339dd0207941e4cefa5751935a33735bc73bb7071a005b'
|
||||||
# App Admin
|
# [ App ] task
|
||||||
# OAuth2
|
|
||||||
OAUTH2_GITHUB_CLIENT_ID='test'
|
|
||||||
OAUTH2_GITHUB_CLIENT_SECRET='test'
|
|
||||||
OAUTH2_LINUX_DO_CLIENT_ID='test'
|
|
||||||
OAUTH2_LINUX_DO_CLIENT_SECRET='test'
|
|
||||||
# App Task
|
|
||||||
# Celery
|
# Celery
|
||||||
CELERY_BROKER_REDIS_DATABASE=1
|
CELERY_BROKER_REDIS_DATABASE=1
|
||||||
# Rabbitmq
|
# Rabbitmq
|
||||||
@@ -29,3 +23,11 @@ CELERY_RABBITMQ_HOST='127.0.0.1'
|
|||||||
CELERY_RABBITMQ_PORT=5672
|
CELERY_RABBITMQ_PORT=5672
|
||||||
CELERY_RABBITMQ_USERNAME='guest'
|
CELERY_RABBITMQ_USERNAME='guest'
|
||||||
CELERY_RABBITMQ_PASSWORD='guest'
|
CELERY_RABBITMQ_PASSWORD='guest'
|
||||||
|
# [ Plugin ] oauth2
|
||||||
|
OAUTH2_GITHUB_CLIENT_ID='test'
|
||||||
|
OAUTH2_GITHUB_CLIENT_SECRET='test'
|
||||||
|
OAUTH2_LINUX_DO_CLIENT_ID='test'
|
||||||
|
OAUTH2_LINUX_DO_CLIENT_SECRET='test'
|
||||||
|
# [ Plugin ] email
|
||||||
|
EMAIL_USERNAME=''
|
||||||
|
EMAIL_PASSWORD=''
|
||||||
|
|||||||
+6
-1
@@ -1,9 +1,14 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from backend.common.i18n import i18n
|
||||||
from backend.utils.console import console
|
from backend.utils.console import console
|
||||||
|
|
||||||
__version__ = '1.7.0'
|
__version__ = '1.8.0'
|
||||||
|
|
||||||
|
|
||||||
def get_version() -> str | None:
|
def get_version() -> str | None:
|
||||||
console.print(f'[cyan]{__version__}[/]')
|
console.print(f'[cyan]{__version__}[/]')
|
||||||
|
|
||||||
|
|
||||||
|
# 初始化 i18n
|
||||||
|
i18n.load_locales()
|
||||||
|
|||||||
@@ -25,9 +25,11 @@ def get_app_models():
|
|||||||
try:
|
try:
|
||||||
module_path = f'backend.app.{app}.model'
|
module_path = f'backend.app.{app}.model'
|
||||||
module = import_module_cached(module_path)
|
module = import_module_cached(module_path)
|
||||||
except Exception as e:
|
except ModuleNotFoundError as e:
|
||||||
log.warning(f'应用 {app} 中不包含 model 相关配置: {e}')
|
log.warning(f'应用 {app} 中不包含 model 相关配置: {e}')
|
||||||
continue
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
for name, obj in inspect.getmembers(module):
|
for name, obj in inspect.getmembers(module):
|
||||||
if inspect.isclass(obj):
|
if inspect.isclass(obj):
|
||||||
|
|||||||
@@ -1,23 +1,15 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from backend.common.response.response_schema import ResponseModel, response_base
|
from backend.common.response.response_schema import ResponseModel, response_base
|
||||||
from backend.common.security.jwt import DependsJwtAuth
|
from backend.common.security.jwt import DependsJwtAuth
|
||||||
from backend.common.security.permission import RequestPermission
|
|
||||||
from backend.utils.redis_info import redis_info
|
from backend.utils.redis_info import redis_info
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get('', summary='redis 监控', dependencies=[DependsJwtAuth])
|
||||||
'',
|
|
||||||
summary='redis 监控',
|
|
||||||
dependencies=[
|
|
||||||
Depends(RequestPermission('sys:monitor:redis')),
|
|
||||||
DependsJwtAuth,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def get_redis_info() -> ResponseModel:
|
async def get_redis_info() -> ResponseModel:
|
||||||
data = {
|
data = {
|
||||||
'info': await redis_info.get_info(),
|
'info': await redis_info.get_info(),
|
||||||
|
|||||||
@@ -1,24 +1,16 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter
|
||||||
from starlette.concurrency import run_in_threadpool
|
from starlette.concurrency import run_in_threadpool
|
||||||
|
|
||||||
from backend.common.response.response_schema import ResponseModel, response_base
|
from backend.common.response.response_schema import ResponseModel, response_base
|
||||||
from backend.common.security.jwt import DependsJwtAuth
|
from backend.common.security.jwt import DependsJwtAuth
|
||||||
from backend.common.security.permission import RequestPermission
|
|
||||||
from backend.utils.server_info import server_info
|
from backend.utils.server_info import server_info
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get('', summary='server 监控', dependencies=[DependsJwtAuth])
|
||||||
'',
|
|
||||||
summary='server 监控',
|
|
||||||
dependencies=[
|
|
||||||
Depends(RequestPermission('sys:monitor:server')),
|
|
||||||
DependsJwtAuth,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def get_server_info() -> ResponseModel:
|
async def get_server_info() -> ResponseModel:
|
||||||
data = {
|
data = {
|
||||||
# 扔到线程池,避免阻塞
|
# 扔到线程池,避免阻塞
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Path, Query, Request
|
from fastapi import APIRouter, Body, Depends, Path, Query, Request
|
||||||
|
|
||||||
from backend.app.admin.schema.role import GetRoleDetail
|
from backend.app.admin.schema.role import GetRoleDetail
|
||||||
from backend.app.admin.schema.user import (
|
from backend.app.admin.schema.user import (
|
||||||
@@ -71,7 +71,7 @@ async def create_user(request: Request, obj: AddUserParam) -> ResponseSchemaMode
|
|||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.put('/{pk}', summary='更新用户信息', dependencies=[DependsJwtAuth])
|
@router.put('/{pk}', summary='更新用户信息', dependencies=[DependsRBAC])
|
||||||
async def update_user(
|
async def update_user(
|
||||||
request: Request, pk: Annotated[int, Path(description='用户 ID')], obj: UpdateUserParam
|
request: Request, pk: Annotated[int, Path(description='用户 ID')], obj: UpdateUserParam
|
||||||
) -> ResponseModel:
|
) -> ResponseModel:
|
||||||
@@ -93,11 +93,53 @@ async def update_user_permission(
|
|||||||
return response_base.fail()
|
return response_base.fail()
|
||||||
|
|
||||||
|
|
||||||
@router.put('/{pk}/password', summary='重置用户密码', dependencies=[DependsJwtAuth])
|
@router.put('/me/password', summary='更新当前用户密码', dependencies=[DependsJwtAuth])
|
||||||
|
async def update_user_password(request: Request, obj: ResetPasswordParam) -> ResponseModel:
|
||||||
|
count = await user_service.update_password(request=request, obj=obj)
|
||||||
|
if count > 0:
|
||||||
|
return response_base.success()
|
||||||
|
return response_base.fail()
|
||||||
|
|
||||||
|
|
||||||
|
@router.put('/{pk}/password', summary='重置用户密码', dependencies=[DependsRBAC])
|
||||||
async def reset_user_password(
|
async def reset_user_password(
|
||||||
pk: Annotated[int, Path(description='用户 ID')], obj: ResetPasswordParam
|
request: Request,
|
||||||
|
pk: Annotated[int, Path(description='用户 ID')],
|
||||||
|
password: Annotated[str, Body(embed=True, description='新密码')],
|
||||||
) -> ResponseModel:
|
) -> ResponseModel:
|
||||||
count = await user_service.reset_pwd(pk=pk, obj=obj)
|
count = await user_service.reset_password(request=request, pk=pk, password=password)
|
||||||
|
if count > 0:
|
||||||
|
return response_base.success()
|
||||||
|
return response_base.fail()
|
||||||
|
|
||||||
|
|
||||||
|
@router.put('/me/nickname', summary='更新当前用户昵称', dependencies=[DependsJwtAuth])
|
||||||
|
async def update_user_nickname(
|
||||||
|
request: Request, nickname: Annotated[str, Body(embed=True, description='用户昵称')]
|
||||||
|
) -> ResponseModel:
|
||||||
|
count = await user_service.update_nickname(request=request, nickname=nickname)
|
||||||
|
if count > 0:
|
||||||
|
return response_base.success()
|
||||||
|
return response_base.fail()
|
||||||
|
|
||||||
|
|
||||||
|
@router.put('/me/avatar', summary='更新当前用户头像', dependencies=[DependsJwtAuth])
|
||||||
|
async def update_user_avatar(
|
||||||
|
request: Request, avatar: Annotated[str, Body(embed=True, description='用户头像地址')]
|
||||||
|
) -> ResponseModel:
|
||||||
|
count = await user_service.update_avatar(request=request, avatar=avatar)
|
||||||
|
if count > 0:
|
||||||
|
return response_base.success()
|
||||||
|
return response_base.fail()
|
||||||
|
|
||||||
|
|
||||||
|
@router.put('/me/email', summary='更新当前用户邮箱', dependencies=[DependsJwtAuth])
|
||||||
|
async def update_user_email(
|
||||||
|
request: Request,
|
||||||
|
captcha: Annotated[str, Body(embed=True, description='邮箱验证码')],
|
||||||
|
email: Annotated[str, Body(embed=True, description='用户邮箱')],
|
||||||
|
) -> ResponseModel:
|
||||||
|
count = await user_service.update_email(request=request, captcha=captcha, email=email)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
return response_base.fail()
|
return response_base.fail()
|
||||||
|
|||||||
@@ -37,11 +37,21 @@ class CRUDOperaLogDao(CRUDPlus[OperaLog]):
|
|||||||
创建操作日志
|
创建操作日志
|
||||||
|
|
||||||
:param db: 数据库会话
|
:param db: 数据库会话
|
||||||
:param obj: 创建操作日志参数
|
:param obj: 操作日志创建参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
await self.create_model(db, obj)
|
await self.create_model(db, obj)
|
||||||
|
|
||||||
|
async def bulk_create(self, db: AsyncSession, objs: list[CreateOperaLogParam]) -> None:
|
||||||
|
"""
|
||||||
|
批量创建操作日志
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param objs: 操作日志创建参数列表
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
await self.create_models(db, objs)
|
||||||
|
|
||||||
async def delete(self, db: AsyncSession, pks: list[int]) -> int:
|
async def delete(self, db: AsyncSession, pks: list[int]) -> int:
|
||||||
"""
|
"""
|
||||||
批量删除操作日志
|
批量删除操作日志
|
||||||
|
|||||||
@@ -89,10 +89,8 @@ class CRUDUser(CRUDPlus[User]):
|
|||||||
:param obj: 注册用户参数
|
:param obj: 注册用户参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
salt = bcrypt.gensalt()
|
|
||||||
obj.password = get_hash_password(obj.password, salt)
|
|
||||||
dict_obj = obj.model_dump()
|
dict_obj = obj.model_dump()
|
||||||
dict_obj.update({'is_staff': True, 'salt': salt})
|
dict_obj.update({'is_staff': True, 'salt': None})
|
||||||
new_user = self.model(**dict_obj)
|
new_user = self.model(**dict_obj)
|
||||||
|
|
||||||
stmt = select(Role)
|
stmt = select(Role)
|
||||||
@@ -119,6 +117,17 @@ class CRUDUser(CRUDPlus[User]):
|
|||||||
input_user.roles = roles.scalars().all()
|
input_user.roles = roles.scalars().all()
|
||||||
return count
|
return count
|
||||||
|
|
||||||
|
async def update_nickname(self, db: AsyncSession, user_id: int, nickname: str) -> int:
|
||||||
|
"""
|
||||||
|
更新用户昵称
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param user_id: 用户 ID
|
||||||
|
:param nickname: 用户昵称
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return await self.update_model(db, user_id, {'nickname': nickname})
|
||||||
|
|
||||||
async def update_avatar(self, db: AsyncSession, user_id: int, avatar: str) -> int:
|
async def update_avatar(self, db: AsyncSession, user_id: int, avatar: str) -> int:
|
||||||
"""
|
"""
|
||||||
更新用户头像
|
更新用户头像
|
||||||
@@ -130,6 +139,17 @@ class CRUDUser(CRUDPlus[User]):
|
|||||||
"""
|
"""
|
||||||
return await self.update_model(db, user_id, {'avatar': avatar})
|
return await self.update_model(db, user_id, {'avatar': avatar})
|
||||||
|
|
||||||
|
async def update_email(self, db: AsyncSession, user_id: int, email: str) -> int:
|
||||||
|
"""
|
||||||
|
更新用户邮箱
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param user_id: 用户 ID
|
||||||
|
:param email: 邮箱
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return await self.update_model(db, user_id, {'email': email})
|
||||||
|
|
||||||
async def delete(self, db: AsyncSession, user_id: int) -> int:
|
async def delete(self, db: AsyncSession, user_id: int) -> int:
|
||||||
"""
|
"""
|
||||||
删除用户
|
删除用户
|
||||||
@@ -150,16 +170,18 @@ class CRUDUser(CRUDPlus[User]):
|
|||||||
"""
|
"""
|
||||||
return await self.select_model_by_column(db, email=email)
|
return await self.select_model_by_column(db, email=email)
|
||||||
|
|
||||||
async def reset_password(self, db: AsyncSession, pk: int, new_pwd: str) -> int:
|
async def reset_password(self, db: AsyncSession, pk: int, password: str) -> int:
|
||||||
"""
|
"""
|
||||||
重置用户密码
|
重置用户密码
|
||||||
|
|
||||||
:param db: 数据库会话
|
:param db: 数据库会话
|
||||||
:param pk: 用户 ID
|
:param pk: 用户 ID
|
||||||
:param new_pwd: 新密码(已加密)
|
:param password: 新密码
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.update_model(db, pk, {'password': new_pwd})
|
salt = bcrypt.gensalt()
|
||||||
|
new_pwd = get_hash_password(password, salt)
|
||||||
|
return await self.update_model(db, pk, {'password': new_pwd, 'salt': salt})
|
||||||
|
|
||||||
async def get_list(self, dept: int | None, username: str | None, phone: str | None, status: int | None) -> Select:
|
async def get_list(self, dept: int | None, username: str | None, phone: str | None, status: int | None) -> Select:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ class User(Base):
|
|||||||
uuid: Mapped[str] = mapped_column(String(50), init=False, default_factory=uuid4_str, unique=True)
|
uuid: Mapped[str] = mapped_column(String(50), init=False, default_factory=uuid4_str, unique=True)
|
||||||
username: Mapped[str] = mapped_column(String(20), unique=True, index=True, comment='用户名')
|
username: Mapped[str] = mapped_column(String(20), unique=True, index=True, comment='用户名')
|
||||||
nickname: Mapped[str] = mapped_column(String(20), comment='昵称')
|
nickname: Mapped[str] = mapped_column(String(20), comment='昵称')
|
||||||
password: Mapped[str] = mapped_column(String(255), comment='密码')
|
password: Mapped[str | None] = mapped_column(String(255), comment='密码')
|
||||||
salt: Mapped[bytes] = mapped_column(VARBINARY(255).with_variant(BYTEA(255), 'postgresql'), comment='加密盐')
|
salt: Mapped[bytes | None] = mapped_column(VARBINARY(255).with_variant(BYTEA(255), 'postgresql'), comment='加密盐')
|
||||||
email: Mapped[str | None] = mapped_column(String(50), default=None, unique=True, index=True, comment='邮箱')
|
email: Mapped[str | None] = mapped_column(String(50), default=None, unique=True, index=True, comment='邮箱')
|
||||||
phone: Mapped[str | None] = mapped_column(String(11), default=None, comment='手机号')
|
phone: Mapped[str | None] = mapped_column(String(11), default=None, comment='手机号')
|
||||||
avatar: Mapped[str | None] = mapped_column(String(255), default=None, comment='头像')
|
avatar: Mapped[str | None] = mapped_column(String(255), default=None, comment='头像')
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from pydantic import ConfigDict, EmailStr, Field, HttpUrl, model_validator
|
from pydantic import ConfigDict, Field, HttpUrl, model_validator
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
|
|
||||||
from backend.app.admin.schema.dept import GetDeptDetail
|
from backend.app.admin.schema.dept import GetDeptDetail
|
||||||
@@ -16,7 +16,7 @@ class AuthSchemaBase(SchemaBase):
|
|||||||
"""用户认证基础模型"""
|
"""用户认证基础模型"""
|
||||||
|
|
||||||
username: str = Field(description='用户名')
|
username: str = Field(description='用户名')
|
||||||
password: str | None = Field(description='密码')
|
password: str = Field(description='密码')
|
||||||
|
|
||||||
|
|
||||||
class AuthLoginParam(AuthSchemaBase):
|
class AuthLoginParam(AuthSchemaBase):
|
||||||
@@ -28,16 +28,19 @@ class AuthLoginParam(AuthSchemaBase):
|
|||||||
class AddUserParam(AuthSchemaBase):
|
class AddUserParam(AuthSchemaBase):
|
||||||
"""添加用户参数"""
|
"""添加用户参数"""
|
||||||
|
|
||||||
|
nickname: str | None = Field(None, description='昵称')
|
||||||
|
email: CustomEmailStr | None = Field(None, description='邮箱')
|
||||||
|
phone: CustomPhoneNumber | None = Field(None, description='手机号码')
|
||||||
dept_id: int = Field(description='部门 ID')
|
dept_id: int = Field(description='部门 ID')
|
||||||
roles: list[int] = Field(description='角色 ID 列表')
|
roles: list[int] = Field(description='角色 ID 列表')
|
||||||
nickname: str | None = Field(None, description='昵称')
|
|
||||||
|
|
||||||
|
|
||||||
class AddOAuth2UserParam(AuthSchemaBase):
|
class AddOAuth2UserParam(AuthSchemaBase):
|
||||||
"""添加 OAuth2 用户参数"""
|
"""添加 OAuth2 用户参数"""
|
||||||
|
|
||||||
|
password: str | None = Field(None, description='密码')
|
||||||
nickname: str | None = Field(None, description='昵称')
|
nickname: str | None = Field(None, description='昵称')
|
||||||
email: EmailStr = Field(description='邮箱')
|
email: CustomEmailStr | None = Field(None, description='邮箱')
|
||||||
avatar: HttpUrl | None = Field(None, description='头像地址')
|
avatar: HttpUrl | None = Field(None, description='头像地址')
|
||||||
|
|
||||||
|
|
||||||
@@ -56,6 +59,8 @@ class UserInfoSchemaBase(SchemaBase):
|
|||||||
username: str = Field(description='用户名')
|
username: str = Field(description='用户名')
|
||||||
nickname: str = Field(description='昵称')
|
nickname: str = Field(description='昵称')
|
||||||
avatar: HttpUrl | None = Field(None, description='头像地址')
|
avatar: HttpUrl | None = Field(None, description='头像地址')
|
||||||
|
email: CustomEmailStr | None = Field(None, description='邮箱')
|
||||||
|
phone: CustomPhoneNumber | None = Field(None, description='手机号')
|
||||||
|
|
||||||
|
|
||||||
class UpdateUserParam(UserInfoSchemaBase):
|
class UpdateUserParam(UserInfoSchemaBase):
|
||||||
@@ -72,8 +77,6 @@ class GetUserInfoDetail(UserInfoSchemaBase):
|
|||||||
dept_id: int | None = Field(None, description='部门 ID')
|
dept_id: int | None = Field(None, description='部门 ID')
|
||||||
id: int = Field(description='用户 ID')
|
id: int = Field(description='用户 ID')
|
||||||
uuid: str = Field(description='用户 UUID')
|
uuid: str = Field(description='用户 UUID')
|
||||||
email: CustomEmailStr | None = Field(None, description='邮箱')
|
|
||||||
phone: CustomPhoneNumber | None = Field(None, description='手机号')
|
|
||||||
status: StatusType = Field(description='状态')
|
status: StatusType = Field(description='状态')
|
||||||
is_superuser: bool = Field(description='是否超级管理员')
|
is_superuser: bool = Field(description='是否超级管理员')
|
||||||
is_staff: bool = Field(description='是否管理员')
|
is_staff: bool = Field(description='是否管理员')
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from backend.app.admin.schema.user import AuthLoginParam
|
|||||||
from backend.app.admin.service.login_log_service import login_log_service
|
from backend.app.admin.service.login_log_service import login_log_service
|
||||||
from backend.common.enums import LoginLogStatusType
|
from backend.common.enums import LoginLogStatusType
|
||||||
from backend.common.exception import errors
|
from backend.common.exception import errors
|
||||||
|
from backend.common.i18n import t
|
||||||
from backend.common.log import log
|
from backend.common.log import log
|
||||||
from backend.common.response.response_code import CustomErrorCode
|
from backend.common.response.response_code import CustomErrorCode
|
||||||
from backend.common.security.jwt import (
|
from backend.common.security.jwt import (
|
||||||
@@ -33,7 +34,7 @@ class AuthService:
|
|||||||
"""认证服务类"""
|
"""认证服务类"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def user_verify(db: AsyncSession, username: str, password: str | None) -> User:
|
async def user_verify(db: AsyncSession, username: str, password: str) -> User:
|
||||||
"""
|
"""
|
||||||
验证用户名和密码
|
验证用户名和密码
|
||||||
|
|
||||||
@@ -93,7 +94,7 @@ class AuthService:
|
|||||||
user = await self.user_verify(db, obj.username, obj.password)
|
user = await self.user_verify(db, obj.username, obj.password)
|
||||||
captcha_code = await redis_client.get(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
|
captcha_code = await redis_client.get(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
|
||||||
if not captcha_code:
|
if not captcha_code:
|
||||||
raise errors.RequestError(msg='验证码失效,请重新获取')
|
raise errors.RequestError(msg=t('error.captcha.expired'))
|
||||||
if captcha_code.lower() != obj.captcha.lower():
|
if captcha_code.lower() != obj.captcha.lower():
|
||||||
raise errors.CustomError(error=CustomErrorCode.CAPTCHA_ERROR)
|
raise errors.CustomError(error=CustomErrorCode.CAPTCHA_ERROR)
|
||||||
await redis_client.delete(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
|
await redis_client.delete(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
|
||||||
@@ -137,7 +138,7 @@ class AuthService:
|
|||||||
msg=e.msg,
|
msg=e.msg,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
raise errors.RequestError(msg=e.msg, background=task)
|
raise errors.RequestError(code=e.code, msg=e.msg, background=task)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f'登陆错误: {e}')
|
log.error(f'登陆错误: {e}')
|
||||||
raise e
|
raise e
|
||||||
@@ -151,7 +152,7 @@ class AuthService:
|
|||||||
username=obj.username,
|
username=obj.username,
|
||||||
login_time=timezone.now(),
|
login_time=timezone.now(),
|
||||||
status=LoginLogStatusType.success.value,
|
status=LoginLogStatusType.success.value,
|
||||||
msg='登录成功',
|
msg=t('success.login.success'),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
data = GetLoginToken(
|
data = GetLoginToken(
|
||||||
|
|||||||
@@ -33,6 +33,17 @@ class OperaLogService:
|
|||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
await opera_log_dao.create(db, obj)
|
await opera_log_dao.create(db, obj)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def bulk_create(*, objs: list[CreateOperaLogParam]) -> None:
|
||||||
|
"""
|
||||||
|
批量创建操作日志
|
||||||
|
|
||||||
|
:param objs: 操作日志创建参数列表
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
async with async_db_session.begin() as db:
|
||||||
|
await opera_log_dao.bulk_create(db, objs)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def delete(*, obj: DeleteOperaLogParam) -> int:
|
async def delete(*, obj: DeleteOperaLogParam) -> int:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class PluginService:
|
|||||||
keys = []
|
keys = []
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
async for key in redis_client.scan_iter(f'{settings.PLUGIN_REDIS_PREFIX}:info:*'):
|
async for key in redis_client.scan_iter(f'{settings.PLUGIN_REDIS_PREFIX}:*'):
|
||||||
keys.append(key)
|
keys.append(key)
|
||||||
|
|
||||||
for info in await redis_client.mget(*keys):
|
for info in await redis_client.mget(*keys):
|
||||||
@@ -74,8 +74,7 @@ class PluginService:
|
|||||||
await uninstall_requirements_async(plugin)
|
await uninstall_requirements_async(plugin)
|
||||||
bacup_dir = os.path.join(PLUGIN_DIR, f'{plugin}.{timezone.now().strftime("%Y%m%d%H%M%S")}.backup')
|
bacup_dir = os.path.join(PLUGIN_DIR, f'{plugin}.{timezone.now().strftime("%Y%m%d%H%M%S")}.backup')
|
||||||
shutil.move(plugin_dir, bacup_dir)
|
shutil.move(plugin_dir, bacup_dir)
|
||||||
await redis_client.delete(f'{settings.PLUGIN_REDIS_PREFIX}:info:{plugin}')
|
await redis_client.delete(f'{settings.PLUGIN_REDIS_PREFIX}:{plugin}')
|
||||||
await redis_client.hdel(f'{settings.PLUGIN_REDIS_PREFIX}:status', plugin)
|
|
||||||
await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:changed', 'ture')
|
await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:changed', 'ture')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -86,7 +85,7 @@ class PluginService:
|
|||||||
:param plugin: 插件名称
|
:param plugin: 插件名称
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
plugin_info = await redis_client.get(f'{settings.PLUGIN_REDIS_PREFIX}:info:{plugin}')
|
plugin_info = await redis_client.get(f'{settings.PLUGIN_REDIS_PREFIX}:{plugin}')
|
||||||
if not plugin_info:
|
if not plugin_info:
|
||||||
raise errors.NotFoundError(msg='插件不存在')
|
raise errors.NotFoundError(msg='插件不存在')
|
||||||
plugin_info = json.loads(plugin_info)
|
plugin_info = json.loads(plugin_info)
|
||||||
@@ -98,10 +97,7 @@ class PluginService:
|
|||||||
else str(StatusType.disable.value)
|
else str(StatusType.disable.value)
|
||||||
)
|
)
|
||||||
plugin_info['plugin']['enable'] = new_status
|
plugin_info['plugin']['enable'] = new_status
|
||||||
await redis_client.set(
|
await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:{plugin}', json.dumps(plugin_info, ensure_ascii=False))
|
||||||
f'{settings.PLUGIN_REDIS_PREFIX}:info:{plugin}', json.dumps(plugin_info, ensure_ascii=False)
|
|
||||||
)
|
|
||||||
await redis_client.hset(f'{settings.PLUGIN_REDIS_PREFIX}:status', plugin, new_status)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def build(*, plugin: str) -> io.BytesIO:
|
async def build(*, plugin: str) -> io.BytesIO:
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ from backend.app.admin.schema.user import (
|
|||||||
)
|
)
|
||||||
from backend.common.enums import UserPermissionType
|
from backend.common.enums import UserPermissionType
|
||||||
from backend.common.exception import errors
|
from backend.common.exception import errors
|
||||||
from backend.common.security.jwt import get_hash_password, get_token, jwt_decode, password_verify, superuser_verify
|
from backend.common.response.response_code import CustomErrorCode
|
||||||
|
from backend.common.security.jwt import get_token, jwt_decode, password_verify, superuser_verify
|
||||||
from backend.core.conf import settings
|
from backend.core.conf import settings
|
||||||
from backend.database.db import async_db_session
|
from backend.database.db import async_db_session
|
||||||
from backend.database.redis import redis_client
|
from backend.database.redis import redis_client
|
||||||
@@ -103,11 +104,10 @@ class UserService:
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
|
superuser_verify(request)
|
||||||
user = await user_dao.get_with_relation(db, user_id=pk)
|
user = await user_dao.get_with_relation(db, user_id=pk)
|
||||||
if not user:
|
if not user:
|
||||||
raise errors.NotFoundError(msg='用户不存在')
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
if request.user.username != user.username:
|
|
||||||
raise errors.ForbiddenError(msg='只能修改自己的信息')
|
|
||||||
if obj.username != user.username:
|
if obj.username != user.username:
|
||||||
if await user_dao.get_by_username(db, obj.username):
|
if await user_dao.get_by_username(db, obj.username):
|
||||||
raise errors.ConflictError(msg='用户名已注册')
|
raise errors.ConflictError(msg='用户名已注册')
|
||||||
@@ -119,98 +119,7 @@ class UserService:
|
|||||||
return count
|
return count
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update_superuser(*, request: Request, pk: int) -> int:
|
async def update_permission(*, request: Request, pk: int, type: UserPermissionType) -> int:
|
||||||
"""
|
|
||||||
更新用户管理员状态
|
|
||||||
|
|
||||||
:param request: FastAPI 请求对象
|
|
||||||
:param pk: 用户 ID
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
async with async_db_session.begin() as db:
|
|
||||||
superuser_verify(request)
|
|
||||||
user = await user_dao.get(db, pk)
|
|
||||||
if not user:
|
|
||||||
raise errors.NotFoundError(msg='用户不存在')
|
|
||||||
if pk == request.user.id:
|
|
||||||
raise errors.ForbiddenError(msg='禁止修改自身权限')
|
|
||||||
count = await user_dao.set_super(db, pk, not user.status)
|
|
||||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
|
||||||
return count
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def update_staff(*, request: Request, pk: int) -> int:
|
|
||||||
"""
|
|
||||||
更新用户职员状态
|
|
||||||
|
|
||||||
:param request: FastAPI 请求对象
|
|
||||||
:param pk: 用户 ID
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
async with async_db_session.begin() as db:
|
|
||||||
superuser_verify(request)
|
|
||||||
user = await user_dao.get(db, pk)
|
|
||||||
if not user:
|
|
||||||
raise errors.NotFoundError(msg='用户不存在')
|
|
||||||
if pk == request.user.id:
|
|
||||||
raise errors.ForbiddenError(msg='禁止修改自身权限')
|
|
||||||
count = await user_dao.set_staff(db, pk, not user.is_staff)
|
|
||||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
|
||||||
return count
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def update_status(*, request: Request, pk: int) -> int:
|
|
||||||
"""
|
|
||||||
更新用户状态
|
|
||||||
|
|
||||||
:param request: FastAPI 请求对象
|
|
||||||
:param pk: 用户 ID
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
async with async_db_session.begin() as db:
|
|
||||||
superuser_verify(request)
|
|
||||||
user = await user_dao.get(db, pk)
|
|
||||||
if not user:
|
|
||||||
raise errors.NotFoundError(msg='用户不存在')
|
|
||||||
if pk == request.user.id:
|
|
||||||
raise errors.ForbiddenError(msg='禁止修改自身权限')
|
|
||||||
count = await user_dao.set_status(db, pk, 0 if user.status == 1 else 1)
|
|
||||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
|
||||||
return count
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def update_multi_login(*, request: Request, pk: int) -> int:
|
|
||||||
"""
|
|
||||||
更新用户多端登录状态
|
|
||||||
|
|
||||||
:param request: FastAPI 请求对象
|
|
||||||
:param pk: 用户 ID
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
async with async_db_session.begin() as db:
|
|
||||||
superuser_verify(request)
|
|
||||||
user = await user_dao.get(db, pk)
|
|
||||||
if not user:
|
|
||||||
raise errors.NotFoundError(msg='用户不存在')
|
|
||||||
multi_login = user.is_multi_login if pk != user.id else request.user.is_multi_login
|
|
||||||
new_multi_login = not multi_login
|
|
||||||
count = await user_dao.set_multi_login(db, pk, new_multi_login)
|
|
||||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
|
||||||
token = get_token(request)
|
|
||||||
token_payload = jwt_decode(token)
|
|
||||||
if pk == user.id:
|
|
||||||
# 系统管理员修改自身时,除当前 token 外,其他 token 失效
|
|
||||||
if not new_multi_login:
|
|
||||||
key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{user.id}'
|
|
||||||
await redis_client.delete_prefix(key_prefix, exclude=f'{key_prefix}:{token_payload.session_uuid}')
|
|
||||||
else:
|
|
||||||
# 系统管理员修改他人时,他人 token 全部失效
|
|
||||||
if not new_multi_login:
|
|
||||||
key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{user.id}'
|
|
||||||
await redis_client.delete_prefix(key_prefix)
|
|
||||||
return count
|
|
||||||
|
|
||||||
async def update_permission(self, *, request: Request, pk: int, type: UserPermissionType) -> int:
|
|
||||||
"""
|
"""
|
||||||
更新用户权限
|
更新用户权限
|
||||||
|
|
||||||
@@ -219,38 +128,166 @@ class UserService:
|
|||||||
:param type: 权限类型
|
:param type: 权限类型
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
match type:
|
async with async_db_session.begin() as db:
|
||||||
case UserPermissionType.superuser:
|
superuser_verify(request)
|
||||||
count = await self.update_superuser(request=request, pk=pk)
|
match type:
|
||||||
case UserPermissionType.staff:
|
case UserPermissionType.superuser:
|
||||||
count = await self.update_staff(request=request, pk=pk)
|
user = await user_dao.get(db, pk)
|
||||||
case UserPermissionType.status:
|
if not user:
|
||||||
count = await self.update_status(request=request, pk=pk)
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
case UserPermissionType.multi_login:
|
if pk == request.user.id:
|
||||||
count = await self.update_multi_login(request=request, pk=pk)
|
raise errors.ForbiddenError(msg='禁止修改自身权限')
|
||||||
case _:
|
count = await user_dao.set_super(db, pk, not user.status)
|
||||||
raise errors.RequestError(msg='权限类型不存在')
|
case UserPermissionType.staff:
|
||||||
|
user = await user_dao.get(db, pk)
|
||||||
|
if not user:
|
||||||
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
|
if pk == request.user.id:
|
||||||
|
raise errors.ForbiddenError(msg='禁止修改自身权限')
|
||||||
|
count = await user_dao.set_staff(db, pk, not user.is_staff)
|
||||||
|
case UserPermissionType.status:
|
||||||
|
user = await user_dao.get(db, pk)
|
||||||
|
if not user:
|
||||||
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
|
if pk == request.user.id:
|
||||||
|
raise errors.ForbiddenError(msg='禁止修改自身权限')
|
||||||
|
count = await user_dao.set_status(db, pk, 0 if user.status == 1 else 1)
|
||||||
|
case UserPermissionType.multi_login:
|
||||||
|
user = await user_dao.get(db, pk)
|
||||||
|
if not user:
|
||||||
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
|
multi_login = user.is_multi_login if pk != user.id else request.user.is_multi_login
|
||||||
|
new_multi_login = not multi_login
|
||||||
|
count = await user_dao.set_multi_login(db, pk, new_multi_login)
|
||||||
|
token = get_token(request)
|
||||||
|
token_payload = jwt_decode(token)
|
||||||
|
if pk == user.id:
|
||||||
|
# 系统管理员修改自身时,除当前 token 外,其他 token 失效
|
||||||
|
if not new_multi_login:
|
||||||
|
key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{user.id}'
|
||||||
|
await redis_client.delete_prefix(
|
||||||
|
key_prefix, exclude=f'{key_prefix}:{token_payload.session_uuid}'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# 系统管理员修改他人时,他人 token 全部失效
|
||||||
|
if not new_multi_login:
|
||||||
|
key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{user.id}'
|
||||||
|
await redis_client.delete_prefix(key_prefix)
|
||||||
|
case _:
|
||||||
|
raise errors.RequestError(msg='权限类型不存在')
|
||||||
|
|
||||||
|
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||||
return count
|
return count
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def reset_pwd(*, pk: int, obj: ResetPasswordParam) -> int:
|
async def reset_password(*, request: Request, pk: int, password: str) -> int:
|
||||||
"""
|
"""
|
||||||
重置用户密码
|
重置用户密码
|
||||||
|
|
||||||
|
:param request: FastAPI 请求对象
|
||||||
:param pk: 用户 ID
|
:param pk: 用户 ID
|
||||||
|
:param password: 新密码
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
async with async_db_session.begin() as db:
|
||||||
|
superuser_verify(request)
|
||||||
|
user = await user_dao.get(db, pk)
|
||||||
|
if not user:
|
||||||
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
|
count = await user_dao.reset_password(db, user.id, password)
|
||||||
|
key_prefix = [
|
||||||
|
f'{settings.TOKEN_REDIS_PREFIX}:{user.id}',
|
||||||
|
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user.id}',
|
||||||
|
f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}',
|
||||||
|
]
|
||||||
|
for prefix in key_prefix:
|
||||||
|
await redis_client.delete(prefix)
|
||||||
|
return count
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def update_nickname(*, request: Request, nickname: str) -> int:
|
||||||
|
"""
|
||||||
|
更新当前用户昵称
|
||||||
|
|
||||||
|
:param request: FastAPI 请求对象
|
||||||
|
:param nickname: 用户昵称
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
async with async_db_session.begin() as db:
|
||||||
|
token = get_token(request)
|
||||||
|
token_payload = jwt_decode(token)
|
||||||
|
user = await user_dao.get(db, token_payload.id)
|
||||||
|
if not user:
|
||||||
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
|
count = await user_dao.update_nickname(db, token_payload.id, nickname)
|
||||||
|
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||||
|
return count
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def update_avatar(*, request: Request, avatar: str) -> int:
|
||||||
|
"""
|
||||||
|
更新当前用户头像
|
||||||
|
|
||||||
|
:param request: FastAPI 请求对象
|
||||||
|
:param avatar: 头像地址
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
async with async_db_session.begin() as db:
|
||||||
|
token = get_token(request)
|
||||||
|
token_payload = jwt_decode(token)
|
||||||
|
user = await user_dao.get(db, token_payload.id)
|
||||||
|
if not user:
|
||||||
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
|
count = await user_dao.update_avatar(db, token_payload.id, avatar)
|
||||||
|
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||||
|
return count
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def update_email(*, request: Request, captcha: str, email: str) -> int:
|
||||||
|
"""
|
||||||
|
更新当前用户邮箱
|
||||||
|
|
||||||
|
:param request: FastAPI 请求对象
|
||||||
|
:param captcha: 邮箱验证码
|
||||||
|
:param email: 邮箱
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
async with async_db_session.begin() as db:
|
||||||
|
token = get_token(request)
|
||||||
|
token_payload = jwt_decode(token)
|
||||||
|
user = await user_dao.get(db, token_payload.id)
|
||||||
|
if not user:
|
||||||
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
|
captcha_code = await redis_client.get(f'{settings.EMAIL_CAPTCHA_REDIS_PREFIX}:{request.state.ip}')
|
||||||
|
if not captcha_code:
|
||||||
|
raise errors.RequestError(msg='验证码已失效,请重新获取')
|
||||||
|
if captcha != captcha_code:
|
||||||
|
raise errors.CustomError(error=CustomErrorCode.CAPTCHA_ERROR)
|
||||||
|
await redis_client.delete(f'{settings.EMAIL_CAPTCHA_REDIS_PREFIX}:{request.state.ip}')
|
||||||
|
count = await user_dao.update_email(db, token_payload.id, email)
|
||||||
|
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||||
|
return count
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def update_password(*, request: Request, obj: ResetPasswordParam) -> int:
|
||||||
|
"""
|
||||||
|
更新当前用户密码
|
||||||
|
|
||||||
|
:param request: FastAPI 请求对象
|
||||||
:param obj: 密码重置参数
|
:param obj: 密码重置参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
user = await user_dao.get(db, pk)
|
token = get_token(request)
|
||||||
|
token_payload = jwt_decode(token)
|
||||||
|
user = await user_dao.get(db, token_payload.id)
|
||||||
if not user:
|
if not user:
|
||||||
raise errors.NotFoundError(msg='用户不存在')
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
if not password_verify(obj.old_password, user.password):
|
if not password_verify(obj.old_password, user.password):
|
||||||
raise errors.RequestError(msg='原密码错误')
|
raise errors.RequestError(msg='原密码错误')
|
||||||
if obj.new_password != obj.confirm_password:
|
if obj.new_password != obj.confirm_password:
|
||||||
raise errors.RequestError(msg='密码输入不一致')
|
raise errors.RequestError(msg='密码输入不一致')
|
||||||
new_pwd = get_hash_password(obj.new_password, user.salt)
|
count = await user_dao.reset_password(db, user.id, obj.new_password)
|
||||||
count = await user_dao.reset_password(db, user.id, new_pwd)
|
|
||||||
key_prefix = [
|
key_prefix = [
|
||||||
f'{settings.TOKEN_REDIS_PREFIX}:{user.id}',
|
f'{settings.TOKEN_REDIS_PREFIX}:{user.id}',
|
||||||
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user.id}',
|
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user.id}',
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from backend.app.task.api.v1.control import router as task_control_router
|
||||||
from backend.app.task.api.v1.result import router as task_result_router
|
from backend.app.task.api.v1.result import router as task_result_router
|
||||||
from backend.app.task.api.v1.scheduler import router as task_scheduler_router
|
from backend.app.task.api.v1.scheduler import router as task_scheduler_router
|
||||||
from backend.core.conf import settings
|
from backend.core.conf import settings
|
||||||
|
|
||||||
v1 = APIRouter(prefix=f'{settings.FASTAPI_API_V1_PATH}/task', tags=['任务'])
|
v1 = APIRouter(prefix=f'{settings.FASTAPI_API_V1_PATH}/tasks', tags=['任务'])
|
||||||
|
|
||||||
|
v1.include_router(task_control_router)
|
||||||
v1.include_router(task_result_router, prefix='/results')
|
v1.include_router(task_result_router, prefix='/results')
|
||||||
v1.include_router(task_scheduler_router, prefix='/schedulers')
|
v1.include_router(task_scheduler_router, prefix='/schedulers')
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, Path
|
||||||
|
from starlette.concurrency import run_in_threadpool
|
||||||
|
|
||||||
|
from backend.app.task import celery_app
|
||||||
|
from backend.app.task.schema.control import TaskRegisteredDetail
|
||||||
|
from backend.common.exception import errors
|
||||||
|
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
||||||
|
from backend.common.security.jwt import DependsJwtAuth
|
||||||
|
from backend.common.security.permission import RequestPermission
|
||||||
|
from backend.common.security.rbac import DependsRBAC
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/registered', summary='获取已注册的任务', dependencies=[DependsJwtAuth])
|
||||||
|
async def get_task_registered() -> ResponseSchemaModel[list[TaskRegisteredDetail]]:
|
||||||
|
inspector = celery_app.control.inspect(timeout=0.5)
|
||||||
|
registered = await run_in_threadpool(inspector.registered)
|
||||||
|
if not registered:
|
||||||
|
raise errors.ServerError(msg='Celery Worker 暂不可用,请稍后重试')
|
||||||
|
task_registered = []
|
||||||
|
celery_app_tasks = celery_app.tasks
|
||||||
|
for _, tasks in registered.items():
|
||||||
|
for task in tasks:
|
||||||
|
task_ins = celery_app_tasks.get(task)
|
||||||
|
if task_ins:
|
||||||
|
task_doc = task_ins.__doc__
|
||||||
|
task_registered.append({'name': task_doc or task_ins, 'task': task_ins})
|
||||||
|
else:
|
||||||
|
task_registered.append({'name': task, 'task': task})
|
||||||
|
return response_base.success(data=task_registered)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete(
|
||||||
|
'/{task_id}/cancel',
|
||||||
|
summary='撤销任务',
|
||||||
|
dependencies=[
|
||||||
|
Depends(RequestPermission('sys:task:revoke')),
|
||||||
|
DependsRBAC,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def revoke_task(task_id: Annotated[str, Path(description='任务 UUID')]) -> ResponseModel:
|
||||||
|
workers = await run_in_threadpool(celery_app.control.ping, timeout=0.5)
|
||||||
|
if not workers:
|
||||||
|
raise errors.ServerError(msg='Celery Worker 暂不可用,请稍后重试')
|
||||||
|
celery_app.control.revoke(task_id)
|
||||||
|
return response_base.success()
|
||||||
@@ -119,16 +119,3 @@ async def delete_task_scheduler(pk: Annotated[int, Path(description='任务调
|
|||||||
async def execute_task(pk: Annotated[int, Path(description='任务调度 ID')]) -> ResponseModel:
|
async def execute_task(pk: Annotated[int, Path(description='任务调度 ID')]) -> ResponseModel:
|
||||||
await task_scheduler_service.execute(pk=pk)
|
await task_scheduler_service.execute(pk=pk)
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|
||||||
|
|
||||||
@router.delete(
|
|
||||||
'/{task_id}/cancel',
|
|
||||||
summary='撤销任务',
|
|
||||||
dependencies=[
|
|
||||||
Depends(RequestPermission('sys:task:revoke')),
|
|
||||||
DependsRBAC,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def revoke_task(task_id: Annotated[str, Path(description='任务 UUID')]) -> ResponseModel:
|
|
||||||
await task_scheduler_service.revoke(task_id=task_id)
|
|
||||||
return response_base.success()
|
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ from backend.core.path_conf import BASE_PATH
|
|||||||
|
|
||||||
def find_task_packages():
|
def find_task_packages():
|
||||||
packages = []
|
packages = []
|
||||||
for root, dirs, files in os.walk(os.path.join(BASE_PATH, 'app', 'task', 'tasks')):
|
task_dir = os.path.join(BASE_PATH, 'app', 'task', 'tasks')
|
||||||
|
for root, dirs, files in os.walk(task_dir):
|
||||||
if 'tasks.py' in files:
|
if 'tasks.py' in files:
|
||||||
package = root.replace(str(BASE_PATH.parent) + os.path.sep, '').replace(os.path.sep, '.')
|
package = root.replace(str(BASE_PATH.parent) + os.path.sep, '').replace(os.path.sep, '.')
|
||||||
packages.append(package)
|
packages.append(package)
|
||||||
@@ -43,7 +44,8 @@ def init_celery() -> celery.Celery:
|
|||||||
'group': OVERWRITE_CELERY_RESULT_GROUP_TABLE_NAME,
|
'group': OVERWRITE_CELERY_RESULT_GROUP_TABLE_NAME,
|
||||||
},
|
},
|
||||||
result_extended=True,
|
result_extended=True,
|
||||||
# result_expires=0, # 任务结果自动清理,0 或 None 表示不清理
|
# result_expires=0, # 清理任务结果,默认每天凌晨 4 点,0 或 None 表示不清理
|
||||||
|
# beat_sync_every=1, # 保存任务状态周期,默认 3 * 60 秒
|
||||||
beat_schedule=LOCAL_BEAT_SCHEDULE,
|
beat_schedule=LOCAL_BEAT_SCHEDULE,
|
||||||
beat_scheduler='backend.app.task.utils.schedulers:DatabaseScheduler',
|
beat_scheduler='backend.app.task.utils.schedulers:DatabaseScheduler',
|
||||||
task_cls='backend.app.task.tasks.base:TaskBase',
|
task_cls='backend.app.task.tasks.base:TaskBase',
|
||||||
@@ -53,7 +55,8 @@ def init_celery() -> celery.Celery:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 自动发现任务
|
# 自动发现任务
|
||||||
app.autodiscover_tasks(find_task_packages())
|
packages = find_task_packages()
|
||||||
|
app.autodiscover_tasks(packages)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class CRUDTaskResult(CRUDPlus[TaskResult]):
|
|||||||
if task_id is not None:
|
if task_id is not None:
|
||||||
filters['task_id'] = task_id
|
filters['task_id'] = task_id
|
||||||
|
|
||||||
return await self.select_order('id', **filters)
|
return await self.select_order('id', 'desc', **filters)
|
||||||
|
|
||||||
async def delete(self, db: AsyncSession, pks: list[int]) -> int:
|
async def delete(self, db: AsyncSession, pks: list[int]) -> int:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from backend.common.schema import SchemaBase
|
||||||
|
|
||||||
|
|
||||||
|
class TaskRegisteredDetail(SchemaBase):
|
||||||
|
name: str
|
||||||
|
task: str
|
||||||
@@ -3,8 +3,9 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from pydantic import ConfigDict, Field
|
from pydantic import ConfigDict, Field, field_serializer
|
||||||
|
|
||||||
|
from backend.app.task import celery_app
|
||||||
from backend.common.schema import SchemaBase
|
from backend.common.schema import SchemaBase
|
||||||
|
|
||||||
|
|
||||||
@@ -36,3 +37,7 @@ class GetTaskResultDetail(TaskResultSchemaBase):
|
|||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
id: int = Field(description='任务结果 ID')
|
id: int = Field(description='任务结果 ID')
|
||||||
|
|
||||||
|
@field_serializer('args', 'kwargs', when_used='unless-none')
|
||||||
|
def serialize_params(self, value: bytes | None, _info) -> Any:
|
||||||
|
return celery_app.backend.decode(value)
|
||||||
|
|||||||
@@ -65,14 +65,7 @@ class TaskSchedulerService:
|
|||||||
if task_scheduler:
|
if task_scheduler:
|
||||||
raise errors.ConflictError(msg='任务调度已存在')
|
raise errors.ConflictError(msg='任务调度已存在')
|
||||||
if obj.type == TaskSchedulerType.CRONTAB:
|
if obj.type == TaskSchedulerType.CRONTAB:
|
||||||
crontab_split = obj.crontab.split(' ')
|
crontab_verify(obj.crontab)
|
||||||
if len(crontab_split) != 5:
|
|
||||||
raise errors.RequestError(msg='Crontab 表达式非法')
|
|
||||||
crontab_verify('m', crontab_split[0])
|
|
||||||
crontab_verify('h', crontab_split[1])
|
|
||||||
crontab_verify('dow', crontab_split[2])
|
|
||||||
crontab_verify('dom', crontab_split[3])
|
|
||||||
crontab_verify('moy', crontab_split[4])
|
|
||||||
await task_scheduler_dao.create(db, obj)
|
await task_scheduler_dao.create(db, obj)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -92,14 +85,7 @@ class TaskSchedulerService:
|
|||||||
if await task_scheduler_dao.get_by_name(db, obj.name):
|
if await task_scheduler_dao.get_by_name(db, obj.name):
|
||||||
raise errors.ConflictError(msg='任务调度已存在')
|
raise errors.ConflictError(msg='任务调度已存在')
|
||||||
if task_scheduler.type == TaskSchedulerType.CRONTAB:
|
if task_scheduler.type == TaskSchedulerType.CRONTAB:
|
||||||
crontab_split = obj.crontab.split(' ')
|
crontab_verify(obj.crontab)
|
||||||
if len(crontab_split) != 5:
|
|
||||||
raise errors.RequestError(msg='Crontab 表达式非法')
|
|
||||||
crontab_verify('m', crontab_split[0])
|
|
||||||
crontab_verify('h', crontab_split[1])
|
|
||||||
crontab_verify('dow', crontab_split[2])
|
|
||||||
crontab_verify('dom', crontab_split[3])
|
|
||||||
crontab_verify('moy', crontab_split[4])
|
|
||||||
count = await task_scheduler_dao.update(db, pk, obj)
|
count = await task_scheduler_dao.update(db, pk, obj)
|
||||||
return count
|
return count
|
||||||
|
|
||||||
@@ -156,18 +142,5 @@ class TaskSchedulerService:
|
|||||||
else:
|
else:
|
||||||
celery_app.send_task(name=task_scheduler.task, args=args, kwargs=kwargs)
|
celery_app.send_task(name=task_scheduler.task, args=args, kwargs=kwargs)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def revoke(*, task_id: str) -> None:
|
|
||||||
"""
|
|
||||||
撤销指定的任务
|
|
||||||
|
|
||||||
:param task_id: 任务 UUID
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
workers = await run_in_threadpool(celery_app.control.ping, timeout=0.5)
|
|
||||||
if not workers:
|
|
||||||
raise errors.ServerError(msg='Celery Worker 暂不可用,请稍后重试')
|
|
||||||
celery_app.control.revoke(task_id)
|
|
||||||
|
|
||||||
|
|
||||||
task_scheduler_service: TaskSchedulerService = TaskSchedulerService()
|
task_scheduler_service: TaskSchedulerService = TaskSchedulerService()
|
||||||
|
|||||||
@@ -4,15 +4,22 @@ from celery.schedules import schedule
|
|||||||
|
|
||||||
from backend.app.task.utils.tzcrontab import TzAwareCrontab
|
from backend.app.task.utils.tzcrontab import TzAwareCrontab
|
||||||
|
|
||||||
|
# 参考:https://docs.celeryq.dev/en/stable/userguide/periodic-tasks.html
|
||||||
LOCAL_BEAT_SCHEDULE = {
|
LOCAL_BEAT_SCHEDULE = {
|
||||||
'测试同步任务': {
|
'测试同步任务': {
|
||||||
'task': 'task_demo',
|
'task': 'task_demo',
|
||||||
'schedule': schedule(5),
|
'schedule': schedule(30),
|
||||||
},
|
},
|
||||||
'测试异步任务': {
|
'测试异步任务': {
|
||||||
'task': 'task_demo_async',
|
'task': 'task_demo_async',
|
||||||
'schedule': TzAwareCrontab('1'),
|
'schedule': TzAwareCrontab('1'),
|
||||||
},
|
},
|
||||||
|
'测试传参任务': {
|
||||||
|
'task': 'task_demo_params',
|
||||||
|
'schedule': TzAwareCrontab('1'),
|
||||||
|
'args': ['你好,'],
|
||||||
|
'kwargs': {'world': '世界'},
|
||||||
|
},
|
||||||
'清理操作日志': {
|
'清理操作日志': {
|
||||||
'task': 'backend.app.task.tasks.db_log.tasks.delete_db_opera_log',
|
'task': 'backend.app.task.tasks.db_log.tasks.delete_db_opera_log',
|
||||||
'schedule': TzAwareCrontab('0', '0', day_of_week='6'),
|
'schedule': TzAwareCrontab('0', '0', day_of_week='6'),
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from celery import shared_task
|
||||||
|
|
||||||
from backend.app.admin.service.login_log_service import login_log_service
|
from backend.app.admin.service.login_log_service import login_log_service
|
||||||
from backend.app.admin.service.opera_log_service import opera_log_service
|
from backend.app.admin.service.opera_log_service import opera_log_service
|
||||||
from backend.app.task.celery import celery_app
|
|
||||||
|
|
||||||
|
|
||||||
@celery_app.task
|
@shared_task
|
||||||
async def delete_db_opera_log() -> str:
|
async def delete_db_opera_log() -> str:
|
||||||
"""自动删除数据库操作日志"""
|
"""自动删除数据库操作日志"""
|
||||||
await opera_log_service.delete_all()
|
await opera_log_service.delete_all()
|
||||||
return 'Success'
|
return 'Success'
|
||||||
|
|
||||||
|
|
||||||
@celery_app.task
|
@shared_task
|
||||||
async def delete_db_login_log() -> str:
|
async def delete_db_login_log() -> str:
|
||||||
"""自动删除数据库登录日志"""
|
"""自动删除数据库登录日志"""
|
||||||
await login_log_service.delete_all()
|
await login_log_service.delete_all()
|
||||||
|
|||||||
@@ -10,12 +10,18 @@ from backend.app.task.celery import celery_app
|
|||||||
@celery_app.task(name='task_demo')
|
@celery_app.task(name='task_demo')
|
||||||
def task_demo() -> str:
|
def task_demo() -> str:
|
||||||
"""示例任务,模拟耗时操作"""
|
"""示例任务,模拟耗时操作"""
|
||||||
sleep(20)
|
sleep(30)
|
||||||
return 'test async'
|
return 'test async'
|
||||||
|
|
||||||
|
|
||||||
@celery_app.task(name='task_demo_async')
|
@celery_app.task(name='task_demo_async')
|
||||||
async def task_demo_async() -> str:
|
async def task_demo_async() -> str:
|
||||||
"""异步示例任务,模拟耗时操作"""
|
"""异步示例任务,模拟耗时操作"""
|
||||||
await asleep(20)
|
await asleep(30)
|
||||||
return 'test async'
|
return 'test async'
|
||||||
|
|
||||||
|
|
||||||
|
@celery_app.task(name='task_demo_params')
|
||||||
|
async def task_demo_params(hello: str, world: str | None = None) -> str:
|
||||||
|
"""参数示例任务,模拟传参操作"""
|
||||||
|
return hello + world
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ from multiprocessing.util import Finalize
|
|||||||
|
|
||||||
from celery import current_app, schedules
|
from celery import current_app, schedules
|
||||||
from celery.beat import ScheduleEntry, Scheduler
|
from celery.beat import ScheduleEntry, Scheduler
|
||||||
|
from celery.signals import beat_init
|
||||||
from celery.utils.log import get_logger
|
from celery.utils.log import get_logger
|
||||||
|
from redis.asyncio.lock import Lock
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.exc import DatabaseError, InterfaceError
|
from sqlalchemy.exc import DatabaseError, InterfaceError
|
||||||
|
|
||||||
@@ -28,6 +30,38 @@ from backend.utils.timezone import timezone
|
|||||||
# 此计划程序必须比常规的 5 分钟更频繁地唤醒,因为它需要考虑对计划的外部更改
|
# 此计划程序必须比常规的 5 分钟更频繁地唤醒,因为它需要考虑对计划的外部更改
|
||||||
DEFAULT_MAX_INTERVAL = 5 # seconds
|
DEFAULT_MAX_INTERVAL = 5 # seconds
|
||||||
|
|
||||||
|
# 计划锁时长,避免重复创建
|
||||||
|
DEFAULT_MAX_LOCK_TIMEOUT = 300 # seconds
|
||||||
|
|
||||||
|
# 锁检测周期,应小于计划锁时长
|
||||||
|
DEFAULT_LOCK_INTERVAL = 60 # seconds
|
||||||
|
|
||||||
|
# Copied from:
|
||||||
|
# https://github.com/andymccurdy/redis-py/blob/master/redis/lock.py#L33
|
||||||
|
# Changes:
|
||||||
|
# The second line from the bottom: The original Lua script intends
|
||||||
|
# to extend time to (lock remaining time + additional time); while
|
||||||
|
# the script here extend time to an expected expiration time.
|
||||||
|
# KEYS[1] - lock name
|
||||||
|
# ARGS[1] - token
|
||||||
|
# ARGS[2] - additional milliseconds
|
||||||
|
# return 1 if the locks time was extended, otherwise 0
|
||||||
|
LUA_EXTEND_TO_SCRIPT = """
|
||||||
|
local token = redis.call('get', KEYS[1])
|
||||||
|
if not token or token ~= ARGV[1] then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
local expiration = redis.call('pttl', KEYS[1])
|
||||||
|
if not expiration then
|
||||||
|
expiration = 0
|
||||||
|
end
|
||||||
|
if expiration < 0 then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
redis.call('pexpire', KEYS[1], ARGV[2])
|
||||||
|
return 1
|
||||||
|
"""
|
||||||
|
|
||||||
logger = get_logger('fba.schedulers')
|
logger = get_logger('fba.schedulers')
|
||||||
|
|
||||||
|
|
||||||
@@ -188,21 +222,12 @@ class ModelEntry(ScheduleEntry):
|
|||||||
if not obj:
|
if not obj:
|
||||||
obj = TaskScheduler(**CreateTaskSchedulerParam(task=task, **spec).model_dump())
|
obj = TaskScheduler(**CreateTaskSchedulerParam(task=task, **spec).model_dump())
|
||||||
elif isinstance(schedule, schedules.crontab):
|
elif isinstance(schedule, schedules.crontab):
|
||||||
crontab_minute = schedule._orig_minute if crontab_verify('m', schedule._orig_minute, False) else '*'
|
crontab = f'{schedule._orig_minute} {schedule._orig_hour} {schedule._orig_day_of_week} {schedule._orig_day_of_month} {schedule._orig_month_of_year}' # noqa: E501
|
||||||
crontab_hour = schedule._orig_hour if crontab_verify('h', schedule._orig_hour, False) else '*'
|
crontab_verify(crontab)
|
||||||
crontab_day_of_week = (
|
|
||||||
schedule._orig_day_of_week if crontab_verify('dom', schedule._orig_day_of_week, False) else '*'
|
|
||||||
)
|
|
||||||
crontab_day_of_month = (
|
|
||||||
schedule._orig_day_of_month if crontab_verify('dom', schedule._orig_day_of_month, False) else '*'
|
|
||||||
)
|
|
||||||
crontab_month_of_year = (
|
|
||||||
schedule._orig_month_of_year if crontab_verify('moy', schedule._orig_month_of_year, False) else '*'
|
|
||||||
)
|
|
||||||
spec = {
|
spec = {
|
||||||
'name': name,
|
'name': name,
|
||||||
'type': TaskSchedulerType.CRONTAB.value,
|
'type': TaskSchedulerType.CRONTAB.value,
|
||||||
'crontab': f'{crontab_minute} {crontab_hour} {crontab_day_of_week} {crontab_day_of_month} {crontab_month_of_year}', # noqa: E501
|
'crontab': crontab,
|
||||||
}
|
}
|
||||||
stmt = select(TaskScheduler).filter_by(**spec)
|
stmt = select(TaskScheduler).filter_by(**spec)
|
||||||
query = await db.execute(stmt)
|
query = await db.execute(stmt)
|
||||||
@@ -233,8 +258,8 @@ class ModelEntry(ScheduleEntry):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
continue
|
continue
|
||||||
model_dict.update(
|
model_dict.update(
|
||||||
args=json.dumps(args) if args else None,
|
args=json.dumps(args, ensure_ascii=False) if args else None,
|
||||||
kwargs=json.dumps(kwargs) if kwargs else None,
|
kwargs=json.dumps(kwargs, ensure_ascii=False) if kwargs else None,
|
||||||
**cls._unpack_options(**options or {}),
|
**cls._unpack_options(**options or {}),
|
||||||
**entry,
|
**entry,
|
||||||
)
|
)
|
||||||
@@ -269,6 +294,8 @@ class ModelEntry(ScheduleEntry):
|
|||||||
|
|
||||||
|
|
||||||
class DatabaseScheduler(Scheduler):
|
class DatabaseScheduler(Scheduler):
|
||||||
|
"""数据库调度程序"""
|
||||||
|
|
||||||
Entry = ModelEntry
|
Entry = ModelEntry
|
||||||
|
|
||||||
_schedule = None
|
_schedule = None
|
||||||
@@ -276,6 +303,9 @@ class DatabaseScheduler(Scheduler):
|
|||||||
_initial_read = True
|
_initial_read = True
|
||||||
_heap_invalidated = False
|
_heap_invalidated = False
|
||||||
|
|
||||||
|
lock: Lock | None = None
|
||||||
|
lock_key = f'{settings.CELERY_REDIS_PREFIX}:beat_lock'
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.app = kwargs['app']
|
self.app = kwargs['app']
|
||||||
self._dirty = set()
|
self._dirty = set()
|
||||||
@@ -324,6 +354,16 @@ class DatabaseScheduler(Scheduler):
|
|||||||
self._dirty.add(new_entry.name)
|
self._dirty.add(new_entry.name)
|
||||||
return new_entry
|
return new_entry
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""重写父函数"""
|
||||||
|
if self.lock:
|
||||||
|
logger.info('beat: Releasing lock')
|
||||||
|
if run_await(self.lock.owned)():
|
||||||
|
run_await(self.lock.release)()
|
||||||
|
self.lock = None
|
||||||
|
|
||||||
|
super().close()
|
||||||
|
|
||||||
def sync(self):
|
def sync(self):
|
||||||
"""重写父函数"""
|
"""重写父函数"""
|
||||||
_tried = set()
|
_tried = set()
|
||||||
@@ -410,3 +450,48 @@ class DatabaseScheduler(Scheduler):
|
|||||||
|
|
||||||
# logger.debug(self._schedule)
|
# logger.debug(self._schedule)
|
||||||
return self._schedule
|
return self._schedule
|
||||||
|
|
||||||
|
|
||||||
|
async def extend_scheduler_lock(lock):
|
||||||
|
"""
|
||||||
|
延长调度程序锁
|
||||||
|
|
||||||
|
:param lock: 计划程序锁
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(DEFAULT_LOCK_INTERVAL)
|
||||||
|
if lock:
|
||||||
|
try:
|
||||||
|
await lock.extend(DEFAULT_MAX_LOCK_TIMEOUT)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Failed to extend lock: {e}')
|
||||||
|
|
||||||
|
|
||||||
|
@beat_init.connect
|
||||||
|
def acquire_distributed_beat_lock(sender=None, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
尝试在启动时获取锁
|
||||||
|
|
||||||
|
:param sender: 接收方应响应的发送方
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
scheduler = sender.scheduler
|
||||||
|
if not scheduler.lock_key:
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.debug('beat: Acquiring lock...')
|
||||||
|
lock = redis_client.lock(
|
||||||
|
scheduler.lock_key,
|
||||||
|
timeout=DEFAULT_MAX_LOCK_TIMEOUT,
|
||||||
|
sleep=scheduler.max_interval,
|
||||||
|
)
|
||||||
|
# overwrite redis-py's extend script
|
||||||
|
# which will add additional timeout instead of extend to a new timeout
|
||||||
|
lock.lua_extend = redis_client.register_script(LUA_EXTEND_TO_SCRIPT)
|
||||||
|
run_await(lock.acquire)()
|
||||||
|
logger.info('beat: Acquired lock')
|
||||||
|
scheduler.lock = lock
|
||||||
|
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
loop.create_task(extend_scheduler_lock(scheduler.lock))
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Literal
|
|
||||||
|
|
||||||
from celery import schedules
|
from celery import schedules
|
||||||
from celery.schedules import ParseException, crontab_parser
|
from celery.schedules import ParseException, crontab_parser
|
||||||
@@ -53,34 +52,22 @@ class TzAwareCrontab(schedules.crontab):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def crontab_verify(filed: Literal['m', 'h', 'dow', 'dom', 'moy'], value: str, raise_exc: bool = True) -> bool:
|
def crontab_verify(crontab: str) -> None:
|
||||||
"""
|
"""
|
||||||
验证 Celery crontab 表达式
|
验证 Celery crontab 表达式
|
||||||
|
|
||||||
:param filed: 验证的字段
|
:param crontab: 计划表达式
|
||||||
:param value: 验证的值
|
|
||||||
:param raise_exc: 是否抛出异常
|
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
valid = True
|
crontab_split = crontab.split(' ')
|
||||||
|
if len(crontab_split) != 5:
|
||||||
|
raise errors.RequestError(msg='Crontab 表达式非法')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
match filed:
|
crontab_parser(60, 0).parse(crontab_split[0]) # minute
|
||||||
case 'm':
|
crontab_parser(24, 0).parse(crontab_split[1]) # hour
|
||||||
crontab_parser(60, 0).parse(value)
|
crontab_parser(7, 0).parse(crontab_split[2]) # day_of_week
|
||||||
case 'h':
|
crontab_parser(31, 1).parse(crontab_split[3]) # day_of_month
|
||||||
crontab_parser(24, 0).parse(value)
|
crontab_parser(12, 1).parse(crontab_split[4]) # month_of_year
|
||||||
case 'dow':
|
|
||||||
crontab_parser(7, 0).parse(value)
|
|
||||||
case 'dom':
|
|
||||||
crontab_parser(31, 1).parse(value)
|
|
||||||
case 'moy':
|
|
||||||
crontab_parser(12, 1).parse(value)
|
|
||||||
case _:
|
|
||||||
raise errors.ServerError(msg=f'无效字段:{filed}')
|
|
||||||
except ParseException:
|
except ParseException:
|
||||||
valid = False
|
raise errors.RequestError(msg='Crontab 表达式非法')
|
||||||
if raise_exc:
|
|
||||||
raise errors.RequestError(msg=f'crontab 值 {value} 非法')
|
|
||||||
|
|
||||||
return valid
|
|
||||||
|
|||||||
+9
-5
@@ -23,6 +23,13 @@ from backend.plugin.tools import get_plugin_sql
|
|||||||
from backend.utils.file_ops import install_git_plugin, install_zip_plugin, parse_sql_script
|
from backend.utils.file_ops import install_git_plugin, install_zip_plugin, parse_sql_script
|
||||||
|
|
||||||
|
|
||||||
|
class CustomReloadFilter(PythonFilter):
|
||||||
|
"""自定义重载过滤器"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(extra_extensions=['.json', '.yaml', '.yml'])
|
||||||
|
|
||||||
|
|
||||||
def run(host: str, port: int, reload: bool, workers: int | None) -> None:
|
def run(host: str, port: int, reload: bool, workers: int | None) -> None:
|
||||||
url = f'http://{host}:{port}'
|
url = f'http://{host}:{port}'
|
||||||
docs_url = url + settings.FASTAPI_DOCS_URL
|
docs_url = url + settings.FASTAPI_DOCS_URL
|
||||||
@@ -45,7 +52,7 @@ def run(host: str, port: int, reload: bool, workers: int | None) -> None:
|
|||||||
address=host,
|
address=host,
|
||||||
port=port,
|
port=port,
|
||||||
reload=not reload,
|
reload=not reload,
|
||||||
reload_filter=PythonFilter,
|
reload_filter=CustomReloadFilter,
|
||||||
workers=workers or 1,
|
workers=workers or 1,
|
||||||
).serve()
|
).serve()
|
||||||
|
|
||||||
@@ -184,10 +191,7 @@ class Flower:
|
|||||||
@cappa.command(help='运行 Celery 服务')
|
@cappa.command(help='运行 Celery 服务')
|
||||||
@dataclass
|
@dataclass
|
||||||
class Celery:
|
class Celery:
|
||||||
subcmd: cappa.Subcommands[Worker | Beat | Flower | None] = None
|
subcmd: cappa.Subcommands[Worker | Beat | Flower]
|
||||||
|
|
||||||
def __call__(self):
|
|
||||||
console.print('\n更多信息,尝试 "[cyan]--help[/]"')
|
|
||||||
|
|
||||||
|
|
||||||
@cappa.command(help='新增插件')
|
@cappa.command(help='新增插件')
|
||||||
|
|||||||
@@ -38,9 +38,15 @@ class CustomError(BaseExceptionMixin):
|
|||||||
class RequestError(BaseExceptionMixin):
|
class RequestError(BaseExceptionMixin):
|
||||||
"""请求异常"""
|
"""请求异常"""
|
||||||
|
|
||||||
code = StandardResponseCode.HTTP_400
|
def __init__(
|
||||||
|
self,
|
||||||
def __init__(self, *, msg: str = 'Bad Request', data: Any = None, background: BackgroundTask | None = None):
|
*,
|
||||||
|
code: int = StandardResponseCode.HTTP_400,
|
||||||
|
msg: str = 'Bad Request',
|
||||||
|
data: Any = None,
|
||||||
|
background: BackgroundTask | None = None,
|
||||||
|
):
|
||||||
|
self.code = code
|
||||||
super().__init__(msg=msg, data=data, background=background)
|
super().__init__(msg=msg, data=data, background=background)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,9 @@ from starlette.middleware.cors import CORSMiddleware
|
|||||||
from uvicorn.protocols.http.h11_impl import STATUS_PHRASES
|
from uvicorn.protocols.http.h11_impl import STATUS_PHRASES
|
||||||
|
|
||||||
from backend.common.exception.errors import BaseExceptionMixin
|
from backend.common.exception.errors import BaseExceptionMixin
|
||||||
|
from backend.common.i18n import i18n, t
|
||||||
from backend.common.response.response_code import CustomResponseCode, StandardResponseCode
|
from backend.common.response.response_code import CustomResponseCode, StandardResponseCode
|
||||||
from backend.common.response.response_schema import response_base
|
from backend.common.response.response_schema import response_base
|
||||||
from backend.common.schema import (
|
|
||||||
CUSTOM_VALIDATION_ERROR_MESSAGES,
|
|
||||||
)
|
|
||||||
from backend.core.conf import settings
|
from backend.core.conf import settings
|
||||||
from backend.utils.serializers import MsgSpecJSONResponse
|
from backend.utils.serializers import MsgSpecJSONResponse
|
||||||
from backend.utils.trace_id import get_request_trace_id
|
from backend.utils.trace_id import get_request_trace_id
|
||||||
@@ -46,18 +44,20 @@ async def _validation_exception_handler(request: Request, exc: RequestValidation
|
|||||||
"""
|
"""
|
||||||
errors = []
|
errors = []
|
||||||
for error in exc.errors():
|
for error in exc.errors():
|
||||||
custom_message = CUSTOM_VALIDATION_ERROR_MESSAGES.get(error['type'])
|
# 非 en-US 语言下,使用自定义错误信息
|
||||||
if custom_message:
|
if i18n.current_language != 'en-US':
|
||||||
ctx = error.get('ctx')
|
custom_message = t(f'pydantic.{error["type"]}')
|
||||||
if not ctx:
|
if custom_message:
|
||||||
error['msg'] = custom_message
|
ctx = error.get('ctx')
|
||||||
else:
|
if not ctx:
|
||||||
error['msg'] = custom_message.format(**ctx)
|
error['msg'] = custom_message
|
||||||
ctx_error = ctx.get('error')
|
else:
|
||||||
if ctx_error:
|
ctx_error = ctx.get('error')
|
||||||
error['ctx']['error'] = (
|
if ctx_error:
|
||||||
ctx_error.__str__().replace("'", '"') if isinstance(ctx_error, Exception) else None
|
error['msg'] = custom_message.format(**ctx)
|
||||||
)
|
error['ctx']['error'] = (
|
||||||
|
ctx_error.__str__().replace("'", '"') if isinstance(ctx_error, Exception) else None
|
||||||
|
)
|
||||||
errors.append(error)
|
errors.append(error)
|
||||||
error = errors[0]
|
error = errors[0]
|
||||||
if error.get('type') == 'json_invalid':
|
if error.get('type') == 'json_invalid':
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from backend.core.conf import settings
|
||||||
|
from backend.core.path_conf import LOCALE_DIR
|
||||||
|
|
||||||
|
|
||||||
|
class I18n:
|
||||||
|
"""国际化管理器"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.locales: dict[str, dict[str, Any]] = {}
|
||||||
|
self.current_language: str = settings.I18N_DEFAULT_LANGUAGE
|
||||||
|
|
||||||
|
def load_locales(self):
|
||||||
|
"""加载语言文本"""
|
||||||
|
patterns = [
|
||||||
|
os.path.join(LOCALE_DIR, '*.json'),
|
||||||
|
os.path.join(LOCALE_DIR, '*.yaml'),
|
||||||
|
os.path.join(LOCALE_DIR, '*.yml'),
|
||||||
|
]
|
||||||
|
|
||||||
|
lang_files = []
|
||||||
|
|
||||||
|
for pattern in patterns:
|
||||||
|
lang_files.extend(glob.glob(pattern))
|
||||||
|
|
||||||
|
for lang_file in lang_files:
|
||||||
|
with open(lang_file, 'r', encoding='utf-8') as f:
|
||||||
|
lang = Path(lang_file).stem
|
||||||
|
file_type = Path(lang_file).suffix[1:]
|
||||||
|
match file_type:
|
||||||
|
case 'json':
|
||||||
|
self.locales[lang] = json.loads(f.read())
|
||||||
|
case 'yaml' | 'yml':
|
||||||
|
self.locales[lang] = yaml.full_load(f.read())
|
||||||
|
|
||||||
|
def t(self, key: str, default: Any | None = None, **kwargs) -> str:
|
||||||
|
"""
|
||||||
|
翻译函数
|
||||||
|
|
||||||
|
:param key: 目标文本键,支持点分隔,例如 'response.success'
|
||||||
|
:param default: 目标语言文本不存在时的默认文本
|
||||||
|
:param kwargs: 目标文本中的变量参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
keys = key.split('.')
|
||||||
|
|
||||||
|
try:
|
||||||
|
translation = self.locales[self.current_language]
|
||||||
|
except KeyError:
|
||||||
|
keys = 'error.language_not_found'
|
||||||
|
translation = self.locales[settings.I18N_DEFAULT_LANGUAGE]
|
||||||
|
|
||||||
|
for k in keys:
|
||||||
|
if isinstance(translation, dict) and k in list(translation.keys()):
|
||||||
|
translation = translation[k]
|
||||||
|
else:
|
||||||
|
# Pydantic 兼容
|
||||||
|
if keys[0] == 'pydantic':
|
||||||
|
translation = None
|
||||||
|
else:
|
||||||
|
translation = key
|
||||||
|
|
||||||
|
if translation and kwargs:
|
||||||
|
translation = translation.format(**kwargs)
|
||||||
|
|
||||||
|
return translation or default
|
||||||
|
|
||||||
|
|
||||||
|
# 创建 i18n 单例
|
||||||
|
i18n = I18n()
|
||||||
|
|
||||||
|
# 创建翻译函数实例
|
||||||
|
t = i18n.t
|
||||||
+24
-7
@@ -3,6 +3,7 @@
|
|||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from asgi_correlation_id import correlation_id
|
from asgi_correlation_id import correlation_id
|
||||||
@@ -36,6 +37,18 @@ class InterceptHandler(logging.Handler):
|
|||||||
logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
|
logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
|
||||||
|
|
||||||
|
|
||||||
|
def default_formatter(record):
|
||||||
|
"""默认日志格式化程序"""
|
||||||
|
|
||||||
|
# 重写 sqlalchemy echo 输出
|
||||||
|
# https://github.com/sqlalchemy/sqlalchemy/discussions/12791
|
||||||
|
record_name = record['name'] or ''
|
||||||
|
if record_name.startswith('sqlalchemy'):
|
||||||
|
record['message'] = re.sub(r'\s+', ' ', record['message']).strip()
|
||||||
|
|
||||||
|
return settings.LOG_FORMAT if settings.LOG_FORMAT.endswith('\n') else f'{settings.LOG_FORMAT}\n'
|
||||||
|
|
||||||
|
|
||||||
def setup_logging() -> None:
|
def setup_logging() -> None:
|
||||||
"""
|
"""
|
||||||
设置日志处理器
|
设置日志处理器
|
||||||
@@ -48,9 +61,11 @@ def setup_logging() -> None:
|
|||||||
logging.root.handlers = [InterceptHandler()]
|
logging.root.handlers = [InterceptHandler()]
|
||||||
logging.root.setLevel(settings.LOG_STD_LEVEL)
|
logging.root.setLevel(settings.LOG_STD_LEVEL)
|
||||||
|
|
||||||
# 配置日志传播规则
|
|
||||||
for name in logging.root.manager.loggerDict.keys():
|
for name in logging.root.manager.loggerDict.keys():
|
||||||
|
# 清空所有默认日志处理器
|
||||||
logging.getLogger(name).handlers = []
|
logging.getLogger(name).handlers = []
|
||||||
|
|
||||||
|
# 配置日志传播规则
|
||||||
if 'uvicorn.access' in name or 'watchfiles.main' in name:
|
if 'uvicorn.access' in name or 'watchfiles.main' in name:
|
||||||
logging.getLogger(name).propagate = False
|
logging.getLogger(name).propagate = False
|
||||||
else:
|
else:
|
||||||
@@ -59,22 +74,24 @@ def setup_logging() -> None:
|
|||||||
# Debug log handlers
|
# Debug log handlers
|
||||||
# logging.debug(f'{logging.getLogger(name)}, {logging.getLogger(name).propagate}')
|
# logging.debug(f'{logging.getLogger(name)}, {logging.getLogger(name).propagate}')
|
||||||
|
|
||||||
|
# 移除 loguru 默认处理器
|
||||||
|
logger.remove()
|
||||||
|
|
||||||
# correlation_id 过滤器
|
# correlation_id 过滤器
|
||||||
# https://github.com/snok/asgi-correlation-id/issues/7
|
# https://github.com/snok/asgi-correlation-id/issues/7
|
||||||
def correlation_id_filter(record):
|
def correlation_id_filter(record):
|
||||||
cid = correlation_id.get(settings.TRACE_ID_LOG_DEFAULT_VALUE)
|
cid = correlation_id.get(settings.TRACE_ID_LOG_DEFAULT_VALUE)
|
||||||
record['correlation_id'] = cid[: settings.TRACE_ID_LOG_UUID_LENGTH]
|
record['correlation_id'] = cid[: settings.TRACE_ID_LOG_LENGTH]
|
||||||
return record
|
return record
|
||||||
|
|
||||||
# 配置 loguru 处理器
|
# 配置 loguru 处理器
|
||||||
logger.remove() # 移除默认处理器
|
|
||||||
logger.configure(
|
logger.configure(
|
||||||
handlers=[
|
handlers=[
|
||||||
{
|
{
|
||||||
'sink': sys.stdout,
|
'sink': sys.stdout,
|
||||||
'level': settings.LOG_STD_LEVEL,
|
'level': settings.LOG_STD_LEVEL,
|
||||||
|
'format': default_formatter,
|
||||||
'filter': lambda record: correlation_id_filter(record),
|
'filter': lambda record: correlation_id_filter(record),
|
||||||
'format': settings.LOG_STD_FORMAT,
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@@ -100,7 +117,7 @@ def set_custom_logfile() -> None:
|
|||||||
# 日志文件通用配置
|
# 日志文件通用配置
|
||||||
# https://loguru.readthedocs.io/en/stable/api/logger.html#loguru._logger.Logger.add
|
# https://loguru.readthedocs.io/en/stable/api/logger.html#loguru._logger.Logger.add
|
||||||
log_config = {
|
log_config = {
|
||||||
'format': settings.LOG_FILE_FORMAT,
|
'format': default_formatter,
|
||||||
'enqueue': True,
|
'enqueue': True,
|
||||||
'rotation': '00:00',
|
'rotation': '00:00',
|
||||||
'retention': '7 days',
|
'retention': '7 days',
|
||||||
@@ -110,7 +127,7 @@ def set_custom_logfile() -> None:
|
|||||||
# 标准输出文件
|
# 标准输出文件
|
||||||
logger.add(
|
logger.add(
|
||||||
str(log_access_file),
|
str(log_access_file),
|
||||||
level=settings.LOG_ACCESS_FILE_LEVEL,
|
level=settings.LOG_FILE_ACCESS_LEVEL,
|
||||||
filter=lambda record: record['level'].no <= 25,
|
filter=lambda record: record['level'].no <= 25,
|
||||||
backtrace=False,
|
backtrace=False,
|
||||||
diagnose=False,
|
diagnose=False,
|
||||||
@@ -120,7 +137,7 @@ def set_custom_logfile() -> None:
|
|||||||
# 标准错误文件
|
# 标准错误文件
|
||||||
logger.add(
|
logger.add(
|
||||||
str(log_error_file),
|
str(log_error_file),
|
||||||
level=settings.LOG_ERROR_FILE_LEVEL,
|
level=settings.LOG_FILE_ERROR_LEVEL,
|
||||||
filter=lambda record: record['level'].no >= 30,
|
filter=lambda record: record['level'].no >= 30,
|
||||||
backtrace=True,
|
backtrace=True,
|
||||||
diagnose=True,
|
diagnose=True,
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from asyncio import Queue
|
||||||
|
|
||||||
|
|
||||||
|
async def batch_dequeue(queue: Queue, max_items: int, timeout: float) -> list:
|
||||||
|
"""
|
||||||
|
从异步队列中获取多个项目
|
||||||
|
|
||||||
|
:param queue: 用于获取项目的 `asyncio.Queue` 队列
|
||||||
|
:param max_items: 从队列中获取的最大项目数量
|
||||||
|
:param timeout: 总的等待超时时间(秒)
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
items = []
|
||||||
|
|
||||||
|
async def collector():
|
||||||
|
while len(items) < max_items:
|
||||||
|
item = await queue.get()
|
||||||
|
items.append(item)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(collector(), timeout=timeout)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return items
|
||||||
@@ -4,6 +4,8 @@ import dataclasses
|
|||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
from backend.common.i18n import t
|
||||||
|
|
||||||
|
|
||||||
class CustomCodeBase(Enum):
|
class CustomCodeBase(Enum):
|
||||||
"""自定义状态码基类"""
|
"""自定义状态码基类"""
|
||||||
@@ -16,21 +18,22 @@ class CustomCodeBase(Enum):
|
|||||||
@property
|
@property
|
||||||
def msg(self) -> str:
|
def msg(self) -> str:
|
||||||
"""获取状态码信息"""
|
"""获取状态码信息"""
|
||||||
return self.value[1]
|
message = self.value[1]
|
||||||
|
return t(message)
|
||||||
|
|
||||||
|
|
||||||
class CustomResponseCode(CustomCodeBase):
|
class CustomResponseCode(CustomCodeBase):
|
||||||
"""自定义响应状态码"""
|
"""自定义响应状态码"""
|
||||||
|
|
||||||
HTTP_200 = (200, '请求成功')
|
HTTP_200 = (200, 'response.success')
|
||||||
HTTP_400 = (400, '请求错误')
|
HTTP_400 = (400, 'response.error')
|
||||||
HTTP_500 = (500, '服务器内部错误')
|
HTTP_500 = (500, '服务器内部错误')
|
||||||
|
|
||||||
|
|
||||||
class CustomErrorCode(CustomCodeBase):
|
class CustomErrorCode(CustomCodeBase):
|
||||||
"""自定义错误状态码"""
|
"""自定义错误状态码"""
|
||||||
|
|
||||||
CAPTCHA_ERROR = (40001, '验证码错误')
|
CAPTCHA_ERROR = (40001, 'error.captcha.error')
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
|
|||||||
+6
-103
@@ -5,108 +5,7 @@ from typing import Annotated
|
|||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, EmailStr, Field, validate_email
|
from pydantic import BaseModel, ConfigDict, EmailStr, Field, validate_email
|
||||||
|
|
||||||
from backend.core.conf import settings
|
from backend.utils.timezone import timezone
|
||||||
|
|
||||||
# 自定义验证错误信息,参考:
|
|
||||||
# https://github.com/pydantic/pydantic-core/blob/a5cb7382643415b716b1a7a5392914e50f726528/tests/test_errors.py#L266
|
|
||||||
# https://github.com/pydantic/pydantic/blob/caa78016433ec9b16a973f92f187a7b6bfde6cb5/docs/errors/errors.md?plain=1#L232
|
|
||||||
CUSTOM_VALIDATION_ERROR_MESSAGES = {
|
|
||||||
'no_such_attribute': "对象没有属性 '{attribute}'",
|
|
||||||
'json_invalid': '无效的 JSON: {error}',
|
|
||||||
'json_type': 'JSON 输入应为字符串、字节或字节数组',
|
|
||||||
'recursion_loop': '递归错误 - 检测到循环引用',
|
|
||||||
'model_type': '输入应为有效的字典或 {class_name} 的实例',
|
|
||||||
'model_attributes_type': '输入应为有效的字典或可提取字段的对象',
|
|
||||||
'dataclass_exact_type': '输入应为 {class_name} 的实例',
|
|
||||||
'dataclass_type': '输入应为字典或 {class_name} 的实例',
|
|
||||||
'missing': '字段为必填项',
|
|
||||||
'frozen_field': '字段已冻结',
|
|
||||||
'frozen_instance': '实例已冻结',
|
|
||||||
'extra_forbidden': '不允许额外的输入',
|
|
||||||
'invalid_key': '键应为字符串',
|
|
||||||
'get_attribute_error': '提取属性时出错: {error}',
|
|
||||||
'none_required': '输入应为 None',
|
|
||||||
'enum': '输入应为 {expected}',
|
|
||||||
'greater_than': '输入应大于 {gt}',
|
|
||||||
'greater_than_equal': '输入应大于或等于 {ge}',
|
|
||||||
'less_than': '输入应小于 {lt}',
|
|
||||||
'less_than_equal': '输入应小于或等于 {le}',
|
|
||||||
'finite_number': '输入应为有限数字',
|
|
||||||
'too_short': '{field_type} 在验证后应至少有 {min_length} 个项目,而不是 {actual_length}',
|
|
||||||
'too_long': '{field_type} 在验证后最多应有 {max_length} 个项目,而不是 {actual_length}',
|
|
||||||
'string_type': '输入应为有效的字符串',
|
|
||||||
'string_sub_type': '输入应为字符串,而不是 str 子类的实例',
|
|
||||||
'string_unicode': '输入应为有效的字符串,无法将原始数据解析为 Unicode 字符串',
|
|
||||||
'string_pattern_mismatch': "字符串应匹配模式 '{pattern}'",
|
|
||||||
'string_too_short': '字符串应至少有 {min_length} 个字符',
|
|
||||||
'string_too_long': '字符串最多应有 {max_length} 个字符',
|
|
||||||
'dict_type': '输入应为有效的字典',
|
|
||||||
'mapping_type': '输入应为有效的映射,错误: {error}',
|
|
||||||
'iterable_type': '输入应为可迭代对象',
|
|
||||||
'iteration_error': '迭代对象时出错,错误: {error}',
|
|
||||||
'list_type': '输入应为有效的列表',
|
|
||||||
'tuple_type': '输入应为有效的元组',
|
|
||||||
'set_type': '输入应为有效的集合',
|
|
||||||
'bool_type': '输入应为有效的布尔值',
|
|
||||||
'bool_parsing': '输入应为有效的布尔值,无法解释输入',
|
|
||||||
'int_type': '输入应为有效的整数',
|
|
||||||
'int_parsing': '输入应为有效的整数,无法将字符串解析为整数',
|
|
||||||
'int_parsing_size': '无法将输入字符串解析为整数,超出最大大小',
|
|
||||||
'int_from_float': '输入应为有效的整数,得到一个带有小数部分的数字',
|
|
||||||
'multiple_of': '输入应为 {multiple_of} 的倍数',
|
|
||||||
'float_type': '输入应为有效的数字',
|
|
||||||
'float_parsing': '输入应为有效的数字,无法将字符串解析为数字',
|
|
||||||
'bytes_type': '输入应为有效的字节',
|
|
||||||
'bytes_too_short': '数据应至少有 {min_length} 个字节',
|
|
||||||
'bytes_too_long': '数据最多应有 {max_length} 个字节',
|
|
||||||
'value_error': '值错误,{error}',
|
|
||||||
'assertion_error': '断言失败,{error}',
|
|
||||||
'literal_error': '输入应为 {expected}',
|
|
||||||
'date_type': '输入应为有效的日期',
|
|
||||||
'date_parsing': '输入应为 YYYY-MM-DD 格式的有效日期,{error}',
|
|
||||||
'date_from_datetime_parsing': '输入应为有效的日期或日期时间,{error}',
|
|
||||||
'date_from_datetime_inexact': '提供给日期的日期时间应具有零时间 - 例如为精确日期',
|
|
||||||
'date_past': '日期应为过去的时间',
|
|
||||||
'date_future': '日期应为未来的时间',
|
|
||||||
'time_type': '输入应为有效的时间',
|
|
||||||
'time_parsing': '输入应为有效的时间格式,{error}',
|
|
||||||
'datetime_type': '输入应为有效的日期时间',
|
|
||||||
'datetime_parsing': '输入应为有效的日期时间,{error}',
|
|
||||||
'datetime_object_invalid': '无效的日期时间对象,得到 {error}',
|
|
||||||
'datetime_past': '输入应为过去的时间',
|
|
||||||
'datetime_future': '输入应为未来的时间',
|
|
||||||
'timezone_naive': '输入不应包含时区信息',
|
|
||||||
'timezone_aware': '输入应包含时区信息',
|
|
||||||
'timezone_offset': '需要时区偏移为 {tz_expected},实际得到 {tz_actual}',
|
|
||||||
'time_delta_type': '输入应为有效的时间差',
|
|
||||||
'time_delta_parsing': '输入应为有效的时间差,{error}',
|
|
||||||
'frozen_set_type': '输入应为有效的冻结集合',
|
|
||||||
'is_instance_of': '输入应为 {class} 的实例',
|
|
||||||
'is_subclass_of': '输入应为 {class} 的子类',
|
|
||||||
'callable_type': '输入应为可调用对象',
|
|
||||||
'union_tag_invalid': "使用 {discriminator} 找到的输入标签 '{tag}' 与任何预期标签不匹配: {expected_tags}",
|
|
||||||
'union_tag_not_found': '无法使用区分器 {discriminator} 提取标签',
|
|
||||||
'arguments_type': '参数必须是元组、列表或字典',
|
|
||||||
'missing_argument': '缺少必需参数',
|
|
||||||
'unexpected_keyword_argument': '意外的关键字参数',
|
|
||||||
'missing_keyword_only_argument': '缺少必需的关键字专用参数',
|
|
||||||
'unexpected_positional_argument': '意外的位置参数',
|
|
||||||
'missing_positional_only_argument': '缺少必需的位置专用参数',
|
|
||||||
'multiple_argument_values': '为参数提供了多个值',
|
|
||||||
'url_type': 'URL 输入应为字符串或 URL',
|
|
||||||
'url_parsing': '输入应为有效的 URL,{error}',
|
|
||||||
'url_syntax_violation': '输入违反了严格的 URL 语法规则,{error}',
|
|
||||||
'url_too_long': 'URL 最多应有 {max_length} 个字符',
|
|
||||||
'url_scheme': 'URL 方案应为 {expected_schemes}',
|
|
||||||
'uuid_type': 'UUID 输入应为字符串、字节或 UUID 对象',
|
|
||||||
'uuid_parsing': '输入应为有效的 UUID,{error}',
|
|
||||||
'uuid_version': '预期 UUID 版本为 {expected_version}',
|
|
||||||
'decimal_type': '十进制输入应为整数、浮点数、字符串或 Decimal 对象',
|
|
||||||
'decimal_parsing': '输入应为有效的十进制数',
|
|
||||||
'decimal_max_digits': '十进制输入总共应不超过 {max_digits} 位数字',
|
|
||||||
'decimal_max_places': '十进制输入应不超过 {decimal_places} 位小数',
|
|
||||||
'decimal_whole_digits': '十进制输入在小数点前应不超过 {whole_digits} 位数字',
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomPhoneNumber = Annotated[str, Field(pattern=r'^1[3-9]\d{9}$')]
|
CustomPhoneNumber = Annotated[str, Field(pattern=r'^1[3-9]\d{9}$')]
|
||||||
|
|
||||||
@@ -124,5 +23,9 @@ class SchemaBase(BaseModel):
|
|||||||
|
|
||||||
model_config = ConfigDict(
|
model_config = ConfigDict(
|
||||||
use_enum_values=True,
|
use_enum_values=True,
|
||||||
json_encoders={datetime: lambda x: x.strftime(settings.DATETIME_FORMAT)},
|
json_encoders={
|
||||||
|
datetime: lambda x: timezone.to_str(timezone.from_datetime(x))
|
||||||
|
if x.tzinfo is not None
|
||||||
|
else timezone.to_str(x)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ async def jwt_authentication(token: str) -> GetUserInfoWithRelationDetail:
|
|||||||
user = GetUserInfoWithRelationDetail(**select_as_dict(current_user))
|
user = GetUserInfoWithRelationDetail(**select_as_dict(current_user))
|
||||||
await redis_client.setex(
|
await redis_client.setex(
|
||||||
f'{settings.JWT_USER_REDIS_PREFIX}:{user_id}',
|
f'{settings.JWT_USER_REDIS_PREFIX}:{user_id}',
|
||||||
settings.JWT_USER_REDIS_EXPIRE_SECONDS,
|
settings.TOKEN_EXPIRE_SECONDS,
|
||||||
user.model_dump_json(),
|
user.model_dump_json(),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ async def rbac_verify(request: Request, _token: str = DependsJwtAuth) -> None:
|
|||||||
# API 鉴权白名单
|
# API 鉴权白名单
|
||||||
if path in settings.TOKEN_REQUEST_PATH_EXCLUDE:
|
if path in settings.TOKEN_REQUEST_PATH_EXCLUDE:
|
||||||
return
|
return
|
||||||
|
for pattern in settings.TOKEN_REQUEST_PATH_EXCLUDE_PATTERN:
|
||||||
|
if pattern.match(path):
|
||||||
|
return
|
||||||
|
|
||||||
# JWT 授权状态强制校验
|
# JWT 授权状态强制校验
|
||||||
if not request.auth.scopes:
|
if not request.auth.scopes:
|
||||||
|
|||||||
+79
-50
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal, Pattern
|
||||||
|
|
||||||
from pydantic import model_validator
|
from pydantic import model_validator
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
@@ -19,27 +19,8 @@ class Settings(BaseSettings):
|
|||||||
case_sensitive=True,
|
case_sensitive=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# .env 环境
|
# .env 当前环境
|
||||||
ENVIRONMENT: Literal['dev', 'pro']
|
ENVIRONMENT: Literal['dev', 'prod']
|
||||||
|
|
||||||
# .env 数据库
|
|
||||||
DATABASE_TYPE: Literal['mysql', 'postgresql']
|
|
||||||
DATABASE_HOST: str
|
|
||||||
DATABASE_PORT: int
|
|
||||||
DATABASE_USER: str
|
|
||||||
DATABASE_PASSWORD: str
|
|
||||||
|
|
||||||
# .env Redis
|
|
||||||
REDIS_HOST: str
|
|
||||||
REDIS_PORT: int
|
|
||||||
REDIS_PASSWORD: str
|
|
||||||
REDIS_DATABASE: int
|
|
||||||
|
|
||||||
# .env Token
|
|
||||||
TOKEN_SECRET_KEY: str # 密钥 secrets.token_urlsafe(32)
|
|
||||||
|
|
||||||
# .env 操作日志加密密钥
|
|
||||||
OPERA_LOG_ENCRYPT_SECRET_KEY: str # 密钥 os.urandom(32), 需使用 bytes.hex() 方法转换为 str
|
|
||||||
|
|
||||||
# FastAPI
|
# FastAPI
|
||||||
FASTAPI_API_V1_PATH: str = '/api/v1'
|
FASTAPI_API_V1_PATH: str = '/api/v1'
|
||||||
@@ -51,15 +32,31 @@ class Settings(BaseSettings):
|
|||||||
FASTAPI_OPENAPI_URL: str | None = '/openapi'
|
FASTAPI_OPENAPI_URL: str | None = '/openapi'
|
||||||
FASTAPI_STATIC_FILES: bool = True
|
FASTAPI_STATIC_FILES: bool = True
|
||||||
|
|
||||||
|
# .env 数据库
|
||||||
|
DATABASE_TYPE: Literal['mysql', 'postgresql']
|
||||||
|
DATABASE_HOST: str
|
||||||
|
DATABASE_PORT: int
|
||||||
|
DATABASE_USER: str
|
||||||
|
DATABASE_PASSWORD: str
|
||||||
|
|
||||||
# 数据库
|
# 数据库
|
||||||
DATABASE_ECHO: bool | Literal['debug'] = False
|
DATABASE_ECHO: bool | Literal['debug'] = False
|
||||||
DATABASE_POOL_ECHO: bool | Literal['debug'] = False
|
DATABASE_POOL_ECHO: bool | Literal['debug'] = False
|
||||||
DATABASE_SCHEMA: str = 'fba'
|
DATABASE_SCHEMA: str = 'fba'
|
||||||
DATABASE_CHARSET: str = 'utf8mb4'
|
DATABASE_CHARSET: str = 'utf8mb4'
|
||||||
|
|
||||||
|
# .env Redis
|
||||||
|
REDIS_HOST: str
|
||||||
|
REDIS_PORT: int
|
||||||
|
REDIS_PASSWORD: str
|
||||||
|
REDIS_DATABASE: int
|
||||||
|
|
||||||
# Redis
|
# Redis
|
||||||
REDIS_TIMEOUT: int = 5
|
REDIS_TIMEOUT: int = 5
|
||||||
|
|
||||||
|
# .env Token
|
||||||
|
TOKEN_SECRET_KEY: str # 密钥 secrets.token_urlsafe(32)
|
||||||
|
|
||||||
# Token
|
# Token
|
||||||
TOKEN_ALGORITHM: str = 'HS256'
|
TOKEN_ALGORITHM: str = 'HS256'
|
||||||
TOKEN_EXPIRE_SECONDS: int = 60 * 60 * 24 # 1 天
|
TOKEN_EXPIRE_SECONDS: int = 60 * 60 * 24 # 1 天
|
||||||
@@ -70,12 +67,13 @@ class Settings(BaseSettings):
|
|||||||
TOKEN_REFRESH_REDIS_PREFIX: str = 'fba:refresh_token'
|
TOKEN_REFRESH_REDIS_PREFIX: str = 'fba:refresh_token'
|
||||||
TOKEN_REQUEST_PATH_EXCLUDE: list[str] = [ # JWT / RBAC 路由白名单
|
TOKEN_REQUEST_PATH_EXCLUDE: list[str] = [ # JWT / RBAC 路由白名单
|
||||||
f'{FASTAPI_API_V1_PATH}/auth/login',
|
f'{FASTAPI_API_V1_PATH}/auth/login',
|
||||||
f'{FASTAPI_API_V1_PATH}/auth/logout',
|
]
|
||||||
|
TOKEN_REQUEST_PATH_EXCLUDE_PATTERN: list[Pattern[str]] = [ # JWT / RBAC 路由白名单(正则)
|
||||||
|
rf'^{FASTAPI_API_V1_PATH}/monitors/(redis|server)$',
|
||||||
]
|
]
|
||||||
|
|
||||||
# JWT
|
# JWT
|
||||||
JWT_USER_REDIS_PREFIX: str = 'fba:user'
|
JWT_USER_REDIS_PREFIX: str = 'fba:user'
|
||||||
JWT_USER_REDIS_EXPIRE_SECONDS: int = 60 * 60 * 24 * 7 # 7 天
|
|
||||||
|
|
||||||
# RBAC
|
# RBAC
|
||||||
RBAC_ROLE_MENU_MODE: bool = True
|
RBAC_ROLE_MENU_MODE: bool = True
|
||||||
@@ -88,7 +86,11 @@ class Settings(BaseSettings):
|
|||||||
COOKIE_REFRESH_TOKEN_KEY: str = 'fba_refresh_token'
|
COOKIE_REFRESH_TOKEN_KEY: str = 'fba_refresh_token'
|
||||||
COOKIE_REFRESH_TOKEN_EXPIRE_SECONDS: int = 60 * 60 * 24 * 7 # 7 天
|
COOKIE_REFRESH_TOKEN_EXPIRE_SECONDS: int = 60 * 60 * 24 * 7 # 7 天
|
||||||
|
|
||||||
# 数据权限配置
|
# 验证码
|
||||||
|
CAPTCHA_LOGIN_REDIS_PREFIX: str = 'fba:login:captcha'
|
||||||
|
CAPTCHA_LOGIN_EXPIRE_SECONDS: int = 60 * 5 # 3 分钟
|
||||||
|
|
||||||
|
# 数据权限
|
||||||
DATA_PERMISSION_MODELS: dict[str, str] = { # 允许进行数据过滤的 SQLA 模型,它必须以模块字符串的方式定义
|
DATA_PERMISSION_MODELS: dict[str, str] = { # 允许进行数据过滤的 SQLA 模型,它必须以模块字符串的方式定义
|
||||||
'部门': 'backend.app.admin.model.Dept',
|
'部门': 'backend.app.admin.model.Dept',
|
||||||
}
|
}
|
||||||
@@ -142,22 +144,27 @@ class Settings(BaseSettings):
|
|||||||
IP_LOCATION_REDIS_PREFIX: str = 'fba:ip:location'
|
IP_LOCATION_REDIS_PREFIX: str = 'fba:ip:location'
|
||||||
IP_LOCATION_EXPIRE_SECONDS: int = 60 * 60 * 24 # 1 天
|
IP_LOCATION_EXPIRE_SECONDS: int = 60 * 60 * 24 # 1 天
|
||||||
|
|
||||||
# 日志(Trace ID)
|
# Trace ID
|
||||||
TRACE_ID_REQUEST_HEADER_KEY: str = 'X-Request-ID'
|
TRACE_ID_REQUEST_HEADER_KEY: str = 'X-Request-ID'
|
||||||
|
TRACE_ID_LOG_LENGTH: int = 32 # UUID 长度,必须小于等于 32
|
||||||
TRACE_ID_LOG_DEFAULT_VALUE: str = '-'
|
TRACE_ID_LOG_DEFAULT_VALUE: str = '-'
|
||||||
TRACE_ID_LOG_UUID_LENGTH: int = 32 # UUID 长度,必须小于等于 32
|
|
||||||
|
# 日志
|
||||||
|
LOG_FORMAT: str = (
|
||||||
|
'<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</> | <lvl>{level: <8}</> | <cyan>{correlation_id}</> | <lvl>{message}</>'
|
||||||
|
)
|
||||||
|
|
||||||
# 日志(控制台)
|
# 日志(控制台)
|
||||||
LOG_STD_LEVEL: str = 'INFO'
|
LOG_STD_LEVEL: str = 'INFO'
|
||||||
LOG_STD_FORMAT: str = (
|
|
||||||
'<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</> | <lvl>{level: <8}</> | <cyan>{correlation_id}</> | <lvl>{message}</>'
|
|
||||||
)
|
|
||||||
# 日志(文件)
|
# 日志(文件)
|
||||||
LOG_ACCESS_FILE_LEVEL: str = 'INFO'
|
LOG_FILE_ACCESS_LEVEL: str = 'INFO'
|
||||||
LOG_ERROR_FILE_LEVEL: str = 'ERROR'
|
LOG_FILE_ERROR_LEVEL: str = 'ERROR'
|
||||||
LOG_ACCESS_FILENAME: str = 'fba_access.log'
|
LOG_ACCESS_FILENAME: str = 'fba_access.log'
|
||||||
LOG_ERROR_FILENAME: str = 'fba_error.log'
|
LOG_ERROR_FILENAME: str = 'fba_error.log'
|
||||||
LOG_FILE_FORMAT: str = '{time:YYYY-MM-DD HH:mm:ss.SSS} | <lvl>{level: <8}</> | {correlation_id} | <lvl>{message}</>'
|
|
||||||
|
# .env 操作日志
|
||||||
|
OPERA_LOG_ENCRYPT_SECRET_KEY: str # 密钥 os.urandom(32), 需使用 bytes.hex() 方法转换为 str
|
||||||
|
|
||||||
# 操作日志
|
# 操作日志
|
||||||
OPERA_LOG_PATH_EXCLUDE: list[str] = [
|
OPERA_LOG_PATH_EXCLUDE: list[str] = [
|
||||||
@@ -176,27 +183,20 @@ class Settings(BaseSettings):
|
|||||||
'new_password',
|
'new_password',
|
||||||
'confirm_password',
|
'confirm_password',
|
||||||
]
|
]
|
||||||
|
OPERA_LOG_QUEUE_BATCH_CONSUME_SIZE: int = 100
|
||||||
|
OPERA_LOG_QUEUE_TIMEOUT: int = 60 # 1 分钟
|
||||||
|
|
||||||
# Plugin 配置
|
# Plugin 配置
|
||||||
PLUGIN_PIP_CHINA: bool = True
|
PLUGIN_PIP_CHINA: bool = True
|
||||||
PLUGIN_PIP_INDEX_URL: str = 'https://mirrors.aliyun.com/pypi/simple/'
|
PLUGIN_PIP_INDEX_URL: str = 'https://mirrors.aliyun.com/pypi/simple/'
|
||||||
PLUGIN_REDIS_PREFIX: str = 'fba:plugin'
|
PLUGIN_REDIS_PREFIX: str = 'fba:plugin'
|
||||||
|
|
||||||
# App Admin
|
# I18n 配置
|
||||||
# .env OAuth2
|
I18N_DEFAULT_LANGUAGE: str = 'zh-CN'
|
||||||
OAUTH2_GITHUB_CLIENT_ID: str
|
|
||||||
OAUTH2_GITHUB_CLIENT_SECRET: str
|
|
||||||
OAUTH2_LINUX_DO_CLIENT_ID: str
|
|
||||||
OAUTH2_LINUX_DO_CLIENT_SECRET: str
|
|
||||||
|
|
||||||
# OAuth2
|
##################################################
|
||||||
OAUTH2_FRONTEND_REDIRECT_URI: str = 'http://localhost:5173/oauth2/callback'
|
# [ App ] task
|
||||||
|
##################################################
|
||||||
# 验证码
|
|
||||||
CAPTCHA_LOGIN_REDIS_PREFIX: str = 'fba:login:captcha'
|
|
||||||
CAPTCHA_LOGIN_EXPIRE_SECONDS: int = 60 * 5 # 3 分钟
|
|
||||||
|
|
||||||
# App Task
|
|
||||||
# .env Redis
|
# .env Redis
|
||||||
CELERY_BROKER_REDIS_DATABASE: int
|
CELERY_BROKER_REDIS_DATABASE: int
|
||||||
|
|
||||||
@@ -212,18 +212,47 @@ class Settings(BaseSettings):
|
|||||||
CELERY_REDIS_PREFIX: str = 'fba:celery'
|
CELERY_REDIS_PREFIX: str = 'fba:celery'
|
||||||
CELERY_TASK_MAX_RETRIES: int = 5
|
CELERY_TASK_MAX_RETRIES: int = 5
|
||||||
|
|
||||||
# Plugin Code Generator
|
##################################################
|
||||||
|
# [ Plugin ] code_generator
|
||||||
|
##################################################
|
||||||
CODE_GENERATOR_DOWNLOAD_ZIP_FILENAME: str = 'fba_generator'
|
CODE_GENERATOR_DOWNLOAD_ZIP_FILENAME: str = 'fba_generator'
|
||||||
|
|
||||||
|
##################################################
|
||||||
|
# [ Plugin ] oauth2
|
||||||
|
##################################################
|
||||||
|
# .env
|
||||||
|
OAUTH2_GITHUB_CLIENT_ID: str
|
||||||
|
OAUTH2_GITHUB_CLIENT_SECRET: str
|
||||||
|
OAUTH2_LINUX_DO_CLIENT_ID: str
|
||||||
|
OAUTH2_LINUX_DO_CLIENT_SECRET: str
|
||||||
|
|
||||||
|
# 基础配置
|
||||||
|
OAUTH2_FRONTEND_REDIRECT_URI: str = 'http://localhost:5173/oauth2/callback'
|
||||||
|
|
||||||
|
##################################################
|
||||||
|
# [ Plugin ] email
|
||||||
|
##################################################
|
||||||
|
# .env
|
||||||
|
EMAIL_USERNAME: str
|
||||||
|
EMAIL_PASSWORD: str
|
||||||
|
|
||||||
|
# 基础配置
|
||||||
|
EMAIL_HOST: str = 'smtp.qq.com'
|
||||||
|
EMAIL_PORT: int = 465
|
||||||
|
EMAIL_SSL: bool = True
|
||||||
|
EMAIL_CAPTCHA_REDIS_PREFIX: str = 'fba:email:captcha'
|
||||||
|
EMAIL_CAPTCHA_EXPIRE_SECONDS: int = 60 * 3 # 3 分钟
|
||||||
|
|
||||||
@model_validator(mode='before')
|
@model_validator(mode='before')
|
||||||
@classmethod
|
@classmethod
|
||||||
def check_env(cls, values: Any) -> Any:
|
def check_env(cls, values: Any) -> Any:
|
||||||
"""检查环境变量"""
|
"""检查环境变量"""
|
||||||
if values.get('ENVIRONMENT') == 'pro':
|
if values.get('ENVIRONMENT') == 'prod':
|
||||||
# FastAPI
|
# FastAPI
|
||||||
values['FASTAPI_OPENAPI_URL'] = None
|
values['FASTAPI_OPENAPI_URL'] = None
|
||||||
values['FASTAPI_STATIC_FILES'] = False
|
values['FASTAPI_STATIC_FILES'] = False
|
||||||
# Task
|
|
||||||
|
# task
|
||||||
values['CELERY_BROKER'] = 'rabbitmq'
|
values['CELERY_BROKER'] = 'rabbitmq'
|
||||||
|
|
||||||
return values
|
return values
|
||||||
|
|||||||
@@ -17,8 +17,11 @@ STATIC_DIR = BASE_PATH / 'static'
|
|||||||
# 上传文件目录
|
# 上传文件目录
|
||||||
UPLOAD_DIR = STATIC_DIR / 'upload'
|
UPLOAD_DIR = STATIC_DIR / 'upload'
|
||||||
|
|
||||||
|
# 离线 IP 数据库路径
|
||||||
|
IP2REGION_XDB = STATIC_DIR / 'ip2region.xdb'
|
||||||
|
|
||||||
# 插件目录
|
# 插件目录
|
||||||
PLUGIN_DIR = BASE_PATH / 'plugin'
|
PLUGIN_DIR = BASE_PATH / 'plugin'
|
||||||
|
|
||||||
# 离线 IP 数据库路径
|
# 国际化文件目录
|
||||||
IP2REGION_XDB = STATIC_DIR / 'ip2region.xdb'
|
LOCALE_DIR = BASE_PATH / 'locale'
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from asyncio import create_task
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from typing import AsyncGenerator
|
from typing import AsyncGenerator
|
||||||
|
|
||||||
@@ -18,9 +19,10 @@ from backend.common.exception.exception_handler import register_exception
|
|||||||
from backend.common.log import set_custom_logfile, setup_logging
|
from backend.common.log import set_custom_logfile, setup_logging
|
||||||
from backend.core.conf import settings
|
from backend.core.conf import settings
|
||||||
from backend.core.path_conf import STATIC_DIR, UPLOAD_DIR
|
from backend.core.path_conf import STATIC_DIR, UPLOAD_DIR
|
||||||
from backend.database.db import create_table
|
from backend.database.db import create_tables
|
||||||
from backend.database.redis import redis_client
|
from backend.database.redis import redis_client
|
||||||
from backend.middleware.access_middleware import AccessMiddleware
|
from backend.middleware.access_middleware import AccessMiddleware
|
||||||
|
from backend.middleware.i18n_middleware import I18nMiddleware
|
||||||
from backend.middleware.jwt_auth_middleware import JwtAuthMiddleware
|
from backend.middleware.jwt_auth_middleware import JwtAuthMiddleware
|
||||||
from backend.middleware.opera_log_middleware import OperaLogMiddleware
|
from backend.middleware.opera_log_middleware import OperaLogMiddleware
|
||||||
from backend.middleware.state_middleware import StateMiddleware
|
from backend.middleware.state_middleware import StateMiddleware
|
||||||
@@ -40,7 +42,11 @@ async def register_init(app: FastAPI) -> AsyncGenerator[None, None]:
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
# 创建数据库表
|
# 创建数据库表
|
||||||
await create_table()
|
await create_tables()
|
||||||
|
|
||||||
|
# 初始化 redis
|
||||||
|
await redis_client.open()
|
||||||
|
|
||||||
# 初始化 limiter
|
# 初始化 limiter
|
||||||
await FastAPILimiter.init(
|
await FastAPILimiter.init(
|
||||||
redis=redis_client,
|
redis=redis_client,
|
||||||
@@ -48,12 +54,13 @@ async def register_init(app: FastAPI) -> AsyncGenerator[None, None]:
|
|||||||
http_callback=http_limit_callback,
|
http_callback=http_limit_callback,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 创建操作日志任务
|
||||||
|
create_task(OperaLogMiddleware.consumer())
|
||||||
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
# 关闭 redis 连接
|
# 关闭 redis 连接
|
||||||
await redis_client.close()
|
await redis_client.aclose()
|
||||||
# 关闭 limiter
|
|
||||||
await FastAPILimiter.close()
|
|
||||||
|
|
||||||
|
|
||||||
def register_app() -> FastAPI:
|
def register_app() -> FastAPI:
|
||||||
@@ -124,6 +131,9 @@ def register_middleware(app: FastAPI) -> None:
|
|||||||
on_error=JwtAuthMiddleware.auth_exception_handler,
|
on_error=JwtAuthMiddleware.auth_exception_handler,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# I18n
|
||||||
|
app.add_middleware(I18nMiddleware)
|
||||||
|
|
||||||
# CORS
|
# CORS
|
||||||
if settings.MIDDLEWARE_CORS:
|
if settings.MIDDLEWARE_CORS:
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
|||||||
yield session
|
yield session
|
||||||
|
|
||||||
|
|
||||||
async def create_table() -> None:
|
async def create_tables() -> None:
|
||||||
"""创建数据库表"""
|
"""创建数据库表"""
|
||||||
async with async_engine.begin() as coon:
|
async with async_engine.begin() as coon:
|
||||||
await coon.run_sync(MappedBase.metadata.create_all)
|
await coon.run_sync(MappedBase.metadata.create_all)
|
||||||
@@ -86,7 +86,11 @@ def uuid4_str() -> str:
|
|||||||
return str(uuid4())
|
return str(uuid4())
|
||||||
|
|
||||||
|
|
||||||
|
# SQLA 数据库链接
|
||||||
SQLALCHEMY_DATABASE_URL = create_database_url()
|
SQLALCHEMY_DATABASE_URL = create_database_url()
|
||||||
|
|
||||||
|
# SALA 异步引擎和会话
|
||||||
async_engine, async_db_session = create_async_engine_and_session(SQLALCHEMY_DATABASE_URL)
|
async_engine, async_db_session = create_async_engine_and_session(SQLALCHEMY_DATABASE_URL)
|
||||||
|
|
||||||
# Session Annotated
|
# Session Annotated
|
||||||
CurrentSession = Annotated[AsyncSession, Depends(get_db)]
|
CurrentSession = Annotated[AsyncSession, Depends(get_db)]
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"captcha": {
|
||||||
|
"error": "Captcha error",
|
||||||
|
"expired": "Captcha has expired, please try again"
|
||||||
|
},
|
||||||
|
"language_not_found": "Current language pack is not initialized or does not exist"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"error": "Request error",
|
||||||
|
"success": "Request success"
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"login": {
|
||||||
|
"oauth2_success": "Login success (OAuth2)",
|
||||||
|
"success": "Login success"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
error:
|
||||||
|
captcha:
|
||||||
|
error: 验证码错误
|
||||||
|
expired: 验证码已过期,请重新获取
|
||||||
|
language_not_found: 当前语言包未初始化或不存在
|
||||||
|
pydantic:
|
||||||
|
# 自定义验证错误信息,参考:
|
||||||
|
# https://github.com/pydantic/pydantic-core/blob/a5cb7382643415b716b1a7a5392914e50f726528/tests/test_errors.py#L266
|
||||||
|
# https://github.com/pydantic/pydantic/blob/caa78016433ec9b16a973f92f187a7b6bfde6cb5/docs/errors/errors.md?plain=1#L232
|
||||||
|
arguments_type: 参数必须是元组、列表或字典
|
||||||
|
assertion_error: '断言失败,{error}'
|
||||||
|
bool_parsing: 输入应为有效的布尔值,无法解释输入
|
||||||
|
bool_type: 输入应为有效的布尔值
|
||||||
|
bytes_too_long: '数据最多应有 {max_length} 个字节'
|
||||||
|
bytes_too_short: '数据应至少有 {min_length} 个字节'
|
||||||
|
bytes_type: 输入应为有效的字节
|
||||||
|
callable_type: 输入应为可调用对象
|
||||||
|
dataclass_exact_type: '输入应为 {class_name} 的实例'
|
||||||
|
dataclass_type: '输入应为字典或 {class_name} 的实例'
|
||||||
|
date_from_datetime_inexact: 提供给日期的日期时间应具有零时间 - 例如为精确日期
|
||||||
|
date_from_datetime_parsing: '输入应为有效的日期或日期时间,{error}'
|
||||||
|
date_future: 日期应为未来的时间
|
||||||
|
date_past: 日期应为过去的时间
|
||||||
|
date_parsing: '输入应为 YYYY-MM-DD 格式的有效日期,{error}'
|
||||||
|
date_type: 输入应为有效的日期
|
||||||
|
datetime_future: 输入应为未来的时间
|
||||||
|
datetime_object_invalid: '无效的日期时间对象,得到 {error}'
|
||||||
|
datetime_past: 输入应为过去的时间
|
||||||
|
datetime_parsing: '输入应为有效的日期时间,{error}'
|
||||||
|
datetime_type: 输入应为有效的日期时间
|
||||||
|
decimal_max_digits: '十进制输入总共应不超过 {max_digits} 位数字'
|
||||||
|
decimal_max_places: '十进制输入应不超过 {decimal_places} 位小数'
|
||||||
|
decimal_parsing: 输入应为有效的十进制数
|
||||||
|
decimal_type: 十进制输入应为整数、浮点数、字符串或 Decimal 对象
|
||||||
|
decimal_whole_digits: '十进制输入在小数点前应不超过 {whole_digits} 位数字'
|
||||||
|
dict_type: 输入应为有效的字典
|
||||||
|
email_parsing: '输入应为有效的邮箱地址,{error}'
|
||||||
|
email_type: 输入应为有效的邮箱地址
|
||||||
|
enum: '输入应为 {expected}'
|
||||||
|
extra_forbidden: 不允许额外的输入
|
||||||
|
finite_number: 输入应为有限数字
|
||||||
|
float_parsing: 输入应为有效的数字,无法将字符串解析为数字
|
||||||
|
float_type: 输入应为有效的数字
|
||||||
|
frozen_field: 字段已冻结
|
||||||
|
frozen_instance: 实例已冻结
|
||||||
|
frozen_set_type: 输入应为有效的冻结集合
|
||||||
|
get_attribute_error: '提取属性时出错: {error}'
|
||||||
|
greater_than: '输入应大于 {gt}'
|
||||||
|
greater_than_equal: '输入应大于或等于 {ge}'
|
||||||
|
int_from_float: 输入应为有效的整数,得到一个带有小数部分的数字
|
||||||
|
int_parsing: 输入应为有效的整数,无法将字符串解析为整数
|
||||||
|
int_parsing_size: 无法将输入字符串解析为整数,超出最大大小
|
||||||
|
int_type: 输入应为有效的整数
|
||||||
|
invalid_key: 键应为字符串
|
||||||
|
is_instance_of: '输入应为 {class} 的实例'
|
||||||
|
is_subclass_of: '输入应为 {class} 的子类'
|
||||||
|
iteration_error: '迭代对象时出错,错误: {error}'
|
||||||
|
iterable_type: 输入应为可迭代对象
|
||||||
|
json_invalid: '无效的 JSON: {error}'
|
||||||
|
json_type: JSON 输入应为字符串、字节或字节数组
|
||||||
|
less_than: '输入应小于 {lt}'
|
||||||
|
less_than_equal: '输入应小于或等于 {le}'
|
||||||
|
list_type: 输入应为有效的列表
|
||||||
|
literal_error: '输入应为 {expected}'
|
||||||
|
mapping_type: '输入应为有效的映射,错误: {error}'
|
||||||
|
missing: 字段为必填项
|
||||||
|
missing_argument: 缺少必需参数
|
||||||
|
missing_keyword_only_argument: 缺少必需的关键字专用参数
|
||||||
|
missing_positional_only_argument: 缺少必需的位置专用参数
|
||||||
|
model_attributes_type: 输入应为有效的字典或可提取字段的对象
|
||||||
|
model_type: '输入应为有效的字典或 {class_name} 的实例'
|
||||||
|
multiple_argument_values: 为参数提供了多个值
|
||||||
|
multiple_of: '输入应为 {multiple_of} 的倍数'
|
||||||
|
no_such_attribute: '对象没有属性 ''{attribute}'''
|
||||||
|
none_required: 输入应为 None
|
||||||
|
recursion_loop: 递归错误 - 检测到循环引用
|
||||||
|
set_type: 输入应为有效的集合
|
||||||
|
string_pattern_mismatch: '字符串应匹配模式 ''{pattern}'''
|
||||||
|
string_sub_type: 输入应为字符串,而不是 str 子类的实例
|
||||||
|
string_too_long: '字符串最多应有 {max_length} 个字符'
|
||||||
|
string_too_short: '字符串应至少有 {min_length} 个字符'
|
||||||
|
string_type: 输入应为有效的字符串
|
||||||
|
string_unicode: 输入应为有效的字符串,无法将原始数据解析为 Unicode 字符串
|
||||||
|
time_delta_parsing: '输入应为有效的时间差,{error}'
|
||||||
|
time_delta_type: 输入应为有效的时间差
|
||||||
|
time_parsing: '输入应为有效的时间格式,{error}'
|
||||||
|
time_type: 输入应为有效的时间
|
||||||
|
timezone_aware: 输入应包含时区信息
|
||||||
|
timezone_naive: 输入不应包含时区信息
|
||||||
|
timezone_offset: '需要时区偏移为 {tz_expected},实际得到 {tz_actual}'
|
||||||
|
too_long: '{field_type} 在验证后最多应有 {max_length} 个项目,而不是 {actual_length}'
|
||||||
|
too_short: '{field_type} 在验证后应至少有 {min_length} 个项目,而不是 {actual_length}'
|
||||||
|
tuple_type: 输入应为有效的元组
|
||||||
|
union_tag_invalid: '使用 {discriminator} 找到的输入标签 ''{tag}'' 与任何预期标签不匹配: {expected_tags}'
|
||||||
|
union_tag_not_found: '无法使用区分器 {discriminator} 提取标签'
|
||||||
|
unexpected_keyword_argument: 意外的关键字参数
|
||||||
|
unexpected_positional_argument: 意外的位置参数
|
||||||
|
url_parsing: '输入应为有效的 URL,{error}'
|
||||||
|
url_scheme: 'URL 方案应为 {expected_schemes}'
|
||||||
|
url_syntax_violation: '输入违反了严格的 URL 语法规则,{error}'
|
||||||
|
url_too_long: 'URL 最多应有 {max_length} 个字符'
|
||||||
|
url_type: URL 输入应为字符串或 URL
|
||||||
|
uuid_parsing: '输入应为有效的 UUID,{error}'
|
||||||
|
uuid_type: UUID 输入应为字符串、字节或 UUID 对象
|
||||||
|
uuid_version: '预期 UUID 版本为 {expected_version}'
|
||||||
|
value_error: '值错误,{error}'
|
||||||
|
response:
|
||||||
|
error: 请求错误
|
||||||
|
success: 请求成功
|
||||||
|
success:
|
||||||
|
login:
|
||||||
|
oauth2_success: 登录成功(OAuth2)
|
||||||
|
success: 登录成功
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from functools import lru_cache
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
from fastapi import Request, Response
|
||||||
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
|
|
||||||
|
from backend.common.i18n import i18n
|
||||||
|
|
||||||
|
|
||||||
|
class I18nMiddleware(BaseHTTPMiddleware):
|
||||||
|
"""国际化中间件"""
|
||||||
|
|
||||||
|
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
||||||
|
"""
|
||||||
|
处理请求并设置国际化语言
|
||||||
|
|
||||||
|
:param request: FastAPI 请求对象
|
||||||
|
:param call_next: 下一个中间件或路由处理函数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
language = self.get_current_language(request)
|
||||||
|
|
||||||
|
# 设置国际化语言
|
||||||
|
if language and i18n.current_language != language:
|
||||||
|
i18n.current_language = language
|
||||||
|
|
||||||
|
response = await call_next(request)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
@lru_cache(maxsize=128)
|
||||||
|
def get_current_language(self, request: Request) -> str | None:
|
||||||
|
"""
|
||||||
|
获取当前请求的语言偏好
|
||||||
|
|
||||||
|
:param request: FastAPI 请求对象
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
accept_language = request.headers.get('Accept-Language', '')
|
||||||
|
if not accept_language:
|
||||||
|
return None
|
||||||
|
|
||||||
|
languages = [lang.split(';')[0] for lang in accept_language.split(',')]
|
||||||
|
lang = languages[0].lower().strip()
|
||||||
|
|
||||||
|
# 语言映射
|
||||||
|
lang_mapping = {
|
||||||
|
'zh': 'zh-CN',
|
||||||
|
'zh-cn': 'zh-CN',
|
||||||
|
'zh-hans': 'zh-CN',
|
||||||
|
'en': 'en-US',
|
||||||
|
'en-us': 'en-US',
|
||||||
|
}
|
||||||
|
|
||||||
|
return lang_mapping.get(lang, lang)
|
||||||
@@ -59,8 +59,12 @@ class JwtAuthMiddleware(AuthenticationBackend):
|
|||||||
if not token:
|
if not token:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if request.url.path in settings.TOKEN_REQUEST_PATH_EXCLUDE:
|
path = request.url.path
|
||||||
|
if path in settings.TOKEN_REQUEST_PATH_EXCLUDE:
|
||||||
return None
|
return None
|
||||||
|
for pattern in settings.TOKEN_REQUEST_PATH_EXCLUDE_PATTERN:
|
||||||
|
if pattern.match(path):
|
||||||
|
return None
|
||||||
|
|
||||||
scheme, token = get_authorization_scheme_param(token)
|
scheme, token = get_authorization_scheme_param(token)
|
||||||
if scheme.lower() != 'bearer':
|
if scheme.lower() != 'bearer':
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from asyncio import create_task
|
from asyncio import Queue
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from asgiref.sync import sync_to_async
|
from asgiref.sync import sync_to_async
|
||||||
@@ -15,6 +15,7 @@ from backend.app.admin.schema.opera_log import CreateOperaLogParam
|
|||||||
from backend.app.admin.service.opera_log_service import opera_log_service
|
from backend.app.admin.service.opera_log_service import opera_log_service
|
||||||
from backend.common.enums import OperaLogCipherType, StatusType
|
from backend.common.enums import OperaLogCipherType, StatusType
|
||||||
from backend.common.log import log
|
from backend.common.log import log
|
||||||
|
from backend.common.queue import batch_dequeue
|
||||||
from backend.core.conf import settings
|
from backend.core.conf import settings
|
||||||
from backend.utils.encrypt import AESCipher, ItsDCipher, Md5Cipher
|
from backend.utils.encrypt import AESCipher, ItsDCipher, Md5Cipher
|
||||||
from backend.utils.trace_id import get_request_trace_id
|
from backend.utils.trace_id import get_request_trace_id
|
||||||
@@ -23,6 +24,8 @@ from backend.utils.trace_id import get_request_trace_id
|
|||||||
class OperaLogMiddleware(BaseHTTPMiddleware):
|
class OperaLogMiddleware(BaseHTTPMiddleware):
|
||||||
"""操作日志中间件"""
|
"""操作日志中间件"""
|
||||||
|
|
||||||
|
opera_log_queue: Queue = Queue(maxsize=100000)
|
||||||
|
|
||||||
async def dispatch(self, request: Request, call_next: Any) -> Response:
|
async def dispatch(self, request: Request, call_next: Any) -> Response:
|
||||||
"""
|
"""
|
||||||
处理请求并记录操作日志
|
处理请求并记录操作日志
|
||||||
@@ -39,7 +42,6 @@ class OperaLogMiddleware(BaseHTTPMiddleware):
|
|||||||
else:
|
else:
|
||||||
method = request.method
|
method = request.method
|
||||||
args = await self.get_request_args(request)
|
args = await self.get_request_args(request)
|
||||||
args = await self.desensitization(args)
|
|
||||||
|
|
||||||
# 执行请求
|
# 执行请求
|
||||||
elapsed = 0.0
|
elapsed = 0.0
|
||||||
@@ -108,7 +110,7 @@ class OperaLogMiddleware(BaseHTTPMiddleware):
|
|||||||
cost_time=elapsed, # 可能和日志存在微小差异(可忽略)
|
cost_time=elapsed, # 可能和日志存在微小差异(可忽略)
|
||||||
opera_time=request.state.start_time,
|
opera_time=request.state.start_time,
|
||||||
)
|
)
|
||||||
create_task(opera_log_service.create(obj=opera_log_in)) # noqa: ignore
|
await self.opera_log_queue.put(opera_log_in)
|
||||||
|
|
||||||
# 错误抛出
|
# 错误抛出
|
||||||
if error:
|
if error:
|
||||||
@@ -116,8 +118,7 @@ class OperaLogMiddleware(BaseHTTPMiddleware):
|
|||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@staticmethod
|
async def get_request_args(self, request: Request) -> dict[str, Any] | None:
|
||||||
async def get_request_args(request: Request) -> dict[str, Any]:
|
|
||||||
"""
|
"""
|
||||||
获取请求参数
|
获取请求参数
|
||||||
|
|
||||||
@@ -125,26 +126,35 @@ class OperaLogMiddleware(BaseHTTPMiddleware):
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
args = {}
|
args = {}
|
||||||
|
|
||||||
|
# 查询参数
|
||||||
query_params = dict(request.query_params)
|
query_params = dict(request.query_params)
|
||||||
if query_params:
|
if query_params:
|
||||||
args['query_params'] = query_params
|
args['query_params'] = await self.desensitization(query_params)
|
||||||
|
|
||||||
|
# 路径参数
|
||||||
path_params = request.path_params
|
path_params = request.path_params
|
||||||
if path_params:
|
if path_params:
|
||||||
args['path_params'] = path_params
|
args['path_params'] = await self.desensitization(path_params)
|
||||||
|
|
||||||
# Tip: .body() 必须在 .form() 之前获取
|
# Tip: .body() 必须在 .form() 之前获取
|
||||||
# https://github.com/encode/starlette/discussions/1933
|
# https://github.com/encode/starlette/discussions/1933
|
||||||
content_type = request.headers.get('Content-Type', '').split(';')
|
content_type = request.headers.get('Content-Type', '').split(';')
|
||||||
|
|
||||||
|
# 请求体
|
||||||
body_data = await request.body()
|
body_data = await request.body()
|
||||||
if body_data:
|
if body_data:
|
||||||
# 注意:非 json 数据默认使用 body 作为键
|
# 注意:非 json 数据默认使用 data 作为键
|
||||||
if 'application/json' not in content_type:
|
if 'application/json' not in content_type:
|
||||||
args['data'] = str(body_data)
|
args['data'] = str(body_data)
|
||||||
else:
|
else:
|
||||||
json_data = await request.json()
|
json_data = await request.json()
|
||||||
if isinstance(json_data, dict):
|
if isinstance(json_data, dict):
|
||||||
args['json'] = json_data
|
args['json'] = await self.desensitization(json_data)
|
||||||
else:
|
else:
|
||||||
args['data'] = str(body_data)
|
args['data'] = str(body_data)
|
||||||
|
|
||||||
|
# 表单参数
|
||||||
form_data = await request.form()
|
form_data = await request.form()
|
||||||
if len(form_data) > 0:
|
if len(form_data) > 0:
|
||||||
for k, v in form_data.items():
|
for k, v in form_data.items():
|
||||||
@@ -153,39 +163,51 @@ class OperaLogMiddleware(BaseHTTPMiddleware):
|
|||||||
else:
|
else:
|
||||||
form_data = {k: v}
|
form_data = {k: v}
|
||||||
if 'multipart/form-data' not in content_type:
|
if 'multipart/form-data' not in content_type:
|
||||||
args['x-www-form-urlencoded'] = form_data
|
args['x-www-form-urlencoded'] = await self.desensitization(form_data)
|
||||||
else:
|
else:
|
||||||
args['form-data'] = form_data
|
args['form-data'] = await self.desensitization(form_data)
|
||||||
|
|
||||||
return args
|
return None if not args else args
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@sync_to_async
|
@sync_to_async
|
||||||
def desensitization(args: dict[str, Any]) -> dict[str, Any] | None:
|
def desensitization(args: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
脱敏处理
|
脱敏处理
|
||||||
|
|
||||||
:param args: 需要脱敏的参数字典
|
:param args: 需要脱敏的参数字典
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
if not args:
|
|
||||||
return None
|
|
||||||
|
|
||||||
encrypt_type = settings.OPERA_LOG_ENCRYPT_TYPE
|
|
||||||
encrypt_key_include = settings.OPERA_LOG_ENCRYPT_KEY_INCLUDE
|
|
||||||
encrypt_secret_key = settings.OPERA_LOG_ENCRYPT_SECRET_KEY
|
|
||||||
|
|
||||||
for key, value in args.items():
|
for key, value in args.items():
|
||||||
if key in encrypt_key_include:
|
if key in settings.OPERA_LOG_ENCRYPT_KEY_INCLUDE:
|
||||||
match encrypt_type:
|
match settings.OPERA_LOG_ENCRYPT_TYPE:
|
||||||
case OperaLogCipherType.aes:
|
case OperaLogCipherType.aes:
|
||||||
args[key] = (AESCipher(encrypt_secret_key).encrypt(value)).hex()
|
args[key] = (AESCipher(settings.OPERA_LOG_ENCRYPT_SECRET_KEY).encrypt(value)).hex()
|
||||||
case OperaLogCipherType.md5:
|
case OperaLogCipherType.md5:
|
||||||
args[key] = Md5Cipher.encrypt(value)
|
args[key] = Md5Cipher.encrypt(value)
|
||||||
case OperaLogCipherType.itsdangerous:
|
case OperaLogCipherType.itsdangerous:
|
||||||
args[key] = ItsDCipher(encrypt_secret_key).encrypt(value)
|
args[key] = ItsDCipher(settings.OPERA_LOG_ENCRYPT_SECRET_KEY).encrypt(value)
|
||||||
case OperaLogCipherType.plan:
|
case OperaLogCipherType.plan:
|
||||||
pass
|
pass
|
||||||
case _:
|
case _:
|
||||||
args[key] = '******'
|
args[key] = '******'
|
||||||
|
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def consumer(cls) -> None:
|
||||||
|
"""操作日志消费者"""
|
||||||
|
while True:
|
||||||
|
logs = await batch_dequeue(
|
||||||
|
cls.opera_log_queue,
|
||||||
|
max_items=settings.OPERA_LOG_QUEUE_BATCH_CONSUME_SIZE,
|
||||||
|
timeout=settings.OPERA_LOG_QUEUE_TIMEOUT,
|
||||||
|
)
|
||||||
|
if logs:
|
||||||
|
try:
|
||||||
|
if settings.DATABASE_ECHO:
|
||||||
|
log.info('自动执行【操作日志批量创建】任务...')
|
||||||
|
await opera_log_service.bulk_create(objs=logs)
|
||||||
|
finally:
|
||||||
|
if not cls.opera_log_queue.empty():
|
||||||
|
cls.opera_log_queue.task_done()
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
包含代码生成的相关配置,详情查看:`generator/model/gen_business.py`
|
包含代码生成的相关配置,详情查看:`generator/model/gen_business.py`
|
||||||
|
|
||||||
### 2. 代码生成模型
|
### 2. 代码生成模型列
|
||||||
|
|
||||||
包含代码生成所需要的模型列信息,就像正常定义模型列一样,目前支持的功能有限
|
包含代码生成所需要的模型列信息,就像正常定义模型列一样,目前支持的功能有限
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ from fastapi import APIRouter
|
|||||||
|
|
||||||
from backend.core.conf import settings
|
from backend.core.conf import settings
|
||||||
from backend.plugin.code_generator.api.v1.business import router as business_router
|
from backend.plugin.code_generator.api.v1.business import router as business_router
|
||||||
from backend.plugin.code_generator.api.v1.column import router as model_router
|
from backend.plugin.code_generator.api.v1.code import router as gen_router
|
||||||
from backend.plugin.code_generator.api.v1.gen import router as gen_router
|
from backend.plugin.code_generator.api.v1.column import router as column_router
|
||||||
|
|
||||||
v1 = APIRouter(prefix=f'{settings.FASTAPI_API_V1_PATH}/gen', tags=['代码生成'])
|
v1 = APIRouter(prefix=f'{settings.FASTAPI_API_V1_PATH}/generates', tags=['代码生成'])
|
||||||
|
|
||||||
v1.include_router(business_router, prefix='/businesses')
|
v1.include_router(business_router, prefix='/businesses')
|
||||||
v1.include_router(model_router, prefix='/models')
|
v1.include_router(column_router, prefix='/columns')
|
||||||
v1.include_router(gen_router, prefix='/codes')
|
v1.include_router(gen_router, prefix='/codes')
|
||||||
|
|||||||
@@ -2,24 +2,32 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Path
|
from fastapi import APIRouter, Depends, Path, Query
|
||||||
|
|
||||||
|
from backend.common.pagination import DependsPagination, PageData, paging_data
|
||||||
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
||||||
from backend.common.security.jwt import DependsJwtAuth
|
from backend.common.security.jwt import DependsJwtAuth
|
||||||
from backend.common.security.permission import RequestPermission
|
from backend.common.security.permission import RequestPermission
|
||||||
from backend.common.security.rbac import DependsRBAC
|
from backend.common.security.rbac import DependsRBAC
|
||||||
|
from backend.database.db import CurrentSession
|
||||||
from backend.plugin.code_generator.schema.business import (
|
from backend.plugin.code_generator.schema.business import (
|
||||||
CreateGenBusinessParam,
|
CreateGenBusinessParam,
|
||||||
GetGenBusinessDetail,
|
GetGenBusinessDetail,
|
||||||
UpdateGenBusinessParam,
|
UpdateGenBusinessParam,
|
||||||
)
|
)
|
||||||
from backend.plugin.code_generator.schema.column import GetGenModelDetail
|
from backend.plugin.code_generator.schema.column import GetGenColumnDetail
|
||||||
from backend.plugin.code_generator.service.business_service import gen_business_service
|
from backend.plugin.code_generator.service.business_service import gen_business_service
|
||||||
from backend.plugin.code_generator.service.column_service import gen_model_service
|
from backend.plugin.code_generator.service.column_service import gen_column_service
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/all', summary='获取所有代码生成业务', dependencies=[DependsJwtAuth])
|
||||||
|
async def get_all_businesses() -> ResponseSchemaModel[list[GetGenBusinessDetail]]:
|
||||||
|
data = await gen_business_service.get_all()
|
||||||
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{pk}', summary='获取代码生成业务详情', dependencies=[DependsJwtAuth])
|
@router.get('/{pk}', summary='获取代码生成业务详情', dependencies=[DependsJwtAuth])
|
||||||
async def get_business(
|
async def get_business(
|
||||||
pk: Annotated[int, Path(description='业务 ID')],
|
pk: Annotated[int, Path(description='业务 ID')],
|
||||||
@@ -28,17 +36,28 @@ async def get_business(
|
|||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.get('', summary='获取所有代码生成业务', dependencies=[DependsJwtAuth])
|
@router.get(
|
||||||
async def get_all_businesses() -> ResponseSchemaModel[list[GetGenBusinessDetail]]:
|
'',
|
||||||
data = await gen_business_service.get_all()
|
summary='分页获取所有代码生成业务',
|
||||||
return response_base.success(data=data)
|
dependencies=[
|
||||||
|
DependsJwtAuth,
|
||||||
|
DependsPagination,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def get_businesses_paged(
|
||||||
|
db: CurrentSession,
|
||||||
|
table_name: Annotated[str | None, Query(description='代码生成业务表名称')] = None,
|
||||||
|
) -> ResponseSchemaModel[PageData[GetGenBusinessDetail]]:
|
||||||
|
business_select = await gen_business_service.get_select(table_name=table_name)
|
||||||
|
page_data = await paging_data(db, business_select)
|
||||||
|
return response_base.success(data=page_data)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{pk}/models', summary='获取代码生成业务所有模型', dependencies=[DependsJwtAuth])
|
@router.get('/{pk}/columns', summary='获取代码生成业务所有模型列', dependencies=[DependsJwtAuth])
|
||||||
async def get_business_all_models(
|
async def get_business_all_columns(
|
||||||
pk: Annotated[int, Path(description='业务 ID')],
|
pk: Annotated[int, Path(description='业务 ID')],
|
||||||
) -> ResponseSchemaModel[list[GetGenModelDetail]]:
|
) -> ResponseSchemaModel[list[GetGenColumnDetail]]:
|
||||||
data = await gen_model_service.get_models(business_id=pk)
|
data = await gen_column_service.get_columns(business_id=pk)
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -10,8 +10,8 @@ from backend.common.security.jwt import DependsJwtAuth
|
|||||||
from backend.common.security.permission import RequestPermission
|
from backend.common.security.permission import RequestPermission
|
||||||
from backend.common.security.rbac import DependsRBAC
|
from backend.common.security.rbac import DependsRBAC
|
||||||
from backend.core.conf import settings
|
from backend.core.conf import settings
|
||||||
from backend.plugin.code_generator.schema.gen import ImportParam
|
from backend.plugin.code_generator.schema.code import ImportParam
|
||||||
from backend.plugin.code_generator.service.gen_service import gen_service
|
from backend.plugin.code_generator.service.code_service import gen_service
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ router = APIRouter()
|
|||||||
@router.get('/tables', summary='获取数据库表')
|
@router.get('/tables', summary='获取数据库表')
|
||||||
async def get_all_tables(
|
async def get_all_tables(
|
||||||
table_schema: Annotated[str, Query(description='数据库名')] = 'fba',
|
table_schema: Annotated[str, Query(description='数据库名')] = 'fba',
|
||||||
) -> ResponseSchemaModel[list[str]]:
|
) -> ResponseSchemaModel[list[dict[str, str]]]:
|
||||||
data = await gen_service.get_tables(table_schema=table_schema)
|
data = await gen_service.get_tables(table_schema=table_schema)
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
@@ -8,47 +8,47 @@ from backend.common.response.response_schema import ResponseModel, ResponseSchem
|
|||||||
from backend.common.security.jwt import DependsJwtAuth
|
from backend.common.security.jwt import DependsJwtAuth
|
||||||
from backend.common.security.permission import RequestPermission
|
from backend.common.security.permission import RequestPermission
|
||||||
from backend.common.security.rbac import DependsRBAC
|
from backend.common.security.rbac import DependsRBAC
|
||||||
from backend.plugin.code_generator.schema.column import CreateGenModelParam, GetGenModelDetail, UpdateGenModelParam
|
from backend.plugin.code_generator.schema.column import CreateGenColumnParam, GetGenColumnDetail, UpdateGenColumnParam
|
||||||
from backend.plugin.code_generator.service.column_service import gen_model_service
|
from backend.plugin.code_generator.service.column_service import gen_column_service
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get('/types', summary='获取代码生成模型列类型', dependencies=[DependsJwtAuth])
|
@router.get('/types', summary='获取代码生成模型列类型', dependencies=[DependsJwtAuth])
|
||||||
async def get_model_types() -> ResponseSchemaModel[list[str]]:
|
async def get_column_types() -> ResponseSchemaModel[list[str]]:
|
||||||
model_types = await gen_model_service.get_types()
|
column_types = await gen_column_service.get_types()
|
||||||
return response_base.success(data=model_types)
|
return response_base.success(data=column_types)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{pk}', summary='获取代码生成模型详情', dependencies=[DependsJwtAuth])
|
@router.get('/{pk}', summary='获取代码生成模型列详情', dependencies=[DependsJwtAuth])
|
||||||
async def get_model(pk: Annotated[int, Path(description='模型 ID')]) -> ResponseSchemaModel[GetGenModelDetail]:
|
async def get_column(pk: Annotated[int, Path(description='模型列 ID')]) -> ResponseSchemaModel[GetGenColumnDetail]:
|
||||||
data = await gen_model_service.get(pk=pk)
|
data = await gen_column_service.get(pk=pk)
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
'',
|
'',
|
||||||
summary='创建代码生成模型',
|
summary='创建代码生成模型列',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('codegen:model:add')),
|
Depends(RequestPermission('codegen:column:add')),
|
||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def create_model(obj: CreateGenModelParam) -> ResponseModel:
|
async def create_column(obj: CreateGenColumnParam) -> ResponseModel:
|
||||||
await gen_model_service.create(obj=obj)
|
await gen_column_service.create(obj=obj)
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|
||||||
|
|
||||||
@router.put(
|
@router.put(
|
||||||
'/{pk}',
|
'/{pk}',
|
||||||
summary='更新代码生成模型',
|
summary='更新代码生成模型列',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('codegen:model:edit')),
|
Depends(RequestPermission('codegen:column:edit')),
|
||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_model(pk: Annotated[int, Path(description='模型 ID')], obj: UpdateGenModelParam) -> ResponseModel:
|
async def update_column(pk: Annotated[int, Path(description='模型列 ID')], obj: UpdateGenColumnParam) -> ResponseModel:
|
||||||
count = await gen_model_service.update(pk=pk, obj=obj)
|
count = await gen_column_service.update(pk=pk, obj=obj)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
return response_base.fail()
|
return response_base.fail()
|
||||||
@@ -56,14 +56,14 @@ async def update_model(pk: Annotated[int, Path(description='模型 ID')], obj: U
|
|||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
'/{pk}',
|
'/{pk}',
|
||||||
summary='删除代码生成模型',
|
summary='删除代码生成模型列',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('codegen:model:del')),
|
Depends(RequestPermission('codegen:column:del')),
|
||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def delete_model(pk: Annotated[int, Path(description='模型 ID')]) -> ResponseModel:
|
async def delete_column(pk: Annotated[int, Path(description='模型列 ID')]) -> ResponseModel:
|
||||||
count = await gen_model_service.delete(pk=pk)
|
count = await gen_column_service.delete(pk=pk)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
return response_base.fail()
|
return response_base.fail()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
|
|
||||||
|
from sqlalchemy import Select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy_crud_plus import CRUDPlus
|
from sqlalchemy_crud_plus import CRUDPlus
|
||||||
|
|
||||||
@@ -41,6 +42,20 @@ class CRUDGenBusiness(CRUDPlus[GenBusiness]):
|
|||||||
"""
|
"""
|
||||||
return await self.select_models(db)
|
return await self.select_models(db)
|
||||||
|
|
||||||
|
async def get_list(self, table_name: str | None) -> Select:
|
||||||
|
"""
|
||||||
|
获取所有代码生成业务
|
||||||
|
|
||||||
|
:param table_name: 业务表名
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
filters = {}
|
||||||
|
|
||||||
|
if table_name is not None:
|
||||||
|
filters['table_name__like'] = f'%{table_name}%'
|
||||||
|
|
||||||
|
return await self.select_order('id', 'desc', load_strategies={'gen_column': 'noload'}, **filters)
|
||||||
|
|
||||||
async def create(self, db: AsyncSession, obj: CreateGenBusinessParam) -> None:
|
async def create(self, db: AsyncSession, obj: CreateGenBusinessParam) -> None:
|
||||||
"""
|
"""
|
||||||
创建代码生成业务
|
创建代码生成业务
|
||||||
|
|||||||
+20
-16
@@ -2,7 +2,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
|
|
||||||
from sqlalchemy import Row, text
|
from sqlalchemy import Row, RowMapping, text
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from backend.core.conf import settings
|
from backend.core.conf import settings
|
||||||
@@ -12,7 +12,7 @@ class CRUDGen:
|
|||||||
"""代码生成 CRUD 类"""
|
"""代码生成 CRUD 类"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_all_tables(db: AsyncSession, table_schema: str) -> Sequence[str]:
|
async def get_all_tables(db: AsyncSession, table_schema: str) -> Sequence[RowMapping]:
|
||||||
"""
|
"""
|
||||||
获取所有表名
|
获取所有表名
|
||||||
|
|
||||||
@@ -22,20 +22,23 @@ class CRUDGen:
|
|||||||
"""
|
"""
|
||||||
if settings.DATABASE_TYPE == 'mysql':
|
if settings.DATABASE_TYPE == 'mysql':
|
||||||
sql = """
|
sql = """
|
||||||
SELECT table_name AS table_name FROM information_schema.tables
|
SELECT table_name AS table_name, table_comment AS table_comment
|
||||||
WHERE table_name NOT LIKE 'sys_gen_%'
|
FROM information_schema.tables
|
||||||
|
WHERE table_name NOT LIKE 'sys_gen_%'
|
||||||
AND table_schema = :table_schema;
|
AND table_schema = :table_schema;
|
||||||
"""
|
"""
|
||||||
else:
|
else:
|
||||||
sql = """
|
sql = """
|
||||||
SELECT table_name AS table_name FROM information_schema.tables
|
SELECT c.relname AS table_name, obj_description(c.oid) AS table_comment
|
||||||
WHERE table_name NOT LIKE 'sys_gen_%'
|
FROM pg_class c
|
||||||
AND table_catalog = :table_schema
|
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||||
AND table_schema = 'public'; -- schema 通常是 'public'
|
WHERE c.relkind = 'r'
|
||||||
|
AND n.nspname = 'public' -- schema 通常是 'public'
|
||||||
|
AND c.relname NOT LIKE 'sys_gen_%';
|
||||||
"""
|
"""
|
||||||
stmt = text(sql).bindparams(table_schema=table_schema)
|
stmt = text(sql).bindparams(table_schema=table_schema)
|
||||||
result = await db.execute(stmt)
|
result = await db.execute(stmt)
|
||||||
return result.scalars().all()
|
return result.mappings().all()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_table(db: AsyncSession, table_name: str) -> Row[tuple]:
|
async def get_table(db: AsyncSession, table_name: str) -> Row[tuple]:
|
||||||
@@ -48,18 +51,19 @@ class CRUDGen:
|
|||||||
"""
|
"""
|
||||||
if settings.DATABASE_TYPE == 'mysql':
|
if settings.DATABASE_TYPE == 'mysql':
|
||||||
sql = """
|
sql = """
|
||||||
SELECT table_name AS table_name, table_comment AS table_comment FROM information_schema.tables
|
SELECT table_name AS table_name, table_comment AS table_comment
|
||||||
|
FROM information_schema.tables
|
||||||
WHERE table_name NOT LIKE 'sys_gen_%'
|
WHERE table_name NOT LIKE 'sys_gen_%'
|
||||||
AND table_name = :table_name;
|
AND table_name = :table_name;
|
||||||
"""
|
"""
|
||||||
else:
|
else:
|
||||||
sql = """
|
sql = """
|
||||||
SELECT t.tablename AS table_name,
|
SELECT c.relname AS table_name, obj_description(c.oid) AS table_comment
|
||||||
pg_catalog.obj_description(t.tablename::regclass, 'pg_class') AS table_comment
|
FROM pg_class c
|
||||||
FROM pg_tables t
|
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||||
WHERE t.tablename NOT LIKE 'sys_gen_%'
|
WHERE c.relkind = 'r'
|
||||||
AND t.tablename = :table_name
|
AND n.nspname = 'public' -- schema 通常是 'public'
|
||||||
AND t.schemaname = 'public'; -- schema 通常是 'public'
|
AND c.relname NOT LIKE 'sys_gen_%';
|
||||||
"""
|
"""
|
||||||
stmt = text(sql).bindparams(table_name=table_name)
|
stmt = text(sql).bindparams(table_name=table_name)
|
||||||
result = await db.execute(stmt)
|
result = await db.execute(stmt)
|
||||||
@@ -6,11 +6,11 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
from sqlalchemy_crud_plus import CRUDPlus
|
from sqlalchemy_crud_plus import CRUDPlus
|
||||||
|
|
||||||
from backend.plugin.code_generator.model import GenColumn
|
from backend.plugin.code_generator.model import GenColumn
|
||||||
from backend.plugin.code_generator.schema.column import CreateGenModelParam, UpdateGenModelParam
|
from backend.plugin.code_generator.schema.column import CreateGenColumnParam, UpdateGenColumnParam
|
||||||
|
|
||||||
|
|
||||||
class CRUDGenModel(CRUDPlus[GenColumn]):
|
class CRUDGenColumn(CRUDPlus[GenColumn]):
|
||||||
"""代码生成模型 CRUD 类"""
|
"""代码生成模型列 CRUD 类"""
|
||||||
|
|
||||||
async def get(self, db: AsyncSession, pk: int) -> GenColumn | None:
|
async def get(self, db: AsyncSession, pk: int) -> GenColumn | None:
|
||||||
"""
|
"""
|
||||||
@@ -32,24 +32,24 @@ class CRUDGenModel(CRUDPlus[GenColumn]):
|
|||||||
"""
|
"""
|
||||||
return await self.select_models_order(db, sort_columns='sort', gen_business_id=business_id)
|
return await self.select_models_order(db, sort_columns='sort', gen_business_id=business_id)
|
||||||
|
|
||||||
async def create(self, db: AsyncSession, obj: CreateGenModelParam, pd_type: str | None) -> None:
|
async def create(self, db: AsyncSession, obj: CreateGenColumnParam, pd_type: str | None) -> None:
|
||||||
"""
|
"""
|
||||||
创建代码生成模型
|
创建代码生成模型列
|
||||||
|
|
||||||
:param db: 数据库会话
|
:param db: 数据库会话
|
||||||
:param obj: 创建代码生成模型参数
|
:param obj: 创建代码生成模型列参数
|
||||||
:param pd_type: Pydantic 类型
|
:param pd_type: Pydantic 类型
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
await self.create_model(db, obj, pd_type=pd_type)
|
await self.create_model(db, obj, pd_type=pd_type)
|
||||||
|
|
||||||
async def update(self, db: AsyncSession, pk: int, obj: UpdateGenModelParam, pd_type: str | None) -> int:
|
async def update(self, db: AsyncSession, pk: int, obj: UpdateGenColumnParam, pd_type: str | None) -> int:
|
||||||
"""
|
"""
|
||||||
更新代码生成模型
|
更新代码生成模型列
|
||||||
|
|
||||||
:param db: 数据库会话
|
:param db: 数据库会话
|
||||||
:param pk: 代码生成模型 ID
|
:param pk: 代码生成模型列 ID
|
||||||
:param obj: 更新代码生成模型参数
|
:param obj: 更新代码生成模型列参数
|
||||||
:param pd_type: Pydantic 类型
|
:param pd_type: Pydantic 类型
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
@@ -57,13 +57,13 @@ class CRUDGenModel(CRUDPlus[GenColumn]):
|
|||||||
|
|
||||||
async def delete(self, db: AsyncSession, pk: int) -> int:
|
async def delete(self, db: AsyncSession, pk: int) -> int:
|
||||||
"""
|
"""
|
||||||
删除代码生成模型
|
删除代码生成模型列
|
||||||
|
|
||||||
:param db: 数据库会话
|
:param db: 数据库会话
|
||||||
:param pk: 代码生成模型 ID
|
:param pk: 代码生成模型列 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.delete_model(db, pk)
|
return await self.delete_model(db, pk)
|
||||||
|
|
||||||
|
|
||||||
gen_model_dao: CRUDGenModel = CRUDGenModel(GenColumn)
|
gen_column_dao: CRUDGenColumn = CRUDGenColumn(GenColumn)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
from backend.common.enums import StrEnum
|
from backend.common.enums import StrEnum
|
||||||
|
|
||||||
|
|
||||||
class GenModelMySQLColumnType(StrEnum):
|
class GenMySQLColumnType(StrEnum):
|
||||||
"""代码生成模型列类型(MySQL)"""
|
"""代码生成模型列类型(MySQL)"""
|
||||||
|
|
||||||
# Python 类型映射
|
# Python 类型映射
|
||||||
@@ -68,7 +68,7 @@ class GenModelMySQLColumnType(StrEnum):
|
|||||||
YEAR = 'int'
|
YEAR = 'int'
|
||||||
|
|
||||||
|
|
||||||
class GenModelPostgreSQLColumnType(StrEnum):
|
class GenPostgreSQLColumnType(StrEnum):
|
||||||
"""代码生成模型列类型(PostgreSQL)"""
|
"""代码生成模型列类型(PostgreSQL)"""
|
||||||
|
|
||||||
# Python 类型映射
|
# Python 类型映射
|
||||||
|
|||||||
@@ -33,5 +33,5 @@ class GenBusiness(Base):
|
|||||||
remark: Mapped[str | None] = mapped_column(
|
remark: Mapped[str | None] = mapped_column(
|
||||||
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='备注'
|
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='备注'
|
||||||
)
|
)
|
||||||
# 代码生成业务模型一对多
|
# 代码生成业务模型列一对多
|
||||||
gen_column: Mapped[list['GenColumn']] = relationship(init=False, back_populates='gen_business')
|
gen_column: Mapped[list['GenColumn']] = relationship(init=False, back_populates='gen_business')
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class GenColumn(DataClassBase):
|
|||||||
is_pk: Mapped[bool] = mapped_column(default=False, comment='是否主键')
|
is_pk: Mapped[bool] = mapped_column(default=False, comment='是否主键')
|
||||||
is_nullable: Mapped[bool] = mapped_column(default=False, comment='是否可为空')
|
is_nullable: Mapped[bool] = mapped_column(default=False, comment='是否可为空')
|
||||||
|
|
||||||
# 代码生成业务模型一对多
|
# 代码生成业务模型列一对多
|
||||||
gen_business_id: Mapped[int] = mapped_column(
|
gen_business_id: Mapped[int] = mapped_column(
|
||||||
BigInteger, ForeignKey('gen_business.id', ondelete='CASCADE'), default=0, comment='代码生成业务ID'
|
BigInteger, ForeignKey('gen_business.id', ondelete='CASCADE'), default=0, comment='代码生成业务ID'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[plugin]
|
[plugin]
|
||||||
summary = '代码生成'
|
summary = '代码生成'
|
||||||
version = '0.0.1'
|
version = '0.0.3'
|
||||||
description = '生成通用业务代码'
|
description = '生成通用业务代码'
|
||||||
author = 'wu-clan'
|
author = 'wu-clan'
|
||||||
|
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ class GenBusinessSchemaBase(SchemaBase):
|
|||||||
table_name: str = Field(description='表名称(英文)')
|
table_name: str = Field(description='表名称(英文)')
|
||||||
doc_comment: str = Field(description='文档注释(用于函数/参数文档)')
|
doc_comment: str = Field(description='文档注释(用于函数/参数文档)')
|
||||||
table_comment: str | None = Field(None, description='表描述')
|
table_comment: str | None = Field(None, description='表描述')
|
||||||
class_name: str | None = Field(None, description='基础类名 (默认为英文表名称)')
|
class_name: str | None = Field(None, description='用于 python 代码基础类名')
|
||||||
schema_name: str | None = Field(None, description='Schema 名称 (默认为英文表名称)')
|
schema_name: str | None = Field(None, description='用于 python Schema 代码基础类名')
|
||||||
filename: str | None = Field(None, description='基础文件名(默认为英文表名称)')
|
filename: str | None = Field(None, description='用于 python 代码基础文件名')
|
||||||
default_datetime_column: bool = Field(True, description='是否存在默认时间列')
|
default_datetime_column: bool = Field(True, description='是否存在默认时间列')
|
||||||
api_version: str = Field('v1', description='代码生成 api 版本')
|
api_version: str = Field('v1', description='代码生成 api 版本')
|
||||||
gen_path: str | None = Field(None, description='代码生成路径(默认为 app 根路径)')
|
gen_path: str | None = Field(None, description='代码生成路径')
|
||||||
remark: str | None = Field(None, description='备注')
|
remark: str | None = Field(None, description='备注')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from backend.common.schema import SchemaBase
|
|||||||
from backend.plugin.code_generator.utils.type_conversion import sql_type_to_sqlalchemy
|
from backend.plugin.code_generator.utils.type_conversion import sql_type_to_sqlalchemy
|
||||||
|
|
||||||
|
|
||||||
class GenModelSchemaBase(SchemaBase):
|
class GenColumnSchemaBase(SchemaBase):
|
||||||
"""代码生成模型基础模型"""
|
"""代码生成模型基础模型"""
|
||||||
|
|
||||||
name: str = Field(description='列名称')
|
name: str = Field(description='列名称')
|
||||||
@@ -26,16 +26,16 @@ class GenModelSchemaBase(SchemaBase):
|
|||||||
return sql_type_to_sqlalchemy(v)
|
return sql_type_to_sqlalchemy(v)
|
||||||
|
|
||||||
|
|
||||||
class CreateGenModelParam(GenModelSchemaBase):
|
class CreateGenColumnParam(GenColumnSchemaBase):
|
||||||
"""创建代码生成模型参数"""
|
"""创建代码生成模型列参数"""
|
||||||
|
|
||||||
|
|
||||||
class UpdateGenModelParam(GenModelSchemaBase):
|
class UpdateGenColumnParam(GenColumnSchemaBase):
|
||||||
"""更新代码生成模型参数"""
|
"""更新代码生成模型列参数"""
|
||||||
|
|
||||||
|
|
||||||
class GetGenModelDetail(GenModelSchemaBase):
|
class GetGenColumnDetail(GenColumnSchemaBase):
|
||||||
"""获取代码生成模型详情"""
|
"""获取代码生成模型列详情"""
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
|
|
||||||
|
from sqlalchemy import Select
|
||||||
|
|
||||||
from backend.common.exception import errors
|
from backend.common.exception import errors
|
||||||
from backend.database.db import async_db_session
|
from backend.database.db import async_db_session
|
||||||
from backend.plugin.code_generator.crud.crud_business import gen_business_dao
|
from backend.plugin.code_generator.crud.crud_business import gen_business_dao
|
||||||
@@ -32,6 +34,16 @@ class GenBusinessService:
|
|||||||
async with async_db_session() as db:
|
async with async_db_session() as db:
|
||||||
return await gen_business_dao.get_all(db)
|
return await gen_business_dao.get_all(db)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_select(*, table_name: str) -> Select:
|
||||||
|
"""
|
||||||
|
获取代码生成业务列表查询条件
|
||||||
|
|
||||||
|
:param table_name: 业务表名
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return await gen_business_dao.get_list(table_name=table_name)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def create(*, obj: CreateGenBusinessParam) -> None:
|
async def create(*, obj: CreateGenBusinessParam) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
+28
-14
@@ -10,19 +10,20 @@ from typing import Sequence
|
|||||||
import aiofiles
|
import aiofiles
|
||||||
|
|
||||||
from pydantic.alias_generators import to_pascal
|
from pydantic.alias_generators import to_pascal
|
||||||
|
from sqlalchemy import RowMapping
|
||||||
|
|
||||||
from backend.common.exception import errors
|
from backend.common.exception import errors
|
||||||
from backend.core.path_conf import BASE_PATH
|
from backend.core.path_conf import BASE_PATH
|
||||||
from backend.database.db import async_db_session
|
from backend.database.db import async_db_session
|
||||||
from backend.plugin.code_generator.crud.crud_business import gen_business_dao
|
from backend.plugin.code_generator.crud.crud_business import gen_business_dao
|
||||||
from backend.plugin.code_generator.crud.crud_column import gen_model_dao
|
from backend.plugin.code_generator.crud.crud_code import gen_dao
|
||||||
from backend.plugin.code_generator.crud.crud_gen import gen_dao
|
from backend.plugin.code_generator.crud.crud_column import gen_column_dao
|
||||||
from backend.plugin.code_generator.model import GenBusiness
|
from backend.plugin.code_generator.model import GenBusiness
|
||||||
from backend.plugin.code_generator.schema.business import CreateGenBusinessParam
|
from backend.plugin.code_generator.schema.business import CreateGenBusinessParam
|
||||||
from backend.plugin.code_generator.schema.column import CreateGenModelParam
|
from backend.plugin.code_generator.schema.code import ImportParam
|
||||||
from backend.plugin.code_generator.schema.gen import ImportParam
|
from backend.plugin.code_generator.schema.column import CreateGenColumnParam
|
||||||
from backend.plugin.code_generator.service.column_service import gen_model_service
|
from backend.plugin.code_generator.service.column_service import gen_column_service
|
||||||
from backend.plugin.code_generator.utils.gen_template import gen_template
|
from backend.plugin.code_generator.utils.code_template import gen_template
|
||||||
from backend.plugin.code_generator.utils.type_conversion import sql_type_to_pydantic
|
from backend.plugin.code_generator.utils.type_conversion import sql_type_to_pydantic
|
||||||
|
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ class GenService:
|
|||||||
"""代码生成服务类"""
|
"""代码生成服务类"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_tables(*, table_schema: str) -> Sequence[str]:
|
async def get_tables(*, table_schema: str) -> Sequence[RowMapping]:
|
||||||
"""
|
"""
|
||||||
获取指定 schema 下的所有表名
|
获取指定 schema 下的所有表名
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ class GenService:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
async def import_business_and_model(*, obj: ImportParam) -> None:
|
async def import_business_and_model(*, obj: ImportParam) -> None:
|
||||||
"""
|
"""
|
||||||
导入业务和模型数据
|
导入业务和模型列数据
|
||||||
|
|
||||||
:param obj: 导入参数对象
|
:param obj: 导入参数对象
|
||||||
:return:
|
:return:
|
||||||
@@ -64,8 +65,8 @@ class GenService:
|
|||||||
table_name=table_name,
|
table_name=table_name,
|
||||||
doc_comment=table_info[1] or table_name.split('_')[-1],
|
doc_comment=table_info[1] or table_name.split('_')[-1],
|
||||||
table_comment=table_info[1],
|
table_comment=table_info[1],
|
||||||
class_name=table_name,
|
class_name=to_pascal(table_name),
|
||||||
schema_name=table_name,
|
schema_name=to_pascal(table_name),
|
||||||
filename=table_name,
|
filename=table_name,
|
||||||
).model_dump()
|
).model_dump()
|
||||||
)
|
)
|
||||||
@@ -76,9 +77,9 @@ class GenService:
|
|||||||
for column in column_info:
|
for column in column_info:
|
||||||
column_type = column[-1].split('(')[0].upper()
|
column_type = column[-1].split('(')[0].upper()
|
||||||
pd_type = sql_type_to_pydantic(column_type)
|
pd_type = sql_type_to_pydantic(column_type)
|
||||||
await gen_model_dao.create(
|
await gen_column_dao.create(
|
||||||
db,
|
db,
|
||||||
CreateGenModelParam(
|
CreateGenColumnParam(
|
||||||
name=column[0],
|
name=column[0],
|
||||||
comment=column[-2],
|
comment=column[-2],
|
||||||
type=column_type,
|
type=column_type,
|
||||||
@@ -99,7 +100,7 @@ class GenService:
|
|||||||
:param business: 业务对象
|
:param business: 业务对象
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
gen_models = await gen_model_service.get_models(business_id=business.id)
|
gen_models = await gen_column_service.get_columns(business_id=business.id)
|
||||||
if not gen_models:
|
if not gen_models:
|
||||||
raise errors.NotFoundError(msg='代码生成模型表为空')
|
raise errors.NotFoundError(msg='代码生成模型表为空')
|
||||||
|
|
||||||
@@ -126,7 +127,20 @@ class GenService:
|
|||||||
codes = {}
|
codes = {}
|
||||||
for tpl, code in tpl_code_map.items():
|
for tpl, code in tpl_code_map.items():
|
||||||
if tpl.startswith('python'):
|
if tpl.startswith('python'):
|
||||||
codes[tpl.replace('.jinja', '.py').split('/')[-1]] = code.encode('utf-8')
|
rootpath = f'fastapi_best_architecture/backend/app/{business.app_name}'
|
||||||
|
template_name = tpl.split('/')[-1]
|
||||||
|
match template_name:
|
||||||
|
case 'api.jinja':
|
||||||
|
filepath = f'{rootpath}/api/{business.api_version}/{business.app_name}.py'
|
||||||
|
case 'crud.jinja':
|
||||||
|
filepath = f'{rootpath}/crud/crud_{business.app_name}.py'
|
||||||
|
case 'model.jinja':
|
||||||
|
filepath = f'{rootpath}/model/{business.app_name}.py'
|
||||||
|
case 'schema.jinja':
|
||||||
|
filepath = f'{rootpath}/schema/{business.app_name}.py'
|
||||||
|
case 'service.jinja':
|
||||||
|
filepath = f'{rootpath}/service/{business.app_name}_service.py'
|
||||||
|
codes[filepath] = code.encode('utf-8')
|
||||||
|
|
||||||
return codes
|
return codes
|
||||||
|
|
||||||
@@ -4,93 +4,93 @@ from typing import Sequence
|
|||||||
|
|
||||||
from backend.common.exception import errors
|
from backend.common.exception import errors
|
||||||
from backend.database.db import async_db_session
|
from backend.database.db import async_db_session
|
||||||
from backend.plugin.code_generator.crud.crud_column import gen_model_dao
|
from backend.plugin.code_generator.crud.crud_column import gen_column_dao
|
||||||
from backend.plugin.code_generator.enums import GenModelMySQLColumnType
|
from backend.plugin.code_generator.enums import GenMySQLColumnType
|
||||||
from backend.plugin.code_generator.model import GenColumn
|
from backend.plugin.code_generator.model import GenColumn
|
||||||
from backend.plugin.code_generator.schema.column import CreateGenModelParam, UpdateGenModelParam
|
from backend.plugin.code_generator.schema.column import CreateGenColumnParam, UpdateGenColumnParam
|
||||||
from backend.plugin.code_generator.utils.type_conversion import sql_type_to_pydantic
|
from backend.plugin.code_generator.utils.type_conversion import sql_type_to_pydantic
|
||||||
|
|
||||||
|
|
||||||
class GenModelService:
|
class GenColumnService:
|
||||||
"""代码生成模型服务类"""
|
"""代码生成模型列服务类"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get(*, pk: int) -> GenColumn:
|
async def get(*, pk: int) -> GenColumn:
|
||||||
"""
|
"""
|
||||||
获取指定 ID 的模型
|
获取指定 ID 的模型列
|
||||||
|
|
||||||
:param pk: 模型 ID
|
:param pk: 模型列 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session() as db:
|
async with async_db_session() as db:
|
||||||
model = await gen_model_dao.get(db, pk)
|
column = await gen_column_dao.get(db, pk)
|
||||||
if not model:
|
if not column:
|
||||||
raise errors.NotFoundError(msg='代码生成模型不存在')
|
raise errors.NotFoundError(msg='代码生成模型列不存在')
|
||||||
return model
|
return column
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_types() -> list[str]:
|
async def get_types() -> list[str]:
|
||||||
"""获取所有 MySQL 列类型"""
|
"""获取所有 MySQL 列类型"""
|
||||||
types = GenModelMySQLColumnType.get_member_keys()
|
types = GenMySQLColumnType.get_member_keys()
|
||||||
types.sort()
|
types.sort()
|
||||||
return types
|
return types
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_models(*, business_id: int) -> Sequence[GenColumn]:
|
async def get_columns(*, business_id: int) -> Sequence[GenColumn]:
|
||||||
"""
|
"""
|
||||||
获取指定业务的所有模型
|
获取指定业务的所有模型列
|
||||||
|
|
||||||
:param business_id: 业务 ID
|
:param business_id: 业务 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session() as db:
|
async with async_db_session() as db:
|
||||||
return await gen_model_dao.get_all_by_business(db, business_id)
|
return await gen_column_dao.get_all_by_business(db, business_id)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def create(*, obj: CreateGenModelParam) -> None:
|
async def create(*, obj: CreateGenColumnParam) -> None:
|
||||||
"""
|
"""
|
||||||
创建模型
|
创建模型列
|
||||||
|
|
||||||
:param obj: 创建模型参数
|
:param obj: 创建模型列参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
gen_models = await gen_model_dao.get_all_by_business(db, obj.gen_business_id)
|
gen_columns = await gen_column_dao.get_all_by_business(db, obj.gen_business_id)
|
||||||
if obj.name in [gen_model.name for gen_model in gen_models]:
|
if obj.name in [gen_column.name for gen_column in gen_columns]:
|
||||||
raise errors.ForbiddenError(msg='禁止添加相同列到同一模型表')
|
raise errors.ForbiddenError(msg='模型列已存在')
|
||||||
|
|
||||||
pd_type = sql_type_to_pydantic(obj.type)
|
pd_type = sql_type_to_pydantic(obj.type)
|
||||||
await gen_model_dao.create(db, obj, pd_type=pd_type)
|
await gen_column_dao.create(db, obj, pd_type=pd_type)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update(*, pk: int, obj: UpdateGenModelParam) -> int:
|
async def update(*, pk: int, obj: UpdateGenColumnParam) -> int:
|
||||||
"""
|
"""
|
||||||
更新模型
|
更新模型列
|
||||||
|
|
||||||
:param pk: 模型 ID
|
:param pk: 模型列 ID
|
||||||
:param obj: 更新模型参数
|
:param obj: 更新模型列参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
model = await gen_model_dao.get(db, pk)
|
column = await gen_column_dao.get(db, pk)
|
||||||
if obj.name != model.name:
|
if obj.name != column.name:
|
||||||
gen_models = await gen_model_dao.get_all_by_business(db, obj.gen_business_id)
|
gen_columns = await gen_column_dao.get_all_by_business(db, obj.gen_business_id)
|
||||||
if obj.name in [gen_model.name for gen_model in gen_models]:
|
if obj.name in [gen_column.name for gen_column in gen_columns]:
|
||||||
raise errors.ConflictError(msg='模型列名已存在')
|
raise errors.ConflictError(msg='模型列名已存在')
|
||||||
|
|
||||||
pd_type = sql_type_to_pydantic(obj.type)
|
pd_type = sql_type_to_pydantic(obj.type)
|
||||||
return await gen_model_dao.update(db, pk, obj, pd_type=pd_type)
|
return await gen_column_dao.update(db, pk, obj, pd_type=pd_type)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def delete(*, pk: int) -> int:
|
async def delete(*, pk: int) -> int:
|
||||||
"""
|
"""
|
||||||
删除模型
|
删除模型列
|
||||||
|
|
||||||
:param pk: 模型 ID
|
:param pk: 模型列 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
return await gen_model_dao.delete(db, pk)
|
return await gen_column_dao.delete(db, pk)
|
||||||
|
|
||||||
|
|
||||||
gen_model_service: GenModelService = GenModelService()
|
gen_column_service: GenColumnService = GenColumnService()
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class CRUD{{ class_name }}(CRUDPlus[{{ schema_name }}]):
|
|||||||
|
|
||||||
async def get_list(self) -> Select:
|
async def get_list(self) -> Select:
|
||||||
"""获取{{ doc_comment }}列表"""
|
"""获取{{ doc_comment }}列表"""
|
||||||
return await self.select_order('created_time', 'desc')
|
return await self.select_order('id', 'desc')
|
||||||
|
|
||||||
async def get_all(self, db: AsyncSession) -> Sequence[{{ class_name }}]:
|
async def get_all(self, db: AsyncSession) -> Sequence[{{ class_name }}]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from backend.core.conf import settings
|
from backend.core.conf import settings
|
||||||
from backend.plugin.code_generator.enums import GenModelMySQLColumnType, GenModelPostgreSQLColumnType
|
from backend.plugin.code_generator.enums import GenMySQLColumnType, GenPostgreSQLColumnType
|
||||||
|
|
||||||
|
|
||||||
def sql_type_to_sqlalchemy(typing: str) -> str:
|
def sql_type_to_sqlalchemy(typing: str) -> str:
|
||||||
@@ -12,10 +12,10 @@ def sql_type_to_sqlalchemy(typing: str) -> str:
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
if settings.DATABASE_TYPE == 'mysql':
|
if settings.DATABASE_TYPE == 'mysql':
|
||||||
if typing in GenModelMySQLColumnType.get_member_keys():
|
if typing in GenMySQLColumnType.get_member_keys():
|
||||||
return typing
|
return typing
|
||||||
else:
|
else:
|
||||||
if typing in GenModelPostgreSQLColumnType.get_member_keys():
|
if typing in GenPostgreSQLColumnType.get_member_keys():
|
||||||
return typing
|
return typing
|
||||||
return 'String'
|
return 'String'
|
||||||
|
|
||||||
@@ -29,9 +29,9 @@ def sql_type_to_pydantic(typing: str) -> str:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if settings.DATABASE_TYPE == 'mysql':
|
if settings.DATABASE_TYPE == 'mysql':
|
||||||
return GenModelMySQLColumnType[typing].value
|
return GenMySQLColumnType[typing].value
|
||||||
if typing == 'CHARACTER VARYING': # postgresql 中 DDL VARCHAR 的别名
|
if typing == 'CHARACTER VARYING': # postgresql 中 DDL VARCHAR 的别名
|
||||||
return 'str'
|
return 'str'
|
||||||
return GenModelPostgreSQLColumnType[typing].value
|
return GenPostgreSQLColumnType[typing].value
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return 'str'
|
return 'str'
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
from sqlalchemy import Select
|
from sqlalchemy import Select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
@@ -22,6 +23,16 @@ class CRUDConfig(CRUDPlus[Config]):
|
|||||||
"""
|
"""
|
||||||
return await self.select_model_by_column(db, id=pk)
|
return await self.select_model_by_column(db, id=pk)
|
||||||
|
|
||||||
|
async def get_by_type(self, db: AsyncSession, type: str) -> Sequence[Config | None]:
|
||||||
|
"""
|
||||||
|
通过键名获取参数配置
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param type: 参数配置类型
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return await self.select_models(db, type=type)
|
||||||
|
|
||||||
async def get_by_key(self, db: AsyncSession, key: str) -> Config | None:
|
async def get_by_key(self, db: AsyncSession, key: str) -> Config | None:
|
||||||
"""
|
"""
|
||||||
通过键名获取参数配置
|
通过键名获取参数配置
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
## 参数配置
|
||||||
|
|
||||||
|
默认使用本地电子邮件配置
|
||||||
|
|
||||||
|
支持通过 `config 插件` 动态配置电子邮件参数,当动态配置 `EMAIL_STATUS` 为 `1` 时,将自动应用动态配置
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from backend.core.conf import settings
|
||||||
|
from backend.plugin.email.api.v1.email import router as email_router
|
||||||
|
|
||||||
|
v1 = APIRouter(prefix=f'{settings.FASTAPI_API_V1_PATH}/emails', tags=['电子邮件'])
|
||||||
|
|
||||||
|
v1.include_router(email_router)
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import random
|
||||||
|
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Body, Request
|
||||||
|
|
||||||
|
from backend.common.response.response_schema import response_base
|
||||||
|
from backend.common.security.jwt import DependsJwtAuth
|
||||||
|
from backend.core.conf import settings
|
||||||
|
from backend.database.db import CurrentSession
|
||||||
|
from backend.database.redis import redis_client
|
||||||
|
from backend.plugin.email.utils.send import send_email
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post('/captcha', summary='发送电子邮件验证码', dependencies=[DependsJwtAuth])
|
||||||
|
async def send_email_captcha(
|
||||||
|
request: Request,
|
||||||
|
db: CurrentSession,
|
||||||
|
recipients: Annotated[str | list[str], Body(embed=True, description='邮件接收者')],
|
||||||
|
):
|
||||||
|
code = ''.join([str(random.randint(1, 9)) for _ in range(6)])
|
||||||
|
ip = request.state.ip
|
||||||
|
await redis_client.set(
|
||||||
|
f'{settings.EMAIL_CAPTCHA_REDIS_PREFIX}:{ip}',
|
||||||
|
code,
|
||||||
|
ex=settings.EMAIL_CAPTCHA_EXPIRE_SECONDS,
|
||||||
|
)
|
||||||
|
content = {'code': code, 'expired': int(settings.EMAIL_CAPTCHA_EXPIRE_SECONDS / 60)}
|
||||||
|
await send_email(db, recipients, 'FBA 验证码', content, 'captcha.html')
|
||||||
|
return response_base.success()
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[plugin]
|
||||||
|
summary = '电子邮件'
|
||||||
|
version = '0.0.1'
|
||||||
|
description = '发送电子邮件,例如验证码、通知等'
|
||||||
|
author = 'wu-clan'
|
||||||
|
|
||||||
|
[app]
|
||||||
|
router = ['v1']
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
aiosmtplib
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>验证码</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 4px;
|
||||||
|
color: #2f54eb;
|
||||||
|
background-color: #f0f3ff;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" bgcolor="#f5f7fa">
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="padding: 40px 0;">
|
||||||
|
<div class="container">
|
||||||
|
<div class="title">验证码</div>
|
||||||
|
<p style="margin-top:20px;">您好,您正在进行绑定操作,请使用以下验证码:</p>
|
||||||
|
<span class="code">{{code}}</span>
|
||||||
|
<p class="tips">验证码有效期为 <strong>{{expired}}分钟</strong>,请勿泄露给他人。</p>
|
||||||
|
<p class="tips">
|
||||||
|
如有疑问,可通过【
|
||||||
|
<a href="https://fastapi-practices.github.io/fastapi_best_architecture_docs/backend/reference/config.html#captcha-login-expire-seconds">
|
||||||
|
官网-互动
|
||||||
|
</a>
|
||||||
|
】获取帮助,感谢您对 fba 的支持!
|
||||||
|
</p>
|
||||||
|
<div class="footer">
|
||||||
|
如果这不是您的操作,请忽略此邮件。<br>
|
||||||
|
本邮件由系统自动发送,请勿直接回复。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
|
|
||||||
|
from aiosmtplib import SMTP
|
||||||
|
from jinja2 import Template
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from backend.common.exception import errors
|
||||||
|
from backend.common.log import log
|
||||||
|
from backend.core.conf import settings
|
||||||
|
from backend.core.path_conf import PLUGIN_DIR
|
||||||
|
from backend.database.db import async_engine
|
||||||
|
from backend.plugin.config.crud.crud_config import config_dao
|
||||||
|
from backend.utils.serializers import select_list_serialize
|
||||||
|
from backend.utils.timezone import timezone
|
||||||
|
|
||||||
|
|
||||||
|
async def render_message(subject: str, from_header: str, content: str | dict, template: str | None) -> bytes:
|
||||||
|
"""
|
||||||
|
渲染邮件内容
|
||||||
|
|
||||||
|
:param subject: 邮件内容主题
|
||||||
|
:param from_header: 邮件来源
|
||||||
|
:param content: 邮件内容
|
||||||
|
:param template: 邮件内容模板
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
message = MIMEMultipart()
|
||||||
|
message['Subject'] = subject
|
||||||
|
message['From'] = from_header
|
||||||
|
message['date'] = timezone.now().strftime('%a, %d %b %Y %H:%M:%S %z')
|
||||||
|
|
||||||
|
if template:
|
||||||
|
async with aiofiles.open(os.path.join(PLUGIN_DIR, 'email', 'templates', template), 'r', encoding='utf-8') as f:
|
||||||
|
html = Template(await f.read(), enable_async=True)
|
||||||
|
mail_body = MIMEText(await html.render_async(**content), 'html', 'utf-8')
|
||||||
|
else:
|
||||||
|
mail_body = MIMEText(content, 'plain', 'utf-8')
|
||||||
|
|
||||||
|
message.attach(mail_body)
|
||||||
|
|
||||||
|
return message.as_bytes()
|
||||||
|
|
||||||
|
|
||||||
|
async def send_email(
|
||||||
|
db: AsyncSession,
|
||||||
|
recipients: str | list[str],
|
||||||
|
subject: str,
|
||||||
|
content: str | dict,
|
||||||
|
template: str | None = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
发送电子邮件
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param recipients: 邮件接收者
|
||||||
|
:param subject: 邮件内容主题
|
||||||
|
:param content: 邮件内容
|
||||||
|
:param template: 邮件内容模板
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
# 动态配置
|
||||||
|
dynamic_email_config = None
|
||||||
|
|
||||||
|
# 检查 config 插件配置
|
||||||
|
def get_config_table(conn):
|
||||||
|
inspector = inspect(conn)
|
||||||
|
return inspector.has_table('sys_config', schema=None)
|
||||||
|
|
||||||
|
async with async_engine.begin() as coon:
|
||||||
|
exists = await coon.run_sync(get_config_table)
|
||||||
|
|
||||||
|
if exists:
|
||||||
|
dynamic_email_config = await config_dao.get_by_type(db, 'email')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 动态配置发送
|
||||||
|
if dynamic_email_config:
|
||||||
|
configs = {d['key']: d for d in select_list_serialize(dynamic_email_config)}
|
||||||
|
if configs.get('EMAIL_STATUS'):
|
||||||
|
if len(dynamic_email_config) < 6:
|
||||||
|
raise errors.NotFoundError(msg='缺少邮件动态配置,请检查系统参数配置-邮件配置')
|
||||||
|
smtp_client = SMTP(
|
||||||
|
hostname=configs.get('EMAIL_HOST'),
|
||||||
|
port=configs.get('EMAIL_PORT'),
|
||||||
|
use_tls=configs.get('EMAIL_SSL') == '1',
|
||||||
|
)
|
||||||
|
message = await render_message(subject, configs.get('EMAIL_USERNAME'), content, template) # type: ignore
|
||||||
|
async with smtp_client:
|
||||||
|
await smtp_client.login(configs.get('EMAIL_USERNAME'), configs.get('EMAIL_PASSWORD')) # type: ignore
|
||||||
|
await smtp_client.sendmail(configs.get('EMAIL_USERNAME'), recipients, message) # type: ignore
|
||||||
|
|
||||||
|
# 本地配置发送
|
||||||
|
message = await render_message(subject, settings.EMAIL_USERNAME, content, template)
|
||||||
|
smtp_client = SMTP(
|
||||||
|
hostname=settings.EMAIL_HOST,
|
||||||
|
port=settings.EMAIL_PORT,
|
||||||
|
use_tls=settings.EMAIL_SSL,
|
||||||
|
)
|
||||||
|
async with smtp_client:
|
||||||
|
await smtp_client.login(settings.EMAIL_USERNAME, settings.EMAIL_PASSWORD)
|
||||||
|
await smtp_client.sendmail(settings.EMAIL_USERNAME, recipients, message)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f'电子邮件发送失败:{e}')
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[plugin]
|
[plugin]
|
||||||
summary = 'OAuth 2.0'
|
summary = 'OAuth 2.0'
|
||||||
version = '0.0.3'
|
version = '0.0.4'
|
||||||
description = '通过 OAuth 2.0 的方式登录系统'
|
description = '通过 OAuth 2.0 的方式登录系统'
|
||||||
author = 'wu-clan'
|
author = 'wu-clan'
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from backend.app.admin.schema.token import GetLoginToken
|
|||||||
from backend.app.admin.schema.user import AddOAuth2UserParam
|
from backend.app.admin.schema.user import AddOAuth2UserParam
|
||||||
from backend.app.admin.service.login_log_service import login_log_service
|
from backend.app.admin.service.login_log_service import login_log_service
|
||||||
from backend.common.enums import LoginLogStatusType, UserSocialType
|
from backend.common.enums import LoginLogStatusType, UserSocialType
|
||||||
|
from backend.common.i18n import t
|
||||||
from backend.common.security import jwt
|
from backend.common.security import jwt
|
||||||
from backend.core.conf import settings
|
from backend.core.conf import settings
|
||||||
from backend.database.db import async_db_session
|
from backend.database.db import async_db_session
|
||||||
@@ -57,35 +58,36 @@ class OAuth2Service:
|
|||||||
sid = user.get('id')
|
sid = user.get('id')
|
||||||
nickname = user.get('name')
|
nickname = user.get('name')
|
||||||
|
|
||||||
sys_user = None
|
|
||||||
user_social = await user_social_dao.get_by_sid(db, str(sid), str(social.value))
|
user_social = await user_social_dao.get_by_sid(db, str(sid), str(social.value))
|
||||||
if not user_social:
|
if user_social:
|
||||||
if email:
|
|
||||||
sys_user = await user_dao.check_email(db, email)
|
|
||||||
|
|
||||||
# 创建系统用户
|
|
||||||
if not sys_user:
|
|
||||||
while await user_dao.get_by_username(db, username):
|
|
||||||
username = f'{username}_{text_captcha(5)}'
|
|
||||||
new_sys_user = AddOAuth2UserParam(
|
|
||||||
username=username,
|
|
||||||
password='123456', # 默认密码,可修改系统用户表进行默认密码检测并配合前端进行修改密码提示
|
|
||||||
nickname=nickname,
|
|
||||||
email=email,
|
|
||||||
avatar=avatar,
|
|
||||||
)
|
|
||||||
await user_dao.add_by_oauth2(db, new_sys_user)
|
|
||||||
await db.flush()
|
|
||||||
sys_user = await user_dao.get_by_username(db, username)
|
|
||||||
|
|
||||||
# 绑定社交用户
|
|
||||||
new_user_social = CreateUserSocialParam(sid=str(sid), source=social.value, user_id=sys_user.id)
|
|
||||||
await user_social_dao.create(db, new_user_social)
|
|
||||||
|
|
||||||
if not sys_user:
|
|
||||||
sys_user = await user_dao.get(db, user_social.user_id)
|
sys_user = await user_dao.get(db, user_social.user_id)
|
||||||
if avatar:
|
# 更新用户头像
|
||||||
|
if not sys_user.avatar and avatar is not None:
|
||||||
await user_dao.update_avatar(db, sys_user.id, avatar)
|
await user_dao.update_avatar(db, sys_user.id, avatar)
|
||||||
|
else:
|
||||||
|
sys_user = None
|
||||||
|
# 检测系统用户是否已存在
|
||||||
|
if email:
|
||||||
|
sys_user = await user_dao.check_email(db, email) # 通过邮箱验证绑定保证邮箱真实性
|
||||||
|
|
||||||
|
# 创建系统用户
|
||||||
|
if not sys_user:
|
||||||
|
while await user_dao.get_by_username(db, username):
|
||||||
|
username = f'{username}_{text_captcha(5)}'
|
||||||
|
new_sys_user = AddOAuth2UserParam(
|
||||||
|
username=username,
|
||||||
|
password=None,
|
||||||
|
nickname=nickname,
|
||||||
|
email=email,
|
||||||
|
avatar=avatar,
|
||||||
|
)
|
||||||
|
await user_dao.add_by_oauth2(db, new_sys_user)
|
||||||
|
await db.flush()
|
||||||
|
sys_user = await user_dao.get_by_username(db, username)
|
||||||
|
|
||||||
|
# 绑定社交账号
|
||||||
|
new_user_social = CreateUserSocialParam(sid=str(sid), source=social.value, user_id=sys_user.id)
|
||||||
|
await user_social_dao.create(db, new_user_social)
|
||||||
|
|
||||||
# 创建 token
|
# 创建 token
|
||||||
access_token = await jwt.create_access_token(
|
access_token = await jwt.create_access_token(
|
||||||
@@ -112,7 +114,7 @@ class OAuth2Service:
|
|||||||
username=sys_user.username,
|
username=sys_user.username,
|
||||||
login_time=timezone.now(),
|
login_time=timezone.now(),
|
||||||
status=LoginLogStatusType.success.value,
|
status=LoginLogStatusType.success.value,
|
||||||
msg='登录成功(OAuth2)',
|
msg=t('success.login.oauth2_success'),
|
||||||
)
|
)
|
||||||
background_tasks.add_task(login_log_service.create, **login_log)
|
background_tasks.add_task(login_log_service.create, **login_log)
|
||||||
await redis_client.delete(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
|
await redis_client.delete(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
|
||||||
|
|||||||
+15
-16
@@ -128,12 +128,11 @@ def parse_plugin_config() -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|||||||
|
|
||||||
# 使用独立单例,避免与主线程冲突
|
# 使用独立单例,避免与主线程冲突
|
||||||
current_redis_client = RedisCli()
|
current_redis_client = RedisCli()
|
||||||
run_await(current_redis_client.open)()
|
|
||||||
|
|
||||||
run_await(current_redis_client.delete_prefix)(f'{settings.PLUGIN_REDIS_PREFIX}:info', exclude=plugins)
|
# 清理未知插件信息
|
||||||
plugin_status = run_await(current_redis_client.hgetall)(f'{settings.PLUGIN_REDIS_PREFIX}:status')
|
run_await(current_redis_client.delete_prefix)(
|
||||||
if not plugin_status:
|
settings.PLUGIN_REDIS_PREFIX, exclude=[f'{settings.PLUGIN_REDIS_PREFIX}:{key}' for key in plugins]
|
||||||
plugin_status = {}
|
)
|
||||||
|
|
||||||
for plugin in plugins:
|
for plugin in plugins:
|
||||||
data = load_plugin_config(plugin)
|
data = load_plugin_config(plugin)
|
||||||
@@ -164,16 +163,19 @@ def parse_plugin_config() -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|||||||
app_plugins.append(data)
|
app_plugins.append(data)
|
||||||
|
|
||||||
# 补充插件信息
|
# 补充插件信息
|
||||||
data['plugin']['enable'] = plugin_status.setdefault(plugin, str(StatusType.enable.value))
|
plugin_cache_info = run_await(current_redis_client.get)(f'{settings.PLUGIN_REDIS_PREFIX}:{plugin}')
|
||||||
|
if plugin_cache_info:
|
||||||
|
data['plugin']['enable'] = json.loads(plugin_cache_info)['plugin']['enable']
|
||||||
|
else:
|
||||||
|
data['plugin']['enable'] = str(StatusType.enable.value)
|
||||||
data['plugin']['name'] = plugin
|
data['plugin']['name'] = plugin
|
||||||
|
|
||||||
# 缓存插件信息
|
# 缓存最新插件信息
|
||||||
run_await(current_redis_client.set)(
|
run_await(current_redis_client.set)(
|
||||||
f'{settings.PLUGIN_REDIS_PREFIX}:info:{plugin}', json.dumps(data, ensure_ascii=False)
|
f'{settings.PLUGIN_REDIS_PREFIX}:{plugin}', json.dumps(data, ensure_ascii=False)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 缓存插件状态
|
# 重置插件变更状态
|
||||||
run_await(current_redis_client.hset)(f'{settings.PLUGIN_REDIS_PREFIX}:status', mapping=plugin_status)
|
|
||||||
run_await(current_redis_client.delete)(f'{settings.PLUGIN_REDIS_PREFIX}:changed')
|
run_await(current_redis_client.delete)(f'{settings.PLUGIN_REDIS_PREFIX}:changed')
|
||||||
|
|
||||||
return extend_plugins, app_plugins
|
return extend_plugins, app_plugins
|
||||||
@@ -380,13 +382,10 @@ class PluginStatusChecker:
|
|||||||
:param request: FastAPI 请求对象
|
:param request: FastAPI 请求对象
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
plugin_status = await redis_client.hgetall(f'{settings.PLUGIN_REDIS_PREFIX}:status')
|
plugin_info = await redis_client.get(f'{settings.PLUGIN_REDIS_PREFIX}:{self.plugin}')
|
||||||
if not plugin_status:
|
if not plugin_info:
|
||||||
log.error('插件状态未初始化或丢失,需重启服务自动修复')
|
log.error('插件状态未初始化或丢失,需重启服务自动修复')
|
||||||
raise PluginInjectError('插件状态未初始化或丢失,请联系系统管理员')
|
raise PluginInjectError('插件状态未初始化或丢失,请联系系统管理员')
|
||||||
|
|
||||||
if self.plugin not in plugin_status:
|
if not int(json.loads(plugin_info)['plugin']['enable']):
|
||||||
log.error(f'插件 {self.plugin} 状态未初始化或丢失,需重启服务自动修复')
|
|
||||||
raise PluginInjectError(f'插件 {self.plugin} 状态未初始化或丢失,请联系系统管理员')
|
|
||||||
if not int(plugin_status.get(self.plugin)):
|
|
||||||
raise errors.ServerError(msg=f'插件 {self.plugin} 未启用,请联系系统管理员')
|
raise errors.ServerError(msg=f'插件 {self.plugin} 未启用,请联系系统管理员')
|
||||||
|
|||||||
+8
-1
@@ -6,8 +6,15 @@ import uvicorn
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# 为什么独立此启动文件:https://stackoverflow.com/questions/64003384
|
# 为什么独立此启动文件:https://stackoverflow.com/questions/64003384
|
||||||
|
|
||||||
|
# DEBUG:
|
||||||
# 如果你喜欢在 IDE 中进行 DEBUG,可在 IDE 中直接右键启动此文件
|
# 如果你喜欢在 IDE 中进行 DEBUG,可在 IDE 中直接右键启动此文件
|
||||||
# 如果你喜欢通过 print 方式进行调试,建议使用 fastapi cli 方式启动服务
|
# 如果你喜欢通过 print 方式进行调试,建议使用 fba cli 方式启动服务
|
||||||
|
|
||||||
|
# Warning:
|
||||||
|
# 如果你正在通过 python 命令启动此文件,请遵循以下事宜:
|
||||||
|
# 1. 按照官方文档通过 uv 安装依赖
|
||||||
|
# 2. 命令行空间位于 backend 目录下
|
||||||
try:
|
try:
|
||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
app='backend.main:app',
|
app='backend.main:app',
|
||||||
|
|||||||
@@ -3,76 +3,76 @@ values (2048601258595581952, '测试', 0, null, null, null, 1, 0, null, now(), n
|
|||||||
|
|
||||||
insert into sys_menu (id, title, name, path, sort, icon, type, component, perms, status, display, cache, link, remark, parent_id, created_time, updated_time)
|
insert into sys_menu (id, title, name, path, sort, icon, type, component, perms, status, display, cache, link, remark, parent_id, created_time, updated_time)
|
||||||
values
|
values
|
||||||
(2049629108245233664, '概览', 'Dashboard', '/dashboard', 0, 'ant-design:dashboard-outlined', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
(2049629108245233664, 'page.dashboard.title', 'Dashboard', '/dashboard', 0, 'ant-design:dashboard-outlined', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233665, '分析页', 'Analytics', '/analytics', 0, 'lucide:area-chart', 1, '/dashboard/analytics/index', null, 1, 1, 1, '', null, 2049629108245233664, '2025-06-26 20:29:06', null),
|
(2049629108245233665, 'page.dashboard.analytics', 'Analytics', '/analytics', 0, 'lucide:area-chart', 1, '/dashboard/analytics/index', null, 1, 1, 1, '', null, 2049629108245233664, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233666, '工作台', 'Workspace', '/workspace', 1, 'carbon:workspace', 1, '/dashboard/workspace/index', null, 1, 1, 1, '', null, 2049629108245233664, '2025-06-26 20:29:06', null),
|
(2049629108245233666, 'page.dashboard.workspace', 'Workspace', '/workspace', 1, 'carbon:workspace', 1, '/dashboard/workspace/index', null, 1, 1, 1, '', null, 2049629108245233664, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233667, '系统管理', 'System', '/system', 1, 'eos-icons:admin', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
(2049629108245233667, 'page.menu.system', 'System', '/system', 1, 'eos-icons:admin', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233668, '部门管理', 'SysDept', '/system/dept', 1, 'mingcute:department-line', 1, '/system/dept/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null),
|
(2049629108245233668, 'page.menu.sysDept', 'SysDept', '/system/dept', 1, 'mingcute:department-line', 1, '/system/dept/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233669, '新增', 'AddSysDept', null, 0, null, 2, null, 'sys:dept:add', 1, 0, 1, '', null, 2049629108245233668, '2025-06-26 20:29:06', null),
|
(2049629108245233669, '新增', 'AddSysDept', null, 0, null, 2, null, 'sys:dept:add', 1, 0, 1, '', null, 2049629108245233668, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233670, '修改', 'EditSysDept', null, 0, null, 2, null, 'sys:dept:edit', 1, 0, 1, '', null, 2049629108245233668, '2025-06-26 20:29:06', null),
|
(2049629108245233670, '修改', 'EditSysDept', null, 0, null, 2, null, 'sys:dept:edit', 1, 0, 1, '', null, 2049629108245233668, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233671, '删除', 'DeleteSysDept', null, 0, null, 2, null, 'sys:dept:del', 1, 0, 1, '', null, 2049629108245233668, '2025-06-26 20:29:06', null),
|
(2049629108245233671, '删除', 'DeleteSysDept', null, 0, null, 2, null, 'sys:dept:del', 1, 0, 1, '', null, 2049629108245233668, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233672, '用户管理', 'SysUser', '/system/user', 2, 'ant-design:user-outlined', 1, '/system/user/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null),
|
(2049629108245233672, 'page.menu.sysUser', 'SysUser', '/system/user', 2, 'ant-design:user-outlined', 1, '/system/user/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233673, '删除', 'DeleteSysUser', null, 0, null, 2, null, 'sys:user:del', 1, 0, 1, '', null, 2049629108245233672, '2025-06-26 20:29:06', null),
|
(2049629108245233673, '删除', 'DeleteSysUser', null, 0, null, 2, null, 'sys:user:del', 1, 0, 1, '', null, 2049629108245233672, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233674, '角色管理', 'SysRole', '/system/role', 3, 'carbon:user-role', 1, '/system/role/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null),
|
(2049629108245233674, 'page.menu.sysRole', 'SysRole', '/system/role', 3, 'carbon:user-role', 1, '/system/role/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233675, '新增', 'AddSysRole', null, 0, null, 2, null, 'sys:role:add', 1, 0, 1, '', null, 2049629108245233674, '2025-06-26 20:29:06', null),
|
(2049629108245233675, '新增', 'AddSysRole', null, 0, null, 2, null, 'sys:role:add', 1, 0, 1, '', null, 2049629108245233674, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233676, '修改', 'EditSysRole', null, 0, null, 2, null, 'sys:role:edit', 1, 0, 1, '', null, 2049629108245233674, '2025-06-26 20:29:06', null),
|
(2049629108245233676, '修改', 'EditSysRole', null, 0, null, 2, null, 'sys:role:edit', 1, 0, 1, '', null, 2049629108245233674, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233677, '修改角色菜单', 'EditSysRoleMenu', null, 0, null, 2, null, 'sys:role:menu:edit', 1, 0, 1, '', null, 2049629108245233674, '2025-06-26 20:29:06', null),
|
(2049629108245233677, '修改角色菜单', 'EditSysRoleMenu', null, 0, null, 2, null, 'sys:role:menu:edit', 1, 0, 1, '', null, 2049629108245233674, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233678, '修改角色数据范围', 'EditSysRoleScope', null, 0, null, 2, null, 'sys:role:scope:edit', 1, 0, 1, '', null, 2049629108245233674, '2025-06-26 20:29:06', null),
|
(2049629108245233678, '修改角色数据范围', 'EditSysRoleScope', null, 0, null, 2, null, 'sys:role:scope:edit', 1, 0, 1, '', null, 2049629108245233674, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233679, '删除', 'DeleteSysRole', null, 0, null, 2, null, 'sys:role:del', 1, 0, 1, '', null, 2049629108245233674, '2025-06-26 20:29:06', null),
|
(2049629108245233679, '删除', 'DeleteSysRole', null, 0, null, 2, null, 'sys:role:del', 1, 0, 1, '', null, 2049629108245233674, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233680, '菜单管理', 'SysMenu', '/system/menu', 4, 'ant-design:menu-outlined', 1, '/system/menu/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null),
|
(2049629108245233680, 'page.menu.sysMenu', 'SysMenu', '/system/menu', 4, 'ant-design:menu-outlined', 1, '/system/menu/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233681, '新增', 'AddSysMenu', null, 0, null, 2, null, 'sys:menu:add', 1, 0, 1, '', null, 2049629108245233680, '2025-06-26 20:29:06', null),
|
(2049629108245233681, '新增', 'AddSysMenu', null, 0, null, 2, null, 'sys:menu:add', 1, 0, 1, '', null, 2049629108245233680, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233682, '修改', 'EditSysMenu', null, 0, null, 2, null, 'sys:menu:edit', 1, 0, 1, '', null, 2049629108245233680, '2025-06-26 20:29:06', null),
|
(2049629108245233682, '修改', 'EditSysMenu', null, 0, null, 2, null, 'sys:menu:edit', 1, 0, 1, '', null, 2049629108245233680, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427968, '删除', 'DeleteSysMenu', null, 0, null, 2, null, 'sys:menu:del', 1, 0, 1, '', null, 2049629108245233680, '2025-06-26 20:29:06', null),
|
(2049629108249427968, '删除', 'DeleteSysMenu', null, 0, null, 2, null, 'sys:menu:del', 1, 0, 1, '', null, 2049629108245233680, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427969, '数据权限', 'SysDataPermission', '/system/data-permission', 5, 'icon-park-outline:permissions', 0, null, null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null),
|
(2049629108249427969, 'page.menu.sysDataPermission', 'SysDataPermission', '/system/data-permission', 5, 'icon-park-outline:permissions', 0, null, null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427970, '数据范围', 'SysDataScope', '/system/data-scope', 6, 'cuida:scope-outline', 1, '/system/data-permission/scope/index', null, 1, 1, 1, '', null, 2049629108249427969, '2025-06-26 20:29:06', '2025-06-26 20:37:26'),
|
(2049629108249427970, 'page.menu.sysDataScope', 'SysDataScope', '/system/data-scope', 6, 'cuida:scope-outline', 1, '/system/data-permission/scope/index', null, 1, 1, 1, '', null, 2049629108249427969, '2025-06-26 20:29:06', '2025-06-26 20:37:26'),
|
||||||
(2049629108249427971, '新增', 'AddSysDataScope', null, 0, null, 2, null, 'data:scope:add', 1, 0, 1, '', null, 2049629108249427970, '2025-06-26 20:29:06', null),
|
(2049629108249427971, '新增', 'AddSysDataScope', null, 0, null, 2, null, 'data:scope:add', 1, 0, 1, '', null, 2049629108249427970, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427972, '修改', 'EditSysDataScope', null, 0, null, 2, null, 'data:scope:edit', 1, 0, 1, '', null, 2049629108249427970, '2025-06-26 20:29:06', null),
|
(2049629108249427972, '修改', 'EditSysDataScope', null, 0, null, 2, null, 'data:scope:edit', 1, 0, 1, '', null, 2049629108249427970, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427973, '修改数据范围规则', 'EditDataScopeRule', null, 0, null, 2, null, 'data:scope:rule:edit', 1, 0, 1, '', null, 2049629108249427970, '2025-06-26 20:29:06', null),
|
(2049629108249427973, '修改数据范围规则', 'EditDataScopeRule', null, 0, null, 2, null, 'data:scope:rule:edit', 1, 0, 1, '', null, 2049629108249427970, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427974, '删除', 'DeleteSysDataScope', null, 0, null, 2, null, 'data:scope:del', 1, 0, 1, '', null, 2049629108249427970, '2025-06-26 20:29:06', null),
|
(2049629108249427974, '删除', 'DeleteSysDataScope', null, 0, null, 2, null, 'data:scope:del', 1, 0, 1, '', null, 2049629108249427970, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427975, '数据规则', 'SysDataRule', '/system/data-rule', 7, 'material-symbols:rule', 1, '/system/data-permission/rule/index', null, 1, 1, 1, '', null, 2049629108249427969, '2025-06-26 20:29:06', '2025-06-26 20:37:40'),
|
(2049629108249427975, 'page.menu.sysDataRule', 'SysDataRule', '/system/data-rule', 7, 'material-symbols:rule', 1, '/system/data-permission/rule/index', null, 1, 1, 1, '', null, 2049629108249427969, '2025-06-26 20:29:06', '2025-06-26 20:37:40'),
|
||||||
(2049629108249427976, '新增', 'AddSysDataRule', null, 0, null, 2, null, 'data:rule:add', 1, 0, 1, '', null, 2049629108249427975, '2025-06-26 20:29:06', null),
|
(2049629108249427976, '新增', 'AddSysDataRule', null, 0, null, 2, null, 'data:rule:add', 1, 0, 1, '', null, 2049629108249427975, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427977, '修改', 'EditSysDataRule', null, 0, null, 2, null, 'data:rule:edit', 1, 0, 1, '', null, 2049629108249427975, '2025-06-26 20:29:06', null),
|
(2049629108249427977, '修改', 'EditSysDataRule', null, 0, null, 2, null, 'data:rule:edit', 1, 0, 1, '', null, 2049629108249427975, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427978, '删除', 'DeleteSysDataRule', null, 0, null, 2, null, 'data:rule:del', 1, 0, 1, '', null, 2049629108249427975, '2025-06-26 20:29:06', null),
|
(2049629108249427978, '删除', 'DeleteSysDataRule', null, 0, null, 2, null, 'data:rule:del', 1, 0, 1, '', null, 2049629108249427975, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427979, '插件管理', 'SysPlugin', '/system/plugin', 8, 'clarity:plugin-line', 1, '/system/plugin/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null),
|
(2049629108249427979, 'page.menu.sysPlugin', 'SysPlugin', '/system/plugin', 8, 'clarity:plugin-line', 1, '/system/plugin/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427980, '安装', 'InstallSysPlugin', null, 0, null, 2, null, 'sys:plugin:install', 1, 0, 1, '', null, 2049629108249427979, '2025-06-26 20:29:06', null),
|
(2049629108249427980, '安装', 'InstallSysPlugin', null, 0, null, 2, null, 'sys:plugin:install', 1, 0, 1, '', null, 2049629108249427979, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427981, '卸载', 'UninstallSysPlugin', null, 0, null, 2, null, 'sys:plugin:uninstall', 1, 0, 1, '', null, 2049629108249427979, '2025-06-26 20:29:06', null),
|
(2049629108249427981, '卸载', 'UninstallSysPlugin', null, 0, null, 2, null, 'sys:plugin:uninstall', 1, 0, 1, '', null, 2049629108249427979, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427982, '修改', 'EditSysPlugin', null, 0, null, 2, null, 'sys:plugin:edit', 1, 0, 1, '', null, 2049629108249427979, '2025-06-26 20:29:06', null),
|
(2049629108249427982, '修改', 'EditSysPlugin', null, 0, null, 2, null, 'sys:plugin:edit', 1, 0, 1, '', null, 2049629108249427979, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427983, '任务调度', 'Scheduler', '/scheduler', 2, 'material-symbols:automation', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
(2049629108249427983, 'page.menu.scheduler', 'Scheduler', '/scheduler', 2, 'material-symbols:automation', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427984, '调度管理', 'SchedulerManage', '/scheduler/manage', 1, 'ix:scheduler', 1, '/scheduler/manage/index', null, 1, 1, 1, '', null, 2049629108249427983, '2025-06-26 20:29:06', null),
|
(2049629108249427984, 'page.menu.schedulerManage', 'SchedulerManage', '/scheduler/manage', 1, 'ix:scheduler', 1, '/scheduler/manage/index', null, 1, 1, 1, '', null, 2049629108249427983, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427985, '调度记录', 'SchedulerRecord', '/scheduler/record', 2, 'ix:scheduler', 1, '/scheduler/record/index', null, 1, 1, 1, '', null, 2049629108249427983, '2025-06-26 20:29:06', null),
|
(2049629108249427985, 'page.menu.schedulerRecord', 'SchedulerRecord', '/scheduler/record', 2, 'ix:scheduler', 1, '/scheduler/record/index', null, 1, 1, 1, '', null, 2049629108249427983, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427986, '日志管理', 'Log', '/log', 3, 'carbon:cloud-logging', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
(2049629108249427986, 'page.menu.log', 'Log', '/log', 3, 'carbon:cloud-logging', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427987, '登录日志', 'LoginLog', '/log/login', 1, 'mdi:login', 1, '/log/login/index', null, 1, 1, 1, '', null, 2049629108249427986, '2025-06-26 20:29:06', null),
|
(2049629108249427987, 'page.menu.login', 'LoginLog', '/log/login', 1, 'mdi:login', 1, '/log/login/index', null, 1, 1, 1, '', null, 2049629108249427986, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427988, '删除', 'DeleteLoginLog', null, 0, null, 2, null, 'log:login:del', 1, 0, 1, '', null, 2049629108249427987, '2025-06-26 20:29:06', null),
|
(2049629108249427988, '删除', 'DeleteLoginLog', null, 0, null, 2, null, 'log:login:del', 1, 0, 1, '', null, 2049629108249427987, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427989, '清空', 'EmptyLoginLog', null, 0, null, 2, null, 'log:login:clear', 1, 0, 1, '', null, 2049629108249427987, '2025-06-26 20:29:06', null),
|
(2049629108249427989, '清空', 'EmptyLoginLog', null, 0, null, 2, null, 'log:login:clear', 1, 0, 1, '', null, 2049629108249427987, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427990, '操作日志', 'OperaLog', '/log/opera', 2, 'carbon:operations-record', 1, '/log/opera/index', null, 1, 1, 1, '', null, 2049629108249427986, '2025-06-26 20:29:06', null),
|
(2049629108249427990, 'page.menu.opera', 'OperaLog', '/log/opera', 2, 'carbon:operations-record', 1, '/log/opera/index', null, 1, 1, 1, '', null, 2049629108249427986, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427991, '删除', 'DeleteOperaLog', null, 0, null, 2, null, 'log:opera:del', 1, 0, 1, '', null, 2049629108249427990, '2025-06-26 20:29:06', null),
|
(2049629108249427991, '删除', 'DeleteOperaLog', null, 0, null, 2, null, 'log:opera:del', 1, 0, 1, '', null, 2049629108249427990, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622272, '清空', 'EmptyOperaLog', null, 0, null, 2, null, 'log:opera:clear', 1, 0, 1, '', null, 2049629108249427990, '2025-06-26 20:29:06', null),
|
(2049629108253622272, '清空', 'EmptyOperaLog', null, 0, null, 2, null, 'log:opera:clear', 1, 0, 1, '', null, 2049629108249427990, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622273, '系统监控', 'Monitor', '/monitor', 4, 'mdi:monitor-eye', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
(2049629108253622273, 'page.menu.monitor', 'Monitor', '/monitor', 4, 'mdi:monitor-eye', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622274, '在线用户', 'Online', '/log/online', 1, 'wpf:online', 1, '/monitor/online/index', null, 1, 1, 1, '', null, 2049629108253622273, '2025-06-26 20:29:06', null),
|
(2049629108253622274, 'page.menu.online', 'Online', '/log/online', 1, 'wpf:online', 1, '/monitor/online/index', null, 1, 1, 1, '', null, 2049629108253622273, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622275, '下线', 'KickOutOnline', null, 0, null, 2, null, 'sys:session:delete', 1, 0, 1, '', null, 2049629108253622274, '2025-06-26 20:29:06', null),
|
(2049629108253622275, '下线', 'KickOutOnline', null, 0, null, 2, null, 'sys:session:delete', 1, 0, 1, '', null, 2049629108253622274, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622276, 'Redis', 'Redis', '/monitor/redis', 2, 'devicon:redis', 1, '/monitor/redis/index', null, 1, 1, 1, '', null, 2049629108253622273, '2025-06-26 20:29:06', null),
|
(2049629108253622276, 'page.menu.redis', 'Redis', '/monitor/redis', 2, 'devicon:redis', 1, '/monitor/redis/index', null, 1, 1, 1, '', null, 2049629108253622273, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622277, 'Server', 'Server', '/monitor/server', 3, 'mdi:server-outline', 1, '/monitor/server/index', null, 1, 1, 1, '', null, 2049629108253622273, '2025-06-26 20:29:06', null),
|
(2049629108253622277, 'page.menu.server', 'Server', '/monitor/server', 3, 'mdi:server-outline', 1, '/monitor/server/index', null, 1, 1, 1, '', null, 2049629108253622273, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622278, '项目', 'Project', '/fba', 5, 'https://wu-clan.github.io/picx-images-hosting/logo/fba.png', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
(2049629108253622278, '项目', 'Project', '/fba', 5, 'https://wu-clan.github.io/picx-images-hosting/logo/fba.png', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622279, '文档', 'Document', '/fba/document', 1, 'lucide:book-open-text', 4, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://fastapi-practices.github.io/fastapi_best_architecture_docs', null, 2049629108253622278, '2025-06-26 20:29:06', null),
|
(2049629108253622279, '文档', 'Document', '/fba/document', 1, 'lucide:book-open-text', 4, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://fastapi-practices.github.io/fastapi_best_architecture_docs', null, 2049629108253622278, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622280, 'Github', 'Github', '/fba/github', 2, 'ant-design:github-filled', 4, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://github.com/fastapi-practices/fastapi_best_architecture', null, 2049629108253622278, '2025-06-26 20:29:06', null),
|
(2049629108253622280, 'Github', 'Github', '/fba/github', 2, 'ant-design:github-filled', 4, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://github.com/fastapi-practices/fastapi_best_architecture', null, 2049629108253622278, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622281, 'Apifox', 'Apifox', '/fba/apifox', 3, 'simple-icons:apifox', 3, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://apifox.com/apidoc/shared-28a93f02-730b-4f33-bb5e-4dad92058cc0', null, 2049629108253622278, '2025-06-26 20:29:06', null),
|
(2049629108253622281, 'Apifox', 'Apifox', '/fba/apifox', 3, 'simple-icons:apifox', 3, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://apifox.com/apidoc/shared-28a93f02-730b-4f33-bb5e-4dad92058cc0', null, 2049629108253622278, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622282, '个人中心', 'Profile', '/profile', 6, 'ant-design:profile-outlined', 1, '/_core/profile/index', null, 1, 0, 1, '', null, null, '2025-06-26 20:29:06', null),
|
(2049629108253622282, 'page.menu.profile', 'Profile', '/profile', 6, 'ant-design:profile-outlined', 1, '/_core/profile/index', null, 1, 0, 1, '', null, null, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622283, '参数管理', 'PluginConfig', '/plugins/config', 7, 'codicon:symbol-parameter', 1, '/plugins/config/views/index', null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', '2025-06-26 20:34:51'),
|
(2049629108253622283, 'config.menu', 'PluginConfig', '/plugins/config', 7, 'codicon:symbol-parameter', 1, '/plugins/config/views/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', '2025-06-26 20:34:51'),
|
||||||
(2049629108253622284, '新增', 'AddConfig', null, 0, null, 2, null, 'sys:config:add', 1, 0, 1, '', null, 2049629108253622283, '2025-06-26 20:29:06', null),
|
(2049629108253622284, '新增', 'AddConfig', null, 0, null, 2, null, 'sys:config:add', 1, 0, 1, '', null, 2049629108253622283, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622285, '修改', 'EditConfig', null, 0, null, 2, null, 'sys:config:edit', 1, 0, 1, '', null, 2049629108253622283, '2025-06-26 20:29:06', null),
|
(2049629108253622285, '修改', 'EditConfig', null, 0, null, 2, null, 'sys:config:edit', 1, 0, 1, '', null, 2049629108253622283, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622286, '删除', 'DeleteConfig', null, 0, null, 2, null, 'sys:config:del', 1, 0, 1, '', null, 2049629108253622283, '2025-06-26 20:29:06', null),
|
(2049629108253622286, '删除', 'DeleteConfig', null, 0, null, 2, null, 'sys:config:del', 1, 0, 1, '', null, 2049629108253622283, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622287, '字典管理', 'PluginDict', '/plugins/dict', 8, 'fluent-mdl2:dictionary', 1, '/plugins/dict/views/index', null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', '2025-06-26 20:35:07'),
|
(2049629108253622287, 'dict.menu', 'PluginDict', '/plugins/dict', 8, 'fluent-mdl2:dictionary', 1, '/plugins/dict/views/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', '2025-06-26 20:35:07'),
|
||||||
(2049629108253622288, '新增类型', 'AddDictType', null, 0, null, 2, null, 'dict:type:add', 1, 0, 1, '', null, 2049629108253622287, '2025-06-26 20:29:06', null),
|
(2049629108253622288, '新增类型', 'AddDictType', null, 0, null, 2, null, 'dict:type:add', 1, 0, 1, '', null, 2049629108253622287, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622289, '修改类型', 'EditDictType', null, 0, null, 2, null, 'dict:type:edit', 1, 0, 1, '', null, 2049629108253622287, '2025-06-26 20:29:06', null),
|
(2049629108253622289, '修改类型', 'EditDictType', null, 0, null, 2, null, 'dict:type:edit', 1, 0, 1, '', null, 2049629108253622287, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622290, '删除类型', 'DeleteDictType', null, 0, null, 2, null, 'dict:type:del', 1, 0, 1, '', null, 2049629108253622287, '2025-06-26 20:29:06', null),
|
(2049629108253622290, '删除类型', 'DeleteDictType', null, 0, null, 2, null, 'dict:type:del', 1, 0, 1, '', null, 2049629108253622287, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622291, '新增数据', 'AddDictData', null, 0, null, 2, null, 'dict:data:add', 1, 0, 1, '', null, 2049629108253622287, '2025-06-26 20:29:06', null),
|
(2049629108253622291, '新增数据', 'AddDictData', null, 0, null, 2, null, 'dict:data:add', 1, 0, 1, '', null, 2049629108253622287, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622292, '修改数据', 'EditDictData', null, 0, null, 2, null, 'dict:data:edit', 1, 0, 1, '', null, 2049629108253622287, '2025-06-26 20:29:06', null),
|
(2049629108253622292, '修改数据', 'EditDictData', null, 0, null, 2, null, 'dict:data:edit', 1, 0, 1, '', null, 2049629108253622287, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622293, '删除数据', 'DeleteDictData', null, 0, null, 2, null, 'dict:data:del', 1, 0, 1, '', null, 2049629108253622287, '2025-06-26 20:29:06', null),
|
(2049629108253622293, '删除数据', 'DeleteDictData', null, 0, null, 2, null, 'dict:data:del', 1, 0, 1, '', null, 2049629108253622287, '2025-06-26 20:29:06', null),
|
||||||
(2049629108257816576, '通知公告', 'PluginNotice', '/plugins/notice', 9, 'fe:notice-push', 1, '/plugins/notice/views/index', null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', '2025-06-26 20:35:14'),
|
(2049629108257816576, 'notice.menu', 'PluginNotice', '/plugins/notice', 9, 'fe:notice-push', 1, '/plugins/notice/views/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', '2025-06-26 20:35:14'),
|
||||||
(2049629108257816577, '新增', 'AddNotice', null, 0, null, 2, null, 'sys:notice:add', 1, 0, 1, '', null, 2049629108257816576, '2025-06-26 20:29:06', null),
|
(2049629108257816577, '新增', 'AddNotice', null, 0, null, 2, null, 'sys:notice:add', 1, 0, 1, '', null, 2049629108257816576, '2025-06-26 20:29:06', null),
|
||||||
(2049629108257816578, '修改', 'EditNotice', null, 0, null, 2, null, 'sys:notice:edit', 1, 0, 1, '', null, 2049629108257816576, '2025-06-26 20:29:06', null),
|
(2049629108257816578, '修改', 'EditNotice', null, 0, null, 2, null, 'sys:notice:edit', 1, 0, 1, '', null, 2049629108257816576, '2025-06-26 20:29:06', null),
|
||||||
(2049629108257816579, '删除', 'DeleteNotice', null, 0, null, 2, null, 'sys:notice:del', 1, 0, 1, '', null, 2049629108257816576, '2025-06-26 20:29:06', null),
|
(2049629108257816579, '删除', 'DeleteNotice', null, 0, null, 2, null, 'sys:notice:del', 1, 0, 1, '', null, 2049629108257816576, '2025-06-26 20:29:06', null),
|
||||||
(2049629108257816580, '代码生成', 'PluginCodeGenerator', '/plugins/code-generator', 10, 'tabler:code', 1, '/plugins/code_generator/views/index', null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', '2025-06-26 20:35:25'),
|
(2049629108257816580, 'code_generator.menu', 'PluginCodeGenerator', '/plugins/code-generator', 10, 'tabler:code', 1, '/plugins/code_generator/views/index', null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', '2025-06-26 20:35:25'),
|
||||||
(2049629108257816581, '新增业务', 'AddGenCodeBusiness', '', 0, null, 2, null, 'codegen:business:add', 1, 0, 1, '', null, 2049629108257816580, '2025-06-26 20:29:06', '2025-06-26 20:45:16'),
|
(2049629108257816581, '新增业务', 'AddGenCodeBusiness', '', 0, null, 2, null, 'codegen:business:add', 1, 0, 1, '', null, 2049629108257816580, '2025-06-26 20:29:06', '2025-06-26 20:45:16'),
|
||||||
(2049629108257816582, '修改业务', 'EditGenCodeBusiness', null, 0, null, 2, null, 'codegen:business:edit', 1, 0, 1, '', null, 2049629108257816580, '2025-06-26 20:29:06', null),
|
(2049629108257816582, '修改业务', 'EditGenCodeBusiness', null, 0, null, 2, null, 'codegen:business:edit', 1, 0, 1, '', null, 2049629108257816580, '2025-06-26 20:29:06', null),
|
||||||
(2049629108257816583, '删除业务', 'DeleteGenCodeBusiness', null, 0, null, 2, null, 'codegen:business:del', 1, 0, 1, '', null, 2049629108257816580, '2025-06-26 20:29:06', null),
|
(2049629108257816583, '删除业务', 'DeleteGenCodeBusiness', null, 0, null, 2, null, 'codegen:business:del', 1, 0, 1, '', null, 2049629108257816580, '2025-06-26 20:29:06', null),
|
||||||
|
|||||||
@@ -3,76 +3,76 @@ values (1, '测试', 0, null, null, null, 1, 0, null, now(), null);
|
|||||||
|
|
||||||
insert into sys_menu (id, title, name, path, sort, icon, type, component, perms, status, display, cache, link, remark, parent_id, created_time, updated_time)
|
insert into sys_menu (id, title, name, path, sort, icon, type, component, perms, status, display, cache, link, remark, parent_id, created_time, updated_time)
|
||||||
values
|
values
|
||||||
(1, '概览', 'Dashboard', '/dashboard', 0, 'ant-design:dashboard-outlined', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
(1, 'page.dashboard.title', 'Dashboard', '/dashboard', 0, 'ant-design:dashboard-outlined', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
||||||
(2, '分析页', 'Analytics', '/analytics', 0, 'lucide:area-chart', 1, '/dashboard/analytics/index', null, 1, 1, 1, '', null, 1, '2025-06-26 20:29:06', null),
|
(2, 'page.dashboard.analytics', 'Analytics', '/analytics', 0, 'lucide:area-chart', 1, '/dashboard/analytics/index', null, 1, 1, 1, '', null, 1, '2025-06-26 20:29:06', null),
|
||||||
(3, '工作台', 'Workspace', '/workspace', 1, 'carbon:workspace', 1, '/dashboard/workspace/index', null, 1, 1, 1, '', null, 1, '2025-06-26 20:29:06', null),
|
(3, 'page.dashboard.workspace', 'Workspace', '/workspace', 1, 'carbon:workspace', 1, '/dashboard/workspace/index', null, 1, 1, 1, '', null, 1, '2025-06-26 20:29:06', null),
|
||||||
(4, '系统管理', 'System', '/system', 1, 'eos-icons:admin', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
(4, 'page.menu.system', 'System', '/system', 1, 'eos-icons:admin', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
||||||
(5, '部门管理', 'SysDept', '/system/dept', 1, 'mingcute:department-line', 1, '/system/dept/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null),
|
(5, 'page.menu.sysDept', 'SysDept', '/system/dept', 1, 'mingcute:department-line', 1, '/system/dept/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null),
|
||||||
(6, '新增', 'AddSysDept', null, 0, null, 2, null, 'sys:dept:add', 1, 0, 1, '', null, 5, '2025-06-26 20:29:06', null),
|
(6, '新增', 'AddSysDept', null, 0, null, 2, null, 'sys:dept:add', 1, 0, 1, '', null, 5, '2025-06-26 20:29:06', null),
|
||||||
(7, '修改', 'EditSysDept', null, 0, null, 2, null, 'sys:dept:edit', 1, 0, 1, '', null, 5, '2025-06-26 20:29:06', null),
|
(7, '修改', 'EditSysDept', null, 0, null, 2, null, 'sys:dept:edit', 1, 0, 1, '', null, 5, '2025-06-26 20:29:06', null),
|
||||||
(8, '删除', 'DeleteSysDept', null, 0, null, 2, null, 'sys:dept:del', 1, 0, 1, '', null, 5, '2025-06-26 20:29:06', null),
|
(8, '删除', 'DeleteSysDept', null, 0, null, 2, null, 'sys:dept:del', 1, 0, 1, '', null, 5, '2025-06-26 20:29:06', null),
|
||||||
(9, '用户管理', 'SysUser', '/system/user', 2, 'ant-design:user-outlined', 1, '/system/user/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null),
|
(9, 'page.menu.sysUser', 'SysUser', '/system/user', 2, 'ant-design:user-outlined', 1, '/system/user/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null),
|
||||||
(10, '删除', 'DeleteSysUser', null, 0, null, 2, null, 'sys:user:del', 1, 0, 1, '', null, 9, '2025-06-26 20:29:06', null),
|
(10, '删除', 'DeleteSysUser', null, 0, null, 2, null, 'sys:user:del', 1, 0, 1, '', null, 9, '2025-06-26 20:29:06', null),
|
||||||
(11, '角色管理', 'SysRole', '/system/role', 3, 'carbon:user-role', 1, '/system/role/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null),
|
(11, 'page.menu.sysRole', 'SysRole', '/system/role', 3, 'carbon:user-role', 1, '/system/role/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null),
|
||||||
(12, '新增', 'AddSysRole', null, 0, null, 2, null, 'sys:role:add', 1, 0, 1, '', null, 11, '2025-06-26 20:29:06', null),
|
(12, '新增', 'AddSysRole', null, 0, null, 2, null, 'sys:role:add', 1, 0, 1, '', null, 11, '2025-06-26 20:29:06', null),
|
||||||
(13, '修改', 'EditSysRole', null, 0, null, 2, null, 'sys:role:edit', 1, 0, 1, '', null, 11, '2025-06-26 20:29:06', null),
|
(13, '修改', 'EditSysRole', null, 0, null, 2, null, 'sys:role:edit', 1, 0, 1, '', null, 11, '2025-06-26 20:29:06', null),
|
||||||
(14, '修改角色菜单', 'EditSysRoleMenu', null, 0, null, 2, null, 'sys:role:menu:edit', 1, 0, 1, '', null, 11, '2025-06-26 20:29:06', null),
|
(14, '修改角色菜单', 'EditSysRoleMenu', null, 0, null, 2, null, 'sys:role:menu:edit', 1, 0, 1, '', null, 11, '2025-06-26 20:29:06', null),
|
||||||
(15, '修改角色数据范围', 'EditSysRoleScope', null, 0, null, 2, null, 'sys:role:scope:edit', 1, 0, 1, '', null, 11, '2025-06-26 20:29:06', null),
|
(15, '修改角色数据范围', 'EditSysRoleScope', null, 0, null, 2, null, 'sys:role:scope:edit', 1, 0, 1, '', null, 11, '2025-06-26 20:29:06', null),
|
||||||
(16, '删除', 'DeleteSysRole', null, 0, null, 2, null, 'sys:role:del', 1, 0, 1, '', null, 11, '2025-06-26 20:29:06', null),
|
(16, '删除', 'DeleteSysRole', null, 0, null, 2, null, 'sys:role:del', 1, 0, 1, '', null, 11, '2025-06-26 20:29:06', null),
|
||||||
(17, '菜单管理', 'SysMenu', '/system/menu', 4, 'ant-design:menu-outlined', 1, '/system/menu/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null),
|
(17, 'page.menu.sysMenu', 'SysMenu', '/system/menu', 4, 'ant-design:menu-outlined', 1, '/system/menu/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null),
|
||||||
(18, '新增', 'AddSysMenu', null, 0, null, 2, null, 'sys:menu:add', 1, 0, 1, '', null, 17, '2025-06-26 20:29:06', null),
|
(18, '新增', 'AddSysMenu', null, 0, null, 2, null, 'sys:menu:add', 1, 0, 1, '', null, 17, '2025-06-26 20:29:06', null),
|
||||||
(19, '修改', 'EditSysMenu', null, 0, null, 2, null, 'sys:menu:edit', 1, 0, 1, '', null, 17, '2025-06-26 20:29:06', null),
|
(19, '修改', 'EditSysMenu', null, 0, null, 2, null, 'sys:menu:edit', 1, 0, 1, '', null, 17, '2025-06-26 20:29:06', null),
|
||||||
(20, '删除', 'DeleteSysMenu', null, 0, null, 2, null, 'sys:menu:del', 1, 0, 1, '', null, 17, '2025-06-26 20:29:06', null),
|
(20, '删除', 'DeleteSysMenu', null, 0, null, 2, null, 'sys:menu:del', 1, 0, 1, '', null, 17, '2025-06-26 20:29:06', null),
|
||||||
(21, '数据权限', 'SysDataPermission', '/system/data-permission', 5, 'icon-park-outline:permissions', 0, null, null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null),
|
(21, 'page.menu.sysDataPermission', 'SysDataPermission', '/system/data-permission', 5, 'icon-park-outline:permissions', 0, null, null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null),
|
||||||
(22, '数据范围', 'SysDataScope', '/system/data-scope', 6, 'cuida:scope-outline', 1, '/system/data-permission/scope/index', null, 1, 1, 1, '', null, 21, '2025-06-26 20:29:06', '2025-06-26 20:37:26'),
|
(22, 'page.menu.sysDataScope', 'SysDataScope', '/system/data-scope', 6, 'cuida:scope-outline', 1, '/system/data-permission/scope/index', null, 1, 1, 1, '', null, 21, '2025-06-26 20:29:06', '2025-06-26 20:37:26'),
|
||||||
(23, '新增', 'AddSysDataScope', null, 0, null, 2, null, 'data:scope:add', 1, 0, 1, '', null, 22, '2025-06-26 20:29:06', null),
|
(23, '新增', 'AddSysDataScope', null, 0, null, 2, null, 'data:scope:add', 1, 0, 1, '', null, 22, '2025-06-26 20:29:06', null),
|
||||||
(24, '修改', 'EditSysDataScope', null, 0, null, 2, null, 'data:scope:edit', 1, 0, 1, '', null, 22, '2025-06-26 20:29:06', null),
|
(24, '修改', 'EditSysDataScope', null, 0, null, 2, null, 'data:scope:edit', 1, 0, 1, '', null, 22, '2025-06-26 20:29:06', null),
|
||||||
(25, '修改数据范围规则', 'EditDataScopeRule', null, 0, null, 2, null, 'data:scope:rule:edit', 1, 0, 1, '', null, 22, '2025-06-26 20:29:06', null),
|
(25, '修改数据范围规则', 'EditDataScopeRule', null, 0, null, 2, null, 'data:scope:rule:edit', 1, 0, 1, '', null, 22, '2025-06-26 20:29:06', null),
|
||||||
(26, '删除', 'DeleteSysDataScope', null, 0, null, 2, null, 'data:scope:del', 1, 0, 1, '', null, 22, '2025-06-26 20:29:06', null),
|
(26, '删除', 'DeleteSysDataScope', null, 0, null, 2, null, 'data:scope:del', 1, 0, 1, '', null, 22, '2025-06-26 20:29:06', null),
|
||||||
(27, '数据规则', 'SysDataRule', '/system/data-rule', 7, 'material-symbols:rule', 1, '/system/data-permission/rule/index', null, 1, 1, 1, '', null, 21, '2025-06-26 20:29:06', '2025-06-26 20:37:40'),
|
(27, 'page.menu.sysDataRule', 'SysDataRule', '/system/data-rule', 7, 'material-symbols:rule', 1, '/system/data-permission/rule/index', null, 1, 1, 1, '', null, 21, '2025-06-26 20:29:06', '2025-06-26 20:37:40'),
|
||||||
(28, '新增', 'AddSysDataRule', null, 0, null, 2, null, 'data:rule:add', 1, 0, 1, '', null, 27, '2025-06-26 20:29:06', null),
|
(28, '新增', 'AddSysDataRule', null, 0, null, 2, null, 'data:rule:add', 1, 0, 1, '', null, 27, '2025-06-26 20:29:06', null),
|
||||||
(29, '修改', 'EditSysDataRule', null, 0, null, 2, null, 'data:rule:edit', 1, 0, 1, '', null, 27, '2025-06-26 20:29:06', null),
|
(29, '修改', 'EditSysDataRule', null, 0, null, 2, null, 'data:rule:edit', 1, 0, 1, '', null, 27, '2025-06-26 20:29:06', null),
|
||||||
(30, '删除', 'DeleteSysDataRule', null, 0, null, 2, null, 'data:rule:del', 1, 0, 1, '', null, 27, '2025-06-26 20:29:06', null),
|
(30, '删除', 'DeleteSysDataRule', null, 0, null, 2, null, 'data:rule:del', 1, 0, 1, '', null, 27, '2025-06-26 20:29:06', null),
|
||||||
(31, '插件管理', 'SysPlugin', '/system/plugin', 8, 'clarity:plugin-line', 1, '/system/plugin/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null),
|
(31, 'page.menu.sysPlugin', 'SysPlugin', '/system/plugin', 8, 'clarity:plugin-line', 1, '/system/plugin/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null),
|
||||||
(32, '安装', 'InstallSysPlugin', null, 0, null, 2, null, 'sys:plugin:install', 1, 0, 1, '', null, 31, '2025-06-26 20:29:06', null),
|
(32, '安装', 'InstallSysPlugin', null, 0, null, 2, null, 'sys:plugin:install', 1, 0, 1, '', null, 31, '2025-06-26 20:29:06', null),
|
||||||
(33, '卸载', 'UninstallSysPlugin', null, 0, null, 2, null, 'sys:plugin:uninstall', 1, 0, 1, '', null, 31, '2025-06-26 20:29:06', null),
|
(33, '卸载', 'UninstallSysPlugin', null, 0, null, 2, null, 'sys:plugin:uninstall', 1, 0, 1, '', null, 31, '2025-06-26 20:29:06', null),
|
||||||
(34, '修改', 'EditSysPlugin', null, 0, null, 2, null, 'sys:plugin:edit', 1, 0, 1, '', null, 31, '2025-06-26 20:29:06', null),
|
(34, '修改', 'EditSysPlugin', null, 0, null, 2, null, 'sys:plugin:edit', 1, 0, 1, '', null, 31, '2025-06-26 20:29:06', null),
|
||||||
(35, '任务调度', 'Scheduler', '/scheduler', 2, 'material-symbols:automation', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
(35, 'page.menu.scheduler', 'Scheduler', '/scheduler', 2, 'material-symbols:automation', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
||||||
(36, '调度管理', 'SchedulerManage', '/scheduler/manage', 1, 'ix:scheduler', 1, '/scheduler/manage/index', null, 1, 1, 1, '', null, 35, '2025-06-26 20:29:06', null),
|
(36, 'page.menu.schedulerManage', 'SchedulerManage', '/scheduler/manage', 1, 'ix:scheduler', 1, '/scheduler/manage/index', null, 1, 1, 1, '', null, 35, '2025-06-26 20:29:06', null),
|
||||||
(37, '调度记录', 'SchedulerRecord', '/scheduler/record', 2, 'ix:scheduler', 1, '/scheduler/record/index', null, 1, 1, 1, '', null, 35, '2025-06-26 20:29:06', null),
|
(37, 'page.menu.schedulerRecord', 'SchedulerRecord', '/scheduler/record', 2, 'ix:scheduler', 1, '/scheduler/record/index', null, 1, 1, 1, '', null, 35, '2025-06-26 20:29:06', null),
|
||||||
(38, '日志管理', 'Log', '/log', 3, 'carbon:cloud-logging', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
(38, 'page.menu.log', 'Log', '/log', 3, 'carbon:cloud-logging', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
||||||
(39, '登录日志', 'LoginLog', '/log/login', 1, 'mdi:login', 1, '/log/login/index', null, 1, 1, 1, '', null, 38, '2025-06-26 20:29:06', null),
|
(39, 'page.menu.login', 'LoginLog', '/log/login', 1, 'mdi:login', 1, '/log/login/index', null, 1, 1, 1, '', null, 38, '2025-06-26 20:29:06', null),
|
||||||
(40, '删除', 'DeleteLoginLog', null, 0, null, 2, null, 'log:login:del', 1, 0, 1, '', null, 39, '2025-06-26 20:29:06', null),
|
(40, '删除', 'DeleteLoginLog', null, 0, null, 2, null, 'log:login:del', 1, 0, 1, '', null, 39, '2025-06-26 20:29:06', null),
|
||||||
(41, '清空', 'EmptyLoginLog', null, 0, null, 2, null, 'log:login:clear', 1, 0, 1, '', null, 39, '2025-06-26 20:29:06', null),
|
(41, '清空', 'EmptyLoginLog', null, 0, null, 2, null, 'log:login:clear', 1, 0, 1, '', null, 39, '2025-06-26 20:29:06', null),
|
||||||
(42, '操作日志', 'OperaLog', '/log/opera', 2, 'carbon:operations-record', 1, '/log/opera/index', null, 1, 1, 1, '', null, 38, '2025-06-26 20:29:06', null),
|
(42, 'page.menu.opera', 'OperaLog', '/log/opera', 2, 'carbon:operations-record', 1, '/log/opera/index', null, 1, 1, 1, '', null, 38, '2025-06-26 20:29:06', null),
|
||||||
(43, '删除', 'DeleteOperaLog', null, 0, null, 2, null, 'log:opera:del', 1, 0, 1, '', null, 42, '2025-06-26 20:29:06', null),
|
(43, '删除', 'DeleteOperaLog', null, 0, null, 2, null, 'log:opera:del', 1, 0, 1, '', null, 42, '2025-06-26 20:29:06', null),
|
||||||
(44, '清空', 'EmptyOperaLog', null, 0, null, 2, null, 'log:opera:clear', 1, 0, 1, '', null, 42, '2025-06-26 20:29:06', null),
|
(44, '清空', 'EmptyOperaLog', null, 0, null, 2, null, 'log:opera:clear', 1, 0, 1, '', null, 42, '2025-06-26 20:29:06', null),
|
||||||
(45, '系统监控', 'Monitor', '/monitor', 4, 'mdi:monitor-eye', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
(45, 'page.menu.monitor', 'Monitor', '/monitor', 4, 'mdi:monitor-eye', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
||||||
(46, '在线用户', 'Online', '/log/online', 1, 'wpf:online', 1, '/monitor/online/index', null, 1, 1, 1, '', null, 45, '2025-06-26 20:29:06', null),
|
(46, 'page.menu.online', 'Online', '/log/online', 1, 'wpf:online', 1, '/monitor/online/index', null, 1, 1, 1, '', null, 45, '2025-06-26 20:29:06', null),
|
||||||
(47, '下线', 'KickOutOnline', null, 0, null, 2, null, 'sys:session:delete', 1, 0, 1, '', null, 46, '2025-06-26 20:29:06', null),
|
(47, '下线', 'KickOutOnline', null, 0, null, 2, null, 'sys:session:delete', 1, 0, 1, '', null, 46, '2025-06-26 20:29:06', null),
|
||||||
(48, 'Redis', 'Redis', '/monitor/redis', 2, 'devicon:redis', 1, '/monitor/redis/index', null, 1, 1, 1, '', null, 45, '2025-06-26 20:29:06', null),
|
(48, 'page.menu.redis', 'Redis', '/monitor/redis', 2, 'devicon:redis', 1, '/monitor/redis/index', null, 1, 1, 1, '', null, 45, '2025-06-26 20:29:06', null),
|
||||||
(49, 'Server', 'Server', '/monitor/server', 3, 'mdi:server-outline', 1, '/monitor/server/index', null, 1, 1, 1, '', null, 45, '2025-06-26 20:29:06', null),
|
(49, 'page.menu.server', 'Server', '/monitor/server', 3, 'mdi:server-outline', 1, '/monitor/server/index', null, 1, 1, 1, '', null, 45, '2025-06-26 20:29:06', null),
|
||||||
(50, '项目', 'Project', '/fba', 5, 'https://wu-clan.github.io/picx-images-hosting/logo/fba.png', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
(50, '项目', 'Project', '/fba', 5, 'https://wu-clan.github.io/picx-images-hosting/logo/fba.png', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
||||||
(51, '文档', 'Document', '/fba/document', 1, 'lucide:book-open-text', 4, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://fastapi-practices.github.io/fastapi_best_architecture_docs', null, 50, '2025-06-26 20:29:06', null),
|
(51, '文档', 'Document', '/fba/document', 1, 'lucide:book-open-text', 4, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://fastapi-practices.github.io/fastapi_best_architecture_docs', null, 50, '2025-06-26 20:29:06', null),
|
||||||
(52, 'Github', 'Github', '/fba/github', 2, 'ant-design:github-filled', 4, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://github.com/fastapi-practices/fastapi_best_architecture', null, 50, '2025-06-26 20:29:06', null),
|
(52, 'Github', 'Github', '/fba/github', 2, 'ant-design:github-filled', 4, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://github.com/fastapi-practices/fastapi_best_architecture', null, 50, '2025-06-26 20:29:06', null),
|
||||||
(53, 'Apifox', 'Apifox', '/fba/apifox', 3, 'simple-icons:apifox', 3, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://apifox.com/apidoc/shared-28a93f02-730b-4f33-bb5e-4dad92058cc0', null, 50, '2025-06-26 20:29:06', null),
|
(53, 'Apifox', 'Apifox', '/fba/apifox', 3, 'simple-icons:apifox', 3, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://apifox.com/apidoc/shared-28a93f02-730b-4f33-bb5e-4dad92058cc0', null, 50, '2025-06-26 20:29:06', null),
|
||||||
(54, '个人中心', 'Profile', '/profile', 6, 'ant-design:profile-outlined', 1, '/_core/profile/index', null, 1, 0, 1, '', null, null, '2025-06-26 20:29:06', null),
|
(54, 'page.menu.profile', 'Profile', '/profile', 6, 'ant-design:profile-outlined', 1, '/_core/profile/index', null, 1, 0, 1, '', null, null, '2025-06-26 20:29:06', null),
|
||||||
(55, '参数管理', 'PluginConfig', '/plugins/config', 7, 'codicon:symbol-parameter', 1, '/plugins/config/views/index', null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', '2025-06-26 20:34:51'),
|
(55, 'config.menu', 'PluginConfig', '/plugins/config', 7, 'codicon:symbol-parameter', 1, '/plugins/config/views/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', '2025-06-26 20:34:51'),
|
||||||
(56, '新增', 'AddConfig', null, 0, null, 2, null, 'sys:config:add', 1, 0, 1, '', null, 55, '2025-06-26 20:29:06', null),
|
(56, '新增', 'AddConfig', null, 0, null, 2, null, 'sys:config:add', 1, 0, 1, '', null, 55, '2025-06-26 20:29:06', null),
|
||||||
(57, '修改', 'EditConfig', null, 0, null, 2, null, 'sys:config:edit', 1, 0, 1, '', null, 55, '2025-06-26 20:29:06', null),
|
(57, '修改', 'EditConfig', null, 0, null, 2, null, 'sys:config:edit', 1, 0, 1, '', null, 55, '2025-06-26 20:29:06', null),
|
||||||
(58, '删除', 'DeleteConfig', null, 0, null, 2, null, 'sys:config:del', 1, 0, 1, '', null, 55, '2025-06-26 20:29:06', null),
|
(58, '删除', 'DeleteConfig', null, 0, null, 2, null, 'sys:config:del', 1, 0, 1, '', null, 55, '2025-06-26 20:29:06', null),
|
||||||
(59, '字典管理', 'PluginDict', '/plugins/dict', 8, 'fluent-mdl2:dictionary', 1, '/plugins/dict/views/index', null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', '2025-06-26 20:35:07'),
|
(59, 'dict.menu', 'PluginDict', '/plugins/dict', 8, 'fluent-mdl2:dictionary', 1, '/plugins/dict/views/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', '2025-06-26 20:35:07'),
|
||||||
(60, '新增类型', 'AddDictType', null, 0, null, 2, null, 'dict:type:add', 1, 0, 1, '', null, 59, '2025-06-26 20:29:06', null),
|
(60, '新增类型', 'AddDictType', null, 0, null, 2, null, 'dict:type:add', 1, 0, 1, '', null, 59, '2025-06-26 20:29:06', null),
|
||||||
(61, '修改类型', 'EditDictType', null, 0, null, 2, null, 'dict:type:edit', 1, 0, 1, '', null, 59, '2025-06-26 20:29:06', null),
|
(61, '修改类型', 'EditDictType', null, 0, null, 2, null, 'dict:type:edit', 1, 0, 1, '', null, 59, '2025-06-26 20:29:06', null),
|
||||||
(62, '删除类型', 'DeleteDictType', null, 0, null, 2, null, 'dict:type:del', 1, 0, 1, '', null, 59, '2025-06-26 20:29:06', null),
|
(62, '删除类型', 'DeleteDictType', null, 0, null, 2, null, 'dict:type:del', 1, 0, 1, '', null, 59, '2025-06-26 20:29:06', null),
|
||||||
(63, '新增数据', 'AddDictData', null, 0, null, 2, null, 'dict:data:add', 1, 0, 1, '', null, 59, '2025-06-26 20:29:06', null),
|
(63, '新增数据', 'AddDictData', null, 0, null, 2, null, 'dict:data:add', 1, 0, 1, '', null, 59, '2025-06-26 20:29:06', null),
|
||||||
(64, '修改数据', 'EditDictData', null, 0, null, 2, null, 'dict:data:edit', 1, 0, 1, '', null, 59, '2025-06-26 20:29:06', null),
|
(64, '修改数据', 'EditDictData', null, 0, null, 2, null, 'dict:data:edit', 1, 0, 1, '', null, 59, '2025-06-26 20:29:06', null),
|
||||||
(65, '删除数据', 'DeleteDictData', null, 0, null, 2, null, 'dict:data:del', 1, 0, 1, '', null, 59, '2025-06-26 20:29:06', null),
|
(65, '删除数据', 'DeleteDictData', null, 0, null, 2, null, 'dict:data:del', 1, 0, 1, '', null, 59, '2025-06-26 20:29:06', null),
|
||||||
(66, '通知公告', 'PluginNotice', '/plugins/notice', 9, 'fe:notice-push', 1, '/plugins/notice/views/index', null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', '2025-06-26 20:35:14'),
|
(66, 'notice.menu', 'PluginNotice', '/plugins/notice', 9, 'fe:notice-push', 1, '/plugins/notice/views/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', '2025-06-26 20:35:14'),
|
||||||
(67, '新增', 'AddNotice', null, 0, null, 2, null, 'sys:notice:add', 1, 0, 1, '', null, 66, '2025-06-26 20:29:06', null),
|
(67, '新增', 'AddNotice', null, 0, null, 2, null, 'sys:notice:add', 1, 0, 1, '', null, 66, '2025-06-26 20:29:06', null),
|
||||||
(68, '修改', 'EditNotice', null, 0, null, 2, null, 'sys:notice:edit', 1, 0, 1, '', null, 66, '2025-06-26 20:29:06', null),
|
(68, '修改', 'EditNotice', null, 0, null, 2, null, 'sys:notice:edit', 1, 0, 1, '', null, 66, '2025-06-26 20:29:06', null),
|
||||||
(69, '删除', 'DeleteNotice', null, 0, null, 2, null, 'sys:notice:del', 1, 0, 1, '', null, 66, '2025-06-26 20:29:06', null),
|
(69, '删除', 'DeleteNotice', null, 0, null, 2, null, 'sys:notice:del', 1, 0, 1, '', null, 66, '2025-06-26 20:29:06', null),
|
||||||
(70, '代码生成', 'PluginCodeGenerator', '/plugins/code-generator', 10, 'tabler:code', 1, '/plugins/code_generator/views/index', null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', '2025-06-26 20:35:25'),
|
(70, 'code_generator.menu', 'PluginCodeGenerator', '/plugins/code-generator', 10, 'tabler:code', 1, '/plugins/code_generator/views/index', null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', '2025-06-26 20:35:25'),
|
||||||
(71, '新增业务', 'AddGenCodeBusiness', '', 0, null, 2, null, 'codegen:business:add', 1, 0, 1, '', null, 70, '2025-06-26 20:29:06', '2025-06-26 20:45:16'),
|
(71, '新增业务', 'AddGenCodeBusiness', '', 0, null, 2, null, 'codegen:business:add', 1, 0, 1, '', null, 70, '2025-06-26 20:29:06', '2025-06-26 20:45:16'),
|
||||||
(72, '修改业务', 'EditGenCodeBusiness', null, 0, null, 2, null, 'codegen:business:edit', 1, 0, 1, '', null, 70, '2025-06-26 20:29:06', null),
|
(72, '修改业务', 'EditGenCodeBusiness', null, 0, null, 2, null, 'codegen:business:edit', 1, 0, 1, '', null, 70, '2025-06-26 20:29:06', null),
|
||||||
(73, '删除业务', 'DeleteGenCodeBusiness', null, 0, null, 2, null, 'codegen:business:del', 1, 0, 1, '', null, 70, '2025-06-26 20:29:06', null),
|
(73, '删除业务', 'DeleteGenCodeBusiness', null, 0, null, 2, null, 'codegen:business:del', 1, 0, 1, '', null, 70, '2025-06-26 20:29:06', null),
|
||||||
|
|||||||
@@ -3,76 +3,76 @@ values (2048601264366944256, '测试', 0, null, null, null, 1, 0, null, now(), n
|
|||||||
|
|
||||||
insert into sys_menu (id, title, name, path, sort, icon, type, component, perms, status, display, cache, link, remark, parent_id, created_time, updated_time)
|
insert into sys_menu (id, title, name, path, sort, icon, type, component, perms, status, display, cache, link, remark, parent_id, created_time, updated_time)
|
||||||
values
|
values
|
||||||
(2049629108245233664, '概览', 'Dashboard', '/dashboard', 0, 'ant-design:dashboard-outlined', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
(2049629108245233664, 'page.dashboard.title', 'Dashboard', '/dashboard', 0, 'ant-design:dashboard-outlined', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233665, '分析页', 'Analytics', '/analytics', 0, 'lucide:area-chart', 1, '/dashboard/analytics/index', null, 1, 1, 1, '', null, 2049629108245233664, '2025-06-26 20:29:06', null),
|
(2049629108245233665, 'page.dashboard.analytics', 'Analytics', '/analytics', 0, 'lucide:area-chart', 1, '/dashboard/analytics/index', null, 1, 1, 1, '', null, 2049629108245233664, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233666, '工作台', 'Workspace', '/workspace', 1, 'carbon:workspace', 1, '/dashboard/workspace/index', null, 1, 1, 1, '', null, 2049629108245233664, '2025-06-26 20:29:06', null),
|
(2049629108245233666, 'page.dashboard.workspace', 'Workspace', '/workspace', 1, 'carbon:workspace', 1, '/dashboard/workspace/index', null, 1, 1, 1, '', null, 2049629108245233664, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233667, '系统管理', 'System', '/system', 1, 'eos-icons:admin', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
(2049629108245233667, 'page.menu.system', 'System', '/system', 1, 'eos-icons:admin', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233668, '部门管理', 'SysDept', '/system/dept', 1, 'mingcute:department-line', 1, '/system/dept/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null),
|
(2049629108245233668, 'page.menu.sysDept', 'SysDept', '/system/dept', 1, 'mingcute:department-line', 1, '/system/dept/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233669, '新增', 'AddSysDept', null, 0, null, 2, null, 'sys:dept:add', 1, 0, 1, '', null, 2049629108245233668, '2025-06-26 20:29:06', null),
|
(2049629108245233669, '新增', 'AddSysDept', null, 0, null, 2, null, 'sys:dept:add', 1, 0, 1, '', null, 2049629108245233668, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233670, '修改', 'EditSysDept', null, 0, null, 2, null, 'sys:dept:edit', 1, 0, 1, '', null, 2049629108245233668, '2025-06-26 20:29:06', null),
|
(2049629108245233670, '修改', 'EditSysDept', null, 0, null, 2, null, 'sys:dept:edit', 1, 0, 1, '', null, 2049629108245233668, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233671, '删除', 'DeleteSysDept', null, 0, null, 2, null, 'sys:dept:del', 1, 0, 1, '', null, 2049629108245233668, '2025-06-26 20:29:06', null),
|
(2049629108245233671, '删除', 'DeleteSysDept', null, 0, null, 2, null, 'sys:dept:del', 1, 0, 1, '', null, 2049629108245233668, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233672, '用户管理', 'SysUser', '/system/user', 2, 'ant-design:user-outlined', 1, '/system/user/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null),
|
(2049629108245233672, 'page.menu.sysUser', 'SysUser', '/system/user', 2, 'ant-design:user-outlined', 1, '/system/user/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233673, '删除', 'DeleteSysUser', null, 0, null, 2, null, 'sys:user:del', 1, 0, 1, '', null, 2049629108245233672, '2025-06-26 20:29:06', null),
|
(2049629108245233673, '删除', 'DeleteSysUser', null, 0, null, 2, null, 'sys:user:del', 1, 0, 1, '', null, 2049629108245233672, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233674, '角色管理', 'SysRole', '/system/role', 3, 'carbon:user-role', 1, '/system/role/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null),
|
(2049629108245233674, 'page.menu.sysRole', 'SysRole', '/system/role', 3, 'carbon:user-role', 1, '/system/role/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233675, '新增', 'AddSysRole', null, 0, null, 2, null, 'sys:role:add', 1, 0, 1, '', null, 2049629108245233674, '2025-06-26 20:29:06', null),
|
(2049629108245233675, '新增', 'AddSysRole', null, 0, null, 2, null, 'sys:role:add', 1, 0, 1, '', null, 2049629108245233674, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233676, '修改', 'EditSysRole', null, 0, null, 2, null, 'sys:role:edit', 1, 0, 1, '', null, 2049629108245233674, '2025-06-26 20:29:06', null),
|
(2049629108245233676, '修改', 'EditSysRole', null, 0, null, 2, null, 'sys:role:edit', 1, 0, 1, '', null, 2049629108245233674, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233677, '修改角色菜单', 'EditSysRoleMenu', null, 0, null, 2, null, 'sys:role:menu:edit', 1, 0, 1, '', null, 2049629108245233674, '2025-06-26 20:29:06', null),
|
(2049629108245233677, '修改角色菜单', 'EditSysRoleMenu', null, 0, null, 2, null, 'sys:role:menu:edit', 1, 0, 1, '', null, 2049629108245233674, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233678, '修改角色数据范围', 'EditSysRoleScope', null, 0, null, 2, null, 'sys:role:scope:edit', 1, 0, 1, '', null, 2049629108245233674, '2025-06-26 20:29:06', null),
|
(2049629108245233678, '修改角色数据范围', 'EditSysRoleScope', null, 0, null, 2, null, 'sys:role:scope:edit', 1, 0, 1, '', null, 2049629108245233674, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233679, '删除', 'DeleteSysRole', null, 0, null, 2, null, 'sys:role:del', 1, 0, 1, '', null, 2049629108245233674, '2025-06-26 20:29:06', null),
|
(2049629108245233679, '删除', 'DeleteSysRole', null, 0, null, 2, null, 'sys:role:del', 1, 0, 1, '', null, 2049629108245233674, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233680, '菜单管理', 'SysMenu', '/system/menu', 4, 'ant-design:menu-outlined', 1, '/system/menu/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null),
|
(2049629108245233680, 'page.menu.sysMenu', 'SysMenu', '/system/menu', 4, 'ant-design:menu-outlined', 1, '/system/menu/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233681, '新增', 'AddSysMenu', null, 0, null, 2, null, 'sys:menu:add', 1, 0, 1, '', null, 2049629108245233680, '2025-06-26 20:29:06', null),
|
(2049629108245233681, '新增', 'AddSysMenu', null, 0, null, 2, null, 'sys:menu:add', 1, 0, 1, '', null, 2049629108245233680, '2025-06-26 20:29:06', null),
|
||||||
(2049629108245233682, '修改', 'EditSysMenu', null, 0, null, 2, null, 'sys:menu:edit', 1, 0, 1, '', null, 2049629108245233680, '2025-06-26 20:29:06', null),
|
(2049629108245233682, '修改', 'EditSysMenu', null, 0, null, 2, null, 'sys:menu:edit', 1, 0, 1, '', null, 2049629108245233680, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427968, '删除', 'DeleteSysMenu', null, 0, null, 2, null, 'sys:menu:del', 1, 0, 1, '', null, 2049629108245233680, '2025-06-26 20:29:06', null),
|
(2049629108249427968, '删除', 'DeleteSysMenu', null, 0, null, 2, null, 'sys:menu:del', 1, 0, 1, '', null, 2049629108245233680, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427969, '数据权限', 'SysDataPermission', '/system/data-permission', 5, 'icon-park-outline:permissions', 0, null, null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null),
|
(2049629108249427969, 'page.menu.sysDataPermission', 'SysDataPermission', '/system/data-permission', 5, 'icon-park-outline:permissions', 0, null, null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427970, '数据范围', 'SysDataScope', '/system/data-scope', 6, 'cuida:scope-outline', 1, '/system/data-permission/scope/index', null, 1, 1, 1, '', null, 2049629108249427969, '2025-06-26 20:29:06', '2025-06-26 20:37:26'),
|
(2049629108249427970, 'page.menu.sysDataScope', 'SysDataScope', '/system/data-scope', 6, 'cuida:scope-outline', 1, '/system/data-permission/scope/index', null, 1, 1, 1, '', null, 2049629108249427969, '2025-06-26 20:29:06', '2025-06-26 20:37:26'),
|
||||||
(2049629108249427971, '新增', 'AddSysDataScope', null, 0, null, 2, null, 'data:scope:add', 1, 0, 1, '', null, 2049629108249427970, '2025-06-26 20:29:06', null),
|
(2049629108249427971, '新增', 'AddSysDataScope', null, 0, null, 2, null, 'data:scope:add', 1, 0, 1, '', null, 2049629108249427970, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427972, '修改', 'EditSysDataScope', null, 0, null, 2, null, 'data:scope:edit', 1, 0, 1, '', null, 2049629108249427970, '2025-06-26 20:29:06', null),
|
(2049629108249427972, '修改', 'EditSysDataScope', null, 0, null, 2, null, 'data:scope:edit', 1, 0, 1, '', null, 2049629108249427970, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427973, '修改数据范围规则', 'EditDataScopeRule', null, 0, null, 2, null, 'data:scope:rule:edit', 1, 0, 1, '', null, 2049629108249427970, '2025-06-26 20:29:06', null),
|
(2049629108249427973, '修改数据范围规则', 'EditDataScopeRule', null, 0, null, 2, null, 'data:scope:rule:edit', 1, 0, 1, '', null, 2049629108249427970, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427974, '删除', 'DeleteSysDataScope', null, 0, null, 2, null, 'data:scope:del', 1, 0, 1, '', null, 2049629108249427970, '2025-06-26 20:29:06', null),
|
(2049629108249427974, '删除', 'DeleteSysDataScope', null, 0, null, 2, null, 'data:scope:del', 1, 0, 1, '', null, 2049629108249427970, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427975, '数据规则', 'SysDataRule', '/system/data-rule', 7, 'material-symbols:rule', 1, '/system/data-permission/rule/index', null, 1, 1, 1, '', null, 2049629108249427969, '2025-06-26 20:29:06', '2025-06-26 20:37:40'),
|
(2049629108249427975, 'page.menu.sysDataRule', 'SysDataRule', '/system/data-rule', 7, 'material-symbols:rule', 1, '/system/data-permission/rule/index', null, 1, 1, 1, '', null, 2049629108249427969, '2025-06-26 20:29:06', '2025-06-26 20:37:40'),
|
||||||
(2049629108249427976, '新增', 'AddSysDataRule', null, 0, null, 2, null, 'data:rule:add', 1, 0, 1, '', null, 2049629108249427975, '2025-06-26 20:29:06', null),
|
(2049629108249427976, '新增', 'AddSysDataRule', null, 0, null, 2, null, 'data:rule:add', 1, 0, 1, '', null, 2049629108249427975, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427977, '修改', 'EditSysDataRule', null, 0, null, 2, null, 'data:rule:edit', 1, 0, 1, '', null, 2049629108249427975, '2025-06-26 20:29:06', null),
|
(2049629108249427977, '修改', 'EditSysDataRule', null, 0, null, 2, null, 'data:rule:edit', 1, 0, 1, '', null, 2049629108249427975, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427978, '删除', 'DeleteSysDataRule', null, 0, null, 2, null, 'data:rule:del', 1, 0, 1, '', null, 2049629108249427975, '2025-06-26 20:29:06', null),
|
(2049629108249427978, '删除', 'DeleteSysDataRule', null, 0, null, 2, null, 'data:rule:del', 1, 0, 1, '', null, 2049629108249427975, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427979, '插件管理', 'SysPlugin', '/system/plugin', 8, 'clarity:plugin-line', 1, '/system/plugin/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null),
|
(2049629108249427979, 'page.menu.sysPlugin', 'SysPlugin', '/system/plugin', 8, 'clarity:plugin-line', 1, '/system/plugin/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427980, '安装', 'InstallSysPlugin', null, 0, null, 2, null, 'sys:plugin:install', 1, 0, 1, '', null, 2049629108249427979, '2025-06-26 20:29:06', null),
|
(2049629108249427980, '安装', 'InstallSysPlugin', null, 0, null, 2, null, 'sys:plugin:install', 1, 0, 1, '', null, 2049629108249427979, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427981, '卸载', 'UninstallSysPlugin', null, 0, null, 2, null, 'sys:plugin:uninstall', 1, 0, 1, '', null, 2049629108249427979, '2025-06-26 20:29:06', null),
|
(2049629108249427981, '卸载', 'UninstallSysPlugin', null, 0, null, 2, null, 'sys:plugin:uninstall', 1, 0, 1, '', null, 2049629108249427979, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427982, '修改', 'EditSysPlugin', null, 0, null, 2, null, 'sys:plugin:edit', 1, 0, 1, '', null, 2049629108249427979, '2025-06-26 20:29:06', null),
|
(2049629108249427982, '修改', 'EditSysPlugin', null, 0, null, 2, null, 'sys:plugin:edit', 1, 0, 1, '', null, 2049629108249427979, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427983, '任务调度', 'Scheduler', '/scheduler', 2, 'material-symbols:automation', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
(2049629108249427983, 'page.menu.scheduler', 'Scheduler', '/scheduler', 2, 'material-symbols:automation', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427984, '调度管理', 'SchedulerManage', '/scheduler/manage', 1, 'ix:scheduler', 1, '/scheduler/manage/index', null, 1, 1, 1, '', null, 2049629108249427983, '2025-06-26 20:29:06', null),
|
(2049629108249427984, 'page.menu.schedulerManage', 'SchedulerManage', '/scheduler/manage', 1, 'ix:scheduler', 1, '/scheduler/manage/index', null, 1, 1, 1, '', null, 2049629108249427983, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427985, '调度记录', 'SchedulerRecord', '/scheduler/record', 2, 'ix:scheduler', 1, '/scheduler/record/index', null, 1, 1, 1, '', null, 2049629108249427983, '2025-06-26 20:29:06', null),
|
(2049629108249427985, 'page.menu.schedulerRecord', 'SchedulerRecord', '/scheduler/record', 2, 'ix:scheduler', 1, '/scheduler/record/index', null, 1, 1, 1, '', null, 2049629108249427983, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427986, '日志管理', 'Log', '/log', 3, 'carbon:cloud-logging', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
(2049629108249427986, 'page.menu.log', 'Log', '/log', 3, 'carbon:cloud-logging', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427987, '登录日志', 'LoginLog', '/log/login', 1, 'mdi:login', 1, '/log/login/index', null, 1, 1, 1, '', null, 2049629108249427986, '2025-06-26 20:29:06', null),
|
(2049629108249427987, 'page.menu.login', 'LoginLog', '/log/login', 1, 'mdi:login', 1, '/log/login/index', null, 1, 1, 1, '', null, 2049629108249427986, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427988, '删除', 'DeleteLoginLog', null, 0, null, 2, null, 'log:login:del', 1, 0, 1, '', null, 2049629108249427987, '2025-06-26 20:29:06', null),
|
(2049629108249427988, '删除', 'DeleteLoginLog', null, 0, null, 2, null, 'log:login:del', 1, 0, 1, '', null, 2049629108249427987, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427989, '清空', 'EmptyLoginLog', null, 0, null, 2, null, 'log:login:clear', 1, 0, 1, '', null, 2049629108249427987, '2025-06-26 20:29:06', null),
|
(2049629108249427989, '清空', 'EmptyLoginLog', null, 0, null, 2, null, 'log:login:clear', 1, 0, 1, '', null, 2049629108249427987, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427990, '操作日志', 'OperaLog', '/log/opera', 2, 'carbon:operations-record', 1, '/log/opera/index', null, 1, 1, 1, '', null, 2049629108249427986, '2025-06-26 20:29:06', null),
|
(2049629108249427990, 'page.menu.opera', 'OperaLog', '/log/opera', 2, 'carbon:operations-record', 1, '/log/opera/index', null, 1, 1, 1, '', null, 2049629108249427986, '2025-06-26 20:29:06', null),
|
||||||
(2049629108249427991, '删除', 'DeleteOperaLog', null, 0, null, 2, null, 'log:opera:del', 1, 0, 1, '', null, 2049629108249427990, '2025-06-26 20:29:06', null),
|
(2049629108249427991, '删除', 'DeleteOperaLog', null, 0, null, 2, null, 'log:opera:del', 1, 0, 1, '', null, 2049629108249427990, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622272, '清空', 'EmptyOperaLog', null, 0, null, 2, null, 'log:opera:clear', 1, 0, 1, '', null, 2049629108249427990, '2025-06-26 20:29:06', null),
|
(2049629108253622272, '清空', 'EmptyOperaLog', null, 0, null, 2, null, 'log:opera:clear', 1, 0, 1, '', null, 2049629108249427990, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622273, '系统监控', 'Monitor', '/monitor', 4, 'mdi:monitor-eye', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
(2049629108253622273, 'page.menu.monitor', 'Monitor', '/monitor', 4, 'mdi:monitor-eye', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622274, '在线用户', 'Online', '/log/online', 1, 'wpf:online', 1, '/monitor/online/index', null, 1, 1, 1, '', null, 2049629108253622273, '2025-06-26 20:29:06', null),
|
(2049629108253622274, 'page.menu.online', 'Online', '/log/online', 1, 'wpf:online', 1, '/monitor/online/index', null, 1, 1, 1, '', null, 2049629108253622273, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622275, '下线', 'KickOutOnline', null, 0, null, 2, null, 'sys:session:delete', 1, 0, 1, '', null, 2049629108253622274, '2025-06-26 20:29:06', null),
|
(2049629108253622275, '下线', 'KickOutOnline', null, 0, null, 2, null, 'sys:session:delete', 1, 0, 1, '', null, 2049629108253622274, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622276, 'Redis', 'Redis', '/monitor/redis', 2, 'devicon:redis', 1, '/monitor/redis/index', null, 1, 1, 1, '', null, 2049629108253622273, '2025-06-26 20:29:06', null),
|
(2049629108253622276, 'page.menu.redis', 'Redis', '/monitor/redis', 2, 'devicon:redis', 1, '/monitor/redis/index', null, 1, 1, 1, '', null, 2049629108253622273, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622277, 'Server', 'Server', '/monitor/server', 3, 'mdi:server-outline', 1, '/monitor/server/index', null, 1, 1, 1, '', null, 2049629108253622273, '2025-06-26 20:29:06', null),
|
(2049629108253622277, 'page.menu.server', 'Server', '/monitor/server', 3, 'mdi:server-outline', 1, '/monitor/server/index', null, 1, 1, 1, '', null, 2049629108253622273, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622278, '项目', 'Project', '/fba', 5, 'https://wu-clan.github.io/picx-images-hosting/logo/fba.png', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
(2049629108253622278, '项目', 'Project', '/fba', 5, 'https://wu-clan.github.io/picx-images-hosting/logo/fba.png', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622279, '文档', 'Document', '/fba/document', 1, 'lucide:book-open-text', 4, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://fastapi-practices.github.io/fastapi_best_architecture_docs', null, 2049629108253622278, '2025-06-26 20:29:06', null),
|
(2049629108253622279, '文档', 'Document', '/fba/document', 1, 'lucide:book-open-text', 4, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://fastapi-practices.github.io/fastapi_best_architecture_docs', null, 2049629108253622278, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622280, 'Github', 'Github', '/fba/github', 2, 'ant-design:github-filled', 4, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://github.com/fastapi-practices/fastapi_best_architecture', null, 2049629108253622278, '2025-06-26 20:29:06', null),
|
(2049629108253622280, 'Github', 'Github', '/fba/github', 2, 'ant-design:github-filled', 4, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://github.com/fastapi-practices/fastapi_best_architecture', null, 2049629108253622278, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622281, 'Apifox', 'Apifox', '/fba/apifox', 3, 'simple-icons:apifox', 3, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://apifox.com/apidoc/shared-28a93f02-730b-4f33-bb5e-4dad92058cc0', null, 2049629108253622278, '2025-06-26 20:29:06', null),
|
(2049629108253622281, 'Apifox', 'Apifox', '/fba/apifox', 3, 'simple-icons:apifox', 3, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://apifox.com/apidoc/shared-28a93f02-730b-4f33-bb5e-4dad92058cc0', null, 2049629108253622278, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622282, '个人中心', 'Profile', '/profile', 6, 'ant-design:profile-outlined', 1, '/_core/profile/index', null, 1, 0, 1, '', null, null, '2025-06-26 20:29:06', null),
|
(2049629108253622282, 'page.menu.profile', 'Profile', '/profile', 6, 'ant-design:profile-outlined', 1, '/_core/profile/index', null, 1, 0, 1, '', null, null, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622283, '参数管理', 'PluginConfig', '/plugins/config', 7, 'codicon:symbol-parameter', 1, '/plugins/config/views/index', null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', '2025-06-26 20:34:51'),
|
(2049629108253622283, 'config.menu', 'PluginConfig', '/plugins/config', 7, 'codicon:symbol-parameter', 1, '/plugins/config/views/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', '2025-06-26 20:34:51'),
|
||||||
(2049629108253622284, '新增', 'AddConfig', null, 0, null, 2, null, 'sys:config:add', 1, 0, 1, '', null, 2049629108253622283, '2025-06-26 20:29:06', null),
|
(2049629108253622284, '新增', 'AddConfig', null, 0, null, 2, null, 'sys:config:add', 1, 0, 1, '', null, 2049629108253622283, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622285, '修改', 'EditConfig', null, 0, null, 2, null, 'sys:config:edit', 1, 0, 1, '', null, 2049629108253622283, '2025-06-26 20:29:06', null),
|
(2049629108253622285, '修改', 'EditConfig', null, 0, null, 2, null, 'sys:config:edit', 1, 0, 1, '', null, 2049629108253622283, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622286, '删除', 'DeleteConfig', null, 0, null, 2, null, 'sys:config:del', 1, 0, 1, '', null, 2049629108253622283, '2025-06-26 20:29:06', null),
|
(2049629108253622286, '删除', 'DeleteConfig', null, 0, null, 2, null, 'sys:config:del', 1, 0, 1, '', null, 2049629108253622283, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622287, '字典管理', 'PluginDict', '/plugins/dict', 8, 'fluent-mdl2:dictionary', 1, '/plugins/dict/views/index', null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', '2025-06-26 20:35:07'),
|
(2049629108253622287, 'dict.menu', 'PluginDict', '/plugins/dict', 8, 'fluent-mdl2:dictionary', 1, '/plugins/dict/views/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', '2025-06-26 20:35:07'),
|
||||||
(2049629108253622288, '新增类型', 'AddDictType', null, 0, null, 2, null, 'dict:type:add', 1, 0, 1, '', null, 2049629108253622287, '2025-06-26 20:29:06', null),
|
(2049629108253622288, '新增类型', 'AddDictType', null, 0, null, 2, null, 'dict:type:add', 1, 0, 1, '', null, 2049629108253622287, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622289, '修改类型', 'EditDictType', null, 0, null, 2, null, 'dict:type:edit', 1, 0, 1, '', null, 2049629108253622287, '2025-06-26 20:29:06', null),
|
(2049629108253622289, '修改类型', 'EditDictType', null, 0, null, 2, null, 'dict:type:edit', 1, 0, 1, '', null, 2049629108253622287, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622290, '删除类型', 'DeleteDictType', null, 0, null, 2, null, 'dict:type:del', 1, 0, 1, '', null, 2049629108253622287, '2025-06-26 20:29:06', null),
|
(2049629108253622290, '删除类型', 'DeleteDictType', null, 0, null, 2, null, 'dict:type:del', 1, 0, 1, '', null, 2049629108253622287, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622291, '新增数据', 'AddDictData', null, 0, null, 2, null, 'dict:data:add', 1, 0, 1, '', null, 2049629108253622287, '2025-06-26 20:29:06', null),
|
(2049629108253622291, '新增数据', 'AddDictData', null, 0, null, 2, null, 'dict:data:add', 1, 0, 1, '', null, 2049629108253622287, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622292, '修改数据', 'EditDictData', null, 0, null, 2, null, 'dict:data:edit', 1, 0, 1, '', null, 2049629108253622287, '2025-06-26 20:29:06', null),
|
(2049629108253622292, '修改数据', 'EditDictData', null, 0, null, 2, null, 'dict:data:edit', 1, 0, 1, '', null, 2049629108253622287, '2025-06-26 20:29:06', null),
|
||||||
(2049629108253622293, '删除数据', 'DeleteDictData', null, 0, null, 2, null, 'dict:data:del', 1, 0, 1, '', null, 2049629108253622287, '2025-06-26 20:29:06', null),
|
(2049629108253622293, '删除数据', 'DeleteDictData', null, 0, null, 2, null, 'dict:data:del', 1, 0, 1, '', null, 2049629108253622287, '2025-06-26 20:29:06', null),
|
||||||
(2049629108257816576, '通知公告', 'PluginNotice', '/plugins/notice', 9, 'fe:notice-push', 1, '/plugins/notice/views/index', null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', '2025-06-26 20:35:14'),
|
(2049629108257816576, 'notice.menu', 'PluginNotice', '/plugins/notice', 9, 'fe:notice-push', 1, '/plugins/notice/views/index', null, 1, 1, 1, '', null, 2049629108245233667, '2025-06-26 20:29:06', '2025-06-26 20:35:14'),
|
||||||
(2049629108257816577, '新增', 'AddNotice', null, 0, null, 2, null, 'sys:notice:add', 1, 0, 1, '', null, 2049629108257816576, '2025-06-26 20:29:06', null),
|
(2049629108257816577, '新增', 'AddNotice', null, 0, null, 2, null, 'sys:notice:add', 1, 0, 1, '', null, 2049629108257816576, '2025-06-26 20:29:06', null),
|
||||||
(2049629108257816578, '修改', 'EditNotice', null, 0, null, 2, null, 'sys:notice:edit', 1, 0, 1, '', null, 2049629108257816576, '2025-06-26 20:29:06', null),
|
(2049629108257816578, '修改', 'EditNotice', null, 0, null, 2, null, 'sys:notice:edit', 1, 0, 1, '', null, 2049629108257816576, '2025-06-26 20:29:06', null),
|
||||||
(2049629108257816579, '删除', 'DeleteNotice', null, 0, null, 2, null, 'sys:notice:del', 1, 0, 1, '', null, 2049629108257816576, '2025-06-26 20:29:06', null),
|
(2049629108257816579, '删除', 'DeleteNotice', null, 0, null, 2, null, 'sys:notice:del', 1, 0, 1, '', null, 2049629108257816576, '2025-06-26 20:29:06', null),
|
||||||
(2049629108257816580, '代码生成', 'PluginCodeGenerator', '/plugins/code-generator', 10, 'tabler:code', 1, '/plugins/code_generator/views/index', null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', '2025-06-26 20:35:25'),
|
(2049629108257816580, 'code_generator.menu', 'PluginCodeGenerator', '/plugins/code-generator', 10, 'tabler:code', 1, '/plugins/code_generator/views/index', null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', '2025-06-26 20:35:25'),
|
||||||
(2049629108257816581, '新增业务', 'AddGenCodeBusiness', '', 0, null, 2, null, 'codegen:business:add', 1, 0, 1, '', null, 2049629108257816580, '2025-06-26 20:29:06', '2025-06-26 20:45:16'),
|
(2049629108257816581, '新增业务', 'AddGenCodeBusiness', '', 0, null, 2, null, 'codegen:business:add', 1, 0, 1, '', null, 2049629108257816580, '2025-06-26 20:29:06', '2025-06-26 20:45:16'),
|
||||||
(2049629108257816582, '修改业务', 'EditGenCodeBusiness', null, 0, null, 2, null, 'codegen:business:edit', 1, 0, 1, '', null, 2049629108257816580, '2025-06-26 20:29:06', null),
|
(2049629108257816582, '修改业务', 'EditGenCodeBusiness', null, 0, null, 2, null, 'codegen:business:edit', 1, 0, 1, '', null, 2049629108257816580, '2025-06-26 20:29:06', null),
|
||||||
(2049629108257816583, '删除业务', 'DeleteGenCodeBusiness', null, 0, null, 2, null, 'codegen:business:del', 1, 0, 1, '', null, 2049629108257816580, '2025-06-26 20:29:06', null),
|
(2049629108257816583, '删除业务', 'DeleteGenCodeBusiness', null, 0, null, 2, null, 'codegen:business:del', 1, 0, 1, '', null, 2049629108257816580, '2025-06-26 20:29:06', null),
|
||||||
|
|||||||
@@ -3,76 +3,76 @@ values (1, '测试', 0, null, null, null, 1, 0, null, now(), null);
|
|||||||
|
|
||||||
insert into sys_menu (id, title, name, path, sort, icon, type, component, perms, status, display, cache, link, remark, parent_id, created_time, updated_time)
|
insert into sys_menu (id, title, name, path, sort, icon, type, component, perms, status, display, cache, link, remark, parent_id, created_time, updated_time)
|
||||||
values
|
values
|
||||||
(1, '概览', 'Dashboard', '/dashboard', 0, 'ant-design:dashboard-outlined', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
(1, 'page.dashboard.title', 'Dashboard', '/dashboard', 0, 'ant-design:dashboard-outlined', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
||||||
(2, '分析页', 'Analytics', '/analytics', 0, 'lucide:area-chart', 1, '/dashboard/analytics/index', null, 1, 1, 1, '', null, 1, '2025-06-26 20:29:06', null),
|
(2, 'page.dashboard.analytics', 'Analytics', '/analytics', 0, 'lucide:area-chart', 1, '/dashboard/analytics/index', null, 1, 1, 1, '', null, 1, '2025-06-26 20:29:06', null),
|
||||||
(3, '工作台', 'Workspace', '/workspace', 1, 'carbon:workspace', 1, '/dashboard/workspace/index', null, 1, 1, 1, '', null, 1, '2025-06-26 20:29:06', null),
|
(3, 'page.dashboard.workspace', 'Workspace', '/workspace', 1, 'carbon:workspace', 1, '/dashboard/workspace/index', null, 1, 1, 1, '', null, 1, '2025-06-26 20:29:06', null),
|
||||||
(4, '系统管理', 'System', '/system', 1, 'eos-icons:admin', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
(4, 'page.menu.system', 'System', '/system', 1, 'eos-icons:admin', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
||||||
(5, '部门管理', 'SysDept', '/system/dept', 1, 'mingcute:department-line', 1, '/system/dept/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null),
|
(5, 'page.menu.sysDept', 'SysDept', '/system/dept', 1, 'mingcute:department-line', 1, '/system/dept/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null),
|
||||||
(6, '新增', 'AddSysDept', null, 0, null, 2, null, 'sys:dept:add', 1, 0, 1, '', null, 5, '2025-06-26 20:29:06', null),
|
(6, '新增', 'AddSysDept', null, 0, null, 2, null, 'sys:dept:add', 1, 0, 1, '', null, 5, '2025-06-26 20:29:06', null),
|
||||||
(7, '修改', 'EditSysDept', null, 0, null, 2, null, 'sys:dept:edit', 1, 0, 1, '', null, 5, '2025-06-26 20:29:06', null),
|
(7, '修改', 'EditSysDept', null, 0, null, 2, null, 'sys:dept:edit', 1, 0, 1, '', null, 5, '2025-06-26 20:29:06', null),
|
||||||
(8, '删除', 'DeleteSysDept', null, 0, null, 2, null, 'sys:dept:del', 1, 0, 1, '', null, 5, '2025-06-26 20:29:06', null),
|
(8, '删除', 'DeleteSysDept', null, 0, null, 2, null, 'sys:dept:del', 1, 0, 1, '', null, 5, '2025-06-26 20:29:06', null),
|
||||||
(9, '用户管理', 'SysUser', '/system/user', 2, 'ant-design:user-outlined', 1, '/system/user/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null),
|
(9, 'page.menu.sysUser', 'SysUser', '/system/user', 2, 'ant-design:user-outlined', 1, '/system/user/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null),
|
||||||
(10, '删除', 'DeleteSysUser', null, 0, null, 2, null, 'sys:user:del', 1, 0, 1, '', null, 9, '2025-06-26 20:29:06', null),
|
(10, '删除', 'DeleteSysUser', null, 0, null, 2, null, 'sys:user:del', 1, 0, 1, '', null, 9, '2025-06-26 20:29:06', null),
|
||||||
(11, '角色管理', 'SysRole', '/system/role', 3, 'carbon:user-role', 1, '/system/role/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null),
|
(11, 'page.menu.sysRole', 'SysRole', '/system/role', 3, 'carbon:user-role', 1, '/system/role/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null),
|
||||||
(12, '新增', 'AddSysRole', null, 0, null, 2, null, 'sys:role:add', 1, 0, 1, '', null, 11, '2025-06-26 20:29:06', null),
|
(12, '新增', 'AddSysRole', null, 0, null, 2, null, 'sys:role:add', 1, 0, 1, '', null, 11, '2025-06-26 20:29:06', null),
|
||||||
(13, '修改', 'EditSysRole', null, 0, null, 2, null, 'sys:role:edit', 1, 0, 1, '', null, 11, '2025-06-26 20:29:06', null),
|
(13, '修改', 'EditSysRole', null, 0, null, 2, null, 'sys:role:edit', 1, 0, 1, '', null, 11, '2025-06-26 20:29:06', null),
|
||||||
(14, '修改角色菜单', 'EditSysRoleMenu', null, 0, null, 2, null, 'sys:role:menu:edit', 1, 0, 1, '', null, 11, '2025-06-26 20:29:06', null),
|
(14, '修改角色菜单', 'EditSysRoleMenu', null, 0, null, 2, null, 'sys:role:menu:edit', 1, 0, 1, '', null, 11, '2025-06-26 20:29:06', null),
|
||||||
(15, '修改角色数据范围', 'EditSysRoleScope', null, 0, null, 2, null, 'sys:role:scope:edit', 1, 0, 1, '', null, 11, '2025-06-26 20:29:06', null),
|
(15, '修改角色数据范围', 'EditSysRoleScope', null, 0, null, 2, null, 'sys:role:scope:edit', 1, 0, 1, '', null, 11, '2025-06-26 20:29:06', null),
|
||||||
(16, '删除', 'DeleteSysRole', null, 0, null, 2, null, 'sys:role:del', 1, 0, 1, '', null, 11, '2025-06-26 20:29:06', null),
|
(16, '删除', 'DeleteSysRole', null, 0, null, 2, null, 'sys:role:del', 1, 0, 1, '', null, 11, '2025-06-26 20:29:06', null),
|
||||||
(17, '菜单管理', 'SysMenu', '/system/menu', 4, 'ant-design:menu-outlined', 1, '/system/menu/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null),
|
(17, 'page.menu.sysMenu', 'SysMenu', '/system/menu', 4, 'ant-design:menu-outlined', 1, '/system/menu/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null),
|
||||||
(18, '新增', 'AddSysMenu', null, 0, null, 2, null, 'sys:menu:add', 1, 0, 1, '', null, 17, '2025-06-26 20:29:06', null),
|
(18, '新增', 'AddSysMenu', null, 0, null, 2, null, 'sys:menu:add', 1, 0, 1, '', null, 17, '2025-06-26 20:29:06', null),
|
||||||
(19, '修改', 'EditSysMenu', null, 0, null, 2, null, 'sys:menu:edit', 1, 0, 1, '', null, 17, '2025-06-26 20:29:06', null),
|
(19, '修改', 'EditSysMenu', null, 0, null, 2, null, 'sys:menu:edit', 1, 0, 1, '', null, 17, '2025-06-26 20:29:06', null),
|
||||||
(20, '删除', 'DeleteSysMenu', null, 0, null, 2, null, 'sys:menu:del', 1, 0, 1, '', null, 17, '2025-06-26 20:29:06', null),
|
(20, '删除', 'DeleteSysMenu', null, 0, null, 2, null, 'sys:menu:del', 1, 0, 1, '', null, 17, '2025-06-26 20:29:06', null),
|
||||||
(21, '数据权限', 'SysDataPermission', '/system/data-permission', 5, 'icon-park-outline:permissions', 0, null, null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null),
|
(21, 'page.menu.sysDataPermission', 'SysDataPermission', '/system/data-permission', 5, 'icon-park-outline:permissions', 0, null, null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null),
|
||||||
(22, '数据范围', 'SysDataScope', '/system/data-scope', 6, 'cuida:scope-outline', 1, '/system/data-permission/scope/index', null, 1, 1, 1, '', null, 21, '2025-06-26 20:29:06', '2025-06-26 20:37:26'),
|
(22, 'page.menu.sysDataScope', 'SysDataScope', '/system/data-scope', 6, 'cuida:scope-outline', 1, '/system/data-permission/scope/index', null, 1, 1, 1, '', null, 21, '2025-06-26 20:29:06', '2025-06-26 20:37:26'),
|
||||||
(23, '新增', 'AddSysDataScope', null, 0, null, 2, null, 'data:scope:add', 1, 0, 1, '', null, 22, '2025-06-26 20:29:06', null),
|
(23, '新增', 'AddSysDataScope', null, 0, null, 2, null, 'data:scope:add', 1, 0, 1, '', null, 22, '2025-06-26 20:29:06', null),
|
||||||
(24, '修改', 'EditSysDataScope', null, 0, null, 2, null, 'data:scope:edit', 1, 0, 1, '', null, 22, '2025-06-26 20:29:06', null),
|
(24, '修改', 'EditSysDataScope', null, 0, null, 2, null, 'data:scope:edit', 1, 0, 1, '', null, 22, '2025-06-26 20:29:06', null),
|
||||||
(25, '修改数据范围规则', 'EditDataScopeRule', null, 0, null, 2, null, 'data:scope:rule:edit', 1, 0, 1, '', null, 22, '2025-06-26 20:29:06', null),
|
(25, '修改数据范围规则', 'EditDataScopeRule', null, 0, null, 2, null, 'data:scope:rule:edit', 1, 0, 1, '', null, 22, '2025-06-26 20:29:06', null),
|
||||||
(26, '删除', 'DeleteSysDataScope', null, 0, null, 2, null, 'data:scope:del', 1, 0, 1, '', null, 22, '2025-06-26 20:29:06', null),
|
(26, '删除', 'DeleteSysDataScope', null, 0, null, 2, null, 'data:scope:del', 1, 0, 1, '', null, 22, '2025-06-26 20:29:06', null),
|
||||||
(27, '数据规则', 'SysDataRule', '/system/data-rule', 7, 'material-symbols:rule', 1, '/system/data-permission/rule/index', null, 1, 1, 1, '', null, 21, '2025-06-26 20:29:06', '2025-06-26 20:37:40'),
|
(27, 'page.menu.sysDataRule', 'SysDataRule', '/system/data-rule', 7, 'material-symbols:rule', 1, '/system/data-permission/rule/index', null, 1, 1, 1, '', null, 21, '2025-06-26 20:29:06', '2025-06-26 20:37:40'),
|
||||||
(28, '新增', 'AddSysDataRule', null, 0, null, 2, null, 'data:rule:add', 1, 0, 1, '', null, 27, '2025-06-26 20:29:06', null),
|
(28, '新增', 'AddSysDataRule', null, 0, null, 2, null, 'data:rule:add', 1, 0, 1, '', null, 27, '2025-06-26 20:29:06', null),
|
||||||
(29, '修改', 'EditSysDataRule', null, 0, null, 2, null, 'data:rule:edit', 1, 0, 1, '', null, 27, '2025-06-26 20:29:06', null),
|
(29, '修改', 'EditSysDataRule', null, 0, null, 2, null, 'data:rule:edit', 1, 0, 1, '', null, 27, '2025-06-26 20:29:06', null),
|
||||||
(30, '删除', 'DeleteSysDataRule', null, 0, null, 2, null, 'data:rule:del', 1, 0, 1, '', null, 27, '2025-06-26 20:29:06', null),
|
(30, '删除', 'DeleteSysDataRule', null, 0, null, 2, null, 'data:rule:del', 1, 0, 1, '', null, 27, '2025-06-26 20:29:06', null),
|
||||||
(31, '插件管理', 'SysPlugin', '/system/plugin', 8, 'clarity:plugin-line', 1, '/system/plugin/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null),
|
(31, 'page.menu.sysPlugin', 'SysPlugin', '/system/plugin', 8, 'clarity:plugin-line', 1, '/system/plugin/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', null),
|
||||||
(32, '安装', 'InstallSysPlugin', null, 0, null, 2, null, 'sys:plugin:install', 1, 0, 1, '', null, 31, '2025-06-26 20:29:06', null),
|
(32, '安装', 'InstallSysPlugin', null, 0, null, 2, null, 'sys:plugin:install', 1, 0, 1, '', null, 31, '2025-06-26 20:29:06', null),
|
||||||
(33, '卸载', 'UninstallSysPlugin', null, 0, null, 2, null, 'sys:plugin:uninstall', 1, 0, 1, '', null, 31, '2025-06-26 20:29:06', null),
|
(33, '卸载', 'UninstallSysPlugin', null, 0, null, 2, null, 'sys:plugin:uninstall', 1, 0, 1, '', null, 31, '2025-06-26 20:29:06', null),
|
||||||
(34, '修改', 'EditSysPlugin', null, 0, null, 2, null, 'sys:plugin:edit', 1, 0, 1, '', null, 31, '2025-06-26 20:29:06', null),
|
(34, '修改', 'EditSysPlugin', null, 0, null, 2, null, 'sys:plugin:edit', 1, 0, 1, '', null, 31, '2025-06-26 20:29:06', null),
|
||||||
(35, '任务调度', 'Scheduler', '/scheduler', 2, 'material-symbols:automation', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
(35, 'page.menu.scheduler', 'Scheduler', '/scheduler', 2, 'material-symbols:automation', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
||||||
(36, '调度管理', 'SchedulerManage', '/scheduler/manage', 1, 'ix:scheduler', 1, '/scheduler/manage/index', null, 1, 1, 1, '', null, 35, '2025-06-26 20:29:06', null),
|
(36, 'page.menu.schedulerManage', 'SchedulerManage', '/scheduler/manage', 1, 'ix:scheduler', 1, '/scheduler/manage/index', null, 1, 1, 1, '', null, 35, '2025-06-26 20:29:06', null),
|
||||||
(37, '调度记录', 'SchedulerRecord', '/scheduler/record', 2, 'ix:scheduler', 1, '/scheduler/record/index', null, 1, 1, 1, '', null, 35, '2025-06-26 20:29:06', null),
|
(37, 'page.menu.schedulerRecord', 'SchedulerRecord', '/scheduler/record', 2, 'ix:scheduler', 1, '/scheduler/record/index', null, 1, 1, 1, '', null, 35, '2025-06-26 20:29:06', null),
|
||||||
(38, '日志管理', 'Log', '/log', 3, 'carbon:cloud-logging', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
(38, 'page.menu.log', 'Log', '/log', 3, 'carbon:cloud-logging', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
||||||
(39, '登录日志', 'LoginLog', '/log/login', 1, 'mdi:login', 1, '/log/login/index', null, 1, 1, 1, '', null, 38, '2025-06-26 20:29:06', null),
|
(39, 'page.menu.login', 'LoginLog', '/log/login', 1, 'mdi:login', 1, '/log/login/index', null, 1, 1, 1, '', null, 38, '2025-06-26 20:29:06', null),
|
||||||
(40, '删除', 'DeleteLoginLog', null, 0, null, 2, null, 'log:login:del', 1, 0, 1, '', null, 39, '2025-06-26 20:29:06', null),
|
(40, '删除', 'DeleteLoginLog', null, 0, null, 2, null, 'log:login:del', 1, 0, 1, '', null, 39, '2025-06-26 20:29:06', null),
|
||||||
(41, '清空', 'EmptyLoginLog', null, 0, null, 2, null, 'log:login:clear', 1, 0, 1, '', null, 39, '2025-06-26 20:29:06', null),
|
(41, '清空', 'EmptyLoginLog', null, 0, null, 2, null, 'log:login:clear', 1, 0, 1, '', null, 39, '2025-06-26 20:29:06', null),
|
||||||
(42, '操作日志', 'OperaLog', '/log/opera', 2, 'carbon:operations-record', 1, '/log/opera/index', null, 1, 1, 1, '', null, 38, '2025-06-26 20:29:06', null),
|
(42, 'page.menu.opera', 'OperaLog', '/log/opera', 2, 'carbon:operations-record', 1, '/log/opera/index', null, 1, 1, 1, '', null, 38, '2025-06-26 20:29:06', null),
|
||||||
(43, '删除', 'DeleteOperaLog', null, 0, null, 2, null, 'log:opera:del', 1, 0, 1, '', null, 42, '2025-06-26 20:29:06', null),
|
(43, '删除', 'DeleteOperaLog', null, 0, null, 2, null, 'log:opera:del', 1, 0, 1, '', null, 42, '2025-06-26 20:29:06', null),
|
||||||
(44, '清空', 'EmptyOperaLog', null, 0, null, 2, null, 'log:opera:clear', 1, 0, 1, '', null, 42, '2025-06-26 20:29:06', null),
|
(44, '清空', 'EmptyOperaLog', null, 0, null, 2, null, 'log:opera:clear', 1, 0, 1, '', null, 42, '2025-06-26 20:29:06', null),
|
||||||
(45, '系统监控', 'Monitor', '/monitor', 4, 'mdi:monitor-eye', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
(45, 'page.menu.monitor', 'Monitor', '/monitor', 4, 'mdi:monitor-eye', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
||||||
(46, '在线用户', 'Online', '/log/online', 1, 'wpf:online', 1, '/monitor/online/index', null, 1, 1, 1, '', null, 45, '2025-06-26 20:29:06', null),
|
(46, 'page.menu.online', 'Online', '/log/online', 1, 'wpf:online', 1, '/monitor/online/index', null, 1, 1, 1, '', null, 45, '2025-06-26 20:29:06', null),
|
||||||
(47, '下线', 'KickOutOnline', null, 0, null, 2, null, 'sys:session:delete', 1, 0, 1, '', null, 46, '2025-06-26 20:29:06', null),
|
(47, '下线', 'KickOutOnline', null, 0, null, 2, null, 'sys:session:delete', 1, 0, 1, '', null, 46, '2025-06-26 20:29:06', null),
|
||||||
(48, 'Redis', 'Redis', '/monitor/redis', 2, 'devicon:redis', 1, '/monitor/redis/index', null, 1, 1, 1, '', null, 45, '2025-06-26 20:29:06', null),
|
(48, 'page.menu.redis', 'Redis', '/monitor/redis', 2, 'devicon:redis', 1, '/monitor/redis/index', null, 1, 1, 1, '', null, 45, '2025-06-26 20:29:06', null),
|
||||||
(49, 'Server', 'Server', '/monitor/server', 3, 'mdi:server-outline', 1, '/monitor/server/index', null, 1, 1, 1, '', null, 45, '2025-06-26 20:29:06', null),
|
(49, 'page.menu.server', 'Server', '/monitor/server', 3, 'mdi:server-outline', 1, '/monitor/server/index', null, 1, 1, 1, '', null, 45, '2025-06-26 20:29:06', null),
|
||||||
(50, '项目', 'Project', '/fba', 5, 'https://wu-clan.github.io/picx-images-hosting/logo/fba.png', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
(50, '项目', 'Project', '/fba', 5, 'https://wu-clan.github.io/picx-images-hosting/logo/fba.png', 0, null, null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', null),
|
||||||
(51, '文档', 'Document', '/fba/document', 1, 'lucide:book-open-text', 4, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://fastapi-practices.github.io/fastapi_best_architecture_docs', null, 50, '2025-06-26 20:29:06', null),
|
(51, '文档', 'Document', '/fba/document', 1, 'lucide:book-open-text', 4, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://fastapi-practices.github.io/fastapi_best_architecture_docs', null, 50, '2025-06-26 20:29:06', null),
|
||||||
(52, 'Github', 'Github', '/fba/github', 2, 'ant-design:github-filled', 4, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://github.com/fastapi-practices/fastapi_best_architecture', null, 50, '2025-06-26 20:29:06', null),
|
(52, 'Github', 'Github', '/fba/github', 2, 'ant-design:github-filled', 4, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://github.com/fastapi-practices/fastapi_best_architecture', null, 50, '2025-06-26 20:29:06', null),
|
||||||
(53, 'Apifox', 'Apifox', '/fba/apifox', 3, 'simple-icons:apifox', 3, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://apifox.com/apidoc/shared-28a93f02-730b-4f33-bb5e-4dad92058cc0', null, 50, '2025-06-26 20:29:06', null),
|
(53, 'Apifox', 'Apifox', '/fba/apifox', 3, 'simple-icons:apifox', 3, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://apifox.com/apidoc/shared-28a93f02-730b-4f33-bb5e-4dad92058cc0', null, 50, '2025-06-26 20:29:06', null),
|
||||||
(54, '个人中心', 'Profile', '/profile', 6, 'ant-design:profile-outlined', 1, '/_core/profile/index', null, 1, 0, 1, '', null, null, '2025-06-26 20:29:06', null),
|
(54, 'page.menu.profile', 'Profile', '/profile', 6, 'ant-design:profile-outlined', 1, '/_core/profile/index', null, 1, 0, 1, '', null, null, '2025-06-26 20:29:06', null),
|
||||||
(55, '参数管理', 'PluginConfig', '/plugins/config', 7, 'codicon:symbol-parameter', 1, '/plugins/config/views/index', null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', '2025-06-26 20:34:51'),
|
(55, 'config.menu', 'PluginConfig', '/plugins/config', 7, 'codicon:symbol-parameter', 1, '/plugins/config/views/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', '2025-06-26 20:34:51'),
|
||||||
(56, '新增', 'AddConfig', null, 0, null, 2, null, 'sys:config:add', 1, 0, 1, '', null, 55, '2025-06-26 20:29:06', null),
|
(56, '新增', 'AddConfig', null, 0, null, 2, null, 'sys:config:add', 1, 0, 1, '', null, 55, '2025-06-26 20:29:06', null),
|
||||||
(57, '修改', 'EditConfig', null, 0, null, 2, null, 'sys:config:edit', 1, 0, 1, '', null, 55, '2025-06-26 20:29:06', null),
|
(57, '修改', 'EditConfig', null, 0, null, 2, null, 'sys:config:edit', 1, 0, 1, '', null, 55, '2025-06-26 20:29:06', null),
|
||||||
(58, '删除', 'DeleteConfig', null, 0, null, 2, null, 'sys:config:del', 1, 0, 1, '', null, 55, '2025-06-26 20:29:06', null),
|
(58, '删除', 'DeleteConfig', null, 0, null, 2, null, 'sys:config:del', 1, 0, 1, '', null, 55, '2025-06-26 20:29:06', null),
|
||||||
(59, '字典管理', 'PluginDict', '/plugins/dict', 8, 'fluent-mdl2:dictionary', 1, '/plugins/dict/views/index', null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', '2025-06-26 20:35:07'),
|
(59, 'dict.menu', 'PluginDict', '/plugins/dict', 8, 'fluent-mdl2:dictionary', 1, '/plugins/dict/views/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', '2025-06-26 20:35:07'),
|
||||||
(60, '新增类型', 'AddDictType', null, 0, null, 2, null, 'dict:type:add', 1, 0, 1, '', null, 59, '2025-06-26 20:29:06', null),
|
(60, '新增类型', 'AddDictType', null, 0, null, 2, null, 'dict:type:add', 1, 0, 1, '', null, 59, '2025-06-26 20:29:06', null),
|
||||||
(61, '修改类型', 'EditDictType', null, 0, null, 2, null, 'dict:type:edit', 1, 0, 1, '', null, 59, '2025-06-26 20:29:06', null),
|
(61, '修改类型', 'EditDictType', null, 0, null, 2, null, 'dict:type:edit', 1, 0, 1, '', null, 59, '2025-06-26 20:29:06', null),
|
||||||
(62, '删除类型', 'DeleteDictType', null, 0, null, 2, null, 'dict:type:del', 1, 0, 1, '', null, 59, '2025-06-26 20:29:06', null),
|
(62, '删除类型', 'DeleteDictType', null, 0, null, 2, null, 'dict:type:del', 1, 0, 1, '', null, 59, '2025-06-26 20:29:06', null),
|
||||||
(63, '新增数据', 'AddDictData', null, 0, null, 2, null, 'dict:data:add', 1, 0, 1, '', null, 59, '2025-06-26 20:29:06', null),
|
(63, '新增数据', 'AddDictData', null, 0, null, 2, null, 'dict:data:add', 1, 0, 1, '', null, 59, '2025-06-26 20:29:06', null),
|
||||||
(64, '修改数据', 'EditDictData', null, 0, null, 2, null, 'dict:data:edit', 1, 0, 1, '', null, 59, '2025-06-26 20:29:06', null),
|
(64, '修改数据', 'EditDictData', null, 0, null, 2, null, 'dict:data:edit', 1, 0, 1, '', null, 59, '2025-06-26 20:29:06', null),
|
||||||
(65, '删除数据', 'DeleteDictData', null, 0, null, 2, null, 'dict:data:del', 1, 0, 1, '', null, 59, '2025-06-26 20:29:06', null),
|
(65, '删除数据', 'DeleteDictData', null, 0, null, 2, null, 'dict:data:del', 1, 0, 1, '', null, 59, '2025-06-26 20:29:06', null),
|
||||||
(66, '通知公告', 'PluginNotice', '/plugins/notice', 9, 'fe:notice-push', 1, '/plugins/notice/views/index', null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', '2025-06-26 20:35:14'),
|
(66, 'notice.menu', 'PluginNotice', '/plugins/notice', 9, 'fe:notice-push', 1, '/plugins/notice/views/index', null, 1, 1, 1, '', null, 4, '2025-06-26 20:29:06', '2025-06-26 20:35:14'),
|
||||||
(67, '新增', 'AddNotice', null, 0, null, 2, null, 'sys:notice:add', 1, 0, 1, '', null, 66, '2025-06-26 20:29:06', null),
|
(67, '新增', 'AddNotice', null, 0, null, 2, null, 'sys:notice:add', 1, 0, 1, '', null, 66, '2025-06-26 20:29:06', null),
|
||||||
(68, '修改', 'EditNotice', null, 0, null, 2, null, 'sys:notice:edit', 1, 0, 1, '', null, 66, '2025-06-26 20:29:06', null),
|
(68, '修改', 'EditNotice', null, 0, null, 2, null, 'sys:notice:edit', 1, 0, 1, '', null, 66, '2025-06-26 20:29:06', null),
|
||||||
(69, '删除', 'DeleteNotice', null, 0, null, 2, null, 'sys:notice:del', 1, 0, 1, '', null, 66, '2025-06-26 20:29:06', null),
|
(69, '删除', 'DeleteNotice', null, 0, null, 2, null, 'sys:notice:del', 1, 0, 1, '', null, 66, '2025-06-26 20:29:06', null),
|
||||||
(70, '代码生成', 'PluginCodeGenerator', '/plugins/code-generator', 10, 'tabler:code', 1, '/plugins/code_generator/views/index', null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', '2025-06-26 20:35:25'),
|
(70, 'code_generator.menu', 'PluginCodeGenerator', '/plugins/code-generator', 10, 'tabler:code', 1, '/plugins/code_generator/views/index', null, 1, 1, 1, '', null, null, '2025-06-26 20:29:06', '2025-06-26 20:35:25'),
|
||||||
(71, '新增业务', 'AddGenCodeBusiness', '', 0, null, 2, null, 'codegen:business:add', 1, 0, 1, '', null, 70, '2025-06-26 20:29:06', '2025-06-26 20:45:16'),
|
(71, '新增业务', 'AddGenCodeBusiness', '', 0, null, 2, null, 'codegen:business:add', 1, 0, 1, '', null, 70, '2025-06-26 20:29:06', '2025-06-26 20:45:16'),
|
||||||
(72, '修改业务', 'EditGenCodeBusiness', null, 0, null, 2, null, 'codegen:business:edit', 1, 0, 1, '', null, 70, '2025-06-26 20:29:06', null),
|
(72, '修改业务', 'EditGenCodeBusiness', null, 0, null, 2, null, 'codegen:business:edit', 1, 0, 1, '', null, 70, '2025-06-26 20:29:06', null),
|
||||||
(73, '删除业务', 'DeleteGenCodeBusiness', null, 0, null, 2, null, 'codegen:business:del', 1, 0, 1, '', null, 70, '2025-06-26 20:29:06', null),
|
(73, '删除业务', 'DeleteGenCodeBusiness', null, 0, null, 2, null, 'codegen:business:del', 1, 0, 1, '', null, 70, '2025-06-26 20:29:06', null),
|
||||||
|
|||||||
+26
-20
@@ -1,17 +1,16 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import atexit
|
import atexit
|
||||||
import threading
|
import threading
|
||||||
import weakref
|
import weakref
|
||||||
|
|
||||||
from typing import Awaitable, Callable, TypeVar
|
from functools import wraps
|
||||||
|
from typing import Any, Awaitable, Callable, Coroutine, TypeVar
|
||||||
|
|
||||||
T = TypeVar('T')
|
T = TypeVar('T')
|
||||||
|
|
||||||
|
|
||||||
class _TaskRunner:
|
class _TaskRunner:
|
||||||
"""A task runner that runs an asyncio event loop on a background thread."""
|
"""在后台线程上运行 asyncio 事件循环的任务运行器"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.__loop: asyncio.AbstractEventLoop | None = None
|
self.__loop: asyncio.AbstractEventLoop | None = None
|
||||||
@@ -20,48 +19,55 @@ class _TaskRunner:
|
|||||||
atexit.register(self.close)
|
atexit.register(self.close)
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""关闭事件循环"""
|
"""关闭事件循环并清理"""
|
||||||
if self.__loop:
|
if self.__loop:
|
||||||
self.__loop.stop()
|
self.__loop.stop()
|
||||||
|
self.__loop = None
|
||||||
|
if self.__thread:
|
||||||
|
self.__thread.join()
|
||||||
|
self.__thread = None
|
||||||
|
name = f'TaskRunner-{threading.get_ident()}'
|
||||||
|
_runner_map.pop(name, None)
|
||||||
|
|
||||||
def _target(self):
|
def _target(self):
|
||||||
"""后台线程目标"""
|
"""后台线程的目标函数"""
|
||||||
loop = self.__loop
|
|
||||||
try:
|
try:
|
||||||
loop.run_forever()
|
self.__loop.run_forever()
|
||||||
finally:
|
finally:
|
||||||
loop.close()
|
self.__loop.close()
|
||||||
|
|
||||||
def run(self, coro):
|
def run(self, coro: Awaitable[T]) -> T:
|
||||||
"""在后台线程上同步运行协程"""
|
"""在后台事件循环上运行协程并返回其结果"""
|
||||||
with self.__lock:
|
with self.__lock:
|
||||||
name = f'{threading.current_thread().name} - runner'
|
name = f'TaskRunner-{threading.get_ident()}'
|
||||||
if self.__loop is None:
|
if self.__loop is None:
|
||||||
self.__loop = asyncio.new_event_loop()
|
self.__loop = asyncio.new_event_loop()
|
||||||
self.__thread = threading.Thread(target=self._target, daemon=True, name=name)
|
self.__thread = threading.Thread(target=self._target, daemon=True, name=name)
|
||||||
self.__thread.start()
|
self.__thread.start()
|
||||||
fut = asyncio.run_coroutine_threadsafe(coro, self.__loop)
|
future = asyncio.run_coroutine_threadsafe(coro, self.__loop)
|
||||||
return fut.result(None)
|
return future.result()
|
||||||
|
|
||||||
|
|
||||||
_runner_map = weakref.WeakValueDictionary()
|
_runner_map = weakref.WeakValueDictionary()
|
||||||
|
|
||||||
|
|
||||||
def run_await(coro: Callable[..., Awaitable[T]]) -> Callable[..., T]:
|
def run_await(coro: Callable[..., Awaitable[T]] | Callable[..., Coroutine[Any, Any, T]]) -> Callable[..., T]:
|
||||||
"""将协程包装在一个函数中,该函数会阻塞,直到它执行完为止"""
|
"""将协程包装在函数中,该函数将在后台事件循环上运行,直到它执行完为止"""
|
||||||
|
|
||||||
|
@wraps(coro)
|
||||||
def wrapped(*args, **kwargs):
|
def wrapped(*args, **kwargs):
|
||||||
name = threading.current_thread().name
|
|
||||||
inner = coro(*args, **kwargs)
|
inner = coro(*args, **kwargs)
|
||||||
|
if not asyncio.iscoroutine(inner) and not asyncio.isfuture(inner):
|
||||||
|
raise TypeError(f'Expected coroutine, got {type(inner)}')
|
||||||
try:
|
try:
|
||||||
# 如果当前此线程中正在运行循环
|
# 如果事件循环正在运行,则使用任务调用
|
||||||
# 使用任务运行程序
|
|
||||||
asyncio.get_running_loop()
|
asyncio.get_running_loop()
|
||||||
|
name = f'TaskRunner-{threading.get_ident()}'
|
||||||
if name not in _runner_map:
|
if name not in _runner_map:
|
||||||
_runner_map[name] = _TaskRunner()
|
_runner_map[name] = _TaskRunner()
|
||||||
return _runner_map[name].run(inner)
|
return _runner_map[name].run(inner)
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
# 如果没有,请创建一个新的事件循环
|
# 如果没有,则创建一个新的事件循环
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
return loop.run_until_complete(inner)
|
return loop.run_until_complete(inner)
|
||||||
|
|
||||||
|
|||||||
@@ -6,15 +6,18 @@ from backend.common.enums import BuildTreeType
|
|||||||
from backend.utils.serializers import RowData, select_list_serialize
|
from backend.utils.serializers import RowData, select_list_serialize
|
||||||
|
|
||||||
|
|
||||||
def get_tree_nodes(row: Sequence[RowData]) -> list[dict[str, Any]]:
|
def get_tree_nodes(row: Sequence[RowData], is_sort: bool, sort_key: str) -> list[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
获取所有树形结构节点
|
获取所有树形结构节点
|
||||||
|
|
||||||
:param row: 原始数据行序列
|
:param row: 原始数据行序列
|
||||||
|
:param is_sort: 是否启用结果排序
|
||||||
|
:param sort_key: 基于此键对结果进行进行排序
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
tree_nodes = select_list_serialize(row)
|
tree_nodes = select_list_serialize(row)
|
||||||
tree_nodes.sort(key=lambda x: x['sort'])
|
if is_sort:
|
||||||
|
tree_nodes.sort(key=lambda x: x[sort_key])
|
||||||
return tree_nodes
|
return tree_nodes
|
||||||
|
|
||||||
|
|
||||||
@@ -65,7 +68,12 @@ def recursive_to_tree(nodes: list[dict[str, Any]], *, parent_id: int | None = No
|
|||||||
|
|
||||||
|
|
||||||
def get_tree_data(
|
def get_tree_data(
|
||||||
row: Sequence[RowData], build_type: BuildTreeType = BuildTreeType.traversal, *, parent_id: int | None = None
|
row: Sequence[RowData],
|
||||||
|
build_type: BuildTreeType = BuildTreeType.traversal,
|
||||||
|
*,
|
||||||
|
parent_id: int | None = None,
|
||||||
|
is_sort: bool = True,
|
||||||
|
sort_key: str = 'sort',
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
获取树形结构数据
|
获取树形结构数据
|
||||||
@@ -73,9 +81,11 @@ def get_tree_data(
|
|||||||
:param row: 原始数据行序列
|
:param row: 原始数据行序列
|
||||||
:param build_type: 构建树形结构的算法类型,默认为遍历算法
|
:param build_type: 构建树形结构的算法类型,默认为遍历算法
|
||||||
:param parent_id: 父节点 ID,仅在递归算法中使用
|
:param parent_id: 父节点 ID,仅在递归算法中使用
|
||||||
|
:param is_sort: 是否启用结果排序
|
||||||
|
:param sort_key: 基于此键对结果进行进行排序
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
nodes = get_tree_nodes(row)
|
nodes = get_tree_nodes(row, is_sort, sort_key)
|
||||||
match build_type:
|
match build_type:
|
||||||
case BuildTreeType.traversal:
|
case BuildTreeType.traversal:
|
||||||
tree = traversal_to_tree(nodes)
|
tree = traversal_to_tree(nodes)
|
||||||
@@ -86,11 +96,13 @@ def get_tree_data(
|
|||||||
return tree
|
return tree
|
||||||
|
|
||||||
|
|
||||||
def get_vben5_tree_data(row: Sequence[RowData]) -> list[dict[str, Any]]:
|
def get_vben5_tree_data(row: Sequence[RowData], is_sort: bool = True, sort_key: str = 'sort') -> list[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
获取 vben5 菜单树形结构数据
|
获取 vben5 菜单树形结构数据
|
||||||
|
|
||||||
:param row: 原始数据行序列
|
:param row: 原始数据行序列
|
||||||
|
:param is_sort: 是否启用结果排序
|
||||||
|
:param sort_key: 基于此键对结果进行进行排序
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
meta_keys = {'title', 'icon', 'link', 'cache', 'display', 'status'}
|
meta_keys = {'title', 'icon', 'link', 'cache', 'display', 'status'}
|
||||||
@@ -108,7 +120,7 @@ def get_vben5_tree_data(row: Sequence[RowData]) -> list[dict[str, Any]]:
|
|||||||
'menuVisibleWithForbidden': not bool(node['status']),
|
'menuVisibleWithForbidden': not bool(node['status']),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for node in get_tree_nodes(row)
|
for node in get_tree_nodes(row, is_sort, sort_key)
|
||||||
]
|
]
|
||||||
|
|
||||||
return traversal_to_tree(vben5_nodes)
|
return traversal_to_tree(vben5_nodes)
|
||||||
|
|||||||
Reference in New Issue
Block a user