大家好!我是CSDN的Python新手博主~ 上一篇我们完成了看板的合规管控闭环,解决了权限与审计问题,但很多企业用户反馈核心痛点:① 高频流程依赖人工,如每日销售日报汇总、异常数据告警、文件自动分发等,重复操作耗时且易出错;② 流程与看板数据脱节,OA任务(如日报提交)无法自动同步至看板,需手动录入;③ 提醒方式单一,仅靠企业微信消息,无法根据紧急程度适配多渠道提醒(短信、邮件、电话)。今天就带来超落地的新手实战项目------办公看板集成OA流程自动化+AI智能提醒+定时任务调度!
本次基于之前的"合规管控看板"代码,新增4大核心功能:① 定时任务调度(基于APScheduler,支持定时汇总数据、生成报表、推送通知);② OA流程自动化对接(企业微信OA/自建OA,实现日报提交、任务分配自动同步至看板,看板数据异常自动触发OA审批);③ AI智能提醒(基于OpenAI识别数据异常、流程逾期,适配企业微信/短信/邮件多渠道提醒,自动生成提醒话术);④ 流程可视化监控(在看板中展示定时任务状态、OA流程进度,支持任务手动触发与终止)。全程基于现有技术栈(Flask+MySQL+企业微信API),新增定时任务引擎、OA对接模块、AI提醒服务,代码注释详细,新手只需配置任务规则与提醒渠道,跟着步骤复制操作就能成功,让办公流程完全实现"无人值守"自动化~
一、本次学习目标
-
掌握APScheduler定时任务框架的使用,实现固定时间、间隔周期、CRON表达式三种调度方式,适配不同办公场景;
-
学会对接企业微信OA API,实现OA任务与看板数据的双向同步,自动触发流程与审批;
-
理解AI智能提醒逻辑,基于数据分析异常、流程逾期,封装多渠道提醒函数,自动生成个性化提醒话术;
-
实现流程可视化监控,实时展示定时任务状态、OA流程进度,支持任务手动干预(触发/终止/重试);
-
确保自动化流程与现有合规体系联动,定时任务操作留痕、OA流程触发日志可审计,符合企业合规要求。
二、前期准备
- 安装核心依赖库
安装核心依赖(APScheduler定时任务,yagmail邮件,aliyun-python-sdk-core短信)
pip3 install apscheduler yagmail aliyun-python-sdk-core aliyun-python-sdk-dysmsapi python-dotenv -i https://pypi.tuna.tsinghua.edu.cn/simple
确保已有依赖正常(Flask、requests、SQLAlchemy等)
pip3 install --upgrade flask flask-login gunicorn requests pandas pymysql sqlalchemy -i https://pypi.tuna.tsinghua.edu.cn/simple
说明:短信提醒以阿里云短信服务为例,需提前注册阿里云账号并开通短信服务;邮件提醒支持QQ邮箱、企业邮箱,需开启SMTP服务;定时任务基于APScheduler,支持多线程调度,适配生产环境。
- 第三方服务配置
-
定时任务规则:梳理需自动化的流程,确定调度方式(如每日8点汇总昨日销售数据、每小时检查数据异常、每周一9点生成周报);
-
企业微信OA配置:登录企业微信后台,记录OA应用ID、Secret,创建"日报提交""数据异常告警"两类OA模板,获取模板ID;
-
多渠道提醒配置:
-
邮件:开启邮箱SMTP服务(如QQ邮箱开启POP3/SMTP,获取授权码);
-
短信:阿里云短信服务中创建签名、模板,记录AccessKeyId、AccessKeySecret、短信模板ID;
-
企业微信:复用之前的应用配置,确保能发送应用消息。
-
-
安全配置:在.env文件中补充第三方服务敏感信息(邮箱授权码、阿里云短信密钥、OA应用Secret),避免硬编码。
- 数据库表设计与创建
-- 连接MySQL数据库(替换为你的数据库信息)
mysql -u office_user -p -h 47.108.xxx.xxx office_data
-- 创建定时任务表(scheduled_task)
CREATE TABLE scheduled_task (
id INT AUTO_INCREMENT PRIMARY KEY,
task_name VARCHAR(100) NOT NULL COMMENT '任务名称',
task_type ENUM('data_summary', 'report_generate', 'alert_check', 'file_distribute') NOT NULL COMMENT '任务类型:数据汇总/报表生成/告警检查/文件分发',
schedule_type ENUM('interval', 'cron', 'date') NOT NULL COMMENT '调度类型:间隔/CRON/固定时间',
schedule_params JSON NOT NULL COMMENT '调度参数(JSON格式,如{"hours":1}、{"cron":"0 8 * * *"})',
status ENUM('running', 'paused', 'stopped') DEFAULT 'running' COMMENT '任务状态',
last_execute_time DATETIME NULL COMMENT '上次执行时间',
next_execute_time DATETIME NULL COMMENT '下次执行时间',
execute_result ENUM('success', 'fail', 'none') DEFAULT 'none' COMMENT '上次执行结果',
error_msg TEXT NULL COMMENT '执行失败原因',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
UNIQUE KEY uk_task_name (task_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='定时任务表';
-- 创建流程日志表(process_log)
CREATE TABLE process_log (
id INT AUTO_INCREMENT PRIMARY KEY,
process_type ENUM('scheduled_task', 'oa_process', 'ai_alert') NOT NULL COMMENT '流程类型:定时任务/OA流程/AI提醒',
process_name VARCHAR(100) NOT NULL COMMENT '流程名称',
process_id VARCHAR(50) NULL COMMENT '关联ID(任务ID/OA流程ID)',
content JSON NOT NULL COMMENT '流程内容(JSON格式)',
status ENUM('success', 'fail', 'processing') NOT NULL COMMENT '流程状态',
execute_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '执行时间',
channel VARCHAR(20) NULL COMMENT '提醒渠道(wecom/sms/email)',
receiver VARCHAR(100) NOT NULL COMMENT '接收人(用户名/手机号/邮箱)',
KEY idx_process_type (process_type),
KEY idx_execute_time (execute_time),
KEY idx_process_id (process_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='流程日志表';
三、实战:OA自动化+AI提醒+定时任务集成
- 第一步:封装定时任务引擎,实现多类型调度
-- coding: utf-8 --
scheduler.py 定时任务调度脚本
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.interval import IntervalTrigger
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.date import DateTrigger
from datetime import datetime, timedelta
from flask import Blueprint, request, jsonify
from dotenv import load_dotenv
import os
import json
from models import db, ScheduledTask, ProcessLog
from data_process import generate_daily_summary, check_data_abnormal, generate_weekly_report, distribute_file # 复用/新增数据处理函数
from logger import save_operation_log # 复用日志记录函数
加载环境变量
load_dotenv()
scheduler_bp = Blueprint("scheduler", name)
初始化定时任务调度器(后台运行,支持多线程)
scheduler = BackgroundScheduler(timezone="Asia/Shanghai")
启动调度器(应用启动时执行)
scheduler.start()
====================== 任务执行函数(核心逻辑) ======================
def execute_task(task_id):
"""执行定时任务,记录执行结果与流程日志"""
task = ScheduledTask.query.get(task_id)
if not task or task.status != "running":
return
task_name = task.task_name
task_type = task.task_type
process_log = {
"process_type": "scheduled_task",
"process_name": task_name,
"process_id": str(task_id),
"content": {"task_type": task_type, "schedule_params": task.get_schedule_params()},
"status": "processing",
"receiver": "system"
}
try:
# 根据任务类型执行对应逻辑
if task_type == "data_summary":
# 数据汇总任务(每日汇总昨日销售数据)
summary_date = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
summary_data = generate_daily_summary(summary_date)
process_log["content"]["result"] = f"成功汇总{summary_date}数据,共{len(summary_data)}条"
elif task_type == "report_generate":
# 报表生成任务(每周一生成上周周报)
report_week = (datetime.now() - timedelta(days=datetime.now().weekday() + 7)).strftime("%Y-%m-%d") + "至" + (datetime.now() - timedelta(days=datetime.now().weekday() + 1)).strftime("%Y-%m-%d")
report_path = generate_weekly_report(report_week)
process_log["content"]["result"] = f"成功生成周报({report_week}),路径:{report_path}"
elif task_type == "alert_check":
# 数据异常检查任务(每小时检查数据是否异常)
abnormal_data = check_data_abnormal()
if abnormal_data:
process_log["content"]["result"] = f"发现{len(abnormal_data)}条异常数据,已触发AI提醒"
# 触发AI异常提醒(后续实现)
from ai_alert import send_ai_alert
send_ai_alert("data_abnormal", abnormal_data)
else:
process_log["content"]["result"] = "未发现异常数据"
elif task_type == "file_distribute":
# 文件自动分发任务(每日9点分发昨日汇总文件)
distribute_date = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
distribute_result = distribute_file(distribute_date)
process_log["content"]["result"] = distribute_result
# 更新任务状态(成功)
task.last_execute_time = datetime.now()
task.execute_result = "success"
task.error_msg = None
process_log["status"] = "success"
except Exception as e:
# 更新任务状态(失败)
task.last_execute_time = datetime.now()
task.execute_result = "fail"
task.error_msg = str(e)
process_log["status"] = "fail"
process_log["content"]["error_msg"] = str(e)
# 记录错误日志
save_operation_log({
"username": "system",
"user_role": "system",
"operation_type": "scheduled_task",
"operation_content": {"task_name": task_name, "error": str(e)},
"operation_result": "fail",
"ip_address": "127.0.0.1",
"user_agent": "system"
})
finally:
# 计算下次执行时间
task.next_execute_time = get_next_execute_time(task)
# 保存任务更新与流程日志
db.session.commit()
save_process_log(process_log)
====================== 任务调度管理接口 ======================
@scheduler_bp.route("/task/add", methods=["POST"])
def add_task():
"""新增定时任务"""
data = request.get_json()
校验必填参数
required_params = ["task_name", "task_type", "schedule_type", "schedule_params"]
for param in required_params:
if param not in data:
return jsonify({"success": False, "error": f"缺少必填参数:{param}"}), 400
# 校验任务名称唯一性
if ScheduledTask.query.filter_by(task_name=data["task_name"]).first():
return jsonify({"success": False, "error": "任务名称已存在"}), 400
# 构建任务对象
new_task = ScheduledTask(
task_name=data["task_name"],
task_type=data["task_type"],
schedule_type=data["schedule_type"],
schedule_params=json.dumps(data["schedule_params"]),
status="running",
next_execute_time=get_next_execute_time(None, data["schedule_type"], data["schedule_params"])
)
db.session.add(new_task)
db.session.commit()
# 添加任务到调度器
add_task_to_scheduler(new_task)
return jsonify({"success": True, "msg": "任务新增成功", "data": {"task_id": new_task.id}})
@scheduler_bp.route("/task/toggle", methods=["POST"])
def toggle_task():
"""启停/暂停任务"""
data = request.get_json()
task_id = data.get("task_id")
action = data.get("action") # running/paused/stopped
if not task_id or action not in ["running", "paused", "stopped"]:
return jsonify({"success": False, "error": "参数错误,需指定task_id与action"}), 400
task = ScheduledTask.query.get(task_id)
if not task:
return jsonify({"success": False, "error": "未找到对应任务"}), 404
# 更新任务状态
task.status = action
if action == "running":
# 启动任务(添加到调度器)
add_task_to_scheduler(task)
msg = "任务已启动"
else:
# 暂停/停止任务(从调度器移除)
remove_task_from_scheduler(task_id)
msg = "任务已暂停" if action == "paused" else "任务已停止"
db.session.commit()
return jsonify({"success": True, "msg": msg})
@scheduler_bp.route("/task/list", methods=["GET"])
def get_task_list():
"""获取所有定时任务状态"""
tasks = ScheduledTask.query.all()
task_list = []
for task in tasks:
task_list.append({
"task_id": task.id,
"task_name": task.task_name,
"task_type": get_task_type_name(task.task_type),
"schedule_type": get_schedule_type_name(task.schedule_type),
"schedule_params": task.get_schedule_params(),
"status": task.status,
"status_text": {"running": "运行中", "paused": "已暂停", "stopped": "已停止"}[task.status],
"last_execute_time": task.last_execute_time.strftime("%Y-%m-%d %H:%M:%S") if task.last_execute_time else "未执行",
"next_execute_time": task.next_execute_time.strftime("%Y-%m-%d %H:%M:%S") if task.next_execute_time else "未设置",
"execute_result": {"success": "成功", "fail": "失败", "none": "未执行"}[task.execute_result],
"error_msg": task.error_msg
})
return jsonify({"success": True, "data": task_list})
====================== 辅助函数 ======================
def add_task_to_scheduler(task):
"""将任务添加到调度器"""
移除已存在的同名任务(避免重复)
remove_task_from_scheduler(task.id)
# 根据调度类型创建触发器
schedule_type = task.schedule_type
schedule_params = task.get_schedule_params()
if schedule_type == "interval":
trigger = IntervalTrigger(**schedule_params)
elif schedule_type == "cron":
trigger = CronTrigger.from_crontab(schedule_params["cron"])
elif schedule_type == "date":
trigger = DateTrigger(run_date=datetime.strptime(schedule_params["date"], "%Y-%m-%d %H:%M:%S"))
else:
return
# 添加任务到调度器
scheduler.add_job(
execute_task,
trigger=trigger,
args=[task.id],
id=str(task.id),
name=task.task_name,
replace_existing=True
)
def remove_task_from_scheduler(task_id):
"""从调度器移除任务"""
if scheduler.get_job(str(task_id)):
scheduler.remove_job(str(task_id))
def get_next_execute_time(task, schedule_type=None, schedule_params=None):
"""计算下次执行时间"""
if task:
schedule_type = task.schedule_type
schedule_params = task.get_schedule_params()
now = datetime.now()
if schedule_type == "interval":
# 间隔调度(如每1小时)
hours = schedule_params.get("hours", 0)
minutes = schedule_params.get("minutes", 0)
seconds = schedule_params.get("seconds", 0)
return now + timedelta(hours=hours, minutes=minutes, seconds=seconds)
elif schedule_type == "cron":
# CRON调度(如每日8点)
trigger = CronTrigger.from_crontab(schedule_params["cron"])
return trigger.get_next_fire_time(None, now)
elif schedule_type == "date":
# 固定时间调度
return datetime.strptime(schedule_params["date"], "%Y-%m-%d %H:%M:%S")
return None
def get_task_type_name(task_type):
"""获取任务类型中文名称"""
type_map = {
"data_summary": "数据汇总",
"report_generate": "报表生成",
"alert_check": "异常检查",
"file_distribute": "文件分发"
}
return type_map.get(task_type, "未知任务类型")
def get_schedule_type_name(schedule_type):
"""获取调度类型中文名称"""
type_map = {"interval": "间隔调度", "cron": "CRON调度", "date": "固定时间调度"}
return type_map.get(schedule_type, "未知调度类型")
def save_process_log(log_data):
"""保存流程日志"""
try:
new_log = ProcessLog(
process_type=log_data["process_type"],
process_name=log_data["process_name"],
process_id=log_data.get("process_id"),
content=json.dumps(log_data["content"], ensure_ascii=False),
status=log_data["status"],
channel=log_data.get("channel"),
receiver=log_data["receiver"],
execute_time=datetime.now()
)
db.session.add(new_log)
db.session.commit()
except Exception as e:
db.session.rollback()
print(f"流程日志存储失败:{str(e)}")
应用启动时加载所有运行中的任务
def load_running_tasks():
running_tasks = ScheduledTask.query.filter_by(status="running").all()
for task in running_tasks:
add_task_to_scheduler(task)
- 第二步:创建ORM模型,关联数据库表
-- coding: utf-8 --
models.py 补充ORM模型(添加到原有模型之后)
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
import json
db = SQLAlchemy()
原有User、Approval、PermissionRule、OperationLog模型保留(省略重复代码)
...
定时任务模型(对应scheduled_task表)
class ScheduledTask(db.Model):
tablename = "scheduled_task"
id = db.Column(db.Integer, primary_key=True)
task_name = db.Column(db.String(100), unique=True, nullable=False, comment="任务名称")
task_type = db.Column(db.Enum("data_summary", "report_generate", "alert_check", "file_distribute"), nullable=False, comment="任务类型")
schedule_type = db.Column(db.Enum("interval", "cron", "date"), nullable=False, comment="调度类型")
schedule_params = db.Column(db.Text, nullable=False, comment="调度参数(JSON格式)")
status = db.Column(db.Enum("running", "paused", "stopped"), default="running", comment="任务状态")
last_execute_time = db.Column(db.DateTime, nullable=True, comment="上次执行时间")
next_execute_time = db.Column(db.DateTime, nullable=True, comment="下次执行时间")
execute_result = db.Column(db.Enum("success", "fail", "none"), default="none", comment="上次执行结果")
error_msg = db.Column(db.Text, nullable=True, comment="执行失败原因")
create_time = db.Column(db.DateTime, default=datetime.now, comment="创建时间")
update_time = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now, comment="更新时间")
# 辅助方法:解析调度参数
def get_schedule_params(self):
return json.loads(self.schedule_params) if self.schedule_params else {}
流程日志模型(对应process_log表)
class ProcessLog(db.Model):
tablename = "process_log"
id = db.Column(db.Integer, primary_key=True)
process_type = db.Column(db.Enum("scheduled_task", "oa_process", "ai_alert"), nullable=False, comment="流程类型")
process_name = db.Column(db.String(100), nullable=False, comment="流程名称")
process_id = db.Column(db.String(50), nullable=True, comment="关联ID")
content = db.Column(db.Text, nullable=False, comment="流程内容(JSON格式)")
status = db.Column(db.Enum("success", "fail", "processing"), nullable=False, comment="流程状态")
execute_time = db.Column(db.DateTime, default=datetime.now, comment="执行时间")
channel = db.Column(db.String(20), nullable=True, comment="提醒渠道")
receiver = db.Column(db.String(100), nullable=False, comment="接收人")
# 辅助方法:解析流程内容
def get_content(self):
return json.loads(self.content) if self.content else {}
- 第三步:对接OA流程自动化,实现双向同步
-- coding: utf-8 --
oa_process.py OA流程自动化脚本
import requests
import json
from datetime import datetime
from flask import Blueprint, request, jsonify
from dotenv import load_dotenv
import os
from models import db, ProcessLog, Approval
from wecom_login import get_wechat_access_token # 复用企业微信access_token获取函数
from scheduler import save_process_log
加载环境变量
load_dotenv()
oa_bp = Blueprint("oa", name)
====================== OA配置(新手修改这里) ======================
CORP_ID = os.getenv("CORP_ID")
OA_APP_SECRET = os.getenv("OA_APP_SECRET") # OA应用Secret
OA模板ID(日报提交、数据异常审批)
OA_TEMPLATE_IDS = {
"daily_report": "xxxxxxxxxxxxxxxxxxxx", # 日报提交模板ID
"data_abnormal_approval": "xxxxxxxxxxxxxxxxxxxx" # 数据异常审批模板ID
}
企业微信OA API地址
OA_ADD_TASK_URL = "https://qyapi.weixin.qq.com/cgi-bin/oa/task/add"
OA_GET_TASK_URL = "https://qyapi.weixin.qq.com/cgi-bin/oa/task/get"
OA_APPROVAL_CREATE_URL = "https://qyapi.weixin.qq.com/cgi-bin/oa/applyevent" # 复用审批API
====================== OA流程核心功能 ======================
@oa_bp.route("/oa/daily_report/sync", methods=["POST"])
def sync_daily_report():
"""OA日报提交同步至看板"""
data = request.get_json()
解析企业微信OA回调数据(日报提交后触发)
oa_task_id = data.get("task_id")
userid = data.get("creator_userid")
if not oa_task_id or not userid:
return jsonify({"success": False, "error": "缺少OA任务ID或创建人ID"}), 400
# 获取OA日报详情
access_token = get_wechat_access_token(OA_APP_SECRET) # 用OA应用Secret获取token
if not access_token:
return jsonify({"success": False, "error": "获取企业微信access_token失败"}), 500
try:
# 调用OA API获取任务详情
response = requests.post(
f"{OA_GET_TASK_URL}?access_token={access_token}",
json={"task_id": oa_task_id}
)
result = response.json()
if result.get("errcode") != 0:
return jsonify({"success": False, "error": f"获取OA日报失败:{result.get('errmsg')}"}), 500
# 解析日报内容(按OA模板字段提取)
task_info = result.get("task_info")
report_content = {}
for field in task_info.get("content", []):
title = field.get("title")
value = field.get("value")
if title == "日期":
report_content["date"] = value
elif title == "今日销售额":
report_content["sales"] = value
elif title == "今日总结":
report_content["summary"] = value
elif title == "明日计划":
report_content["plan"] = value
# 同步至看板数据库(可新增日报表存储,此处省略)
# ...
# 记录流程日志
save_process_log({
"process_type": "oa_process",
"process_name": "OA日报同步",
"process_id": oa_task_id,
"content": {"report_content": report_content, "userid": userid},
"status": "success",
"receiver": userid
})
return jsonify({"success": True, "msg": "OA日报同步至看板成功", "data": report_content})
except Exception as e:
save_process_log({
"process_type": "oa_process",
"process_name": "OA日报同步",
"process_id": oa_task_id,
"content": {"error_msg": str(e)},
"status": "fail",
"receiver": userid
})
return jsonify({"success": False, "error": f"OA日报同步失败:{str(e)}"}), 500
@oa_bp.route("/oa/data_abnormal/approval", methods=["POST"])
def create_abnormal_approval():
"""数据异常自动触发OA审批"""
data = request.get_json()
abnormal_data = data.get("abnormal_data")
approver_userid = data.get("approver_userid") # 审批人企业微信ID
if not abnormal_data or not approver_userid:
return jsonify({"success": False, "error": "缺少异常数据或审批人ID"}), 400
# 生成审批单号
approval_no = f"APV-ABN-{datetime.now().strftime('%Y%m%d%H%M%S')}-{len(abnormal_data)}"
# 构建OA审批参数
access_token = get_wechat_access_token(OA_APP_SECRET)
if not access_token:
return jsonify({"success": False, "error": "获取企业微信access_token失败"}), 500
approval_params = {
"creator_userid": "system", # 系统自动发起
"template_id": OA_TEMPLATE_IDS["data_abnormal_approval"],
"approvers": [{"userid": approver_userid}],
"apply_data": {
"contents": [
{"title": "异常时间", "value": datetime.now().strftime("%Y-%m-%d %H:%M:%S")},
{"title": "异常数据条数", "value": str(len(abnormal_data))},
{"title": "异常详情", "value": json.dumps(abnormal_data, ensure_ascii=False)[:500]} # 限制长度
]
},
"summary_list": [{"summary_info": "数据异常审批", "details": [f"异常条数:{len(abnormal_data)}"]}]
}
try:
# 调用OA API发起审批
response = requests.post(
f"{OA_APPROVAL_CREATE_URL}?access_token={access_token}",
json=approval_params
)
result = response.json()
if result.get("errcode") != 0:
return jsonify({"success": False, "error": f"发起异常审批失败:{result.get('errmsg')}"}), 500
# 存储审批记录(关联之前的Approval模型)
new_approval = Approval(
approval_no=approval_no,
template_id=OA_TEMPLATE_IDS["data_abnormal_approval"],
applicant="system",
applicant_wecom_id="system",
approval_type="modify",
content=json.dumps({"abnormal_data": abnormal_data}),
status="pending"
)
db.session.add(new_approval)
db.session.commit()
# 记录流程日志
save_process_log({
"process_type": "oa_process",
"process_name": "数据异常OA审批",
"process_id": approval_no,
"content": {"abnormal_data": abnormal_data, "approval_url": result.get("url")},
"status": "success",
"receiver": approver_userid
})
return jsonify({"success": True, "msg": "数据异常审批已发起", "data": {"approval_url": result.get("url")}})
except Exception as e:
db.session.rollback()
save_process_log({
"process_type": "oa_process",
"process_name": "数据异常OA审批",
"process_id": approval_no,
"content": {"error_msg": str(e)},
"status": "fail",
"receiver": approver_userid
})
return jsonify({"success": False, "error": f"发起异常审批失败:{str(e)}"}), 500
看板数据变更同步至OA任务
@oa_bp.route("/oa/task/sync", methods=["POST"])
def sync_to_oa_task():
"""看板数据变更(如销售额达标)自动创建OA任务"""
data = request.get_json()
userid = data.get("userid")
task_content = data.get("task_content")
if not userid or not task_content:
return jsonify({"success": False, "error": "缺少用户ID或任务内容"}), 400
access_token = get_wechat_access_token(OA_APP_SECRET)
if not access_token:
return jsonify({"success": False, "error": "获取企业微信access_token失败"}), 500
# 构建OA任务参数
task_params = {
"creator_userid": "system",
"title": "看板数据任务提醒",
"content": task_content,
"due_date": (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d %H:%M:%S"), # 截止时间1天后
"priority": 2, # 优先级:2-中
"participants": [{"userid": userid}]
}
try:
response = requests.post(
f"{OA_ADD_TASK_URL}?access_token={access_token}",
json=task_params
)
result = response.json()
if result.get("errcode") != 0:
return jsonify({"success": False, "error": f"创建OA任务失败:{result.get('errmsg')}"}), 500
# 记录流程日志
save_process_log({
"process_type": "oa_process",
"process_name": "看板数据同步OA任务",
"process_id": result.get("task_id"),
"content": task_params,
"status": "success",
"receiver": userid
})
return jsonify({"success": True, "msg": "OA任务创建成功", "data": {"task_id": result.get("task_id")}})
except Exception as e:
save_process_log({
"process_type": "oa_process",
"process_name": "看板数据同步OA任务",
"process_id": "",
"content": {"error_msg": str(e)},
"status": "fail",
"receiver": userid
})
return jsonify({"success": False, "error": f"创建OA任务失败:{str(e)}"}), 500
- 第四步:封装AI智能提醒服务,实现多渠道通知
-- coding: utf-8 --
ai_alert.py AI智能提醒服务脚本
import requests
import json
import yagmail
from aliyunsdkcore.client import AcsClient
from aliyunsdkdysmsapi.request.v20170525.SendSmsRequest import SendSmsRequest
from datetime import datetime
from dotenv import load_dotenv
import os
from openai import OpenAI # 复用之前的OpenAI集成
from models import User # 关联用户表获取联系方式
from scheduler import save_process_log
加载环境变量
load_dotenv()
====================== 多渠道配置(新手修改这里) ======================
OpenAI配置
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
OPENAI_MODEL = "gpt-3.5-turbo"
client = OpenAI(api_key=OPENAI_API_KEY)
企业微信提醒配置
CORP_ID = os.getenv("CORP_ID")
WECOM_APP_SECRET = os.getenv("WECOM_APP_SECRET")
WECOM_AGENT_ID = os.getenv("WECOM_AGENT_ID")
邮件提醒配置
EMAIL_USER = os.getenv("EMAIL_USER") # 发送方邮箱
EMAIL_PASSWORD = os.getenv("EMAIL_PASSWORD") # 邮箱授权码
EMAIL_HOST = os.getenv("EMAIL_HOST", "smtp.qq.com")
EMAIL_PORT = int(os.getenv("EMAIL_PORT", 465))
阿里云短信配置
SMS_ACCESS_KEY_ID = os.getenv("SMS_ACCESS_KEY_ID")
SMS_ACCESS_KEY_SECRET = os.getenv("SMS_ACCESS_KEY_SECRET")
SMS_SIGN_NAME = os.getenv("SMS_SIGN_NAME") # 短信签名
SMS_TEMPLATE_ID = os.getenv("SMS_TEMPLATE_ID") # 短信模板ID
紧急程度与提醒渠道映射
URGENCY_CHANNEL_MAP = {
"high": ["wecom", "sms", "email"], # 高紧急:企业微信+短信+邮件
"medium": ["wecom", "email"], # 中紧急:企业微信+邮件
"low": ["wecom"] # 低紧急:仅企业微信
}
====================== AI话术生成 ======================
def generate_alert_content(alert_type, content):
"""基于OpenAI生成个性化提醒话术"""
prompt_map = {
"data_abnormal": f"作为办公自动化助手,帮我生成一条数据异常提醒话术,需包含异常数据条数、异常时间,语气正式且简洁,提醒接收人及时处理。异常数据:{json.dumps(content, ensure_ascii=False)[:300]}",
"task_overdue": f"作为办公自动化助手,帮我生成一条任务逾期提醒话术,需包含任务名称、逾期时间,语气友好且催促及时处理。逾期任务:{json.dumps(content, ensure_ascii=False)[:300]}",
"report_remind": f"作为办公自动化助手,帮我生成一条日报提交提醒话术,需包含提交截止时间,语气温和提醒。提醒信息:{json.dumps(content, ensure_ascii=False)[:300]}"
}
prompt = prompt_map.get(alert_type, f"生成一条办公提醒话术,内容:{content}")
try:
response = client.chat.completions.create(
model=OPENAI_MODEL,
messages=[{"role": "user", "content": prompt}]
)
return response.choices[0].message.content.strip()
except Exception as e:
# 生成失败时返回默认话术
default_map = {
"data_abnormal": f"【数据异常提醒】{datetime.now().strftime('%Y-%m-%d %H:%M')}发现{len(content)}条异常数据,请及时登录看板查看并处理。",
"task_overdue": f"【任务逾期提醒】{content['task_name']}已逾期,请尽快完成并同步至OA。",
"report_remind": f"【日报提交提醒】今日日报提交截止时间为{content['deadline']},请及时提交至OA。"
}
return default_map.get(alert_type, f"【办公提醒】{content}")
====================== 多渠道提醒函数 ======================
def send_wecom_alert(userid, content):
"""企业微信提醒"""
from wecom_login import get_wechat_access_token
access_token = get_wechat_access_token(WECOM_APP_SECRET)
if not access_token:
return False, "获取企业微信access_token失败"
wecom_params = {
"touser": userid,
"msgtype": "text",
"agentid": WECOM_AGENT_ID,
"text": {"content": content},
"safe": 0
}
response = requests.post(
f"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}",
json=wecom_params
)
result = response.json()
if result.get("errcode") != 0:
return False, f"企业微信提醒发送失败:{result.get('errmsg')}"
return True, "企业微信提醒发送成功"
def send_sms_alert(phone, content):
"""短信提醒(阿里云)"""
client = AcsClient(SMS_ACCESS_KEY_ID, SMS_ACCESS_KEY_SECRET, "cn-hangzhou")
request = SendSmsRequest()
request.set_accept_format("json")
request.set_PhoneNumbers(phone)
request.set_SignName(SMS_SIGN_NAME)
request.set_TemplateCode(SMS_TEMPLATE_ID)
request.set_TemplateParam(json.dumps({"content": content}))
try:
response = client.do_action_with_exception(request)
result = json.loads(response)
if result.get("Code") == "OK":
return True, "短信提醒发送成功"
return False, f"短信提醒发送失败:{result.get('Message')}"
except Exception as e:
return False, f"短信提醒发送异常:{str(e)}"
def send_email_alert(email, subject, content):
"""邮件提醒"""
try:
yag = yagmail.SMTP(user=EMAIL_USER, password=EMAIL_PASSWORD, host=EMAIL_HOST, port=EMAIL_PORT)
yag.send(to=email, subject=subject, contents=content)
yag.close()
return True, "邮件提醒发送成功"
except Exception as e:
return False, f"邮件提醒发送失败:{str(e)}"
====================== 核心提醒入口 ======================
def send_ai_alert(alert_type, content, urgency="medium", receivers=None):
"""AI智能提醒核心入口"""
生成提醒话术
alert_content = generate_alert_content(alert_type, content)
确定提醒渠道
channels = URGENCY_CHANNEL_MAP.get(urgency, ["wecom"])
确定接收人(默认管理员)
if not receivers:
receivers = User.query.filter_by(role="leader").all() # 管理员为默认接收人
elif isinstance(receivers, str):
receivers = [User.query.filter_by(username=receivers).first()]
for receiver in receivers:
if not receiver:
continue
# 记录提醒结果
alert_result = []
# 按渠道发送提醒
for channel in channels:
if channel == "wecom" and receiver.wecom_id:
success, msg = send_wecom_alert(receiver.wecom_id, alert_content)
alert_result.append(f"企业微信:{msg}")
elif channel == "sms" and receiver.phone:
success, msg = send_sms_alert(receiver.phone, alert_content)
alert_result.append(f"短信:{msg}")
elif channel == "email" and receiver.email:
subject = {"data_abnormal": "【数据异常提醒】", "task_overdue": "【任务逾期提醒】", "report_remind": "【日报提交提醒】"}.get(alert_type, "【办公提醒】")
success, msg = send_email_alert(receiver.email, subject, alert_content)
alert_result.append(f"邮件:{msg}")
# 保存流程日志
save_process_log({
"process_type": "ai_alert",
"process_name": f"{get_alert_type_name(alert_type)}提醒",
"content": {"alert_content": alert_content, "result": alert_result},
"status": "success" if any("成功" in res for res in alert_result) else "fail",
"channel": ",".join(channels),
"receiver": receiver.username
})
return True, "AI提醒已发起"
====================== 辅助函数 ======================
def get_alert_type_name(alert_type):
"""获取提醒类型中文名称"""
type_map = {"data_abnormal": "数据异常", "task_overdue": "任务逾期", "report_remind": "日报提交"}
return type_map.get(alert_type, "未知")
- 第五步:集成流程可视化监控,实现任务干预
在app.py中新增/修改以下内容
from scheduler import scheduler_bp, load_running_tasks
from oa_process import oa_bp
from ai_alert import send_ai_alert
from models import ScheduledTask, ProcessLog
from datetime import datetime, timedelta
注册蓝图
app.register_blueprint(scheduler_bp)
app.register_blueprint(oa_bp)
应用启动时加载运行中的定时任务
with app.app_context():
load_running_tasks()
====================== 流程监控接口 ======================
@app.route("/process/monitor", methods=["GET"])
@login_required
def process_monitor():
"""获取流程监控数据(定时任务+OA流程+AI提醒)"""
获取定时任务状态
tasks = ScheduledTask.query.all()
task_data = []
for task in tasks:
task_data.append({
"task_id": task.id,
"task_name": task.task_name,
"task_type": get_task_type_name(task.task_type),
"status": task.status,
"status_text": {"running": "运行中", "paused": "已暂停", "stopped": "已停止"}[task.status],
"last_execute": task.last_execute_time.strftime("%Y-%m-%d %H:%M:%S") if task.last_execute_time else "未执行",
"next_execute": task.next_execute_time.strftime("%Y-%m-%d %H:%M:%S") if task.next_execute_time else "未设置",
"result": {"success": "成功", "fail": "失败", "none": "未执行"}[task.execute_result]
})
# 获取近7天流程日志
seven_days_ago = datetime.now() - timedelta(days=7)
logs = ProcessLog.query.filter(ProcessLog.execute_time >= seven_days_ago).order_by(ProcessLog.execute_time.desc()).limit(50).all()
log_data = []
for log in logs:
log_data.append({
"log_id": log.id,
"process_type": get_process_type_name(log.process_type),
"process_name": log.process_name,
"status": {"success": "成功", "fail": "失败", "processing": "处理中"}[log.status],
"channel": log.channel if log.channel else "无",
"receiver": log.receiver,
"execute_time": log.execute_time.strftime("%Y-%m-%d %H:%M:%S"),
"content": log.get_content()
})
return jsonify({
"success": True,
"data": {
"tasks": task_data,
"logs": log_data
}
})
====================== 手动触发任务接口 ======================
@app.route("/task/manual/trigger", methods=["POST"])
@login_required
def manual_trigger_task():
"""手动触发定时任务"""
if current_user.role != "leader":
return jsonify({"success": False, "error": "仅管理员可手动触发任务"}), 403
data = request.get_json()
task_id = data.get("task_id")
task = ScheduledTask.query.get(task_id)
if not task:
return jsonify({"success": False, "error": "未找到对应任务"}), 404
# 手动执行任务
from scheduler import execute_task
execute_task(task_id)
return jsonify({"success": True, "msg": "任务已手动触发,正在执行"})
====================== 辅助函数 ======================
def get_task_type_name(task_type):
"""复用任务类型中文名称函数"""
from scheduler import get_task_type_name as get_task_name
return get_task_name(task_type)
def get_process_type_name(process_type):
"""获取流程类型中文名称"""
type_map = {"scheduled_task": "定时任务", "oa_process": "OA流程", "ai_alert": "AI提醒"}
return type_map.get(process_type, "未知流程")
{% block content %}
流程自动化监控
刷新