Files
Django/orders/services/fake_order_pool.py
XingQue ddfd3d3ac7 feat: 假单后台管理接口(按俱乐部+商品类型 CRUD)
新增 fake-grab-order-list/manage/avatar 接口,权限 2200a;支持增删改价格介绍头像等;假单展示使用自定义头像。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-05 22:28:13 +08:00

238 lines
7.7 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 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.dashou_exam import user_has_active_member
from orders.models import FakeGrabOrder, FakeGrabOrderShuffleState
DEFAULT_DISPATCHER_AVATAR = 'beijing/morentouxiang.jpg'
SHUFFLE_STATE_PK = 1
def dashou_should_use_fake_pool(user):
"""
打手是否只能看假单:
- 账号封禁zhanghaozhuangtai != 1
- 或未开通有效会员 且 押金不大于 5
"""
try:
dashou = user.DashouProfile
except ObjectDoesNotExist:
return True
if dashou.zhanghaozhuangtai != 1:
return True
yajin = Decimal(str(dashou.yajin or 0))
if yajin > Decimal('5'):
return False
if user_has_active_member(user.UserUID):
return False
return True
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': '',
'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,
},
})