电商项目实战:从零搭一个能跑的商城系统 电商项目实战从零搭一个能跑的商城系统系列专栏从Java到AI应用开发| 第4篇写在前面前三篇我们学了API搭建、数据模型、Service层设计知识点都是零散的。今天来干一票大的——把这些全串起来搭一个完整的电商系统。不是Demo级别的增删改查而是真正能跑的业务商品管理、购物车、下单、支付、发货、退款整个订单生命周期。每个环节都有真实的业务规则不是存进去就行。一、项目结构先想清楚别上来就写代码。先想清楚这个商城有哪些模块、每个模块干什么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数据模型public class Product {private String id; // 商品IDprivate 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商品不只是增删改查javapublic interface ProductService {Product createProduct(Product product);Product updateProduct(String id, Product product);Product getProduct(String id);ListProduct listProducts(String category, String keyword);void updateStock(String productId, int quantity); // 扣/补库存ListProduct getHotProducts(int limit); // 热销排行}Servicepublic class ProductServiceImpl implements ProductService {Autowiredprivate ProductRepository productRepository;Overridepublic Product createProduct(Product product) {product.setId(UUID.randomUUID().toString());product.setSold(0);product.setCreateTime(LocalDateTime.now());product.setUpdateTime(LocalDateTime.now());return productRepository.save(product);}OverrideTransactionalpublic 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);}OverrideTransactional(readOnly true)public ListProduct getHotProducts(int limit) {return productRepository.findAll().stream().sorted(Comparator.comparingInt(Product::getSold).reversed()).limit(limit).collect(Collectors.toList());}// ... 其他方法}注意updateStock的设计quantity可以是正数补库存也可以是负数扣库存一个方法搞定两个方向。比起写deductStock和addStock两个方法更简洁也更不容易漏。三、购物车临时态的业务逻辑购物车是电商里最容易写乱的模块。它有个特点数据是临时的可能随时变不一定最后都下单。数据模型public class CartItem {private String id;private String userId;private String productId;private int quantity;private LocalDateTime addTime;}Service购物车的三个关键操作public interface CartService {CartItem addToCart(String userId, String productId, int quantity);CartItem updateQuantity(String cartItemId, int quantity);void removeFromCart(String cartItemId);ListCartItem getCart(String userId);BigDecimal calculateTotal(String userId); // 计算购物车总价void clearCart(String userId); // 下单后清空}Servicepublic class CartServiceImpl implements CartService {Autowiredprivate CartRepository cartRepository;Autowiredprivate ProductRepository productRepository;Overridepublic 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. 如果已在购物车里叠加数量OptionalCartItem 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);}OverrideTransactional(readOnly true)public BigDecimal calculateTotal(String userId) {ListCartItem 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;}// ... 其他方法}这里有个细节加购时只校验库存不扣库存。库存真正扣减是在下单的时候。购物车里的东西随时可能删改提前扣库存会导致别人买不了。四、订单模块整个系统的核心订单是电商最复杂的模块因为它串联了商品、库存、购物车、优惠券还要管理状态流转。订单状态机待支付(PENDING) → 已支付(PAID) → 已发货(SHIPPED) → 已签收(DELIVERED)↓ ↓ ↓已取消(CANCELLED) 退款中(REFUNDING) → 已退款(REFUNDED)public enum OrderStatus {PENDING, // 待支付PAID, // 已支付SHIPPED, // 已发货DELIVERED, // 已签收CANCELLED, // 已取消REFUNDING, // 退款中REFUNDED // 已退款}数据模型public class Order {private String id;private String userId;private ListOrderItem items; // 订单包含多个商品private BigDecimal totalAmount; // 商品总金额private BigDecimal discountAmount; // 优惠金额private BigDecimal payAmount; // 实付金额 totalAmount - discountAmountprivate 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核心业务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);ListOrder getUserOrders(String userId);}下单最复杂的一个方法Servicepublic class OrderServiceImpl implements OrderService {Autowired private OrderRepository orderRepository;Autowired private CartRepository cartRepository;Autowired private ProductRepository productRepository;Autowired private CouponService couponService;OverrideTransactionalpublic Order createOrder(String userId, String address, String couponId) {// 第1步从购物车获取商品 ListCartItem cartItems cartRepository.findByUserId(userId);if (cartItems.isEmpty()) {throw new BusinessException(购物车为空无法下单);}// 第2步校验并扣减库存遍历每个商品 ListOrderItem 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()); // 订单号不是UUIDorder.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任何一步失败全部回滚——库存不扣、订单不建、购物车不清。状态流转每次操作都要校验当前能不能做OverrideTransactionalpublic 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);}OverrideTransactionalpublic 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);}OverrideTransactionalpublic 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);}取消和退款要还库存OverrideTransactionalpublic 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);}OverrideTransactionalpublic 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层设计。数据模型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折value8表示8折SHIPPING // 包邮无门槛免运费}ServiceServicepublic class CouponServiceImpl implements CouponService {Autowiredprivate CouponRepository couponRepository;OverrideTransactional(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;}}OverrideTransactionalpublic void useCoupon(String couponId, String orderId) {Coupon coupon couponRepository.findById(couponId).orElseThrow(() - new BusinessException(优惠券不存在));coupon.setUsed(true);coupon.setUsedOrderId(orderId);couponRepository.save(coupon);}OverrideTransactionalpublic void refundCoupon(String couponId) {Coupon coupon couponRepository.findById(couponId).orElseThrow(() - new BusinessException(优惠券不存在));coupon.setUsed(false);coupon.setUsedOrderId(null);couponRepository.save(coupon);}}六、全局异常处理让错误也体面业务校验抛了各种BusinessException如果不管它用户会看到Spring默认的错误页——一堆堆栈信息。加一个全局异常处理器RestControllerAdvicepublic class GlobalExceptionHandler {ExceptionHandler(BusinessException.class)public ResponseEntityMapString, Object handleBusiness(BusinessException e) {MapString, 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 ResponseEntityMapString, Object handleOther(Exception e) {MapString, Object body new HashMap();body.put(success, false);body.put(message, 服务器内部错误);body.put(timestamp, LocalDateTime.now());return ResponseEntity.internalServerError().body(body);}}// 自定义业务异常public class BusinessException extends RuntimeException {public BusinessException(String message) {super(message);}}BusinessException对用户可见抛的是业务提示其他异常统一返回服务器内部错误不暴露细节。七、完整的下单流程梳理把整个流程串一遍感受一下各模块是怎么协作的plaintext用户点击下单│▼CartService.getCart() ← 取购物车│▼ProductService.updateStock() ← 校验扣库存循环每个商品│▼CouponService.calculateDiscount() ← 计算优惠│▼OrderService.createOrder() ← 创建订单│├→ CartService.clearCart() ← 清空购物车└→ CouponService.useCoupon() ← 核销优惠券任何一个环节失败Transactional保证全部回滚。这就是事务的威力。八、API接口一览表格方法路径说明GET/api/products?categorykeyword商品列表GET/api/products/{id}商品详情GET/api/products/hot?limit10热销排行POST/api/cart/add加入购物车PUT/api/cart/{id}?quantity3修改数量DELETE/api/cart/{id}移出购物车GET/api/cart?userIdxxx查看购物车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?userIdxxx我的优惠券和AI应用的关系电商系统的业务复杂度和AI应用是同一个量级的表格电商概念AI应用对应订单状态机AI对话的状态管理等待输入→处理中→完成/失败库存扣减API调用额度扣减优惠券计算Prompt模板的动态参数替换和价格计算价格快照AI调用的模型版本和参数快照退款还库存AI任务失败重试和资源回收你在Service层练的业务设计能力做AI应用时直接用。AI只是换了一个组件——把支付接口换成大模型API把库存换成调用额度业务编排的思路完全一样。思考题如果两个用户同时买同一件商品库存只剩1件怎么保证不超卖提示数据库乐观锁/悲观锁下单时扣库存但用户一直不付款怎么办提示超时自动取消可以用定时任务检查这两个问题下一篇数据持久化会详细讲。下篇见