From fc93f1c8fc8452c668c7014de0de57cf630b0dec Mon Sep 17 00:00:00 2001 From: XingQue Date: Thu, 16 Jul 2026 19:49:38 +0800 Subject: [PATCH] =?UTF-8?q?P0=E9=98=B2=E5=8F=8C=E7=AC=94=EF=BC=9A=E6=9F=A5?= =?UTF-8?q?=E4=B8=8D=E6=B8=85=E7=BB=9D=E4=B8=8D=E6=96=B0=E5=BB=BA=E7=AC=AC?= =?UTF-8?q?=E4=BA=8C=E7=AC=94=EF=BC=9B=E6=96=B0=E5=A2=9E=E6=8F=90=E7=8E=B0?= =?UTF-8?q?=E6=8A=BD=E6=88=90=E5=88=A9=E6=B6=A6=E7=BB=9F=E8=AE=A1=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- houtai/tixian_profit_views.py | 328 +++++++++++++++++++++++++++++++ houtai/urls.py | 2 + yonghu/tixian_shenhe_services.py | 91 ++++----- yonghu/tixian_shenhe_views.py | 18 +- 4 files changed, 374 insertions(+), 65 deletions(-) create mode 100644 houtai/tixian_profit_views.py diff --git a/houtai/tixian_profit_views.py b/houtai/tixian_profit_views.py new file mode 100644 index 0000000..5dc634a --- /dev/null +++ b/houtai/tixian_profit_views.py @@ -0,0 +1,328 @@ +""" +提现抽成利润统计(真实抽成 = 申请扣款额 − 实际到账额 = shouxufei) + +口径说明(务必与分账一致): +- 仅统计「提现成功」记录:Tixianjilu.zhuangtai = 2 +- 利润 = COALESCE(shouxufei, max(shenqing_jine - jine, 0)) +- 手动/自动一并计入(dakuan_mode 可筛) +- 日期维度默认按 update_time(成功落账日);可按 create_time(申请日) +- 不含会员/押金充值/商家充值等,那些不是抽成利润 +""" +import logging +from datetime import datetime, time +from decimal import Decimal + +from django.db.models import ( + Count, DecimalField, F, Q, Sum, Value, +) +from django.db.models.functions import Coalesce, Greatest, TruncDate, TruncMonth +from django.utils import timezone +from rest_framework.parsers import JSONParser +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from yonghu.models import Tixianjilu + +from .view import verify_kefu_permission + +logger = logging.getLogger(__name__) + +_LEIXING_LABEL = { + 1: '打手佣金', + 2: '管事分红', + 3: '组长分红', + 4: '审核官分佣', + 5: '打手押金', + 6: '商家余额', +} + +_FEE_EXPR = Coalesce( + F('shouxufei'), + Greatest( + Coalesce(F('shenqing_jine'), Value(Decimal('0.00'))) + - Coalesce(F('jine'), Value(Decimal('0.00'))), + Value(Decimal('0.00')), + ), + Value(Decimal('0.00')), + output_field=DecimalField(max_digits=12, decimal_places=2), +) + + +def _parse_date(s): + if not s: + return None + try: + return datetime.strptime(str(s).strip()[:10], '%Y-%m-%d').date() + except (TypeError, ValueError): + return None + + +def _day_bounds(d): + """本地日历日 → aware datetime 区间 [start, end)""" + start = datetime.combine(d, time.min) + end = datetime.combine(d, time.max) + if timezone.is_naive(start): + tz = timezone.get_current_timezone() + start = timezone.make_aware(start, tz) + end = timezone.make_aware(end, tz) + return start, end + + +class TixianChouchengProfitView(APIView): + """ + POST /houtai/txchouchengtj + 提现抽成利润统计(分账专用口径) + + 参数: + phone: 客服手机号 + date_from / date_to: YYYY-MM-DD(闭区间,按成功日或申请日) + date_field: update_time(默认,成功日) | create_time(申请日) + leixing: 1~6 可选;不传=全部身份 + dakuan_mode: 1手动 2自动 可选;不传=全部 + group_by: day | month | leixing | none(默认 day) + page / page_size: 明细分页(可选,仅 group_by=none 时返回 list) + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + phone = (request.data.get('phone') or request.data.get('username') or '').strip() + kefu, permissions = verify_kefu_permission(request, phone) + if kefu is None: + return permissions + if 'caiwu' not in permissions and not any( + p in permissions for p in ('5500a', '5500b', '5500c') + ): + return Response({'code': 403, 'msg': '无提现抽成利润查看权限'}) + + date_from = _parse_date(request.data.get('date_from') or request.data.get('start_date')) + date_to = _parse_date(request.data.get('date_to') or request.data.get('end_date')) + if not date_from or not date_to: + today = timezone.localdate() + date_from = date_from or today + date_to = date_to or today + if date_from > date_to: + return Response({'code': 400, 'msg': 'date_from 不能晚于 date_to'}) + + date_field = (request.data.get('date_field') or 'update_time').strip() + if date_field not in ('update_time', 'create_time'): + return Response({'code': 400, 'msg': 'date_field 仅支持 update_time / create_time'}) + + group_by = (request.data.get('group_by') or 'day').strip() + if group_by not in ('day', 'month', 'leixing', 'none'): + return Response({'code': 400, 'msg': 'group_by 仅支持 day/month/leixing/none'}) + + leixing_val = None + raw_leixing = request.data.get('leixing') + if raw_leixing is not None and str(raw_leixing).strip() != '': + try: + leixing_val = int(raw_leixing) + except (TypeError, ValueError): + return Response({'code': 400, 'msg': 'leixing 无效'}) + if leixing_val not in (1, 2, 3, 4, 5, 6): + return Response({'code': 400, 'msg': 'leixing 仅支持 1~6'}) + + dakuan_mode_val = None + raw_mode = request.data.get('dakuan_mode') + if raw_mode is not None and str(raw_mode).strip() != '': + try: + dakuan_mode_val = int(raw_mode) + except (TypeError, ValueError): + return Response({'code': 400, 'msg': 'dakuan_mode 无效'}) + if dakuan_mode_val not in (1, 2): + return Response({'code': 400, 'msg': 'dakuan_mode 仅支持 1/2'}) + + start_dt, _ = _day_bounds(date_from) + _, end_dt = _day_bounds(date_to) + + # 仅提现成功;手动+自动 + q = Q(zhuangtai=2) + if date_field == 'create_time': + q &= Q(create_time__gte=start_dt, create_time__lte=end_dt) + else: + q &= Q(update_time__gte=start_dt, update_time__lte=end_dt) + if leixing_val is not None: + q &= Q(leixing=leixing_val) + if dakuan_mode_val is not None: + q &= Q(dakuan_mode=dakuan_mode_val) + + qs = Tixianjilu.objects.filter(q) + annotated = qs.annotate(fee=_FEE_EXPR) + + total_agg = annotated.aggregate( + fee_sum=Sum('fee'), + apply_sum=Sum(Coalesce(F('shenqing_jine'), F('jine'), Value(Decimal('0.00')))), + payout_sum=Sum(Coalesce(F('jine'), Value(Decimal('0.00')))), + cnt=Count('id'), + ) + fee_total = total_agg.get('fee_sum') or Decimal('0.00') + apply_total = total_agg.get('apply_sum') or Decimal('0.00') + payout_total = total_agg.get('payout_sum') or Decimal('0.00') + # 交叉校验:申请 − 到账 应约等于抽成(允许历史脏数据偏差) + cross_check = (apply_total - payout_total).quantize(Decimal('0.01')) + + summary = { + 'profit_total': str(fee_total.quantize(Decimal('0.01'))), + 'apply_total': str(apply_total.quantize(Decimal('0.01'))), + 'payout_total': str(payout_total.quantize(Decimal('0.01'))), + 'apply_minus_payout': str(cross_check), + 'success_count': int(total_agg.get('cnt') or 0), + 'date_from': date_from.isoformat(), + 'date_to': date_to.isoformat(), + 'date_field': date_field, + 'leixing': leixing_val, + 'leixing_label': _LEIXING_LABEL.get(leixing_val, '全部身份'), + 'dakuan_mode': dakuan_mode_val, + 'formula': '利润=申请扣款额(shenqing_jine)−实际到账(jine)=手续费(shouxufei);仅统计提现成功(zhuangtai=2)', + } + + rows = [] + if group_by == 'day': + trunc = TruncDate(date_field) + grouped = ( + annotated.annotate(period=trunc) + .values('period') + .annotate( + profit=Sum('fee'), + apply_amount=Sum(Coalesce(F('shenqing_jine'), F('jine'), Value(Decimal('0.00')))), + payout_amount=Sum(Coalesce(F('jine'), Value(Decimal('0.00')))), + count=Count('id'), + ) + .order_by('period') + ) + for g in grouped: + period = g['period'] + rows.append({ + 'period': period.isoformat() if period else '', + 'profit': str((g['profit'] or Decimal('0')).quantize(Decimal('0.01'))), + 'apply_amount': str((g['apply_amount'] or Decimal('0')).quantize(Decimal('0.01'))), + 'payout_amount': str((g['payout_amount'] or Decimal('0')).quantize(Decimal('0.01'))), + 'count': g['count'] or 0, + }) + elif group_by == 'month': + trunc = TruncMonth(date_field) + grouped = ( + annotated.annotate(period=trunc) + .values('period') + .annotate( + profit=Sum('fee'), + apply_amount=Sum(Coalesce(F('shenqing_jine'), F('jine'), Value(Decimal('0.00')))), + payout_amount=Sum(Coalesce(F('jine'), Value(Decimal('0.00')))), + count=Count('id'), + ) + .order_by('period') + ) + for g in grouped: + period = g['period'] + rows.append({ + 'period': period.strftime('%Y-%m') if period else '', + 'profit': str((g['profit'] or Decimal('0')).quantize(Decimal('0.01'))), + 'apply_amount': str((g['apply_amount'] or Decimal('0')).quantize(Decimal('0.01'))), + 'payout_amount': str((g['payout_amount'] or Decimal('0')).quantize(Decimal('0.01'))), + 'count': g['count'] or 0, + }) + elif group_by == 'leixing': + grouped = ( + annotated.values('leixing') + .annotate( + profit=Sum('fee'), + apply_amount=Sum(Coalesce(F('shenqing_jine'), F('jine'), Value(Decimal('0.00')))), + payout_amount=Sum(Coalesce(F('jine'), Value(Decimal('0.00')))), + count=Count('id'), + ) + .order_by('leixing') + ) + for g in grouped: + lx = g['leixing'] + rows.append({ + 'leixing': lx, + 'leixing_label': _LEIXING_LABEL.get(lx, f'类型{lx}'), + 'profit': str((g['profit'] or Decimal('0')).quantize(Decimal('0.01'))), + 'apply_amount': str((g['apply_amount'] or Decimal('0')).quantize(Decimal('0.01'))), + 'payout_amount': str((g['payout_amount'] or Decimal('0')).quantize(Decimal('0.01'))), + 'count': g['count'] or 0, + }) + else: + # 明细 + try: + page = max(1, int(request.data.get('page') or 1)) + page_size = min(200, max(1, int(request.data.get('page_size') or 50))) + except (TypeError, ValueError): + page, page_size = 1, 50 + total = annotated.count() + items = list( + annotated.order_by(f'-{date_field}', '-id') + [(page - 1) * page_size: page * page_size] + .values( + 'id', 'yonghuid', 'leixing', 'dakuan_mode', + 'shenqing_jine', 'jine', 'shouxufei', 'fee', + 'create_time', 'update_time', 'shenhe_danhao', + ) + ) + for it in items: + fee = it.get('fee') or Decimal('0') + rows.append({ + 'id': it['id'], + 'yonghuid': it['yonghuid'], + 'leixing': it['leixing'], + 'leixing_label': _LEIXING_LABEL.get(it['leixing'], ''), + 'dakuan_mode': it['dakuan_mode'], + 'dakuan_mode_label': '自动' if it['dakuan_mode'] == 2 else '手动', + 'shenqing_jine': str(it['shenqing_jine'] or '0.00'), + 'shijidaozhang': str(it['jine'] or '0.00'), + 'shouxufei': str(it['shouxufei'] if it['shouxufei'] is not None else fee), + 'profit': str(Decimal(fee).quantize(Decimal('0.01'))), + 'shenhe_danhao': it.get('shenhe_danhao') or '', + 'create_time': it['create_time'].strftime('%Y-%m-%d %H:%M:%S') if it.get('create_time') else '', + 'update_time': it['update_time'].strftime('%Y-%m-%d %H:%M:%S') if it.get('update_time') else '', + }) + return Response({ + 'code': 0, + 'data': { + 'summary': summary, + 'group_by': group_by, + 'list': rows, + 'total': total, + 'page': page, + 'page_size': page_size, + }, + }) + + # 按身份拆分汇总(便于分账核对,始终附带) + by_leixing = [] + for g in ( + annotated.values('leixing') + .annotate(profit=Sum('fee'), count=Count('id')) + .order_by('leixing') + ): + by_leixing.append({ + 'leixing': g['leixing'], + 'leixing_label': _LEIXING_LABEL.get(g['leixing'], ''), + 'profit': str((g['profit'] or Decimal('0')).quantize(Decimal('0.01'))), + 'count': g['count'] or 0, + }) + + by_mode = [] + for g in ( + annotated.values('dakuan_mode') + .annotate(profit=Sum('fee'), count=Count('id')) + .order_by('dakuan_mode') + ): + by_mode.append({ + 'dakuan_mode': g['dakuan_mode'], + 'dakuan_mode_label': '自动' if g['dakuan_mode'] == 2 else '手动', + 'profit': str((g['profit'] or Decimal('0')).quantize(Decimal('0.01'))), + 'count': g['count'] or 0, + }) + + return Response({ + 'code': 0, + 'data': { + 'summary': summary, + 'group_by': group_by, + 'rows': rows, + 'by_leixing': by_leixing, + 'by_dakuan_mode': by_mode, + }, + }) diff --git a/houtai/urls.py b/houtai/urls.py index 2032f86..15c0a30 100644 --- a/houtai/urls.py +++ b/houtai/urls.py @@ -18,6 +18,7 @@ from .view import GetClubConfigView, GetCrossOrderListView, PartnerGetOrderDataV CaiwuView, CwhybkhqView, HybkjtsjView, SzxxView, CwddhqlxView, CwhqjtddsjView, CwqtczhqView, KhpzhqView, ChzsgcView, \ KhgglView, ShgxgsjView, ZxkfghdsView, KptxwztjbView from .dongjie_views import DongjieUserConfigView +from .tixian_profit_views import TixianChouchengProfitView from .huashu_views import HuashuListAPIView, HuashuModifyAPIView from .xieyi_views import ( XieyiListAPIView, XieyiModifyAPIView, XieyiImageUploadAPIView, @@ -120,6 +121,7 @@ urlpatterns = [ path('bkxg', BkxgView.as_view(), name='板块增删改查'), # 板块增删改 path('caiwu', CaiwuView.as_view(), name='财务页面基础数据'), + path('txchouchengtj', TixianChouchengProfitView.as_view(), name='提现抽成利润统计'), path('cwhybkhq', CwhybkhqView.as_view(), name='财务会员模块'), path('hybkjtsj', HybkjtsjView.as_view(), name='财务会员统计'), diff --git a/yonghu/tixian_shenhe_services.py b/yonghu/tixian_shenhe_services.py index 3526f9b..e4dec73 100644 --- a/yonghu/tixian_shenhe_services.py +++ b/yonghu/tixian_shenhe_services.py @@ -78,7 +78,7 @@ RECONCILE_WAIT_CONFIRM = 'wait_confirm' RECONCILE_ALLOW_NEW = 'allow_new' RECONCILE_PENDING = 'pending_query' -# 查单失败且本地 zhuangtai=0 超过该分钟数,才允许新建打款记录 +# 历史兼容:宽限分钟数常量仍保留(对账已改为「有 package 查无单也不新建」) PENDING_QUERY_GRACE_MINUTES = 10 @@ -1280,17 +1280,11 @@ def mark_transfer_success(auto_record, *, wechat_transfer_no=None, with_accounti def _sync_record_from_wx_query(auto_record, wx_data): """根据微信查单结果修正本地打款记录,返回 (action, payload)""" if wx_data.get('_not_found'): - # 从未拿到 package = 未真正进入待确认,查无单应立刻放行换号/重发, - # 不要卡 10 分钟宽限期(否则首户日额度失败后重试永远进不了多商户轮换)。 + # 微信明确无此单。若本地曾有 package,可能查单串户/短暂异常 → 禁止新建第二笔 has_package = bool(_parse_package_info(auto_record.wechat_package)) - age = timezone.now() - auto_record.create_time - if ( - auto_record.zhuangtai == 0 - and has_package - and age < timedelta(minutes=PENDING_QUERY_GRACE_MINUTES) - ): + if has_package: return RECONCILE_PENDING, { - 'msg': '有收款处理中,请稍后再试', + 'msg': '本地已有收款凭证但微信暂查无单,请稍后再试,切勿重复点击', 'tixian_id': auto_record.tixian_id, } if auto_record.zhuangtai == 0: @@ -1358,12 +1352,11 @@ def _sync_record_from_wx_query(auto_record, wx_data): release_collect_quota_for_record(auto_record) return RECONCILE_ALLOW_NEW, {} - if auto_record.zhuangtai == 0: - return RECONCILE_PENDING, { - 'msg': f'收款处理中(微信状态:{state or "未知"}),请稍后再试', - 'tixian_id': auto_record.tixian_id, - } - return RECONCILE_ALLOW_NEW, {} + # 未知状态:宁可卡住,绝不放行新建第二笔 + return RECONCILE_PENDING, { + 'msg': f'收款处理中(微信状态:{state or "未知"}),请稍后再试', + 'tixian_id': auto_record.tixian_id, + } def get_auto_record_for_collect(shenhe_danhao): @@ -1394,10 +1387,11 @@ def _mark_auto_record_failed_local(rec, reason): def reconcile_shenhe_wechat_bills(shenhe_danhao): """ - P0 防双笔:同一 shenhe_danhao 下所有打款记录逐条问微信 + P0 防双笔(宁可提不出也不开第二笔): - SUCCESS → completed - - 真正待确认(有 package) → wait_confirm(禁止换户) - - 其余(无 package / 限额失败 / IP / 查单失败)→ 标失败,ALLOW_NEW 以便换商户 + - WAIT_USER_CONFIRM(有 package)→ wait_confirm + - ACCEPTED / PROCESSING / CANCELING / 查单失败(None) → PENDING,禁止新建 + - 仅微信明确 FAIL / 查无单(_not_found) → 可 ALLOW_NEW """ records = list( TixianAutoRecord.objects.filter(shenhe_danhao=shenhe_danhao).order_by('-create_time') @@ -1410,7 +1404,6 @@ def reconcile_shenhe_wechat_bills(shenhe_danhao): if not records: return RECONCILE_ALLOW_NEW, {} - pending_with_package = None for rec in records: if rec.zhuangtai == 1: ok, msg = ensure_shenhe_collect_completed(shenhe_danhao) @@ -1418,64 +1411,48 @@ def reconcile_shenhe_wechat_bills(shenhe_danhao): local_pkg = _parse_package_info(rec.wechat_package) - # 本地已有收款包:必须先对齐数字,禁止为换号新建第二笔待确认 - if rec.zhuangtai == 0 and local_pkg: + # 进行中记录:必须先对齐微信,查不清绝不新建 + if rec.zhuangtai == 0: wx_data = query_wechat_transfer_bill(rec.tixian_id) if wx_data is None: + logger.error( + '【防双笔】查单失败禁止新建 shenhe=%s bill=%s mch=%s has_pkg=%s', + shenhe_danhao, rec.tixian_id, rec.mch_id, bool(local_pkg), + ) return RECONCILE_PENDING, { - 'msg': '有收款处理中,系统正在核对微信状态,请稍后再试', + 'msg': '有收款处理中,系统正在核对微信状态,请稍后再试,切勿重复点击', 'tixian_id': rec.tixian_id, } action, payload = _sync_record_from_wx_query(rec, wx_data) if action in (RECONCILE_COMPLETED, RECONCILE_WAIT_CONFIRM, RECONCILE_PENDING): return action, payload + # ALLOW_NEW:本条已明确失败(查无单或 FAIL),继续看其它记录 continue - # 无 package:商户侧基本未进入待确认。限额/IP/卡单都不能挡住换号。 - if rec.zhuangtai == 0 and not local_pkg: - wx_data = query_wechat_transfer_bill(rec.tixian_id) - if wx_data is None or wx_data.get('_not_found'): - _mark_auto_record_failed_local( - rec, '未进入待确认(查无单或查单失败),已换商户重试', - ) - continue - action, payload = _sync_record_from_wx_query(rec, wx_data) - if action == RECONCILE_COMPLETED: - return action, payload - if action == RECONCILE_WAIT_CONFIRM: - return action, payload - # PENDING / ALLOW_NEW:若仍无 package,标失败继续换号 - if action == RECONCILE_PENDING: - pkg = ( - (payload or {}).get('package_info') - if isinstance(payload, dict) else None - ) or wx_data.get('package_info') - if pkg: - if pending_with_package is None: - pending_with_package = (action, payload) - continue - _mark_auto_record_failed_local( - rec, - f'微信不可用状态:{_wx_transfer_state(wx_data) or "未知"},已换商户重试', - ) - continue - - # 已失败记录:仍问微信。若仍待确认必须回旧 package,禁止新建第二笔(防双收) + # 本地已失败:微信若仍有效/处理中,禁止新建第二笔 if rec.zhuangtai == 2: wx_data = query_wechat_transfer_bill(rec.tixian_id) - if wx_data is None or wx_data.get('_not_found'): + if wx_data is None: + logger.error( + '【防双笔】失败单查单不明禁止新建 shenhe=%s bill=%s mch=%s', + shenhe_danhao, rec.tixian_id, rec.mch_id, + ) + return RECONCILE_PENDING, { + 'msg': '有历史收款单状态核对中,请稍后再试,切勿重复点击', + 'tixian_id': rec.tixian_id, + } + if wx_data.get('_not_found'): continue action, payload = _sync_record_from_wx_query(rec, wx_data) - if action in (RECONCILE_COMPLETED, RECONCILE_WAIT_CONFIRM): + if action in (RECONCILE_COMPLETED, RECONCILE_WAIT_CONFIRM, RECONCILE_PENDING): logger.warning( '本地失败单微信仍有效 shenhe=%s bill=%s mch=%s action=%s(禁止新建第二笔)', shenhe_danhao, rec.tixian_id, rec.mch_id, action, ) return action, payload + # ALLOW_NEW:微信确认终态失败,可继续 continue - if pending_with_package: - return pending_with_package return RECONCILE_ALLOW_NEW, {} diff --git a/yonghu/tixian_shenhe_views.py b/yonghu/tixian_shenhe_views.py index f4714bf..25009e9 100644 --- a/yonghu/tixian_shenhe_views.py +++ b/yonghu/tixian_shenhe_views.py @@ -421,17 +421,19 @@ def process_audit_collect(request): # 选户:绑定用户仅 1 户;未绑定按 priority。 # 仅「商户整户日/月额度」+ 查单确认未建单 才静默换号;其它失败立刻回前端。 + # 跨次仅跳过「整户额度」失败过的商户,个人限额/IP/余额失败不跨次换号。 if not is_user_bound and len(mch_cfgs) > 1: - failed_mchs = set( - TixianAutoRecord.objects.filter( - shenhe_danhao=shenhe_danhao, zhuangtai=2, - ).exclude(mch_id='').values_list('mch_id', flat=True) - ) - prefer_cfgs = [c for c in mch_cfgs if c.get('MCHID') not in failed_mchs] + failed_quota_mchs = set() + for fr, mid in TixianAutoRecord.objects.filter( + shenhe_danhao=shenhe_danhao, zhuangtai=2, + ).exclude(mch_id='').values_list('fail_reason', 'mch_id'): + if mid and _is_wx_mch_total_quota_exhausted('', fr or ''): + failed_quota_mchs.add(mid) + prefer_cfgs = [c for c in mch_cfgs if c.get('MCHID') not in failed_quota_mchs] if prefer_cfgs and len(prefer_cfgs) < len(mch_cfgs): logger.warning( - '收款优先未失败商户 shenhe=%s skip=%s try=%s', - shenhe_danhao, sorted(failed_mchs), + '收款优先未触整户额度商户 shenhe=%s skip=%s try=%s', + shenhe_danhao, sorted(failed_quota_mchs), [c.get('MCHID') for c in prefer_cfgs], ) mch_cfgs = prefer_cfgs