阿龙电竞后端已经修复了提现问题,聊天问题,管式商家分红最新代码
This commit is contained in:
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
23
dingdan/migrations/0017_dingdan_guanshi_shangjia_fencheng.py
Normal file
23
dingdan/migrations/0017_dingdan_guanshi_shangjia_fencheng.py
Normal 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='管事商家派单分成',
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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='打手留言')
|
||||
|
||||
183
dingdan/utils.py
183
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}")
|
||||
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}打手身份无邀请管事")
|
||||
@@ -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
175
houtai/huashu_views.py
Normal 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': '删除成功'})
|
||||
@@ -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='跨屏要对方平台处罚通知接口'),
|
||||
|
||||
@@ -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))
|
||||
|
||||
153
peizhi/migrations/0015_huashuconfig.py
Normal file
153
peizhi/migrations/0015_huashuconfig.py
Normal 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),
|
||||
]
|
||||
@@ -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}'
|
||||
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()}'
|
||||
@@ -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
133
peizhi/views_huashu.py
Normal 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},
|
||||
})
|
||||
42
shangpin/migrations/0010_gsfenhong_fenhong_leixing_unique.py
Normal file
42
shangpin/migrations/0010_gsfenhong_fenhong_leixing_unique.py
Normal 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',
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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']),
|
||||
|
||||
@@ -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
|
||||
@@ -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', '')
|
||||
|
||||
@@ -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)
|
||||
|
||||
1752
yonghu/views.py
1752
yonghu/views.py
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user