【Uni-App+SSM 宠物项目实战】Day16:订单提交

大家好!今天是宠物服务平台实战的第 16 天,我们聚焦订单提交核心功能 ------ 这是连接 "购物车结算" 与 "支付流程" 的关键环节。不同于单纯的单表插入,订单提交需处理多表联动 (订单主表 + 明细表 + 购物车 + 商品库存)、事务一致性 (确保步骤要么全成、要么全败)和唯一订单号生成,同时前端需实现地址动态联动,让用户操作更流畅。

即使是新手,也能通过本文的 "分步代码 + 实战细节" 掌握:MyBatis-Plus 事务管理、Hutool 订单号生成、Uni-App 地址选择器联动,这些都是电商项目的必备技能,学会后可直接复用到其他商城类项目中!

一、前置准备

开始编码前,需确保环境与数据满足以下条件,避免开发中卡壳:

准备项 具体要求 注意事项
数据库表结构 需创建 3 张核心表,字段如下:1. orders(订单主表):id(主键)、order_no(订单号,唯一)、user_id(用户 ID)、address_id(地址 ID)、total_amount(总金额)、order_status(状态:0 - 待付款 / 1 - 待发货等)、create_time(创建时间)2. order_item(订单明细表):id(主键)、order_no(关联订单号)、goods_id(商品 ID)、goods_name(商品名)、goods_price(下单单价)、quantity(数量)3. address(用户地址表):id(主键)、user_id(用户 ID)、receiver(收货人)、phone(手机号)、full_address(详细地址)、is_default(是否默认:0 - 否 / 1 - 是) 若表缺失,执行下方建表 SQL:sql-- 订单主表CREATE TABLE orders (id BIGINT PRIMARY KEY AUTO_INCREMENT,order_no VARCHAR(50) NOT NULL COMMENT '唯一订单号',user_id BIGINT NOT NULL COMMENT '用户ID',address_id BIGINT NOT NULL COMMENT '收货地址ID',total_amount DECIMAL(10,2) NOT NULL COMMENT '总金额',order_status TINYINT DEFAULT 0 COMMENT '0-待付款/1-待发货/2-已完成',create_time DATETIME DEFAULT CURRENT_TIMESTAMP,UNIQUE KEY uk_order_no (order_no));-- 订单明细表CREATE TABLE order_item (id BIGINT PRIMARY KEY AUTO_INCREMENT,order_no VARCHAR(50) NOT NULL COMMENT '关联订单号',goods_id BIGINT NOT NULL COMMENT '商品ID',goods_name VARCHAR(100) NOT NULL COMMENT '商品名称',goods_price DECIMAL(10,2) NOT NULL COMMENT '下单单价',quantity INT NOT NULL COMMENT '购买数量',FOREIGN KEY (order_no) REFERENCES orders(order_no));-- 用户地址表CREATE TABLE address (id BIGINT PRIMARY KEY AUTO_INCREMENT,user_id BIGINT NOT NULL COMMENT '用户ID',receiver VARCHAR(20) NOT NULL COMMENT '收货人',phone VARCHAR(20) NOT NULL COMMENT '手机号',full_address VARCHAR(255) NOT NULL COMMENT '详细地址',is_default TINYINT DEFAULT 0 COMMENT '0-非默认/1-默认');
后端依赖与配置 1. pom.xml需引入:- MyBatis-Plus(3.5.x,事务 + CRUD)- Hutool(4.0.12+,生成订单号)- JWT(Day10 登录用,验证用户身份)2. Spring 事务配置:在启动类添加@EnableTransactionManagement开启事务支持 若 Hutool 依赖缺失,补充:xmlcn.hutoolhutool-all4.0.12
前端组件与数据 1. Uni-App 项目已导入uni-picker(地址选择)、uni-icons(图标)组件2. 登录后uni.getStorageSync('mypet_token')可获取有效 Token,mypet_userId存储用户 ID3. 从购物车跳转结算页时,携带选中的购物车 ID 列表(如cartIds=1,2,3) uni-picker安装:HBuilder X→插件市场→搜索 "uni-ui"→导入 "uni-picker";购物车 ID 通过路由参数传递,确保能查询选中的商品
测试数据 1. cart表有选中的购物车数据(关联user_id=1、goods_id=1/2)2. address表有user_id=1的地址数据(至少 1 条,含默认地址)3. goods表有商品库存(如goods_id=1库存≥购买数量) 可手动插入测试地址:sqlINSERT INTO address (user_id, receiver, phone, full_address, is_default) VALUES (1, '张三', '13812345678', '北京市朝阳区XX小区1号楼101', 1);

二、核心逻辑流程图

订单提交涉及多步操作,需用事务确保一致性,先通过流程图理清步骤:

flowchart TD A[用户在结算页点击提交订单] --> B[前端校验
1选地址 2有商品 3库存大于等于数量] B -- 校验失败 --> C[提示错误 如请选择地址] B -- 校验成功 --> D[调用后端/orders/submit接口
携带user_id address_id cartIds] D --> E[后端拦截器验证Token
解析user_id,防越权] E -- Token无效 --> F[返回登录过期,请重新登录] E -- Token有效 --> G[开启事务] G --> H[1查询选中的购物车数据
含goods_id quantity goods_info] H --> I[2校验商品库存
若库存不足,事务回滚] I --> J[3生成唯一订单号
时间戳+随机数,避免重复] J --> K[4插入订单主表orders
order_no user_id total_amount等] K --> L[5批量插入订单明细表order_item
每条商品对应1条明细] L --> M[6扣减商品库存
goods.stock = stock - quantity] M --> N[7删除购物车中已下单的商品
cart表删除选中的cartIds] N --> O[事务提交] O --> P[返回订单号+总金额给前端] P --> Q[前端跳转至支付页
携带order_no total_amount]

三、代码实现

3.1 后端:订单提交接口(含事务、订单号生成)

3.1.1 1. 实体类(Entity)

需创建OrdersEntity(订单主表)、OrderItemEntity(订单明细表),映射数据库字段:

kotlin 复制代码
// src/main/java/com/entity/OrdersEntity.java(订单主表)
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@Data
@TableName("orders")
public class OrdersEntity {
    @TableId(type = IdType.AUTO)
    private Long id;
    @TableField("order_no")
    private String orderNo; // 唯一订单号
    @TableField("user_id")
    private Long userId; // 关联用户ID
    @TableField("address_id")
    private Long addressId; // 关联地址ID
    @TableField("total_amount")
    private BigDecimal totalAmount; // 总金额
    @TableField("order_status")
    private Integer orderStatus; // 订单状态:0-待付款
    @TableField(value = "create_time", fill = FieldFill.INSERT)
    private Date createTime; // 创建时间,自动填充
}
// src/main/java/com/entity/OrderItemEntity.java(订单明细表)
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@Data
@TableName("order_item")
public class OrderItemEntity {
    @TableId(type = IdType.AUTO)
    private Long id;
    @TableField("order_no")
    private String orderNo; // 关联订单号
    @TableField("goods_id")
    private Long goodsId; // 商品ID
    @TableField("goods_name")
    private String goodsName; // 商品名称
    @TableField("goods_price")
    private BigDecimal goodsPrice; // 下单单价
    @TableField("quantity")
    private Integer quantity; // 购买数量
    @TableField(value = "create_time", fill = FieldFill.INSERT)
    private Date createTime; // 创建时间,自动填充
}

3.1.2 2. Mapper 层(数据访问)

继承 MP 的BaseMapper,无需自定义 SQL,直接使用 CRUD 方法:

java 复制代码
// src/main/java/com/mapper/OrdersMapper.java
import com.entity.OrdersEntity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OrdersMapper extends BaseMapper<OrdersEntity> {}
// src/main/java/com/mapper/OrderItemMapper.java
import com.entity.OrderItemEntity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OrderItemMapper extends BaseMapper<OrderItemEntity> {}

3.1.3 3. Service 层(业务逻辑 + 事务)

核心层,处理订单生成、库存扣减、购物车删除,并用@Transactional确保事务一致性:

java 复制代码
// src/main/java/com/service/OrdersService.java(接口)
import com.entity.OrdersEntity;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
import java.util.Map;
public interface OrdersService extends IService<OrdersEntity> {
    // 提交订单:参数为用户ID、地址ID、选中的购物车ID列表
    Map<String, Object> submitOrder(Long userId, Long addressId, List<Long> cartIds);
}
// src/main/java/com/service/impl/OrdersServiceImpl.java(实现)
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.entity.*;
import com.mapper.*;
import com.service.OrdersService;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class OrdersServiceImpl extends ServiceImpl<OrdersMapper, OrdersEntity> implements OrdersService {
    @Autowired
    private CartMapper cartMapper;
    @Autowired
    private GoodsMapper goodsMapper;
    @Autowired
    private OrderItemMapper orderItemMapper;
    /**
     * 提交订单(核心方法,事务保证一致性)
     * @param userId 用户ID(从Token解析,防篡改)
     * @param addressId 收货地址ID
     * @param cartIds 选中的购物车ID列表
     * @return 订单号+总金额
     */
    @Override
    @Transactional(rollbackFor = Exception.class) // 关键:异常时回滚所有操作
    public Map<String, Object> submitOrder(Long userId, Long addressId, List<Long> cartIds) {
        // 1. 查询选中的购物车数据(关联商品信息)
        QueryWrapper<CartEntity> cartWrapper = new QueryWrapper<>();
        cartWrapper.in("id", cartIds).eq("user_id", userId).eq("is_deleted", 0);
        List<CartEntity> cartList = cartMapper.selectList(cartWrapper);
        if (cartList.isEmpty()) {
            throw new RuntimeException("购物车商品已失效,请重新选择!");
        }
        // 2. 校验库存+计算总金额
        BigDecimal totalAmount = BigDecimal.ZERO;
        List<OrderItemEntity> orderItemList = new ArrayList<>(); // 订单明细列表
        for (CartEntity cart : cartList) {
            Long goodsId = cart.getGoodsId();
            Integer buyQuantity = cart.getQuantity();
            // 查商品信息
            GoodsEntity goods = goodsMapper.selectById(goodsId);
            if (goods == null || goods.getStatus() != 1) { // 1-上架状态
                throw new RuntimeException("商品【" + goods.getGoodsName() + "】已下架或不存在!");
            }
            // 校验库存
            if (goods.getStock() < buyQuantity) {
                throw new RuntimeException("商品【" + goods.getGoodsName() + "】库存不足,当前库存:" + goods.getStock());
            }
            // 计算总金额(单价*数量)
            BigDecimal itemAmount = goods.getPrice().multiply(new BigDecimal(buyQuantity));
            totalAmount = totalAmount.add(itemAmount);
            // 构建订单明细
            OrderItemEntity item = new OrderItemEntity();
            item.setGoodsId(goodsId);
            item.setGoodsName(goods.getGoodsName());
            item.setGoodsPrice(goods.getPrice());
            item.setQuantity(buyQuantity);
            orderItemList.add(item);
        }
        // 3. 生成唯一订单号(格式:YYYYMMDD + 6位随机数,如20240520123456)
        String orderNo = DateUtil.format(DateUtil.date(), "yyyyMMdd") + IdUtil.randomNumbers(6);
        // 双重校验:确保订单号不重复(极端情况)
        if (baseMapper.selectCount(new QueryWrapper<OrdersEntity>().eq("order_no", orderNo)) > 0) {
            orderNo = DateUtil.format(DateUtil.date(), "yyyyMMdd") + IdUtil.randomNumbers(6); // 重新生成
        }
        // 4. 插入订单主表
        OrdersEntity order = new OrdersEntity();
        order.setOrderNo(orderNo);
        order.setUserId(userId);
        order.setAddressId(addressId);
        order.setTotalAmount(totalAmount);
        order.setOrderStatus(0); // 0-待付款
        baseMapper.insert(order);
        // 5. 批量插入订单明细表(给每条明细设置订单号)
        for (OrderItemEntity item : orderItemList) {
            item.setOrderNo(orderNo);
        }
        orderItemMapper.insertBatchSomeColumn(orderItemList); // MP批量插入方法
        // 6. 扣减商品库存(库存=原库存-购买数量)
        for (CartEntity cart : cartList) {
            GoodsEntity goods = goodsMapper.selectById(cart.getGoodsId());
            goods.setStock(goods.getStock() - cart.getQuantity());
            goodsMapper.updateById(goods);
        }
        // 7. 删除购物车中已下单的商品(逻辑删除,is_deleted=1)
        CartEntity deleteCart = new CartEntity();
        deleteCart.setIsDeleted(1);
        cartMapper.update(deleteCart, cartWrapper);
        // 8. 返回订单号和总金额
        Map<String, Object> result = new HashMap<>();
        result.put("orderNo", orderNo);
        result.put("totalAmount", totalAmount);
        return result;
    }
}

