yeg
2026-09-17 e3f412c5474311a471345ea508db4875c9ec648e
talent-card-server/src/main/java/com/talentcard/server/business/portal/PendingOrderPortalService.java
@@ -50,6 +50,10 @@
    * 到期时间不等于没收到钱。这些订单交给对账任务先向通道问清(已收款就落成已支付,通道说失败就
    * 把流水置为已失败),下一轮清理自然会收掉它们。到期就自动取消会把已经收下的钱变成一笔没有
    * 订单、也无法退款的悬账。
    *
    * <p>{@code CANCEL_UNCONFIRMED} 与 {@code CLOSING} 一并排除:它们是「关不掉的预支付单」,
    * 通道既没说关掉也没说钱收下了,所以在通道侧仍然可能被支付。自动取消这样一笔订单,等于把已经
    * 收下的钱变成没有出口的悬账。它们由关单补偿给出终止条件(重试上限后落成已取消并保留盯防)。
    */
   @Transactional
   public int expireDueOrders(Long talentId) {
@@ -60,7 +64,8 @@
           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);
@@ -73,7 +78,8 @@
      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, "支付已发起,不能再修改优惠券");
@@ -233,13 +239,17 @@
      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);
@@ -270,6 +280,15 @@
      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("""
@@ -279,7 +298,7 @@
           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);
   }