新增消息类型:语音,视频
This commit is contained in:
@@ -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)
|
||||||
# 生成唯一文件名
|
# 生成唯一文件名
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user