列表按 owner_user_id/create_by 过滤,详情与 CUD 校验 scope;公海客户列表保持租户内全员可见。 Co-authored-by: Cursor <cursoragent@cursor.com>
334 lines
11 KiB
Python
334 lines
11 KiB
Python
"""CRM 线索统计分析"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timedelta
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from sqlalchemy import func
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.models.crm_models import CrmClue
|
|
from app.crud.crm.data_scope_helpers import apply_crm_owner_scope
|
|
|
|
|
|
def _base_query(db: Session, tenant_no: str, owner_user_id: str = None, user_info=None):
|
|
q = db.query(CrmClue).filter(
|
|
CrmClue.tenant_no == tenant_no,
|
|
CrmClue.del_flag == 0,
|
|
)
|
|
if owner_user_id:
|
|
q = q.filter(CrmClue.owner_user_id == owner_user_id)
|
|
if user_info:
|
|
q = apply_crm_owner_scope(q, CrmClue, user_info, db, tenant_no)
|
|
return q
|
|
|
|
|
|
def _apply_create_range(q, begin_time: str = None, end_time: str = None):
|
|
if begin_time:
|
|
q = q.filter(CrmClue.create_time >= begin_time)
|
|
if end_time:
|
|
q = q.filter(CrmClue.create_time <= f"{end_time} 23:59:59")
|
|
return q
|
|
|
|
|
|
def get_clue_overview_statistics(
|
|
db: Session,
|
|
tenant_no: str,
|
|
user_id: str = None,
|
|
owner_user_id: str = None,
|
|
begin_time: str = None,
|
|
end_time: str = None,
|
|
user_info=None,
|
|
) -> Dict[str, Any]:
|
|
"""线索统计概览(默认当前用户负责范围)"""
|
|
effective_owner = owner_user_id or user_id
|
|
base = _base_query(db, tenant_no, effective_owner, user_info=user_info)
|
|
ranged = _apply_create_range(base, begin_time, end_time)
|
|
|
|
total_count = ranged.count()
|
|
|
|
to_follow_count = (
|
|
_base_query(db, tenant_no, effective_owner, user_info=user_info)
|
|
.filter(
|
|
CrmClue.is_converted == 0,
|
|
CrmClue.next_time.isnot(None),
|
|
CrmClue.next_time <= datetime.now(),
|
|
)
|
|
.count()
|
|
)
|
|
|
|
converted_q = _base_query(db, tenant_no, effective_owner, user_info=user_info).filter(CrmClue.is_converted == 1)
|
|
if begin_time:
|
|
converted_q = converted_q.filter(CrmClue.converted_time >= begin_time)
|
|
if end_time:
|
|
converted_q = converted_q.filter(CrmClue.converted_time <= f"{end_time} 23:59:59")
|
|
converted_count = converted_q.count()
|
|
|
|
conversion_rate = f"{(converted_count * 100.0 / total_count):.2f}" if total_count else "0.00"
|
|
|
|
invalid_count = (
|
|
_apply_create_range(
|
|
_base_query(db, tenant_no, effective_owner, user_info=user_info).filter(CrmClue.status == "invalid"),
|
|
begin_time,
|
|
end_time,
|
|
).count()
|
|
)
|
|
|
|
a_level_count = (
|
|
_apply_create_range(
|
|
_base_query(db, tenant_no, effective_owner, user_info=user_info).filter(CrmClue.level == "A"),
|
|
begin_time,
|
|
end_time,
|
|
).count()
|
|
)
|
|
|
|
week_start = datetime.now() - timedelta(days=7)
|
|
week_new_count = (
|
|
_base_query(db, tenant_no, effective_owner, user_info=user_info)
|
|
.filter(CrmClue.create_time >= week_start)
|
|
.count()
|
|
)
|
|
|
|
follow_status_count = (
|
|
_base_query(db, tenant_no, effective_owner, user_info=user_info)
|
|
.filter(CrmClue.status == "follow", CrmClue.is_converted == 0)
|
|
.count()
|
|
)
|
|
|
|
return {
|
|
"totalCount": total_count,
|
|
"toFollowCount": to_follow_count,
|
|
"convertedCount": converted_count,
|
|
"conversionRate": conversion_rate,
|
|
"invalidCount": invalid_count,
|
|
"aLevelCount": a_level_count,
|
|
"weekNewCount": week_new_count,
|
|
# 前端统计页字段别名
|
|
"weekAddCount": week_new_count,
|
|
"followCount": follow_status_count,
|
|
"weekConvertCount": converted_count,
|
|
"convertRate": conversion_rate,
|
|
}
|
|
|
|
|
|
def get_clue_source_statistics(
|
|
db: Session,
|
|
tenant_no: str,
|
|
begin_time: str = None,
|
|
end_time: str = None,
|
|
user_info=None,
|
|
) -> List[Dict[str, Any]]:
|
|
q = _base_query(db, tenant_no, user_info=user_info)
|
|
q = _apply_create_range(q, begin_time, end_time)
|
|
rows = (
|
|
q.with_entities(CrmClue.source, func.count(CrmClue.id).label("count"))
|
|
.group_by(CrmClue.source)
|
|
.order_by(func.count(CrmClue.id).desc())
|
|
.all()
|
|
)
|
|
result: List[Dict[str, Any]] = []
|
|
for source, count in rows:
|
|
if not source:
|
|
continue
|
|
converted_q = _base_query(db, tenant_no, user_info=user_info).filter(
|
|
CrmClue.source == source,
|
|
CrmClue.is_converted == 1,
|
|
)
|
|
if begin_time:
|
|
converted_q = converted_q.filter(CrmClue.converted_time >= begin_time)
|
|
if end_time:
|
|
converted_q = converted_q.filter(CrmClue.converted_time <= f"{end_time} 23:59:59")
|
|
converted_count = converted_q.count()
|
|
rate = f"{(converted_count * 100.0 / count):.2f}" if count else "0.00"
|
|
result.append(
|
|
{
|
|
"source": source,
|
|
"sourceName": source,
|
|
"count": count,
|
|
"convertedCount": converted_count,
|
|
"conversionRate": rate,
|
|
"convertRate": float(rate),
|
|
}
|
|
)
|
|
return result
|
|
|
|
|
|
def get_clue_level_statistics(
|
|
db: Session, tenant_no: str, user_info=None
|
|
) -> List[Dict[str, Any]]:
|
|
total = _base_query(db, tenant_no, user_info=user_info).count()
|
|
if total == 0:
|
|
return []
|
|
|
|
levels = [
|
|
("A", "A级(重点)"),
|
|
("B", "B级(良好)"),
|
|
("C", "C级(一般)"),
|
|
("D", "D级(低)"),
|
|
("unrated", "未评级"),
|
|
]
|
|
result: List[Dict[str, Any]] = []
|
|
for level, level_name in levels:
|
|
count = _base_query(db, tenant_no, user_info=user_info).filter(CrmClue.level == level).count()
|
|
if count == 0 and level == "unrated":
|
|
continue
|
|
converted_count = (
|
|
_base_query(db, tenant_no, user_info=user_info)
|
|
.filter(CrmClue.level == level, CrmClue.is_converted == 1)
|
|
.count()
|
|
)
|
|
percent = f"{(count * 100.0 / total):.2f}"
|
|
rate = f"{(converted_count * 100.0 / count):.2f}" if count else "0.00"
|
|
result.append(
|
|
{
|
|
"level": level,
|
|
"levelName": level_name,
|
|
"count": count,
|
|
"percent": percent,
|
|
"convertedCount": converted_count,
|
|
"conversionRate": rate,
|
|
}
|
|
)
|
|
return result
|
|
|
|
|
|
def get_clue_trend_statistics(
|
|
db: Session,
|
|
tenant_no: str,
|
|
begin_time: str = None,
|
|
end_time: str = None,
|
|
time_unit: str = "day",
|
|
user_info=None,
|
|
) -> List[Dict[str, Any]]:
|
|
end_dt = datetime.strptime(end_time, "%Y-%m-%d") if end_time else datetime.now()
|
|
start_dt = (
|
|
datetime.strptime(begin_time, "%Y-%m-%d")
|
|
if begin_time
|
|
else end_dt - timedelta(days=30)
|
|
)
|
|
|
|
result: List[Dict[str, Any]] = []
|
|
current = start_dt.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
while current <= end_dt:
|
|
if time_unit == "month":
|
|
label = current.strftime("%Y-%m")
|
|
next_point = (current.replace(day=1) + timedelta(days=32)).replace(day=1)
|
|
range_start = current.replace(day=1)
|
|
range_end = next_point - timedelta(seconds=1)
|
|
elif time_unit == "week":
|
|
label = current.strftime("%Y-%W")
|
|
range_start = current
|
|
range_end = current + timedelta(days=6, hours=23, minutes=59, seconds=59)
|
|
next_point = current + timedelta(days=7)
|
|
else:
|
|
label = current.strftime("%Y-%m-%d")
|
|
range_start = current
|
|
range_end = current.replace(hour=23, minute=59, second=59)
|
|
next_point = current + timedelta(days=1)
|
|
|
|
new_count = (
|
|
_base_query(db, tenant_no, user_info=user_info)
|
|
.filter(
|
|
CrmClue.create_time >= range_start,
|
|
CrmClue.create_time <= range_end,
|
|
)
|
|
.count()
|
|
)
|
|
converted_count = (
|
|
_base_query(db, tenant_no, user_info=user_info)
|
|
.filter(
|
|
CrmClue.is_converted == 1,
|
|
CrmClue.converted_time >= range_start,
|
|
CrmClue.converted_time <= range_end,
|
|
)
|
|
.count()
|
|
)
|
|
rate = f"{(converted_count * 100.0 / new_count):.2f}" if new_count else "0.00"
|
|
result.append(
|
|
{
|
|
"time": label,
|
|
"date": label,
|
|
"newCount": new_count,
|
|
"addCount": new_count,
|
|
"convertedCount": converted_count,
|
|
"conversionRate": rate,
|
|
}
|
|
)
|
|
current = next_point
|
|
|
|
return result
|
|
|
|
|
|
def get_clue_owner_statistics(
|
|
db: Session,
|
|
tenant_no: str,
|
|
begin_time: str = None,
|
|
end_time: str = None,
|
|
user_info=None,
|
|
) -> List[Dict[str, Any]]:
|
|
owners = (
|
|
_base_query(db, tenant_no, user_info=user_info)
|
|
.filter(CrmClue.owner_user_id.isnot(None), CrmClue.owner_user_id != "")
|
|
.with_entities(CrmClue.owner_user_id, CrmClue.owner_user_name)
|
|
.group_by(CrmClue.owner_user_id, CrmClue.owner_user_name)
|
|
.all()
|
|
)
|
|
result: List[Dict[str, Any]] = []
|
|
for owner_id, owner_name in owners:
|
|
total_q = _base_query(db, tenant_no, user_info=user_info).filter(CrmClue.owner_user_id == owner_id)
|
|
total_q = _apply_create_range(total_q, begin_time, end_time)
|
|
total_count = total_q.count()
|
|
|
|
a_level_count = (
|
|
_base_query(db, tenant_no, user_info=user_info)
|
|
.filter(CrmClue.owner_user_id == owner_id, CrmClue.level == "A")
|
|
.count()
|
|
)
|
|
|
|
converted_q = _base_query(db, tenant_no, user_info=user_info).filter(
|
|
CrmClue.owner_user_id == owner_id,
|
|
CrmClue.is_converted == 1,
|
|
)
|
|
if begin_time:
|
|
converted_q = converted_q.filter(CrmClue.converted_time >= begin_time)
|
|
if end_time:
|
|
converted_q = converted_q.filter(CrmClue.converted_time <= f"{end_time} 23:59:59")
|
|
converted_count = converted_q.count()
|
|
|
|
rate = f"{(converted_count * 100.0 / total_count):.2f}" if total_count else "0.00"
|
|
|
|
follow_count = (
|
|
_apply_create_range(
|
|
_base_query(db, tenant_no, user_info=user_info)
|
|
.filter(
|
|
CrmClue.owner_user_id == owner_id,
|
|
CrmClue.status == "follow",
|
|
CrmClue.is_converted == 0,
|
|
),
|
|
begin_time,
|
|
end_time,
|
|
).count()
|
|
)
|
|
|
|
clues = total_q.with_entities(CrmClue.follow_count).all()
|
|
avg_follow = (
|
|
sum((c[0] or 0) for c in clues) / len(clues) if clues else 0.0
|
|
)
|
|
|
|
result.append(
|
|
{
|
|
"ownerUserId": owner_id,
|
|
"ownerUserName": owner_name or "无",
|
|
"totalCount": total_count,
|
|
"aLevelCount": a_level_count,
|
|
"convertedCount": converted_count,
|
|
"followCount": follow_count,
|
|
"conversionRate": rate,
|
|
"convertRate": float(rate),
|
|
"avgFollowCount": float(f"{avg_follow:.1f}"),
|
|
}
|
|
)
|
|
|
|
result.sort(key=lambda x: x["totalCount"], reverse=True)
|
|
return result
|