电商项目实战:从零搭一个能跑的商城系统

系列专栏:从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可以是正数(补库存)也可以是负数(扣库存),一个方法搞定两个方向。 比起写 deductStockaddStock 两个方法,更简洁也更不容易漏。

三、购物车:临时态的业务逻辑

购物车是电商里最容易写乱的模块。它有个特点:数据是临时的,可能随时变,不一定最后都下单。

数据模型

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

}

两个关键设计:

  1. OrderItem冗余存储了productName和price → 商品可能改价改名,但订单里的价格和名称是下单时刻的快照,不能变
  2. 分了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个步骤,缺一不可,顺序也不能乱:

  1. 取购物车 → 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. 如果两个用户同时买同一件商品(库存只剩1件),怎么保证不超卖?(提示:数据库乐观锁/悲观锁)
  2. 下单时扣库存,但用户一直不付款怎么办?(提示:超时自动取消,可以用定时任务检查)

这两个问题下一篇数据持久化会详细讲。下篇见!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