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')
# 主动查单确认支付状态