知識庫 / Spring / Spring Boot RSS 訂閱

基於 Spring 的簡單電商實現

Spring Boot
HongKong
16
01:43 PM · Dec 06 ,2025

1. 我們的電子商務應用程序概述

在本教程中,我們將實現一個簡單的電子商務應用程序。我們將使用 Spring Boot 開發一個 API,以及使用 Angular 消費該 API 的客户端應用程序。

基本上,用户將能夠將產品從產品列表中添加到/從購物車中,並進行訂單處理。

2. 後端部分

為了開發 API,我們將使用 Spring Boot 的最新版本。我們還使用 JPA 和 H2 數據庫來處理持久化方面的工作。

要了解更多關於 Spring Boot 的信息,您可以查看我們的 Spring Boot 系列文章,如果您想熟悉如何構建 REST API,請查看另一系列文章

2.1. Maven 依賴

讓我們準備好我們的項目並導入所需的依賴項到我們的 <em >pom.xml</em> 中。

我們需要一些核心 Spring Boot 依賴項

<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>

然後,H2 數據庫:

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>2.1.214</version>
    <scope>runtime</scope>
</dependency>

最後 – Jackson 庫:

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>2.9.6</version>
</dependency>

我們使用 Spring Initializr 快速設置項目並添加所需的依賴項。

2.2. 數據庫設置

雖然我們可以直接使用 Spring Boot 內置的內存 H2 數據庫,但在開始開發 API 之前,我們仍然會進行一些調整。

我們將啓用 H2 控制枱,在我們的 application.properties 文件中,以便我們能夠實際檢查數據庫狀態並確保一切按預期進行。

此外,在開發過程中記錄 SQL 查詢到控制枱也可能很有用:

spring.datasource.name=ecommercedb
spring.jpa.show-sql=true

#H2 settings
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

在添加這些設置後,我們就可以通過 http://localhost:8080/h2-console 訪問數據庫,使用 jdbc:h2:mem:ecommercedb 作為 JDBC URL,以及用户 sa,沒有密碼。

2.3. 項目結構

項目將被組織成若干個標準包,Angular 應用位於前端文件夾中:

├───pom.xml            
├───src
    ├───main
    │   ├───frontend
    │   ├───java
    │   │   └───com
    │   │       └───baeldung
    │   │           └───ecommerce
    │   │               │   EcommerceApplication.java
    │   │               ├───controller 
    │   │               ├───dto  
    │   │               ├───exception
    │   │               ├───model
    │   │               ├───repository
    │   │               └───service
    │   │                       
    │   └───resources
    │       │   application.properties
    │       ├───static
    │       └───templates
    └───test
        └───java
            └───com
                └───baeldung
                    └───ecommerce
                            EcommerceApplicationIntegrationTest.java

我們應指出,倉庫包中的所有接口都簡單且繼承了 Spring Data 的 CrudRepository,因此我們此處省略不顯示它們。

2.4 異常處理

我們需要為我們的 API 添加異常處理程序,以便正確處理可能發生的異常。

您可以在我們的“REST 錯誤處理”和“REST API 自定義錯誤消息處理”文章中找到更多關於該主題的詳細信息。

在這裏,我們重點關注 ConstraintViolationException 和我們的自定義 ResourceNotFoundException

@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);
    }
}

2.5. 產品

如果您需要更多關於 Spring 中持久化的知識,Spring Persistence 系列中有大量有用的文章。

我們的應用程序將僅從數據庫中讀取產品信息,因此我們需要添加一些初始產品。

讓我們創建一個簡單的 類:

@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;

    // all arguments contructor 
    // standard getters and setters
}

儘管用户無法通過應用程序添加產品,但我們將支持將產品保存到數據庫以預填充產品列表。

一個簡單的服務就足以滿足我們的需求:

@Service
@Transactional
public class ProductServiceImpl implements ProductService {

    // productRepository constructor injection