3.1.4 4. Controller 层(接口入口)

接收前端请求,验证用户身份,调用 Service 处理:

kotlin 复制代码
// src/main/java/com/controller/OrdersController.java
import com.service.OrdersService;
import com.utils.R;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/orders")
public class OrdersController {
    @Autowired
    private OrdersService ordersService;
    // JWT密钥(与Day10登录一致,建议配置在application.properties)
    private static final String JWT_SECRET = "mypet-secret-2024";
    /**
     * 订单提交接口(需登录,Token验证)
     * @param param 前端传递参数:addressId(地址ID)、cartIds(购物车ID列表)
     * @param request 获取Token
     */
    @PostMapping("/submit")
    public R submitOrder(@RequestBody Map<String, Object> param, HttpServletRequest request) {
        try {
            // 1. 从Token解析用户ID(防篡改,比前端传userId更安全)
            String token = request.getHeader("token");
            Claims claims = Jwts.parser().setSigningKey(JWT_SECRET).parseClaimsJws(token).getBody();
            Long userId = Long.parseLong(claims.getSubject());
            // 2. 解析前端参数
            Long addressId = Long.parseLong(param.get("addressId").toString());
            List<Long> cartIds = (List<Long>) param.get("cartIds");
            // 3. 调用Service提交订单
            Map<String, Object> orderResult = ordersService.submitOrder(userId, addressId, cartIds);
            // 4. 返回成功结果(订单号+总金额,供前端跳转支付)
            return R.ok("订单提交成功").put("data", orderResult);
        } catch (RuntimeException e) {
            // 业务异常(如库存不足),返回具体错误信息
            return R.error(e.getMessage());
        } catch (Exception e) {
            // 系统异常(如Token解析失败)
            e.printStackTrace();
            return R.error("订单提交失败,请重试!");
        }
    }
    /**
     * 辅助接口:获取用户地址列表(供前端联动)
     */
    @PostMapping("/getAddressList")
    public R getAddressList(HttpServletRequest request) {
        // 从Token解析用户ID
        String token = request.getHeader("token");
        Claims claims = Jwts.parser().setSigningKey(JWT_SECRET).parseClaimsJws(token).getBody();
        Long userId = Long.parseLong(claims.getSubject());
        // 查询该用户的所有地址(按默认地址排序)
        QueryWrapper<AddressEntity> addressWrapper = new QueryWrapper<>();
        addressWrapper.eq("user_id", userId).orderByDesc("is_default");
        List<AddressEntity> addressList = addressMapper.selectList(addressWrapper);
        return R.ok().put("data", addressList);
    }
}

3.2 前端:结算页(settle.vue)+ 地址联动

前端需实现 "地址选择→商品预览→总价计算→提交订单" 全流程,核心是uni-picker地址联动和表单校验:

