Files
Django/orders/services/fake_order_pool.py

320 lines
11 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.
"""抢单大厅假单:按俱乐部开关混排/过滤。"""
import logging
import random
from datetime import datetime, timedelta
from decimal import Decimal
from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction
from django.utils import timezone
from django.utils.dateparse import parse_datetime
from rest_framework.response import Response
from backend.utils import fmt_datetime, pick_leixing_id
from jituan.services.club_context import resolve_effective_club_id
from jituan.services.fake_grab_switch import (
get_fake_yajin_hide_threshold,
is_club_fake_grab_enabled,
)
from jituan.services.huiyuan_bundle import normalize_huiyuan_id, user_has_huiyuan_access
from orders.models import FakeGrabOrder, FakeGrabOrderShuffleState
logger = logging.getLogger(__name__)
DEFAULT_DISPATCHER_AVATAR = 'beijing/morentouxiang.jpg'
SHUFFLE_STATE_PK = 1
MODE_OFF = 'off'
MODE_FAKE_ONLY = 'fake_only'
MODE_MIX = 'mix'
MODE_REAL_ONLY = 'real_only'
def _leixing_membership_meta(leixing_id):
"""返回 (required_huiyuan_id, yaoqiuleixing)。"""
if leixing_id is None:
return '', None
try:
from products.models import ShangpinLeixing
row = ShangpinLeixing.query.filter(id=int(leixing_id)).values(
'huiyuan_id', 'yaoqiuleixing',
).first()
if not row:
return '', None
return (
normalize_huiyuan_id(row.get('huiyuan_id')),
row.get('yaoqiuleixing'),
)
except Exception:
logger.exception('read ShangpinLeixing membership meta failed leixing_id=%s', leixing_id)
return '', None
def _user_has_type_membership(user, club_id, leixing_id):
required, _yaoqiu = _leixing_membership_meta(leixing_id)
if not required:
return False
try:
return bool(user_has_huiyuan_access(user.UserUID, required, club_id=club_id))
except Exception:
logger.exception(
'user_has_huiyuan_access failed uid=%s required=%s club=%s',
getattr(user, 'UserUID', ''), required, club_id,
)
return False
def resolve_fake_pool_mode(user, club_id=None, leixing_id=None):
"""
- off俱乐部未开 → 只真单
- fake_only封禁 → 只假单
- real_only有对应会员或押金≥门槛 → 过滤假单
- mix未充对应会员且押金未达门槛 → 真单+假单
"""
cid = (club_id or '').strip()
if not cid:
return MODE_OFF
try:
enabled = is_club_fake_grab_enabled(cid)
except Exception:
logger.exception('is_club_fake_grab_enabled failed club=%s', cid)
return MODE_OFF
if not enabled:
return MODE_OFF
try:
dashou = user.DashouProfile
except ObjectDoesNotExist:
return MODE_FAKE_ONLY
except Exception:
logger.exception('read DashouProfile failed uid=%s', getattr(user, 'UserUID', ''))
return MODE_FAKE_ONLY
if int(getattr(dashou, 'zhanghaozhuangtai', 0) or 0) != 1:
return MODE_FAKE_ONLY
if leixing_id is not None and _user_has_type_membership(user, cid, leixing_id):
return MODE_REAL_ONLY
try:
yajin = Decimal(str(getattr(dashou, 'yajin', 0) or 0))
except Exception:
yajin = Decimal('0')
if yajin >= get_fake_yajin_hide_threshold(cid):
return MODE_REAL_ONLY
return MODE_MIX
def dashou_should_use_fake_pool(user, club_id=None, leixing_id=None):
"""兼容旧调用:是否整页只假单。"""
return resolve_fake_pool_mode(user, club_id=club_id, leixing_id=leixing_id) == MODE_FAKE_ONLY
def list_formatted_fake_orders(club_id, leixing_id):
rows, anchor_dt = _load_fake_rows_in_saved_order(club_id, leixing_id)
return [_format_fake_row(r, anchor_dt) for r in rows]
def _scope_key(club_id, leixing_id):
lid = leixing_id if leixing_id is not None else 'all'
return f'{club_id}|{lid}'
def _ensure_aware(dt):
if dt is None:
return None
if timezone.is_naive(dt):
return timezone.make_aware(dt, timezone.get_current_timezone())
return dt
def _to_unix_ts(dt):
"""统一转为 Unix 时间戳,避免 naive/aware 直接相减报错。"""
if dt is None:
return None
if timezone.is_aware(dt):
return dt.timestamp()
try:
return timezone.make_aware(dt, timezone.get_current_timezone()).timestamp()
except Exception:
return dt.timestamp()
def _elapsed_seconds(since_dt, until_dt):
since_ts = _to_unix_ts(since_dt)
until_ts = _to_unix_ts(until_dt)
if since_ts is None or until_ts is None:
return None
return until_ts - since_ts
def _parse_shuffle_time(value):
if not value:
return None
if isinstance(value, str):
dt = parse_datetime(value)
return _ensure_aware(dt)
return _ensure_aware(value)
def _display_time_str_for_order(order_id, anchor_dt):
"""同一假单在打乱窗口内展示时间固定。"""
seed = abs(hash(str(order_id))) % (2 ** 32)
seconds_ago = 120 + (seed % 181)
base_ts = _to_unix_ts(_ensure_aware(anchor_dt) or timezone.now())
if base_ts is None:
base_ts = _to_unix_ts(timezone.now()) or 0
display_dt = datetime.fromtimestamp(
base_ts - seconds_ago,
tz=timezone.get_current_timezone(),
)
return fmt_datetime(display_dt)
def _load_fake_rows_in_saved_order(club_id, leixing_id):
"""
读取/更新 id=1 打乱状态表:
- 未满 IntervalSeconds按库中 order_ids 顺序返回(全用户一致)
- 已满:重新打乱并写回 order_ids
- 新增假单:追加到顺序末尾并写回;已删假单:自动跳过
返回 (rows, anchor_dt) 供展示时间固定。
"""
qs = FakeGrabOrder.query.filter(ClubID=club_id)
if leixing_id is not None:
qs = qs.filter(ProductTypeID=leixing_id)
all_rows = list(qs)
id_to_row = {r.OrderID: r for r in all_rows if r.OrderID}
all_ids = list(id_to_row.keys())
scope = _scope_key(club_id, leixing_id)
now = timezone.now()
with transaction.atomic():
state, created = FakeGrabOrderShuffleState.objects.select_for_update().get_or_create(
pk=SHUFFLE_STATE_PK,
defaults={
'LastShuffleTime': now - timedelta(days=1),
'IntervalSeconds': 120,
'OrderSequence': {},
},
)
interval = max(1, int(state.IntervalSeconds or 120))
seq_map = dict(state.OrderSequence or {})
entry = dict(seq_map.get(scope) or {})
stored_ids = list(entry.get('order_ids') or [])
last_dt = _parse_shuffle_time(entry.get('last_shuffle_time'))
if not last_dt and not created:
last_dt = _ensure_aware(state.LastShuffleTime)
elapsed = _elapsed_seconds(last_dt, now)
need_reshuffle = (
not stored_ids
or last_dt is None
or elapsed is None
or elapsed >= interval
)
if need_reshuffle:
stored_ids = list(all_ids)
random.shuffle(stored_ids)
entry = {
'last_shuffle_time': now.isoformat(),
'order_ids': stored_ids,
}
seq_map[scope] = entry
state.OrderSequence = seq_map
state.LastShuffleTime = now
state.save(update_fields=['OrderSequence', 'LastShuffleTime', 'UpdateTime'])
else:
valid_ids = [oid for oid in stored_ids if oid in id_to_row]
new_ids = [oid for oid in all_ids if oid not in set(valid_ids)]
if len(valid_ids) != len(stored_ids) or new_ids:
valid_ids.extend(new_ids)
entry['order_ids'] = valid_ids
seq_map[scope] = entry
state.OrderSequence = seq_map
state.save(update_fields=['OrderSequence', 'UpdateTime'])
stored_ids = valid_ids
anchor_dt = _parse_shuffle_time(entry.get('last_shuffle_time')) or _ensure_aware(now)
ordered = [id_to_row[oid] for oid in stored_ids if oid in id_to_row]
return ordered, anchor_dt
def _format_fake_row(row, anchor_dt=None):
"""字段与 DashouDingdanHuoquView 真实单列表保持一致。"""
avatar = (getattr(row, 'DispatcherAvatar', None) or '').strip() or DEFAULT_DISPATCHER_AVATAR
platform = int(row.Platform or 1)
oid = row.OrderID
price = float(row.Amount or 0)
ct = _display_time_str_for_order(oid, anchor_dt)
zid = (row.DispatcherUserID or '').strip()
sj_name = (getattr(row, 'MerchantNickname', None) or '').strip()
return {
'dingdan_id': oid,
'zhuangtai': 1,
'pingtai': platform,
'dashou_fencheng': price,
'leixing_id': row.ProductTypeID or 0,
'tupian': avatar,
'yaoqiuleixing': 0,
'huiyuan_id': '',
# 假单始终展示价格(前端 hasRequiredMember=true 才显示金额)
'hasRequiredMember': True,
'has_required_member': True,
'is_fake': True,
'yajin': 0.0,
'jieshao': row.Description or '',
'beizhu': row.Remark or '',
'creat_time': ct,
'create_time': ct,
'sjnicheng': sj_name if platform == 2 else '',
'sj_avatar': avatar if platform == 2 else '',
'zhiding_uid': zid,
'zhiding_avatar': avatar,
'zhiding_nicheng': '',
'xuqiu_biaoqian': [],
'dashou_biaoqian': [],
'shangjia_biaoqian': [],
'shangjia_identity_biaoqian': [],
'zhiding_identity_biaoqian': [],
'shangjia_youzhi': bool(row.IsPremiumMerchant) if platform == 2 else False,
}
def build_fake_order_pool_response(request):
data = request.data
leixing_id = pick_leixing_id(data)
page = max(1, int(data.get('page', 1)))
page_size = max(1, min(int(data.get('page_size', 10)), 50))
club_id = resolve_effective_club_id(request, getattr(request, 'user', None))
rows, anchor_dt = _load_fake_rows_in_saved_order(club_id, leixing_id)
total = len(rows)
offset = (page - 1) * page_size
page_rows = rows[offset:offset + page_size]
formatted_list = [_format_fake_row(r, anchor_dt) for r in page_rows]
has_more = (page * page_size) < total
return Response({
'code': 200,
'msg': '获取成功',
'data': {
'list': formatted_list,
'has_more': has_more,
'total': total,
'current_page': page,
'page_size': page_size,
'total_pages': (total + page_size - 1) // page_size if page_size else 0,
'type_pending_counts': {},
'pool_pending_total': 0,
'team_pending_count': 0,
'is_fake_pool': True,
},
})