This commit is contained in:
2026-02-27 16:03:04 +08:00
commit 5aedf1665d
137 changed files with 17604 additions and 0 deletions

0
db/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

216
db/chat_log_db.py Normal file
View File

@@ -0,0 +1,216 @@
"""
聊天记录数据库SQLite
每条消息独立存储按客户ID分开支持查询和展示。
"""
import sqlite3
import os
from datetime import datetime
from typing import List, Dict, Optional
_DB_PATH = os.path.join(os.path.dirname(__file__), "chat_log_db", "chats.db")
def _get_conn() -> sqlite3.Connection:
os.makedirs(os.path.dirname(_DB_PATH), exist_ok=True)
conn = sqlite3.connect(_DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def init_db():
"""建表(首次运行时自动调用)"""
with _get_conn() as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS chat_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
customer_id TEXT NOT NULL,
customer_name TEXT DEFAULT '',
acc_id TEXT DEFAULT '',
platform TEXT DEFAULT '',
direction TEXT NOT NULL CHECK(direction IN ('in','out')),
message TEXT NOT NULL,
msg_type INTEGER DEFAULT 0,
timestamp TEXT NOT NULL
)
""")
conn.execute("CREATE INDEX IF NOT EXISTS idx_customer ON chat_logs(customer_id)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_ts ON chat_logs(timestamp)")
# 兼容旧表:若缺少 acc_id 列则补上(必须在创建该列索引之前)
try:
conn.execute("ALTER TABLE chat_logs ADD COLUMN acc_id TEXT DEFAULT ''")
except Exception:
pass
conn.execute("CREATE INDEX IF NOT EXISTS idx_acc ON chat_logs(acc_id)")
conn.commit()
init_db()
# ========== 写入 ==========
def log_message(
customer_id: str,
message: str,
direction: str, # "in" = 客户发来,"out" = 客服回复
customer_name: str = "",
acc_id: str = "", # 店铺账号ID
platform: str = "",
msg_type: int = 0,
):
"""记录一条聊天消息"""
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with _get_conn() as conn:
conn.execute(
"INSERT INTO chat_logs "
"(customer_id, customer_name, acc_id, platform, direction, message, msg_type, timestamp) "
"VALUES (?,?,?,?,?,?,?,?)",
(customer_id, customer_name, acc_id, platform, direction, message, msg_type, ts),
)
conn.commit()
# ========== 查询 ==========
def get_customers(limit: int = 100) -> List[Dict]:
"""返回所有有记录的客户列表(按最新消息时间排序)"""
with _get_conn() as conn:
rows = conn.execute("""
SELECT
customer_id,
MAX(customer_name) AS customer_name,
MAX(platform) AS platform,
COUNT(*) AS total_msgs,
SUM(direction='in') AS recv,
SUM(direction='out') AS sent,
MAX(timestamp) AS last_time
FROM chat_logs
GROUP BY customer_id
ORDER BY last_time DESC
LIMIT ?
""", (limit,)).fetchall()
return [dict(r) for r in rows]
def get_conversation(customer_id: str, limit: int = 200) -> List[Dict]:
"""返回某客户的全部对话记录(按时间升序)"""
with _get_conn() as conn:
rows = conn.execute("""
SELECT id, direction, message, msg_type, timestamp, acc_id
FROM chat_logs
WHERE customer_id = ?
ORDER BY timestamp ASC, id ASC
LIMIT ?
""", (customer_id, limit)).fetchall()
return [dict(r) for r in rows]
def get_recent_conversation(customer_id: str, acc_id: str = "", limit: int = 10) -> List[Dict]:
"""返回某客户近期对话(同店铺),用于企微推送保持连贯"""
with _get_conn() as conn:
if acc_id:
rows = conn.execute("""
SELECT id, direction, message, timestamp, acc_id
FROM chat_logs
WHERE customer_id = ? AND acc_id = ?
ORDER BY id DESC
LIMIT ?
""", (customer_id, acc_id, limit)).fetchall()
else:
rows = conn.execute("""
SELECT id, direction, message, timestamp, acc_id
FROM chat_logs
WHERE customer_id = ?
ORDER BY id DESC
LIMIT ?
""", (customer_id, limit)).fetchall()
out = [dict(r) for r in reversed(rows)]
return out
def get_conversation_today(customer_id: str) -> List[Dict]:
"""返回某客户今天的对话"""
today = datetime.now().strftime("%Y-%m-%d")
with _get_conn() as conn:
rows = conn.execute("""
SELECT id, direction, message, msg_type, timestamp
FROM chat_logs
WHERE customer_id = ? AND timestamp LIKE ?
ORDER BY timestamp ASC, id ASC
""", (customer_id, f"{today}%")).fetchall()
return [dict(r) for r in rows]
def get_daily_stats(date: str = "") -> List[Dict]:
"""
返回指定日期各店铺的统计数据。
date 格式 'YYYY-MM-DD',默认今天。
每条记录对应一个 acc_id店铺
"""
if not date:
date = datetime.now().strftime("%Y-%m-%d")
with _get_conn() as conn:
rows = conn.execute("""
SELECT
acc_id,
platform,
COUNT(DISTINCT customer_id) AS unique_customers,
COUNT(*) AS total_msgs,
SUM(direction='in') AS recv,
SUM(direction='out') AS sent,
MIN(timestamp) AS first_msg,
MAX(timestamp) AS last_msg
FROM chat_logs
WHERE timestamp LIKE ?
GROUP BY acc_id
ORDER BY unique_customers DESC
""", (f"{date}%",)).fetchall()
return [dict(r) for r in rows]
def get_daily_conversations(date: str = "") -> List[Dict]:
"""
返回指定日期每个客户的对话摘要每人最多取前5条消息用于 AI 摘要)。
"""
if not date:
date = datetime.now().strftime("%Y-%m-%d")
with _get_conn() as conn:
rows = conn.execute("""
SELECT
acc_id,
customer_id,
MAX(customer_name) AS customer_name,
COUNT(*) AS msg_count,
GROUP_CONCAT(
CASE WHEN direction='in' THEN '买:' || SUBSTR(message,1,40)
ELSE '客:' || SUBSTR(message,1,40) END,
' | '
) AS snippet
FROM chat_logs
WHERE timestamp LIKE ?
GROUP BY acc_id, customer_id
ORDER BY acc_id, MAX(timestamp) DESC
""", (f"{date}%",)).fetchall()
return [dict(r) for r in rows]
def search_messages(keyword: str, customer_id: Optional[str] = None, limit: int = 50) -> List[Dict]:
"""全文搜索消息"""
if customer_id:
with _get_conn() as conn:
rows = conn.execute("""
SELECT customer_id, customer_name, direction, message, timestamp
FROM chat_logs
WHERE customer_id = ? AND message LIKE ?
ORDER BY timestamp DESC LIMIT ?
""", (customer_id, f"%{keyword}%", limit)).fetchall()
else:
with _get_conn() as conn:
rows = conn.execute("""
SELECT customer_id, customer_name, direction, message, timestamp
FROM chat_logs
WHERE message LIKE ?
ORDER BY timestamp DESC LIMIT ?
""", (f"%{keyword}%", limit)).fetchall()
return [dict(r) for r in rows]

BIN
db/chat_log_db/chats.db Normal file

Binary file not shown.

729
db/customer_db.py Normal file
View File

@@ -0,0 +1,729 @@
"""客户画像数据库"""
import os
import json
from datetime import datetime
from typing import Optional, Dict, Any, List
from dataclasses import dataclass, asdict
from collections import defaultdict
@dataclass
class CustomerProfile:
"""客户画像"""
# 基础信息
customer_id: str
name: str = ""
nickname: str = ""
email: str = ""
phone: str = ""
wechat: str = "" # 微信号
address: str = "" # 收货地址
# 账号信息
platform: str = "" # 平台(淘宝/天猫/京东等)
platform_id: str = "" # 平台账号ID
# 需求相关
budget: str = "" # 预算50-100元
budget_range_min: float = 0 # 预算下限
budget_range_max: float = 0 # 预算上限
requirements: List[str] = None # 历史需求列表
preference_services: List[str] = None # 偏好服务类型(修图/找图/设计等)
# 消费分析
total_orders: int = 0
total_spent: float = 0.0
avg_order_value: float = 0.0 # 平均客单价
purchase_frequency: str = "" # 购买频次(高/中/低)
last_order_date: str = "" # 最后下单时间
first_order_date: str = "" # 首次下单时间
# 订单相关
order_ids: List[str] = None
pending_orders: int = 0 # 待处理订单
completed_orders: int = 0 # 已完成订单
refund_count: int = 0 # 退款次数
# 性格/行为
personality: List[str] = None # 性格标签(爽快/纠结/砍价狂/爽快/墨迹)
communication_prefer: str = "" # 沟通偏好(文字/语音/图片)
response_speed: str = "" # 响应速度偏好(快/慢)
patience_level: str = "" # 耐心程度(高/中/低)
# 客户价值
customer_level: str = "C" # 客户等级A/B/C/D
vip: bool = False
vip_level: int = 0 # VIP等级
# 报价记录
last_price: int = 0 # 上次报价
last_price_time: str = "" # 上次报价时间
last_quote_no_convert: bool = False # 上次报价后未成交,下次可适当降低
last_min_price: int = 0 # 最近图片分析的最低价
last_image_url: str = "" # 最近一次发来的图片URL
last_image_time: str = "" # 图片发送时间
last_gemini_prompt: str = "" # 最近一次图片的 Gemini 处理提示词
last_aspect_ratio: str = "1:1" # 最近一次图片的建议输出比例
last_perspective: str = "no" # 最近一次图片的透视状态
# 当前任务状态
processing_status: str = "" # 待处理/处理中/等待确认/已完成
processing_image_url: str = "" # 当前正在处理的图片URL
expected_done_at: str = "" # 预计完成时间
# 让价记录
discount_given_count: int = 0 # 历史累计让价次数
lowest_price_accepted: int = 0 # 客户接受过的最低价
# 格式偏好
preferred_format: str = "" # jpg / psd / png
preferred_size: str = "" # 有没有问过分辨率/尺寸要求
# 对话摘要
last_conversation_summary: str = "" # 上次对话摘要(一句话)
last_conversation_time: str = "" # 上次对话时间
# 来图习惯
total_images_sent: int = 0 # 历史发图总数
complexity_history: List[str] = None # 历史复杂度列表,如 ["hard","complex","normal"]
image_type_history: List[str] = None # 图片类型历史,如 ["印花","logo","人物"]
# AI 决策辅助
price_sensitivity: str = "" # 价格敏感度:高/中/低(自动计算)
decision_speed: str = "" # 决策速度:快/慢(自动标记)
revision_count: int = 0 # 历史总改稿次数
revision_orders: int = 0 # 有改稿的订单数
total_completed_orders: int = 0 # 已完成出图的订单数
# 业务分析
bulk_potential: str = "" # 批量潜力:有/无/未知
churn_risk: str = "" # 流失风险:高/中/低(自动计算)
upsell_opportunity: List[str] = None # 加购机会,如 ["分层PSD","批量打包"]
# 运营
blacklist: bool = False # 黑名单
blacklist_reason: str = "" # 拉黑原因
vip_custom_price: int = 0 # VIP专属报价0=无)
last_email_status: str = "" # 最后一次邮件发送状态sent/failed
# 评价
good_reviews: int = 0 # 好评数
bad_reviews: int = 0 # 差评数
dispute_count: int = 0 # 纠纷次数
# 跟进信息
follow_up_by: str = "" # 跟进销售
follow_up_date: str = ""
next_follow_date: str = "" # 下次跟进日期
# 来源
source: str = "" # 来源渠道(自然流量/推广/老客户转介绍)
coupon_used: str = "" # 使用过的优惠券
# 备注
notes: List[str] = None # 备注列表
tags: List[str] = None # 自定义标签
# 时间
created_at: str = ""
last_contact: str = ""
last_update: str = ""
def __post_init__(self):
if self.requirements is None:
self.requirements = []
if self.preference_services is None:
self.preference_services = []
if self.order_ids is None:
self.order_ids = []
if self.personality is None:
self.personality = []
if self.notes is None:
self.notes = []
if self.tags is None:
self.tags = []
if self.complexity_history is None:
self.complexity_history = []
if self.image_type_history is None:
self.image_type_history = []
if self.upsell_opportunity is None:
self.upsell_opportunity = []
class CustomerDatabase:
"""客户数据库"""
def __init__(self, db_path: str = "customer_db"):
self.db_path = db_path
self.customers_file = os.path.join(db_path, "customers.json")
self._ensure_db()
def _ensure_db(self):
if not os.path.exists(self.db_path):
os.makedirs(self.db_path)
if not os.path.exists(self.customers_file):
self._save_customers({})
def _load_customers(self) -> Dict[str, dict]:
try:
with open(self.customers_file, 'r', encoding='utf-8') as f:
return json.load(f)
except:
return {}
def _save_customers(self, customers: Dict[str, dict]):
with open(self.customers_file, 'w', encoding='utf-8') as f:
json.dump(customers, f, ensure_ascii=False, indent=2)
def get_customer(self, customer_id: str) -> CustomerProfile:
customers = self._load_customers()
data = customers.get(customer_id, {})
# 确保不重复传递 customer_id
data.pop('customer_id', None)
return CustomerProfile(customer_id=customer_id, **data)
def save_customer(self, profile: CustomerProfile):
profile.last_update = datetime.now().isoformat()
customers = self._load_customers()
customers[profile.customer_id] = asdict(profile)
self._save_customers(customers)
# ========== 基础信息 ==========
def update_info(self, customer_id: str, **kwargs):
"""批量更新客户信息"""
profile = self.get_customer(customer_id)
for key, value in kwargs.items():
if hasattr(profile, key):
setattr(profile, key, value)
self.save_customer(profile)
def update_email(self, customer_id: str, email: str):
profile = self.get_customer(customer_id)
profile.email = email
self.save_customer(profile)
def update_phone(self, customer_id: str, phone: str):
profile = self.get_customer(customer_id)
profile.phone = phone
self.save_customer(profile)
def update_wechat(self, customer_id: str, wechat: str):
profile = self.get_customer(customer_id)
profile.wechat = wechat
self.save_customer(profile)
def update_address(self, customer_id: str, address: str):
profile = self.get_customer(customer_id)
profile.address = address
self.save_customer(profile)
# ========== 需求相关 ==========
def add_requirement(self, customer_id: str, requirement: str):
"""添加需求"""
profile = self.get_customer(customer_id)
if requirement not in profile.requirements:
profile.requirements.append(requirement)
profile.last_contact = datetime.now().isoformat()
self.save_customer(profile)
def set_budget(self, customer_id: str, budget: str, min_val: float = 0, max_val: float = 0):
"""设置预算"""
profile = self.get_customer(customer_id)
profile.budget = budget
profile.budget_range_min = min_val
profile.budget_range_max = max_val
self.save_customer(profile)
def add_preference_service(self, customer_id: str, service: str):
"""添加偏好服务"""
profile = self.get_customer(customer_id)
if service not in profile.preference_services:
profile.preference_services.append(service)
self.save_customer(profile)
# ========== 消费分析 ==========
def add_order(self, customer_id: str, order_id: str, amount: float = 0):
"""添加订单"""
profile = self.get_customer(customer_id)
if order_id not in profile.order_ids:
profile.order_ids.append(order_id)
profile.total_orders += 1
profile.total_spent += amount
# 计算平均客单价
if profile.total_orders > 0:
profile.avg_order_value = profile.total_spent / profile.total_orders
# 更新客单价等级
if profile.avg_order_value >= 100:
profile.customer_level = "A"
elif profile.avg_order_value >= 50:
profile.customer_level = "B"
elif profile.avg_order_value >= 20:
profile.customer_level = "C"
else:
profile.customer_level = "D"
profile.last_order_date = datetime.now().isoformat()
if not profile.first_order_date:
profile.first_order_date = datetime.now().isoformat()
self.save_customer(profile)
def update_order_status(self, customer_id: str, pending: int = None, completed: int = None):
"""更新订单状态"""
profile = self.get_customer(customer_id)
if pending is not None:
profile.pending_orders = pending
if completed is not None:
profile.completed_orders = completed
self.save_customer(profile)
def add_refund(self, customer_id: str):
"""增加退款次数"""
profile = self.get_customer(customer_id)
profile.refund_count += 1
self.save_customer(profile)
# ========== 性格分析 ==========
def add_personality_tag(self, customer_id: str, tag: str):
"""添加性格标签"""
profile = self.get_customer(customer_id)
if tag not in profile.personality:
profile.personality.append(tag)
self.save_customer(profile)
def set_communication_prefer(self, customer_id: str, prefer: str):
"""设置沟通偏好"""
profile = self.get_customer(customer_id)
profile.communication_prefer = prefer
self.save_customer(profile)
def analyze_purchase_frequency(self, customer_id: str):
"""分析购买频次"""
profile = self.get_customer(customer_id)
if profile.first_order_date and profile.last_order_date:
try:
first = datetime.fromisoformat(profile.first_order_date)
last = datetime.fromisoformat(profile.last_order_date)
days = (last - first).days
if days == 0:
profile.purchase_frequency = ""
elif days < 30:
profile.purchase_frequency = ""
elif days < 90:
profile.purchase_frequency = ""
else:
profile.purchase_frequency = ""
self.save_customer(profile)
except:
pass
# ========== 评价相关 ==========
def add_good_review(self, customer_id: str):
profile = self.get_customer(customer_id)
profile.good_reviews += 1
self.save_customer(profile)
def add_bad_review(self, customer_id: str):
profile = self.get_customer(customer_id)
profile.bad_reviews += 1
self.save_customer(profile)
def add_dispute(self, customer_id: str):
profile = self.get_customer(customer_id)
profile.dispute_count += 1
self.save_customer(profile)
# ========== 标签/备注 ==========
def add_tag(self, customer_id: str, tag: str):
profile = self.get_customer(customer_id)
if tag not in profile.tags:
profile.tags.append(tag)
self.save_customer(profile)
def remove_tag(self, customer_id: str, tag: str):
profile = self.get_customer(customer_id)
if tag in profile.tags:
profile.tags.remove(tag)
self.save_customer(profile)
def add_note(self, customer_id: str, note: str):
profile = self.get_customer(customer_id)
note_with_time = f"[{datetime.now().strftime('%Y-%m-%d %H:%M')}] {note}"
profile.notes.append(note_with_time)
self.save_customer(profile)
# ========== 跟进 ==========
def set_follow_up(self, customer_id: str, by: str, next_date: str = ""):
profile = self.get_customer(customer_id)
profile.follow_up_by = by
profile.follow_up_date = datetime.now().isoformat()
profile.next_follow_date = next_date
self.save_customer(profile)
def set_source(self, customer_id: str, source: str):
profile = self.get_customer(customer_id)
profile.source = source
self.save_customer(profile)
# ========== 报价记录 ==========
def update_last_price(self, customer_id: str, price: int):
"""更新最近一次报价"""
profile = self.get_customer(customer_id)
profile.last_price = price
profile.last_price_time = datetime.now().isoformat()
self.save_customer(profile)
def update_last_min_price(self, customer_id: str, min_price: int):
"""更新最近图片的最低价(用于杀价拦截与策略)"""
profile = self.get_customer(customer_id)
profile.last_min_price = int(min_price or 0)
self.save_customer(profile)
def mark_quote_no_convert(self, customer_id: str):
"""标记:上次报价后未成交,下次可适当降低"""
profile = self.get_customer(customer_id)
profile.last_quote_no_convert = True
self.save_customer(profile)
def clear_quote_no_convert(self, customer_id: str):
"""成交后清除未成交标记"""
profile = self.get_customer(customer_id)
profile.last_quote_no_convert = False
self.save_customer(profile)
def update_last_image(
self,
customer_id: str,
image_url: str,
complexity: str = "",
gemini_prompt: str = "",
aspect_ratio: str = "",
perspective: str = "",
):
"""更新客户最近发来的图片URL并记录复杂度历史及处理参数"""
profile = self.get_customer(customer_id)
profile.last_image_url = image_url
profile.last_image_time = datetime.now().isoformat()
profile.total_images_sent += 1
if complexity:
profile.complexity_history.append(complexity)
profile.complexity_history = profile.complexity_history[-20:]
if gemini_prompt:
profile.last_gemini_prompt = gemini_prompt
if aspect_ratio:
profile.last_aspect_ratio = aspect_ratio
if perspective:
profile.last_perspective = perspective
self.save_customer(profile)
def update_processing_status(self, customer_id: str, status: str, image_url: str = "", expected_done_at: str = ""):
"""更新当前任务处理状态"""
profile = self.get_customer(customer_id)
profile.processing_status = status
if image_url:
profile.processing_image_url = image_url
if expected_done_at:
profile.expected_done_at = expected_done_at
self.save_customer(profile)
def record_discount(self, customer_id: str, final_price: int):
"""记录一次让价,并更新客户接受的最低价"""
profile = self.get_customer(customer_id)
profile.discount_given_count += 1
if final_price > 0 and (profile.lowest_price_accepted == 0 or final_price < profile.lowest_price_accepted):
profile.lowest_price_accepted = final_price
self.save_customer(profile)
def update_preferred_format(self, customer_id: str, fmt: str):
"""更新客户格式偏好jpg/psd/png"""
profile = self.get_customer(customer_id)
profile.preferred_format = fmt
self.save_customer(profile)
def update_preferred_size(self, customer_id: str, size_desc: str):
"""更新客户尺寸/分辨率偏好"""
profile = self.get_customer(customer_id)
profile.preferred_size = size_desc
self.save_customer(profile)
def save_conversation_summary(self, customer_id: str, summary: str):
"""保存本次对话摘要(一句话)"""
profile = self.get_customer(customer_id)
profile.last_conversation_summary = summary
profile.last_conversation_time = datetime.now().isoformat()
self.save_customer(profile)
def add_image_type(self, customer_id: str, image_type: str):
"""记录客户发图的类型(印花/logo/人物/产品等)"""
profile = self.get_customer(customer_id)
profile.image_type_history.append(image_type)
profile.image_type_history = profile.image_type_history[-20:]
self.save_customer(profile)
def update_decision_speed(self, customer_id: str, speed: str):
"""更新决策速度:快/慢"""
profile = self.get_customer(customer_id)
profile.decision_speed = speed
self.save_customer(profile)
def record_revision(self, customer_id: str):
"""记录一次改稿"""
profile = self.get_customer(customer_id)
profile.revision_count += 1
self.save_customer(profile)
def complete_order(self, customer_id: str, had_revision: bool = False):
"""标记一笔订单完成出图"""
profile = self.get_customer(customer_id)
profile.total_completed_orders += 1
if had_revision:
profile.revision_orders += 1
self.save_customer(profile)
def set_bulk_potential(self, customer_id: str, potential: str):
"""设置批量潜力:有/无/未知"""
profile = self.get_customer(customer_id)
profile.bulk_potential = potential
self.save_customer(profile)
def add_upsell_opportunity(self, customer_id: str, opportunity: str):
"""添加加购机会标记,如 '分层PSD' / '批量打包'"""
profile = self.get_customer(customer_id)
if opportunity not in profile.upsell_opportunity:
profile.upsell_opportunity.append(opportunity)
self.save_customer(profile)
def set_blacklist(self, customer_id: str, reason: str = ""):
"""将客户加入黑名单"""
profile = self.get_customer(customer_id)
profile.blacklist = True
profile.blacklist_reason = reason
self.save_customer(profile)
def unset_blacklist(self, customer_id: str):
"""解除黑名单"""
profile = self.get_customer(customer_id)
profile.blacklist = False
profile.blacklist_reason = ""
self.save_customer(profile)
def set_vip_custom_price(self, customer_id: str, price: int):
"""设置 VIP 专属报价0=取消专属价)"""
profile = self.get_customer(customer_id)
profile.vip_custom_price = price
self.save_customer(profile)
def update_email_status(self, customer_id: str, status: str):
"""更新邮件发送状态sent/failed"""
profile = self.get_customer(customer_id)
profile.last_email_status = status
self.save_customer(profile)
def auto_compute_tags(self, customer_id: str):
"""自动计算并更新衍生标签(价格敏感度、流失风险、决策速度)"""
profile = self.get_customer(customer_id)
# 价格敏感度:根据让价次数和订单数
if profile.total_orders > 0:
ratio = profile.discount_given_count / max(profile.total_orders, 1)
if ratio >= 0.6 or profile.discount_given_count >= 3:
profile.price_sensitivity = ""
elif ratio >= 0.2 or profile.discount_given_count >= 1:
profile.price_sensitivity = ""
else:
profile.price_sensitivity = ""
# 流失风险:根据最后联系时间
if profile.last_contact:
try:
last = datetime.fromisoformat(profile.last_contact)
days = (datetime.now() - last).days
if days > 60:
profile.churn_risk = ""
elif days > 30:
profile.churn_risk = ""
else:
profile.churn_risk = ""
except Exception:
pass
self.save_customer(profile)
# ========== VIP ==========
def set_vip(self, customer_id: str, vip_level: int = 1):
profile = self.get_customer(customer_id)
profile.vip = True
profile.vip_level = vip_level
self.save_customer(profile)
def cancel_vip(self, customer_id: str):
profile = self.get_customer(customer_id)
profile.vip = False
profile.vip_level = 0
self.save_customer(profile)
# ========== 搜索 ==========
def _make_profile(self, cid: str, data: dict) -> CustomerProfile:
"""从原始数据构建 CustomerProfile避免 customer_id 重复"""
d = dict(data)
d.pop('customer_id', None)
return CustomerProfile(customer_id=cid, **d)
def search_by_requirement(self, keyword: str) -> List[CustomerProfile]:
"""按需求搜索"""
customers = self._load_customers()
results = []
for cid, data in customers.items():
requirements = data.get('requirements', [])
if any(keyword in req for req in requirements):
results.append(self._make_profile(cid, data))
return results
def search_by_tag(self, tag: str) -> List[CustomerProfile]:
"""按标签搜索"""
customers = self._load_customers()
results = []
for cid, data in customers.items():
tags = data.get('tags', [])
if tag in tags:
results.append(self._make_profile(cid, data))
return results
def search_by_level(self, level: str) -> List[CustomerProfile]:
"""按客户等级搜索"""
customers = self._load_customers()
results = []
for cid, data in customers.items():
if data.get('customer_level') == level:
results.append(self._make_profile(cid, data))
return results
def get_vip_customers(self) -> List[CustomerProfile]:
"""获取所有VIP客户"""
customers = self._load_customers()
results = []
for cid, data in customers.items():
if data.get('vip'):
results.append(self._make_profile(cid, data))
return results
def get_high_value_customers(self) -> List[CustomerProfile]:
"""获取高价值客户A级"""
return self.search_by_level("A")
# ========== 统计 ==========
def get_stats(self) -> dict:
"""获取统计信息"""
customers = self._load_customers()
total = len(customers)
levels = defaultdict(int)
vip_count = 0
total_revenue = 0
total_orders = 0
for cid, data in customers.items():
levels[data.get('customer_level', 'C')] += 1
if data.get('vip'):
vip_count += 1
total_revenue += data.get('total_spent', 0)
total_orders += data.get('total_orders', 0)
return {
"total_customers": total,
"level_distribution": dict(levels),
"vip_count": vip_count,
"total_revenue": total_revenue,
"total_orders": total_orders,
"avg_order_value": total_revenue / total_orders if total_orders > 0 else 0
}
# ========== 输出 ==========
def get_profile_text(self, customer_id: str) -> str:
"""获取客户画像文本用于AI"""
p = self.get_customer(customer_id)
# 平均复杂度
avg_complexity = ""
if p.complexity_history:
level_map = {"simple": 1, "normal": 2, "complex": 3, "hard": 4}
label_map = {1: "简单", 2: "一般", 3: "复杂", 4: "很复杂"}
avg = sum(level_map.get(c, 2) for c in p.complexity_history) / len(p.complexity_history)
avg_complexity = label_map.get(round(avg), "一般")
# 改稿率
revision_rate = ""
if p.total_completed_orders > 0:
rate = p.revision_orders / p.total_completed_orders
revision_rate = f"{rate:.0%}{p.revision_orders}/{p.total_completed_orders}单有改稿)"
# 常见图片类型
top_types = ""
if p.image_type_history:
from collections import Counter
top = Counter(p.image_type_history).most_common(3)
top_types = "".join(f"{t}({c}次)" for t, c in top)
blacklist_line = f"\n⚠️ 黑名单:{p.blacklist_reason or '已拉黑'}" if p.blacklist else ""
text = f"""
=== 客户档案 ==={blacklist_line}
客户ID: {p.customer_id}
姓名: {p.name or '未知'}
邮箱: {p.email or '未记录'}
电话: {p.phone or '未记录'}
微信: {p.wechat or '未记录'}
--- 消费分析 ---
客户等级: {p.customer_level}{'VIP' if p.vip else ''}
总订单数: {p.total_orders} | 总消费: {p.total_spent}
购买频次: {p.purchase_frequency or '未分析'}
--- 报价与让价 ---
上次报价: {f"{p.last_price}" if p.last_price else "暂无"}{f' | VIP专属价: {p.vip_custom_price}' if p.vip_custom_price else ''}
历史让价: {p.discount_given_count}次 | 接受过的最低价: {f"{p.lowest_price_accepted}" if p.lowest_price_accepted else "暂无"}
价格敏感度: {p.price_sensitivity or '未分析'} | 决策速度: {p.decision_speed or '未知'}
--- 图片习惯 ---
累计发图: {p.total_images_sent}张 | 平均复杂度: {avg_complexity or '暂无'}
常见类型: {top_types or '暂无'}
格式偏好: {p.preferred_format or '未知'} | 尺寸要求: {p.preferred_size or '未知'}
上次发图: {p.last_image_url or '暂无'}
--- 当前任务 ---
处理状态: {p.processing_status or ''} | 预计完成: {p.expected_done_at or '未知'}
--- 服务质量 ---
改稿率: {revision_rate or '暂无'}
批量潜力: {p.bulk_potential or '未知'} | 流失风险: {p.churn_risk or '未分析'}
加购机会: {', '.join(p.upsell_opportunity) if p.upsell_opportunity else '暂无'}
--- 上次对话 ---
摘要: {p.last_conversation_summary or '暂无'}
时间: {p.last_conversation_time or '暂无'}
--- 邮件 ---
最后发送状态: {p.last_email_status or '未知'}
--- 性格与备注 ---
性格标签: {', '.join(p.personality) if p.personality else '暂无'}
备注: {'; '.join(p.notes[-5:]) if p.notes else '暂无'}
"""
return text
# 全局实例
db = CustomerDatabase()

153
db/deal_outcome_db.py Normal file
View File

@@ -0,0 +1,153 @@
# -*- coding: utf-8 -*-
"""
成交/未成交记录 - 用于日报与数据分析
"""
import sqlite3
import os
from datetime import datetime
from typing import List, Dict, Optional
_DB_PATH = os.path.join(os.path.dirname(__file__), "deal_outcome_db", "outcomes.db")
def _get_conn() -> sqlite3.Connection:
os.makedirs(os.path.dirname(_DB_PATH), exist_ok=True)
conn = sqlite3.connect(_DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def _init_db():
with _get_conn() as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS deal_outcomes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
customer_id TEXT NOT NULL,
customer_name TEXT DEFAULT '',
acc_id TEXT DEFAULT '',
platform TEXT DEFAULT '',
date TEXT NOT NULL,
outcome TEXT NOT NULL CHECK(outcome IN ('成交','未成交')),
reason TEXT DEFAULT '',
order_id TEXT DEFAULT '',
amount REAL DEFAULT 0,
discount_given INTEGER DEFAULT 0,
timestamp TEXT NOT NULL
)
""")
conn.execute("CREATE INDEX IF NOT EXISTS idx_deal_date ON deal_outcomes(date)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_deal_customer ON deal_outcomes(customer_id)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_deal_acc ON deal_outcomes(acc_id)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_deal_outcome ON deal_outcomes(outcome)")
conn.commit()
_init_db()
def record_deal(
customer_id: str,
outcome: str,
reason: str = "",
customer_name: str = "",
acc_id: str = "",
platform: str = "",
order_id: str = "",
amount: float = 0,
discount_given: bool = False,
):
"""记录一笔成交或未成交"""
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
date = datetime.now().strftime("%Y-%m-%d")
with _get_conn() as conn:
conn.execute(
"""INSERT INTO deal_outcomes
(customer_id, customer_name, acc_id, platform, date, outcome, reason,
order_id, amount, discount_given, timestamp)
VALUES (?,?,?,?,?,?,?,?,?,?,?)""",
(
customer_id,
customer_name or "",
acc_id or "",
platform or "",
date,
outcome,
reason or "",
order_id or "",
amount,
1 if discount_given else 0,
ts,
),
)
conn.commit()
def get_daily_outcomes(date: str = "") -> List[Dict]:
"""获取指定日期的成交/未成交记录,用于日报"""
if not date:
date = datetime.now().strftime("%Y-%m-%d")
with _get_conn() as conn:
rows = conn.execute(
"""
SELECT customer_id, customer_name, acc_id, outcome, reason,
order_id, amount, discount_given, timestamp
FROM deal_outcomes
WHERE date = ?
ORDER BY timestamp ASC
""",
(date,),
).fetchall()
return [dict(r) for r in rows]
def get_daily_summary(date: str = "") -> Dict:
"""获取指定日期的成交/未成交汇总统计"""
outcomes = get_daily_outcomes(date)
success = [o for o in outcomes if o["outcome"] == "成交"]
fail = [o for o in outcomes if o["outcome"] == "未成交"]
# 按原因分组
fail_by_reason: Dict[str, int] = {}
for o in fail:
r = o.get("reason") or "其他"
fail_by_reason[r] = fail_by_reason.get(r, 0) + 1
return {
"date": date or datetime.now().strftime("%Y-%m-%d"),
"成交数": len(success),
"未成交数": len(fail),
"成交金额": sum(o.get("amount") or 0 for o in success),
"成交明细": success,
"未成交明细": fail,
"未成交原因分布": fail_by_reason,
}
def export_for_analysis(start_date: str = "", end_date: str = "") -> List[Dict]:
"""
导出成交/未成交记录,供数据库分析。
日期格式 YYYY-MM-DD留空则查全部。
"""
with _get_conn() as conn:
if start_date and end_date:
rows = conn.execute(
"""SELECT * FROM deal_outcomes
WHERE date BETWEEN ? AND ?
ORDER BY date, timestamp""",
(start_date, end_date),
).fetchall()
elif start_date:
rows = conn.execute(
"""SELECT * FROM deal_outcomes WHERE date >= ? ORDER BY date, timestamp""",
(start_date,),
).fetchall()
elif end_date:
rows = conn.execute(
"""SELECT * FROM deal_outcomes WHERE date <= ? ORDER BY date, timestamp""",
(end_date,),
).fetchall()
else:
rows = conn.execute(
"""SELECT * FROM deal_outcomes ORDER BY date, timestamp"""
).fetchall()
return [dict(r) for r in rows]

