签入版本

This commit is contained in:
2026-09-11 11:39:07 +08:00
commit c381201858
928 changed files with 116443 additions and 0 deletions
View File
+3
View File
@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.
+129
View File
@@ -0,0 +1,129 @@
# +======================================================================
# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ]
# +----------------------------------------------------------------------
# | Copyright (c) 2021-2026 小蚂蚁云团队 版权所有
# +----------------------------------------------------------------------
# | 开源协议: Apache-2.0
# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。
# +----------------------------------------------------------------------
# | 官方网站: https://www.djangoadmin.cn
# +----------------------------------------------------------------------
# | 研发团队: 小蚂蚁云团队 荣誉出品
# +======================================================================
# | 版权和免责声明:
# | 一、知识产权声明
# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、
# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权,
# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。
# | 二、使用限制与禁止条款
# | 您理解并同意,禁止将本产品用于以下任何行为:
# | 1. 任何违反中华人民共和国法律法规的用途;
# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为;
# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。
# | 三、责任豁免
# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失
# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三
# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。
# | 四、违约与法律责任
# | 如发现任何违反本声明的行为,本团队有权:
# | 1. 无条件配合公安机关及其他有权机关的调查取证工作;
# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失;
# | 3. 保留进一步采取刑事控告等法律措施的权利。
# | 五、许可范围说明
# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包
# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通
# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、
# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造
# | 成的一切经济损失及商誉损害。
# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件
# | 许可协议》附件为准。
# +======================================================================
"""
定时任务管理应用配置模块
=============================================================
该模块定义了定时任务管理应用的配置类,用于Django应用注册和初始化。
在应用就绪时自动启动定时任务调度器。
"""
from django.apps import AppConfig
import logging
import os
import threading
import time
logger = logging.getLogger(__name__)
# =============================================================
# 定时任务应用配置类
# =============================================================
class JobConfig(AppConfig):
"""
定时任务管理应用配置类
在应用就绪时自动初始化并启动定时任务调度器。
"""
# 指定默认的主键字段类型
# 使用 BigAutoField(64位整数)而非默认的 AutoField(32位整数)
default_auto_field = 'django.db.models.BigAutoField'
# 指定该应用在项目中的Python导入路径
name = 'application.job'
# 应用在admin后台显示的友好名称
verbose_name = '定时任务管理'
def ready(self):
"""
应用就绪时执行
用于初始化定时任务调度器。
使用延迟加载避免在应用初始化时访问数据库导致的警告。
通过多种条件判断避免在管理命令和自动重载时重复启动。
"""
# 判断是否应该启动调度器
# 条件1:开发环境重载时只启动一次(RUN_MAIN=true)
# 条件2:生产环境(非自动重载模式)
should_start = (
os.environ.get('RUN_MAIN') == 'true' or
not os.environ.get('DJANGO_AUTORELOAD')
)
# 额外检查:避免在管理命令中启动调度器
import sys
is_management_command = (
'manage.py' in sys.argv[0] and
len(sys.argv) > 1 and
sys.argv[1] in ['makemigrations', 'migrate', 'shell', 'dbshell', 'test']
)
# 仅在满足条件时启动调度器
if should_start and not is_management_command:
def start_scheduler():
"""
延迟启动调度器
等待 Django 完全初始化后,再启动调度器。
使用独立线程避免阻塞 Django 启动过程。
"""
# 等待 Django 完全启动
time.sleep(2)
try:
from application.job.scheduler import init_scheduler
success_count = init_scheduler()
if success_count > 0:
logger.info(f"定时任务调度器已成功启动,加载了 {success_count} 个任务")
else:
logger.info("定时任务调度器已启动,暂无需要调度的任务")
except Exception as e:
logger.warning(f"调度器启动失败(可能数据库未就绪): {e}")
# 使用独立线程启动调度器,避免阻塞 Django 启动
thread = threading.Thread(target=start_scheduler, name="JobSchedulerInit")
thread.daemon = True
thread.start()
logger.info("定时任务调度器初始化线程已启动")
+252
View File
@@ -0,0 +1,252 @@
# +======================================================================
# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ]
# +----------------------------------------------------------------------
# | Copyright (c) 2021-2026 小蚂蚁云团队 版权所有
# +----------------------------------------------------------------------
# | 开源协议: Apache-2.0
# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。
# +----------------------------------------------------------------------
# | 官方网站: https://www.djangoadmin.cn
# +----------------------------------------------------------------------
# | 研发团队: 小蚂蚁云团队 荣誉出品
# +======================================================================
# | 版权和免责声明:
# | 一、知识产权声明
# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、
# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权,
# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。
# | 二、使用限制与禁止条款
# | 您理解并同意,禁止将本产品用于以下任何行为:
# | 1. 任何违反中华人民共和国法律法规的用途;
# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为;
# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。
# | 三、责任豁免
# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失
# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三
# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。
# | 四、违约与法律责任
# | 如发现任何违反本声明的行为,本团队有权:
# | 1. 无条件配合公安机关及其他有权机关的调查取证工作;
# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失;
# | 3. 保留进一步采取刑事控告等法律措施的权利。
# | 五、许可范围说明
# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包
# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通
# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、
# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造
# | 成的一切经济损失及商誉损害。
# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件
# | 许可协议》附件为准。
# +======================================================================
"""
定时任务表单验证模块
=============================================================
该模块定义了定时任务管理功能中使用的表单验证类。
用于对前端提交的任务数据进行验证,确保数据符合业务规则和数据库约束。
"""
from django import forms
from application.job import models
# =============================================================
# 定时任务表单验证类
# =============================================================
class JobForm(forms.ModelForm):
"""
定时任务表单验证类
用于创建和更新定时任务信息时的表单验证。
"""
# ----------------------------------------------------------
# 任务名称
# ----------------------------------------------------------
# 字段说明: 任务的显示名称
# 字段类型: 字符型(CharField)
# 验证规则: 必填(required=True),最大长度255字符
# 业务说明: 用于前端展示和日志记录
jobName = forms.CharField(
required=True,
max_length=255,
error_messages={
'required': '任务名称不能为空',
'max_length': '任务名称长度不得超过255个字符',
}
)
# ----------------------------------------------------------
# 任务别名
# ----------------------------------------------------------
# 字段说明: 任务的别名
# 字段类型: 字符型(CharField)
# 验证规则: 可选填(required=False),最大长度255字符
# 业务说明: 用于展示更友好的任务名称
jobAlias = forms.CharField(
required=False,
max_length=255,
error_messages={
'max_length': '任务别名长度不得超过255个字符',
}
)
# ----------------------------------------------------------
# 任务分组
# ----------------------------------------------------------
# 字段说明: 任务所属分组
# 字段类型: 字符型(CharField)
# 验证规则: 可选填(required=False),最大长度255字符
# 业务说明: 对任务进行分类管理,便于批量操作
jobGroup = forms.CharField(
required=False,
max_length=255,
error_messages={
'max_length': '任务分组长度不得超过255个字符',
}
)
# ----------------------------------------------------------
# 任务触发器
# ----------------------------------------------------------
# 字段说明: 任务触发器标识
# 字段类型: 字符型(CharField)
# 验证规则: 可选填(required=False),最大长度255字符
# 业务说明: 映射到具体的执行方法,用于内部方法调用
jobTrigger = forms.CharField(
required=False,
max_length=255,
error_messages={
'max_length': '任务触发器长度不得超过255个字符',
}
)
# ----------------------------------------------------------
# 任务状态
# ----------------------------------------------------------
# 字段说明: 任务的运行状态
# 字段类型: 整型(IntegerField)
# 验证规则: 必填(required=True),取值范围0-3
# 可选值: 0-未发布, 1-运行中, 2-暂停, 3-删除
status = forms.IntegerField(
required=True,
min_value=0,
max_value=3,
error_messages={
'required': '任务状态不能为空',
'min_value': '任务状态值在0~3之间',
'max_value': '任务状态值在0~3之间',
}
)
# ----------------------------------------------------------
# Cron表达式
# ----------------------------------------------------------
# 字段说明: 任务的调度规则
# 字段类型: 字符型(CharField)
# 验证规则: 可选填(required=False),最大长度255字符
# 业务说明: 定义任务的执行时间规则,如"0 2 * * *"表示每天凌晨2点执行
cronExpression = forms.CharField(
required=False,
max_length=255,
error_messages={
'max_length': 'Cron表达式长度不得超过255个字符',
}
)
# ----------------------------------------------------------
# 执行策略
# ----------------------------------------------------------
# 字段说明: 任务触发后的执行策略
# 字段类型: 整型(IntegerField)
# 验证规则: 必填(required=True),取值范围1-3
# 可选值: 1-立即执行, 2-执行一次, 3-放弃执行
executePolicy = forms.IntegerField(
required=True,
min_value=1,
max_value=3,
error_messages={
'required': '执行策略不能为空',
'min_value': '执行策略值在1~3之间',
'max_value': '执行策略值在1~3之间',
}
)
# ----------------------------------------------------------
# 是否同步执行
# ----------------------------------------------------------
# 字段说明: 任务是否同步执行
# 字段类型: 整型(IntegerField)
# 验证规则: 必填(required=True),取值范围0-1
# 可选值: 0-异步, 1-同步
isSync = forms.IntegerField(
required=True,
min_value=0,
max_value=1,
error_messages={
'required': '是否同步任务不能为空',
'min_value': '是否同步任务值在0~1之间',
'max_value': '是否同步任务值在0~1之间',
}
)
# ----------------------------------------------------------
# 任务URL
# ----------------------------------------------------------
# 字段说明: 任务调用的URL地址
# 字段类型: 字符型(CharField)
# 验证规则: 可选填(required=False),最大长度255字符
# 业务说明: 当任务通过HTTP方式执行时,调用此URL
url = forms.CharField(
required=False,
max_length=255,
error_messages={
'max_length': '任务URL长度不得超过255个字符',
}
)
# ----------------------------------------------------------
# 任务备注
# ----------------------------------------------------------
# 字段说明: 任务的说明信息
# 字段类型: 字符型(CharField)
# 验证规则: 可选填(required=False),最大长度255字符
# 业务说明: 存储任务的描述信息,如用途、注意事项等
note = forms.CharField(
required=False,
max_length=255,
error_messages={
'max_length': '任务备注长度不得超过255个字符',
}
)
# =============================================================
# 表单元数据配置
# =============================================================
class Meta:
"""
表单元数据配置类
定义表单绑定的模型和需要验证的字段。
"""
# 绑定模型
# 指定该表单对应的数据模型为 Job(定时任务)
model = models.Job
# 指定需要验证的字段
fields = [
'jobName',
'jobAlias',
'jobGroup',
'jobTrigger',
'status',
'cronExpression',
'executePolicy',
'isSync',
'url',
'note'
]
@@ -0,0 +1,43 @@
# Generated by Django 6.0.3 on 2026-09-04 11:44
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Job',
fields=[
('job_name', models.CharField(db_comment='任务名称', db_index=True, help_text='任务名称', max_length=255, verbose_name='任务名称')),
('job_alias', models.CharField(blank=True, db_comment='任务别名', help_text='任务别名', max_length=255, null=True, verbose_name='任务别名')),
('job_group', models.CharField(blank=True, db_comment='任务分组', db_default='DEFAULT', default='DEFAULT', help_text='任务分组', max_length=255, null=True, verbose_name='任务分组')),
('job_trigger', models.CharField(blank=True, db_comment='任务触发器', help_text='任务触发器', max_length=255, null=True, verbose_name='任务触发器')),
('status', models.PositiveIntegerField(choices=[(0, '未发布'), (1, '运行中'), (2, '暂停'), (3, '删除')], db_comment='任务状态:0-未发布 1-运行中 2-暂停 3-删除', db_default=0, default=0, help_text='任务状态:0-未发布 1-运行中 2-暂停 3-删除', verbose_name='任务状态:0-未发布 1-运行中 2-暂停 3-删除')),
('cron_expression', models.CharField(blank=True, db_comment='Cron表达式', help_text='Cron表达式', max_length=255, null=True, verbose_name='Cron表达式')),
('execute_policy', models.SmallIntegerField(choices=[(1, '立即执行'), (2, '执行一次'), (3, '放弃执行')], db_comment='执行策略:1-立即执行 2-执行一次 3-放弃执行', db_default=3, default=3, help_text='执行策略:1-立即执行 2-执行一次 3-放弃执行', verbose_name='执行策略:1-立即执行 2-执行一次 3-放弃执行')),
('is_sync', models.PositiveIntegerField(choices=[(0, '异步'), (1, '同步')], db_comment='是否同步任务:0-否 1-是', db_default=0, default=0, help_text='是否同步任务:0-否 1-是', verbose_name='是否同步任务:0-否 1-是')),
('url', models.CharField(blank=True, db_comment='任务URL', help_text='任务URL', max_length=255, null=True, verbose_name='任务URL')),
('params', models.TextField(blank=True, db_comment='任务参数', help_text='任务参数', null=True, verbose_name='任务参数')),
('note', models.CharField(blank=True, db_comment='任务备注', help_text='任务备注', max_length=255, null=True, verbose_name='任务备注')),
('id', models.AutoField(auto_created=True, db_comment='主键ID', primary_key=True, serialize=False, verbose_name='主键ID')),
('create_user', models.CharField(db_comment='创建人', db_default=None, default=None, max_length=50, null=True, verbose_name='创建人')),
('create_time', models.DateTimeField(auto_now_add=True, db_comment='创建时间', max_length=11, null=True, verbose_name='创建时间')),
('update_user', models.CharField(db_comment='更新人', db_default=None, default=None, max_length=50, null=True, verbose_name='更新人')),
('update_time', models.DateTimeField(auto_now=True, db_comment='更新时间', max_length=11, null=True, verbose_name='更新时间')),
('is_delete', models.BooleanField(db_comment='逻辑删除:0-正常 1-已删除', db_default=0, default=0, verbose_name='逻辑删除')),
],
options={
'verbose_name': '定时任务表',
'verbose_name_plural': '定时任务表',
'db_table': 'django_job',
'db_table_comment': '任务名称',
'ordering': ('-create_time',),
},
),
]
+338
View File
@@ -0,0 +1,338 @@
# +======================================================================
# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ]
# +----------------------------------------------------------------------
# | Copyright (c) 2021-2026 小蚂蚁云团队 版权所有
# +----------------------------------------------------------------------
# | 开源协议: Apache-2.0
# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。
# +----------------------------------------------------------------------
# | 官方网站: https://www.djangoadmin.cn
# +----------------------------------------------------------------------
# | 研发团队: 小蚂蚁云团队 荣誉出品
# +======================================================================
# | 版权和免责声明:
# | 一、知识产权声明
# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、
# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权,
# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。
# | 二、使用限制与禁止条款
# | 您理解并同意,禁止将本产品用于以下任何行为:
# | 1. 任何违反中华人民共和国法律法规的用途;
# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为;
# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。
# | 三、责任豁免
# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失
# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三
# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。
# | 四、违约与法律责任
# | 如发现任何违反本声明的行为,本团队有权:
# | 1. 无条件配合公安机关及其他有权机关的调查取证工作;
# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失;
# | 3. 保留进一步采取刑事控告等法律措施的权利。
# | 五、许可范围说明
# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包
# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通
# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、
# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造
# | 成的一切经济损失及商誉损害。
# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件
# | 许可协议》附件为准。
# +======================================================================
"""
定时任务模型模块
=============================================================
该模块定义了定时任务相关的数据库模型。
用于存储系统中的定时任务信息,支持任务的创建、调度和执行。
"""
from django.db import models
from application.models import BaseModel
from utils.common import get_table_name
# =============================================================
# 定时任务模型类
# =============================================================
class Job(BaseModel):
"""
定时任务模型类
用于存储系统中的定时任务信息,每个任务代表一个可调度的作业。
任务支持 Cron 表达式,可以设置执行策略和状态管理。
继承自 BaseModel,自动包含:
- id: 主键
- is_delete: 逻辑删除标识
- create_user: 创建人
- create_time: 创建时间
- update_user: 更新人
- update_time: 更新时间
主要业务字段:
- job_name: 任务名称
- job_alias: 任务别名
- job_group: 任务分组
- job_trigger: 任务触发器
- status: 任务状态(0-未发布, 1-运行中, 2-暂停, 3-删除)
- cron_expression: Cron表达式
- execute_policy: 执行策略(1-立即执行, 2-执行一次, 3-放弃执行)
- is_sync: 是否同步执行(0-异步, 1-同步)
- url: 任务URL
- note: 任务备注
"""
# ----------------------------------------------------------
# 状态常量定义
# ----------------------------------------------------------
STATUS_CHOICES = (
(0, "未发布"), # 未发布状态:任务未启用
(1, "运行中"), # 运行中状态:任务正常调度
(2, "暂停"), # 暂停状态:任务暂停执行
(3, "删除"), # 删除状态:任务已删除
)
# ----------------------------------------------------------
# 执行策略常量定义
# ----------------------------------------------------------
POLICY_CHOICES = (
(1, "立即执行"), # 立即执行:任务触发后立即执行
(2, "执行一次"), # 执行一次:任务只执行一次
(3, "放弃执行"), # 放弃执行:任务触发后放弃执行
)
# ----------------------------------------------------------
# 同步标识常量定义
# ----------------------------------------------------------
SYNC_CHOICES = (
(0, "异步"), # 异步执行:不等待执行结果
(1, "同步"), # 同步执行:等待执行结果
)
# ----------------------------------------------------------
# 任务名称
# ----------------------------------------------------------
# 字段说明: 任务的显示名称
# 字段类型: 字符串(CharField)
# 约束: 非空(null=False),最大长度255字符
# 索引: 添加了数据库索引(db_index=True)以支持名称搜索
# 作用: 用于前端展示和日志记录
job_name = models.CharField(
max_length=255,
null=False,
db_index=True,
verbose_name="任务名称",
help_text="任务名称",
db_comment='任务名称' # 数据库字段注释
)
# ----------------------------------------------------------
# 任务别名
# ----------------------------------------------------------
# 字段说明: 任务的别名
# 字段类型: 字符串(CharField)
# 约束: 允许为空(null=True, blank=True),最大长度255字符
# 作用: 用于展示更友好的任务名称
job_alias = models.CharField(
max_length=255,
null=True,
blank=True,
verbose_name="任务别名",
help_text="任务别名",
db_comment='任务别名' # 数据库字段注释
)
# ----------------------------------------------------------
# 任务分组
# ----------------------------------------------------------
# 字段说明: 任务所属分组
# 字段类型: 字符串(CharField)
# 约束: 允许为空(null=True, blank=True),最大长度255字符
# 默认值: "DEFAULT"
# 作用: 对任务进行分类管理,便于批量操作
job_group = models.CharField(
max_length=255,
null=True,
blank=True,
verbose_name="任务分组",
help_text="任务分组",
default="DEFAULT",
db_comment='任务分组', # 数据库字段注释
db_default="DEFAULT"
)
# ----------------------------------------------------------
# 任务触发器
# ----------------------------------------------------------
# 字段说明: 任务触发器标识
# 字段类型: 字符串(CharField)
# 约束: 允许为空(null=True, blank=True),最大长度255字符
# 作用: 映射到具体的执行方法,用于内部方法调用
job_trigger = models.CharField(
max_length=255,
null=True,
blank=True,
verbose_name="任务触发器",
help_text="任务触发器",
db_comment='任务触发器' # 数据库字段注释
)
# ----------------------------------------------------------
# 任务状态
# ----------------------------------------------------------
# 字段说明: 任务的运行状态
# 字段类型: 正整数(PositiveIntegerField)
# 约束: 使用 STATUS_CHOICES 限制可选值,默认值为0(未发布)
# 可选值: 0-未发布, 1-运行中, 2-暂停, 3-删除
# 作用: 控制任务是否在调度器中运行
status = models.PositiveIntegerField(
choices=STATUS_CHOICES,
default=0,
verbose_name="任务状态:0-未发布 1-运行中 2-暂停 3-删除",
help_text="任务状态:0-未发布 1-运行中 2-暂停 3-删除",
db_comment='任务状态:0-未发布 1-运行中 2-暂停 3-删除', # 数据库字段注释
db_default=0
)
# ----------------------------------------------------------
# Cron表达式
# ----------------------------------------------------------
# 字段说明: 任务的调度规则
# 字段类型: 字符串(CharField)
# 约束: 允许为空(null=True, blank=True),最大长度255字符
# 示例: "0 2 * * *" 表示每天凌晨2点执行
# 作用: 定义任务的执行时间规则
cron_expression = models.CharField(
max_length=255,
null=True,
blank=True,
verbose_name="Cron表达式",
help_text="Cron表达式",
db_comment='Cron表达式' # 数据库字段注释
)
# ----------------------------------------------------------
# 执行策略
# ----------------------------------------------------------
# 字段说明: 任务触发后的执行策略
# 字段类型: 小整数(SmallIntegerField)
# 约束: 使用 POLICY_CHOICES 限制可选值,默认值为3(放弃执行)
# 可选值: 1-立即执行, 2-执行一次, 3-放弃执行
# 作用: 控制任务在触发后的具体执行行为
execute_policy = models.SmallIntegerField(
choices=POLICY_CHOICES,
default=3,
verbose_name="执行策略:1-立即执行 2-执行一次 3-放弃执行",
help_text="执行策略:1-立即执行 2-执行一次 3-放弃执行",
db_comment='执行策略:1-立即执行 2-执行一次 3-放弃执行', # 数据库字段注释
db_default=3
)
# ----------------------------------------------------------
# 是否同步执行
# ----------------------------------------------------------
# 字段说明: 任务是否同步执行
# 字段类型: 正整数(PositiveIntegerField)
# 约束: 使用 SYNC_CHOICES 限制可选值,默认值为0(异步)
# 可选值: 0-异步, 1-同步
# 作用: 控制任务执行时是否等待结果返回
is_sync = models.PositiveIntegerField(
choices=SYNC_CHOICES,
default=0,
verbose_name="是否同步任务:0-否 1-是",
help_text="是否同步任务:0-否 1-是",
db_comment='是否同步任务:0-否 1-是', # 数据库字段注释
db_default=0
)
# ----------------------------------------------------------
# 任务URL
# ----------------------------------------------------------
# 字段说明: 任务调用的URL地址
# 字段类型: 字符串(CharField)
# 约束: 允许为空(null=True, blank=True),最大长度255字符
# 作用: 当任务通过HTTP方式执行时,调用此URL
url = models.CharField(
max_length=255,
null=True,
blank=True,
verbose_name="任务URL",
help_text="任务URL",
db_comment='任务URL' # 数据库字段注释
)
# ----------------------------------------------------------
# 任务参数
# ----------------------------------------------------------
# 字段说明: 定时任务的执行参数
# 字段类型: 长文本(TextField)
# 约束: 允许为空(null=True, blank=True)
# 存储: JSON格式字符串
# 作用: 为任务执行提供动态配置参数
params = models.TextField(
null=True,
blank=True,
verbose_name="任务参数",
help_text="任务参数",
db_comment='任务参数'
)
# ----------------------------------------------------------
# 任务备注
# ----------------------------------------------------------
# 字段说明: 任务的说明信息
# 字段类型: 字符串(CharField)
# 约束: 允许为空(null=True, blank=True),最大长度255字符
# 作用: 存储任务的描述信息,如用途、注意事项等
note = models.CharField(
max_length=255,
null=True,
blank=True,
verbose_name="任务备注",
help_text="任务备注",
db_comment='任务备注' # 数据库字段注释
)
# =============================================================
# 模型元数据配置
# =============================================================
class Meta:
"""
模型元数据配置类
定义模型在数据库中的表名、显示名称、默认排序规则等。
"""
# 数据表名
# 使用配置文件中的表前缀 + "job",避免与其他应用表名冲突
db_table = get_table_name('job')
# 数据表注释
db_table_comment = "任务名称"
# 模型在admin后台显示的友好名称(单数)
verbose_name = "定时任务表"
# 模型在admin后台显示的友好名称(复数)
verbose_name_plural = verbose_name
# 默认排序规则
# 按照创建时间倒序排列,最新的在前
ordering = ("-create_time",)
# =============================================================
# 实例方法
# =============================================================
def __str__(self):
"""
模型的字符串表示方法
当需要将模型实例转换为字符串时调用,如admin后台显示、日志输出等。
Returns:
str: 包含任务名称的字符串
"""
return f"任务{self.job_name}"
+752
View File
@@ -0,0 +1,752 @@
# +======================================================================
# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ]
# +----------------------------------------------------------------------
# | Copyright (c) 2021-2026 小蚂蚁云团队 版权所有
# +----------------------------------------------------------------------
# | 开源协议: Apache-2.0
# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。
# +----------------------------------------------------------------------
# | 官方网站: https://www.djangoadmin.cn
# +----------------------------------------------------------------------
# | 研发团队: 小蚂蚁云团队 荣誉出品
# +======================================================================
# | 版权和免责声明:
# | 一、知识产权声明
# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、
# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权,
# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。
# | 二、使用限制与禁止条款
# | 您理解并同意,禁止将本产品用于以下任何行为:
# | 1. 任何违反中华人民共和国法律法规的用途;
# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为;
# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。
# | 三、责任豁免
# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失
# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三
# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。
# | 四、违约与法律责任
# | 如发现任何违反本声明的行为,本团队有权:
# | 1. 无条件配合公安机关及其他有权机关的调查取证工作;
# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失;
# | 3. 保留进一步采取刑事控告等法律措施的权利。
# | 五、许可范围说明
# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包
# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通
# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、
# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造
# | 成的一切经济损失及商誉损害。
# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件
# | 许可协议》附件为准。
# +======================================================================
"""
定时任务调度核心模块
=============================================================
该模块实现了定时任务的调度核心功能。
使用 APScheduler 库来管理和执行定时任务。
主要功能:
1. 任务调度管理(添加、移除、暂停、恢复)
2. URL请求发送(支持同步/异步)
3. 触发器方法映射
4. 任务立即执行
5. 调度器初始化
"""
import logging
from datetime import datetime
from typing import Dict, Any, Optional, Union
import requests
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from apscheduler.jobstores.base import JobLookupError
from django.conf import settings
from django.utils import timezone
import threading
_thread_local = threading.local()
def _get_start_time():
"""获取当前线程的开始时间"""
return getattr(_thread_local, 'job_start_time', None)
def _set_start_time(time):
"""设置当前线程的开始时间"""
_thread_local.job_start_time = time
# 延迟初始化调度器
_scheduler = None
def get_scheduler():
"""获取调度器实例(延迟初始化)"""
global _scheduler
if _scheduler is None:
from django.conf import settings
_scheduler = BackgroundScheduler(timezone=settings.TIME_ZONE)
return _scheduler
class _SchedulerProxy:
"""调度器代理类,延迟获取实际调度器"""
def __getattr__(self, name):
return getattr(get_scheduler(), name)
def __setattr__(self, name, value):
return setattr(get_scheduler(), name, value)
scheduler = _SchedulerProxy()
# =============================================================
# URL构建辅助函数
# =============================================================
def build_full_url(url: str) -> str:
"""
构建完整的URL
将相对路径转换为完整URL,如果已经是完整URL则直接返回。
Args:
url: 原始URL(可能是相对路径或完整URL)
Returns:
str: 完整URL
Example:
>>> build_full_url('/api/job/test/sms')
'http://localhost:8000/api/job/test/sms'
>>> build_full_url('https://api.example.com/webhook')
'https://api.example.com/webhook'
"""
if not url:
return url
# 如果已经是完整URL(包含http://或https://),直接返回
if url.startswith(('http://', 'https://')):
return url
# 如果是相对路径,加上域名
if url.startswith('/'):
domain = getattr(settings, 'SITE_DOMAIN', 'http://localhost:8000')
return f"{domain}{url}"
return url
# =============================================================
# 任务执行核心函数
# =============================================================
def execute_job(
job_id: int,
url: Optional[str] = None,
is_sync: int = 0,
job_trigger: Optional[str] = None,
params: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
执行任务(统一入口)
支持两种执行方式:
1. URL方式:发送HTTP请求到指定URL
2. 触发器方式:调用内部注册的方法
Args:
job_id: 任务ID
url: 任务URL(可以是相对路径或完整URL)
is_sync: 是否同步执行(1-同步,0-异步)
job_trigger: 任务触发器(内部方法标识)
params: 额外参数
Returns:
Dict[str, Any]: 执行结果,包含success状态和具体数据
"""
try:
# 记录开始时间 - 使用线程局部存储
start_time = timezone.now()
_set_start_time(start_time)
logging.info(f"执行定时任务 [ID: {job_id}, URL: {url}, Trigger: {job_trigger}, Sync: {is_sync}]")
result = None
# 方式1:URL方式(优先级高)
if url:
result = _execute_by_url(job_id, url, is_sync, params)
# 方式2:触发器方式
elif job_trigger:
result = _execute_by_trigger(job_trigger, job_id, params)
else:
logging.warning(f"任务既没有URL也没有触发器,无法执行 [ID: {job_id}]")
result = {
'success': False,
'error': '任务既没有URL也没有触发器',
'job_id': job_id
}
# 记录执行日志
status = 'success' if result.get('success', False) else 'failed'
error_msg = result.get('error', '') if status == 'failed' else ''
status_code = result.get('status_code') if url else None
if is_sync == 1 or not url:
_record_execution_log(job_id, status_code, status, error_msg)
return result
except Exception as e:
logging.error(f"执行任务异常 [ID: {job_id}, 错误: {str(e)}]", exc_info=True)
_record_execution_log(job_id, None, 'failed', str(e))
return {
'success': False,
'error': str(e),
'job_id': job_id
}
finally:
_set_start_time(None)
def _execute_by_url(
job_id: int,
url: str,
is_sync: int,
params: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
通过URL方式执行任务
Args:
job_id: 任务ID
url: 任务URL
is_sync: 是否同步执行
params: 额外参数
Returns:
Dict[str, Any]: 执行结果
"""
try:
full_url = build_full_url(url)
logging.info(f"完整URL: {full_url}")
request_data = {
'job_id': job_id,
'timestamp': datetime.now().isoformat(),
'params': params or {}
}
if is_sync == 1:
return _send_sync_request(full_url, request_data, job_id)
else:
return _send_async_request(full_url, request_data, job_id)
except Exception as e:
logging.error(f"URL执行失败 [ID: {job_id}, URL: {url}, 错误: {str(e)}]")
return {
'success': False,
'error': str(e),
'job_id': job_id,
'method': 'url'
}
def _send_sync_request(url: str, data: Dict[str, Any], job_id: int) -> Dict[str, Any]:
"""
发送同步HTTP请求
Args:
url: 请求URL
data: 请求数据
job_id: 任务ID
Returns:
Dict[str, Any]: 请求结果
"""
try:
response = requests.post(
url,
json=data,
timeout=30,
headers={'Content-Type': 'application/json'}
)
response.raise_for_status()
try:
result_data = response.json()
except:
result_data = {'text': response.text[:200]}
logging.info(f"同步任务执行完成 [ID: {job_id}, 状态码: {response.status_code}]")
return {
'success': True,
'method': 'url_sync',
'status_code': response.status_code,
'data': result_data,
'job_id': job_id
}
except requests.exceptions.MissingSchema as e:
error_msg = f"URL格式错误: {url}"
logging.error(error_msg)
return {'success': False, 'error': error_msg, 'job_id': job_id}
except requests.exceptions.ConnectionError as e:
error_msg = f"连接失败: {url}"
logging.error(error_msg)
return {'success': False, 'error': error_msg, 'job_id': job_id}
except requests.exceptions.Timeout as e:
error_msg = "请求超时"
logging.error(error_msg)
return {'success': False, 'error': error_msg, 'job_id': job_id}
except requests.exceptions.HTTPError as e:
error_msg = f"HTTP错误: {e.response.status_code if e.response else '未知'}"
logging.error(error_msg)
return {'success': False, 'error': error_msg, 'job_id': job_id}
def _send_async_request(url: str, data: Dict[str, Any], job_id: int) -> Dict[str, Any]:
"""
发送异步HTTP请求(使用线程池)
Args:
url: 请求URL
data: 请求数据
job_id: 任务ID
Returns:
Dict[str, Any]: 触发结果
"""
from concurrent.futures import ThreadPoolExecutor
executor = getattr(settings, 'JOB_THREAD_POOL', ThreadPoolExecutor(max_workers=10))
def async_request():
"""异步请求任务"""
start_time = timezone.now()
_set_start_time(start_time)
try:
resp = requests.post(
url,
json=data,
timeout=30,
headers={'Content-Type': 'application/json'}
)
logging.info(f"异步任务执行完成 [ID: {job_id}, 状态码: {resp.status_code}]")
_record_execution_log(job_id, resp.status_code, 'success')
except Exception as e:
logging.error(f"异步任务执行异常 [ID: {job_id}, 错误: {str(e)}]")
_record_execution_log(job_id, None, 'failed', str(e))
finally:
_set_start_time(None)
executor.submit(async_request)
logging.info(f"异步任务已触发 [ID: {job_id}]")
return {
'success': True,
'method': 'url_async',
'status': 'triggered',
'job_id': job_id
}
def _record_execution_log(job_id: int, status_code: Optional[int], status: str, error: str = ''):
"""
记录任务执行日志
将任务的执行情况记录到数据库中的 JobLog 表。
Args:
job_id: 任务ID
status_code: HTTP状态码(仅对HTTP请求有效)
status: 执行状态('success' 或 'failed')
error: 错误信息(失败时记录)
"""
try:
from django.utils import timezone
from application.job.models import Job
from application.job_log.models import JobLog
end_time = timezone.now()
job = Job.objects.filter(id=job_id, is_delete=0).first()
if not job:
logging.error(f"记录日志失败:任务不存在 [ID: {job_id}]")
return
start_time = _get_start_time()
consume_time = 0
if start_time and isinstance(start_time, datetime):
consume_time = int((end_time - start_time).total_seconds() * 1000)
job_message = f"任务执行{'成功' if status == 'success' else '失败'}"
if status_code:
job_message += f",HTTP状态码: {status_code}"
job_log = JobLog.objects.create(
job_id=job_id,
job_name=job.job_name,
job_group=job.job_group,
job_trigger=job.job_trigger,
cron_expression=job.cron_expression,
status=0 if status == 'success' else 1,
job_message=job_message,
start_time=start_time,
end_time=end_time,
consume_time=consume_time,
exception_info=error if error else None,
create_user='system',
update_user='system',
)
logging.info(f"任务执行日志已记录 [ID: {job_log.id}, 任务ID: {job_id}, 状态: {status}]")
except ImportError as e:
logging.error(f"记录执行日志失败:模型导入错误 {str(e)}")
except Exception as e:
logging.error(f"记录执行日志异常: {str(e)}", exc_info=True)
def _execute_by_trigger(trigger: str, job_id: int, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""
通过触发器方式执行任务
根据 trigger 标识调用对应的内部方法。
Args:
trigger: 触发器标识
job_id: 任务ID
params: 执行参数
Returns:
Dict[str, Any]: 执行结果
"""
try:
logging.info(f"通过触发器执行任务 [ID: {job_id}, Trigger: {trigger}]")
from application.job.executor import job_executor
result = job_executor.execute(trigger, job_id=job_id, **(params or {}))
return {
'success': True,
'method': 'trigger',
'trigger': trigger,
'data': result,
'job_id': job_id
}
except ImportError:
error_msg = "任务执行器未实现"
logging.error(error_msg)
return {'success': False, 'error': error_msg, 'job_id': job_id}
except KeyError as e:
error_msg = f"未找到触发器对应的方法: {trigger}"
logging.error(error_msg)
return {'success': False, 'error': error_msg, 'job_id': job_id}
except Exception as e:
error_msg = f"触发器执行失败: {str(e)}"
logging.error(error_msg, exc_info=True)
return {'success': False, 'error': error_msg, 'job_id': job_id}
# =============================================================
# 调度器管理函数
# =============================================================
def add_job_to_scheduler(job_id: int) -> bool:
"""
添加任务到调度器
Args:
job_id: 任务ID
Returns:
bool: 是否添加成功
"""
try:
from application.job.models import Job
job = Job.objects.filter(id=job_id, is_delete=0).first()
if not job:
logging.warning(f"任务不存在 [ID: {job_id}]")
return False
if job.status != 1:
logging.info(f"任务状态不可用,跳过调度 [ID: {job.id}, 状态: {job.status}]")
return False
if not job.cron_expression:
logging.warning(f"任务Cron表达式为空 [ID: {job.id}]")
return False
scheduler_job_id = f"job_{job.id}"
try:
scheduler.remove_job(scheduler_job_id)
logging.info(f"已移除现有任务 [ID: {job.id}]")
except JobLookupError:
pass
if job.url:
func = execute_job
args = [job.id, job.url, job.is_sync, job.job_trigger, {}]
elif job.job_trigger:
func = execute_job
args = [job.id, None, job.is_sync, job.job_trigger, {}]
else:
logging.warning(f"任务既没有URL也没有触发器 [ID: {job.id}]")
return False
scheduler.add_job(
func=func,
trigger=CronTrigger.from_crontab(job.cron_expression),
args=args,
id=scheduler_job_id,
name=job.job_name,
replace_existing=True,
misfire_grace_time=30,
coalesce=True
)
logging.info(f"任务已添加到调度器 [ID: {job.id}, 名称: {job.job_name}, Cron: {job.cron_expression}]")
return True
except Exception as e:
logging.error(f"添加任务到调度器异常 [ID: {job_id}, 错误: {str(e)}]", exc_info=True)
return False
def remove_job_from_scheduler(job_id: int) -> bool:
"""
从调度器移除任务
Args:
job_id: 任务ID
Returns:
bool: 是否移除成功
"""
try:
scheduler_job_id = f"job_{job_id}"
scheduler.remove_job(scheduler_job_id)
logging.info(f"任务已从调度器移除 [ID: {job_id}]")
return True
except JobLookupError:
logging.info(f"任务不存在于调度器中 [ID: {job_id}]")
return False
except Exception as e:
logging.error(f"从调度器移除任务异常 [ID: {job_id}, 错误: {str(e)}]")
return False
def pause_job_in_scheduler(job_id: int) -> bool:
"""
暂停调度器中的任务
Args:
job_id: 任务ID
Returns:
bool: 是否暂停成功
"""
try:
scheduler_job_id = f"job_{job_id}"
scheduler.pause_job(scheduler_job_id)
logging.info(f"任务已暂停 [ID: {job_id}]")
return True
except JobLookupError:
logging.warning(f"任务不存在于调度器中 [ID: {job_id}]")
return False
except Exception as e:
logging.error(f"暂停任务异常 [ID: {job_id}, 错误: {str(e)}]")
return False
def resume_job_in_scheduler(job_id: int) -> bool:
"""
恢复调度器中的任务
Args:
job_id: 任务ID
Returns:
bool: 是否恢复成功
"""
try:
scheduler_job_id = f"job_{job_id}"
scheduler.resume_job(scheduler_job_id)
logging.info(f"任务已恢复 [ID: {job_id}]")
return True
except JobLookupError:
logging.warning(f"任务不存在于调度器中 [ID: {job_id}]")
return False
except Exception as e:
logging.error(f"恢复任务异常 [ID: {job_id}, 错误: {str(e)}]")
return False
# =============================================================
# 任务立即执行函数
# =============================================================
def run_job_once(job_id: int, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""
立即执行一次任务(不依赖调度器)
Args:
job_id: 任务ID
params: 执行参数(可选)
Returns:
Dict[str, Any]: 执行结果
"""
try:
from application.job.models import Job
job = Job.objects.filter(id=job_id, is_delete=0).first()
if not job:
error_msg = f"任务不存在 [ID: {job_id}]"
logging.error(error_msg)
return {'success': False, 'error': error_msg}
logging.info(f"立即执行任务 [ID: {job_id}, 名称: {job.job_name}]")
result = execute_job(
job_id=job.id,
url=job.url,
is_sync=1,
job_trigger=job.job_trigger,
params=params or {}
)
return result
except Exception as e:
logging.error(f"立即执行任务异常 [ID: {job_id}, 错误: {str(e)}]", exc_info=True)
return {'success': False, 'error': str(e)}
# =============================================================
# 调度器初始化和关闭函数
# =============================================================
def init_scheduler() -> int:
"""
初始化调度器,加载所有运行中的任务
在 Django 启动时调用,加载所有状态为运行中的任务到调度器。
Returns:
int: 成功加载的任务数量
"""
try:
from application.job.models import Job
if not scheduler.running:
scheduler.start()
logging.info("调度器已启动")
jobs = Job.objects.filter(
status=1,
is_delete=0
).exclude(
cron_expression__isnull=True
).exclude(
cron_expression=''
)
logging.info(f"找到 {len(jobs)} 个待加载的任务")
success_count = 0
for job in jobs:
if add_job_to_scheduler(job.id):
success_count += 1
logging.info(f"调度器初始化完成,成功加载 {success_count}/{len(jobs)} 个任务")
_print_scheduler_status()
return success_count
except Exception as e:
logging.error(f"初始化调度器异常: {str(e)}", exc_info=True)
return 0
def _print_scheduler_status():
"""打印调度器当前状态(用于调试)"""
try:
jobs = scheduler.get_jobs()
logging.info(f"调度器当前状态: 运行中={scheduler.running}, 任务数={len(jobs)}")
for job in jobs:
next_run = job.next_run_time
next_run_str = next_run.strftime('%Y-%m-%d %H:%M:%S') if next_run else '未知'
logging.info(f" - 任务 [{job.id}] {job.name}, 下次执行: {next_run_str}")
except Exception as e:
logging.error(f"获取调度器状态失败: {str(e)}")
def shutdown_scheduler(wait: bool = True) -> bool:
"""
关闭调度器
Args:
wait: 是否等待正在执行的任务完成
Returns:
bool: 是否成功关闭
"""
try:
if scheduler.running:
scheduler.shutdown(wait=wait)
logging.info(f"调度器已关闭 (wait={wait})")
return True
except Exception as e:
logging.error(f"关闭调度器异常: {str(e)}")
return False
def reload_scheduler() -> int:
"""
重新加载调度器(清除所有任务后重新加载)
Returns:
int: 成功加载的任务数量
"""
try:
scheduler.remove_all_jobs()
logging.info("已移除所有调度任务")
return init_scheduler()
except Exception as e:
logging.error(f"重新加载调度器异常: {str(e)}")
return 0
+918
View File
@@ -0,0 +1,918 @@
# +======================================================================
# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ]
# +----------------------------------------------------------------------
# | Copyright (c) 2021-2026 小蚂蚁云团队 版权所有
# +----------------------------------------------------------------------
# | 开源协议: Apache-2.0
# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。
# +----------------------------------------------------------------------
# | 官方网站: https://www.djangoadmin.cn
# +----------------------------------------------------------------------
# | 研发团队: 小蚂蚁云团队 荣誉出品
# +======================================================================
# | 版权和免责声明:
# | 一、知识产权声明
# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、
# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权,
# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。
# | 二、使用限制与禁止条款
# | 您理解并同意,禁止将本产品用于以下任何行为:
# | 1. 任何违反中华人民共和国法律法规的用途;
# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为;
# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。
# | 三、责任豁免
# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失
# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三
# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。
# | 四、违约与法律责任
# | 如发现任何违反本声明的行为,本团队有权:
# | 1. 无条件配合公安机关及其他有权机关的调查取证工作;
# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失;
# | 3. 保留进一步采取刑事控告等法律措施的权利。
# | 五、许可范围说明
# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包
# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通
# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、
# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造
# | 成的一切经济损失及商誉损害。
# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件
# | 许可协议》附件为准。
# +======================================================================
"""
定时任务管理服务模块
=============================================================
该模块实现了定时任务管理相关的所有业务逻辑。
包括任务的分页查询、详情查询、添加、更新、删除、状态设置等功能。
主要功能:
1. 分页查询:支持按任务名称、状态等条件筛选
2. 详情查询:根据ID获取任务详细信息
3. 任务管理:添加、更新、删除(支持批量)
4. 状态管理:启用、暂停、恢复任务
5. 立即执行:手动触发任务执行
6. 调度器同步:自动同步任务状态到调度器
"""
import logging
from typing import Dict, Any, Optional, List, Tuple
from django.core.paginator import Paginator, PageNotAnInteger, InvalidPage, EmptyPage
from django.db.models import QuerySet
from django.http import HttpRequest
from application.job import forms
from application.job.models import Job
from application.job.scheduler import (
add_job_to_scheduler,
remove_job_from_scheduler,
pause_job_in_scheduler,
resume_job_in_scheduler,
run_job_once as scheduler_run_job_once,
reload_scheduler
)
from constant.constants import PAGE_SIZE
from utils import R, regular
from utils.common import format_datetime, parse_request_body
from utils.security import get_username
# 配置日志器
logger = logging.getLogger(__name__)
# =============================================================
# 分页查询服务
# =============================================================
def get_job_page(request: HttpRequest) -> R:
"""
查询定时任务分页数据
支持按任务名称模糊搜索、状态筛选,按创建时间倒序排列。
Args:
request: HttpRequest对象,包含查询参数
- pageNo: 当前页码(可选,默认为1)
- pageSize: 每页记录数(可选,默认为PAGE_SIZE)
- jobName: 任务名称(可选,模糊查询)
- status: 任务状态(可选,精确匹配)
Returns:
R: 包含分页数据的JSON响应,格式为:
{
"records": 任务列表,
"total": 总记录数,
"size": 每页大小,
"current": 当前页码,
"pages": 总页数
}
"""
try:
# 获取分页参数
page_no = int(request.GET.get('pageNo', 1))
page_size = int(request.GET.get('pageSize', PAGE_SIZE))
# 参数校验
if page_no < 1:
page_no = 1
if page_size < 1 or page_size > 100:
page_size = PAGE_SIZE
# 构建基础查询(只查询未删除的记录)
query = Job.objects.filter(is_delete=0)
# 应用筛选条件
query = _apply_filters(query, request)
# 排序(按创建时间倒序)
query = query.order_by("-create_time")
# 分页查询
paginator = Paginator(query, page_size)
try:
page_list = paginator.page(page_no)
except (PageNotAnInteger, InvalidPage, EmptyPage):
# 页码异常时返回第一页
page_list = paginator.page(1)
page_no = 1
# 构建返回数据
page_data = _build_page_data(page_list, paginator, page_no, page_size)
logger.info(f"查询任务分页成功,共{len(page_list)}条记录,总记录数:{paginator.count}")
return R.ok(data=page_data)
except ValueError as e:
logger.error(f"分页参数格式错误: {str(e)}")
return R.failed(msg="请求参数格式错误")
except Exception as e:
logger.error(f"查询定时任务分页数据异常: {str(e)}", exc_info=True)
return R.failed(msg="查询失败,请稍后重试")
def _apply_filters(queryset: QuerySet, request: HttpRequest) -> QuerySet:
"""
应用查询筛选条件
Args:
queryset: 初始查询集
request: HttpRequest对象
Returns:
QuerySet: 应用筛选后的查询集
"""
# 任务名称模糊筛选(使用icontains实现不区分大小写)
job_name = request.GET.get('jobName')
if job_name:
queryset = queryset.filter(job_name__icontains=job_name)
# 任务状态筛选
status = request.GET.get('status')
if status and status.isdigit():
status_val = int(status)
# 验证状态值是否有效
if status_val in dict(Job.STATUS_CHOICES):
queryset = queryset.filter(status=status_val)
# 任务分组筛选
job_group = request.GET.get('jobGroup')
if job_group:
queryset = queryset.filter(job_group=job_group)
# 是否同步筛选
is_sync = request.GET.get('isSync')
if is_sync and is_sync.isdigit():
queryset = queryset.filter(is_sync=int(is_sync))
return queryset
def _build_page_data(page_list, paginator, page_no: int, page_size: int) -> Dict[str, Any]:
"""
构建分页返回数据
Args:
page_list: 当前页数据列表
paginator: 分页器对象
page_no: 当前页码
page_size: 每页大小
Returns:
dict: 分页数据,包含records、total、size、current、pages字段
"""
records = []
for item in page_list:
# 获取执行策略文本
policy_text = dict(Job.POLICY_CHOICES).get(item.execute_policy, '')
# 获取状态文本
status_text = dict(Job.STATUS_CHOICES).get(item.status, '')
records.append({
'id': item.id,
'jobName': item.job_name,
'jobAlias': item.job_alias,
'jobGroup': item.job_group,
'jobTrigger': item.job_trigger,
'status': item.status,
'statusText': status_text,
'cronExpression': item.cron_expression,
'executePolicy': item.execute_policy,
'executePolicyText': policy_text,
'isSync': item.is_sync,
'url': item.url,
'note': item.note,
'createUser': item.create_user,
'createTime': format_datetime(item.create_time),
'updateUser': item.update_user,
'updateTime': format_datetime(item.update_time),
})
return {
'records': records,
'total': paginator.count,
'size': page_size,
'current': page_no,
'pages': paginator.num_pages
}
# =============================================================
# 详情查询服务
# =============================================================
def get_job_detail(job_id: int) -> Optional[Dict[str, Any]]:
"""
根据ID查询定时任务详情
Args:
job_id: 任务ID
Returns:
dict/None: 任务详情或None(任务不存在时返回None)
"""
try:
# 参数校验
if not job_id or job_id <= 0:
logger.warning(f"无效的任务ID: {job_id}")
return None
# 查询任务
job = Job.objects.filter(is_delete=0, id=job_id).first()
if not job:
logger.info(f"任务不存在,ID: {job_id}")
return None
# 构建返回数据
result = {
'id': job.id,
'jobName': job.job_name,
'jobAlias': job.job_alias,
'jobGroup': job.job_group,
'jobTrigger': job.job_trigger,
'status': job.status,
'cronExpression': job.cron_expression,
'executePolicy': job.execute_policy,
'isSync': job.is_sync,
'url': job.url,
'note': job.note,
}
logger.info(f"查询任务详情成功,ID: {job_id}")
return result
except Exception as e:
logger.error(f"查询定时任务详情异常,ID: {job_id}, 错误: {str(e)}", exc_info=True)
return None
# =============================================================
# 添加任务服务
# =============================================================
def add_job(request: HttpRequest) -> R:
"""
添加定时任务
创建新的定时任务记录,如果状态为运行中且Cron表达式有效,
会自动添加到调度器中。
Args:
request: HttpRequest对象,包含任务信息的JSON数据
Returns:
R: 操作结果
- 成功: R.ok(msg="创建成功")
- 失败: R.failed(msg="错误信息")
请求参数示例:
{
"jobName": "数据同步任务", # 任务名称(必填)
"jobAlias": "同步", # 任务别名(可选)
"jobGroup": "DEFAULT", # 任务分组(可选,默认DEFAULT)
"jobTrigger": "sync_user", # 任务触发器(必填)
"status": 1, # 状态:0-未发布, 1-运行中, 2-暂停, 3-已完成
"cronExpression": "0 2 * * *", # Cron表达式(必填)
"executePolicy": 1, # 执行策略:1-GET, 2-POST, 3-PUT, 4-DELETE
"isSync": 1, # 是否同步:0-否, 1-是
"url": "/api/job/test/sms", # 执行URL(必填)
"note": "每天凌晨2点同步用户数据" # 备注(可选)
}
"""
# 解析请求参数
data, error = parse_request_body(request)
if error:
return R.failed(msg=error)
# 表单验证
form = forms.JobForm(data)
if not form.is_valid():
error_msg = regular.get_err(form)
logger.warning(f"添加任务参数验证失败: {error_msg}")
return R.failed(msg=error_msg)
try:
# 获取表单数据
cleaned_data = form.cleaned_data
# 处理URL:如果是相对路径且没有协议,确保以斜杠开头
url = cleaned_data.get('url')
if url and not url.startswith(('http://', 'https://', '/')):
url = '/' + url.lstrip('/')
# 创建任务记录
job = Job.objects.create(
job_name=cleaned_data.get('jobName'),
job_alias=cleaned_data.get('jobAlias'),
job_group=cleaned_data.get('jobGroup') or 'DEFAULT',
job_trigger=cleaned_data.get('jobTrigger'),
status=cleaned_data.get('status'),
cron_expression=cleaned_data.get('cronExpression'),
execute_policy=cleaned_data.get('executePolicy'),
is_sync=cleaned_data.get('isSync'),
url=url,
note=cleaned_data.get('note'),
create_user=get_username(request),
update_user=get_username(request),
is_delete=0
)
# 如果状态是运行中且Cron表达式不为空,添加到调度器
if job.status == 1 and job.cron_expression:
success = add_job_to_scheduler(job.id)
if success:
logger.info(f"任务已添加到调度器,ID: {job.id}")
else:
logger.warning(f"任务添加到调度器失败,ID: {job.id}")
logger.info(f"添加任务成功,ID: {job.id}, 名称: {job.job_name}")
return R.ok(msg="创建成功")
except Exception as e:
logger.error(f"添加定时任务异常: {str(e)}", exc_info=True)
return R.failed(msg="添加失败,请稍后重试")
# =============================================================
# 更新任务服务
# =============================================================
def update_job(request: HttpRequest) -> R:
"""
更新定时任务
更新现有任务信息,并根据状态变化自动同步调度器。
Args:
request: HttpRequest对象,包含任务ID和更新信息的JSON数据
Returns:
R: 操作结果
- 成功: R.ok(msg="更新成功")
- 失败: R.failed(msg="错误信息")
请求参数示例:
{
"id": 1, # 任务ID(必填)
"jobName": "更新后的任务名", # 任务名称
"jobAlias": "同步", # 任务别名
"jobGroup": "DEFAULT", # 任务分组
"jobTrigger": "sync_user", # 任务触发器
"status": 2, # 状态:暂停
"cronExpression": "0 3 * * *", # 新的Cron表达式
"executePolicy": 1, # 执行策略
"isSync": 1, # 是否同步
"url": "/api/job/test/sms", # 执行URL
"note": "更新后的备注" # 备注
}
"""
# 解析请求参数
data, error = parse_request_body(request)
if error:
return R.failed(msg=error)
# 验证任务ID
job_id = data.get('id')
if not job_id or not str(job_id).isdigit() or int(job_id) <= 0:
return R.failed("任务ID无效")
job_id = int(job_id)
# 表单验证
form = forms.JobForm(data)
if not form.is_valid():
error_msg = regular.get_err(form)
logger.warning(f"更新任务参数验证失败,ID: {job_id}, 错误: {error_msg}")
return R.failed(msg=error_msg)
try:
# 查询任务
job = Job.objects.filter(id=job_id, is_delete=0).first()
if not job:
logger.warning(f"任务不存在,ID: {job_id}")
return R.failed("任务不存在")
# 记录旧状态,用于后续调度器处理
old_status = job.status
old_cron = job.cron_expression
old_url = job.url
# 获取表单数据
cleaned_data = form.cleaned_data
# 处理URL
url = cleaned_data.get('url')
if url and not url.startswith(('http://', 'https://', '/')):
url = '/' + url.lstrip('/')
# 更新任务信息
job.job_name = cleaned_data.get('jobName')
job.job_alias = cleaned_data.get('jobAlias')
job.job_group = cleaned_data.get('jobGroup')
job.job_trigger = cleaned_data.get('jobTrigger')
job.status = cleaned_data.get('status')
job.cron_expression = cleaned_data.get('cronExpression')
job.execute_policy = cleaned_data.get('executePolicy')
job.is_sync = cleaned_data.get('isSync')
job.url = url
job.note = cleaned_data.get('note')
job.update_user = get_username(request)
job.save()
# 处理调度器中的任务
scheduler_result = _handle_scheduler_after_update(
job, old_status, old_cron, old_url
)
if scheduler_result:
logger.info(f"调度器同步成功,ID: {job_id}")
logger.info(f"更新任务成功,ID: {job_id}")
return R.ok(msg="更新成功")
except Exception as e:
logger.error(f"更新定时任务异常,ID: {job_id}, 错误: {str(e)}", exc_info=True)
return R.failed(msg="更新失败,请稍后重试")
def _handle_scheduler_after_update(job: Job, old_status: int, old_cron: str, old_url: str) -> bool:
"""
处理更新后的调度器逻辑
根据任务状态和Cron表达式的变化,决定如何同步调度器。
Args:
job: 更新后的任务对象
old_status: 旧状态
old_cron: 旧Cron表达式
old_url: 旧URL
Returns:
bool: 是否成功处理
"""
try:
# 情况1:从非运行中变为运行中,且Cron表达式不为空
if old_status != 1 and job.status == 1 and job.cron_expression:
logger.info(f"任务状态从{old_status}变为运行中,添加到调度器")
return add_job_to_scheduler(job.id)
# 情况2:从运行中变为非运行中
elif old_status == 1 and job.status != 1:
logger.info(f"任务从运行中变为状态{job.status},从调度器移除")
return remove_job_from_scheduler(job.id)
# 情况3:保持运行中,但Cron表达式变化
elif job.status == 1 and old_cron != job.cron_expression:
logger.info(f"任务运行中,Cron表达式从{old_cron}变为{job.cron_expression},重新添加到调度器")
if job.cron_expression:
return add_job_to_scheduler(job.id)
else:
return remove_job_from_scheduler(job.id)
# 情况4:保持运行中,但URL变化(不需要重新调度,因为URL只在执行时使用)
elif job.status == 1 and old_url != job.url:
logger.info(f"任务运行中,URL从{old_url}变为{job.url},无需重新调度")
return True
return True
except Exception as e:
logger.error(f"处理调度器更新异常: {str(e)}", exc_info=True)
return False
# =============================================================
# 删除任务服务(支持批量)
# =============================================================
def delete_jobs(job_ids: str) -> R:
"""
删除定时任务(批量删除)
支持单个ID或多个ID(用逗号分隔)的批量删除。
删除前会先从调度器中移除任务。
Args:
job_ids: 任务ID字符串,多个ID用逗号分隔
例如: "1" 或 "1,2,3"
Returns:
R: 操作结果,包含实际删除的记录数
- 成功: R.ok(msg="本次共删除N条数据")
- 失败: R.failed(msg="错误信息")
"""
if not job_ids:
return R.failed("记录ID不存在")
try:
# 解析ID列表
id_list = []
for id_str in job_ids.split(','):
id_str = id_str.strip()
if id_str and id_str.isdigit():
id_list.append(int(id_str))
if not id_list:
return R.failed("无效的记录ID")
logger.info(f"开始删除任务,IDs: {id_list}")
# 先移除调度器中的任务
removed_count = 0
for job_id in id_list:
if remove_job_from_scheduler(job_id):
removed_count += 1
# 批量更新删除标识(软删除)
updated_count = Job.objects.filter(
id__in=id_list,
is_delete=0
).update(is_delete=1)
logger.info(f"删除任务完成,从调度器移除{removed_count}个,数据库删除{updated_count}条")
return R.ok(msg=f"本次共删除{updated_count}条数据")
except ValueError as e:
logger.error(f"记录ID格式错误: {job_ids}, 错误: {str(e)}")
return R.failed("记录ID格式错误")
except Exception as e:
logger.error(f"删除定时任务异常,IDs: {job_ids}, 错误: {str(e)}", exc_info=True)
return R.failed(msg="删除失败,请稍后重试")
# =============================================================
# 状态管理服务
# =============================================================
def status_job(request: HttpRequest) -> R:
"""
设置任务状态(启用/暂停)
更新任务的运行状态,并同步到调度器。
Args:
request: HttpRequest对象,包含任务ID和目标状态
Returns:
R: 操作结果
- 成功: R.ok(msg="已启用/已暂停/已恢复")
- 失败: R.failed(msg="错误信息")
请求参数示例:
{
"id": 1, # 任务ID(必填)
"status": 1 # 目标状态:0-未发布, 1-运行中, 2-暂停, 3-已完成
}
"""
# 解析请求参数
data, error = parse_request_body(request)
if error:
return R.failed(msg=error)
job_id = data.get('id')
target_status = data.get('status')
if not job_id:
return R.failed("任务ID不能为空")
if target_status is None:
return R.failed("目标状态不能为空")
try:
# 验证状态值
target_status = int(target_status)
if target_status not in [0, 1, 2, 3]:
return R.failed("无效的状态值")
# 查询任务
job = Job.objects.filter(id=job_id, is_delete=0).first()
if not job:
logger.warning(f"任务不存在,ID: {job_id}")
return R.failed("任务不存在")
# 记录旧状态
old_status = job.status
# 如果状态相同,无需处理
if old_status == target_status:
return R.ok(msg="状态未变化")
# 更新状态
job.status = target_status
job.save(update_fields=['status', 'update_time'])
# 根据新旧状态处理调度器
result = _handle_status_change(job, old_status, target_status)
status_text = dict(Job.STATUS_CHOICES).get(target_status, '未知')
logger.info(f"设置任务状态成功,ID: {job_id}, 新状态: {status_text}")
return R.ok(msg=f"已{status_text}")
except ValueError as e:
logger.error(f"状态值格式错误: {str(e)}")
return R.failed("状态值格式错误")
except Exception as e:
logger.error(f"设置任务状态异常,ID: {job_id}, 错误: {str(e)}", exc_info=True)
return R.failed(msg="设置失败,请稍后重试")
def _handle_status_change(job: Job, old_status: int, new_status: int) -> bool:
"""
处理状态变更时的调度器操作
Args:
job: 任务对象
old_status: 旧状态
new_status: 新状态
Returns:
bool: 是否成功处理
"""
try:
# 变为运行中
if new_status == 1:
if job.cron_expression:
logger.info(f"任务启用,添加到调度器,ID: {job.id}")
return add_job_to_scheduler(job.id)
else:
logger.warning(f"任务启用但Cron表达式为空,无法添加到调度器,ID: {job.id}")
return False
# 从运行中变为其他状态
elif old_status == 1:
if new_status == 2: # 暂停
logger.info(f"任务暂停,ID: {job.id}")
return pause_job_in_scheduler(job.id)
else: # 未发布、删除等
logger.info(f"任务停止,从调度器移除,ID: {job.id}")
return remove_job_from_scheduler(job.id)
return True
except Exception as e:
logger.error(f"处理状态变更异常: {str(e)}", exc_info=True)
return False
# =============================================================
# 暂停任务服务
# =============================================================
def pause_job(job_id: int) -> R:
"""
暂停任务
将任务状态设置为暂停,并从调度器中暂停执行。
Args:
job_id: 任务ID
Returns:
R: 操作结果
- 成功: R.ok(msg="暂停成功")
- 失败: R.failed(msg="错误信息")
"""
try:
# 参数校验
if not job_id or job_id <= 0:
return R.failed("任务ID无效")
# 查询任务
job = Job.objects.filter(id=job_id, is_delete=0).first()
if not job:
logger.warning(f"任务不存在,ID: {job_id}")
return R.failed("任务不存在")
# 如果已经是暂停状态
if job.status == 2:
return R.ok(msg="任务已处于暂停状态")
# 更新状态
job.status = 2
job.save(update_fields=['status', 'update_time'])
# 暂停调度器中的任务
if pause_job_in_scheduler(job.id):
logger.info(f"暂停任务成功,ID: {job_id}")
return R.ok(msg="暂停成功")
else:
# 调度器暂停失败,但数据库已更新
logger.warning(f"调度器暂停失败,但数据库已更新,ID: {job_id}")
return R.ok(msg="任务已暂停,但调度器同步失败")
except Exception as e:
logger.error(f"暂停任务异常,ID: {job_id}, 错误: {str(e)}", exc_info=True)
return R.failed(msg="暂停失败,请稍后重试")
# =============================================================
# 恢复任务服务
# =============================================================
def resume_job(job_id: int) -> R:
"""
恢复任务
将任务状态设置为运行中,并添加到调度器执行。
Args:
job_id: 任务ID
Returns:
R: 操作结果
- 成功: R.ok(msg="恢复成功")
- 失败: R.failed(msg="错误信息")
"""
try:
# 参数校验
if not job_id or job_id <= 0:
return R.failed("任务ID无效")
# 查询任务
job = Job.objects.filter(id=job_id, is_delete=0).first()
if not job:
logger.warning(f"任务不存在,ID: {job_id}")
return R.failed("任务不存在")
# 如果已经是运行中状态
if job.status == 1:
return R.ok(msg="任务已处于运行中状态")
# 检查Cron表达式
if not job.cron_expression:
return R.failed("任务Cron表达式为空,无法恢复")
# 更新状态
job.status = 1
job.save(update_fields=['status', 'update_time'])
# 添加到调度器
success = False
if job.cron_expression:
success = add_job_to_scheduler(job.id)
else:
success = resume_job_in_scheduler(job.id)
if success:
logger.info(f"恢复任务成功,ID: {job_id}")
return R.ok(msg="恢复成功")
else:
logger.warning(f"调度器恢复失败,但数据库已更新,ID: {job_id}")
return R.ok(msg="任务已恢复,但调度器同步失败")
except Exception as e:
logger.error(f"恢复任务异常,ID: {job_id}, 错误: {str(e)}", exc_info=True)
return R.failed(msg="恢复失败,请稍后重试")
# =============================================================
# 立即执行服务
# =============================================================
def run_job_once_service(job_id: int, params: Optional[Dict[str, Any]] = None) -> R:
"""
立即执行一次任务
手动触发任务执行,不依赖调度器的定时规则。
Args:
job_id: 任务ID
params: 执行参数(可选)
Returns:
R: 操作结果,包含执行结果数据
- 成功: R.ok(data=result, msg="执行成功")
- 失败: R.failed(msg="错误信息")
"""
try:
# 参数校验
if not job_id or job_id <= 0:
return R.failed("任务ID无效")
# 查询任务
job = Job.objects.filter(id=job_id, is_delete=0).first()
if not job:
logger.warning(f"任务不存在,ID: {job_id}")
return R.failed("任务不存在")
logger.info(f"开始立即执行任务,ID: {job_id}, 名称: {job.job_name}")
# 调用调度器的立即执行函数
result = scheduler_run_job_once(job.id, params)
if result.get('success'):
logger.info(f"任务立即执行成功,ID: {job_id}, 结果: {result}")
return R.ok(data=result, msg="执行成功")
else:
error_msg = result.get('error', '未知错误')
logger.error(f"任务立即执行失败,ID: {job_id}, 错误: {error_msg}")
return R.failed(msg=f"执行失败: {error_msg}")
except Exception as e:
logger.error(f"立即执行任务异常,ID: {job_id}, 错误: {str(e)}", exc_info=True)
return R.failed(msg="执行失败,请稍后重试")
# =============================================================
# 调度器管理服务
# =============================================================
def reload_all_jobs() -> R:
"""
重新加载所有任务到调度器
清除当前调度器中的所有任务,然后从数据库重新加载所有运行中的任务。
Returns:
R: 操作结果,包含加载的任务数量
- 成功: R.ok(data=count, msg="已重新加载 N 个任务")
- 失败: R.failed(msg="错误信息")
"""
try:
logger.info("开始重新加载所有任务到调度器")
# 调用调度器的重新加载函数
loaded_count = reload_scheduler()
logger.info(f"重新加载任务完成,共加载 {loaded_count} 个任务")
return R.ok(data=loaded_count, msg=f"已重新加载 {loaded_count} 个任务")
except Exception as e:
logger.error(f"重新加载任务异常: {str(e)}", exc_info=True)
return R.failed(msg="重新加载失败,请稍后重试")
def get_scheduler_status() -> R:
"""
获取调度器当前状态
Returns:
R: 操作结果,包含调度器状态信息
- 成功: R.ok(data=status_info, msg="获取成功")
- 失败: R.failed(msg="错误信息")
"""
try:
from application.job.scheduler import scheduler, _print_scheduler_status
# 获取调度器中的任务列表
jobs = scheduler.get_jobs()
job_list = []
for job in jobs:
next_run = job.next_run_time
job_list.append({
'id': job.id,
'name': job.name,
'next_run_time': next_run.strftime('%Y-%m-%d %H:%M:%S') if next_run else None,
'pending': job.pending
})
status_info = {
'running': scheduler.running,
'job_count': len(jobs),
'jobs': job_list
}
return R.ok(data=status_info, msg="获取成功")
except Exception as e:
logger.error(f"获取调度器状态异常: {str(e)}", exc_info=True)
return R.failed(msg="获取失败")
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+176
View File
@@ -0,0 +1,176 @@
# +======================================================================
# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ]
# +----------------------------------------------------------------------
# | Copyright (c) 2021-2026 小蚂蚁云团队 版权所有
# +----------------------------------------------------------------------
# | 开源协议: Apache-2.0
# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。
# +----------------------------------------------------------------------
# | 官方网站: https://www.djangoadmin.cn
# +----------------------------------------------------------------------
# | 研发团队: 小蚂蚁云团队 荣誉出品
# +======================================================================
# | 版权和免责声明:
# | 一、知识产权声明
# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、
# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权,
# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。
# | 二、使用限制与禁止条款
# | 您理解并同意,禁止将本产品用于以下任何行为:
# | 1. 任何违反中华人民共和国法律法规的用途;
# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为;
# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。
# | 三、责任豁免
# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失
# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三
# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。
# | 四、违约与法律责任
# | 如发现任何违反本声明的行为,本团队有权:
# | 1. 无条件配合公安机关及其他有权机关的调查取证工作;
# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失;
# | 3. 保留进一步采取刑事控告等法律措施的权利。
# | 五、许可范围说明
# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包
# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通
# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、
# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造
# | 成的一切经济损失及商誉损害。
# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件
# | 许可协议》附件为准。
# +======================================================================
"""
定时任务管理模块路由配置
=============================================================
URL端点概览:
------------------------------------------------------------------------
| 功能说明 | 请求方法 | URL路径 | 视图类 |
|-----------------|----------|-----------------------------|---------------------|
| 分页查询 | GET | /job/page | JobPageView |
| 详情查询 | GET | /job/detail/<id> | JobDetailView |
| 添加任务 | POST | /job/add | JobAddView |
| 更新任务 | PUT | /job/update | JobUpdateView |
| 删除任务 | DELETE | /job/delete/<ids> | JobDeleteView |
| 设置状态 | PUT | /job/status | JobStatusView |
| 暂停任务 | PUT | /job/pause/<id> | JobPauseView |
| 恢复任务 | PUT | /job/resume/<id> | JobResumeView |
| 立即执行 | POST | /job/runOnce/<id> | JobRunOnceView |
| 短信测试 | POST | /job/sendSms | JobSendSmsView |
------------------------------------------------------------------------
URL参数说明:
- job_id: int类型,定时任务ID(用于查询详情、暂停、恢复、立即执行)
- job_id: str类型,逗号分隔的任务ID字符串(用于批量删除)
使用示例:
1. 分页查询:GET /api/job/page?pageNo=1&pageSize=10&name=同步
2. 详情查询:GET /api/job/detail/5
3. 添加任务:POST /api/job/add
4. 更新任务:PUT /api/job/update
5. 删除任务:DELETE /api/job/delete/1,2,3
6. 设置状态:PUT /api/job/status
7. 暂停任务:PUT /api/job/pause/5
8. 恢复任务:PUT /api/job/resume/5
9. 立即执行:POST /api/job/runOnce/5
10. 短信测试:POST /api/job/sendSms
URL命名规范:
- page:分页查询
- detail:详情查询(带ID参数)
- add:添加操作
- update:更新操作
- delete:删除操作(带ID参数)
- status:状态设置操作
- pause:暂停任务操作
- resume:恢复任务操作
- runOnce:立即执行操作
- sendSms:短信测试操作
"""
from django.urls import path # 导入Django URL路由配置模块
# 导入定时任务模块的视图层
# views模块包含了所有定时任务相关的视图类,如分页查询、详情查询、增删改查、状态控制等
from application.job import views
# 定时任务模块URL路由配置列表
# 每个路径都映射到对应的视图类,通过.as_view()方法将类视图转换为函数视图
urlpatterns = [
# ============================ 查询操作 ============================
# 查询定时任务分页列表
# URL: /job/page
# 请求方法: GET
# 功能: 获取定时任务分页数据,支持按任务名称、状态、执行器等条件筛选
path('page', views.JobPageView.as_view()),
# 查询定时任务详情
# URL: /job/detail/<int:job_id>
# 请求方法: GET
# URL参数: job_id - 定时任务ID(整数类型)
# 功能: 根据ID获取指定定时任务的详细信息
path('detail/<int:job_id>', views.JobDetailView.as_view()),
# ============================ 新增操作 ============================
# 添加定时任务
# URL: /job/add
# 请求方法: POST
# 功能: 创建新的定时任务记录,请求体需包含任务信息(名称、执行器、Cron表达式、状态等)
path('add', views.JobAddView.as_view()),
# ============================ 修改操作 ============================
# 更新定时任务
# URL: /job/update
# 请求方法: PUT
# 功能: 更新现有定时任务信息,请求体需包含任务ID和更新内容
path('update', views.JobUpdateView.as_view()),
# 设置任务状态
# URL: /job/status
# 请求方法: PUT
# 功能: 启用或禁用定时任务,请求体需包含任务ID和目标状态
path('status', views.JobStatusView.as_view()),
# ============================ 删除操作 ============================
# 删除定时任务
# URL: /job/delete/<str:job_id>
# 请求方法: DELETE
# URL参数: job_id - 任务ID字符串,支持单个ID或逗号分隔的多个ID(如 "1,2,3")
# 功能: 删除指定定时任务(物理删除或逻辑删除)
path('delete/<str:job_id>', views.JobDeleteView.as_view()),
# ============================ 任务控制操作 ============================
# 暂停任务
# URL: /job/pause/<int:job_id>
# 请求方法: PUT
# URL参数: job_id - 定时任务ID(整数类型)
# 功能: 暂停正在运行的定时任务,任务将不再按计划执行
path('pause/<int:job_id>', views.JobPauseView.as_view()),
# 恢复任务
# URL: /job/resume/<int:job_id>
# 请求方法: PUT
# URL参数: job_id - 定时任务ID(整数类型)
# 功能: 恢复已暂停的定时任务,按原有Cron表达式继续执行
path('resume/<int:job_id>', views.JobResumeView.as_view()),
# 立即执行任务
# URL: /job/runOnce/<int:job_id>
# 请求方法: POST
# URL参数: job_id - 定时任务ID(整数类型)
# 功能: 立即执行一次定时任务,不影响原有的调度计划
path('runOnce/<int:job_id>', views.JobRunOnceView.as_view()),
# ============================ 测试操作 ============================
# 短信测试
# URL: /job/sendSms
# 请求方法: POST
# 功能: 测试短信发送功能,用于验证短信配置是否正确
path('sendSms', views.JobSendSmsView.as_view()),
]
+482
View File
@@ -0,0 +1,482 @@
# +======================================================================
# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ]
# +----------------------------------------------------------------------
# | Copyright (c) 2021-2026 小蚂蚁云团队 版权所有
# +----------------------------------------------------------------------
# | 开源协议: Apache-2.0
# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。
# +----------------------------------------------------------------------
# | 官方网站: https://www.djangoadmin.cn
# +----------------------------------------------------------------------
# | 研发团队: 小蚂蚁云团队 荣誉出品
# +======================================================================
# | 版权和免责声明:
# | 一、知识产权声明
# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、
# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权,
# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。
# | 二、使用限制与禁止条款
# | 您理解并同意,禁止将本产品用于以下任何行为:
# | 1. 任何违反中华人民共和国法律法规的用途;
# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为;
# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。
# | 三、责任豁免
# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失
# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三
# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。
# | 四、违约与法律责任
# | 如发现任何违反本声明的行为,本团队有权:
# | 1. 无条件配合公安机关及其他有权机关的调查取证工作;
# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失;
# | 3. 保留进一步采取刑事控告等法律措施的权利。
# | 五、许可范围说明
# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包
# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通
# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、
# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造
# | 成的一切经济损失及商誉损害。
# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件
# | 许可协议》附件为准。
# +======================================================================
"""
定时任务管理视图模块
=============================================================
该模块定义了定时任务管理功能中使用的所有视图类。
用于处理定时任务信息的HTTP请求,包括分页查询、详情查询、添加、更新、删除等操作。
"""
import json
import logging
from django.conf import settings
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from application.job import services
from application.operation_log.constants import LogType
from application.operation_log.decorators import operation_log
from config.env import DJANGO_DEMO
from middleware.login_middleware import check_login
from middleware.permission_middleware import PermissionRequired
from utils import R
# =============================================================
# 分页查询视图
# =============================================================
@method_decorator(check_login, name="get")
class JobPageView(PermissionRequired, View):
"""
定时任务分页数据查询视图
URL: /job/page
请求方法: GET
权限要求:sys:job:page
"""
permission_required = ("sys:job:page",)
@operation_log(
title="定时任务-查询分页列表",
log_type=LogType.QUERY
)
def get(self, request):
result = services.get_job_page(request)
return result
# =============================================================
# 详情查询视图
# =============================================================
@method_decorator(check_login, name="get")
class JobDetailView(PermissionRequired, View):
"""
定时任务详情查询视图
URL: /job/detail/<int:job_id>
请求方法: GET
权限要求:sys:job:detail
"""
permission_required = ("sys:job:detail",)
@operation_log(
title="定时任务-查询记录详情",
log_type=LogType.QUERY
)
def get(self, request, job_id):
data = services.get_job_detail(job_id)
if data:
return R.ok(data=data)
return R.failed("任务不存在")
# =============================================================
# 添加视图
# =============================================================
@method_decorator(check_login, name="post")
class JobAddView(PermissionRequired, View):
"""
添加定时任务视图
URL: /job/add
请求方法: POST
权限要求:sys:job:add
"""
permission_required = ("sys:job:add",)
@operation_log(
title="定时任务-添加记录",
log_type=LogType.ADD
)
def post(self, request):
if DJANGO_DEMO:
return R.failed("演示环境,暂无操作权限")
result = services.add_job(request)
return result
# =============================================================
# 更新视图
# =============================================================
@method_decorator(check_login, name="put")
class JobUpdateView(PermissionRequired, View):
"""
更新定时任务视图
URL: /job/update
请求方法: PUT
权限要求:sys:job:update
"""
permission_required = ('sys:job:update',)
@operation_log(
title="定时任务-更新记录",
log_type=LogType.UPDATE
)
def put(self, request):
if DJANGO_DEMO:
return R.failed("演示环境,暂无操作权限")
result = services.update_job(request)
return result
# =============================================================
# 删除视图(支持批量)
# =============================================================
@method_decorator(check_login, name="delete")
class JobDeleteView(PermissionRequired, View):
"""
删除定时任务视图(支持批量)
URL: /job/delete/<str:job_id>
请求方法: DELETE
权限要求:sys:job:delete
"""
permission_required = ('sys:job:delete',)
@operation_log(
title="定时任务-删除记录",
log_type=LogType.DELETE
)
def delete(self, request, job_id):
if DJANGO_DEMO:
return R.failed("演示环境,暂无操作权限")
result = services.delete_jobs(job_id)
return result
# =============================================================
# 状态设置视图
# =============================================================
@method_decorator(check_login, name="post")
class JobStatusView(PermissionRequired, View):
"""
设置任务状态视图
URL: /job/status
请求方法: POST
权限要求:sys:job:status
"""
permission_required = ('sys:job:status',)
@operation_log(
title="定时任务-设置状态",
log_type=LogType.STATUS
)
def post(self, request):
if DJANGO_DEMO:
return R.failed("演示环境,暂无操作权限")
result = services.status_job(request)
return result
# =============================================================
# 暂停任务视图
# =============================================================
@method_decorator(check_login, name="get")
class JobPauseView(PermissionRequired, View):
"""
暂停任务视图
URL: /job/pause/<int:job_id>
请求方法: GET
权限要求:sys:job:pause
"""
permission_required = ('sys:job:pause',)
@operation_log(
title="定时任务-暂停任务",
log_type=LogType.OTHER
)
def get(self, request, job_id):
if DJANGO_DEMO:
return R.failed("演示环境,暂无操作权限")
result = services.pause_job(job_id)
return result
# =============================================================
# 恢复任务视图
# =============================================================
@method_decorator(check_login, name="get")
class JobResumeView(PermissionRequired, View):
"""
恢复任务视图
URL: /job/resume/<int:job_id>
请求方法: GET
权限要求:sys:job:resume
"""
permission_required = ('sys:job:resume',)
@operation_log(
title="定时任务-恢复任务",
log_type=LogType.OTHER
)
def get(self, request, job_id):
if DJANGO_DEMO:
return R.failed("演示环境,暂无操作权限")
result = services.resume_job(job_id)
return result
# =============================================================
# 立即执行视图
# =============================================================
@method_decorator(check_login, name="get")
class JobRunOnceView(PermissionRequired, View):
"""
立即执行一次任务视图
URL: /job/runOnce/<int:job_id>
请求方法: GET
权限要求:sys:job:runOnce
"""
permission_required = ('sys:job:runOnce',)
@operation_log(
title="定时任务-立即执行",
log_type=LogType.OTHER
)
def get(self, request, job_id):
if DJANGO_DEMO:
return R.failed("演示环境,暂无操作权限")
result = services.run_job_once_service(job_id)
return result
# =============================================================
# 短信测试视图
# =============================================================
class JobSendSmsView(View):
"""
短信测试视图
用于测试短信发送功能。
"""
def post(self, request):
"""
发送测试短信
Args:
request: HttpRequest对象
请求参数:
- phone: 手机号(必填)
- content: 短信内容(可选,默认为"这是一条测试短信")
- template_id: 模板ID(可选)
- template_params: 模板参数(可选)
Returns:
JsonResponse: 包含发送结果的JSON响应
"""
try:
data = json.loads(request.body.decode('utf-8'))
phone = data.get('phone')
content = data.get('content', '这是一条测试短信')
template_id = data.get('template_id')
template_params = data.get('template_params', {})
if not phone:
return R.failed(msg="手机号不能为空")
logging.info(f"[短信测试] 手机号: {phone}, 内容: {content}, 模板ID: {template_id}")
# 根据配置选择短信服务商
sms_provider = getattr(settings, 'SMS_PROVIDER', 'mock')
if sms_provider == 'aliyun':
result = self.send_aliyun_sms(phone, content, template_id, template_params)
elif sms_provider == 'tencent':
result = self.send_tencent_sms(phone, content, template_id, template_params)
else:
result = self.send_mock_sms(phone, content)
if result.get('success'):
return R.ok(data=result, msg="短信发送成功")
else:
return R.failed(msg=result.get('message', '短信发送失败'))
except json.JSONDecodeError:
return R.failed(msg="请求数据格式错误")
except Exception as e:
logging.error(f"短信发送异常: {str(e)}", exc_info=True)
return R.failed(msg=f"短信发送失败: {str(e)}")
def send_mock_sms(self, phone, content):
"""模拟短信发送(用于测试)"""
logging.info(f"[模拟短信] 发送至 {phone}: {content}")
return {
'success': True,
'provider': 'mock',
'phone': phone,
'content': content,
'message_id': f'mock_{phone}_{id}'
}
def send_aliyun_sms(self, phone, content, template_id, template_params):
"""阿里云短信发送"""
try:
from aliyunsdkcore.client import AcsClient
from aliyunsdkcore.request import CommonRequest
access_key_id = getattr(settings, 'ALIYUN_ACCESS_KEY_ID', '')
access_secret = getattr(settings, 'ALIYUN_ACCESS_SECRET', '')
sign_name = getattr(settings, 'ALIYUN_SMS_SIGN', '')
if not all([access_key_id, access_secret, sign_name, template_id]):
return {'success': False, 'message': '阿里云短信配置不完整'}
client = AcsClient(access_key_id, access_secret, 'cn-hangzhou')
request = CommonRequest()
request.set_accept_format('json')
request.set_domain('dysmsapi.aliyuncs.com')
request.set_method('POST')
request.set_protocol_type('https')
request.set_version('2017-05-25')
request.set_action_name('SendSms')
request.add_query_param('PhoneNumbers', phone)
request.add_query_param('SignName', sign_name)
request.add_query_param('TemplateCode', template_id)
if template_params:
import json
request.add_query_param('TemplateParam', json.dumps(template_params))
response = client.do_action_with_exception(request)
result = json.loads(response)
if result.get('Code') == 'OK':
return {
'success': True,
'provider': 'aliyun',
'phone': phone,
'request_id': result.get('RequestId'),
'biz_id': result.get('BizId')
}
else:
return {
'success': False,
'provider': 'aliyun',
'message': result.get('Message', '发送失败')
}
except Exception as e:
logging.error(f"阿里云短信发送异常: {str(e)}")
return {'success': False, 'message': str(e)}
def send_tencent_sms(self, phone, content, template_id, template_params):
"""腾讯云短信发送"""
try:
from tencentcloud.common import credential
from tencentcloud.sms.v20210111 import sms_client, models
secret_id = getattr(settings, 'TENCENT_SECRET_ID', '')
secret_key = getattr(settings, 'TENCENT_SECRET_KEY', '')
sdk_app_id = getattr(settings, 'TENCENT_SMS_APP_ID', '')
sign_name = getattr(settings, 'TENCENT_SMS_SIGN', '')
if not all([secret_id, secret_key, sdk_app_id, sign_name, template_id]):
return {'success': False, 'message': '腾讯云短信配置不完整'}
cred = credential.Credential(secret_id, secret_key)
client = sms_client.SmsClient(cred, "ap-guangzhou")
req = models.SendSmsRequest()
req.SmsSdkAppId = sdk_app_id
req.SignName = sign_name
req.TemplateId = template_id
req.PhoneNumberSet = [phone]
if template_params:
req.TemplateParamSet = list(template_params.values())
resp = client.SendSms(req)
if resp.SendStatusSet and resp.SendStatusSet[0].Code == 'Ok':
return {
'success': True,
'provider': 'tencent',
'phone': phone,
'serial_no': resp.SendStatusSet[0].SerialNo
}
else:
return {
'success': False,
'provider': 'tencent',
'message': resp.SendStatusSet[0].Message if resp.SendStatusSet else '发送失败'
}
except Exception as e:
logging.error(f"腾讯云短信发送异常: {str(e)}")
return {'success': False, 'message': str(e)}