feat: 提现驳回可选返还手续费与入账冻结(单笔/批量)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-29 00:57:12 +08:00
parent 8e7481e25d
commit 36e1bf902a
4 changed files with 184 additions and 19 deletions

View File

@@ -68,7 +68,8 @@ from ..models import (
)
from users.business_models import User
from ..tixian_shenhe_services import (
load_audit_meta_map, refund_balance, sync_audit_and_jilu_status,
load_audit_meta_map, refund_balance, refund_withdraw_reject,
estimate_manual_shouxufei, sync_audit_and_jilu_status,
mark_transfer_success, release_collect_quota_for_record,
close_audit_wechat_failed, query_wechat_transfer_bill,
check_shijidaozhang_within_limit,
@@ -337,8 +338,9 @@ class KefuWithdrawListView(APIView):
for item in records:
shenhe_jilu_id = getattr(item, 'shenhe_jilu_id', None) if shenhe_field_ok else None
shenhe_danhao = (getattr(item, 'shenhe_danhao', None) or '') if shenhe_field_ok else ''
meta = audit_meta_map.get(shenhe_jilu_id, {}) if shenhe_jilu_id else {}
if shenhe_jilu_id and not shenhe_danhao:
shenhe_danhao = audit_meta_map.get(shenhe_jilu_id, {}).get('shenhe_danhao', '')
shenhe_danhao = meta.get('shenhe_danhao', '')
list_data.append({
'id': item.id,
'shenhe_jilu_id': shenhe_jilu_id,
@@ -351,6 +353,8 @@ class KefuWithdrawListView(APIView):
'zhifu': item.zhifu or '',
'skzhanghao': item.skzhanghao or '',
'jine': str(item.jine) if item.jine is not None else '0.00',
'shouxufei': meta.get('shouxufei', '0.00') if shenhe_jilu_id else '0.00',
'shijidaozhang': meta.get('shijidaozhang') or (str(item.jine) if item.jine is not None else '0.00'),
'zhuangtai': item.zhuangtai,
'fangshi': item.fangshi,
'dakuan_mode': self._resolve_row_dakuan_mode(item, audit_mode_map, shenhe_field_ok),
@@ -532,13 +536,32 @@ class KefuWithdrawActionView(APIView):
单条:{ phone, action, tixian_id, yonghuid, leixing, reason? }
批量:{ phone, action, batch_list: [{tixian_id, yonghuid, leixing}, ...], reason? }
自动打款(dakuan_mode=2)同意→6待收款拒绝→5+驳回原因+退还可到账金额(shijidaozhang),手续费不退,不更新平台收支/每日统计
驳回(action=2)可选(默认 false与现网一致
refund_fee: 是否退还手续费
apply_entry_freeze: 退回余额是否走入账冻结(按俱乐部配置;押金/商家无效)
自动打款(dakuan_mode=2)同意→6待收款拒绝→5+驳回原因+退款,不更新平台收支/每日统计
手动打款(dakuan_mode=1):原逻辑不变
状态要求Tixianjilu.zhuangtai=1 且 TixianShenheJilu.zhuangtai=1审核中其他状态一律拒绝
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
@staticmethod
def _parse_bool(raw, default=False):
if raw is None:
return bool(default)
if isinstance(raw, bool):
return raw
if isinstance(raw, (int, float)):
return bool(raw)
s = str(raw).strip().lower()
if s in ('1', 'true', 'yes', 'y', 'on'):
return True
if s in ('0', 'false', 'no', 'n', 'off', ''):
return False
return bool(default)
def _verify_kefu(self, request, phone):
"""与 menu-access / verify_kefu_permission 对齐:按后台账号放行,不因 UserType 推断失败而 401。"""
from jituan.services.admin_context import is_kefu_backend_account, is_system_super_admin
@@ -598,7 +621,8 @@ class KefuWithdrawActionView(APIView):
return None, Response({'code': 400, 'msg': '参数格式错误'}, status=status.HTTP_400_BAD_REQUEST)
return [item], None
def _process_auto_item(self, request, item, action, reason, kefu_user):
def _process_auto_item(self, request, item, action, reason, kefu_user,
refund_fee=False, apply_entry_freeze=False):
"""自动打款单条处理(须在 transaction.atomic 内调用)"""
tixian_id = item['tixian_id']
req_yonghuid = item['yonghuid']
@@ -651,6 +675,7 @@ class KefuWithdrawActionView(APIView):
bhliyou=limit_msg,
bo_hui_ren_id=kefu_user.UserUID,
)
# 超限自动驳回:保持现网(不退手续费、不走冻结)
refund_balance(user, audit.leixing, audit.shijidaozhang)
tixian.shenheid = kefu_user.UserUID
tixian.bhliyou = limit_msg
@@ -665,13 +690,22 @@ class KefuWithdrawActionView(APIView):
bhliyou=reason,
bo_hui_ren_id=kefu_user.UserUID,
)
# 退还可到账金额,申请时扣的 shenqing_jine 中的手续费(shouxufei)不退
refund_balance(user, audit.leixing, audit.shijidaozhang)
refund_withdraw_reject(
user,
audit.leixing,
shijidaozhang=audit.shijidaozhang,
shouxufei=audit.shouxufei,
refund_fee=refund_fee,
apply_entry_freeze=apply_entry_freeze,
biz_id=f'withdraw_reject:auto:{tixian_id}',
freeze_reason=f'提现驳回退款 #{tixian_id}',
)
tixian.shenheid = kefu_user.UserUID
tixian.bhliyou = reason
tixian.save(update_fields=['shenheid', 'bhliyou', 'UpdateTime'])
def _process_manual_item(self, request, item, action, reason, phone):
def _process_manual_item(self, request, item, action, reason, phone,
refund_fee=False, apply_entry_freeze=False):
"""手动打款单条处理(原逻辑,须在 transaction.atomic 内调用)"""
tixian_id = item['tixian_id']
try:
@@ -713,8 +747,18 @@ class KefuWithdrawActionView(APIView):
except Exception as e:
logger.error(f'更新收支记录失败: {e}')
else:
fee = estimate_manual_shouxufei(user, tixian.leixing, tixian.jine) if refund_fee else Decimal('0')
try:
refund_balance(user, tixian.leixing, tixian.jine)
refund_withdraw_reject(
user,
tixian.leixing,
shijidaozhang=tixian.jine,
shouxufei=fee,
refund_fee=refund_fee,
apply_entry_freeze=apply_entry_freeze,
biz_id=f'withdraw_reject:manual:{tixian_id}',
freeze_reason=f'提现驳回退款 #{tixian_id}',
)
except ValueError as e:
raise ValueError(str(e))
tixian.zhuangtai = 3
@@ -730,6 +774,8 @@ class KefuWithdrawActionView(APIView):
phone = request.data.get('phone', '').strip()
action_raw = request.data.get('action')
reason = request.data.get('reason', '').strip()
refund_fee = self._parse_bool(request.data.get('refund_fee'), False)
apply_entry_freeze = self._parse_bool(request.data.get('apply_entry_freeze'), False)
try:
action = int(action_raw)
@@ -773,11 +819,19 @@ class KefuWithdrawActionView(APIView):
is_auto = bool(getattr(tixian_probe, 'shenhe_jilu_id', None))
if is_auto:
self._process_auto_item(request, item, action, reason, kefu_user)
self._process_auto_item(
request, item, action, reason, kefu_user,
refund_fee=refund_fee if action == 2 else False,
apply_entry_freeze=apply_entry_freeze if action == 2 else False,
)
else:
if is_batch:
raise ValueError(f'提现记录{tixian_id}:批量操作仅支持自动打款记录')
self._process_manual_item(request, item, action, reason, phone)
self._process_manual_item(
request, item, action, reason, phone,
refund_fee=refund_fee if action == 2 else False,
apply_entry_freeze=apply_entry_freeze if action == 2 else False,
)
except ValueError as e:
return Response({'code': 400, 'msg': str(e)}, status=status.HTTP_400_BAD_REQUEST)