Binary file not shown.

159
db/designer_roster_db.py Normal file
View File

@@ -0,0 +1,159 @@
# -*- coding: utf-8 -*-
"""
设计师派单数据库SQLite
同一设计师在不同店铺对应不同 group_id派单时从在线设计师中轮询。
企微群「上线」/「下线」通过 update_online(wechat_user_id, is_online) 更新。
"""
import sqlite3
import os
from typing import Optional
_DB_PATH = os.path.join(os.path.dirname(__file__), "designer_roster_db", "roster.db")
def _get_conn() -> sqlite3.Connection:
os.makedirs(os.path.dirname(_DB_PATH), exist_ok=True)
conn = sqlite3.connect(_DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def init_db():
with _get_conn() as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS designers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
wechat_user_id TEXT UNIQUE NOT NULL
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS designer_shops (
designer_id INTEGER NOT NULL,
shop_id TEXT NOT NULL,
group_id TEXT NOT NULL,
PRIMARY KEY (designer_id, shop_id),
FOREIGN KEY (designer_id) REFERENCES designers(id)
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS designer_online (
wechat_user_id TEXT PRIMARY KEY,
is_online INTEGER NOT NULL DEFAULT 0,
updated_at TEXT
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS round_robin (
shop_id TEXT PRIMARY KEY,
last_index INTEGER NOT NULL DEFAULT 0
)
""")
conn.commit()
init_db()
# ========== 设计师管理 ==========
def add_designer(name: str, wechat_user_id: str) -> int:
"""添加设计师,返回 id"""
with _get_conn() as conn:
conn.execute(
"INSERT OR IGNORE INTO designers (name, wechat_user_id) VALUES (?, ?)",
(name, wechat_user_id),
)
conn.commit()
row = conn.execute("SELECT id FROM designers WHERE wechat_user_id = ?", (wechat_user_id,)).fetchone()
return row["id"] if row else 0
def set_designer_shop(designer_id: int, shop_id: str, group_id: str):
"""设置设计师在某店铺的分组 ID同一设计师不同店铺不同 group_id"""
with _get_conn() as conn:
conn.execute(
"INSERT OR REPLACE INTO designer_shops (designer_id, shop_id, group_id) VALUES (?, ?, ?)",
(designer_id, shop_id, group_id),
)
conn.commit()
def update_online(wechat_user_id: str, is_online: bool):
"""更新设计师在线状态(企微群「上线」/「下线」解析后调用)"""
from datetime import datetime
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with _get_conn() as conn:
conn.execute(
"INSERT OR REPLACE INTO designer_online (wechat_user_id, is_online, updated_at) VALUES (?, ?, ?)",
(wechat_user_id, 1 if is_online else 0, ts),
)
conn.commit()
# ========== 派单 ==========
def get_transfer_group_for_shop(shop_id: str) -> Optional[str]:
"""
为店铺轮询派单,返回分组 ID。
从该店铺的在线设计师中轮询选一个,返回其在该店铺的 group_id。
无人在线则返回 None。
"""
with _get_conn() as conn:
rows = conn.execute("""
SELECT d.wechat_user_id, ds.group_id
FROM designer_shops ds
JOIN designers d ON d.id = ds.designer_id
JOIN designer_online o ON o.wechat_user_id = d.wechat_user_id AND o.is_online = 1
WHERE ds.shop_id = ?
""", (shop_id,)).fetchall()
if not rows:
return None
with _get_conn() as conn:
rr = conn.execute("SELECT last_index FROM round_robin WHERE shop_id = ?", (shop_id,)).fetchone()
last = rr["last_index"] if rr else 0
idx = last % len(rows)
chosen = rows[idx]
conn.execute(
"INSERT OR REPLACE INTO round_robin (shop_id, last_index) VALUES (?, ?)",
(shop_id, idx + 1),
)
conn.commit()
return chosen["group_id"]
# ========== 查询 ==========
def get_all_wechat_user_ids() -> list:
"""获取所有设计师的 wechat_user_id用于同步在线状态"""
with _get_conn() as conn:
rows = conn.execute("SELECT wechat_user_id FROM designers").fetchall()
return [r["wechat_user_id"] for r in rows]
def list_designers():
"""列出所有设计师及其店铺分组"""
with _get_conn() as conn:
designers = conn.execute("SELECT id, name, wechat_user_id FROM designers").fetchall()
result = []
for d in designers:
shops = conn.execute(
"SELECT shop_id, group_id FROM designer_shops WHERE designer_id = ?",
(d["id"],),
).fetchall()
online = conn.execute(
"SELECT is_online FROM designer_online WHERE wechat_user_id = ?",
(d["wechat_user_id"],),
).fetchone()
result.append({
"id": d["id"],
"name": d["name"],
"wechat_user_id": d["wechat_user_id"],
"shops": {s["shop_id"]: s["group_id"] for s in shops},
"is_online": bool(online and online["is_online"]),
})
return result

Binary file not shown.