feat: 新增用户地址、收藏、商品评价和订单项模块
- 新增 UserAddress/UserFavorite/ProductReview/OrderItem 实体类 - 新增对应的 DTO、Repository、Service 和 Controller - 新增 OrderMigrationService 订单数据迁移服务
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
package com.org.flashsalesystem.controller;
|
||||
|
||||
import com.org.flashsalesystem.dto.ProductReviewDTO;
|
||||
import com.org.flashsalesystem.dto.UserDTO;
|
||||
import com.org.flashsalesystem.service.ProductReviewService;
|
||||
import com.org.flashsalesystem.service.UserService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/review")
|
||||
public class ProductReviewController {
|
||||
|
||||
@Autowired
|
||||
private ProductReviewService productReviewService;
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@GetMapping("/product/{productId}")
|
||||
public ResponseEntity<Map<String, Object>> getProductReviews(@PathVariable Long productId) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "获取评价成功");
|
||||
response.put("data", productReviewService.getProductReviews(productId));
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<Map<String, Object>> createReview(@Validated @RequestBody ProductReviewDTO.CreateDTO createDTO,
|
||||
HttpServletRequest request) {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "评价提交成功");
|
||||
response.put("data", productReviewService.createReview(userId, createDTO));
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
private Long getCurrentUserId(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session == null) {
|
||||
return null;
|
||||
}
|
||||
String token = (String) session.getAttribute("token");
|
||||
UserDTO user = userService.getUserByToken(token);
|
||||
return user != null ? user.getId() : null;
|
||||
}
|
||||
|
||||
private ResponseEntity<Map<String, Object>> unauthorized() {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "用户未登录或登录已过期");
|
||||
return ResponseEntity.status(401).body(response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.org.flashsalesystem.controller;
|
||||
|
||||
import com.org.flashsalesystem.dto.UserAddressDTO;
|
||||
import com.org.flashsalesystem.dto.UserDTO;
|
||||
import com.org.flashsalesystem.service.UserAddressService;
|
||||
import com.org.flashsalesystem.service.UserService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/address")
|
||||
public class UserAddressController {
|
||||
|
||||
@Autowired
|
||||
private UserAddressService userAddressService;
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<Map<String, Object>> getAddresses(HttpServletRequest request) {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
List<UserAddressDTO> data = userAddressService.getUserAddresses(userId);
|
||||
return ok(data, "获取地址成功");
|
||||
}
|
||||
|
||||
@GetMapping("/default")
|
||||
public ResponseEntity<Map<String, Object>> getDefaultAddress(HttpServletRequest request) {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
return ok(userAddressService.getDefaultAddress(userId), "获取默认地址成功");
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<Map<String, Object>> createAddress(@Validated @RequestBody UserAddressDTO.SaveDTO saveDTO,
|
||||
HttpServletRequest request) {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
return ok(userAddressService.createAddress(userId, saveDTO), "新增地址成功");
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<Map<String, Object>> updateAddress(@PathVariable Long id,
|
||||
@Validated @RequestBody UserAddressDTO.SaveDTO saveDTO,
|
||||
HttpServletRequest request) {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
return ok(userAddressService.updateAddress(userId, id, saveDTO), "更新地址成功");
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/default")
|
||||
public ResponseEntity<Map<String, Object>> setDefault(@PathVariable Long id, HttpServletRequest request) {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
return ok(userAddressService.setDefault(userId, id), "设置默认地址成功");
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Map<String, Object>> deleteAddress(@PathVariable Long id, HttpServletRequest request) {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
userAddressService.deleteAddress(userId, id);
|
||||
return ok(null, "删除地址成功");
|
||||
}
|
||||
|
||||
private Long getCurrentUserId(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session == null) {
|
||||
return null;
|
||||
}
|
||||
String token = (String) session.getAttribute("token");
|
||||
UserDTO user = userService.getUserByToken(token);
|
||||
return user != null ? user.getId() : null;
|
||||
}
|
||||
|
||||
private ResponseEntity<Map<String, Object>> ok(Object data, String message) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", message);
|
||||
response.put("data", data);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
private ResponseEntity<Map<String, Object>> unauthorized() {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "用户未登录或登录已过期");
|
||||
return ResponseEntity.status(401).body(response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.org.flashsalesystem.controller;
|
||||
|
||||
import com.org.flashsalesystem.dto.UserDTO;
|
||||
import com.org.flashsalesystem.dto.UserFavoriteDTO;
|
||||
import com.org.flashsalesystem.service.UserFavoriteService;
|
||||
import com.org.flashsalesystem.service.UserService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/favorite")
|
||||
public class UserFavoriteController {
|
||||
|
||||
@Autowired
|
||||
private UserFavoriteService userFavoriteService;
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<Map<String, Object>> getFavorites(HttpServletRequest request) {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) return unauthorized();
|
||||
return ok(userFavoriteService.getFavorites(userId), "获取收藏列表成功");
|
||||
}
|
||||
|
||||
@GetMapping("/count")
|
||||
public ResponseEntity<Map<String, Object>> getCount(HttpServletRequest request) {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) return unauthorized();
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("count", userFavoriteService.getFavoriteCount(userId));
|
||||
return ok(data, "获取收藏数量成功");
|
||||
}
|
||||
|
||||
@GetMapping("/check")
|
||||
public ResponseEntity<Map<String, Object>> checkFavorite(@RequestParam Long productId, HttpServletRequest request) {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) return unauthorized();
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("favorited", userFavoriteService.isFavorited(userId, productId));
|
||||
return ok(data, "获取收藏状态成功");
|
||||
}
|
||||
|
||||
@PostMapping("/toggle")
|
||||
public ResponseEntity<Map<String, Object>> toggleFavorite(@Validated @RequestBody UserFavoriteDTO.ToggleDTO toggleDTO,
|
||||
HttpServletRequest request) {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) return unauthorized();
|
||||
boolean favorited = userFavoriteService.toggleFavorite(userId, toggleDTO.getProductId());
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("favorited", favorited);
|
||||
return ok(data, favorited ? "收藏成功" : "已取消收藏");
|
||||
}
|
||||
|
||||
private Long getCurrentUserId(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session == null) return null;
|
||||
String token = (String) session.getAttribute("token");
|
||||
UserDTO user = userService.getUserByToken(token);
|
||||
return user != null ? user.getId() : null;
|
||||
}
|
||||
|
||||
private ResponseEntity<Map<String, Object>> ok(Object data, String message) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", message);
|
||||
response.put("data", data);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
private ResponseEntity<Map<String, Object>> unauthorized() {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "用户未登录或登录已过期");
|
||||
return ResponseEntity.status(401).body(response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.org.flashsalesystem.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.validation.constraints.Max;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ProductReviewDTO {
|
||||
private Long id;
|
||||
private Long productId;
|
||||
private Long userId;
|
||||
private Long orderId;
|
||||
private String username;
|
||||
private Integer rating;
|
||||
private String content;
|
||||
private Integer status;
|
||||
private String statusText;
|
||||
private String adminReply;
|
||||
private LocalDateTime repliedAt;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class CreateDTO {
|
||||
@NotNull(message = "订单ID不能为空")
|
||||
private Long orderId;
|
||||
|
||||
@NotNull(message = "商品ID不能为空")
|
||||
private Long productId;
|
||||
|
||||
@NotNull(message = "评分不能为空")
|
||||
@Min(value = 1, message = "评分最低为1")
|
||||
@Max(value = 5, message = "评分最高为5")
|
||||
private Integer rating;
|
||||
|
||||
@NotBlank(message = "评价内容不能为空")
|
||||
private String content;
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class SummaryDTO {
|
||||
private Double averageRating;
|
||||
private Long totalReviews;
|
||||
private List<ProductReviewDTO> reviews;
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class UpdateDTO {
|
||||
private Integer status;
|
||||
private String adminReply;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.org.flashsalesystem.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.Pattern;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UserAddressDTO {
|
||||
private Long id;
|
||||
private Long userId;
|
||||
private String name;
|
||||
private String phone;
|
||||
private String province;
|
||||
private String city;
|
||||
private String district;
|
||||
private String address;
|
||||
private Boolean isDefault;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class SaveDTO {
|
||||
@NotBlank(message = "收货人不能为空")
|
||||
private String name;
|
||||
|
||||
@NotBlank(message = "手机号不能为空")
|
||||
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
||||
private String phone;
|
||||
|
||||
@NotBlank(message = "省份不能为空")
|
||||
private String province;
|
||||
|
||||
@NotBlank(message = "城市不能为空")
|
||||
private String city;
|
||||
|
||||
@NotBlank(message = "区县不能为空")
|
||||
private String district;
|
||||
|
||||
@NotBlank(message = "详细地址不能为空")
|
||||
private String address;
|
||||
|
||||
private Boolean isDefault = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.org.flashsalesystem.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UserFavoriteDTO {
|
||||
private Long id;
|
||||
private Long userId;
|
||||
private Long productId;
|
||||
private String productName;
|
||||
private String productImageUrl;
|
||||
private java.math.BigDecimal productPrice;
|
||||
private String productCategory;
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class ToggleDTO {
|
||||
@NotNull(message = "商品ID不能为空")
|
||||
private Long productId;
|
||||
}
|
||||
}
|
||||
50
src/main/java/com/org/flashsalesystem/entity/OrderItem.java
Normal file
50
src/main/java/com/org/flashsalesystem/entity/OrderItem.java
Normal file
@@ -0,0 +1,50 @@
|
||||
package com.org.flashsalesystem.entity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "order_items")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class OrderItem {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "order_id", nullable = false)
|
||||
private Long orderId;
|
||||
|
||||
@Column(name = "product_id", nullable = false)
|
||||
private Long productId;
|
||||
|
||||
@Column(name = "product_name", nullable = false, length = 200)
|
||||
private String productName;
|
||||
|
||||
@Column(name = "product_image_url", length = 500)
|
||||
private String productImageUrl;
|
||||
|
||||
@Column(name = "price", nullable = false, precision = 10, scale = 2)
|
||||
private BigDecimal price;
|
||||
|
||||
@Column(name = "quantity", nullable = false)
|
||||
private Integer quantity;
|
||||
|
||||
@Column(name = "subtotal", nullable = false, precision = 10, scale = 2)
|
||||
private BigDecimal subtotal;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.org.flashsalesystem.entity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "product_reviews", uniqueConstraints = {
|
||||
@UniqueConstraint(name = "uk_review_order_user_product", columnNames = {"order_id", "user_id", "product_id"})
|
||||
})
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ProductReview {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "product_id", nullable = false)
|
||||
private Long productId;
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@Column(name = "order_id", nullable = false)
|
||||
private Long orderId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Integer rating = 5;
|
||||
|
||||
@Column(nullable = false, columnDefinition = "TEXT")
|
||||
private String content;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Integer status = 1;
|
||||
|
||||
@Column(name = "admin_reply", columnDefinition = "TEXT")
|
||||
private String adminReply;
|
||||
|
||||
@Column(name = "replied_at")
|
||||
private LocalDateTime repliedAt;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.org.flashsalesystem.entity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "user_addresses")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UserAddress {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@Column(nullable = false, length = 100)
|
||||
private String name;
|
||||
|
||||
@Column(nullable = false, length = 20)
|
||||
private String phone;
|
||||
|
||||
@Column(length = 50)
|
||||
private String province;
|
||||
|
||||
@Column(length = 50)
|
||||
private String city;
|
||||
|
||||
@Column(length = 50)
|
||||
private String district;
|
||||
|
||||
@Column(nullable = false, length = 255)
|
||||
private String address;
|
||||
|
||||
@Column(name = "is_default", nullable = false)
|
||||
private Boolean isDefault = false;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.org.flashsalesystem.entity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "user_favorites", uniqueConstraints = {
|
||||
@UniqueConstraint(name = "uk_favorite_user_product", columnNames = {"user_id", "product_id"})
|
||||
})
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UserFavorite {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@Column(name = "product_id", nullable = false)
|
||||
private Long productId;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.org.flashsalesystem.repository;
|
||||
|
||||
import com.org.flashsalesystem.entity.OrderItem;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface OrderItemRepository extends JpaRepository<OrderItem, Long> {
|
||||
List<OrderItem> findByOrderIdOrderByIdAsc(Long orderId);
|
||||
|
||||
boolean existsByOrderId(Long orderId);
|
||||
|
||||
long countByProductId(Long productId);
|
||||
|
||||
@Query("SELECT COALESCE(SUM(i.subtotal), 0) FROM OrderItem i JOIN Order o ON i.orderId = o.id WHERE i.productId = :productId AND o.status IN (2,3,4)")
|
||||
BigDecimal sumSubtotalByProductId(@Param("productId") Long productId);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.org.flashsalesystem.repository;
|
||||
|
||||
import com.org.flashsalesystem.entity.ProductReview;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface ProductReviewRepository extends JpaRepository<ProductReview, Long> {
|
||||
List<ProductReview> findByProductIdOrderByCreatedAtDesc(Long productId);
|
||||
List<ProductReview> findByProductIdAndStatusOrderByCreatedAtDesc(Long productId, Integer status);
|
||||
Optional<ProductReview> findByOrderIdAndUserIdAndProductId(Long orderId, Long userId, Long productId);
|
||||
boolean existsByOrderIdAndUserIdAndProductId(Long orderId, Long userId, Long productId);
|
||||
|
||||
long countByProductId(Long productId);
|
||||
|
||||
@Query("SELECT AVG(r.rating) FROM ProductReview r WHERE r.productId = :productId")
|
||||
Double findAverageRatingByProductId(@Param("productId") Long productId);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.org.flashsalesystem.repository;
|
||||
|
||||
import com.org.flashsalesystem.entity.UserAddress;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface UserAddressRepository extends JpaRepository<UserAddress, Long> {
|
||||
List<UserAddress> findByUserIdOrderByIsDefaultDescUpdatedAtDesc(Long userId);
|
||||
Optional<UserAddress> findByUserIdAndIsDefaultTrue(Long userId);
|
||||
Optional<UserAddress> findByIdAndUserId(Long id, Long userId);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.org.flashsalesystem.repository;
|
||||
|
||||
import com.org.flashsalesystem.entity.UserFavorite;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface UserFavoriteRepository extends JpaRepository<UserFavorite, Long> {
|
||||
List<UserFavorite> findByUserIdOrderByCreatedAtDesc(Long userId);
|
||||
Optional<UserFavorite> findByUserIdAndProductId(Long userId, Long productId);
|
||||
boolean existsByUserIdAndProductId(Long userId, Long productId);
|
||||
long countByUserId(Long userId);
|
||||
void deleteByUserIdAndProductId(Long userId, Long productId);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import com.org.flashsalesystem.dto.ProductDTO;
|
||||
import com.org.flashsalesystem.entity.Order;
|
||||
import com.org.flashsalesystem.entity.OrderItem;
|
||||
import com.org.flashsalesystem.repository.OrderItemRepository;
|
||||
import com.org.flashsalesystem.repository.OrderRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class OrderMigrationService {
|
||||
|
||||
@Autowired
|
||||
private OrderRepository orderRepository;
|
||||
|
||||
@Autowired
|
||||
private OrderItemRepository orderItemRepository;
|
||||
|
||||
@Autowired
|
||||
private ProductService productService;
|
||||
|
||||
@Transactional
|
||||
public Map<String, Object> migrateLegacyOrderItems() {
|
||||
List<Order> orders = orderRepository.findAll();
|
||||
int migrated = 0;
|
||||
int skipped = 0;
|
||||
|
||||
for (Order order : orders) {
|
||||
if (orderItemRepository.existsByOrderId(order.getId())) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
ProductDTO product = productService.getProductById(order.getProductId());
|
||||
OrderItem orderItem = new OrderItem();
|
||||
orderItem.setOrderId(order.getId());
|
||||
orderItem.setProductId(order.getProductId());
|
||||
orderItem.setProductName(product != null ? product.getName() : "未知商品");
|
||||
orderItem.setProductImageUrl(product != null ? product.getImageUrl() : null);
|
||||
orderItem.setPrice(order.getQuantity() != null && order.getQuantity() > 0
|
||||
? order.getTotalPrice().divide(java.math.BigDecimal.valueOf(order.getQuantity()), 2, java.math.RoundingMode.HALF_UP)
|
||||
: order.getTotalPrice());
|
||||
orderItem.setQuantity(order.getQuantity());
|
||||
orderItem.setSubtotal(order.getTotalPrice());
|
||||
orderItemRepository.save(orderItem);
|
||||
migrated++;
|
||||
}
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("totalOrders", orders.size());
|
||||
result.put("migrated", migrated);
|
||||
result.put("skipped", skipped);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import com.org.flashsalesystem.dto.ProductReviewDTO;
|
||||
import com.org.flashsalesystem.dto.UserDTO;
|
||||
import com.org.flashsalesystem.entity.Order;
|
||||
import com.org.flashsalesystem.entity.OrderItem;
|
||||
import com.org.flashsalesystem.entity.ProductReview;
|
||||
import com.org.flashsalesystem.repository.OrderItemRepository;
|
||||
import com.org.flashsalesystem.repository.OrderRepository;
|
||||
import com.org.flashsalesystem.repository.ProductReviewRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class ProductReviewService {
|
||||
|
||||
@Autowired
|
||||
private ProductReviewRepository productReviewRepository;
|
||||
|
||||
@Autowired
|
||||
private OrderRepository orderRepository;
|
||||
|
||||
@Autowired
|
||||
private OrderItemRepository orderItemRepository;
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Transactional
|
||||
public ProductReviewDTO createReview(Long userId, ProductReviewDTO.CreateDTO createDTO) {
|
||||
Order order = orderRepository.findById(createDTO.getOrderId())
|
||||
.orElseThrow(() -> new RuntimeException("订单不存在"));
|
||||
|
||||
if (!order.getUserId().equals(userId)) {
|
||||
throw new RuntimeException("无权限评价此订单");
|
||||
}
|
||||
java.util.List<OrderItem> orderItems = orderItemRepository.findByOrderIdOrderByIdAsc(order.getId());
|
||||
boolean matchProduct = orderItems.isEmpty()
|
||||
? order.getProductId().equals(createDTO.getProductId())
|
||||
: orderItems.stream().anyMatch(item -> item.getProductId().equals(createDTO.getProductId()));
|
||||
if (!matchProduct) {
|
||||
throw new RuntimeException("订单商品不匹配");
|
||||
}
|
||||
if (order.getStatus() != 4) {
|
||||
throw new RuntimeException("仅已完成订单允许评价");
|
||||
}
|
||||
if (productReviewRepository.existsByOrderIdAndUserIdAndProductId(createDTO.getOrderId(), userId, createDTO.getProductId())) {
|
||||
throw new RuntimeException("该订单已评价");
|
||||
}
|
||||
|
||||
ProductReview review = new ProductReview();
|
||||
BeanUtils.copyProperties(createDTO, review);
|
||||
review.setUserId(userId);
|
||||
review = productReviewRepository.save(review);
|
||||
return toDTO(review);
|
||||
}
|
||||
|
||||
public ProductReviewDTO.SummaryDTO getProductReviews(Long productId) {
|
||||
List<ProductReviewDTO> reviews = productReviewRepository.findByProductIdAndStatusOrderByCreatedAtDesc(productId, 1)
|
||||
.stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
Double average = productReviewRepository.findAverageRatingByProductId(productId);
|
||||
Long total = productReviewRepository.countByProductId(productId);
|
||||
return new ProductReviewDTO.SummaryDTO(average == null ? 0.0 : average, total, reviews);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ProductReviewDTO updateReview(Long reviewId, ProductReviewDTO.UpdateDTO updateDTO) {
|
||||
ProductReview review = productReviewRepository.findById(reviewId)
|
||||
.orElseThrow(() -> new RuntimeException("评价不存在"));
|
||||
|
||||
if (updateDTO.getStatus() != null) {
|
||||
review.setStatus(updateDTO.getStatus());
|
||||
}
|
||||
if (updateDTO.getAdminReply() != null) {
|
||||
review.setAdminReply(updateDTO.getAdminReply());
|
||||
review.setRepliedAt(java.time.LocalDateTime.now());
|
||||
}
|
||||
|
||||
review = productReviewRepository.save(review);
|
||||
return toDTO(review);
|
||||
}
|
||||
|
||||
private ProductReviewDTO toDTO(ProductReview review) {
|
||||
ProductReviewDTO dto = new ProductReviewDTO();
|
||||
BeanUtils.copyProperties(review, dto);
|
||||
UserDTO user = userService.getUserById(review.getUserId());
|
||||
dto.setUsername(user != null ? user.getUsername() : "匿名用户");
|
||||
dto.setStatusText(review.getStatus() != null && review.getStatus() == 1 ? "显示" : "隐藏");
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import com.org.flashsalesystem.dto.UserAddressDTO;
|
||||
import com.org.flashsalesystem.entity.UserAddress;
|
||||
import com.org.flashsalesystem.repository.UserAddressRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class UserAddressService {
|
||||
|
||||
@Autowired
|
||||
private UserAddressRepository userAddressRepository;
|
||||
|
||||
public List<UserAddressDTO> getUserAddresses(Long userId) {
|
||||
return userAddressRepository.findByUserIdOrderByIsDefaultDescUpdatedAtDesc(userId)
|
||||
.stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public UserAddressDTO getDefaultAddress(Long userId) {
|
||||
return userAddressRepository.findByUserIdAndIsDefaultTrue(userId)
|
||||
.map(this::toDTO)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public UserAddressDTO createAddress(Long userId, UserAddressDTO.SaveDTO saveDTO) {
|
||||
if (Boolean.TRUE.equals(saveDTO.getIsDefault())) {
|
||||
clearDefault(userId);
|
||||
}
|
||||
|
||||
UserAddress address = new UserAddress();
|
||||
BeanUtils.copyProperties(saveDTO, address);
|
||||
address.setUserId(userId);
|
||||
|
||||
if (userAddressRepository.findByUserIdOrderByIsDefaultDescUpdatedAtDesc(userId).isEmpty()) {
|
||||
address.setIsDefault(true);
|
||||
}
|
||||
|
||||
return toDTO(userAddressRepository.save(address));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public UserAddressDTO updateAddress(Long userId, Long addressId, UserAddressDTO.SaveDTO saveDTO) {
|
||||
UserAddress address = userAddressRepository.findByIdAndUserId(addressId, userId)
|
||||
.orElseThrow(() -> new RuntimeException("地址不存在"));
|
||||
|
||||
if (Boolean.TRUE.equals(saveDTO.getIsDefault())) {
|
||||
clearDefault(userId);
|
||||
}
|
||||
|
||||
BeanUtils.copyProperties(saveDTO, address);
|
||||
address.setId(addressId);
|
||||
address.setUserId(userId);
|
||||
return toDTO(userAddressRepository.save(address));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteAddress(Long userId, Long addressId) {
|
||||
UserAddress address = userAddressRepository.findByIdAndUserId(addressId, userId)
|
||||
.orElseThrow(() -> new RuntimeException("地址不存在"));
|
||||
boolean wasDefault = Boolean.TRUE.equals(address.getIsDefault());
|
||||
userAddressRepository.delete(address);
|
||||
|
||||
if (wasDefault) {
|
||||
Optional<UserAddress> next = userAddressRepository.findByUserIdOrderByIsDefaultDescUpdatedAtDesc(userId)
|
||||
.stream()
|
||||
.findFirst();
|
||||
if (next.isPresent()) {
|
||||
UserAddress nextAddress = next.get();
|
||||
nextAddress.setIsDefault(true);
|
||||
userAddressRepository.save(nextAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public UserAddressDTO setDefault(Long userId, Long addressId) {
|
||||
UserAddress address = userAddressRepository.findByIdAndUserId(addressId, userId)
|
||||
.orElseThrow(() -> new RuntimeException("地址不存在"));
|
||||
clearDefault(userId);
|
||||
address.setIsDefault(true);
|
||||
return toDTO(userAddressRepository.save(address));
|
||||
}
|
||||
|
||||
private void clearDefault(Long userId) {
|
||||
userAddressRepository.findByUserIdOrderByIsDefaultDescUpdatedAtDesc(userId)
|
||||
.forEach(item -> {
|
||||
if (Boolean.TRUE.equals(item.getIsDefault())) {
|
||||
item.setIsDefault(false);
|
||||
userAddressRepository.save(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private UserAddressDTO toDTO(UserAddress address) {
|
||||
UserAddressDTO dto = new UserAddressDTO();
|
||||
BeanUtils.copyProperties(address, dto);
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import com.org.flashsalesystem.dto.ProductDTO;
|
||||
import com.org.flashsalesystem.dto.UserFavoriteDTO;
|
||||
import com.org.flashsalesystem.entity.UserFavorite;
|
||||
import com.org.flashsalesystem.repository.UserFavoriteRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class UserFavoriteService {
|
||||
|
||||
@Autowired
|
||||
private UserFavoriteRepository userFavoriteRepository;
|
||||
|
||||
@Autowired
|
||||
private ProductService productService;
|
||||
|
||||
public List<UserFavoriteDTO> getFavorites(Long userId) {
|
||||
return userFavoriteRepository.findByUserIdOrderByCreatedAtDesc(userId)
|
||||
.stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public boolean isFavorited(Long userId, Long productId) {
|
||||
return userFavoriteRepository.existsByUserIdAndProductId(userId, productId);
|
||||
}
|
||||
|
||||
public long getFavoriteCount(Long userId) {
|
||||
return userFavoriteRepository.countByUserId(userId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public boolean toggleFavorite(Long userId, Long productId) {
|
||||
ProductDTO product = productService.getProductById(productId);
|
||||
if (product == null) {
|
||||
throw new RuntimeException("商品不存在");
|
||||
}
|
||||
|
||||
if (userFavoriteRepository.existsByUserIdAndProductId(userId, productId)) {
|
||||
userFavoriteRepository.deleteByUserIdAndProductId(userId, productId);
|
||||
return false;
|
||||
}
|
||||
|
||||
UserFavorite favorite = new UserFavorite();
|
||||
favorite.setUserId(userId);
|
||||
favorite.setProductId(productId);
|
||||
userFavoriteRepository.save(favorite);
|
||||
return true;
|
||||
}
|
||||
|
||||
private UserFavoriteDTO toDTO(UserFavorite favorite) {
|
||||
ProductDTO product = productService.getProductById(favorite.getProductId());
|
||||
UserFavoriteDTO dto = new UserFavoriteDTO();
|
||||
dto.setId(favorite.getId());
|
||||
dto.setUserId(favorite.getUserId());
|
||||
dto.setProductId(favorite.getProductId());
|
||||
dto.setCreatedAt(favorite.getCreatedAt());
|
||||
if (product != null) {
|
||||
dto.setProductName(product.getName());
|
||||
dto.setProductImageUrl(product.getImageUrl());
|
||||
dto.setProductPrice(product.getPrice());
|
||||
dto.setProductCategory(product.getCategory());
|
||||
}
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user