fix: 加固支付宝充值回调幂等履约,避免付成功不到账与误删单

禁止 Czjilu 回调地址回落到订单 notify;回调失败回 fail 重试;取消支付改关单不删;客服补单走同一履约防多加。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-20 22:59:59 +08:00
parent 48d2e13037
commit 1ab59efddb
13 changed files with 283 additions and 106 deletions

View File

@@ -33,22 +33,43 @@ def get_alipay_config(club_id=None):
else:
cfg = getattr(settings, 'ALIPAY_PROD', {})
# 注意NOTIFY_URL_CZJILU 不得静默回落到订单 NOTIFY_URL否则充值回调会打进订单回调被吞掉
czjilu_notify = (cfg.get('NOTIFY_URL_CZJILU') or '').strip()
order_notify = (cfg.get('NOTIFY_URL') or '').strip()
return {
'APP_ID': cfg.get('APP_ID', ''),
'PID': cfg.get('PID', ''),
'APP_PRIVATE_KEY': cfg.get('APP_PRIVATE_KEY', ''),
'ALIPAY_PUBLIC_KEY': cfg.get('ALIPAY_PUBLIC_KEY', ''),
'GATEWAY': cfg.get('GATEWAY', 'https://openapi.alipay.com/gateway.do'),
'NOTIFY_URL': cfg.get('NOTIFY_URL', ''),
'NOTIFY_URL_CZJILU': cfg.get('NOTIFY_URL_CZJILU', '') or cfg.get('NOTIFY_URL', ''),
'NOTIFY_URL': order_notify,
'NOTIFY_URL_CZJILU': czjilu_notify,
'RETURN_URL': cfg.get('RETURN_URL', ''),
'sandbox': sandbox,
}
def get_czjilu_notify_url(club_id=None):
"""获取 Czjilu 类充值(押金/积分/会员/商家/考核/罚款)的支付宝异步回调地址。"""
return get_alipay_config(club_id)['NOTIFY_URL_CZJILU']
"""获取 Czjilu 类充值的支付宝异步回调地址;未单独配置则抛错,禁止回落到订单回调"""
cfg = get_alipay_config(club_id)
notify_url = (cfg.get('NOTIFY_URL_CZJILU') or '').strip()
if not notify_url:
raise Exception(
'支付宝配置缺少 NOTIFY_URL_CZJILU须指向 /shangpin/alipay-czjilu-notify/'
'禁止使用订单回调地址,否则充值到账会失败'
)
order_notify = (cfg.get('NOTIFY_URL') or '').strip()
if order_notify and notify_url.rstrip('/') == order_notify.rstrip('/'):
raise Exception(
'NOTIFY_URL_CZJILU 不能与订单 NOTIFY_URL 相同,'
'请配置为 .../shangpin/alipay-czjilu-notify/'
)
if 'alipay-czjilu-notify' not in notify_url:
logger.warning(
'NOTIFY_URL_CZJILU=%s 未包含 alipay-czjilu-notify请确认是否配错',
notify_url,
)
return notify_url
def generate_alipay_czjilu_pay_params(dingdanid, jine, subject, club_id=None, expire_minutes=30):

View File

