fix: 工单管理字段语义统一 + AI建议修复 + 租户筛选

1. 统一字段语义:title=问题标题,description=问题详细描述,resolution=解决方案
2. 修复详情页展示:问题描述取 description 而非 title
3. 修复编辑页标签:description 标注为'问题详细描述'而非'处理过程'
4. 统一分类选项:创建和编辑使用相同的分类列表(技术问题/APP功能/远程控制/车辆绑定/系统故障/OTA升级/其他)
5. 修复 AI 建议生成:用 title+description 搜索知识库,prompt 中明确区分标题和描述
6. 修复入库逻辑:question 使用 title+description 完整内容,入库时带上工单的 tenant_id
7. 工单列表新增租户筛选器,API 支持 tenant_id 过滤
8. 工单列表和详情 API 返回 tenant_id 字段
9. 租户选择器同时填充对话和工单筛选下拉框
This commit is contained in:
2026-04-02 10:04:39 +08:00
parent 7950cd8237
commit d691007c86
3 changed files with 56 additions and 28 deletions

View File

@@ -84,6 +84,7 @@ def get_workorders():
per_page = request.args.get('per_page', 10, type=int)
status_filter = request.args.get('status', '')
priority_filter = request.args.get('priority', '')
tenant_id = request.args.get('tenant_id', '')
# 从数据库获取分页数据
from src.core.database import db_manager
@@ -98,6 +99,8 @@ def get_workorders():
query = query.filter(WorkOrder.status == status_filter)
if priority_filter:
query = query.filter(WorkOrder.priority == priority_filter)
if tenant_id:
query = query.filter(WorkOrder.tenant_id == tenant_id)
# 按创建时间倒序排列
query = query.order_by(WorkOrder.created_at.desc())
@@ -114,6 +117,7 @@ def get_workorders():
workorders_data.append({
'id': workorder.id,
'order_id': workorder.order_id,
'tenant_id': workorder.tenant_id,
'title': workorder.title,
'description': workorder.description,
'category': workorder.category,
@@ -184,6 +188,7 @@ def get_workorder_details(workorder_id):
workorder = {
"id": w.id,
"order_id": w.order_id,
"tenant_id": w.tenant_id,
"title": w.title,
"description": w.description,
"category": w.category,
@@ -315,15 +320,15 @@ def generate_workorder_ai_suggestion(workorder_id):
if not w:
return jsonify({"error": "工单不存在"}), 404
# 调用知识库搜索与LLM生成
# 使用问题描述title而不是处理过程description作为主要查询依据
query = f"{w.title}"
# 使用 title + description 作为查询依据,获取更精准的知识库匹配
query = f"{w.title} {w.description or ''}"
kb_results = service_manager.get_assistant().search_knowledge(query, top_k=3)
kb_list = kb_results.get('results', []) if isinstance(kb_results, dict) else []
# 组装提示词
context = "\n".join([f"Q: {k.get('question','')}\nA: {k.get('answer','')}" for k in kb_list])
from src.core.llm_client import QwenClient
llm = QwenClient()
prompt = f"请基于以下工单问题描述与知识库片段,给出简洁、可执行的处理建议。\n\n问题描述:\n{w.title}\n\n处理过程(仅供参考):\n{w.description}\n\n知识库片段:\n{context}\n\n请直接输出建议文本:"
prompt = f"请基于以下工单问题与知识库片段,给出简洁、可执行的处理建议。\n\n问题标题:\n{w.title}\n\n问题详细描述:\n{w.description or ''}\n\n知识库片段:\n{context}\n\n请直接输出建议文本:"
llm_resp = llm.chat_completion(messages=[{"role":"user","content":prompt}], temperature=0.3, max_tokens=800)
suggestion = ""
if llm_resp and 'choices' in llm_resp:
@@ -432,10 +437,18 @@ def approve_workorder_to_knowledge(workorder_id):
return jsonify({"error": "未找到可入库的内容"}), 400
# 入库为知识条目
# question 使用标题+描述,确保知识条目的问题完整
question_text = w.title or ''
if w.description:
question_text = f"{w.title}\n{w.description}" if w.title else w.description
if not question_text:
question_text = '工单问题'
entry = KnowledgeEntry(
question=w.title or (w.description[:20] if w.description else '工单问题'),
question=question_text,
answer=answer_content,
category=w.category or '其他',
tenant_id=w.tenant_id,
confidence_score=confidence_score,
is_active=True,
is_verified=True,

View File

@@ -386,13 +386,23 @@ class TSPDashboard {
const tenants = await response.json();
if (!Array.isArray(tenants)) return;
const selectors = document.querySelectorAll('#chat-tenant-id');
selectors.forEach(select => {
const currentVal = select.value;
select.innerHTML = tenants.map(t =>
// 填充对话租户选择器
const chatSelect = document.getElementById('chat-tenant-id');
if (chatSelect) {
const currentVal = chatSelect.value;
chatSelect.innerHTML = tenants.map(t =>
`<option value="${t.tenant_id}"${t.tenant_id === currentVal ? ' selected' : ''}>${t.name} (${t.tenant_id})</option>`
).join('');
});
}
// 填充工单租户筛选器
const woFilter = document.getElementById('workorder-tenant-filter');
if (woFilter) {
const currentVal = woFilter.value;
woFilter.innerHTML = '<option value="all">全部租户</option>' + tenants.map(t =>
`<option value="${t.tenant_id}"${t.tenant_id === currentVal ? ' selected' : ''}>${t.name}</option>`
).join('');
}
} catch (e) {
console.warn('加载租户列表失败:', e);
}
@@ -2800,6 +2810,7 @@ class TSPDashboard {
try {
const statusFilter = document.getElementById('workorder-status-filter')?.value || 'all';
const priorityFilter = document.getElementById('workorder-priority-filter')?.value || 'all';
const tenantFilter = document.getElementById('workorder-tenant-filter')?.value || 'all';
let url = '/api/workorders';
const params = new URLSearchParams();
@@ -2808,6 +2819,7 @@ class TSPDashboard {
params.append('per_page', pageSize.toString());
if (statusFilter !== 'all') params.append('status', statusFilter);
if (priorityFilter !== 'all') params.append('priority', priorityFilter);
if (tenantFilter !== 'all') params.append('tenant_id', tenantFilter);
// 添加强制刷新参数
if (forceRefresh) {
@@ -2872,7 +2884,7 @@ class TSPDashboard {
<input type="checkbox" class="form-check-input me-2 workorder-checkbox" value="${workorder.id}" onchange="dashboard.updateBatchDeleteButton()">
<div class="flex-grow-1">
<h6 class="mb-1">${workorder.title}</h6>
<p class="text-muted mb-2">${workorder.description ? workorder.description.substring(0, 100) + (workorder.description.length > 100 ? '...' : '') : '无处理过程'}</p>
<p class="text-muted mb-2">${workorder.description ? workorder.description.substring(0, 100) + (workorder.description.length > 100 ? '...' : '') : '无问题描述'}</p>
<div class="d-flex gap-3">
<span class="badge bg-${this.getPriorityColor(workorder.priority)}">${this.getPriorityText(workorder.priority)}</span>
<span class="badge bg-${this.getStatusColor(workorder.status)}">${this.getStatusText(workorder.status)}</span>
@@ -3133,14 +3145,8 @@ class TSPDashboard {
<div class="col-md-6">
<h6>问题描述</h6>
<div class="border p-3 rounded">
${workorder.title || '无问题描述'}
${workorder.description || '无问题描述'}
</div>
${workorder.description ? `
<h6 class="mt-3">处理过程</h6>
<div class="border p-3 rounded bg-light">
${workorder.description}
</div>
` : ''}
${workorder.resolution ? `
<h6 class="mt-3">解决方案</h6>
<div class="border p-3 rounded bg-light">
@@ -3310,7 +3316,7 @@ class TSPDashboard {
<div class="row">
<div class="col-md-8">
<div class="mb-3">
<label for="editTitle" class="form-label">标题 *</label>
<label for="editTitle" class="form-label">问题标题 *</label>
<input type="text" class="form-control" id="editTitle" value="${workorder.title}" required>
</div>
</div>
@@ -3333,9 +3339,11 @@ class TSPDashboard {
<label for="editCategory" class="form-label">分类</label>
<select class="form-select" id="editCategory">
<option value="技术问题" ${workorder.category === '技术问题' ? 'selected' : ''}>技术问题</option>
<option value="业务问题" ${workorder.category === '业务问题' ? 'selected' : ''}>业务问题</option>
<option value="APP功能" ${workorder.category === 'APP功能' ? 'selected' : ''}>APP功能</option>
<option value="远程控制" ${workorder.category === '远程控制' ? 'selected' : ''}>远程控制</option>
<option value="车辆绑定" ${workorder.category === '车辆绑定' ? 'selected' : ''}>车辆绑定</option>
<option value="系统故障" ${workorder.category === '系统故障' ? 'selected' : ''}>系统故障</option>
<option value="功能需求" ${workorder.category === '功能需求' ? 'selected' : ''}>功能需求</option>
<option value="OTA升级" ${workorder.category === 'OTA升级' ? 'selected' : ''}>OTA升级</option>
<option value="其他" ${workorder.category === '其他' ? 'selected' : ''}>其他</option>
</select>
</div>
@@ -3354,7 +3362,7 @@ class TSPDashboard {
</div>
<div class="mb-3">
<label for="editDescription" class="form-label">处理过程 *</label>
<label for="editDescription" class="form-label">问题详细描述 *</label>
<textarea class="form-control" id="editDescription" rows="4" required>${workorder.description}</textarea>
</div>

View File

@@ -989,7 +989,12 @@
<div class="card-body">
<div class="mb-3">
<div class="row">
<div class="col-md-4">
<div class="col-md-3">
<select class="form-select" id="workorder-tenant-filter" onchange="dashboard.loadWorkOrders()">
<option value="all">全部租户</option>
</select>
</div>
<div class="col-md-3">
<select class="form-select" id="workorder-status-filter">
<option value="all">全部状态</option>
<option value="open">待处理</option>
@@ -998,7 +1003,7 @@
<option value="closed">已关闭</option>
</select>
</div>
<div class="col-md-4">
<div class="col-md-3">
<select class="form-select" id="workorder-priority-filter">
<option value="all">全部优先级</option>
<option value="low"></option>
@@ -1007,7 +1012,7 @@
<option value="urgent">紧急</option>
</select>
</div>
<div class="col-md-4">
<div class="col-md-3">
<button class="btn btn-outline-secondary w-100" id="refresh-workorders">
<i class="fas fa-sync-alt me-1"></i>刷新
</button>
@@ -2294,12 +2299,12 @@
<div class="modal-body">
<form id="work-order-form">
<div class="mb-3">
<label class="form-label">工单标题</label>
<input type="text" class="form-control" id="wo-title" required>
<label class="form-label">问题标题</label>
<input type="text" class="form-control" id="wo-title" placeholder="简要描述问题,如:远程启动失败" required>
</div>
<div class="mb-3">
<label class="form-label">问题描述</label>
<textarea class="form-control" id="wo-description" rows="3" required></textarea>
<label class="form-label">问题详细描述</label>
<textarea class="form-control" id="wo-description" rows="3" placeholder="详细描述问题现象、复现步骤等" required></textarea>
</div>
<div class="row">
<div class="col-md-6">
@@ -2310,6 +2315,8 @@
<option value="APP功能">APP功能</option>
<option value="远程控制">远程控制</option>
<option value="车辆绑定">车辆绑定</option>
<option value="系统故障">系统故障</option>
<option value="OTA升级">OTA升级</option>
<option value="其他">其他</option>
</select>
</div>