feat: 成交指标新表+可控自动结算(默认关,仅新进结算中eligible)
按设计文档:SettlementTime起表;关开关清eligible;定时任务只结到期eligible单;打款走统一结单;指标从stats_since起幂等记账。不打开旧全局ORDER_AUTO_SETTLEMENT。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
249
jituan/services/order_deal.py
Normal file
249
jituan/services/order_deal.py
Normal file
@@ -0,0 +1,249 @@
|
||||
"""成交指标 + 可控自动结算(俱乐部开关,默认全关)。
|
||||
|
||||
对齐设计文档:成交率与自动结算_设计方案.md
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db import transaction
|
||||
from django.db.models import F
|
||||
from django.utils import timezone
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_or_create_deal_config(club_id: str):
|
||||
from jituan.models import ClubOrderDealConfig
|
||||
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.save()
|
||||
return row
|
||||
|
||||
|
||||
def config_to_dict(row) -> dict:
|
||||
if not row:
|
||||
return {
|
||||
'club_id': '',
|
||||
'auto_settle_enabled': False,
|
||||
'auto_settle_enabled_at': '',
|
||||
'hours_platform': 48,
|
||||
'hours_merchant': 48,
|
||||
'stats_enabled': False,
|
||||
'stats_since': '',
|
||||
}
|
||||
return {
|
||||
'club_id': row.club_id,
|
||||
'auto_settle_enabled': bool(row.auto_settle_enabled),
|
||||
'auto_settle_enabled_at': row.auto_settle_enabled_at.isoformat() if row.auto_settle_enabled_at else '',
|
||||
'hours_platform': int(row.hours_platform or 48),
|
||||
'hours_merchant': int(row.hours_merchant or 48),
|
||||
'stats_enabled': bool(row.stats_enabled),
|
||||
'stats_since': row.stats_since.isoformat() if row.stats_since else '',
|
||||
}
|
||||
|
||||
|
||||
def save_deal_config(club_id: str, data: dict, updated_by: str = '') -> dict:
|
||||
"""保存配置。关自动结算时清空本俱乐部所有 eligible。"""
|
||||
row = get_or_create_deal_config(club_id)
|
||||
if not row:
|
||||
raise ValueError('俱乐部不能为空')
|
||||
|
||||
was_auto = bool(row.auto_settle_enabled)
|
||||
was_stats = bool(row.stats_enabled)
|
||||
now = timezone.now()
|
||||
|
||||
if 'hours_platform' in data and data['hours_platform'] is not None:
|
||||
row.hours_platform = max(1, min(int(data['hours_platform']), 720))
|
||||
if 'hours_merchant' in data and data['hours_merchant'] is not None:
|
||||
row.hours_merchant = max(1, min(int(data['hours_merchant']), 720))
|
||||
|
||||
if 'auto_settle_enabled' in data and data['auto_settle_enabled'] is not None:
|
||||
enabled = bool(data['auto_settle_enabled'])
|
||||
row.auto_settle_enabled = enabled
|
||||
if enabled and not was_auto:
|
||||
row.auto_settle_enabled_at = now
|
||||
if not enabled:
|
||||
row.auto_settle_enabled_at = None
|
||||
_clear_club_auto_settle_eligible(row.club_id)
|
||||
|
||||
if 'stats_enabled' in data and data['stats_enabled'] is not None:
|
||||
enabled = bool(data['stats_enabled'])
|
||||
row.stats_enabled = enabled
|
||||
if enabled and not was_stats:
|
||||
row.stats_since = now
|
||||
if not enabled:
|
||||
# 停止写入;保留 since 供只读理解历史起点;再次开启会刷新 since
|
||||
pass
|
||||
if enabled and was_stats and data.get('reset_stats_since'):
|
||||
row.stats_since = now
|
||||
|
||||
# 若刚打开统计且 since 空
|
||||
if row.stats_enabled and not row.stats_since:
|
||||
row.stats_since = now
|
||||
|
||||
row.updated_by = (updated_by or '')[:32]
|
||||
row.save()
|
||||
return config_to_dict(row)
|
||||
|
||||
|
||||
def _clear_club_auto_settle_eligible(club_id: str):
|
||||
from orders.models import Order
|
||||
Order.objects.filter(ClubID=club_id, AutoSettleEligible=True).update(
|
||||
AutoSettleEligible=False,
|
||||
AutoExpireAt=None,
|
||||
PendingDispatch=False,
|
||||
AutoTaskID='',
|
||||
)
|
||||
|
||||
|
||||
def apply_enter_status_8(instance):
|
||||
"""
|
||||
订单进入结算中(8):写 SettlementTime;若俱乐部自动结算已开且本次开启后进入则打 eligible。
|
||||
由 signal 调用。instance 为即将 save 的 Order。
|
||||
"""
|
||||
now = timezone.now()
|
||||
instance.SettlementTime = now
|
||||
instance.PendingDispatch = False
|
||||
instance.AutoTaskID = ''
|
||||
|
||||
club_id = (getattr(instance, 'ClubID', None) or '').strip()
|
||||
cfg = get_or_create_deal_config(club_id) if club_id else None
|
||||
if (
|
||||
cfg
|
||||
and cfg.auto_settle_enabled
|
||||
and cfg.auto_settle_enabled_at
|
||||
and now >= cfg.auto_settle_enabled_at
|
||||
):
|
||||
hours = int(cfg.hours_merchant if instance.Platform == 2 else cfg.hours_platform) or 48
|
||||
instance.AutoSettleEligible = True
|
||||
instance.AutoExpireAt = now + timedelta(hours=hours)
|
||||
else:
|
||||
instance.AutoSettleEligible = False
|
||||
instance.AutoExpireAt = None
|
||||
|
||||
|
||||
def apply_leave_status_8(instance):
|
||||
instance.SettlementTime = None
|
||||
instance.AutoSettleEligible = False
|
||||
instance.AutoExpireAt = None
|
||||
instance.PendingDispatch = False
|
||||
instance.AutoTaskID = ''
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def mark_order_completed_unified(order, *, source='auto_settle', operator=''):
|
||||
"""
|
||||
与客服强制结单等价的核心结算(打手入账 + 商家成交 + 管事分红)。
|
||||
自动结算路径额外要求 AutoSettleEligible。
|
||||
"""
|
||||
from orders.models import Order
|
||||
from users.business_models import User
|
||||
from orders.utils import settle_shangjia_order_guanshi_fenhong
|
||||
from jituan.services.fund_freeze import credit_dashou_order_settle
|
||||
|
||||
locked = Order.objects.select_for_update().filter(OrderID=order.OrderID).first()
|
||||
if not locked:
|
||||
return False, '订单不存在'
|
||||
if locked.Status != 8:
|
||||
return False, f'状态不是结算中({locked.Status})'
|
||||
if source == 'auto_settle':
|
||||
if not locked.AutoSettleEligible:
|
||||
return False, '非自动结算资格单'
|
||||
if locked.AutoExpireAt and locked.AutoExpireAt > timezone.now():
|
||||
return False, '未到自动结算时间'
|
||||
|
||||
dashou_id = locked.PlayerID
|
||||
dashou_fencheng = locked.PlayerCommission
|
||||
if not dashou_id:
|
||||
return False, '无接单打手'
|
||||
if not dashou_fencheng or dashou_fencheng <= 0:
|
||||
return False, '打手分成无效'
|
||||
|
||||
try:
|
||||
dashou_user = User.query.get(UserUID=dashou_id)
|
||||
dashou_profile = dashou_user.DashouProfile
|
||||
except Exception:
|
||||
return False, '打手不存在'
|
||||
|
||||
credit_dashou_order_settle(
|
||||
order=locked,
|
||||
dashou_profile=dashou_profile,
|
||||
user_id=dashou_id,
|
||||
amount=dashou_fencheng,
|
||||
)
|
||||
|
||||
if locked.Platform == 2:
|
||||
try:
|
||||
ext = getattr(locked, 'shangjia_kuozhan', None)
|
||||
mid = getattr(ext, 'MerchantID', None) if ext else None
|
||||
if mid:
|
||||
shangjia_user = User.query.get(UserUID=mid)
|
||||
profile = shangjia_user.ShopProfile
|
||||
type(profile).objects.filter(pk=profile.pk).update(chengjiao=F('chengjiao') + 1)
|
||||
try:
|
||||
from backend.utils import update_shangjia_daily
|
||||
update_shangjia_daily(
|
||||
yonghuid=mid,
|
||||
amount=Decimal(str(locked.Amount or 0)),
|
||||
action=2,
|
||||
order_id=locked.OrderID,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning('商家日统计跳过 %s: %s', locked.OrderID, e)
|
||||
except Exception as e:
|
||||
logger.warning('商家成交累加跳过 %s: %s', locked.OrderID, e)
|
||||
settle_shangjia_order_guanshi_fenhong(locked, dashou_id)
|
||||
|
||||
locked.Status = 3
|
||||
locked.AutoSettleEligible = False
|
||||
locked.AutoExpireAt = None
|
||||
# pre_save 会清 SettlementTime;必须写入 update_fields 才能落库
|
||||
update_fields = ['Status', 'AutoSettleEligible', 'AutoExpireAt', 'SettlementTime', 'UpdateTime']
|
||||
if operator:
|
||||
locked.AssignedCS = (operator or '')[:20]
|
||||
update_fields.append('AssignedCS')
|
||||
locked.save(update_fields=update_fields)
|
||||
# 成交指标由 post_save signal 幂等记账,此处不重复调用
|
||||
return True, 'ok'
|
||||
|
||||
|
||||
def scan_due_auto_settle(limit: int = 100) -> dict:
|
||||
"""只处理 eligible 到期单;无关自动结算的俱乐部空跑。"""
|
||||
from jituan.models import ClubOrderDealConfig
|
||||
from orders.models import Order
|
||||
|
||||
clubs = list(
|
||||
ClubOrderDealConfig.query.filter(auto_settle_enabled=True).values_list('club_id', flat=True)
|
||||
)
|
||||
if not clubs:
|
||||
return {'scanned': 0, 'settled': 0, 'failed': 0, 'clubs': 0}
|
||||
|
||||
now = timezone.now()
|
||||
qs = (
|
||||
Order.objects.filter(
|
||||
ClubID__in=list(clubs),
|
||||
Status=8,
|
||||
AutoSettleEligible=True,
|
||||
AutoExpireAt__isnull=False,
|
||||
AutoExpireAt__lte=now,
|
||||
)
|
||||
.order_by('AutoExpireAt')[:limit]
|
||||
)
|
||||
settled = 0
|
||||
failed = 0
|
||||
for order in qs:
|
||||
ok, msg = mark_order_completed_unified(order, source='auto_settle', operator='auto')
|
||||
if ok:
|
||||
settled += 1
|
||||
logger.info('自动结算成功 %s', order.OrderID)
|
||||
else:
|
||||
failed += 1
|
||||
logger.warning('自动结算跳过 %s: %s', order.OrderID, msg)
|
||||
return {'scanned': settled + failed, 'settled': settled, 'failed': failed, 'clubs': len(clubs)}
|
||||
Reference in New Issue
Block a user