diff --git a/config/views/miniapp_config.py b/config/views/miniapp_config.py index b9a0ad6..5e92785 100644 --- a/config/views/miniapp_config.py +++ b/config/views/miniapp_config.py @@ -1,348 +1,346 @@ -import io -import os -import re -import time -import random -import secrets -import hashlib -import threading -import urllib.parse -from decimal import Decimal, InvalidOperation - -from django.conf import settings -from django.db import transaction, connection -from django.db.models import Q, F, Max, Prefetch -from gvsdsdk.fluent import db, func, FQ -from django.core.cache import cache -from django.core.paginator import Paginator -from django.utils import timezone - -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status, permissions -from rest_framework.permissions import AllowAny, IsAuthenticated -from rest_framework.parsers import JSONParser, MultiPartParser, FormParser -from rest_framework.throttling import AnonRateThrottle, SimpleRateThrottle - -from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image -from utils.weixin_broadcast import WeixinBroadcastSender -from utils.weixin_token import get_weixin_mini_access_token, is_weixin_token_invalid -# from utils.ip_security import * -from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode - -from backend.utils import update_shangjia_daily -from utils.chat_utils import subscribe_merchant_link_chat -from utils.pdd_order_validator import validate_pdd_order_id, validate_cn_mobile - -from ..models import ( - Gonggao, Lunbo, Tupianpeizhi, Qunpeizhi, - ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig, - MiniappScriptScene, MiniappScriptAutoReply, -) - -from users.models import ( - AdminProfile, UserDashou, UserBoss, - UserShangjia, UserGuanshi, UserZuzhang -) -from users.business_models import User - -from orders.models import CommissionRate, Order, MerchantOrderExt -from orders.notice_tasks import dingdan_guangbo - -from rank.models import Chenghao, DingdanBiaoqian - -from products.models import ShangpinLeixing, Huiyuangoumai - -from ..serializers import PopupConfigSerializer -import traceback -import requests - -import logging -logger = logging.getLogger(__name__) - -class GetDynamicConfigView(APIView): - permission_classes = [AllowAny] - parser_classes = [JSONParser] - - def post(self, request): - try: - from jituan.services.display_config import get_tupianpeizhi_url - - from jituan.services.miniapp_assets import build_miniapp_assets_payload - - cos_oss_url = getattr(settings, 'COS_DOMAIN', '') - if cos_oss_url and not cos_oss_url.endswith('/'): - cos_oss_url += '/' - cos_bucket = getattr(settings, 'COS_BUCKET', '') - cos_region = getattr(settings, 'COS_REGION', 'ap-shanghai') - goeasy_host = 'hangzhou.goeasy.io' - goeasy_appkey = getattr(settings, 'GOEASY_APPKEY', '') - kefu_link = getattr(settings, 'KEFU_LINK', '') - kefu_enterprise_id = getattr(settings, 'KEFU_ENTERPRISE_ID', '') - rule_img = get_tupianpeizhi_url(request, 1) - avatar_img = get_tupianpeizhi_url(request, 2) - morentouxiang = avatar_img or 'beijing/morentouxiang.jpg' - dashouguize = rule_img or 'a_long/dashouguize.jpg' - - # 构造返回数据 - data = { - "code": 0, - "data": { - "cos": { - "bucket": cos_bucket, - "region": cos_region, - "ossImageUrl": cos_oss_url, - "uploadPathPrefix": "order/" - }, - "goEasy": { - "host": goeasy_host, - "appkey": goeasy_appkey - }, - "otherConfig": { - "morentouxiang": morentouxiang, - "dashouguize": dashouguize - }, - "kefu": { - "link": kefu_link, - "enterpriseId": kefu_enterprise_id - }, - "miniappAssets": build_miniapp_assets_payload(request), - } - } - return Response(data) - - except Exception as e: - logger.error(f"获取动态配置失败: {str(e)}", exc_info=True) - return Response({"code": 500, "msg": "服务器内部错误"}, status=500) - - -class HaibaoPeizhiView(APIView): - """小程序推广海报背景配置(按俱乐部)。""" - permission_classes = [AllowAny] - parser_classes = [JSONParser] - - def post(self, request): - try: - from jituan.services.display_config import _display_club_id - from jituan.services.miniapp_assets import get_poster_payload - - club_id = _display_club_id(request) - poster = get_poster_payload(club_id) - return Response({ - 'code': 0, - 'msg': 'success', - 'data': { - 'club_id': club_id, - 'poster': poster, - }, - }) - except Exception as e: - logger.error('获取海报配置失败: %s', e, exc_info=True) - return Response({'code': 500, 'msg': '服务器内部错误'}, status=500) - - - - -class PopupConfigView(APIView): - """ - 获取指定页面的弹窗配置 - 请求格式:POST { "pageKey": "dashouzhongxin" } - 返回格式:{ "code": 0, "data": { "serverTime": "...", "popups": [...] } } - """ - authentication_classes = [] - permission_classes = [AllowAny] - - def post(self, request): - # 获取参数 pageKey - page_key = request.data.get('pageKey') - if not page_key: - return Response({ - 'code': 400, - 'msg': '缺少参数 pageKey' - }, status=status.HTTP_400_BAD_REQUEST) - - # 3. 查询页面配置(按俱乐部) - from jituan.services.display_config import get_gonggao_content - from jituan.services.club_context import resolve_club_id_from_request - - club_id = resolve_club_id_from_request(request) - try: - popup_page = PopupPage.query.get(club_id=club_id, page_key=page_key, is_active=True) - except PopupPage.DoesNotExist: - # 该页面没有配置弹窗,返回空 - return Response({ - 'code': 0, - 'data': { - 'serverTime': timezone.now().isoformat(), - 'popups': [] - } - }) - - # 4. 查询该页面下所有启用的弹窗(且在有效时间范围内) - # start/end 为空视为不限制(避免后台「配了却永远查不到」) - from django.db.models import Q - - now = timezone.now() - popups = PopupConfig.query.filter( - page=popup_page, - is_active=True, - ).filter( - Q(start_time__isnull=True) | Q(start_time__lte=now), - Q(end_time__isnull=True) | Q(end_time__gte=now), - ).prefetch_related('images') # 预加载图片,避免N+1查询 - # 按 sort_order 排序(model Meta 已定义) - - # 5. 序列化 - serializer = PopupConfigSerializer(popups, many=True) - - # 6. 返回数据 - return Response({ - 'code': 0, - 'data': { - 'serverTime': now.isoformat(), - 'ignore_user_mute': popup_page.ignore_user_mute, # 新增 - 'popups': serializer.data - } - }) - - - - -class GetWithdrawModeView(APIView): - """ - 获取提现模式配置 - 前端请求 /peizhi/hqtxzsym - 返回格式: { "code": 0, "data": { "mode": 1 or 2 } } - """ - permission_classes = [IsAuthenticated] # JWT 认证 - - def post(self, request, *args, **kwargs): - from jituan.services.club_context import resolve_club_id_from_request - from jituan.services.club_user import get_user_club_id - from jituan.services.withdraw_config import get_withdraw_mode - - user = getattr(request, 'user', None) - club_id = get_user_club_id(user) if user and getattr(user, 'is_authenticated', False) else None - if not club_id: - club_id = resolve_club_id_from_request(request) - mode = get_withdraw_mode(club_id) - - return Response({ - 'code': 0, - 'data': { - 'mode': mode, - 'club_id': club_id, - } - }) - - - - - -class CheckPhoneAuthView(APIView): - """ - 判断当前登录用户是否需要强制手机号认证 - - 规则: - - 已认证过手机号(PhoneVerified=True) → 不需要 - - 未认证时,满足以下任一条件 → 需要认证: - 打手:有会员购买记录,或押金 > 0 - 商家:已是商家(存在商家扩展表) - 管事:已是管事(存在管事扩展表) - 组长:已是组长(存在组长扩展表) - 老板:有下单记录(alldingdan > 0) - 子客服:已是有效商家客服(merchant_staff_member 活跃) - """ - - permission_classes = [IsAuthenticated] - - @staticmethod - def _phone_auth_completed(user): - """是否已完成手机号认证(兼容 PhoneVerified 未写入的老数据)。""" - if bool(getattr(user, 'PhoneVerified', False)): - return True - phone = (getattr(user, 'Phone', None) or '').strip() - return len(phone) >= 11 - - def post(self, request): - uid = getattr(request.user, 'UserUID', None) - user = request.user - if uid: - try: - user = User.query.get(UserUID=uid) - except User.DoesNotExist: - user = request.user - - # 俱乐部关闭强制收集手机号 → 前端 ensurePhoneAuth 不再跳转认证页 - try: - from jituan.services.club_context import resolve_effective_club_id - from jituan.services.force_phone_auth import is_club_force_phone_auth - club_id = resolve_effective_club_id(request, user) - if not is_club_force_phone_auth(club_id): - return Response({'code': 0, 'need_auth': False}) - except Exception: - pass - - if self._phone_auth_completed(user): - return Response({'code': 0, 'need_auth': False}) - - if self._dashou_meets_threshold(user): - return Response({'code': 0, 'need_auth': True}) - if self._shangjia_meets_threshold(user): - return Response({'code': 0, 'need_auth': True}) - if self._guanshi_meets_threshold(user): - return Response({'code': 0, 'need_auth': True}) - if self._zuzhang_meets_threshold(user): - return Response({'code': 0, 'need_auth': True}) - if self._boss_meets_threshold(user): - return Response({'code': 0, 'need_auth': True}) - if self._staff_meets_threshold(user): - return Response({'code': 0, 'need_auth': True}) - - return Response({'code': 0, 'need_auth': False}) - - def _staff_meets_threshold(self, user): - """子客服:已是有效商家客服""" - try: - from merchant_ops.services.authz import get_active_staff_member - return get_active_staff_member(user) is not None - except Exception: - return False - - def _dashou_meets_threshold(self, user): - """打手:有会员记录、押金 > 0、或有过接单/成交订单""" - try: - dashou = UserDashou.query.get(user=user) - except UserDashou.DoesNotExist: - return False - if dashou.yajin and dashou.yajin > 0: - return True - from jituan.services.club_context import resolve_effective_club_id - club_id = resolve_effective_club_id(self.request, user) - if Huiyuangoumai.query.filter(yonghu_id=user.UserUID, club_id=club_id).exists(): - return True - if (dashou.jiedanzongliang or 0) > 0 or (dashou.chengjiaozongliang or 0) > 0: - return True - return Order.query.filter(PlayerID=user.UserUID).exists() - - def _shangjia_meets_threshold(self, user): - """商家:已是商家身份""" - return UserShangjia.query.filter(user=user).exists() - - def _guanshi_meets_threshold(self, user): - """管事:已是管事身份""" - return UserGuanshi.query.filter(user=user).exists() - - def _zuzhang_meets_threshold(self, user): - """组长:已是组长身份""" - return UserZuzhang.query.filter(user=user).exists() - - def _boss_meets_threshold(self, user): - """老板:有下单记录""" - try: - boss = UserBoss.query.get(user=user) - except UserBoss.DoesNotExist: - return False - return (boss.alldingdan or 0) > 0 - - +import io +import os +import re +import time +import random +import secrets +import hashlib +import threading +import urllib.parse +from decimal import Decimal, InvalidOperation + +from django.conf import settings +from django.db import transaction, connection +from django.db.models import Q, F, Max, Prefetch +from gvsdsdk.fluent import db, func, FQ +from django.core.cache import cache +from django.core.paginator import Paginator +from django.utils import timezone + +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status, permissions +from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.parsers import JSONParser, MultiPartParser, FormParser +from rest_framework.throttling import AnonRateThrottle, SimpleRateThrottle + +from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image +from utils.weixin_broadcast import WeixinBroadcastSender +from utils.weixin_token import get_weixin_mini_access_token, is_weixin_token_invalid +# from utils.ip_security import * +from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode + +from backend.utils import update_shangjia_daily +from utils.chat_utils import subscribe_merchant_link_chat +from utils.pdd_order_validator import validate_pdd_order_id, validate_cn_mobile + +from ..models import ( + Gonggao, Lunbo, Tupianpeizhi, Qunpeizhi, + ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig, + MiniappScriptScene, MiniappScriptAutoReply, +) + +from users.models import ( + AdminProfile, UserDashou, UserBoss, + UserShangjia, UserGuanshi, UserZuzhang +) +from users.business_models import User + +from orders.models import CommissionRate, Order, MerchantOrderExt +from orders.notice_tasks import dingdan_guangbo + +from rank.models import Chenghao, DingdanBiaoqian + +from products.models import ShangpinLeixing, Huiyuangoumai + +from ..serializers import PopupConfigSerializer +import traceback +import requests + +import logging +logger = logging.getLogger(__name__) + +class GetDynamicConfigView(APIView): + permission_classes = [AllowAny] + parser_classes = [JSONParser] + + def post(self, request): + try: + from jituan.services.display_config import get_tupianpeizhi_url + + from jituan.services.miniapp_assets import build_miniapp_assets_payload + + cos_oss_url = getattr(settings, 'COS_DOMAIN', '') + if cos_oss_url and not cos_oss_url.endswith('/'): + cos_oss_url += '/' + cos_bucket = getattr(settings, 'COS_BUCKET', '') + cos_region = getattr(settings, 'COS_REGION', 'ap-shanghai') + goeasy_host = 'hangzhou.goeasy.io' + goeasy_appkey = getattr(settings, 'GOEASY_APPKEY', '') + kefu_link = getattr(settings, 'KEFU_LINK', '') + kefu_enterprise_id = getattr(settings, 'KEFU_ENTERPRISE_ID', '') + rule_img = get_tupianpeizhi_url(request, 1) + avatar_img = get_tupianpeizhi_url(request, 2) + morentouxiang = avatar_img or 'beijing/morentouxiang.jpg' + dashouguize = rule_img or 'a_long/dashouguize.jpg' + + # 构造返回数据 + data = { + "code": 0, + "data": { + "cos": { + "bucket": cos_bucket, + "region": cos_region, + "ossImageUrl": cos_oss_url, + "uploadPathPrefix": "order/" + }, + "goEasy": { + "host": goeasy_host, + "appkey": goeasy_appkey + }, + "otherConfig": { + "morentouxiang": morentouxiang, + "dashouguize": dashouguize + }, + "kefu": { + "link": kefu_link, + "enterpriseId": kefu_enterprise_id + }, + "miniappAssets": build_miniapp_assets_payload(request), + } + } + return Response(data) + + except Exception as e: + logger.error(f"获取动态配置失败: {str(e)}", exc_info=True) + return Response({"code": 500, "msg": "服务器内部错误"}, status=500) + + +class HaibaoPeizhiView(APIView): + """小程序推广海报背景配置(按俱乐部)。""" + permission_classes = [AllowAny] + parser_classes = [JSONParser] + + def post(self, request): + try: + from jituan.services.display_config import _display_club_id + from jituan.services.miniapp_assets import get_poster_payload + + club_id = _display_club_id(request) + poster = get_poster_payload(club_id) + return Response({ + 'code': 0, + 'msg': 'success', + 'data': { + 'club_id': club_id, + 'poster': poster, + }, + }) + except Exception as e: + logger.error('获取海报配置失败: %s', e, exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}, status=500) + + + + +class PopupConfigView(APIView): + """ + 获取指定页面的弹窗配置 + 请求格式:POST { "pageKey": "dashouzhongxin" } + 返回格式:{ "code": 0, "data": { "serverTime": "...", "popups": [...] } } + """ + authentication_classes = [] + permission_classes = [AllowAny] + + def post(self, request): + # 获取参数 pageKey + page_key = request.data.get('pageKey') + if not page_key: + return Response({ + 'code': 400, + 'msg': '缺少参数 pageKey' + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 查询页面配置(按俱乐部) + from jituan.services.display_config import get_gonggao_content + from jituan.services.club_context import resolve_club_id_from_request + + club_id = resolve_club_id_from_request(request) + try: + popup_page = PopupPage.query.get(club_id=club_id, page_key=page_key, is_active=True) + except PopupPage.DoesNotExist: + # 该页面没有配置弹窗,返回空 + return Response({ + 'code': 0, + 'data': { + 'serverTime': timezone.now().isoformat(), + 'popups': [] + } + }) + + # 4. 查询该页面下所有启用的弹窗(时间窗为空=长期有效) + now = timezone.now() + popups = PopupConfig.query.filter( + page=popup_page, + is_active=True, + ).filter( + Q(start_time__isnull=True) | Q(start_time__lte=now), + Q(end_time__isnull=True) | Q(end_time__gte=now), + ).prefetch_related('images') # 预加载图片,避免N+1查询 + # 按 sort_order 排序(model Meta 已定义) + + # 5. 序列化 + serializer = PopupConfigSerializer(popups, many=True) + + # 6. 返回数据 + return Response({ + 'code': 0, + 'data': { + 'serverTime': now.isoformat(), + 'ignore_user_mute': popup_page.ignore_user_mute, # 新增 + 'popups': serializer.data + } + }) + + + + +class GetWithdrawModeView(APIView): + """ + 获取提现模式配置 + 前端请求 /peizhi/hqtxzsym + 返回格式: { "code": 0, "data": { "mode": 1 or 2 } } + """ + permission_classes = [IsAuthenticated] # JWT 认证 + + def post(self, request, *args, **kwargs): + from jituan.services.club_context import resolve_club_id_from_request + from jituan.services.club_user import get_user_club_id + from jituan.services.withdraw_config import get_withdraw_mode + + user = getattr(request, 'user', None) + club_id = get_user_club_id(user) if user and getattr(user, 'is_authenticated', False) else None + if not club_id: + club_id = resolve_club_id_from_request(request) + mode = get_withdraw_mode(club_id) + + return Response({ + 'code': 0, + 'data': { + 'mode': mode, + 'club_id': club_id, + } + }) + + + + + +class CheckPhoneAuthView(APIView): + """ + 判断当前登录用户是否需要强制手机号认证 + + 规则: + - 已认证过手机号(PhoneVerified=True) → 不需要 + - 未认证时,满足以下任一条件 → 需要认证: + 打手:有会员购买记录,或押金 > 0 + 商家:已是商家(存在商家扩展表) + 管事:已是管事(存在管事扩展表) + 组长:已是组长(存在组长扩展表) + 老板:有下单记录(alldingdan > 0) + 子客服:已是有效商家客服(merchant_staff_member 活跃) + """ + + permission_classes = [IsAuthenticated] + + @staticmethod + def _phone_auth_completed(user): + """是否已完成手机号认证(兼容 PhoneVerified 未写入的老数据)。""" + if bool(getattr(user, 'PhoneVerified', False)): + return True + phone = (getattr(user, 'Phone', None) or '').strip() + return len(phone) >= 11 + + def post(self, request): + uid = getattr(request.user, 'UserUID', None) + user = request.user + if uid: + try: + user = User.query.get(UserUID=uid) + except User.DoesNotExist: + user = request.user + + # 俱乐部关闭强制收集手机号 → 前端 ensurePhoneAuth 不再跳转认证页 + try: + from jituan.services.club_context import resolve_effective_club_id + from jituan.services.force_phone_auth import is_club_force_phone_auth + club_id = resolve_effective_club_id(request, user) + if not is_club_force_phone_auth(club_id): + return Response({'code': 0, 'need_auth': False}) + except Exception: + pass + + if self._phone_auth_completed(user): + return Response({'code': 0, 'need_auth': False}) + + if self._dashou_meets_threshold(user): + return Response({'code': 0, 'need_auth': True}) + if self._shangjia_meets_threshold(user): + return Response({'code': 0, 'need_auth': True}) + if self._guanshi_meets_threshold(user): + return Response({'code': 0, 'need_auth': True}) + if self._zuzhang_meets_threshold(user): + return Response({'code': 0, 'need_auth': True}) + if self._boss_meets_threshold(user): + return Response({'code': 0, 'need_auth': True}) + if self._staff_meets_threshold(user): + return Response({'code': 0, 'need_auth': True}) + + return Response({'code': 0, 'need_auth': False}) + + def _staff_meets_threshold(self, user): + """子客服:已是有效商家客服""" + try: + from merchant_ops.services.authz import get_active_staff_member + return get_active_staff_member(user) is not None + except Exception: + return False + + def _dashou_meets_threshold(self, user): + """打手:有会员记录、押金 > 0、或有过接单/成交订单""" + try: + dashou = UserDashou.query.get(user=user) + except UserDashou.DoesNotExist: + return False + from jituan.services.fund_freeze import get_effective_yajin + if get_effective_yajin(dashou, user_id=getattr(user, 'UserUID', '')) > 0: + return True + from jituan.services.club_context import resolve_effective_club_id + club_id = resolve_effective_club_id(self.request, user) + if Huiyuangoumai.query.filter(yonghu_id=user.UserUID, club_id=club_id).exists(): + return True + if (dashou.jiedanzongliang or 0) > 0 or (dashou.chengjiaozongliang or 0) > 0: + return True + return Order.query.filter(PlayerID=user.UserUID).exists() + + def _shangjia_meets_threshold(self, user): + """商家:已是商家身份""" + return UserShangjia.query.filter(user=user).exists() + + def _guanshi_meets_threshold(self, user): + """管事:已是管事身份""" + return UserGuanshi.query.filter(user=user).exists() + + def _zuzhang_meets_threshold(self, user): + """组长:已是组长身份""" + return UserZuzhang.query.filter(user=user).exists() + + def _boss_meets_threshold(self, user): + """老板:有下单记录""" + try: + boss = UserBoss.query.get(user=user) + except UserBoss.DoesNotExist: + return False + return (boss.alldingdan or 0) > 0 + + diff --git a/jituan/services/czjilu_fulfill.py b/jituan/services/czjilu_fulfill.py index 2f647a9..4ea27c1 100644 --- a/jituan/services/czjilu_fulfill.py +++ b/jituan/services/czjilu_fulfill.py @@ -117,20 +117,37 @@ def fulfill_yajin_recharge(dingdan_id, paid_amount_yuan=None): except UserDashou.DoesNotExist: raise CzjiluFulfillError('打手不存在,无法充值押金', 'dashou_not_found') - dashou.yajin += amount - dashou.save(update_fields=['yajin']) + from jituan.services.fund_freeze import ( + SOURCE_YAJIN_RECHARGE, + credit_dashou_yajin, + display_yajin_str, + ) + + club_id = getattr(order, 'club_id', None) or CLUB_ID_DEFAULT + uid = str(order.yonghuid or '') + freeze_ret = credit_dashou_yajin( + user_id=uid, + amount=amount, + biz_id=str(dingdan_id), + profile=dashou, + club_id=club_id, + czjilu_id=str(dingdan_id), + source_type=SOURCE_YAJIN_RECHARGE, + freeze_reason=f'押金充值 {dingdan_id}', + ) + if freeze_ret.get('idempotent'): + logger.warning('押金履约幂等跳过入账 dingdan=%s ledger=%s', dingdan_id, freeze_ret.get('ledger_id')) + dashou.refresh_from_db(fields=['yajin', 'dongjie_yue', 'jifen']) try: from jituan.services.yajin_jifen_switch import maybe_fill_jifen_after_yajin - club_id_for_jifen = getattr(order, 'club_id', None) or CLUB_ID_DEFAULT - maybe_fill_jifen_after_yajin(dashou, club_id_for_jifen) + maybe_fill_jifen_after_yajin(dashou, club_id) except Exception as exc: logger.error('押金补积分失败 dingdan=%s: %s', dingdan_id, exc, exc_info=True) order.zhuangtai = 3 order.save(update_fields=['zhuangtai']) - club_id = getattr(order, 'club_id', None) or CLUB_ID_DEFAULT try: from jituan.services.szjilu_accounting import repair_wechat_income_if_missing repair_wechat_income_if_missing(amount, club_id, biz_ref=f'cz:{dingdan_id}') @@ -142,8 +159,20 @@ def fulfill_yajin_recharge(dingdan_id, paid_amount_yuan=None): except Exception as exc: logger.error('押金分红失败但押金已到账 dingdan=%s: %s', dingdan_id, exc, exc_info=True) - logger.info('押金履约完成 dingdan=%s amount=%s', dingdan_id, amount) - return {'already_done': False, 'dingdan_id': dingdan_id, 'zhuangtai': 3, 'yajin': dashou.yajin} + display_yajin = display_yajin_str(dashou, user_id=uid) + logger.info( + '押金履约完成 dingdan=%s amount=%s available=%s frozen=%s display_yajin=%s', + dingdan_id, amount, freeze_ret.get('available'), freeze_ret.get('frozen'), display_yajin, + ) + return { + 'already_done': False, + 'dingdan_id': dingdan_id, + 'zhuangtai': 3, + 'yajin': display_yajin, + 'yajin_available': str(dashou.yajin), + 'yajin_frozen': str(freeze_ret.get('frozen') or 0), + 'freeze': freeze_ret, + } def _apply_yajin_fenhong(order, dashou, amount): diff --git a/jituan/services/fund_freeze.py b/jituan/services/fund_freeze.py index 3a94ce2..a4319bd 100644 --- a/jituan/services/fund_freeze.py +++ b/jituan/services/fund_freeze.py @@ -31,10 +31,82 @@ SOURCE_FENHONG = 'fenhong' SOURCE_KAOHE_FEE = 'kaohe_fee' SOURCE_WITHDRAW_REJECT = 'withdraw_reject' SOURCE_CLUB_MIGRATE = 'club_migrate' +# 押金/保证金入账:进同一 dongjie_yue,解冻回 yajin(绝不能回 yue) +SOURCE_YAJIN_RECHARGE = 'yajin_recharge' +SOURCE_YAJIN_EXCHANGE = 'yajin_exchange' + +YAJIN_SOURCE_TYPES = frozenset({SOURCE_YAJIN_RECHARGE, SOURCE_YAJIN_EXCHANGE}) +CREDIT_TARGET_YUE = 'yue' +CREDIT_TARGET_YAJIN = 'yajin' _CFG_CACHE_TTL = 60 +def is_yajin_source(source_type: str) -> bool: + return (source_type or '').strip() in YAJIN_SOURCE_TYPES + + +def _ledger_credit_target(ledger: FundFreezeLedger) -> str: + """解冻目标:押金类 ledger → yajin;其余 → 可提现佣金。""" + if is_yajin_source(getattr(ledger, 'source_type', '') or ''): + return CREDIT_TARGET_YAJIN + snap = getattr(ledger, 'config_snapshot', None) or {} + if (snap.get('credit_target') or '').strip() == CREDIT_TARGET_YAJIN: + return CREDIT_TARGET_YAJIN + return CREDIT_TARGET_YUE + + +def get_frozen_yajin_remaining(user_id: str) -> Decimal: + """押金类冻结单未解冻剩余合计(不含佣金冻结)。""" + uid = str(user_id or '').strip() + if not uid: + return Decimal('0.00') + rows = FundFreezeLedger.query.filter( + user_id=uid, + role=ROLE_DASHOU, + source_type__in=list(YAJIN_SOURCE_TYPES), + status__in=[FundFreezeLedger.STATUS_FROZEN, FundFreezeLedger.STATUS_PARTIAL], + ).values_list('amount_total', 'amount_unfrozen') + total = Decimal('0.00') + for amount_total, amount_unfrozen in rows: + remain = _money(amount_total) - _money(amount_unfrozen) + if remain > 0: + total += remain + return _money(total) + + +def get_effective_yajin(dashou_profile=None, *, user_id: str = '', yajin=None) -> Decimal: + """ + 接单资格 / 小程序展示用的「在押总额」= 真实 yajin + 押金冻结剩余。 + 提现路径禁止使用本函数,必须读库表 yajin。 + """ + uid = str(user_id or '').strip() + if yajin is None: + if dashou_profile is not None: + yajin = getattr(dashou_profile, 'yajin', 0) or 0 + if not uid: + try: + user = getattr(dashou_profile, 'user', None) + uid = str(getattr(user, 'UserUID', None) or getattr(dashou_profile, 'user_id', '') or '') + except Exception: + uid = '' + else: + yajin = 0 + base = _money(yajin) + if not uid and dashou_profile is not None: + try: + user = getattr(dashou_profile, 'user', None) + uid = str(getattr(user, 'UserUID', None) or '') + except Exception: + uid = '' + return _money(base + get_frozen_yajin_remaining(uid)) + + +def display_yajin_str(dashou_profile=None, *, user_id: str = '', yajin=None) -> str: + """对外 API 伪装字段:前端仍读 yajin,实为在押总额。""" + return str(get_effective_yajin(dashou_profile, user_id=user_id, yajin=yajin)) + + def _money(v) -> Decimal: return Decimal(str(v or 0)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP) @@ -94,6 +166,19 @@ def calc_freeze_amount(credit: Decimal, cfg: ClubFundFreezeConfig) -> Decimal: def _source_allowed(cfg: ClubFundFreezeConfig, source_type: str, role: str = '') -> bool: scope = cfg.source_scope + # 押金冻结必须显式勾选;空 scope 也不自动冻押金,避免误伤现网 + if is_yajin_source(source_type): + if not scope: + return False + if isinstance(scope, str): + items = [scope] + else: + try: + items = list(scope) + except TypeError: + return False + return source_type in items + if not scope: return True if isinstance(scope, str): @@ -148,11 +233,25 @@ def _first_step_delay_days(cfg: ClubFundFreezeConfig) -> int: return max(0, int(cfg.freeze_min_days or 0)) -def _add_withdrawable(role: str, profile, available: Decimal, also_zonge: bool = False): - """原子增加可提现;组长字段为 ketixian_jine。""" +def _add_withdrawable( + role: str, + profile, + available: Decimal, + also_zonge: bool = False, + *, + credit_target: str = CREDIT_TARGET_YUE, +): + """原子增加可提现(或押金可提部分);组长字段为 ketixian_jine。""" available = _money(available) if available == 0: return + if credit_target == CREDIT_TARGET_YAJIN: + if role != ROLE_DASHOU: + raise ValueError('押金入账仅支持打手角色') + type(profile).query.filter(pk=profile.pk).update( + yajin=F('yajin') + available, + ) + return if role == ROLE_ZUZHANG: type(profile).query.filter(pk=profile.pk).update( ketixian_jine=F('ketixian_jine') + available, @@ -187,6 +286,7 @@ def credit_role_balance( freeze_reason: str = '', also_credit_zonge: bool = False, ignore_source_scope: bool = False, + credit_target: str = CREDIT_TARGET_YUE, ) -> Dict[str, Any]: """ 统一入账。返回 {available, frozen, ledger_id, skipped_freeze}。 @@ -194,12 +294,18 @@ def credit_role_balance( 调用方须在外层 transaction.atomic() 内,并已持有/即将更新的业务一致性。 profile: 已加载的角色扩展实例(UserDashou 等);若空则按 role+user_id 查。 ignore_source_scope: True 时跳过 source_scope 检查(仍受 enabled / 金额规则约束)。 + credit_target: yue=可提现佣金(默认);yajin=押金可提部分(冻结进同一 dongjie_yue)。 """ role = (role or '').strip().lower() cid = _club(club_id) credit = _money(amount) biz_id = (biz_id or '').strip() source_type = (source_type or '').strip() or 'unknown' + credit_target = (credit_target or CREDIT_TARGET_YUE).strip() or CREDIT_TARGET_YUE + if is_yajin_source(source_type): + credit_target = CREDIT_TARGET_YAJIN + if credit_target == CREDIT_TARGET_YAJIN and role != ROLE_DASHOU: + raise ValueError('押金入账仅支持打手角色') if credit <= 0: return {'available': Decimal('0.00'), 'frozen': Decimal('0.00'), 'ledger_id': None, 'skipped_freeze': True} @@ -239,7 +345,7 @@ def credit_role_balance( getattr(cfg, 'freeze_mode', None), getattr(cfg, 'freeze_rate', None), ) - # 幂等:已存在同 biz 冻结单则只补可提现差额不重复冻(极端重入) + # 幂等:已存在同 biz 冻结单则不再入账(极端重入) if freeze_on and frozen > 0: existed = FundFreezeLedger.query.filter( club_id=cid, role=role, source_type=source_type, biz_id=biz_id, @@ -258,12 +364,16 @@ def credit_role_balance( 'skip_reason': 'idempotent', } - _add_withdrawable(role, profile, available, also_zonge=also_credit_zonge) + _add_withdrawable( + role, profile, available, also_zonge=also_credit_zonge, credit_target=credit_target, + ) ledger_id = None if frozen > 0: _add_dongjie(profile, frozen) delay = _first_step_delay_days(cfg) next_at = timezone.now() + timedelta(days=delay) if delay else timezone.now() + snap = _config_snapshot(cfg) + snap['credit_target'] = credit_target ledger = FundFreezeLedger.query.create( club_id=cid, user_id=str(user_id), @@ -280,15 +390,15 @@ def credit_role_balance( or (getattr(cfg, 'remark', None) if cfg else None) or f'{source_type}冻结' )[:255], - config_snapshot=_config_snapshot(cfg), + config_snapshot=snap, current_step=1, next_eligible_at=next_at, ) ledger_id = ledger.id logger.info( - 'fund_freeze applied club=%s role=%s source=%s biz=%s user=%s ' + 'fund_freeze applied club=%s role=%s source=%s target=%s biz=%s user=%s ' 'credit=%s available=%s frozen=%s ledger=%s', - cid, role, source_type, biz_id, user_id, credit, available, frozen, ledger_id, + cid, role, source_type, credit_target, biz_id, user_id, credit, available, frozen, ledger_id, ) return { @@ -298,6 +408,7 @@ def credit_role_balance( 'skipped_freeze': frozen <= 0, 'skip_reason': skip_reason or None, 'club_id': cid, + 'credit_target': credit_target, } @@ -484,6 +595,45 @@ def credit_dashou_order_settle( return result +def credit_dashou_yajin( + *, + user_id: str, + amount, + biz_id: str, + profile=None, + club_id: Optional[str] = None, + czjilu_id: str = '', + source_type: str = SOURCE_YAJIN_RECHARGE, + freeze_reason: str = '', +) -> Dict[str, Any]: + """ + 打手押金入账网关。 + + - 未开启冻结 / 未勾选押金来源:全额进 yajin(与现网一致) + - 开启且勾选:按规则拆「可提押金 yajin + 冻结池 dongjie_yue」,ledger.source=yajin_* + - 解冻时回 yajin,绝不进佣金 yue + 调用方须在 transaction.atomic() 内。 + """ + uid = str(user_id or '').strip() + src = (source_type or SOURCE_YAJIN_RECHARGE).strip() + if not is_yajin_source(src): + src = SOURCE_YAJIN_RECHARGE + cid = settle_freeze_club_id(user_id=uid) if not club_id else _club(club_id) + return credit_role_balance( + club_id=cid, + user_id=uid, + role=ROLE_DASHOU, + amount=amount, + source_type=src, + biz_id=str(biz_id or '').strip(), + profile=profile, + czjilu_id=(czjilu_id or biz_id or '')[:64], + freeze_reason=freeze_reason or '押金充值冻结', + also_credit_zonge=False, + credit_target=CREDIT_TARGET_YAJIN, + ) + + def _step_unfreeze_amount(ledger: FundFreezeLedger, step: dict) -> Decimal: remain = _money(ledger.amount_total - ledger.amount_unfrozen) if remain <= 0: @@ -654,7 +804,20 @@ def apply_unfreeze_step( if amount <= 0: return {'ok': False, 'msg': '冻结池余额不足'} - if ledger.role == ROLE_ZUZHANG: + # 押金冻结必须解回 yajin;佣金/分红解回可提现(组长 ketixian_jine) + credit_target = _ledger_credit_target(ledger) + if credit_target == CREDIT_TARGET_YAJIN: + if ledger.role != ROLE_DASHOU: + return {'ok': False, 'msg': '押金冻结单角色异常,拒绝解冻'} + model.query.filter(pk=profile.pk).update( + dongjie_yue=F('dongjie_yue') - amount, + yajin=F('yajin') + amount, + ) + logger.info( + 'fund_unfreeze to_yajin ledger=%s user=%s amount=%s', + ledger.id, ledger.user_id, amount, + ) + elif ledger.role == ROLE_ZUZHANG: model.query.filter(pk=profile.pk).update( dongjie_yue=F('dongjie_yue') - amount, ketixian_jine=F('ketixian_jine') + amount, diff --git a/jituan/services/fund_freeze_admin.py b/jituan/services/fund_freeze_admin.py index 68faa13..4d17859 100644 --- a/jituan/services/fund_freeze_admin.py +++ b/jituan/services/fund_freeze_admin.py @@ -289,6 +289,18 @@ def list_ledgers(request, data: dict) -> Dict[str, Any]: 'frozen_at': r.frozen_at.isoformat() if r.frozen_at else '', 'next_eligible_at': r.next_eligible_at.isoformat() if r.next_eligible_at else '', 'source_type': r.source_type, + 'source_label': { + 'order_settle': '订单结算', + 'fenhong': '分红/分佣', + 'withdraw_reject': '提现驳回', + 'club_migrate': '跨店迁移', + 'kaohe_fee': '考核代收', + 'yajin_recharge': '押金充值', + 'yajin_exchange': '兑换押金', + }.get(r.source_type or '', r.source_type or ''), + 'credit_target': ((r.config_snapshot or {}).get('credit_target') + or ('yajin' if (r.source_type or '') in ( + 'yajin_recharge', 'yajin_exchange') else 'yue')), 'order_id': r.order_id, 'czjilu_id': r.czjilu_id, 'biz_id': r.biz_id, diff --git a/orders/services/fake_order_pool.py b/orders/services/fake_order_pool.py index 91c40c6..42ff9a4 100644 --- a/orders/services/fake_order_pool.py +++ b/orders/services/fake_order_pool.py @@ -99,7 +99,10 @@ def resolve_fake_pool_mode(user, club_id=None, leixing_id=None): return MODE_REAL_ONLY try: - yajin = Decimal(str(getattr(dashou, 'yajin', 0) or 0)) + from jituan.services.fund_freeze import get_effective_yajin + yajin = get_effective_yajin( + dashou, user_id=str(getattr(user, 'UserUID', '') or ''), + ) except Exception: yajin = Decimal('0') if yajin >= get_fake_yajin_hide_threshold(cid): diff --git a/orders/services/team_recruit.py b/orders/services/team_recruit.py index 2fe53dd..36d81a8 100644 --- a/orders/services/team_recruit.py +++ b/orders/services/team_recruit.py @@ -569,8 +569,10 @@ def join_recruit(*, recruit_id: int, user_uid: str, game_id: str, request=None, if not user_has_huiyuan_access_any(user_uid, required_ids, club_id=rec.club_id): raise ValueError('未开通对应会员或会员已过期') elif order.GrabRequirement == 2: + from jituan.services.fund_freeze import get_effective_yajin + yajin_required = order.CommissionReq or 0 - if (profile.yajin or 0) < yajin_required: + if get_effective_yajin(profile) < yajin_required: raise ValueError(f'押金不足,需要{yajin_required}元') now = timezone.now() diff --git a/orders/views/admin.py b/orders/views/admin.py index 5595b37..6fb0820 100644 --- a/orders/views/admin.py +++ b/orders/views/admin.py @@ -426,6 +426,8 @@ class AdGengHuanDaShou(APIView): ) elif yaoqiuleixing == 2: # 押金抢单 + from jituan.services.fund_freeze import get_effective_yajin + # 获取订单的押金(yongjin字段) order_yajin = dingdan_obj.CommissionReq if order_yajin is None: @@ -434,8 +436,8 @@ class AdGengHuanDaShou(APIView): status=status.HTTP_400_BAD_REQUEST ) - # 验证打手押金是否大于等于订单押金 - if new_dashou_profile.yajin < order_yajin: + # 验证打手押金是否大于等于订单押金(含押金冻结池) + if get_effective_yajin(new_dashou_profile) < order_yajin: return Response( {'code': 400, 'message': '打手押金不足', 'data': None}, status=status.HTTP_400_BAD_REQUEST diff --git a/orders/views/dashou.py b/orders/views/dashou.py index 8c20f9e..6768758 100644 --- a/orders/views/dashou.py +++ b/orders/views/dashou.py @@ -679,14 +679,15 @@ class QiangdanView(APIView): } def _validate_yajin_requirement(self, order, dashou_profile): + from jituan.services.fund_freeze import get_effective_yajin + yajin_required = order.CommissionReq or 0 - dashou_yajin = dashou_profile.yajin or 0 try: need = float(yajin_required) except (TypeError, ValueError): need = 0.0 try: - have = float(dashou_yajin) + have = float(get_effective_yajin(dashou_profile)) except (TypeError, ValueError): have = 0.0 if have < need: diff --git a/products/views/chongzhi_payment.py b/products/views/chongzhi_payment.py index 6e514c1..dbab32a 100644 --- a/products/views/chongzhi_payment.py +++ b/products/views/chongzhi_payment.py @@ -1092,20 +1092,33 @@ class DsqrgmdhView(APIView): elif leixing == 2: try: dashou = UserDashou.objects.select_for_update().get(user=user) - dashou.yajin += yajin_jine - dashou.save(update_fields=['yajin']) + from jituan.services.fund_freeze import ( + SOURCE_YAJIN_EXCHANGE, + credit_dashou_yajin, + display_yajin_str, + ) + from jituan.services.club_user import get_user_club_id + yajin_club = getattr(czjilu, 'club_id', None) or get_user_club_id(user) or '' + uid = str(getattr(user, 'UserUID', '') or '') + credit_dashou_yajin( + user_id=uid, + amount=yajin_jine, + biz_id=str(getattr(czjilu, 'dingdan_id', None) or czjilu.pk), + profile=dashou, + club_id=yajin_club, + czjilu_id=str(getattr(czjilu, 'dingdan_id', None) or ''), + source_type=SOURCE_YAJIN_EXCHANGE, + freeze_reason='余额兑换押金', + ) + dashou.refresh_from_db(fields=['yajin', 'dongjie_yue', 'jifen']) try: from jituan.services.yajin_jifen_switch import maybe_fill_jifen_after_yajin - from jituan.services.club_user import get_user_club_id - maybe_fill_jifen_after_yajin( - dashou, - getattr(czjilu, 'club_id', None) or get_user_club_id(user) or '', - ) + maybe_fill_jifen_after_yajin(dashou, yajin_club) except Exception as jifen_exc: logger.error('余额抵扣押金补积分失败: %s', jifen_exc, exc_info=True) czjilu.zhuangtai = MEMBER_PAID_STATUS czjilu.save(update_fields=['zhuangtai']) - response_data['yajin'] = dashou.yajin + response_data['yajin'] = display_yajin_str(dashou, user_id=uid) response_data['jifen'] = dashou.jifen except UserDashou.DoesNotExist: setattr(locked_profile, identity['balance_field'], current_balance) diff --git a/products/views/yajin_payment.py b/products/views/yajin_payment.py index e244d7e..09270c0 100644 --- a/products/views/yajin_payment.py +++ b/products/views/yajin_payment.py @@ -627,10 +627,14 @@ class YajinLunxun(APIView): 'message': '订单尚未支付完成' }, status=status.HTTP_200_OK) - # 4. 获取打手押金(通过反向查询,最快捷的方式) + # 4. 获取打手押金(对外=在押总额,含押金冻结池,供前端全局变量) try: + from jituan.services.fund_freeze import get_effective_yajin + dashou = request.user.DashouProfile - yajin = float(dashou.yajin) # 转为浮点数方便前端处理 + yajin = float(get_effective_yajin( + dashou, user_id=str(request.user.UserUID), + )) logger.info(f"轮询成功: 订单{dingdanid}已支付,用户{request.user.UserUID}押金为{yajin}元") diff --git a/users/views/auth.py b/users/views/auth.py index b9ec0c6..cc6c147 100644 --- a/users/views/auth.py +++ b/users/views/auth.py @@ -651,7 +651,7 @@ class DashouZhuceView(APIView): 'zhanghaostatus': dashou_profile.zhanghaozhuangtai, 'yongjin': str(dashou_profile.yue), # Decimal转字符串 'zonge': str(dashou_profile.zonge), - 'yajin': str(dashou_profile.yajin), + 'yajin': __import__('jituan.services.fund_freeze', fromlist=['display_yajin_str']).display_yajin_str(dashou_profile), 'chenghao': dashou_profile.chenghao, 'jinfen': dashou_profile.jifen, 'chengjiaoliang': dashou_profile.chengjiaozongliang, @@ -1259,7 +1259,7 @@ class WechatLoginAndDashouRegisterView(APIView): 'zhanghaostatus': dashou_profile.zhanghaozhuangtai, 'yongjin': str(dashou_profile.yue), # Decimal转字符串 'zonge': str(dashou_profile.zonge), - 'yajin': str(dashou_profile.yajin), + 'yajin': __import__('jituan.services.fund_freeze', fromlist=['display_yajin_str']).display_yajin_str(dashou_profile), 'chenghao': dashou_profile.chenghao, 'jinfen': dashou_profile.jifen, 'chengjiaoliang': dashou_profile.chengjiaozongliang, @@ -1462,7 +1462,7 @@ class WechatLoginAndGuanshiRegisterView(APIView): 'zhanghaostatus': dashou_profile.zhanghaozhuangtai, 'yongjin': str(dashou_profile.yue), 'zonge': str(dashou_profile.zonge), - 'yajin': str(dashou_profile.yajin), + 'yajin': __import__('jituan.services.fund_freeze', fromlist=['display_yajin_str']).display_yajin_str(dashou_profile), 'chenghao': dashou_profile.chenghao, 'jinfen': dashou_profile.jifen, 'chengjiaoliang': dashou_profile.chengjiaozongliang, @@ -1687,7 +1687,7 @@ class GuanshiRegisterView(APIView): 'zhanghaostatus': dashou_profile.zhanghaozhuangtai, 'yongjin': str(dashou_profile.yue), 'zonge': str(dashou_profile.zonge), - 'yajin': str(dashou_profile.yajin), + 'yajin': __import__('jituan.services.fund_freeze', fromlist=['display_yajin_str']).display_yajin_str(dashou_profile), 'chenghao': dashou_profile.chenghao, 'jinfen': dashou_profile.jifen, 'chengjiaoliang': dashou_profile.chengjiaozongliang, diff --git a/users/views/kefu_orders.py b/users/views/kefu_orders.py index 6fa5ccd..6f2d998 100644 --- a/users/views/kefu_orders.py +++ b/users/views/kefu_orders.py @@ -602,11 +602,13 @@ class KefuChangeDashouView(APIView): return Response({'code': 400, 'msg': '打手的会员已过期'}, status=status.HTTP_400_BAD_REQUEST) elif yaoqiuleixing == 2: # 押金抢单 + from jituan.services.fund_freeze import get_effective_yajin + order_yajin = order.CommissionReq # 假设订单的 yongjin 字段存储押金要求 if order_yajin is None: return Response({'code': 400, 'msg': '订单押金为空'}, status=status.HTTP_400_BAD_REQUEST) - if new_dashou_profile.yajin < order_yajin: + if get_effective_yajin(new_dashou_profile) < order_yajin: return Response({'code': 400, 'msg': '打手押金不足'}, status=status.HTTP_400_BAD_REQUEST) else: return Response({'code': 400, 'msg': '订单抢单要求类型错误'}, status=status.HTTP_400_BAD_REQUEST) diff --git a/users/views/user_info.py b/users/views/user_info.py index 7364db6..7272cc6 100644 --- a/users/views/user_info.py +++ b/users/views/user_info.py @@ -399,7 +399,8 @@ class DashouXinxiAPIView(APIView): 'data': None }, status=status.HTTP_400_BAD_REQUEST) - # 3. 构建基础信息响应(原有字段完全保留) + # 3. 构建基础信息响应(yajin 对外=在押总额,含押金冻结) + from jituan.services.fund_freeze import display_yajin_str data = { 'dashounicheng': dashou_profile.nicheng or '', 'zhanghaostatus': dashou_profile.zhanghaozhuangtai, @@ -409,7 +410,7 @@ class DashouXinxiAPIView(APIView): 'yongjin': str(dashou_profile.yue) if dashou_profile.yue is not None else '0.00', 'dongjie_yue': str(getattr(dashou_profile, 'dongjie_yue', 0) or 0), 'zonge': str(dashou_profile.zonge) if dashou_profile.zonge is not None else '0.00', - 'yajin': str(dashou_profile.yajin) if dashou_profile.yajin is not None else '0.00', + 'yajin': display_yajin_str(dashou_profile, user_id=getattr(current_user, 'UserUID', '')), 'jiedanZongliang':dashou_profile.jiedanzongliang if dashou_profile.jiedanzongliang is not None else '0', 'zuijinTixian': str(dashou_profile.jinritixian_jine) if dashou_profile.jinritixian_jine is not None else '0.00', 'chengjiaoliang': dashou_profile.chengjiaozongliang, @@ -1155,10 +1156,11 @@ class DashouJianquanView(APIView): 'msg': '鉴权通过(俱乐部允许无会员聊天)' }) - # ========== 3. 押金达到俱乐部门槛 ========== + # ========== 3. 押金达到俱乐部门槛(含押金冻结池) ========== + from jituan.services.fund_freeze import get_effective_yajin threshold = get_dashou_chat_yajin_threshold(club_id) try: - yajin = Decimal(str(getattr(dashou, 'yajin', 0) or 0)) + yajin = get_effective_yajin(dashou, user_id=getattr(user, 'UserUID', '')) except Exception: yajin = Decimal('0') if yajin >= threshold: