新增 消息定时推送功能

This commit is contained in:
liuwei
2025-06-10 11:24:08 +08:00
parent a83c627747
commit a7e40784a7
8 changed files with 2015 additions and 3 deletions

View File

@@ -0,0 +1,220 @@
from flask import Blueprint, render_template, jsonify, request, current_app
from .auth import login_required
from loguru import logger
import json
import uuid
from datetime import datetime
# 创建消息推送管理蓝图
message_push_bp = Blueprint('message_push', __name__, url_prefix='/message_push')
# 消息推送管理页面
@message_push_bp.route('/')
@login_required
def message_push_management():
"""消息推送管理页面"""
return render_template('message_push_management.html')
# API路由
@message_push_bp.route('/api/tasks', methods=['GET'])
@login_required
def api_tasks_list():
"""获取任务列表API"""
try:
# 获取查询参数
status = request.args.get('status')
start_time = request.args.get('start_time')
end_time = request.args.get('end_time')
page = int(request.args.get('page', 1))
limit = int(request.args.get('limit', 20))
# 获取任务列表
db = current_app.dashboard_server.task_db
tasks, total = db.get_tasks_list(status, start_time, end_time, page, limit)
return jsonify({
"success": True,
"data": {
"tasks": tasks,
"total": total,
"page": page,
"limit": limit
}
})
except Exception as e:
logger.error(f"获取任务列表失败: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@message_push_bp.route('/api/tasks', methods=['POST'])
@login_required
def api_create_task():
"""创建任务API"""
try:
data = request.json
if not data:
return jsonify({"success": False, "error": "无效的请求数据"}), 400
# 生成任务ID
data['task_id'] = str(uuid.uuid4())
data['creator_id'] = request.user.get('id')
# 创建任务
db = current_app.dashboard_server.task_db
task = db.create_task(data)
if not task:
return jsonify({"success": False, "error": "创建任务失败"}), 500
return jsonify({
"success": True,
"data": {
"task": task
}
})
except Exception as e:
logger.error(f"创建任务失败: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@message_push_bp.route('/api/tasks/<task_id>', methods=['PUT'])
@login_required
def api_update_task(task_id):
"""更新任务API"""
try:
data = request.json
if not data:
return jsonify({"success": False, "error": "无效的请求数据"}), 400
# 获取任务
db = current_app.dashboard_server.task_db
task = db.get_task(task_id)
if not task:
return jsonify({"success": False, "error": "任务不存在"}), 404
# 更新任务
if not db.update_task(task_id, data):
return jsonify({"success": False, "error": "更新任务失败"}), 500
# 获取更新后的任务
updated_task = db.get_task(task_id)
return jsonify({
"success": True,
"data": {
"task": updated_task
}
})
except Exception as e:
logger.error(f"更新任务失败: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@message_push_bp.route('/api/tasks/<task_id>', methods=['DELETE'])
@login_required
def api_delete_task(task_id):
"""删除任务API"""
try:
# 获取任务
db = current_app.dashboard_server.task_db
task = db.get_task(task_id)
if not task:
return jsonify({"success": False, "error": "任务不存在"}), 404
# 删除任务
if not db.delete_task(task_id):
return jsonify({"success": False, "error": "删除任务失败"}), 500
return jsonify({
"success": True,
"message": "任务已删除"
})
except Exception as e:
logger.error(f"删除任务失败: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@message_push_bp.route('/api/tasks/<task_id>/pause', methods=['POST'])
@login_required
def api_pause_task(task_id):
"""暂停任务API"""
try:
# 获取任务
db = current_app.dashboard_server.task_db
task = db.get_task(task_id)
if not task:
return jsonify({"success": False, "error": "任务不存在"}), 404
# 暂停任务
if not db.update_task(task_id, {'status': 'paused'}):
return jsonify({"success": False, "error": "暂停任务失败"}), 500
return jsonify({
"success": True,
"message": "任务已暂停"
})
except Exception as e:
logger.error(f"暂停任务失败: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@message_push_bp.route('/api/tasks/<task_id>/resume', methods=['POST'])
@login_required
def api_resume_task(task_id):
"""恢复任务API"""
try:
# 获取任务
db = current_app.dashboard_server.task_db
task = db.get_task(task_id)
if not task:
return jsonify({"success": False, "error": "任务不存在"}), 404
# 恢复任务
if not db.update_task(task_id, {'status': 'scheduled'}):
return jsonify({"success": False, "error": "恢复任务失败"}), 500
return jsonify({
"success": True,
"message": "任务已恢复"
})
except Exception as e:
logger.error(f"恢复任务失败: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@message_push_bp.route('/api/tasks/<task_id>/preview', methods=['POST'])
@login_required
def api_preview_task(task_id):
"""预览任务API"""
try:
# 获取任务
db = current_app.dashboard_server.task_db
task = db.get_task(task_id)
if not task:
return jsonify({"success": False, "error": "任务不存在"}), 404
# 发送预览
message_push = current_app.dashboard_server.message_push_task.message_push
if not message_push.send_preview(task, [request.user.get('id')]):
return jsonify({"success": False, "error": "发送预览失败"}), 500
return jsonify({
"success": True,
"message": "预览已发送"
})
except Exception as e:
logger.error(f"发送预览失败: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@message_push_bp.route('/api/tasks/<task_id>/logs', methods=['GET'])
@login_required
def api_task_logs(task_id):
"""获取任务日志API"""
try:
# 获取查询参数
page = int(request.args.get('page', 1))
limit = int(request.args.get('limit', 20))
# 查询日志
db = current_app.dashboard_server.task_db
logs_data = db.get_task_logs_with_pagination(task_id, page, limit)
return jsonify({
"success": True,
"data": logs_data
})
except Exception as e:
logger.error(f"获取任务日志失败: {e}")
return jsonify({"success": False, "error": str(e)}), 500

View File

@@ -5,14 +5,15 @@
import os
import sys
import threading
import toml
from flask import Flask, send_from_directory
from loguru import logger
from db.contacts_db import ContactsDBOperator
from db.message_storage import MessageStorageDB
from db.stats_db import StatsDBOperator
from flask import Flask, send_from_directory
import toml
from db.task_db import TaskDBOperator
from wechat_ipad import WechatAPIClient
# 添加项目根目录到系统路径,确保可以导入项目模块
@@ -42,6 +43,7 @@ class DashboardServer:
self.stats_db = StatsDBOperator(self.db_manager)
self.message_storage = MessageStorageDB(self.db_manager)
self.contact_db: ContactsDBOperator = ContactsDBOperator(self.db_manager)
self.task_db: TaskDBOperator = TaskDBOperator(self.db_manager)
# 获取联系人管理器实例
self.contact_manager = robot_instance.contact_manager
self.plugin_manager = robot_instance.plugin_manager

View File

@@ -0,0 +1,439 @@
{% extends "base.html" %}
{% block title %}消息推送管理{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">消息推送管理</h3>
<div class="card-tools">
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#createTaskModal">
<i class="fas fa-plus"></i> 新建任务
</button>
</div>
</div>
<div class="card-body">
<!-- 搜索栏 -->
<div class="row mb-3">
<div class="col-md-3">
<select class="form-control" id="statusFilter">
<option value="">全部状态</option>
<option value="draft">草稿</option>
<option value="scheduled">已排期</option>
<option value="paused">已暂停</option>
<option value="completed">已完成</option>
</select>
</div>
<div class="col-md-3">
<input type="date" class="form-control" id="startTimeFilter" placeholder="开始时间">
</div>
<div class="col-md-3">
<input type="date" class="form-control" id="endTimeFilter" placeholder="结束时间">
</div>
<div class="col-md-3">
<button type="button" class="btn btn-info" id="searchBtn">
<i class="fas fa-search"></i> 搜索
</button>
<button type="button" class="btn btn-secondary" id="resetBtn">
<i class="fas fa-redo"></i> 重置
</button>
</div>
</div>
<!-- 任务列表 -->
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>任务ID</th>
<th>任务名称</th>
<th>状态</th>
<th>计划时间</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="taskList">
<!-- 任务列表将通过JavaScript动态加载 -->
</tbody>
</table>
<!-- 分页 -->
<div class="row">
<div class="col-md-6">
<div class="dataTables_info" id="taskInfo" role="status" aria-live="polite">
显示 0 到 0 条,共 0 条记录
</div>
</div>
<div class="col-md-6">
<div class="dataTables_paginate paging_simple_numbers" id="taskPagination">
<!-- 分页将通过JavaScript动态加载 -->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 新建任务模态框 -->
<div class="modal fade" id="createTaskModal" tabindex="-1" role="dialog" aria-labelledby="createTaskModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="createTaskModalLabel">新建任务</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form id="createTaskForm">
<div class="form-group">
<label for="taskName">任务名称</label>
<input type="text" class="form-control" id="taskName" name="task_name" required>
</div>
<div class="form-group">
<label for="scheduleTime">计划时间</label>
<input type="datetime-local" class="form-control" id="scheduleTime" name="schedule_time" required>
</div>
<div class="form-group">
<label for="groups">目标群组</label>
<select class="form-control" id="groups" name="groups" multiple required>
<!-- 群组列表将通过JavaScript动态加载 -->
</select>
</div>
<div class="form-group">
<label for="contentText">文本内容</label>
<textarea class="form-control" id="contentText" name="content_text" rows="3"></textarea>
</div>
<div class="form-group">
<label for="contentImage">图片内容</label>
<input type="file" class="form-control-file" id="contentImage" name="content_image">
</div>
<div class="form-group">
<label for="contentLink">链接内容</label>
<input type="url" class="form-control" id="contentLink" name="content_link">
</div>
<div class="form-group">
<label for="contentMiniprogram">小程序内容</label>
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control" id="miniprogramTitle" name="miniprogram_title" placeholder="标题">
</div>
<div class="col-md-6">
<input type="text" class="form-control" id="miniprogramPath" name="miniprogram_path" placeholder="路径">
</div>
</div>
</div>
<div class="form-group">
<label for="previewRecipients">预览接收人</label>
<select class="form-control" id="previewRecipients" name="preview_recipients" multiple>
<!-- 用户列表将通过JavaScript动态加载 -->
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="saveTaskBtn">保存</button>
</div>
</div>
</div>
</div>
<!-- 任务日志模态框 -->
<div class="modal fade" id="taskLogsModal" tabindex="-1" role="dialog" aria-labelledby="taskLogsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="taskLogsModalLabel">任务日志</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<table class="table table-bordered">
<thead>
<tr>
<th>时间</th>
<th>操作</th>
<th>操作人</th>
<th>变更内容</th>
</tr>
</thead>
<tbody id="taskLogsList">
<!-- 日志列表将通过JavaScript动态加载 -->
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
// 全局变量
let currentPage = 1;
let pageSize = 20;
let totalPages = 0;
// 加载任务列表
function loadTasks(page = 1) {
const status = $('#statusFilter').val();
const startTime = $('#startTimeFilter').val();
const endTime = $('#endTimeFilter').val();
$.get('/message_push/api/tasks', {
page: page,
limit: pageSize,
status: status,
start_time: startTime,
end_time: endTime
}, function(response) {
if (response.success) {
const { tasks, total } = response.data;
totalPages = Math.ceil(total / pageSize);
// 渲染任务列表
let html = '';
tasks.forEach(task => {
html += `
<tr>
<td>${task.task_id}</td>
<td>${task.task_name}</td>
<td>${getStatusText(task.status)}</td>
<td>${formatDateTime(task.schedule_time)}</td>
<td>${formatDateTime(task.created_at)}</td>
<td>
<button class="btn btn-sm btn-info" onclick="previewTask('${task.task_id}')">
<i class="fas fa-eye"></i> 预览
</button>
<button class="btn btn-sm btn-warning" onclick="editTask('${task.task_id}')">
<i class="fas fa-edit"></i> 编辑
</button>
${task.status === 'scheduled' ? `
<button class="btn btn-sm btn-secondary" onclick="pauseTask('${task.task_id}')">
<i class="fas fa-pause"></i> 暂停
</button>
` : task.status === 'paused' ? `
<button class="btn btn-sm btn-success" onclick="resumeTask('${task.task_id}')">
<i class="fas fa-play"></i> 恢复
</button>
` : ''}
<button class="btn btn-sm btn-danger" onclick="deleteTask('${task.task_id}')">
<i class="fas fa-trash"></i> 删除
</button>
<button class="btn btn-sm btn-primary" onclick="viewLogs('${task.task_id}')">
<i class="fas fa-history"></i> 日志
</button>
</td>
</tr>
`;
});
$('#taskList').html(html);
// 更新分页信息
updatePagination(page, total);
} else {
showError('加载任务列表失败:' + response.error);
}
});
}
// 更新分页信息
function updatePagination(currentPage, total) {
// 更新信息文本
const start = (currentPage - 1) * pageSize + 1;
const end = Math.min(currentPage * pageSize, total);
$('#taskInfo').text(`显示 ${start}${end} 条,共 ${total} 条记录`);
// 更新分页按钮
let html = '<ul class="pagination">';
// 上一页
html += `
<li class="page-item ${currentPage === 1 ? 'disabled' : ''}">
<a class="page-link" href="#" onclick="return loadTasks(${currentPage - 1})">上一页</a>
</li>
`;
// 页码
for (let i = 1; i <= totalPages; i++) {
if (i === 1 || i === totalPages || (i >= currentPage - 2 && i <= currentPage + 2)) {
html += `
<li class="page-item ${i === currentPage ? 'active' : ''}">
<a class="page-link" href="#" onclick="return loadTasks(${i})">${i}</a>
</li>
`;
} else if (i === currentPage - 3 || i === currentPage + 3) {
html += '<li class="page-item disabled"><a class="page-link">...</a></li>';
}
}
// 下一页
html += `
<li class="page-item ${currentPage === totalPages ? 'disabled' : ''}">
<a class="page-link" href="#" onclick="return loadTasks(${currentPage + 1})">下一页</a>
</li>
`;
html += '</ul>';
$('#taskPagination').html(html);
}
// 工具函数
function getStatusText(status) {
const statusMap = {
'draft': '草稿',
'scheduled': '已排期',
'paused': '已暂停',
'completed': '已完成'
};
return statusMap[status] || status;
}
function formatDateTime(datetime) {
return new Date(datetime).toLocaleString();
}
function showError(message) {
toastr.error(message);
}
function showSuccess(message) {
toastr.success(message);
}
// 事件处理
$('#searchBtn').click(function() {
loadTasks(1);
});
$('#resetBtn').click(function() {
$('#statusFilter').val('');
$('#startTimeFilter').val('');
$('#endTimeFilter').val('');
loadTasks(1);
});
$('#saveTaskBtn').click(function() {
const formData = new FormData($('#createTaskForm')[0]);
const data = {};
formData.forEach((value, key) => {
if (key === 'groups' || key === 'preview_recipients') {
data[key] = Array.from($(`#${key}`).val());
} else {
data[key] = value;
}
});
// 处理小程序内容
data.content_miniprogram = {
title: $('#miniprogramTitle').val(),
path: $('#miniprogramPath').val()
};
$.ajax({
url: '/message_push/api/tasks',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(data),
success: function(response) {
if (response.success) {
showSuccess('创建任务成功');
$('#createTaskModal').modal('hide');
loadTasks(1);
} else {
showError('创建任务失败:' + response.error);
}
}
});
});
// 初始化
loadTasks(1);
});
// 任务操作函数
function previewTask(taskId) {
$.post(`/message_push/api/tasks/${taskId}/preview`, function(response) {
if (response.success) {
showSuccess('预览已发送');
} else {
showError('发送预览失败:' + response.error);
}
});
}
function editTask(taskId) {
// TODO: 实现编辑任务功能
}
function pauseTask(taskId) {
$.post(`/message_push/api/tasks/${taskId}/pause`, function(response) {
if (response.success) {
showSuccess('任务已暂停');
loadTasks(currentPage);
} else {
showError('暂停任务失败:' + response.error);
}
});
}
function resumeTask(taskId) {
$.post(`/message_push/api/tasks/${taskId}/resume`, function(response) {
if (response.success) {
showSuccess('任务已恢复');
loadTasks(currentPage);
} else {
showError('恢复任务失败:' + response.error);
}
});
}
function deleteTask(taskId) {
if (confirm('确定要删除这个任务吗?')) {
$.ajax({
url: `/message_push/api/tasks/${taskId}`,
type: 'DELETE',
success: function(response) {
if (response.success) {
showSuccess('任务已删除');
loadTasks(currentPage);
} else {
showError('删除任务失败:' + response.error);
}
}
});
}
}
function viewLogs(taskId) {
$.get(`/message_push/api/tasks/${taskId}/logs`, function(response) {
if (response.success) {
const { logs } = response.data;
let html = '';
logs.forEach(log => {
html += `
<tr>
<td>${formatDateTime(log.timestamp)}</td>
<td>${log.action}</td>
<td>${log.operator_id}</td>
<td>${JSON.stringify(log.changes)}</td>
</tr>
`;
});
$('#taskLogsList').html(html);
$('#taskLogsModal').modal('show');
} else {
showError('获取任务日志失败:' + response.error);
}
});
}
</script>
{% endblock %}