From 0e2169f295daffbdfffcdbc56ff2ba01d1c4ab3e Mon Sep 17 00:00:00 2001 From: liuwei Date: Thu, 12 Jun 2025 11:37:20 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=B6=88=E6=81=AF=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=EF=BC=9A=E8=AF=AD=E9=9F=B3,=E8=A7=86=E9=A2=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/dashboard/blueprints/message_push.py | 24 +++- .../templates/message_push_management.html | 110 ++++++++++++++++++ db/task_db.py | 10 +- plugins/message_push_task/main.py | 25 +++- 4 files changed, 154 insertions(+), 15 deletions(-) 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 @@ Preview + + + 点击上传 +
只能上传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: