商城、外卖、上门家政的订单先支付再履约,退款留下可追溯的流水。对账时不能只看订单上的一个「已支付」。用户每一次发起支付都会记支付流水,每一次向渠道退钱都会记退款流水,订单退款单则记店铺同意、拒绝和退货这些业务步骤。
支付流水记一次申请
POST trade/pay 在扣款前就生成新的商户单号。注释写明:未支付的订单理论上可以复用单号,但参数一旦变化,复用是无效的,所以每次申请都新开一号,把提交参数和渠道返回记下来。流水来源是充值、单店订单或合并订单。状态是待支付、成功、关闭。
单店订单要同时满足:订单存在且属于该用户、收款商户一致、仍待付款、金额一致。合并支付中的某一单被单独支付时,合并单改为关闭,流水落在这一张订单上。充值流水的收款商户固定为 0,渠道不能是余额。余额支付会先看用户余额够不够,不够则整次失败,不写一条成功流水。
并发靠锁。键里有用户编号和这次参数的摘要,持有 30 秒。第二下相同请求直接提示正在处理,避免两次余额扣减或两次微信下单。
// app/api/service/trade/TradePayService.php
$lockKey = sprintf(LockKeyManager::LOCK_API_TRADE_PAY_KEY, '_' . $this->user_id . '_' . md5(serialize($data)));
$lockValue = KvManager::lock()->acquire($lockKey, 30);
if (!$lockValue) {
throw new SystemException('支付正在处理中,请稍后再试');
}
$out_trade_no = generateOutTradeNo($data['pay_channel'], $data['user_id']);
支付流水状态只有三档:
// app/common/enum/trade/TradePayEnum.php
const PAY_STATUS_PENDING = 0; // 待支付
const PAY_STATUS_SUCCESS = 1; // 成功
const PAY_STATUS_CLOSED = 2; // 关闭
回调地址带商户、渠道、场景和类型,不要求用户 token。用户自己查结果用 pay/status。平台后台查的是支付流水列表和支付配置;配置决定这个商户在某个场景下启用了哪些渠道。店铺后台看的是某一张订单的支付记录,用来和已经发货、配送或上门的订单对照。
支付成功之后的积分、成长值、商户通知和小票,由监听器处理,不写进支付流水的状态机。通知模板用订单号和支付金额。小票失败不应把已经成功的支付流水改回待支付。
退款流水只表示渠道结果
订单退款单的状态更长。用户申请是 10,店铺同意 20,拒绝 30。退货还有用户已退货 40、店铺已收货 50。处理中 60,成功 70,失败 80,关闭 90,取消 100。店铺主动退是 0。
这些数字描述的是人和货。渠道退款流水只有待退款、成功、失败。
// app/common/enum/trade/TradeRefundEnum.php
const REFUND_STATUS_PENDING = 0; // 待退款
const REFUND_STATUS_SUCCESS = 1; // 退款成功
const REFUND_STATUS_FAILED = 2; // 退款失败
退款方式决定流水怎么产生:原路退回调用当时的微信或支付宝,退回余额改用户余额,人工处理记在退款单上。线下确认收款的订单,退款按余额退。
店铺点重试,是在业务状态已经是处理中或失败、渠道那次没退成的时候再试一次,例如店铺待结算余额不足、原路退回失败。平台也能对退款单发起重试,并查看退款日志。同意、拒绝、确认收到退货、代客创建退款在店铺路由上。用户端是申请、修改、取消、登记退货和查看。
退款成功的监听只在退货退款时恢复库存、减少销量。整单退货时订单商品编号为 0,按整单处理;指定了订单商品则只动那一行。仅退款和系统退款不走这段库存。
// app/listener/order/OrderRefundSuccessListener.php
if ($refund_info['refund_type'] == TblOrderRefundEnum::TYPE_REFUND_GOODS) {
switch ($refund_info['order_goods_id']) {
case 0:
// 0 全部退货
$condition[] = ['order_id', '=', $refund_info['order_id']];
break;
default:
$condition[] = ['id', '=', $refund_info['order_goods_id']];
break;
}
}
仅退款不走这段库存。
和订单金额的关系
店铺可以在订单上改价,改的是待付款或业务允许改价时的应付金额。用户再发起 trade/pay 时,申请金额必须等于当时的应付金额,否则支付直接失败,不会用旧流水的金额去付新价格。退款金额记在退款单上,不能用支付流水的成功金额代替退款流水。一单可以有多次支付申请(多次未支付的单号),成功的应是其中一条;退款可以重试,成功的退款流水应和退款单的成功状态对在一起。
商城、外卖、上门家政共用这套流水。付完之后是发货、配送还是上门,由订单履约处理。钱的申请、回调、同意和重试都留在流水和退款单上。