xml 复制代码
<template>
  <view class="settle-page">
    <!-- 1. 地址选择器 -->
    <view class="address-container">
      <view class="address-title">收货地址</view>
      <uni-picker 
        mode="selector" 
        :range="addressList" 
        :range-key="'addressDesc'" 
        @change="onAddressChange"
        class="address-picker"
      >
        <view class="address-content">
          <!-- 显示默认地址或选中地址 -->
          <view v-if="selectedAddress" class="address-info">
            <view class="receiver">{{ selectedAddress.receiver }} {{ selectedAddress.phone }}</view>
            <view class="full-address">{{ selectedAddress.fullAddress }}</view>
          </view>
          <view v-else class="address-placeholder">请选择收货地址</view>
        </view>
      </uni-picker>
      <!-- 新增地址按钮 -->
      <button class="add-address-btn" @click="navToAddAddress">+ 新增地址</button>
    </view>
    <!-- 2. 订单商品列表 -->
    <view class="goods-list">
      <view class="list-title">订单商品({{ cartList.length }}件)</view>
      <view class="goods-item" v-for="(item, index) in cartList" :key="index">
        <image :src="item.goodsImage" mode="widthFix" class="goods-img"></image>
        <view class="goods-info">
          <view class="goods-name">{{ item.goodsName }}</view>
          <view class="goods-price">¥{{ item.goodsPrice.toFixed(2) }}</view>
        </view>
        <view class="goods-quantity">x{{ item.quantity }}</view>
      </view>
    </view>
    <!-- 3. 订单金额 -->
    <view class="order-amount">
      <view class="amount-item">
        <view class="label">商品总价</view>
        <view class="value">¥{{ totalAmount.toFixed(2) }}</view>
      </view>
      <view class="amount-item total">
        <view class="label">实付金额</view>
        <view class="value">¥{{ totalAmount.toFixed(2) }}</view>
      </view>
    </view>
    <!-- 4. 提交订单按钮 -->
    <button 
      class="submit-btn" 
      @click="submitOrder" 
      :disabled="!selectedAddress || isLoading"
    >
      {{ isLoading ? '提交中...' : `提交订单(¥${totalAmount.toFixed(2)})` }}
    </button>
  </view>
