电商项目实战:从零搭一个能跑的商城系统
电商项目实战:从零搭一个能跑的商城系统
系列专栏:从Java到AI应用开发 | 第4篇
写在前面
前三篇我们学了API搭建、数据模型、Service层设计,知识点都是零散的。今天来干一票大的——把这些全串起来,搭一个完整的电商系统。
不是Demo级别的增删改查,而是真正能跑的业务:商品管理、购物车、下单、支付、发货、退款,整个订单生命周期。每个环节都有真实的业务规则,不是"存进去就行"。
一、项目结构先想清楚
别上来就写代码。先想清楚这个商城有哪些模块、每个模块干什么:
plaintext
99
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
ecommerce/
├── controller/ # 接收请求
│ ├── ProductController.java
│ ├── CartController.java
│ ├── OrderController.java
│ └── CouponController.java
├── service/ # 业务逻辑
│ ├── ProductService.java
│ ├── CartService.java
│ ├── OrderService.java
│ ├── CouponService.java
│ └── impl/ # 实现类
├── model/ # 数据模型
│ ├── Product.java
│ ├── CartItem.java
│ ├── Order.java
│ ├── OrderItem.java
│ └── Coupon.java
├── repository/ # 数据存储(目前用Excel)
│ ├── ProductRepository.java
│ ├── CartRepository.java
│ ├── OrderRepository.java
│ └── CouponRepository.java
├── enums/
│ ├── OrderStatus.java
│ └── CouponType.java
└── exception/
├── BusinessException.java
└── GlobalExceptionHandler.java
核心原则:Controller只转发,Service管业务,Repository管数据。 这话说了三篇了,但到了实战你才会真正体会到为什么要这样分。
二、商品模块:不只是CRUD
数据模型
java
99
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Product {
private String id; // 商品ID
private String name; // 商品名称
private String category; // 分类(数码、服饰、食品...)
private BigDecimal price; // 售价
private BigDecimal costPrice; // 成本价
private int stock; // 库存
private int sold; // 已售数量
private String description; // 描述
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
Service:商品不只是增删改查
java
9
1
2
3
4
5
6
7
8
9
public interface ProductService {
Product createProduct(Product product);
Product updateProduct(String id, Product product);
Product getProduct(String id);
List<Product> listProducts(String category, String keyword);
void updateStock(String productId, int quantity); // 扣/补库存
List<Product> getHotProducts(int limit); // 热销排行
}
java
99
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Service
public class ProductServiceImpl implements ProductService {
@Autowired
private ProductRepository productRepository;
@Override
public Product createProduct(Product product) {
product.setId(UUID.randomUUID().toString());
product.setSold(0);
product.setCreateTime(LocalDateTime.now());
product.setUpdateTime(LocalDateTime.now());
return productRepository.save(product);
}
@Override
@Transactional
public void updateStock(String productId, int quantity) {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new BusinessException("商品不存在"));
int newStock = product.getStock() + quantity; // quantity为负数表示扣减
if (newStock < 0) {
throw new BusinessException("库存不足,当前库存:" + product.getStock());
}
product.setStock(newStock);
product.setUpdateTime(LocalDateTime.now());
productRepository.save(product);
}
@Override
@Transactional(readOnly = true)
public List<Product> getHotProducts(int limit) {
return productRepository.findAll().stream()
.sorted(Comparator.comparingInt(Product::getSold).reversed())
.limit(limit)
.collect(Collectors.toList());
}
// ... 其他方法
}
注意 updateStock 的设计:quantity可以是正数(补库存)也可以是负数(扣库存),一个方法搞定两个方向。 比起写 deductStock 和 addStock 两个方法,更简洁也更不容易漏。
三、购物车:临时态的业务逻辑
购物车是电商里最容易写乱的模块。它有个特点:数据是临时的,可能随时变,不一定最后都下单。
数据模型
java
9
1
2
3
4
5
6
7
8
public class CartItem {
private String id;
private String userId;
private String productId;
private int quantity;
private LocalDateTime addTime;
}
Service:购物车的三个关键操作
java
9
1
2
3
4
5
6
7
8
9
public interface CartService {
CartItem addToCart(String userId, String productId, int quantity);
CartItem updateQuantity(String cartItemId, int quantity);
void removeFromCart(String cartItemId);
List<CartItem> getCart(String userId);
BigDecimal calculateTotal(String userId); // 计算购物车总价
void clearCart(String userId); // 下单后清空
}
java
99
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@Service
public class CartServiceImpl implements CartService {
@Autowired
private CartRepository cartRepository;
@Autowired
private ProductRepository productRepository;
@Override
public CartItem addToCart(String userId, String productId, int quantity) {
// 1. 商品存在性校验
Product product = productRepository.findById(productId)
.orElseThrow(() -> new BusinessException("商品不存在"));
// 2. 库存校验
if (product.getStock() < quantity) {
throw new BusinessException("库存不足");
}
// 3. 如果已在购物车里,叠加数量
Optional<CartItem> existing = cartRepository.findByUserIdAndProductId(userId, productId);
if (existing.isPresent()) {
CartItem item = existing.get();
int newQuantity = item.getQuantity() + quantity;
if (newQuantity > product.getStock()) {
throw new BusinessException("超出库存,当前购物车已有" + item.getQuantity() + "件");
}
item.setQuantity(newQuantity);
return cartRepository.save(item);
}
// 4. 新增购物车项
CartItem item = new CartItem();
item.setId(UUID.randomUUID().toString());
item.setUserId(userId);
item.setProductId(productId);
item.setQuantity(quantity);
item.setAddTime(LocalDateTime.now());
return cartRepository.save(item);
}
@Override
@Transactional(readOnly = true)
public BigDecimal calculateTotal(String userId) {
List<CartItem> items = cartRepository.findByUserId(userId);
BigDecimal total = BigDecimal.ZERO;
for (CartItem item : items) {
Product product = productRepository.findById(item.getProductId()).orElseThrow();
total = total.add(product.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())));
}
return total;
}
// ... 其他方法
}
这里有个细节:加购时只校验库存,不扣库存。 库存真正扣减是在下单的时候。购物车里的东西随时可能删改,提前扣库存会导致别人买不了。
四、订单模块:整个系统的核心
订单是电商最复杂的模块,因为它串联了商品、库存、购物车、优惠券,还要管理状态流转。
订单状态机
plaintext
9
1
2
3
4
待支付(PENDING) → 已支付(PAID) → 已发货(SHIPPED) → 已签收(DELIVERED)
↓ ↓ ↓
已取消(CANCELLED) 退款中(REFUNDING) → 已退款(REFUNDED)
java
99
1
2
3
4
5
6
7
8
9
10
public enum OrderStatus {
PENDING, // 待支付
PAID, // 已支付
SHIPPED, // 已发货
DELIVERED, // 已签收
CANCELLED, // 已取消
REFUNDING, // 退款中
REFUNDED // 已退款
}
数据模型
java
99
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Order {
private String id;
private String userId;
private List<OrderItem> items; // 订单包含多个商品
private BigDecimal totalAmount; // 商品总金额
private BigDecimal discountAmount; // 优惠金额
private BigDecimal payAmount; // 实付金额 = totalAmount - discountAmount
private OrderStatus status;
private String couponId; // 使用的优惠券
private String address; // 收货地址
private LocalDateTime createTime;
private LocalDateTime payTime;
private LocalDateTime shipTime;
private LocalDateTime deliverTime;
}
public class OrderItem {
private String id;
private String orderId;
private String productId;
private String productName; // 冗余存储,防止商品改名
private BigDecimal price; // 下单时价格快照
private int quantity;
private BigDecimal subtotal; // = price × quantity
}
两个关键设计:
- OrderItem冗余存储了productName和price → 商品可能改价改名,但订单里的价格和名称是下单时刻的快照,不能变
- 分了totalAmount、discountAmount、payAmount → 金额拆分清楚,对账方便
OrderService:核心业务
java
99
1
2
3
4
5
6
7
8
9
10
11
public interface OrderService {
Order createOrder(String userId, String address, String couponId);
Order payOrder(String orderId);
Order shipOrder(String orderId);
Order deliverOrder(String orderId);
Order cancelOrder(String orderId);
Order refundOrder(String orderId);
Order getOrder(String orderId);
List<Order> getUserOrders(String userId);
}
下单:最复杂的一个方法
java
99
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
@Service
public class OrderServiceImpl implements OrderService {
@Autowired private OrderRepository orderRepository;
@Autowired private CartRepository cartRepository;
@Autowired private ProductRepository productRepository;
@Autowired private CouponService couponService;
@Override
@Transactional
public Order createOrder(String userId, String address, String couponId) {
// === 第1步:从购物车获取商品 ===
List<CartItem> cartItems = cartRepository.findByUserId(userId);
if (cartItems.isEmpty()) {
throw new BusinessException("购物车为空,无法下单");
}
// === 第2步:校验并扣减库存(遍历每个商品) ===
List<OrderItem> orderItems = new ArrayList<>();
BigDecimal totalAmount = BigDecimal.ZERO;
for (CartItem cartItem : cartItems) {
Product product = productRepository.findById(cartItem.getProductId())
.orElseThrow(() -> new BusinessException("商品[" + cartItem.getProductId() + "]不存在"));
// 再次校验库存(购物车加入时校验过,但下单时可能已经变了)
if (product.getStock() < cartItem.getQuantity()) {
throw new BusinessException("商品【" + product.getName() + "】库存不足,当前库存:" + product.getStock());
}
// 扣库存
product.setStock(product.getStock() - cartItem.getQuantity());
product.setSold(product.getSold() + cartItem.getQuantity());
productRepository.save(product);
// 构建订单项(价格快照!)
OrderItem orderItem = new OrderItem();
orderItem.setId(UUID.randomUUID().toString());
orderItem.setProductId(product.getId());
orderItem.setProductName(product.getName()); // 冗余存储
orderItem.setPrice(product.getPrice()); // 价格快照
orderItem.setQuantity(cartItem.getQuantity());
orderItem.setSubtotal(product.getPrice().multiply(BigDecimal.valueOf(cartItem.getQuantity())));
orderItems.add(orderItem);
totalAmount = totalAmount.add(orderItem.getSubtotal());
}
// === 第3步:计算优惠 ===
BigDecimal discountAmount = BigDecimal.ZERO;
if (couponId != null && !couponId.isEmpty()) {
discountAmount = couponService.calculateDiscount(couponId, totalAmount);
}
BigDecimal payAmount = totalAmount.subtract(discountAmount);
// === 第4步:创建订单 ===
Order order = new Order();
order.setId(generateOrderNo()); // 订单号,不是UUID
order.setUserId(userId);
order.setItems(orderItems);
order.setTotalAmount(totalAmount);
order.setDiscountAmount(discountAmount);
order.setPayAmount(payAmount);
order.setCouponId(couponId);
order.setAddress(address);
order.setStatus(OrderStatus.PENDING);
order.setCreateTime(LocalDateTime.now());
orderRepository.save(order);
// === 第5步:清空购物车 ===
cartRepository.deleteByUserId(userId);
// === 第6步:核销优惠券(如果有) ===
if (couponId != null && !couponId.isEmpty()) {
couponService.useCoupon(couponId, order.getId());
}
return order;
}
/**
* 生成订单号:年月日时分秒 + 4位随机数
* 订单号要有业务含义,UUID看不出顺序
*/
private String generateOrderNo() {
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
String random = String.format("%04d", new Random().nextInt(10000));
return "ORD" + timestamp + random;
}
// ... 其他方法见下文
}
下单方法的5个步骤,缺一不可,顺序也不能乱:
- 取购物车 → 2. 校验+扣库存 → 3. 算优惠 → 4. 创建订单 → 5. 清购物车+核销优惠券
整个方法加了 @Transactional,任何一步失败全部回滚——库存不扣、订单不建、购物车不清。
状态流转:每次操作都要校验"当前能不能做"
java
99
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Override
@Transactional
public Order payOrder(String orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new BusinessException("订单不存在"));
// 只有"待支付"的订单才能付款
if (order.getStatus() != OrderStatus.PENDING) {
throw new BusinessException("订单状态异常,无法支付,当前状态:" + order.getStatus());
}
order.setStatus(OrderStatus.PAID);
order.setPayTime(LocalDateTime.now());
return orderRepository.save(order);
}
@Override
@Transactional
public Order shipOrder(String orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new BusinessException("订单不存在"));
if (order.getStatus() != OrderStatus.PAID) {
throw new BusinessException("只有已支付订单才能发货");
}
order.setStatus(OrderStatus.SHIPPED);
order.setShipTime(LocalDateTime.now());
return orderRepository.save(order);
}
@Override
@Transactional
public Order deliverOrder(String orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new BusinessException("订单不存在"));
if (order.getStatus() != OrderStatus.SHIPPED) {
throw new BusinessException("只有已发货订单才能确认签收");
}
order.setStatus(OrderStatus.DELIVERED);
order.setDeliverTime(LocalDateTime.now());
return orderRepository.save(order);
}
取消和退款:要还库存
java
99
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@Override
@Transactional
public Order cancelOrder(String orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new BusinessException("订单不存在"));
// 已发货/已签收不能取消,要走退款
if (order.getStatus() == OrderStatus.SHIPPED || order.getStatus() == OrderStatus.DELIVERED) {
throw new BusinessException("订单已发货,请申请退款");
}
if (order.getStatus() == OrderStatus.CANCELLED) {
throw new BusinessException("订单已取消");
}
// 归还库存
restoreStock(order);
// 如果用了优惠券,退回
if (order.getCouponId() != null) {
couponService.refundCoupon(order.getCouponId());
}
order.setStatus(OrderStatus.CANCELLED);
return orderRepository.save(order);
}
@Override
@Transactional
public Order refundOrder(String orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new BusinessException("订单不存在"));
if (order.getStatus() != OrderStatus.PAID && order.getStatus() != OrderStatus.DELIVERED) {
throw new BusinessException("当前状态不支持退款");
}
// 归还库存
restoreStock(order);
// 退回优惠券
if (order.getCouponId() != null) {
couponService.refundCoupon(order.getCouponId());
}
order.setStatus(OrderStatus.REFUNDED);
return orderRepository.save(order);
}
/**
* 归还库存的公共方法
*/
private void restoreStock(Order order) {
for (OrderItem item : order.getItems()) {
Product product = productRepository.findById(item.getProductId()).orElseThrow();
product.setStock(product.getStock() + item.getQuantity());
product.setSold(product.getSold() - item.getQuantity());
productRepository.save(product);
}
}
取消和退款都有"还库存+退优惠券"的逻辑,所以抽成 restoreStock 私有方法复用。 这就是Service内部方法抽取的典型场景。
五、优惠券模块:让业务更有趣
优惠券是电商里最"花"的模块,规则多、计算复杂,特别适合练Service层设计。
数据模型
java
99
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Coupon {
private String id;
private String userId;
private CouponType type; // 类型
private BigDecimal threshold; // 使用门槛
private BigDecimal value; // 优惠值
private boolean used; // 是否已使用
private String usedOrderId; // 使用的订单号
private LocalDateTime expireTime; // 过期时间
}
public enum CouponType {
FIXED, // 满减:满threshold减value元
PERCENT, // 折扣:满threshold打value折(value=8表示8折)
SHIPPING // 包邮:无门槛免运费
}
Service
java
99
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@Service
public class CouponServiceImpl implements CouponService {
@Autowired
private CouponRepository couponRepository;
@Override
@Transactional(readOnly = true)
public BigDecimal calculateDiscount(String couponId, BigDecimal orderAmount) {
Coupon coupon = couponRepository.findById(couponId)
.orElseThrow(() -> new BusinessException("优惠券不存在"));
// 校验是否可用
if (coupon.isUsed()) {
throw new BusinessException("优惠券已使用");
}
if (coupon.getExpireTime().isBefore(LocalDateTime.now())) {
throw new BusinessException("优惠券已过期");
}
if (orderAmount.compareTo(coupon.getThreshold()) < 0) {
throw new BusinessException("未达使用门槛,需满" + coupon.getThreshold() + "元");
}
// 按类型计算优惠金额
switch (coupon.getType()) {
case FIXED:
return coupon.getValue();
case PERCENT:
BigDecimal discount = orderAmount.multiply(
BigDecimal.ONE.subtract(coupon.getValue().divide(BigDecimal.TEN))
);
return discount.setScale(2, RoundingMode.HALF_UP);
case SHIPPING:
return BigDecimal.TEN; // 假设运费10元
default:
return BigDecimal.ZERO;
}
}
@Override
@Transactional
public void useCoupon(String couponId, String orderId) {
Coupon coupon = couponRepository.findById(couponId)
.orElseThrow(() -> new BusinessException("优惠券不存在"));
coupon.setUsed(true);
coupon.setUsedOrderId(orderId);
couponRepository.save(coupon);
}
@Override
@Transactional
public void refundCoupon(String couponId) {
Coupon coupon = couponRepository.findById(couponId)
.orElseThrow(() -> new BusinessException("优惠券不存在"));
coupon.setUsed(false);
coupon.setUsedOrderId(null);
couponRepository.save(coupon);
}
}
六、全局异常处理:让错误也体面
业务校验抛了各种 BusinessException,如果不管它,用户会看到Spring默认的错误页——一堆堆栈信息。加一个全局异常处理器:
java
99
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<Map<String, Object>> handleBusiness(BusinessException e) {
Map<String, Object> body = new HashMap<>();
body.put("success", false);
body.put("message", e.getMessage());
body.put("timestamp", LocalDateTime.now());
return ResponseEntity.badRequest().body(body);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleOther(Exception e) {
Map<String, Object> body = new HashMap<>();
body.put("success", false);
body.put("message", "服务器内部错误");
body.put("timestamp", LocalDateTime.now());
return ResponseEntity.internalServerError().body(body);
}
}
java
9
1
2
3
4
5
6
7
// 自定义业务异常
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
}
BusinessException对用户可见,抛的是业务提示;其他异常统一返回"服务器内部错误",不暴露细节。
七、完整的下单流程梳理
把整个流程串一遍,感受一下各模块是怎么协作的:
plaintext
99
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
用户点击"下单"
│
▼
CartService.getCart() ← 取购物车
│
▼
ProductService.updateStock() ← 校验+扣库存(循环每个商品)
│
▼
CouponService.calculateDiscount() ← 计算优惠
│
▼
OrderService.createOrder() ← 创建订单
│
├→ CartService.clearCart() ← 清空购物车
└→ CouponService.useCoupon() ← 核销优惠券
任何一个环节失败,@Transactional保证全部回滚。 这就是事务的威力。
八、API接口一览
表格
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/products?category=&keyword= | 商品列表 |
| GET | /api/products/{id} | 商品详情 |
| GET | /api/products/hot?limit=10 | 热销排行 |
| POST | /api/cart/add | 加入购物车 |
| PUT | /api/cart/{id}?quantity=3 | 修改数量 |
| DELETE | /api/cart/{id} | 移出购物车 |
| GET | /api/cart?userId=xxx | 查看购物车 |
| POST | /api/orders | 下单 |
| POST | /api/orders/{id}/pay | 支付 |
| POST | /api/orders/{id}/ship | 发货 |
| POST | /api/orders/{id}/deliver | 签收 |
| POST | /api/orders/{id}/cancel | 取消 |
| POST | /api/orders/{id}/refund | 退款 |
| GET | /api/coupons?userId=xxx | 我的优惠券 |
和AI应用的关系
电商系统的业务复杂度,和AI应用是同一个量级的:
表格
| 电商概念 | AI应用对应 |
|---|---|
| 订单状态机 | AI对话的状态管理(等待输入→处理中→完成/失败) |
| 库存扣减 | API调用额度扣减 |
| 优惠券计算 | Prompt模板的动态参数替换和价格计算 |
| 价格快照 | AI调用的模型版本和参数快照 |
| 退款还库存 | AI任务失败重试和资源回收 |
你在Service层练的业务设计能力,做AI应用时直接用。 AI只是换了一个"组件"——把支付接口换成大模型API,把库存换成调用额度,业务编排的思路完全一样。
思考题
- 如果两个用户同时买同一件商品(库存只剩1件),怎么保证不超卖?(提示:数据库乐观锁/悲观锁)
- 下单时扣库存,但用户一直不付款怎么办?(提示:超时自动取消,可以用定时任务检查)
这两个问题下一篇数据持久化会详细讲。下篇见!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)