peixue-dev/Archive/peidu-temp-files/docs/🔥🔥🔥支付失败-事务回滚问题修复-2026-01-23.md

9.3 KiB
Raw Blame History

🔥🔥🔥 支付失败 - 事务回滚问题修复

问题时间: 2026-01-23
问题描述: 支付页面显示"支付成功",但后端返回 400 错误:"Transaction rolled back because it has been marked as rollback-only"


🔍 问题分析

现象

  1. 前端支付页面显示"支付成功"
  2. 控制台显示后端返回 400 错误
  3. 错误信息:Transaction rolled back because it has been marked as rollback-only

根本原因

问题出在 UserPackage 实体类的时间字段配置

@TableField(value = "create_time", fill = FieldFill.INSERT)
private LocalDateTime createTime;

@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;

冲突点

  1. @TableField 注解指定了自动填充(fill = FieldFill.INSERT
  2. PackageServiceImpl.purchasePackage() 方法中手动设置了这些字段
  3. 如果没有配置 MetaObjectHandler,自动填充会失败
  4. 导致事务回滚

调用链

前端支付 
  → OrderController.payOrder()
    → OrderServiceImpl.payOrder()
      → PackageServiceImpl.purchasePackage()  ← 这里出错
        → userPackageMapper.insert(userPackage)  ← 事务回滚

🔧 解决方案

方案1: 移除自动填充配置(推荐)

优点: 简单直接,不需要额外配置
缺点: 需要手动设置时间字段

修改 UserPackage.java

@Data
@TableName("user_package")
public class UserPackage {
    @TableId(type = IdType.AUTO)
    private Long id;
    
    private Long tenantId;
    private Long userId;
    private Long packageId;
    private String packageName;
    private String packageType;
    private BigDecimal totalHours;
    private BigDecimal usedHours;
    private BigDecimal remainingHours;
    private Integer totalCount;
    private Integer usedCount;
    private Integer remainingCount;
    private BigDecimal price;
    private Integer status;
    private LocalDate startDate;
    private LocalDate expireDate;
    
    // 🔥 移除 fill 属性,只保留字段映射
    @TableField(value = "create_time")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    
    @TableField(value = "update_time")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
}

方案2: 配置 MetaObjectHandler完整方案

优点: 统一管理时间字段,其他实体也能受益
缺点: 需要额外配置类

创建 MyMetaObjectHandler.java

package com.peidu.config;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("开始插入填充...");
        
        // 自动填充 createTime
        this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
        
        // 自动填充 updateTime
        this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("开始更新填充...");
        
        // 自动填充 updateTime
        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }
}

修改 PackageServiceImpl.java

@Transactional
public UserPackage purchasePackage(Long userId, Long packageId) {
    Package pkg = packageMapper.selectById(packageId);
    if (pkg == null) {
        throw new RuntimeException("套餐不存在");
    }
    
    if (pkg.getStatus() != 1) {
        throw new RuntimeException("套餐已下架");
    }
    
    // 创建用户套餐
    UserPackage userPackage = new UserPackage();
    userPackage.setUserId(userId);
    userPackage.setPackageId(packageId);
    userPackage.setPackageName(pkg.getPackageName());
    userPackage.setPackageType(pkg.getPackageType());
    userPackage.setTotalHours(pkg.getTotalHours());
    userPackage.setUsedHours(BigDecimal.ZERO);
    userPackage.setRemainingHours(pkg.getTotalHours());
    userPackage.setTotalCount(pkg.getServiceCount());
    userPackage.setUsedCount(0);
    userPackage.setRemainingCount(pkg.getServiceCount());
    userPackage.setPrice(pkg.getPrice());
    userPackage.setStatus(1);
    userPackage.setStartDate(LocalDate.now());
    userPackage.setExpireDate(LocalDate.now().plusDays(pkg.getValidDays()));
    
    // 🔥 移除手动设置时间字段,让 MetaObjectHandler 自动填充
    // userPackage.setCreateTime(LocalDateTime.now());
    // userPackage.setUpdateTime(LocalDateTime.now());
    
    userPackageMapper.insert(userPackage);
    
    return userPackage;
}

🚀 推荐实施步骤

步骤1: 使用方案1快速修复

  1. 修改 UserPackage.java,移除 fill 属性
  2. 重新编译后端
  3. 测试支付功能

步骤2: 修复前端显示逻辑