</template>
<script>
import request from '@/api/request.js';
import uniPicker from '@dcloudio/uni-ui/lib/uni-picker/uni-picker';
export default {
  components: { uniPicker },
  data() {
    return {
      cartIds: [],          // 选中的购物车ID列表(从路由参数获取)
      cartList: [],         // 购物车商品列表(含商品信息)
      addressList: [],      // 用户地址列表(供picker联动)
      selectedAddress: null,// 选中的地址对象
      totalAmount: 0,       // 订单总金额
      isLoading: false      // 加载状态(防重复提交)
    };
  },
  onLoad(options) {
    // 1. 从路由参数获取选中的购物车ID(如cartIds=1,2,3)
    this.cartIds = options.cartIds.split(',').map(id => parseInt(id));
    // 2. 加载购物车商品和地址列表
    this.getCartGoods();
    this.getAddressList();
  },
  methods: {
    // 1. 加载选中的购物车商品
    getCartGoods() {
      this.isLoading = true;
      request.post('/cart/getSelectedGoods', { cartIds: this.cartIds })
        .then(res => {
          if (res.data.code === 0) {
            this.cartList = res.data.data;
            // 计算总金额
            this.calculateTotal();
          } else {
            uni.showToast({ title: res.data.msg, icon: 'none' });
          }
        })
        .catch(err => {
          uni.showToast({ title: '加载商品失败', icon: 'none' });
          console.error(err);
        })
        .finally(() => {
          this.isLoading = false;
        });
    },
    // 2. 加载用户地址列表(供picker联动)
    getAddressList() {
      request.post('/orders/getAddressList', {}, {
        headers: { token: uni.getStorageSync('mypet_token') }
      })
        .then(res => {
          if (res.data.code === 0) {
            this.addressList = res.data.data;
            // 自动选中默认地址(is_default=1)
            const defaultAddr = this.addressList.find(addr => addr.isDefault === 1);
            if (defaultAddr) {
              this.selectedAddress = defaultAddr;
            }
            // 格式化地址显示文本(供picker选项)
            this.addressList.forEach(addr => {
              addr.addressDesc = `${addr.receiver} ${addr.phone} | ${addr.fullAddress}`;
            });
          }
        })
        .catch(err => {
          uni.showToast({ title: '加载地址失败', icon: 'none' });
          console.error(err);
        });
    },
    // 3. 地址选择变化(picker回调)
    onAddressChange(e) {
      const index = e.detail.value;
      this.selectedAddress = this.addressList[index];
    },
    // 4. 计算订单总金额
    calculateTotal() {
      this.totalAmount = this.cartList.reduce((sum, item) => {
        return sum + (item.goodsPrice * item.quantity);
      }, 0);
    },
    // 5. 提交订单
    submitOrder() {
      // 前端兜底校验
      if (!this.selectedAddress) {
        return uni.showToast({ title: '请选择收货地址', icon: 'none' });
      }
      if (this.cartList.length === 0) {
        return uni.showToast({ title: '订单无商品,请重新选择', icon: 'none' });
      }
      this.isLoading = true;
      // 调用后端提交接口
      request.post('/orders/submit', {
        addressId: this.selectedAddress.id,
        cartIds: this.cartIds
      }, {
        headers: { token: uni.getStorageSync('mypet_token') }
      })
        .then(res => {
          if (res.data.code === 0) {
            // 提交成功:跳转至支付页(携带订单号和金额)
            const { orderNo, totalAmount } = res.data.data;
            uni.showToast({ title: '订单提交成功', icon: 'success' });
            setTimeout(() => {
              uni.navigateTo({
                url: `/pages/pay/index?orderNo=${orderNo}&totalAmount=${totalAmount}`
              });
            }, 1500);
          } else {
            uni.showToast({ title: res.data.msg, icon: 'none' });
          }
        })
        .catch(err => {
          uni.showToast({ title: '提交失败,请重试', icon: 'none' });
          console.error(err);
        })
        .finally(() => {
          this.isLoading = false;
        });
    },
    // 6. 跳转到新增地址页
    navToAddAddress() {
      uni.navigateTo({ url: '/pages/address/add' });
    }
  }
};
</script>
<style scoped>
/* 页面整体样式 */
.settle-page {
  padding: 20rpx;
  background-color: #f5f5f5;
  min-height: 100vh;
}
/* 地址容器 */
.address-container {
  background-color: #fff;
  border-radius: 16rpx;
  padding: 20rpx;
  margin-bottom: 20rpx;
}
.address-title {
  font-size: 28rpx;
  font-weight: bold;
  color: #333;
  margin-bottom: 20rpx;
}
.address-picker {
  width: 100%;
}
.address-content {
  padding: 20rpx;
  border: 1px solid #eee;
  border-radius: 10rpx;
  min-height: 120rpx;
  display: flex;
  align-items: center;
}
.address-placeholder {
  color: #999;
  font-size: 26rpx;
}
.address-info {
  font-size: 26rpx;
  color: #333;
}
.receiver {
  margin-bottom: 10rpx;
}
.full-address {
  color: #666;
}
.add-address-btn {
  margin-top: 20rpx;
  padding: 15rpx 0;
  background-color: #f5f5f5;
  color: #007aff;
  font-size: 26rpx;
  border-radius: 10rpx;
}
/* 商品列表 */
.goods-list {
  background-color: #fff;
  border-radius: 16rpx;
  padding: 20rpx;
  margin-bottom: 20rpx;
}
.list-title {
  font-size: 28rpx;
  font-weight: bold;
  color: #333;
  margin-bottom: 20rpx;
}
.goods-item {
  display: flex;
  align-items: center;
  padding: 15rpx 0;
  border-bottom: 1px solid #eee;
}
.goods-img {
  width: 120rpx;
  height: 120rpx;
  border-radius: 8rpx;
  margin-right: 20rpx;
}
.goods-info {
  flex: 1;
}
.goods-name {
  font-size: 26rpx;
  color: #333;
  margin-bottom: 10rpx;
  display: -webkit-box;
  -webkit-line-clamp: 1;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.goods-price {
  font-size: 26rpx;
  color: #f53f3f;
}
.goods-quantity {
  font-size: 26rpx;
  color: #666;
}
/* 订单金额 */
.order-amount {
  background-color: #fff;
  border-radius: 16rpx;
  padding: 20rpx;
  margin-bottom: 30rpx;
}
.amount-item {
  display: flex;
  justify-content: space-between;
  font-size: 26rpx;
  margin-bottom: 15rpx;
}
.amount-item.total {
  font-weight: bold;
  color: #f53f3f;
  font-size: 28rpx;
  margin-top: 20rpx;
  padding-top: 20rpx;
  border-top: 1px solid #eee;
}
.label {
  color: #666;
}
.value {
  color: #333;
}
/* 提交按钮 */
.submit-btn {
  width: 100%;
  padding: 24rpx 0;
  background-color: #007aff;
  color: #fff;
  font-size: 30rpx;
  border-radius: 16rpx;
  position: fixed;
  bottom: 30rpx;
  left: 20rpx;
  right: 20rpx;
}
.submit-btn:disabled {
  background-color: #8cc5ff;
}
</style>

四、效果验证

需分后端接口测试前端全流程测试,确保数据一致性和功能正常:

✅ 1. 后端接口测试(Postman)

测试 1:提交订单接口

  1. 请求地址http://localhost:8080/orders/submit
  1. 请求方式:POST
  1. 请求头:token: 有效用户Token
  1. 请求体(JSON)
json 复制代码
{
  "addressId": 1,
  "cartIds": [1, 2]
}
  1. 成功返回
css 复制代码
{
  "code": 0,
  "msg": "订单提交成功",
  "data": {
    "orderNo": "20240520123456",
    "totalAmount": 269.80
  }
}
  1. 数据库验证
    • orders表:新增 1 条记录,order_no=20240520123456,order_status=0;
    • order_item表:新增 2 条明细,关联该订单号;
    • cart表:选中的cartIds=1,2记录is_deleted=1(逻辑删除);
    • goods表:对应商品库存已扣减(如原库存 100→98)。

测试 2:库存不足场景

  1. 手动将goods表中某商品库存改为 1,购买数量设为 2;
  1. 重新发送请求,返回:{"code":1,"msg":"商品【宠物狗粮10kg】库存不足,当前库存:1"};
  1. 数据库验证:所有表无新增 / 修改,事务回滚成功。

✅ 2. 前端全流程测试(Uni-App 模拟器)

  1. 跳转结算页:从购物车选中 2 件商品,点击 "结算"→携带cartIds=1,2跳转至settle.vue;
  1. 地址联动:页面自动加载地址列表,默认选中is_default=1的地址,点击地址可下拉切换其他地址;
  1. 商品预览:显示选中的商品图片、名称、单价、数量,总金额自动计算(如 2 件商品共 269.8 元);
  1. 提交订单:点击 "提交订单"→按钮显示 "提交中..."→提示 "订单提交成功"→1.5 秒后跳转至支付页;
  1. 支付页验证:支付页携带orderNo=20240520123456和totalAmount=269.8,数据正确。

五、常见问题与排查

问题现象 可能原因 解决方式
1. 订单提交后部分表更新,部分未更新 1. 未加@Transactional注解;2. 异常未抛到 Service 层;3. 事务隔离级别配置错误 1. 确保submitOrder方法加@Transactional(rollbackFor = Exception.class);2. 所有业务异常(如库存不足)需抛RuntimeException;3. 检查 Spring 事务配置,默认隔离级别即可
2. 前端 picker 不显示地址选项 1. addressList为空;2. range-key配置错误;3. 地址列表未格式化addressDesc 1. 用 Postman 测试/orders/getAddressList,确认返回地址数据;2. 确保range-key="addressDesc",且地址对象有该字段;3. 检查getAddressList中是否执行addr.addressDesc = ...
3. 订单号重复 1. 生成策略重复(如仅用随机数);2. 高并发下未双重校验 1. 改用 "时间戳 + 随机数" 策略(如本文的yyyyMMdd+6位随机数);2. 生成后加baseMapper.selectCount(...)双重校验,重复则重新生成
4. 前端提交时提示 "购物车商品已失效" 1. 购物车数据被删除;2. cartIds传递错误;3. 购物车is_deleted=1 1. 检查cart表,确保cartIds对应的记录is_deleted=0;2. 打印前端this.cartIds,确认与后端接收一致;3. 结算页加载时重新查询购物车,避免数据过时
5. 扣减库存后订单提交失败,库存未回滚 1. 库存扣减代码在事务外;2. 异常类型未被rollbackFor捕获 1. 确保库存扣减逻辑在@Transactional注解的方法内;2. rollbackFor = Exception.class捕获所有异常,避免只回滚 RuntimeException

六、工具类 / 框架特性拓展

6.1 Hutool 订单号生成策略对比

本文用 "时间戳 + 6 位随机数",适合中小项目,其他常用策略:

策略 实现方式 优点 缺点 适用场景
时间戳 + 随机数 DateUtil.format(...) + IdUtil.randomNumbers(6) 可读性高(含日期),重复率低 高并发下需双重校验 中小项目、非秒杀场景
UUID(无横线) IdUtil.simpleUUID() 代码简单,全球唯一 无意义(难追溯日期),字符串长 无需可读性的场景
雪花算法(Snowflake) IdUtil.getSnowflake().nextIdStr() 分布式环境唯一,有序递增 依赖机器 ID,配置复杂 分布式项目、高并发场景

6.2 Uni-App picker 高级用法:省市区三级联动

若需更精细的地址选择(省→市→区),可改用uni-picker-cascader组件:

ini 复制代码
<uni-picker-cascader 
  :options="areaOptions" 
  @change="onAreaChange"
  placeholder="选择省市区"
></uni-picker-cascader>
  • areaOptions:省市区数据源(可从后端接口获取,格式为[{value: '110000', label: '北京市', children: [...]}, ...]);
  • onAreaChange:选择后获取省市区 ID,拼接成完整地址。

七、下节预告

👉 明天 Day17:订单列表与状态筛选!我们将学习:

  1. 实现订单列表分页加载(按状态筛选:待付款 / 待发货 / 已完成);
  1. 前端用uni-segmented-control实现状态切换标签;
  1. 订单详情页开发(展示主表 + 明细表数据);
  1. 待付款订单的 "取消订单" 功能(事务回滚库存)。

提前复习 MP 的条件查询(QueryWrapper)和 Uni-App 的分段控制器组件!

相关推荐
大学生资源网18 分钟前
基于springboot的万亩助农网站的设计与实现源代码(源码+文档)
java·spring boot·后端·mysql·毕业设计·源码
苏三的开发日记27 分钟前
linux端进行kafka集群服务的搭建
后端
苏三的开发日记1 小时前
windows系统搭建kafka环境
后端
爬山算法1 小时前
Netty(19)Netty的性能优化手段有哪些?
java·后端
Tony Bai1 小时前
Cloudflare 2025 年度报告发布——Go 语言再次“屠榜”API 领域,AI 流量激增!
开发语言·人工智能·后端·golang
想用offer打牌1 小时前
虚拟内存与寻址方式解析(面试版)
java·后端·面试·系统架构
無量1 小时前
AQS抽象队列同步器原理与应用
后端
9号达人2 小时前
支付成功订单却没了?MyBatis连接池的坑我踩了
java·后端·面试
用户497357337982 小时前
【轻松掌握通信协议】C#的通信过程与协议实操 | 2024全新
后端
草莓熊Lotso2 小时前
C++11 核心精髓:类新功能、lambda与包装器实战
开发语言·c++·人工智能·经验分享·后端·nginx·asp.net