Перейти к основному содержимому

Простая реализация электронной коммерции с помощью Spring

· 17 мин. чтения

1. Обзор нашего приложения для электронной коммерции

В этом руководстве мы реализуем простое приложение электронной коммерции. Мы разработаем API с помощью Spring Boot и клиентское приложение, которое будет использовать API с помощью Angular .

По сути, пользователь сможет добавлять/удалять товары из списка товаров в/из корзины и размещать заказ.

2. Бэкенд-часть

Для разработки API мы будем использовать последнюю версию Spring Boot. Мы также используем базу данных JPA и H2 для обеспечения устойчивости.

Чтобы узнать больше о Spring Boot, вы можете ознакомиться с нашей серией статей о Spring Boot, а если вы хотите познакомиться с созданием REST API, ознакомьтесь с другой серией статей .

2.1. Зависимости Maven

Давайте подготовим наш проект и импортируем необходимые зависимости в наш pom.xml .

Нам понадобятся некоторые основные зависимости Spring Boot :

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>

Затем база данных H2 :

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

И наконец — библиотека Джексона :

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

Мы использовали Spring Initializr для быстрой настройки проекта с необходимыми зависимостями.

2.2. Настройка базы данных

Хотя мы могли бы использовать встроенную базу данных H2 в памяти с Spring Boot, мы все же внесем некоторые коррективы, прежде чем приступим к разработке нашего 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 в качестве URL-адреса JDBC и пользователя sa без пароля.

2.3. Структура проекта

Проект будет организован в несколько стандартных пакетов, а приложение Angular будет помещено в папку внешнего интерфейса:

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

Следует отметить, что все интерфейсы в пакете репозитория просты и расширяют CrudRepository Spring Data , поэтому мы не будем их здесь отображать.

2.4. Обработка исключений

Нам понадобится обработчик исключений для нашего API, чтобы правильно обрабатывать возможные исключения.

Вы можете найти более подробную информацию по этой теме в наших статьях Обработка ошибок для REST с помощью Spring и Пользовательская обработка сообщений об ошибках для 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 есть много полезных статей .

Наше приложение будет поддерживать только чтение продуктов из базы данных , поэтому нам нужно сначала добавить некоторые из них.

Давайте создадим простой класс Product :

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

Если мы сейчас запустим наше приложение, мы сможем получить список продуктов через 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 первичного ключа, так как это было бы избыточно.

Нам нужно, чтобы продукт отображался пользователю, поэтому у нас есть временный метод 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);
}

Прежде всего, мы принимаем список продуктов с их соответствующими количествами . После этого мы проверяем, все ли товары существуют в базе данных, а затем создаем и сохраняем новый заказ . Мы сохраняем ссылку на вновь созданный объект, чтобы мы могли добавить к нему детали заказа.

Наконец, мы создаем заголовок «Местоположение» .

Подробная реализация есть в репозитории — ссылка на него указана в конце этой статьи.

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. В файле pakage.json мы можем проверить, какие версии наших зависимостей установлены. Это руководство основано на Angular v6.0.3, но более старые версии должны работать, по крайней мере версии 4.3 и новее ( HttpClient , который мы используем здесь, был введен в Angular 4.3).

Следует отметить, что мы будем запускать все наши команды из папки /frontend , если не указано иное.

Этой настройки достаточно, чтобы запустить приложение Angular, выполнив команду ng serve . По умолчанию он работает на http://localhost:4200 , и если мы сейчас перейдем туда, мы увидим загруженное базовое приложение Angular.

3.2. Добавление начальной загрузки

Прежде чем мы приступим к созданию наших собственных компонентов, давайте сначала добавим Bootstrap в наш проект, чтобы наши страницы выглядели красиво.

Нам нужно всего несколько вещей, чтобы достичь этого. Во-первых, нам нужно запустить команду для его установки :

npm install --save bootstrap

