Files
Neo-ZQYY/apps/backend/app/ai/data_fetchers/assistant_data.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

254 lines
8.3 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.
"""助教数据获取模块(应用 4/5 共用)。
从 ETL 库 app.v_* RLS 视图获取助教基本信息和助教-客户服务历史。
使用 is_delete 字段排除废单is_delete=0 为正常),禁止使用已废弃的 dwd_assistant_trash_event 表。
"""
# CHANGE 2026-03-23 | Prompt: FDW 迁移——fdw_etl.* → app.* 直连 ETL 库
# intent: 将所有 fdw_etl.* 外部表引用改为 app.v_* RLS 视图(直连 ETL 库),列名同步修正
# 连接方式不变get_etl_readonly_connection仅改 SQL 表名和列名
from __future__ import annotations
import asyncio
import logging
from datetime import date, datetime
from decimal import Decimal
from functools import partial
from typing import Any
from app.database import get_etl_readonly_connection
logger = logging.getLogger(__name__)
FDW_QUERY_TIMEOUT_SEC = 5
async def fetch_assistant_info(
site_id: int,
assistant_id: int,
) -> dict[str, Any]:
"""获取助教基本信息。
返回:
{
"nickname": str,
"level": str,
"hire_date": str,
"tenure_months": int,
"monthly_customers": int,
"performance_tier": str,
}
Raises:
ValueError: 助教不存在
TimeoutError: FDW 查询超时
ConnectionError: FDW 连接失败
"""
loop = asyncio.get_event_loop()
return await loop.run_in_executor(
None,
partial(_fetch_assistant_info_sync, site_id, assistant_id),
)
def _fetch_assistant_info_sync(site_id: int, assistant_id: int) -> dict[str, Any]:
"""同步实现。"""
conn = None
try:
conn = get_etl_readonly_connection(site_id)
# RLS 隔离 + 语句超时get_etl_readonly_connection 的 SET LOCAL 在 commit 后失效,
# 需在查询事务中重新设置)
with conn.cursor() as cur:
cur.execute(
"SET LOCAL app.current_site_id = %s", (str(site_id),)
)
cur.execute(
"SET LOCAL statement_timeout = %s",
(f"{FDW_QUERY_TIMEOUT_SEC * 1000}",),
)
# 基本信息
# ⚠️ v_dim_assistant 列名: hire_date→entry_time
cur.execute(
"""
SELECT nickname, level, entry_time AS hire_date
FROM app.v_dim_assistant
WHERE assistant_id = %s AND scd2_is_current = 1
LIMIT 1
""",
(assistant_id,),
)
row = cur.fetchone()
if not row:
raise ValueError(f"assistant not found: assistant_id={assistant_id}")
nickname = row[0] or ""
level = row[1] or ""
hire_date = row[2]
# 计算工龄
tenure_months = 0
if hire_date and isinstance(hire_date, date):
today = date.today()
tenure_months = (today.year - hire_date.year) * 12 + (today.month - hire_date.month)
# 绩效数据
# ⚠️ 列名映射: monthly_customers 不存在(用 0 占位performance_tier→tier_name
# ⚠️ salary_month 是 date 类型YYYY-MM-01按月降序取最新
cur.execute(
"""
SELECT
0 AS monthly_customers,
COALESCE(tier_name, '') AS performance_tier
FROM app.v_dws_assistant_salary_calc
WHERE assistant_id = %s
ORDER BY salary_month DESC
LIMIT 1
""",
(assistant_id,),
)
perf_row = cur.fetchone()
monthly_customers = perf_row[0] if perf_row else 0
performance_tier = perf_row[1] if perf_row else ""
conn.commit()
return {
"nickname": nickname,
"level": level,
"hire_date": hire_date.isoformat() if isinstance(hire_date, date) else "",
"tenure_months": tenure_months,
"monthly_customers": monthly_customers,
"performance_tier": performance_tier,
}
except (ValueError, TimeoutError, ConnectionError):
raise
except Exception as e:
err_msg = str(e).lower()
if "statement timeout" in err_msg or "timeout" in err_msg:
raise TimeoutError(
f"FDW 查询超时: assistant_id={assistant_id}"
) from e
if "connection" in err_msg or "connect" in err_msg:
raise ConnectionError(
f"FDW 连接失败: assistant_id={assistant_id}"
) from e
raise
finally:
if conn:
conn.close()
async def fetch_service_history(
site_id: int,
assistant_id: int,
member_id: int,
months: int = 3,
) -> list[dict[str, Any]]:
"""获取助教服务该客户的历史记录。
使用 is_delete 排除废单WHERE is_delete = 0
返回:
[
{
"service_date": str,
"duration_minutes": int,
"items_sum": float,
"room_name": str,
"is_pd": bool,
},
...
]
Raises:
TimeoutError: FDW 查询超时
ConnectionError: FDW 连接失败
"""
loop = asyncio.get_event_loop()
return await loop.run_in_executor(
None,
partial(_fetch_service_history_sync, site_id, assistant_id, member_id, months),
)
def _fetch_service_history_sync(
site_id: int,
assistant_id: int,
member_id: int,
months: int,
) -> list[dict[str, Any]]:
"""同步实现。"""
conn = None
try:
conn = get_etl_readonly_connection(site_id)
# RLS 隔离 + 语句超时get_etl_readonly_connection 的 SET LOCAL 在 commit 后失效,
# 需在查询事务中重新设置)
with conn.cursor() as cur:
cur.execute(
"SET LOCAL app.current_site_id = %s", (str(site_id),)
)
cur.execute(
"SET LOCAL statement_timeout = %s",
(f"{FDW_QUERY_TIMEOUT_SEC * 1000}",),
)
# ⚠️ 列名映射: assistant_id→site_assistant_id, member_id→tenant_member_id,
# is_trash=false→is_delete=0, service_date→create_time,
# duration_minutes→real_use_seconds/60, items_sum→ledger_amount,
# room_name→site_table_id, is_pd→(order_assistant_type=1)
cur.execute(
"""
SELECT
create_time AS service_date,
COALESCE(real_use_seconds / 60, 0) AS duration_minutes,
ledger_amount AS items_sum,
site_table_id AS room_name,
(order_assistant_type = 1) AS is_pd
FROM app.v_dwd_assistant_service_log
WHERE site_assistant_id = %s
AND tenant_member_id = %s
AND is_delete = 0
AND create_time >= (CURRENT_DATE - INTERVAL '%s months')
ORDER BY create_time DESC
""",
(assistant_id, member_id, months),
)
columns = [desc[0] for desc in cur.description]
rows = cur.fetchall()
conn.commit()
records = []
for row in rows:
record = {}
for col, val in zip(columns, row):
if isinstance(val, (date, datetime)):
record[col] = val.isoformat()
elif isinstance(val, Decimal):
record[col] = float(val)
elif isinstance(val, bool):
record[col] = val
else:
record[col] = val
records.append(record)
return records
except (TimeoutError, ConnectionError):
raise
except Exception as e:
err_msg = str(e).lower()
if "statement timeout" in err_msg or "timeout" in err_msg:
raise TimeoutError(
f"FDW 查询超时: assistant_id={assistant_id}, member_id={member_id}"
) from e
if "connection" in err_msg or "connect" in err_msg:
raise ConnectionError(
f"FDW 连接失败: assistant_id={assistant_id}, member_id={member_id}"
) from e
raise
finally:
if conn:
conn.close()