feat: 新增产品管理模块 + AI基于产品详情生成范式对话 + 一键远程部署脚本

- schema.sql: 新增 product 表(positioning/selling_points) + deal 表加 product_id + 序列重置 + ALTER TABLE 迁移
- server.py: 产品 CRUD API + deal 关联 product_id + 仪表盘产品线统计 + 范式对话动态读取产品详情
- index.html: 产品管理页面 + 录入成交产品下拉 + 仪表盘增强 + 范式对话产品选择 + 导航通用化
- run_demo.sh: python 改 python3 + 预置成交关联 product_id
- deploy.sh: 一键远程部署脚本(rsync+ssh+重启)
- deploy_remote.sh: 远程数据库迁移脚本
This commit is contained in:
selfrelease
2026-07-15 11:37:33 +08:00
parent 5926c73ff1
commit 2b0c35220a
6 changed files with 843 additions and 53 deletions
+277 -14
View File
@@ -75,6 +75,7 @@ class DealCreate(BaseModel):
salesperson_id: int
customer_id: int | None = None
contact_id: int | None = None
product_id: int | None = None
product_name: str
amount: float
deal_date: str
@@ -82,6 +83,32 @@ class DealCreate(BaseModel):
status: str = "closed"
class ProductCreate(BaseModel):
"""新增产品请求模型。"""
name: str
category: str
spec: str | None = None
price: float | None = None
description: str | None = None
positioning: str | None = None
selling_points: list[str] | None = None
target_audience: str | None = None
status: str = "active"
class ProductUpdate(BaseModel):
"""编辑产品请求模型。"""
name: str | None = None
category: str | None = None
spec: str | None = None
price: float | None = None
description: str | None = None
positioning: str | None = None
selling_points: list[str] | None = None
target_audience: str | None = None
status: str | None = None
class AnalyzeRequest(BaseModel):
mode: str = "rule"
force: bool = False
@@ -134,11 +161,33 @@ def dashboard():
for r in cur.fetchall()
]
# 按产品线统计本月销售额
cur.execute("""
SELECT p.category,
COALESCE(sum(d.amount), 0) AS amount,
count(*) AS deal_count
FROM deal d
LEFT JOIN product p ON p.id = d.product_id
WHERE d.status = 'closed'
AND d.deal_date >= date_trunc('month', now())
GROUP BY p.category
ORDER BY amount DESC
""")
product_stats = [
{
"category": r[0] or "未分类",
"amount": float(r[1]),
"deal_count": r[2],
}
for r in cur.fetchall()
]
return {
"total_messages": total_messages,
"active_customers": active_customers,
"monthly_deal_amount": monthly_deal_amount,
"salespersons": salespersons,
"product_stats": product_stats,
}
finally:
put_conn(conn)
@@ -371,14 +420,23 @@ def create_deal(deal: DealCreate):
if row:
contact_id = row[0]
# 如果有 product_id 但没有 product_name,从 product 表获取
product_name = deal.product_name
if deal.product_id and not product_name:
cur.execute("SELECT name, spec FROM product WHERE id = %s", (deal.product_id,))
prow = cur.fetchone()
if prow:
product_name = f"{prow[0]} {prow[1]}" if prow[1] else prow[0]
cur.execute("""
INSERT INTO deal (salesperson_id, customer_id, contact_id, product_name,
INSERT INTO deal (salesperson_id, customer_id, contact_id, product_id, product_name,
amount, deal_date, status, notes)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING id
""", (
deal.salesperson_id, deal.customer_id, contact_id,
deal.product_name, deal.amount, deal.deal_date,
deal.product_id, product_name,
deal.amount, deal.deal_date,
deal.status, deal.notes,
))
deal_id = cur.fetchone()[0]
@@ -394,6 +452,7 @@ def create_deal(deal: DealCreate):
@app.get("/api/deals")
def list_deals(
salesperson_id: int | None = Query(None),
product_id: int | None = Query(None),
start_date: str | None = Query(None),
end_date: str | None = Query(None),
):
@@ -403,7 +462,7 @@ def list_deals(
sql = """
SELECT d.id, d.salesperson_id, s.name AS salesperson_name,
d.customer_id, cu.customer_name,
d.product_name, d.amount, d.deal_date, d.status, d.notes
d.product_id, d.product_name, d.amount, d.deal_date, d.status, d.notes
FROM deal d
JOIN salesperson s ON s.id = d.salesperson_id
LEFT JOIN customer cu ON cu.id = d.customer_id
@@ -413,6 +472,9 @@ def list_deals(
if salesperson_id is not None:
conditions.append("d.salesperson_id = %s")
params.append(salesperson_id)
if product_id is not None:
conditions.append("d.product_id = %s")
params.append(product_id)
if start_date:
conditions.append("d.deal_date >= %s")
params.append(start_date)
@@ -428,10 +490,11 @@ def list_deals(
"id": r[0], "salesperson_id": r[1],
"salesperson_name": r[2],
"customer_id": r[3], "customer_name": r[4],
"product_name": r[5],
"amount": float(r[6]),
"deal_date": r[7].isoformat() if r[7] else None,
"status": r[8], "notes": r[9],
"product_id": r[5],
"product_name": r[6],
"amount": float(r[7]),
"deal_date": r[8].isoformat() if r[8] else None,
"status": r[9], "notes": r[10],
}
for r in cur.fetchall()
]
@@ -439,6 +502,155 @@ def list_deals(
put_conn(conn)
# ---------------------------------------------------------------------------
# 产品管理 API
# ---------------------------------------------------------------------------
@app.get("/api/products")
def list_products(
category: str | None = Query(None),
status: str | None = Query(None),
):
"""获取产品列表,支持按产品线和状态筛选。"""
conn = get_conn()
try:
with conn.cursor() as cur:
sql = """
SELECT p.id, p.name, p.category, p.spec, p.price,
p.description, p.positioning, p.selling_points,
p.target_audience, p.status, p.created_at,
COALESCE(ds.deal_count, 0) AS deal_count,
COALESCE(ds.total_amount, 0) AS total_amount
FROM product p
LEFT JOIN LATERAL (
SELECT count(*) AS deal_count, COALESCE(sum(d.amount), 0) AS total_amount
FROM deal d WHERE d.product_id = p.id AND d.status = 'closed'
) ds ON true
"""
conditions = []
params = []
if category:
conditions.append("p.category = %s")
params.append(category)
if status:
conditions.append("p.status = %s")
params.append(status)
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += " ORDER BY p.category, p.id"
cur.execute(sql, params)
return [
{
"id": r[0], "name": r[1], "category": r[2],
"spec": r[3], "price": float(r[4]) if r[4] else None,
"description": r[5], "positioning": r[6],
"selling_points": parse_pg_array(r[7]),
"target_audience": r[8],
"status": r[9],
"created_at": r[10].isoformat() if r[10] else None,
"deal_count": r[11],
"total_amount": float(r[12]),
}
for r in cur.fetchall()
]
finally:
put_conn(conn)
@app.post("/api/products")
def create_product(product: ProductCreate):
"""新增产品。"""
conn = get_conn()
try:
with conn.cursor() as cur:
cur.execute("""
INSERT INTO product (name, category, spec, price, description, positioning, selling_points, target_audience, status)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING id
""", (
product.name, product.category, product.spec,
product.price, product.description, product.positioning,
product.selling_points, product.target_audience,
product.status,
))
product_id = cur.fetchone()[0]
conn.commit()
return {"id": product_id, "message": "产品已创建"}
except Exception:
conn.rollback()
raise
finally:
put_conn(conn)
@app.put("/api/products/{product_id}")
def update_product(product_id: int, product: ProductUpdate):
"""编辑产品信息。"""
conn = get_conn()
try:
with conn.cursor() as cur:
fields = []
params = []
for field in ["name", "category", "spec", "price", "description", "positioning", "selling_points", "target_audience", "status"]:
val = getattr(product, field)
if val is not None:
fields.append(f"{field} = %s")
params.append(val)
if not fields:
raise HTTPException(status_code=400, detail="没有需要更新的字段")
params.append(product_id)
cur.execute(
f"UPDATE product SET {', '.join(fields)} WHERE id = %s RETURNING id",
params,
)
row = cur.fetchone()
if not row:
raise HTTPException(status_code=404, detail="产品不存在")
conn.commit()
return {"id": product_id, "message": "产品已更新"}
except Exception:
conn.rollback()
raise
finally:
put_conn(conn)
@app.delete("/api/products/{product_id}")
def delete_product(product_id: int):
"""删除产品(软删除,设为 inactive)。"""
conn = get_conn()
try:
with conn.cursor() as cur:
cur.execute(
"UPDATE product SET status = 'inactive' WHERE id = %s RETURNING id",
(product_id,),
)
row = cur.fetchone()
if not row:
raise HTTPException(status_code=404, detail="产品不存在")
conn.commit()
return {"id": product_id, "message": "产品已下架"}
except Exception:
conn.rollback()
raise
finally:
put_conn(conn)
@app.get("/api/products/categories")
def list_product_categories():
"""获取所有产品线(去重)。"""
conn = get_conn()
try:
with conn.cursor() as cur:
cur.execute(
"SELECT DISTINCT category FROM product WHERE status = 'active' ORDER BY category"
)
return [r[0] for r in cur.fetchall()]
finally:
put_conn(conn)
@app.post("/api/analyze")
def trigger_analyze(req: AnalyzeRequest):
"""触发 AI 分析(调用 analyze.py 脚本)。"""
@@ -510,8 +722,11 @@ def list_conversations(
@app.post("/api/conversations/{customer_id}/paradigm")
def generate_paradigm(customer_id: int):
"""调用 LLM 生成标准范式对话。"""
def generate_paradigm(
customer_id: int,
product_id: int | None = Query(None),
):
"""调用 LLM 生成标准范式对话,基于产品详情动态构建 prompt。"""
import os as _os
import urllib.request as _urllib
import urllib.error as _urllib_err
@@ -535,6 +750,53 @@ def generate_paradigm(customer_id: int):
if not customer:
raise HTTPException(status_code=404, detail="客户不存在")
# 如果未指定 product_id,尝试从客户成交记录中推断
if product_id is None:
cur.execute("""
SELECT d.product_id FROM deal d
WHERE d.customer_id = %s AND d.product_id IS NOT NULL
ORDER BY d.deal_date DESC LIMIT 1
""", (customer_id,))
deal_row = cur.fetchone()
if deal_row:
product_id = deal_row[0]
# 从产品表获取产品详情;如果仍无 product_id,取所有 active 产品汇总
product_info_text = ""
if product_id is not None:
cur.execute("""
SELECT name, spec, price, description, positioning,
selling_points, target_audience, category
FROM product WHERE id = %s
""", (product_id,))
prow = cur.fetchone()
if prow:
sp_list = parse_pg_array(prow[4])
product_info_text = (
f"### 主推产品:{prow[0]}{prow[1]}\n"
f"- 产品线:{prow[7]}\n"
f"- 价格:¥{float(prow[2]):.2f}\n"
f"- 定位:{prow[4] or '未设定'}\n"
f"- 目标人群:{prow[6] or '未设定'}\n"
f"- 产品描述:{prow[3] or '未设定'}\n"
f"- 核心卖点:\n" + "\n".join(f" {i+1}. {sp}" for i, sp in enumerate(sp_list))
)
else:
cur.execute("""
SELECT name, spec, price, positioning, target_audience, category
FROM product WHERE status = 'active' ORDER BY category, id
""")
products = cur.fetchall()
if products:
lines = []
for p in products:
lines.append(
f"- {p[0]}{p[1]})¥{float(p[2]):.2f} | {p[5]} | 定位:{p[3] or '未设定'} | 目标:{p[4] or '未设定'}"
)
product_info_text = "### 全部在售产品\n" + "\n".join(lines)
else:
product_info_text = "暂无产品数据"
# 获取对话记录
cur.execute("SELECT contact_id FROM customer WHERE id = %s", (customer_id,))
contact_id = cur.fetchone()[0]
@@ -570,10 +832,10 @@ def generate_paradigm(customer_id: int):
objections_str = "".join(parse_pg_array(objections)) if objections else ""
key_needs_str = "".join(parse_pg_array(key_needs)) if key_needs else "未明确"
prompt = f"""你是一个微信销售对话分析专家。请基于以下真实销售对话,生成一套标准范式对话模板。
prompt = f"""你是一个微信销售对话分析专家。请基于以下产品详情和真实销售对话,生成一套标准范式对话模板。
## 产品背景
益童宝儿童益生菌粉:丹麦进口菌株,主打小儿抗过敏(湿疹、鼻炎、食物过敏),298元/盒,3盒套餐798元,6盒套餐1499元。目标客户是宝妈。
{product_info_text}
## 客户信息
- 客户: {customer_name}
@@ -588,12 +850,13 @@ def generate_paradigm(customer_id: int):
{dialog_text}
## 输出要求
请基于上述真实对话,提炼并生成一套**标准范式对话模板**,即针对此类客户的最优销售话术流程。输出严格 JSON:
请基于上述产品详情(定位、卖点、目标人群)和真实对话,提炼并生成一套**标准范式对话模板**,即针对此类客户的最优销售话术流程。话术要自然融入产品的核心卖点和定位,不要生硬推销。输出严格 JSON
- customer_type: 客户类型描述(如"湿疹宝妈-价格敏感型"
- product_positioning: 对该客户的产品推荐理由(结合产品定位和客户需求,1-2句话)
- stages: 按销售阶段排列的对话步骤数组,每个元素包含:
- stage: 阶段名称(建立联系/需求发现/报价/异议处理/成交)
- goal: 该阶段目标
- sales_script: 销售话术(1-3句,具体可执行)
- sales_script: 销售话术(1-3句,具体可执行,自然融入产品卖点
- expected_response: 预期客户回应
- tips: 该阶段技巧提示
- key_techniques: 核心销售技巧总结(数组,每条一句话)