补全 PR3 Phase 2.4–2.7,列表 apply_data_scope、详情/CUD assert_row_in_data_scope,合同/报价主表支持 owner_user_id。 Co-authored-by: Cursor <cursoragent@cursor.com>
137 lines
4.2 KiB
Python
137 lines
4.2 KiB
Python
"""CRM 报价单分享访问日志 CRUD — 对照 CrmQuoteShareLogServiceImpl.java"""
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from datetime import datetime
|
||
from typing import Any, Dict, List, Optional
|
||
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.models.crm_models import CrmQuoteShareLog
|
||
|
||
_FMT = "%Y-%m-%d %H:%M:%S"
|
||
_MOBILE_UA_RE = re.compile(r"(mobile|android|iphone|ipad|ipod|blackberry|windows phone)", re.I)
|
||
|
||
|
||
def _row_to_dict(row: CrmQuoteShareLog) -> Dict[str, Any]:
|
||
return {
|
||
"id": row.id,
|
||
"quoteId": row.quote_id or "",
|
||
"shareCode": row.share_code or "",
|
||
"visitTime": row.visit_time.strftime(_FMT) if row.visit_time else "",
|
||
"ipAddress": row.ip_address or "",
|
||
"location": row.location or "",
|
||
"deviceType": row.device_type or "",
|
||
"browser": row.browser or "",
|
||
"stayDuration": row.stay_duration if row.stay_duration is not None else 0,
|
||
"pageViews": row.page_views if row.page_views is not None else 1,
|
||
"createTime": row.create_time.strftime(_FMT) if row.create_time else "",
|
||
}
|
||
|
||
|
||
def _parse_user_agent(user_agent: Optional[str]) -> tuple[str, str]:
|
||
"""简易 User-Agent 解析(替代 Hutool UserAgentUtil)"""
|
||
ua = (user_agent or "").strip()
|
||
if not ua:
|
||
return "Unknown", "Unknown"
|
||
device = "Mobile" if _MOBILE_UA_RE.search(ua) else "PC"
|
||
browser = ua[:100] if len(ua) > 100 else ua
|
||
return device, browser
|
||
|
||
|
||
def record_visit(
|
||
db: Session,
|
||
*,
|
||
tenant_no: str,
|
||
quote_id: str,
|
||
share_code: str,
|
||
ip_address: str = "",
|
||
user_agent: str = "",
|
||
user_id: str = None,
|
||
dept_id: str = "",
|
||
) -> bool:
|
||
"""Java: recordVisit"""
|
||
device_type, browser = _parse_user_agent(user_agent)
|
||
entity = CrmQuoteShareLog(
|
||
id=generate_uuid(),
|
||
quote_id=quote_id,
|
||
share_code=share_code,
|
||
visit_time=datetime.now(),
|
||
ip_address=ip_address or "",
|
||
device_type=device_type,
|
||
browser=browser,
|
||
stay_duration=0,
|
||
page_views=1,
|
||
tenant_no=tenant_no,
|
||
create_by=user_id,
|
||
create_dept_id=dept_id or "",
|
||
del_flag=0,
|
||
)
|
||
db.add(entity)
|
||
db.flush()
|
||
return True
|
||
|
||
|
||
def list_share_logs(
|
||
db: Session,
|
||
quote_id: str,
|
||
*,
|
||
tenant_no: str = None,
|
||
user_info=None,
|
||
) -> List[Dict[str, Any]]:
|
||
"""Java: queryLogsByQuoteId"""
|
||
if tenant_no and user_info:
|
||
from app.crud.crm.quote import _get_quote_in_scope
|
||
|
||
_get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info)
|
||
q = db.query(CrmQuoteShareLog).filter(
|
||
CrmQuoteShareLog.quote_id == quote_id,
|
||
CrmQuoteShareLog.del_flag == 0,
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(CrmQuoteShareLog.tenant_no == tenant_no)
|
||
rows = q.order_by(CrmQuoteShareLog.visit_time.desc()).all()
|
||
return [_row_to_dict(r) for r in rows]
|
||
|
||
|
||
def share_log_statistics(
|
||
db: Session,
|
||
quote_id: str,
|
||
*,
|
||
tenant_no: str = None,
|
||
user_info=None,
|
||
) -> Dict[str, Any]:
|
||
"""Java: statisticsVisits"""
|
||
if tenant_no and user_info:
|
||
from app.crud.crm.quote import _get_quote_in_scope
|
||
|
||
_get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info)
|
||
q = db.query(CrmQuoteShareLog).filter(
|
||
CrmQuoteShareLog.quote_id == quote_id,
|
||
CrmQuoteShareLog.del_flag == 0,
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(CrmQuoteShareLog.tenant_no == tenant_no)
|
||
logs = q.all()
|
||
|
||
ips = {log.ip_address for log in logs if (log.ip_address or "").strip()}
|
||
visit_times = [log.visit_time for log in logs if log.visit_time]
|
||
|
||
device_stats: Dict[str, int] = {}
|
||
for log in logs:
|
||
key = (log.device_type or "").strip() or "Unknown"
|
||
device_stats[key] = device_stats.get(key, 0) + 1
|
||
|
||
total_stay = sum(log.stay_duration or 0 for log in logs)
|
||
avg_stay = (total_stay / len(logs)) if logs else 0.0
|
||
|
||
return {
|
||
"totalVisits": len(logs),
|
||
"uniqueIps": len(ips),
|
||
"lastVisitTime": max(visit_times).strftime(_FMT) if visit_times else None,
|
||
"firstVisitTime": min(visit_times).strftime(_FMT) if visit_times else None,
|
||
"deviceStats": device_stats,
|
||
"totalStayDuration": total_stay,
|
||
"avgStayDuration": avg_stay,
|
||
}
|