阿龙电竞后端已经修复了提现问题,聊天问题,管式商家分红最新代码

This commit is contained in:
XingQue
2026-06-15 23:52:35 +08:00
parent 7fa713c335
commit 790f397893
18 changed files with 1256 additions and 1814 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@@ -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='管事商家派单分成',
),
),
]

View File

@@ -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='打手留言')

View File

@@ -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}")
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}打手身份无邀请管事")

View File

@@ -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,

175
houtai/huashu_views.py Normal file
View File

@@ -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': '删除成功'})

View File

@@ -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='跨屏要对方平台处罚通知接口'),

View File

@@ -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. 订单/押金分红费率Lilubiao1平台订单/3商家订单/12押金管事/13商家单管事分红)
# 1b. 订单/押金分红费率Lilubiao1平台订单/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.leixing4~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))

View File

@@ -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),
]

View File

@@ -611,3 +611,62 @@ class PopupImage(models.Model):
def __str__(self):
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()}'

View File

@@ -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='小程序客服自动回复匹配'),
]

133
peizhi/views_huashu.py Normal file
View File

@@ -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},
})

View File

@@ -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',
),
),
]

View File

@@ -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']),

View File

@@ -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
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

View File

@@ -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', '')

View File

@@ -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)

File diff suppressed because it is too large Load Diff