1417 lines
56 KiB
Python
1417 lines
56 KiB
Python
# yonghu/tixian_shenhe_services.py
|
||
"""
|
||
自动提现审核流程业务服务层(仅服务于 zddksh 申请接口 与 tixiansq 收款接口)
|
||
|
||
流程:
|
||
① zddksh 提交审核:完整资格校验 → 扣款 → 写审核表+提现记录表(状态=1)
|
||
允许多笔并行申请;单笔实际到账(shijidaozhang)不得超过200元
|
||
② 后台审核通过:校验到账≤200 → 状态 6(待收款)
|
||
③ tixiansq 收款:严格资格+限额 → 到账>200则关单退款(不退手续费),不调微信
|
||
微信官方 FAIL 关单不退款;SUCCESS 落库;进行中禁止新建打款单
|
||
④ 打款成功(tixianqr/回调/查单):若收款前已预占则不再累加;未预占的旧单/查单直成则补记
|
||
|
||
状态码(与前端 TX_STATUS 一致):
|
||
1=审核中 2=提现成功 3=提现失败 4=审核成功 5=审核失败 6=待收款
|
||
"""
|
||
import decimal
|
||
import json
|
||
import logging
|
||
import random
|
||
import time
|
||
from datetime import date, timedelta
|
||
|
||
import requests
|
||
from django.conf import settings
|
||
from django.db import transaction
|
||
from django.utils import timezone
|
||
|
||
from orders.models import Order, MerchantOrderExt, Penalty
|
||
from jituan.services.wechat_pay import club_id_for_tixian_yonghuid
|
||
from jituan.services.withdraw_config import (
|
||
get_or_create_platform_daily_stat,
|
||
get_personal_daily_quota,
|
||
get_platform_daily_limit,
|
||
get_platform_daily_total,
|
||
get_tixian_feilv as _get_tixian_feilv_for_club,
|
||
)
|
||
|
||
from utils.fadan_utils import check_fadan_qiangdan_eligible
|
||
|
||
from products.models import Huiyuangoumai
|
||
from backend.models import WithdrawalDailyStats
|
||
|
||
from utils.wechat_v3 import build_authorization
|
||
|
||
from .models import (
|
||
TixianAutoRecord,
|
||
TixianShenheJilu,
|
||
Tixianjilu,
|
||
UserDashou,
|
||
UserGuanshi,
|
||
UserShenheguan,
|
||
UserShangjia,
|
||
UserZuzhang,
|
||
)
|
||
from users.business_models import User
|
||
|
||
from orders.utils import update_daily_payout
|
||
|
||
from django.db.models import Sum
|
||
|
||
from gvsdsdk.fluent import db, func, FQ
|
||
|
||
logger = logging.getLogger('yonghu.tixian_shenhe')
|
||
|
||
ORDER_STATUS_COMPLETED = 3
|
||
ORDER_STATUS_REFUNDED = 5
|
||
|
||
LEIXING_RATE_KEY = {
|
||
1: '5',
|
||
2: '6',
|
||
3: '8',
|
||
4: '9',
|
||
5: '11',
|
||
6: '10',
|
||
}
|
||
|
||
def _club_id_for_user(user_main):
|
||
return club_id_for_tixian_yonghuid(user_main.UserUID)
|
||
|
||
|
||
def get_tixian_feilv(leixing, yonghuid=None, club_id=None):
|
||
"""按用户所属俱乐部读取提现手续费率。"""
|
||
if club_id is None:
|
||
if not yonghuid:
|
||
raise ValueError('缺少用户或俱乐部')
|
||
club_id = club_id_for_tixian_yonghuid(yonghuid)
|
||
return _get_tixian_feilv_for_club(club_id, leixing)
|
||
|
||
|
||
DAKUAN_MODE_MANUAL = 1
|
||
DAKUAN_MODE_AUTO = 2
|
||
|
||
# 微信转账单状态(查单接口返回 state 字段)
|
||
WX_STATE_SUCCESS = ('SUCCESS',)
|
||
WX_STATE_WAIT = ('WAIT_USER_CONFIRM', 'TRANSFERING', 'ACCEPTED', 'PROCESSING')
|
||
WX_STATE_FAIL = ('FAIL', 'CLOSED', 'CANCELLED')
|
||
WX_STATE_CANCELING = ('CANCELING',) # 撤销中,非终态,禁止换新单号
|
||
|
||
RECONCILE_COMPLETED = 'completed'
|
||
RECONCILE_WAIT_CONFIRM = 'wait_confirm'
|
||
RECONCILE_ALLOW_NEW = 'allow_new'
|
||
RECONCILE_PENDING = 'pending_query'
|
||
RECONCILE_AUDIT_CLOSED = 'audit_closed'
|
||
|
||
# 单笔实际到账上限(仅收款时校验)
|
||
MAX_SINGLE_COLLECT_AMOUNT = decimal.Decimal('200')
|
||
|
||
# 查单失败且本地 zhuangtai=0 超过该分钟数,才允许新建打款记录
|
||
PENDING_QUERY_GRACE_MINUTES = 10
|
||
|
||
|
||
def has_dakuan_mode_field():
|
||
"""生产库未跑 0032 迁移时模型无此字段,需兼容"""
|
||
try:
|
||
TixianShenheJilu._meta.get_field('dakuan_mode')
|
||
return True
|
||
except Exception:
|
||
return False
|
||
|
||
|
||
def is_auto_dakuan_audit(audit):
|
||
"""判断是否自动打款审核单;无 dakuan_mode 属性时,有审核记录即视为 zddksh 自动流程"""
|
||
if audit is None:
|
||
return False
|
||
mode = getattr(audit, 'dakuan_mode', None)
|
||
if mode is None:
|
||
return True
|
||
try:
|
||
return int(mode) == DAKUAN_MODE_AUTO
|
||
except (TypeError, ValueError):
|
||
return True
|
||
|
||
|
||
def load_audit_dakuan_mode_map(audit_ids):
|
||
"""批量取审核单打款方式;字段不存在时有关联审核即视为自动(2)"""
|
||
return {
|
||
aid: meta.get('dakuan_mode', DAKUAN_MODE_AUTO)
|
||
for aid, meta in load_audit_meta_map(audit_ids).items()
|
||
}
|
||
|
||
|
||
def load_audit_meta_map(audit_ids):
|
||
"""
|
||
批量取审核表元数据(供后台列表等)
|
||
返回 {审核记录id: {'dakuan_mode': int, 'shenhe_danhao': str}}
|
||
"""
|
||
if not audit_ids:
|
||
return {}
|
||
value_fields = ['id', 'shenhe_danhao']
|
||
if has_dakuan_mode_field():
|
||
value_fields.append('dakuan_mode')
|
||
result = {}
|
||
for row in TixianShenheJilu.query.filter(id__in=audit_ids).values(*value_fields):
|
||
result[row['id']] = {
|
||
'shenhe_danhao': row.get('shenhe_danhao') or '',
|
||
'dakuan_mode': row.get('dakuan_mode', DAKUAN_MODE_AUTO),
|
||
}
|
||
return result
|
||
|
||
|
||
def generate_shenhe_danhao():
|
||
"""生成唯一审核单号:TXSH + 时间戳后10位 + 4位随机数"""
|
||
for _ in range(10):
|
||
timestamp = str(int(time.time() * 1000))[-10:]
|
||
random_part = str(random.randint(1000, 9999))
|
||
danhao = f'TXSH{timestamp}{random_part}'
|
||
if not TixianShenheJilu.query.filter(shenhe_danhao=danhao).exists():
|
||
return danhao
|
||
raise RuntimeError('生成审核单号失败,请重试')
|
||
|
||
|
||
def calc_fee_amounts(jine, feilv):
|
||
"""申请金额 jine;实际到账 = jine - 手续费"""
|
||
shouxufei = (jine * feilv).quantize(decimal.Decimal('0.01'))
|
||
shijidaozhang = (jine - shouxufei).quantize(decimal.Decimal('0.01'))
|
||
return shouxufei, shijidaozhang
|
||
|
||
|
||
def check_dashou_has_valid_huiyuan(yonghuid):
|
||
"""
|
||
打手至少持有一个未过期会员(Huiyuangoumai + jiance_shifou_daoqi 同步状态)
|
||
返回 (True, '') 或 (False, 原因)
|
||
"""
|
||
|
||
records = Huiyuangoumai.query.filter(yonghu_id=yonghuid)
|
||
if not records.exists():
|
||
return False, '您暂无会员记录,无法提现'
|
||
|
||
for record in records:
|
||
is_expired = record.jiance_shifou_daoqi()
|
||
if not is_expired and record.huiyuan_zhuangtai == 1:
|
||
return True, ''
|
||
return False, '您的会员已全部过期,请先续费后再操作'
|
||
|
||
|
||
def validate_withdraw_eligibility(user_main, leixing, jine):
|
||
"""
|
||
提现申请资格校验(完整基本条件;仅放宽「不限制并行申请笔数」)。
|
||
与手动提现 TixianShenqingView 对齐。
|
||
"""
|
||
yonghuid = user_main.UserUID
|
||
|
||
fadan_ok, fadan_msg = check_fadan_qiangdan_eligible(yonghuid, 2)
|
||
if not fadan_ok:
|
||
return False, fadan_msg, {}
|
||
|
||
if leixing == 1:
|
||
try:
|
||
dashou = UserDashou.query.get(user=user_main)
|
||
except UserDashou.DoesNotExist:
|
||
return False, '您不是打手身份,无法提现佣金', {}
|
||
if dashou.zhanghaozhuangtai != 1:
|
||
return False, '打手账号已被封禁,无法提现', {}
|
||
if dashou.zhuangtai != 1:
|
||
return False, '有订单进行中,请完成后提现', {}
|
||
if dashou.jifen != 10:
|
||
return False, '积分不足10分,请补充积分后提现', {}
|
||
huiyuan_ok, huiyuan_msg = check_dashou_has_valid_huiyuan(yonghuid)
|
||
if not huiyuan_ok:
|
||
return False, huiyuan_msg, {}
|
||
if dashou.yue < jine:
|
||
return False, f'余额不足,当前余额: {dashou.yue}', {}
|
||
return True, '', {'nicheng': dashou.nicheng or ''}
|
||
|
||
if leixing == 2:
|
||
try:
|
||
guanshi = UserGuanshi.query.get(user=user_main)
|
||
except UserGuanshi.DoesNotExist:
|
||
return False, '您不是管事身份,无法提现分红', {}
|
||
if guanshi.zhuangtai != 1:
|
||
return False, '管事账号已被封禁,无法提现', {}
|
||
if guanshi.yue < jine:
|
||
return False, f'余额不足,当前余额: {guanshi.yue}', {}
|
||
return True, '', {'nicheng': '管事用户'}
|
||
|
||
if leixing == 3:
|
||
try:
|
||
zuzhang = UserZuzhang.query.get(user=user_main)
|
||
except UserZuzhang.DoesNotExist:
|
||
return False, '您不是组长身份,无法提现组长分红', {}
|
||
if zuzhang.zhuangtai != 1:
|
||
return False, '组长账号已被封禁,无法提现', {}
|
||
if zuzhang.ketixian_jine < jine:
|
||
return False, f'可提现金额不足,当前可提: {zuzhang.ketixian_jine}', {}
|
||
return True, '', {'nicheng': ''}
|
||
|
||
if leixing == 4:
|
||
try:
|
||
kaoheguan = UserShenheguan.query.get(user=user_main)
|
||
except UserShenheguan.DoesNotExist:
|
||
return False, '您不是考核官身份,无法提现分佣', {}
|
||
if kaoheguan.zhuangtai != 1:
|
||
return False, '考核官账号已被封禁,无法提现', {}
|
||
if kaoheguan.yue < jine:
|
||
return False, f'考核官可提现金额不足,当前余额: {kaoheguan.yue}', {}
|
||
return True, '', {'nicheng': '考核官'}
|
||
|
||
if leixing == 5:
|
||
try:
|
||
dashou = UserDashou.query.get(user=user_main)
|
||
except UserDashou.DoesNotExist:
|
||
return False, '您不是打手身份,无法提取押金', {}
|
||
if dashou.zhanghaozhuangtai != 1:
|
||
return False, '打手账号已禁用,无法提取押金', {}
|
||
if dashou.zhuangtai != 1:
|
||
return False, '您有订单进行中,请完成后提取押金', {}
|
||
if dashou.jifen != 10:
|
||
return False, '积分不足10分,请补充积分后提取押金', {}
|
||
huiyuan_ok, huiyuan_msg = check_dashou_has_valid_huiyuan(yonghuid)
|
||
if not huiyuan_ok:
|
||
return False, huiyuan_msg, {}
|
||
if dashou.yajin < jine:
|
||
return False, f'押金余额不足,当前押金: {dashou.yajin}', {}
|
||
unfinished = Order.query.filter(
|
||
PlayerID=yonghuid,
|
||
).exclude(Status__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]).exists()
|
||
if unfinished:
|
||
return False, '您还有未完成或未退款的订单,请先处理完毕再提取押金', {}
|
||
self_fadan = Penalty.query.filter(PenalizedUserID=yonghuid).exclude(Status__in=[2, 4]).exists()
|
||
if self_fadan:
|
||
return False, '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理', {}
|
||
return True, '', {'nicheng': dashou.nicheng or ''}
|
||
|
||
if leixing == 6:
|
||
try:
|
||
shangjia = UserShangjia.query.get(user=user_main)
|
||
except UserShangjia.DoesNotExist:
|
||
return False, '您不是商家身份,无法提现商家余额', {}
|
||
if shangjia.zhuangtai != 1:
|
||
return False, '商家账号已被禁用,无法提现', {}
|
||
if shangjia.yue < jine:
|
||
return False, f'商家余额不足,当前余额: {shangjia.yue}', {}
|
||
unfinished_orders = MerchantOrderExt.query.filter(
|
||
MerchantID=yonghuid,
|
||
).exclude(Order__Status__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]).exists()
|
||
if unfinished_orders:
|
||
return False, '您还有未完成或未退款的订单,请先处理完毕再提现余额', {}
|
||
self_fadan = Penalty.query.filter(PenalizedUserID=yonghuid).exclude(Status__in=[2, 4]).exists()
|
||
if self_fadan:
|
||
return False, '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理', {}
|
||
applied_fadan = Penalty.query.filter(ApplicantID=yonghuid).exclude(Status__in=[2, 4]).exists()
|
||
if applied_fadan:
|
||
return False, '您发起的对其他用户的处罚尚未处理完毕,请等待完结', {}
|
||
return True, '', {'nicheng': shangjia.nicheng or ''}
|
||
|
||
return False, '无效的提现类型', {}
|
||
|
||
|
||
def check_shijidaozhang_within_limit(shijidaozhang):
|
||
"""校验单笔实际到账不超过上限(申请/审核/收款共用)"""
|
||
if shijidaozhang > MAX_SINGLE_COLLECT_AMOUNT:
|
||
return False, f'单笔实际到账不能超过{MAX_SINGLE_COLLECT_AMOUNT}元,请调整提现金额后重新申请'
|
||
return True, ''
|
||
|
||
|
||
def validate_collect_amount(audit):
|
||
"""收款时校验单笔实际到账上限(超限将关单退款)"""
|
||
return check_shijidaozhang_within_limit(audit.shijidaozhang)
|
||
|
||
|
||
def validate_collect_eligibility(user_main, leixing):
|
||
"""
|
||
收款前严格校验(申请时已扣款,不再校余额)。
|
||
打手佣金/押金:罚单、封禁、接单中、积分、会员、订单等;
|
||
其他身份:账号状态、罚单/订单等。
|
||
"""
|
||
yonghuid = user_main.UserUID
|
||
|
||
fadan_ok, fadan_msg = check_fadan_qiangdan_eligible(yonghuid, 2)
|
||
if not fadan_ok:
|
||
return False, fadan_msg
|
||
|
||
if leixing == 1:
|
||
try:
|
||
dashou = UserDashou.query.get(user=user_main)
|
||
except UserDashou.DoesNotExist:
|
||
return False, '您不是打手身份,无法收款'
|
||
if dashou.zhanghaozhuangtai != 1:
|
||
return False, '打手账号已被封禁,无法收款'
|
||
if dashou.zhuangtai != 1:
|
||
return False, '您有订单进行中,请完成后再收款'
|
||
if dashou.jifen != 10:
|
||
return False, '积分不足10分,无法收款'
|
||
huiyuan_ok, huiyuan_msg = check_dashou_has_valid_huiyuan(yonghuid)
|
||
if not huiyuan_ok:
|
||
return False, huiyuan_msg
|
||
return True, ''
|
||
|
||
if leixing == 5:
|
||
try:
|
||
dashou = UserDashou.query.get(user=user_main)
|
||
except UserDashou.DoesNotExist:
|
||
return False, '您不是打手身份,无法收款'
|
||
if dashou.zhanghaozhuangtai != 1:
|
||
return False, '打手账号已禁用,无法收款'
|
||
if dashou.zhuangtai != 1:
|
||
return False, '您有订单进行中,请完成后再收款'
|
||
if dashou.jifen != 10:
|
||
return False, '积分不足10分,无法收款'
|
||
huiyuan_ok, huiyuan_msg = check_dashou_has_valid_huiyuan(yonghuid)
|
||
if not huiyuan_ok:
|
||
return False, huiyuan_msg
|
||
unfinished = Order.query.filter(
|
||
PlayerID=yonghuid,
|
||
).exclude(Status__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]).exists()
|
||
if unfinished:
|
||
return False, '您还有未完成或未退款的订单,无法收款'
|
||
self_fadan = Penalty.query.filter(PenalizedUserID=yonghuid).exclude(Status__in=[2, 4]).exists()
|
||
if self_fadan:
|
||
return False, '您有未处理完毕的自身罚单,无法收款'
|
||
return True, ''
|
||
|
||
if leixing == 2:
|
||
try:
|
||
guanshi = UserGuanshi.query.get(user=user_main)
|
||
except UserGuanshi.DoesNotExist:
|
||
return False, '您不是管事身份,无法收款'
|
||
if guanshi.zhuangtai != 1:
|
||
return False, '管事账号已被封禁,无法收款'
|
||
return True, ''
|
||
|
||
if leixing == 6:
|
||
try:
|
||
shangjia = UserShangjia.query.get(user=user_main)
|
||
except UserShangjia.DoesNotExist:
|
||
return False, '您不是商家身份,无法收款'
|
||
if shangjia.zhuangtai != 1:
|
||
return False, '商家账号已被禁用,无法收款'
|
||
unfinished_orders = MerchantOrderExt.query.filter(
|
||
MerchantID=yonghuid,
|
||
).exclude(Order__Status__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]).exists()
|
||
if unfinished_orders:
|
||
return False, '您还有未完成或未退款的订单,无法收款'
|
||
self_fadan = Penalty.query.filter(PenalizedUserID=yonghuid).exclude(Status__in=[2, 4]).exists()
|
||
if self_fadan:
|
||
return False, '您有未处理完毕的自身罚单,无法收款'
|
||
applied_fadan = Penalty.query.filter(ApplicantID=yonghuid).exclude(Status__in=[2, 4]).exists()
|
||
if applied_fadan:
|
||
return False, '您发起的处罚尚未处理完毕,无法收款'
|
||
return True, ''
|
||
|
||
if leixing == 3:
|
||
try:
|
||
zuzhang = UserZuzhang.query.get(user=user_main)
|
||
except UserZuzhang.DoesNotExist:
|
||
return False, '您不是组长身份,无法收款'
|
||
if zuzhang.zhuangtai != 1:
|
||
return False, '组长账号已被封禁,无法收款'
|
||
return True, ''
|
||
|
||
if leixing == 4:
|
||
try:
|
||
kaoheguan = UserShenheguan.query.get(user=user_main)
|
||
except UserShenheguan.DoesNotExist:
|
||
return False, '您不是考核官身份,无法收款'
|
||
if kaoheguan.zhuangtai != 1:
|
||
return False, '考核官账号已被封禁,无法收款'
|
||
return True, ''
|
||
|
||
return False, '无效的提现类型'
|
||
|
||
|
||
def _get_personal_daily_quota(club_id, leixing, profile):
|
||
return get_personal_daily_quota(club_id, leixing, profile)
|
||
|
||
|
||
def _get_active_auto_record(shenhe_danhao):
|
||
if not shenhe_danhao:
|
||
return None
|
||
return (
|
||
TixianAutoRecord.query.filter(
|
||
shenhe_danhao=shenhe_danhao,
|
||
zhuangtai__in=[0, 1],
|
||
)
|
||
.order_by('-CreateTime')
|
||
.first()
|
||
)
|
||
|
||
|
||
def _get_platform_daily_total(club_id, leixing, today=None):
|
||
return get_platform_daily_total(club_id, leixing, today)
|
||
|
||
|
||
def _calc_platform_projected(club_id, leixing, shijidaozhang, shenhe_danhao=''):
|
||
daily_used = _get_platform_daily_total(club_id, leixing)
|
||
active_rec = _get_active_auto_record(shenhe_danhao)
|
||
if active_rec:
|
||
return daily_used, daily_used, decimal.Decimal('0.00')
|
||
additional = shijidaozhang or decimal.Decimal('0.00')
|
||
return daily_used, daily_used + additional, additional
|
||
|
||
|
||
def _projected_personal_total(personal_today, jine, shenhe_danhao=''):
|
||
"""个人今日占用预估(申请金额口径)"""
|
||
rec = _get_active_auto_record(shenhe_danhao)
|
||
already_counted = rec.jine if rec else decimal.Decimal('0.00')
|
||
return personal_today - already_counted + jine
|
||
|
||
|
||
def _get_platform_daily_limit(club_id, leixing):
|
||
limit = get_platform_daily_limit(club_id, leixing)
|
||
if limit <= 0:
|
||
logger.warning(
|
||
'平台每日总限额为0 club_id=%s leixing=%s',
|
||
club_id, leixing,
|
||
)
|
||
return limit
|
||
|
||
|
||
def _check_personal_quota_readonly(user_main, leixing, jine, shenhe_danhao=''):
|
||
"""个人每日限额只读校验(打手/管事/组长)"""
|
||
if leixing not in (1, 2, 3):
|
||
return True, '', ''
|
||
|
||
today = date.today()
|
||
if leixing == 1:
|
||
try:
|
||
profile = UserDashou.query.get(user=user_main)
|
||
except UserDashou.DoesNotExist:
|
||
return False, '打手信息不存在', 'personal'
|
||
if profile.last_tixian_date != today:
|
||
personal_today = decimal.Decimal('0.00')
|
||
else:
|
||
personal_today = profile.jinritixian_jine
|
||
elif leixing == 2:
|
||
try:
|
||
profile = UserGuanshi.query.get(user=user_main)
|
||
except UserGuanshi.DoesNotExist:
|
||
return False, '管事信息不存在', 'personal'
|
||
if profile.last_tixian_date != today:
|
||
personal_today = decimal.Decimal('0.00')
|
||
else:
|
||
personal_today = profile.jinritixian_jine
|
||
else:
|
||
try:
|
||
profile = UserZuzhang.query.get(user=user_main)
|
||
except UserZuzhang.DoesNotExist:
|
||
return False, '组长信息不存在', 'personal'
|
||
if profile.last_tixian_time and profile.last_tixian_time.date() == today:
|
||
personal_today = profile.jinri_tixian
|
||
else:
|
||
personal_today = decimal.Decimal('0.00')
|
||
|
||
personal_quota = _get_personal_daily_quota(_club_id_for_user(user_main), leixing, profile)
|
||
projected_personal = _projected_personal_total(personal_today, jine, shenhe_danhao)
|
||
if projected_personal > personal_quota:
|
||
return (
|
||
False,
|
||
f'今日提现已达个人限额,今日已提{personal_today}元,本次申请{jine}元,限额{personal_quota}元',
|
||
'personal',
|
||
)
|
||
return True, '', ''
|
||
|
||
|
||
def _check_platform_quota_readonly(club_id, leixing, shijidaozhang, shenhe_danhao=''):
|
||
"""平台每日总限额只读校验:今日已用 + 本笔到账 > 总限额 则拒绝"""
|
||
platform_limit = _get_platform_daily_limit(club_id, leixing)
|
||
shijidaozhang = shijidaozhang or decimal.Decimal('0.00')
|
||
|
||
if platform_limit <= 0 and shijidaozhang > 0:
|
||
return False, '今日该角色提现已关闭(总限额为0),暂无法收款', 'platform'
|
||
|
||
daily_used, projected, additional = _calc_platform_projected(
|
||
club_id, leixing, shijidaozhang, shenhe_danhao,
|
||
)
|
||
if projected > platform_limit:
|
||
logger.warning(
|
||
'平台总限额拦截 club_id=%s leixing=%s shenhe_danhao=%s daily_used=%s additional=%s '
|
||
'projected=%s limit=%s',
|
||
club_id, leixing, shenhe_danhao, daily_used, additional, projected, platform_limit,
|
||
)
|
||
return (
|
||
False,
|
||
f'今日该角色提现总额已达上限,今日已提{daily_used}元,本次到账{additional}元,限额{platform_limit}元',
|
||
'platform',
|
||
)
|
||
|
||
return True, '', ''
|
||
|
||
|
||
def check_collect_quota_limits(user_main, leixing, jine, shijidaozhang, shenhe_danhao=''):
|
||
"""
|
||
收款限额只读校验(复用待确认打款单等场景,不写入)。
|
||
返回 (True, '', '') 或 (False, msg, 'personal'|'platform')
|
||
"""
|
||
ok, msg, kind = _check_personal_quota_readonly(user_main, leixing, jine, shenhe_danhao)
|
||
if not ok:
|
||
return False, msg, kind
|
||
club_id = _club_id_for_user(user_main)
|
||
return _check_platform_quota_readonly(club_id, leixing, shijidaozhang, shenhe_danhao)
|
||
|
||
|
||
def reserve_collect_quota_limits(user_main, leixing, jine, shijidaozhang, shenhe_danhao=''):
|
||
"""
|
||
收款前原子预占每日限额(必须在 transaction.atomic 内调用)。
|
||
锁定 WithdrawalDailyStats + 用户行,校验通过后立即写入统计,防止高并发超额。
|
||
本审核单已有进行中/成功打款记录则视为已预占,不重复加计。
|
||
返回 (True, '', '') 或 (False, msg, 'personal'|'platform')
|
||
"""
|
||
|
||
today = date.today()
|
||
shijidaozhang = shijidaozhang or decimal.Decimal('0.00')
|
||
club_id = _club_id_for_user(user_main)
|
||
|
||
if _get_active_auto_record(shenhe_danhao):
|
||
return True, '', ''
|
||
|
||
personal_profile = None
|
||
if leixing in (1, 2, 3):
|
||
if leixing == 1:
|
||
personal_profile = UserDashou.objects.select_for_update().get(user=user_main)
|
||
if personal_profile.last_tixian_date != today:
|
||
personal_profile.jinritixian_jine = decimal.Decimal('0.00')
|
||
personal_profile.last_tixian_date = today
|
||
personal_today = personal_profile.jinritixian_jine
|
||
elif leixing == 2:
|
||
personal_profile = UserGuanshi.objects.select_for_update().get(user=user_main)
|
||
if personal_profile.last_tixian_date != today:
|
||
personal_profile.jinritixian_jine = decimal.Decimal('0.00')
|
||
personal_profile.last_tixian_date = today
|
||
personal_today = personal_profile.jinritixian_jine
|
||
else:
|
||
personal_profile = UserZuzhang.objects.select_for_update().get(user=user_main)
|
||
if personal_profile.last_tixian_time and personal_profile.last_tixian_time.date() != today:
|
||
personal_profile.jinri_tixian = decimal.Decimal('0.00')
|
||
elif not personal_profile.last_tixian_time:
|
||
personal_profile.jinri_tixian = decimal.Decimal('0.00')
|
||
personal_today = personal_profile.jinri_tixian
|
||
|
||
personal_quota = _get_personal_daily_quota(club_id, leixing, personal_profile)
|
||
if personal_today + jine > personal_quota:
|
||
return (
|
||
False,
|
||
f'今日提现已达个人限额,今日已提{personal_today}元,本次申请{jine}元,限额{personal_quota}元',
|
||
'personal',
|
||
)
|
||
|
||
platform_stat = get_or_create_platform_daily_stat(club_id, leixing, today)
|
||
platform_limit = _get_platform_daily_limit(club_id, leixing)
|
||
if platform_limit <= 0 and shijidaozhang > 0:
|
||
return False, '今日该角色提现已关闭(总限额为0),暂无法收款', 'platform'
|
||
if platform_stat.total_amount + shijidaozhang > platform_limit:
|
||
return (
|
||
False,
|
||
f'今日该角色提现总额已达上限,今日已提{platform_stat.total_amount}元,'
|
||
f'本次到账{shijidaozhang}元,限额{platform_limit}元',
|
||
'platform',
|
||
)
|
||
|
||
if personal_profile is not None:
|
||
if leixing == 1:
|
||
personal_profile.jinritixian_jine += jine
|
||
personal_profile.save(update_fields=['jinritixian_jine', 'last_tixian_date'])
|
||
elif leixing == 2:
|
||
personal_profile.jinritixian_jine += jine
|
||
personal_profile.save(update_fields=['jinritixian_jine', 'last_tixian_date'])
|
||
else:
|
||
personal_profile.jinri_tixian += jine
|
||
personal_profile.last_tixian_time = timezone.now()
|
||
personal_profile.save(update_fields=['jinri_tixian', 'last_tixian_time'])
|
||
|
||
platform_stat.total_amount += shijidaozhang
|
||
platform_stat.save(update_fields=['total_amount'])
|
||
|
||
logger.info(
|
||
'收款限额预占成功 yonghuid=%s leixing=%s shenhe_danhao=%s jine=%s shijidaozhang=%s platform_total=%s',
|
||
user_main.UserUID, leixing, shenhe_danhao, jine, shijidaozhang, platform_stat.total_amount,
|
||
)
|
||
return True, '', ''
|
||
|
||
|
||
def release_collect_quota_reservation(user_main, leixing, jine, shijidaozhang):
|
||
"""微信打款明确失败/用户取消时回滚收款预占的限额"""
|
||
today = date.today()
|
||
shijidaozhang = shijidaozhang or decimal.Decimal('0.00')
|
||
club_id = _club_id_for_user(user_main)
|
||
try:
|
||
with transaction.atomic():
|
||
if leixing == 1:
|
||
dashou = UserDashou.objects.select_for_update().get(user=user_main)
|
||
if dashou.last_tixian_date == today and dashou.jinritixian_jine >= jine:
|
||
dashou.jinritixian_jine -= jine
|
||
dashou.save(update_fields=['jinritixian_jine'])
|
||
elif leixing == 2:
|
||
guanshi = UserGuanshi.objects.select_for_update().get(user=user_main)
|
||
if guanshi.last_tixian_date == today and guanshi.jinritixian_jine >= jine:
|
||
guanshi.jinritixian_jine -= jine
|
||
guanshi.save(update_fields=['jinritixian_jine'])
|
||
elif leixing == 3:
|
||
zuzhang = UserZuzhang.objects.select_for_update().get(user=user_main)
|
||
if zuzhang.jinri_tixian >= jine:
|
||
zuzhang.jinri_tixian -= jine
|
||
zuzhang.save(update_fields=['jinri_tixian'])
|
||
|
||
try:
|
||
stat = WithdrawalDailyStats.objects.select_for_update().get(
|
||
club_id=club_id, Date=today, WithdrawalType=leixing,
|
||
)
|
||
stat.total_amount = max(
|
||
stat.total_amount - shijidaozhang,
|
||
decimal.Decimal('0.00'),
|
||
)
|
||
stat.save(update_fields=['total_amount'])
|
||
except WithdrawalDailyStats.DoesNotExist:
|
||
pass
|
||
except Exception as e:
|
||
logger.error(
|
||
'回滚收款限额预占失败 yonghuid=%s leixing=%s err=%s',
|
||
user_main.UserUID, leixing, e, exc_info=True,
|
||
)
|
||
return
|
||
|
||
logger.info(
|
||
'收款限额预占已回滚 yonghuid=%s leixing=%s jine=%s shijidaozhang=%s',
|
||
user_main.UserUID, leixing, jine, shijidaozhang,
|
||
)
|
||
|
||
|
||
def release_collect_quota_for_record(auto_record):
|
||
"""幂等回滚单笔打款对应的收款预占(查单/回调/取消等多路径共用,防重复扣减)"""
|
||
if not auto_record.shenhe_danhao:
|
||
return
|
||
if not _quota_already_reserved_for_record(auto_record):
|
||
return
|
||
try:
|
||
user_main = User.query.get(UserUID=auto_record.yonghuid)
|
||
except User.DoesNotExist:
|
||
logger.error('回滚限额预占失败:用户不存在 %s', auto_record.yonghuid)
|
||
return
|
||
release_collect_quota_reservation(
|
||
user_main,
|
||
auto_record.leixing,
|
||
auto_record.jine,
|
||
auto_record.shijidaozhang,
|
||
)
|
||
|
||
|
||
def _quota_already_reserved_for_record(auto_record):
|
||
"""判断本笔打款是否已在收款预占时写入日统计(避免成功时重复累加)"""
|
||
if not auto_record.shenhe_danhao:
|
||
return False
|
||
|
||
today = date.today()
|
||
shijidaozhang = auto_record.shijidaozhang or decimal.Decimal('0.00')
|
||
club_id = club_id_for_tixian_yonghuid(auto_record.yonghuid)
|
||
stat_total = _get_platform_daily_total(club_id, auto_record.leixing, today)
|
||
other_sum = TixianAutoRecord.query.filter(
|
||
club_id=club_id,
|
||
leixing=auto_record.leixing,
|
||
zhuangtai__in=[0, 1],
|
||
CreateTime__date=today,
|
||
).exclude(tixian_id=auto_record.tixian_id).aggregate(
|
||
total=Sum('shijidaozhang'),
|
||
)['total'] or decimal.Decimal('0.00')
|
||
return stat_total >= other_sum + shijidaozhang
|
||
|
||
|
||
def apply_transfer_success_quota(auto_record):
|
||
"""
|
||
打款成功时累加每日限额统计(仅自动审核流程 shenhe_danhao 非空)。
|
||
- houtai.WithdrawalDailyStats.total_amount += 实际到账
|
||
- 用户扩展表个人今日已提 += 申请金额(打手/管事/组长)
|
||
须在 transaction.atomic 内、且打款记录 zhuangtai 由 0→1 时调用一次。
|
||
"""
|
||
if not auto_record.shenhe_danhao:
|
||
return
|
||
today = date.today()
|
||
leixing = auto_record.leixing
|
||
jine = auto_record.jine
|
||
shijidaozhang = auto_record.shijidaozhang or decimal.Decimal('0.00')
|
||
|
||
try:
|
||
user_main = User.query.get(UserUID=auto_record.yonghuid)
|
||
except User.DoesNotExist:
|
||
logger.error('打款成功累加限额失败:用户不存在 %s', auto_record.yonghuid)
|
||
return
|
||
|
||
club_id = _club_id_for_user(user_main)
|
||
platform_stat = get_or_create_platform_daily_stat(club_id, leixing, today)
|
||
platform_stat.total_amount += shijidaozhang
|
||
platform_stat.save(update_fields=['total_amount'])
|
||
|
||
if leixing == 1:
|
||
dashou = UserDashou.objects.select_for_update().get(user=user_main)
|
||
if dashou.last_tixian_date != today:
|
||
dashou.jinritixian_jine = decimal.Decimal('0.00')
|
||
dashou.last_tixian_date = today
|
||
dashou.jinritixian_jine += jine
|
||
dashou.save(update_fields=['jinritixian_jine', 'last_tixian_date'])
|
||
elif leixing == 2:
|
||
guanshi = UserGuanshi.objects.select_for_update().get(user=user_main)
|
||
if guanshi.last_tixian_date != today:
|
||
guanshi.jinritixian_jine = decimal.Decimal('0.00')
|
||
guanshi.last_tixian_date = today
|
||
guanshi.jinritixian_jine += jine
|
||
guanshi.save(update_fields=['jinritixian_jine', 'last_tixian_date'])
|
||
elif leixing == 3:
|
||
zuzhang = UserZuzhang.objects.select_for_update().get(user=user_main)
|
||
if zuzhang.last_tixian_time and zuzhang.last_tixian_time.date() != today:
|
||
zuzhang.jinri_tixian = decimal.Decimal('0.00')
|
||
elif not zuzhang.last_tixian_time:
|
||
zuzhang.jinri_tixian = decimal.Decimal('0.00')
|
||
zuzhang.jinri_tixian += jine
|
||
zuzhang.last_tixian_time = timezone.now()
|
||
zuzhang.save(update_fields=['jinri_tixian', 'last_tixian_time'])
|
||
|
||
logger.info(
|
||
'打款成功已累加限额 bill=%s yonghuid=%s leixing=%s jine=%s shijidaozhang=%s '
|
||
'platform_total=%s',
|
||
auto_record.tixian_id, auto_record.yonghuid, leixing, jine, shijidaozhang,
|
||
platform_stat.total_amount,
|
||
)
|
||
|
||
|
||
def refund_balance(user_main, leixing, jine):
|
||
"""审核拒绝时退还可到账金额 shijidaozhang(须在 transaction.atomic 内;手续费已在申请扣款时扣除不退还)"""
|
||
if leixing == 1:
|
||
dashou = UserDashou.objects.select_for_update().get(user=user_main)
|
||
dashou.yue += jine
|
||
dashou.save()
|
||
elif leixing == 2:
|
||
guanshi = UserGuanshi.objects.select_for_update().get(user=user_main)
|
||
guanshi.yue += jine
|
||
guanshi.save()
|
||
elif leixing == 3:
|
||
zuzhang = UserZuzhang.objects.select_for_update().get(user=user_main)
|
||
zuzhang.ketixian_jine += jine
|
||
zuzhang.save()
|
||
elif leixing == 4:
|
||
kaoheguan = UserShenheguan.objects.select_for_update().get(user=user_main)
|
||
kaoheguan.yue += jine
|
||
kaoheguan.save()
|
||
elif leixing == 5:
|
||
dashou = UserDashou.objects.select_for_update().get(user=user_main)
|
||
dashou.yajin += jine
|
||
dashou.zhanghaozhuangtai = 1
|
||
dashou.save()
|
||
elif leixing == 6:
|
||
shangjia = UserShangjia.objects.select_for_update().get(user=user_main)
|
||
shangjia.yue += jine
|
||
shangjia.save()
|
||
else:
|
||
raise ValueError('无效的提现类型')
|
||
|
||
|
||
def deduct_balance(user_main, leixing, jine):
|
||
"""扣减申请金额 jine(必须在 transaction.atomic 内调用)"""
|
||
if leixing == 1:
|
||
dashou = UserDashou.objects.select_for_update().get(user=user_main)
|
||
if dashou.yue < jine:
|
||
raise ValueError('余额不足')
|
||
dashou.yue -= jine
|
||
dashou.save()
|
||
return dashou.nicheng or ''
|
||
if leixing == 2:
|
||
guanshi = UserGuanshi.objects.select_for_update().get(user=user_main)
|
||
if guanshi.yue < jine:
|
||
raise ValueError('余额不足')
|
||
guanshi.yue -= jine
|
||
guanshi.save()
|
||
return '管事用户'
|
||
if leixing == 3:
|
||
zuzhang = UserZuzhang.objects.select_for_update().get(user=user_main)
|
||
if zuzhang.ketixian_jine < jine:
|
||
raise ValueError('可提现金额不足')
|
||
zuzhang.ketixian_jine -= jine
|
||
zuzhang.save()
|
||
return ''
|
||
if leixing == 4:
|
||
kaoheguan = UserShenheguan.objects.select_for_update().get(user=user_main)
|
||
if kaoheguan.yue < jine:
|
||
raise ValueError('可提现金额不足')
|
||
kaoheguan.yue -= jine
|
||
kaoheguan.save()
|
||
return '考核官'
|
||
if leixing == 5:
|
||
dashou = UserDashou.objects.select_for_update().get(user=user_main)
|
||
if dashou.yajin < jine:
|
||
raise ValueError('押金余额不足')
|
||
dashou.yajin -= jine
|
||
dashou.zhanghaozhuangtai = 0
|
||
dashou.save()
|
||
return dashou.nicheng or ''
|
||
if leixing == 6:
|
||
shangjia = UserShangjia.objects.select_for_update().get(user=user_main)
|
||
if shangjia.yue < jine:
|
||
raise ValueError('商家余额不足')
|
||
shangjia.yue -= jine
|
||
shangjia.save()
|
||
return shangjia.nicheng or ''
|
||
raise ValueError('无效的提现类型')
|
||
|
||
|
||
def get_balance_response_data(user_main, leixing):
|
||
data = {}
|
||
if leixing == 1:
|
||
try:
|
||
dashou = UserDashou.query.get(user=user_main)
|
||
data['new_yongjin'] = str(dashou.yue.quantize(decimal.Decimal('0.01')))
|
||
except UserDashou.DoesNotExist:
|
||
pass
|
||
elif leixing == 2:
|
||
try:
|
||
guanshi = UserGuanshi.query.get(user=user_main)
|
||
data['new_fenyong'] = str(guanshi.yue.quantize(decimal.Decimal('0.01')))
|
||
except UserGuanshi.DoesNotExist:
|
||
pass
|
||
elif leixing == 3:
|
||
try:
|
||
zuzhang = UserZuzhang.query.get(user=user_main)
|
||
data['new_zuzhang_ketixian'] = str(zuzhang.ketixian_jine.quantize(decimal.Decimal('0.01')))
|
||
except UserZuzhang.DoesNotExist:
|
||
pass
|
||
elif leixing == 4:
|
||
try:
|
||
kg = UserShenheguan.query.get(user=user_main)
|
||
data['new_kaoheguan_yue'] = str(kg.yue.quantize(decimal.Decimal('0.01')))
|
||
except UserShenheguan.DoesNotExist:
|
||
pass
|
||
return data
|
||
|
||
|
||
def sync_audit_and_jilu_status(audit, new_status, **extra_fields):
|
||
audit.zhuangtai = new_status
|
||
for k, v in extra_fields.items():
|
||
setattr(audit, k, v)
|
||
audit.save()
|
||
if audit.tixianjilu_id:
|
||
update_fields = {'zhuangtai': new_status, 'UpdateTime': timezone.now()}
|
||
if 'bhliyou' in extra_fields:
|
||
update_fields['bhliyou'] = extra_fields['bhliyou']
|
||
Tixianjilu.query.filter(id=audit.tixianjilu_id).update(**update_fields)
|
||
|
||
|
||
def _close_auto_record_failed(auto_record, fail_reason):
|
||
"""标记打款记录失败并释放收款预占限额"""
|
||
if auto_record is None or auto_record.zhuangtai == 1:
|
||
return
|
||
reason = (fail_reason or '打款失败')[:500]
|
||
auto_record.zhuangtai = 2
|
||
auto_record.fail_reason = reason
|
||
auto_record.save(update_fields=['zhuangtai', 'fail_reason', 'UpdateTime'])
|
||
release_collect_quota_for_record(auto_record)
|
||
|
||
|
||
def close_audit_wechat_failed(audit, auto_record=None, fail_reason='微信转账失败'):
|
||
"""
|
||
微信官方终态 FAIL/CANCELLED:审核单关闭(3),不退款(申请时已扣款,手续费不退)。
|
||
须在 transaction.atomic 内调用。
|
||
"""
|
||
_close_auto_record_failed(auto_record, fail_reason)
|
||
|
||
audit = TixianShenheJilu.objects.select_for_update().get(pk=audit.pk)
|
||
if audit.zhuangtai in (2, 3, 5):
|
||
return False
|
||
|
||
reason = (fail_reason or '微信转账失败')[:500]
|
||
sync_audit_and_jilu_status(
|
||
audit, 3,
|
||
bhliyou=reason,
|
||
fail_reason=reason,
|
||
tixian_auto_id=None,
|
||
)
|
||
logger.warning(
|
||
'微信失败关单(不退款) yonghuid=%s shenhe_danhao=%s reason=%s',
|
||
audit.yonghuid, audit.shenhe_danhao, reason,
|
||
)
|
||
return True
|
||
|
||
|
||
def close_audit_over_limit_refund(audit, auto_record=None, fail_reason='单笔实际到账超过限额'):
|
||
"""
|
||
到账超过200元等:审核单关闭(3) + 退还可到账金额 shijidaozhang(手续费不退)。
|
||
须在 transaction.atomic 内调用。
|
||
"""
|
||
reason = (fail_reason or '单笔实际到账超过限额')[:500]
|
||
_close_auto_record_failed(auto_record, reason)
|
||
|
||
audit = TixianShenheJilu.objects.select_for_update().get(pk=audit.pk)
|
||
if audit.zhuangtai in (2, 3, 5):
|
||
return False
|
||
|
||
user_main = User.objects.select_for_update().get(UserUID=audit.yonghuid)
|
||
refund_balance(user_main, audit.leixing, audit.shijidaozhang)
|
||
sync_audit_and_jilu_status(
|
||
audit, 3,
|
||
bhliyou=reason,
|
||
fail_reason=reason,
|
||
tixian_auto_id=None,
|
||
)
|
||
logger.warning(
|
||
'超限关单并退款 yonghuid=%s shenhe_danhao=%s reason=%s',
|
||
audit.yonghuid, audit.shenhe_danhao, reason,
|
||
)
|
||
return True
|
||
|
||
|
||
def query_wechat_transfer_bill(out_bill_no, club_id=None):
|
||
"""
|
||
向微信查转账单状态
|
||
GET /v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no}
|
||
返回 dict;网络/系统异常返回 None;微信无此单返回 {'_not_found': True}
|
||
"""
|
||
from jituan.services.wechat_pay import club_id_for_tixian_yonghuid
|
||
|
||
if not club_id:
|
||
try:
|
||
rec = TixianAutoRecord.query.filter(tixian_id=out_bill_no).first()
|
||
if rec:
|
||
club_id = club_id_for_tixian_yonghuid(rec.yonghuid)
|
||
except Exception:
|
||
pass
|
||
|
||
url = (
|
||
'https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/'
|
||
f'transfer-bills/out-bill-no/{out_bill_no}'
|
||
)
|
||
try:
|
||
auth = build_authorization('GET', url, '', club_id=club_id)
|
||
headers = {
|
||
'Authorization': auth,
|
||
'Accept': 'application/json',
|
||
'User-Agent': 'Mozilla/5.0',
|
||
}
|
||
resp = requests.get(url, headers=headers, timeout=10)
|
||
if resp.status_code == 200:
|
||
return resp.json()
|
||
if resp.status_code == 404:
|
||
return {'_not_found': True}
|
||
logger.error(
|
||
'微信查单失败 bill=%s HTTP%s body=%s',
|
||
out_bill_no, resp.status_code, resp.text[:300],
|
||
)
|
||
return None
|
||
except (requests.RequestException, OSError, ValueError) as e:
|
||
logger.error('微信查单异常 bill=%s err=%s', out_bill_no, e, exc_info=True)
|
||
return None
|
||
except Exception as e:
|
||
logger.error('微信查单未知异常 bill=%s err=%s', out_bill_no, e, exc_info=True)
|
||
return None
|
||
|
||
|
||
def _parse_package_info(raw):
|
||
if not raw:
|
||
return None
|
||
if isinstance(raw, dict):
|
||
return raw
|
||
try:
|
||
return json.loads(raw)
|
||
except (TypeError, ValueError):
|
||
return None
|
||
|
||
|
||
def _apply_platform_accounting(auto_record):
|
||
"""平台收支统计(幂等由 mark_transfer_success 外层保证只调一次)"""
|
||
from jituan.services.szjilu_accounting import apply_szjilu_expense
|
||
from jituan.services.wechat_pay import club_id_for_tixian_yonghuid
|
||
payout_club = club_id_for_tixian_yonghuid(auto_record.yonghuid)
|
||
update_daily_payout(auto_record.shijidaozhang, payout_club)
|
||
apply_szjilu_expense(auto_record.shijidaozhang, payout_club)
|
||
|
||
|
||
def mark_transfer_success(auto_record, *, wechat_transfer_no=None, with_accounting=True):
|
||
"""
|
||
打款成功落库(tixianqr / callback / 查单对账 共用)
|
||
select_for_update + zhuangtai==0 才更新并记账,防双写
|
||
返回 True=本次新标记成功;False=已处理过
|
||
"""
|
||
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', 'UpdateTime']
|
||
if wechat_transfer_no:
|
||
ar.wechat_transfer_no = wechat_transfer_no
|
||
update_fields.append('wechat_transfer_no')
|
||
ar.save(update_fields=update_fields)
|
||
|
||
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)
|
||
|
||
if ar.shenhe_danhao and not _quota_already_reserved_for_record(ar):
|
||
apply_transfer_success_quota(ar)
|
||
|
||
if with_accounting:
|
||
_apply_platform_accounting(ar)
|
||
|
||
return True
|
||
|
||
|
||
def _sync_record_from_wx_query(auto_record, wx_data):
|
||
"""根据微信查单结果修正本地打款记录,返回 (action, payload)"""
|
||
if wx_data.get('_not_found'):
|
||
age = timezone.now() - auto_record.CreateTime
|
||
if auto_record.zhuangtai == 0 and age < timedelta(minutes=PENDING_QUERY_GRACE_MINUTES):
|
||
return RECONCILE_PENDING, {
|
||
'msg': '有收款处理中,请稍后再试',
|
||
'tixian_id': auto_record.tixian_id,
|
||
}
|
||
if auto_record.zhuangtai == 0:
|
||
auto_record.zhuangtai = 2
|
||
auto_record.fail_reason = '微信未找到该打款单,请重新发起收款'
|
||
auto_record.save(update_fields=['zhuangtai', 'fail_reason', 'UpdateTime'])
|
||
release_collect_quota_for_record(auto_record)
|
||
return RECONCILE_ALLOW_NEW, {}
|
||
|
||
state = (wx_data.get('state') or '').upper()
|
||
|
||
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': '该提现已完成,请勿重复操作'}
|
||
|
||
if state in WX_STATE_WAIT:
|
||
package = wx_data.get('package_info') or _parse_package_info(auto_record.wechat_package)
|
||
if not package:
|
||
return RECONCILE_PENDING, {
|
||
'msg': '有收款处理中,请稍后再试',
|
||
'tixian_id': auto_record.tixian_id,
|
||
}
|
||
if auto_record.zhuangtai == 0:
|
||
auto_record.wechat_package = json.dumps(package, ensure_ascii=False)
|
||
auto_record.save(update_fields=['wechat_package', 'UpdateTime'])
|
||
wx_cfg = settings.WECHAT_PAY_V3_CONFIG
|
||
return RECONCILE_WAIT_CONFIRM, {
|
||
'tixian_id': auto_record.tixian_id,
|
||
'package_info': package,
|
||
'shouxufei': str(auto_record.shouxufei),
|
||
'shijidaozhang': str(auto_record.shijidaozhang),
|
||
'current_rate': str(auto_record.feilv),
|
||
'mch_id': wx_cfg.get('MCHID', ''),
|
||
'shenhe_danhao': auto_record.shenhe_danhao or '',
|
||
}
|
||
|
||
if state in WX_STATE_CANCELING:
|
||
return RECONCILE_PENDING, {
|
||
'msg': '收款撤销处理中,请稍后再试',
|
||
'tixian_id': auto_record.tixian_id,
|
||
}
|
||
|
||
if state in WX_STATE_FAIL:
|
||
fail_reason = (
|
||
wx_data.get('fail_reason') or wx_data.get('close_reason') or state
|
||
)[:500]
|
||
if auto_record.zhuangtai != 1:
|
||
auto_record.zhuangtai = 2
|
||
auto_record.fail_reason = fail_reason
|
||
auto_record.save(update_fields=['zhuangtai', 'fail_reason', 'UpdateTime'])
|
||
release_collect_quota_for_record(auto_record)
|
||
if auto_record.shenhe_danhao:
|
||
try:
|
||
audit = TixianShenheJilu.query.get(shenhe_danhao=auto_record.shenhe_danhao)
|
||
with transaction.atomic():
|
||
close_audit_wechat_failed(audit, None, fail_reason)
|
||
except TixianShenheJilu.DoesNotExist:
|
||
logger.warning('微信失败但审核记录不存在: %s', auto_record.shenhe_danhao)
|
||
return RECONCILE_AUDIT_CLOSED, {
|
||
'msg': '微信已确认转账失败,该笔提现已关闭,请联系客服处理',
|
||
}
|
||
|
||
if auto_record.zhuangtai == 0:
|
||
return RECONCILE_PENDING, {
|
||
'msg': f'收款处理中(微信状态:{state or "未知"}),请稍后再试',
|
||
'tixian_id': auto_record.tixian_id,
|
||
}
|
||
return RECONCILE_ALLOW_NEW, {}
|
||
|
||
|
||
def get_auto_record_for_collect(shenhe_danhao):
|
||
"""
|
||
取本笔提现申请应对接微信的那条打款记录:
|
||
优先 zhuangtai=0(进行中),否则取最新一条(可能本地误标失败但微信未终态)
|
||
"""
|
||
pending = (
|
||
TixianAutoRecord.query.filter(shenhe_danhao=shenhe_danhao, zhuangtai=0)
|
||
.order_by('-CreateTime')
|
||
.first()
|
||
)
|
||
if pending:
|
||
return pending
|
||
return (
|
||
TixianAutoRecord.query.filter(shenhe_danhao=shenhe_danhao)
|
||
.order_by('-CreateTime')
|
||
.first()
|
||
)
|
||
|
||
|
||
def reconcile_shenhe_wechat_bills(shenhe_danhao):
|
||
"""
|
||
P0 防双笔:同一 shenhe_danhao 下所有打款记录逐条问微信
|
||
任一 SUCCESS / 进行中 → 禁止新建;全部终态失败才 ALLOW_NEW
|
||
返回 (action, payload)
|
||
"""
|
||
records = list(
|
||
TixianAutoRecord.query.filter(shenhe_danhao=shenhe_danhao).order_by('-CreateTime')
|
||
)
|
||
|
||
if any(r.zhuangtai == 1 for r in records):
|
||
try:
|
||
audit = TixianShenheJilu.query.get(shenhe_danhao=shenhe_danhao)
|
||
if audit.zhuangtai != 2:
|
||
sync_audit_and_jilu_status(audit, 2)
|
||
except TixianShenheJilu.DoesNotExist:
|
||
pass
|
||
return RECONCILE_COMPLETED, {'msg': '该提现已完成,请勿重复操作'}
|
||
|
||
if not records:
|
||
return RECONCILE_ALLOW_NEW, {}
|
||
|
||
pending_hit = None
|
||
for rec in records:
|
||
if rec.zhuangtai == 1:
|
||
return RECONCILE_COMPLETED, {'msg': '该提现已完成,请勿重复操作'}
|
||
|
||
wx_data = query_wechat_transfer_bill(rec.tixian_id, club_id_for_tixian_yonghuid(rec.yonghuid))
|
||
if wx_data is None:
|
||
logger.error(
|
||
'对账查单不可用 shenhe_danhao=%s bill=%s',
|
||
shenhe_danhao, rec.tixian_id,
|
||
)
|
||
return RECONCILE_PENDING, {
|
||
'msg': '有收款处理中,系统正在核对微信状态,请稍后再试',
|
||
'tixian_id': rec.tixian_id,
|
||
}
|
||
|
||
action, payload = _sync_record_from_wx_query(rec, wx_data)
|
||
if action == RECONCILE_COMPLETED:
|
||
return action, payload
|
||
if action == RECONCILE_WAIT_CONFIRM:
|
||
return action, payload
|
||
if action == RECONCILE_AUDIT_CLOSED:
|
||
return action, payload
|
||
if action == RECONCILE_PENDING:
|
||
if pending_hit is None:
|
||
pending_hit = (action, payload)
|
||
continue
|
||
|
||
if pending_hit:
|
||
return pending_hit
|
||
return RECONCILE_ALLOW_NEW, {}
|
||
|
||
|
||
def resolve_collect_context(yonghuid, tixianjilu_id=None, shenhe_danhao='', shenhe_jilu_id=None):
|
||
"""
|
||
从提现记录 ID 进门(P0 收款主路径):
|
||
Tixianjilu → TixianShenheJilu,校验归属、状态=6 待收款
|
||
返回 ({jilu, audit}, err_msg)
|
||
"""
|
||
if tixianjilu_id is None and not shenhe_danhao:
|
||
return None, '缺少提现记录ID'
|
||
|
||
jilu = None
|
||
if tixianjilu_id is not None:
|
||
try:
|
||
jilu = Tixianjilu.query.get(id=int(tixianjilu_id), yonghuid=yonghuid)
|
||
except (Tixianjilu.DoesNotExist, TypeError, ValueError):
|
||
return None, '提现记录不存在或不属于当前用户'
|
||
|
||
if jilu.zhuangtai != 6:
|
||
return None, f'当前状态不可收款(状态{jilu.zhuangtai}),请等待审核通过'
|
||
if not jilu.shenhe_jilu_id or not jilu.shenhe_danhao:
|
||
return None, '提现记录未关联审核单,无法收款'
|
||
|
||
if shenhe_jilu_id is not None:
|
||
try:
|
||
if int(shenhe_jilu_id) != jilu.shenhe_jilu_id:
|
||
return None, '审核记录ID与提现记录不匹配'
|
||
except (TypeError, ValueError):
|
||
return None, '审核记录ID格式错误'
|
||
|
||
if shenhe_danhao and shenhe_danhao != jilu.shenhe_danhao:
|
||
return None, '审核单号与提现记录不匹配'
|
||
|
||
shenhe_danhao = jilu.shenhe_danhao
|
||
shenhe_jilu_id = jilu.shenhe_jilu_id
|
||
|
||
try:
|
||
if shenhe_jilu_id:
|
||
audit = TixianShenheJilu.query.get(
|
||
id=int(shenhe_jilu_id),
|
||
shenhe_danhao=shenhe_danhao,
|
||
yonghuid=yonghuid,
|
||
)
|
||
else:
|
||
audit = TixianShenheJilu.query.get(
|
||
shenhe_danhao=shenhe_danhao,
|
||
yonghuid=yonghuid,
|
||
)
|
||
except (TixianShenheJilu.DoesNotExist, TypeError, ValueError):
|
||
return None, '审核记录不存在'
|
||
|
||
if audit.zhuangtai == 2:
|
||
return None, '该提现已完成,请勿重复操作'
|
||
if audit.zhuangtai != 6:
|
||
return None, f'当前状态不可收款(状态{audit.zhuangtai}),请等待审核通过'
|
||
if not is_auto_dakuan_audit(audit):
|
||
return None, '该提现单为手动打款,无法使用自动收款'
|
||
|
||
if jilu is None:
|
||
try:
|
||
jilu = Tixianjilu.query.get(
|
||
id=audit.tixianjilu_id,
|
||
yonghuid=yonghuid,
|
||
) if audit.tixianjilu_id else None
|
||
except Tixianjilu.DoesNotExist:
|
||
jilu = None
|
||
|
||
return {'jilu': jilu, 'audit': audit, 'shenhe_danhao': shenhe_danhao}, ''
|
||
|
||
|
||
def handle_post_transfer_failure(tixian_id, shenhe_danhao, err_code='', err_msg=''):
|
||
"""
|
||
发起转账 HTTP 非成功时:先查微信再决定是否标失败(P0,禁止盲目标失败导致双笔)
|
||
返回 (action, payload_or_user_msg)
|
||
"""
|
||
try:
|
||
ar = TixianAutoRecord.query.get(tixian_id=tixian_id)
|
||
except TixianAutoRecord.DoesNotExist:
|
||
return RECONCILE_PENDING, '收款处理中,请稍后再试'
|
||
|
||
wx_data = query_wechat_transfer_bill(tixian_id, club_id_for_tixian_yonghuid(ar.yonghuid))
|
||
if wx_data is None:
|
||
logger.error(
|
||
'发起转账失败且查单不可用 bill=%s code=%s msg=%s,保持待查单',
|
||
tixian_id, err_code, err_msg,
|
||
)
|
||
return RECONCILE_PENDING, '收款请求已提交,系统正在核对状态,请稍后再试,切勿重复点击'
|
||
|
||
action, payload = _sync_record_from_wx_query(ar, wx_data)
|
||
if action == RECONCILE_ALLOW_NEW:
|
||
return RECONCILE_ALLOW_NEW, payload
|
||
if action == RECONCILE_WAIT_CONFIRM:
|
||
return RECONCILE_WAIT_CONFIRM, payload
|
||
if action == RECONCILE_COMPLETED:
|
||
return RECONCILE_COMPLETED, payload
|
||
return RECONCILE_PENDING, payload.get('msg', '收款处理中,请稍后再试')
|
||
|
||
|
||
def get_audit_for_collect(shenhe_danhao, yonghuid, shenhe_jilu_id=None):
|
||
"""
|
||
收款前数据库校验(须在 transaction.atomic 内调用;微信查单在事务外单独做)
|
||
"""
|
||
try:
|
||
audit = TixianShenheJilu.objects.select_for_update().get(
|
||
shenhe_danhao=shenhe_danhao,
|
||
yonghuid=yonghuid,
|
||
)
|
||
except TixianShenheJilu.DoesNotExist:
|
||
return None, '审核记录不存在'
|
||
|
||
if shenhe_jilu_id is not None:
|
||
try:
|
||
if int(shenhe_jilu_id) != audit.id:
|
||
return None, '审核记录ID不匹配'
|
||
except (TypeError, ValueError):
|
||
return None, '审核记录ID格式错误'
|
||
|
||
if audit.zhuangtai == 2:
|
||
return None, '该提现已完成,请勿重复操作'
|
||
|
||
if audit.zhuangtai != 6:
|
||
return None, f'当前状态不可收款(状态{audit.zhuangtai}),请等待审核通过'
|
||
|
||
if not is_auto_dakuan_audit(audit):
|
||
return None, '该提现单为手动打款,无法使用自动收款'
|
||
|
||
return audit, ''
|
||
|
||
|
||
def create_audit_application(user_main, leixing, jine):
|
||
"""
|
||
提交审核申请:校验 → 扣款 → 写审核表 + 提现记录表
|
||
不创建打款记录表(TixianAutoRecord 仅在收款时创建)
|
||
允许多笔并行申请(不阻塞待收款/审核中单)
|
||
"""
|
||
yonghuid = user_main.UserUID
|
||
|
||
ok, msg, extra = validate_withdraw_eligibility(user_main, leixing, jine)
|
||
if not ok:
|
||
raise ValueError(msg)
|
||
|
||
feilv = get_tixian_feilv(leixing, yonghuid=yonghuid)
|
||
shouxufei, shijidaozhang = calc_fee_amounts(jine, feilv)
|
||
if shijidaozhang <= 0:
|
||
raise ValueError('提现金额过低,扣除手续费后无实际到账')
|
||
|
||
ok, limit_msg = check_shijidaozhang_within_limit(shijidaozhang)
|
||
if not ok:
|
||
raise ValueError(limit_msg)
|
||
|
||
shenhe_danhao = generate_shenhe_danhao()
|
||
nicheng = extra.get('nicheng', '')
|
||
|
||
with transaction.atomic():
|
||
nicheng = deduct_balance(user_main, leixing, jine) or nicheng
|
||
club_id = club_id_for_tixian_yonghuid(yonghuid)
|
||
|
||
audit_kwargs = {
|
||
'shenhe_danhao': shenhe_danhao,
|
||
'club_id': club_id,
|
||
'yonghuid': yonghuid,
|
||
'leixing': leixing,
|
||
'zhuangtai': 1,
|
||
'shenqing_jine': jine,
|
||
'shouxufei': shouxufei,
|
||
'shijidaozhang': shijidaozhang,
|
||
'feilv': feilv,
|
||
}
|
||
if has_dakuan_mode_field():
|
||
audit_kwargs['dakuan_mode'] = DAKUAN_MODE_AUTO
|
||
audit = TixianShenheJilu.query.create(**audit_kwargs)
|
||
|
||
tixian_record = Tixianjilu.query.create(
|
||
club_id=club_id,
|
||
yonghuid=yonghuid,
|
||
avatar=user_main.Avatar or '',
|
||
phone=user_main.Phone or '',
|
||
nicheng=nicheng,
|
||
leixing=leixing,
|
||
zhifu=user_main.PaymentQRCode or '',
|
||
skzhanghao=user_main.PaymentAccount or '',
|
||
jine=shijidaozhang,
|
||
zhuangtai=1,
|
||
fangshi=1,
|
||
shenheid=None,
|
||
bhliyou='',
|
||
shenhe_jilu_id=audit.id,
|
||
shenhe_danhao=shenhe_danhao,
|
||
)
|
||
|
||
audit.tixianjilu_id = tixian_record.id
|
||
audit.save(update_fields=['tixianjilu_id'])
|
||
|
||
response_data = get_balance_response_data(user_main, leixing)
|
||
response_data.update({
|
||
'shenhe_danhao': shenhe_danhao,
|
||
'shenhe_jilu_id': audit.id,
|
||
'tixianjilu_id': tixian_record.id,
|
||
'shouxufei': str(shouxufei),
|
||
'shijidaozhang': str(shijidaozhang),
|
||
'current_rate': str(feilv.quantize(decimal.Decimal('0.0001'))),
|
||
'leixing': leixing,
|
||
'zhuangtai': 1,
|
||
})
|
||
return response_data
|