fix: 数据分析模块修复 添加 API 端点 + 租户维度
1. 新增 GET /api/analytics 端点(之前不存在,前端一直请求 404) 2. query_optimizer.get_analytics_optimized 支持 tenant_id 参数 3. 工单/预警/对话查询按 tenant_id 过滤 4. 数据分析控制面板新增租户筛选下拉框 5. updateCharts 传递 tenant_id 参数 6. populateTenantSelectors 填充分析页租户筛选器
This commit is contained in:
Binary file not shown.
@@ -236,25 +236,31 @@ class QueryOptimizer:
|
|||||||
logger.error(f"批量更新工单失败: {e}")
|
logger.error(f"批量更新工单失败: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_analytics_optimized(self, days: int = 30) -> Dict[str, Any]:
|
def get_analytics_optimized(self, days: int = 30, tenant_id: str = None) -> Dict[str, Any]:
|
||||||
"""优化版分析数据查询"""
|
"""优化版分析数据查询(支持按租户筛选)"""
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with db_manager.get_session() as session:
|
with db_manager.get_session() as session:
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
end_time = datetime.now()
|
# 查询工单
|
||||||
start_time_query = end_time - timedelta(days=days-1)
|
wo_query = session.query(WorkOrder)
|
||||||
|
if tenant_id:
|
||||||
|
wo_query = wo_query.filter(WorkOrder.tenant_id == tenant_id)
|
||||||
|
workorders = wo_query.all()
|
||||||
|
|
||||||
# 批量查询所有需要的数据
|
# 查询预警
|
||||||
# 修改:查询所有工单,不限制时间范围
|
alert_query = session.query(Alert)
|
||||||
workorders = session.query(WorkOrder).all()
|
if tenant_id:
|
||||||
|
alert_query = alert_query.filter(Alert.tenant_id == tenant_id)
|
||||||
|
alerts = alert_query.all()
|
||||||
|
|
||||||
# 修改:查询所有预警和对话,不限制时间范围
|
# 查询对话
|
||||||
alerts = session.query(Alert).all()
|
conv_query = session.query(Conversation)
|
||||||
|
if tenant_id:
|
||||||
conversations = session.query(Conversation).all()
|
conv_query = conv_query.filter(Conversation.tenant_id == tenant_id)
|
||||||
|
conversations = conv_query.all()
|
||||||
|
|
||||||
# 处理数据
|
# 处理数据
|
||||||
analytics = self._process_analytics_data(workorders, alerts, conversations, days)
|
analytics = self._process_analytics_data(workorders, alerts, conversations, days)
|
||||||
|
|||||||
@@ -294,28 +294,25 @@ class FeishuLongConnService:
|
|||||||
这个方法会阻塞当前线程,持续监听飞书事件
|
这个方法会阻塞当前线程,持续监听飞书事件
|
||||||
"""
|
"""
|
||||||
logger.info("=" * 80)
|
logger.info("=" * 80)
|
||||||
logger.info("🚀 启动飞书长连接客户端")
|
logger.info(" 启动飞书长连接客户端")
|
||||||
logger.info("=" * 80)
|
logger.info("=" * 80)
|
||||||
logger.info(f"📋 配置信息:")
|
logger.info(f"📋 配置信息:")
|
||||||
logger.info(f" - App ID: {self.app_id}")
|
logger.info(f" - App ID: {self.app_id}")
|
||||||
logger.info(f" - 模式: 事件订阅 2.0(长连接)")
|
logger.info(" 等待消息中... (按 Ctrl+C 停止)")
|
||||||
logger.info(f" - 优势: 无需公网域名和 webhook 配置")
|
|
||||||
logger.info("=" * 80)
|
|
||||||
logger.info("💡 等待消息中... (按 Ctrl+C 停止)")
|
|
||||||
logger.info("=" * 80)
|
logger.info("=" * 80)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 创建长连接客户端
|
# 创建长连接客户端
|
||||||
cli = lark.ws.Client(self.app_id, self.app_secret, event_handler=self.event_handler)
|
cli = lark.ws.Client(self.app_id, self.app_secret, event_handler=self.event_handler)
|
||||||
|
|
||||||
logger.info("🔌 正在建立与飞书服务器的连接...")
|
logger.info("正在建立与飞书服务器的连接...")
|
||||||
|
|
||||||
# 启动长连接(会阻塞)
|
# 启动长连接(会阻塞)
|
||||||
cli.start()
|
cli.start()
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logger.info("")
|
logger.info("")
|
||||||
logger.info("⏹️ 用户中断,停止飞书长连接客户端")
|
logger.info("用户中断,停止飞书长连接客户端")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"飞书长连接客户端异常: {e}", exc_info=True)
|
logger.error(f"飞书长连接客户端异常: {e}", exc_info=True)
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -341,7 +341,6 @@ class FlexibleFieldMapper:
|
|||||||
# 保存配置
|
# 保存配置
|
||||||
self._save_current_config()
|
self._save_current_config()
|
||||||
|
|
||||||
logger.info(f"添加字段映射: {feishu_field} -> {local_field}")
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -431,7 +430,6 @@ class FlexibleFieldMapper:
|
|||||||
'mapped': True,
|
'mapped': True,
|
||||||
'value': value
|
'value': value
|
||||||
}
|
}
|
||||||
logger.debug(f"映射字段 {feishu_field} -> {local_field}: {str(value)[:50]}")
|
|
||||||
else:
|
else:
|
||||||
conversion_stats['unmapped_fields'].append(feishu_field)
|
conversion_stats['unmapped_fields'].append(feishu_field)
|
||||||
conversion_stats['mapping_details'][feishu_field] = {
|
conversion_stats['mapping_details'][feishu_field] = {
|
||||||
@@ -439,9 +437,7 @@ class FlexibleFieldMapper:
|
|||||||
'value': value,
|
'value': value,
|
||||||
'suggestions': self._suggest_mapping(feishu_field)
|
'suggestions': self._suggest_mapping(feishu_field)
|
||||||
}
|
}
|
||||||
logger.info(f"飞书字段 {feishu_field} 不存在于数据中")
|
|
||||||
|
|
||||||
logger.debug(f"字段转换完成: 已映射 {conversion_stats['mapped_fields']}, "
|
logger.debug(f"未映射 {len(conversion_stats['unmapped_fields'])}")
|
||||||
f"未映射 {len(conversion_stats['unmapped_fields'])}")
|
|
||||||
|
|
||||||
return local_data, conversion_stats
|
return local_data, conversion_stats
|
||||||
|
|||||||
@@ -4,49 +4,63 @@
|
|||||||
处理数据分析、报告生成等功能
|
处理数据分析、报告生成等功能
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import Blueprint, request, jsonify, send_file
|
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
|
from flask import Blueprint, request, jsonify, send_file
|
||||||
|
from src.core.query_optimizer import query_optimizer
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
analytics_bp = Blueprint('analytics', __name__, url_prefix='/api/analytics')
|
analytics_bp = Blueprint('analytics', __name__, url_prefix='/api/analytics')
|
||||||
|
|
||||||
|
|
||||||
|
@analytics_bp.route('')
|
||||||
|
def get_analytics():
|
||||||
|
"""获取分析数据(支持租户筛选和时间范围)"""
|
||||||
|
try:
|
||||||
|
time_range = request.args.get('timeRange', '30')
|
||||||
|
tenant_id = request.args.get('tenant_id')
|
||||||
|
|
||||||
|
try:
|
||||||
|
days = int(time_range)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
days = 30
|
||||||
|
|
||||||
|
analytics = query_optimizer.get_analytics_optimized(days, tenant_id=tenant_id)
|
||||||
|
return jsonify(analytics)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取分析数据失败: {e}")
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
@analytics_bp.route('/export')
|
@analytics_bp.route('/export')
|
||||||
def export_analytics():
|
def export_analytics():
|
||||||
"""导出分析报告"""
|
"""导出分析报告"""
|
||||||
try:
|
try:
|
||||||
from src.web.service_manager import service_manager
|
|
||||||
from src.core.query_optimizer import query_optimizer
|
|
||||||
from openpyxl import Workbook
|
from openpyxl import Workbook
|
||||||
from openpyxl.styles import Font
|
from openpyxl.styles import Font
|
||||||
|
|
||||||
# 生成Excel报告(使用数据库真实数据)
|
|
||||||
analytics = query_optimizer.get_analytics_optimized(30)
|
analytics = query_optimizer.get_analytics_optimized(30)
|
||||||
|
|
||||||
# 创建工作簿
|
|
||||||
wb = Workbook()
|
wb = Workbook()
|
||||||
ws = wb.active
|
ws = wb.active
|
||||||
ws.title = "分析报告"
|
ws.title = "分析报告"
|
||||||
|
|
||||||
# 添加标题
|
|
||||||
ws['A1'] = 'TSP智能助手分析报告'
|
ws['A1'] = 'TSP智能助手分析报告'
|
||||||
ws['A1'].font = Font(size=16, bold=True)
|
ws['A1'].font = Font(size=16, bold=True)
|
||||||
|
|
||||||
# 添加工单统计
|
|
||||||
ws['A3'] = '工单统计'
|
ws['A3'] = '工单统计'
|
||||||
ws['A3'].font = Font(bold=True)
|
ws['A3'].font = Font(bold=True)
|
||||||
ws['A4'] = '总工单数'
|
ws['A4'] = '总工单数'
|
||||||
ws['B4'] = analytics['workorders']['total']
|
ws['B4'] = analytics.get('workorders', {}).get('total', 0)
|
||||||
ws['A5'] = '待处理'
|
ws['A5'] = '待处理'
|
||||||
ws['B5'] = analytics['workorders']['open']
|
ws['B5'] = analytics.get('workorders', {}).get('open', 0)
|
||||||
ws['A6'] = '已解决'
|
ws['A6'] = '已解决'
|
||||||
ws['B6'] = analytics['workorders']['resolved']
|
ws['B6'] = analytics.get('workorders', {}).get('resolved', 0)
|
||||||
|
|
||||||
# 保存文件
|
|
||||||
report_path = 'uploads/analytics_report.xlsx'
|
report_path = 'uploads/analytics_report.xlsx'
|
||||||
os.makedirs('uploads', exist_ok=True)
|
os.makedirs('uploads', exist_ok=True)
|
||||||
wb.save(report_path)
|
wb.save(report_path)
|
||||||
|
|
||||||
return send_file(report_path, as_attachment=True, download_name='analytics_report.xlsx')
|
return send_file(report_path, as_attachment=True, download_name='analytics_report.xlsx')
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.error(f"导出分析报告失败: {e}")
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|||||||
@@ -165,6 +165,12 @@ class TSPDashboard {
|
|||||||
if (woCreate) {
|
if (woCreate) {
|
||||||
woCreate.innerHTML = tenants.map(t => `<option value="${t.tenant_id}">${t.name} (${t.tenant_id})</option>`).join('');
|
woCreate.innerHTML = tenants.map(t => `<option value="${t.tenant_id}">${t.name} (${t.tenant_id})</option>`).join('');
|
||||||
}
|
}
|
||||||
|
// 数据分析租户筛选器
|
||||||
|
const analyticsFilter = document.getElementById('analytics-tenant-filter');
|
||||||
|
if (analyticsFilter) {
|
||||||
|
const cv = analyticsFilter.value;
|
||||||
|
analyticsFilter.innerHTML = '<option value="">全部租户</option>' + tenants.map(t => `<option value="${t.tenant_id}"${t.tenant_id === cv ? ' selected' : ''}>${t.name}</option>`).join('');
|
||||||
|
}
|
||||||
} catch (e) { console.warn('加载租户列表失败:', e); }
|
} catch (e) { console.warn('加载租户列表失败:', e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -392,9 +392,11 @@ Object.assign(TSPDashboard.prototype, {
|
|||||||
const timeRange = document.getElementById('timeRange').value;
|
const timeRange = document.getElementById('timeRange').value;
|
||||||
const chartType = document.getElementById('chartType').value;
|
const chartType = document.getElementById('chartType').value;
|
||||||
const dataDimension = document.getElementById('dataDimension').value;
|
const dataDimension = document.getElementById('dataDimension').value;
|
||||||
|
const tenantId = document.getElementById('analytics-tenant-filter')?.value || '';
|
||||||
|
|
||||||
// 获取数据
|
let url = `/api/analytics?timeRange=${timeRange}&dimension=${dataDimension}`;
|
||||||
const response = await fetch(`/api/analytics?timeRange=${timeRange}&dimension=${dataDimension}`);
|
if (tenantId) url += `&tenant_id=${encodeURIComponent(tenantId)}`;
|
||||||
|
const response = await fetch(url);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
// 更新统计卡片
|
// 更新统计卡片
|
||||||
|
|||||||
@@ -1856,7 +1856,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-3">
|
<div class="col-md-2">
|
||||||
|
<label class="form-label">租户</label>
|
||||||
|
<select class="form-select" id="analytics-tenant-filter">
|
||||||
|
<option value="">全部租户</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
<label class="form-label">时间范围</label>
|
<label class="form-label">时间范围</label>
|
||||||
<select class="form-select" id="timeRange">
|
<select class="form-select" id="timeRange">
|
||||||
<option value="7">最近7天</option>
|
<option value="7">最近7天</option>
|
||||||
@@ -1865,18 +1871,16 @@
|
|||||||
<option value="365">最近1年</option>
|
<option value="365">最近1年</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-2">
|
||||||
<label class="form-label">图表类型</label>
|
<label class="form-label">图表类型</label>
|
||||||
<select class="form-select" id="chartType">
|
<select class="form-select" id="chartType">
|
||||||
<option value="line">折线图</option>
|
<option value="line">折线图</option>
|
||||||
<option value="bar" selected>柱状图</option>
|
<option value="bar" selected>柱状图</option>
|
||||||
<option value="pie">饼图</option>
|
<option value="pie">饼图</option>
|
||||||
<option value="doughnut">环形图</option>
|
<option value="doughnut">环形图</option>
|
||||||
<option value="radar">雷达图</option>
|
|
||||||
<option value="polar">极坐标图</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-2">
|
||||||
<label class="form-label">数据维度</label>
|
<label class="form-label">数据维度</label>
|
||||||
<select class="form-select" id="dataDimension">
|
<select class="form-select" id="dataDimension">
|
||||||
<option value="workorders" selected>工单统计</option>
|
<option value="workorders" selected>工单统计</option>
|
||||||
@@ -1885,11 +1889,11 @@
|
|||||||
<option value="satisfaction">满意度分析</option>
|
<option value="satisfaction">满意度分析</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-2">
|
||||||
<label class="form-label">操作</label>
|
<label class="form-label">操作</label>
|
||||||
<div class="d-grid">
|
<div class="d-grid">
|
||||||
<button class="btn btn-primary" onclick="dashboard.updateCharts()">
|
<button class="btn btn-primary" onclick="dashboard.updateCharts()">
|
||||||
<i class="fas fa-sync-alt me-1"></i>刷新图表
|
<i class="fas fa-sync-alt me-1"></i>刷新
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user