320 lines
11 KiB
Python
320 lines
11 KiB
Python
"""抢单大厅假单:按俱乐部开关混排/过滤。"""
|
||
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,
|
||
},
|
||
})
|