diff --git a/admin/dashboard/blueprints/message_push.py b/admin/dashboard/blueprints/message_push.py
index 07ae997..c90eabe 100644
--- a/admin/dashboard/blueprints/message_push.py
+++ b/admin/dashboard/blueprints/message_push.py
@@ -23,8 +23,12 @@ message_thread_pool = ThreadPoolExecutor(max_workers=10, thread_name_prefix="mes
shared_loop = None
loop_lock = threading.Lock()
-# 允许的图片文件扩展名
-ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
+# 允许的文件扩展名
+ALLOWED_EXTENSIONS = {
+ 'image': {'png', 'jpg', 'jpeg', 'gif'},
+ 'voice': {'mp3', 'wav'},
+ 'video': {'mp4'}
+}
def get_or_create_loop():
@@ -69,8 +73,9 @@ def send_message_in_thread(func, *args, **kwargs):
message_thread_pool.submit(run)
-def allowed_file(filename):
- return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
+def allowed_file(filename, file_type='image'):
+ """检查文件类型是否允许上传"""
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS.get(file_type, set())
# 消息推送管理页面
@@ -503,7 +508,7 @@ def api_statistics():
@message_push_bp.route('/api/upload', methods=['POST'])
def upload_file():
- """处理图片上传"""
+ """处理文件上传"""
if 'file' not in request.files:
return jsonify({
'success': False,
@@ -517,7 +522,14 @@ def upload_file():
'message': '没有选择文件'
})
- if file and allowed_file(file.filename):
+ # 根据文件类型检查
+ file_type = 'image' # 默认类型
+ if file.content_type.startswith('audio/'):
+ file_type = 'voice'
+ elif file.content_type.startswith('video/'):
+ file_type = 'video'
+
+ if file and allowed_file(file.filename, file_type):
# 生成安全的文件名
filename = secure_filename(file.filename)
# 生成唯一文件名
diff --git a/admin/dashboard/templates/message_push_management.html b/admin/dashboard/templates/message_push_management.html
index 8327b21..58faa6d 100644
--- a/admin/dashboard/templates/message_push_management.html
+++ b/admin/dashboard/templates/message_push_management.html
@@ -339,6 +339,36 @@
+
+
+ 点击上传
+ 只能上传mp3/wav文件,且不超过10MB
+
+
+
+
+
+
+
+ 点击上传
+ 只能上传mp4文件,且不超过20MB
+
+
+
+
+
@@ -441,6 +471,8 @@ new Vue({
groups: [],
content_text: '',
content_image: '',
+ content_voice: '',
+ content_video: '',
content_link: {
title: '',
des: '',
@@ -498,6 +530,12 @@ new Vue({
previewVisible: false,
previewUrl: '',
thumbnailList: [],
+ voiceList: [],
+ voicePreviewVisible: false,
+ voicePreviewUrl: '',
+ videoList: [],
+ videoPreviewVisible: false,
+ videoPreviewUrl: '',
isEdit: false
}
},
@@ -599,6 +637,8 @@ new Vue({
groups: [],
content_text: '',
content_image: '',
+ content_voice: '',
+ content_video: '',
content_link: {
title: '',
des: '',
@@ -914,6 +954,76 @@ new Vue({
this.previewVisible = true;
},
+ // 语音上传相关
+ handleVoiceSuccess(response, file) {
+ if (response.success) {
+ this.taskForm.content_voice = response.data.url;
+ const fileName = file.name;
+ this.voiceList = [{
+ name: fileName,
+ url: `/static/uploads/${response.data.url.split('/').pop()}`
+ }];
+ } else {
+ this.$message.error('上传失败');
+ }
+ },
+
+ beforeVoiceUpload(file) {
+ const isVoice = file.type === 'audio/mp3' || file.type === 'audio/wav';
+ const isLt10M = file.size / 1024 / 1024 < 10;
+
+ if (!isVoice) {
+ this.$message.error('只能上传mp3/wav文件!');
+ return false;
+ }
+ if (!isLt10M) {
+ this.$message.error('语音文件大小不能超过 10MB!');
+ return false;
+ }
+ return true;
+ },
+
+ handleVoicePreview(file) {
+ const fileName = file.url.split('/').pop();
+ this.voicePreviewUrl = `/static/uploads/${fileName}`;
+ this.voicePreviewVisible = true;
+ },
+
+ // 视频上传相关
+ handleVideoSuccess(response, file) {
+ if (response.success) {
+ this.taskForm.content_video = response.data.url;
+ const fileName = file.name;
+ this.videoList = [{
+ name: fileName,
+ url: `/static/uploads/${response.data.url.split('/').pop()}`
+ }];
+ } else {
+ this.$message.error('上传失败');
+ }
+ },
+
+ beforeVideoUpload(file) {
+ const isVideo = file.type === 'video/mp4';
+ const isLt20M = file.size / 1024 / 1024 < 20;
+
+ if (!isVideo) {
+ this.$message.error('只能上传mp4文件!');
+ return false;
+ }
+ if (!isLt20M) {
+ this.$message.error('视频文件大小不能超过 20MB!');
+ return false;
+ }
+ return true;
+ },
+
+ handleVideoPreview(file) {
+ const fileName = file.url.split('/').pop();
+ this.videoPreviewUrl = `/static/uploads/${fileName}`;
+ this.videoPreviewVisible = true;
+ },
+
// 工具函数
getStatusType(status) {
const typeMap = {
diff --git a/db/task_db.py b/db/task_db.py
index 2305c1b..1655060 100644
--- a/db/task_db.py
+++ b/db/task_db.py
@@ -32,6 +32,8 @@ class TaskDBOperator(BaseDBOperator):
content_image VARCHAR(255),
content_link JSON,
content_miniprogram JSON,
+ content_voice VARCHAR(255), -- 语音消息文件路径
+ content_video VARCHAR(255), -- 视频消息文件路径
groups JSON,
priority ENUM('high', 'medium', 'low') DEFAULT 'medium',
status ENUM('draft', 'scheduled', 'running', 'completed', 'failed', 'paused') DEFAULT 'draft',
@@ -97,10 +99,10 @@ class TaskDBOperator(BaseDBOperator):
INSERT INTO t_push_tasks (
task_id, name, schedule_type, schedule_time, recurring_interval,
recurring_end, recurring_time, weekly_days, monthly_day, content_text,
- content_image, content_link, content_miniprogram, groups, priority,
- status, creator_id, preview_recipients
+ content_image, content_link, content_miniprogram, content_voice, content_video,
+ groups, priority, status, creator_id, preview_recipients
) VALUES (
- %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s
+ %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s
)
"""
# 将字典和列表类型转换为JSON字符串
@@ -134,6 +136,8 @@ class TaskDBOperator(BaseDBOperator):
task_data.get('content_image'),
content_link,
content_miniprogram,
+ task_data.get('content_voice'),
+ task_data.get('content_video'),
groups,
task_data.get('priority', 'medium'),
task_data.get('status', 'draft'),
diff --git a/plugins/message_push_task/main.py b/plugins/message_push_task/main.py
index a31e8fb..d915f69 100644
--- a/plugins/message_push_task/main.py
+++ b/plugins/message_push_task/main.py
@@ -35,7 +35,7 @@ class MessagePushTask(MessagePluginInterface):
@property
def description(self) -> str:
- return "提供消息推送功能,支持定时推送和群发消息"
+ return "提供消息推送功能,支持定时推送、群发消息、语音消息和视频消息"
@property
def author(self) -> str:
@@ -124,6 +124,8 @@ class MessagePushTask(MessagePluginInterface):
if roomid and gbm.get_group_permission(roomid, self.feature) == PermissionStatus.DISABLED:
return False, "没有权限"
+ return False, None
+
def _get_execution_key(self, task_id: str, hour: int, minute: int) -> str:
"""生成任务执行记录的唯一键"""
now = datetime.now()
@@ -192,11 +194,8 @@ class MessagePushTask(MessagePluginInterface):
content_image = task.get('content_image')
content_link = task.get('content_link')
content_miniprogram = task.get('content_miniprogram')
- groups = task.get('groups', [])
- try:
- groups = json.loads(groups)
- except json.JSONDecodeError:
- raise Exception("无发送清单")
+ content_voice = task.get('content_voice') # 语音消息
+ content_video = task.get('content_video') # 视频消息
# 记录任务开始执行
self.db.log_task_action({
@@ -230,6 +229,12 @@ class MessagePushTask(MessagePluginInterface):
success_count = 0
fail_count = 0
+ groups = task.get('groups', [])
+ try:
+ groups = json.loads(groups)
+ except json.JSONDecodeError:
+ raise Exception("无发送清单")
+
for group_id in groups:
try:
# 发送文本消息
@@ -251,6 +256,14 @@ class MessagePushTask(MessagePluginInterface):
)
await self.bot.send_link_xml_message(xml_content, group_id)
+ # 发送语音消息
+ if content_voice:
+ await self.bot.send_voice_message(group_id, Path(content_voice))
+
+ # 发送视频消息
+ if content_video:
+ await self.bot.send_video_message(group_id, Path(content_video))
+
success_count += 1
except Exception as e: