diff --git a/backend/app/routes/spam_routes.py b/backend/app/routes/spam_routes.py index 3d62970..5399739 100644 --- a/backend/app/routes/spam_routes.py +++ b/backend/app/routes/spam_routes.py @@ -1,4 +1,4 @@ -from flask import Blueprint, current_app, request +from flask import Blueprint, current_app, request, send_file, after_this_request from flask_jwt_extended import jwt_required from app.extensions import db @@ -367,3 +367,56 @@ def import_samples(): db.session.commit() return ok({"created": created, "updated": updated}, "样本导入完成") + + +@spam_bp.post("/export/xlsx") +@jwt_required() +def export_xlsx(): + user = current_user() + if not user: + return fail("用户不存在", 404) + + payload = request.get_json(silent=True) or {} + items = payload.get("items") or [] + if not isinstance(items, list) or not items: + return fail("items 必须是非空数组", 400) + + import os + import tempfile + import pandas as pd + + rows = [] + for item in items: + tokens = item.get("reason_tokens") or [] + token_str = "; ".join(t.get("token", "") for t in tokens) if isinstance(tokens, list) else "" + prediction_text = "垃圾信息" if item.get("prediction") == "spam" else "正常信息" + + rows.append({ + "文本": item.get("text", ""), + "判定结果": prediction_text, + "分类标签": item.get("category_label", ""), + "置信度": f"{float(item.get("confidence", 0) or 0) * 100:.2f}%", + "垃圾概率": f"{float(item.get("spam_probability", 0) or 0) * 100:.2f}%", + "正常概率": f"{float(item.get("ham_probability", 0) or 0) * 100:.2f}%", + "风险关键词": token_str, + }) + + df = pd.DataFrame(rows) + tmp = tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) + tmp.close() + df.to_excel(tmp.name, index=False, engine="openpyxl") + + @after_this_request + def cleanup(response): + try: + os.unlink(tmp.name) + except Exception: + pass + return response + + return send_file( + tmp.name, + mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + as_attachment=True, + download_name="batch_detect.xlsx", + ) diff --git a/backend/requirements.txt b/backend/requirements.txt index 08fd22b..9843c52 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -11,3 +11,4 @@ joblib==1.4.2 python-dotenv==1.0.1 requests==2.32.3 Werkzeug==3.1.3 +openpyxl==3.1.5 diff --git a/miniprogram/pages/batch/index.js b/miniprogram/pages/batch/index.js index dda7ece..8570236 100644 --- a/miniprogram/pages/batch/index.js +++ b/miniprogram/pages/batch/index.js @@ -141,46 +141,66 @@ Page({ return [headers.join(','), ...rows].join('\n') }, - exportCSV() { + exportXLSX() { const items = this.data.items if (!items.length) { wx.showToast({ title: '暂无识别结果可导出', icon: 'none' }) return } - const csvContent = this.generateCSV() - const timestamp = new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-') - const filename = `batch_detect_${timestamp}.csv` + wx.showLoading({ title: '生成文件中...' }) - // 写入临时文件 - const fs = wx.getFileSystemManager() - const tempPath = `${wx.env.USER_DATA_PATH}/${filename}` + const app = getApp() + const token = app.globalData.token || wx.getStorageSync('token') || '' + const baseURL = app.globalData.baseURL || 'http://127.0.0.1:5000/api' - try { - fs.writeFileSync(tempPath, csvContent, 'utf8') - wx.showModal({ - title: '导出成功', - content: `CSV文件已生成,是否打开查看?\n文件名:${filename}`, - confirmText: '打开', - cancelText: '关闭', - success: (res) => { - if (res.confirm) { - wx.openDocument({ - filePath: tempPath, - fileType: 'csv', - showMenu: true, - fail: (err) => { - console.error('打开文件失败', err) - wx.showToast({ title: '打开失败,请检查文件管理器', icon: 'none' }) - } - }) - } + wx.request({ + url: `${baseURL}/spam/export/xlsx`, + method: 'POST', + data: { items }, + header: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + responseType: 'arraybuffer', + success(res) { + wx.hideLoading() + + if (res.statusCode !== 200) { + wx.showToast({ title: '导出失败', icon: 'none' }) + return } - }) - } catch (err) { - console.error('写入文件失败', err) - wx.showToast({ title: '导出失败', icon: 'none' }) - } + + const timestamp = new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-') + const filename = `batch_detect_${timestamp}.xlsx` + const fs = wx.getFileSystemManager() + const tempPath = `${wx.env.USER_DATA_PATH}/${filename}` + + try { + fs.writeFileSync(tempPath, res.data) + wx.openDocument({ + filePath: tempPath, + fileType: 'xlsx', + showMenu: true, + success: () => { + wx.showToast({ title: '导出成功', icon: 'success' }) + }, + fail: (err) => { + console.error('打开文件失败', err) + wx.showToast({ title: '打开失败', icon: 'none' }) + } + }) + } catch (err) { + console.error('写入文件失败', err) + wx.showToast({ title: '导出失败', icon: 'none' }) + } + }, + fail(err) { + wx.hideLoading() + console.error('导出请求失败', err) + wx.showToast({ title: '导出失败,请检查网络', icon: 'none' }) + } + }) }, copyCSVToClipboard() { diff --git a/miniprogram/pages/batch/index.wxml b/miniprogram/pages/batch/index.wxml index ba3f31e..f9219e7 100644 --- a/miniprogram/pages/batch/index.wxml +++ b/miniprogram/pages/batch/index.wxml @@ -77,7 +77,7 @@ 明细结果 - +