【商城实战(15)】订单创建与提交:技术与实战的深度融合
【商城实战】专栏重磅来袭!这是一份专为开发者与电商从业者打造的超详细指南。从项目基础搭建,运用 uniapp、Element Plus、SpringBoot 搭建商城框架,到用户、商品、订单等核心模块开发,再到性能优化、安全加固、多端适配,乃至运营推广策略,102 章内容层层递进。无论是想深入钻研技术细节,还是探寻商城运营之道,本专栏都能提供从 0 到 1 的系统讲解,助力你打造独具竞争力的电商平台,开启电商实战之旅。
目录
- 一、订单表及订单详情表设计
- 1.1 订单表设计
- 1.2 订单详情表设计
- 二、前端订单创建页面开发
- 2.1 页面布局搭建
- 2.2 购物车商品转化逻辑
- 三、后端订单创建接口编写
- 3.1 接口功能概述
- 3.2 订单数据生成
- 3.3 库存扣减
一、订单表及订单详情表设计
在电商系统中,订单表和订单详情表是核心表之一,用于存储订单的关键信息和订单中商品的详细信息,为订单管理、用户查询、财务结算等功能提供数据支持。
1.1 订单表设计
订单表用于记录订单的总体信息,包括订单编号、用户 ID、订单总金额、订单状态、创建时间等。以 MySQL 为例,建表语句如下:
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
order_no VARCHAR(50) NOT NULL UNIQUE COMMENT '订单编号,唯一标识一个订单',
user_id INT NOT NULL COMMENT '下单用户的ID,关联用户表',
total_amount DECIMAL(10, 2) NOT NULL COMMENT '订单总金额',
order_status ENUM('created', 'paid','shipped', 'delivered', 'canceled') NOT NULL DEFAULT 'created' COMMENT '订单状态,分别表示创建、已支付、已发货、已送达、已取消',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '订单创建时间',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '订单更新时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
- id:订单表的主键,自增长整数,用于唯一标识每条订单记录。
- order_no:订单编号,采用唯一的字符串,方便在系统中追踪和识别订单。
- user_id:下单用户的 ID,与用户表中的 ID 关联,用于标识订单所属用户。
- total_amount:订单总金额,精确到小数点后两位,用于记录订单的总费用。
- order_status:订单状态,使用枚举类型,限制订单状态只能是指定的几种,便于管理和跟踪订单流程。
- create_time:订单创建时间,在订单创建时自动记录当前时间。
- update_time:订单更新时间,在订单状态发生变化时自动更新为当前时间。
1.2 订单详情表设计
订单详情表用于记录每个订单中具体商品的详细信息,包括订单详情 ID、订单 ID、商品 ID、商品数量、商品单价等。建表语句如下:
CREATE TABLE order_items (
id INT AUTO_INCREMENT PRIMARY KEY,
order_id INT NOT NULL COMMENT '所属订单的ID,关联订单表的id',
product_id INT NOT NULL COMMENT '商品ID,关联商品表',
quantity INT NOT NULL COMMENT '商品数量',
price DECIMAL(10, 2) NOT NULL COMMENT '商品单价',
INDEX (order_id),
INDEX (product_id),
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
- id:订单详情表的主键,自增长整数,唯一标识每条订单详情记录。
- order_id:所属订单的 ID,与订单表中的id关联,用于建立订单与订单详情的关系。
- product_id:商品 ID,与商品表中的 ID 关联,用于标识订单中的商品。
- quantity:商品数量,记录该商品在订单中的购买数量。
- price:商品单价,记录该商品的销售单价。
- INDEX (order_id) 和 INDEX (product_id):分别为order_id和product_id字段创建索引,提高查询订单详情时的效率。
- FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE ON UPDATE CASCADE:设置外键约束,关联订单表的id字段,当订单表中的订单记录被删除或更新时,订单详情表中对应的记录也会相应地被删除或更新 ,保证数据的一致性和完整性。
二、前端订单创建页面开发
前端订单创建页面是用户将购物车商品转化为订单的关键交互界面,使用 uniapp 开发,结合 Vue.js 的响应式原理和组件化开发模式,为用户提供流畅的下单体验。
2.1 页面布局搭建
首先,在 uniapp 项目的pages目录下创建orderCreate文件夹,并在其中创建orderCreate.vue文件。在orderCreate.vue中,使用view组件搭建基本布局结构,利用flex布局实现页面元素的排列和自适应。
<template>
<view class="order-create-container">
<!-- 收货地址区域 -->
<view class="address-section">
<view class="address-item" v-for="(address, index) in addressList" :key="index">
<text>{{ address.name }}: {{ address.phone }}</text>
<text>{{ address.address }}</text>
</view>
<button @click="showAddAddressModal = true">添加新地址</button>
</view>
<!-- 商品列表区域 -->
<view class="product-list-section">
<view class="product-item" v-for="(product, index) in productList" :key="index">
<image :src="product.imageUrl" mode="aspectFill"></image>
<view class="product-info">
<text>{{ product.name }}</text>
<text>单价: {{ product.price }}</text>
<text>数量: {{ product.quantity }}</text>
</view>
</view>
</view>
<!-- 订单金额区域 -->
<view class="total-amount-section">
<text>订单总金额: {{ totalAmount }}</text>
</view>
<!-- 提交订单按钮 -->
<button @click="submitOrder">提交订单</button>
</view>
</template>
<script>
export default {
data() {
return {
addressList: [],
productList: [],
totalAmount: 0,
showAddAddressModal: false
};
},
onLoad() {
// 初始化页面数据,如获取用户地址、购物车商品等
this.getAddressList();
this.getProductList();
},
methods: {
getAddressList() {
// 模拟从后端获取地址列表
this.addressList = [
{ name: '张三', phone: '13800138000', address: '北京市朝阳区XX街道XX号' },
{ name: '李四', phone: '13900139000', address: '上海市浦东新区XX街道XX号' }
];
},
getProductList() {
// 从购物车获取商品列表,假设购物车数据存储在本地缓存中
const cartList = uni.getStorageSync('cartList');
this.productList = cartList.map(product => ({
name: product.name,
imageUrl: product.imageUrl,
price: product.price,
quantity: product.quantity
}));
this.calculateTotalAmount();
},
calculateTotalAmount() {
this.totalAmount = this.productList.reduce((total, product) => {
return total + product.price * product.quantity;
}, 0);
},
submitOrder() {
// 提交订单逻辑,发送订单数据到后端
console.log('提交订单,订单数据:', {
address: this.addressList[0],
products: this.productList,
totalAmount: this.totalAmount
});
}
}
};
</script>
<style lang="scss">
.order-create-container {
padding: 20rpx;
background-color: #f5f5f5;
.address-section {
margin-bottom: 30rpx;
background-color: #fff;
padding: 20rpx;
border-radius: 10rpx;
.address-item {
margin-bottom: 15rpx;
}
}
.product-list-section {
margin-bottom: 30rpx;
background-color: #fff;
padding: 20rpx;
border-radius: 10rpx;
.product-item {
display: flex;
align-items: center;
padding: 15rpx 0;
border-bottom: 1rpx solid #e1e1e1;
image {
width: 100rpx;
height: 100rpx;
margin-right: 20rpx;
}
.product-info {
flex: 1;
text {
display: block;
margin-bottom: 5rpx;
}
}
}
}
.total-amount-section {
margin-bottom: 30rpx;
background-color: #fff;
padding: 20rpx;
border-radius: 10rpx;
text-align: right;
font-size: 30rpx;
font-weight: bold;
}
button {
width: 100%;
padding: 20rpx;
background-color: #49bdfb;
color: #fff;
border: none;
border-radius: 10rpx;
font-size: 30rpx;
}
}
</style>
2.2 购物车商品转化逻辑
从购物车页面跳转到订单创建页面时,需要传递购物车中的商品数据。在购物车页面,当用户点击 “去结算” 按钮时,将购物车商品数据通过uni.navigateTo的url参数传递给订单创建页面。
在购物车页面cart.vue中:
<template>
<view class="cart-container">
<!-- 购物车商品列表 -->
<view class="cart-item" v-for="(product, index) in cartList" :key="index">
<image :src="product.imageUrl" mode="aspectFill"></image>
<view class="cart-item-info">
<text>{{ product.name }}</text>
<text>单价: {{ product.price }}</text>
<text>数量: {{ product.quantity }}</text>
</view>
</view>
<!-- 去结算按钮 -->
<button @click="goToOrderCreate">去结算</button>
</view>
</template>
<script>
export default {
data() {
return {
cartList: []
};
},
onLoad() {
// 从本地缓存获取购物车数据
this.cartList = uni.getStorageSync('cartList');
},
methods: {
goToOrderCreate() {
const cartData = JSON.stringify(this.cartList);
uni.navigateTo({
url: `/pages/orderCreate/orderCreate?cartData=${cartData}`
});
}
}
};
</script>
<style lang="scss">
.cart-container {
padding: 20rpx;
background-color: #f5f5f5;
.cart-item {
display: flex;
align-items: center;
padding: 15rpx 0;
border-bottom: 1rpx solid #e1e1e1;
image {
width: 100rpx;
height: 100rpx;
margin-right: 20rpx;
}
.cart-item-info {
flex: 1;
text {
display: block;
margin-bottom: 5rpx;
}
}
}
button {
width: 100%;
padding: 20rpx;
background-color: #49bdfb;
color: #fff;
border: none;
border-radius: 10rpx;
font-size: 30rpx;
}
}
</style>
在订单创建页面orderCreate.vue的onLoad生命周期函数中接收传递过来的商品数据:
onLoad(options) {
const cartData = JSON.parse(options.cartData);
this.productList = cartData.map(product => ({
name: product.name,
imageUrl: product.imageUrl,
price: product.price,
quantity: product.quantity
}));
this.calculateTotalAmount();
}
2.3 交互效果实现
- 选择商品:在订单创建页面,用户可以对商品数量进行调整。通过在商品列表中添加增减按钮,并绑定点击事件来实现数量的改变。
<view class="product-item" v-for="(product, index) in productList" :key="index">
<image :src="product.imageUrl" mode="aspectFill"></image>
<view class="product-info">
<text>{{ product.name }}</text>
<text>单价: {{ product.price }}</text>
<view class="quantity-control">
<button @click="decreaseQuantity(index)">-</button>
<text>{{ product.quantity }}</text>
<button @click="increaseQuantity(index)">+</button>
</view>
</view>
</view>
methods: {
decreaseQuantity(index) {
if (this.productList[index].quantity > 1) {
this.productList[index].quantity--;
this.calculateTotalAmount();
}
},
increaseQuantity(index) {
this.productList[index].quantity++;
this.calculateTotalAmount();
},
// 其他方法...
}
- 修改数量:用户在输入框中直接输入数量时,实时更新商品数量和订单总金额。
<view class="quantity-control">
<input type="number" v-model="productList[index].quantity" @input="calculateTotalAmount">
</view>
- 提交订单:点击 “提交订单” 按钮,触发submitOrder方法,在该方法中收集订单数据,包括收货地址、商品列表、订单总金额等,并通过uni.request发送 POST 请求到后端订单创建接口。
submitOrder() {
const orderData = {
address: this.addressList[0],
products: this.productList,
totalAmount: this.totalAmount
};
uni.request({
url: 'https://your-backend.com/api/order/create',
method: 'POST',
data: orderData,
success: res => {
if (res.statusCode === 200) {
uni.showToast({
title: '订单提交成功',
icon:'success'
});
// 清空购物车数据
uni.removeStorageSync('cartList');
// 跳转到订单详情页面
uni.navigateTo({
url: `/pages/orderDetail/orderDetail?orderId=${res.data.orderId}`
});
} else {
uni.showToast({
title: '订单提交失败',
icon: 'none'
});
}
},
fail: err => {
uni.showToast({
title: '网络错误,请稍后重试',
icon: 'none'
});
}
});
}
- 添加地址:点击 “添加新地址” 按钮,显示地址添加模态框,用户填写地址信息后,点击保存按钮,将新地址添加到地址列表中,并关闭模态框。
<view class="address-section">
<!-- 已有地址列表 -->
<view class="address-item" v-for="(address, index) in addressList" :key="index">
<text>{{ address.name }}: {{ address.phone }}</text>
<text>{{ address.address }}</text>
</view>
<button @click="showAddAddressModal = true">添加新地址</button>
<!-- 地址添加模态框 -->
<view v-if="showAddAddressModal" class="add-address-modal">
<view class="modal-content">
<input type="text" placeholder="姓名" v-model="newAddress.name">
<input type="text" placeholder="电话" v-model="newAddress.phone">
<input type="text" placeholder="地址" v-model="newAddress.address">
<button @click="addNewAddress">保存</button>
<button @click="showAddAddressModal = false">取消</button>
</view>
</view>
</view>
data() {
return {
addressList: [],
newAddress: {
name: '',
phone: '',
address: ''
},
showAddAddressModal: false
};
},
methods: {
addNewAddress() {
this.addressList.push(this.newAddress);
this.showAddAddressModal = false;
this.newAddress = {
name: '',
phone: '',
address: ''
};
},
// 其他方法...
}
通过以上前端开发,实现了订单创建页面的基本功能和交互效果,为用户提供了便捷的下单体验。
三、后端订单创建接口编写
在后端,使用 Spring Boot 框架来编写订单创建接口,确保订单创建过程的可靠性和高效性,同时处理库存扣减和事务管理,以保证数据的一致性。
3.1 接口功能概述
订单创建接口主要负责接收前端传来的订单数据,这些数据包括用户信息、收货地址、购物车商品列表及订单总金额等。接口需要对这些数据进行校验和处理,生成订单数据并插入到订单表和订单详情表中。同时,根据订单中的商品信息,对商品库存进行扣减操作,在整个过程中需要确保事务的一致性,即订单数据插入和库存扣减要么全部成功,要么全部失败,避免出现数据不一致的情况。
3.2 订单数据生成
假设使用 Spring Boot + MyBatis 来开发后端接口,首先定义订单相关的实体类Order和OrderItem,对应数据库中的订单表和订单详情表。
// Order.java
public class Order {
private Integer id;
private String orderNo;
private Integer userId;
private BigDecimal totalAmount;
private String orderStatus;
private Date createTime;
private Date updateTime;
// 省略getter和setter方法
}
// OrderItem.java
public class OrderItem {
private Integer id;
private Integer orderId;
private Integer productId;
private Integer quantity;
private BigDecimal price;
// 省略getter和setter方法
}
在订单创建接口的实现方法中,接收前端传递的订单数据,生成Order和OrderItem对象,并插入到数据库中。
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.Date;
import java.util.List;
import java.util.UUID;
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private OrderItemMapper orderItemMapper;
@Transactional
public String createOrder(OrderDto orderDto) {
// 生成订单编号
String orderNo = UUID.randomUUID().toString().replace("-", "");
// 创建订单对象
Order order = new Order();
order.setOrderNo(orderNo);
order.setUserId(orderDto.getUserId());
order.setTotalAmount(orderDto.getTotalAmount());
order.setOrderStatus("created");
order.setCreateTime(new Date());
order.setUpdateTime(new Date());
// 插入订单数据到订单表
orderMapper.insertOrder(order);
// 获取订单ID
Integer orderId = order.getId();
// 创建订单详情对象并插入到订单详情表
List<OrderItemDto> orderItemDtoList = orderDto.getOrderItemDtoList();
for (OrderItemDto orderItemDto : orderItemDtoList) {
OrderItem orderItem = new OrderItem();
orderItem.setOrderId(orderId);
orderItem.setProductId(orderItemDto.getProductId());
orderItem.setQuantity(orderItemDto.getQuantity());
orderItem.setPrice(orderItemDto.getPrice());
orderItemMapper.insertOrderItem(orderItem);
}
return orderNo;
}
}
其中,OrderDto和OrderItemDto是用于接收前端数据的 DTO(数据传输对象),OrderMapper和OrderItemMapper是 MyBatis 的 Mapper 接口,用于执行数据库操作。
3.3 库存扣减
库存扣减是订单创建过程中的关键环节,为了确保库存数据的准确性,避免超卖情况的发生,需要在订单创建时对商品库存进行扣减。扣减库存的实现方式通常是在数据库中对商品库存表进行更新操作。在上述createOrder方法中,在插入订单详情数据后,添加库存扣减逻辑。
@Autowired
private ProductMapper productMapper;
@Transactional
public String createOrder(OrderDto orderDto) {
// 生成订单编号...
// 创建订单对象并插入订单表...
// 创建订单详情对象并插入订单详情表...
// 扣减库存
List<OrderItemDto> orderItemDtoList = orderDto.getOrderItemDtoList();
for (OrderItemDto orderItemDto : orderItemDtoList) {
Integer productId = orderItemDto.getProductId();
Integer quantity = orderItemDto.getQuantity();
productMapper.reduceStock(productId, quantity);
}
return orderNo;
}
ProductMapper是用于操作商品表的 Mapper 接口,reduceStock方法用于扣减商品库存。
<!-- ProductMapper.xml -->
<mapper namespace="com.example.demo.mapper.ProductMapper">
<update id="reduceStock">
UPDATE products
SET stock = stock - #{quantity}
WHERE id = #{productId} AND stock >= #{quantity}
</update>
</mapper>
在这个 SQL 语句中,使用UPDATE语句更新商品表中的库存字段,只有当库存大于等于要扣减的数量时才执行更新操作,从而避免超卖。
在整个订单创建过程中,使用@Transactional注解来声明事务。该注解会将被注解的方法包装在一个事务中,如果方法执行过程中出现异常,事务会自动回滚,确保订单数据插入和库存扣减操作的原子性和一致性,保证数据的完整性。通过以上后端代码的实现,完成了订单创建接口的开发,实现了订单数据生成和库存扣减的功能 ,并保证了事务的正确处理。