diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/dingdan/migrations/0017_dingdan_guanshi_shangjia_fencheng.py b/dingdan/migrations/0017_dingdan_guanshi_shangjia_fencheng.py new file mode 100644 index 0000000..4cd6fab --- /dev/null +++ b/dingdan/migrations/0017_dingdan_guanshi_shangjia_fencheng.py @@ -0,0 +1,23 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dingdan', '0016_dingdan_guanshi_fencheng'), + ] + + operations = [ + migrations.AddField( + model_name='dingdan', + name='guanshi_shangjia_fencheng', + field=models.DecimalField( + blank=True, + decimal_places=2, + default=0, + max_digits=10, + null=True, + verbose_name='管事商家派单分成', + ), + ), + ] diff --git a/dingdan/models.py b/dingdan/models.py index 6aa213b..2207dda 100644 --- a/dingdan/models.py +++ b/dingdan/models.py @@ -11,6 +11,10 @@ class Dingdan(models.Model): jine = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='订单金额') dashou_fencheng = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='打手分成') guanshi_fencheng = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, default=0, verbose_name='管事分成') + guanshi_shangjia_fencheng = models.DecimalField( + max_digits=10, decimal_places=2, null=True, blank=True, default=0, + verbose_name='管事商家派单分成' + ) # 用户与商品关联ID jiedan_dashou_id = models.CharField(max_length=32, null=True, db_index=True,blank=True, verbose_name='接单打手ID') dashou_liuyan = models.TextField(null=True, blank=True, verbose_name='打手留言') diff --git a/dingdan/utils.py b/dingdan/utils.py index 66113f9..7016aa8 100644 --- a/dingdan/utils.py +++ b/dingdan/utils.py @@ -342,7 +342,10 @@ from dingdan.models import Lilubiao def calc_shangjia_order_fencheng(jine): """ 商家发单时计算打手/管事分成。 - 打手优先:打手+管事 > 订单金额时,管事分成为 0。 + - 打手分成(Lilubiao=3)最高不超过订单金额 + - 管事打手接单分红(Lilubiao=13) + - 管事商家派单分红(Lilubiao=15) + 打手优先;三项合计超过订单金额时,优先清零管事商家分红,再清零管事打手分红。 """ jine = Decimal(str(jine)) try: @@ -351,92 +354,158 @@ def calc_shangjia_order_fencheng(jine): except Lilubiao.DoesNotExist: rate_dashou = Decimal('1') - lilu_guanshi_obj = Lilubiao.objects.filter(fadanpingtai='13').first() - rate_guanshi = ( - Decimal(str(lilu_guanshi_obj.lilu)) - if lilu_guanshi_obj and lilu_guanshi_obj.lilu is not None + lilu_guanshi_dashou_obj = Lilubiao.objects.filter(fadanpingtai='13').first() + rate_guanshi_dashou = ( + Decimal(str(lilu_guanshi_dashou_obj.lilu)) + if lilu_guanshi_dashou_obj and lilu_guanshi_dashou_obj.lilu is not None else Decimal('0') ) - dashou_fencheng = (jine * rate_dashou).quantize(Decimal('0.01')) - guanshi_raw = (jine * rate_guanshi).quantize(Decimal('0.01')) + lilu_guanshi_shangjia_obj = Lilubiao.objects.filter(fadanpingtai='15').first() + rate_guanshi_shangjia = ( + Decimal(str(lilu_guanshi_shangjia_obj.lilu)) + if lilu_guanshi_shangjia_obj and lilu_guanshi_shangjia_obj.lilu is not None + else Decimal('0') + ) - if dashou_fencheng + guanshi_raw > jine: + dashou_fencheng = min( + (jine * rate_dashou).quantize(Decimal('0.01')), + jine, + ) + guanshi_fencheng = (jine * rate_guanshi_dashou).quantize(Decimal('0.01')) + guanshi_shangjia_fencheng = (jine * rate_guanshi_shangjia).quantize(Decimal('0.01')) + + if dashou_fencheng + guanshi_fencheng > jine: guanshi_fencheng = Decimal('0.00') - else: - guanshi_fencheng = guanshi_raw - return dashou_fencheng, guanshi_fencheng + if dashou_fencheng + guanshi_fencheng + guanshi_shangjia_fencheng > jine: + guanshi_shangjia_fencheng = Decimal('0.00') + + if dashou_fencheng + guanshi_fencheng + guanshi_shangjia_fencheng > jine: + guanshi_fencheng = Decimal('0.00') + + return dashou_fencheng, guanshi_fencheng, guanshi_shangjia_fencheng -def settle_shangjia_order_guanshi_fenhong(order, dashou_id): - """ - 商家订单结单时结算管事分红(幂等,失败不抛异常阻断主流程)。 - """ +def _pay_guanshi_order_fenhong(order, guanshi_id, amount, related_user_id, nicheng, avatar, + fenhong_leixing, shuoming): + """向管事发放单笔商家订单分红(幂等)""" from shangpin.models import Gsfenhong - from yonghu.models import UserDashou, UserGuanshi + from yonghu.models import UserGuanshi - if getattr(order, 'fadan_pingtai', None) != 2: - return + amount = amount or Decimal('0') + if amount <= 0 or not guanshi_id: + return False - guanshi_fencheng = order.guanshi_fencheng or Decimal('0') - if guanshi_fencheng <= 0: - return - - if Gsfenhong.objects.filter(dingdan_id=order.dingdan_id).exists(): - logger.info(f"商家订单管事分红已处理: {order.dingdan_id}") - return - - if not dashou_id: - return - - try: - dashou = UserDashou.objects.select_related('user').get(user__yonghuid=dashou_id) - except UserDashou.DoesNotExist: - logger.info(f"商家订单管事分红跳过: 打手{dashou_id}不存在") - return - - guanshi_id = dashou.yaoqingren - if not guanshi_id: - logger.info(f"商家订单管事分红跳过: 打手{dashou_id}无邀请管事") - return + if Gsfenhong.objects.filter(dingdan_id=order.dingdan_id, fenhong_leixing=fenhong_leixing).exists(): + logger.info(f"管事分红已处理: 订单{order.dingdan_id}, 类型{fenhong_leixing}") + return False try: with transaction.atomic(): guanshi = UserGuanshi.objects.select_for_update().get(user__yonghuid=guanshi_id) UserGuanshi.objects.filter(id=guanshi.id).update( - yue=F('yue') + guanshi_fencheng, - chongzhifenrun=F('chongzhifenrun') + guanshi_fencheng, + yue=F('yue') + amount, + chongzhifenrun=F('chongzhifenrun') + amount, ) - - user_main = dashou.user Gsfenhong.objects.create( dingdan_id=order.dingdan_id, guanshi=guanshi_id, - dashouid=dashou_id, - shuoming='商家订单分红', - fenhong=guanshi_fencheng, - avatar=user_main.avatar if user_main else None, - nicheng=dashou.nicheng or '未知打手', - fenhong_leixing=3, + dashouid=related_user_id, + shuoming=shuoming, + fenhong=amount, + avatar=avatar, + nicheng=nicheng or '未知用户', + fenhong_leixing=fenhong_leixing, ) logger.info( - f"商家订单管事分红成功: 订单{order.dingdan_id}, 管事{guanshi_id}, " - f"打手{dashou_id}, 金额{guanshi_fencheng}元" + f"管事分红成功: 订单{order.dingdan_id}, 管事{guanshi_id}, " + f"关联用户{related_user_id}, 类型{fenhong_leixing}, 金额{amount}元" ) except UserGuanshi.DoesNotExist: - logger.warning(f"商家订单管事分红跳过: 管事{guanshi_id}不存在") - return + logger.warning(f"管事分红跳过: 管事{guanshi_id}不存在") + return False except Exception as e: - logger.error(f"商家订单管事分红失败: {e}", exc_info=True) - return + logger.error(f"管事分红失败: {e}", exc_info=True) + return False try: from houtai.utils import update_guanshi_daily_by_action update_guanshi_daily_by_action( yonghuid=guanshi_id, action=3, - amount=guanshi_fencheng, + amount=amount, ) except Exception as e: - logger.error(f"商家订单管事日统计更新失败: {e}") \ No newline at end of file + logger.error(f"管事每日统计更新失败: {e}") + return True + + +def settle_shangjia_order_guanshi_fenhong(order, dashou_id): + """ + 商家订单结单时结算管事分红(幂等,失败不抛异常阻断主流程)。 + - 管事打手接单分红:接单打手的邀请管事(guanshi_fencheng) + - 管事商家派单分红:派单商家打手身份的邀请管事(guanshi_shangjia_fencheng) + 同一管事可同时获得两笔分红。 + """ + from dingdan.models import DingdanShangjia + from yonghu.models import UserDashou + + if getattr(order, 'fadan_pingtai', None) != 2: + return + + # 1. 接单打手邀请管事分红 + guanshi_fencheng = order.guanshi_fencheng or Decimal('0') + if guanshi_fencheng > 0 and dashou_id: + try: + dashou = UserDashou.objects.select_related('user').get(user__yonghuid=dashou_id) + except UserDashou.DoesNotExist: + logger.info(f"管事打手分红跳过: 打手{dashou_id}不存在") + dashou = None + + if dashou and dashou.yaoqingren: + user_main = dashou.user + _pay_guanshi_order_fenhong( + order=order, + guanshi_id=dashou.yaoqingren, + amount=guanshi_fencheng, + related_user_id=dashou_id, + nicheng=dashou.nicheng, + avatar=user_main.avatar if user_main else None, + fenhong_leixing=3, + shuoming='商家订单打手分红', + ) + else: + logger.info(f"管事打手分红跳过: 打手{dashou_id}无邀请管事") + + # 2. 派单商家邀请管事分红 + guanshi_shangjia_fencheng = order.guanshi_shangjia_fencheng or Decimal('0') + if guanshi_shangjia_fencheng > 0: + try: + shangjia_ext = DingdanShangjia.objects.select_related('dingdan').get(dingdan__dingdan_id=order.dingdan_id) + shangjia_id = shangjia_ext.shangjia_id + except DingdanShangjia.DoesNotExist: + logger.info(f"管事商家分红跳过: 订单{order.dingdan_id}无商家扩展") + shangjia_id = None + + if shangjia_id: + try: + merchant_dashou = UserDashou.objects.select_related('user').get(user__yonghuid=shangjia_id) + except UserDashou.DoesNotExist: + logger.info(f"管事商家分红跳过: 商家{shangjia_id}无打手扩展") + merchant_dashou = None + + if merchant_dashou and merchant_dashou.yaoqingren: + user_main = merchant_dashou.user + _pay_guanshi_order_fenhong( + order=order, + guanshi_id=merchant_dashou.yaoqingren, + amount=guanshi_shangjia_fencheng, + related_user_id=shangjia_id, + nicheng=shangjia_ext.sjnicheng or merchant_dashou.nicheng, + avatar=user_main.avatar if user_main else None, + fenhong_leixing=4, + shuoming='商家派单管事分红', + ) + else: + logger.info(f"管事商家分红跳过: 商家{shangjia_id}打手身份无邀请管事") \ No newline at end of file diff --git a/dingdan/views.py b/dingdan/views.py index 7d0f7e1..2fee917 100644 --- a/dingdan/views.py +++ b/dingdan/views.py @@ -2665,7 +2665,7 @@ class ShangjiaPaifaView(APIView): # ----- 10. 利率(打手+管事分成,打手优先) ----- from dingdan.utils import calc_shangjia_order_fencheng - dashou_fencheng, guanshi_fencheng = calc_shangjia_order_fencheng(jiage) + dashou_fencheng, guanshi_fencheng, guanshi_shangjia_fencheng = calc_shangjia_order_fencheng(jiage) # ----- 11. 生成订单ID ----- def generate_dingdan_id(): @@ -2686,6 +2686,7 @@ class ShangjiaPaifaView(APIView): jine=jiage, dashou_fencheng=dashou_fencheng, guanshi_fencheng=guanshi_fencheng, + guanshi_shangjia_fencheng=guanshi_shangjia_fencheng, zhiding_id=zhiding_uid if zhiding_dashou else '', shangpin_id=0, leixing_id=int(shangpin_type_id), @@ -4153,7 +4154,7 @@ logger = logging.getLogger(__name__) from peizhi.models import ClubConfig -from utils.chat_utils import establish_order_chat # 【新增】导入聊天工具 +from utils.chat_utils import establish_order_chat_with_retry class QiangdanView(APIView): authentication_classes = [JWTAuthentication] permission_classes = [IsAuthenticated] @@ -4164,6 +4165,7 @@ class QiangdanView(APIView): if not dingdan_id: return Response({'code': 400, 'msg': '订单ID不能为空'}, status=400) + chat_success = False try: with transaction.atomic(): # 1. 验证打手身份和状态 @@ -4237,8 +4239,8 @@ class QiangdanView(APIView): return Response({'code': 400, 'msg': validation_result['message']}) self._execute_qiangdan(order, dashou_profile, request.user.yonghuid) - # ========== 【唯一改动】聊天建立方式 ========== - chat_success = establish_order_chat(dingdan_id) + # 事务提交后再建群/建聊,避免长耗时 HTTP 拖住行锁,并支持重试 + chat_success = establish_order_chat_with_retry(dingdan_id) try: update_dashou_daily_by_action( @@ -6357,6 +6359,10 @@ class AdQiangZhiJieDan(APIView): # 商家扩展表不存在,跳过商家更新 logger.warning(f"强制结单:商家{shangjia_id}扩展信息不存在,跳过商家更新") + if fadan_pingtai == 2 and jiedan_dashou_id: + from dingdan.utils import settle_shangjia_order_guanshi_fenhong + settle_shangjia_order_guanshi_fenhong(dingdan_obj, jiedan_dashou_id) + # 11. 返回成功响应 return Response({ 'code': 0, diff --git a/houtai/huashu_views.py b/houtai/huashu_views.py new file mode 100644 index 0000000..0147775 --- /dev/null +++ b/houtai/huashu_views.py @@ -0,0 +1,175 @@ +# houtai/huashu_views.py — 后台话术配置管理 +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated + +from peizhi.models import HuashuConfig +from .utils import verify_kefu_permission + +HUASHU_REQUIRED_PERMISSION = '8080a' + + +def _serialize_item(item): + return { + 'id': item.id, + 'scene_key': item.scene_key, + 'scene_label': item.get_scene_key_display(), + 'item_type': item.item_type, + 'item_type_label': item.get_item_type_display(), + 'title': item.title, + 'content': item.content, + 'confirm_text': item.confirm_text, + 'cancel_text': item.cancel_text, + 'keywords': item.keywords or [], + 'match_mode': item.match_mode, + 'priority': item.priority, + 'is_active': item.is_active, + 'sort_order': item.sort_order, + 'created_at': item.created_at.strftime('%Y-%m-%d %H:%M:%S') if item.created_at else '', + 'updated_at': item.updated_at.strftime('%Y-%m-%d %H:%M:%S') if item.updated_at else '', + } + + +class HuashuListAPIView(APIView): + """POST /houtai/hthqhs — 获取话术配置列表""" + + permission_classes = [IsAuthenticated] + + def post(self, request): + username_frontend = request.data.get('username') + kefu, permissions = verify_kefu_permission(request, username_frontend) + if kefu is None: + return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'}) + if HUASHU_REQUIRED_PERMISSION not in permissions: + return Response({'code': 403, 'msg': '您没有权限访问话术配置功能'}) + + scene_key = request.data.get('scene_key') + qs = HuashuConfig.objects.all().order_by('scene_key', '-priority', 'sort_order', 'id') + if scene_key: + qs = qs.filter(scene_key=scene_key) + + items = [_serialize_item(i) for i in qs] + scenes = [{'key': c[0], 'label': c[1]} for c in HuashuConfig.SCENE_CHOICES] + return Response({'code': 0, 'data': {'items': items, 'scenes': scenes}}) + + +class HuashuModifyAPIView(APIView): + """POST /houtai/htxghs — 话术配置增删改""" + + permission_classes = [IsAuthenticated] + + def post(self, request): + username_frontend = request.data.get('username') + kefu, permissions = verify_kefu_permission(request, username_frontend) + if kefu is None: + return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'}) + if HUASHU_REQUIRED_PERMISSION not in permissions: + return Response({'code': 403, 'msg': '您没有权限操作话术配置'}) + + action = request.data.get('action') + if not action: + return Response({'code': 400, 'msg': '缺少 action 参数'}) + + try: + if action == 'create': + return self._create(request) + if action == 'update': + return self._update(request) + if action == 'delete': + return self._delete(request) + return Response({'code': 400, 'msg': f'未知的 action: {action}'}) + except Exception as e: + return Response({'code': 500, 'msg': f'操作失败: {str(e)}'}) + + def _create(self, request): + scene_key = request.data.get('scene_key') + item_type = request.data.get('item_type') + content = (request.data.get('content') or '').strip() + + if not scene_key or not item_type or not content: + return Response({'code': 400, 'msg': 'scene_key、item_type、content 为必填'}) + + valid_scenes = {c[0] for c in HuashuConfig.SCENE_CHOICES} + valid_types = {c[0] for c in HuashuConfig.ITEM_TYPE_CHOICES} + if scene_key not in valid_scenes: + return Response({'code': 400, 'msg': '无效的场景标识'}) + if item_type not in valid_types: + return Response({'code': 400, 'msg': '无效的条目类型'}) + + keywords = request.data.get('keywords') or [] + if not isinstance(keywords, list): + keywords = [] + + item = HuashuConfig.objects.create( + scene_key=scene_key, + item_type=item_type, + title=request.data.get('title', ''), + content=content, + confirm_text=request.data.get('confirm_text', '我知道了'), + cancel_text=request.data.get('cancel_text', '取消'), + keywords=keywords, + match_mode=request.data.get('match_mode', HuashuConfig.MATCH_CONTAINS), + priority=int(request.data.get('priority', 0)), + is_active=bool(request.data.get('is_active', True)), + sort_order=int(request.data.get('sort_order', 0)), + ) + return Response({'code': 0, 'msg': '创建成功', 'data': _serialize_item(item)}) + + def _update(self, request): + item_id = request.data.get('id') + if not item_id: + return Response({'code': 400, 'msg': '缺少 id'}) + + try: + item = HuashuConfig.objects.get(id=item_id) + except HuashuConfig.DoesNotExist: + return Response({'code': 404, 'msg': '记录不存在'}) + + if 'scene_key' in request.data: + scene_key = request.data.get('scene_key') + valid_scenes = {c[0] for c in HuashuConfig.SCENE_CHOICES} + if scene_key not in valid_scenes: + return Response({'code': 400, 'msg': '无效的场景标识'}) + item.scene_key = scene_key + + if 'item_type' in request.data: + item_type = request.data.get('item_type') + valid_types = {c[0] for c in HuashuConfig.ITEM_TYPE_CHOICES} + if item_type not in valid_types: + return Response({'code': 400, 'msg': '无效的条目类型'}) + item.item_type = item_type + + if 'title' in request.data: + item.title = request.data.get('title', '') + if 'content' in request.data: + content = (request.data.get('content') or '').strip() + if not content: + return Response({'code': 400, 'msg': 'content 不能为空'}) + item.content = content + if 'confirm_text' in request.data: + item.confirm_text = request.data.get('confirm_text', '我知道了') + if 'cancel_text' in request.data: + item.cancel_text = request.data.get('cancel_text', '取消') + if 'keywords' in request.data: + keywords = request.data.get('keywords') or [] + item.keywords = keywords if isinstance(keywords, list) else [] + if 'match_mode' in request.data: + item.match_mode = request.data.get('match_mode', HuashuConfig.MATCH_CONTAINS) + if 'priority' in request.data: + item.priority = int(request.data.get('priority', 0)) + if 'is_active' in request.data: + item.is_active = bool(request.data.get('is_active')) + if 'sort_order' in request.data: + item.sort_order = int(request.data.get('sort_order', 0)) + + item.save() + return Response({'code': 0, 'msg': '更新成功', 'data': _serialize_item(item)}) + + def _delete(self, request): + item_id = request.data.get('id') + if not item_id: + return Response({'code': 400, 'msg': '缺少 id'}) + deleted, _ = HuashuConfig.objects.filter(id=item_id).delete() + if not deleted: + return Response({'code': 404, 'msg': '记录不存在'}) + return Response({'code': 0, 'msg': '删除成功'}) diff --git a/houtai/urls.py b/houtai/urls.py index ee630e3..0d3f02e 100644 --- a/houtai/urls.py +++ b/houtai/urls.py @@ -17,6 +17,7 @@ from .view import GetClubConfigView, GetCrossOrderListView, PartnerGetOrderDataV FaKuanTongJiView, FaKuanLieBiaoView, FaKuanChuLiView, FaKuanChuangJianView,HqbkxxView, BkxgView, \ CaiwuView, CwhybkhqView, HybkjtsjView, SzxxView, CwddhqlxView, CwhqjtddsjView, CwqtczhqView, KhpzhqView, ChzsgcView, \ KhgglView, ShgxgsjView, ZxkfghdsView, KptxwztjbView +from .huashu_views import HuashuListAPIView, HuashuModifyAPIView urlpatterns = [ @@ -86,6 +87,8 @@ urlpatterns = [ path('htxgfl', ModifyRateView.as_view(), name='后台修改费率'), path('hthqtcxx', PopupNoticeListAPIView.as_view(), name='后台获取弹窗配置'), path('htxgtcxx', PopupNoticeModifyAPIView.as_view(), name='后台更新创建弹窗配置'), + path('hthqhs', HuashuListAPIView.as_view(), name='后台获取话术配置'), + path('htxghs', HuashuModifyAPIView.as_view(), name='后台更新话术配置'), path('kffkdssq', FineApplyView.as_view(), name='跨平台处罚罚款申请接口'), path('kffkdssq_notify', PartnerFineNotifyView.as_view(), name='跨屏要对方平台处罚通知接口'), diff --git a/houtai/view.py b/houtai/view.py index 38b83d5..e226974 100644 --- a/houtai/view.py +++ b/houtai/view.py @@ -6696,9 +6696,9 @@ class GetWithdrawSettingsView(APIView): obj = Lilubiao.objects.filter(fadanpingtai=code).first() rate_map[leixing] = float(obj.lilu) if obj and obj.lilu is not None else 0.0 - # 1b. 订单/押金分红费率(Lilubiao:1平台订单/3商家订单/12押金管事/13商家订单管事分红) + # 1b. 订单/押金分红费率(Lilubiao:1平台订单/3商家订单/12押金管事/13管事打手接单分红/15商家派单管事分红) order_rate_map = {} - for leixing, code in [(1, '1'), (3, '3'), (12, '12'), (13, '13')]: + for leixing, code in [(1, '1'), (3, '3'), (12, '12'), (13, '13'), (15, '15')]: obj = Lilubiao.objects.filter(fadanpingtai=code).first() order_rate_map[leixing] = float(obj.lilu) if obj and obj.lilu is not None else 0.0 @@ -6773,8 +6773,8 @@ class UpdateWithdrawSettingsView(APIView): role_perms = {1: '5500a', 2: '5500b', 3: '5500c'} # 利率代码映射(1-3 有水缸限额;4审核官/5打手押金/6商家余额仅费率) rate_code_map = {1: '5', 2: '6', 3: '8', 4: '9', 5: '11', 6: '10'} - # 订单/押金分红(1平台订单/3商家订单/12押金管事/13商家订单管事分红) - order_rate_code_map = {1: '1', 3: '3', 12: '12', 13: '13'} + # 订单/押金分红(1平台订单/3商家订单/12押金管事/13管事打手接单分红/15商家派单管事分红) + order_rate_code_map = {1: '1', 3: '3', 12: '12', 13: '13', 15: '15'} # 总限额:提现类型 → TixianQuotaDefault.leixing(4~9) total_code_map = {1: 4, 2: 5, 3: 6, 4: 7, 5: 8, 6: 9} @@ -6805,11 +6805,12 @@ class UpdateWithdrawSettingsView(APIView): return Response({'code': 403, 'msg': f'无权限修改{extra_role_names[role]}总限额'}) # 订单/押金分红费率:需 5500a/b/c 任一 - for role in [1, 3, 12, 13]: + for role in [1, 3, 12, 13, 15]: if _rate_key_present(order_rates, role) and not any(p in permissions for p in ('5500a', '5500b', '5500c')): order_names = { 1: '平台订单打手分红', 3: '商家订单打手分红', - 12: '押金管事分红', 13: '管事订单分红', + 12: '押金管事分红', 13: '管事打手接单分红', + 15: '商家派单管事分红', } return Response({'code': 403, 'msg': f'无权限修改{order_names[role]}费率'}) @@ -6828,7 +6829,7 @@ class UpdateWithdrawSettingsView(APIView): obj.save() # 1b. 修改订单/押金分红费率 - for role in [1, 3, 12, 13]: + for role in [1, 3, 12, 13, 15]: val = order_rates.get(str(role), order_rates.get(role)) if val is not None: rate = Decimal(str(val)) diff --git a/peizhi/migrations/0015_huashuconfig.py b/peizhi/migrations/0015_huashuconfig.py new file mode 100644 index 0000000..d790af8 --- /dev/null +++ b/peizhi/migrations/0015_huashuconfig.py @@ -0,0 +1,153 @@ +# Generated manually for HuashuConfig +from django.db import migrations, models + + +def seed_huashu_defaults(apps, schema_editor): + HuashuConfig = apps.get_model('peizhi', 'HuashuConfig') + defaults = [ + { + 'scene_key': 'chat_image_confirm', + 'item_type': 'confirm_modal', + 'title': '温馨提示', + 'content': ( + '平台禁止任何违法违规行为,聊天内容仅限于商家与接单员正常业务对接。' + '严禁赌博、诈骗及其他非法行为,违者后果自负。' + ), + 'confirm_text': '我已知晓,继续发送', + 'cancel_text': '取消', + 'keywords': [], + 'match_mode': 'contains', + 'priority': 0, + 'is_active': True, + 'sort_order': 0, + }, + { + 'scene_key': 'cs_welcome', + 'item_type': 'text_display', + 'title': '', + 'content': '你好,请问有什么可以帮到您的?', + 'confirm_text': '我知道了', + 'cancel_text': '取消', + 'keywords': [], + 'match_mode': 'contains', + 'priority': 0, + 'is_active': True, + 'sort_order': 0, + }, + { + 'scene_key': 'merchant_dispatch_confirm', + 'item_type': 'confirm_modal', + 'title': '派单确认', + 'content': ( + '请确认订单信息无误。派单成功后将扣除相应余额,' + '请确保订单描述合法合规,仅限正常业务对接。' + ), + 'confirm_text': '确认派单', + 'cancel_text': '取消', + 'keywords': [], + 'match_mode': 'contains', + 'priority': 0, + 'is_active': True, + 'sort_order': 0, + }, + { + 'scene_key': 'cs_auto_reply', + 'item_type': 'auto_reply', + 'title': '', + 'content': '退款相关问题,请提供订单号,客服将尽快为您处理。', + 'confirm_text': '我知道了', + 'cancel_text': '取消', + 'keywords': ['退款', '怎么退', '退钱'], + 'match_mode': 'contains', + 'priority': 10, + 'is_active': True, + 'sort_order': 0, + }, + { + 'scene_key': 'cs_auto_reply', + 'item_type': 'auto_reply', + 'title': '', + 'content': '提现相关问题,请前往【我的-提现】查看规则,或留下您的具体问题。', + 'confirm_text': '我知道了', + 'cancel_text': '取消', + 'keywords': ['提现', '取钱', '到账'], + 'match_mode': 'contains', + 'priority': 9, + 'is_active': True, + 'sort_order': 1, + }, + { + 'scene_key': 'cs_auto_reply', + 'item_type': 'auto_reply', + 'title': '', + 'content': '您好,客服正在为您服务,请稍候,我们会尽快回复您。', + 'confirm_text': '我知道了', + 'cancel_text': '取消', + 'keywords': ['你好', '在吗', '有人吗'], + 'match_mode': 'contains', + 'priority': 5, + 'is_active': True, + 'sort_order': 2, + }, + ] + for row in defaults: + HuashuConfig.objects.create(**row) + + +class Migration(migrations.Migration): + + dependencies = [ + ('peizhi', '0014_alter_tixianquotadefault_leixing'), + ] + + operations = [ + migrations.CreateModel( + name='HuashuConfig', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('scene_key', models.CharField( + choices=[ + ('chat_image_confirm', '聊天发图确认'), + ('cs_welcome', '客服欢迎语'), + ('cs_auto_reply', '客服自动回复'), + ('merchant_dispatch_confirm', '商家派单确认'), + ], + db_index=True, + max_length=50, + verbose_name='场景标识', + )), + ('item_type', models.CharField( + choices=[ + ('confirm_modal', '确认弹窗'), + ('text_display', '纯文本展示'), + ('auto_reply', '自动回复'), + ], + max_length=20, + verbose_name='条目类型', + )), + ('title', models.CharField(blank=True, default='', max_length=200, verbose_name='标题')), + ('content', models.TextField(verbose_name='正文内容')), + ('confirm_text', models.CharField(blank=True, default='我知道了', max_length=50, verbose_name='确认按钮文案')), + ('cancel_text', models.CharField(blank=True, default='取消', max_length=50, verbose_name='取消按钮文案')), + ('keywords', models.JSONField(blank=True, default=list, verbose_name='触发关键词(JSON数组)')), + ('match_mode', models.CharField( + choices=[('contains', '包含匹配'), ('exact', '精确匹配')], + default='contains', + max_length=20, + verbose_name='匹配模式', + )), + ('priority', models.IntegerField(default=0, verbose_name='优先级(越大越先匹配)')), + ('is_active', models.BooleanField(default=True, verbose_name='是否启用')), + ('sort_order', models.IntegerField(default=0, verbose_name='排序')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '话术配置', + 'verbose_name_plural': '话术配置', + 'db_table': 'huashu_config', + 'ordering': ['scene_key', '-priority', 'sort_order', 'id'], + }, + ), + migrations.RunPython(seed_huashu_defaults, migrations.RunPython.noop), + ] diff --git a/peizhi/models.py b/peizhi/models.py index ec3419d..79177ce 100644 --- a/peizhi/models.py +++ b/peizhi/models.py @@ -610,4 +610,63 @@ class PopupImage(models.Model): ordering = ['sort_order', 'id'] def __str__(self): - return f'{self.popup_config.popup_id} - 图片{self.sort_order}' \ No newline at end of file + return f'{self.popup_config.popup_id} - 图片{self.sort_order}' + + +class HuashuConfig(models.Model): + """小程序话术/确认弹窗配置(聊天发图、客服欢迎/自动回复、商家派单等)""" + + SCENE_CHAT_IMAGE_CONFIRM = 'chat_image_confirm' + SCENE_CS_WELCOME = 'cs_welcome' + SCENE_CS_AUTO_REPLY = 'cs_auto_reply' + SCENE_MERCHANT_DISPATCH = 'merchant_dispatch_confirm' + + SCENE_CHOICES = ( + (SCENE_CHAT_IMAGE_CONFIRM, '聊天发图确认'), + (SCENE_CS_WELCOME, '客服欢迎语'), + (SCENE_CS_AUTO_REPLY, '客服自动回复'), + (SCENE_MERCHANT_DISPATCH, '商家派单确认'), + ) + + ITEM_CONFIRM = 'confirm_modal' + ITEM_TEXT = 'text_display' + ITEM_AUTO_REPLY = 'auto_reply' + + ITEM_TYPE_CHOICES = ( + (ITEM_CONFIRM, '确认弹窗'), + (ITEM_TEXT, '纯文本展示'), + (ITEM_AUTO_REPLY, '自动回复'), + ) + + MATCH_CONTAINS = 'contains' + MATCH_EXACT = 'exact' + + MATCH_MODE_CHOICES = ( + (MATCH_CONTAINS, '包含匹配'), + (MATCH_EXACT, '精确匹配'), + ) + + scene_key = models.CharField(max_length=50, choices=SCENE_CHOICES, db_index=True, verbose_name='场景标识') + item_type = models.CharField(max_length=20, choices=ITEM_TYPE_CHOICES, verbose_name='条目类型') + title = models.CharField(max_length=200, blank=True, default='', verbose_name='标题') + content = models.TextField(verbose_name='正文内容') + confirm_text = models.CharField(max_length=50, blank=True, default='我知道了', verbose_name='确认按钮文案') + cancel_text = models.CharField(max_length=50, blank=True, default='取消', verbose_name='取消按钮文案') + keywords = models.JSONField(default=list, blank=True, verbose_name='触发关键词(JSON数组)') + match_mode = models.CharField( + max_length=20, choices=MATCH_MODE_CHOICES, default=MATCH_CONTAINS, verbose_name='匹配模式' + ) + priority = models.IntegerField(default=0, verbose_name='优先级(越大越先匹配)') + is_active = models.BooleanField(default=True, verbose_name='是否启用') + sort_order = models.IntegerField(default=0, verbose_name='排序') + created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'huashu_config' + verbose_name = '话术配置' + verbose_name_plural = '话术配置' + ordering = ['scene_key', '-priority', 'sort_order', 'id'] + + def __str__(self): + return f'{self.get_scene_key_display()} - {self.get_item_type_display()}' \ No newline at end of file diff --git a/peizhi/urls.py b/peizhi/urls.py index 7d7ed8d..7bb6dda 100644 --- a/peizhi/urls.py +++ b/peizhi/urls.py @@ -6,6 +6,7 @@ from .views import ShangpinGonggaoView, AdminUpdateConfigView, AdminUploadImageV KehuTianxieDingdanView, AdminSelfBatchGenerateLinkView, ShangjiaMobanListViewpl, GuanshiQRCodeView,\ GuanZhuAListView, ZuzhangHaibaoView,GetDynamicConfigView, PopupConfigView, GetWithdrawModeView,\ CheckPhoneAuthView,ShangjiaLianjieListView +from .views_huashu import HuashuQueryView, HuashuMatchView urlpatterns = [ path('shangpingonggao/', ShangpinGonggaoView.as_view(), name='商品公告轮播获取'), @@ -40,4 +41,6 @@ urlpatterns = [ path('hqtxzsym', GetWithdrawModeView.as_view(), name='小程序获取提现方式'), path('yhbdsjh', CheckPhoneAuthView.as_view(), name='判断是否需要手机号绑定'), path('hqsjlslj', ShangjiaLianjieListView.as_view(), name='商家获取生成链接'), + path('huashuhq', HuashuQueryView.as_view(), name='小程序获取话术配置'), + path('huashu_match', HuashuMatchView.as_view(), name='小程序客服自动回复匹配'), ] \ No newline at end of file diff --git a/peizhi/views_huashu.py b/peizhi/views_huashu.py new file mode 100644 index 0000000..66a9c4f --- /dev/null +++ b/peizhi/views_huashu.py @@ -0,0 +1,133 @@ +# peizhi/views_huashu.py — 小程序话术查询与自动回复匹配 +import json + +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated +from rest_framework import status + +from .models import HuashuConfig + + +def _serialize_huashu_item(item): + return { + 'id': item.id, + 'scene_key': item.scene_key, + 'item_type': item.item_type, + 'title': item.title, + 'content': item.content, + 'confirm_text': item.confirm_text, + 'cancel_text': item.cancel_text, + 'keywords': item.keywords or [], + 'match_mode': item.match_mode, + 'priority': item.priority, + 'is_active': item.is_active, + 'sort_order': item.sort_order, + } + + +def _match_auto_reply(user_text, queryset): + text = (user_text or '').strip().lower() + if not text: + return None + + for item in queryset: + keywords = item.keywords or [] + if not isinstance(keywords, list): + try: + keywords = json.loads(keywords) if keywords else [] + except (TypeError, ValueError): + keywords = [] + + for kw in keywords: + kw_norm = str(kw).strip().lower() + if not kw_norm: + continue + if item.match_mode == HuashuConfig.MATCH_EXACT and text == kw_norm: + return item + if item.match_mode == HuashuConfig.MATCH_CONTAINS and kw_norm in text: + return item + return None + + +class HuashuQueryView(APIView): + """ + 批量获取话术配置 + POST /peizhi/huashuhq + 请求: { "scene_keys": ["chat_image_confirm", "cs_welcome"] } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + scene_keys = request.data.get('scene_keys') or [] + if not isinstance(scene_keys, list) or not scene_keys: + return Response({'code': 400, 'msg': '缺少参数 scene_keys'}, status=status.HTTP_400_BAD_REQUEST) + + allowed = {c[0] for c in HuashuConfig.SCENE_CHOICES} + result = {} + + for scene_key in scene_keys: + if scene_key not in allowed: + continue + + if scene_key == HuashuConfig.SCENE_CS_AUTO_REPLY: + items = HuashuConfig.objects.filter( + scene_key=scene_key, + item_type=HuashuConfig.ITEM_AUTO_REPLY, + is_active=True, + ).order_by('-priority', 'sort_order', 'id') + result[scene_key] = { + 'item_type': HuashuConfig.ITEM_AUTO_REPLY, + 'items': [_serialize_huashu_item(i) for i in items], + } + else: + item = HuashuConfig.objects.filter( + scene_key=scene_key, + is_active=True, + ).order_by('-priority', 'sort_order', 'id').first() + if item: + result[scene_key] = _serialize_huashu_item(item) + else: + result[scene_key] = None + + return Response({'code': 200, 'msg': '获取成功', 'data': result}) + + +class HuashuMatchView(APIView): + """ + 客服自动回复关键词匹配 + POST /peizhi/huashu_match + 请求: { "scene_key": "cs_auto_reply", "user_text": "怎么退款" } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + scene_key = request.data.get('scene_key', HuashuConfig.SCENE_CS_AUTO_REPLY) + user_text = request.data.get('user_text', '') + + if scene_key != HuashuConfig.SCENE_CS_AUTO_REPLY: + return Response({'code': 400, 'msg': 'scene_key 仅支持 cs_auto_reply'}, status=status.HTTP_400_BAD_REQUEST) + + queryset = HuashuConfig.objects.filter( + scene_key=scene_key, + item_type=HuashuConfig.ITEM_AUTO_REPLY, + is_active=True, + ).order_by('-priority', 'sort_order', 'id') + + matched = _match_auto_reply(user_text, queryset) + if matched: + return Response({ + 'code': 200, + 'msg': '匹配成功', + 'data': { + 'matched': True, + 'content': matched.content, + 'rule_id': matched.id, + }, + }) + + return Response({ + 'code': 200, + 'msg': '未匹配', + 'data': {'matched': False, 'content': '', 'rule_id': None}, + }) diff --git a/shangpin/migrations/0010_gsfenhong_fenhong_leixing_unique.py b/shangpin/migrations/0010_gsfenhong_fenhong_leixing_unique.py new file mode 100644 index 0000000..90dcab4 --- /dev/null +++ b/shangpin/migrations/0010_gsfenhong_fenhong_leixing_unique.py @@ -0,0 +1,42 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shangpin', '0009_gsfenhong_fenhong_leixing'), + ] + + operations = [ + migrations.AlterField( + model_name='gsfenhong', + name='dingdan_id', + field=models.CharField(db_index=True, max_length=32, verbose_name='订单ID'), + ), + migrations.AlterField( + model_name='gsfenhong', + name='dashouid', + field=models.CharField(db_index=True, max_length=7, verbose_name='关联用户ID'), + ), + migrations.AlterField( + model_name='gsfenhong', + name='fenhong_leixing', + field=models.PositiveSmallIntegerField( + choices=[ + (1, '会员分红'), + (2, '押金分红'), + (3, '商家订单分红'), + (4, '商家派单管事分红'), + ], + default=1, + verbose_name='分红类型', + ), + ), + migrations.AddConstraint( + model_name='gsfenhong', + constraint=models.UniqueConstraint( + fields=('dingdan_id', 'fenhong_leixing'), + name='unique_gsfenhong_dingdan_leixing', + ), + ), + ] diff --git a/shangpin/models.py b/shangpin/models.py index 010c271..d9a692a 100644 --- a/shangpin/models.py +++ b/shangpin/models.py @@ -193,9 +193,9 @@ class Czjilu(models.Model): class Gsfenhong(models.Model): - dingdan_id = models.CharField(max_length=32, unique=True, verbose_name='订单ID') + dingdan_id = models.CharField(max_length=32, db_index=True, verbose_name='订单ID') guanshi = models.CharField(max_length=7, db_index=True, verbose_name='管事ID') - dashouid = models.CharField(max_length=7,unique=True, db_index=True,verbose_name='打手ID') + dashouid = models.CharField(max_length=7, db_index=True, verbose_name='关联用户ID') shuoming = models.CharField(max_length=100, null=True, blank=True, verbose_name='订单说明') fenhong = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='订单分红金额') avatar = models.CharField(max_length=500, null=True, blank=True, verbose_name='打手头像') @@ -206,7 +206,7 @@ class Gsfenhong(models.Model): huiyuan_id = models.CharField(max_length=6, null=True, blank=True, verbose_name='会员ID') fenhong_leixing = models.PositiveSmallIntegerField( default=1, - choices=[(1, '会员分红'), (2, '押金分红'), (3, '商家订单分红')], + choices=[(1, '会员分红'), (2, '押金分红'), (3, '商家订单分红'), (4, '商家派单管事分红')], verbose_name='分红类型' ) @@ -217,7 +217,12 @@ class Gsfenhong(models.Model): db_table = 'gsfenhong' verbose_name = '管事分红记录' verbose_name_plural = '管事分红记录' - # 为高频查询字段添加索引 + constraints = [ + models.UniqueConstraint( + fields=['dingdan_id', 'fenhong_leixing'], + name='unique_gsfenhong_dingdan_leixing', + ), + ] indexes = [ models.Index(fields=['guanshi']), models.Index(fields=['dashouid']), diff --git a/utils/chat_utils.py b/utils/chat_utils.py index d6e6bed..a377dc5 100644 --- a/utils/chat_utils.py +++ b/utils/chat_utils.py @@ -1,9 +1,10 @@ # utils/chat_utils.py import json import logging +import time import requests from django.conf import settings -from dingdan.models import Dingdan, DingdanPingtai, DingdanShangjia +from dingdan.models import Dingdan, DingdanPingtai, DingdanShangjia, Liaotian from peizhi.models import ClubConfig from yonghu.models import UserMain, UserDashou, UserBoss, UserShangjia @@ -137,6 +138,118 @@ def _send_group_message(appkey, secret, group_id, sender_id, sender_name, sender logger.error(f"发送群聊消息异常: {e}", exc_info=True) return False +def _retry_call(func, max_retries=5, delay=0.4, *args, **kwargs): + """对 GoEasy 等外部调用做短重试,提高抢单后建聊成功率""" + for attempt in range(1, max_retries + 1): + try: + if func(*args, **kwargs): + return True + except Exception as e: + logger.warning(f"{func.__name__} 第{attempt}次失败: {e}") + if attempt < max_retries: + time.sleep(delay * attempt) + return False + +def _send_private_init_message(appkey, secret, sender_id, sender_name, sender_avatar, + receiver_id, receiver_name, receiver_avatar, message_text): + if not appkey: + appkey = _get_self_goeasy_appkey() + if not secret: + secret = _get_self_goeasy_secret() + if not appkey: + return False + + url = 'https://rest-hangzhou.goeasy.io/v2/im/message' + request_body = { + "appkey": appkey, + "senderId": sender_id, + "senderData": {"avatar": sender_avatar or "", "name": sender_name or sender_id}, + "to": { + "type": "private", + "id": receiver_id, + "data": {"avatar": receiver_avatar or "", "name": receiver_name or receiver_id} + }, + "type": "text", + "payload": message_text + } + headers = {"Content-Type": "application/json"} + if secret: + headers["Authorization"] = f"Bearer {secret}" + try: + resp = requests.post(url, headers=headers, json=request_body, timeout=10) + if resp.status_code == 200: + return True + logger.error(f"私聊初始化消息失败: {resp.status_code} {resp.text}") + return False + except Exception as e: + logger.error(f"私聊初始化消息异常: {e}", exc_info=True) + return False + +def _ensure_liaotian_record(dingdan_id, partner_goeasy_id, dashou_goeasy_id): + try: + Liaotian.objects.update_or_create( + dingdan_id=dingdan_id, + defaults={ + 'user1_id': partner_goeasy_id, + 'user2_id': dashou_goeasy_id, + 'zhuangtai': 1, + } + ) + return True + except Exception as e: + logger.error(f"写入 Liaotian 记录失败 order={dingdan_id}: {e}", exc_info=True) + return False + +def _establish_private_chat(appkey, secret, dashou_goeasy_id, dashou_name, dashou_avatar, + partner_goeasy_id, partner_name, partner_avatar): + msg_dashou = "订单已接单,可在此处沟通。" + msg_partner = "打手已接单,可在此处沟通。" + ok1 = _retry_call( + _send_private_init_message, 3, 0.4, + appkey, secret, dashou_goeasy_id, dashou_name, dashou_avatar, + partner_goeasy_id, partner_name, partner_avatar, msg_dashou + ) + ok2 = _retry_call( + _send_private_init_message, 3, 0.4, + appkey, secret, partner_goeasy_id, partner_name, partner_avatar, + dashou_goeasy_id, dashou_name, dashou_avatar, msg_partner + ) + return ok1 or ok2 + +def _resolve_goeasy_user_display(goeasy_id): + if not goeasy_id: + return "用户", _full_local_avatar('') + avatar = _full_local_avatar('') + name = f"用户{goeasy_id[-6:]}" + try: + if goeasy_id.startswith('Boss'): + uid = goeasy_id[4:] + boss_user = UserMain.objects.get(yonghuid=uid) + avatar = _full_local_avatar(boss_user.avatar or '') + boss_profile = UserBoss.objects.get(user=boss_user) + if boss_profile.nickname: + name = boss_profile.nickname + elif goeasy_id.startswith('Sj'): + uid = goeasy_id[2:] + sj_user = UserMain.objects.get(yonghuid=uid) + avatar = _full_local_avatar(sj_user.avatar or '') + try: + sj_profile = UserShangjia.objects.get(user=sj_user) + if sj_profile.nicheng: + name = sj_profile.nicheng + except Exception: + pass + elif goeasy_id.startswith('Ds'): + uid = goeasy_id[2:] + ds_user = UserMain.objects.get(yonghuid=uid) + avatar = _full_local_avatar(ds_user.avatar or '') + ds_profile = UserDashou.objects.get(user=ds_user) + if ds_profile.nicheng: + name = ds_profile.nicheng + except Exception as e: + logger.warning(f"解析 GoEasy 用户展示信息失败 {goeasy_id}: {e}") + return name, avatar + # ========== 核心入口 ========== def establish_order_chat(dingdan_id): try: @@ -177,6 +290,20 @@ def establish_order_chat(dingdan_id): else: return False +def establish_order_chat_with_retry(dingdan_id, max_rounds=3): + """抢单后建聊入口:多轮重试,尽量保证建立成功""" + for round_num in range(1, max_rounds + 1): + try: + if establish_order_chat(dingdan_id): + logger.info(f"订单 {dingdan_id} 建聊成功(第{round_num}轮)") + return True + except Exception as e: + logger.error(f"订单 {dingdan_id} 建聊第{round_num}轮异常: {e}", exc_info=True) + if round_num < max_rounds: + time.sleep(0.8 * round_num) + logger.error(f"订单 {dingdan_id} 建聊最终失败") + return False + # ========== 普通订单 / 我方派单 ========== def _handle_local_order(order, dashou_goeasy_id, dashou_name, dashou_avatar, appkey, secret): group_id = f"group_{order.dingdan_id}" @@ -185,36 +312,61 @@ def _handle_local_order(order, dashou_goeasy_id, dashou_name, dashou_avatar, app partner_goeasy_id, partner_name, partner_avatar = _get_local_partner_info(order) if not partner_goeasy_id: - return False + logger.error(f"订单 {order.dingdan_id} 无法获取下单方 GoEasy 标识") + subscribe_ok = _retry_call( + _subscribe_users_to_group, 5, 0.4, + [dashou_goeasy_id], [group_id], appkey, secret + ) + msg_ok = _retry_call( + _send_group_message, 3, 0.4, + appkey, secret, group_id, dashou_goeasy_id, dashou_name, group_avatar, + "订单已接单,请等待对方上线。", None, group_name=group_name, + order_id=order.dingdan_id, is_cross=order.is_cross + ) + return subscribe_ok or msg_ok - # 订阅双方 - if not _subscribe_users_to_group([dashou_goeasy_id, partner_goeasy_id], [group_id], appkey, secret): - return False + subscribe_ok = _retry_call( + _subscribe_users_to_group, 5, 0.4, + [dashou_goeasy_id, partner_goeasy_id], [group_id], appkey, secret + ) - # 1. 打手发送初始化消息 init_msg_text = f"订单已接单,内容:{order.jieshao},备注:{order.beizhu},游戏ID:{order.nicheng}" - success1 = _send_group_message( + success1 = _retry_call( + _send_group_message, 3, 0.4, appkey, secret, group_id, dashou_goeasy_id, dashou_name, group_avatar, init_msg_text, None, group_name=group_name, order_id=order.dingdan_id, is_cross=order.is_cross ) - # 2. 【新增】派单方也发一条消息,确保他也能看到群聊 - partner_msg = f"订单已确认,请开始服务。" - success2 = _send_group_message( + partner_msg = "订单已确认,请开始服务。" + success2 = _retry_call( + _send_group_message, 3, 0.4, appkey, secret, group_id, partner_goeasy_id, partner_name, partner_avatar, partner_msg, None, group_name=group_name, order_id=order.dingdan_id, is_cross=order.is_cross ) - return success1 and success2 + liaotian_ok = _ensure_liaotian_record(order.dingdan_id, partner_goeasy_id, dashou_goeasy_id) + private_ok = _establish_private_chat( + appkey, secret, dashou_goeasy_id, dashou_name, dashou_avatar, + partner_goeasy_id, partner_name, partner_avatar + ) + + result = subscribe_ok or success1 or success2 or liaotian_ok or private_ok + if not result: + logger.error(f"订单 {order.dingdan_id} 群聊/私聊均未建立成功") + return result def _get_local_partner_info(order): """本地订单下单方:老板或商家""" - if order.fadan_pingtai == 1: # 老板 + if order.fadan_pingtai == 1: try: ext = order.pingtai_kuozhan laoban_id = ext.laoban_id + except Exception: + ext = DingdanPingtai.objects.filter(dingdan__dingdan_id=order.dingdan_id).first() + laoban_id = ext.laoban_id if ext else None + if laoban_id: avatar_full = _full_local_avatar('') nickname = f'老板{laoban_id[:6]}' try: @@ -226,14 +378,20 @@ def _get_local_partner_info(order): except Exception: pass return f"Boss{laoban_id}", nickname, avatar_full - except Exception as e: - logger.error(f"获取老板信息失败: {e}") - return None, None, None - elif order.fadan_pingtai == 2: # 商家 + elif order.fadan_pingtai == 2: try: ext = order.shangjia_kuozhan shangjia_id = ext.shangjia_id nickname = ext.sjnicheng or f'商家{shangjia_id[:6]}' + except Exception: + ext = DingdanShangjia.objects.filter(dingdan__dingdan_id=order.dingdan_id).first() + if not ext: + shangjia_id = None + nickname = '商家' + else: + shangjia_id = ext.shangjia_id + nickname = ext.sjnicheng or f'商家{shangjia_id[:6]}' + if shangjia_id: avatar_full = _full_local_avatar('') try: sj_user = UserMain.objects.get(yonghuid=shangjia_id) @@ -241,13 +399,11 @@ def _get_local_partner_info(order): except Exception: pass return f"Sj{shangjia_id}", nickname, avatar_full - except Exception as e: - logger.error(f"获取商家信息失败: {e}") - return None, None, None - else: - if order.user1_id: - return order.user1_id, "用户", _full_local_avatar('') - return None, None, None + + if order.user1_id: + name, avatar = _resolve_goeasy_user_display(order.user1_id) + return order.user1_id, name, avatar + return None, None, None # ========== 跨平台 + 对方派单 ========== def _handle_cross_partner_order(order, dashou_goeasy_id, dashou_name, dashou_avatar, appkey, secret): @@ -370,5 +526,16 @@ def _get_cross_partner_identity(order, partner_order_id): def _create_local_group_only(order, dashou_goeasy_id, dashou_name, dashou_avatar, appkey, secret): group_id = f"group_{order.dingdan_id}" - _subscribe_users_to_group([dashou_goeasy_id], [group_id], appkey, secret) - return False \ No newline at end of file + group_name = (order.jieshao[:20] + '…') if order.jieshao and len(order.jieshao) > 20 else (order.jieshao or f"订单{order.dingdan_id}") + group_avatar = _full_local_avatar(order.tupian) if order.tupian else _full_local_avatar('') + subscribe_ok = _retry_call( + _subscribe_users_to_group, 5, 0.4, + [dashou_goeasy_id], [group_id], appkey, secret + ) + msg_ok = _retry_call( + _send_group_message, 3, 0.4, + appkey, secret, group_id, dashou_goeasy_id, dashou_name, group_avatar, + "订单已接单。", None, group_name=group_name, order_id=order.dingdan_id, + is_cross=order.is_cross + ) + return subscribe_ok or msg_ok \ No newline at end of file diff --git a/yonghu/tixian_shenhe_services.py b/yonghu/tixian_shenhe_services.py index 634e60a..f7ae212 100644 --- a/yonghu/tixian_shenhe_services.py +++ b/yonghu/tixian_shenhe_services.py @@ -56,9 +56,15 @@ LEIXING_RATE_KEY = { 6: '10', } -# 进行中的审核状态(不允许重复提交申请) +# 进行中的审核状态(仅用于统计/展示,不再阻止用户并行提交多笔申请) PENDING_AUDIT_STATUSES = [1, 4, 6] +# 微信商家转账单笔到账上限(元):实际到账 shijidaozhang 须严格小于 200 +WECHAT_TRANSFER_MAX = decimal.Decimal('200.00') + +# 仍持有申请扣款、可驳回退款的审核状态 +REFUNDABLE_AUDIT_STATUSES = [1, 4, 6] + DAKUAN_MODE_MANUAL = 1 DAKUAN_MODE_AUTO = 2 @@ -154,6 +160,36 @@ def calc_fee_amounts(jine, feilv): return shouxufei, shijidaozhang +def shijidaozhang_exceeds_wechat_limit(shijidaozhang): + """实际到账 >= 200 元时微信官方不予转账""" + try: + amt = decimal.Decimal(str(shijidaozhang)).quantize(decimal.Decimal('0.01')) + except (ValueError, TypeError, decimal.InvalidOperation): + return True + return amt >= WECHAT_TRANSFER_MAX + + +def wechat_limit_apply_message(shijidaozhang): + amt = decimal.Decimal(str(shijidaozhang)).quantize(decimal.Decimal('0.01')) + return ( + f'扣除手续费后实际到账金额为{amt}元,已达到或超过微信官方单笔限额200元,无法提交提现申请。' + f'请减小申请金额,确保到账金额小于200元后再试。' + ) + + +def wechat_limit_collect_message(shijidaozhang): + amt = decimal.Decimal(str(shijidaozhang)).quantize(decimal.Decimal('0.01')) + return ( + f'该笔提现实际到账金额为{amt}元,已达到或超过微信官方单笔限额200元,无法收款。' + f'系统已驳回并将申请金额退回您对应身份账户余额,请重新发起到账金额小于200元的提现。' + ) + + +def assert_shijidaozhang_below_wechat_limit(shijidaozhang): + if shijidaozhang_exceeds_wechat_limit(shijidaozhang): + raise ValueError(wechat_limit_apply_message(shijidaozhang)) + + def check_dashou_has_valid_huiyuan(yonghuid): """ 打手至少持有一个未过期会员(Huiyuangoumai + jiance_shifou_daoqi 同步状态) @@ -763,8 +799,61 @@ def apply_transfer_success_quota(auto_record): ) +def reject_audit_and_refund(user_main, audit, reason): + """ + 驳回审核单并按 leixing 将 shenqing_jine 退回对应身份余额(须在业务层调用)。 + 返回 (True, 用户提示文案) 或 (False, 说明) + """ + if audit is None: + return False, '审核记录不存在' + + reason = (reason or '提现失败').strip() + audit_id = audit.id if hasattr(audit, 'id') else audit + + with transaction.atomic(): + audit = TixianShenheJilu.objects.select_for_update().get(pk=audit_id) + + if audit.yonghuid != user_main.yonghuid: + return False, '审核单不属于当前用户' + + if audit.zhuangtai == 2: + return False, '该提现已完成,请勿重复操作' + + if audit.zhuangtai in (3, 5): + return False, audit.bhliyou or audit.fail_reason or reason + + if audit.zhuangtai not in REFUNDABLE_AUDIT_STATUSES: + return False, f'当前状态不可驳回退款(状态{audit.zhuangtai})' + + pending_records = list( + TixianAutoRecord.objects.select_for_update().filter( + shenhe_danhao=audit.shenhe_danhao, + zhuangtai=0, + ) + ) + for rec in pending_records: + rec.zhuangtai = 2 + rec.fail_reason = reason[:500] + rec.save(update_fields=['zhuangtai', 'fail_reason', 'update_time']) + release_collect_quota_for_record(rec) + + sync_audit_and_jilu_status( + audit, 3, + bhliyou=reason, + fail_reason=reason, + ) + refund_balance(user_main, audit.leixing, audit.shenqing_jine) + + logger.info( + '提现驳回退款 shenhe_danhao=%s yonghuid=%s leixing=%s refund=%s reason=%s', + audit.shenhe_danhao, audit.yonghuid, audit.leixing, audit.shenqing_jine, reason, + ) + + return True, reason + + def refund_balance(user_main, leixing, jine): - """审核拒绝时退还可到账金额 shijidaozhang(须在 transaction.atomic 内;手续费已在申请扣款时扣除不退还)""" + """审核驳回时退还申请扣款额 shenqing_jine(须在 transaction.atomic 内)""" if leixing == 1: dashou = UserDashou.objects.select_for_update().get(user=user_main) dashou.yue += jine @@ -958,41 +1047,119 @@ def _apply_platform_accounting(auto_record): ) +def _wx_transfer_state(wx_data): + """兼容查单 state 与回调 transfer_status 字段""" + if not wx_data or wx_data.get('_not_found'): + return '' + return (wx_data.get('state') or wx_data.get('transfer_status') or '').upper() + + +def _wx_state_is_success(wx_data): + return _wx_transfer_state(wx_data) in WX_STATE_SUCCESS + + +def _sync_audit_success_for_auto_record(ar): + """打款已成功时,强制同步审核表+提现记录表为 2=提现成功""" + if not ar.shenhe_danhao: + return False + try: + audit = TixianShenheJilu.objects.select_for_update().get(shenhe_danhao=ar.shenhe_danhao) + except TixianShenheJilu.DoesNotExist: + logger.warning('打款成功但审核记录不存在: %s', ar.shenhe_danhao) + return False + if audit.zhuangtai != 2: + sync_audit_and_jilu_status(audit, 2) + logger.info( + '已补齐提现成功状态 shenhe_danhao=%s audit_id=%s jilu_id=%s', + ar.shenhe_danhao, audit.id, audit.tixianjilu_id, + ) + return True + return False + + +def _apply_transfer_success_side_effects(ar): + """记账/限额等非关键副作用(独立事务),失败不回滚主状态""" + try: + with transaction.atomic(): + if ar.shenhe_danhao and not _quota_already_reserved_for_record(ar): + apply_transfer_success_quota(ar) + _apply_platform_accounting(ar) + except Exception as e: + logger.error('打款成功副作用失败 bill=%s err=%s', ar.tixian_id, e, exc_info=True) + + +def ensure_shenhe_collect_completed(shenhe_danhao): + """ + 根据本地打款记录或微信查单,强制补齐「提现成功」状态。 + 用于:首次回调/确认漏写、用户再次点收款时对账修复。 + 返回 (True, msg) 或 (False, msg) + """ + if not shenhe_danhao: + return False, '缺少审核单号' + + records = list( + TixianAutoRecord.objects.filter(shenhe_danhao=shenhe_danhao).order_by('-create_time') + ) + if not records: + return False, '未找到打款记录' + + for rec in records: + if rec.zhuangtai == 1: + with transaction.atomic(): + ar = TixianAutoRecord.objects.select_for_update().get(pk=rec.pk) + _sync_audit_success_for_auto_record(ar) + return True, '提现已成功到账' + + wx_data = query_wechat_transfer_bill(rec.tixian_id) + if wx_data is None: + continue + if _wx_state_is_success(wx_data): + mark_transfer_success( + rec, + wechat_transfer_no=wx_data.get('transfer_bill_no') or wx_data.get('transfer_no'), + ) + return True, '提现已成功到账' + + return False, '该提现已完成,请勿重复操作' + + def mark_transfer_success(auto_record, *, wechat_transfer_no=None, with_accounting=True): """ 打款成功落库(tixianqr / callback / 查单对账 共用) - select_for_update + zhuangtai==0 才更新并记账,防双写 - 返回 True=本次新标记成功;False=已处理过 + 状态同步与记账分离:记账失败不影响状态落库;打款已成功时必定补齐审核单状态。 + 返回 True=本次新标记打款成功;False=打款记录早已是成功态(仍可能补齐了审核单) """ + newly_marked = False + ar = None with transaction.atomic(): ar = TixianAutoRecord.objects.select_for_update().get(pk=auto_record.pk) - if ar.zhuangtai != 0: - return False - ar.zhuangtai = 1 - update_fields = ['zhuangtai', 'update_time'] - if wechat_transfer_no: + if ar.zhuangtai == 0: + ar.zhuangtai = 1 + update_fields = ['zhuangtai', 'update_time'] + if wechat_transfer_no: + ar.wechat_transfer_no = wechat_transfer_no + update_fields.append('wechat_transfer_no') + ar.save(update_fields=update_fields) + newly_marked = True + elif ar.zhuangtai == 1: + if wechat_transfer_no and ar.wechat_transfer_no != wechat_transfer_no: + ar.wechat_transfer_no = wechat_transfer_no + ar.save(update_fields=['wechat_transfer_no', 'update_time']) + elif ar.zhuangtai == 2 and wechat_transfer_no: + # 本地误标失败但微信已成功:恢复为成功 + ar.zhuangtai = 1 ar.wechat_transfer_no = wechat_transfer_no - update_fields.append('wechat_transfer_no') - ar.save(update_fields=update_fields) + ar.fail_reason = None + ar.save(update_fields=['zhuangtai', 'wechat_transfer_no', 'fail_reason', 'update_time']) + newly_marked = True - if ar.shenhe_danhao: - try: - audit = TixianShenheJilu.objects.select_for_update().get( - shenhe_danhao=ar.shenhe_danhao, - ) - if audit.zhuangtai != 2: - sync_audit_and_jilu_status(audit, 2) - except TixianShenheJilu.DoesNotExist: - logger.warning('打款成功但审核记录不存在: %s', ar.shenhe_danhao) + _sync_audit_success_for_auto_record(ar) - if ar.shenhe_danhao and not _quota_already_reserved_for_record(ar): - apply_transfer_success_quota(ar) + if with_accounting and newly_marked and ar is not None: + _apply_transfer_success_side_effects(ar) - if with_accounting: - _apply_platform_accounting(ar) - - return True + return newly_marked def _sync_record_from_wx_query(auto_record, wx_data): @@ -1011,14 +1178,14 @@ def _sync_record_from_wx_query(auto_record, wx_data): release_collect_quota_for_record(auto_record) return RECONCILE_ALLOW_NEW, {} - state = (wx_data.get('state') or '').upper() + state = _wx_transfer_state(wx_data) if state in WX_STATE_SUCCESS: mark_transfer_success( auto_record, wechat_transfer_no=wx_data.get('transfer_bill_no') or wx_data.get('transfer_no'), ) - return RECONCILE_COMPLETED, {'msg': '该提现已完成,请勿重复操作'} + return RECONCILE_COMPLETED, {'msg': '提现已成功到账', 'synced': True} if state in WX_STATE_WAIT: package = wx_data.get('package_info') or _parse_package_info(auto_record.wechat_package) @@ -1095,13 +1262,8 @@ def reconcile_shenhe_wechat_bills(shenhe_danhao): ) if any(r.zhuangtai == 1 for r in records): - try: - audit = TixianShenheJilu.objects.get(shenhe_danhao=shenhe_danhao) - if audit.zhuangtai != 2: - sync_audit_and_jilu_status(audit, 2) - except TixianShenheJilu.DoesNotExist: - pass - return RECONCILE_COMPLETED, {'msg': '该提现已完成,请勿重复操作'} + ok, msg = ensure_shenhe_collect_completed(shenhe_danhao) + return RECONCILE_COMPLETED, {'msg': msg, 'synced': ok} if not records: return RECONCILE_ALLOW_NEW, {} @@ -1109,7 +1271,8 @@ def reconcile_shenhe_wechat_bills(shenhe_danhao): pending_hit = None for rec in records: if rec.zhuangtai == 1: - return RECONCILE_COMPLETED, {'msg': '该提现已完成,请勿重复操作'} + ok, msg = ensure_shenhe_collect_completed(shenhe_danhao) + return RECONCILE_COMPLETED, {'msg': msg, 'synced': ok} wx_data = query_wechat_transfer_bill(rec.tixian_id) if wx_data is None: @@ -1271,9 +1434,6 @@ def create_audit_application(user_main, leixing, jine): """ yonghuid = user_main.yonghuid - if has_pending_audit(yonghuid): - raise ValueError('账户余额不足或您有进行中的提现申请,请等待处理完成或10分钟后后再提交') - ok, msg, extra = validate_withdraw_eligibility(user_main, leixing, jine) if not ok: raise ValueError(msg) @@ -1281,7 +1441,8 @@ def create_audit_application(user_main, leixing, jine): feilv = get_tixian_feilv(leixing) shouxufei, shijidaozhang = calc_fee_amounts(jine, feilv) if shijidaozhang <= 0: - raise ValueError('提现金额过低,扣除手续费后无实际到账') + raise ValueError('提现金额过低,扣除手续费后无实际到账金额,请增加申请金额') + assert_shijidaozhang_below_wechat_limit(shijidaozhang) shenhe_danhao = generate_shenhe_danhao() nicheng = extra.get('nicheng', '') diff --git a/yonghu/tixian_shenhe_views.py b/yonghu/tixian_shenhe_views.py index ae2b47c..6f9e309 100644 --- a/yonghu/tixian_shenhe_views.py +++ b/yonghu/tixian_shenhe_views.py @@ -27,13 +27,17 @@ from .tixian_shenhe_services import ( RECONCILE_WAIT_CONFIRM, check_collect_quota_limits, create_audit_application, + ensure_shenhe_collect_completed, get_audit_for_collect, handle_post_transfer_failure, reconcile_shenhe_wechat_bills, + reject_audit_and_refund, release_collect_quota_for_record, reserve_collect_quota_limits, resolve_collect_context, + shijidaozhang_exceeds_wechat_limit, validate_collect_eligibility, + wechat_limit_collect_message, ) logger = logging.getLogger('yonghu.tixian_shenhe') @@ -219,6 +223,12 @@ def process_audit_collect(request): tixianjilu_id_val = ctx['jilu'].id if ctx.get('jilu') else audit.tixianjilu_id leixing = audit.leixing + # 微信单笔到账限额:与申请阶段同一标准(shijidaozhang >= 200 驳回并退款) + if shijidaozhang_exceeds_wechat_limit(audit.shijidaozhang): + reason = wechat_limit_collect_message(audit.shijidaozhang) + _, msg = reject_audit_and_refund(user_main, audit, reason) + return Response({'code': 400, 'msg': msg}) + # 二次资格校验:不通过仅返回错误,审核单保持待收款(6),不退款,用户补齐资质后可再点收款 collect_ok, collect_msg = validate_collect_eligibility(user_main, leixing) if not collect_ok: @@ -241,7 +251,13 @@ def process_audit_collect(request): try: action, payload = reconcile_shenhe_wechat_bills(shenhe_danhao) if action == RECONCILE_COMPLETED: - return Response({'code': 8, 'msg': payload.get('msg', '该提现已完成,请勿重复操作')}) + ensure_shenhe_collect_completed(shenhe_danhao) + msg = payload.get('msg', '提现已成功到账') if isinstance(payload, dict) else '提现已成功到账' + return Response({ + 'code': 0, + 'msg': msg, + 'data': {'already_completed': True, 'tixianjilu_id': tixianjilu_id_val}, + }) if action == RECONCILE_WAIT_CONFIRM: quota_resp = _verify_collect_quota_or_response(user_main, audit, shenhe_danhao) if quota_resp: @@ -267,6 +283,11 @@ def process_audit_collect(request): shijidaozhang = audit.shijidaozhang feilv = audit.feilv + if shijidaozhang_exceeds_wechat_limit(shijidaozhang): + reason = wechat_limit_collect_message(shijidaozhang) + _, msg = reject_audit_and_refund(user_main, audit, reason) + return Response({'code': 400, 'msg': msg}) + ok, msg, limit_kind = reserve_collect_quota_limits( user_main, leixing, jine, shijidaozhang, shenhe_danhao, ) @@ -344,7 +365,12 @@ def process_audit_collect(request): return quota_resp return _build_collect_success_response(post_payload) if post_action == RECONCILE_COMPLETED: - return Response({'code': 8, 'msg': '该提现已完成,请勿重复操作'}) + ensure_shenhe_collect_completed(shenhe_danhao) + return Response({ + 'code': 0, + 'msg': '提现已成功到账', + 'data': {'already_completed': True, 'tixianjilu_id': tixianjilu_id_val}, + }) if post_action == RECONCILE_PENDING: pending_msg = ( post_payload.get('msg', post_payload) diff --git a/yonghu/views.py b/yonghu/views.py index d4b2827..d2591b2 100644 --- a/yonghu/views.py +++ b/yonghu/views.py @@ -327,240 +327,6 @@ class WechatMiniProgramLoginView(APIView): -'''class WechatMiniProgramLoginView(APIView): - """ - 微信小程序登录接口 - """ - throttle_classes = [AnonRateThrottle] - permission_classes = [AllowAny] - - def post(self, request): - try: - code = request.data.get('code', '').strip() - if not code: - return Response({'code': 1, 'msg': '微信授权码不能为空', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - # 获取微信openid和unionid - wechat_data = self.get_wechat_openid(code) - if not wechat_data or 'openid' not in wechat_data: - error_msg = wechat_data.get('errmsg', '微信授权失败') - return Response({'code': 2, 'msg': f'微信登录失败: {error_msg}', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - openid = wechat_data['openid'] - session_key = wechat_data.get('session_key', '') - unionid = wechat_data.get('unionid', '') # 🆕【新增】获取unionid - - # 获取客户端真实IP - kehuduan_ip = self.huoquKehuduanIP(request) - - with transaction.atomic(): - # 创建或获取用户 - user_main, created = UserMain.objects.select_for_update().get_or_create( - openid=openid, - defaults={'yonghuid': self.shengchengYonghuID(), 'user_type': 'normal'} - ) - - # 🆕【新增】保存unionid - if unionid and unionid != user_main.unionid: - user_main.unionid = unionid - - cunchu_ip = kehuduan_ip - user_main.ip = cunchu_ip - user_main.last_login_time = timezone.now() - user_main.save() - - if created: - UserBoss.objects.create(user=user_main, nickname='板板大人') - - # 检查扩展表 - dashou_status = 0 - shangjia_status = 0 - guanshi_status = 0 - boss_nickname = '微信用户' - - try: - boss_profile = UserBoss.objects.get(user=user_main) - boss_nickname = boss_profile.nickname if boss_profile.nickname else '微信用户' - except UserBoss.DoesNotExist: - pass - - try: - UserDashou.objects.get(user=user_main) - dashou_status = 1 - except UserDashou.DoesNotExist: - dashou_status = 0 - - try: - UserShangjia.objects.get(user=user_main) - shangjia_status = 1 - except UserShangjia.DoesNotExist: - shangjia_status = 0 - - try: - UserGuanshi.objects.get(user=user_main) - guanshi_status = 1 - except UserGuanshi.DoesNotExist: - guanshi_status = 0 - - # 生成JWT token - refresh = RefreshToken.for_user(user_main) - token = str(refresh.access_token) - - # 计算订单数量 - order_counts = self.jisuanDingdanShuliang(user_main.yonghuid) - - # 获取群配置 - group_info = self.huoquQunPeizhi() - - # 返回数据 - response_data = { - 'token': token, - 'nicheng': boss_nickname, - 'uid': user_main.yonghuid, - 'touxiang': user_main.avatar or '', - 'shangjiastatus': shangjia_status, - 'dashoustatus': dashou_status, - 'guanshistatus': guanshi_status, - 'dingdantiaoshu': order_counts, - 'dashouqun': group_info.get('dashouqun', ''), - 'dashouqunid': group_info.get('dashouqunid', ''), - 'guanshiqun': group_info.get('guanshiqun', ''), - 'guanshiqunid': group_info.get('guanshiqunid', '') - } - - return Response({'code': 0, 'msg': '登录成功', 'data': response_data}) - - except Exception as e: - import logging - logger = logging.getLogger(__name__) - error_openid = locals().get('openid', 'N/A') - logger.error(f"微信登录异常 - openid: {error_openid}, 错误类型: {type(e).__name__}, 错误详情: {str(e)}", - exc_info=True) - - return Response({'code': 99, 'msg': '系统繁忙,请稍后重试', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - def get_wechat_openid(self, code): - """ - 调用微信接口获取openid和unionid - """ - try: - appid = getattr(settings, 'WEIXIN_APPID', '') - secret = getattr(settings, 'WEIXIN_SECRET', '') - - if not appid or not secret: - raise ValueError('微信配置未设置') - - url = 'https://api.weixin.qq.com/sns/jscode2session' - params = {'appid': appid, 'secret': secret, 'js_code': code, 'grant_type': 'authorization_code'} - - response = requests.get(url, params=params, timeout=10) - result = response.json() - - if 'openid' in result: - return {'openid': result['openid'], 'session_key': result.get('session_key', ''), - 'unionid': result.get('unionid', '')} - else: - errcode = result.get('errcode', 'unknown') - errmsg = result.get('errmsg', '未知错误') - return {'errmsg': f'[{errcode}]{errmsg}'} - - except requests.exceptions.Timeout: - return {'errmsg': '请求微信接口超时'} - except Exception as e: - return {'errmsg': f'请求微信接口异常: {str(e)}'} - - def shengchengYonghuID(self): - """ - 生成7位数字用户ID - """ - for _ in range(10): - timestamp_part = str(int(time.time()))[-5:].zfill(5) - random_part = str(random.randint(0, 99)).zfill(2) - user_id = timestamp_part + random_part - - if len(user_id) == 7 and user_id.isdigit(): - if not UserMain.objects.filter(yonghuid=user_id).exists(): - return user_id - - raise Exception('生成用户ID失败') - - def huoquKehuduanIP(self, request): - """ - 获取客户端真实IP地址 - """ - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - if x_forwarded_for: - ip = x_forwarded_for.split(',')[0].strip() - if ip: - return ip - - ip = request.META.get('REMOTE_ADDR', '') - return ip if ip else '0.0.0.0' - - def jisuanDingdanShuliang(self, yonghuid): - """ - 统计订单数量 - """ - try: - if not DingdanPingtai.objects.filter(laoban_id=yonghuid).exists(): - return {'daifuwu': 0, 'fuwuzhong': 0, 'yiwancheng': 0, 'yituikuan': 0} - - dingdan_ids = DingdanPingtai.objects.filter(laoban_id=yonghuid).values_list('dingdan__id', flat=True) - - from django.db.models import Q, Count - from django.db.models import Case, When, IntegerField - - results = Dingdan.objects.filter(id__in=dingdan_ids).aggregate( - daifuwu=Count(Case(When(Q(zhuangtai=1) | Q(zhuangtai=7), then=1), output_field=IntegerField())), - fuwuzhong=Count(Case(When(zhuangtai=2, then=1), output_field=IntegerField())), - yiwancheng=Count(Case(When(zhuangtai=3, then=1), output_field=IntegerField())), - yituikuan=Count(Case(When(zhuangtai=5, then=1), output_field=IntegerField())) - ) - - return { - 'daifuwu': results['daifuwu'] or 0, - 'fuwuzhong': results['fuwuzhong'] or 0, - 'yiwancheng': results['yiwancheng'] or 0, - 'yituikuan': results['yituikuan'] or 0 - } - - except Exception: - return {'daifuwu': 0, 'fuwuzhong': 0, 'yiwancheng': 0, 'yituikuan': 0} - - def huoquQunPeizhi(self): - """ - 获取群配置信息 - """ - try: - qun_configs = Qunpeizhi.objects.filter(id__in=[1, 2]) - - result = { - 'dashouqun': '', - 'dashouqunid': '', - 'guanshiqun': '', - 'guanshiqunid': '' - } - - for config in qun_configs: - if config.id == 1: - result['dashouqun'] = config.neirong or '' - result['dashouqunid'] = config.qunid or '' - elif config.id == 2: - result['guanshiqun'] = config.neirong or '' - result['guanshiqunid'] = config.qunid or '' - - return result - - except Exception: - return { - 'dashouqun': '', - 'dashouqunid': '', - 'guanshiqun': '', - 'guanshiqunid': '' - }''' # views/weixin_official.py - 完整文件 @@ -738,720 +504,6 @@ class WeixinOfficialCallbackView(APIView): - - - - -'''class WechatMiniProgramLoginView(APIView): - throttle_classes = [AnonRateThrottle] - permission_classes = [AllowAny] - - def post(self, request): - try: - code = request.data.get('code', '').strip() - if not code: - return Response({'code': 1, 'msg': '微信授权码不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST) - - # 【优化点1】增强微信API调用日志,失败时打印详情 - wechat_data = self.get_wechat_openid(code) - if not wechat_data or 'openid' not in wechat_data: - # 这里打印详细错误,方便你定位是配置错误还是网络问题 - error_msg = wechat_data.get('errmsg', '微信授权失败') - - return Response({ - 'code': 2, - 'msg': f'微信登录失败: {error_msg}', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - openid = wechat_data['openid'] - session_key = wechat_data.get('session_key', '') - - with transaction.atomic(): - # 【优化点2】使用select_for_update避免并发创建用户,并用first()直接获取对象 - user_main, created = UserMain.objects.select_for_update().get_or_create( - openid=openid, - defaults={ - 'yonghuid': self.generate_user_id(), - 'user_type': 'normal', - } - ) - - if created: - # 新用户:创建老板扩展表 - UserBoss.objects.create(user=user_main, nickname='板板大人') - - # 【优化点3】核心优化:使用反向关联一次性获取所有扩展表信息,将至少3次查询合并为1次! - # 通过主表user_main,利用反向关联名(related_name)直接预取关联对象,性能极高 - user_main_with_profiles = UserMain.objects.select_related( - 'boss_profile', # 老板扩展表 - ).prefetch_related( - # 以下为反向查询,名字来自模型定义(如UserDashou模型里 related_name='dashou_profile') - 'dashou_profile', - 'shop_profile', - 'guanshi_profile' - ).get(pk=user_main.pk) - - # 【优化点4】直接从预取的对象判断状态,避免额外的 exists() 查询 - dashou_status = 1 if hasattr(user_main_with_profiles, 'dashou_profile') and user_main_with_profiles.dashou_profile is not None else 0 - shangjia_status = 1 if hasattr(user_main_with_profiles, 'shop_profile') and user_main_with_profiles.shop_profile is not None else 0 - guanshi_status = 1 if hasattr(user_main_with_profiles, 'guanshi_profile') and user_main_with_profiles.guanshi_profile is not None else 0 - - # 生成JWT token - refresh = RefreshToken.for_user(user_main_with_profiles) - token = str(refresh.access_token) - - # 【优化点5】昵称获取:直接从预取的boss_profile获取,无需额外查询 - boss_nickname = getattr(user_main_with_profiles.boss_profile, 'nickname', None) if hasattr(user_main_with_profiles, 'boss_profile') else None - nicheng = boss_nickname if boss_nickname else '微信用户' - - # 统计订单数量 - order_counts = self.get_order_counts(user_main_with_profiles.yonghuid) - - # 获取群配置信息 - group_info = self.get_group_config() - - # 准备返回数据(字段名称与你的前端完全一致,未作任何修改) - response_data = { - 'token': token, - 'nicheng': nicheng, - 'uid': user_main_with_profiles.yonghuid, - 'touxiang': user_main_with_profiles.avatar or '', - 'shangjiastatus': shangjia_status, - 'dashoustatus': dashou_status, - 'guanshistatus': guanshi_status, - 'dingdantiaoshu': order_counts, - 'dashouqun': group_info.get('dashouqun', ''), - 'dashouqunid': group_info.get('dashouqunid', ''), - 'guanshiqun': group_info.get('guanshiqun', ''), - 'guanshiqunid': group_info.get('guanshiqunid', '') - } - - return Response({'code': 0, 'msg': '登录成功', 'data': response_data}) - - except Exception as e: - # 【优化点6】记录更详细的异常日志,包括openid(如有),方便追踪 - error_openid = locals().get('openid', 'N/A') - - return Response({ - 'code': 99, - 'msg': f'系统错误: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - def get_wechat_openid(self, code): - """调用微信接口获取openid""" - try: - appid = getattr(settings, 'WEIXIN_APPID', '') - secret = getattr(settings, 'WEIXIN_SECRET', '') - - if not appid or not secret: - # 【优化点7】配置缺失时抛出明确异常 - raise ValueError('微信配置(WEIXIN_APPID或WEIXIN_SECRET)未在settings.py中设置') - - url = 'https://api.weixin.qq.com/sns/jscode2session' - params = {'appid': appid, 'secret': secret, 'js_code': code, 'grant_type': 'authorization_code'} - response = requests.get(url, params=params, timeout=10) - result = response.json() - - if 'openid' in result: - return result - else: - # 将微信返回的完整错误信息记录下来 - errcode = result.get('errcode', 'unknown') - errmsg = result.get('errmsg', '未知错误') - # 常见错误码:40029- code无效, 45011- 频率限制, 40125- 无效的appsecret - return {'errmsg': f'[{errcode}]{errmsg}'} - - except requests.exceptions.Timeout: - return {'errmsg': '请求微信接口超时,请检查网络或重试'} - except Exception as e: - return {'errmsg': f'请求微信接口异常: {str(e)}'} - - # 【优化点8】generate_user_id 方法可以加一个分布式锁或更安全的唯一性检查,但考虑到你的用户量,当前方法风险极低,可保留。 - def generate_user_id(self): - """生成7位数字用户ID""" - for _ in range(10): - timestamp_part = str(int(time.time()))[-5:].zfill(5) - random_part = str(random.randint(0, 99)).zfill(2) - user_id = timestamp_part + random_part - if len(user_id) == 7 and user_id.isdigit(): - if not UserMain.objects.filter(yonghuid=user_id).exists(): - return user_id - raise Exception('生成用户ID失败,请重试') - - # get_order_counts 和 get_group_config 方法逻辑清晰,无需优化,可保持不变。 - def get_order_counts(self, yonghuid): - """统计订单数量""" - try: - pingtai_orders = DingdanPingtai.objects.filter(laoban_id=yonghuid) - if not pingtai_orders: - return {'daifuwu': 0, 'fuwuzhong': 0, 'yiwancheng': 0, 'yituikuan': 0} - - dingdan_ids = pingtai_orders.values_list('dingdan__id', flat=True) - # 使用一次查询配合Case-When或多次独立查询?考虑到状态码分散,当前写法OK。 - daifuwu_count = Dingdan.objects.filter(id__in=dingdan_ids, zhuangtai__in=[1, 7]).count() - fuwuzhong_count = Dingdan.objects.filter(id__in=dingdan_ids, zhuangtai=2).count() - yiwancheng_count = Dingdan.objects.filter(id__in=dingdan_ids, zhuangtai=3).count() - yituikuan_count = Dingdan.objects.filter(id__in=dingdan_ids, zhuangtai=5).count() - - return { - 'daifuwu': daifuwu_count, - 'fuwuzhong': fuwuzhong_count, - 'yiwancheng': yiwancheng_count, - 'yituikuan': yituikuan_count - } - except Exception: - return {'daifuwu': 0, 'fuwuzhong': 0, 'yiwancheng': 0, 'yituikuan': 0} - - def get_group_config(self): - """获取群配置信息""" - try: - # 使用一次查询获取两个群信息,更高效 - qun_configs = Qunpeizhi.objects.filter(id__in=[1, 2]) - config_map = {config.id: config for config in qun_configs} - dashou_config = config_map.get(1) - guanshi_config = config_map.get(2) - - return { - 'dashouqun': getattr(dashou_config, 'neirong', ''), - 'dashouqunid': getattr(dashou_config, 'qunid', ''), - 'guanshiqun': getattr(guanshi_config, 'neirong', ''), - 'guanshiqunid': getattr(guanshi_config, 'qunid', '') - } - except Exception: - return {'dashouqun': '', 'dashouqunid': '', 'guanshiqun': '', 'guanshiqunid': ''}''' - - -'''class WechatMiniProgramLoginView(APIView): - """ - 微信小程序登录接口(修复版) - """ - - throttle_classes = [AnonRateThrottle] - permission_classes = [AllowAny] - - def post(self, request): - try: - code = request.data.get('code', '').strip() - if not code: - return Response({'code': 1, 'msg': '微信授权码不能为空', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - # 获取微信openid - wechat_data = self.get_wechat_openid(code) - if not wechat_data or 'openid' not in wechat_data: - error_msg = wechat_data.get('errmsg', '微信授权失败') - return Response({ - 'code': 2, - 'msg': f'微信登录失败: {error_msg}', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - openid = wechat_data['openid'] - session_key = wechat_data.get('session_key', '') - - # 获取客户端真实IP - kehuduan_ip = self.huoquKehuduanIP(request) - - with transaction.atomic(): - # 创建或获取用户 - user_main, created = UserMain.objects.select_for_update().get_or_create( - openid=openid, - defaults={ - 'yonghuid': self.shengchengYonghuID(), - 'user_type': 'normal', - } - ) - - - cunchu_ip = kehuduan_ip - - user_main.ip = cunchu_ip - user_main.last_login_time = timezone.now() - user_main.save() - - if created: - # 新用户:创建老板扩展表 - UserBoss.objects.create(user=user_main, nickname='板板大人') - - # 【关键修复】使用安全的查询方式 - # 方法1:使用try-except逐个查询 - dashou_status = 0 - shangjia_status = 0 - guanshi_status = 0 - boss_nickname = '微信用户' - - # 检查老板扩展表 - try: - boss_profile = UserBoss.objects.get(user=user_main) - boss_nickname = boss_profile.nickname if boss_profile.nickname else '微信用户' - except UserBoss.DoesNotExist: - pass - - # 检查打手扩展表 - try: - UserDashou.objects.get(user=user_main) - dashou_status = 1 - except UserDashou.DoesNotExist: - dashou_status = 0 - - # 检查商家扩展表 - try: - UserShangjia.objects.get(user=user_main) - shangjia_status = 1 - except UserShangjia.DoesNotExist: - shangjia_status = 0 - - # 检查管事扩展表 - try: - UserGuanshi.objects.get(user=user_main) - guanshi_status = 1 - except UserGuanshi.DoesNotExist: - guanshi_status = 0 - - # 生成JWT token - refresh = RefreshToken.for_user(user_main) - token = str(refresh.access_token) - - # 计算订单数量 - order_counts = self.jisuanDingdanShuliang(user_main.yonghuid) - - # 获取群配置 - group_info = self.huoquQunPeizhi() - - # 返回数据 - response_data = { - 'token': token, - 'nicheng': boss_nickname, - 'uid': user_main.yonghuid, - 'touxiang': user_main.avatar or '', - 'shangjiastatus': shangjia_status, - 'dashoustatus': dashou_status, - 'guanshistatus': guanshi_status, - 'dingdantiaoshu': order_counts, - 'dashouqun': group_info.get('dashouqun', ''), - 'dashouqunid': group_info.get('dashouqunid', ''), - 'guanshiqun': group_info.get('guanshiqun', ''), - 'guanshiqunid': group_info.get('guanshiqunid', '') - } - - return Response({'code': 0, 'msg': '登录成功', 'data': response_data}) - - except Exception as e: - # 记录详细的错误日志 - import logging - logger = logging.getLogger(__name__) - error_openid = locals().get('openid', 'N/A') - logger.error(f"微信登录异常 - openid: {error_openid}, 错误类型: {type(e).__name__}, 错误详情: {str(e)}", - exc_info=True) - - # 前端返回友好信息 - return Response({ - 'code': 99, - 'msg': '系统繁忙,请稍后重试', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - def get_wechat_openid(self, code): - """ - 调用微信接口获取openid - """ - try: - appid = getattr(settings, 'WEIXIN_APPID', '') - secret = getattr(settings, 'WEIXIN_SECRET', '') - - if not appid or not secret: - raise ValueError('微信配置未设置') - - url = 'https://api.weixin.qq.com/sns/jscode2session' - params = { - 'appid': appid, - 'secret': secret, - 'js_code': code, - 'grant_type': 'authorization_code' - } - - response = requests.get(url, params=params, timeout=10) - result = response.json() - - if 'openid' in result: - return result - else: - errcode = result.get('errcode', 'unknown') - errmsg = result.get('errmsg', '未知错误') - return {'errmsg': f'[{errcode}]{errmsg}'} - - except requests.exceptions.Timeout: - return {'errmsg': '请求微信接口超时'} - except Exception as e: - return {'errmsg': f'请求微信接口异常: {str(e)}'} - - def shengchengYonghuID(self): - """ - 生成7位数字用户ID - """ - for _ in range(10): - timestamp_part = str(int(time.time()))[-5:].zfill(5) - random_part = str(random.randint(0, 99)).zfill(2) - user_id = timestamp_part + random_part - - if len(user_id) == 7 and user_id.isdigit(): - if not UserMain.objects.filter(yonghuid=user_id).exists(): - return user_id - - raise Exception('生成用户ID失败') - - def huoquKehuduanIP(self, request): - """ - 获取客户端真实IP地址 - """ - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - if x_forwarded_for: - ip = x_forwarded_for.split(',')[0].strip() - if ip: - return ip - - ip = request.META.get('REMOTE_ADDR', '') - return ip if ip else '0.0.0.0' - - def jisuanDingdanShuliang(self, yonghuid): - """ - 统计订单数量 - """ - try: - # 使用exists()快速检查 - if not DingdanPingtai.objects.filter(laoban_id=yonghuid).exists(): - return {'daifuwu': 0, 'fuwuzhong': 0, 'yiwancheng': 0, 'yituikuan': 0} - - # 获取所有相关订单ID - dingdan_ids = DingdanPingtai.objects.filter( - laoban_id=yonghuid - ).values_list('dingdan__id', flat=True) - - # 使用条件聚合 - from django.db.models import Q, Count - from django.db.models import Case, When, IntegerField - - results = Dingdan.objects.filter(id__in=dingdan_ids).aggregate( - daifuwu=Count(Case(When(Q(zhuangtai=1) | Q(zhuangtai=7), then=1), output_field=IntegerField())), - fuwuzhong=Count(Case(When(zhuangtai=2, then=1), output_field=IntegerField())), - yiwancheng=Count(Case(When(zhuangtai=3, then=1), output_field=IntegerField())), - yituikuan=Count(Case(When(zhuangtai=5, then=1), output_field=IntegerField())) - ) - - return { - 'daifuwu': results['daifuwu'] or 0, - 'fuwuzhong': results['fuwuzhong'] or 0, - 'yiwancheng': results['yiwancheng'] or 0, - 'yituikuan': results['yituikuan'] or 0 - } - - except Exception: - return {'daifuwu': 0, 'fuwuzhong': 0, 'yiwancheng': 0, 'yituikuan': 0} - - def huoquQunPeizhi(self): - """ - 获取群配置信息 - """ - try: - qun_configs = Qunpeizhi.objects.filter(id__in=[1, 2]) - - result = { - 'dashouqun': '', - 'dashouqunid': '', - 'guanshiqun': '', - 'guanshiqunid': '' - } - - for config in qun_configs: - if config.id == 1: - result['dashouqun'] = config.neirong or '' - result['dashouqunid'] = config.qunid or '' - elif config.id == 2: - result['guanshiqun'] = config.neirong or '' - result['guanshiqunid'] = config.qunid or '' - - return result - - except Exception: - return { - 'dashouqun': '', - 'dashouqunid': '', - 'guanshiqun': '', - 'guanshiqunid': '' - }''' - -'''class WechatMiniProgramLoginView(APIView): - """ - 微信小程序登录接口(优化版) - 保持原有字段不变,只优化查询性能和添加IP记录 - """ - - throttle_classes = [AnonRateThrottle] - permission_classes = [AllowAny] - - def post(self, request): - try: - code = request.data.get('code', '').strip() - if not code: - return Response({'code': 1, 'msg': '微信授权码不能为空', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - # 【优化点1】使用更加可靠的微信API调用 - wechat_data = self.get_wechat_openid(code) - if not wechat_data or 'openid' not in wechat_data: - error_msg = wechat_data.get('errmsg', '微信授权失败') - return Response({ - 'code': 2, - 'msg': f'微信登录失败: {error_msg}', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - openid = wechat_data['openid'] - session_key = wechat_data.get('session_key', '') - - # 【新增】获取客户端真实IP - kehuduan_ip = self.huoquKehuduanIP(request) - - with transaction.atomic(): - # 【优化点2】使用更简洁的get_or_create写法 - user_main, created = UserMain.objects.select_for_update().get_or_create( - openid=openid, - defaults={ - 'yonghuid': self.shengchengYonghuID(), - 'user_type': 'normal', - } - ) - - # 【新增】更新用户IP和最后登录时间 - # 处理IP地址长度,适配现有字段长度(11) - if kehuduan_ip and len(kehuduan_ip) > 11: - cunchu_ip = kehuduan_ip[:11] # 截取前11位 - else: - cunchu_ip = kehuduan_ip - - # 更新用户信息 - user_main.ip = cunchu_ip - # 注意:需要先在UserMain模型中添加last_login_time字段 - # 如果没有该字段,请先添加:last_login_time = models.DateTimeField(null=True, blank=True, verbose_name='最后登录时间') - user_main.last_login_time = timezone.now() - user_main.save() - - if created: - # 新用户:创建老板扩展表 - UserBoss.objects.create(user=user_main, nickname='板板大人') - - # 【优化点3】优化查询:使用select_related一次性获取所有一对一关联 - # 由于boss_profile是一对一,可以用select_related - # dashou_profile, shop_profile, guanshi_profile也都是OneToOneField,可以用select_related - user_main_with_profiles = UserMain.objects.select_related( - 'boss_profile', # 老板扩展表 - 'dashou_profile', # 打手扩展表 - 'shop_profile', # 商家扩展表 - 'guanshi_profile' # 管事扩展表 - ).get(pk=user_main.pk) - - # 【优化点4】简化状态判断 - dashou_status = 1 if user_main_with_profiles.dashou_profile else 0 - shangjia_status = 1 if user_main_with_profiles.shop_profile else 0 - guanshi_status = 1 if user_main_with_profiles.guanshi_profile else 0 - - # 生成JWT token(保持原有方式) - refresh = RefreshToken.for_user(user_main_with_profiles) - token = str(refresh.access_token) - - # 【优化点5】更简洁的昵称获取方式 - boss_nickname = getattr(user_main_with_profiles.boss_profile, 'nickname', None) - nicheng = boss_nickname if boss_nickname else '微信用户' - - # 【优化点6】优化订单统计查询 - order_counts = self.jisuanDingdanShuliang(user_main_with_profiles.yonghuid) - - # 【优化点7】优化群配置查询 - group_info = self.huoquQunPeizhi() - - # 返回数据(保持原有字段名不变) - response_data = { - 'token': token, - 'nicheng': nicheng, - 'uid': user_main_with_profiles.yonghuid, - 'touxiang': user_main_with_profiles.avatar or '', - 'shangjiastatus': shangjia_status, - 'dashoustatus': dashou_status, - 'guanshistatus': guanshi_status, - 'dingdantiaoshu': order_counts, - 'dashouqun': group_info.get('dashouqun', ''), - 'dashouqunid': group_info.get('dashouqunid', ''), - 'guanshiqun': group_info.get('guanshiqun', ''), - 'guanshiqunid': group_info.get('guanshiqunid', '') - } - - return Response({'code': 0, 'msg': '登录成功', 'data': response_data}) - - except Exception as e: - # 【优化点8】更详细的异常日志,但不暴露给前端 - error_openid = locals().get('openid', 'N/A') - # 记录日志到系统 - import logging - logger = logging.getLogger(__name__) - logger.error(f"微信登录异常 - openid: {error_openid}, 错误: {str(e)}") - - # 前端返回友好信息(不暴露具体错误) - return Response({ - 'code': 99, - 'msg': '系统繁忙,请稍后重试', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - def get_wechat_openid(self, code): - """ - 调用微信接口获取openid - """ - try: - appid = getattr(settings, 'WEIXIN_APPID', '') - secret = getattr(settings, 'WEIXIN_SECRET', '') - - if not appid or not secret: - raise ValueError('微信配置(WEIXIN_APPID或WEIXIN_SECRET)未在settings.py中设置') - - url = 'https://api.weixin.qq.com/sns/jscode2session' - params = { - 'appid': appid, - 'secret': secret, - 'js_code': code, - 'grant_type': 'authorization_code' - } - - # 设置超时和重试 - response = requests.get(url, params=params, timeout=10) - result = response.json() - - if 'openid' in result: - return result - else: - errcode = result.get('errcode', 'unknown') - errmsg = result.get('errmsg', '未知错误') - return {'errmsg': f'[{errcode}]{errmsg}'} - - except requests.exceptions.Timeout: - return {'errmsg': '请求微信接口超时,请检查网络或重试'} - except Exception as e: - return {'errmsg': f'请求微信接口异常: {str(e)}'} - - def shengchengYonghuID(self): - """ - 生成7位数字用户ID(优化版) - """ - for _ in range(10): - # 使用时间戳后5位 - timestamp_part = str(int(time.time()))[-5:].zfill(5) - # 生成2位随机数 - random_part = str(random.randint(0, 99)).zfill(2) - user_id = timestamp_part + random_part - - # 验证格式 - if len(user_id) == 7 and user_id.isdigit(): - # 检查是否已存在 - if not UserMain.objects.filter(yonghuid=user_id).exists(): - return user_id - - # 如果10次都失败,抛出异常 - raise Exception('生成用户ID失败,请重试') - - def huoquKehuduanIP(self, request): - """ - 获取客户端真实IP地址 - """ - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - if x_forwarded_for: - # 可能有多个IP,取第一个 - ip = x_forwarded_for.split(',')[0].strip() - if ip: - return ip - - # 如果没有代理,直接从REMOTE_ADDR获取 - ip = request.META.get('REMOTE_ADDR', '') - - # 如果还是获取不到,返回默认值 - if not ip: - ip = '0.0.0.0' - - return ip - - def jisuanDingdanShuliang(self, yonghuid): - """ - 统计订单数量(优化版) - """ - try: - # 使用exists()快速检查是否有订单 - if not DingdanPingtai.objects.filter(laoban_id=yonghuid).exists(): - return {'daifuwu': 0, 'fuwuzhong': 0, 'yiwancheng': 0, 'yituikuan': 0} - - # 获取所有相关订单ID - dingdan_ids = DingdanPingtai.objects.filter( - laoban_id=yonghuid - ).values_list('dingdan__id', flat=True) - - # 使用一个查询获取所有状态的数量(使用条件聚合优化) - from django.db.models import Q, Count - from django.db.models import Case, When, IntegerField - - # 更高效的单次查询方式 - results = Dingdan.objects.filter(id__in=dingdan_ids).aggregate( - daifuwu=Count(Case(When(Q(zhuangtai=1) | Q(zhuangtai=7), then=1), output_field=IntegerField())), - fuwuzhong=Count(Case(When(zhuangtai=2, then=1), output_field=IntegerField())), - yiwancheng=Count(Case(When(zhuangtai=3, then=1), output_field=IntegerField())), - yituikuan=Count(Case(When(zhuangtai=5, then=1), output_field=IntegerField())) - ) - - return { - 'daifuwu': results['daifuwu'] or 0, - 'fuwuzhong': results['fuwuzhong'] or 0, - 'yiwancheng': results['yiwancheng'] or 0, - 'yituikuan': results['yituikuan'] or 0 - } - - except Exception: - # 发生异常时返回默认值 - return {'daifuwu': 0, 'fuwuzhong': 0, 'yiwancheng': 0, 'yituikuan': 0} - - def huoquQunPeizhi(self): - """ - 获取群配置信息(优化版) - """ - try: - # 使用更高效的查询方式 - qun_configs = Qunpeizhi.objects.filter(id__in=[1, 2]) - - # 直接构建结果字典 - result = { - 'dashouqun': '', - 'dashouqunid': '', - 'guanshiqun': '', - 'guanshiqunid': '' - } - - for config in qun_configs: - if config.id == 1: - result['dashouqun'] = config.neirong or '' - result['dashouqunid'] = config.qunid or '' - elif config.id == 2: - result['guanshiqun'] = config.neirong or '' - result['guanshiqunid'] = config.qunid or '' - - return result - - except Exception: - # 发生异常时返回空值 - return { - 'dashouqun': '', - 'dashouqunid': '', - 'guanshiqun': '', - 'guanshiqunid': '' - }''' - - @@ -2603,45 +1655,7 @@ class TixianXinxiHuoquView(APIView): }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) -'''class TixianXinxiHuoquView(APIView): - """ - 获取用户提现收款信息接口 - 返回用户已设置的手机号、收款码、收款账号 - """ - permission_classes = [permissions.IsAuthenticated] - def post(self, request): - """ - 处理获取收款信息的POST请求 - 直接返回用户当前的收款设置 - """ - try: - # 从request.user获取当前用户 - user_main = request.user - - # 构建返回数据 - response_data = { - 'txdianhua': user_main.phone if user_main.phone else '', - 'txtupian': user_main.zhifu if user_main.zhifu else '', - 'txzh': user_main.skzhanghao if user_main.skzhanghao else '' - } - - # 返回成功响应 - return Response({ - 'code': 0, - 'msg': '获取成功', - 'data': response_data - }) - - except Exception as e: - return Response({ - 'code': 99, - 'msg': f'系统错误: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - -from rest_framework.parsers import MultiPartParser, FormParser''' class ShoukuanXinxiShangchuanView(APIView): @@ -3172,411 +2186,6 @@ class TixianShenqingView(APIView): -'''class TixianShenqingView(APIView): - """ - 用户提现申请接口(修复版) - 支持打手佣金提现和管事分红提现 - 严格校验所有业务条件,修复400错误问题 - """ - permission_classes = [permissions.IsAuthenticated] - - def post(self, request): - """ - 处理提现申请的POST请求 - 加强参数验证和错误处理 - """ - try: - # 调试:打印接收到的所有数据 - # print("=== 提现申请请求数据 ===") - # print(f"请求用户: {request.user.yonghuid}") - # print(f"请求数据: {request.data}") - # print("=====================") - - # 获取当前用户 - user_main = request.user - yonghuid = user_main.yonghuid - - # 1. 获取并严格验证前端参数 - leixing = request.data.get('leixing') # 提现类型:1-佣金 2-分红 - jine = request.data.get('jine') # 提现金额 - fangshi = request.data.get('fangshi') # 收款方式:1-微信 2-支付宝 - txskfs = request.data.get('txskfs', 0) # 收款方式标识 - - # 调试:打印每个参数 - # print(f"参数解析 - leixing: {leixing}, jine: {jine}, fangshi: {fangshi}, txskfs: {txskfs}") - - # 验证必需参数是否存在 - missing_params = [] - if leixing is None: - missing_params.append('leixing') - if jine is None: - missing_params.append('jine') - if fangshi is None: - missing_params.append('fangshi') - - if missing_params: - return Response({ - 'code': 1, - 'msg': f'缺少必需参数: {", ".join(missing_params)}', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 验证参数类型和格式 - try: - leixing = int(leixing) - # 金额可能已经是字符串格式的浮点数 - if isinstance(jine, str): - jine = decimal.Decimal(jine) - else: - jine = decimal.Decimal(str(jine)) - fangshi = int(fangshi) - txskfs = int(txskfs) - except (ValueError, TypeError, decimal.InvalidOperation) as e: - # print(f"参数格式转换错误: {e}") - return Response({ - 'code': 2, - 'msg': f'参数格式错误: {str(e)}', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 验证提现金额必须大于10 - if jine <= decimal.Decimal('10'): - return Response({ - 'code': 3, - 'msg': '提现金额必须大于10', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 验证提现类型有效性 - if leixing not in [1, 2]: - return Response({ - 'code': 4, - 'msg': '无效的提现类型,只能是1(佣金)或2(分红)', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 验证收款方式有效性 - if fangshi not in [1, 2]: - return Response({ - 'code': 5, - 'msg': '无效的收款方式,只能是1(微信)或2(支付宝)', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 🔥【重要修改开始】获取对应费率 - current_rate = decimal.Decimal('0.00') - if leixing == 1: - # 打手提现费率 (fadanpingtai='5') - dashou_rate_obj = Lilubiao.objects.filter(fadanpingtai='5').first() - if dashou_rate_obj: - current_rate = decimal.Decimal(str(dashou_rate_obj.lilu)) - elif leixing == 2: - # 管事提现费率 (fadanpingtai='6') - guanshi_rate_obj = Lilubiao.objects.filter(fadanpingtai='6').first() - if guanshi_rate_obj: - current_rate = decimal.Decimal(str(guanshi_rate_obj.lilu)) - - # 计算手续费和实际到账金额 - shouxufei = jine * current_rate - shijidaozhang = jine - shouxufei - - # 验证手续费和实际到账金额的有效性 - if shijidaozhang <= decimal.Decimal('0'): - return Response({ - 'code': 6, - 'msg': '提现金额过低,扣除手续费后无实际到账', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 🔥【重要修改结束】 - - # 2. 根据提现类型进行不同的校验 - if leixing == 1: - # 佣金提现 - 打手 - result = self.yanzheng_dashou_tixian(user_main, jine) - if result['code'] != 0: - # print(f"打手提现校验失败: {result['msg']}") - return Response(result, status=status.HTTP_400_BAD_REQUEST) - - # 获取打手信息 - dashou_info = result['data'] - nicheng = dashou_info.get('nicheng', '') - - # 使用事务更新打手余额 - try: - with transaction.atomic(): - # 使用select_for_update锁定记录 - dashou_profile = UserDashou.objects.select_for_update().get(user=user_main) - - # 再次验证余额(防止并发问题) - if dashou_profile.yue < jine: - return Response({ - 'code': 13, - 'msg': '余额不足,无法提现', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 更新余额 - dashou_profile.yue = F('yue') - jine - dashou_profile.save() - - # 重新获取更新后的值 - dashou_profile.refresh_from_db() - - except Exception as e: - # print(f"更新打手余额失败: {e}") - return Response({ - 'code': 99, - 'msg': f'更新余额失败: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - elif leixing == 2: - # 分红提现 - 管事 - result = self.yanzheng_guanshi_tixian(user_main, jine) - if result['code'] != 0: - # print(f"管事提现校验失败: {result['msg']}") - return Response(result, status=status.HTTP_400_BAD_REQUEST) - - # 获取管事信息 - nicheng = "管事用户" - - # 使用事务更新管事余额 - try: - with transaction.atomic(): - guanshi_profile = UserGuanshi.objects.select_for_update().get(user=user_main) - - # 再次验证余额 - if guanshi_profile.yue < jine: - return Response({ - 'code': 22, - 'msg': '余额不足,无法提现', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 更新余额 - guanshi_profile.yue = F('yue') - jine - guanshi_profile.save() - - # 重新获取更新后的值 - guanshi_profile.refresh_from_db() - - except Exception as e: - # print(f"更新管事余额失败: {e}") - return Response({ - 'code': 99, - 'msg': f'更新余额失败: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 3. 创建提现记录 - try: - # 获取用户相关信息 - avatar = user_main.avatar if user_main.avatar else '' - phone = user_main.phone if user_main.phone else '' - zhifu = user_main.zhifu if user_main.zhifu else '' - skzhanghao = user_main.skzhanghao if user_main.skzhanghao else '' - - # 对于打手,从扩展表获取昵称 - if leixing == 1: - try: - dashou_profile = UserDashou.objects.get(user=user_main) - nicheng = dashou_profile.nicheng if dashou_profile.nicheng else '' - except UserDashou.DoesNotExist: - nicheng = '' - - # print(f"创建提现记录 - 用户: {yonghuid}, 类型: {leixing}, 金额: {jine}") - - # 🔥【重要修改开始】创建提现记录时存储手续费和实际到账金额 - tixian_jilu = Tixianjilu.objects.create( - yonghuid=yonghuid, - avatar=avatar, - phone=phone, - nicheng=nicheng, - leixing=leixing, - zhifu=zhifu, - skzhanghao=skzhanghao, - jine=shijidaozhang, - # jine=jine, - zhuangtai=1, # 1-审核中 - fangshi=fangshi, - shenheid=None, - bhliyou='', - # 🔥 新增字段:记录手续费和实际到账金额 - # shouxufei=shouxufei, - # shijidaozhang=shijidaozhang, - # feilv=current_rate - ) - - # print(f"提现记录创建成功,ID: {tixian_jilu.id}") - - # 4. 返回成功响应 - return Response({ - 'code': 0, - 'msg': '提现申请提交成功', - 'data': { - 'tixian_id': tixian_jilu.id, - 'leixing': leixing, - 'jine': str(jine.quantize(decimal.Decimal('0.01'))), - 'fangshi': fangshi, - 'zhuangtai': 1, - 'create_time': tixian_jilu.create_time.strftime('%Y-%m-%d %H:%M:%S'), - # 🔥【重要修改开始】返回手续费相关信息给前端显示 - 'current_rate': str(current_rate.quantize(decimal.Decimal('0.0000'))), - 'shouxufei': str(shouxufei.quantize(decimal.Decimal('0.01'))), - 'shijidaozhang': str(shijidaozhang.quantize(decimal.Decimal('0.01'))), - # 🔥【重要修改结束】 - 'tip': f'本次提现手续费率{current_rate * 100}%,手续费{shouxufei}元,实际到账{shijidaozhang}元' - } - }) - - except Exception as e: - # print(f"创建提现记录失败: {e}") - traceback.print_exc() - return Response({ - 'code': 99, - 'msg': f'创建提现记录失败: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - except Exception as e: - # print(f"提现申请接口异常: {e}") - traceback.print_exc() - return Response({ - 'code': 99, - 'msg': f'系统错误: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - def yanzheng_dashou_tixian(self, user_main, jine): - """ - 校验打手佣金提现条件(修复版) - 修正了状态判断逻辑 - """ - try: - # print(f"开始校验打手提现 - 用户: {user_main.yonghuid}, 金额: {jine}") - - # 查询打手扩展表 - try: - dashou_profile = UserDashou.objects.get(user=user_main) - # print( - # f"打手信息 - 状态: {dashou_profile.zhuangtai}, 账号状态: {dashou_profile.zhanghaozhuangtai}, 积分: {dashou_profile.jifen}, 余额: {dashou_profile.yue}") - except UserDashou.DoesNotExist: - # print("打手扩展表记录不存在") - return { - 'code': 10, - 'msg': '您不是打手身份,无法提现佣金', - 'data': None - } - - # 校验1: 打手账号状态 (zhanghaozhuangtai != 1 则封禁) - if dashou_profile.zhanghaozhuangtai != 1: - return { - 'code': 11, - 'msg': '打手账号已被封禁,无法提现', - 'data': None - } - - # 校验2: 打手接单状态 (zhuangtai != 1 则有订单进行中) - 修复这里 - if dashou_profile.zhuangtai != 1: - return { - 'code': 12, - 'msg': '您有订单进行中,请完成后提现', - 'data': None - } - - # 校验3: 积分检查 (jifen != 10 则不足) - 新增 - if dashou_profile.jifen != 10: - return { - 'code': 15, # 使用新的错误码 - 'msg': '积分不足10分,请补充积分后提现', - 'data': None - } - - # 校验4: 余额是否足够 (yue < jine) - if dashou_profile.yue < jine: - return { - 'code': 13, - 'msg': f'余额不足,当前余额: {dashou_profile.yue},提现金额: {jine}', - 'data': None - } - - # print("打手提现校验通过") - - # 所有校验通过 - return { - 'code': 0, - 'msg': '校验通过', - 'data': { - 'nicheng': dashou_profile.nicheng if dashou_profile.nicheng else '', - 'yue': dashou_profile.yue, - 'jifen': dashou_profile.jifen - } - } - - except Exception as e: - # print(f"打手提现校验异常: {e}") - return { - 'code': 99, - 'msg': f'校验过程出错: {str(e)}', - 'data': None - } - - def yanzheng_guanshi_tixian(self, user_main, jine): - """ - 校验管事分红提现条件 - """ - try: - # print(f"开始校验管事提现 - 用户: {user_main.yonghuid}, 金额: {jine}") - - # 查询管事扩展表 - try: - guanshi_profile = UserGuanshi.objects.get(user=user_main) - # print(f"管事信息 - 状态: {guanshi_profile.zhuangtai}, 余额: {guanshi_profile.yue}") - except UserGuanshi.DoesNotExist: - # print("管事扩展表记录不存在") - return { - 'code': 20, - 'msg': '您不是管事身份,无法提现分红', - 'data': None - } - - # 校验1: 管事账号状态 (zhuangtai != 1) - if guanshi_profile.zhuangtai != 1: - return { - 'code': 21, - 'msg': '管事账号已被封禁,无法提现', - 'data': None - } - - # 校验2: 余额是否足够 (yue < jine) - if guanshi_profile.yue < jine: - return { - 'code': 22, - 'msg': f'余额不足,当前余额: {guanshi_profile.yue},提现金额: {jine}', - 'data': None - } - - # print("管事提现校验通过") - - # 所有校验通过 - return { - 'code': 0, - 'msg': '校验通过', - 'data': { - 'yue': guanshi_profile.yue - } - } - - except Exception as e: - # print(f"管事提现校验异常: {e}") - return { - 'code': 99, - 'msg': f'校验过程出错: {str(e)}', - 'data': None - }''' - - @@ -3703,102 +2312,6 @@ class TixianJiluHuoquViewV2(APIView): 'data': None, }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) -'''class TixianJiluHuoquViewV2(APIView): - """ - 提现记录获取接口(优化版) - 使用更高效的查询方式,减少数据库压力 - """ - permission_classes = [permissions.IsAuthenticated] - - def post(self, request): - try: - # 获取当前用户 - user_main = request.user - yonghuid = user_main.yonghuid - - # 获取分页参数 - page = request.data.get('page', 1) - limit = request.data.get('limit', 50) - - # 参数验证 - try: - page = int(page) - limit = int(limit) - if page < 1 or limit < 1: - return Response({ - 'code': 1, - 'msg': '参数错误', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - except: - return Response({ - 'code': 1, - 'msg': '参数格式错误', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 限制最大查询数量 - if limit > 100: - limit = 100 - - # 计算偏移量 - offset = (page - 1) * limit - - # 高效查询:只查询需要的字段,使用切片限制数量 - # 这里查询limit+1条,用于判断是否还有更多数据 - query_limit = limit + 1 - records_queryset = Tixianjilu.objects.filter( - yonghuid=yonghuid - ).order_by('-create_time').values( - 'leixing', 'jine', 'zhuangtai', 'create_time', 'fangshi' - )[offset:offset + query_limit] - - # 转换为列表 - records_list = list(records_queryset) - records_count = len(records_list) - - # 判断是否有更多数据 - has_more = records_count > limit - - # 如果查询到的数量大于limit,只返回limit条 - if has_more: - records_to_return = records_list[:limit] - else: - records_to_return = records_list - - # 格式化数据 - formatted_records = [] - for record in records_to_return: - formatted_records.append({ - 'leixing': record['leixing'], - 'jine': float(record['jine']) if record['jine'] else 0.00, - 'zhuangtai': record['zhuangtai'], - 'create': record['create_time'].strftime('%Y-%m-%d %H:%M:%S'), - 'fangshi': record['fangshi'] - }) - - # 构建响应 - response_data = { - 'list': formatted_records, - 'has_more': has_more, - 'page': page, - 'limit': limit, - 'current_count': len(formatted_records) - } - - return Response({ - 'code': 0, - 'msg': '获取成功', - 'data': response_data - }) - - except Exception as e: - return Response({ - 'code': 99, - 'msg': f'系统错误: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)''' - # yonghu/views.py import jwt @@ -14563,106 +13076,83 @@ class TixianCallbackV3View(APIView): # 3. 获取关键信息 out_bill_no = callback_data.get('out_bill_no') # 商户提现单号(即 tixian_id) - transfer_status = callback_data.get('transfer_status') # SUCCESS / FAIL - wechat_transfer_no = callback_data.get('transfer_no') # 微信转账单号 - fail_reason = callback_data.get('fail_reason', '') + transfer_status = ( + callback_data.get('transfer_status') or callback_data.get('state') or '' + ).upper() + wechat_transfer_no = callback_data.get('transfer_no') or callback_data.get('transfer_bill_no') + fail_reason = callback_data.get('fail_reason', '') or callback_data.get('close_reason', '') if not out_bill_no: return Response({'code': 'FAIL', 'message': 'out_bill_no缺失'}, status=status.HTTP_400_BAD_REQUEST) - # 4. 幂等处理:只更新状态为0(处理中)的记录,已最终状态不再处理 + from .tixian_shenhe_services import ( + ensure_shenhe_collect_completed, + mark_transfer_success, + release_collect_quota_for_record, + sync_audit_and_jilu_status, + ) + from .models import TixianShenheJilu + + # 4. 幂等处理 with transaction.atomic(): try: auto_record = TixianAutoRecord.objects.select_for_update().get(tixian_id=out_bill_no) except TixianAutoRecord.DoesNotExist: - # 记录不存在,视为已处理,返回成功 return Response({'code': 'SUCCESS', 'message': 'ok'}) - # 如果已经是最新状态,直接返回成功(幂等) + if transfer_status == 'SUCCESS': + if auto_record.zhuangtai == 0: + mark_transfer_success( + auto_record, + wechat_transfer_no=wechat_transfer_no, + with_accounting=True, + ) + elif auto_record.shenhe_danhao: + ensure_shenhe_collect_completed(auto_record.shenhe_danhao) + return Response({'code': 'SUCCESS', 'message': 'ok'}) + if auto_record.zhuangtai != 0: return Response({'code': 'SUCCESS', 'message': 'already processed'}) - if transfer_status == 'SUCCESS': - # 微信转账成功 - auto_record.zhuangtai = 1 - auto_record.wechat_transfer_no = wechat_transfer_no - auto_record.save(update_fields=['zhuangtai', 'wechat_transfer_no']) + # 微信转账失败 + auto_record.zhuangtai = 2 + auto_record.fail_reason = (fail_reason or '微信转账失败')[:500] + auto_record.save(update_fields=['zhuangtai', 'fail_reason']) + release_collect_quota_for_record(auto_record) - # 新审核流程:同步审核表/提现记录表为 2=提现成功 - if auto_record.shenhe_danhao: - from .models import TixianShenheJilu - from .tixian_shenhe_services import sync_audit_and_jilu_status - try: - audit = TixianShenheJilu.objects.select_for_update().get( - shenhe_danhao=auto_record.shenhe_danhao - ) - if audit.zhuangtai != 2: - sync_audit_and_jilu_status(audit, 2) - except TixianShenheJilu.DoesNotExist: - logger.warning(f'回调成功但审核记录不存在: {auto_record.shenhe_danhao}') - - # 更新收支记录(平台资金统计) + if auto_record.shenhe_danhao: try: - from dingdan.utils import update_daily_payout - update_daily_payout(auto_record.shijidaozhang) - - szjilu = Szjilu.objects.select_for_update().get(id=1) - szjilu.zongsy -= auto_record.shijidaozhang - szjilu.zongzc += auto_record.shijidaozhang - szjilu.jrzc += auto_record.shijidaozhang - szjilu.save() - except Szjilu.DoesNotExist: - Szjilu.objects.create( - id=1, - zongsy=-auto_record.shijidaozhang, - zongzc=auto_record.shijidaozhang, - jrzc=auto_record.shijidaozhang + audit = TixianShenheJilu.objects.select_for_update().get( + shenhe_danhao=auto_record.shenhe_danhao ) - except Exception as e: - logger.error(f"更新收支记录失败: {str(e)}", exc_info=True) - raise - - else: # transfer_status == 'FAIL' - auto_record.zhuangtai = 2 - auto_record.fail_reason = fail_reason or '微信转账失败' - auto_record.save(update_fields=['zhuangtai', 'fail_reason']) - - if auto_record.shenhe_danhao: - # 新审核流程:申请时已扣款,回调失败不退余额,恢复待收款(6)可重试 - from .models import TixianShenheJilu - from .tixian_shenhe_services import sync_audit_and_jilu_status - try: - audit = TixianShenheJilu.objects.select_for_update().get( - shenhe_danhao=auto_record.shenhe_danhao - ) - audit.tixian_auto_id = None - audit.fail_reason = auto_record.fail_reason - sync_audit_and_jilu_status(audit, 6, fail_reason=auto_record.fail_reason) - except TixianShenheJilu.DoesNotExist: - logger.warning(f'回调失败但审核记录不存在: {auto_record.shenhe_danhao}') - decrease_tixian_daily_stat(auto_record.leixing, auto_record.shijidaozhang) + audit.tixian_auto_id = None + audit.fail_reason = auto_record.fail_reason + sync_audit_and_jilu_status(audit, 6, fail_reason=auto_record.fail_reason) + except TixianShenheJilu.DoesNotExist: + logger.warning(f'回调失败但审核记录不存在: {auto_record.shenhe_danhao}') + decrease_tixian_daily_stat(auto_record.leixing, auto_record.shijidaozhang) + else: + # 旧流程:回滚余额 + yonghuid = auto_record.yonghuid + jine = auto_record.jine + leixing = auto_record.leixing + user_main = UserMain.objects.get(yonghuid=yonghuid) + if leixing == 1: + dashou = UserDashou.objects.select_for_update().get(user=user_main) + dashou.yue += jine + dashou.jinritixian_jine -= jine + dashou.save() + elif leixing == 2: + guanshi = UserGuanshi.objects.select_for_update().get(user=user_main) + guanshi.yue += jine + guanshi.jinritixian_jine -= jine + guanshi.save() else: - # 旧流程:回滚余额 - yonghuid = auto_record.yonghuid - jine = auto_record.jine - leixing = auto_record.leixing - user_main = UserMain.objects.get(yonghuid=yonghuid) - if leixing == 1: - dashou = UserDashou.objects.select_for_update().get(user=user_main) - dashou.yue += jine - dashou.jinritixian_jine -= jine - dashou.save() - elif leixing == 2: - guanshi = UserGuanshi.objects.select_for_update().get(user=user_main) - guanshi.yue += jine - guanshi.jinritixian_jine -= jine - guanshi.save() - else: - zuzhang = UserZuzhang.objects.select_for_update().get(user=user_main) - zuzhang.ketixian_jine += jine - zuzhang.jinri_tixian -= jine - zuzhang.save() - decrease_tixian_daily_stat(auto_record.leixing, auto_record.shijidaozhang) + zuzhang = UserZuzhang.objects.select_for_update().get(user=user_main) + zuzhang.ketixian_jine += jine + zuzhang.jinri_tixian -= jine + zuzhang.save() + decrease_tixian_daily_stat(auto_record.leixing, auto_record.shijidaozhang) return Response({'code': 'SUCCESS', 'message': 'ok'}) @@ -14694,6 +13184,8 @@ class TixianQueRenAutoView(APIView): user_main = request.user + from .tixian_shenhe_services import mark_transfer_success, release_collect_quota_for_record + with transaction.atomic(): try: auto_record = TixianAutoRecord.objects.select_for_update().get( @@ -14704,53 +13196,24 @@ class TixianQueRenAutoView(APIView): return Response({'code': 4, 'msg': '提现记录不存在'}, status=status.HTTP_404_NOT_FOUND) if auto_record.zhuangtai != 0: + if result == 1: + if auto_record.shenhe_danhao: + from .tixian_shenhe_services import ensure_shenhe_collect_completed + ensure_shenhe_collect_completed(auto_record.shenhe_danhao) + return Response({'code': 0, 'msg': '提现成功', 'data': None}) return Response({'code': 5, 'msg': '该提现已处理'}) is_audit_flow = bool(auto_record.shenhe_danhao) if result == 1: - auto_record.zhuangtai = 1 - auto_record.save(update_fields=['zhuangtai']) - - # 新审核流程:同步 TixianShenheJilu + Tixianjilu → 2=提现成功(不再扣款、不新建提现记录) - if is_audit_flow: - from .models import TixianShenheJilu - from .tixian_shenhe_services import sync_audit_and_jilu_status - try: - audit = TixianShenheJilu.objects.select_for_update().get( - shenhe_danhao=auto_record.shenhe_danhao, - ) - if audit.zhuangtai != 2: - sync_audit_and_jilu_status(audit, 2) - except TixianShenheJilu.DoesNotExist: - logger.warning(f'确认成功但审核记录不存在: {auto_record.shenhe_danhao}') - - try: - from dingdan.utils import update_daily_payout - update_daily_payout(auto_record.shijidaozhang) - - szjilu = Szjilu.objects.select_for_update().get(id=1) - szjilu.zongsy -= auto_record.shijidaozhang - szjilu.zongzc += auto_record.shijidaozhang - szjilu.jrzc += auto_record.shijidaozhang - szjilu.save() - except Szjilu.DoesNotExist: - Szjilu.objects.create( - id=1, - zongsy=-auto_record.shijidaozhang, - zongzc=auto_record.shijidaozhang, - jrzc=auto_record.shijidaozhang, - ) - except Exception as e: - logger.error(f'更新收支记录失败: {str(e)}', exc_info=True) - raise - + mark_transfer_success(auto_record, with_accounting=True) return Response({'code': 0, 'msg': '提现成功', 'data': None}) # 用户取消或失败 auto_record.zhuangtai = 2 auto_record.fail_reason = '用户取消收款' auto_record.save(update_fields=['zhuangtai', 'fail_reason']) + release_collect_quota_for_record(auto_record) if is_audit_flow: # 新审核流程:申请时已扣款,取消不退余额,审核单恢复 6 待收款,可再次点收款 @@ -15712,11 +14175,7 @@ URL: /yonghu/dshqltqx 方法: POST 认证: JWT Token (request.user) -鉴权逻辑(满足任意一条即通过): - 1. 押金 >= 5 元 → 通过 - 2. 拥有未过期会员 → 通过 - 3. 存在订单状态为 2 或 8 的订单(作为接单打手) → 通过 - 以上均不满足 → 拒绝 +鉴权逻辑:已注册打手且押金 > 0 即通过 """ @@ -15725,12 +14184,10 @@ class DashouJianquanView(APIView): permission_classes = [IsAuthenticated] def post(self, request): - # ========== 1. 通过 JWT 获取当前登录用户 ========== - user = request.user # UserMain 实例 + user = request.user - # ========== 2. 检查是否为已注册打手 ========== try: - dashou = user.dashou_profile # OneToOneField 反向查询 + dashou = user.dashou_profile except UserDashou.DoesNotExist: return Response({ 'code': 0, @@ -15738,48 +14195,18 @@ class DashouJianquanView(APIView): 'msg': '您尚未注册打手身份,请先注册' }) - # ========== 3. 条件一:押金 >= 5 元 ========== - if dashou.yajin and dashou.yajin >= 5: + yajin = dashou.yajin or 0 + if yajin > 0: return Response({ 'code': 0, 'allow': 1, 'msg': '鉴权通过(押金满足条件)' }) - # ========== 4. 条件二:存在未过期会员 ========== - # 高效查询:利用 yonghu_id + huiyuan_zhuangtai 复合索引 - huiyuan_list = Huiyuangoumai.objects.filter( - yonghu_id=user.yonghuid, - huiyuan_zhuangtai=1 # 只查状态为"可用"的,减少内存遍历 - ).only('id', 'daoqi_time', 'huiyuan_zhuangtai') - - for hy in huiyuan_list: - if not hy.jiance_shifou_daoqi(): # 未过期返回 False - return Response({ - 'code': 0, - 'allow': 1, - 'msg': '鉴权通过(会员未过期)' - }) - - # ========== 5. 条件三:存在订单状态为 2 或 8 的订单 ========== - # 利用 jiedan_dashou_id + zhuangtai 复合索引,只查 1 条 - has_valid_order = Dingdan.objects.filter( - jiedan_dashou_id=user.yonghuid, - zhuangtai__in=[2, 8] - ).only('id').exists() # exists() 高性能判断,只发 SELECT 1 LIMIT 1 - - if has_valid_order: - return Response({ - 'code': 0, - 'allow': 1, - 'msg': '鉴权通过(存在有效订单)' - }) - - # ========== 6. 以上均不满足 → 拒绝 ========== return Response({ 'code': 0, 'allow': 0, - 'msg': '消息权限不足,请充值会员或联系管理员' + 'msg': '消息权限不足,请先缴纳押金' }) @@ -15791,42 +14218,19 @@ URL: /yonghu/lbhqltqx 方法: POST 认证: JWT Token (request.user) -鉴权逻辑: - 查询平台订单扩展表(DingdanPingtai), - 通过关联的订单主表(Dingdan)查找该老板的订单, - 只要存在任意一条订单状态在 1~8 范围内 → 通过 +鉴权逻辑:已登录用户默认允许使用消息功能 """ - class LaobanJianquanView(APIView): """老板消息权限鉴权""" permission_classes = [IsAuthenticated] def post(self, request): - # ========== 1. 通过 JWT 获取当前登录用户 ========== - user = request.user # UserMain 实例 - - # ========== 2. 高效查询平台订单扩展表 ========== - # 利用 DingdanPingtai.laoban_id 索引 + Dingdan.zhuangtai 索引 - # 只查 1 条,用 exists() 实现最高性能 - has_valid_order = DingdanPingtai.objects.filter( - laoban_id=user.yonghuid - ).filter( - dingdan__zhuangtai__in=[1, 2, 3, 4, 5, 6, 7, 8] - ).only('id').exists() - - if has_valid_order: - return Response({ - 'code': 0, - 'allow': 1, - 'msg': '鉴权通过' - }) - return Response({ 'code': 0, - 'allow': 0, - 'msg': '您暂无订单记录,消息功能暂不可用' + 'allow': 1, + 'msg': '鉴权通过' })