feat: 假单与真单混排,会员或押金达标后过滤假单
This commit is contained in:
@@ -114,6 +114,7 @@ def build_fake_order_list_payload(request, leixing_id=None, page=1, page_size=20
|
||||
'scope_error': CLUB_READ_SCOPE_MSG,
|
||||
'leixing_options': build_leixing_options(),
|
||||
'fake_grab_enabled': False,
|
||||
'fake_yajin_hide_threshold': 10,
|
||||
**list_response_meta(request),
|
||||
}
|
||||
|
||||
@@ -130,7 +131,10 @@ def build_fake_order_list_payload(request, leixing_id=None, page=1, page_size=20
|
||||
rows = list(qs[offset:offset + page_size])
|
||||
names = _leixing_name_map()
|
||||
|
||||
from jituan.services.fake_grab_switch import is_club_fake_grab_enabled
|
||||
from jituan.services.fake_grab_switch import (
|
||||
get_fake_yajin_hide_threshold,
|
||||
is_club_fake_grab_enabled,
|
||||
)
|
||||
|
||||
return {
|
||||
'list': [_row_to_dict(r, names) for r in rows],
|
||||
@@ -141,6 +145,7 @@ def build_fake_order_list_payload(request, leixing_id=None, page=1, page_size=20
|
||||
'scope': resolve_club_scope(request),
|
||||
'leixing_options': build_leixing_options(),
|
||||
'fake_grab_enabled': is_club_fake_grab_enabled(club_id),
|
||||
'fake_yajin_hide_threshold': float(get_fake_yajin_hide_threshold(club_id)),
|
||||
**list_response_meta(request),
|
||||
}
|
||||
|
||||
@@ -152,6 +157,7 @@ def set_fake_grab_enabled(request, enabled):
|
||||
|
||||
from jituan.models import Club
|
||||
from jituan.services.fake_grab_switch import (
|
||||
get_fake_yajin_hide_threshold,
|
||||
is_club_fake_grab_enabled,
|
||||
set_club_fake_grab_enabled,
|
||||
)
|
||||
@@ -166,6 +172,33 @@ def set_fake_grab_enabled(request, enabled):
|
||||
return {
|
||||
'club_id': club_id,
|
||||
'fake_grab_enabled': is_club_fake_grab_enabled(club_id),
|
||||
'fake_yajin_hide_threshold': float(get_fake_yajin_hide_threshold(club_id)),
|
||||
}, None
|
||||
|
||||
|
||||
def set_fake_yajin_threshold(request, threshold):
|
||||
"""设置押金 ≥ 该值时过滤假单。"""
|
||||
if club_write_blocked(request):
|
||||
return None, CLUB_WRITE_SCOPE_MSG
|
||||
|
||||
from jituan.models import Club
|
||||
from jituan.services.fake_grab_switch import (
|
||||
get_fake_yajin_hide_threshold,
|
||||
is_club_fake_grab_enabled,
|
||||
set_fake_yajin_hide_threshold,
|
||||
)
|
||||
|
||||
club_id = club_id_for_write(request)
|
||||
club = Club.query.filter(club_id=club_id).first()
|
||||
if not club:
|
||||
return None, '俱乐部不存在'
|
||||
|
||||
val = set_fake_yajin_hide_threshold(club, threshold)
|
||||
club.save(update_fields=['config_json'])
|
||||
return {
|
||||
'club_id': club_id,
|
||||
'fake_grab_enabled': is_club_fake_grab_enabled(club_id),
|
||||
'fake_yajin_hide_threshold': float(val),
|
||||
}, None
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
"""俱乐部级假单功能开关(config_json.fake_grab_order_enabled)。"""
|
||||
"""俱乐部假单开关 + 押金过滤门槛(config_json)。"""
|
||||
import logging
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_KEY = 'fake_grab_order_enabled'
|
||||
YAJIN_THRESHOLD_KEY = 'fake_grab_yajin_hide_threshold'
|
||||
DEFAULT_YAJIN_HIDE_THRESHOLD = Decimal('10')
|
||||
|
||||
|
||||
def is_club_fake_grab_enabled(club_id: str) -> bool:
|
||||
@@ -34,8 +37,50 @@ def is_club_fake_grab_enabled(club_id: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def get_fake_yajin_hide_threshold(club_id: str) -> Decimal:
|
||||
"""
|
||||
押金 ≥ 该值时过滤假单(只看真单)。
|
||||
后台可配,默认 10。
|
||||
"""
|
||||
cid = (club_id or '').strip()
|
||||
if not cid:
|
||||
return DEFAULT_YAJIN_HIDE_THRESHOLD
|
||||
try:
|
||||
from jituan.models import Club
|
||||
club = Club.query.filter(club_id=cid).first()
|
||||
if not club:
|
||||
return DEFAULT_YAJIN_HIDE_THRESHOLD
|
||||
cfg = club.config_json or {}
|
||||
raw = cfg.get(YAJIN_THRESHOLD_KEY, DEFAULT_YAJIN_HIDE_THRESHOLD)
|
||||
try:
|
||||
val = Decimal(str(raw))
|
||||
except (InvalidOperation, TypeError, ValueError):
|
||||
return DEFAULT_YAJIN_HIDE_THRESHOLD
|
||||
if val < 0:
|
||||
return DEFAULT_YAJIN_HIDE_THRESHOLD
|
||||
return val
|
||||
except Exception:
|
||||
logger.exception('read fake_grab_yajin_hide_threshold failed club=%s', cid)
|
||||
return DEFAULT_YAJIN_HIDE_THRESHOLD
|
||||
|
||||
|
||||
def set_club_fake_grab_enabled(club, enabled: bool) -> None:
|
||||
"""写入 club.config_json,调用方负责 save。"""
|
||||
cfg = dict(getattr(club, 'config_json', None) or {})
|
||||
cfg[CONFIG_KEY] = bool(enabled)
|
||||
club.config_json = cfg
|
||||
|
||||
|
||||
def set_fake_yajin_hide_threshold(club, threshold) -> Decimal:
|
||||
"""写入押金过滤门槛,调用方负责 save。返回规范化后的值。"""
|
||||
try:
|
||||
val = Decimal(str(threshold))
|
||||
except (InvalidOperation, TypeError, ValueError):
|
||||
val = DEFAULT_YAJIN_HIDE_THRESHOLD
|
||||
if val < 0:
|
||||
val = DEFAULT_YAJIN_HIDE_THRESHOLD
|
||||
cfg = dict(getattr(club, 'config_json', None) or {})
|
||||
# 存成可 JSON 序列化的数字
|
||||
cfg[YAJIN_THRESHOLD_KEY] = float(val)
|
||||
club.config_json = cfg
|
||||
return val
|
||||
|
||||
@@ -60,6 +60,7 @@ class FakeGrabOrderManageView(APIView):
|
||||
create_fake_order,
|
||||
delete_fake_order,
|
||||
set_fake_grab_enabled,
|
||||
set_fake_yajin_threshold,
|
||||
update_fake_order,
|
||||
)
|
||||
|
||||
@@ -103,6 +104,17 @@ class FakeGrabOrderManageView(APIView):
|
||||
'data': data,
|
||||
})
|
||||
|
||||
if action in ('set_yajin_threshold', 'set_threshold'):
|
||||
raw = request.data.get('threshold', request.data.get('fake_yajin_hide_threshold'))
|
||||
data, err_msg = set_fake_yajin_threshold(request, raw)
|
||||
if err_msg:
|
||||
return Response({'code': 400, 'msg': err_msg})
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': f'押金过滤门槛已设为 ≥ {data.get("fake_yajin_hide_threshold")}',
|
||||
'data': data,
|
||||
})
|
||||
|
||||
return Response({'code': 400, 'msg': '无效的 action'})
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""抢单大厅假单:按俱乐部开关 + 按商品类型会员资格展示假数据。"""
|
||||
"""抢单大厅假单:按俱乐部开关混排/过滤。"""
|
||||
import logging
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import transaction
|
||||
@@ -11,7 +12,10 @@ 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 is_club_fake_grab_enabled
|
||||
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
|
||||
|
||||
@@ -20,12 +24,14 @@ 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)。
|
||||
required 为空表示类型未绑定会员 ID。
|
||||
"""
|
||||
"""返回 (required_huiyuan_id, yaoqiuleixing)。"""
|
||||
if leixing_id is None:
|
||||
return '', None
|
||||
try:
|
||||
@@ -44,77 +50,77 @@ def _leixing_membership_meta(leixing_id):
|
||||
return '', None
|
||||
|
||||
|
||||
def dashou_should_use_fake_pool(user, club_id=None, leixing_id=None):
|
||||
"""
|
||||
当前商品类型 Tab 是否只展示假单(结果必须稳定,同用户同类型每次请求一致):
|
||||
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
|
||||
|
||||
- 俱乐部未开启 → 真单
|
||||
- 已开启后:
|
||||
- 账号异常/封禁 → 假单
|
||||
- 类型绑定了会员 ID:有对应会员(含总会员覆盖子类型)且未过期 → 真单;否则 → 假单
|
||||
- 类型要求会员但未绑定 ID → 假单(无法核验资格时不开真池)
|
||||
- 类型不要求会员 → 真单
|
||||
不同类型互不影响。查库异常时 fail-closed:偏向假单,避免闪回真单。
|
||||
|
||||
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 False
|
||||
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 False
|
||||
return MODE_OFF
|
||||
if not enabled:
|
||||
return False
|
||||
return MODE_OFF
|
||||
|
||||
# ---------- 俱乐部假单已开启:默认假单,只有核验通过才出真单 ----------
|
||||
try:
|
||||
dashou = user.DashouProfile
|
||||
except ObjectDoesNotExist:
|
||||
return True
|
||||
return MODE_FAKE_ONLY
|
||||
except Exception:
|
||||
logger.exception('read DashouProfile failed uid=%s', getattr(user, 'UserUID', ''))
|
||||
return True
|
||||
return MODE_FAKE_ONLY
|
||||
|
||||
if int(getattr(dashou, 'zhanghaozhuangtai', 0) or 0) != 1:
|
||||
return True
|
||||
return MODE_FAKE_ONLY
|
||||
|
||||
if leixing_id is None:
|
||||
# 未带类型时无法判断「对应会员」,开启假单后不出真池
|
||||
return True
|
||||
if leixing_id is not None and _user_has_type_membership(user, cid, leixing_id):
|
||||
return MODE_REAL_ONLY
|
||||
|
||||
required, yaoqiu = _leixing_membership_meta(leixing_id)
|
||||
|
||||
if required:
|
||||
try:
|
||||
if user_has_huiyuan_access(user.UserUID, required, club_id=cid):
|
||||
return False # 有对应会员(或总会员含该子类型)→ 真单
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'user_has_huiyuan_access failed uid=%s required=%s club=%s',
|
||||
getattr(user, 'UserUID', ''), required, cid,
|
||||
)
|
||||
return True # 异常 → 假单,避免闪真单
|
||||
return True # 无会员 / 已过期 → 假单
|
||||
|
||||
# 未绑定会员 ID
|
||||
try:
|
||||
yaoqiu_int = int(yaoqiu) if yaoqiu is not None else 0
|
||||
except (TypeError, ValueError):
|
||||
yaoqiu_int = 0
|
||||
if yaoqiu_int == 1:
|
||||
# 要求会员却没配 ID:不能放行真单
|
||||
return True
|
||||
# 不要求会员(押金单等)→ 真单
|
||||
return False
|
||||
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
|
||||
@@ -255,6 +261,10 @@ def _format_fake_row(row, anchor_dt=None):
|
||||
'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 '',
|
||||
|
||||
@@ -98,8 +98,11 @@ class DashouDingdanHuoquView(APIView):
|
||||
def post(self, request):
|
||||
try:
|
||||
from orders.services.fake_order_pool import (
|
||||
dashou_should_use_fake_pool,
|
||||
MODE_FAKE_ONLY,
|
||||
MODE_MIX,
|
||||
build_fake_order_pool_response,
|
||||
list_formatted_fake_orders,
|
||||
resolve_fake_pool_mode,
|
||||
)
|
||||
|
||||
# 1. 获取请求参数
|
||||
@@ -116,8 +119,11 @@ class DashouDingdanHuoquView(APIView):
|
||||
)
|
||||
club_id = resolve_effective_club_id(request, getattr(request, 'user', None))
|
||||
|
||||
# 假单:按俱乐部开关 + 当前商品类型是否具备对应会员(含总会员覆盖)
|
||||
if dashou_should_use_fake_pool(request.user, club_id=club_id, leixing_id=leixing_id):
|
||||
# 假单模式:封禁只假单;未充会员且押金未达门槛 → 真假混排;有会员/高押金 → 只真单
|
||||
fake_mode = resolve_fake_pool_mode(
|
||||
request.user, club_id=club_id, leixing_id=leixing_id,
|
||||
)
|
||||
if fake_mode == MODE_FAKE_ONLY:
|
||||
return build_fake_order_pool_response(request)
|
||||
|
||||
yonghuid = request.user.UserUID
|
||||
@@ -125,6 +131,11 @@ class DashouDingdanHuoquView(APIView):
|
||||
yonghuid, club_id=club_id,
|
||||
)
|
||||
|
||||
fake_prefix = []
|
||||
if fake_mode == MODE_MIX:
|
||||
fake_prefix = list_formatted_fake_orders(club_id, leixing_id)
|
||||
fake_n = len(fake_prefix)
|
||||
|
||||
# 2. 构建基础查询条件:本店待抢单 + 集团配置的单向互通店
|
||||
from jituan.services.order_grab_share import order_club_q_for_grabber
|
||||
base_query = Q(Status__in=[1, 7]) & order_club_q_for_grabber(club_id)
|
||||
@@ -141,15 +152,30 @@ class DashouDingdanHuoquView(APIView):
|
||||
# 4. 构建基础查询集
|
||||
base_qs = Order.query.filter(base_query).order_by('-CreateTime').distinct()
|
||||
|
||||
# 5. 获取总记录数
|
||||
total = base_qs.count()
|
||||
# 5. 真单总数;混排时列表前半段是假单
|
||||
real_total = base_qs.count()
|
||||
total = fake_n + real_total
|
||||
|
||||
# 6. 计算分页偏移量
|
||||
# 6. 混排分页:前 fake_n 条为假单,之后为真单
|
||||
offset = (page - 1) * page_size
|
||||
limit = page_size
|
||||
page_fake = []
|
||||
real_offset = 0
|
||||
real_limit = page_size
|
||||
if fake_n > 0:
|
||||
if offset < fake_n:
|
||||
page_fake = fake_prefix[offset:offset + page_size]
|
||||
real_limit = page_size - len(page_fake)
|
||||
real_offset = 0
|
||||
else:
|
||||
page_fake = []
|
||||
real_offset = offset - fake_n
|
||||
real_limit = page_size
|
||||
|
||||
# 7. 获取当前页数据
|
||||
page_qs = base_qs[offset:offset + limit]
|
||||
# 7. 获取当前页真单
|
||||
if real_limit > 0:
|
||||
page_qs = base_qs[real_offset:real_offset + real_limit]
|
||||
else:
|
||||
page_qs = base_qs[0:0]
|
||||
|
||||
# 8. 使用子查询获取商家昵称(Subquery 须用 Django 原生 QuerySet,不能用 .query.values())
|
||||
shangjia_sub = MerchantOrderExt.objects.filter(
|
||||
@@ -381,6 +407,10 @@ class DashouDingdanHuoquView(APIView):
|
||||
row['shangjia_deal_stat'] = merchant_stat_map[sj_uid]
|
||||
formatted_list.append(row)
|
||||
|
||||
# 混排:假单在前(展示价格),真单在后(未开通会员时前端显示「未开通会员」)
|
||||
if page_fake:
|
||||
formatted_list = list(page_fake) + formatted_list
|
||||
|
||||
has_more = (page * page_size) < total
|
||||
|
||||
badge = {}
|
||||
@@ -399,9 +429,10 @@ class DashouDingdanHuoquView(APIView):
|
||||
'total': total,
|
||||
'current_page': page,
|
||||
'page_size': page_size,
|
||||
'total_pages': (total + page_size - 1) // page_size,
|
||||
'total_pages': (total + page_size - 1) // page_size if page_size else 0,
|
||||
'clumber': user_clumber,
|
||||
'is_fake_pool': False,
|
||||
'fake_pool_mode': fake_mode,
|
||||
**badge,
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user