Files
along_django/yonghu/tixian_shenhe_services.py

1493 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_apply_b.py
"""
自动提现审核流程业务服务层(仅服务于 zddksh 申请接口 与 tixiansq 收款接口)
流程:
① zddksh 提交审核:资格校验 → 扣款(shenqing_jine) → 写审核表+提现记录表(状态=1)
(不在申请阶段校验/占用每日限额)
② 后台审核通过:状态变为 6待收款—— 由原有后台接口处理,此处不改动
③ tixiansq 收款:资格校验 → 只读预估 → 事务内原子预占限额 → 新建打款记录 → 调微信
微信明确失败/取消则回滚预占;待查单期间保留预占防并发超额
④ 打款成功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 dingdan.models import Dingdan, DingdanShangjia, Fadan, Lilubiao
from peizhi.models import Szjilu, TixianQuotaDefault
from utils.fadan_utils import check_fadan_qiangdan_eligible
from .models import (
TixianAutoRecord,
TixianShenheJilu,
Tixianjilu,
UserDashou,
UserGuanshi,
UserMain,
UserShenheguan,
UserShangjia,
UserZuzhang,
)
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',
}
# 进行中的审核状态(仅用于统计/展示,不再阻止用户并行提交多笔申请)
PENDING_AUDIT_STATUSES = [1, 4, 6]
# 微信商家转账单笔到账上限(元):实际到账 shijidaozhang 须严格小于 200
WECHAT_TRANSFER_MAX = decimal.Decimal('200.00')
# 仍持有申请扣款、可驳回退款的审核状态
REFUNDABLE_AUDIT_STATUSES = [1, 4, 6]
DAKUAN_MODE_MANUAL = 1
DAKUAN_MODE_AUTO = 2
# 微信转账单状态(查单接口返回 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'
# 查单失败且本地 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.objects.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.objects.filter(shenhe_danhao=danhao).exists():
return danhao
raise RuntimeError('生成审核单号失败,请重试')
def get_tixian_feilv(leixing):
rate_key = LEIXING_RATE_KEY.get(leixing)
if not rate_key:
raise ValueError('无效的提现类型')
rate_obj = Lilubiao.objects.filter(fadanpingtai=rate_key).first()
if not rate_obj:
raise ValueError(f'提现费率未配置(类型{leixing}')
return decimal.Decimal(str(rate_obj.lilu))
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 shijidaozhang_exceeds_wechat_limit(shijidaozhang):
"""实际到账 >= 200 元时微信官方不予转账"""
try:
amt = decimal.Decimal(str(shijidaozhang)).quantize(decimal.Decimal('0.01'))
except (ValueError, TypeError, decimal.InvalidOperation):
return True
return amt >= WECHAT_TRANSFER_MAX
def wechat_limit_apply_message(shijidaozhang):
amt = decimal.Decimal(str(shijidaozhang)).quantize(decimal.Decimal('0.01'))
return (
f'扣除手续费后实际到账金额为{amt}已达到或超过微信官方单笔限额200元无法提交提现申请。'
f'请减小申请金额确保到账金额小于200元后再试。'
)
def wechat_limit_collect_message(shijidaozhang):
amt = decimal.Decimal(str(shijidaozhang)).quantize(decimal.Decimal('0.01'))
return (
f'该笔提现实际到账金额为{amt}已达到或超过微信官方单笔限额200元无法收款。'
f'系统已驳回并将申请金额退回您对应身份账户余额请重新发起到账金额小于200元的提现。'
)
def assert_shijidaozhang_below_wechat_limit(shijidaozhang):
if shijidaozhang_exceeds_wechat_limit(shijidaozhang):
raise ValueError(wechat_limit_apply_message(shijidaozhang))
def check_dashou_has_valid_huiyuan(yonghuid):
"""
打手至少持有一个未过期会员Huiyuangoumai + jiance_shifou_daoqi 同步状态)
返回 (True, '') 或 (False, 原因)
"""
from shangpin.models import Huiyuangoumai
records = Huiyuangoumai.objects.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.yonghuid
if leixing != 6:
fadan_ok, fadan_msg = check_fadan_qiangdan_eligible(yonghuid, 2)
if not fadan_ok:
return False, fadan_msg, {}
if leixing == 1:
try:
dashou = UserDashou.objects.get(user=user_main)
except UserDashou.DoesNotExist:
return False, '您不是打手身份,无法提现佣金', {}
if dashou.zhanghaozhuangtai != 1:
return False, '打手账号已被封禁,无法提现', {}
if dashou.zhuangtai != 1:
return False, '有订单进行中,请完成后提现', {}
if dashou.yue < jine:
return False, f'余额不足,当前余额: {dashou.yue}', {}
return True, '', {'nicheng': dashou.nicheng or ''}
if leixing == 2:
try:
guanshi = UserGuanshi.objects.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.objects.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.objects.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.objects.get(user=user_main)
except UserDashou.DoesNotExist:
return False, '您不是打手身份,无法提取押金', {}
if dashou.zhanghaozhuangtai != 1:
return False, '打手账号已禁用,无法提取押金', {}
if dashou.zhuangtai != 1:
return False, '您有订单进行中,请完成后提取押金', {}
if dashou.yajin < jine:
return False, f'押金余额不足,当前押金: {dashou.yajin}', {}
unfinished = Dingdan.objects.filter(
jiedan_dashou_id=yonghuid,
).exclude(zhuangtai__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]).exists()
if unfinished:
return False, '您还有未完成或未退款的订单,请先处理完毕再提取押金', {}
self_fadan = Fadan.objects.filter(beichufa_id=yonghuid).exclude(zhuangtai__in=[2, 4]).exists()
if self_fadan:
return False, '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理', {}
return True, '', {'nicheng': dashou.nicheng or ''}
if leixing == 6:
try:
shangjia = UserShangjia.objects.get(user=user_main)
except UserShangjia.DoesNotExist:
return False, '您不是商家身份,无法提现商家余额', {}
if shangjia.zhuangtai != 1:
return False, '商家账号已被禁用,无法提现', {}
if shangjia.yue < jine:
return False, f'商家余额不足,当前余额: {shangjia.yue}', {}
return True, '', {'nicheng': shangjia.nicheng or ''}
return False, '无效的提现类型', {}
def validate_collect_eligibility(user_main, leixing):
"""
收款前二次校验(申请时已扣款,不再校余额/金额)。
打手佣金/押金:罚单、封禁、接单中、积分、会员等;
其他身份:账号状态、罚单/订单等(不含金额)。
"""
yonghuid = user_main.yonghuid
if leixing != 6:
fadan_ok, fadan_msg = check_fadan_qiangdan_eligible(yonghuid, 2)
if not fadan_ok:
return False, fadan_msg
if leixing == 1:
try:
dashou = UserDashou.objects.get(user=user_main)
except UserDashou.DoesNotExist:
return False, '您不是打手身份,无法收款'
if dashou.zhanghaozhuangtai != 1:
return False, '打手账号已被封禁,无法收款'
if dashou.zhuangtai != 1:
return False, '您有订单进行中,请完成后再收款'
return True, ''
if leixing == 5:
try:
dashou = UserDashou.objects.get(user=user_main)
except UserDashou.DoesNotExist:
return False, '您不是打手身份,无法收款'
if dashou.zhanghaozhuangtai != 1:
return False, '打手账号已禁用,无法收款'
if dashou.zhuangtai != 1:
return False, '您有订单进行中,请完成后再收款'
unfinished = Dingdan.objects.filter(
jiedan_dashou_id=yonghuid,
).exclude(zhuangtai__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]).exists()
if unfinished:
return False, '您还有未完成或未退款的订单,无法收款'
self_fadan = Fadan.objects.filter(beichufa_id=yonghuid).exclude(zhuangtai__in=[2, 4]).exists()
if self_fadan:
return False, '您有未处理完毕的自身罚单,无法收款'
return True, ''
if leixing == 2:
try:
guanshi = UserGuanshi.objects.get(user=user_main)
except UserGuanshi.DoesNotExist:
return False, '您不是管事身份,无法收款'
if guanshi.zhuangtai != 1:
return False, '管事账号已被封禁,无法收款'
return True, ''
if leixing == 3:
try:
zuzhang = UserZuzhang.objects.get(user=user_main)
except UserZuzhang.DoesNotExist:
return False, '您不是组长身份,无法收款'
if zuzhang.zhuangtai != 1:
return False, '组长账号已被封禁,无法收款'
return True, ''
if leixing == 4:
try:
kaoheguan = UserShenheguan.objects.get(user=user_main)
except UserShenheguan.DoesNotExist:
return False, '您不是考核官身份,无法收款'
if kaoheguan.zhuangtai != 1:
return False, '考核官账号已被封禁,无法收款'
return True, ''
if leixing == 6:
try:
shangjia = UserShangjia.objects.get(user=user_main)
except UserShangjia.DoesNotExist:
return False, '您不是商家身份,无法收款'
if shangjia.zhuangtai != 1:
return False, '商家账号已被禁用,无法收款'
return True, ''
return False, '无效的提现类型'
def _get_personal_daily_quota(leixing, profile):
"""打手/管事/组长个人每日限额:额外开启优先,否则默认配置"""
if leixing == 1:
if profile.kaioi_ewai_xiane and profile.ewai_xiane > 0:
return profile.ewai_xiane
return TixianQuotaDefault.objects.get(leixing=1).default_quota
if leixing == 2:
if profile.kaioi_ewai_xiane and profile.ewai_xiane > 0:
return profile.ewai_xiane
return TixianQuotaDefault.objects.get(leixing=2).default_quota
if leixing == 3:
try:
default_quota = TixianQuotaDefault.objects.get(leixing=3).default_quota
except TixianQuotaDefault.DoesNotExist:
default_quota = decimal.Decimal('0.00')
if profile.kaioi_ewai_tixian and profile.ewai_tixian_xiane > 0:
return profile.ewai_tixian_xiane
return default_quota
raise ValueError('无效的个人限额身份')
def _get_active_auto_record(shenhe_danhao):
if not shenhe_danhao:
return None
return (
TixianAutoRecord.objects.filter(
shenhe_danhao=shenhe_danhao,
zhuangtai__in=[0, 1],
)
.order_by('-create_time')
.first()
)
def _get_platform_daily_total(leixing, today=None):
"""平台今日已提现总额:只读 houtai.TixianRiTongji到账金额口径"""
from houtai.models import TixianRiTongji
if today is None:
today = date.today()
try:
return TixianRiTongji.objects.get(riqi=today, leixing=leixing).total_amount
except TixianRiTongji.DoesNotExist:
return decimal.Decimal('0.00')
def _calc_platform_projected(leixing, shijidaozhang, shenhe_danhao=''):
"""
平台今日预占后总额(收款前只读预估)。
本审核单若已有进行中打款记录,其金额尚未写入统计表,但不应重复加计。
返回 (daily_used, projected_total, additional_amount)
"""
daily_used = _get_platform_daily_total(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(leixing):
"""
平台每日总限额peizhi.TixianQuotaDefault
提现身份1打手→配置42管事→53组长→64考核官→75押金→86商家→9
"""
quota_leixing = leixing + 3
try:
return TixianQuotaDefault.objects.get(leixing=quota_leixing).default_quota
except TixianQuotaDefault.DoesNotExist:
logger.warning(
'平台每日总限额未配置 leixing=%s(读取quota_leixing=%s)按0处理禁止新增收款',
leixing, quota_leixing,
)
return decimal.Decimal('0.00')
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.objects.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.objects.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.objects.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(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(leixing, shijidaozhang, shenhe_danhao=''):
"""平台每日总限额只读校验:今日已用 + 本笔到账 > 总限额 则拒绝"""
platform_limit = _get_platform_daily_limit(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(
leixing, shijidaozhang, shenhe_danhao,
)
if projected > platform_limit:
logger.warning(
'平台总限额拦截 leixing=%s shenhe_danhao=%s daily_used=%s additional=%s '
'projected=%s limit=%s',
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
return _check_platform_quota_readonly(leixing, shijidaozhang, shenhe_danhao)
def reserve_collect_quota_limits(user_main, leixing, jine, shijidaozhang, shenhe_danhao=''):
"""
收款前原子预占每日限额(必须在 transaction.atomic 内调用)。
锁定 TixianRiTongji + 用户行,校验通过后立即写入统计,防止高并发超额。
本审核单已有进行中/成功打款记录则视为已预占,不重复加计。
返回 (True, '', '') 或 (False, msg, 'personal'|'platform')
"""
from houtai.models import TixianRiTongji
today = date.today()
shijidaozhang = shijidaozhang or decimal.Decimal('0.00')
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(leixing, personal_profile)
if personal_today + jine > personal_quota:
return (
False,
f'今日提现已达个人限额,今日已提{personal_today}元,本次申请{jine}元,限额{personal_quota}',
'personal',
)
platform_stat, _ = TixianRiTongji.objects.select_for_update().get_or_create(
riqi=today,
leixing=leixing,
defaults={'total_amount': decimal.Decimal('0.00')},
)
platform_limit = _get_platform_daily_limit(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.yonghuid, leixing, shenhe_danhao, jine, shijidaozhang, platform_stat.total_amount,
)
return True, '', ''
def release_collect_quota_reservation(user_main, leixing, jine, shijidaozhang):
"""微信打款明确失败/用户取消时回滚收款预占的限额"""
from houtai.models import TixianRiTongji
today = date.today()
shijidaozhang = shijidaozhang or decimal.Decimal('0.00')
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 = TixianRiTongji.objects.select_for_update().get(
riqi=today, leixing=leixing,
)
stat.total_amount = max(
stat.total_amount - shijidaozhang,
decimal.Decimal('0.00'),
)
stat.save(update_fields=['total_amount'])
except TixianRiTongji.DoesNotExist:
pass
except Exception as e:
logger.error(
'回滚收款限额预占失败 yonghuid=%s leixing=%s err=%s',
user_main.yonghuid, leixing, e, exc_info=True,
)
return
logger.info(
'收款限额预占已回滚 yonghuid=%s leixing=%s jine=%s shijidaozhang=%s',
user_main.yonghuid, 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 = UserMain.objects.get(yonghuid=auto_record.yonghuid)
except UserMain.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
from django.db.models import Sum
today = date.today()
shijidaozhang = auto_record.shijidaozhang or decimal.Decimal('0.00')
stat_total = _get_platform_daily_total(auto_record.leixing, today)
other_sum = TixianAutoRecord.objects.filter(
leixing=auto_record.leixing,
zhuangtai__in=[0, 1],
create_time__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.TixianRiTongji.total_amount += 实际到账
- 用户扩展表个人今日已提 += 申请金额(打手/管事/组长)
须在 transaction.atomic 内、且打款记录 zhuangtai 由 0→1 时调用一次。
"""
if not auto_record.shenhe_danhao:
return
from houtai.models import TixianRiTongji
today = date.today()
leixing = auto_record.leixing
jine = auto_record.jine
shijidaozhang = auto_record.shijidaozhang or decimal.Decimal('0.00')
try:
user_main = UserMain.objects.get(yonghuid=auto_record.yonghuid)
except UserMain.DoesNotExist:
logger.error('打款成功累加限额失败:用户不存在 %s', auto_record.yonghuid)
return
platform_stat, _ = TixianRiTongji.objects.select_for_update().get_or_create(
riqi=today,
leixing=leixing,
defaults={'total_amount': decimal.Decimal('0.00')},
)
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 reject_audit_and_refund(user_main, audit, reason):
"""
驳回审核单并按 leixing 将 shenqing_jine 退回对应身份余额(须在业务层调用)。
返回 (True, 用户提示文案) 或 (False, 说明)
"""
if audit is None:
return False, '审核记录不存在'
reason = (reason or '提现失败').strip()
audit_id = audit.id if hasattr(audit, 'id') else audit
with transaction.atomic():
audit = TixianShenheJilu.objects.select_for_update().get(pk=audit_id)
if audit.yonghuid != user_main.yonghuid:
return False, '审核单不属于当前用户'
if audit.zhuangtai == 2:
return False, '该提现已完成,请勿重复操作'
if audit.zhuangtai in (3, 5):
return False, audit.bhliyou or audit.fail_reason or reason
if audit.zhuangtai not in REFUNDABLE_AUDIT_STATUSES:
return False, f'当前状态不可驳回退款(状态{audit.zhuangtai}'
pending_records = list(
TixianAutoRecord.objects.select_for_update().filter(
shenhe_danhao=audit.shenhe_danhao,
zhuangtai=0,
)
)
for rec in pending_records:
rec.zhuangtai = 2
rec.fail_reason = reason[:500]
rec.save(update_fields=['zhuangtai', 'fail_reason', 'update_time'])
release_collect_quota_for_record(rec)
sync_audit_and_jilu_status(
audit, 3,
bhliyou=reason,
fail_reason=reason,
)
refund_balance(user_main, audit.leixing, audit.shenqing_jine)
logger.info(
'提现驳回退款 shenhe_danhao=%s yonghuid=%s leixing=%s refund=%s reason=%s',
audit.shenhe_danhao, audit.yonghuid, audit.leixing, audit.shenqing_jine, reason,
)
return True, reason
def get_tixianjilu_refund_amount(tixian):
"""手动提现驳回应退还的申请扣款额(非到账额)。"""
if getattr(tixian, 'shenqing_jine', None) not in (None, ''):
return decimal.Decimal(str(tixian.shenqing_jine))
fee = getattr(tixian, 'shouxufei', None) or decimal.Decimal('0')
base = decimal.Decimal(str(tixian.jine or 0))
if fee:
return base + decimal.Decimal(str(fee))
return base
def refund_manual_tixianjilu(user_main, tixian):
"""手动提现记录驳回退款(须在 transaction.atomic 内)。"""
amount = get_tixianjilu_refund_amount(tixian)
refund_balance(user_main, tixian.leixing, amount)
"""审核驳回时退还申请扣款额 shenqing_jine须在 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.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.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.objects.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.objects.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.objects.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.objects.get(user=user_main)
data['new_kaoheguan_yue'] = str(kg.yue.quantize(decimal.Decimal('0.01')))
except UserShenheguan.DoesNotExist:
pass
return data
def has_pending_audit(yonghuid):
return TixianShenheJilu.objects.filter(
yonghuid=yonghuid,
zhuangtai__in=PENDING_AUDIT_STATUSES,
).exists()
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, 'update_time': timezone.now()}
if 'bhliyou' in extra_fields:
update_fields['bhliyou'] = extra_fields['bhliyou']
Tixianjilu.objects.filter(id=audit.tixianjilu_id).update(**update_fields)
def query_wechat_transfer_bill(out_bill_no):
"""
向微信查转账单状态
GET /v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no}
返回 dict网络/系统异常返回 None微信无此单返回 {'_not_found': True}
"""
from utils.wechat_v3 import build_authorization
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, '')
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 dingdan.utils import update_daily_payout
update_daily_payout(auto_record.shijidaozhang)
try:
szjilu = Szjilu.objects.select_for_update().get(id=1)
szjilu.zongsy -= auto_record.shijidaozhang
szjilu.zongzc += auto_record.shijidaozhang
szjilu.jrzc += auto_record.shijidaozhang
szjilu.save()
except Szjilu.DoesNotExist:
Szjilu.objects.create(
id=1,
zongsy=-auto_record.shijidaozhang,
zongzc=auto_record.shijidaozhang,
jrzc=auto_record.shijidaozhang,
)
def _wx_transfer_state(wx_data):
"""兼容查单 state 与回调 transfer_status 字段"""
if not wx_data or wx_data.get('_not_found'):
return ''
return (wx_data.get('state') or wx_data.get('transfer_status') or '').upper()
def _wx_state_is_success(wx_data):
return _wx_transfer_state(wx_data) in WX_STATE_SUCCESS
def _sync_audit_success_for_auto_record(ar):
"""打款已成功时,强制同步审核表+提现记录表为 2=提现成功"""
if not ar.shenhe_danhao:
return False
try:
audit = TixianShenheJilu.objects.select_for_update().get(shenhe_danhao=ar.shenhe_danhao)
except TixianShenheJilu.DoesNotExist:
logger.warning('打款成功但审核记录不存在: %s', ar.shenhe_danhao)
return False
if audit.zhuangtai != 2:
sync_audit_and_jilu_status(audit, 2)
logger.info(
'已补齐提现成功状态 shenhe_danhao=%s audit_id=%s jilu_id=%s',
ar.shenhe_danhao, audit.id, audit.tixianjilu_id,
)
return True
return False
def _apply_transfer_success_side_effects(ar):
"""记账/限额等非关键副作用(独立事务),失败不回滚主状态"""
try:
with transaction.atomic():
if ar.shenhe_danhao and not _quota_already_reserved_for_record(ar):
apply_transfer_success_quota(ar)
_apply_platform_accounting(ar)
except Exception as e:
logger.error('打款成功副作用失败 bill=%s err=%s', ar.tixian_id, e, exc_info=True)
def ensure_shenhe_collect_completed(shenhe_danhao):
"""
根据本地打款记录或微信查单,强制补齐「提现成功」状态。
用于:首次回调/确认漏写、用户再次点收款时对账修复。
返回 (True, msg) 或 (False, msg)
"""
if not shenhe_danhao:
return False, '缺少审核单号'
records = list(
TixianAutoRecord.objects.filter(shenhe_danhao=shenhe_danhao).order_by('-create_time')
)
if not records:
return False, '未找到打款记录'
for rec in records:
if rec.zhuangtai == 1:
with transaction.atomic():
ar = TixianAutoRecord.objects.select_for_update().get(pk=rec.pk)
_sync_audit_success_for_auto_record(ar)
return True, '提现已成功到账'
wx_data = query_wechat_transfer_bill(rec.tixian_id)
if wx_data is None:
continue
if _wx_state_is_success(wx_data):
mark_transfer_success(
rec,
wechat_transfer_no=wx_data.get('transfer_bill_no') or wx_data.get('transfer_no'),
)
return True, '提现已成功到账'
return False, '该提现已完成,请勿重复操作'
def mark_transfer_success(auto_record, *, wechat_transfer_no=None, with_accounting=True):
"""
打款成功落库tixianqr / callback / 查单对账 共用)
状态同步与记账分离:记账失败不影响状态落库;打款已成功时必定补齐审核单状态。
返回 True=本次新标记打款成功False=打款记录早已是成功态(仍可能补齐了审核单)
"""
newly_marked = False
ar = None
with transaction.atomic():
ar = TixianAutoRecord.objects.select_for_update().get(pk=auto_record.pk)
if ar.zhuangtai == 0:
ar.zhuangtai = 1
update_fields = ['zhuangtai', 'update_time']
if wechat_transfer_no:
ar.wechat_transfer_no = wechat_transfer_no
update_fields.append('wechat_transfer_no')
ar.save(update_fields=update_fields)
newly_marked = True
elif ar.zhuangtai == 1:
if wechat_transfer_no and ar.wechat_transfer_no != wechat_transfer_no:
ar.wechat_transfer_no = wechat_transfer_no
ar.save(update_fields=['wechat_transfer_no', 'update_time'])
elif ar.zhuangtai == 2 and wechat_transfer_no:
# 本地误标失败但微信已成功:恢复为成功
ar.zhuangtai = 1
ar.wechat_transfer_no = wechat_transfer_no
ar.fail_reason = None
ar.save(update_fields=['zhuangtai', 'wechat_transfer_no', 'fail_reason', 'update_time'])
newly_marked = True
_sync_audit_success_for_auto_record(ar)
if with_accounting and newly_marked and ar is not None:
_apply_transfer_success_side_effects(ar)
return newly_marked
def _sync_record_from_wx_query(auto_record, wx_data):
"""根据微信查单结果修正本地打款记录,返回 (action, payload)"""
if wx_data.get('_not_found'):
age = timezone.now() - auto_record.create_time
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', 'update_time'])
release_collect_quota_for_record(auto_record)
return RECONCILE_ALLOW_NEW, {}
state = _wx_transfer_state(wx_data)
if state in WX_STATE_SUCCESS:
mark_transfer_success(
auto_record,
wechat_transfer_no=wx_data.get('transfer_bill_no') or wx_data.get('transfer_no'),
)
return RECONCILE_COMPLETED, {'msg': '提现已成功到账', 'synced': True}
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', 'update_time'])
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:
if auto_record.zhuangtai != 1:
auto_record.zhuangtai = 2
auto_record.fail_reason = (
wx_data.get('fail_reason') or wx_data.get('close_reason') or state
)[:500]
auto_record.save(update_fields=['zhuangtai', 'fail_reason', 'update_time'])
release_collect_quota_for_record(auto_record)
return RECONCILE_ALLOW_NEW, {}
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.objects.filter(shenhe_danhao=shenhe_danhao, zhuangtai=0)
.order_by('-create_time')
.first()
)
if pending:
return pending
return (
TixianAutoRecord.objects.filter(shenhe_danhao=shenhe_danhao)
.order_by('-create_time')
.first()
)
def reconcile_shenhe_wechat_bills(shenhe_danhao):
"""
P0 防双笔:同一 shenhe_danhao 下所有打款记录逐条问微信
任一 SUCCESS / 进行中 → 禁止新建;全部终态失败才 ALLOW_NEW
返回 (action, payload)
"""
records = list(
TixianAutoRecord.objects.filter(shenhe_danhao=shenhe_danhao).order_by('-create_time')
)
if any(r.zhuangtai == 1 for r in records):
ok, msg = ensure_shenhe_collect_completed(shenhe_danhao)
return RECONCILE_COMPLETED, {'msg': msg, 'synced': ok}
if not records:
return RECONCILE_ALLOW_NEW, {}
pending_hit = None
for rec in records:
if rec.zhuangtai == 1:
ok, msg = ensure_shenhe_collect_completed(shenhe_danhao)
return RECONCILE_COMPLETED, {'msg': msg, 'synced': ok}
wx_data = query_wechat_transfer_bill(rec.tixian_id)
if wx_data is None:
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_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.objects.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.objects.get(
id=int(shenhe_jilu_id),
shenhe_danhao=shenhe_danhao,
yonghuid=yonghuid,
)
else:
audit = TixianShenheJilu.objects.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.objects.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.objects.get(tixian_id=tixian_id)
except TixianAutoRecord.DoesNotExist:
return RECONCILE_PENDING, '收款处理中,请稍后再试'
wx_data = query_wechat_transfer_bill(tixian_id)
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.yonghuid
ok, msg, extra = validate_withdraw_eligibility(user_main, leixing, jine)
if not ok:
raise ValueError(msg)
feilv = get_tixian_feilv(leixing)
shouxufei, shijidaozhang = calc_fee_amounts(jine, feilv)
if shijidaozhang <= 0:
raise ValueError('提现金额过低,扣除手续费后无实际到账金额,请增加申请金额')
assert_shijidaozhang_below_wechat_limit(shijidaozhang)
shenhe_danhao = generate_shenhe_danhao()
nicheng = extra.get('nicheng', '')
with transaction.atomic():
nicheng = deduct_balance(user_main, leixing, jine) or nicheng
audit_kwargs = {
'shenhe_danhao': shenhe_danhao,
'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.objects.create(**audit_kwargs)
tixian_record = Tixianjilu.objects.create(
yonghuid=yonghuid,
avatar=user_main.avatar or '',
phone=user_main.phone or '',
nicheng=nicheng,
leixing=leixing,
zhifu=user_main.zhifu or '',
skzhanghao=user_main.skzhanghao 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