调整json格式存储link信息

This commit is contained in:
liuwei
2025-06-10 14:29:08 +08:00
parent 4286bb21a2
commit 1d61576ae7
6 changed files with 191 additions and 39 deletions

View File

@@ -8,6 +8,8 @@ from datetime import datetime
from flask import Blueprint, render_template, jsonify, request, current_app, session from flask import Blueprint, render_template, jsonify, request, current_app, session
from pathlib import Path from pathlib import Path
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from wechat_ipad.models.appmsg_xml import LINK_XML_NORMAL
from .auth import login_required from .auth import login_required
from loguru import logger from loguru import logger
@@ -268,32 +270,49 @@ def api_preview_task(task_id):
preview_user = session.get('username') preview_user = session.get('username')
if not preview_user: if not preview_user:
return jsonify({"success": False, "error": "未登录或会话已过期"}), 401 return jsonify({"success": False, "error": "未登录或会话已过期"}), 401
# 获取预览接收者并解析JSON # 获取预览接收者并解析JSON
preview_recipients_str = task.get("preview_recipients", "[]") preview_recipients_str = task.get("preview_recipients", "[]")
try: try:
preview_recipients = json.loads(preview_recipients_str) preview_recipients = json.loads(preview_recipients_str)
except json.JSONDecodeError: except json.JSONDecodeError:
return jsonify({"success": False, "error": "预览接收者格式错误"}), 400 return jsonify({"success": False, "error": "预览接收者格式错误"}), 400
if not preview_recipients: if not preview_recipients:
return jsonify({"success": False, "error": "未设置预览接收者"}), 400 return jsonify({"success": False, "error": "未设置预览接收者"}), 400
# 为每个接收者发送预览消息 # 为每个接收者发送预览消息
for recipient in preview_recipients: for recipient in preview_recipients:
try: try:
# 发送文本消息 # 发送文本消息
if task.get('content_text'): if task.get('content_text'):
send_message_in_thread(server.client.send_text_message, recipient, task['content_text']) send_message_in_thread(server.client.send_text_message, recipient, task['content_text'])
# 发送图片消息 # 发送图片消息
if task.get('content_image'): if task.get('content_image'):
send_message_in_thread(server.client.send_image_message, recipient, Path(task['content_image'])) send_message_in_thread(server.client.send_image_message, recipient, Path(task['content_image']))
# 发送链接消息 # 发送链接消息
if task.get('content_link'): if task.get('content_link'):
send_message_in_thread(server.client.send_link_message, recipient, task['content_link']) try:
link_data = json.loads(task['content_link'])
# content_link json 读取内容
xml_content = f"{LINK_XML_NORMAL}".format(title=link_data.get('title', ''),
des=link_data.get('des', ''),
url=link_data.get('url', ''),
thumburl=link_data.get('thumburl', '')
)
send_message_in_thread(
server.client.send_link_xml_message,
xml_content,
recipient
)
except json.JSONDecodeError:
logger.error(f"解析链接内容失败: {task['content_link']}")
continue
# # 发送小程序消息 # # 发送小程序消息
# if task.get('content_miniprogram'): # if task.get('content_miniprogram'):
# miniprogram = task['content_miniprogram'] # miniprogram = task['content_miniprogram']
@@ -347,17 +366,17 @@ def api_statistics():
try: try:
# 获取任务数据库实例 # 获取任务数据库实例
db = current_app.dashboard_server.task_db db = current_app.dashboard_server.task_db
# 获取各种状态的任务数量 # 获取各种状态的任务数量
total = db.get_tasks_count() total = db.get_tasks_count()
scheduled = db.get_tasks_count_by_status('scheduled') scheduled = db.get_tasks_count_by_status('scheduled')
paused = db.get_tasks_count_by_status('paused') paused = db.get_tasks_count_by_status('paused')
completed = db.get_tasks_count_by_status('completed') completed = db.get_tasks_count_by_status('completed')
failed = db.get_tasks_count_by_status('failed') failed = db.get_tasks_count_by_status('failed')
# 获取今日任务数量 # 获取今日任务数量
today = db.get_tasks_count_by_date(datetime.now().strftime('%Y-%m-%d')) today = db.get_tasks_count_by_date(datetime.now().strftime('%Y-%m-%d'))
return jsonify({ return jsonify({
"success": True, "success": True,
"data": { "data": {
@@ -382,28 +401,28 @@ def upload_file():
'success': False, 'success': False,
'message': '没有文件' 'message': '没有文件'
}) })
file = request.files['file'] file = request.files['file']
if file.filename == '': if file.filename == '':
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': '没有选择文件' 'message': '没有选择文件'
}) })
if file and allowed_file(file.filename): if file and allowed_file(file.filename):
# 生成安全的文件名 # 生成安全的文件名
filename = secure_filename(file.filename) filename = secure_filename(file.filename)
# 生成唯一文件名 # 生成唯一文件名
unique_filename = f"{uuid.uuid4().hex}_{filename}" unique_filename = f"{uuid.uuid4().hex}_{filename}"
# 确保上传目录存在 # 确保上传目录存在
upload_folder = os.path.join(current_app.root_path, 'static', 'uploads') upload_folder = os.path.join(current_app.root_path, 'static', 'uploads')
os.makedirs(upload_folder, exist_ok=True) os.makedirs(upload_folder, exist_ok=True)
# 保存文件 # 保存文件
file_path = os.path.join(upload_folder, unique_filename) file_path = os.path.join(upload_folder, unique_filename)
file.save(file_path) file.save(file_path)
# 返回文件的绝对路径 # 返回文件的绝对路径
return jsonify({ return jsonify({
'success': True, 'success': True,
@@ -411,7 +430,7 @@ def upload_file():
'url': file_path # 返回绝对路径 'url': file_path # 返回绝对路径
} }
}) })
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': '不支持的文件类型' 'message': '不支持的文件类型'
@@ -431,17 +450,17 @@ def audit_task(task_id):
'success': False, 'success': False,
'message': '任务不存在' 'message': '任务不存在'
}) })
# 检查任务状态 # 检查任务状态
if task['status'] != 'draft': if task['status'] != 'draft':
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': '只能审核草稿状态的任务' 'message': '只能审核草稿状态的任务'
}) })
# 更新任务状态为已排期 # 更新任务状态为已排期
db.update_task(task_id, {'status': 'scheduled'}) db.update_task(task_id, {'status': 'scheduled'})
# 记录操作日志 # 记录操作日志
db.log_task_action({ db.log_task_action({
'log_id': f"log_{datetime.now().strftime('%Y%m%d%H%M%S')}", 'log_id': f"log_{datetime.now().strftime('%Y%m%d%H%M%S')}",
@@ -450,7 +469,7 @@ def audit_task(task_id):
'user_id': session.get('user_id'), 'user_id': session.get('user_id'),
'changes': {'status': 'scheduled', 'action': 'audit'} 'changes': {'status': 'scheduled', 'action': 'audit'}
}) })
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': '审核成功' 'message': '审核成功'