@@ -11,6 +11,7 @@ from jituan.constants import CLUB_ID_DEFAULT
from jituan.services.club_penalty import resolve_gsfenhong_club_id
from orders.models import CommissionRate
from products.models import Czjilu, Gsfenhong
from products.czjilu_types import is_czjilu_fulfillable_status
from users.models import UserDashou, UserGuanshi
logger = logging.getLogger(__name__)
@@ -28,13 +29,13 @@ class CzjiluFulfillError(Exception):
@transaction.atomic
def fulfill_yajin_recharge(dingdan_id, paid_amount_yuan=None):
"""押金充值幂等履约:仅 zhuangtai==9 时入账"""
"""押金充值幂等履约:仅待履约状态(9/用户关单2)时入账;已付(3)直接跳过防多加"""
order = Czjilu.query.filter(dingdan_id=dingdan_id).select_for_update().first()
if not order:
raise CzjiluFulfillError('订单不存在', 'not_found')
if order.leixing != 2:
raise CzjiluFulfillError('订单类型不是押金', 'type_error')
if order.zhuangtai != 9:
if not is_czjilu_fulfillable_status(order.zhuangtai):
try:
from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
repair_wechat_income_if_missing(
@@ -118,13 +119,13 @@ def _apply_yajin_fenhong(order, dashou, amount):
@transaction.atomic
def fulfill_jifen_recharge(dingdan_id, paid_amount_yuan=None):
"""积分补充幂等履约:仅 zhuangtai==9 时入账。"""
"""积分补充幂等履约:仅待履约状态(9/2)时入账。"""
order = Czjilu.query.filter(dingdan_id=dingdan_id).select_for_update().first()
if not order:
raise CzjiluFulfillError('订单不存在', 'not_found')
if order.leixing != 3:
raise CzjiluFulfillError('订单类型不是积分', 'type_error')
if order.zhuangtai != 9:
if not is_czjilu_fulfillable_status(order.zhuangtai):
try:
from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
repair_wechat_income_if_missing(
@@ -179,7 +180,7 @@ def fulfill_fakuan(dingdan_id, paid_amount_yuan=None):
raise CzjiluFulfillError('订单不存在', 'not_found')
if order.leixing != 6:
raise CzjiluFulfillError('订单类型不是罚款', 'type_error')
if order.zhuangtai != 9:
if not is_czjilu_fulfillable_status(order.zhuangtai):
try:
from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
repair_wechat_income_if_missing(
@@ -260,7 +261,7 @@ def fulfill_fakuan(dingdan_id, paid_amount_yuan=None):
@transaction.atomic
def fulfill_kaohe(dingdan_id, paid_amount_yuan=None):
"""考核支付幂等履约:仅 zhuangtai==9 时入账。"""
"""考核支付幂等履约:仅待履约状态(9/2)时入账。"""
from rank.models import KaohePayTemp
from rank.utils import create_shenhe_jilu_from_temp
from users.business_models import User
@@ -270,7 +271,7 @@ def fulfill_kaohe(dingdan_id, paid_amount_yuan=None):
raise CzjiluFulfillError('订单不存在', 'not_found')
if order.leixing != 5:
raise CzjiluFulfillError('订单类型不是考核', 'type_error')
if order.zhuangtai != 9:
if not is_czjilu_fulfillable_status(order.zhuangtai):
try:
from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
repair_wechat_income_if_missing(
@@ -323,7 +324,7 @@ def fulfill_kaohe(dingdan_id, paid_amount_yuan=None):
@transaction.atomic
def fulfill_shangjia(dingdan_id, paid_amount_yuan=None):
"""商家充值幂等履约:仅 zhuangtai==9 时入账"""
"""商家充值幂等履约:仅待履约状态(9/2)时入账;已付(3)跳过防多加"""
from users.models import UserShangjia
order = Czjilu.query.filter(dingdan_id=dingdan_id).select_for_update().first()
@@ -331,7 +332,7 @@ def fulfill_shangjia(dingdan_id, paid_amount_yuan=None):
raise CzjiluFulfillError('订单不存在', 'not_found')
if order.leixing != 4:
raise CzjiluFulfillError('订单类型不是商家充值', 'type_error')
if order.zhuangtai != 9:
if not is_czjilu_fulfillable_status(order.zhuangtai):
try:
from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
repair_wechat_income_if_missing(
@@ -394,7 +395,7 @@ def confirm_czjilu_paid(dingdan_id, yonghuid, expected_leixing):
logger.error('充值单补记收支失败 dingdan=%s: %s', dingdan_id, exc, exc_info=True)
return {'already_done': True, 'zhuangtai': 3}
if order.zhuangtai != 9:
if not is_czjilu_fulfillable_status(order.zhuangtai):
raise CzjiluFulfillError(f'订单状态异常: {order.zhuangtai}', 'status_error')
# 主动查单确认支付状态
@@ -421,7 +422,7 @@ def confirm_czjilu_paid(dingdan_id, yonghuid, expected_leixing):
else:
raise CzjiluFulfillError('微信侧尚未支付成功,请稍后再试', 'not_paid')
# 查单确认支付成功,执行履约
# 查单确认支付成功,执行履约fulfill_* 内行锁+仅 9/2→3防多加
if expected_leixing == 1:
from jituan.services.member_recharge import fulfill_member_recharge
return fulfill_member_recharge(dingdan_id, paid_amount_yuan=paid, source='confirm')
@@ -436,3 +437,12 @@ def confirm_czjilu_paid(dingdan_id, yonghuid, expected_leixing):
if expected_leixing == 6:
return fulfill_fakuan(dingdan_id, paid_amount_yuan=paid)
raise CzjiluFulfillError('不支持的充值类型', 'type_error')
def repair_czjilu_payment(dingdan_id):
"""客服补单:官方查单确认已付后,走同一套幂等履约(绝不多加)。"""
try:
order = Czjilu.query.get(dingdan_id=dingdan_id)
except Czjilu.DoesNotExist:
raise CzjiluFulfillError('订单不存在', 'not_found')
return confirm_czjilu_paid(dingdan_id, order.yonghuid, expected_leixing=order.leixing)

View File

@@ -48,6 +48,9 @@ MEMBER_PAID_STATUS = 3
MEMBER_PENDING_STATUS = 9
MEMBER_FAILED_STATUS = 8
MEMBER_CANCELLED_STATUS = 2
# 与 czjilu_types.CZJILU_FULFILLABLE_STATUSES 一致:仅这些状态允许入账
MEMBER_FULFILLABLE_STATUSES = (MEMBER_PENDING_STATUS, MEMBER_CANCELLED_STATUS)
MEMBER_CANCELLED_STATUS = 2
ENTITLEMENT_REVOKED_MARKER = '权益已撤销'
MEMBER_JIFEN_CAP = 10
@@ -923,7 +926,7 @@ def fulfill_member_recharge(dingdan_id, paid_amount_yuan=None, source='callback'
if order.leixing != MEMBER_LEIXING:
raise MemberRechargeError('订单类型不是会员充值', 'type_error')
if order.zhuangtai != MEMBER_PENDING_STATUS:
if order.zhuangtai not in MEMBER_FULFILLABLE_STATUSES:
logger.info(
'会员订单已处理,跳过履约 dingdan=%s zhuangtai=%s source=%s',
dingdan_id, order.zhuangtai, source,
@@ -1093,7 +1096,7 @@ def confirm_member_recharge_paid(dingdan_id, yonghuid):
'is_trial': goumai.is_trial,
}
if order.zhuangtai != MEMBER_PENDING_STATUS:
if order.zhuangtai != MEMBER_PENDING_STATUS and order.zhuangtai != MEMBER_CANCELLED_STATUS:
raise MemberRechargeError(f'订单状态异常: {order.zhuangtai}', 'status_error')
# 主动查单确认支付状态

View File

@@ -457,6 +457,45 @@ class AlipayPayNotifyView(WechatPayNotifyView):
# 处理支付结果
if trade_status in ('TRADE_SUCCESS', 'TRADE_FINISHED'):
logger.warning(f"[ALIPAY_NOTIFY_DEBUG] 调用 _handle_success")
# 充值单误打进订单回调:转交 Czjilu 履约,禁止空 success 吞掉通知
from products.czjilu_types import looks_like_czjilu_out_trade_no
from products.models import Czjilu
if looks_like_czjilu_out_trade_no(out_trade_no) or Czjilu.query.filter(dingdan_id=out_trade_no).exists():
logger.error(
'[ALIPAY_NOTIFY_DEBUG] 充值单误入订单回调,改走 Czjilu 履约: %s',
out_trade_no,
)
from products.views.alipay_czjilu_notify import _fulfill_czjilu_by_leixing
cz = Czjilu.query.filter(dingdan_id=out_trade_no).first()
if not cz:
logger.error('[ALIPAY_NOTIFY_DEBUG] Czjilu 不存在仍回 fail: %s', out_trade_no)
return HttpResponse('fail', content_type='text/plain')
if cz.zhuangtai == 3:
return HttpResponse('success', content_type='text/plain')
from products.czjilu_types import is_czjilu_fulfillable_status
if not is_czjilu_fulfillable_status(cz.zhuangtai):
logger.warning(
'[ALIPAY_NOTIFY_DEBUG] Czjilu 终态不再履约 %s zhuangtai=%s',
out_trade_no, cz.zhuangtai,
)
return HttpResponse('success', content_type='text/plain')
try:
paid_amount = params.get('total_amount') or cz.jine
_fulfill_czjilu_by_leixing(out_trade_no, cz.leixing, paid_amount, source='order_notify_redirect')
except Exception as redirect_exc:
logger.error(
'[ALIPAY_NOTIFY_DEBUG] 充值单转履约失败 %s: %s',
out_trade_no, redirect_exc, exc_info=True,
)
return HttpResponse('fail', content_type='text/plain')
return HttpResponse('success', content_type='text/plain')
# 老板订单:不存在则 fail让支付宝重试禁止假 success
from orders.models import Order
if not Order.query.filter(OrderID=out_trade_no).exists():
logger.error(f"[ALIPAY_NOTIFY_DEBUG] 订单不存在,回 fail 以便重试: {out_trade_no}")
return HttpResponse('fail', content_type='text/plain')
self._handle_success(out_trade_no, trade_no, None)
logger.warning(f"[ALIPAY_NOTIFY_DEBUG] _handle_success 完成")
else:

View File

@@ -11,6 +11,11 @@ CZJILU_LEIXING_KAOHE = 5
CZJILU_LEIXING_FAKUAN = 6
CZJILU_PAID_STATUS = 3
# 用户取消/关闭:保留流水,不物理删除;若官方已付款,回调/查单仍可履约入账(防少加)
CZJILU_CANCELLED_STATUS = 2
CZJILU_PENDING_STATUS = 9
# 可被「官方已付款」触发履约的状态(仅这些会入账,已付 3 不会再加 → 防多加)
CZJILU_FULFILLABLE_STATUSES = (CZJILU_PENDING_STATUS, CZJILU_CANCELLED_STATUS)
FAKUAN_ORDER_PREFIX = 'FK'
PENALTY_SHUOMING_MARKER = '罚款'
@@ -79,3 +84,41 @@ def penalty_payment_cz_qs(qs):
| Q(leixing=CZJILU_LEIXING_SHANGJIA, shuoming__icontains=PENALTY_SHUOMING_MARKER)
| Q(leixing=CZJILU_LEIXING_SHANGJIA, shuoming__icontains=PENALTY_FADAN_TAG)
)
def is_czjilu_fulfillable_status(zhuangtai):
"""是否允许在确认官方已付款后执行履约9 待付 / 2 用户取消但仍可能已扣款)。"""
try:
return int(zhuangtai) in CZJILU_FULFILLABLE_STATUSES
except (TypeError, ValueError):
return False
def close_unpaid_czjilu(order, reason='用户取消支付'):
"""
关闭未支付充值单:只改状态为取消(2),绝不 delete。
若用户其实已付款,后续回调/查单仍可通过 is_czjilu_fulfillable_status 履约。
返回 True 表示本次从 9 改为 2。
"""
if int(getattr(order, 'zhuangtai', 0) or 0) != CZJILU_PENDING_STATUS:
return False
note = (getattr(order, 'shuoming', None) or '').strip()
tag = f'|关单:{reason}'
if tag not in note:
merged = f'{note}{tag}' if note else f'关单:{reason}'
order.shuoming = merged[:100]
order.zhuangtai = CZJILU_CANCELLED_STATUS
order.save(update_fields=['zhuangtai', 'shuoming'])
return True
def looks_like_czjilu_out_trade_no(out_trade_no):
"""粗判商户订单号是否为 Czjilu 充值单(避免打进老板订单回调后被 success 吞掉)。"""
s = (out_trade_no or '').strip()
if not s:
return False
prefixes = (
'CzHY', 'CzYJ', 'CzJF', 'CzSJ', 'KH', 'FK',
getattr(settings, 'SHANGJIA_CZ_PREFIX', 'CzSJ') or 'CzSJ',
)
return any(s.startswith(p) for p in prefixes if p)

View File

@@ -7,9 +7,9 @@ from .views import ShangpinHuoquView, ShangpinXiangqingHuoquView, DashouHuiyuanL
ShangjiaChongzhi, ShangjiaHuitiao, ShangjiaShibai, GuanshiFenhongListAPI, AdGetAllDataView, ShangpinLeixingFabuView, \
ShangpinZhuanquFabuView, ShangpinFabuView, GuizeTupianShangchuanView, AdHuiyuanHqView, HuiyuanXiugaiView, \
AdHuiyuanFbView, AdShangpinHuQu, AdLeixingXiuGai, AdLunboXiuGai, AdZhuanquXiuGai, AdGonggaoXiuGai, \
AdShangpinGuizeXiuGai, AdShangpinXiuGai, AdShangpinNeiRongHuoQu,\
AdShangpinGuizeXiuGai, AdShangpinXiuGai, AdShangpinNeiRongHuoQu, \
HuiyuanTypeListView, HuiyuanRecordListView, ZuzhangFenhongView, CzhqdyView, DsqrgmdhView, \
AlipayCzjiluNotifyView
AlipayCzjiluNotifyView, CzjiluRepairView
urlpatterns = [
#path('shangpinhuoqu/', ShangpinHuoquView.as_view(), name='获取商品数据商品类型和专区'),
@@ -37,6 +37,8 @@ urlpatterns = [
path('sjhuitiao', csrf_exempt(ShangjiaHuitiao.as_view()), name='商家支付官方回调'),
# 支付宝 Czjilu 充值统一回调(押金/积分/会员/商家/考核/罚款)
path('alipay-czjilu-notify/', csrf_exempt(AlipayCzjiluNotifyView.as_view()), name='支付宝Czjilu充值统一回调'),
# 客服补单:官方查单确认后幂等履约
path('czjilu-budan', CzjiluRepairView.as_view(), name='充值单客服补单'),
#path('sjhuitiao', ShangjiaHuitiao.as_view(), name='商家支付官方回调'), # 商家支付回调
path('sjshibai', ShangjiaShibai.as_view(), name='商家支付失败'), # 商家支付失败处理
path('sjcg', ShangjiaChenggong.as_view(), name='商家支付成功'), # 商家支付成功轮询

View File

@@ -56,7 +56,7 @@ from .chongzhi_payment import (
MAX_INTEGRAL,
IDENTITY_CONFIG,
)
from .alipay_czjilu_notify import AlipayCzjiluNotifyView
from .alipay_czjilu_notify import AlipayCzjiluNotifyView, CzjiluRepairView
from .shangpin_admin import (
ShangpinLeixingFabuView,
ShangpinZhuanquFabuView,
@@ -98,7 +98,7 @@ __all__ = [
'CzhqdyView', 'DsqrgmdhView',
'INTEGRAL_PRICE', 'INTEGRAL_INCREMENT', 'MAX_INTEGRAL', 'IDENTITY_CONFIG',
# alipay_czjilu_notify
'AlipayCzjiluNotifyView',
'AlipayCzjiluNotifyView', 'CzjiluRepairView',
# shangpin_admin
'ShangpinLeixingFabuView', 'ShangpinZhuanquFabuView', 'ShangpinFabuView',
'GuizeTupianShangchuanView',

View File

@@ -8,16 +8,44 @@ import traceback
from django.http import HttpResponse
from django.views import View
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import status
from products.models import Czjilu
from products.czjilu_types import (
CZJILU_LEIXING_HUIYUAN, CZJILU_LEIXING_YAJIN, CZJILU_LEIXING_JIFEN,
CZJILU_LEIXING_SHANGJIA, CZJILU_LEIXING_KAOHE, CZJILU_LEIXING_FAKUAN,
CZJILU_PAID_STATUS, is_czjilu_fulfillable_status,
)
logger = logging.getLogger(__name__)
def _fulfill_czjilu_by_leixing(out_trade_no, leixing, paid_amount, source='callback'):
"""按 leixing 调幂等履约;入账只在 fulfill_* 内发生一次。"""
if leixing == CZJILU_LEIXING_HUIYUAN:
from jituan.services.member_recharge import fulfill_member_recharge
return fulfill_member_recharge(out_trade_no, paid_amount_yuan=paid_amount, source=source)
if leixing == CZJILU_LEIXING_YAJIN:
from jituan.services.czjilu_fulfill import fulfill_yajin_recharge
return fulfill_yajin_recharge(out_trade_no, paid_amount_yuan=paid_amount)
if leixing == CZJILU_LEIXING_JIFEN:
from jituan.services.czjilu_fulfill import fulfill_jifen_recharge
return fulfill_jifen_recharge(out_trade_no, paid_amount_yuan=paid_amount)
if leixing == CZJILU_LEIXING_SHANGJIA:
from jituan.services.czjilu_fulfill import fulfill_shangjia
return fulfill_shangjia(out_trade_no, paid_amount_yuan=paid_amount)
if leixing == CZJILU_LEIXING_KAOHE:
from jituan.services.czjilu_fulfill import fulfill_kaohe
return fulfill_kaohe(out_trade_no, paid_amount_yuan=paid_amount)
if leixing == CZJILU_LEIXING_FAKUAN:
from jituan.services.czjilu_fulfill import fulfill_fakuan
return fulfill_fakuan(out_trade_no, paid_amount_yuan=paid_amount)
raise ValueError(f'未知 leixing={leixing}')
class AlipayCzjiluNotifyView(View):
"""支付宝 Czjilu 充值统一回调:验签 + 按 leixing 路由履约。
@@ -60,58 +88,81 @@ class AlipayCzjiluNotifyView(View):
# 查询充值单
order = Czjilu.query.filter(dingdan_id=out_trade_no).first()
if not order:
# 找不到单回 fail让支付宝重试避免「钱付了永久无履约」
logger.error('[ALIPAY_CZJILU_NOTIFY] 充值单不存在: %s', out_trade_no)
return HttpResponse('fail', content_type='text/plain')
# 已支付:补记收支后直接返回成功(幂等
if order.zhuangtai == 3:
# 已支付:幂等成功(绝不再加余额/会员
if order.zhuangtai == CZJILU_PAID_STATUS:
logger.info('[ALIPAY_CZJILU_NOTIFY] 订单已支付,幂等返回: %s', out_trade_no)
return HttpResponse('success', content_type='text/plain')
if order.zhuangtai != 9:
logger.warning(
'[ALIPAY_CZJILU_NOTIFY] 订单状态异常: %s, zhuangtai=%s',
out_trade_no, order.zhuangtai,
)
# 待付(9) 或用户关单(2):官方已付则履约
if is_czjilu_fulfillable_status(order.zhuangtai):
paid_amount = total_amount or order.jine
leixing = order.leixing
try:
_fulfill_czjilu_by_leixing(out_trade_no, leixing, paid_amount, source='callback')
except Exception as fulfill_exc:
logger.error(
'[ALIPAY_CZJILU_NOTIFY] 履约异常 out_trade_no=%s leixing=%s: %s',
out_trade_no, leixing, fulfill_exc, exc_info=True,
)
# 履约异常返回 fail让支付宝重试
return HttpResponse('fail', content_type='text/plain')
logger.info('[ALIPAY_CZJILU_NOTIFY] 履约完成: %s, leixing=%s', out_trade_no, leixing)
return HttpResponse('success', content_type='text/plain')
# 按 leixing 路由到对应履约函数
paid_amount = total_amount or order.jine
leixing = order.leixing
try:
if leixing == CZJILU_LEIXING_HUIYUAN:
from jituan.services.member_recharge import fulfill_member_recharge
fulfill_member_recharge(out_trade_no, paid_amount_yuan=paid_amount, source='callback')
elif leixing == CZJILU_LEIXING_YAJIN:
from jituan.services.czjilu_fulfill import fulfill_yajin_recharge
fulfill_yajin_recharge(out_trade_no, paid_amount_yuan=paid_amount)
elif leixing == CZJILU_LEIXING_JIFEN:
from jituan.services.czjilu_fulfill import fulfill_jifen_recharge
fulfill_jifen_recharge(out_trade_no, paid_amount_yuan=paid_amount)
elif leixing == CZJILU_LEIXING_SHANGJIA:
from jituan.services.czjilu_fulfill import fulfill_shangjia
fulfill_shangjia(out_trade_no, paid_amount_yuan=paid_amount)
elif leixing == CZJILU_LEIXING_KAOHE:
from jituan.services.czjilu_fulfill import fulfill_kaohe
fulfill_kaohe(out_trade_no, paid_amount_yuan=paid_amount)
elif leixing == CZJILU_LEIXING_FAKUAN:
from jituan.services.czjilu_fulfill import fulfill_fakuan
fulfill_fakuan(out_trade_no, paid_amount_yuan=paid_amount)
else:
logger.error('[ALIPAY_CZJILU_NOTIFY] 未知 leixing=%s: %s', leixing, out_trade_no)
return HttpResponse('fail', content_type='text/plain')
except Exception as fulfill_exc:
logger.error(
'[ALIPAY_CZJILU_NOTIFY] 履约异常 out_trade_no=%s leixing=%s: %s',
out_trade_no, leixing, fulfill_exc, exc_info=True,
)
# 履约异常返回 fail让支付宝重试
return HttpResponse('fail', content_type='text/plain')
logger.info('[ALIPAY_CZJILU_NOTIFY] 履约完成: %s, leixing=%s', out_trade_no, leixing)
# 终态异常(如会员履约失败 8、考核业务失败 10停止重试留人工处理
logger.warning(
'[ALIPAY_CZJILU_NOTIFY] 订单终态不再履约: %s, zhuangtai=%s',
out_trade_no, order.zhuangtai,
)
return HttpResponse('success', content_type='text/plain')
except Exception as e:
logger.error('[ALIPAY_CZJILU_NOTIFY] 回调处理异常: %s', str(e))
logger.error(traceback.format_exc())
return HttpResponse('fail', content_type='text/plain')
class CzjiluRepairView(APIView):
"""客服补单:查官方支付结果后走同一幂等履约。
路径POST /shangpin/czjilu-budan
参数dingdanid
"""
permission_classes = [IsAuthenticated]
def post(self, request):
from backend.utils import verify_kefu_permission
from jituan.services.czjilu_fulfill import repair_czjilu_payment, CzjiluFulfillError
from jituan.services.member_recharge import MemberRechargeError
username = (
request.data.get('username')
or request.data.get('phone')
or getattr(request.user, 'Phone', None)
)
kefu, permissions_or_resp = verify_kefu_permission(request, username)
if not kefu:
return permissions_or_resp
dingdanid = (request.data.get('dingdanid') or request.data.get('dingdan_id') or '').strip()
if not dingdanid:
return Response({'code': 400, 'message': '订单ID不能为空'}, status=status.HTTP_400_BAD_REQUEST)
try:
result = repair_czjilu_payment(dingdanid)
return Response({
'code': 200,
'message': '补单成功' if not result.get('already_done') else '订单已到账(幂等)',
'data': result,
})
except (CzjiluFulfillError, MemberRechargeError) as exc:
logger.warning('[CZJILU_REPAIR] 补单未完成 dingdan=%s: %s', dingdanid, exc)
return Response({'code': 400, 'message': str(exc)}, status=status.HTTP_400_BAD_REQUEST)
except Exception as exc:
logger.error('[CZJILU_REPAIR] 补单异常 dingdan=%s: %s', dingdanid, exc, exc_info=True)
return Response({'code': 500, 'message': '补单失败,请稍后重试'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

View File

@@ -540,20 +540,20 @@ class ShangjiaShibai(APIView):
'message': '订单不存在或不属于当前用户'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 检查订单状态是否为未支付9
# 3. 检查订单状态是否为未支付9:关单不删
if order.zhuangtai == 9:
# 删除订单
order.delete()
logger.info(f"商家{request.user.ShopProfile.nicheng}删除未支付订单: {dingdanid}")
from products.czjilu_types import close_unpaid_czjilu
close_unpaid_czjilu(order, reason='用户取消支付')
logger.info(f"商家{request.user.ShopProfile.nicheng}关闭未支付订单(不删): {dingdanid}")
return Response({
'code': 200,
'message': '订单已删除'
'message': '订单已取消'
}, status=status.HTTP_200_OK)
else:
return Response({
'code': 400,
'message': '订单状态不是未支付,无法删除'
'message': '订单状态不是未支付,无法取消'
}, status=status.HTTP_400_BAD_REQUEST)
except UserShangjia.DoesNotExist:
@@ -606,8 +606,9 @@ class ShangjiaChenggong(APIView):
'message': '订单不存在或不属于当前用户'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 订单未支付时,尝试补偿履约(回调延迟/失败时通过轮询兜底)
if order.zhuangtai == 9:
# 3. 未支付/已关单时,尝试补偿履约(回调延迟/失败时通过轮询兜底)
from products.czjilu_types import is_czjilu_fulfillable_status
if is_czjilu_fulfillable_status(order.zhuangtai):
from jituan.services.czjilu_fulfill import confirm_czjilu_paid, CzjiluFulfillError
try:
confirm_czjilu_paid(dingdanid, request.user.UserUID, expected_leixing=4)

View File

@@ -551,20 +551,20 @@ class HuiyuanShibai(APIView):
'message': '订单类型错误,不是会员订单'
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 检查订单状态是否为未支付9
# 4. 检查订单状态是否为未支付9:关单不删,保留流水便于官方已付后补履约
if order.zhuangtai == 9:
# 删除订单
order.delete()
logger.info(f"用户{request.user.UserUID}删除未支付会员订单: {dingdanid}")
from products.czjilu_types import close_unpaid_czjilu
close_unpaid_czjilu(order, reason='用户取消支付')
logger.info(f"用户{request.user.UserUID}关闭未支付会员订单(不删): {dingdanid}")
return Response({
'code': 200,
'message': '会员订单已删除'
'message': '会员订单已取消'
}, status=status.HTTP_200_OK)
else:
return Response({
'code': 400,
'message': '会员订单状态不是未支付,无法删除'
'message': '会员订单状态不是未支付,无法取消'
}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
@@ -616,8 +616,9 @@ class HuiyuanLunxun(APIView):
'message': '订单类型错误,不是会员订单'
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 未支付查微信并幂等履约与回调互斥zhuangtai!=9 则跳过)
if order.zhuangtai == 9:
# 4. 未支付/已关单:查官方并幂等履约
from products.czjilu_types import is_czjilu_fulfillable_status
if is_czjilu_fulfillable_status(order.zhuangtai):
from jituan.services.member_recharge import (
confirm_member_recharge_paid,
MemberRechargeError,

View File

@@ -511,20 +511,20 @@ class JifenShibai(APIView):
'message': '订单类型错误,不是积分订单'
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 检查订单状态是否为未支付9
# 4. 检查订单状态是否为未支付9:关单不删
if order.zhuangtai == 9:
# 删除订单
order.delete()
logger.info(f"用户{request.user.UserUID}删除未支付积分订单: {dingdanid}")
from products.czjilu_types import close_unpaid_czjilu
close_unpaid_czjilu(order, reason='用户取消支付')
logger.info(f"用户{request.user.UserUID}关闭未支付积分订单(不删): {dingdanid}")
return Response({
'code': 200,
'message': '积分订单已删除'
'message': '积分订单已取消'
}, status=status.HTTP_200_OK)
else:
return Response({
'code': 400,
'message': '积分订单状态不是未支付,无法删除'
'message': '积分订单状态不是未支付,无法取消'
}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
@@ -576,8 +576,9 @@ class JifenLunxun(APIView):
'message': '订单类型错误,不是积分订单'
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 未支付则查微信并幂等履约
if order.zhuangtai == 9:
# 4. 未支付/已关单则查官方并幂等履约
from products.czjilu_types import is_czjilu_fulfillable_status
if is_czjilu_fulfillable_status(order.zhuangtai):
from jituan.services.czjilu_fulfill import confirm_czjilu_paid, CzjiluFulfillError
try:
confirm_czjilu_paid(dingdanid, request.user.UserUID, expected_leixing=3)

View File

@@ -524,20 +524,20 @@ class YajinShibai(APIView):
'message': '订单不存在或不属于当前用户'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 检查订单状态是否为未支付9
# 3. 检查订单状态是否为未支付9:关单不删
if order.zhuangtai == 9:
# 删除订单
order.delete()
logger.info(f"用户{request.user.UserUID}删除未支付订单: {dingdanid}")
from products.czjilu_types import close_unpaid_czjilu
close_unpaid_czjilu(order, reason='用户取消支付')
logger.info(f"用户{request.user.UserUID}关闭未支付押金订单(不删): {dingdanid}")
return Response({
'code': 200,
'message': '订单已删除'
'message': '订单已取消'
}, status=status.HTTP_200_OK)
else:
return Response({
'code': 400,
'message': '订单状态不是未支付,无法删除'
'message': '订单状态不是未支付,无法取消'
}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
@@ -582,8 +582,9 @@ class YajinLunxun(APIView):
'message': '订单不存在或不属于当前用户'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 未支付则查微信并幂等履约
if order.zhuangtai == 9:
# 3. 未支付/已关单则查官方并幂等履约
from products.czjilu_types import is_czjilu_fulfillable_status
if is_czjilu_fulfillable_status(order.zhuangtai):
from jituan.services.czjilu_fulfill import confirm_czjilu_paid, CzjiluFulfillError
try:
confirm_czjilu_paid(dingdanid, request.user.UserUID, expected_leixing=2)

View File

@@ -471,8 +471,9 @@ class FaKuanPayPollView(APIView):
return Response({'code': 400, 'message': '订单ID不能为空'})
order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID)
# 订单未支付时,尝试补偿履约(回调延迟/失败时通过轮询兜底)
if order.zhuangtai == 9:
# 订单未支付/已关单待补履约时,尝试补偿履约(回调延迟/失败时通过轮询兜底)
from products.czjilu_types import is_czjilu_fulfillable_status
if is_czjilu_fulfillable_status(order.zhuangtai):
from jituan.services.czjilu_fulfill import confirm_czjilu_paid, CzjiluFulfillError
try:
confirm_czjilu_paid(dingdanid, request.user.UserUID, expected_leixing=6)
@@ -503,12 +504,13 @@ class FaKuanPayFailView(APIView):
return Response({'code': 400, 'message': '订单ID不能为空'})
order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID)
if order.zhuangtai == 9: # 未支付
order.delete()
logger.info(f"用户{request.user.UserUID}删除未支付订单: {dingdanid}")
return Response({'code': 200, 'message': '订单已删除'})
if order.zhuangtai == 9: # 未支付:关单不删
from products.czjilu_types import close_unpaid_czjilu
close_unpaid_czjilu(order, reason='用户取消支付')
logger.info(f"用户{request.user.UserUID}关闭未支付罚款订单(不删): {dingdanid}")
return Response({'code': 200, 'message': '订单已取消'})
else:
return Response({'code': 400, 'message': '订单状态不是未支付,无法删除'})
return Response({'code': 400, 'message': '订单状态不是未支付,无法取消'})
except Czjilu.DoesNotExist:
return Response({'code': 400, 'message': '订单不存在'})
except Exception as e:
@@ -1004,8 +1006,9 @@ class KaohePayPollView(APIView):
return Response({'code': 400, 'message': '订单ID不能为空'})
try:
order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID, leixing=5)
# 订单未支付时,尝试补偿履约(回调延迟/失败时通过轮询兜底)
if order.zhuangtai == 9:
# 订单未支付/已关单待补履约时,尝试补偿履约
from products.czjilu_types import is_czjilu_fulfillable_status
if is_czjilu_fulfillable_status(order.zhuangtai):
from jituan.services.czjilu_fulfill import confirm_czjilu_paid, CzjiluFulfillError
try:
confirm_czjilu_paid(dingdanid, request.user.UserUID, expected_leixing=5)
@@ -1032,8 +1035,9 @@ class KaohePayFailView(APIView):
try:
order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID, leixing=5)
if order.zhuangtai == 9:
order.delete()
return Response({'code': 200, 'message': '订单已删除'})
return Response({'code': 400, 'message': '订单状态无法删除'})
from products.czjilu_types import close_unpaid_czjilu
close_unpaid_czjilu(order, reason='用户取消支付')
return Response({'code': 200, 'message': '订单已取消'})
return Response({'code': 400, 'message': '订单状态无法取消'})
except Czjilu.DoesNotExist:
return Response({'code': 400, 'message': '订单不存在'})