а затем сказать Angular, чтобы он действительно использовал его . Для этого нам нужно открыть файл src/main/frontend/angular.json и добавить node_modules/bootstrap/dist/css/bootstrap.min.css в свойство «styles» . Вот и все.

3.3. Компоненты и модели

Прежде чем мы начнем создавать компоненты для нашего приложения, давайте сначала посмотрим, как наше приложение будет выглядеть на самом деле:

./6e24df4d7bc85d1d8e0ce97ed3a9e377.png

Теперь мы создадим базовый компонент с именем 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[] = [];
}

Последняя упомянутая модель соответствует нашей форме заказа на бэкэнде .

3.4. Базовый компонент

В верхней части нашего компонента электронной коммерции мы поместим панель навигации со ссылкой «Главная» справа:

<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
<div class="container">
<a class="navbar-brand" href="#">ForEach 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 в app.component.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;
}
}

Как мы видим, нажатие на ссылку « Главная » приведет к сбросу дочерних компонентов. Нам нужно получить доступ к методам и полю внутри дочерних компонентов от родителя, поэтому мы сохраняем ссылки на дочерние компоненты и используем их внутри метода 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
}

Как мы могли заметить, здесь относительно простые вещи. Мы делаем запросы GET и POST для связи с API. Кроме того, мы делаем данные, которыми мы должны делиться между компонентами, наблюдаемыми, чтобы мы могли подписаться на них позже.

Тем не менее, мы должны отметить одну вещь, касающуюся связи с API. Если мы запустим приложение сейчас, мы получим 404 и не получим никаких данных. Причина этого в том, что, поскольку мы используем относительные URL-адреса, Angular по умолчанию попытается сделать вызов http://localhost:4200/api/products, а наше серверное приложение работает на localhost:8080 .

Конечно, мы могли бы жестко запрограммировать URL-адреса на localhost:8080 , но это не то, что мы хотим делать. Вместо этого при работе с разными доменами мы должны создать файл с именем proxy-conf.json в нашей папке /frontend :

{
"/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;
}

Наконец, мы создадим метод reset (), о котором упоминалось в разделе 3.4:

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. Корзина покупателя

В компоненте ShoppingCart мы также внедрим сервис. Мы будем использовать его, чтобы подписаться на изменения в ProductsComponent (чтобы заметить, когда продукт выбран для помещения в корзину), а затем обновить содержимое корзины и соответствующим образом пересчитать общую стоимость:

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-консоль , либо нажав 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

Мы закончили разработку обоих наших приложений и, наверное, проще разрабатывать их отдельно, как это делали мы. Но в продакшене было бы гораздо удобнее иметь одно приложение, так что давайте теперь объединим эти два.

Что мы хотим сделать здесь, так это создать приложение Angular, которое вызывает Webpack для объединения всех ресурсов и помещения их в каталог /resources/static приложения Spring Boot . Таким образом, мы можем просто запустить приложение Spring Boot, протестировать наше приложение, упаковать все это и развернуть как одно приложение.

Чтобы это стало возможным, нам нужно снова открыть ' package.json ' и добавить несколько новых скриптов после скриптов . построить :

"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 все помещает) в нашу статическую папку.

Теперь нам просто нужно запустить команду npm run build, и она должна запустить все эти команды, и конечным результатом будет наше упакованное приложение в статической папке .

Затем мы запускаем наше приложение Spring Boot на порту 8080, получаем к нему доступ и используем приложение Angular.

5. Вывод

В этой статье мы создали простое приложение для электронной коммерции. Мы создали API на серверной части с помощью Spring Boot, а затем использовали его в нашем внешнем приложении, созданном на Angular. Мы продемонстрировали, как сделать нужные нам компоненты, заставить их общаться друг с другом и получать/отправлять данные из/в API.

Наконец, мы показали, как объединить оба приложения в одно, упакованное веб-приложение внутри статической папки.

Как всегда, полный проект, который мы описали в этой статье, можно найти в проекте GitHub.