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>
This commit is contained in:
Neo
2026-04-06 00:03:48 +08:00
parent 70324d8542
commit 6f8f12314f
515 changed files with 76604 additions and 7456 deletions

View File

@@ -27,9 +27,11 @@ from typing import Any
from fastapi import HTTPException, status
from app.ai.bailian_client import BailianClient
from app.ai.config import AIConfig
from app.ai.dashscope_client import DashScopeClient
from app.database import get_connection
from app.services import fdw_queries
from app.trace.decorators import trace_service
logger = logging.getLogger(__name__)
@@ -46,6 +48,7 @@ class ChatService:
# CHAT-1: 对话历史列表
# ------------------------------------------------------------------
@trace_service("查询对话历史", "Get chat history")
def get_chat_history(
self,
user_id: int,
@@ -149,6 +152,7 @@ class ChatService:
# 对话复用 / 创建
# ------------------------------------------------------------------
@trace_service("查找或创建对话", "Get or create session")
def get_or_create_session(
self,
user_id: int,
@@ -213,7 +217,7 @@ class ChatService:
context_type: str,
context_id: str | None,
) -> int:
"""创建新对话记录,返回 conversation_id。"""
"""创建新对话记录,返回 conversation_id。同时生成 session_id。"""
conn = get_connection()
try:
with conn.cursor() as cur:
@@ -230,11 +234,23 @@ class ChatService:
INSERT INTO biz.ai_conversations
(user_id, nickname, app_id, site_id, context_type, context_id)
VALUES (%s, %s, %s, %s, %s, %s)
RETURNING id
RETURNING id, EXTRACT(EPOCH FROM created_at)::bigint
""",
(str(user_id), nickname, APP_ID, site_id, context_type, context_id),
)
new_id = cur.fetchone()[0]
result = cur.fetchone()
new_id = result[0]
created_ts = result[1]
# 生成 session_id 并回写格式conv_{id}_{timestamp}
session_id = f"conv_{new_id}_{created_ts}"
cur.execute(
"""
UPDATE biz.ai_conversations SET session_id = %s WHERE id = %s
""",
(session_id, new_id),
)
conn.commit()
return new_id
except Exception:
@@ -243,10 +259,26 @@ class ChatService:
finally:
conn.close()
@trace_service("获取对话 session_id", "Get session ID")
def get_session_id(self, chat_id: int) -> str | None:
"""获取对话的 session_id。无记录或字段为空时返回 None。"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"SELECT session_id FROM biz.ai_conversations WHERE id = %s",
(chat_id,),
)
row = cur.fetchone()
return row[0] if row and row[0] else None
finally:
conn.close()
# ------------------------------------------------------------------
# CHAT-2: 消息列表
# ------------------------------------------------------------------
@trace_service("查询消息列表", "Get messages")
def get_messages(
self,
chat_id: int,
@@ -312,6 +344,7 @@ class ChatService:
# CHAT-3: 发送消息(同步回复)
# ------------------------------------------------------------------
@trace_service("发送消息并获取回复", "Send message sync")
async def send_message_sync(
self,
chat_id: int,
@@ -368,6 +401,7 @@ class ChatService:
# referenceCard 组装
# ------------------------------------------------------------------
@trace_service("构建引用卡片", "Build reference card")
def build_reference_card(
self,
customer_id: int,
@@ -438,6 +472,7 @@ class ChatService:
# 标题生成
# ------------------------------------------------------------------
@trace_service("生成对话标题", "Generate title")
def generate_title(
self,
title: str | None = None,
@@ -582,11 +617,13 @@ class ChatService:
user_id: int,
site_id: int,
) -> tuple[str, int | None]:
"""调用百炼 API 获取非流式回复,返回 (reply_text, tokens_used)。
"""调用 DashScope Application API 获取非流式回复,返回 (reply_text, tokens_used)。
构建历史消息上下文发送给 AI
通过 Application.call() 调用 App1通用对话prompt 为最近历史拼接
"""
bailian = _get_bailian_client()
# CHANGE 2026-03-22 | BailianClient → DashScopeClientP14 迁移收尾)
client = _get_dashscope_client()
ai_config = AIConfig.from_env()
# 获取历史消息作为上下文(最近 20 条)
conn = get_connection()
@@ -604,33 +641,21 @@ class ChatService:
finally:
conn.close()
# 构建消息列表
messages: list[dict] = []
# 取最近 20 条(含刚写入的 user 消息)
# 拼接历史消息为 prompt 文本
recent = history[-20:] if len(history) > 20 else history
prompt_parts: list[str] = []
for role, msg_content in recent:
messages.append({"role": role, "content": msg_content})
prompt_parts.append(f"[{role}]: {msg_content}")
prompt = "\n".join(prompt_parts)
# 如果没有 system 消息,添加默认 system prompt
if not messages or messages[0]["role"] != "system":
system_prompt = {
"role": "system",
"content": json.dumps(
{"task": "你是台球门店的 AI 助手,根据用户的问题和当前页面上下文提供帮助。"},
ensure_ascii=False,
),
}
messages.insert(0, system_prompt)
# 通过 Application API 调用 App1
result, tokens_used, _session_id = await client.call_app(
ai_config.app_id_1_chat, prompt,
)
# 非流式调用chat_stream 用于 SSE这里用 chat_stream 收集完整回复
full_parts: list[str] = []
async for chunk in bailian.chat_stream(messages):
full_parts.append(chunk)
reply = "".join(full_parts)
# 流式模式不返回 tokens_used按字符数估算
estimated_tokens = len(reply)
return reply, estimated_tokens
# 从返回结果提取文本回复
reply = result.get("text", "") if isinstance(result, dict) else str(result)
return reply, tokens_used
@staticmethod
def _get_consumption_30d(conn: Any, site_id: int, member_id: int) -> Decimal | None:
@@ -673,13 +698,8 @@ class ChatService:
# ── 模块级辅助函数 ──────────────────────────────────────────────
def _get_bailian_client() -> BailianClient:
"""从环境变量构建 BailianClient缺失时报错。"""
api_key = os.environ.get("BAILIAN_API_KEY")
base_url = os.environ.get("BAILIAN_BASE_URL")
model = os.environ.get("BAILIAN_MODEL")
if not api_key or not base_url or not model:
raise RuntimeError(
"百炼 API 环境变量缺失,需要 BAILIAN_API_KEY、BAILIAN_BASE_URL、BAILIAN_MODEL"
)
return BailianClient(api_key=api_key, base_url=base_url, model=model)
def _get_dashscope_client() -> DashScopeClient:
"""从环境变量构建 DashScopeClient缺失时报错。"""
# CHANGE 2026-03-22 | BailianClient → DashScopeClientP14 迁移收尾)
ai_config = AIConfig.from_env()
return DashScopeClient(api_key=ai_config.api_key, workspace_id=ai_config.workspace_id)