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 from __future__ import annotations
import logging import logging
from datetime import timedelta from datetime import datetime, timedelta
from decimal import Decimal from decimal import Decimal
from django.conf import settings
from django.db import transaction from django.db import transaction
from django.db.models import F from django.db.models import F
from django.utils import timezone from django.utils import timezone
@@ -15,9 +16,25 @@ from django.utils import timezone
logger = logging.getLogger(__name__) 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): 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
@@ -27,7 +44,7 @@ def get_or_create_deal_config(club_id: str):
row = ClubOrderDealConfig( row = ClubOrderDealConfig(
club_id=cid, club_id=cid,
stats_enabled=True, stats_enabled=True,
stats_since=timezone.make_aware(datetime(2020, 1, 1)), stats_since=_default_stats_since(),
) )
row.save() row.save()
return row return row
@@ -54,12 +71,8 @@ def order_auto_settle_payload(order) -> dict:
expire_at = getattr(order, 'AutoExpireAt', None) expire_at = getattr(order, 'AutoExpireAt', None)
if not expire_at: if not expire_at:
return {} return {}
now = timezone.now() now = _db_datetime(timezone.now())
# 与项目 USE_TZ 对齐,避免剩余秒数偏差 expire_at = _db_datetime(expire_at)
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()) remain = int((expire_at - now).total_seconds())
try: try:
expire_ts = int(expire_at.timestamp()) 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_auto = bool(row.auto_settle_enabled)
was_stats = bool(row.stats_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: if 'hours_platform' in data and data['hours_platform'] is not None:
row.hours_platform = max(1, min(int(data['hours_platform']), 720)) 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 row.stats_enabled = enabled
if enabled and not was_stats: if enabled and not was_stats:
# 开启时默认从较早起点计,便于旧单刷新也能出成交率 # 开启时默认从较早起点计,便于旧单刷新也能出成交率
from datetime import datetime row.stats_since = _default_stats_since()
row.stats_since = timezone.make_aware(datetime(2020, 1, 1))
if not enabled: if not enabled:
# 停止写入;保留 since 供只读理解历史起点;再次开启会刷新 since # 停止写入;保留 since 供只读理解历史起点;再次开启会刷新 since
pass pass
@@ -135,8 +147,11 @@ 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:
from datetime import datetime row.stats_since = _default_stats_since()
row.stats_since = timezone.make_aware(datetime(2020, 1, 1))
# 历史脏数据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.updated_by = (updated_by or '')[:32]
row.save() row.save()
@@ -166,18 +181,19 @@ def apply_enter_status_8(instance):
订单进入结算中(8):写 SettlementTime若俱乐部自动结算已开且本次开启后进入则打 eligible。 订单进入结算中(8):写 SettlementTime若俱乐部自动结算已开且本次开启后进入则打 eligible。
由 signal 调用。instance 为即将 save 的 Order。 由 signal 调用。instance 为即将 save 的 Order。
""" """
now = timezone.now() now = _db_datetime(timezone.now())
instance.SettlementTime = now instance.SettlementTime = now
instance.PendingDispatch = False instance.PendingDispatch = False
instance.AutoTaskID = '' instance.AutoTaskID = ''
club_id = (getattr(instance, 'ClubID', None) or '').strip() club_id = (getattr(instance, 'ClubID', None) or '').strip()
cfg = get_or_create_deal_config(club_id) if club_id else None 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 ( if (
cfg cfg
and cfg.auto_settle_enabled and cfg.auto_settle_enabled
and cfg.auto_settle_enabled_at and enabled_at
and now >= cfg.auto_settle_enabled_at and now >= enabled_at
): ):
hours = int(cfg.hours_merchant if instance.Platform == 2 else cfg.hours_platform) or 48 hours = int(cfg.hours_merchant if instance.Platform == 2 else cfg.hours_platform) or 48
instance.AutoSettleEligible = True instance.AutoSettleEligible = True
@@ -215,16 +231,19 @@ def mark_order_completed_unified(order, *, source='auto_settle', operator=''):
# 三重闸:资格位 + 到期时间 + 俱乐部开关仍开(关开关后立刻停,不靠等下一轮扫) # 三重闸:资格位 + 到期时间 + 俱乐部开关仍开(关开关后立刻停,不靠等下一轮扫)
if not locked.AutoSettleEligible: if not locked.AutoSettleEligible:
return False, '非自动结算资格单' 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, '未到自动结算时间' return False, '未到自动结算时间'
club_id = (getattr(locked, 'ClubID', None) or '').strip() club_id = (getattr(locked, 'ClubID', None) or '').strip()
cfg = get_or_create_deal_config(club_id) if club_id else None cfg = get_or_create_deal_config(club_id) if club_id else None
if not cfg or not cfg.auto_settle_enabled: if not cfg or not cfg.auto_settle_enabled:
return False, '俱乐部已关闭自动结算' return False, '俱乐部已关闭自动结算'
# 只认「本次打开之后」进结算中的单;开启前的旧 8 状态单不会带 eligible # 只认「本次打开之后」进结算中的单;开启前的旧 8 状态单不会带 eligible
if cfg.auto_settle_enabled_at and getattr(locked, 'SettlementTime', None): enabled_at = _db_datetime(cfg.auto_settle_enabled_at)
if locked.SettlementTime < cfg.auto_settle_enabled_at: settle_at = _db_datetime(getattr(locked, 'SettlementTime', None))
return False, '订单进入结算中早于本次开启,不自动结算' if enabled_at and settle_at and settle_at < enabled_at:
return False, '订单进入结算中早于本次开启,不自动结算'
dashou_id = locked.PlayerID dashou_id = locked.PlayerID
dashou_fencheng = locked.PlayerCommission dashou_fencheng = locked.PlayerCommission
@@ -323,7 +342,7 @@ def scan_due_auto_settle(limit: int = 100) -> dict:
if not clubs: if not clubs:
return {'scanned': 0, 'settled': 0, 'failed': 0, 'clubs': 0} return {'scanned': 0, 'settled': 0, 'failed': 0, 'clubs': 0}
now = timezone.now() now = _db_datetime(timezone.now())
qs = ( qs = (
Order.objects.filter( Order.objects.filter(
ClubID__in=list(clubs), 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 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',或保持原逻辑) # 5.4 更新主表的用户类型 (如果需要,可以设为'dashou',或保持原逻辑)
# current_user.UserType = 'dashou' # 根据你的业务逻辑决定 # current_user.UserType = 'dashou' # 根据你的业务逻辑决定
@@ -613,10 +617,14 @@ class DashouZhuceView(APIView):
# 6. 注册成功,返回信息 (新注册用户没有会员记录clumber返回空列表) # 6. 注册成功,返回信息 (新注册用户没有会员记录clumber返回空列表)
fanhui_data = self.zhuangbeiFanhuiShuju(dashou_profile, current_user, is_new=True) fanhui_data = self.zhuangbeiFanhuiShuju(dashou_profile, current_user, is_new=True)
if (club_id or '').strip() == 'lxs':
ok_msg = '注册成功!已开通打手、管事身份。'
else:
ok_msg = '注册成功!'
return Response({ return Response({
'code': 200, 'code': 200,
'message': '注册成功!已开通打手、管事身份。', 'message': ok_msg,
'msg': '注册成功!已开通打手、管事身份。', 'msg': ok_msg,
'data': fanhui_data '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) locked_guanshi = UserGuanshi.objects.select_for_update().get(pk=guanshi_profile.pk)
@@ -946,9 +958,13 @@ class WechatLoginAndDashouRegisterView(APIView):
# 6. 准备返回数据 # 6. 准备返回数据
response_data = self.zhuangbeiFanhuiShuju(user_main, dashou_profile, token) response_data = self.zhuangbeiFanhuiShuju(user_main, dashou_profile, token)
if (club_id or '').strip() == 'lxs':
ok_msg = '登录并注册成功!已开通打手、管事等身份。'
else:
ok_msg = '登录并注册成功!'
return Response({ return Response({
'code': 0, 'code': 0,
'msg': '登录并注册成功!已开通打手、管事等身份。', 'msg': ok_msg,
'data': response_data 'data': response_data
}) })