661 lines
22 KiB
Python
661 lines
22 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
客户服务 —— CUST-1 客户详情、CUST-2 客户服务记录。
|
||
|
||
数据来源:
|
||
- ETL 直连(fdw_queries):会员信息、余额、消费、关系指数、服务记录
|
||
- 业务库(biz.*):AI 缓存、维客线索、备注、助教任务
|
||
|
||
⚠️ DWD-DOC 强制规则:
|
||
- 规则 1: 金额使用 items_sum 口径(ledger_amount),禁止 consume_money
|
||
- 规则 2: 助教费用使用 assistant_pd_money + assistant_cx_money,禁止 service_fee
|
||
- DQ-6: 会员信息通过 member_id JOIN v_dim_member (scd2_is_current=1)
|
||
- DQ-7: 余额通过 member_id JOIN v_dim_member_card_account (scd2_is_current=1)
|
||
- 废单排除: is_delete = 0
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import logging
|
||
|
||
from fastapi import HTTPException
|
||
|
||
from decimal import Decimal
|
||
|
||
from app.services import fdw_queries
|
||
from app.services.task_generator import compute_heart_icon
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
# ── 颜色/样式映射 ──────────────────────────────────────────
|
||
|
||
LEVEL_COLOR_MAP = {
|
||
"星级": "#FF6B6B",
|
||
"高级": "#FFA726",
|
||
"中级": "#42A5F5",
|
||
"初级": "#66BB6A",
|
||
}
|
||
|
||
TASK_TYPE_MAP = {
|
||
"follow_up_visit": {"label": "回访", "color": "#42A5F5", "bg_class": "bg-blue"},
|
||
"high_priority_recall": {"label": "紧急召回", "color": "#FF6B6B", "bg_class": "bg-red"},
|
||
"priority_recall": {"label": "优先召回", "color": "#FFA726", "bg_class": "bg-orange"},
|
||
}
|
||
|
||
LEVEL_BG_MAP = {
|
||
"星级": "bg-red",
|
||
"高级": "bg-orange",
|
||
"中级": "bg-blue",
|
||
"初级": "bg-green",
|
||
}
|
||
|
||
|
||
def _mask_phone(phone: str | None) -> str:
|
||
"""手机号脱敏:139****5678 格式。"""
|
||
if not phone or len(phone) < 7:
|
||
return phone or ""
|
||
return f"{phone[:3]}****{phone[-4:]}"
|
||
|
||
|
||
def _get_biz_connection():
|
||
"""延迟导入业务库连接。"""
|
||
from app.database import get_connection
|
||
return get_connection()
|
||
|
||
|
||
# ── 3.1 核心函数 ──────────────────────────────────────────
|
||
|
||
async def get_customer_detail(customer_id: int, site_id: int) -> dict:
|
||
"""
|
||
客户详情(CUST-1)。
|
||
|
||
核心字段查询失败 → 500;扩展模块查询失败 → 空默认值(优雅降级)。
|
||
"""
|
||
conn = _get_biz_connection()
|
||
try:
|
||
# ── 核心字段(失败直接抛 500)──
|
||
member_info_map = fdw_queries.get_member_info(conn, site_id, [customer_id])
|
||
if customer_id not in member_info_map:
|
||
raise HTTPException(status_code=404, detail="客户不存在")
|
||
|
||
info = member_info_map[customer_id]
|
||
phone_full = info.get("mobile") or ""
|
||
phone = _mask_phone(phone_full)
|
||
name = info.get("nickname") or ""
|
||
|
||
# Banner 字段:查询失败返回 null(需求 1.7)
|
||
balance = None
|
||
try:
|
||
balance_map = fdw_queries.get_member_balance(conn, site_id, [customer_id])
|
||
if customer_id in balance_map:
|
||
balance = float(balance_map[customer_id])
|
||
except Exception:
|
||
logger.warning("查询 balance 失败,降级为 null", exc_info=True)
|
||
|
||
consumption_60d = None
|
||
try:
|
||
val = fdw_queries.get_consumption_60d(conn, site_id, customer_id)
|
||
if val is not None:
|
||
consumption_60d = float(val)
|
||
except Exception:
|
||
logger.warning("查询 consumption_60d 失败,降级为 null", exc_info=True)
|
||
|
||
days_since_visit = None
|
||
try:
|
||
visit_map = fdw_queries.get_last_visit_days(conn, site_id, [customer_id])
|
||
if customer_id in visit_map:
|
||
days_since_visit = visit_map[customer_id]
|
||
except Exception:
|
||
logger.warning("查询 daysSinceVisit 失败,降级为 null", exc_info=True)
|
||
|
||
# ── 扩展模块(独立 try/except 优雅降级)──
|
||
try:
|
||
ai_insight = _build_ai_insight(customer_id, conn)
|
||
except Exception:
|
||
logger.warning("构建 aiInsight 失败,降级为空", exc_info=True)
|
||
ai_insight = {"summary": "", "strategies": []}
|
||
|
||
try:
|
||
retention_clues = _build_retention_clues(customer_id, conn)
|
||
except Exception:
|
||
logger.warning("构建 retentionClues 失败,降级为空列表", exc_info=True)
|
||
retention_clues = []
|
||
|
||
try:
|
||
notes = _build_notes(customer_id, conn)
|
||
except Exception:
|
||
logger.warning("构建 notes 失败,降级为空列表", exc_info=True)
|
||
notes = []
|
||
|
||
try:
|
||
consumption_records = _build_consumption_records(customer_id, site_id, conn)
|
||
except Exception:
|
||
logger.warning("构建 consumptionRecords 失败,降级为空列表", exc_info=True)
|
||
consumption_records = []
|
||
|
||
try:
|
||
coach_tasks = _build_coach_tasks(customer_id, site_id, conn)
|
||
except Exception:
|
||
logger.warning("构建 coachTasks 失败,降级为空列表", exc_info=True)
|
||
coach_tasks = []
|
||
|
||
try:
|
||
favorite_coaches = _build_favorite_coaches(customer_id, site_id, conn)
|
||
except Exception:
|
||
logger.warning("构建 favoriteCoaches 失败,降级为空列表", exc_info=True)
|
||
favorite_coaches = []
|
||
|
||
return {
|
||
"id": customer_id,
|
||
"name": name,
|
||
"phone": phone,
|
||
"phone_full": phone_full,
|
||
"avatar": "",
|
||
"member_level": "",
|
||
"relation_index": "",
|
||
"tags": [],
|
||
# Banner
|
||
"balance": balance,
|
||
"consumption_60d": consumption_60d,
|
||
"ideal_interval": None,
|
||
"days_since_visit": days_since_visit,
|
||
# 扩展模块
|
||
"ai_insight": ai_insight,
|
||
"coach_tasks": coach_tasks,
|
||
"favorite_coaches": favorite_coaches,
|
||
"retention_clues": retention_clues,
|
||
"consumption_records": consumption_records,
|
||
"notes": notes,
|
||
}
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
# ── 3.2 AI 洞察 / 维客线索 / 备注 ──────────────────────────
|
||
|
||
|
||
def _build_ai_insight(customer_id: int, conn) -> dict:
|
||
"""
|
||
构建 aiInsight 模块。
|
||
|
||
查询 biz.ai_cache WHERE cache_type='app4_analysis' AND target_id=customerId,
|
||
解析 result_json JSON。无缓存时返回空默认值。
|
||
"""
|
||
with conn.cursor() as cur:
|
||
cur.execute(
|
||
"""
|
||
SELECT result_json
|
||
FROM biz.ai_cache
|
||
WHERE cache_type = 'app4_analysis'
|
||
AND target_id = %s
|
||
ORDER BY created_at DESC
|
||
LIMIT 1
|
||
""",
|
||
(str(customer_id),),
|
||
)
|
||
row = cur.fetchone()
|
||
|
||
if not row or not row[0]:
|
||
return {"summary": "", "strategies": []}
|
||
|
||
try:
|
||
data = json.loads(row[0]) if isinstance(row[0], str) else row[0]
|
||
except (json.JSONDecodeError, TypeError):
|
||
return {"summary": "", "strategies": []}
|
||
|
||
summary = data.get("summary", "")
|
||
strategies_raw = data.get("strategies", [])
|
||
strategies = []
|
||
for s in strategies_raw:
|
||
if isinstance(s, dict):
|
||
strategies.append({
|
||
"color": s.get("color", ""),
|
||
"text": s.get("text", ""),
|
||
})
|
||
|
||
return {"summary": summary, "strategies": strategies}
|
||
|
||
|
||
def _build_retention_clues(customer_id: int, conn) -> list[dict]:
|
||
"""
|
||
构建 retentionClues 模块。
|
||
|
||
查询 public.member_retention_clue,按 created_at 倒序。
|
||
"""
|
||
with conn.cursor() as cur:
|
||
cur.execute(
|
||
"""
|
||
SELECT clue_type, clue_text
|
||
FROM public.member_retention_clue
|
||
WHERE member_id = %s
|
||
ORDER BY created_at DESC
|
||
""",
|
||
(customer_id,),
|
||
)
|
||
rows = cur.fetchall()
|
||
|
||
return [{"type": r[0] or "", "text": r[1] or ""} for r in rows]
|
||
|
||
|
||
def _build_notes(customer_id: int, conn) -> list[dict]:
|
||
"""
|
||
构建 notes 模块。
|
||
|
||
查询 biz.notes WHERE target_type='member',最多 20 条,按 created_at 倒序。
|
||
"""
|
||
with conn.cursor() as cur:
|
||
cur.execute(
|
||
"""
|
||
SELECT id, type, created_at, content
|
||
FROM biz.notes
|
||
WHERE target_type = 'member'
|
||
AND target_id = %s
|
||
ORDER BY created_at DESC
|
||
LIMIT 20
|
||
""",
|
||
(customer_id,),
|
||
)
|
||
rows = cur.fetchall()
|
||
|
||
return [
|
||
{
|
||
"id": r[0],
|
||
"tag_label": r[1] or "",
|
||
"created_at": r[2].isoformat() if r[2] else "",
|
||
"content": r[3] or "",
|
||
}
|
||
for r in rows
|
||
]
|
||
|
||
|
||
# ── 3.3 消费记录 ──────────────────────────────────────────
|
||
|
||
|
||
def _build_consumption_records(
|
||
customer_id: int, site_id: int, conn
|
||
) -> list[dict]:
|
||
"""
|
||
构建 consumptionRecords 模块。
|
||
|
||
调用 fdw_queries.get_consumption_records() 获取结算单列表。
|
||
⚠️ DWD-DOC 规则 1: totalAmount 使用 ledger_amount(items_sum 口径)。
|
||
⚠️ DWD-DOC 规则 2: coaches fee 使用 assistant_pd_money / assistant_cx_money。
|
||
⚠️ 废单排除: is_delete = 0,正向交易: settle_type IN (1, 3)。
|
||
"""
|
||
raw_records = fdw_queries.get_consumption_records(
|
||
conn, site_id, customer_id, limit=50, offset=0
|
||
)
|
||
|
||
result = []
|
||
for rec in raw_records:
|
||
# 构建 coaches 子数组
|
||
coaches = []
|
||
pd_money = rec.get("assistant_pd_money", 0.0)
|
||
cx_money = rec.get("assistant_cx_money", 0.0)
|
||
|
||
if pd_money:
|
||
coaches.append({
|
||
"name": rec.get("assistant_name", ""),
|
||
"level": rec.get("level", ""),
|
||
"level_color": LEVEL_COLOR_MAP.get(rec.get("level", ""), ""),
|
||
"course_type": "基础课",
|
||
"hours": rec.get("service_hours", 0.0),
|
||
"perf_hours": None,
|
||
"fee": pd_money,
|
||
})
|
||
if cx_money:
|
||
coaches.append({
|
||
"name": rec.get("assistant_name", ""),
|
||
"level": rec.get("level", ""),
|
||
"level_color": LEVEL_COLOR_MAP.get(rec.get("level", ""), ""),
|
||
"course_type": "激励课",
|
||
"hours": 0.0,
|
||
"perf_hours": None,
|
||
"fee": cx_money,
|
||
})
|
||
|
||
settle_time = rec.get("settle_time")
|
||
date_str = settle_time.strftime("%Y-%m-%d") if settle_time else ""
|
||
start_str = rec.get("start_time")
|
||
end_str = rec.get("end_time")
|
||
|
||
result.append({
|
||
"id": rec.get("id", ""),
|
||
"type": "table",
|
||
"date": date_str,
|
||
"table_name": str(rec.get("table_id")) if rec.get("table_id") else None,
|
||
"start_time": start_str.isoformat() if start_str else None,
|
||
"end_time": end_str.isoformat() if end_str else None,
|
||
"duration": int(rec.get("service_hours", 0) * 60),
|
||
"table_fee": rec.get("table_charge_money", 0.0),
|
||
"table_orig_price": None,
|
||
"coaches": coaches,
|
||
"food_amount": rec.get("goods_money", 0.0),
|
||
"food_orig_price": None,
|
||
"total_amount": rec.get("total_amount", 0.0),
|
||
"total_orig_price": rec.get("total_amount", 0.0),
|
||
"pay_method": "",
|
||
"recharge_amount": None,
|
||
})
|
||
|
||
return result
|
||
|
||
|
||
# ── 3.4 关联助教任务(T2-2)──────────────────────────────
|
||
|
||
|
||
def _build_coach_tasks(
|
||
customer_id: int, site_id: int, conn
|
||
) -> list[dict]:
|
||
"""
|
||
构建 coachTasks 模块。
|
||
|
||
1. 查询 biz.coach_tasks WHERE member_id=customer_id
|
||
2. 对每位助教:fdw_queries 获取等级、近 60 天统计
|
||
3. 映射 levelColor/taskColor/bgClass
|
||
"""
|
||
with conn.cursor() as cur:
|
||
cur.execute(
|
||
"""
|
||
SELECT id, assistant_id, task_type, status, updated_at
|
||
FROM biz.coach_tasks
|
||
WHERE member_id = %s
|
||
AND status IN ('active', 'inactive')
|
||
ORDER BY created_at DESC
|
||
""",
|
||
(customer_id,),
|
||
)
|
||
rows = cur.fetchall()
|
||
|
||
if not rows:
|
||
return []
|
||
|
||
# 收集所有助教 ID,批量查询信息
|
||
assistant_ids = list({r[1] for r in rows if r[1]})
|
||
|
||
# 获取助教等级(通过 salary_calc)
|
||
import datetime
|
||
now = datetime.date.today()
|
||
salary_map = {}
|
||
for aid in assistant_ids:
|
||
try:
|
||
sc = fdw_queries.get_salary_calc(conn, site_id, aid, now.year, now.month)
|
||
if sc:
|
||
salary_map[aid] = sc
|
||
except Exception:
|
||
logger.warning("查询助教 %s 绩效失败", aid, exc_info=True)
|
||
|
||
# 获取助教姓名
|
||
assistant_info_map = {}
|
||
for aid in assistant_ids:
|
||
try:
|
||
info = fdw_queries.get_assistant_info(conn, site_id, aid)
|
||
if info:
|
||
assistant_info_map[aid] = info
|
||
except Exception:
|
||
logger.warning("查询助教 %s 信息失败", aid, exc_info=True)
|
||
|
||
result = []
|
||
for row in rows:
|
||
task_id, assistant_id, task_type, status, updated_at = row
|
||
a_info = assistant_info_map.get(assistant_id, {})
|
||
sc = salary_map.get(assistant_id, {})
|
||
level = sc.get("coach_level", a_info.get("level", ""))
|
||
name = a_info.get("name", "")
|
||
|
||
task_meta = TASK_TYPE_MAP.get(task_type, {
|
||
"label": task_type or "",
|
||
"color": "#999",
|
||
"bg_class": "bg-gray",
|
||
})
|
||
|
||
# 近 60 天统计
|
||
try:
|
||
stats = fdw_queries.get_coach_60d_stats(
|
||
conn, site_id, assistant_id, customer_id
|
||
)
|
||
except Exception:
|
||
stats = {"service_count": 0, "total_hours": 0.0, "avg_hours": 0.0}
|
||
|
||
metrics = [
|
||
{"label": "服务次数", "value": str(stats["service_count"]), "color": None},
|
||
{"label": "总时长", "value": f"{stats['total_hours']:.1f}h", "color": None},
|
||
{"label": "次均时长", "value": f"{stats['avg_hours']:.1f}h", "color": None},
|
||
]
|
||
|
||
result.append({
|
||
"name": name,
|
||
"level": level,
|
||
"level_color": LEVEL_COLOR_MAP.get(level, ""),
|
||
"task_type": task_meta["label"],
|
||
"task_color": task_meta["color"],
|
||
"bg_class": LEVEL_BG_MAP.get(level, task_meta["bg_class"]),
|
||
"status": status,
|
||
"last_service": updated_at.isoformat() if updated_at else None,
|
||
"metrics": metrics,
|
||
})
|
||
|
||
return result
|
||
|
||
|
||
# ── 3.5 最亲密助教(T2-3)──────────────────────────────
|
||
|
||
|
||
def _build_favorite_coaches(
|
||
customer_id: int, site_id: int, conn
|
||
) -> list[dict]:
|
||
"""
|
||
构建 favoriteCoaches 模块。
|
||
|
||
1. fdw_queries.get_relation_index() → 关系指数列表(已按降序排列,rs_display 0-10 刻度)
|
||
2. emoji 四级映射(P6 AC3):>8.5→💖 / >7→🧡 / >5→💛 / ≤5→💙
|
||
3. stats 4 项指标
|
||
"""
|
||
relations = fdw_queries.get_relation_index(conn, site_id, customer_id)
|
||
if not relations:
|
||
return []
|
||
|
||
# 获取助教姓名
|
||
assistant_ids = [r["assistant_id"] for r in relations if r.get("assistant_id")]
|
||
assistant_info_map = {}
|
||
for aid in assistant_ids:
|
||
try:
|
||
info = fdw_queries.get_assistant_info(conn, site_id, aid)
|
||
if info:
|
||
assistant_info_map[aid] = info
|
||
except Exception:
|
||
logger.warning("查询助教 %s 信息失败", aid, exc_info=True)
|
||
|
||
result = []
|
||
for rel in relations:
|
||
ri = rel.get("relation_index", 0.0)
|
||
aid = rel.get("assistant_id")
|
||
a_info = assistant_info_map.get(aid, {})
|
||
|
||
# 4-level heart icon 映射(P6 AC3,rs_display 0-10 刻度)
|
||
emoji = compute_heart_icon(Decimal(str(ri)))
|
||
if ri > 8.5:
|
||
index_color, bg_class = "#FF6B6B", "bg-red"
|
||
elif ri > 7:
|
||
index_color, bg_class = "#FF8C00", "bg-orange"
|
||
elif ri > 5:
|
||
index_color, bg_class = "#FFA726", "bg-yellow"
|
||
else:
|
||
index_color, bg_class = "#5B9BD5", "bg-blue"
|
||
|
||
stats = [
|
||
{"label": "基础课时", "value": f"¥{rel.get('total_income', 0):.0f}", "color": None},
|
||
{"label": "激励课时", "value": "¥0", "color": None},
|
||
{"label": "上课次数", "value": str(rel.get("service_count", 0)), "color": None},
|
||
{"label": "总时长", "value": f"{rel.get('total_hours', 0):.1f}h", "color": None},
|
||
]
|
||
|
||
result.append({
|
||
"emoji": emoji,
|
||
"name": a_info.get("name", ""),
|
||
"relation_index": f"{ri:.2f}",
|
||
"index_color": index_color,
|
||
"bg_class": bg_class,
|
||
"stats": stats,
|
||
})
|
||
|
||
return result
|
||
|
||
|
||
# ── CUST-2 客户服务记录(T2-4)──────────────────────────
|
||
|
||
|
||
async def get_customer_records(
|
||
customer_id: int,
|
||
site_id: int,
|
||
year: int,
|
||
month: int,
|
||
table: str | None,
|
||
page: int,
|
||
page_size: int,
|
||
) -> dict:
|
||
"""
|
||
客户服务记录(CUST-2)。
|
||
|
||
1. fdw_queries.get_member_info() → customerName/customerPhone(DQ-6)
|
||
2. fdw_queries.get_customer_service_records() → 按月分页记录 + total_count
|
||
3. 聚合 monthCount/monthHours(从 total_count 和记录工时)
|
||
4. fdw_queries.get_total_service_count() → totalServiceCount(跨月)
|
||
5. 构建 ServiceRecordItem 列表,含 recordType/isEstimate
|
||
6. hasMore = total_count > page * page_size
|
||
"""
|
||
conn = _get_biz_connection()
|
||
try:
|
||
# ── 客户基础信息(DQ-6)──
|
||
member_info_map = fdw_queries.get_member_info(conn, site_id, [customer_id])
|
||
if customer_id not in member_info_map:
|
||
raise HTTPException(status_code=404, detail="客户不存在")
|
||
|
||
info = member_info_map[customer_id]
|
||
phone_full = info.get("mobile") or ""
|
||
phone = _mask_phone(phone_full)
|
||
customer_name = info.get("nickname") or ""
|
||
|
||
# ── 按月分页服务记录 ──
|
||
offset = (page - 1) * page_size
|
||
records_raw, total_count = fdw_queries.get_customer_service_records(
|
||
conn, site_id, customer_id,
|
||
year, month, table,
|
||
limit=page_size, offset=offset,
|
||
)
|
||
|
||
# ── 月度统计汇总(从全量 total_count + 当页记录工时聚合)──
|
||
# monthCount = 当月总记录数(不是当页),monthHours = 当月总工时
|
||
# 需要单独查询当月汇总,因为分页记录只是子集
|
||
month_count, month_hours = _get_month_aggregation(
|
||
conn, site_id, customer_id, year, month, table
|
||
)
|
||
|
||
# ── 累计服务总次数(跨所有月份)──
|
||
total_service_count = fdw_queries.get_total_service_count(
|
||
conn, site_id, customer_id
|
||
)
|
||
|
||
# ── 构建记录列表 ──
|
||
records = []
|
||
for rec in records_raw:
|
||
create_time = rec.get("create_time")
|
||
date_str = create_time.strftime("%Y-%m-%d") if create_time else ""
|
||
start_time = rec.get("start_time")
|
||
end_time = rec.get("end_time")
|
||
|
||
# 时间范围格式化
|
||
time_range = None
|
||
if start_time and end_time:
|
||
time_range = f"{start_time.strftime('%H:%M')}-{end_time.strftime('%H:%M')}"
|
||
|
||
# recordType: 根据 course_type 判断
|
||
course_type = rec.get("course_type", "")
|
||
record_type = "recharge" if "充值" in course_type else "course"
|
||
|
||
# type / type_class 映射
|
||
if record_type == "recharge":
|
||
type_label = "充值"
|
||
type_class = "tag-recharge"
|
||
else:
|
||
type_label = course_type or "课程"
|
||
type_class = "tag-course"
|
||
|
||
records.append({
|
||
"id": str(rec.get("id", "")),
|
||
"date": date_str,
|
||
"time_range": time_range,
|
||
"table": str(rec.get("table_id")) if rec.get("table_id") else None,
|
||
"type": type_label,
|
||
"type_class": type_class,
|
||
"record_type": record_type,
|
||
"duration": rec.get("service_hours", 0.0),
|
||
"duration_raw": rec.get("service_hours_raw"),
|
||
"income": rec.get("income", 0.0),
|
||
"is_estimate": rec.get("is_estimate", False),
|
||
"drinks": None,
|
||
})
|
||
|
||
has_more = total_count > page * page_size
|
||
|
||
return {
|
||
"customer_name": customer_name,
|
||
"customer_phone": phone,
|
||
"customer_phone_full": phone_full,
|
||
"relation_index": "",
|
||
"tables": [],
|
||
"total_service_count": total_service_count,
|
||
"month_count": month_count,
|
||
"month_hours": round(month_hours, 2),
|
||
"records": records,
|
||
"has_more": has_more,
|
||
}
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
def _get_month_aggregation(
|
||
conn, site_id: int, customer_id: int,
|
||
year: int, month: int, table: str | None,
|
||
) -> tuple[int, float]:
|
||
"""
|
||
查询当月汇总统计(monthCount + monthHours)。
|
||
|
||
复用 fdw_queries 的 _fdw_context 直连 ETL 库。
|
||
⚠️ 废单排除: is_delete = 0。
|
||
"""
|
||
start_date = f"{year}-{month:02d}-01"
|
||
if month == 12:
|
||
end_date = f"{year + 1}-01-01"
|
||
else:
|
||
end_date = f"{year}-{month + 1:02d}-01"
|
||
|
||
base_where = """
|
||
tenant_member_id = %s
|
||
AND is_delete = 0
|
||
AND create_time >= %s::timestamptz
|
||
AND create_time < %s::timestamptz
|
||
"""
|
||
params: list = [customer_id, start_date, end_date]
|
||
if table:
|
||
base_where += " AND site_table_id::text = %s"
|
||
params.append(table)
|
||
|
||
with fdw_queries._fdw_context(conn, site_id) as cur:
|
||
cur.execute(
|
||
f"""
|
||
SELECT COUNT(*) AS month_count,
|
||
COALESCE(SUM(income_seconds / 3600.0), 0) AS month_hours
|
||
FROM app.v_dwd_assistant_service_log
|
||
WHERE {base_where}
|
||
""",
|
||
params,
|
||
)
|
||
row = cur.fetchone()
|
||
|
||
if not row:
|
||
return 0, 0.0
|
||
return row[0] or 0, float(row[1]) if row[1] is not None else 0.0
|