代码 同一个 out_trade_no 第二次下单:支付宝四个错误码怎么分诊、怎么收尾

2026-11-02 09:01:14

同一个 out_trade_no 第二次下单:支付宝四个错误码怎么分诊、怎么收尾

适用边界先讲清楚:下面全部基于支付宝国内版 openapi.alipay.com 的单笔支付与收银台场景(alipay.trade.page.pay / alipay.trade.wap.pay / alipay.trade.app.pay / alipay.trade.pay),不涉及间连模式和国际版。文中错误码、trade_status 语义取自开放平台文档与技术支持 FAQ,部分调用细节未在真实商户号上逐条复现,接入前建议用沙箱或小额真实交易验证一遍。

现象:同一笔单号第二次下单,报错取决于支付宝侧当前状态

用户支付未完成就返回、前端重复点击、后端重试时没换 out_trade_no,第二次调同一个下单接口会拿到不同的错误码——不是接口不稳定,而是这笔 out_trade_no 在支付宝侧当前的 trade_status 不一样。所以排障的第一步不是看错误码文本,而是先查这笔单在支付宝侧处于哪一态。

按 trade_status 分诊

统一收单交易查询 alipay.trade.query 返回四个业务状态:

trade_status含义
WAIT_BUYER_PAY交易创建,等待买家付款
TRADE_CLOSED未付款交易超时关闭,或支付完成后全额退款
TRADE_SUCCESS交易支付成功
TRADE_FINISHED交易结束,不可退款

异步通知默认只开 TRADE_SUCCESS(tradeStatus.TRADE_SUCCESS 默认 1),TRADE_CLOSED / TRADE_FINISHED / WAIT_BUYER_PAY 默认关闭(0)。只靠通知判断状态会漏。

同一个 out_trade_no 复用时的行为:

  • WAIT_BUYER_PAY:订单已创建未完成,商户用相同订单号且其他业务参数不变,重新请求可以正常唤起支付页面;但必须是之前创建该订单的用户账户才能付款,不支持更换付款账户。参数改了就报 ACQ.CONTEXT_INCONSISTENT / ALIN42282「商家订单参数异常,请尝试返回后重新付款或联系商家确认」。
  • TRADE_SUCCESS / TRADE_FINISHED:报 ALIN42279「订单已付款,请勿重复提交」,或 ACQ.TRADE_HAS_SUCCESS「交易已被支付」。
  • TRADE_CLOSED:报 ALIN42280「抱歉,订单已关闭,你可以重新购买并付款」,或 ACQ.TRADE_HAS_CLOSE「交易已经关闭」。

alipay.trade.pay 的同步业务错误码原文:

  • ACQ.TRADE_HAS_SUCCESS 400 交易已被支付:确认该订单号是否重复支付,如果是新订单,请更换商户订单号后重新提交支付。
  • ACQ.TRADE_HAS_CLOSE 400 交易已经关闭:商户订单号对应的交易已经关闭(超时未支付或者已全额退款),请更换商户订单号后重新发起支付请求。
  • ACQ.CONTEXT_INCONSISTENT 400 订单信息不一致:商户订单号已经创建交易成功,且本次请求的交易关键信息(如订单金额、订单标题等)与已存交易的信息不一致;如原有交易未支付成功,请更换订单号重新发起支付请求。
  • ACQ.TRADE_BUYER_NOT_MATCH 400 交易买家不匹配:确认该订单号是否重复支付,如果是新订单,请更换商户订单号后重新提交支付。
  • ACQ.TRADE_NOT_EXIST(查询接口):查询的交易不存在,检查传入的交易号是否正确。
  • ACQ.INVALID_PARAMETER 参数无效。
  • ACQ.SYSTEM_ERROR 400 系统异常:该笔交易可能成功也可能失败,请调用查询订单 API 查询当前订单状态,并根据订单状态决定下一步操作;多次重试仍报此错误联系支付宝客服。

对应 FAQ:ACQ.TRADE_HAS_SUCCESS 说明、订单已付款,请勿重复提交(ALIN42279)。

关键一点:TRADE_CLOSED 既可能是「从来没付过、超时关掉」,也可能是「付过、后来全额退款」。同一个状态两种来源,本地不能只看支付宝状态就决定该不该退款。

正确收尾:查询优先,撤销和退款不要混用

没拿到交易结果时不要直接要求用户再付一次——可能是用户已付款成功、只是商户侧网络问题没拿到结果。确认已支付就先退款,再让用户重付。

异常处理口径(蚂蚁开放平台接口调用规范,接口调用规范/单边账):

  • 支付接口 alipay.trade.pay 异常:立即调查询;查询返回 ACQ.TRADE_NOT_EXIST 时用相同参数重调支付;网络超时或未知异常继续查询一分钟,仍超时/未知则记录异常交易走人工,不能推断为付款失败。
  • 查询 alipay.trade.query、撤销 alipay.trade.cancel 异常:立即重试一分钟,仍失败记录异常交易走人工。
  • 预下单 alipay.trade.precreate 异常:换新的 out_trade_no 重新预下单。
  • 退款 alipay.trade.refund 异常:相同参数重试一分钟,仍失败记录走人工,不能推断退款成功或失败。
  • 唤起收银台的交易(返回状态 10003)建议轮询:总时长 30 秒,平均间隔 3 秒。让用户再次支付前必须先查询确认当前订单状态。

撤销只处理未支付订单,超过 24 小时无法撤销;退款走 alipay.trade.refund。两者混用会让订单状态和业务状态对不上,监控上也看不出异常。

out_trade_no 怎么生成

String(64),商家自定义,仅支持字母、数字、下划线,且必须保证商户端不重复。新建订单都要换号,重试也走跳号处理,不要图省事复用上一笔的单号。参考如何设置 out_trade_no。

另外单品总金额要与订单金额一致,避免实收与应收不匹配。

超时撤销

未支付订单及时调 alipay.trade.cancel,超过 24 小时就撤不掉了。撤销的目的不是清账,而是把「用户不会再付」的单收口掉,避免后面同一个单号被再次提交时落进 WAIT_BUYER_PAY 分支唤起一个早已过期的收银台。

退款:out_request_no 必须换

同一笔交易多次部分退款,每次都要更换退款请求号 out_request_no,否则请求号冲突导致部分退款失败。把它当成幂等键用:同一个 out_request_no 重试是安全的,想再退一次就必须换号。

SYSTEM_ERROR 与 isp.unknow-error

接口返回 code=20000 且 sub_code=isp.unknow-error 或 ACQ.SYSTEM_ERROR 时,业务处理结果未知——注意是未知,不是失败。这两种情况都只能落到「查询 → 再决策」这条路径上,不要在 catch 分支里直接把订单置为失败或让用户重付。

代码收口

Go 版本,三个动作:查询优先、条件更新、幂等键。支付宝调用封装成接口,具体 SDK 调用按各自接入方式实现。

type TradeStatus string

const (
	WaitBuyerPay  TradeStatus = "WAIT_BUYER_PAY"
	TradeClosed   TradeStatus = "TRADE_CLOSED"
	TradeSuccess  TradeStatus = "TRADE_SUCCESS"
	TradeFinished TradeStatus = "TRADE_FINISHED"
)

// QueryResult 对应 alipay.trade.query 的业务响应参数
type QueryResult struct {
	Found       bool        // false 即 ACQ.TRADE_NOT_EXIST
	TradeStatus TradeStatus
	TotalAmount string // 元,字符串,别用浮点
}

var ErrUnknown = errors.New("trade result unknown") // 超时 / ACQ.SYSTEM_ERROR / isp.unknow-error

func (s *Service) Pay(ctx context.Context, order Order, req PayRequest) (Decision, error) {
	// 幂等:request_id 唯一索引 + 状态条件更新,抢不到说明已处理过,直接回放结果
	acquired, err := s.repo.TryLockAttempt(ctx, order.ID, req.RequestID)
	if err != nil {
		return Decision{}, err
	}
	if !acquired {
		return s.repo.LoadDecision(ctx, req.RequestID)
	}

	// 查询优先,异常按规范重试一分钟
	q, err := s.alipay.QueryWithRetry(ctx, order.OutTradeNo, time.Minute)
	if err != nil {
		return Decision{Action: ActionManual, Reason: "query unknown"}, nil // 记异常交易走人工
	}
	if !q.Found {
		// ACQ.TRADE_NOT_EXIST:支付宝侧没这笔,按原单号创建
		return s.createAndPay(ctx, order, req)
	}

	switch q.TradeStatus {
	case WaitBuyerPay:
		// 复用原单号的前提:金额、标题等关键参数与已存交易完全一致,且付款账户不变
		if !sameBizParams(order, req) {
			return Decision{Action: ActionNewOutTradeNo, Reason: "ACQ.CONTEXT_INCONSISTENT"}, nil
		}
		return Decision{Action: ActionReopenCashier}, nil

	case TradeSuccess, TradeFinished:
		// ALIN42279 / ACQ.TRADE_HAS_SUCCESS:本地补记成功,不再重发支付
		return Decision{Action: ActionMarkPaid}, nil

	case TradeClosed:
		// ALIN42280 / ACQ.TRADE_HAS_CLOSE:换号重建;是否要先退款看本地是否有成功支付记录
		if s.repo.HasPaidRecord(ctx, order.ID) {
			return Decision{Action: ActionRefundFirst, Reason: "closed after refund"}, nil
		}
		return Decision{Action: ActionNewOutTradeNo}, nil
	}
	return Decision{Action: ActionManual, Reason: "unexpected trade_status"}, nil
}

条件更新那一步(示例 SQL,靠影响行数判断是否抢到):

UPDATE orders
   SET status = 'paying', last_request_id = ?, updated_at = NOW()
 WHERE id = ?
   AND status IN ('created', 'wait_pay')

影响行数为 0 就是并发重复提交,回放上一次结果即可,不要第二次调用支付接口。

推荐文章

程序员茄子在线接单