1499 lines
58 KiB
Python
1499 lines
58 KiB
Python
# 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
|
||
|
||
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}', {}
|
||
unfinished_orders = DingdanShangjia.objects.filter(
|
||
shangjia_id=yonghuid,
|
||
).exclude(dingdan__zhuangtai__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]).exists()
|
||
if unfinished_orders:
|
||
return False, '您还有未完成或未退款的订单,请先处理完毕再提现余额', {}
|
||
self_fadan = Fadan.objects.filter(beichufa_id=yonghuid).exclude(zhuangtai__in=[2, 4]).exists()
|
||
if self_fadan:
|
||
return False, '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理', {}
|
||
applied_fadan = Fadan.objects.filter(shenqing_chufa=yonghuid).exclude(zhuangtai__in=[2, 4]).exists()
|
||
if applied_fadan:
|
||
return False, '您发起的对其他用户的处罚尚未处理完毕,请等待完结', {}
|
||
return True, '', {'nicheng': shangjia.nicheng or ''}
|
||
|
||
return False, '无效的提现类型', {}
|
||
|
||
|
||
def validate_collect_eligibility(user_main, leixing):
|
||
"""
|
||
收款前二次校验(申请时已扣款,不再校余额/金额)。
|
||
打手佣金/押金:罚单、封禁、接单中、积分、会员等;
|
||
其他身份:账号状态、罚单/订单等(不含金额)。
|
||
"""
|
||
yonghuid = user_main.yonghuid
|
||
|
||
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, '商家账号已被禁用,无法收款'
|
||
unfinished_orders = DingdanShangjia.objects.filter(
|
||
shangjia_id=yonghuid,
|
||
).exclude(dingdan__zhuangtai__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]).exists()
|
||
if unfinished_orders:
|
||
return False, '您还有未完成或未退款的订单,无法收款'
|
||
self_fadan = Fadan.objects.filter(beichufa_id=yonghuid).exclude(zhuangtai__in=[2, 4]).exists()
|
||
if self_fadan:
|
||
return False, '您有未处理完毕的自身罚单,无法收款'
|
||
applied_fadan = Fadan.objects.filter(shenqing_chufa=yonghuid).exclude(zhuangtai__in=[2, 4]).exists()
|
||
if applied_fadan:
|
||
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打手→配置4,2管事→5,3组长→6,4考核官→7,5押金→8,6商家→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 refund_balance(user_main, leixing, jine):
|
||
"""审核驳回时退还申请扣款额 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.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.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
|