fix: 自动结算写入对齐 USE_TZ;lxs 打手注册同步开通管事

order_deal 不再 make_aware 入库以免 MySQL 报错;龙先生打手注册成功时创建管事且邀请人与打手相同。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-27 19:15:16 +08:00
parent 955b76cbea
commit 75fb69e6da
3 changed files with 108 additions and 27 deletions

View File

@@ -5,9 +5,10 @@
from __future__ import annotations
import logging
from datetime import timedelta
from datetime import datetime, timedelta
from decimal import Decimal
from django.conf import settings
from django.db import transaction
from django.db.models import F
from django.utils import timezone
@@ -15,9 +16,25 @@ from django.utils import timezone
logger = logging.getLogger(__name__)
def _db_datetime(dt):
"""写入 DateTimeField 用:与 USE_TZ 对齐,避免 MySQL 拒收 aware datetime。"""
if dt is None:
return None
if settings.USE_TZ:
if timezone.is_naive(dt):
return timezone.make_aware(dt, timezone.get_current_timezone())
return dt
if timezone.is_aware(dt):
return timezone.make_naive(dt, timezone.get_current_timezone())
return dt
def _default_stats_since():
return _db_datetime(datetime(2020, 1, 1))
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
@@ -27,7 +44,7 @@ def get_or_create_deal_config(club_id: str):
row = ClubOrderDealConfig(
club_id=cid,
stats_enabled=True,
stats_since=timezone.make_aware(datetime(2020, 1, 1)),
stats_since=_default_stats_since(),
)
row.save()
return row
@@ -54,12 +71,8 @@ def order_auto_settle_payload(order) -> dict:
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())
now = _db_datetime(timezone.now())
expire_at = _db_datetime(expire_at)
remain = int((expire_at - now).total_seconds())
try:
expire_ts = int(expire_at.timestamp())
@@ -104,7 +117,7 @@ def save_deal_config(club_id: str, data: dict, updated_by: str = '') -> dict:
was_auto = bool(row.auto_settle_enabled)
was_stats = bool(row.stats_enabled)
now = timezone.now()
now = _db_datetime(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))
@@ -125,8 +138,7 @@ def save_deal_config(club_id: str, data: dict, updated_by: str = '') -> dict:
row.stats_enabled = enabled
if enabled and not was_stats:
# 开启时默认从较早起点计,便于旧单刷新也能出成交率
from datetime import datetime
row.stats_since = timezone.make_aware(datetime(2020, 1, 1))
row.stats_since = _default_stats_since()
if not enabled:
# 停止写入;保留 since 供只读理解历史起点;再次开启会刷新 since
pass
@@ -135,8 +147,11 @@ def save_deal_config(club_id: str, data: dict, updated_by: str = '') -> dict:
# 若刚打开统计且 since 空
if row.stats_enabled and not row.stats_since:
from datetime import datetime
row.stats_since = timezone.make_aware(datetime(2020, 1, 1))
row.stats_since = _default_stats_since()
# 历史脏数据aware datetime 在 USE_TZ=False 下无法落库,保存前统一清洗
row.auto_settle_enabled_at = _db_datetime(row.auto_settle_enabled_at)
row.stats_since = _db_datetime(row.stats_since)
row.updated_by = (updated_by or '')[:32]
row.save()
@@ -166,18 +181,19 @@ def apply_enter_status_8(instance):
订单进入结算中(8):写 SettlementTime若俱乐部自动结算已开且本次开启后进入则打 eligible。
由 signal 调用。instance 为即将 save 的 Order。
"""
now = timezone.now()
now = _db_datetime(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
enabled_at = _db_datetime(getattr(cfg, 'auto_settle_enabled_at', None)) if cfg else None
if (
cfg
and cfg.auto_settle_enabled
and cfg.auto_settle_enabled_at
and now >= cfg.auto_settle_enabled_at
and enabled_at
and now >= enabled_at
):
hours = int(cfg.hours_merchant if instance.Platform == 2 else cfg.hours_platform) or 48
instance.AutoSettleEligible = True
@@ -215,15 +231,18 @@ def mark_order_completed_unified(order, *, source='auto_settle', operator=''):
# 三重闸:资格位 + 到期时间 + 俱乐部开关仍开(关开关后立刻停,不靠等下一轮扫)
if not locked.AutoSettleEligible:
return False, '非自动结算资格单'
if locked.AutoExpireAt and locked.AutoExpireAt > timezone.now():
now = _db_datetime(timezone.now())
expire_at = _db_datetime(locked.AutoExpireAt)
if expire_at and expire_at > now:
return False, '未到自动结算时间'
club_id = (getattr(locked, 'ClubID', None) or '').strip()
cfg = get_or_create_deal_config(club_id) if club_id else None
if not cfg or not cfg.auto_settle_enabled:
return False, '俱乐部已关闭自动结算'
# 只认「本次打开之后」进结算中的单;开启前的旧 8 状态单不会带 eligible
if cfg.auto_settle_enabled_at and getattr(locked, 'SettlementTime', None):
if locked.SettlementTime < cfg.auto_settle_enabled_at:
enabled_at = _db_datetime(cfg.auto_settle_enabled_at)
settle_at = _db_datetime(getattr(locked, 'SettlementTime', None))
if enabled_at and settle_at and settle_at < enabled_at:
return False, '订单进入结算中早于本次开启,不自动结算'
dashou_id = locked.PlayerID
@@ -323,7 +342,7 @@ def scan_due_auto_settle(limit: int = 100) -> dict:
if not clubs:
return {'scanned': 0, 'settled': 0, 'failed': 0, 'clubs': 0}
now = timezone.now()
now = _db_datetime(timezone.now())
qs = (
Order.objects.filter(
ClubID__in=list(clubs),

View File

@@ -0,0 +1,46 @@
"""龙先生(lxs):打手注册成功时同步开通管事。
管事邀请人 = 打手邀请人(邀请该用户的管事 UID
其它俱乐部不处理。
"""
from __future__ import annotations
import logging
from utils.invitationcode_utils import CreateInvitationCode
logger = logging.getLogger(__name__)
LXS_CLUB_ID = 'lxs'
def ensure_lxs_guanshi_on_dashou_register(user, inviter_uid, club_id):
"""
仅 club_id=lxs若尚无管事身份则创建。
返回新建或已有的 UserGuanshi非 lxs / 参数不全则返回 None。
"""
from users.models import UserGuanshi
if (club_id or '').strip() != LXS_CLUB_ID:
return None
if not user:
return None
existing = UserGuanshi.query.filter(user=user).first()
if existing:
return existing
inviter = (inviter_uid or '').strip()[:7] or None
yaoqingma = CreateInvitationCode(str(user.UserUID))
profile = UserGuanshi.query.create(
user=user,
yaoqingma=yaoqingma,
yaoqingren=inviter,
)
logger.info(
'lxs 打手注册同步开通管事 uid=%s yaoqingren=%s yaoqingma=%s',
getattr(user, 'UserUID', ''),
inviter,
yaoqingma,
)
return profile

View File

@@ -580,7 +580,11 @@ class DashouZhuceView(APIView):
pass
# 5.3 创建管事扩展表
# 5.3 龙先生:同步开通管事,邀请人与打手邀请人相同
from users.services.lxs_dashou_guanshi import ensure_lxs_guanshi_on_dashou_register
ensure_lxs_guanshi_on_dashou_register(
current_user, guanshi_yonghuid, club_id,
)
# 5.4 更新主表的用户类型 (如果需要,可以设为'dashou',或保持原逻辑)
# current_user.UserType = 'dashou' # 根据你的业务逻辑决定
@@ -613,10 +617,14 @@ class DashouZhuceView(APIView):
# 6. 注册成功,返回信息 (新注册用户没有会员记录clumber返回空列表)
fanhui_data = self.zhuangbeiFanhuiShuju(dashou_profile, current_user, is_new=True)
if (club_id or '').strip() == 'lxs':
ok_msg = '注册成功!已开通打手、管事身份。'
else:
ok_msg = '注册成功!'
return Response({
'code': 200,
'message': '注册成功!已开通打手、管事身份。',
'msg': '注册成功!已开通打手、管事身份。',
'message': ok_msg,
'msg': ok_msg,
'data': fanhui_data
})
@@ -921,7 +929,11 @@ class WechatLoginAndDashouRegisterView(APIView):
# 创建商家扩展表
# 创建管事扩展表
# 龙先生:同步开通管事,邀请人与打手邀请人相同
from users.services.lxs_dashou_guanshi import ensure_lxs_guanshi_on_dashou_register
ensure_lxs_guanshi_on_dashou_register(
user_main, guanshi_yonghuid, club_id,
)
# 更新原管事的邀请数量
locked_guanshi = UserGuanshi.objects.select_for_update().get(pk=guanshi_profile.pk)
@@ -946,9 +958,13 @@ class WechatLoginAndDashouRegisterView(APIView):
# 6. 准备返回数据
response_data = self.zhuangbeiFanhuiShuju(user_main, dashou_profile, token)
if (club_id or '').strip() == 'lxs':
ok_msg = '登录并注册成功!已开通打手、管事等身份。'
else:
ok_msg = '登录并注册成功!'
return Response({
'code': 0,
'msg': '登录并注册成功!已开通打手、管事等身份。',
'msg': ok_msg,
'data': response_data
})