mirror of
https://github.com/fastapi-practices/fastapi-best-architecture.git
synced 2026-09-21 05:02:49 +00:00
Update the ruff rules and format the code (#846)
* Update the ruff rules and format the code * Update the per-file-ignores * Update the ci * Update rules * Fix codes * Fix pagination * Update rules
This commit is contained in:
@@ -12,7 +12,7 @@ jobs:
|
|||||||
name: lint ${{ matrix.python-version }}
|
name: lint ${{ matrix.python-version }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: [ '3.10', '3.11', '3.12', '3.13' ]
|
python-version: [ '3.10', '3.11', '3.12', '3.13', '3.14' ]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|||||||
+174
@@ -0,0 +1,174 @@
|
|||||||
|
line-length = 120
|
||||||
|
preview = true
|
||||||
|
fix = true
|
||||||
|
unsafe-fixes = true
|
||||||
|
show-fixes = true
|
||||||
|
required-version = ">=0.13.0"
|
||||||
|
|
||||||
|
[lint]
|
||||||
|
select = [
|
||||||
|
"FAST",
|
||||||
|
"ANN001",
|
||||||
|
"ANN201",
|
||||||
|
"ANN202",
|
||||||
|
"ANN204",
|
||||||
|
"ANN205",
|
||||||
|
"ANN206",
|
||||||
|
"ASYNC110",
|
||||||
|
"ASYNC116",
|
||||||
|
"ASYNC210",
|
||||||
|
"ASYNC212",
|
||||||
|
"ASYNC230",
|
||||||
|
"ASYNC240",
|
||||||
|
"ASYNC250",
|
||||||
|
"ASYNC251",
|
||||||
|
"S310",
|
||||||
|
"FBT001",
|
||||||
|
"FBT002",
|
||||||
|
"B002",
|
||||||
|
"B005",
|
||||||
|
"B006",
|
||||||
|
"B007",
|
||||||
|
"B008",
|
||||||
|
"B009",
|
||||||
|
"B010",
|
||||||
|
"B013",
|
||||||
|
"B014",
|
||||||
|
"B019",
|
||||||
|
"B020",
|
||||||
|
"B021",
|
||||||
|
"B024",
|
||||||
|
"B025",
|
||||||
|
"B026",
|
||||||
|
"B027",
|
||||||
|
"B039",
|
||||||
|
"COM",
|
||||||
|
"C402",
|
||||||
|
"C403",
|
||||||
|
"C404",
|
||||||
|
"C408",
|
||||||
|
"C410",
|
||||||
|
"C411",
|
||||||
|
"C414",
|
||||||
|
"C416",
|
||||||
|
"C417",
|
||||||
|
"C418",
|
||||||
|
"C419",
|
||||||
|
"C420",
|
||||||
|
"DTZ",
|
||||||
|
"EXE",
|
||||||
|
"ISC001",
|
||||||
|
"ISC002",
|
||||||
|
"ISC003",
|
||||||
|
"PIE",
|
||||||
|
"PYI009",
|
||||||
|
"PYI010",
|
||||||
|
"PYI011",
|
||||||
|
"PYI012",
|
||||||
|
"PYI013",
|
||||||
|
"PYI016",
|
||||||
|
"PYI017",
|
||||||
|
"PYI019",
|
||||||
|
"PYI020",
|
||||||
|
"PYI021",
|
||||||
|
"PYI024",
|
||||||
|
"PYI026",
|
||||||
|
"PYI030",
|
||||||
|
"PYI033",
|
||||||
|
"PYI034",
|
||||||
|
"PYI036",
|
||||||
|
"PYI041",
|
||||||
|
"PYI042",
|
||||||
|
"PYI055",
|
||||||
|
"PYI061",
|
||||||
|
"PYI062",
|
||||||
|
"PYI063",
|
||||||
|
"Q001",
|
||||||
|
"Q002",
|
||||||
|
"RSE102",
|
||||||
|
"RET501",
|
||||||
|
"RET505",
|
||||||
|
"RET506",
|
||||||
|
"RET507",
|
||||||
|
"RET508",
|
||||||
|
"SIM101",
|
||||||
|
"SIM102",
|
||||||
|
"SIM103",
|
||||||
|
"SIM107",
|
||||||
|
"SIM108",
|
||||||
|
"SIM109",
|
||||||
|
"SIM110",
|
||||||
|
"SIM114",
|
||||||
|
"SIM115",
|
||||||
|
"SIM201",
|
||||||
|
"SIM202",
|
||||||
|
"SIM210",
|
||||||
|
"SIM211",
|
||||||
|
"SIM212",
|
||||||
|
"SIM300",
|
||||||
|
"SIM401",
|
||||||
|
"SIM910",
|
||||||
|
"TID252",
|
||||||
|
"TC",
|
||||||
|
"FLY",
|
||||||
|
"I",
|
||||||
|
"C901",
|
||||||
|
"N",
|
||||||
|
"PERF",
|
||||||
|
"E",
|
||||||
|
"W",
|
||||||
|
"D404",
|
||||||
|
"D417",
|
||||||
|
"D419",
|
||||||
|
"F",
|
||||||
|
"PGH",
|
||||||
|
"PLC1901",
|
||||||
|
"UP",
|
||||||
|
"FURB",
|
||||||
|
"RUF",
|
||||||
|
"TRY",
|
||||||
|
]
|
||||||
|
ignore = [
|
||||||
|
"COM812",
|
||||||
|
"PGH003",
|
||||||
|
"RUF001",
|
||||||
|
"RUF002",
|
||||||
|
"RUF003",
|
||||||
|
"RUF006",
|
||||||
|
"RUF012",
|
||||||
|
"TRY400",
|
||||||
|
"TRY003",
|
||||||
|
"TRY301"
|
||||||
|
]
|
||||||
|
|
||||||
|
[lint.per-file-ignores]
|
||||||
|
"**/model/*.py" = ["TC003"]
|
||||||
|
"backend/common/socketio/server.py" = ["ANN001"]
|
||||||
|
"backend/common/exception/exception_handler.py" = ["ANN202","RUF029"]
|
||||||
|
|
||||||
|
[lint.flake8-pytest-style]
|
||||||
|
parametrize-names-type = "list"
|
||||||
|
parametrize-values-row-type = "list"
|
||||||
|
parametrize-values-type = "list"
|
||||||
|
|
||||||
|
[lint.flake8-quotes]
|
||||||
|
inline-quotes = "single"
|
||||||
|
|
||||||
|
[lint.flake8-type-checking]
|
||||||
|
runtime-evaluated-base-classes = ["pydantic.BaseModel", "sqlalchemy.orm.DeclarativeBase"]
|
||||||
|
|
||||||
|
[lint.flake8-unused-arguments]
|
||||||
|
ignore-variadic-names = true
|
||||||
|
|
||||||
|
[lint.isort]
|
||||||
|
case-sensitive = true
|
||||||
|
lines-between-types = 1
|
||||||
|
order-by-type = true
|
||||||
|
|
||||||
|
[lint.pylint]
|
||||||
|
allow-dunder-method-names = ["__tablename__", "__table_args__"]
|
||||||
|
|
||||||
|
[format]
|
||||||
|
docstring-code-format = true
|
||||||
|
preview = true
|
||||||
|
quote-style = "single"
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
line-length = 120
|
|
||||||
cache-dir = ".ruff_cache"
|
|
||||||
target-version = "py310"
|
|
||||||
unsafe-fixes = true
|
|
||||||
show-fixes = true
|
|
||||||
|
|
||||||
[lint]
|
|
||||||
select = [
|
|
||||||
"E",
|
|
||||||
"F",
|
|
||||||
"I",
|
|
||||||
"TC",
|
|
||||||
# W
|
|
||||||
"W505",
|
|
||||||
# PT
|
|
||||||
"PT018",
|
|
||||||
# SIM
|
|
||||||
"SIM101",
|
|
||||||
"SIM114",
|
|
||||||
# PGH
|
|
||||||
"PGH004",
|
|
||||||
# PL
|
|
||||||
"PLE1142",
|
|
||||||
# RUF
|
|
||||||
"RUF100",
|
|
||||||
# UP
|
|
||||||
"UP007"
|
|
||||||
]
|
|
||||||
preview = true
|
|
||||||
ignore = ["FURB101"]
|
|
||||||
|
|
||||||
[lint.flake8-pytest-style]
|
|
||||||
mark-parentheses = false
|
|
||||||
parametrize-names-type = "list"
|
|
||||||
parametrize-values-row-type = "list"
|
|
||||||
parametrize-values-type = "tuple"
|
|
||||||
|
|
||||||
[lint.flake8-unused-arguments]
|
|
||||||
ignore-variadic-names = true
|
|
||||||
|
|
||||||
[lint.isort]
|
|
||||||
lines-between-types = 1
|
|
||||||
order-by-type = true
|
|
||||||
|
|
||||||
[lint.per-file-ignores]
|
|
||||||
"**/api/v1/*.py" = ["TC"]
|
|
||||||
"**/model/*.py" = ["TC003"]
|
|
||||||
"**/model/__init__.py" = ["F401"]
|
|
||||||
"**/tests/*.py" = ["E402"]
|
|
||||||
|
|
||||||
[format]
|
|
||||||
preview = true
|
|
||||||
quote-style = "single"
|
|
||||||
docstring-code-format = true
|
|
||||||
skip-magic-trailing-comma = false
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from backend.common.i18n import i18n
|
from backend.common.i18n import i18n
|
||||||
|
|
||||||
__version__ = '1.8.2'
|
__version__ = '1.8.2'
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# ruff: noqa: F403, F401, I001, RUF100
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from logging.config import fileConfig
|
from logging.config import fileConfig
|
||||||
|
|
||||||
from alembic import context
|
from alembic import context
|
||||||
@@ -40,11 +38,12 @@ target_metadata = MappedBase.metadata
|
|||||||
|
|
||||||
# other values from the config, defined by the needs of env.py,
|
# other values from the config, defined by the needs of env.py,
|
||||||
alembic_config.set_main_option(
|
alembic_config.set_main_option(
|
||||||
'sqlalchemy.url', SQLALCHEMY_DATABASE_URL.render_as_string(hide_password=False).replace('%', '%%')
|
'sqlalchemy.url',
|
||||||
|
SQLALCHEMY_DATABASE_URL.render_as_string(hide_password=False).replace('%', '%%'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def run_migrations_offline():
|
def run_migrations_offline() -> None:
|
||||||
"""Run migrations in 'offline' mode.
|
"""Run migrations in 'offline' mode.
|
||||||
|
|
||||||
This configures the context with just a URL
|
This configures the context with just a URL
|
||||||
@@ -73,7 +72,7 @@ def run_migrations_offline():
|
|||||||
|
|
||||||
def do_run_migrations(connection: Connection) -> None:
|
def do_run_migrations(connection: Connection) -> None:
|
||||||
# 当迁移无变化时,不生成迁移记录
|
# 当迁移无变化时,不生成迁移记录
|
||||||
def process_revision_directives(context, revision, directives):
|
def process_revision_directives(context, revision, directives) -> None: # noqa: ANN001
|
||||||
if alembic_config.cmd_opts.autogenerate:
|
if alembic_config.cmd_opts.autogenerate:
|
||||||
script = directives[0]
|
script = directives[0]
|
||||||
if script.upgrade_ops.is_empty():
|
if script.upgrade_ops.is_empty():
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
from backend.core.path_conf import BASE_PATH
|
from backend.core.path_conf import BASE_PATH
|
||||||
@@ -8,14 +6,10 @@ from backend.utils.import_parse import get_model_objects
|
|||||||
|
|
||||||
def get_app_models() -> list[type]:
|
def get_app_models() -> list[type]:
|
||||||
"""获取 app 所有模型类"""
|
"""获取 app 所有模型类"""
|
||||||
app_path = os.path.join(BASE_PATH, 'app')
|
app_path = BASE_PATH / 'app'
|
||||||
list_dirs = os.listdir(app_path)
|
list_dirs = os.listdir(app_path)
|
||||||
|
|
||||||
apps = []
|
apps = [d for d in list_dirs if os.path.isdir(os.path.join(app_path, d)) and d != '__pycache__']
|
||||||
|
|
||||||
for d in list_dirs:
|
|
||||||
if os.path.isdir(os.path.join(app_path, d)) and d != '__pycache__':
|
|
||||||
apps.append(d)
|
|
||||||
|
|
||||||
objs = []
|
objs = []
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from backend.app.admin.api.v1.auth import router as auth_router
|
from backend.app.admin.api.v1.auth import router as auth_router
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from backend.app.admin.api.v1.auth.auth import router as auth_router
|
from backend.app.admin.api.v1.auth.auth import router as auth_router
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Request, Response
|
from fastapi import APIRouter, Depends, Request, Response
|
||||||
@@ -29,7 +27,10 @@ async def login_swagger(obj: Annotated[HTTPBasicCredentials, Depends()]) -> GetS
|
|||||||
dependencies=[Depends(RateLimiter(times=5, minutes=1))],
|
dependencies=[Depends(RateLimiter(times=5, minutes=1))],
|
||||||
)
|
)
|
||||||
async def login(
|
async def login(
|
||||||
request: Request, response: Response, obj: AuthLoginParam, background_tasks: BackgroundTasks
|
request: Request,
|
||||||
|
response: Response,
|
||||||
|
obj: AuthLoginParam,
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
) -> ResponseSchemaModel[GetLoginToken]:
|
) -> ResponseSchemaModel[GetLoginToken]:
|
||||||
data = await auth_service.login(request=request, response=response, obj=obj, background_tasks=background_tasks)
|
data = await auth_service.login(request=request, response=response, obj=obj, background_tasks=background_tasks)
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from fast_captcha import img_captcha
|
from fast_captcha import img_captcha
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from backend.app.admin.api.v1.log.login_log import router as login_log
|
from backend.app.admin.api.v1.log.login_log import router as login_log
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Query
|
from fastapi import APIRouter, Depends, Query
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Query
|
from fastapi import APIRouter, Depends, Query
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from backend.app.admin.api.v1.monitor.online import router as token_router
|
from backend.app.admin.api.v1.monitor.online import router as token_router
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
@@ -37,8 +35,8 @@ async def get_sessions(
|
|||||||
'browser': extra_info.get('browser', '未知'),
|
'browser': extra_info.get('browser', '未知'),
|
||||||
'device': extra_info.get('device', '未知'),
|
'device': extra_info.get('device', '未知'),
|
||||||
'last_login_time': extra_info.get('last_login_time', '未知'),
|
'last_login_time': extra_info.get('last_login_time', '未知'),
|
||||||
}
|
},
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
for key in token_keys:
|
for key in token_keys:
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from backend.common.response.response_schema import ResponseModel, response_base
|
from backend.common.response.response_schema import ResponseModel, response_base
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from starlette.concurrency import run_in_threadpool
|
from starlette.concurrency import run_in_threadpool
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
# !/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from backend.app.admin.api.v1.sys.data_rule import router as data_rule_router
|
from backend.app.admin.api.v1.sys.data_rule import router as data_rule_router
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Path, Query
|
from fastapi import APIRouter, Depends, Path, Query
|
||||||
@@ -59,7 +57,8 @@ async def get_data_rule(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def get_data_rules_paged(
|
async def get_data_rules_paged(
|
||||||
db: CurrentSession, name: Annotated[str | None, Query(description='规则名称')] = None
|
db: CurrentSession,
|
||||||
|
name: Annotated[str | None, Query(description='规则名称')] = None,
|
||||||
) -> ResponseSchemaModel[PageData[GetDataRuleDetail]]:
|
) -> ResponseSchemaModel[PageData[GetDataRuleDetail]]:
|
||||||
data_rule_select = await data_rule_service.get_select(name=name)
|
data_rule_select = await data_rule_service.get_select(name=name)
|
||||||
page_data = await paging_data(db, data_rule_select)
|
page_data = await paging_data(db, data_rule_select)
|
||||||
@@ -88,7 +87,8 @@ async def create_data_rule(obj: CreateDataRuleParam) -> ResponseModel:
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_data_rule(
|
async def update_data_rule(
|
||||||
pk: Annotated[int, Path(description='数据规则 ID')], obj: UpdateDataRuleParam
|
pk: Annotated[int, Path(description='数据规则 ID')],
|
||||||
|
obj: UpdateDataRuleParam,
|
||||||
) -> ResponseModel:
|
) -> ResponseModel:
|
||||||
count = await data_rule_service.update(pk=pk, obj=obj)
|
count = await data_rule_service.update(pk=pk, obj=obj)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Path, Query
|
from fastapi import APIRouter, Depends, Path, Query
|
||||||
@@ -85,7 +83,8 @@ async def create_data_scope(obj: CreateDataScopeParam) -> ResponseModel:
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_data_scope(
|
async def update_data_scope(
|
||||||
pk: Annotated[int, Path(description='数据范围 ID')], obj: UpdateDataScopeParam
|
pk: Annotated[int, Path(description='数据范围 ID')],
|
||||||
|
obj: UpdateDataScopeParam,
|
||||||
) -> ResponseModel:
|
) -> ResponseModel:
|
||||||
count = await data_scope_service.update(pk=pk, obj=obj)
|
count = await data_scope_service.update(pk=pk, obj=obj)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
@@ -102,8 +101,9 @@ async def update_data_scope(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_data_scope_rules(
|
async def update_data_scope_rules(
|
||||||
pk: Annotated[int, Path(description='数据范围 ID')], rule_ids: UpdateDataScopeRuleParam
|
pk: Annotated[int, Path(description='数据范围 ID')],
|
||||||
):
|
rule_ids: UpdateDataScopeRuleParam,
|
||||||
|
) -> ResponseModel:
|
||||||
count = await data_scope_service.update_data_scope_rule(pk=pk, rule_ids=rule_ids)
|
count = await data_scope_service.update_data_scope_rule(pk=pk, rule_ids=rule_ids)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Path, Query, Request
|
from fastapi import APIRouter, Depends, Path, Query, Request
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, File, UploadFile
|
from fastapi import APIRouter, Depends, File, UploadFile
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Annotated, Any
|
from typing import Annotated, Any
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Path, Query, Request
|
from fastapi import APIRouter, Depends, Path, Query, Request
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Annotated, Any
|
from typing import Annotated, Any
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, File, Path, UploadFile
|
from fastapi import APIRouter, Depends, File, Path, UploadFile
|
||||||
@@ -46,8 +44,9 @@ async def install_plugin(
|
|||||||
plugin_name = await plugin_service.install(type=type, file=file, repo_url=repo_url)
|
plugin_name = await plugin_service.install(type=type, file=file, repo_url=repo_url)
|
||||||
return response_base.success(
|
return response_base.success(
|
||||||
res=CustomResponse(
|
res=CustomResponse(
|
||||||
code=200, msg=f'插件 {plugin_name} 安装成功,请根据插件说明(README.md)进行相关配置并重启服务'
|
code=200,
|
||||||
)
|
msg=f'插件 {plugin_name} 安装成功,请根据插件说明(README.md)进行相关配置并重启服务',
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -63,7 +62,7 @@ async def install_plugin(
|
|||||||
async def uninstall_plugin(plugin: Annotated[str, Path(description='插件名称')]) -> ResponseModel:
|
async def uninstall_plugin(plugin: Annotated[str, Path(description='插件名称')]) -> ResponseModel:
|
||||||
await plugin_service.uninstall(plugin=plugin)
|
await plugin_service.uninstall(plugin=plugin)
|
||||||
return response_base.success(
|
return response_base.success(
|
||||||
res=CustomResponse(code=200, msg=f'插件 {plugin} 卸载成功,请根据插件说明(README.md)移除相关配置并重启服务')
|
res=CustomResponse(code=200, msg=f'插件 {plugin} 卸载成功,请根据插件说明(README.md)移除相关配置并重启服务'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Path, Query
|
from fastapi import APIRouter, Depends, Path, Query
|
||||||
@@ -106,7 +104,8 @@ async def update_role(pk: Annotated[int, Path(description='角色 ID')], obj: Up
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_role_menus(
|
async def update_role_menus(
|
||||||
pk: Annotated[int, Path(description='角色 ID')], menu_ids: UpdateRoleMenuParam
|
pk: Annotated[int, Path(description='角色 ID')],
|
||||||
|
menu_ids: UpdateRoleMenuParam,
|
||||||
) -> ResponseModel:
|
) -> ResponseModel:
|
||||||
count = await role_service.update_role_menu(pk=pk, menu_ids=menu_ids)
|
count = await role_service.update_role_menu(pk=pk, menu_ids=menu_ids)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
@@ -123,7 +122,8 @@ async def update_role_menus(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_role_scopes(
|
async def update_role_scopes(
|
||||||
pk: Annotated[int, Path(description='角色 ID')], scope_ids: UpdateRoleScopeParam
|
pk: Annotated[int, Path(description='角色 ID')],
|
||||||
|
scope_ids: UpdateRoleScopeParam,
|
||||||
) -> ResponseModel:
|
) -> ResponseModel:
|
||||||
count = await role_service.update_role_scope(pk=pk, scope_ids=scope_ids)
|
count = await role_service.update_role_scope(pk=pk, scope_ids=scope_ids)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Body, Depends, Path, Query, Request
|
from fastapi import APIRouter, Body, Depends, Path, Query, Request
|
||||||
@@ -73,7 +71,9 @@ async def create_user(request: Request, obj: AddUserParam) -> ResponseSchemaMode
|
|||||||
|
|
||||||
@router.put('/{pk}', summary='更新用户信息', dependencies=[DependsRBAC])
|
@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:
|
||||||
count = await user_service.update(request=request, pk=pk, obj=obj)
|
count = await user_service.update(request=request, pk=pk, obj=obj)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
@@ -115,7 +115,8 @@ async def reset_user_password(
|
|||||||
|
|
||||||
@router.put('/me/nickname', summary='更新当前用户昵称', dependencies=[DependsJwtAuth])
|
@router.put('/me/nickname', summary='更新当前用户昵称', dependencies=[DependsJwtAuth])
|
||||||
async def update_user_nickname(
|
async def update_user_nickname(
|
||||||
request: Request, nickname: Annotated[str, Body(embed=True, description='用户昵称')]
|
request: Request,
|
||||||
|
nickname: Annotated[str, Body(embed=True, description='用户昵称')],
|
||||||
) -> ResponseModel:
|
) -> ResponseModel:
|
||||||
count = await user_service.update_nickname(request=request, nickname=nickname)
|
count = await user_service.update_nickname(request=request, nickname=nickname)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
@@ -125,7 +126,8 @@ async def update_user_nickname(
|
|||||||
|
|
||||||
@router.put('/me/avatar', summary='更新当前用户头像', dependencies=[DependsJwtAuth])
|
@router.put('/me/avatar', summary='更新当前用户头像', dependencies=[DependsJwtAuth])
|
||||||
async def update_user_avatar(
|
async def update_user_avatar(
|
||||||
request: Request, avatar: Annotated[str, Body(embed=True, description='用户头像地址')]
|
request: Request,
|
||||||
|
avatar: Annotated[str, Body(embed=True, description='用户头像地址')],
|
||||||
) -> ResponseModel:
|
) -> ResponseModel:
|
||||||
count = await user_service.update_avatar(request=request, avatar=avatar)
|
count = await user_service.update_avatar(request=request, avatar=avatar)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
from collections.abc import Sequence
|
||||||
# -*- 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
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
from collections.abc import Sequence
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Sequence
|
|
||||||
|
|
||||||
from sqlalchemy import Select, select
|
from sqlalchemy import Select, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
from collections.abc import Sequence
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Sequence
|
|
||||||
|
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from sqlalchemy import Select
|
from sqlalchemy import Select
|
||||||
from sqlalchemy import delete as sa_delete
|
from sqlalchemy import delete as sa_delete
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
from collections.abc import Sequence
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Sequence
|
|
||||||
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy_crud_plus import CRUDPlus
|
from sqlalchemy_crud_plus import CRUDPlus
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from sqlalchemy import Select
|
from sqlalchemy import Select
|
||||||
from sqlalchemy import delete as sa_delete
|
from sqlalchemy import delete as sa_delete
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
from collections.abc import Sequence
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Sequence
|
|
||||||
|
|
||||||
from sqlalchemy import Select, select
|
from sqlalchemy import Select, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import bcrypt
|
import bcrypt
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
@@ -214,7 +212,7 @@ class CRUDUser(CRUDPlus[User]):
|
|||||||
**filters,
|
**filters,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def set_super(self, db: AsyncSession, user_id: int, is_super: bool) -> int:
|
async def set_super(self, db: AsyncSession, user_id: int, *, is_super: bool) -> int:
|
||||||
"""
|
"""
|
||||||
设置用户超级管理员状态
|
设置用户超级管理员状态
|
||||||
|
|
||||||
@@ -225,7 +223,7 @@ class CRUDUser(CRUDPlus[User]):
|
|||||||
"""
|
"""
|
||||||
return await self.update_model(db, user_id, {'is_superuser': is_super})
|
return await self.update_model(db, user_id, {'is_superuser': is_super})
|
||||||
|
|
||||||
async def set_staff(self, db: AsyncSession, user_id: int, is_staff: bool) -> int:
|
async def set_staff(self, db: AsyncSession, user_id: int, *, is_staff: bool) -> int:
|
||||||
"""
|
"""
|
||||||
设置用户后台登录状态
|
设置用户后台登录状态
|
||||||
|
|
||||||
@@ -247,7 +245,7 @@ class CRUDUser(CRUDPlus[User]):
|
|||||||
"""
|
"""
|
||||||
return await self.update_model(db, user_id, {'status': status})
|
return await self.update_model(db, user_id, {'status': status})
|
||||||
|
|
||||||
async def set_multi_login(self, db: AsyncSession, user_id: int, multi_login: bool) -> int:
|
async def set_multi_login(self, db: AsyncSession, user_id: int, *, multi_login: bool) -> int:
|
||||||
"""
|
"""
|
||||||
设置用户多端登录状态
|
设置用户多端登录状态
|
||||||
|
|
||||||
@@ -259,7 +257,11 @@ class CRUDUser(CRUDPlus[User]):
|
|||||||
return await self.update_model(db, user_id, {'is_multi_login': multi_login})
|
return await self.update_model(db, user_id, {'is_multi_login': multi_login})
|
||||||
|
|
||||||
async def get_with_relation(
|
async def get_with_relation(
|
||||||
self, db: AsyncSession, *, user_id: int | None = None, username: str | None = None
|
self,
|
||||||
|
db: AsyncSession,
|
||||||
|
*,
|
||||||
|
user_id: int | None = None,
|
||||||
|
username: str | None = None,
|
||||||
) -> User | None:
|
) -> User | None:
|
||||||
"""
|
"""
|
||||||
获取用户关联信息
|
获取用户关联信息
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
from backend.app.admin.model.data_rule import DataRule as DataRule
|
||||||
# -*- coding: utf-8 -*-
|
from backend.app.admin.model.data_scope import DataScope as DataScope
|
||||||
from backend.app.admin.model.data_rule import DataRule
|
from backend.app.admin.model.dept import Dept as Dept
|
||||||
from backend.app.admin.model.data_scope import DataScope
|
from backend.app.admin.model.login_log import LoginLog as LoginLog
|
||||||
from backend.app.admin.model.dept import Dept
|
from backend.app.admin.model.menu import Menu as Menu
|
||||||
from backend.app.admin.model.login_log import LoginLog
|
from backend.app.admin.model.opera_log import OperaLog as OperaLog
|
||||||
from backend.app.admin.model.menu import Menu
|
from backend.app.admin.model.role import Role as Role
|
||||||
from backend.app.admin.model.opera_log import OperaLog
|
from backend.app.admin.model.user import User as User
|
||||||
from backend.app.admin.model.role import Role
|
|
||||||
from backend.app.admin.model.user import User
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
@@ -25,7 +23,7 @@ class DataRule(Base):
|
|||||||
column: Mapped[str] = mapped_column(String(20), comment='模型字段名')
|
column: Mapped[str] = mapped_column(String(20), comment='模型字段名')
|
||||||
operator: Mapped[int] = mapped_column(comment='运算符(0:and、1:or)')
|
operator: Mapped[int] = mapped_column(comment='运算符(0:and、1:or)')
|
||||||
expression: Mapped[int] = mapped_column(
|
expression: Mapped[int] = mapped_column(
|
||||||
comment='表达式(0:==、1:!=、2:>、3:>=、4:<、5:<=、6:in、7:not_in)'
|
comment='表达式(0:==、1:!=、2:>、3:>=、4:<、5:<=、6:in、7:not_in)',
|
||||||
)
|
)
|
||||||
value: Mapped[str] = mapped_column(String(255), comment='规则值')
|
value: Mapped[str] = mapped_column(String(255), comment='规则值')
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import BigInteger, Boolean, ForeignKey, String
|
from sqlalchemy import BigInteger, Boolean, ForeignKey, String
|
||||||
from sqlalchemy.dialects.postgresql import INTEGER
|
from sqlalchemy.dialects.postgresql import INTEGER
|
||||||
@@ -27,15 +25,21 @@ class Dept(Base):
|
|||||||
email: Mapped[str | None] = mapped_column(String(50), default=None, comment='邮箱')
|
email: Mapped[str | None] = mapped_column(String(50), default=None, comment='邮箱')
|
||||||
status: Mapped[int] = mapped_column(default=1, comment='部门状态(0停用 1正常)')
|
status: Mapped[int] = mapped_column(default=1, comment='部门状态(0停用 1正常)')
|
||||||
del_flag: Mapped[bool] = mapped_column(
|
del_flag: Mapped[bool] = mapped_column(
|
||||||
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='删除标志(0删除 1存在)'
|
Boolean().with_variant(INTEGER, 'postgresql'),
|
||||||
|
default=False,
|
||||||
|
comment='删除标志(0删除 1存在)',
|
||||||
)
|
)
|
||||||
|
|
||||||
# 父级部门一对多
|
# 父级部门一对多
|
||||||
parent_id: Mapped[int | None] = mapped_column(
|
parent_id: Mapped[int | None] = mapped_column(
|
||||||
BigInteger, ForeignKey('sys_dept.id', ondelete='SET NULL'), default=None, index=True, comment='父部门ID'
|
BigInteger,
|
||||||
|
ForeignKey('sys_dept.id', ondelete='SET NULL'),
|
||||||
|
default=None,
|
||||||
|
index=True,
|
||||||
|
comment='父部门ID',
|
||||||
)
|
)
|
||||||
parent: Mapped[Optional['Dept']] = relationship(init=False, back_populates='children', remote_side=[id])
|
parent: Mapped[Dept | None] = relationship(init=False, back_populates='children', remote_side=[id])
|
||||||
children: Mapped[Optional[list['Dept']]] = relationship(init=False, back_populates='parent')
|
children: Mapped[list[Dept] | None] = relationship(init=False, back_populates='parent')
|
||||||
|
|
||||||
# 部门用户一对多
|
# 部门用户一对多
|
||||||
users: Mapped[list[User]] = relationship(init=False, back_populates='dept')
|
users: Mapped[list[User]] = relationship(init=False, back_populates='dept')
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from sqlalchemy import String
|
from sqlalchemy import String
|
||||||
@@ -31,5 +29,8 @@ class LoginLog(DataClassBase):
|
|||||||
msg: Mapped[str] = mapped_column(LONGTEXT().with_variant(TEXT, 'postgresql'), comment='提示消息')
|
msg: Mapped[str] = mapped_column(LONGTEXT().with_variant(TEXT, 'postgresql'), comment='提示消息')
|
||||||
login_time: Mapped[datetime] = mapped_column(TimeZone, comment='登录时间')
|
login_time: Mapped[datetime] = mapped_column(TimeZone, comment='登录时间')
|
||||||
created_time: Mapped[datetime] = mapped_column(
|
created_time: Mapped[datetime] = mapped_column(
|
||||||
TimeZone, init=False, default_factory=timezone.now, comment='创建时间'
|
TimeZone,
|
||||||
|
init=False,
|
||||||
|
default_factory=timezone.now,
|
||||||
|
comment='创建时间',
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from sqlalchemy import BigInteger, Column, ForeignKey, Table
|
from sqlalchemy import BigInteger, Column, ForeignKey, Table
|
||||||
|
|
||||||
from backend.common.model import MappedBase
|
from backend.common.model import MappedBase
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import BigInteger, ForeignKey, String
|
from sqlalchemy import BigInteger, ForeignKey, String
|
||||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||||
@@ -34,18 +32,26 @@ class Menu(Base):
|
|||||||
display: Mapped[int] = mapped_column(default=1, comment='是否显示(0否 1是)')
|
display: Mapped[int] = mapped_column(default=1, comment='是否显示(0否 1是)')
|
||||||
cache: Mapped[int] = mapped_column(default=1, comment='是否缓存(0否 1是)')
|
cache: Mapped[int] = mapped_column(default=1, comment='是否缓存(0否 1是)')
|
||||||
link: Mapped[str | None] = mapped_column(
|
link: Mapped[str | None] = mapped_column(
|
||||||
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='外链地址'
|
LONGTEXT().with_variant(TEXT, 'postgresql'),
|
||||||
|
default=None,
|
||||||
|
comment='外链地址',
|
||||||
)
|
)
|
||||||
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='备注',
|
||||||
)
|
)
|
||||||
|
|
||||||
# 父级菜单一对多
|
# 父级菜单一对多
|
||||||
parent_id: Mapped[int | None] = mapped_column(
|
parent_id: Mapped[int | None] = mapped_column(
|
||||||
BigInteger, ForeignKey('sys_menu.id', ondelete='SET NULL'), default=None, index=True, comment='父菜单ID'
|
BigInteger,
|
||||||
|
ForeignKey('sys_menu.id', ondelete='SET NULL'),
|
||||||
|
default=None,
|
||||||
|
index=True,
|
||||||
|
comment='父菜单ID',
|
||||||
)
|
)
|
||||||
parent: Mapped[Optional['Menu']] = relationship(init=False, back_populates='children', remote_side=[id])
|
parent: Mapped[Menu | None] = relationship(init=False, back_populates='children', remote_side=[id])
|
||||||
children: Mapped[Optional[list['Menu']]] = relationship(init=False, back_populates='parent')
|
children: Mapped[list[Menu] | None] = relationship(init=False, back_populates='parent')
|
||||||
|
|
||||||
# 菜单角色多对多
|
# 菜单角色多对多
|
||||||
roles: Mapped[list[Role]] = relationship(init=False, secondary=sys_role_menu, back_populates='menus')
|
roles: Mapped[list[Role]] = relationship(init=False, secondary=sys_role_menu, back_populates='menus')
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from sqlalchemy import String
|
from sqlalchemy import String
|
||||||
@@ -37,5 +35,8 @@ class OperaLog(DataClassBase):
|
|||||||
cost_time: Mapped[float] = mapped_column(insert_default=0.0, comment='请求耗时(ms)')
|
cost_time: Mapped[float] = mapped_column(insert_default=0.0, comment='请求耗时(ms)')
|
||||||
opera_time: Mapped[datetime] = mapped_column(TimeZone, comment='操作时间')
|
opera_time: Mapped[datetime] = mapped_column(TimeZone, comment='操作时间')
|
||||||
created_time: Mapped[datetime] = mapped_column(
|
created_time: Mapped[datetime] = mapped_column(
|
||||||
TimeZone, init=False, default_factory=timezone.now, comment='创建时间'
|
TimeZone,
|
||||||
|
init=False,
|
||||||
|
default_factory=timezone.now,
|
||||||
|
comment='创建时间',
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
@@ -25,10 +23,14 @@ class Role(Base):
|
|||||||
name: Mapped[str] = mapped_column(String(20), unique=True, comment='角色名称')
|
name: Mapped[str] = mapped_column(String(20), unique=True, comment='角色名称')
|
||||||
status: Mapped[int] = mapped_column(default=1, comment='角色状态(0停用 1正常)')
|
status: Mapped[int] = mapped_column(default=1, comment='角色状态(0停用 1正常)')
|
||||||
is_filter_scopes: Mapped[bool] = mapped_column(
|
is_filter_scopes: Mapped[bool] = mapped_column(
|
||||||
Boolean().with_variant(INTEGER, 'postgresql'), default=True, comment='过滤数据权限(0否 1是)'
|
Boolean().with_variant(INTEGER, 'postgresql'),
|
||||||
|
default=True,
|
||||||
|
comment='过滤数据权限(0否 1是)',
|
||||||
)
|
)
|
||||||
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='备注',
|
||||||
)
|
)
|
||||||
|
|
||||||
# 角色用户多对多
|
# 角色用户多对多
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -34,22 +32,33 @@ class User(Base):
|
|||||||
avatar: Mapped[str | None] = mapped_column(String(255), default=None, comment='头像')
|
avatar: Mapped[str | None] = mapped_column(String(255), default=None, comment='头像')
|
||||||
status: Mapped[int] = mapped_column(default=1, index=True, comment='用户账号状态(0停用 1正常)')
|
status: Mapped[int] = mapped_column(default=1, index=True, comment='用户账号状态(0停用 1正常)')
|
||||||
is_superuser: Mapped[bool] = mapped_column(
|
is_superuser: Mapped[bool] = mapped_column(
|
||||||
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='超级权限(0否 1是)'
|
Boolean().with_variant(INTEGER, 'postgresql'),
|
||||||
|
default=False,
|
||||||
|
comment='超级权限(0否 1是)',
|
||||||
)
|
)
|
||||||
is_staff: Mapped[bool] = mapped_column(
|
is_staff: Mapped[bool] = mapped_column(
|
||||||
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='后台管理登陆(0否 1是)'
|
Boolean().with_variant(INTEGER, 'postgresql'),
|
||||||
|
default=False,
|
||||||
|
comment='后台管理登陆(0否 1是)',
|
||||||
)
|
)
|
||||||
is_multi_login: Mapped[bool] = mapped_column(
|
is_multi_login: Mapped[bool] = mapped_column(
|
||||||
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='是否重复登陆(0否 1是)'
|
Boolean().with_variant(INTEGER, 'postgresql'),
|
||||||
|
default=False,
|
||||||
|
comment='是否重复登陆(0否 1是)',
|
||||||
)
|
)
|
||||||
join_time: Mapped[datetime] = mapped_column(TimeZone, init=False, default_factory=timezone.now, comment='注册时间')
|
join_time: Mapped[datetime] = mapped_column(TimeZone, init=False, default_factory=timezone.now, comment='注册时间')
|
||||||
last_login_time: Mapped[datetime | None] = mapped_column(
|
last_login_time: Mapped[datetime | None] = mapped_column(
|
||||||
TimeZone, init=False, onupdate=timezone.now, comment='上次登录'
|
TimeZone,
|
||||||
|
init=False,
|
||||||
|
onupdate=timezone.now,
|
||||||
|
comment='上次登录',
|
||||||
)
|
)
|
||||||
|
|
||||||
# 部门用户一对多
|
# 部门用户一对多
|
||||||
dept_id: Mapped[int | None] = mapped_column(
|
dept_id: Mapped[int | None] = mapped_column(
|
||||||
ForeignKey('sys_dept.id', ondelete='SET NULL'), default=None, comment='部门关联ID'
|
ForeignKey('sys_dept.id', ondelete='SET NULL'),
|
||||||
|
default=None,
|
||||||
|
comment='部门关联ID',
|
||||||
)
|
)
|
||||||
dept: Mapped[Dept | None] = relationship(init=False, back_populates='users')
|
dept: Mapped[Dept | None] = relationship(init=False, back_populates='users')
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
|
|
||||||
from backend.common.schema import SchemaBase
|
from backend.common.schema import SchemaBase
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from pydantic import ConfigDict, Field
|
from pydantic import ConfigDict, Field
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from pydantic import ConfigDict, Field
|
from pydantic import ConfigDict, Field
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from pydantic import ConfigDict, Field
|
from pydantic import ConfigDict, Field
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from pydantic import ConfigDict, Field
|
from pydantic import ConfigDict, Field
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from pydantic import ConfigDict, Field
|
from pydantic import ConfigDict, Field
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from pydantic import ConfigDict, Field
|
from pydantic import ConfigDict, Field
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from fastapi import Request, Response
|
from fastapi import Request, Response
|
||||||
from fastapi.security import HTTPBasicCredentials
|
from fastapi.security import HTTPBasicCredentials
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
@@ -49,9 +47,8 @@ class AuthService:
|
|||||||
|
|
||||||
if user.password is None:
|
if user.password is None:
|
||||||
raise errors.AuthorizationError(msg='用户名或密码有误')
|
raise errors.AuthorizationError(msg='用户名或密码有误')
|
||||||
else:
|
if not password_verify(password, user.password):
|
||||||
if not password_verify(password, user.password):
|
raise errors.AuthorizationError(msg='用户名或密码有误')
|
||||||
raise errors.AuthorizationError(msg='用户名或密码有误')
|
|
||||||
|
|
||||||
if not user.status:
|
if not user.status:
|
||||||
raise errors.AuthorizationError(msg='用户已被锁定, 请联系统管理员')
|
raise errors.AuthorizationError(msg='用户已被锁定, 请联系统管理员')
|
||||||
@@ -70,14 +67,19 @@ class AuthService:
|
|||||||
await user_dao.update_login_time(db, obj.username)
|
await user_dao.update_login_time(db, obj.username)
|
||||||
access_token = await create_access_token(
|
access_token = await create_access_token(
|
||||||
user.id,
|
user.id,
|
||||||
user.is_multi_login,
|
multi_login=user.is_multi_login,
|
||||||
# extra info
|
# extra info
|
||||||
swagger=True,
|
swagger=True,
|
||||||
)
|
)
|
||||||
return access_token.access_token, user
|
return access_token.access_token, user
|
||||||
|
|
||||||
async def login(
|
async def login(
|
||||||
self, *, request: Request, response: Response, obj: AuthLoginParam, background_tasks: BackgroundTasks
|
self,
|
||||||
|
*,
|
||||||
|
request: Request,
|
||||||
|
response: Response,
|
||||||
|
obj: AuthLoginParam,
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
) -> GetLoginToken:
|
) -> GetLoginToken:
|
||||||
"""
|
"""
|
||||||
用户登录
|
用户登录
|
||||||
@@ -102,7 +104,7 @@ class AuthService:
|
|||||||
await db.refresh(user)
|
await db.refresh(user)
|
||||||
access_token = await create_access_token(
|
access_token = await create_access_token(
|
||||||
user.id,
|
user.id,
|
||||||
user.is_multi_login,
|
multi_login=user.is_multi_login,
|
||||||
# extra info
|
# extra info
|
||||||
username=user.username,
|
username=user.username,
|
||||||
nickname=user.nickname,
|
nickname=user.nickname,
|
||||||
@@ -112,7 +114,11 @@ class AuthService:
|
|||||||
browser=request.state.browser,
|
browser=request.state.browser,
|
||||||
device=request.state.device,
|
device=request.state.device,
|
||||||
)
|
)
|
||||||
refresh_token = await create_refresh_token(access_token.session_uuid, user.id, user.is_multi_login)
|
refresh_token = await create_refresh_token(
|
||||||
|
access_token.session_uuid,
|
||||||
|
user.id,
|
||||||
|
multi_login=user.is_multi_login,
|
||||||
|
)
|
||||||
response.set_cookie(
|
response.set_cookie(
|
||||||
key=settings.COOKIE_REFRESH_TOKEN_KEY,
|
key=settings.COOKIE_REFRESH_TOKEN_KEY,
|
||||||
value=refresh_token.refresh_token,
|
value=refresh_token.refresh_token,
|
||||||
@@ -128,32 +134,28 @@ class AuthService:
|
|||||||
log.error('登陆错误: 用户密码有误')
|
log.error('登陆错误: 用户密码有误')
|
||||||
task = BackgroundTask(
|
task = BackgroundTask(
|
||||||
login_log_service.create,
|
login_log_service.create,
|
||||||
**dict(
|
db=db,
|
||||||
db=db,
|
request=request,
|
||||||
request=request,
|
user_uuid=user.uuid if user else uuid4_str(),
|
||||||
user_uuid=user.uuid if user else uuid4_str(),
|
username=obj.username,
|
||||||
username=obj.username,
|
login_time=timezone.now(),
|
||||||
login_time=timezone.now(),
|
status=LoginLogStatusType.fail.value,
|
||||||
status=LoginLogStatusType.fail.value,
|
msg=e.msg,
|
||||||
msg=e.msg,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
raise errors.RequestError(code=e.code, 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
|
||||||
else:
|
else:
|
||||||
background_tasks.add_task(
|
background_tasks.add_task(
|
||||||
login_log_service.create,
|
login_log_service.create,
|
||||||
**dict(
|
db=db,
|
||||||
db=db,
|
request=request,
|
||||||
request=request,
|
user_uuid=user.uuid,
|
||||||
user_uuid=user.uuid,
|
username=obj.username,
|
||||||
username=obj.username,
|
login_time=timezone.now(),
|
||||||
login_time=timezone.now(),
|
status=LoginLogStatusType.success.value,
|
||||||
status=LoginLogStatusType.success.value,
|
msg=t('success.login.success'),
|
||||||
msg=t('success.login.success'),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
data = GetLoginToken(
|
data = GetLoginToken(
|
||||||
access_token=access_token.access_token,
|
access_token=access_token.access_token,
|
||||||
@@ -204,16 +206,15 @@ class AuthService:
|
|||||||
user = await user_dao.get(db, token_payload.id)
|
user = await user_dao.get(db, token_payload.id)
|
||||||
if not user:
|
if not user:
|
||||||
raise errors.NotFoundError(msg='用户不存在')
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
elif not user.status:
|
if not user.status:
|
||||||
raise errors.AuthorizationError(msg='用户已被锁定, 请联系统管理员')
|
raise errors.AuthorizationError(msg='用户已被锁定, 请联系统管理员')
|
||||||
if not user.is_multi_login:
|
if not user.is_multi_login and await redis_client.keys(match=f'{settings.TOKEN_REDIS_PREFIX}:{user.id}:*'):
|
||||||
if await redis_client.keys(match=f'{settings.TOKEN_REDIS_PREFIX}:{user.id}:*'):
|
raise errors.ForbiddenError(msg='此用户已在异地登录,请重新登录并及时修改密码')
|
||||||
raise errors.ForbiddenError(msg='此用户已在异地登录,请重新登录并及时修改密码')
|
|
||||||
new_token = await create_new_token(
|
new_token = await create_new_token(
|
||||||
refresh_token,
|
refresh_token,
|
||||||
token_payload.session_uuid,
|
token_payload.session_uuid,
|
||||||
user.id,
|
user.id,
|
||||||
user.is_multi_login,
|
multi_login=user.is_multi_login,
|
||||||
# extra info
|
# extra info
|
||||||
username=user.username,
|
username=user.username,
|
||||||
nickname=user.nickname,
|
nickname=user.nickname,
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
from collections.abc import Sequence
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Sequence
|
|
||||||
|
|
||||||
from sqlalchemy import Select
|
from sqlalchemy import Select
|
||||||
|
|
||||||
@@ -103,9 +101,8 @@ class DataRuleService:
|
|||||||
data_rule = await data_rule_dao.get(db, pk)
|
data_rule = await data_rule_dao.get(db, pk)
|
||||||
if not data_rule:
|
if not data_rule:
|
||||||
raise errors.NotFoundError(msg='数据规则不存在')
|
raise errors.NotFoundError(msg='数据规则不存在')
|
||||||
if data_rule.name != obj.name:
|
if data_rule.name != obj.name and await data_rule_dao.get_by_name(db, obj.name):
|
||||||
if await data_rule_dao.get_by_name(db, obj.name):
|
raise errors.ConflictError(msg='数据规则已存在')
|
||||||
raise errors.ConflictError(msg='数据规则已存在')
|
|
||||||
count = await data_rule_dao.update(db, pk, obj)
|
count = await data_rule_dao.update(db, pk, obj)
|
||||||
return count
|
return count
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
from collections.abc import Sequence
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Sequence
|
|
||||||
|
|
||||||
from sqlalchemy import Select
|
from sqlalchemy import Select
|
||||||
|
|
||||||
@@ -94,9 +92,8 @@ class DataScopeService:
|
|||||||
data_scope = await data_scope_dao.get(db, pk)
|
data_scope = await data_scope_dao.get(db, pk)
|
||||||
if not data_scope:
|
if not data_scope:
|
||||||
raise errors.NotFoundError(msg='数据范围不存在')
|
raise errors.NotFoundError(msg='数据范围不存在')
|
||||||
if data_scope.name != obj.name:
|
if data_scope.name != obj.name and await data_scope_dao.get_by_name(db, obj.name):
|
||||||
if await data_scope_dao.get_by_name(db, obj.name):
|
raise errors.ConflictError(msg='数据范围已存在')
|
||||||
raise errors.ConflictError(msg='数据范围已存在')
|
|
||||||
count = await data_scope_dao.update(db, pk, obj)
|
count = await data_scope_dao.update(db, pk, obj)
|
||||||
for role in await data_scope.awaitable_attrs.roles:
|
for role in await data_scope.awaitable_attrs.roles:
|
||||||
for user in await role.awaitable_attrs.users:
|
for user in await role.awaitable_attrs.users:
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
@@ -33,7 +31,12 @@ class DeptService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_tree(
|
async def get_tree(
|
||||||
*, request: Request, name: str | None, leader: str | None, phone: str | None, status: int | None
|
*,
|
||||||
|
request: Request,
|
||||||
|
name: str | None,
|
||||||
|
leader: str | None,
|
||||||
|
phone: str | None,
|
||||||
|
status: int | None,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
获取部门树形结构
|
获取部门树形结构
|
||||||
@@ -81,9 +84,8 @@ class DeptService:
|
|||||||
dept = await dept_dao.get(db, pk)
|
dept = await dept_dao.get(db, pk)
|
||||||
if not dept:
|
if not dept:
|
||||||
raise errors.NotFoundError(msg='部门不存在')
|
raise errors.NotFoundError(msg='部门不存在')
|
||||||
if dept.name != obj.name:
|
if dept.name != obj.name and await dept_dao.get_by_name(db, obj.name):
|
||||||
if await dept_dao.get_by_name(db, obj.name):
|
raise errors.ConflictError(msg='部门名称已存在')
|
||||||
raise errors.ConflictError(msg='部门名称已存在')
|
|
||||||
if obj.parent_id:
|
if obj.parent_id:
|
||||||
parent_dept = await dept_dao.get(db, obj.parent_id)
|
parent_dept = await dept_dao.get(db, obj.parent_id)
|
||||||
if not parent_dept:
|
if not parent_dept:
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
@@ -61,8 +59,7 @@ class MenuService:
|
|||||||
menu_ids = set()
|
menu_ids = set()
|
||||||
if roles:
|
if roles:
|
||||||
for role in roles:
|
for role in roles:
|
||||||
for menu in role.menus:
|
menu_ids.update(menu.id for menu in role.menus)
|
||||||
menu_ids.add(menu.id)
|
|
||||||
menu_data = await menu_dao.get_sidebar(db, list(menu_ids))
|
menu_data = await menu_dao.get_sidebar(db, list(menu_ids))
|
||||||
menu_tree = get_vben5_tree_data(menu_data)
|
menu_tree = get_vben5_tree_data(menu_data)
|
||||||
return menu_tree
|
return menu_tree
|
||||||
@@ -98,9 +95,8 @@ class MenuService:
|
|||||||
menu = await menu_dao.get(db, pk)
|
menu = await menu_dao.get(db, pk)
|
||||||
if not menu:
|
if not menu:
|
||||||
raise errors.NotFoundError(msg='菜单不存在')
|
raise errors.NotFoundError(msg='菜单不存在')
|
||||||
if menu.title != obj.title:
|
if menu.title != obj.title and await menu_dao.get_by_title(db, obj.title):
|
||||||
if await menu_dao.get_by_title(db, obj.title):
|
raise errors.ConflictError(msg='菜单标题已存在')
|
||||||
raise errors.ConflictError(msg='菜单标题已存在')
|
|
||||||
if obj.parent_id:
|
if obj.parent_id:
|
||||||
parent_menu = await menu_dao.get(db, obj.parent_id)
|
parent_menu = await menu_dao.get(db, obj.parent_id)
|
||||||
if not parent_menu:
|
if not parent_menu:
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from sqlalchemy import Select
|
from sqlalchemy import Select
|
||||||
|
|
||||||
from backend.app.admin.crud.crud_opera_log import opera_log_dao
|
from backend.app.admin.crud.crud_opera_log import opera_log_dao
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -8,6 +6,8 @@ import zipfile
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
import anyio
|
||||||
|
|
||||||
from fastapi import UploadFile
|
from fastapi import UploadFile
|
||||||
|
|
||||||
from backend.common.enums import PluginType, StatusType
|
from backend.common.enums import PluginType, StatusType
|
||||||
@@ -26,14 +26,10 @@ class PluginService:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_all() -> list[dict[str, Any]]:
|
async def get_all() -> list[dict[str, Any]]:
|
||||||
"""获取所有插件"""
|
"""获取所有插件"""
|
||||||
keys = []
|
|
||||||
result = []
|
|
||||||
|
|
||||||
async for key in redis_client.scan_iter(f'{settings.PLUGIN_REDIS_PREFIX}:*'):
|
keys = [key async for key in redis_client.scan_iter(f'{settings.PLUGIN_REDIS_PREFIX}:*')]
|
||||||
keys.append(key)
|
|
||||||
|
|
||||||
for info in await redis_client.mget(*keys):
|
result = [json.loads(info) for info in await redis_client.mget(*keys)]
|
||||||
result.append(json.loads(info))
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -61,24 +57,24 @@ class PluginService:
|
|||||||
return await install_git_plugin(repo_url)
|
return await install_git_plugin(repo_url)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def uninstall(*, plugin: str):
|
async def uninstall(*, plugin: str) -> None:
|
||||||
"""
|
"""
|
||||||
卸载插件
|
卸载插件
|
||||||
|
|
||||||
:param plugin: 插件名称
|
:param plugin: 插件名称
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
plugin_dir = os.path.join(PLUGIN_DIR, plugin)
|
plugin_dir = anyio.Path(PLUGIN_DIR / plugin)
|
||||||
if not os.path.exists(plugin_dir):
|
if not await plugin_dir.exists():
|
||||||
raise errors.NotFoundError(msg='插件不存在')
|
raise errors.NotFoundError(msg='插件不存在')
|
||||||
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 = 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}:{plugin}')
|
await redis_client.delete(f'{settings.PLUGIN_REDIS_PREFIX}:{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
|
||||||
async def update_status(*, plugin: str):
|
async def update_status(*, plugin: str) -> None:
|
||||||
"""
|
"""
|
||||||
更新插件状态
|
更新插件状态
|
||||||
|
|
||||||
@@ -107,8 +103,8 @@ class PluginService:
|
|||||||
:param plugin: 插件名称
|
:param plugin: 插件名称
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
plugin_dir = os.path.join(PLUGIN_DIR, plugin)
|
plugin_dir = anyio.Path(PLUGIN_DIR / plugin)
|
||||||
if not os.path.exists(plugin_dir):
|
if not await plugin_dir.exists():
|
||||||
raise errors.NotFoundError(msg='插件不存在')
|
raise errors.NotFoundError(msg='插件不存在')
|
||||||
|
|
||||||
bio = io.BytesIO()
|
bio = io.BytesIO()
|
||||||
@@ -117,7 +113,7 @@ class PluginService:
|
|||||||
dirs[:] = [d for d in dirs if d != '__pycache__']
|
dirs[:] = [d for d in dirs if d != '__pycache__']
|
||||||
for file in files:
|
for file in files:
|
||||||
file_path = os.path.join(root, file)
|
file_path = os.path.join(root, file)
|
||||||
arcname = os.path.relpath(file_path, start=plugin_dir)
|
arcname = os.path.relpath(file_path, start=plugin_dir) # noqa: ASYNC240
|
||||||
zf.write(file_path, os.path.join(plugin, arcname))
|
zf.write(file_path, os.path.join(plugin, arcname))
|
||||||
|
|
||||||
bio.seek(0)
|
bio.seek(0)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
from collections.abc import Sequence
|
||||||
# -*- coding: utf-8 -*-
|
from typing import Any
|
||||||
from typing import Any, Sequence
|
|
||||||
|
|
||||||
from sqlalchemy import Select
|
from sqlalchemy import Select
|
||||||
|
|
||||||
@@ -114,9 +113,8 @@ class RoleService:
|
|||||||
role = await role_dao.get(db, pk)
|
role = await role_dao.get(db, pk)
|
||||||
if not role:
|
if not role:
|
||||||
raise errors.NotFoundError(msg='角色不存在')
|
raise errors.NotFoundError(msg='角色不存在')
|
||||||
if role.name != obj.name:
|
if role.name != obj.name and await role_dao.get_by_name(db, obj.name):
|
||||||
if await role_dao.get_by_name(db, obj.name):
|
raise errors.ConflictError(msg='角色已存在')
|
||||||
raise errors.ConflictError(msg='角色已存在')
|
|
||||||
count = await role_dao.update(db, pk, obj)
|
count = await role_dao.update(db, pk, obj)
|
||||||
for user in await role.awaitable_attrs.users:
|
for user in await role.awaitable_attrs.users:
|
||||||
await redis_client.delete_prefix(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
await redis_client.delete_prefix(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import random
|
import random
|
||||||
|
|
||||||
from typing import Sequence
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
from sqlalchemy import Select
|
from sqlalchemy import Select
|
||||||
@@ -83,7 +81,7 @@ class UserService:
|
|||||||
superuser_verify(request)
|
superuser_verify(request)
|
||||||
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='用户名已注册')
|
||||||
obj.nickname = obj.nickname if obj.nickname else f'#{random.randrange(88888, 99999)}'
|
obj.nickname = obj.nickname or f'#{random.randrange(88888, 99999)}'
|
||||||
if not obj.password:
|
if not obj.password:
|
||||||
raise errors.RequestError(msg='密码不允许为空')
|
raise errors.RequestError(msg='密码不允许为空')
|
||||||
if not await dept_dao.get(db, obj.dept_id):
|
if not await dept_dao.get(db, obj.dept_id):
|
||||||
@@ -108,9 +106,8 @@ class UserService:
|
|||||||
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 obj.username != user.username:
|
if obj.username != user.username and 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='用户名已注册')
|
|
||||||
for role_id in obj.roles:
|
for role_id in obj.roles:
|
||||||
if not await role_dao.get(db, role_id):
|
if not await role_dao.get(db, role_id):
|
||||||
raise errors.NotFoundError(msg='角色不存在')
|
raise errors.NotFoundError(msg='角色不存在')
|
||||||
@@ -119,7 +116,7 @@ class UserService:
|
|||||||
return count
|
return count
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update_permission(*, request: Request, pk: int, type: UserPermissionType) -> int:
|
async def update_permission(*, request: Request, pk: int, type: UserPermissionType) -> int: # noqa: C901
|
||||||
"""
|
"""
|
||||||
更新用户权限
|
更新用户权限
|
||||||
|
|
||||||
@@ -137,14 +134,14 @@ class UserService:
|
|||||||
raise errors.NotFoundError(msg='用户不存在')
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
if pk == request.user.id:
|
if pk == request.user.id:
|
||||||
raise errors.ForbiddenError(msg='禁止修改自身权限')
|
raise errors.ForbiddenError(msg='禁止修改自身权限')
|
||||||
count = await user_dao.set_super(db, pk, not user.status)
|
count = await user_dao.set_super(db, pk, is_super=not user.status)
|
||||||
case UserPermissionType.staff:
|
case UserPermissionType.staff:
|
||||||
user = await user_dao.get(db, pk)
|
user = await user_dao.get(db, pk)
|
||||||
if not user:
|
if not user:
|
||||||
raise errors.NotFoundError(msg='用户不存在')
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
if pk == request.user.id:
|
if pk == request.user.id:
|
||||||
raise errors.ForbiddenError(msg='禁止修改自身权限')
|
raise errors.ForbiddenError(msg='禁止修改自身权限')
|
||||||
count = await user_dao.set_staff(db, pk, not user.is_staff)
|
count = await user_dao.set_staff(db, pk, is_staff=not user.is_staff)
|
||||||
case UserPermissionType.status:
|
case UserPermissionType.status:
|
||||||
user = await user_dao.get(db, pk)
|
user = await user_dao.get(db, pk)
|
||||||
if not user:
|
if not user:
|
||||||
@@ -158,7 +155,7 @@ class UserService:
|
|||||||
raise errors.NotFoundError(msg='用户不存在')
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
multi_login = user.is_multi_login if pk != user.id else request.user.is_multi_login
|
multi_login = user.is_multi_login if pk != user.id else request.user.is_multi_login
|
||||||
new_multi_login = not multi_login
|
new_multi_login = not multi_login
|
||||||
count = await user_dao.set_multi_login(db, pk, new_multi_login)
|
count = await user_dao.set_multi_login(db, pk, multi_login=new_multi_login)
|
||||||
token = get_token(request)
|
token = get_token(request)
|
||||||
token_payload = jwt_decode(token)
|
token_payload = jwt_decode(token)
|
||||||
if pk == user.id:
|
if pk == user.id:
|
||||||
@@ -166,7 +163,8 @@ class UserService:
|
|||||||
if not new_multi_login:
|
if not new_multi_login:
|
||||||
key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{user.id}'
|
key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{user.id}'
|
||||||
await redis_client.delete_prefix(
|
await redis_client.delete_prefix(
|
||||||
key_prefix, exclude=f'{key_prefix}:{token_payload.session_uuid}'
|
key_prefix,
|
||||||
|
exclude=f'{key_prefix}:{token_payload.session_uuid}',
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# 系统管理员修改他人时,他人 token 全部失效
|
# 系统管理员修改他人时,他人 token 全部失效
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from starlette.testclient import TestClient
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
from collections.abc import Generator
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Generator
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
from collections.abc import AsyncGenerator
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import AsyncGenerator
|
|
||||||
|
|
||||||
from sqlalchemy.ext.asyncio.session import AsyncSession
|
from sqlalchemy.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from backend.app.admin.api.router import v1 as admin_v1
|
from backend.app.admin.api.router import v1 as admin_v1
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from backend.core.path_conf import BASE_PATH
|
from backend.core.path_conf import BASE_PATH
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from starlette.concurrency import run_in_threadpool
|
from starlette.concurrency import run_in_threadpool
|
||||||
|
|
||||||
from backend.app.task.celery import celery_app
|
from backend.app.task.celery import celery_app
|
||||||
@@ -7,7 +5,7 @@ from backend.common.socketio.server import sio
|
|||||||
|
|
||||||
|
|
||||||
@sio.event
|
@sio.event
|
||||||
async def task_worker_status(sid, data):
|
async def task_worker_status(sid, data) -> None: # noqa: ANN001
|
||||||
"""任务 Worker 状态事件"""
|
"""任务 Worker 状态事件"""
|
||||||
worker = await run_in_threadpool(celery_app.control.ping)
|
worker = await run_in_threadpool(celery_app.control.ping)
|
||||||
await sio.emit('task_worker_status', worker, sid)
|
await sio.emit('task_worker_status', worker, sid)
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- 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.control import router as task_control_router
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Path
|
from fastapi import APIRouter, Depends, Path
|
||||||
@@ -24,7 +22,7 @@ async def get_task_registered() -> ResponseSchemaModel[list[TaskRegisteredDetail
|
|||||||
raise errors.ServerError(msg='Celery Worker 暂不可用,请稍后重试')
|
raise errors.ServerError(msg='Celery Worker 暂不可用,请稍后重试')
|
||||||
task_registered = []
|
task_registered = []
|
||||||
celery_app_tasks = celery_app.tasks
|
celery_app_tasks = celery_app.tasks
|
||||||
for _, tasks in registered.items():
|
for tasks in registered.values():
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
task_ins = celery_app_tasks.get(task)
|
task_ins = celery_app_tasks.get(task)
|
||||||
if task_ins:
|
if task_ins:
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Path, Query
|
from fastapi import APIRouter, Depends, Path, Query
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Path, Query
|
from fastapi import APIRouter, Depends, Path, Query
|
||||||
|
|
||||||
from backend.app.task.schema.scheduler import CreateTaskSchedulerParam, GetTaskSchedulerDetail, UpdateTaskSchedulerParam
|
from backend.app.task.schema.scheduler import (
|
||||||
|
CreateTaskSchedulerParam,
|
||||||
|
GetTaskSchedulerDetail,
|
||||||
|
UpdateTaskSchedulerParam,
|
||||||
|
)
|
||||||
from backend.app.task.service.scheduler_service import task_scheduler_service
|
from backend.app.task.service.scheduler_service import task_scheduler_service
|
||||||
from backend.common.pagination import DependsPagination, PageData, paging_data
|
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
|
||||||
@@ -40,7 +42,7 @@ async def get_task_scheduler(
|
|||||||
)
|
)
|
||||||
async def get_task_scheduler_paged(
|
async def get_task_scheduler_paged(
|
||||||
db: CurrentSession,
|
db: CurrentSession,
|
||||||
name: Annotated[int, Path(description='任务调度名称')] = None,
|
name: Annotated[int | None, Path(description='任务调度名称')] = None,
|
||||||
type: Annotated[int | None, Query(description='任务调度类型')] = None,
|
type: Annotated[int | None, Query(description='任务调度类型')] = None,
|
||||||
) -> ResponseSchemaModel[PageData[GetTaskSchedulerDetail]]:
|
) -> ResponseSchemaModel[PageData[GetTaskSchedulerDetail]]:
|
||||||
task_scheduler_select = await task_scheduler_service.get_select(name=name, type=type)
|
task_scheduler_select = await task_scheduler_service.get_select(name=name, type=type)
|
||||||
@@ -70,7 +72,8 @@ async def create_task_scheduler(obj: CreateTaskSchedulerParam) -> ResponseModel:
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_task_scheduler(
|
async def update_task_scheduler(
|
||||||
pk: Annotated[int, Path(description='任务调度 ID')], obj: UpdateTaskSchedulerParam
|
pk: Annotated[int, Path(description='任务调度 ID')],
|
||||||
|
obj: UpdateTaskSchedulerParam,
|
||||||
) -> ResponseModel:
|
) -> ResponseModel:
|
||||||
count = await task_scheduler_service.update(pk=pk, obj=obj)
|
count = await task_scheduler_service.update(pk=pk, obj=obj)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import celery
|
import celery
|
||||||
@@ -10,10 +8,10 @@ from backend.core.conf import settings
|
|||||||
from backend.core.path_conf import BASE_PATH
|
from backend.core.path_conf import BASE_PATH
|
||||||
|
|
||||||
|
|
||||||
def find_task_packages():
|
def find_task_packages() -> list[str]:
|
||||||
packages = []
|
packages = []
|
||||||
task_dir = os.path.join(BASE_PATH, 'app', 'task', 'tasks')
|
task_dir = BASE_PATH / 'app' / 'task' / 'tasks'
|
||||||
for root, dirs, files in os.walk(task_dir):
|
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)
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from sqlalchemy import Select
|
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
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
from collections.abc import Sequence
|
||||||
# -*- 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
|
||||||
@@ -86,7 +84,7 @@ class CRUDTaskScheduler(CRUDPlus[TaskScheduler]):
|
|||||||
TaskScheduler.no_changes = False
|
TaskScheduler.no_changes = False
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
async def set_status(self, db: AsyncSession, pk: int, status: bool) -> int:
|
async def set_status(self, db: AsyncSession, pk: int, *, status: bool) -> int:
|
||||||
"""
|
"""
|
||||||
设置任务调度状态
|
设置任务调度状态
|
||||||
|
|
||||||
@@ -96,7 +94,7 @@ class CRUDTaskScheduler(CRUDPlus[TaskScheduler]):
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
task_scheduler = await self.get(db, pk)
|
task_scheduler = await self.get(db, pk)
|
||||||
setattr(task_scheduler, 'enabled', status)
|
task_scheduler.enabled = status
|
||||||
TaskScheduler.no_changes = False
|
TaskScheduler.no_changes = False
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from celery import states
|
from celery import states
|
||||||
from celery.backends.base import BaseBackend
|
from celery.backends.base import BaseBackend
|
||||||
from celery.backends.database import retry, session_cleanup
|
from celery.backends.database import retry, session_cleanup
|
||||||
from celery.exceptions import ImproperlyConfigured
|
from celery.exceptions import ImproperlyConfigured
|
||||||
from celery.utils.time import maybe_timedelta
|
from celery.utils.time import maybe_timedelta
|
||||||
|
from sqlalchemy import PickleType
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from backend.app.task.model.result import Task, TaskExtended, TaskSet
|
from backend.app.task.model.result import Task, TaskExtended, TaskSet
|
||||||
from backend.app.task.session import SessionManager
|
from backend.app.task.session import SessionManager
|
||||||
@@ -24,7 +24,7 @@ class DatabaseBackend(BaseBackend):
|
|||||||
task_cls = Task
|
task_cls = Task
|
||||||
taskset_cls = TaskSet
|
taskset_cls = TaskSet
|
||||||
|
|
||||||
def __init__(self, dburi=None, engine_options=None, url=None, **kwargs):
|
def __init__(self, dburi=None, engine_options=None, url=None, **kwargs) -> None: # noqa: ANN001
|
||||||
# The `url` argument was added later and is used by
|
# The `url` argument was added later and is used by
|
||||||
# the app to set backend by url (celery.app.backends.by_url)
|
# the app to set backend by url (celery.app.backends.by_url)
|
||||||
super().__init__(expires_type=maybe_timedelta, url=url, **kwargs)
|
super().__init__(expires_type=maybe_timedelta, url=url, **kwargs)
|
||||||
@@ -44,7 +44,7 @@ class DatabaseBackend(BaseBackend):
|
|||||||
|
|
||||||
if not self.url:
|
if not self.url:
|
||||||
raise ImproperlyConfigured(
|
raise ImproperlyConfigured(
|
||||||
'Missing connection string! Do you have the database_url setting set to a real value?'
|
'Missing connection string! Do you have the database_url setting set to a real value?',
|
||||||
)
|
)
|
||||||
|
|
||||||
self.session_manager = SessionManager()
|
self.session_manager = SessionManager()
|
||||||
@@ -54,24 +54,26 @@ class DatabaseBackend(BaseBackend):
|
|||||||
self._create_tables()
|
self._create_tables()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def extended_result(self):
|
def extended_result(self): # noqa: ANN201
|
||||||
return self.app.conf.find_value_for_key('extended', 'result')
|
return self.app.conf.find_value_for_key('extended', 'result')
|
||||||
|
|
||||||
def _create_tables(self):
|
def _create_tables(self) -> None:
|
||||||
"""Create the task and taskset tables."""
|
"""Create the task and taskset tables."""
|
||||||
self.ResultSession()
|
self.result_session()
|
||||||
|
|
||||||
def ResultSession(self, session_manager=None):
|
def result_session(self, session_manager=None) -> Session: # noqa: ANN001
|
||||||
if session_manager is None:
|
if session_manager is None:
|
||||||
session_manager = self.session_manager
|
session_manager = self.session_manager
|
||||||
return session_manager.session_factory(
|
return session_manager.session_factory(
|
||||||
dburi=self.url, short_lived_sessions=self.short_lived_sessions, **self.engine_options
|
dburi=self.url,
|
||||||
|
short_lived_sessions=self.short_lived_sessions,
|
||||||
|
**self.engine_options,
|
||||||
)
|
)
|
||||||
|
|
||||||
@retry
|
@retry
|
||||||
def _store_result(self, task_id, result, state, traceback=None, request=None, **kwargs):
|
def _store_result(self, task_id, result, state, traceback=None, request=None, **kwargs) -> None: # noqa: ANN001
|
||||||
"""Store return value and state of an executed task."""
|
"""Store return value and state of an executed task."""
|
||||||
session = self.ResultSession()
|
session = self.result_session()
|
||||||
with session_cleanup(session):
|
with session_cleanup(session):
|
||||||
task = list(session.query(self.task_cls).filter(self.task_cls.task_id == task_id))
|
task = list(session.query(self.task_cls).filter(self.task_cls.task_id == task_id))
|
||||||
task = task and task[0]
|
task = task and task[0]
|
||||||
@@ -84,9 +86,14 @@ class DatabaseBackend(BaseBackend):
|
|||||||
self._update_result(task, result, state, traceback=traceback, request=request)
|
self._update_result(task, result, state, traceback=traceback, request=request)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
def _update_result(self, task, result, state, traceback=None, request=None):
|
def _update_result(self, task, result, state, traceback=None, request=None) -> None: # noqa: ANN001
|
||||||
meta = self._get_result_meta(
|
meta = self._get_result_meta(
|
||||||
result=result, state=state, traceback=traceback, request=request, format_date=False, encode=True
|
result=result,
|
||||||
|
state=state,
|
||||||
|
traceback=traceback,
|
||||||
|
request=request,
|
||||||
|
format_date=False,
|
||||||
|
encode=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Exclude the primary key id and task_id columns
|
# Exclude the primary key id and task_id columns
|
||||||
@@ -101,9 +108,9 @@ class DatabaseBackend(BaseBackend):
|
|||||||
setattr(task, column, value)
|
setattr(task, column, value)
|
||||||
|
|
||||||
@retry
|
@retry
|
||||||
def _get_task_meta_for(self, task_id):
|
def _get_task_meta_for(self, task_id: str): # noqa: ANN202
|
||||||
"""Get task meta-data for a task by id."""
|
"""Get task meta-data for a task by id."""
|
||||||
session = self.ResultSession()
|
session = self.result_session()
|
||||||
with session_cleanup(session):
|
with session_cleanup(session):
|
||||||
task = list(session.query(self.task_cls).filter(self.task_cls.task_id == task_id))
|
task = list(session.query(self.task_cls).filter(self.task_cls.task_id == task_id))
|
||||||
task = task and task[0]
|
task = task and task[0]
|
||||||
@@ -119,9 +126,9 @@ class DatabaseBackend(BaseBackend):
|
|||||||
return self.meta_from_decoded(data)
|
return self.meta_from_decoded(data)
|
||||||
|
|
||||||
@retry
|
@retry
|
||||||
def _save_group(self, group_id, result):
|
def _save_group(self, group_id: str, result: PickleType): # noqa: ANN202
|
||||||
"""Store the result of an executed group."""
|
"""Store the result of an executed group."""
|
||||||
session = self.ResultSession()
|
session = self.result_session()
|
||||||
with session_cleanup(session):
|
with session_cleanup(session):
|
||||||
group = self.taskset_cls(group_id, result)
|
group = self.taskset_cls(group_id, result)
|
||||||
session.add(group)
|
session.add(group)
|
||||||
@@ -130,34 +137,34 @@ class DatabaseBackend(BaseBackend):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
@retry
|
@retry
|
||||||
def _restore_group(self, group_id):
|
def _restore_group(self, group_id: str) -> dict | None:
|
||||||
"""Get meta-data for group by id."""
|
"""Get meta-data for group by id."""
|
||||||
session = self.ResultSession()
|
session = self.result_session()
|
||||||
with session_cleanup(session):
|
with session_cleanup(session):
|
||||||
group = session.query(self.taskset_cls).filter(self.taskset_cls.taskset_id == group_id).first()
|
group = session.query(self.taskset_cls).filter(self.taskset_cls.taskset_id == group_id).first()
|
||||||
if group:
|
if group:
|
||||||
return group.to_dict()
|
return group.to_dict()
|
||||||
|
|
||||||
@retry
|
@retry
|
||||||
def _delete_group(self, group_id):
|
def _delete_group(self, group_id: str) -> None:
|
||||||
"""Delete meta-data for group by id."""
|
"""Delete meta-data for group by id."""
|
||||||
session = self.ResultSession()
|
session = self.result_session()
|
||||||
with session_cleanup(session):
|
with session_cleanup(session):
|
||||||
session.query(self.taskset_cls).filter(self.taskset_cls.taskset_id == group_id).delete()
|
session.query(self.taskset_cls).filter(self.taskset_cls.taskset_id == group_id).delete()
|
||||||
session.flush()
|
session.flush()
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
@retry
|
@retry
|
||||||
def _forget(self, task_id):
|
def _forget(self, task_id: str) -> None:
|
||||||
"""Forget about result."""
|
"""Forget about result."""
|
||||||
session = self.ResultSession()
|
session = self.result_session()
|
||||||
with session_cleanup(session):
|
with session_cleanup(session):
|
||||||
session.query(self.task_cls).filter(self.task_cls.task_id == task_id).delete()
|
session.query(self.task_cls).filter(self.task_cls.task_id == task_id).delete()
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self) -> None:
|
||||||
"""Delete expired meta-data."""
|
"""Delete expired meta-data."""
|
||||||
session = self.ResultSession()
|
session = self.result_session()
|
||||||
expires = self.expires
|
expires = self.expires
|
||||||
now = self.app.now()
|
now = self.app.now()
|
||||||
with session_cleanup(session):
|
with session_cleanup(session):
|
||||||
@@ -165,7 +172,7 @@ class DatabaseBackend(BaseBackend):
|
|||||||
session.query(self.taskset_cls).filter(self.taskset_cls.date_done < (now - expires)).delete()
|
session.query(self.taskset_cls).filter(self.taskset_cls.date_done < (now - expires)).delete()
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
def __reduce__(self, args=(), kwargs=None):
|
def __reduce__(self, args=(), kwargs=None): # noqa: ANN001, ANN204
|
||||||
kwargs = {} if not kwargs else kwargs
|
kwargs = kwargs or {}
|
||||||
kwargs.update({'dburi': self.url, 'expires': self.expires, 'engine_options': self.engine_options})
|
kwargs.update({'dburi': self.url, 'expires': self.expires, 'engine_options': self.engine_options})
|
||||||
return super().__reduce__(args, kwargs)
|
return super().__reduce__(args, kwargs)
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from backend.common.enums import IntEnum, StrEnum
|
from backend.common.enums import IntEnum, StrEnum
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,2 @@
|
|||||||
#!/usr/bin/env python3
|
from backend.app.task.model.result import TaskExtended as TaskResult # noqa: F401
|
||||||
# -*- coding: utf-8 -*-
|
from backend.app.task.model.scheduler import TaskScheduler as TaskScheduler
|
||||||
from backend.app.task.model.result import TaskExtended as TaskResult
|
|
||||||
from backend.app.task.model.scheduler import TaskScheduler
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
@@ -25,14 +23,17 @@ class Task(MappedBase):
|
|||||||
status = sa.Column(sa.String(50), default=states.PENDING)
|
status = sa.Column(sa.String(50), default=states.PENDING)
|
||||||
result = sa.Column(PickleType, nullable=True)
|
result = sa.Column(PickleType, nullable=True)
|
||||||
date_done = sa.Column(
|
date_done = sa.Column(
|
||||||
sa.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc), nullable=True
|
sa.DateTime,
|
||||||
|
default=datetime.now(timezone.utc),
|
||||||
|
onupdate=datetime.now(timezone.utc),
|
||||||
|
nullable=True,
|
||||||
)
|
)
|
||||||
traceback = sa.Column(sa.Text, nullable=True)
|
traceback = sa.Column(sa.Text, nullable=True)
|
||||||
|
|
||||||
def __init__(self, task_id):
|
def __init__(self, task_id: str) -> None:
|
||||||
self.task_id = task_id
|
self.task_id = task_id
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self) -> dict:
|
||||||
return {
|
return {
|
||||||
'task_id': self.task_id,
|
'task_id': self.task_id,
|
||||||
'status': self.status,
|
'status': self.status,
|
||||||
@@ -41,11 +42,11 @@ class Task(MappedBase):
|
|||||||
'date_done': self.date_done,
|
'date_done': self.date_done,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return '<Task {0.task_id} state: {0.status}>'.format(self)
|
return f'<Task {self.task_id} state: {self.status}>'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def configure(cls, schema=None, name=None):
|
def configure(cls, schema=None, name=None) -> None: # noqa: ANN001
|
||||||
cls.__table__.schema = schema
|
cls.__table__.schema = schema
|
||||||
cls.id.default.schema = schema
|
cls.id.default.schema = schema
|
||||||
cls.__table__.name = name or cls.__tablename__
|
cls.__table__.name = name or cls.__tablename__
|
||||||
@@ -64,7 +65,7 @@ class TaskExtended(Task):
|
|||||||
retries = sa.Column(sa.Integer, nullable=True)
|
retries = sa.Column(sa.Integer, nullable=True)
|
||||||
queue = sa.Column(sa.String(155), nullable=True)
|
queue = sa.Column(sa.String(155), nullable=True)
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self) -> dict:
|
||||||
task_dict = super().to_dict()
|
task_dict = super().to_dict()
|
||||||
task_dict.update({
|
task_dict.update({
|
||||||
'name': self.name,
|
'name': self.name,
|
||||||
@@ -88,22 +89,22 @@ class TaskSet(MappedBase):
|
|||||||
result = sa.Column(PickleType, nullable=True)
|
result = sa.Column(PickleType, nullable=True)
|
||||||
date_done = sa.Column(sa.DateTime, default=datetime.now(timezone.utc), nullable=True)
|
date_done = sa.Column(sa.DateTime, default=datetime.now(timezone.utc), nullable=True)
|
||||||
|
|
||||||
def __init__(self, taskset_id, result):
|
def __init__(self, taskset_id, result) -> None: # noqa: ANN001
|
||||||
self.taskset_id = taskset_id
|
self.taskset_id = taskset_id
|
||||||
self.result = result
|
self.result = result
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self) -> dict:
|
||||||
return {
|
return {
|
||||||
'taskset_id': self.taskset_id,
|
'taskset_id': self.taskset_id,
|
||||||
'result': self.result,
|
'result': self.result,
|
||||||
'date_done': self.date_done,
|
'date_done': self.date_done,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return f'<TaskSet: {self.taskset_id}>'
|
return f'<TaskSet: {self.taskset_id}>'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def configure(cls, schema=None, name=None):
|
def configure(cls, schema=None, name=None) -> None: # noqa: ANN001
|
||||||
cls.__table__.schema = schema
|
cls.__table__.schema = schema
|
||||||
cls.id.default.schema = schema
|
cls.id.default.schema = schema
|
||||||
cls.__table__.name = name or cls.__tablename__
|
cls.__table__.name = name or cls.__tablename__
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -42,36 +40,42 @@ class TaskScheduler(Base):
|
|||||||
interval_period: Mapped[str | None] = mapped_column(String(255), comment='任务运行之间的周期类型')
|
interval_period: Mapped[str | None] = mapped_column(String(255), comment='任务运行之间的周期类型')
|
||||||
crontab: Mapped[str | None] = mapped_column(String(50), default='* * * * *', comment='任务运行的 Crontab 计划')
|
crontab: Mapped[str | None] = mapped_column(String(50), default='* * * * *', comment='任务运行的 Crontab 计划')
|
||||||
one_off: Mapped[bool] = mapped_column(
|
one_off: Mapped[bool] = mapped_column(
|
||||||
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='是否仅运行一次'
|
Boolean().with_variant(INTEGER, 'postgresql'),
|
||||||
|
default=False,
|
||||||
|
comment='是否仅运行一次',
|
||||||
)
|
)
|
||||||
enabled: Mapped[bool] = mapped_column(
|
enabled: Mapped[bool] = mapped_column(
|
||||||
Boolean().with_variant(INTEGER, 'postgresql'), default=True, comment='是否启用任务'
|
Boolean().with_variant(INTEGER, 'postgresql'),
|
||||||
|
default=True,
|
||||||
|
comment='是否启用任务',
|
||||||
)
|
)
|
||||||
total_run_count: Mapped[int] = mapped_column(default=0, comment='任务触发的总次数')
|
total_run_count: Mapped[int] = mapped_column(default=0, comment='任务触发的总次数')
|
||||||
last_run_time: Mapped[datetime | None] = mapped_column(TimeZone, default=None, comment='任务最后触发的时间')
|
last_run_time: Mapped[datetime | None] = mapped_column(TimeZone, default=None, comment='任务最后触发的时间')
|
||||||
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='备注',
|
||||||
)
|
)
|
||||||
|
|
||||||
no_changes: bool = False
|
no_changes: bool = False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def before_insert_or_update(mapper, connection, target):
|
def before_insert_or_update(mapper, connection, target) -> None: # noqa: ANN001
|
||||||
if target.expire_seconds is not None and target.expire_time:
|
if target.expire_seconds is not None and target.expire_time:
|
||||||
raise errors.ConflictError(msg='expires 和 expire_seconds 只能设置一个')
|
raise errors.ConflictError(msg='expires 和 expire_seconds 只能设置一个')
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def changed(cls, mapper, connection, target):
|
def changed(cls, mapper, connection, target) -> None: # noqa: ANN001
|
||||||
if not target.no_changes:
|
if not target.no_changes:
|
||||||
cls.update_changed(mapper, connection, target)
|
cls.update_changed(mapper, connection, target)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def update_changed_async(cls):
|
async def update_changed_async(cls) -> None:
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
await redis_client.set(f'{settings.CELERY_REDIS_PREFIX}:last_update', timezone.to_str(now))
|
await redis_client.set(f'{settings.CELERY_REDIS_PREFIX}:last_update', timezone.to_str(now))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_changed(cls, mapper, connection, target):
|
def update_changed(cls, mapper, connection, target) -> None: # noqa: ANN001
|
||||||
asyncio.create_task(cls.update_changed_async())
|
asyncio.create_task(cls.update_changed_async())
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from backend.common.schema import SchemaBase
|
from backend.common.schema import SchemaBase
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -39,5 +37,5 @@ class GetTaskResultDetail(TaskResultSchemaBase):
|
|||||||
id: int = Field(description='任务结果 ID')
|
id: int = Field(description='任务结果 ID')
|
||||||
|
|
||||||
@field_serializer('args', 'kwargs', when_used='unless-none')
|
@field_serializer('args', 'kwargs', when_used='unless-none')
|
||||||
def serialize_params(self, value: bytes | None, _info) -> Any:
|
def serialize_params(self, value: bytes | None) -> Any:
|
||||||
return celery_app.backend.decode(value)
|
return celery_app.backend.decode(value)
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from pydantic import ConfigDict, Field
|
from pydantic import ConfigDict, Field
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from sqlalchemy import Select
|
from sqlalchemy import Select
|
||||||
|
|
||||||
from backend.app.task.crud.crud_result import task_result_dao
|
from backend.app.task.crud.crud_result import task_result_dao
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user