Files
shanxia2.0/backend/app/crud/crm/clue_statistics.py
T
zlwuandCursor 3107d537f6 feat(crm): 客户与线索模块接入数据权限过滤
列表按 owner_user_id/create_by 过滤,详情与 CUD 校验 scope;公海客户列表保持租户内全员可见。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 20:30:41 +08:00

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