diff --git a/jituan/services/fake_grab_order_admin.py b/jituan/services/fake_grab_order_admin.py index b98bd4b..9f09472 100644 --- a/jituan/services/fake_grab_order_admin.py +++ b/jituan/services/fake_grab_order_admin.py @@ -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 diff --git a/jituan/services/fake_grab_switch.py b/jituan/services/fake_grab_switch.py index 458984c..ac9ba8b 100644 --- a/jituan/services/fake_grab_switch.py +++ b/jituan/services/fake_grab_switch.py @@ -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 diff --git a/jituan/views_fake_grab_order.py b/jituan/views_fake_grab_order.py index de257ee..8a56bba 100644 --- a/jituan/views_fake_grab_order.py +++ b/jituan/views_fake_grab_order.py @@ -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'}) diff --git a/orders/services/fake_order_pool.py b/orders/services/fake_order_pool.py index c39f9ca..4dffecb 100644 --- a/orders/services/fake_order_pool.py +++ b/orders/services/fake_order_pool.py @@ -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 '', diff --git a/orders/views/dashou.py b/orders/views/dashou.py index 85b5c8f..4f35f68 100644 --- a/orders/views/dashou.py +++ b/orders/views/dashou.py @@ -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, } })