View File

@@ -294,8 +294,26 @@
</el-dialog> </el-dialog>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="链接" name="link"> <el-tab-pane label="链接" name="link">
<el-form-item label="链接标题">
<el-input {% raw %}v-model="taskForm.content_link.title" {% endraw %}></el-input>
</el-form-item>
<el-form-item label="链接描述">
<el-input type="textarea" {% raw %}v-model="taskForm.content_link.des" {% endraw %}></el-input>
</el-form-item>
<el-form-item label="链接地址"> <el-form-item label="链接地址">
<el-input {% raw %}v-model="taskForm.content_link" {% endraw %}></el-input> <el-input {% raw %}v-model="taskForm.content_link.url" {% endraw %}></el-input>
</el-form-item>
<el-form-item label="缩略图">
<el-upload
class="upload-demo"
action="/message_push/api/upload"
{% raw %}:on-success="handleThumbnailSuccess"
:before-upload="beforeImageUpload"
:on-preview="handleThumbnailPreview"
:file-list="thumbnailList" {% endraw %}>
<el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">只能上传jpg/png文件且不超过2MB</div>
</el-upload>
</el-form-item> </el-form-item>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="小程序" name="miniprogram"> <el-tab-pane label="小程序" name="miniprogram">
@@ -377,7 +395,12 @@ new Vue({
groups: [], groups: [],
content_text: '', content_text: '',
content_image: '', content_image: '',
content_link: '', content_link: {
title: '',
des: '',
url: '',
thumburl: ''
},
content_miniprogram: { content_miniprogram: {
title: '', title: '',
path: '' path: ''
@@ -412,7 +435,8 @@ new Vue({
}, },
imageList: [], imageList: [],
previewVisible: false, previewVisible: false,
previewUrl: '' previewUrl: '',
thumbnailList: []
} }
}, },
mounted() { mounted() {
@@ -513,7 +537,12 @@ new Vue({
groups: [], groups: [],
content_text: '', content_text: '',
content_image: '', content_image: '',
content_link: '', content_link: {
title: '',
des: '',
url: '',
thumburl: ''
},
content_miniprogram: { content_miniprogram: {
title: '', title: '',
path: '' path: ''
@@ -529,7 +558,13 @@ new Vue({
this.$refs.taskForm.validate(async (valid) => { this.$refs.taskForm.validate(async (valid) => {
if (valid) { if (valid) {
try { try {
const response = await axios.post('/message_push/api/tasks', this.taskForm); // 确保链接内容是JSON字符串
const formData = { ...this.taskForm };
if (formData.content_link) {
formData.content_link = JSON.stringify(formData.content_link);
}
const response = await axios.post('/message_push/api/tasks', formData);
if (response.data.success) { if (response.data.success) {
this.$message.success('保存任务成功'); this.$message.success('保存任务成功');
this.taskDialogVisible = false; this.taskDialogVisible = false;
@@ -559,6 +594,31 @@ new Vue({
editTask(task) { editTask(task) {
this.dialogTitle = '编辑任务'; this.dialogTitle = '编辑任务';
this.taskForm = { ...task }; this.taskForm = { ...task };
// 处理链接内容
if (task.content_link) {
try {
this.taskForm.content_link = typeof task.content_link === 'string'
? JSON.parse(task.content_link)
: task.content_link;
// 如果有缩略图,显示缩略图
if (this.taskForm.content_link.thumburl) {
const fileName = this.taskForm.content_link.thumburl.split('/').pop();
this.thumbnailList = [{
name: '已上传缩略图',
url: `/static/uploads/${fileName}`
}];
}
} catch (e) {
console.error('解析链接内容失败:', e);
this.taskForm.content_link = {
title: '',
des: '',
url: '',
thumburl: ''
};
}
}
if (task.content_image) { if (task.content_image) {
// 编辑时显示图片 // 编辑时显示图片
const fileName = task.content_image.split('/').pop(); const fileName = task.content_image.split('/').pop();
@@ -728,6 +788,28 @@ new Vue({
this.previewVisible = true; this.previewVisible = true;
}, },
// 缩略图上传相关
handleThumbnailSuccess(response, file) {
if (response.success) {
this.taskForm.content_link.thumburl = response.data.url; // 存储绝对路径
// 显示时使用文件名
const fileName = file.name;
this.thumbnailList = [{
name: fileName,
url: `/static/uploads/${response.data.url.split('/').pop()}` // 显示时使用相对路径
}];
} else {
this.$message.error('上传失败');
}
},
handleThumbnailPreview(file) {
// 预览时使用相对路径
const fileName = file.url.split('/').pop();
this.previewUrl = `/static/uploads/${fileName}`;
this.previewVisible = true;
},
// 工具函数 // 工具函数
getStatusType(status) { getStatusType(status) {
const typeMap = { const typeMap = {

View File

@@ -27,7 +27,7 @@ class TaskDBOperator(BaseDBOperator):
recurring_end DATETIME DEFAULT NULL, recurring_end DATETIME DEFAULT NULL,
content_text TEXT(500), content_text TEXT(500),
content_image VARCHAR(255), content_image VARCHAR(255),
content_link VARCHAR(255), content_link JSON,
content_miniprogram JSON, content_miniprogram JSON,
groups JSON, groups JSON,
priority ENUM('high', 'medium', 'low') DEFAULT 'medium', priority ENUM('high', 'medium', 'low') DEFAULT 'medium',
@@ -47,8 +47,7 @@ class TaskDBOperator(BaseDBOperator):
action ENUM('create', 'update', 'delete', 'pause', 'resume') NOT NULL, action ENUM('create', 'update', 'delete', 'pause', 'resume') NOT NULL,
user_id VARCHAR(50) NOT NULL, user_id VARCHAR(50) NOT NULL,
changes JSON, changes JSON,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
FOREIGN KEY (task_id) REFERENCES t_push_tasks(task_id)
) )
""") """)
@@ -61,8 +60,7 @@ class TaskDBOperator(BaseDBOperator):
recipients JSON NOT NULL, recipients JSON NOT NULL,
validation JSON, validation JSON,
status ENUM('sent', 'confirmed', 'modified') DEFAULT 'sent', status ENUM('sent', 'confirmed', 'modified') DEFAULT 'sent',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP
FOREIGN KEY (task_id) REFERENCES t_push_tasks(task_id)
) )
""") """)
@@ -73,8 +71,7 @@ class TaskDBOperator(BaseDBOperator):
task_id VARCHAR(36) NOT NULL, task_id VARCHAR(36) NOT NULL,
user_id VARCHAR(50) NOT NULL, user_id VARCHAR(50) NOT NULL,
content TEXT NOT NULL, content TEXT NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
FOREIGN KEY (task_id) REFERENCES t_push_tasks(task_id)
) )
""") """)

View File

@@ -1,16 +1,15 @@
import xml.etree.ElementTree as ET
from datetime import datetime from datetime import datetime
from typing import Dict, Any, List, Optional, Tuple from typing import Dict, Any, List, Optional, Tuple
import xml.etree.ElementTree as ET
from base.plugin_common.message_plugin_interface import MessagePluginInterface from base.plugin_common.message_plugin_interface import MessagePluginInterface
from base.plugin_common.plugin_interface import PluginStatus from base.plugin_common.plugin_interface import PluginStatus
from db.connection import DBConnectionManager from db.connection import DBConnectionManager
from db.contacts_db import ContactsDBOperator from db.contacts_db import ContactsDBOperator
from utils.robot_cmd.robot_command import Feature, PermissionStatus, GroupBotManager from utils.robot_cmd.robot_command import PermissionStatus, GroupBotManager
from utils.wechat.contact_manager import ContactManager from utils.wechat.contact_manager import ContactManager
from wechat_ipad import WechatAPIClient from wechat_ipad import WechatAPIClient
from wechat_ipad.models.appmsg_xml import LINK_XML from wechat_ipad.models.appmsg_xml import LINK_XML_WELCOME
class GroupMemberChangePlugin(MessagePluginInterface): class GroupMemberChangePlugin(MessagePluginInterface):
@@ -141,7 +140,7 @@ class GroupMemberChangePlugin(MessagePluginInterface):
contact_db.save_chatroom_member_simple(roomid, member_details) contact_db.save_chatroom_member_simple(roomid, member_details)
except Exception as e: except Exception as e:
self.LOG.warning(f"新增群员信息失败: {e}") self.LOG.warning(f"新增群员信息失败: {e}")
xml_content = f"{LINK_XML}".format(nickname=nickname, now=now, head_url=head_url) xml_content = f"{LINK_XML_WELCOME}".format(nickname=nickname, now=now, head_url=head_url)
await bot.send_link_xml_message(xml_content, roomid) await bot.send_link_xml_message(xml_content, roomid)
return True, "已发送进群欢迎语" return True, "已发送进群欢迎语"

View File

@@ -13,6 +13,7 @@ from utils.decorator.async_job import async_job
from utils.decorator.plugin_decorators import plugin_stats_decorator from utils.decorator.plugin_decorators import plugin_stats_decorator
from utils.robot_cmd.robot_command import PermissionStatus, GroupBotManager from utils.robot_cmd.robot_command import PermissionStatus, GroupBotManager
from wechat_ipad import WechatAPIClient from wechat_ipad import WechatAPIClient
from wechat_ipad.models.appmsg_xml import LINK_XML_NORMAL
class MessagePushTask(MessagePluginInterface): class MessagePushTask(MessagePluginInterface):
@@ -174,7 +175,14 @@ class MessagePushTask(MessagePluginInterface):
# 发送链接消息 # 发送链接消息
if content_link: if content_link:
await self.bot.send_link_message(group_id, content_link) # content_link json 读取内容
link_data = json.loads(content_link)
xml_content = f"{LINK_XML_NORMAL}".format(title=link_data.get('title', ''),
des=link_data.get('des', ''),
url=link_data.get('url', ''),
thumburl=link_data.get('thumburl', '')
)
await self.bot.send_link_xml_message(xml_content, group_id)
# # 发送小程序消息 # # 发送小程序消息
# if content_miniprogram: # if content_miniprogram:

View File

@@ -1,4 +1,4 @@
LINK_XML = """ LINK_XML_WELCOME = """
<appmsg appid="" sdkver="1"> <appmsg appid="" sdkver="1">
<title>👏欢迎 {nickname} 加入群聊!🎉</title> <title>👏欢迎 {nickname} 加入群聊!🎉</title>
<des>⌚时间:{now}</des> <des>⌚时间:{now}</des>
@@ -93,3 +93,50 @@ MUSIC_XML = """
</appinfo> </appinfo>
<commenturl /> <commenturl />
""" """
LINK_XML_NORMAL = """
<appmsg appid="" sdkver="1">
<title>{title}</title>
<des>{des}</des>
<action>view</action>
<type>5</type>
<showtype>0</showtype>
<content />
<url>{url}</url>
<dataurl />
<lowurl />
<lowdataurl />
<recorditem />
<thumburl>{thumburl}</thumburl>
<messageaction />
<laninfo />
<extinfo />
<sourceusername />
<sourcedisplayname />
<commenturl />
<appattach>
<totallen>0</totallen>
<attachid />
<emoticonmd5 />
<fileext />
<aeskey />
</appattach>
<webviewshared>
<publisherId />
<publisherReqId>0</publisherReqId>
</webviewshared>
<weappinfo>
<pagepath />
<username />
<appid />
<appservicetype>0</appservicetype>
</weappinfo>
<websearch />
</appmsg>
<fromusername>WXID</fromusername>
<scene>0</scene>
<appinfo>
<version>1</version>
<appname></appname>
</appinfo>
<commenturl></commenturl>
"""