1. 项目概述
本文将带你实现一个轻量级电商系统。后端采用 Spring Boot 构建 RESTful 接口,前端使用 Angular 消费这些接口。功能聚焦于核心电商流程:
✅ 用户浏览商品
✅ 添加/移除购物车商品
✅ 提交订单
整个项目结构清晰,适合快速理解前后端协作模式。最终还会演示如何将前后端打包为单一应用部署。
2. 后端实现
使用 Spring Boot 3.1.5 + JPA + H2 内存数据库快速搭建服务端。H2 方便本地开发调试,无需额外安装数据库。
📌 提示:若想深入学习 Spring Boot 或 REST API 设计,可参考官方文档或社区经典教程。
2.1 Maven 依赖配置
核心依赖如下,通过 Spring Initializr 快速生成:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>3.1.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.1.5</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.1.214</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.9.6</version>
</dependency>
⚠️ 注意:jackson-datatype-jsr310
用于支持 Java 8 时间类型(如 LocalDate
)的 JSON 序列化。
2.2 数据库配置
开启 H2 控制台便于调试,并启用 SQL 日志输出:
spring.datasource.name=ecommercedb
spring.jpa.show-sql=true
# H2 控制台配置
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
启动后访问 http://localhost:8080/h2-console,使用以下参数登录:
- JDBC URL:
jdbc:h2:mem:ecommercedb
- Username:
sa
- Password: 留空
2.3 项目结构
标准 Maven + Spring Boot 分层结构:
├── pom.xml
├── src
│ ├── main
│ │ ├── frontend # Angular 源码
│ │ ├── java
│ │ │ └── com.baeldung.ecommerce
│ │ │ │ EcommerceApplication.java
│ │ │ ├── controller
│ │ │ ├── dto
│ │ │ ├── exception
│ │ │ ├── model
│ │ │ ├── repository
│ │ │ └── service
│ │ └── resources
│ │ │ application.properties
│ │ ├── static # Angular 构建产物存放目录
│ │ └── templates
│ └── test
│ └── java
│ └── com.baeldung.ecommerce
│ └── EcommerceApplicationIntegrationTest.java
✅ 所有 Repository 接口均继承自
CrudRepository
,省略展示。
2.4 全局异常处理
统一处理常见异常,提升 API 友好性:
@RestControllerAdvice
public class ApiExceptionHandler {
@SuppressWarnings("rawtypes")
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ErrorResponse> handle(ConstraintViolationException e) {
ErrorResponse errors = new ErrorResponse();
for (ConstraintViolation violation : e.getConstraintViolations()) {
ErrorItem error = new ErrorItem();
error.setCode(violation.getMessageTemplate());
error.setMessage(violation.getMessage());
errors.addError(error);
}
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
@SuppressWarnings("rawtypes")
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorItem> handle(ResourceNotFoundException e) {
ErrorItem error = new ErrorItem();
error.setMessage(e.getMessage());
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
}
✅ 捕获 JSR-303 参数校验异常
✅ 处理资源未找到(如商品 ID 不存在)
2.5 商品模块(Products)
商品仅支持查询,通过 CommandLineRunner
在启动时预置数据。
实体类
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull(message = "Product name is required.")
@Basic(optional = false)
private String name;
private Double price;
private String pictureUrl;
// 全参构造、getter/setter 省略
}
服务层
@Service
@Transactional
public class ProductServiceImpl implements ProductService {
private final ProductRepository productRepository;
public ProductServiceImpl(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Override
public Iterable<Product> getAllProducts() {
return productRepository.findAll();
}
@Override
public Product getProduct(long id) {
return productRepository
.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Product not found"));
}
@Override
public Product save(Product product) {
return productRepository.save(product);
}
}
控制器
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping(value = { "", "/" })
public @NotNull Iterable<Product> getProducts() {
return productService.getAllProducts();
}
}
初始化数据
@Bean
CommandLineRunner runner(ProductService productService) {
return args -> {
productService.save(new Product("Laptop", 999.99, "/images/laptop.jpg"));
productService.save(new Product("Mouse", 25.50, "/images/mouse.jpg"));
productService.save(new Product("Keyboard", 75.00, "/images/keyboard.jpg"));
};
}
启动后访问 http://localhost:8080/api/products 即可获取商品列表。
2.6 订单模块(Orders)
支持创建订单,包含多个商品及数量。
核心实体设计
⚠️ 踩坑提醒:Order
是 SQL 保留字,必须显式指定表名!
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@JsonFormat(pattern = "dd/MM/yyyy")
private LocalDate dateCreated;
private String status;
@JsonManagedReference
@OneToMany(mappedBy = "pk.order")
@Valid
private List<OrderProduct> orderProducts = new ArrayList<>();
@Transient
public Double getTotalOrderPrice() {
return orderProducts.stream()
.mapToDouble(OrderProduct::getTotalPrice)
.sum();
}
@Transient
public int getNumberOfProducts() {
return this.orderProducts.size();
}
// getter/setter 省略
}
订单项(OrderProduct)与复合主键
使用 @EmbeddedId
实现多对多关联:
@Entity
public class OrderProduct {
@EmbeddedId
@JsonIgnore
private OrderProductPK pk;
@Column(nullable = false)
private Integer quantity;
public OrderProduct(Order order, Product product, Integer quantity) {
pk = new OrderProductPK();
pk.setOrder(order);
pk.setProduct(product);
this.quantity = quantity;
}
@Transient
public Product getProduct() {
return this.pk.getProduct();
}
@Transient
public Double getTotalPrice() {
return getProduct().getPrice() * getQuantity();
}
// getter/setter、equals/hashCode 省略
}
@Embeddable
public class OrderProductPK implements Serializable {
@JsonBackReference
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "product_id")
private Product product;
// getter/setter、equals/hashCode 省略
}
✅
@JsonIgnore
避免序列化循环引用
✅@Transient
方法用于计算总价,不持久化
服务与控制器
@Service
@Transactional
public class OrderServiceImpl implements OrderService {
private final OrderRepository orderRepository;
public OrderServiceImpl(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Override
public Iterable<Order> getAllOrders() {
return orderRepository.findAll();
}
@Override
public Order create(Order order) {
order.setDateCreated(LocalDate.now());
return orderRepository.save(order);
}
@Override
public void update(Order order) {
orderRepository.save(order);
}
}
@PostMapping
public ResponseEntity<Order> create(@RequestBody OrderForm form) {
List<OrderProductDto> formDtos = form.getProductOrders();
validateProductsExistence(formDtos);
Order order = new Order();
order.setStatus("NEW");
List<OrderProduct> orderProducts = new ArrayList<>();
for (OrderProductDto dto : formDtos) {
Product product = productService.getProduct(dto.getProductId());
orderProducts.add(new OrderProduct(order, product, dto.getQuantity()));
}
order.setOrderProducts(orderProducts);
orderService.update(order);
String uri = ServletUriComponentsBuilder
.fromCurrentServletMapping()
.path("/orders/{id}")
.buildAndExpand(order.getId())
.toString();
HttpHeaders headers = new HttpHeaders();
headers.add("Location", uri);
return new ResponseEntity<>(order, headers, HttpStatus.CREATED);
}
✅ 接收 OrderForm
DTO
✅ 校验商品存在性
✅ 返回 201 Created
+ Location
头
3. 前端实现(Angular)
使用 Angular CLI 快速搭建前端工程,版本要求 ≥ 4.3(因使用 HttpClient
)。
3.1 初始化项目
cd src/main
ng new frontend
默认启动端口:http://localhost:4200
3.2 集成 Bootstrap
提升 UI 视觉效果:
npm install --save bootstrap
修改 angular.json
,在 styles
中添加:
"node_modules/bootstrap/dist/css/bootstrap.min.css"
3.3 组件与模型设计
系统界面预览:
创建组件
ng g c ecommerce
ng g c ecommerce/products
ng g c ecommerce/orders
ng g c ecommerce/shopping-cart
数据模型
export class Product {
id: number;
name: string;
price: number;
pictureUrl: string;
constructor(id: number, name: string, price: number, pictureUrl: string) {
this.id = id;
this.name = name;
this.price = price;
this.pictureUrl = pictureUrl;
}
}
export class ProductOrder {
product: Product;
quantity: number;
constructor(product: Product, quantity: number) {
this.product = product;
this.quantity = quantity;
}
}
export class ProductOrders {
productOrders: ProductOrder[] = [];
}
3.4 主组件(EcommerceComponent)
包含导航栏与子组件容器:
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
<div class="container">
<a class="navbar-brand" href="#">Baeldung Ecommerce</a>
<button class="navbar-toggler" type="button" data-toggle="collapse"
data-target="#navbarResponsive" aria-controls="navbarResponsive"
aria-expanded="false" aria-label="Toggle navigation"
(click)="toggleCollapsed()">
<span class="navbar-toggler-icon"></span>
</button>
<div id="navbarResponsive"
[ngClass]="{'collapse': collapsed, 'navbar-collapse': true}">
<ul class="navbar-nav ml-auto">
<li class="nav-item active">
<a class="nav-link" href="#" (click)="reset()">Home
<span class="sr-only">(current)</span>
</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="row">
<div class="col-md-9">
<app-products #productsC [hidden]="orderFinished"></app-products>
</div>
<div class="col-md-3">
<app-shopping-cart (onOrderFinished)="finishOrder($event)" #shoppingCartC
[hidden]="orderFinished"></app-shopping-cart>
</div>
<div class="col-md-6 offset-3">
<app-orders #ordersC [hidden]="!orderFinished"></app-orders>
</div>
</div>
CSS 补丁(避免 navbar 遮挡内容):
.container {
padding-top: 65px;
}
TS 逻辑(父组件控制子组件状态):
export class EcommerceComponent implements OnInit {
private collapsed = true;
orderFinished = false;
@ViewChild('productsC') productsC: ProductsComponent;
@ViewChild('shoppingCartC') shoppingCartC: ShoppingCartComponent;
@ViewChild('ordersC') ordersC: OrdersComponent;
toggleCollapsed(): void {
this.collapsed = !this.collapsed;
}
finishOrder(orderFinished: boolean) {
this.orderFinished = orderFinished;
}
reset() {
this.orderFinished = false;
this.productsC.reset();
this.shoppingCartC.reset();
this.ordersC.paid = false;
}
}
3.5 服务层(EcommerceService)
核心通信与状态管理:
@Injectable()
export class EcommerceService {
private productsUrl = "/api/products";
private ordersUrl = "/api/orders";
private productOrder: ProductOrder;
private orders: ProductOrders = new ProductOrders();
private total: number;
private productOrderSubject = new Subject();
private ordersSubject = new Subject();
private totalSubject = new Subject();
ProductOrderChanged = this.productOrderSubject.asObservable();
OrdersChanged = this.ordersSubject.asObservable();
TotalChanged = this.totalSubject.asObservable();
constructor(private http: HttpClient) {}
getAllProducts() {
return this.http.get(this.productsUrl);
}
saveOrder(order: ProductOrders) {
return this.http.post(this.ordersUrl, order);
}
// getter/setter 省略
}
跨域代理配置
避免硬编码后端地址,创建 proxy-conf.json
:
{
"/api": {
"target": "http://localhost:8080",
"secure": false
}
}
修改 package.json
:
"scripts": {
"start": "ng serve --proxy-config proxy-conf.json",
"build": "ng build"
}
✅ 启动命令:npm start
(而非 ng serve
)
3.6 商品组件(ProductsComponent)
加载商品并支持加入购物车:
export class ProductsComponent implements OnInit {
productOrders: ProductOrder[] = [];
products: Product[] = [];
selectedProductOrder: ProductOrder;
private shoppingCartOrders: ProductOrders;
sub: Subscription;
productSelected: boolean = false;
constructor(private ecommerceService: EcommerceService) {}
ngOnInit() {
this.loadProducts();
this.loadOrders();
}
loadProducts() {
this.ecommerceService.getAllProducts()
.subscribe((products: any[]) => {
this.products = products;
this.products.forEach(product => {
this.productOrders.push(new ProductOrder(product, 0));
});
});
}
loadOrders() {
this.sub = this.ecommerceService.OrdersChanged.subscribe(() => {
this.shoppingCartOrders = this.ecommerceService.ProductOrders;
});
}
addToCart(order: ProductOrder) {
this.ecommerceService.SelectedProductOrder = order;
this.selectedProductOrder = this.ecommerceService.SelectedProductOrder;
this.productSelected = true;
}
removeFromCart(productOrder: ProductOrder) {
const index = this.getProductIndex(productOrder.product);
if (index > -1) {
this.shoppingCartOrders.productOrders.splice(index, 1);
}
this.ecommerceService.ProductOrders = this.shoppingCartOrders;
this.productSelected = false;
}
reset() {
this.productOrders = [];
this.loadProducts();
this.ecommerceService.ProductOrders.productOrders = [];
this.loadOrders();
this.productSelected = false;
}
}
3.7 购物车组件(ShoppingCartComponent)
监听商品变更,实时计算总价:
export class ShoppingCartComponent implements OnInit, OnDestroy {
orderFinished: boolean;
orders: ProductOrders;
total: number;
sub: Subscription;
@Output() onOrderFinished = new EventEmitter<boolean>();
constructor(private ecommerceService: EcommerceService) {
this.total = 0;
this.orderFinished = false;
}
ngOnInit() {
this.orders = new ProductOrders();
this.loadCart();
this.loadTotal();
}
loadTotal() {
this.sub = this.ecommerceService.OrdersChanged.subscribe(() => {
this.total = this.calculateTotal(this.orders.productOrders);
});
}
loadCart() {
this.sub = this.ecommerceService.ProductOrderChanged.subscribe(() => {
const productOrder = this.ecommerceService.SelectedProductOrder;
if (productOrder && productOrder.quantity > 0) {
this.orders.productOrders.push(new ProductOrder(
productOrder.product, productOrder.quantity));
}
this.ecommerceService.ProductOrders = this.orders;
this.total = this.calculateTotal(this.orders.productOrders);
});
}
ngOnDestroy() {
this.sub.unsubscribe();
}
finishOrder() {
this.orderFinished = true;
this.ecommerceService.Total = this.total;
this.onOrderFinished.emit(this.orderFinished);
}
reset() {
this.orderFinished = false;
this.orders = new ProductOrders();
this.total = 0;
}
}
3.8 订单组件(OrdersComponent)
模拟支付流程,提交订单至后端:
export class OrdersComponent implements OnInit {
orders: ProductOrders;
total: number;
paid: boolean = false;
sub: Subscription;
constructor(private ecommerceService: EcommerceService) {
this.orders = this.ecommerceService.ProductOrders;
this.total = this.ecommerceService.Total;
}
ngOnInit() {
this.sub = this.ecommerceService.OrdersChanged.subscribe(() => {
this.orders = this.ecommerceService.ProductOrders;
this.total = this.ecommerceService.Total;
});
}
pay() {
this.paid = true;
this.ecommerceService.saveOrder(this.orders).subscribe();
}
}
模板展示订单详情与支付按钮:
<h2 class="text-center">ORDER</h2>
<ul>
<li *ngFor="let order of orders.productOrders">
{{ order.product.name }} - ${{ order.product.price }} x {{ order.quantity}} pcs.
</li>
</ul>
<h3 class="text-right">Total amount: ${{ total }}</h3>
<button class="btn btn-primary btn-block" (click)="pay()" *ngIf="!paid">Pay</button>
<div class="alert alert-success" role="alert" *ngIf="paid">
<strong>Congratulations!</strong> You successfully made the order.
</div>
4. 前后端合并部署
开发阶段前后端分离更高效,但生产环境建议打包为单一应用。
自动化构建脚本
修改 package.json
,添加构建后处理脚本:
"scripts": {
"build": "ng build",
"postbuild": "npm run deploy",
"predeploy": "rimraf ../resources/static/ && mkdirp ../resources/static",
"deploy": "copyfiles -f dist/frontend/** ../resources/static"
}
安装所需工具:
npm install --save-dev rimraf mkdirp copyfiles
打包流程
- 执行
npm run build
(Angular 构建 + 自动拷贝到static
) - 启动 Spring Boot 应用(
mvn spring-boot:run
) - 访问 http://localhost:8080 即可使用完整应用
✅ 静态资源由 Spring Boot 托管
✅ 前后端共用同一端口,部署简化
5. 总结
本文实现了一个功能完整、结构清晰的简易电商系统:
- ✅ 后端:Spring Boot + JPA + H2,RESTful 接口设计
- ✅ 前端:Angular 组件通信、状态管理、HTTP 交互
- ✅ 工程实践:前后端分离开发 + 一体化部署
项目代码已开源,可作为学习模板快速上手: