Files
Django/users/tixian_shenhe_services.py

1436 lines
57 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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, '您的会员已全部过期,请先续费后再操作'
TRIAL_COMMISSION_WITHDRAW_MSG = (
'体验会员期间不可提现佣金,请升级正式会员或待体验到期'
)
def check_dashou_trial_blocks_commission_withdraw(yonghuid):
"""打手佣金提现/收款:体验会员期内禁止。"""
from jituan.services.member_recharge import dashou_in_active_trial_period
if dashou_in_active_trial_period(yonghuid):
return False, TRIAL_COMMISSION_WITHDRAW_MSG
return True, ''
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, {}
trial_ok, trial_msg = check_dashou_trial_blocks_commission_withdraw(yonghuid)
if not trial_ok:
return False, trial_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
trial_ok, trial_msg = check_dashou_trial_blocks_commission_withdraw(yonghuid)
if not trial_ok:
return False, trial_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