diff --git a/jituan/services/order_deal.py b/jituan/services/order_deal.py index 88967fa..b56d5e5 100644 --- a/jituan/services/order_deal.py +++ b/jituan/services/order_deal.py @@ -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,16 +231,19 @@ 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: - return False, '订单进入结算中早于本次开启,不自动结算' + 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 dashou_fencheng = locked.PlayerCommission @@ -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), diff --git a/users/services/lxs_dashou_guanshi.py b/users/services/lxs_dashou_guanshi.py new file mode 100644 index 0000000..801563c --- /dev/null +++ b/users/services/lxs_dashou_guanshi.py @@ -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 diff --git a/users/views/auth.py b/users/views/auth.py index abab8cd..de35332 100644 --- a/users/views/auth.py +++ b/users/views/auth.py @@ -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 })