feat: 新增用户地址、收藏、商品评价和订单项模块

- 新增 UserAddress/UserFavorite/ProductReview/OrderItem 实体类
- 新增对应的 DTO、Repository、Service 和 Controller
- 新增 OrderMigrationService 订单数据迁移服务
This commit is contained in:
2026-03-10 23:16:57 +08:00
parent 371884a3d1
commit 977db8f333
18 changed files with 1053 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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