diff --git a/jituan/services/alipay_pay.py b/jituan/services/alipay_pay.py index a4a6965..e1d06cf 100644 --- a/jituan/services/alipay_pay.py +++ b/jituan/services/alipay_pay.py @@ -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): diff --git a/jituan/services/czjilu_fulfill.py b/jituan/services/czjilu_fulfill.py index 3e7f7f5..955c66f 100644 --- a/jituan/services/czjilu_fulfill.py +++ b/jituan/services/czjilu_fulfill.py @@ -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) diff --git a/jituan/services/member_recharge.py b/jituan/services/member_recharge.py index 3a93e9b..9253433 100644 --- a/jituan/services/member_recharge.py +++ b/jituan/services/member_recharge.py @@ -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') # 主动查单确认支付状态 diff --git a/orders/views/payment.py b/orders/views/payment.py index c11a81a..945852c 100644 --- a/orders/views/payment.py +++ b/orders/views/payment.py @@ -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: diff --git a/products/czjilu_types.py b/products/czjilu_types.py index 1fcfb11..74800c3 100644 --- a/products/czjilu_types.py +++ b/products/czjilu_types.py @@ -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) diff --git a/products/urls.py b/products/urls.py index 654f192..835e4b5 100644 --- a/products/urls.py +++ b/products/urls.py @@ -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='商家支付成功'), # 商家支付成功轮询 diff --git a/products/views/__init__.py b/products/views/__init__.py index 13c5edb..6f7f297 100644 --- a/products/views/__init__.py +++ b/products/views/__init__.py @@ -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', diff --git a/products/views/alipay_czjilu_notify.py b/products/views/alipay_czjilu_notify.py index 84f855e..b44d898 100644 --- a/products/views/alipay_czjilu_notify.py +++ b/products/views/alipay_czjilu_notify.py @@ -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) diff --git a/products/views/chongzhi_payment.py b/products/views/chongzhi_payment.py index 78e2aef..2f318b6 100644 --- a/products/views/chongzhi_payment.py +++ b/products/views/chongzhi_payment.py @@ -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) diff --git a/products/views/huiyuan_payment.py b/products/views/huiyuan_payment.py index 32484c6..2c8f542 100644 --- a/products/views/huiyuan_payment.py +++ b/products/views/huiyuan_payment.py @@ -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, diff --git a/products/views/jifen_payment.py b/products/views/jifen_payment.py index c09048d..04516cd 100644 --- a/products/views/jifen_payment.py +++ b/products/views/jifen_payment.py @@ -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) diff --git a/products/views/yajin_payment.py b/products/views/yajin_payment.py index e20e2d5..5361d1d 100644 --- a/products/views/yajin_payment.py +++ b/products/views/yajin_payment.py @@ -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) diff --git a/users/views/pay.py b/users/views/pay.py index b87fd9d..36bccf5 100644 --- a/users/views/pay.py +++ b/users/views/pay.py @@ -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': '订单不存在'})