Files
Django/orders/services/fake_order_pool.py

270 lines
8.9 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 hashlib
import logging
import random
import time
from datetime import datetime
from decimal import Decimal
from django.core.exceptions import ObjectDoesNotExist
from django.utils import timezone
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
logger = logging.getLogger(__name__)
DEFAULT_DISPATCHER_AVATAR = 'beijing/morentouxiang.jpg'
# 展示顺序每 N 秒换一桶;同桶内全用户/全 worker 一致(不写库,无并发覆盖)
DEFAULT_SHUFFLE_INTERVAL = 120
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_stable_order(club_id, leixing_id)
return [_format_fake_row(r, anchor_dt) for r in rows]
def _stable_int_seed(*parts):
raw = '|'.join('' if p is None else str(p) for p in parts)
return int(hashlib.md5(raw.encode('utf-8')).hexdigest()[:8], 16)
def _shuffle_interval_seconds():
try:
from orders.models import FakeGrabOrderShuffleState
row = FakeGrabOrderShuffleState.objects.filter(pk=1).only('IntervalSeconds').first()
if row and row.IntervalSeconds:
return max(1, int(row.IntervalSeconds))
except Exception:
pass
return DEFAULT_SHUFFLE_INTERVAL
def _display_time_str_for_order(order_id, anchor_dt):
"""同一假单在打乱窗口内展示时间固定md5不依赖 PYTHONHASHSEED"""
seed = _stable_int_seed(order_id)
seconds_ago = 120 + (seed % 181)
if anchor_dt is None:
base_ts = time.time()
elif timezone.is_aware(anchor_dt):
base_ts = anchor_dt.timestamp()
else:
try:
base_ts = timezone.make_aware(anchor_dt, timezone.get_current_timezone()).timestamp()
except Exception:
base_ts = time.time()
display_dt = datetime.fromtimestamp(
base_ts - seconds_ago,
tz=timezone.get_current_timezone(),
)
return fmt_datetime(display_dt)
def _load_fake_rows_stable_order(club_id, leixing_id):
"""
按俱乐部+类型加载假单,用「时间桶 + md5」稳定打乱
- 同桶内所有进程/用户顺序一致
- 不读写共享 JSON避免并发刷新把列表写空/写丢
"""
cid = (club_id or '').strip()
qs = FakeGrabOrder.query.filter(ClubID=cid)
if leixing_id is not None:
try:
qs = qs.filter(ProductTypeID=int(leixing_id))
except (TypeError, ValueError):
qs = qs.filter(ProductTypeID=leixing_id)
# 先稳定取出,再按桶打乱(避免 DB 顺序漂移)
all_rows = list(qs.order_by('-SortOrder', 'OrderID'))
id_to_row = {r.OrderID: r for r in all_rows if r.OrderID}
all_ids = list(id_to_row.keys())
interval = _shuffle_interval_seconds()
bucket = int(time.time()) // interval
rng = random.Random(_stable_int_seed(cid, leixing_id, bucket))
shuffled_ids = list(all_ids)
rng.shuffle(shuffled_ids)
anchor_dt = datetime.fromtimestamp(
bucket * interval,
tz=timezone.get_current_timezone(),
)
ordered = [id_to_row[oid] for oid in shuffled_ids if oid in id_to_row]
if len(ordered) != len(all_ids):
logger.warning(
'fake pool order mismatch club=%s leixing=%s ids=%s ordered=%s',
cid, leixing_id, len(all_ids), len(ordered),
)
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_stable_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,
},
})