"""CRM 公海池规则 CRUD + 规则执行逻辑""" from __future__ import annotations import logging import math from datetime import datetime, timedelta from typing import Any, Dict, List, Optional, Tuple from sqlalchemy import or_ from sqlalchemy.orm import Session from app.core.delete_guard import assert_row_deletable_by_creator from app.core.utils import generate_uuid from app.models.crm_models import CrmCustomer, CrmCustomerPool, CrmCustomerPoolRule from app.crud.crm.data_scope_helpers import ( apply_crm_create_by_scope, ensure_crm_create_by_row_in_scope, ) logger = logging.getLogger(__name__) _FMT = "%Y-%m-%d %H:%M:%S" def total_pages(total: int, page_size: int) -> int: if page_size <= 0: return 0 return math.ceil(total / page_size) def _row_to_dict(row: CrmCustomerPoolRule) -> Dict[str, Any]: return { "id": row.id, "poolName": row.pool_name or "", "ruleType": row.rule_type or "", "timeoutDays": row.timeout_days if row.timeout_days is not None else 0, "protectDays": row.protect_days if row.protect_days is not None else 0, "status": row.status or "enable", "sort": row.sort if row.sort is not None else 0, "remark": row.remark or "", "createBy": row.create_by or "", "createTime": row.create_time.strftime(_FMT) if row.create_time else "", "updateBy": row.update_by or "", "updateTime": row.update_time.strftime(_FMT) if row.update_time else "", } def list_pool_rules( db: Session, tenant_no: str, *, page: int = 1, page_size: int = 10, pool_name: Optional[str] = None, rule_type: Optional[str] = None, status: Optional[str] = None, user_info=None, max_page_size: Optional[int] = None, ) -> Tuple[List[Dict[str, Any]], int]: q = db.query(CrmCustomerPoolRule).filter( CrmCustomerPoolRule.tenant_no == tenant_no, CrmCustomerPoolRule.del_flag == 0, ) if pool_name: q = q.filter(CrmCustomerPoolRule.pool_name.like(f"%{pool_name}%")) if rule_type: q = q.filter(CrmCustomerPoolRule.rule_type == rule_type) if status: q = q.filter(CrmCustomerPoolRule.status == status) if user_info: q = apply_crm_create_by_scope(q, CrmCustomerPoolRule, user_info, db, tenant_no) total = q.count() q = q.order_by(CrmCustomerPoolRule.sort.asc(), CrmCustomerPoolRule.create_time.desc()) cap = max_page_size if max_page_size is not None else 500 page_size = min(max(1, page_size), cap) page = max(1, page) offset = (page - 1) * page_size rows = q.offset(offset).limit(page_size).all() return [_row_to_dict(r) for r in rows], total def get_pool_rule(db: Session, rule_id: str, tenant_no: str) -> Optional[CrmCustomerPoolRule]: return ( db.query(CrmCustomerPoolRule) .filter( CrmCustomerPoolRule.id == rule_id, CrmCustomerPoolRule.tenant_no == tenant_no, CrmCustomerPoolRule.del_flag == 0, ) .first() ) def create_pool_rule( db: Session, tenant_no: str, *, user_id: Optional[str] = None, pool_name: str, rule_type: str, timeout_days: int, protect_days: int = 0, status: str = "enable", sort: int = 0, remark: Optional[str] = None, ) -> CrmCustomerPoolRule: row = CrmCustomerPoolRule( id=generate_uuid(), pool_name=pool_name, rule_type=rule_type, timeout_days=timeout_days, protect_days=protect_days, status=status, sort=sort, remark=remark, tenant_no=tenant_no, create_by=user_id, create_time=datetime.now(), update_time=datetime.now(), ) db.add(row) db.flush() return row def update_pool_rule( db: Session, rule_id: str, tenant_no: str, *, user_id: Optional[str] = None, user_info=None, pool_name: Optional[str] = None, rule_type: Optional[str] = None, timeout_days: Optional[int] = None, protect_days: Optional[int] = None, status: Optional[str] = None, sort: Optional[int] = None, remark: Optional[str] = None, ) -> CrmCustomerPoolRule: row = get_pool_rule(db, rule_id, tenant_no) if not row: raise ValueError("公海池规则不存在") ensure_crm_create_by_row_in_scope(db, row, user_info, tenant_no, message="无权访问该公海规则") if pool_name is not None: row.pool_name = pool_name if rule_type is not None: row.rule_type = rule_type if timeout_days is not None: row.timeout_days = timeout_days if protect_days is not None: row.protect_days = protect_days if status is not None: row.status = status if sort is not None: row.sort = sort if remark is not None: row.remark = remark row.update_by = user_id row.update_time = datetime.now() db.flush() return row def delete_pool_rules( db: Session, rule_ids: List[str], tenant_no: str, user_info=None ) -> int: count = 0 for rid in rule_ids: row = get_pool_rule(db, rid, tenant_no) if not row: continue ensure_crm_create_by_row_in_scope(db, row, user_info, tenant_no, message="无权访问该公海规则") if row.status == "enable": raise ValueError(f"公海池规则【{row.pool_name}】正在启用中,请先禁用后再删除") assert_row_deletable_by_creator(row, user_info) row.del_flag = 1 row.update_time = datetime.now() count += 1 db.flush() return count def update_pool_rule_status( db: Session, rule_id: str, tenant_no: str, status: str, *, user_id: Optional[str] = None, user_info=None, ) -> CrmCustomerPoolRule: row = get_pool_rule(db, rule_id, tenant_no) if not row: raise ValueError("公海池规则不存在") ensure_crm_create_by_row_in_scope(db, row, user_info, tenant_no, message="无权访问该公海规则") row.status = status row.update_by = user_id row.update_time = datetime.now() db.flush() return row def execute_pool_rules(db: Session, tenant_no: str) -> int: rules = ( db.query(CrmCustomerPoolRule) .filter( CrmCustomerPoolRule.tenant_no == tenant_no, CrmCustomerPoolRule.status == "enable", CrmCustomerPoolRule.del_flag == 0, ) .order_by(CrmCustomerPoolRule.sort.asc()) .all() ) if not rules: logger.info("没有启用的公海规则") return 0 total_recycled = 0 for rule in rules: try: total_recycled += _execute_single_rule(db, tenant_no, rule) except Exception: logger.exception("执行公海规则失败: %s", rule.pool_name) logger.info("公海池规则执行完成,共回收 %d 个客户", total_recycled) return total_recycled def _execute_single_rule(db: Session, tenant_no: str, rule: CrmCustomerPoolRule) -> int: logger.info("执行公海规则: %s", rule.pool_name) q = db.query(CrmCustomer).filter( CrmCustomer.tenant_no == tenant_no, CrmCustomer.del_flag == 0, CrmCustomer.status == "normal", CrmCustomer.deal_status != "dealed", ) timeout_date = datetime.now() - timedelta(days=rule.timeout_days) if rule.rule_type == "no_follow": q = q.filter( or_( CrmCustomer.last_time < timeout_date, CrmCustomer.last_time.is_(None), ) ) elif rule.rule_type == "no_deal": q = q.filter( CrmCustomer.claim_time < timeout_date, CrmCustomer.deal_status == "not_deal", ) else: return 0 customers = q.all() logger.info("规则【%s】匹配到 %d 个客户", rule.pool_name, len(customers)) now = datetime.now() for customer in customers: original_owner_id = customer.owner_user_id or "" original_owner_name = customer.owner_user_name or "" customer.status = "pool" customer.into_pool_time = now customer.into_pool_by = None customer.into_pool_reason = f"系统自动回收-规则:{rule.pool_name}" customer.owner_user_id = None customer.owner_user_name = None customer.update_time = now pool_record = CrmCustomerPool( id=generate_uuid(), customer_id=customer.id, customer_name=customer.customer_name, operation_type="into", from_user_id=original_owner_id, from_user_name=original_owner_name, reason=f"系统自动回收-规则:{rule.pool_name}", tenant_no=tenant_no, create_by=None, create_by_name="系统", create_time=now, update_time=now, ) db.add(pool_record) db.flush() return len(customers)