這系列文章會總結先前包含 JPA 和 Security 的應用,整合成一個小電商專案 side project,針對後端 API 和認證的部分,內容因為前面大量的內容會比較長,大家有興趣的可以好好來了解一下。
如果先回顧前面 Security 部分建構的資料庫結構 UML
會有 users, roles, user_roles 三個表去紀錄關於使用者的資訊,也可以從多對多的關聯中找出使用者的角色權限
現在要來加入電商主要的資料就是商品部分,預計會規畫成下面這樣的關係
會拆分成兩階段來建構:
大致清楚架構之後接下來預計會實作的一些功能也列出來:
商品相關
使用者權限
使用者
這樣就有基本的電商運作模式需要的一些功能了。
那先前 Security 的介紹部分已經把使用者註冊、登入和權限的設置都已經先建置好,就可以直接來接著進行商品部分功能建立
商品相關資料表,刻意多加入一個 supplier 關聯表 可以對應多對一關聯
CREATE TABLE suppliers
(
id INT AUTO_INCREMENT PRIMARY KEY,
supplier_name VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE products
(
id INT AUTO_INCREMENT PRIMARY KEY,
product_name VARCHAR(100) NOT NULL,
unit_price DECIMAL(10, 2),
units_in_stock INT,
discontinued BOOLEAN DEFAULT FALSE,
supplier_id INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (supplier_id) REFERENCES suppliers (id)
);
先建立基本Entity
@Entity
@Data
@Table(name = "products")
public class Product extends BaseEntity {
private String productName;
private Double unitPrice;
private Integer unitsInStock;
private Boolean discontinued = false;
@ManyToOne
private Supplier supplier;
建立時需要的 dto,順便透過 Validation 相關註解來驗證傳入欄位的資訊
@Data
public class ProductRequest {
@NotBlank(message = "ProductName cannot be blank")
private String productName;
@NotNull
@PositiveOrZero(message = "UnitPrice must be zero or positive")
private Double unitPrice;
@NotNull
@PositiveOrZero(message = "UnitInStock must be zero or positive")
private Integer unitsInStock;
@NotNull
private Boolean discontinued;
@NotNull
private Supplier supplier;
private LocalDateTime createAt;
private LocalDateTime updatedAt;
}
@RestController
@RequestMapping("/api/products")
public class ProductController {
@Autowired
private ProductService productService;
@PostMapping
public ResponseEntity<Product> createProduct(@RequestBody @Valid ProductRequest productRequest) {
Product createdProduct = productService.createProduct(productRequest);
return ResponseEntity.status(HttpStatus.CREATED).body(createdProduct);
}
@GetMapping
public ResponseEntity<List<Product>> getProducts() {
List<Product> productList = productService.getAllProducts();
return ResponseEntity.status(HttpStatus.OK).body(productList);
}
@GetMapping("/{id}")
public ResponseEntity<Optional<Product>> getProduct(@PathVariable Integer id) {
Optional<Product> product = productService.getProductById(id);
if (product.isPresent()) {
return ResponseEntity.status(HttpStatus.OK).body(product);
}
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
@PutMapping("/{id}")
public ResponseEntity<Product> updateProduct(@PathVariable Integer id, @Valid @RequestBody ProductRequest productRequest) {
Optional<Product> product = productService.getProductById(id);
if (!product.isPresent()) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
Product updatedProduct = productService.updateProduct(id, productRequest);
return ResponseEntity.status(HttpStatus.OK).body(updatedProduct);
}
@DeleteMapping("/{id}")
public ResponseEntity<?> deleteProduct(@PathVariable Integer id) {
Optional<Product> product = productService.getProductById(id);
if (product.isPresent()) {
productService.deleteProductById(id);
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
}
Service
@Service
public class ProductService {
@Autowired
private ProductDao productDao;
public List<Product> getAllProducts() {
return productDao.findAll();
}
public Optional<Product> getProductById(int id) {
return productDao.findById(id);
}
@Transactional
public Product updateProduct(Integer id, ProductRequest productRequest) {
Optional<Product> product = getProductById(id);
if (product.isPresent()) {
Product updatedProduct = convertToModel(productRequest);
updatedProduct.setId(id);
return productDao.save(updatedProduct);
}
return null;
}
@Transactional
public Product createProduct(ProductRequest productRequest) {
Product product = convertToModel(productRequest);
return productDao.save(product);
}
@Transactional
public void deleteProductById(Integer id) {
productDao.deleteById(id);
}
public List<Product> searchProducts(String productName) {
return productDao.findByProductName(productName);
}
public Product convertToModel(ProductRequest productRequest) {
Product product = new Product();
product.setProductName(productRequest.getProductName());
product.setUnitPrice(productRequest.getUnitPrice());
product.setUnitsInStock(productRequest.getUnitsInStock());
product.setDiscontinued(productRequest.getDiscontinued());
product.setSupplier(productRequest.getSupplier());
return product;
}
}
開一個相關端口接收對應參數,可以對應倒前端搜尋頁面的操作
sortBy 篩選欄位
sortOrder 降冪或升冪排序
page 呈現第幾頁資料
limit 一頁顯示幾筆
@RestController
@RequestMapping("/api/products")
public class ProductController {
@Autowired
private ProductService productService;
// ...
@GetMapping("/search")
public ResponseEntity<List<Product>> searchProducts(
@RequestParam(required = false) String productName,
@RequestParam(defaultValue = "id", required = false) String sortBy,
@RequestParam(defaultValue = "asc", required = false) String sortOrder,
@RequestParam(defaultValue = "0", required = false) int page,
@RequestParam(defaultValue = "5", required = false) int limit
) {
List<Product> productList = new ArrayList<>();
productList = productService.searchAndSortProducts(productName, sortBy, sortOrder, page, limit);
return ResponseEntity.status(HttpStatus.OK).body(productList);
}
}
可以透過 Sort/Pageable 物件進行篩選及資料分頁
@Service
public class ProductService {
@Autowired
private ProductDao productDao;
// ...
public List<Product> searchAndSortProducts(String productName, String sortBy, String sortOrder, int page, int limit) {
Sort sort = sortOrder.equalsIgnoreCase("asc") ? Sort.by(sortBy).ascending() : Sort.by(sortBy).descending();
Pageable pageable = PageRequest.of(page, limit, sort);
Page<Product> productPage = null;
if (productName == null || productName.isEmpty()) {
productPage = productDao.findAll(pageable);
} else {
productPage = productDao.findByProductNameContainingIgnoreCase(productName, pageable);
}
List<Product> products = productPage.getContent();
return products;
}
}
讓 seller 才能建立商品資料,需要再 SecurityConfig 那邊新增相關路徑對應可以操作的權限
也可以同步在端口上加上 @PreAuthorize("hasRole('SELLER')")
,這部分可以不用,兩種選一個即可達成,但如果需要標示清楚易閱讀可以選擇兩邊都加入,但管理上就要注意是否重工,可能調整時兩邊都要設定,兩種都用 @PreAuthorize
可以幫忙切得更精細一點。
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.csrf(customizer -> customizer.disable())
.authorizeHttpRequests((registry) -> registry
.requestMatchers(HttpMethod.POST, "/register", "/login").permitAll()
.requestMatchers(HttpMethod.GET, "/error", "/api/products/**", "/who-am-i").permitAll()
.requestMatchers(HttpMethod.GET, "/checkAuthentication").hasAnyAuthority("ROLE_BUYER", "ROLE_SELLER", "ROLE_ADMIN")
// 需要 ROLE_SELLER 才能修改跟刪除
.requestMatchers(HttpMethod.POST, "/api/products").hasAuthority("ROLE_SELLER")
.requestMatchers(HttpMethod.DELETE, "/api/products").hasAuthority("ROLE_SELLER")
.requestMatchers("/api/users/*").hasAuthority("ROLE_ADMIN")
.anyRequest().authenticated()
)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
這邊提供一些資料先塞進去測試
INSERT INTO suppliers (id, supplier_name)
VALUES (1, 'TechSupply Inc.'),
(2, 'GadgetWorld Ltd.'),
(3, 'ElectroGoods Co.');
INSERT INTO products (product_name, supplier_id, unit_price, units_in_stock, discontinued)
VALUES ('Wireless Mouse', 1, 24.99, 150, FALSE),
('Bluetooth Keyboard', 2, 49.99, 75, FALSE),
('USB-C Charger', 3, 19.99, 200, FALSE),
('Gaming Headset', 1, 79.99, 50, FALSE),
('4K Monitor', 2, 299.99, 40, TRUE),
('External Hard Drive', 1, 89.99, 120, FALSE),
('Mechanical Keyboard', 3, 129.99, 30, TRUE),
('Portable SSD', 3, 159.99, 60, FALSE),
('Wireless Earbuds', 1, 199.99, 80, FALSE),
('Laptop Stand', 3, 39.99, 100, FALSE);
輸入 JWT
輸入新增電文,下面提供簡單電文給大家參考
{
"productName": "Good Mouse",
"unitPrice": 110.99,
"unitsInStock": 100,
"discontinued": false,
"supplier": {
"id": 2
}
}
與前面新增相同,確認這些端口有正常運作。
以上大概是先介紹商品功能的擴充介紹,下一篇來加入訂單功能。
相關文章也會同步更新我的部落格,有興趣也可以在裡面找其他的技術分享跟資訊。