feat: 押金充值接入资金冻结池,接单按在押总额校验
This commit is contained in:
1 parent
6c22a0b77e
commit
fb2cfbcfc9
13 files changed
+619
-388
No files matched your search
+346
-348
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}元")
|
||||
|
||||
|
||||
+4
-4
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user