| | |
| | | * 到期时间不等于没收到钱。这些订单交给对账任务先向通道问清(已收款就落成已支付,通道说失败就 |
| | | * 把流水置为已失败),下一轮清理自然会收掉它们。到期就自动取消会把已经收下的钱变成一笔没有 |
| | | * 订单、也无法退款的悬账。 |
| | | * |
| | | * <p>{@code CANCEL_UNCONFIRMED} 与 {@code CLOSING} 一并排除:它们是「关不掉的预支付单」, |
| | | * 通道既没说关掉也没说钱收下了,所以在通道侧仍然可能被支付。自动取消这样一笔订单,等于把已经 |
| | | * 收下的钱变成没有出口的悬账。它们由关单补偿给出终止条件(重试上限后落成已取消并保留盯防)。 |
| | | */ |
| | | @Transactional |
| | | public int expireDueOrders(Long talentId) { |
| | |
| | | and o.expires_at is not null and o.expires_at <= current_timestamp |
| | | and not exists ( |
| | | select 1 from payment_transaction p |
| | | where p.order_id = o.id and p.status in ('CREATED', 'PREPAY_CREATED', 'CREATE_UNKNOWN') |
| | | where p.order_id = o.id |
| | | and p.status in ('CREATED', 'PREPAY_CREATED', 'CREATE_UNKNOWN', 'CLOSING', 'CANCEL_UNCONFIRMED') |
| | | ) |
| | | """; |
| | | if (talentId == null) return jdbcTemplate.update(sql); |
| | |
| | | ensurePayable(order, request.priceVersion()); |
| | | Integer prepayCount = jdbcTemplate.queryForObject(""" |
| | | select count(*) from payment_transaction |
| | | where order_no = ? and talent_id = ? and status in ('CREATED', 'PREPAY_CREATED', 'SUCCEEDED') |
| | | where order_no = ? and talent_id = ? |
| | | and status in ('CREATED', 'PREPAY_CREATED', 'CREATE_UNKNOWN', 'CLOSING', 'CANCEL_UNCONFIRMED', 'SUCCEEDED') |
| | | """, Integer.class, orderNo, request.talentId()); |
| | | if (prepayCount != null && prepayCount > 0) { |
| | | throw new BusinessException(ErrorCode.CONFLICT, "支付已发起,不能再修改优惠券"); |
| | |
| | | if (longNumber(order.get("orderStatus")) == 2) return view(order); |
| | | if (longNumber(order.get("orderStatus")) != 0) throw new BusinessException(ErrorCode.CONFLICT, "订单当前不可取消"); |
| | | // 回调没到达时「待支付」只说明本地不知道,钱可能已经被通道收下。取消是不可逆的,所以先问通道: |
| | | // 已经收下的钱必须落成已支付(人才端才有退款入口),问不清楚就先不取消;通道说没收款的预支付单 |
| | | // 还要在通道侧关掉,否则用户手上的预支付单仍然能付进来。这一问必须排在本地写状态之前—— |
| | | // 它在自己的事务里,本方法一旦先加锁,它就会等自己的锁。 |
| | | // 已经收下的钱必须落成已支付(人才端才有退款入口);通道说没收款的预支付单还要在通道侧关掉, |
| | | // 否则用户手上的预支付单仍然能付进来。这一问必须排在本地写状态之前——它在自己的事务里, |
| | | // 本方法一旦先加锁,它就会等自己的锁。 |
| | | PendingPaymentConfirmation confirmation = paymentReconciliationService.closePendingPayments(orderNo); |
| | | if (confirmation.collected()) throw new BusinessException(ErrorCode.CONFLICT, "订单已支付,不能取消"); |
| | | if (confirmation.state() == PendingPaymentState.UNCONFIRMED) { |
| | | throw new BusinessException(ErrorCode.CONFLICT, "支付结果确认中,暂时不能取消,请稍后再试"); |
| | | // 关单没有结论(通道答「处理中」或干脆没答上来):这次「支付结果不确定」只属于它所属的 |
| | | // 那一次支付尝试,不该把整笔订单停摆成一个永久报错。取消的意图先被接受并记录,订单本身 |
| | | // 不动——后台每分钟重试关单,有关闭或已收款的结论时再落地(关掉了就取消,钱收下了就 |
| | | // 翻成已支付并告知用户)。页面按「取消处理中」渲染。 |
| | | return cancellationPending(orderNo, talentId, confirmation.paymentNo()); |
| | | } |
| | | // 过了支付时限的订单在这里结束:条件写在 SQL 里,跨实例只有一个赢家。 |
| | | expireOrder(orderNo, talentId); |
| | |
| | | return view(find(orderNo, talentId)); |
| | | } |
| | | |
| | | /** 取消已受理、等通道关单结论:订单仍是待支付,人才端按「取消处理中」渲染。 */ |
| | | private Map<String, Object> cancellationPending(String orderNo, Long talentId, String paymentNo) { |
| | | Map<String, Object> result = view(find(orderNo, talentId)); |
| | | result.put("cancellationPending", true); |
| | | result.put("paymentNo", paymentNo); |
| | | result.put("paymentStatus", "PAYMENT_PROCESSING"); |
| | | return result; |
| | | } |
| | | |
| | | /** 单个订单的到期结束:条件写在 SQL 里,跨实例只有一个赢家。未定局的支付尝试照上面同样的理由排除。 */ |
| | | private void expireOrder(String orderNo, Long talentId) { |
| | | jdbcTemplate.update(""" |
| | |
| | | and o.expires_at is not null and o.expires_at <= current_timestamp |
| | | and not exists ( |
| | | select 1 from payment_transaction p |
| | | where p.order_id = o.id and p.status in ('CREATED', 'PREPAY_CREATED', 'CREATE_UNKNOWN') |
| | | where p.order_id = o.id and p.status in ('CREATED', 'PREPAY_CREATED', 'CREATE_UNKNOWN', 'CLOSING', 'CANCEL_UNCONFIRMED') |
| | | ) |
| | | """, orderNo, talentId); |
| | | } |