Files
Django/jituan/services/order_deal.py
XingQue 1b733d21cc feat: 订单列表/详情返回自动结算倒计时字段,并加小程序结算率接口
仅 eligible 结算中订单下发预计时间;支持 auto_settle_pending 筛选;新增 /dingdan/wo-de-deal-stat。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 02:08:32 +08:00

285 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""成交指标 + 可控自动结算(俱乐部开关,默认全关)。
对齐设计文档成交率与自动结算_设计方案.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 order_auto_settle_payload(order) -> dict:
"""
小程序列表/详情展示用。
仅 Status=8 且 AutoSettleEligible 且有 AutoExpireAt 时返回字段;否则空 dict。
前端有字段才展示,避免关开关/老单误报。
"""
from backend.utils import fmt_datetime
if not order:
return {}
try:
status = int(getattr(order, 'Status', 0) or 0)
except (TypeError, ValueError):
status = 0
if status != 8:
return {}
if not bool(getattr(order, 'AutoSettleEligible', False)):
return {}
expire_at = getattr(order, 'AutoExpireAt', None)
if not expire_at:
return {}
now = timezone.now()
# 与项目 USE_TZ 对齐,避免剩余秒数偏差
if timezone.is_aware(expire_at) and timezone.is_naive(now):
now = timezone.make_aware(now, timezone.get_current_timezone())
elif timezone.is_naive(expire_at) and timezone.is_aware(now):
expire_at = timezone.make_aware(expire_at, timezone.get_current_timezone())
remain = int((expire_at - now).total_seconds())
return {
'auto_settle_eligible': True,
'auto_expire_at': fmt_datetime(expire_at),
'auto_settle_remain_seconds': max(0, remain),
}
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)}