fix: 成交指标始终记账并回填;抢单建聊移出事务并重试
抢单池刷新触发历史回填,旧单也能出成交率/罚款率/均时;建聊失败重试并订阅组队成员,避免消息列表空白。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -15,14 +15,62 @@ def _cfg(club_id: str):
|
||||
|
||||
|
||||
def _stats_on(cfg, order) -> bool:
|
||||
if not cfg or not cfg.stats_enabled or not cfg.stats_since:
|
||||
return False
|
||||
ct = getattr(order, 'CreateTime', None)
|
||||
if not ct or ct < cfg.stats_since:
|
||||
"""
|
||||
成交指标始终记账(抢单池要展示成交率/罚款率/均时)。
|
||||
若配置了 stats_since,只统计该时间点之后的订单。
|
||||
"""
|
||||
if not order:
|
||||
return False
|
||||
since = getattr(cfg, 'stats_since', None) if cfg else None
|
||||
if since:
|
||||
ct = getattr(order, 'CreateTime', None)
|
||||
if ct and ct < since:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def ensure_club_stats_bootstrapped(club_id: str, *, lookback_days: int = 180):
|
||||
"""
|
||||
俱乐部首次拉取抢单池商家指标时,若从未记账则回填近 N 天历史订单。
|
||||
幂等:用 DealStatEvent 打标记。
|
||||
"""
|
||||
cid = (club_id or '').strip()
|
||||
if not cid:
|
||||
return
|
||||
marker = f'{cid}:stats_bootstrap_v1'
|
||||
from jituan.models import DealStatEvent, MerchantDealStat
|
||||
if DealStatEvent.query.filter(event_key=marker).exists():
|
||||
return
|
||||
# 已有商家样本则只打标,避免重复重算
|
||||
if MerchantDealStat.query.filter(club_id=cid, order_count__gt=0).exists():
|
||||
_claim_event(cid, marker, 'stats_bootstrap', subject_uid='system')
|
||||
return
|
||||
try:
|
||||
from orders.models import Order
|
||||
from datetime import timedelta
|
||||
since = timezone.now() - timedelta(days=max(7, int(lookback_days or 180)))
|
||||
qs = (
|
||||
Order.query.filter(ClubID=cid, CreateTime__gte=since)
|
||||
.exclude(PlayerID__isnull=True)
|
||||
.exclude(PlayerID='')
|
||||
.filter(Status__in=[2, 3, 4, 5, 6, 8])
|
||||
.order_by('CreateTime')[:800]
|
||||
)
|
||||
for order in qs:
|
||||
try:
|
||||
on_order_enter_pool(order, force=True)
|
||||
if int(order.Status or 0) == 3:
|
||||
on_order_completed(order, settle_started_at=getattr(order, 'SettlementTime', None), force=True)
|
||||
elif int(order.Status or 0) == 5:
|
||||
on_order_refunded(order, force=True)
|
||||
except Exception as e:
|
||||
logger.warning('回填单条成交指标失败 order=%s: %s', getattr(order, 'OrderID', ''), e)
|
||||
_claim_event(cid, marker, 'stats_bootstrap', subject_uid='system', meta={'n': len(qs)})
|
||||
logger.info('俱乐部 %s 成交指标回填完成 n=%s', cid, len(qs))
|
||||
except Exception as e:
|
||||
logger.error('俱乐部成交指标回填失败 club=%s: %s', cid, e, exc_info=True)
|
||||
|
||||
|
||||
def _claim_event(club_id: str, event_key: str, event_type: str, order_id='', subject_uid='', meta=None) -> bool:
|
||||
from jituan.models import DealStatEvent
|
||||
if DealStatEvent.query.filter(event_key=event_key).exists():
|
||||
@@ -68,11 +116,11 @@ def _merchant_uid(order):
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def on_order_enter_pool(order):
|
||||
"""接单入池:商家/打手 order_count+1。"""
|
||||
def on_order_enter_pool(order, *, force=False):
|
||||
"""接单入池:商家/打手 order_count+1。force=True 用于历史回填(忽略 since)。"""
|
||||
club_id = (getattr(order, 'ClubID', None) or '').strip()
|
||||
cfg = _cfg(club_id)
|
||||
if not _stats_on(cfg, order):
|
||||
if not force and not _stats_on(cfg, order):
|
||||
return
|
||||
oid = order.OrderID
|
||||
dashou = (order.PlayerID or '').strip()
|
||||
@@ -93,16 +141,16 @@ def on_order_enter_pool(order):
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def on_order_completed(order, settle_started_at=None):
|
||||
def on_order_completed(order, settle_started_at=None, *, force=False):
|
||||
club_id = (getattr(order, 'ClubID', None) or '').strip()
|
||||
cfg = _cfg(club_id)
|
||||
if not _stats_on(cfg, order):
|
||||
if not force and not _stats_on(cfg, order):
|
||||
return
|
||||
oid = order.OrderID
|
||||
if not _claim_event(club_id, f'{oid}:completed', 'completed', oid):
|
||||
return
|
||||
# 确保入池
|
||||
on_order_enter_pool(order)
|
||||
on_order_enter_pool(order, force=force)
|
||||
|
||||
st = settle_started_at or getattr(order, '_deal_settle_started_at', None)
|
||||
duration = None
|
||||
@@ -134,10 +182,10 @@ def on_order_completed(order, settle_started_at=None):
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def on_order_refunded(order):
|
||||
def on_order_refunded(order, *, force=False):
|
||||
club_id = (getattr(order, 'ClubID', None) or '').strip()
|
||||
cfg = _cfg(club_id)
|
||||
if not _stats_on(cfg, order) or order.Platform != 2:
|
||||
if (not force and not _stats_on(cfg, order)) or order.Platform != 2:
|
||||
return
|
||||
oid = order.OrderID
|
||||
mid = _merchant_uid(order)
|
||||
@@ -145,7 +193,7 @@ def on_order_refunded(order):
|
||||
return
|
||||
if not _claim_event(club_id, f'{oid}:refunded', 'refunded', oid, mid):
|
||||
return
|
||||
on_order_enter_pool(order)
|
||||
on_order_enter_pool(order, force=force)
|
||||
_ensure_merchant(club_id, mid)
|
||||
from jituan.models import MerchantDealStat
|
||||
MerchantDealStat.objects.filter(club_id=club_id, merchant_uid=mid).update(
|
||||
@@ -160,8 +208,7 @@ def on_penalty_paid(penalty):
|
||||
if int(getattr(penalty, 'Status', 0) or 0) != PENALTY_PAID:
|
||||
return
|
||||
club_id = (getattr(penalty, 'ClubID', None) or '').strip()
|
||||
cfg = _cfg(club_id)
|
||||
if not cfg or not cfg.stats_enabled or not cfg.stats_since:
|
||||
if not club_id:
|
||||
return
|
||||
pid = getattr(penalty, 'id', None) or getattr(penalty, 'pk', None)
|
||||
key = f'penalty:{pid}:paid'
|
||||
@@ -246,6 +293,16 @@ def batch_pool_merchant_stats(club_id: str, merchant_uids, order_club_map=None)
|
||||
if not uids:
|
||||
return {}
|
||||
default_club = (club_id or '').strip()
|
||||
# 首次无样本时回填历史,保证旧单刷新也能看到成交率/罚款率/均时
|
||||
try:
|
||||
if default_club:
|
||||
ensure_club_stats_bootstrapped(default_club)
|
||||
if order_club_map:
|
||||
for cid in set((c or '').strip() for c in order_club_map.values() if c):
|
||||
if cid and cid != default_club:
|
||||
ensure_club_stats_bootstrapped(cid)
|
||||
except Exception as e:
|
||||
logger.warning('抢单池成交指标回填触发失败: %s', e)
|
||||
club_ids = {default_club} if default_club else set()
|
||||
if order_club_map:
|
||||
for cid in order_club_map.values():
|
||||
|
||||
@@ -17,12 +17,18 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
def get_or_create_deal_config(club_id: str):
|
||||
from jituan.models import ClubOrderDealConfig
|
||||
from datetime import datetime
|
||||
cid = (club_id or '').strip()
|
||||
if not cid:
|
||||
return None
|
||||
row = ClubOrderDealConfig.query.filter(club_id=cid).first()
|
||||
if not row:
|
||||
row = ClubOrderDealConfig(club_id=cid)
|
||||
# 新建俱乐部默认开启成交指标,便于抢单池立刻展示成交率/罚款率/均时
|
||||
row = ClubOrderDealConfig(
|
||||
club_id=cid,
|
||||
stats_enabled=True,
|
||||
stats_since=timezone.make_aware(datetime(2020, 1, 1)),
|
||||
)
|
||||
row.save()
|
||||
return row
|
||||
|
||||
@@ -118,7 +124,9 @@ def save_deal_config(club_id: str, data: dict, updated_by: str = '') -> dict:
|
||||
enabled = bool(data['stats_enabled'])
|
||||
row.stats_enabled = enabled
|
||||
if enabled and not was_stats:
|
||||
row.stats_since = now
|
||||
# 开启时默认从较早起点计,便于旧单刷新也能出成交率
|
||||
from datetime import datetime
|
||||
row.stats_since = timezone.make_aware(datetime(2020, 1, 1))
|
||||
if not enabled:
|
||||
# 停止写入;保留 since 供只读理解历史起点;再次开启会刷新 since
|
||||
pass
|
||||
@@ -127,10 +135,19 @@ def save_deal_config(club_id: str, data: dict, updated_by: str = '') -> dict:
|
||||
|
||||
# 若刚打开统计且 since 空
|
||||
if row.stats_enabled and not row.stats_since:
|
||||
row.stats_since = now
|
||||
from datetime import datetime
|
||||
row.stats_since = timezone.make_aware(datetime(2020, 1, 1))
|
||||
|
||||
row.updated_by = (updated_by or '')[:32]
|
||||
row.save()
|
||||
# 保存后触发一次历史回填,保证抢单池马上能刷到成交率/罚款率
|
||||
try:
|
||||
from jituan.models import DealStatEvent
|
||||
from jituan.services.deal_stats import ensure_club_stats_bootstrapped
|
||||
DealStatEvent.objects.filter(event_key=f'{row.club_id}:stats_bootstrap_v1').delete()
|
||||
ensure_club_stats_bootstrapped(row.club_id)
|
||||
except Exception as e:
|
||||
logger.warning('保存成交配置后回填失败 club=%s: %s', row.club_id, e)
|
||||
return config_to_dict(row)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user