Skip to content

Commit ce3be1d

Browse files
authored
Add support for celery dynamic tasks (#715)
* Add support for celery dynamic tasks * Update the celery conf * Update the celery task tables name * Refactor the celery task-related interfaces * Optimize auto-discovery tasks * Remove redundant config * Refine the business codes * Optimize crontab validation returns * Update dependencies in pyproject toml * Fix some bugs * Update dependencies * Update the version to 1.7.0 * Fix update and delete event
1 parent e84ef04 commit ce3be1d

39 files changed

+2577
-1279
lines changed

backend/.env.example

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ OAUTH2_LINUX_DO_CLIENT_SECRET='test'
2424
# App Task
2525
# Celery
2626
CELERY_BROKER_REDIS_DATABASE=1
27-
CELERY_BACKEND_REDIS_DATABASE=2
2827
# Rabbitmq
2928
CELERY_RABBITMQ_HOST='127.0.0.1'
3029
CELERY_RABBITMQ_PORT=5672

backend/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# -*- coding: utf-8 -*-
33
from backend.utils.console import console
44

5-
__version__ = '1.6.0'
5+
__version__ = '1.7.0'
66

77

88
def get_version() -> str | None:

backend/alembic/env.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,16 @@
1313

1414
sys.path.append('../')
1515

16+
from backend.app import get_app_models
1617
from backend.common.model import MappedBase
1718
from backend.core import path_conf
1819
from backend.database.db import SQLALCHEMY_DATABASE_URL
1920
from backend.plugin.tools import get_plugin_models
2021

21-
# import your new model here
22-
from backend.app.admin.model import * # noqa: F401
23-
from backend.plugin.code_generator.model import * # noqa: F401
24-
25-
# import plugin model
26-
for cls in get_plugin_models():
22+
# import models
23+
for cls in get_app_models() + get_plugin_models():
2724
class_name = cls.__name__
28-
if class_name in globals():
29-
print(f'\nWarning: Class "{class_name}" already exists in global namespace.')
30-
else:
25+
if class_name not in globals():
3126
globals()[class_name] = cls
3227

3328
if not os.path.exists(path_conf.ALEMBIC_VERSION_DIR):

backend/app/__init__.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,43 @@
11
#!/usr/bin/env python3
22
# -*- coding: utf-8 -*-
3+
import inspect
4+
import os.path
5+
6+
from backend.common.log import log
7+
from backend.core.path_conf import BASE_PATH
8+
from backend.utils.import_parse import import_module_cached
9+
10+
11+
def get_app_models():
12+
"""获取 app 所有模型类"""
13+
app_path = os.path.join(BASE_PATH, 'app')
14+
list_dirs = os.listdir(app_path)
15+
16+
apps = []
17+
18+
for d in list_dirs:
19+
if os.path.isdir(os.path.join(app_path, d)) and d != '__pycache__':
20+
apps.append(d)
21+
22+
classes = []
23+
24+
for app in apps:
25+
try:
26+
module_path = f'backend.app.{app}.model'
27+
module = import_module_cached(module_path)
28+
except Exception as e:
29+
log.warning(f'应用 {app} 中不包含 model 相关配置: {e}')
30+
continue
31+
32+
for name, obj in inspect.getmembers(module):
33+
if inspect.isclass(obj):
34+
classes.append(obj)
35+
36+
return classes
37+
38+
39+
# import all app models for auto create db tables
40+
for cls in get_app_models():
41+
class_name = cls.__name__
42+
if class_name not in globals():
43+
globals()[class_name] = cls

backend/app/task/README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,21 @@
33
当前任务使用 Celery
44
实现,实施方案请查看 [#225](https://github.com/fastapi-practices/fastapi_best_architecture/discussions/225)
55

6-
## 添加任务
6+
## 定时任务
77

8-
> [!IMPORTANT]
9-
> 由于 Celery 任务扫描规则,使其对任务的目录结构要求及其严格,务必在 celery_task 目录下添加任务
8+
`backend/app/task/tasks/beat.py` 文件内编写相关定时任务
109

1110
### 简单任务
1211

13-
可以直接在 `tasks.py` 文件内编写相关任务代码
12+
`backend/app/task/tasks/tasks.py` 文件内编写相关任务代码
1413

1514
### 层级任务
1615

1716
如果你想对任务进行目录层级划分,使任务结构更加清晰,你可以新建任意目录,但必须注意的是
1817

19-
1. 新建目录后,务必更新任务配置 `CELERY_TASKS_PACKAGES`,将新建目录添加到此列表
20-
2. 在新建目录下,务必添加 `tasks.py` 文件,并在此文件中编写相关任务代码
18+
1.`backend/app/task/tasks` 目录下新建 python 包目录
19+
2. 新建目录后,务必更新 `conf.py` 配置中的 `CELERY_TASKS_PACKAGES`,将新建目录模块路径添加到此列表
20+
3. 在新建目录下,务必添加 `tasks.py` 文件,并在此文件中编写相关任务代码
2121

2222
## 消息代理
2323

backend/app/task/api/router.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
# -*- coding: utf-8 -*-
33
from fastapi import APIRouter
44

5-
from backend.app.task.api.v1.task import router as task_router
5+
from backend.app.task.api.v1.result import router as task_result_router
6+
from backend.app.task.api.v1.scheduler import router as task_scheduler_router
67
from backend.core.conf import settings
78

8-
v1 = APIRouter(prefix=settings.FASTAPI_API_V1_PATH)
9+
v1 = APIRouter(prefix=f'{settings.FASTAPI_API_V1_PATH}/task', tags=['任务'])
910

10-
v1.include_router(task_router, prefix='/tasks', tags=['任务'])
11+
v1.include_router(task_result_router, prefix='/results')
12+
v1.include_router(task_scheduler_router, prefix='/schedulers')

backend/app/task/api/v1/result.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
from typing import Annotated
4+
5+
from fastapi import APIRouter, Depends, Path, Query
6+
7+
from backend.app.task.schema.result import DeleteTaskResultParam, GetTaskResultDetail
8+
from backend.app.task.service.result_service import task_result_service
9+
from backend.common.pagination import DependsPagination, PageData, paging_data
10+
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
11+
from backend.common.security.jwt import DependsJwtAuth
12+
from backend.common.security.permission import RequestPermission
13+
from backend.common.security.rbac import DependsRBAC
14+
from backend.database.db import CurrentSession
15+
16+
router = APIRouter()
17+
18+
19+
@router.get('/{pk}', summary='获取任务结果详情', dependencies=[DependsJwtAuth])
20+
async def get_task_result(
21+
pk: Annotated[int, Path(description='任务结果 ID')],
22+
) -> ResponseSchemaModel[GetTaskResultDetail]:
23+
result = await task_result_service.get(pk=pk)
24+
return response_base.success(data=result)
25+
26+
27+
@router.get(
28+
'',
29+
summary='分页获取所有任务结果',
30+
dependencies=[
31+
DependsJwtAuth,
32+
DependsPagination,
33+
],
34+
)
35+
async def get_task_results_paged(
36+
db: CurrentSession,
37+
name: Annotated[str | None, Query(description='任务名称')] = None,
38+
task_id: Annotated[str | None, Query(description='任务 ID')] = None,
39+
) -> ResponseSchemaModel[PageData[GetTaskResultDetail]]:
40+
result_select = await task_result_service.get_select(name=name, task_id=task_id)
41+
page_data = await paging_data(db, result_select)
42+
return response_base.success(data=page_data)
43+
44+
45+
@router.delete(
46+
'',
47+
summary='批量删除任务结果',
48+
dependencies=[
49+
Depends(RequestPermission('sys:task:del')),
50+
DependsRBAC,
51+
],
52+
)
53+
async def delete_task_result(obj: DeleteTaskResultParam) -> ResponseModel:
54+
count = await task_result_service.delete(obj=obj)
55+
if count > 0:
56+
return response_base.success()
57+
return response_base.fail()

backend/app/task/api/v1/scheduler.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
from typing import Annotated
4+
5+
from fastapi import APIRouter, Depends, Path, Query
6+
7+
from backend.app.task.schema.scheduler import CreateTaskSchedulerParam, GetTaskSchedulerDetail, UpdateTaskSchedulerParam
8+
from backend.app.task.service.scheduler_service import task_scheduler_service
9+
from backend.common.pagination import DependsPagination, PageData, paging_data
10+
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
11+
from backend.common.security.jwt import DependsJwtAuth
12+
from backend.common.security.permission import RequestPermission
13+
from backend.common.security.rbac import DependsRBAC
14+
from backend.database.db import CurrentSession
15+
16+
router = APIRouter()
17+
18+
19+
@router.get('/all', summary='获取所有任务调度', dependencies=[DependsJwtAuth])
20+
async def get_all_task_schedulers() -> ResponseSchemaModel[list[GetTaskSchedulerDetail]]:
21+
schedulers = await task_scheduler_service.get_all()
22+
return response_base.success(data=schedulers)
23+
24+
25+
@router.get('/{pk}', summary='获取任务调度详情', dependencies=[DependsJwtAuth])
26+
async def get_task_scheduler(
27+
pk: Annotated[int, Path(description='任务调度 ID')],
28+
) -> ResponseSchemaModel[GetTaskSchedulerDetail]:
29+
task_scheduler = await task_scheduler_service.get(pk=pk)
30+
return response_base.success(data=task_scheduler)
31+
32+
33+
@router.get(
34+
'',
35+
summary='分页获取所有任务调度',
36+
dependencies=[
37+
DependsJwtAuth,
38+
DependsPagination,
39+
],
40+
)
41+
async def get_task_scheduler_paged(
42+
db: CurrentSession,
43+
name: Annotated[int, Path(description='任务调度名称')] = None,
44+
type: Annotated[int | None, Query(description='任务调度类型')] = None,
45+
) -> ResponseSchemaModel[PageData[GetTaskSchedulerDetail]]:
46+
task_scheduler_select = await task_scheduler_service.get_select(name=name, type=type)
47+
page_data = await paging_data(db, task_scheduler_select)
48+
return response_base.success(data=page_data)
49+
50+
51+
@router.post(
52+
'',
53+
summary='创建任务调度',
54+
dependencies=[
55+
Depends(RequestPermission('sys:task:add')),
56+
DependsRBAC,
57+
],
58+
)
59+
async def create_task_scheduler(obj: CreateTaskSchedulerParam) -> ResponseModel:
60+
await task_scheduler_service.create(obj=obj)
61+
return response_base.success()
62+
63+
64+
@router.put(
65+
'/{pk}',
66+
summary='更新任务调度',
67+
dependencies=[
68+
Depends(RequestPermission('sys:task:edit')),
69+
DependsRBAC,
70+
],
71+
)
72+
async def update_task_scheduler(
73+
pk: Annotated[int, Path(description='任务调度 ID')], obj: UpdateTaskSchedulerParam
74+
) -> ResponseModel:
75+
count = await task_scheduler_service.update(pk=pk, obj=obj)
76+
if count > 0:
77+
return response_base.success()
78+
return response_base.fail()
79+
80+
81+
@router.put(
82+
'/{pk}/status',
83+
summary='更新任务调度状态',
84+
dependencies=[
85+
Depends(RequestPermission('sys:task:edit')),
86+
DependsRBAC,
87+
],
88+
)
89+
async def update_task_scheduler_status(pk: Annotated[int, Path(description='任务调度 ID')]) -> ResponseModel:
90+
count = await task_scheduler_service.update_status(pk=pk)
91+
if count > 0:
92+
return response_base.success()
93+
return response_base.fail()
94+
95+
96+
@router.delete(
97+
'/{pk}',
98+
summary='删除任务调度',
99+
dependencies=[
100+
Depends(RequestPermission('sys:task:del')),
101+
DependsRBAC,
102+
],
103+
)
104+
async def delete_task_scheduler(pk: Annotated[int, Path(description='任务调度 ID')]) -> ResponseModel:
105+
count = await task_scheduler_service.delete(pk=pk)
106+
if count > 0:
107+
return response_base.success()
108+
return response_base.fail()
109+
110+
111+
@router.post(
112+
'/{pk}/executions',
113+
summary='手动执行任务',
114+
dependencies=[
115+
Depends(RequestPermission('sys:task:exec')),
116+
DependsRBAC,
117+
],
118+
)
119+
async def execute_task(pk: Annotated[int, Path(description='任务调度 ID')]) -> ResponseModel:
120+
await task_scheduler_service.execute(pk=pk)
121+
return response_base.success()
122+
123+
124+
@router.delete(
125+
'/{task_id}/cancel',
126+
summary='撤销任务',
127+
dependencies=[
128+
Depends(RequestPermission('sys:task:revoke')),
129+
DependsRBAC,
130+
],
131+
)
132+
async def revoke_task(task_id: Annotated[str, Path(description='任务 UUID')]) -> ResponseModel:
133+
await task_scheduler_service.revoke(task_id=task_id)
134+
return response_base.success()

backend/app/task/api/v1/task.py

Lines changed: 0 additions & 58 deletions
This file was deleted.

0 commit comments

Comments
 (0)