fix: 成交指标始终记账并回填;抢单建聊移出事务并重试

抢单池刷新触发历史回填,旧单也能出成交率/罚款率/均时;建聊失败重试并订阅组队成员,避免消息列表空白。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-27 08:46:22 +08:00
parent 5e38a3d49a
commit e3f9654002
4 changed files with 143 additions and 25 deletions

View File

@@ -15,14 +15,62 @@ def _cfg(club_id: str):
def _stats_on(cfg, order) -> bool: 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) 若配置了 stats_since只统计该时间点之后的订单。
if not ct or ct < cfg.stats_since: """
if not order:
return False 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 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: def _claim_event(club_id: str, event_key: str, event_type: str, order_id='', subject_uid='', meta=None) -> bool:
from jituan.models import DealStatEvent from jituan.models import DealStatEvent
if DealStatEvent.query.filter(event_key=event_key).exists(): if DealStatEvent.query.filter(event_key=event_key).exists():
@@ -68,11 +116,11 @@ def _merchant_uid(order):
@transaction.atomic @transaction.atomic
def on_order_enter_pool(order): def on_order_enter_pool(order, *, force=False):
"""接单入池:商家/打手 order_count+1。""" """接单入池:商家/打手 order_count+1。force=True 用于历史回填(忽略 since"""
club_id = (getattr(order, 'ClubID', None) or '').strip() club_id = (getattr(order, 'ClubID', None) or '').strip()
cfg = _cfg(club_id) cfg = _cfg(club_id)
if not _stats_on(cfg, order): if not force and not _stats_on(cfg, order):
return return
oid = order.OrderID oid = order.OrderID
dashou = (order.PlayerID or '').strip() dashou = (order.PlayerID or '').strip()
@@ -93,16 +141,16 @@ def on_order_enter_pool(order):
@transaction.atomic @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() club_id = (getattr(order, 'ClubID', None) or '').strip()
cfg = _cfg(club_id) cfg = _cfg(club_id)
if not _stats_on(cfg, order): if not force and not _stats_on(cfg, order):
return return
oid = order.OrderID oid = order.OrderID
if not _claim_event(club_id, f'{oid}:completed', 'completed', oid): if not _claim_event(club_id, f'{oid}:completed', 'completed', oid):
return 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) st = settle_started_at or getattr(order, '_deal_settle_started_at', None)
duration = None duration = None
@@ -134,10 +182,10 @@ def on_order_completed(order, settle_started_at=None):
@transaction.atomic @transaction.atomic
def on_order_refunded(order): def on_order_refunded(order, *, force=False):
club_id = (getattr(order, 'ClubID', None) or '').strip() club_id = (getattr(order, 'ClubID', None) or '').strip()
cfg = _cfg(club_id) 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 return
oid = order.OrderID oid = order.OrderID
mid = _merchant_uid(order) mid = _merchant_uid(order)
@@ -145,7 +193,7 @@ def on_order_refunded(order):
return return
if not _claim_event(club_id, f'{oid}:refunded', 'refunded', oid, mid): if not _claim_event(club_id, f'{oid}:refunded', 'refunded', oid, mid):
return return
on_order_enter_pool(order) on_order_enter_pool(order, force=force)
_ensure_merchant(club_id, mid) _ensure_merchant(club_id, mid)
from jituan.models import MerchantDealStat from jituan.models import MerchantDealStat
MerchantDealStat.objects.filter(club_id=club_id, merchant_uid=mid).update( 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: if int(getattr(penalty, 'Status', 0) or 0) != PENALTY_PAID:
return return
club_id = (getattr(penalty, 'ClubID', None) or '').strip() club_id = (getattr(penalty, 'ClubID', None) or '').strip()
cfg = _cfg(club_id) if not club_id:
if not cfg or not cfg.stats_enabled or not cfg.stats_since:
return return
pid = getattr(penalty, 'id', None) or getattr(penalty, 'pk', None) pid = getattr(penalty, 'id', None) or getattr(penalty, 'pk', None)
key = f'penalty:{pid}:paid' 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: if not uids:
return {} return {}
default_club = (club_id or '').strip() 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() club_ids = {default_club} if default_club else set()
if order_club_map: if order_club_map:
for cid in order_club_map.values(): for cid in order_club_map.values():

View File

@@ -17,12 +17,18 @@ logger = logging.getLogger(__name__)
def get_or_create_deal_config(club_id: str): def get_or_create_deal_config(club_id: str):
from jituan.models import ClubOrderDealConfig from jituan.models import ClubOrderDealConfig
from datetime import datetime
cid = (club_id or '').strip() cid = (club_id or '').strip()
if not cid: if not cid:
return None return None
row = ClubOrderDealConfig.query.filter(club_id=cid).first() row = ClubOrderDealConfig.query.filter(club_id=cid).first()
if not row: 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() row.save()
return row return row
@@ -118,7 +124,9 @@ def save_deal_config(club_id: str, data: dict, updated_by: str = '') -> dict:
enabled = bool(data['stats_enabled']) enabled = bool(data['stats_enabled'])
row.stats_enabled = enabled row.stats_enabled = enabled
if enabled and not was_stats: 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: if not enabled:
# 停止写入;保留 since 供只读理解历史起点;再次开启会刷新 since # 停止写入;保留 since 供只读理解历史起点;再次开启会刷新 since
pass pass
@@ -127,10 +135,19 @@ def save_deal_config(club_id: str, data: dict, updated_by: str = '') -> dict:
# 若刚打开统计且 since 空 # 若刚打开统计且 since 空
if row.stats_enabled and not row.stats_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.updated_by = (updated_by or '')[:32]
row.save() 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) return config_to_dict(row)

View File

@@ -507,8 +507,15 @@ class QiangdanView(APIView):
return Response({'code': 400, 'msg': validation_result['message']}) return Response({'code': 400, 'msg': validation_result['message']})
self._execute_qiangdan(order, dashou_profile, request.user.UserUID) self._execute_qiangdan(order, dashou_profile, request.user.UserUID)
# ========== 【唯一改动】聊天建立方式 ========== # 建聊必须在事务提交后GoEasy HTTP 不可放在行锁事务里,失败也不回滚抢单
chat_success = False
try:
chat_success = establish_order_chat(dingdan_id) chat_success = establish_order_chat(dingdan_id)
if not chat_success:
# 再试一次,避免偶发订阅/发消息失败导致消息列表空白
chat_success = establish_order_chat(dingdan_id)
except Exception as e:
logger.error(f"抢单后建立聊天失败 dingdan={dingdan_id}: {e}", exc_info=True)
try: try:
update_dashou_daily_by_action( update_dashou_daily_by_action(

View File

@@ -413,16 +413,40 @@ def establish_order_chat(dingdan_id):
# ========== 普通订单 / 我方派单 ========== # ========== 普通订单 / 我方派单 ==========
def _handle_local_order(order, dashou_goeasy_id, dashou_name, dashou_avatar, appkey, secret): def _handle_local_order(order, dashou_goeasy_id, dashou_name, dashou_avatar, appkey, secret):
group_id = resolve_pair_group_id(order) or f"group_{order.OrderID}" # 与详情「联系」一致:有组队用 group_订单否则配对群
group_id = resolve_order_chat_group_id(order) or f"group_{order.OrderID}"
group_name = (order.Description[:20] + '') if order.Description and len(order.Description) > 20 else (order.Description or f"订单{order.OrderID}") group_name = (order.Description[:20] + '') if order.Description and len(order.Description) > 20 else (order.Description or f"订单{order.OrderID}")
group_avatar = _full_local_avatar(order.ImageURL) if order.ImageURL else _full_local_avatar('') group_avatar = _full_local_avatar(order.ImageURL) if order.ImageURL else _full_local_avatar('')
partner_goeasy_id, partner_name, partner_avatar = _get_local_partner_info(order) partner_goeasy_id, partner_name, partner_avatar = _get_local_partner_info(order)
if not partner_goeasy_id: if not partner_goeasy_id:
logger.error('抢单建聊失败:找不到商家/老板 partner order=%s', order.OrderID)
return False return False
# 订阅双方 subscribe_ids = [dashou_goeasy_id, partner_goeasy_id]
if not _subscribe_users_to_group([dashou_goeasy_id, partner_goeasy_id], [group_id], appkey, secret): # 组队订单:把在队队员也拉进群,避免队员消息列表空白
try:
from orders.models import TeamMember, TeamRecruit
rec = TeamRecruit.query.filter(
order_id=order.OrderID,
status__in=[TeamRecruit.STATUS_RECRUITING, TeamRecruit.STATUS_LOCKED],
).first()
if rec:
for uid in TeamMember.query.filter(
recruit_id=rec.id, join_status=TeamMember.JOIN_ACTIVE,
).values_list('user_uid', flat=True):
u = (uid or '').strip()
if u:
subscribe_ids.append(f'Ds{u}')
except Exception as e:
logger.warning('抢单建聊订阅组队成员失败: %s', e)
# 订阅双方(失败则重试)
subscribed = _subscribe_users_to_group(subscribe_ids, [group_id], appkey, secret)
if not subscribed:
subscribed = _subscribe_users_to_group(subscribe_ids, [group_id], appkey, secret)
if not subscribed:
logger.error('抢单建聊订阅失败 order=%s group=%s', order.OrderID, group_id)
return False return False
# 1. 打手发送初始化消息 # 1. 打手发送初始化消息
@@ -431,15 +455,28 @@ def _handle_local_order(order, dashou_goeasy_id, dashou_name, dashou_avatar, app
appkey, secret, group_id, dashou_goeasy_id, dashou_name, group_avatar, appkey, secret, group_id, dashou_goeasy_id, dashou_name, group_avatar,
init_msg_text, None, group_name=group_name, order_id=order.OrderID init_msg_text, None, group_name=group_name, order_id=order.OrderID
) )
if not success1:
success1 = _send_group_message(
appkey, secret, group_id, dashou_goeasy_id, dashou_name, group_avatar,
init_msg_text, None, group_name=group_name, order_id=order.OrderID
)
# 2. 【新增】派单方也发一条消息,确保他也能看到群聊 # 2. 派单方也发一条,确保商家/老板会话列表能立刻出现该群
partner_msg = f"订单已确认,请开始服务" partner_msg = f"订单已被接单,请及时沟通"
success2 = _send_group_message( success2 = _send_group_message(
appkey, secret, group_id, partner_goeasy_id, partner_name, partner_avatar, appkey, secret, group_id, partner_goeasy_id, partner_name, partner_avatar,
partner_msg, None, group_name=group_name, order_id=order.OrderID partner_msg, None, group_name=group_name, order_id=order.OrderID
) )
if not success2:
success2 = _send_group_message(
appkey, secret, group_id, partner_goeasy_id, partner_name, partner_avatar,
partner_msg, None, group_name=group_name, order_id=order.OrderID
)
return success1 and success2 ok = bool(success1 or success2)
if not ok:
logger.error('抢单建聊发消息失败 order=%s group=%s', order.OrderID, group_id)
return ok
def _get_local_partner_info(order): def _get_local_partner_info(order):
"""本地订单下单方:老板或商家""" """本地订单下单方:老板或商家"""