问题: 前端在后端返回 400 错误时仍然显示"支付成功"

修改 peidu/uniapp/src/pages/payment/index.vue:

// 处理支付
async handlePay() {
  if (this.paying) return
  
  // ... 验证逻辑 ...

  this.paying = true
  uni.showLoading({ title: '支付中...' })

  try {
    console.log('=== 开始支付 ===')
    console.log('订单ID', this.orderId)
    console.log('支付方式:', this.selectedMethod)
    console.log('支付金额:', this.finalAmount)

    // 构建支付参数
    const paymentData = {
      orderId: this.orderId,
      paymentMethod: this.selectedMethod,
      amount: this.finalAmount
    }

    // 添加优惠券
    if (this.selectedCoupon) {
      paymentData.couponId = this.selectedCoupon.id
    }

    // 添加次卡
    if (this.selectedMethod === 'timecard' && this.selectedTimecard) {
      paymentData.timecardId = this.selectedTimecard.id
    }

    // 添加套餐
    if (this.selectedMethod === 'package' && this.selectedPackage) {
      paymentData.packageId = this.selectedPackage.id
    }

    console.log('支付参数:', paymentData)

    // 调用支付接口
    const response = await orderApi.payOrder(paymentData)

    console.log('支付响应:', response)

    // 🔥 修复:检查响应状态
    if (response && response.code === 200) {
      // 支付成功
      this.handlePaymentSuccess()
    } else {
      // 🔥 修复:支付失败,抛出错误
      throw new Error(response.message || '支付失败')
    }

  } catch (error) {
    console.error('支付失败:', error)
    uni.hideLoading()
    this.paying = false
    
    // 🔥 修复:显示详细错误信息
    uni.showModal({
      title: '支付失败',
      content: error.message || '支付过程中出现错误,请重试',
      showCancel: false
    })
  }
}

关键修改:

  1. 检查 response.code === 200 才认为支付成功
  2. 如果 code !== 200,抛出错误
  3. catch 块中显示错误信息

步骤3: 移除重复的 handlePay 方法

问题: payment/index.vue 中有两个 handlePay() 方法定义

修改: 删除第一个 handlePay() 方法(第 456-502 行),保留第二个完整的方法


🧪 测试步骤

1. 测试支付成功场景

1. 创建订单
2. 进入支付页面
3. 选择支付方式(微信/钱包/次卡/套餐)
4. 点击"立即支付"
5. 预期:显示"支付成功",跳转到"待服务"订单列表

2. 测试支付失败场景

1. 创建订单
2. 进入支付页面
3. 选择钱包支付(余额不足)
4. 点击"立即支付"
5. 预期:显示"钱包余额不足"错误提示

3. 测试事务回滚修复

1. 创建订单
2. 进入支付页面
3. 选择任意支付方式
4. 点击"立即支付"
5. 预期:
   - 不再出现 "Transaction rolled back" 错误
   - 支付成功或失败都有明确提示
   - 订单状态正确更新

📝 修改文件清单

后端修改

  1. peidu/backend/src/main/java/com/peidu/entity/UserPackage.java - 移除 fill 属性
  2. (可选)peidu/backend/src/main/java/com/peidu/config/MyMetaObjectHandler.java - 创建自动填充处理器

前端修改

  1. peidu/uniapp/src/pages/payment/index.vue - 修复支付响应处理逻辑
  2. peidu/uniapp/src/pages/payment/index.vue - 删除重复的 handlePay() 方法

⚠️ 注意事项

  1. 编译顺序: 先修改后端,重新编译,再修改前端
  2. 测试覆盖: 测试所有支付方式(微信、钱包、次卡、套餐)
  3. 数据库检查: 确认 user_package 表的 create_timeupdate_time 字段存在
  4. 日志监控: 观察后端日志,确认没有事务回滚错误

🎯 预期效果

修复前

  • 支付页面显示"支付成功"
  • 后端返回 400 错误
  • 事务回滚,数据未保存
  • 用户体验混乱

修复后

  • 支付成功时显示"支付成功"
  • 支付失败时显示具体错误信息
  • 事务正常提交,数据正确保存
  • 用户体验清晰明确

🚀 立即执行

下一步: 按照步骤1修复 UserPackage.java,重新编译后端

预计时间: 10分钟

风险: 低 - 只修改字段配置,不影响业务逻辑