Files
DjangoAdmin_FastAPI_EleVue/modules/job/service.py
T
ClaudeandClaude 64a5504396 feat(phase4): 全部业务模块迁移到 modules/(结构迁移)
- 21 个模块目录(auth/system/dictionary/configuration/cms/member/region/link/notice/job/log/upload/index)
- 每个模块 router.py(合并模块导出多个 router)/schemas.py/service.py/repository.py/models.py
- routers/v1.py 与 routers/__init__.py 改为直接引用 modules,前缀与权限串原样保留
- core/middleware.py 懒加载指向 modules.system.menu / modules.log;core/redis_client.py 指向 modules.job.scheduler
- apps/views|services|forms 全部改为 re-export 垫片
- 迁移后 grep 守门通过:modules/core 无 apps/extensions 残留引用
- 实测登录、27+ 模块接口、写操作 commit、操作日志落库均正常

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 10:13:30 +08:00

383 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import logging
from datetime import datetime, timezone
from sqlalchemy import and_
from shared.enums import JOB_STATUS_LIST, JOB_EXECUTE_POLICY_LIST
from shared.constants import PAGE_SIZE
from modules.job.schemas import JobForm, JobStatusForm
from modules.job.models import Job
from modules.job.scheduler import (
add_job_to_scheduler,
remove_job_from_scheduler,
pause_job_in_scheduler,
get_scheduler,
run_job_once as scheduler_run_job_once,
reload_scheduler,
)
from core.database import get_db_session
from core import R
from core.security import get_user_id
from utils.request import like_contains, parse_id_list
logger = logging.getLogger(__name__)
# 查询定时任务分页数据
def get_job_page(request):
db = get_db_session()
# 页码
page = int(request.query_params.get("pageNo", 1))
# 每页数
limit = int(request.query_params.get("pageSize", PAGE_SIZE))
# 实例化查询对象
query = db.query(Job).filter(Job.is_delete == 0)
# 任务名称
job_name = request.query_params.get('jobName')
if job_name:
query = query.filter(Job.job_name.like(like_contains(job_name), escape='\\'))
# 任务状态
status = request.query_params.get('status')
if status:
query = query.filter(Job.status == status)
# 任务分组
job_group = request.query_params.get('jobGroup')
if job_group:
query = query.filter(Job.job_group == job_group)
# 排序
query = query.order_by(Job.id.desc())
# 记录总数
count = query.count()
# 分页查询
job_list = query.limit(limit).offset((page - 1) * limit).all()
# 若当前页超出末页(数据被并发删除),自动回退到第一页
if page > 1 and not job_list:
page = 1
count = query.count()
job_list = query.limit(limit).offset(0).all()
# 实例化结果
scheduler = get_scheduler()
result = []
if len(job_list) > 0:
for item in job_list:
data = item.to_dict()
data['status_name'] = JOB_STATUS_LIST.get(item.status)
data['execute_policy_name'] = JOB_EXECUTE_POLICY_LIST.get(item.execute_policy)
data['next_run_time'] = _get_next_run_time(scheduler, item.id)
result.append(data)
return R.ok(data=result, count=count)
def _get_next_run_time(scheduler, job_id):
"""从调度器获取任务下次执行时间"""
try:
scheduler_job = scheduler.get_job(f"job_{job_id}")
if scheduler_job and scheduler_job.next_run_time:
return scheduler_job.next_run_time.strftime('%Y-%m-%d %H:%M:%S')
except Exception:
pass
return None
# 根据ID查询任务详情
def get_job_detail(job_id):
db = get_db_session()
job = db.query(Job).filter(and_(Job.id == job_id, Job.is_delete == 0)).first()
if not job:
return None
data = job.to_dict()
data['status_name'] = JOB_STATUS_LIST.get(job.status)
data['execute_policy_name'] = JOB_EXECUTE_POLICY_LIST.get(job.execute_policy)
return data
# 添加定时任务
def add_job(request, data: JobForm):
db = get_db_session()
# 表单数据赋值给对象
job = Job(**data.model_dump(exclude={'id'}))
job.create_user = get_user_id(request)
# 插入数据
job.save()
# 状态为运行中且有Cron表达式,添加到调度器
if job.status == 1 and job.cron_expression:
add_job_to_scheduler(job.to_dict())
return R.ok(msg="添加成功")
# 更新定时任务
def update_job(request, data: JobForm):
db = get_db_session()
# 记录ID判空
if not data.id or data.id <= 0:
return R.failed("记录ID不能为空")
# 根据ID查询记录
job = db.query(Job).filter(and_(Job.id == data.id, Job.is_delete == 0)).first()
if not job:
return R.failed("任务不存在")
old_status = job.status
old_cron = job.cron_expression
# 构造更新数据(排除 id)
updData = data.model_dump(exclude={'id'})
updData['update_user'] = get_user_id(request)
updData['update_time'] = datetime.now(tz=timezone.utc)
result = db.query(Job).filter_by(id=data.id).update(updData)
if not result:
return R.failed("更新失败")
# 处理调度器更新
_handle_scheduler_update(data.model_dump(), old_status, old_cron)
return R.ok(msg="更新成功")
def _handle_scheduler_update(job_data, old_status, old_cron):
"""处理调度器更新(job_data 为完整任务数据,含 execute_policy/params)"""
job_id = job_data.get('id')
new_status = job_data.get('status')
new_cron = job_data.get('cron_expression')
if old_status != 1 and new_status == 1 and new_cron:
add_job_to_scheduler(job_data)
elif old_status == 1 and new_status != 1:
remove_job_from_scheduler(job_id)
elif new_status == 1 and old_cron != new_cron:
if new_cron:
add_job_to_scheduler(job_data)
else:
remove_job_from_scheduler(job_id)
# 删除定时任务
def delete_job(job_id):
# 从调度器移除
id_list = parse_id_list(job_id)
for jid in id_list:
remove_job_from_scheduler(jid)
return Job.batch_delete_r(job_id)
# 设置任务状态
def update_job_status(request, data: JobStatusForm):
db = get_db_session()
# 根据ID查询记录
job = db.query(Job).filter(and_(Job.id == data.id, Job.is_delete == 0)).first()
if not job:
return R.failed("任务不存在")
old_status = job.status
# 更新记录
result = db.query(Job).filter_by(id=data.id).update({
"status": data.status,
"update_user": get_user_id(request),
"update_time": datetime.now(tz=timezone.utc)
})
if not result:
return R.failed("设置失败")
# 处理调度器
if data.status == 1 and job.cron_expression:
job_dict = job.to_dict()
job_dict['status'] = data.status
add_job_to_scheduler(job_dict)
elif data.status == 2:
pause_job_in_scheduler(data.id)
elif old_status == 1 and data.status != 1:
remove_job_from_scheduler(data.id)
return R.ok(msg="设置成功")
# 暂停任务
def pause_job_by_id(job_id):
db = get_db_session()
job = db.query(Job).filter(and_(Job.id == job_id, Job.is_delete == 0)).first()
if not job:
return R.failed("任务不存在")
if job.status == 2:
return R.ok(msg="任务已处于暂停状态")
job.status = 2
job.update_user = job.create_user
job.update_time = datetime.now(tz=timezone.utc)
db.commit()
pause_job_in_scheduler(job_id)
return R.ok(msg="暂停成功")
# 恢复任务
def resume_job_by_id(job_id):
db = get_db_session()
job = db.query(Job).filter(and_(Job.id == job_id, Job.is_delete == 0)).first()
if not job:
return R.failed("任务不存在")
if job.status == 1:
return R.ok(msg="任务已处于运行中状态")
if not job.cron_expression:
return R.failed("Cron表达式为空,无法恢复")
job.status = 1
job.update_user = job.create_user
job.update_time = datetime.now(tz=timezone.utc)
db.commit()
add_job_to_scheduler(job.to_dict())
return R.ok(msg="恢复成功")
# 立即执行一次
def run_job_once_api(job_id):
result = scheduler_run_job_once(job_id)
if result.get('success'):
return R.ok(data=result, msg="执行成功")
else:
return R.failed(msg=result.get('error', '执行失败'))
# 重新加载调度器
def reload_all_jobs():
count = reload_scheduler()
return R.ok(data=count, msg=f"已重新加载 {count} 个任务")
import logging
from sqlalchemy import and_
from shared.enums import JOB_LOG_STATUS_LIST
from shared.constants import PAGE_SIZE
from modules.job.models import Job
from modules.job.models import JobLog
from core.database import get_db_session
from core.database import SessionLocal
from core import R
from utils.request import like_contains, parse_id_list
logger = logging.getLogger(__name__)
# ============================================================
# 日志写入(供调度器调用,可在请求上下文外执行)
# ============================================================
def record_job_log(job_id, start_time, end_time, status, exception_info=None, job_message=None):
"""
记录任务执行日志
使用独立会话写入,调度器线程等无请求上下文场景也可调用。
"""
session = SessionLocal()
try:
job = session.query(Job).filter(Job.id == job_id, Job.is_delete == 0).first()
consume_time = 0
if end_time and start_time:
consume_time = int((end_time - start_time).total_seconds() * 1000)
log = JobLog(
job_id=job_id,
job_name=job.job_name if job else '',
job_group=job.job_group if job else None,
job_trigger=job.job_trigger if job else None,
cron_expression=job.cron_expression if job else None,
job_message=job_message,
status=status,
start_time=start_time,
end_time=end_time,
consume_time=consume_time,
exception_info=exception_info,
)
session.add(log)
session.commit()
return log
except Exception as e:
session.rollback()
logger.error(f"记录任务执行日志异常: {str(e)}")
return None
finally:
session.close()
# ============================================================
# 日志查询/删除
# ============================================================
def get_job_log_page(request):
db = get_db_session()
# 页码
page = int(request.query_params.get("pageNo", 1))
# 每页数
limit = int(request.query_params.get("pageSize", PAGE_SIZE))
# 实例化查询对象
query = db.query(JobLog).filter(JobLog.del_flag == 0)
# 任务名称
job_name = request.query_params.get('jobName')
if job_name:
query = query.filter(JobLog.job_name.like(like_contains(job_name), escape='\\'))
# 任务分组
job_group = request.query_params.get('jobGroup')
if job_group:
query = query.filter(JobLog.job_group == job_group)
# 执行状态
status = request.query_params.get('status')
if status:
query = query.filter(JobLog.status == status)
# 排序
query = query.order_by(JobLog.id.desc())
# 记录总数
count = query.count()
# 分页查询
log_list = query.limit(limit).offset((page - 1) * limit).all()
# 若当前页超出末页(数据被并发删除),自动回退到第一页
if page > 1 and not log_list:
page = 1
count = query.count()
log_list = query.limit(limit).offset(0).all()
# 实例化结果
result = []
for item in log_list:
data = item.to_dict()
data['status_name'] = JOB_LOG_STATUS_LIST.get(item.status)
result.append(data)
return R.ok(data=result, count=count)
# 根据ID查询日志详情
def get_job_log_detail(job_log_id):
db = get_db_session()
log = db.query(JobLog).filter(and_(JobLog.id == job_log_id, JobLog.del_flag == 0)).first()
if not log:
return None
data = log.to_dict()
data['status_name'] = JOB_LOG_STATUS_LIST.get(log.status)
return data
# 删除任务日志(软删除)
def delete_job_log(ids_str):
id_list = parse_id_list(ids_str)
if not id_list:
return R.failed("记录ID不存在")
db = get_db_session()
result = db.query(JobLog).filter(JobLog.id.in_(id_list), JobLog.del_flag == 0).update(
{"del_flag": 1}, synchronize_session=False
)
return R.ok(msg="本次共删除{0}条数据".format(result))