包含多个会话的累积代码变更: - backend: AI 聊天服务、触发器调度、认证增强、WebSocket、调度器最小间隔 - admin-web: ETL 状态页、任务管理、调度配置、登录优化 - miniprogram: 看板页面、聊天集成、UI 组件、导航更新 - etl: DWS 新任务(finance_area_daily/board_cache)、连接器增强 - tenant-admin: 项目初始化 - db: 19 个迁移脚本(etl_feiqiu 11 + zqyy_app 8) - packages/shared: 枚举和工具函数更新 - tools: 数据库工具、报表生成、健康检查 - docs: PRD/架构/部署/合约文档更新 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
283 lines
9.0 KiB
Python
283 lines
9.0 KiB
Python
"""应用 7:客户分析(骨架)。
|
||
|
||
消费事件链中 App8 完成后串行触发,生成客户全量分析与运营建议。
|
||
|
||
使用 items_sum 口径(= table_charge_money + goods_money
|
||
+ assistant_pd_money + assistant_cx_money + electricity_money),
|
||
禁止使用 consume_money。
|
||
|
||
对主观信息(来自备注)标注【来源:XXX,请甄别信息真实性】。
|
||
|
||
app_id = "app7_customer"
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import json
|
||
import logging
|
||
from datetime import datetime
|
||
|
||
from app.ai.dashscope_client import DashScopeClient
|
||
from app.ai.cache_service import AICacheService
|
||
from app.ai.conversation_service import ConversationService
|
||
from app.ai.data_fetchers import fetch_member_consumption_data, fetch_member_notes
|
||
from app.ai.schemas import CacheTypeEnum
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
APP_ID = "app7_customer"
|
||
|
||
# system message content 上限
|
||
_MAX_SYSTEM_CONTENT_LEN = 8000
|
||
|
||
|
||
def _default_member_data() -> dict:
|
||
"""数据获取失败时的默认空值。"""
|
||
return {
|
||
"member_nickname": "",
|
||
"consumption_records": [],
|
||
"member_cards": [],
|
||
"card_balance_total": 0,
|
||
"stored_value_balance_total": 0,
|
||
"expected_visit_date": None,
|
||
"days_since_last_visit": None,
|
||
}
|
||
|
||
|
||
async def build_prompt(
|
||
context: dict,
|
||
cache_svc: AICacheService | None = None,
|
||
) -> list[dict]:
|
||
"""构建 Prompt 消息列表。
|
||
|
||
并发获取消费数据和备注,备注标注来源信息。
|
||
|
||
Args:
|
||
context: 包含 site_id, member_id
|
||
cache_svc: 缓存服务,用于获取 reference 历史数据
|
||
|
||
Returns:
|
||
消息列表
|
||
"""
|
||
site_id = context["site_id"]
|
||
member_id = context["member_id"]
|
||
|
||
# 并发获取消费数据和备注
|
||
results = await asyncio.gather(
|
||
fetch_member_consumption_data(site_id, member_id),
|
||
fetch_member_notes(site_id, member_id),
|
||
return_exceptions=True,
|
||
)
|
||
|
||
fetch_errors: list[str] = []
|
||
|
||
if isinstance(results[0], Exception):
|
||
logger.warning("App7 消费数据获取失败: %s", results[0])
|
||
member_data = _default_member_data()
|
||
fetch_errors.append("消费数据获取失败")
|
||
else:
|
||
member_data = results[0]
|
||
|
||
if isinstance(results[1], Exception):
|
||
logger.warning("App7 备注获取失败: %s", results[1])
|
||
notes_raw: list = []
|
||
fetch_errors.append("备注获取失败")
|
||
else:
|
||
notes_raw = results[1]
|
||
|
||
# 备注标注来源信息
|
||
if notes_raw:
|
||
subjective_notes = []
|
||
for note in notes_raw:
|
||
recorded_by = note.get("recorded_by", "未知")
|
||
annotated = dict(note)
|
||
annotated["content"] = f"{note.get('content', '')}【来源:{recorded_by},请甄别信息真实性】"
|
||
subjective_notes.append(annotated)
|
||
else:
|
||
subjective_notes = "该客户暂无主观备注信息"
|
||
|
||
member_nickname = member_data.get("member_nickname", "")
|
||
|
||
# 构建 reference:最新 + 最近 2 套 App8 历史
|
||
reference = _build_reference(site_id, member_id, cache_svc)
|
||
|
||
system_content: dict = {
|
||
"task": "综合分析客户数据,生成运营策略建议。",
|
||
"app_id": APP_ID,
|
||
"rules": {
|
||
"amount_caliber": "items_sum = table_charge_money + goods_money + assistant_pd_money + assistant_cx_money + electricity_money",
|
||
"禁止使用": "consume_money",
|
||
"subjective_info_label": "对主观信息(来自备注)标注【来源:XXX,请甄别信息真实性】",
|
||
},
|
||
"output_format": {
|
||
"strategies": [
|
||
{"title": "策略标题", "content": "策略内容"}
|
||
],
|
||
"summary": "一句话总结",
|
||
},
|
||
"current_time": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||
"member_id": member_id,
|
||
"member_nickname": member_nickname,
|
||
"objective_data": {
|
||
"consumption_records": member_data.get("consumption_records", []) or "该客户暂无消费记录",
|
||
"member_cards": member_data.get("member_cards", []),
|
||
"card_balance_total": member_data.get("card_balance_total", 0),
|
||
"stored_value_balance_total": member_data.get("stored_value_balance_total", 0),
|
||
"expected_visit_date": member_data.get("expected_visit_date"),
|
||
"days_since_last_visit": member_data.get("days_since_last_visit"),
|
||
},
|
||
"subjective_data": {
|
||
"notes": subjective_notes,
|
||
},
|
||
"reference": reference,
|
||
}
|
||
|
||
if fetch_errors:
|
||
system_content["_data_warnings"] = fetch_errors
|
||
|
||
# Token 预算控制
|
||
content_str = json.dumps(system_content, ensure_ascii=False, default=str)
|
||
if len(content_str) > _MAX_SYSTEM_CONTENT_LEN:
|
||
records = system_content["objective_data"].get("consumption_records")
|
||
if isinstance(records, list) and len(records) > 5:
|
||
system_content["objective_data"]["consumption_records"] = records[:5]
|
||
system_content["objective_data"]["_truncated"] = f"消费记录已截断,原始共 {len(records)} 条"
|
||
content_str = json.dumps(system_content, ensure_ascii=False, default=str)
|
||
if len(content_str) > _MAX_SYSTEM_CONTENT_LEN:
|
||
n = system_content["subjective_data"].get("notes")
|
||
if isinstance(n, list) and len(n) > 10:
|
||
system_content["subjective_data"]["notes"] = n[:10]
|
||
system_content["subjective_data"]["_truncated_notes"] = f"备注已截断,原始共 {len(n)} 条"
|
||
content_str = json.dumps(system_content, ensure_ascii=False, default=str)
|
||
|
||
user_content = (
|
||
f"请综合分析会员 {member_id} 的客户数据,生成运营策略建议。"
|
||
"返回 strategies 数组(每条含 title 和 content)和 summary 字段。"
|
||
"对来自备注的主观信息,请标注【来源:XXX,请甄别信息真实性】。"
|
||
)
|
||
|
||
return [
|
||
{"role": "system", "content": content_str},
|
||
{"role": "user", "content": user_content},
|
||
]
|
||
|
||
|
||
def _build_reference(
|
||
site_id: int,
|
||
member_id: int,
|
||
cache_svc: AICacheService | None,
|
||
) -> dict:
|
||
"""构建 Prompt reference 字段。
|
||
|
||
包含:
|
||
- App8 最新维客线索(如有)
|
||
- 最近 2 套 App8 历史(附 generated_at)
|
||
|
||
缓存不存在时返回空对象 {}。
|
||
"""
|
||
if cache_svc is None:
|
||
return {}
|
||
|
||
reference: dict = {}
|
||
target_id = str(member_id)
|
||
|
||
# App8 最新
|
||
app8_latest = cache_svc.get_latest(
|
||
CacheTypeEnum.APP8_CLUE_CONSOLIDATED.value, site_id, target_id,
|
||
)
|
||
if app8_latest:
|
||
reference["app8_latest"] = {
|
||
"result_json": app8_latest.get("result_json"),
|
||
"generated_at": app8_latest.get("created_at"),
|
||
}
|
||
|
||
# 最近 2 套 App8 历史
|
||
app8_history = cache_svc.get_history(
|
||
CacheTypeEnum.APP8_CLUE_CONSOLIDATED.value, site_id, target_id, limit=2,
|
||
)
|
||
if app8_history:
|
||
reference["app8_history"] = [
|
||
{
|
||
"result_json": h.get("result_json"),
|
||
"generated_at": h.get("created_at"),
|
||
}
|
||
for h in app8_history
|
||
]
|
||
|
||
return reference
|
||
|
||
|
||
async def run(
|
||
context: dict,
|
||
client: DashScopeClient,
|
||
cache_svc: AICacheService,
|
||
conv_svc: ConversationService,
|
||
) -> dict:
|
||
"""执行 App7 客户分析。
|
||
|
||
Args:
|
||
context: site_id, member_id
|
||
bailian: 百炼客户端
|
||
cache_svc: 缓存服务
|
||
conv_svc: 对话服务
|
||
|
||
Returns:
|
||
百炼返回的结构化 JSON(strategies 数组 + summary)
|
||
"""
|
||
site_id = context["site_id"]
|
||
member_id = context["member_id"]
|
||
user_id = context.get("user_id", "system")
|
||
nickname = context.get("nickname", "")
|
||
|
||
# 1. 构建 Prompt
|
||
messages = await build_prompt(context, cache_svc)
|
||
|
||
# 2. 创建对话记录
|
||
conversation_id = conv_svc.create_conversation(
|
||
user_id=user_id,
|
||
nickname=nickname,
|
||
app_id=APP_ID,
|
||
site_id=site_id,
|
||
source_context={"member_id": member_id},
|
||
)
|
||
|
||
# 写入 system + user 消息
|
||
conv_svc.add_message(
|
||
conversation_id=conversation_id,
|
||
role="system",
|
||
content=messages[0]["content"],
|
||
)
|
||
conv_svc.add_message(
|
||
conversation_id=conversation_id,
|
||
role="user",
|
||
content=messages[1]["content"],
|
||
)
|
||
|
||
# 3. 调用百炼 API
|
||
result, tokens_used = await bailian.chat_json(messages)
|
||
|
||
# 4. 写入 assistant 消息
|
||
conv_svc.add_message(
|
||
conversation_id=conversation_id,
|
||
role="assistant",
|
||
content=json.dumps(result, ensure_ascii=False),
|
||
tokens_used=tokens_used,
|
||
)
|
||
|
||
# 5. 写入缓存
|
||
cache_svc.write_cache(
|
||
cache_type=CacheTypeEnum.APP7_CUSTOMER_ANALYSIS.value,
|
||
site_id=site_id,
|
||
target_id=str(member_id),
|
||
result_json=result,
|
||
triggered_by=f"user:{user_id}",
|
||
)
|
||
|
||
logger.info(
|
||
"App7 客户分析完成: site_id=%s member_id=%s conversation_id=%s tokens=%d",
|
||
site_id, member_id, conversation_id, tokens_used,
|
||
)
|
||
|
||
return result
|