    @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 {

    // productService constructor injection

    @GetMapping(value = { "", "/" })
    public @NotNull Iterable<Product> getProducts() {
        return productService.getAllProducts();
    }
}

現在我們需要做的事情是向用户暴露產品列表,只需要在數據庫中實際添加一些產品。因此,我們將使用 CommandLineRunner 類來在我們的主應用程序類中創建一個 Bean

這樣,應用程序啓動時,我們將向數據庫中插入產品:

@Bean
CommandLineRunner runner(ProductService productService) {
    return args -> {
        productService.save(...);
        // more products
}

現在啓動我們的應用程序,我們可以通過以下 URL 獲取產品列表:http://localhost:8080/api/products

此外,如果我們在 http://localhost:8080/h2-console 登錄後,將會看到名為 PRODUCT 的表,其中包含我們剛剛添加的產品。

2.6. 訂單

在API端,我們需要啓用POST請求以保存用户將要創建的訂單。

讓我們首先創建模型:

@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() {
        double sum = 0D;
        List<OrderProduct> orderProducts = getOrderProducts();
        for (OrderProduct op : orderProducts) {
            sum += op.getTotalPrice();
        }
        return sum;
    }

    @Transient
    public int getNumberOfProducts() {
        return this.orderProducts.size();
    }

    // standard getters and setters
}

我們應該注意幾點。其中最值得注意的是記住更改我們表默認名稱。由於我們將類名命名為Order,默認情況下名為ORDER的表將被創建。但是,由於這是一種保留的SQL關鍵字,因此我們添加了@Table(name = “orders”)以避免衝突。

此外,我們有兩個@Transient方法,它們將返回該訂單的總金額及其包含的產品數量。這兩個都代表計算數據,因此無需將其存儲在數據庫中。

最後,我們有一個@OneToMany關係,表示訂單的詳細信息。為此,我們需要另一個實體類:

@Entity
public class OrderProduct {

    @EmbeddedId
    @JsonIgnore
    private OrderProductPK pk;

    @Column(nullable = false)
	private Integer quantity;

    // default constructor

    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();
    }

    // standard getters and setters

    // hashcode() and equals() methods
}

我們有一個複合主鍵在這裏:

@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;

    // standard getters and setters

    // hashcode() and equals() methods
}

這些類都不復雜,但需要注意的是,在 OrderProduct 類中,我們使用了 @JsonIgnore 註解,應用於主鍵。這是因為我們不想將 Order 部分序列化到主鍵中,因為這會造成冗餘。

我們只需要顯示 Product 信息給用户,所以我們有 transient 的 getProduct() 方法。

接下來我們需要一個簡單的服務實現:

@Service
@Transactional
public class OrderServiceImpl implements OrderService {

    // orderRepository constructor injection

    @Override
    public Iterable<Order> getAllOrders() {
        return this.orderRepository.findAll();
    }
	
    @Override
    public Order create(Order order) {
        order.setDateCreated(LocalDate.now());
        return this.orderRepository.save(order);
    }

    @Override
    public void update(Order order) {
        this.orderRepository.save(order);
    }
}

並且有一個控制器映射到 /api/orders,用於處理 Order 請求。

最重要的是 create() 方法:

@PostMapping
public ResponseEntity<Order> create(@RequestBody OrderForm form) {
    List<OrderProductDto> formDtos = form.getProductOrders();
    validateProductsExistence(formDtos);
    // create order logic
    // populate order with products

    order.setOrderProducts(orderProducts);
    this.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);
}

首先,我們接受一個包含產品及其對應數量的列表。隨後,我們檢查所有產品是否存在於數據庫中,然後創建並保存一個新的訂單。我們保留對新創建對象的引用,以便向其添加訂單詳情。

最後,我們創建“Location”頭部。

詳細實現請參考倉庫 – 鏈接位於本文末尾。

3. 前端

現在我們已經構建好 Spring Boot 應用程序,是時候處理項目的 Angular 部分 了。為此,首先我們需要安裝 Node.js 及其 NPM,之後再安裝 Angular CLI,這是一個用於 Angular 的命令行界面。

安裝這兩種工具非常簡單,正如官方文檔中所展示的。

3.1. 設置 Angular 項目

正如我們所提到的,我們將使用 Angular CLI 創建我們的應用程序。為了保持簡潔並集中管理,我們將 Angular 應用程序放在 /src/main/frontend 文件夾中。

要創建它,我們需要在 /src/main 文件夾中打開終端(或命令提示符),然後運行:

ng new frontend

這將創建我們 Angular 應用所需的所有文件和文件夾。在 package.json 文件中,我們可以查看我們已安裝的依賴項的版本。本教程基於 Angular v6.0.3,但較舊的版本也應該可以工作,至少包括 Angular 4.3 及更高版本 (HttpClient,我們在此處使用的版本,在 Angular 4.3 中引入)。

我們應該注意的是,我們將從 /frontend 文件夾運行所有命令,除非另有説明。

這個配置足以通過運行 ng serve 命令啓動 Angular 應用。默認情況下,它將在 http://localhost:4200 上運行,並且如果我們現在訪問該地址,我們將看到基礎 Angular 應用加載。

3.2. 添加 Bootstrap

在開始創建我們自己的組件之前,我們首先需要將 Bootstrap 添加到我們的項目中,以便使我們的頁面看起來更美觀。

我們需要做的事情很少。 首先,我們需要 運行一個命令來安裝它:

npm install --save bootstrap

並且然後告訴 Angular 實際使用它。為此,我們需要打開一個文件 src/main/frontend/angular.json,並在 “styles” 屬性下添加 node_modules/bootstrap/dist/css/bootstrap.min.css。 這樣就完成了。

3.3 組件與模型

在開始為我們的應用程序創建組件之前,我們首先應該瞭解應用程序的實際外觀:

現在,我們將創建一個基礎組件,名為 ecommerce

ng g c ecommerce

這將創建我們的組件位於 /frontend/src/app 文件夾內。 為了在應用程序啓動時加載它,我們將 將其包含 app.component.html

<div class="container">
    <app-ecommerce></app-ecommerce>
</div>

接下來,我們將在這個基礎組件內部創建其他組件:

ng g c /ecommerce/products
ng g c /ecommerce/orders
ng g c /ecommerce/shopping-cart

當然,如果我們更喜歡手動創建所有這些文件夾和文件,我們也可以這樣做。但如果是這樣,我們必須記得在我們的 AppModule 中註冊這些組件

我們還需要一些模型來方便地操作我們的數據:

export class Product {
    id: number;
    name: string;
    price: number;
    pictureUrl: string;

    // all arguments constructor
}
export class ProductOrder {
    product: Product;
    quantity: number;

    // all arguments constructor
}
export class ProductOrders {
    productOrders: ProductOrder[] = [];
}

最後提到的模型與我們的OrderForm在後端匹配。

3.4. 基礎組件

在我們的 電子商務 組件的頂部,我們將添加一個帶有“主頁”鏈接的導航欄,該鏈接位於右側:

<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>

我們應該牢記,為了查看組件中的內容,由於我們使用了 navbar 類,我們需要在 app.component.css 中添加一些 CSS:

.container {
    padding-top: 65px;
}

讓我們在註釋最重要的部分之前,先查看一下 .ts 文件:

@Component({
    selector: 'app-ecommerce',
    templateUrl: './ecommerce.component.html',
    styleUrls: ['./ecommerce.component.css']
})
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;
    }
}

如我們所見,點擊 Home 鏈接將重置子組件。我們需要從父組件訪問子組件的方法和字段,因此我們保留了對子組件的引用並在 reset() 方法中使用它們。

3.5. 服務

為了使 子組件之間進行通信 以及從/向我們的 API 獲取/發送數據,我們需要創建一個服務:

@Injectable()
export class EcommerceService {
    private productsUrl = "/api/products";
    private ordersUrl = "/api/orders";

    private productOrder: ProductOrder;
    private orders: ProductOrders = new ProductOrders();

    private productOrderSubject = new Subject();
    private ordersSubject = new Subject();
    private totalSubject = new Subject();

    private total: number;

    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);
    }

    // getters and setters for shared fields
}

這裏有一些相對簡單的內容,正如我們所能看到的。我們正在向 API 發送 GET 和 POST 請求以進行通信。此外,我們使需要組件之間共享的數據可觀察,以便稍後可以訂閲它。

儘管如此,我們需要指出關於與 API 通信的一個方面。如果現在運行應用程序,我們將會收到 404 錯誤並且不會檢索任何數據。原因是,由於我們正在使用相對 URL,Angular 默認情況下會嘗試調用 http://localhost:4200/api/products,而我們的後端應用程序正在運行在 localhost:8080

當然,我們可以硬編碼 URL 為 localhost:8080,但這並不是我們想要做的。相反,在處理不同域時,我們應該在我們的 /frontend 文件夾中創建一個名為 proxy-conf.json 的文件

{
    "/api": {
        "target": "http://localhost:8080",
        "secure": false
    }
}

然後我們需要打開 package.json 並將 scripts.start 屬性更改為以下內容:

"scripts": {
    ...
    "start": "ng serve --proxy-config proxy-conf.json",
    ...
  }

現在,我們應該記住,啓動應用程序時應該使用 npm start,而不是 ng serve

3.6. 產品

在我們的 ProductsComponent 中,我們將注入我們之前創建的服務,並從 API 加載產品列表,將其轉換為 ProductOrders 列表,因為我們希望為每個產品添加一個數量字段:

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.productOrders = [];
        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));
                    })
                },
                (error) => console.log(error)
            );
    }

    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) {
    let index = this.getProductIndex(productOrder.product);
    if (index > -1) {
        this.shoppingCartOrders.productOrders.splice(
            this.getProductIndex(productOrder.product), 1);
    }
    this.ecommerceService.ProductOrders = this.shoppingCartOrders;
    this.shoppingCartOrders = this.ecommerceService.ProductOrders;
    this.productSelected = false;
}

最後,我們將創建一個在第 3.4 節中提到的 reset() 方法:

reset() {
    this.productOrders = [];
    this.loadProducts();
    this.ecommerceService.ProductOrders.productOrders = [];
    this.loadOrders();
    this.productSelected = false;
}

我們將在 HTML 文件中迭代產品列表,並將其顯示給用户:

<div class="row card-deck">
    <div class="col-lg-4 col-md-6 mb-4" *ngFor="let order of productOrders">
        <div class="card text-center">
            <div class="card-header">
                <h4>{{order.product.name}}</h4>
            </div>
            <div class="card-body">
                <a href="#"><img class="card-img-top" src={{order.product.pictureUrl}} 
                    alt=""></a>
                <h5 class="card-title">${{order.product.price}}</h5>
                <div class="row">
                    <div class="col-4 padding-0" *ngIf="!isProductSelected(order.product)">
                        <input type="number" min="0" class="form-control" 
                            [(ngModel)]=order.quantity>
                    </div>
                    <div class="col-4 padding-0" *ngIf="!isProductSelected(order.product)">
                        <button class="btn btn-primary" (click)="addToCart(order)"
                                [disabled]="order.quantity <= 0">Add To Cart
                        </button>
                    </div>
                    <div class="col-12" *ngIf="isProductSelected(order.product)">
                        <button class="btn btn-primary btn-block"
                                (click)="removeFromCart(order)">Remove From Cart
                        </button>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

我們還會為對應的 CSS 文件添加一個簡單的類,以確保一切都完美地適應:

.padding-0 {
    padding-right: 0;
    padding-left: 1;
}

3.7. 購物車

<em >ShoppingCart</em> 組件中,我們還將注入服務。 我們將使用它來訂閲 <em >ProductsComponent</em> 中發生的更改(以便注意到產品被選中放入購物車時),然後相應地更新購物車內容並重新計算總價:

export class ShoppingCartComponent implements OnInit, OnDestroy {
    orderFinished: boolean;
    orders: ProductOrders;
    total: number;
    sub: Subscription;

    @Output() onOrderFinished: EventEmitter<boolean>;

    constructor(private ecommerceService: EcommerceService) {
        this.total = 0;
        this.orderFinished = false;
        this.onOrderFinished = new EventEmitter<boolean>();
    }

    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(() => {
            let productOrder = this.ecommerceService.SelectedProductOrder;
            if (productOrder) {
                this.orders.productOrders.push(new ProductOrder(
                    productOrder.product, productOrder.quantity));
            }
            this.ecommerceService.ProductOrders = this.orders;
            this.orders = this.ecommerceService.ProductOrders;
            this.total = this.calculateTotal(this.orders.productOrders);
        });
    }

    ngOnDestroy() {
        this.sub.unsubscribe();
    }
}

我們從這裏向父組件發送事件,當訂單完成時,需要跳轉到結算流程。這裏也包含reset() 方法:

finishOrder() {
    this.orderFinished = true;
    this.ecommerceService.Total = this.total;
    this.onOrderFinished.emit(this.orderFinished);
}

reset() {
    this.orderFinished = false;
    this.orders = new ProductOrders();
    this.orders.productOrders = []
    this.loadTotal();
    this.total = 0;
}

HTML 文件很簡單:

<div class="card text-white bg-danger mb-3" style="max-width: 18rem;">
    <div class="card-header text-center">Shopping Cart</div>
    <div class="card-body">
        <h5 class="card-title">Total: ${{total}}</h5>
        <hr>
        <h6 class="card-title">Items bought:</h6>

        <ul>
            <li *ngFor="let order of orders.productOrders">
                {{ order.product.name }} - {{ order.quantity}} pcs.
            </li>
        </ul>

        <button class="btn btn-light btn-block" (click)="finishOrder()"
             [disabled]="orders.productOrders.length == 0">Checkout
        </button>
    </div>
</div>

3.8. 訂單

我們將盡量保持操作的簡潔,並在 OrdersComponent 中模擬支付行為,通過將屬性設置為 true 並將訂單保存到數據庫中來實現。您可以通過 h2-console 檢查訂單是否已保存,或通過訪問 http://localhost:8080/api/orders 來檢查。

此處需要使用 EcommerceService 以檢索購物車中的產品列表和訂單總額:

export class OrdersComponent implements OnInit {
    orders: ProductOrders;
    total: number;
    paid: boolean;
    sub: Subscription;

    constructor(private ecommerceService: EcommerceService) {
        this.orders = this.ecommerceService.ProductOrders;
    }

    ngOnInit() {
        this.paid = false;
        this.sub = this.ecommerceService.OrdersChanged.subscribe(() => {
            this.orders = this.ecommerceService.ProductOrders;
        });
        this.loadTotal();
    }

    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>Congratulation!</strong> You successfully made the order.
</div>

4. 合併 Spring Boot 和 Angular 應用

我們完成了這兩個應用的開發,現在單獨開發可能更方便。但是,在生產環境中,擁有一個單一的應用會更方便,所以我們現在將它們合併。

我們想要做的是構建一個調用 Webpack 打包所有資產並將其推送到 Spring Boot 應用 <em>/resources/static</em> 目錄的 Angular 應用。 這樣,我們就可以運行 Spring Boot 應用並測試我們的應用,並將所有這些打包並部署為一個應用。

為了實現這一點,我們需要再次打開 <em>package.json</em> 並添加一些新的腳本在 <em>scripts</em>.<em>build</em> 之後:

"postbuild": "npm run deploy",
"predeploy": "rimraf ../resources/static/ && mkdirp ../resources/static",
"deploy": "copyfiles -f dist/** ../resources/static",

我們正在使用一些沒有安裝的包,所以我們來安裝它們:

npm install --save-dev rimraf
npm install --save-dev mkdirp
npm install --save-dev copyfiles

rimraf 命令將查看目錄並清理它,而 copyfiles 命令則將從 Angular 分發文件夾中複製文件到我們的 static 文件夾。

現在,我們只需要運行 npm run build 命令,這將會執行所有這些命令,最終輸出結果將是 static 文件夾中的打包好的應用程序。

然後,我們運行 Spring Boot 應用程序,在端口 8080 上訪問它,並使用 Angular 應用程序。

5. 結論

在本文中,我們創建了一個簡單的電子商務應用程序。我們使用 Spring Boot 在後端創建了一個 API,然後我們在 Angular 製作的前端應用程序中消費了它。我們展示瞭如何創建所需的組件、如何使它們相互通信以及如何從/到 API 中檢索/發送數據。

最後,我們展示瞭如何將這兩個應用程序合併到一個靜態文件夾中的打包 Web 應用程序中。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.