From 977db8f333081d3bec65def08d0dcd9a63a3333e Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Tue, 10 Mar 2026 23:16:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=9C=B0=E5=9D=80=E3=80=81=E6=94=B6=E8=97=8F=E3=80=81=E5=95=86?= =?UTF-8?q?=E5=93=81=E8=AF=84=E4=BB=B7=E5=92=8C=E8=AE=A2=E5=8D=95=E9=A1=B9?= =?UTF-8?q?=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 UserAddress/UserFavorite/ProductReview/OrderItem 实体类 - 新增对应的 DTO、Repository、Service 和 Controller - 新增 OrderMigrationService 订单数据迁移服务 --- .../controller/ProductReviewController.java | 67 ++++++++++ .../controller/UserAddressController.java | 117 ++++++++++++++++++ .../controller/UserFavoriteController.java | 85 +++++++++++++ .../flashsalesystem/dto/ProductReviewDTO.java | 67 ++++++++++ .../flashsalesystem/dto/UserAddressDTO.java | 52 ++++++++ .../flashsalesystem/dto/UserFavoriteDTO.java | 30 +++++ .../org/flashsalesystem/entity/OrderItem.java | 50 ++++++++ .../flashsalesystem/entity/ProductReview.java | 63 ++++++++++ .../flashsalesystem/entity/UserAddress.java | 61 +++++++++ .../flashsalesystem/entity/UserFavorite.java | 36 ++++++ .../repository/OrderItemRepository.java | 22 ++++ .../repository/ProductReviewRepository.java | 23 ++++ .../repository/UserAddressRepository.java | 15 +++ .../repository/UserFavoriteRepository.java | 17 +++ .../service/OrderMigrationService.java | 63 ++++++++++ .../service/ProductReviewService.java | 100 +++++++++++++++ .../service/UserAddressService.java | 111 +++++++++++++++++ .../service/UserFavoriteService.java | 74 +++++++++++ 18 files changed, 1053 insertions(+) create mode 100644 src/main/java/com/org/flashsalesystem/controller/ProductReviewController.java create mode 100644 src/main/java/com/org/flashsalesystem/controller/UserAddressController.java create mode 100644 src/main/java/com/org/flashsalesystem/controller/UserFavoriteController.java create mode 100644 src/main/java/com/org/flashsalesystem/dto/ProductReviewDTO.java create mode 100644 src/main/java/com/org/flashsalesystem/dto/UserAddressDTO.java create mode 100644 src/main/java/com/org/flashsalesystem/dto/UserFavoriteDTO.java create mode 100644 src/main/java/com/org/flashsalesystem/entity/OrderItem.java create mode 100644 src/main/java/com/org/flashsalesystem/entity/ProductReview.java create mode 100644 src/main/java/com/org/flashsalesystem/entity/UserAddress.java create mode 100644 src/main/java/com/org/flashsalesystem/entity/UserFavorite.java create mode 100644 src/main/java/com/org/flashsalesystem/repository/OrderItemRepository.java create mode 100644 src/main/java/com/org/flashsalesystem/repository/ProductReviewRepository.java create mode 100644 src/main/java/com/org/flashsalesystem/repository/UserAddressRepository.java create mode 100644 src/main/java/com/org/flashsalesystem/repository/UserFavoriteRepository.java create mode 100644 src/main/java/com/org/flashsalesystem/service/OrderMigrationService.java create mode 100644 src/main/java/com/org/flashsalesystem/service/ProductReviewService.java create mode 100644 src/main/java/com/org/flashsalesystem/service/UserAddressService.java create mode 100644 src/main/java/com/org/flashsalesystem/service/UserFavoriteService.java diff --git a/src/main/java/com/org/flashsalesystem/controller/ProductReviewController.java b/src/main/java/com/org/flashsalesystem/controller/ProductReviewController.java new file mode 100644 index 0000000..9703f4a --- /dev/null +++ b/src/main/java/com/org/flashsalesystem/controller/ProductReviewController.java @@ -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> getProductReviews(@PathVariable Long productId) { + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "获取评价成功"); + response.put("data", productReviewService.getProductReviews(productId)); + return ResponseEntity.ok(response); + } + + @PostMapping + public ResponseEntity> createReview(@Validated @RequestBody ProductReviewDTO.CreateDTO createDTO, + HttpServletRequest request) { + Long userId = getCurrentUserId(request); + if (userId == null) { + return unauthorized(); + } + + Map 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> unauthorized() { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "用户未登录或登录已过期"); + return ResponseEntity.status(401).body(response); + } +} diff --git a/src/main/java/com/org/flashsalesystem/controller/UserAddressController.java b/src/main/java/com/org/flashsalesystem/controller/UserAddressController.java new file mode 100644 index 0000000..8adf9d1 --- /dev/null +++ b/src/main/java/com/org/flashsalesystem/controller/UserAddressController.java @@ -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> getAddresses(HttpServletRequest request) { + Long userId = getCurrentUserId(request); + if (userId == null) { + return unauthorized(); + } + + List data = userAddressService.getUserAddresses(userId); + return ok(data, "获取地址成功"); + } + + @GetMapping("/default") + public ResponseEntity> getDefaultAddress(HttpServletRequest request) { + Long userId = getCurrentUserId(request); + if (userId == null) { + return unauthorized(); + } + + return ok(userAddressService.getDefaultAddress(userId), "获取默认地址成功"); + } + + @PostMapping + public ResponseEntity> 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> 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> 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> 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> ok(Object data, String message) { + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", message); + response.put("data", data); + return ResponseEntity.ok(response); + } + + private ResponseEntity> unauthorized() { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "用户未登录或登录已过期"); + return ResponseEntity.status(401).body(response); + } +} diff --git a/src/main/java/com/org/flashsalesystem/controller/UserFavoriteController.java b/src/main/java/com/org/flashsalesystem/controller/UserFavoriteController.java new file mode 100644 index 0000000..456ea6a --- /dev/null +++ b/src/main/java/com/org/flashsalesystem/controller/UserFavoriteController.java @@ -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> getFavorites(HttpServletRequest request) { + Long userId = getCurrentUserId(request); + if (userId == null) return unauthorized(); + return ok(userFavoriteService.getFavorites(userId), "获取收藏列表成功"); + } + + @GetMapping("/count") + public ResponseEntity> getCount(HttpServletRequest request) { + Long userId = getCurrentUserId(request); + if (userId == null) return unauthorized(); + Map data = new HashMap<>(); + data.put("count", userFavoriteService.getFavoriteCount(userId)); + return ok(data, "获取收藏数量成功"); + } + + @GetMapping("/check") + public ResponseEntity> checkFavorite(@RequestParam Long productId, HttpServletRequest request) { + Long userId = getCurrentUserId(request); + if (userId == null) return unauthorized(); + Map data = new HashMap<>(); + data.put("favorited", userFavoriteService.isFavorited(userId, productId)); + return ok(data, "获取收藏状态成功"); + } + + @PostMapping("/toggle") + public ResponseEntity> 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 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> ok(Object data, String message) { + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", message); + response.put("data", data); + return ResponseEntity.ok(response); + } + + private ResponseEntity> unauthorized() { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "用户未登录或登录已过期"); + return ResponseEntity.status(401).body(response); + } +} diff --git a/src/main/java/com/org/flashsalesystem/dto/ProductReviewDTO.java b/src/main/java/com/org/flashsalesystem/dto/ProductReviewDTO.java new file mode 100644 index 0000000..4136a15 --- /dev/null +++ b/src/main/java/com/org/flashsalesystem/dto/ProductReviewDTO.java @@ -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 reviews; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class UpdateDTO { + private Integer status; + private String adminReply; + } +} diff --git a/src/main/java/com/org/flashsalesystem/dto/UserAddressDTO.java b/src/main/java/com/org/flashsalesystem/dto/UserAddressDTO.java new file mode 100644 index 0000000..9b19f9f --- /dev/null +++ b/src/main/java/com/org/flashsalesystem/dto/UserAddressDTO.java @@ -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; + } +} diff --git a/src/main/java/com/org/flashsalesystem/dto/UserFavoriteDTO.java b/src/main/java/com/org/flashsalesystem/dto/UserFavoriteDTO.java new file mode 100644 index 0000000..f1ce2fd --- /dev/null +++ b/src/main/java/com/org/flashsalesystem/dto/UserFavoriteDTO.java @@ -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; + } +} diff --git a/src/main/java/com/org/flashsalesystem/entity/OrderItem.java b/src/main/java/com/org/flashsalesystem/entity/OrderItem.java new file mode 100644 index 0000000..aab5ed3 --- /dev/null +++ b/src/main/java/com/org/flashsalesystem/entity/OrderItem.java @@ -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(); + } +} diff --git a/src/main/java/com/org/flashsalesystem/entity/ProductReview.java b/src/main/java/com/org/flashsalesystem/entity/ProductReview.java new file mode 100644 index 0000000..03e34ff --- /dev/null +++ b/src/main/java/com/org/flashsalesystem/entity/ProductReview.java @@ -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(); + } +} diff --git a/src/main/java/com/org/flashsalesystem/entity/UserAddress.java b/src/main/java/com/org/flashsalesystem/entity/UserAddress.java new file mode 100644 index 0000000..dcb756d --- /dev/null +++ b/src/main/java/com/org/flashsalesystem/entity/UserAddress.java @@ -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(); + } +} diff --git a/src/main/java/com/org/flashsalesystem/entity/UserFavorite.java b/src/main/java/com/org/flashsalesystem/entity/UserFavorite.java new file mode 100644 index 0000000..b074fd5 --- /dev/null +++ b/src/main/java/com/org/flashsalesystem/entity/UserFavorite.java @@ -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(); + } +} diff --git a/src/main/java/com/org/flashsalesystem/repository/OrderItemRepository.java b/src/main/java/com/org/flashsalesystem/repository/OrderItemRepository.java new file mode 100644 index 0000000..952b08b --- /dev/null +++ b/src/main/java/com/org/flashsalesystem/repository/OrderItemRepository.java @@ -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 { + List 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); +} diff --git a/src/main/java/com/org/flashsalesystem/repository/ProductReviewRepository.java b/src/main/java/com/org/flashsalesystem/repository/ProductReviewRepository.java new file mode 100644 index 0000000..92ce6c6 --- /dev/null +++ b/src/main/java/com/org/flashsalesystem/repository/ProductReviewRepository.java @@ -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 { + List findByProductIdOrderByCreatedAtDesc(Long productId); + List findByProductIdAndStatusOrderByCreatedAtDesc(Long productId, Integer status); + Optional 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); +} diff --git a/src/main/java/com/org/flashsalesystem/repository/UserAddressRepository.java b/src/main/java/com/org/flashsalesystem/repository/UserAddressRepository.java new file mode 100644 index 0000000..2357bf9 --- /dev/null +++ b/src/main/java/com/org/flashsalesystem/repository/UserAddressRepository.java @@ -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 { + List findByUserIdOrderByIsDefaultDescUpdatedAtDesc(Long userId); + Optional findByUserIdAndIsDefaultTrue(Long userId); + Optional findByIdAndUserId(Long id, Long userId); +} diff --git a/src/main/java/com/org/flashsalesystem/repository/UserFavoriteRepository.java b/src/main/java/com/org/flashsalesystem/repository/UserFavoriteRepository.java new file mode 100644 index 0000000..5f56317 --- /dev/null +++ b/src/main/java/com/org/flashsalesystem/repository/UserFavoriteRepository.java @@ -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 { + List findByUserIdOrderByCreatedAtDesc(Long userId); + Optional findByUserIdAndProductId(Long userId, Long productId); + boolean existsByUserIdAndProductId(Long userId, Long productId); + long countByUserId(Long userId); + void deleteByUserIdAndProductId(Long userId, Long productId); +} diff --git a/src/main/java/com/org/flashsalesystem/service/OrderMigrationService.java b/src/main/java/com/org/flashsalesystem/service/OrderMigrationService.java new file mode 100644 index 0000000..0dc1c04 --- /dev/null +++ b/src/main/java/com/org/flashsalesystem/service/OrderMigrationService.java @@ -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 migrateLegacyOrderItems() { + List 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 result = new HashMap<>(); + result.put("totalOrders", orders.size()); + result.put("migrated", migrated); + result.put("skipped", skipped); + return result; + } +} diff --git a/src/main/java/com/org/flashsalesystem/service/ProductReviewService.java b/src/main/java/com/org/flashsalesystem/service/ProductReviewService.java new file mode 100644 index 0000000..24ae04d --- /dev/null +++ b/src/main/java/com/org/flashsalesystem/service/ProductReviewService.java @@ -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 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 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; + } +} diff --git a/src/main/java/com/org/flashsalesystem/service/UserAddressService.java b/src/main/java/com/org/flashsalesystem/service/UserAddressService.java new file mode 100644 index 0000000..3a114b5 --- /dev/null +++ b/src/main/java/com/org/flashsalesystem/service/UserAddressService.java @@ -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 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 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; + } +} diff --git a/src/main/java/com/org/flashsalesystem/service/UserFavoriteService.java b/src/main/java/com/org/flashsalesystem/service/UserFavoriteService.java new file mode 100644 index 0000000..06a41ba --- /dev/null +++ b/src/main/java/com/org/flashsalesystem/service/UserFavoriteService.java @@ -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 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; + } +}