新增消息类型:语音,视频

This commit is contained in:
liuwei
2025-06-12 11:37:20 +08:00
parent 0e198c7c09
commit 0e2169f295
4 changed files with 154 additions and 15 deletions

View File

@@ -23,8 +23,12 @@ message_thread_pool = ThreadPoolExecutor(max_workers=10, thread_name_prefix="mes
shared_loop = None shared_loop = None
loop_lock = threading.Lock() 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(): def get_or_create_loop():
@@ -69,8 +73,9 @@ def send_message_in_thread(func, *args, **kwargs):
message_thread_pool.submit(run) message_thread_pool.submit(run)
def allowed_file(filename): def allowed_file(filename, file_type='image'):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS """检查文件类型是否允许上传"""
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']) @message_push_bp.route('/api/upload', methods=['POST'])
def upload_file(): def upload_file():
"""处理图片上传""" """处理文件上传"""
if 'file' not in request.files: if 'file' not in request.files:
return jsonify({ return jsonify({
'success': False, 'success': False,
@@ -517,7 +522,14 @@ def upload_file():
'message': '没有选择文件' '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) filename = secure_filename(file.filename)
# 生成唯一文件名 # 生成唯一文件名

View File

@@ -339,6 +339,36 @@
<img width="100%" :src="previewUrl" alt="Preview"> <img width="100%" :src="previewUrl" alt="Preview">
</el-dialog> </el-dialog>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="语音" name="voice">
<el-upload
class="upload-demo"
action="/message_push/api/upload"
{% raw %}:on-success="handleVoiceSuccess"
:before-upload="beforeVoiceUpload"
:on-preview="handleVoicePreview"
:file-list="voiceList" {% endraw %}>
<el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">只能上传mp3/wav文件且不超过10MB</div>
</el-upload>
<el-dialog :visible.sync="voicePreviewVisible" append-to-body>
<audio controls :src="voicePreviewUrl" style="width: 100%"></audio>
</el-dialog>
</el-tab-pane>
<el-tab-pane label="视频" name="video">
<el-upload
class="upload-demo"
action="/message_push/api/upload"
{% raw %}:on-success="handleVideoSuccess"
:before-upload="beforeVideoUpload"
:on-preview="handleVideoPreview"
:file-list="videoList" {% endraw %}>
<el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">只能上传mp4文件且不超过20MB</div>
</el-upload>
<el-dialog :visible.sync="videoPreviewVisible" append-to-body>
<video controls :src="videoPreviewUrl" style="width: 100%"></video>
</el-dialog>
</el-tab-pane>
<el-tab-pane label="链接" name="link"> <el-tab-pane label="链接" name="link">
<el-form-item label="链接标题"> <el-form-item label="链接标题">
<el-input {% raw %}v-model="taskForm.content_link.title" {% endraw %}></el-input> <el-input {% raw %}v-model="taskForm.content_link.title" {% endraw %}></el-input>
@@ -441,6 +471,8 @@ new Vue({
groups: [], groups: [],
content_text: '', content_text: '',
content_image: '', content_image: '',
content_voice: '',
content_video: '',
content_link: { content_link: {
title: '', title: '',
des: '', des: '',
@@ -498,6 +530,12 @@ new Vue({
previewVisible: false, previewVisible: false,
previewUrl: '', previewUrl: '',
thumbnailList: [], thumbnailList: [],
voiceList: [],
voicePreviewVisible: false,
voicePreviewUrl: '',
videoList: [],
videoPreviewVisible: false,
videoPreviewUrl: '',
isEdit: false isEdit: false
} }
}, },
@@ -599,6 +637,8 @@ new Vue({
groups: [], groups: [],
content_text: '', content_text: '',
content_image: '', content_image: '',
content_voice: '',
content_video: '',
content_link: { content_link: {
title: '', title: '',
des: '', des: '',
@@ -914,6 +954,76 @@ new Vue({
this.previewVisible = true; 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) { getStatusType(status) {
const typeMap = { const typeMap = {

View File

@@ -32,6 +32,8 @@ class TaskDBOperator(BaseDBOperator):
content_image VARCHAR(255), content_image VARCHAR(255),
content_link JSON, content_link JSON,
content_miniprogram JSON, content_miniprogram JSON,
content_voice VARCHAR(255), -- 语音消息文件路径
content_video VARCHAR(255), -- 视频消息文件路径
groups JSON, groups JSON,
priority ENUM('high', 'medium', 'low') DEFAULT 'medium', priority ENUM('high', 'medium', 'low') DEFAULT 'medium',
status ENUM('draft', 'scheduled', 'running', 'completed', 'failed', 'paused') DEFAULT 'draft', status ENUM('draft', 'scheduled', 'running', 'completed', 'failed', 'paused') DEFAULT 'draft',
@@ -97,10 +99,10 @@ class TaskDBOperator(BaseDBOperator):
INSERT INTO t_push_tasks ( INSERT INTO t_push_tasks (
task_id, name, schedule_type, schedule_time, recurring_interval, task_id, name, schedule_type, schedule_time, recurring_interval,
recurring_end, recurring_time, weekly_days, monthly_day, content_text, recurring_end, recurring_time, weekly_days, monthly_day, content_text,
content_image, content_link, content_miniprogram, groups, priority, content_image, content_link, content_miniprogram, content_voice, content_video,
status, creator_id, preview_recipients groups, priority, status, creator_id, preview_recipients
) VALUES ( ) 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字符串 # 将字典和列表类型转换为JSON字符串
@@ -134,6 +136,8 @@ class TaskDBOperator(BaseDBOperator):
task_data.get('content_image'), task_data.get('content_image'),
content_link, content_link,
content_miniprogram, content_miniprogram,
task_data.get('content_voice'),
task_data.get('content_video'),
groups, groups,
task_data.get('priority', 'medium'), task_data.get('priority', 'medium'),
task_data.get('status', 'draft'), task_data.get('status', 'draft'),

View File

@@ -35,7 +35,7 @@ class MessagePushTask(MessagePluginInterface):
@property @property
def description(self) -> str: def description(self) -> str:
return "提供消息推送功能,支持定时推送群发消息" return "提供消息推送功能,支持定时推送群发消息、语音消息和视频消息"
@property @property
def author(self) -> str: def author(self) -> str:
@@ -124,6 +124,8 @@ class MessagePushTask(MessagePluginInterface):
if roomid and gbm.get_group_permission(roomid, self.feature) == PermissionStatus.DISABLED: if roomid and gbm.get_group_permission(roomid, self.feature) == PermissionStatus.DISABLED:
return False, "没有权限" return False, "没有权限"
return False, None
def _get_execution_key(self, task_id: str, hour: int, minute: int) -> str: def _get_execution_key(self, task_id: str, hour: int, minute: int) -> str:
"""生成任务执行记录的唯一键""" """生成任务执行记录的唯一键"""
now = datetime.now() now = datetime.now()
@@ -192,11 +194,8 @@ class MessagePushTask(MessagePluginInterface):
content_image = task.get('content_image') content_image = task.get('content_image')
content_link = task.get('content_link') content_link = task.get('content_link')
content_miniprogram = task.get('content_miniprogram') content_miniprogram = task.get('content_miniprogram')
groups = task.get('groups', []) content_voice = task.get('content_voice') # 语音消息
try: content_video = task.get('content_video') # 视频消息
groups = json.loads(groups)
except json.JSONDecodeError:
raise Exception("无发送清单")
# 记录任务开始执行 # 记录任务开始执行
self.db.log_task_action({ self.db.log_task_action({
@@ -230,6 +229,12 @@ class MessagePushTask(MessagePluginInterface):
success_count = 0 success_count = 0
fail_count = 0 fail_count = 0
groups = task.get('groups', [])
try:
groups = json.loads(groups)
except json.JSONDecodeError:
raise Exception("无发送清单")
for group_id in groups: for group_id in groups:
try: try:
# 发送文本消息 # 发送文本消息
@@ -251,6 +256,14 @@ class MessagePushTask(MessagePluginInterface):
) )
await self.bot.send_link_xml_message(xml_content, group_id) 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 success_count += 1
except Exception as e: except Exception as e: