deploy: 2026-07-15 14:09 部署更新
This commit is contained in:
+35
-8
@@ -712,15 +712,21 @@ def list_conversations(
|
|||||||
try:
|
try:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
sql = """
|
sql = """
|
||||||
SELECT cu.id, cu.customer_name, s.name AS salesperson_name,
|
SELECT DISTINCT cu.id, cu.customer_name, s.name AS salesperson_name,
|
||||||
cu.industry, cu.intent_level, cu.stage,
|
cu.industry, cu.intent_level, cu.stage,
|
||||||
cu.summary, cu.key_points, cu.objections, cu.next_action,
|
cu.summary, cu.key_points, cu.objections, cu.next_action,
|
||||||
cu.key_needs, cu.last_analysis,
|
cu.key_needs, cu.last_analysis,
|
||||||
(SELECT count(*) FROM message m
|
(SELECT count(*) FROM message m
|
||||||
JOIN conversation conv ON conv.id = m.conversation_id
|
JOIN conversation conv ON conv.id = m.conversation_id
|
||||||
WHERE conv.contact_id = cu.contact_id) AS msg_count
|
WHERE conv.contact_id = cu.contact_id) AS msg_count,
|
||||||
|
string_agg(DISTINCT p.category, ', ') AS deal_categories,
|
||||||
|
string_agg(m2.normalized_content, ' ') AS all_messages
|
||||||
FROM customer cu
|
FROM customer cu
|
||||||
JOIN salesperson s ON s.id = cu.salesperson_id
|
JOIN salesperson s ON s.id = cu.salesperson_id
|
||||||
|
LEFT JOIN deal d ON d.customer_id = cu.id
|
||||||
|
LEFT JOIN product p ON p.id = d.product_id
|
||||||
|
LEFT JOIN conversation conv2 ON conv2.contact_id = cu.contact_id
|
||||||
|
LEFT JOIN message m2 ON m2.conversation_id = conv2.id
|
||||||
"""
|
"""
|
||||||
conditions = []
|
conditions = []
|
||||||
params = []
|
params = []
|
||||||
@@ -732,11 +738,32 @@ def list_conversations(
|
|||||||
params.append(stage)
|
params.append(stage)
|
||||||
if conditions:
|
if conditions:
|
||||||
sql += " WHERE " + " AND ".join(conditions)
|
sql += " WHERE " + " AND ".join(conditions)
|
||||||
sql += " ORDER BY cu.salesperson_id, cu.id"
|
sql += """
|
||||||
|
GROUP BY cu.id, cu.customer_name, s.name,
|
||||||
|
cu.industry, cu.intent_level, cu.stage,
|
||||||
|
cu.summary, cu.key_points, cu.objections, cu.next_action,
|
||||||
|
cu.key_needs, cu.last_analysis
|
||||||
|
ORDER BY cu.salesperson_id, cu.id
|
||||||
|
"""
|
||||||
|
|
||||||
cur.execute(sql, params)
|
cur.execute(sql, params)
|
||||||
return [
|
diet_keywords = ["减肥", "减脂", "瘦身", "B420", "纤益", "代餐", "体脂", "产后减肥", "节食"]
|
||||||
{
|
child_keywords = ["湿疹", "过敏", "鼻炎", "腹泻", "便秘", "免疫力", "胀气",
|
||||||
|
"拉肚子", "打喷嚏", "流鼻涕", "感冒", "食物过敏", "牛奶蛋白",
|
||||||
|
"宝宝", "宝妈", "儿童", "丹麦", "鼠李糖", "LGG", "合生元"]
|
||||||
|
results = []
|
||||||
|
for r in cur.fetchall():
|
||||||
|
deal_cats = r[13]
|
||||||
|
all_msgs = r[14] or ""
|
||||||
|
if deal_cats:
|
||||||
|
product_categories = deal_cats
|
||||||
|
elif any(kw in all_msgs for kw in diet_keywords):
|
||||||
|
product_categories = "女性减肥益生菌"
|
||||||
|
elif any(kw in all_msgs for kw in child_keywords):
|
||||||
|
product_categories = "儿童益生菌"
|
||||||
|
else:
|
||||||
|
product_categories = None
|
||||||
|
results.append({
|
||||||
"id": r[0], "customer_name": r[1],
|
"id": r[0], "customer_name": r[1],
|
||||||
"salesperson_name": r[2], "industry": r[3],
|
"salesperson_name": r[2], "industry": r[3],
|
||||||
"intent_level": r[4], "stage": r[5],
|
"intent_level": r[4], "stage": r[5],
|
||||||
@@ -747,9 +774,9 @@ def list_conversations(
|
|||||||
"key_needs": parse_pg_array(r[10]),
|
"key_needs": parse_pg_array(r[10]),
|
||||||
"last_analysis": r[11].isoformat() if r[11] else None,
|
"last_analysis": r[11].isoformat() if r[11] else None,
|
||||||
"msg_count": r[12],
|
"msg_count": r[12],
|
||||||
}
|
"product_categories": product_categories,
|
||||||
for r in cur.fetchall()
|
})
|
||||||
]
|
return results
|
||||||
finally:
|
finally:
|
||||||
put_conn(conn)
|
put_conn(conn)
|
||||||
|
|
||||||
|
|||||||
+100
-7
@@ -230,14 +230,15 @@ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-
|
|||||||
<div class="bg-white rounded-lg shadow p-4 mb-4">
|
<div class="bg-white rounded-lg shadow p-4 mb-4">
|
||||||
<p class="text-sm text-gray-500 mb-2">选择客户和产品,基于产品详情和真实对话生成标准范式对话模板</p>
|
<p class="text-sm text-gray-500 mb-2">选择客户和产品,基于产品详情和真实对话生成标准范式对话模板</p>
|
||||||
<div class="flex gap-3 items-center">
|
<div class="flex gap-3 items-center">
|
||||||
<select id="paradigm-customer-select" class="border rounded px-3 py-2 text-sm flex-1">
|
<select id="paradigm-customer-select" onchange="onParadigmCustomerChange()" class="border rounded px-3 py-2 text-sm flex-1">
|
||||||
<option value="">请选择客户</option>
|
<option value="">请选择客户</option>
|
||||||
</select>
|
</select>
|
||||||
<select id="paradigm-product-select" class="border rounded px-3 py-2 text-sm flex-1">
|
<select id="paradigm-product-select" onchange="onParadigmProductChange()" class="border rounded px-3 py-2 text-sm flex-1">
|
||||||
<option value="">自动推断产品</option>
|
<option value="">自动推断产品</option>
|
||||||
</select>
|
</select>
|
||||||
<button onclick="generateParadigm()" id="paradigm-btn" class="bg-green-600 text-white px-6 py-2 rounded text-sm hover:bg-green-700">生成范式对话</button>
|
<button onclick="generateParadigm()" id="paradigm-btn" class="bg-green-600 text-white px-6 py-2 rounded text-sm hover:bg-green-700">生成范式对话</button>
|
||||||
</div>
|
</div>
|
||||||
|
<p id="paradigm-match-hint" class="text-xs text-gray-400 mt-2"></p>
|
||||||
</div>
|
</div>
|
||||||
<div id="paradigm-result"></div>
|
<div id="paradigm-result"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -976,22 +977,26 @@ function renderAnalysisList() {
|
|||||||
`).join('');
|
`).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let paradigmCustomers = [];
|
||||||
|
let paradigmProducts = [];
|
||||||
|
|
||||||
function populateParadigmSelect() {
|
function populateParadigmSelect() {
|
||||||
const sel = document.getElementById('paradigm-customer-select');
|
const sel = document.getElementById('paradigm-customer-select');
|
||||||
if (!sel) return;
|
if (!sel) return;
|
||||||
const currentVal = sel.value;
|
paradigmCustomers = conversationsData;
|
||||||
|
// 填充客户下拉
|
||||||
sel.innerHTML = '<option value="">请选择客户</option>';
|
sel.innerHTML = '<option value="">请选择客户</option>';
|
||||||
conversationsData.forEach(c => {
|
paradigmCustomers.forEach(c => {
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = c.id;
|
opt.value = c.id;
|
||||||
opt.textContent = `${c.customer_name} (${c.salesperson_name}, ${c.stage})`;
|
opt.textContent = `${c.customer_name} (${c.salesperson_name}, ${c.stage})`;
|
||||||
sel.appendChild(opt);
|
sel.appendChild(opt);
|
||||||
});
|
});
|
||||||
if (currentVal) sel.value = currentVal;
|
// 填充产品下拉
|
||||||
// 填充产品选择
|
|
||||||
const productSel = document.getElementById('paradigm-product-select');
|
const productSel = document.getElementById('paradigm-product-select');
|
||||||
if (productSel && productSel.options.length <= 1) {
|
productSel.innerHTML = '<option value="">自动推断产品</option>';
|
||||||
api('/products?status=active').then(products => {
|
api('/products?status=active').then(products => {
|
||||||
|
paradigmProducts = products;
|
||||||
products.forEach(p => {
|
products.forEach(p => {
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = p.id;
|
opt.value = p.id;
|
||||||
@@ -1000,6 +1005,94 @@ function populateParadigmSelect() {
|
|||||||
});
|
});
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 选择客户时,自动筛选匹配的产品 */
|
||||||
|
function onParadigmCustomerChange() {
|
||||||
|
const customerId = document.getElementById('paradigm-customer-select').value;
|
||||||
|
const productSel = document.getElementById('paradigm-product-select');
|
||||||
|
const hint = document.getElementById('paradigm-match-hint');
|
||||||
|
if (!customerId) {
|
||||||
|
// 恢复全部产品
|
||||||
|
productSel.innerHTML = '<option value="">自动推断产品</option>';
|
||||||
|
paradigmProducts.forEach(p => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = p.id;
|
||||||
|
opt.textContent = `${p.name} ${p.spec || ''}`;
|
||||||
|
productSel.appendChild(opt);
|
||||||
|
});
|
||||||
|
hint.textContent = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const customer = paradigmCustomers.find(c => String(c.id) === customerId);
|
||||||
|
if (!customer) return;
|
||||||
|
const cats = (customer.product_categories || '').split(', ').filter(x => x);
|
||||||
|
if (cats.length > 0) {
|
||||||
|
// 筛选匹配产品线的产品
|
||||||
|
const matched = paradigmProducts.filter(p => cats.includes(p.category));
|
||||||
|
productSel.innerHTML = '<option value="">自动推断产品</option>';
|
||||||
|
matched.forEach(p => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = p.id;
|
||||||
|
opt.textContent = `${p.name} ${p.spec || ''}`;
|
||||||
|
productSel.appendChild(opt);
|
||||||
|
});
|
||||||
|
hint.textContent = `客户关联产品线: ${cats.join('、')},已筛选 ${matched.length} 个匹配产品`;
|
||||||
|
} else {
|
||||||
|
// 无关联产品线,显示全部
|
||||||
|
productSel.innerHTML = '<option value="">自动推断产品</option>';
|
||||||
|
paradigmProducts.forEach(p => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = p.id;
|
||||||
|
opt.textContent = `${p.name} ${p.spec || ''}`;
|
||||||
|
productSel.appendChild(opt);
|
||||||
|
});
|
||||||
|
hint.textContent = '该客户未关联产品线,显示全部产品';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 选择产品时,筛选匹配产品线的客户 */
|
||||||
|
function onParadigmProductChange() {
|
||||||
|
const productId = document.getElementById('paradigm-product-select').value;
|
||||||
|
const customerSel = document.getElementById('paradigm-customer-select');
|
||||||
|
const hint = document.getElementById('paradigm-match-hint');
|
||||||
|
if (!productId) {
|
||||||
|
// 恢复全部客户
|
||||||
|
customerSel.innerHTML = '<option value="">请选择客户</option>';
|
||||||
|
paradigmCustomers.forEach(c => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = c.id;
|
||||||
|
opt.textContent = `${c.customer_name} (${c.salesperson_name}, ${c.stage})`;
|
||||||
|
customerSel.appendChild(opt);
|
||||||
|
});
|
||||||
|
hint.textContent = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const product = paradigmProducts.find(p => String(p.id) === productId);
|
||||||
|
if (!product) return;
|
||||||
|
// 筛选产品线匹配的客户
|
||||||
|
const matched = paradigmCustomers.filter(c => {
|
||||||
|
const cats = (c.product_categories || '').split(', ').filter(x => x);
|
||||||
|
return cats.includes(product.category);
|
||||||
|
});
|
||||||
|
if (matched.length > 0) {
|
||||||
|
customerSel.innerHTML = '<option value="">请选择客户</option>';
|
||||||
|
matched.forEach(c => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = c.id;
|
||||||
|
opt.textContent = `${c.customer_name} (${c.salesperson_name}, ${c.stage})`;
|
||||||
|
customerSel.appendChild(opt);
|
||||||
|
});
|
||||||
|
hint.textContent = `产品线: ${product.category},已筛选 ${matched.length} 个匹配客户`;
|
||||||
|
} else {
|
||||||
|
customerSel.innerHTML = '<option value="">请选择客户</option>';
|
||||||
|
paradigmCustomers.forEach(c => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = c.id;
|
||||||
|
opt.textContent = `${c.customer_name} (${c.salesperson_name}, ${c.stage})`;
|
||||||
|
customerSel.appendChild(opt);
|
||||||
|
});
|
||||||
|
hint.textContent = `产品线: ${product.category},无匹配客户,显示全部`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateParadigm() {
|
async function generateParadigm() {
|
||||||
|
|||||||
Reference in New Issue
Block a user