Files
Neo-ZQYY/apps/backend/app/ai/apps/app7_customer.py
Neo 6f8f12314f feat: 累积功能变更 — 聊天集成、租户管理、小程序更新、ETL 增强、迁移脚本
包含多个会话的累积代码变更:
- 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>
2026-04-06 00:03:48 +08:00

283 lines
9.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""应用 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:
百炼返回的结构化 JSONstrategies 数组 + 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