feat: 删除JSP视图层,完善评价和通知系统,新增拼团模块
- 删除所有 JSP 页面(20个文件),前端完全迁移至 Vue 3 SPA - 完善评价系统:ReviewDialog 组件、用户评价历史页、评价状态检查API - 新增通知系统:Notification 实体/仓库/服务/控制器,NotificationCenter 接入真实API - 新增拼团模块:GroupBuying 全套后端和前端页面 - 修复 review check API 参数双重包装导致请求格式错误 - 修复通知 API 路径缺少 /api 前缀和响应格式处理 - MessageListenerService 集成 NotificationService 创建持久化通知 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,10 @@ package com.org.flashsalesystem;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
public class FlashSaleSystemApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@@ -292,4 +292,16 @@ public class RedissonConfig {
|
||||
log.info("加载购物车操作Lua脚本");
|
||||
return script;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼团库存扣减Lua脚本
|
||||
*/
|
||||
@Bean
|
||||
public DefaultRedisScript<Long> groupBuyingStockScript() {
|
||||
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
|
||||
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/groupbuying_stock.lua")));
|
||||
script.setResultType(Long.class);
|
||||
log.info("加载拼团库存扣减Lua脚本");
|
||||
return script;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +150,31 @@ public class FlashSaleController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取秒杀活动统计信息
|
||||
*/
|
||||
@GetMapping("/statistics")
|
||||
public ResponseEntity<Map<String, Object>> getFlashSaleStatistics(HttpServletRequest request) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(request);
|
||||
Map<String, Object> stats = flashSaleService.getFlashSaleStatistics(userId);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", stats);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取秒杀统计失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取秒杀活动详情
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
package com.org.flashsalesystem.controller;
|
||||
|
||||
import com.org.flashsalesystem.dto.GroupBuyingDTO;
|
||||
import com.org.flashsalesystem.dto.UserDTO;
|
||||
import com.org.flashsalesystem.service.GroupBuyingService;
|
||||
import com.org.flashsalesystem.service.UserService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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;
|
||||
|
||||
@Tag(name = "拼团管理", description = "拼团活动创建、参与、团组管理等接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/groupbuying")
|
||||
@Slf4j
|
||||
public class GroupBuyingController {
|
||||
|
||||
@Autowired
|
||||
private GroupBuyingService groupBuyingService;
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
// ========== 用户端 ==========
|
||||
|
||||
@Operation(summary = "获取拼团活动列表")
|
||||
@GetMapping("/list")
|
||||
public ResponseEntity<Map<String, Object>> getList(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "10") int size,
|
||||
@RequestParam(required = false) Integer status) {
|
||||
try {
|
||||
Map<String, Object> result = groupBuyingService.getGroupBuyingList(page, size, status);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", result);
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取拼团活动列表失败", e);
|
||||
return badRequest(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "获取拼团活动详情")
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<Map<String, Object>> getDetail(@PathVariable Long id) {
|
||||
try {
|
||||
GroupBuyingDTO detail = groupBuyingService.getGroupBuyingDetail(id);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", detail);
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取拼团活动详情失败", e);
|
||||
return badRequest(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "获取活动下的团组列表")
|
||||
@GetMapping("/{id}/groups")
|
||||
public ResponseEntity<Map<String, Object>> getGroups(
|
||||
@PathVariable Long id,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "10") int size) {
|
||||
try {
|
||||
Map<String, Object> result = groupBuyingService.getGroupsByActivity(id, page, size);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", result);
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取团组列表失败", e);
|
||||
return badRequest(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "参与拼团")
|
||||
@PostMapping("/join")
|
||||
public ResponseEntity<Map<String, Object>> joinGroup(
|
||||
@Validated @RequestBody GroupBuyingDTO.JoinGroupDTO joinDTO,
|
||||
HttpServletRequest request) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return createUnauthorizedResponse();
|
||||
}
|
||||
|
||||
GroupBuyingDTO.JoinResultDTO result = groupBuyingService.joinGroupBuying(joinDTO, userId);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", result.getSuccess());
|
||||
response.put("message", result.getMessage());
|
||||
response.put("data", result);
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("参与拼团失败", e);
|
||||
return badRequest(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "获取团组详情")
|
||||
@GetMapping("/group/{groupId}")
|
||||
public ResponseEntity<Map<String, Object>> getGroupDetail(@PathVariable Long groupId) {
|
||||
try {
|
||||
GroupBuyingDTO.GroupInfoDTO detail = groupBuyingService.getGroupDetail(groupId);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", detail);
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取团组详情失败", e);
|
||||
return badRequest(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "退出团组")
|
||||
@PostMapping("/group/{groupId}/cancel")
|
||||
public ResponseEntity<Map<String, Object>> cancelMembership(
|
||||
@PathVariable Long groupId,
|
||||
HttpServletRequest request) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return createUnauthorizedResponse();
|
||||
}
|
||||
|
||||
groupBuyingService.cancelMembership(groupId, userId);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "已退出团组");
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("退出团组失败", e);
|
||||
return badRequest(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "我的团组")
|
||||
@GetMapping("/my-groups")
|
||||
public ResponseEntity<Map<String, Object>> getMyGroups(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "10") int size,
|
||||
HttpServletRequest request) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return createUnauthorizedResponse();
|
||||
}
|
||||
|
||||
Map<String, Object> result = groupBuyingService.getMyGroups(userId, page, size);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", result);
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取我的团组失败", e);
|
||||
return badRequest(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "拼团统计数据")
|
||||
@GetMapping("/statistics")
|
||||
public ResponseEntity<Map<String, Object>> getStatistics(HttpServletRequest request) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(request);
|
||||
GroupBuyingDTO.StatisticsDTO stats = groupBuyingService.getStatistics(userId);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", stats);
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取拼团统计失败", e);
|
||||
return badRequest(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 管理员端 ==========
|
||||
|
||||
@Operation(summary = "创建拼团活动")
|
||||
@PostMapping("/admin/create")
|
||||
public ResponseEntity<Map<String, Object>> create(
|
||||
@Validated @RequestBody GroupBuyingDTO.CreateDTO createDTO) {
|
||||
try {
|
||||
GroupBuyingDTO result = groupBuyingService.createGroupBuying(createDTO);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "拼团活动创建成功");
|
||||
response.put("data", result);
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("创建拼团活动失败", e);
|
||||
return badRequest(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "更新拼团活动")
|
||||
@PutMapping("/admin/{id}")
|
||||
public ResponseEntity<Map<String, Object>> update(
|
||||
@PathVariable Long id,
|
||||
@Validated @RequestBody GroupBuyingDTO.UpdateDTO updateDTO) {
|
||||
try {
|
||||
GroupBuyingDTO result = groupBuyingService.updateGroupBuying(id, updateDTO);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "拼团活动更新成功");
|
||||
response.put("data", result);
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("更新拼团活动失败", e);
|
||||
return badRequest(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "删除拼团活动")
|
||||
@DeleteMapping("/admin/{id}")
|
||||
public ResponseEntity<Map<String, Object>> delete(@PathVariable Long id) {
|
||||
try {
|
||||
boolean success = groupBuyingService.deleteGroupBuying(id);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", success);
|
||||
response.put("message", success ? "拼团活动删除成功" : "拼团活动删除失败");
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("删除拼团活动失败", e);
|
||||
return badRequest(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "预热所有拼团活动库存")
|
||||
@PostMapping("/admin/preload-all")
|
||||
public ResponseEntity<Map<String, Object>> preloadAll() {
|
||||
try {
|
||||
groupBuyingService.preloadAllActiveStock();
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "拼团活动库存预热完成");
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("预热拼团库存失败", e);
|
||||
return badRequest(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 工具方法 ==========
|
||||
|
||||
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>> createUnauthorizedResponse() {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "用户未登录或登录已过期");
|
||||
return ResponseEntity.status(401).body(response);
|
||||
}
|
||||
|
||||
private ResponseEntity<Map<String, Object>> badRequest(String message) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", message);
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.org.flashsalesystem.controller;
|
||||
|
||||
import com.org.flashsalesystem.dto.UserDTO;
|
||||
import com.org.flashsalesystem.service.NotificationService;
|
||||
import com.org.flashsalesystem.service.UserService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
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/notification")
|
||||
public class NotificationController {
|
||||
|
||||
@Autowired
|
||||
private NotificationService notificationService;
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@GetMapping("/list")
|
||||
public ResponseEntity<Map<String, Object>> getNotifications(
|
||||
@RequestParam(required = false) String type,
|
||||
HttpServletRequest request) {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
if (type != null && !type.isEmpty()) {
|
||||
response.put("data", notificationService.getUserNotificationsByType(userId, type));
|
||||
} else {
|
||||
response.put("data", notificationService.getUserNotifications(userId));
|
||||
}
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@GetMapping("/unread-count")
|
||||
public ResponseEntity<Map<String, Object>> getUnreadCount(HttpServletRequest request) {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", notificationService.getUnreadCount(userId));
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/read")
|
||||
public ResponseEntity<Map<String, Object>> markAsRead(@PathVariable Long id,
|
||||
HttpServletRequest request) {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
notificationService.markAsRead(id, userId);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "已标记为已读");
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@PutMapping("/read-all")
|
||||
public ResponseEntity<Map<String, Object>> markAllAsRead(HttpServletRequest request) {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
notificationService.markAllAsRead(userId);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "已全部标记为已读");
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@DeleteMapping("/clear")
|
||||
public ResponseEntity<Map<String, Object>> clearAll(HttpServletRequest request) {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
notificationService.clearAll(userId);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "已清空所有通知");
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ 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
|
||||
@@ -41,10 +42,52 @@ public class ProductReviewController {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
try {
|
||||
response.put("success", true);
|
||||
response.put("message", "评价提交成功");
|
||||
response.put("data", productReviewService.createReview(userId, createDTO));
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (RuntimeException e) {
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/check")
|
||||
public ResponseEntity<Map<String, Object>> checkReview(@RequestParam Long orderId,
|
||||
@RequestParam Long productId) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "评价提交成功");
|
||||
response.put("data", productReviewService.createReview(userId, createDTO));
|
||||
response.put("data", productReviewService.checkReviewStatus(orderId, productId));
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@GetMapping("/my")
|
||||
public ResponseEntity<Map<String, Object>> getMyReviews(HttpServletRequest request) {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", productReviewService.getUserReviews(userId));
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@GetMapping("/order/{orderId}")
|
||||
public ResponseEntity<Map<String, Object>> getOrderReviews(@PathVariable Long orderId,
|
||||
HttpServletRequest request) {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", productReviewService.getOrderReviews(orderId));
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
|
||||
173
src/main/java/com/org/flashsalesystem/dto/GroupBuyingDTO.java
Normal file
173
src/main/java/com/org/flashsalesystem/dto/GroupBuyingDTO.java
Normal file
@@ -0,0 +1,173 @@
|
||||
package com.org.flashsalesystem.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.validation.constraints.DecimalMin;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class GroupBuyingDTO {
|
||||
|
||||
private Long id;
|
||||
private Long productId;
|
||||
private String productName;
|
||||
private String productImageUrl;
|
||||
private BigDecimal productPrice;
|
||||
private BigDecimal groupPrice;
|
||||
private Integer requiredMembers;
|
||||
private Integer durationMinutes;
|
||||
private Integer totalStock;
|
||||
private Integer remainingStock;
|
||||
private Integer maxPerUser;
|
||||
private Integer status;
|
||||
private String statusDescription;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime startTime;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime endTime;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
private Integer activeGroupCount;
|
||||
private BigDecimal discount;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class CreateDTO {
|
||||
@NotNull(message = "商品ID不能为空")
|
||||
private Long productId;
|
||||
|
||||
@NotNull(message = "拼团价格不能为空")
|
||||
@DecimalMin(value = "0.01", message = "拼团价格必须大于0")
|
||||
private BigDecimal groupPrice;
|
||||
|
||||
@Min(value = 2, message = "成团人数至少为2人")
|
||||
private Integer requiredMembers = 2;
|
||||
|
||||
@Min(value = 1, message = "有效期至少为1分钟")
|
||||
private Integer durationMinutes = 1440;
|
||||
|
||||
@Min(value = 1, message = "总库存至少为1")
|
||||
private Integer totalStock;
|
||||
|
||||
@Min(value = 1, message = "每人限购至少为1")
|
||||
private Integer maxPerUser = 1;
|
||||
|
||||
@NotNull(message = "开始时间不能为空")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime startTime;
|
||||
|
||||
@NotNull(message = "结束时间不能为空")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime endTime;
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class UpdateDTO {
|
||||
private Long productId;
|
||||
private BigDecimal groupPrice;
|
||||
private Integer requiredMembers;
|
||||
private Integer durationMinutes;
|
||||
private Integer totalStock;
|
||||
private Integer maxPerUser;
|
||||
private Integer status;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime startTime;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime endTime;
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class JoinGroupDTO {
|
||||
@NotNull(message = "拼团活动ID不能为空")
|
||||
private Long groupBuyingId;
|
||||
|
||||
private Long groupId;
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class GroupInfoDTO {
|
||||
private Long id;
|
||||
private String groupNo;
|
||||
private Long groupBuyingId;
|
||||
private Long leaderUserId;
|
||||
private String leaderUsername;
|
||||
private Integer requiredMembers;
|
||||
private Integer currentMembers;
|
||||
private Integer status;
|
||||
private String statusDescription;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime expireTime;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime completedAt;
|
||||
|
||||
private List<MemberDTO> members;
|
||||
private GroupBuyingDTO groupBuying;
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class MemberDTO {
|
||||
private Long id;
|
||||
private Long userId;
|
||||
private String username;
|
||||
private String avatar;
|
||||
private Long orderId;
|
||||
private Integer status;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime joinedAt;
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class JoinResultDTO {
|
||||
private Boolean success;
|
||||
private String message;
|
||||
private Long groupId;
|
||||
private String groupNo;
|
||||
private Long orderId;
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class StatisticsDTO {
|
||||
private Long totalActivities;
|
||||
private Long activeActivities;
|
||||
private Long myGroups;
|
||||
private Long successGroups;
|
||||
private BigDecimal totalSaved;
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ public class OrderDTO {
|
||||
private Long userId;
|
||||
private String username;
|
||||
private Long productId;
|
||||
private Long flashSaleId;
|
||||
private String productName;
|
||||
private String productImageUrl;
|
||||
private Integer quantity;
|
||||
|
||||
@@ -20,6 +20,8 @@ public class ProductReviewDTO {
|
||||
private Long userId;
|
||||
private Long orderId;
|
||||
private String username;
|
||||
private String productName;
|
||||
private String productImage;
|
||||
private Integer rating;
|
||||
private String content;
|
||||
private Integer status;
|
||||
@@ -64,4 +66,12 @@ public class ProductReviewDTO {
|
||||
private Integer status;
|
||||
private String adminReply;
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class CheckDTO {
|
||||
private boolean reviewed;
|
||||
private ProductReviewDTO review;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
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 = "group_buying")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class GroupBuying {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "product_id", nullable = false)
|
||||
private Long productId;
|
||||
|
||||
@Column(name = "group_price", nullable = false, precision = 10, scale = 2)
|
||||
private BigDecimal groupPrice;
|
||||
|
||||
@Column(name = "required_members", nullable = false)
|
||||
private Integer requiredMembers = 2;
|
||||
|
||||
@Column(name = "duration_minutes", nullable = false)
|
||||
private Integer durationMinutes = 1440;
|
||||
|
||||
@Column(name = "total_stock", nullable = false)
|
||||
private Integer totalStock;
|
||||
|
||||
@Column(name = "remaining_stock", nullable = false)
|
||||
private Integer remainingStock;
|
||||
|
||||
@Column(name = "max_per_user", nullable = false)
|
||||
private Integer maxPerUser = 1;
|
||||
|
||||
/**
|
||||
* 状态:0-草稿 1-未开始 2-进行中 3-已结束
|
||||
*/
|
||||
@Column(nullable = false)
|
||||
private Integer status = 0;
|
||||
|
||||
@Column(name = "start_time", nullable = false)
|
||||
private LocalDateTime startTime;
|
||||
|
||||
@Column(name = "end_time", nullable = false)
|
||||
private LocalDateTime endTime;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "product_id", insertable = false, updatable = false)
|
||||
private Product product;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
return now.isAfter(startTime) && now.isBefore(endTime) && status == 2;
|
||||
}
|
||||
|
||||
public enum GroupBuyingStatus {
|
||||
DRAFT(0, "草稿"),
|
||||
PENDING(1, "未开始"),
|
||||
ACTIVE(2, "进行中"),
|
||||
ENDED(3, "已结束");
|
||||
|
||||
private final int code;
|
||||
private final String description;
|
||||
|
||||
GroupBuyingStatus(int code, String description) {
|
||||
this.code = code;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public int getCode() { return code; }
|
||||
public String getDescription() { return description; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.org.flashsalesystem.entity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "group_buying_group")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class GroupBuyingGroup {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "group_no", nullable = false, unique = true, length = 64)
|
||||
private String groupNo;
|
||||
|
||||
@Column(name = "group_buying_id", nullable = false)
|
||||
private Long groupBuyingId;
|
||||
|
||||
@Column(name = "leader_user_id", nullable = false)
|
||||
private Long leaderUserId;
|
||||
|
||||
@Column(name = "required_members", nullable = false)
|
||||
private Integer requiredMembers;
|
||||
|
||||
@Column(name = "current_members", nullable = false)
|
||||
private Integer currentMembers = 1;
|
||||
|
||||
/**
|
||||
* 状态:1-拼团中 2-已成团 3-已失败(超时)
|
||||
*/
|
||||
@Column(nullable = false)
|
||||
private Integer status = 1;
|
||||
|
||||
@Column(name = "expire_time", nullable = false)
|
||||
private LocalDateTime expireTime;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "completed_at")
|
||||
private LocalDateTime completedAt;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "group_buying_id", insertable = false, updatable = false)
|
||||
private GroupBuying groupBuying;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "leader_user_id", insertable = false, updatable = false)
|
||||
private User leader;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public enum GroupStatus {
|
||||
FORMING(1, "拼团中"),
|
||||
SUCCESS(2, "已成团"),
|
||||
FAILED(3, "已失败");
|
||||
|
||||
private final int code;
|
||||
private final String description;
|
||||
|
||||
GroupStatus(int code, String description) {
|
||||
this.code = code;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public int getCode() { return code; }
|
||||
public String getDescription() { return description; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.org.flashsalesystem.entity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "group_buying_member", uniqueConstraints = {
|
||||
@UniqueConstraint(name = "uk_group_user", columnNames = {"group_id", "user_id"})
|
||||
})
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class GroupBuyingMember {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "group_id", nullable = false)
|
||||
private Long groupId;
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@Column(name = "order_id")
|
||||
private Long orderId;
|
||||
|
||||
/**
|
||||
* 状态:1-已加入 2-已成团 3-已退出
|
||||
*/
|
||||
@Column(nullable = false)
|
||||
private Integer status = 1;
|
||||
|
||||
@Column(name = "joined_at", nullable = false, updatable = false)
|
||||
private LocalDateTime joinedAt;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "group_id", insertable = false, updatable = false)
|
||||
private GroupBuyingGroup group;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "user_id", insertable = false, updatable = false)
|
||||
private User user;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
joinedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.org.flashsalesystem.entity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "notifications", indexes = {
|
||||
@Index(name = "idx_notification_user_read", columnList = "user_id, is_read"),
|
||||
@Index(name = "idx_notification_user_created", columnList = "user_id, created_at")
|
||||
})
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class Notification {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@Column(nullable = false, length = 32)
|
||||
private String type;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String title;
|
||||
|
||||
@Column(nullable = false, columnDefinition = "TEXT")
|
||||
private String message;
|
||||
|
||||
@Column(length = 255)
|
||||
private String link;
|
||||
|
||||
@Column(name = "is_read", nullable = false)
|
||||
private Boolean read = false;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,12 @@ public class Order {
|
||||
@Column(name = "product_id", nullable = false)
|
||||
private Long productId;
|
||||
|
||||
@Column(name = "flash_sale_id")
|
||||
private Long flashSaleId;
|
||||
|
||||
@Column(name = "group_buying_group_id")
|
||||
private Long groupBuyingGroupId;
|
||||
|
||||
@Min(value = 1, message = "商品数量必须大于0")
|
||||
@Column(nullable = false)
|
||||
private Integer quantity;
|
||||
@@ -145,7 +151,8 @@ public class Order {
|
||||
*/
|
||||
public enum OrderType {
|
||||
NORMAL(1, "普通订单"),
|
||||
FLASH_SALE(2, "秒杀订单");
|
||||
FLASH_SALE(2, "秒杀订单"),
|
||||
GROUP_BUYING(3, "拼团订单");
|
||||
|
||||
private final int code;
|
||||
private final String description;
|
||||
|
||||
@@ -11,7 +11,6 @@ import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 秒杀活动数据访问层
|
||||
@@ -19,16 +18,18 @@ import java.util.Optional;
|
||||
@Repository
|
||||
public interface FlashSaleRepository extends JpaRepository<FlashSale, Long> {
|
||||
|
||||
/**
|
||||
* 根据商品ID查找秒杀活动
|
||||
*/
|
||||
Optional<FlashSale> findByProductId(Long productId);
|
||||
|
||||
/**
|
||||
* 分页查找指定商品的秒杀活动
|
||||
*/
|
||||
Page<FlashSale> findByProductId(Long productId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 查找指定时间点覆盖的商品秒杀活动
|
||||
*/
|
||||
@Query("SELECT f FROM FlashSale f WHERE f.productId = :productId AND f.startTime <= :targetTime AND f.endTime >= :targetTime")
|
||||
List<FlashSale> findByProductIdAndCoveringTime(@Param("productId") Long productId,
|
||||
@Param("targetTime") LocalDateTime targetTime);
|
||||
|
||||
/**
|
||||
* 根据商品ID和状态查找秒杀活动
|
||||
*/
|
||||
@@ -78,6 +79,13 @@ public interface FlashSaleRepository extends JpaRepository<FlashSale, Long> {
|
||||
" >= :quantity")
|
||||
int updateFlashStock(@Param("flashSaleId") Long flashSaleId, @Param("quantity") Integer quantity);
|
||||
|
||||
/**
|
||||
* 恢复秒杀库存(订单取消时使用)
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE FlashSale f SET f.flashStock = f.flashStock + :quantity WHERE f.id = :flashSaleId")
|
||||
int increaseFlashStock(@Param("flashSaleId") Long flashSaleId, @Param("quantity") Integer quantity);
|
||||
|
||||
/**
|
||||
* 更新秒杀活动状态
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.org.flashsalesystem.repository;
|
||||
|
||||
import com.org.flashsalesystem.entity.GroupBuyingGroup;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface GroupBuyingGroupRepository extends JpaRepository<GroupBuyingGroup, Long> {
|
||||
|
||||
List<GroupBuyingGroup> findByGroupBuyingIdAndStatus(Long groupBuyingId, Integer status);
|
||||
|
||||
Optional<GroupBuyingGroup> findByGroupNo(String groupNo);
|
||||
|
||||
@Query("SELECT g FROM GroupBuyingGroup g WHERE g.status = 1 AND g.expireTime < :now")
|
||||
List<GroupBuyingGroup> findExpiredGroups(@Param("now") LocalDateTime now);
|
||||
|
||||
List<GroupBuyingGroup> findByLeaderUserId(Long userId);
|
||||
|
||||
Page<GroupBuyingGroup> findByGroupBuyingId(Long groupBuyingId, Pageable pageable);
|
||||
|
||||
@Query("SELECT g FROM GroupBuyingGroup g WHERE g.id IN " +
|
||||
"(SELECT m.groupId FROM GroupBuyingMember m WHERE m.userId = :userId AND m.status != 3)")
|
||||
Page<GroupBuyingGroup> findByMemberUserId(@Param("userId") Long userId, Pageable pageable);
|
||||
|
||||
long countByGroupBuyingIdAndStatus(Long groupBuyingId, Integer status);
|
||||
|
||||
@Modifying
|
||||
@Query("UPDATE GroupBuyingGroup g SET g.currentMembers = g.currentMembers + 1 WHERE g.id = :id")
|
||||
int incrementCurrentMembers(@Param("id") Long id);
|
||||
|
||||
@Modifying
|
||||
@Query("UPDATE GroupBuyingGroup g SET g.currentMembers = g.currentMembers - 1 WHERE g.id = :id AND g.currentMembers > 0")
|
||||
int decrementCurrentMembers(@Param("id") Long id);
|
||||
|
||||
@Modifying
|
||||
@Query("UPDATE GroupBuyingGroup g SET g.status = :status, g.completedAt = :completedAt WHERE g.id = :id")
|
||||
int updateStatusAndCompletedAt(@Param("id") Long id, @Param("status") Integer status, @Param("completedAt") LocalDateTime completedAt);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.org.flashsalesystem.repository;
|
||||
|
||||
import com.org.flashsalesystem.entity.GroupBuyingMember;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
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 GroupBuyingMemberRepository extends JpaRepository<GroupBuyingMember, Long> {
|
||||
|
||||
List<GroupBuyingMember> findByGroupId(Long groupId);
|
||||
|
||||
List<GroupBuyingMember> findByGroupIdAndStatus(Long groupId, Integer status);
|
||||
|
||||
boolean existsByGroupIdAndUserIdAndStatusNot(Long groupId, Long userId, Integer excludeStatus);
|
||||
|
||||
Optional<GroupBuyingMember> findByGroupIdAndUserId(Long groupId, Long userId);
|
||||
|
||||
long countByGroupIdAndStatusNot(Long groupId, Integer excludeStatus);
|
||||
|
||||
@Modifying
|
||||
@Query("UPDATE GroupBuyingMember m SET m.status = :status WHERE m.groupId = :groupId AND m.status = 1")
|
||||
int updateStatusByGroupId(@Param("groupId") Long groupId, @Param("status") Integer status);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.org.flashsalesystem.repository;
|
||||
|
||||
import com.org.flashsalesystem.entity.GroupBuying;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface GroupBuyingRepository extends JpaRepository<GroupBuying, Long> {
|
||||
|
||||
@Query("SELECT g FROM GroupBuying g WHERE g.startTime <= :now AND g.endTime > :now AND g.status = 2")
|
||||
List<GroupBuying> findActiveGroupBuyings(@Param("now") LocalDateTime now);
|
||||
|
||||
@Query("SELECT g FROM GroupBuying g WHERE g.startTime <= :now AND g.endTime > :now AND g.status = 2")
|
||||
Page<GroupBuying> findActiveGroupBuyings(@Param("now") LocalDateTime now, Pageable pageable);
|
||||
|
||||
@Query("SELECT g FROM GroupBuying g WHERE g.startTime > :now AND g.status = 1")
|
||||
List<GroupBuying> findUpcomingGroupBuyings(@Param("now") LocalDateTime now);
|
||||
|
||||
@Query("SELECT g FROM GroupBuying g WHERE g.endTime <= :now OR g.status = 3")
|
||||
Page<GroupBuying> findEndedGroupBuyings(@Param("now") LocalDateTime now, Pageable pageable);
|
||||
|
||||
Page<GroupBuying> findByStatus(Integer status, Pageable pageable);
|
||||
|
||||
@Modifying
|
||||
@Query("UPDATE GroupBuying g SET g.remainingStock = g.remainingStock - :quantity WHERE g.id = :id AND g.remainingStock >= :quantity")
|
||||
int updateStock(@Param("id") Long id, @Param("quantity") Integer quantity);
|
||||
|
||||
@Modifying
|
||||
@Query("UPDATE GroupBuying g SET g.remainingStock = g.remainingStock + :quantity WHERE g.id = :id")
|
||||
int increaseStock(@Param("id") Long id, @Param("quantity") Integer quantity);
|
||||
|
||||
@Modifying
|
||||
@Query("UPDATE GroupBuying g SET g.status = :status WHERE g.id = :id")
|
||||
int updateStatus(@Param("id") Long id, @Param("status") Integer status);
|
||||
|
||||
long countByStatus(Integer status);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.org.flashsalesystem.repository;
|
||||
|
||||
import com.org.flashsalesystem.entity.Notification;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface NotificationRepository extends JpaRepository<Notification, Long> {
|
||||
|
||||
List<Notification> findByUserIdOrderByCreatedAtDesc(Long userId);
|
||||
|
||||
List<Notification> findByUserIdAndTypeOrderByCreatedAtDesc(Long userId, String type);
|
||||
|
||||
long countByUserIdAndReadFalse(Long userId);
|
||||
|
||||
@Modifying
|
||||
@Query("UPDATE Notification n SET n.read = true WHERE n.userId = :userId AND n.read = false")
|
||||
int markAllAsRead(@Param("userId") Long userId);
|
||||
|
||||
@Modifying
|
||||
@Query("UPDATE Notification n SET n.read = true WHERE n.id = :id AND n.userId = :userId")
|
||||
int markAsRead(@Param("id") Long id, @Param("userId") Long userId);
|
||||
|
||||
void deleteByUserId(Long userId);
|
||||
}
|
||||
@@ -49,6 +49,23 @@ public interface OrderRepository extends JpaRepository<Order, Long> {
|
||||
*/
|
||||
Page<Order> findByOrderType(Integer orderType, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 分页查找用户指定类型的订单
|
||||
*/
|
||||
Page<Order> findByUserIdAndOrderType(Long userId, Integer orderType, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 统计用户指定类型的订单数量
|
||||
*/
|
||||
@Query("SELECT COUNT(o) FROM Order o WHERE o.userId = :userId AND o.orderType = :orderType")
|
||||
Long countByUserIdAndOrderType(@Param("userId") Long userId, @Param("orderType") Integer orderType);
|
||||
|
||||
/**
|
||||
* 统计用户指定类型且非取消的订单数量(抢购成功)
|
||||
*/
|
||||
@Query("SELECT COUNT(o) FROM Order o WHERE o.userId = :userId AND o.orderType = :orderType AND o.status != 5")
|
||||
Long countByUserIdAndOrderTypeAndStatusNot5(@Param("userId") Long userId, @Param("orderType") Integer orderType);
|
||||
|
||||
/**
|
||||
* 查找秒杀订单
|
||||
*/
|
||||
@@ -116,10 +133,14 @@ public interface OrderRepository extends JpaRepository<Order, Long> {
|
||||
List<Order> findFlashSaleOrdersByUserId(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 检查用户是否已经购买过指定商品的秒杀
|
||||
* 检查用户是否已经参与过指定秒杀活动
|
||||
*/
|
||||
@Query("SELECT COUNT(o) > 0 FROM Order o WHERE o.userId = :userId AND o.productId = :productId AND o.orderType = 2")
|
||||
boolean existsFlashSaleOrder(@Param("userId") Long userId, @Param("productId") Long productId);
|
||||
boolean existsByUserIdAndFlashSaleIdAndOrderType(Long userId, Long flashSaleId, Integer orderType);
|
||||
|
||||
/**
|
||||
* 检查指定秒杀活动是否已有订单
|
||||
*/
|
||||
boolean existsByFlashSaleIdAndOrderType(Long flashSaleId, Integer orderType);
|
||||
|
||||
/**
|
||||
* 根据创建时间范围统计订单数量
|
||||
|
||||
@@ -18,6 +18,16 @@ public interface ProductReviewRepository extends JpaRepository<ProductReview, Lo
|
||||
|
||||
long countByProductId(Long productId);
|
||||
|
||||
long countByProductIdAndStatus(Long productId, Integer status);
|
||||
|
||||
@Query("SELECT AVG(r.rating) FROM ProductReview r WHERE r.productId = :productId")
|
||||
Double findAverageRatingByProductId(@Param("productId") Long productId);
|
||||
|
||||
List<ProductReview> findByUserIdOrderByCreatedAtDesc(Long userId);
|
||||
|
||||
List<ProductReview> findByOrderId(Long orderId);
|
||||
|
||||
boolean existsByOrderIdAndProductId(Long orderId, Long productId);
|
||||
|
||||
Optional<ProductReview> findByOrderIdAndProductId(Long orderId, Long productId);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
@@ -318,6 +320,7 @@ public class CartService {
|
||||
/**
|
||||
* 购物车下单
|
||||
*/
|
||||
@Transactional
|
||||
public OrderDTO checkoutCart(Long userId, List<Long> productIds) {
|
||||
log.info("购物车下单: 用户ID={}, 商品IDs={}", userId, productIds);
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@@ -90,10 +91,9 @@ public class FlashSaleService {
|
||||
throw new RuntimeException("开始时间不能早于当前时间");
|
||||
}
|
||||
|
||||
// 检查是否已有该商品的秒杀活动
|
||||
Optional<FlashSale> existingFlashSale = flashSaleRepository.findByProductId(createDTO.getProductId());
|
||||
if (existingFlashSale.isPresent()) {
|
||||
throw new RuntimeException("该商品已有秒杀活动");
|
||||
// 验证秒杀价格必须小于商品原价
|
||||
if (createDTO.getFlashPrice().compareTo(product.getPrice()) >= 0) {
|
||||
throw new RuntimeException("秒杀价格必须小于商品原价");
|
||||
}
|
||||
|
||||
// 创建秒杀活动
|
||||
@@ -157,8 +157,8 @@ public class FlashSaleService {
|
||||
}
|
||||
|
||||
// 检查数据库中是否已有订单
|
||||
if (orderRepository.existsFlashSaleOrder(userId, flashSale.getProductId())) {
|
||||
return createFailResult("您已经购买过该商品");
|
||||
if (orderRepository.existsByUserIdAndFlashSaleIdAndOrderType(userId, flashSale.getId(), 2)) {
|
||||
return createFailResult("您已经参与过该秒杀活动");
|
||||
}
|
||||
|
||||
// 检查购买数量限制
|
||||
@@ -174,6 +174,14 @@ public class FlashSaleService {
|
||||
}
|
||||
|
||||
try {
|
||||
// 二次校验:锁内重新检查用户是否已参与(防止并发竞态)
|
||||
if (redisService.sIsMember(successUsersKey, userId)) {
|
||||
return createFailResult("您已经参与过该秒杀活动");
|
||||
}
|
||||
if (orderRepository.existsByUserIdAndFlashSaleIdAndOrderType(userId, flashSale.getId(), 2)) {
|
||||
return createFailResult("您已经参与过该秒杀活动");
|
||||
}
|
||||
|
||||
// 检查并修复库存数据
|
||||
String stockKey = FLASH_SALE_STOCK_PREFIX + flashSale.getId();
|
||||
String currentStock = redisService.getString(stockKey);
|
||||
@@ -300,9 +308,11 @@ public class FlashSaleService {
|
||||
// 验证排序字段
|
||||
String sortBy = validateSortField(queryDTO.getSortBy());
|
||||
|
||||
// 限制分页大小
|
||||
int pageSize = Math.min(queryDTO.getSize(), 100);
|
||||
// 构建分页和排序
|
||||
Sort sort = Sort.by(Sort.Direction.fromString(queryDTO.getSortDirection()), sortBy);
|
||||
Pageable pageable = PageRequest.of(queryDTO.getPage(), queryDTO.getSize(), sort);
|
||||
Pageable pageable = PageRequest.of(queryDTO.getPage(), pageSize, sort);
|
||||
|
||||
Page<FlashSale> flashSalePage;
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
@@ -621,6 +631,55 @@ public class FlashSaleService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复秒杀库存(订单取消时调用)
|
||||
* 恢复Redis库存、DB库存,并移除成功用户集合记录
|
||||
*/
|
||||
@Transactional
|
||||
public void restoreFlashSaleStock(Long flashSaleId, Long productId, LocalDateTime orderCreatedAt, Long userId,
|
||||
Integer quantity) {
|
||||
log.info("恢复秒杀库存: flashSaleId={}, productId={}, orderCreatedAt={}, userId={}, quantity={}",
|
||||
flashSaleId, productId, orderCreatedAt, userId, quantity);
|
||||
|
||||
Optional<FlashSale> flashSaleOpt = Optional.empty();
|
||||
if (flashSaleId == null) {
|
||||
List<FlashSale> matchedFlashSales = flashSaleRepository.findByProductIdAndCoveringTime(productId,
|
||||
orderCreatedAt);
|
||||
if (matchedFlashSales.size() == 1) {
|
||||
flashSaleOpt = Optional.of(matchedFlashSales.get(0));
|
||||
log.info("根据商品和下单时间回填秒杀活动: flashSaleId={}, productId={}",
|
||||
flashSaleOpt.get().getId(), productId);
|
||||
} else {
|
||||
log.warn("订单未记录秒杀活动ID且无法唯一匹配历史活动,跳过秒杀库存恢复: productId={}, matches={}",
|
||||
productId, matchedFlashSales.size());
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
flashSaleOpt = flashSaleRepository.findById(flashSaleId);
|
||||
}
|
||||
|
||||
if (!flashSaleOpt.isPresent()) {
|
||||
log.warn("未找到对应的秒杀活动,跳过秒杀库存恢复: flashSaleId={}", flashSaleId);
|
||||
return;
|
||||
}
|
||||
|
||||
FlashSale flashSale = flashSaleOpt.get();
|
||||
|
||||
// 恢复Redis库存
|
||||
String stockKey = FLASH_SALE_STOCK_PREFIX + flashSale.getId();
|
||||
redisService.incrBy(stockKey, quantity);
|
||||
|
||||
// 恢复DB库存
|
||||
flashSaleRepository.increaseFlashStock(flashSale.getId(), quantity);
|
||||
|
||||
// 移除成功用户集合记录
|
||||
String successUsersKey = FLASH_SALE_SUCCESS_USERS_PREFIX + flashSale.getId();
|
||||
redisService.sRem(successUsersKey, userId);
|
||||
|
||||
log.info("秒杀库存恢复成功: flashSaleId={}, userId={}, quantity={}",
|
||||
flashSale.getId(), userId, quantity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取秒杀活动剩余库存
|
||||
*/
|
||||
@@ -712,7 +771,7 @@ public class FlashSaleService {
|
||||
}
|
||||
|
||||
// 检查是否有相关订单
|
||||
if (orderRepository.existsFlashSaleOrder(null, flashSale.getProductId())) {
|
||||
if (orderRepository.existsByFlashSaleIdAndOrderType(flashSaleId, 2)) {
|
||||
throw new RuntimeException("该秒杀活动已有订单,无法删除");
|
||||
}
|
||||
|
||||
@@ -886,6 +945,60 @@ public class FlashSaleService {
|
||||
return buildFlashSaleDTO(flashSale, product);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取秒杀活动统计信息(即将开始、正在进行的全局数量 + 用户参与/成功数量)
|
||||
*/
|
||||
public Map<String, Object> getFlashSaleStatistics(Long userId) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
long upcoming = flashSaleRepository.findUpcomingFlashSales(now).size();
|
||||
long active = flashSaleRepository.findActiveFlashSales(now).size();
|
||||
|
||||
long participated = 0;
|
||||
long success = 0;
|
||||
if (userId != null) {
|
||||
participated = orderRepository.countByUserIdAndOrderType(userId, 2);
|
||||
success = orderRepository.countByUserIdAndOrderTypeAndStatusNot5(userId, 2);
|
||||
}
|
||||
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
stats.put("upcoming", upcoming);
|
||||
stats.put("active", active);
|
||||
stats.put("participated", participated);
|
||||
stats.put("success", success);
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时预热即将开始的秒杀活动库存(每5分钟执行一次)
|
||||
*/
|
||||
@Scheduled(fixedRate = 300000)
|
||||
public void scheduledPreloadFlashSales() {
|
||||
log.info("定时任务:检查即将开始的秒杀活动并预热库存");
|
||||
try {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime threshold = now.plusMinutes(30);
|
||||
List<FlashSale> upcomingFlashSales = flashSaleRepository.findUpcomingFlashSales(now);
|
||||
|
||||
int preloadCount = 0;
|
||||
for (FlashSale flashSale : upcomingFlashSales) {
|
||||
if (flashSale.getStartTime().isBefore(threshold)) {
|
||||
preloadFlashSale(flashSale.getId());
|
||||
preloadCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (preloadCount > 0) {
|
||||
log.info("定时预热完成:预热了{}个即将开始的秒杀活动", preloadCount);
|
||||
}
|
||||
|
||||
// 同时更新秒杀活动状态
|
||||
updateFlashSaleStatus();
|
||||
} catch (Exception e) {
|
||||
log.error("定时预热秒杀活动失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新秒杀活动状态
|
||||
*/
|
||||
@@ -1051,6 +1164,7 @@ public class FlashSaleService {
|
||||
order.setOrderNo("FS" + System.currentTimeMillis() + String.format("%03d", new java.util.Random().nextInt(1000)));
|
||||
order.setUserId(userId);
|
||||
order.setProductId(flashSale.getProductId());
|
||||
order.setFlashSaleId(flashSale.getId());
|
||||
order.setQuantity(participateDTO.getQuantity());
|
||||
order.setTotalPrice(flashSale.getFlashPrice().multiply(BigDecimal.valueOf(participateDTO.getQuantity())));
|
||||
order.setStatus(1); // 待支付
|
||||
|
||||
@@ -0,0 +1,652 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import com.org.flashsalesystem.dto.GroupBuyingDTO;
|
||||
import com.org.flashsalesystem.dto.ProductDTO;
|
||||
import com.org.flashsalesystem.dto.UserDTO;
|
||||
import com.org.flashsalesystem.entity.*;
|
||||
import com.org.flashsalesystem.repository.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class GroupBuyingService {
|
||||
|
||||
private static final String GB_STOCK_PREFIX = "groupbuying_stock:";
|
||||
private static final String GB_LOCK_PREFIX = "groupbuying_lock:";
|
||||
private static final String GB_MEMBERS_PREFIX = "groupbuying_members:";
|
||||
|
||||
@Autowired
|
||||
private GroupBuyingRepository groupBuyingRepository;
|
||||
|
||||
@Autowired
|
||||
private GroupBuyingGroupRepository groupBuyingGroupRepository;
|
||||
|
||||
@Autowired
|
||||
private GroupBuyingMemberRepository groupBuyingMemberRepository;
|
||||
|
||||
@Autowired
|
||||
private OrderRepository orderRepository;
|
||||
|
||||
@Autowired
|
||||
private ProductRepository productRepository;
|
||||
|
||||
@Autowired
|
||||
private ProductService productService;
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private RedisService redisService;
|
||||
|
||||
@Autowired
|
||||
private RedissonLockService redissonLockService;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("customStringRedisTemplate")
|
||||
private RedisTemplate<String, String> stringRedisTemplate;
|
||||
|
||||
@Autowired
|
||||
private DefaultRedisScript<Long> groupBuyingStockScript;
|
||||
|
||||
// ========== 管理员操作 ==========
|
||||
|
||||
@Transactional
|
||||
public GroupBuyingDTO createGroupBuying(GroupBuyingDTO.CreateDTO createDTO) {
|
||||
log.info("创建拼团活动: productId={}", createDTO.getProductId());
|
||||
|
||||
Product product = productRepository.findById(createDTO.getProductId())
|
||||
.orElseThrow(() -> new RuntimeException("商品不存在"));
|
||||
|
||||
if (createDTO.getGroupPrice().compareTo(product.getPrice()) >= 0) {
|
||||
throw new RuntimeException("拼团价格必须低于商品原价");
|
||||
}
|
||||
|
||||
if (createDTO.getEndTime().isBefore(createDTO.getStartTime())) {
|
||||
throw new RuntimeException("结束时间不能早于开始时间");
|
||||
}
|
||||
|
||||
GroupBuying gb = new GroupBuying();
|
||||
gb.setProductId(createDTO.getProductId());
|
||||
gb.setGroupPrice(createDTO.getGroupPrice());
|
||||
gb.setRequiredMembers(createDTO.getRequiredMembers());
|
||||
gb.setDurationMinutes(createDTO.getDurationMinutes());
|
||||
gb.setTotalStock(createDTO.getTotalStock());
|
||||
gb.setRemainingStock(createDTO.getTotalStock());
|
||||
gb.setMaxPerUser(createDTO.getMaxPerUser());
|
||||
gb.setStatus(0); // 草稿
|
||||
gb.setStartTime(createDTO.getStartTime());
|
||||
gb.setEndTime(createDTO.getEndTime());
|
||||
|
||||
gb = groupBuyingRepository.save(gb);
|
||||
log.info("拼团活动创建成功: id={}", gb.getId());
|
||||
|
||||
return buildDTO(gb);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public GroupBuyingDTO updateGroupBuying(Long id, GroupBuyingDTO.UpdateDTO updateDTO) {
|
||||
GroupBuying gb = groupBuyingRepository.findById(id)
|
||||
.orElseThrow(() -> new RuntimeException("拼团活动不存在"));
|
||||
|
||||
if (gb.getStatus() == 2) {
|
||||
throw new RuntimeException("进行中的活动不能修改");
|
||||
}
|
||||
|
||||
if (updateDTO.getProductId() != null) gb.setProductId(updateDTO.getProductId());
|
||||
if (updateDTO.getGroupPrice() != null) gb.setGroupPrice(updateDTO.getGroupPrice());
|
||||
if (updateDTO.getRequiredMembers() != null) gb.setRequiredMembers(updateDTO.getRequiredMembers());
|
||||
if (updateDTO.getDurationMinutes() != null) gb.setDurationMinutes(updateDTO.getDurationMinutes());
|
||||
if (updateDTO.getTotalStock() != null) {
|
||||
int diff = updateDTO.getTotalStock() - gb.getTotalStock();
|
||||
gb.setTotalStock(updateDTO.getTotalStock());
|
||||
gb.setRemainingStock(gb.getRemainingStock() + diff);
|
||||
}
|
||||
if (updateDTO.getMaxPerUser() != null) gb.setMaxPerUser(updateDTO.getMaxPerUser());
|
||||
if (updateDTO.getStatus() != null) gb.setStatus(updateDTO.getStatus());
|
||||
if (updateDTO.getStartTime() != null) gb.setStartTime(updateDTO.getStartTime());
|
||||
if (updateDTO.getEndTime() != null) gb.setEndTime(updateDTO.getEndTime());
|
||||
|
||||
gb = groupBuyingRepository.save(gb);
|
||||
return buildDTO(gb);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public boolean deleteGroupBuying(Long id) {
|
||||
GroupBuying gb = groupBuyingRepository.findById(id)
|
||||
.orElseThrow(() -> new RuntimeException("拼团活动不存在"));
|
||||
|
||||
if (gb.getStatus() == 2) {
|
||||
throw new RuntimeException("进行中的活动不能删除");
|
||||
}
|
||||
|
||||
groupBuyingRepository.deleteById(id);
|
||||
redisService.delete(GB_STOCK_PREFIX + id);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ========== 查询操作 ==========
|
||||
|
||||
public Map<String, Object> getGroupBuyingList(int page, int size, Integer status) {
|
||||
int pageSize = Math.min(size, 100);
|
||||
Pageable pageable = PageRequest.of(page, pageSize, Sort.by(Sort.Direction.DESC, "createdAt"));
|
||||
|
||||
Page<GroupBuying> gbPage;
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
if (status != null) {
|
||||
if (status == 2) {
|
||||
gbPage = groupBuyingRepository.findActiveGroupBuyings(now, pageable);
|
||||
} else if (status == 3) {
|
||||
gbPage = groupBuyingRepository.findEndedGroupBuyings(now, pageable);
|
||||
} else {
|
||||
gbPage = groupBuyingRepository.findByStatus(status, pageable);
|
||||
}
|
||||
} else {
|
||||
gbPage = groupBuyingRepository.findAll(pageable);
|
||||
}
|
||||
|
||||
List<GroupBuyingDTO> dtos = gbPage.getContent().stream()
|
||||
.map(this::buildDTO)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("content", dtos);
|
||||
result.put("totalElements", gbPage.getTotalElements());
|
||||
result.put("totalPages", gbPage.getTotalPages());
|
||||
result.put("currentPage", gbPage.getNumber());
|
||||
result.put("size", gbPage.getSize());
|
||||
return result;
|
||||
}
|
||||
|
||||
public GroupBuyingDTO getGroupBuyingDetail(Long id) {
|
||||
GroupBuying gb = groupBuyingRepository.findById(id)
|
||||
.orElseThrow(() -> new RuntimeException("拼团活动不存在"));
|
||||
return buildDTO(gb);
|
||||
}
|
||||
|
||||
public Map<String, Object> getGroupsByActivity(Long groupBuyingId, int page, int size) {
|
||||
Pageable pageable = PageRequest.of(page, Math.min(size, 100), Sort.by(Sort.Direction.DESC, "createdAt"));
|
||||
Page<GroupBuyingGroup> groupPage = groupBuyingGroupRepository.findByGroupBuyingId(groupBuyingId, pageable);
|
||||
|
||||
List<GroupBuyingDTO.GroupInfoDTO> dtos = groupPage.getContent().stream()
|
||||
.map(this::buildGroupInfoDTO)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("content", dtos);
|
||||
result.put("totalElements", groupPage.getTotalElements());
|
||||
result.put("totalPages", groupPage.getTotalPages());
|
||||
result.put("currentPage", groupPage.getNumber());
|
||||
result.put("size", groupPage.getSize());
|
||||
return result;
|
||||
}
|
||||
|
||||
public GroupBuyingDTO.GroupInfoDTO getGroupDetail(Long groupId) {
|
||||
GroupBuyingGroup group = groupBuyingGroupRepository.findById(groupId)
|
||||
.orElseThrow(() -> new RuntimeException("团组不存在"));
|
||||
return buildGroupInfoDTO(group);
|
||||
}
|
||||
|
||||
public Map<String, Object> getMyGroups(Long userId, int page, int size) {
|
||||
Pageable pageable = PageRequest.of(page, Math.min(size, 100), Sort.by(Sort.Direction.DESC, "createdAt"));
|
||||
Page<GroupBuyingGroup> groupPage = groupBuyingGroupRepository.findByMemberUserId(userId, pageable);
|
||||
|
||||
List<GroupBuyingDTO.GroupInfoDTO> dtos = groupPage.getContent().stream()
|
||||
.map(this::buildGroupInfoDTO)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("content", dtos);
|
||||
result.put("totalElements", groupPage.getTotalElements());
|
||||
result.put("totalPages", groupPage.getTotalPages());
|
||||
result.put("currentPage", groupPage.getNumber());
|
||||
result.put("size", groupPage.getSize());
|
||||
return result;
|
||||
}
|
||||
|
||||
public GroupBuyingDTO.StatisticsDTO getStatistics(Long userId) {
|
||||
GroupBuyingDTO.StatisticsDTO stats = new GroupBuyingDTO.StatisticsDTO();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
stats.setTotalActivities(groupBuyingRepository.count());
|
||||
stats.setActiveActivities(groupBuyingRepository.countByStatus(2));
|
||||
|
||||
if (userId != null) {
|
||||
Page<GroupBuyingGroup> myGroups = groupBuyingGroupRepository.findByMemberUserId(userId, PageRequest.of(0, 1));
|
||||
stats.setMyGroups(myGroups.getTotalElements());
|
||||
|
||||
// Count successful groups where user participated
|
||||
long successCount = 0;
|
||||
BigDecimal totalSaved = BigDecimal.ZERO;
|
||||
Page<GroupBuyingGroup> allMyGroups = groupBuyingGroupRepository.findByMemberUserId(userId, PageRequest.of(0, 1000));
|
||||
for (GroupBuyingGroup g : allMyGroups.getContent()) {
|
||||
if (g.getStatus() == 2) {
|
||||
successCount++;
|
||||
GroupBuying gb = groupBuyingRepository.findById(g.getGroupBuyingId()).orElse(null);
|
||||
if (gb != null) {
|
||||
Product product = productRepository.findById(gb.getProductId()).orElse(null);
|
||||
if (product != null) {
|
||||
totalSaved = totalSaved.add(product.getPrice().subtract(gb.getGroupPrice()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stats.setSuccessGroups(successCount);
|
||||
stats.setTotalSaved(totalSaved);
|
||||
} else {
|
||||
stats.setMyGroups(0L);
|
||||
stats.setSuccessGroups(0L);
|
||||
stats.setTotalSaved(BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
// ========== 核心拼团逻辑 ==========
|
||||
|
||||
@Transactional
|
||||
public GroupBuyingDTO.JoinResultDTO joinGroupBuying(GroupBuyingDTO.JoinGroupDTO joinDTO, Long userId) {
|
||||
Long activityId = joinDTO.getGroupBuyingId();
|
||||
String lockKey = GB_LOCK_PREFIX + activityId;
|
||||
|
||||
if (!redissonLockService.tryLock(lockKey, 5, 30)) {
|
||||
throw new RuntimeException("系统繁忙,请稍后重试");
|
||||
}
|
||||
|
||||
try {
|
||||
return doJoinGroupBuying(joinDTO, userId);
|
||||
} finally {
|
||||
redissonLockService.unlock(lockKey);
|
||||
}
|
||||
}
|
||||
|
||||
private GroupBuyingDTO.JoinResultDTO doJoinGroupBuying(GroupBuyingDTO.JoinGroupDTO joinDTO, Long userId) {
|
||||
Long activityId = joinDTO.getGroupBuyingId();
|
||||
|
||||
// 1. 校验活动状态
|
||||
GroupBuying gb = groupBuyingRepository.findById(activityId)
|
||||
.orElseThrow(() -> new RuntimeException("拼团活动不存在"));
|
||||
|
||||
if (!gb.isActive()) {
|
||||
throw new RuntimeException("拼团活动未在进行中");
|
||||
}
|
||||
|
||||
if (gb.getRemainingStock() <= 0) {
|
||||
throw new RuntimeException("库存不足");
|
||||
}
|
||||
|
||||
// 2. 获取商品信息
|
||||
Product product = productRepository.findById(gb.getProductId())
|
||||
.orElseThrow(() -> new RuntimeException("商品不存在"));
|
||||
|
||||
GroupBuyingGroup group;
|
||||
|
||||
if (joinDTO.getGroupId() != null) {
|
||||
// 加入现有团组
|
||||
group = groupBuyingGroupRepository.findById(joinDTO.getGroupId())
|
||||
.orElseThrow(() -> new RuntimeException("团组不存在"));
|
||||
|
||||
if (group.getStatus() != 1) {
|
||||
throw new RuntimeException("该团组已成团或已过期");
|
||||
}
|
||||
|
||||
if (LocalDateTime.now().isAfter(group.getExpireTime())) {
|
||||
throw new RuntimeException("该团组已过期");
|
||||
}
|
||||
|
||||
if (group.getCurrentMembers() >= group.getRequiredMembers()) {
|
||||
throw new RuntimeException("该团组已满员");
|
||||
}
|
||||
|
||||
// 检查用户是否已经在该团组中
|
||||
if (groupBuyingMemberRepository.existsByGroupIdAndUserIdAndStatusNot(group.getId(), userId, 3)) {
|
||||
throw new RuntimeException("您已在该团组中");
|
||||
}
|
||||
} else {
|
||||
// 创建新团组
|
||||
group = new GroupBuyingGroup();
|
||||
group.setGroupNo("GB" + System.currentTimeMillis() + String.format("%03d", new Random().nextInt(1000)));
|
||||
group.setGroupBuyingId(activityId);
|
||||
group.setLeaderUserId(userId);
|
||||
group.setRequiredMembers(gb.getRequiredMembers());
|
||||
group.setCurrentMembers(0); // will be incremented below
|
||||
group.setStatus(1);
|
||||
group.setExpireTime(LocalDateTime.now().plusMinutes(gb.getDurationMinutes()));
|
||||
group = groupBuyingGroupRepository.save(group);
|
||||
}
|
||||
|
||||
// 3. Redis 原子扣库存
|
||||
String stockKey = GB_STOCK_PREFIX + activityId;
|
||||
Long result = stringRedisTemplate.execute(groupBuyingStockScript,
|
||||
Collections.singletonList(stockKey), "1");
|
||||
|
||||
if (result == null || result < 0) {
|
||||
// Redis stock exhausted, double check DB
|
||||
if (gb.getRemainingStock() <= 0) {
|
||||
throw new RuntimeException("库存不足");
|
||||
}
|
||||
}
|
||||
|
||||
// 4. DB 扣库存
|
||||
int updated = groupBuyingRepository.updateStock(activityId, 1);
|
||||
if (updated == 0) {
|
||||
// Restore Redis stock
|
||||
redisService.incrBy(stockKey, 1);
|
||||
throw new RuntimeException("库存不足");
|
||||
}
|
||||
|
||||
// 5. 创建订单
|
||||
Order order = new Order();
|
||||
order.setOrderNo("GB" + System.currentTimeMillis() + String.format("%03d", new Random().nextInt(1000)));
|
||||
order.setUserId(userId);
|
||||
order.setProductId(gb.getProductId());
|
||||
order.setQuantity(1);
|
||||
order.setTotalPrice(gb.getGroupPrice());
|
||||
order.setStatus(1); // 待支付
|
||||
order.setOrderType(3); // 拼团订单
|
||||
order.setGroupBuyingGroupId(group.getId());
|
||||
order = orderRepository.save(order);
|
||||
|
||||
// 6. 创建成员记录
|
||||
GroupBuyingMember member = new GroupBuyingMember();
|
||||
member.setGroupId(group.getId());
|
||||
member.setUserId(userId);
|
||||
member.setOrderId(order.getId());
|
||||
member.setStatus(1);
|
||||
groupBuyingMemberRepository.save(member);
|
||||
|
||||
// 7. 更新团组人数
|
||||
groupBuyingGroupRepository.incrementCurrentMembers(group.getId());
|
||||
|
||||
// Refresh group from DB
|
||||
group = groupBuyingGroupRepository.findById(group.getId()).orElse(group);
|
||||
|
||||
// 8. 检查是否满员 → 成团
|
||||
if (group.getCurrentMembers() >= group.getRequiredMembers()) {
|
||||
groupBuyingGroupRepository.updateStatusAndCompletedAt(group.getId(), 2, LocalDateTime.now());
|
||||
// Update all members status to SUCCESS
|
||||
groupBuyingMemberRepository.updateStatusByGroupId(group.getId(), 2);
|
||||
log.info("拼团成功: groupId={}, groupNo={}", group.getId(), group.getGroupNo());
|
||||
}
|
||||
|
||||
// Add to Redis members set
|
||||
redisService.sAdd(GB_MEMBERS_PREFIX + group.getId(), userId.toString());
|
||||
redisService.expire(GB_MEMBERS_PREFIX + group.getId(), 7, TimeUnit.DAYS);
|
||||
|
||||
GroupBuyingDTO.JoinResultDTO resultDTO = new GroupBuyingDTO.JoinResultDTO();
|
||||
resultDTO.setSuccess(true);
|
||||
resultDTO.setMessage(joinDTO.getGroupId() != null ? "加入拼团成功" : "开团成功");
|
||||
resultDTO.setGroupId(group.getId());
|
||||
resultDTO.setGroupNo(group.getGroupNo());
|
||||
resultDTO.setOrderId(order.getId());
|
||||
|
||||
log.info("用户{}参与拼团成功: activityId={}, groupId={}, orderId={}", userId, activityId, group.getId(), order.getId());
|
||||
return resultDTO;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void cancelMembership(Long groupId, Long userId) {
|
||||
log.info("退出拼团: groupId={}, userId={}", groupId, userId);
|
||||
|
||||
GroupBuyingGroup group = groupBuyingGroupRepository.findById(groupId)
|
||||
.orElseThrow(() -> new RuntimeException("团组不存在"));
|
||||
|
||||
if (group.getStatus() != 1) {
|
||||
throw new RuntimeException("已成团或已失败的团组不能退出");
|
||||
}
|
||||
|
||||
GroupBuyingMember member = groupBuyingMemberRepository.findByGroupIdAndUserId(groupId, userId)
|
||||
.orElseThrow(() -> new RuntimeException("您不在该团组中"));
|
||||
|
||||
if (member.getStatus() == 3) {
|
||||
throw new RuntimeException("您已退出该团组");
|
||||
}
|
||||
|
||||
// 更新成员状态
|
||||
member.setStatus(3);
|
||||
groupBuyingMemberRepository.save(member);
|
||||
|
||||
// 更新团组人数
|
||||
groupBuyingGroupRepository.decrementCurrentMembers(groupId);
|
||||
|
||||
// 恢复库存
|
||||
Long activityId = group.getGroupBuyingId();
|
||||
groupBuyingRepository.increaseStock(activityId, 1);
|
||||
redisService.incrBy(GB_STOCK_PREFIX + activityId, 1);
|
||||
|
||||
// 取消订单
|
||||
if (member.getOrderId() != null) {
|
||||
Optional<Order> orderOpt = orderRepository.findById(member.getOrderId());
|
||||
if (orderOpt.isPresent()) {
|
||||
Order order = orderOpt.get();
|
||||
if (order.getStatus() == 1) {
|
||||
order.setStatus(5);
|
||||
order.setRemark("退出拼团,订单取消");
|
||||
orderRepository.save(order);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from Redis set
|
||||
redisService.sRem(GB_MEMBERS_PREFIX + groupId, userId.toString());
|
||||
|
||||
log.info("退出拼团成功: groupId={}, userId={}", groupId, userId);
|
||||
}
|
||||
|
||||
// ========== 定时任务 ==========
|
||||
|
||||
@Scheduled(fixedRate = 60000) // 每分钟检查
|
||||
@Transactional
|
||||
public void scheduledCheckExpiredGroups() {
|
||||
List<GroupBuyingGroup> expiredGroups = groupBuyingGroupRepository.findExpiredGroups(LocalDateTime.now());
|
||||
for (GroupBuyingGroup group : expiredGroups) {
|
||||
try {
|
||||
handleExpiredGroup(group);
|
||||
} catch (Exception e) {
|
||||
log.error("处理超时团组失败: groupId={}", group.getId(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void handleExpiredGroup(GroupBuyingGroup group) {
|
||||
log.info("处理超时团组: groupId={}, groupNo={}", group.getId(), group.getGroupNo());
|
||||
|
||||
// Mark group as FAILED
|
||||
groupBuyingGroupRepository.updateStatusAndCompletedAt(group.getId(), 3, LocalDateTime.now());
|
||||
|
||||
// Get active members
|
||||
List<GroupBuyingMember> members = groupBuyingMemberRepository.findByGroupIdAndStatus(group.getId(), 1);
|
||||
|
||||
for (GroupBuyingMember member : members) {
|
||||
// Update member status
|
||||
member.setStatus(3);
|
||||
groupBuyingMemberRepository.save(member);
|
||||
|
||||
// Restore stock
|
||||
groupBuyingRepository.increaseStock(group.getGroupBuyingId(), 1);
|
||||
redisService.incrBy(GB_STOCK_PREFIX + group.getGroupBuyingId(), 1);
|
||||
|
||||
// Cancel order
|
||||
if (member.getOrderId() != null) {
|
||||
Optional<Order> orderOpt = orderRepository.findById(member.getOrderId());
|
||||
if (orderOpt.isPresent()) {
|
||||
Order order = orderOpt.get();
|
||||
if (order.getStatus() == 1) {
|
||||
order.setStatus(5);
|
||||
order.setRemark("拼团超时,订单自动取消");
|
||||
orderRepository.save(order);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean Redis
|
||||
redisService.delete(GB_MEMBERS_PREFIX + group.getId());
|
||||
|
||||
log.info("超时团组处理完成: groupId={}", group.getId());
|
||||
}
|
||||
|
||||
@Scheduled(fixedRate = 300000) // 每5分钟
|
||||
@Transactional
|
||||
public void scheduledUpdateStatus() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
// Activate pending activities whose start time has passed
|
||||
List<GroupBuying> upcoming = groupBuyingRepository.findUpcomingGroupBuyings(now);
|
||||
// These are status=1 and startTime > now, so nothing to activate here
|
||||
|
||||
// Actually find activities that should be active: status=1, startTime <= now, endTime > now
|
||||
List<GroupBuying> all = groupBuyingRepository.findAll();
|
||||
for (GroupBuying gb : all) {
|
||||
if (gb.getStatus() == 1 && !now.isBefore(gb.getStartTime()) && now.isBefore(gb.getEndTime())) {
|
||||
groupBuyingRepository.updateStatus(gb.getId(), 2);
|
||||
preloadStock(gb);
|
||||
log.info("拼团活动已激活: id={}", gb.getId());
|
||||
} else if (gb.getStatus() == 2 && !now.isBefore(gb.getEndTime())) {
|
||||
groupBuyingRepository.updateStatus(gb.getId(), 3);
|
||||
log.info("拼团活动已结束: id={}", gb.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void preloadStock(GroupBuying gb) {
|
||||
String stockKey = GB_STOCK_PREFIX + gb.getId();
|
||||
redisService.setString(stockKey, String.valueOf(gb.getRemainingStock()));
|
||||
long ttl = java.time.Duration.between(LocalDateTime.now(), gb.getEndTime()).getSeconds() + 3600;
|
||||
redisService.expire(stockKey, ttl, TimeUnit.SECONDS);
|
||||
log.info("拼团库存预热: id={}, stock={}", gb.getId(), gb.getRemainingStock());
|
||||
}
|
||||
|
||||
public void preloadAllActiveStock() {
|
||||
List<GroupBuying> activeList = groupBuyingRepository.findActiveGroupBuyings(LocalDateTime.now());
|
||||
for (GroupBuying gb : activeList) {
|
||||
preloadStock(gb);
|
||||
}
|
||||
log.info("所有拼团活动库存预热完成, count={}", activeList.size());
|
||||
}
|
||||
|
||||
// ========== 构建 DTO ==========
|
||||
|
||||
private GroupBuyingDTO buildDTO(GroupBuying gb) {
|
||||
GroupBuyingDTO dto = new GroupBuyingDTO();
|
||||
dto.setId(gb.getId());
|
||||
dto.setProductId(gb.getProductId());
|
||||
dto.setGroupPrice(gb.getGroupPrice());
|
||||
dto.setRequiredMembers(gb.getRequiredMembers());
|
||||
dto.setDurationMinutes(gb.getDurationMinutes());
|
||||
dto.setTotalStock(gb.getTotalStock());
|
||||
dto.setRemainingStock(gb.getRemainingStock());
|
||||
dto.setMaxPerUser(gb.getMaxPerUser());
|
||||
dto.setStatus(gb.getStatus());
|
||||
dto.setStatusDescription(getStatusDescription(gb.getStatus()));
|
||||
dto.setStartTime(gb.getStartTime());
|
||||
dto.setEndTime(gb.getEndTime());
|
||||
dto.setCreatedAt(gb.getCreatedAt());
|
||||
dto.setUpdatedAt(gb.getUpdatedAt());
|
||||
|
||||
// Product info
|
||||
ProductDTO product = productService.getProductById(gb.getProductId());
|
||||
if (product != null) {
|
||||
dto.setProductName(product.getName());
|
||||
dto.setProductImageUrl(product.getImageUrl());
|
||||
dto.setProductPrice(product.getPrice());
|
||||
dto.setDiscount(product.getPrice().subtract(gb.getGroupPrice()));
|
||||
}
|
||||
|
||||
// Active group count
|
||||
long activeGroups = groupBuyingGroupRepository.countByGroupBuyingIdAndStatus(gb.getId(), 1);
|
||||
dto.setActiveGroupCount((int) activeGroups);
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
private GroupBuyingDTO.GroupInfoDTO buildGroupInfoDTO(GroupBuyingGroup group) {
|
||||
GroupBuyingDTO.GroupInfoDTO dto = new GroupBuyingDTO.GroupInfoDTO();
|
||||
dto.setId(group.getId());
|
||||
dto.setGroupNo(group.getGroupNo());
|
||||
dto.setGroupBuyingId(group.getGroupBuyingId());
|
||||
dto.setLeaderUserId(group.getLeaderUserId());
|
||||
dto.setRequiredMembers(group.getRequiredMembers());
|
||||
dto.setCurrentMembers(group.getCurrentMembers());
|
||||
dto.setStatus(group.getStatus());
|
||||
dto.setStatusDescription(getGroupStatusDescription(group.getStatus()));
|
||||
dto.setExpireTime(group.getExpireTime());
|
||||
dto.setCreatedAt(group.getCreatedAt());
|
||||
dto.setCompletedAt(group.getCompletedAt());
|
||||
|
||||
// Leader info
|
||||
UserDTO leader = userService.getUserById(group.getLeaderUserId());
|
||||
if (leader != null) {
|
||||
dto.setLeaderUsername(leader.getUsername());
|
||||
}
|
||||
|
||||
// Members
|
||||
List<GroupBuyingMember> members = groupBuyingMemberRepository.findByGroupId(group.getId());
|
||||
List<GroupBuyingDTO.MemberDTO> memberDTOs = members.stream()
|
||||
.filter(m -> m.getStatus() != 3) // exclude exited
|
||||
.map(m -> {
|
||||
GroupBuyingDTO.MemberDTO memberDTO = new GroupBuyingDTO.MemberDTO();
|
||||
memberDTO.setId(m.getId());
|
||||
memberDTO.setUserId(m.getUserId());
|
||||
memberDTO.setOrderId(m.getOrderId());
|
||||
memberDTO.setStatus(m.getStatus());
|
||||
memberDTO.setJoinedAt(m.getJoinedAt());
|
||||
UserDTO user = userService.getUserById(m.getUserId());
|
||||
if (user != null) {
|
||||
memberDTO.setUsername(user.getUsername());
|
||||
memberDTO.setAvatar(user.getAvatar());
|
||||
}
|
||||
return memberDTO;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
dto.setMembers(memberDTOs);
|
||||
|
||||
// Activity info
|
||||
GroupBuying gb = groupBuyingRepository.findById(group.getGroupBuyingId()).orElse(null);
|
||||
if (gb != null) {
|
||||
dto.setGroupBuying(buildDTO(gb));
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
private String getStatusDescription(Integer status) {
|
||||
if (status == null) return "未知";
|
||||
switch (status) {
|
||||
case 0: return "草稿";
|
||||
case 1: return "未开始";
|
||||
case 2: return "进行中";
|
||||
case 3: return "已结束";
|
||||
default: return "未知";
|
||||
}
|
||||
}
|
||||
|
||||
private String getGroupStatusDescription(Integer status) {
|
||||
if (status == null) return "未知";
|
||||
switch (status) {
|
||||
case 1: return "拼团中";
|
||||
case 2: return "已成团";
|
||||
case 3: return "已失败";
|
||||
default: return "未知";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,9 @@ public class MessageListenerService {
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Autowired
|
||||
private NotificationService notificationService;
|
||||
|
||||
/**
|
||||
* 初始化消息监听器
|
||||
*/
|
||||
@@ -62,31 +65,43 @@ public class MessageListenerService {
|
||||
* 处理订单状态变更
|
||||
*/
|
||||
private void handleOrderStatusChange(Long orderId, Long userId, Integer status, String action) {
|
||||
// 可以在这里实现:
|
||||
// 1. 发送邮件通知
|
||||
// 2. 推送消息
|
||||
// 3. 更新统计数据
|
||||
// 4. 触发其他业务流程
|
||||
if (userId == null) {
|
||||
log.warn("订单状态变更缺少用户ID: orderId={}", orderId);
|
||||
return;
|
||||
}
|
||||
|
||||
String title;
|
||||
String message;
|
||||
String link = "/order/" + orderId;
|
||||
|
||||
switch (action) {
|
||||
case "created":
|
||||
log.info("订单创建通知处理: 订单ID={}", orderId);
|
||||
title = "订单创建成功";
|
||||
message = "您的订单 #" + orderId + " 已创建,请尽快完成支付";
|
||||
break;
|
||||
case "paid":
|
||||
log.info("订单支付通知处理: 订单ID={}", orderId);
|
||||
title = "订单支付成功";
|
||||
message = "您的订单 #" + orderId + " 已支付成功,等待商家发货";
|
||||
break;
|
||||
case "shipped":
|
||||
log.info("订单发货通知处理: 订单ID={}", orderId);
|
||||
title = "订单已发货";
|
||||
message = "您的订单 #" + orderId + " 已发货,请注意查收";
|
||||
break;
|
||||
case "completed":
|
||||
log.info("订单完成通知处理: 订单ID={}", orderId);
|
||||
title = "订单已完成";
|
||||
message = "您的订单 #" + orderId + " 已完成,欢迎评价";
|
||||
break;
|
||||
case "cancelled":
|
||||
log.info("订单取消通知处理: 订单ID={}", orderId);
|
||||
title = "订单已取消";
|
||||
message = "您的订单 #" + orderId + " 已取消";
|
||||
break;
|
||||
default:
|
||||
log.info("未知订单状态变更: {}", action);
|
||||
return;
|
||||
}
|
||||
|
||||
notificationService.createNotification(userId, "order", title, message, link);
|
||||
log.info("订单状态变更通知已创建: 订单ID={}, 操作={}", orderId, action);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,20 +127,23 @@ public class MessageListenerService {
|
||||
* 处理秒杀结果
|
||||
*/
|
||||
private void handleFlashSaleResult(Long userId, Long flashSaleId, Boolean success, Map<String, Object> data) {
|
||||
// 可以在这里实现:
|
||||
// 1. 实时通知用户
|
||||
// 2. 统计秒杀数据
|
||||
// 3. 风控分析
|
||||
// 4. 营销推荐
|
||||
if (userId == null) {
|
||||
log.warn("秒杀结果缺少用户ID: flashSaleId={}", flashSaleId);
|
||||
return;
|
||||
}
|
||||
|
||||
String link = "/flashsale/" + flashSaleId;
|
||||
|
||||
if (success) {
|
||||
log.info("秒杀成功处理: 用户ID={}, 秒杀ID={}", userId, flashSaleId);
|
||||
// 发送成功通知
|
||||
sendFlashSaleSuccessNotification(userId, flashSaleId);
|
||||
String title = "秒杀成功";
|
||||
String message = "恭喜您成功抢购秒杀商品,请尽快完成支付!";
|
||||
notificationService.createNotification(userId, "flashsale", title, message, link);
|
||||
log.info("秒杀成功通知已创建: 用户ID={}, 秒杀ID={}", userId, flashSaleId);
|
||||
} else {
|
||||
log.info("秒杀失败处理: 用户ID={}, 秒杀ID={}", userId, flashSaleId);
|
||||
// 可以推荐其他商品
|
||||
recommendAlternativeProducts(userId, flashSaleId);
|
||||
String title = "秒杀未中";
|
||||
String message = "很遗憾,本次秒杀未能抢购成功,下次再来吧!";
|
||||
notificationService.createNotification(userId, "flashsale", title, message, link);
|
||||
log.info("秒杀失败通知已创建: 用户ID={}, 秒杀ID={}", userId, flashSaleId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,26 +179,9 @@ public class MessageListenerService {
|
||||
* 检查库存预警
|
||||
*/
|
||||
private void checkStockAlert(Long productId) {
|
||||
// 实现库存预警逻辑
|
||||
log.debug("检查商品库存预警: 商品ID={}", productId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送秒杀成功通知
|
||||
*/
|
||||
private void sendFlashSaleSuccessNotification(Long userId, Long flashSaleId) {
|
||||
// 实现成功通知逻辑
|
||||
log.debug("发送秒杀成功通知: 用户ID={}, 秒杀ID={}", userId, flashSaleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 推荐替代商品
|
||||
*/
|
||||
private void recommendAlternativeProducts(Long userId, Long flashSaleId) {
|
||||
// 实现商品推荐逻辑
|
||||
log.debug("推荐替代商品: 用户ID={}, 秒杀ID={}", userId, flashSaleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取Long值
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import com.org.flashsalesystem.entity.Notification;
|
||||
import com.org.flashsalesystem.repository.NotificationRepository;
|
||||
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;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class NotificationService {
|
||||
|
||||
@Autowired
|
||||
private NotificationRepository notificationRepository;
|
||||
|
||||
public void createNotification(Long userId, String type, String title, String message, String link) {
|
||||
Notification notification = new Notification();
|
||||
notification.setUserId(userId);
|
||||
notification.setType(type);
|
||||
notification.setTitle(title);
|
||||
notification.setMessage(message);
|
||||
notification.setLink(link);
|
||||
notification.setRead(false);
|
||||
notificationRepository.save(notification);
|
||||
log.debug("通知已创建: userId={}, type={}, title={}", userId, type, title);
|
||||
}
|
||||
|
||||
public List<Notification> getUserNotifications(Long userId) {
|
||||
return notificationRepository.findByUserIdOrderByCreatedAtDesc(userId);
|
||||
}
|
||||
|
||||
public List<Notification> getUserNotificationsByType(Long userId, String type) {
|
||||
return notificationRepository.findByUserIdAndTypeOrderByCreatedAtDesc(userId, type);
|
||||
}
|
||||
|
||||
public long getUnreadCount(Long userId) {
|
||||
return notificationRepository.countByUserIdAndReadFalse(userId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void markAsRead(Long notificationId, Long userId) {
|
||||
notificationRepository.markAsRead(notificationId, userId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void markAllAsRead(Long userId) {
|
||||
notificationRepository.markAllAsRead(userId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void clearAll(Long userId) {
|
||||
notificationRepository.deleteByUserId(userId);
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,10 @@ public class OrderService {
|
||||
private UserService userService;
|
||||
@Autowired
|
||||
private UserAddressRepository userAddressRepository;
|
||||
@Autowired
|
||||
private FlashSaleService flashSaleService;
|
||||
@Autowired
|
||||
private GroupBuyingService groupBuyingService;
|
||||
|
||||
/**
|
||||
* 创建普通订单
|
||||
@@ -219,14 +223,20 @@ public class OrderService {
|
||||
* 获取用户订单列表
|
||||
*/
|
||||
public Map<String, Object> getUserOrders(Long userId, OrderDTO.QueryDTO queryDTO) {
|
||||
// 限制分页大小
|
||||
int pageSize = Math.min(queryDTO.getSize(), 100);
|
||||
// 构建分页和排序
|
||||
Sort sort = Sort.by(Sort.Direction.fromString(queryDTO.getSortDirection()), queryDTO.getSortBy());
|
||||
Pageable pageable = PageRequest.of(queryDTO.getPage(), queryDTO.getSize(), sort);
|
||||
Pageable pageable = PageRequest.of(queryDTO.getPage(), pageSize, sort);
|
||||
|
||||
Page<Order> orderPage;
|
||||
|
||||
// 根据查询条件获取订单
|
||||
if (queryDTO.getStatus() != null) {
|
||||
if (queryDTO.getStatus() != null && queryDTO.getOrderType() != null) {
|
||||
orderPage = orderRepository.findByUserIdAndStatus(userId, queryDTO.getStatus(), pageable);
|
||||
} else if (queryDTO.getOrderType() != null) {
|
||||
orderPage = orderRepository.findByUserIdAndOrderType(userId, queryDTO.getOrderType(), pageable);
|
||||
} else if (queryDTO.getStatus() != null) {
|
||||
orderPage = orderRepository.findByUserIdAndStatus(userId, queryDTO.getStatus(), pageable);
|
||||
} else {
|
||||
orderPage = orderRepository.findByUserId(userId, pageable);
|
||||
@@ -259,9 +269,11 @@ public class OrderService {
|
||||
* 获取所有订单列表(管理员)
|
||||
*/
|
||||
public Map<String, Object> getAllOrders(OrderDTO.QueryDTO queryDTO) {
|
||||
// 限制分页大小
|
||||
int pageSize = Math.min(queryDTO.getSize(), 100);
|
||||
// 构建分页和排序
|
||||
Sort sort = Sort.by(Sort.Direction.fromString(queryDTO.getSortDirection()), queryDTO.getSortBy());
|
||||
Pageable pageable = PageRequest.of(queryDTO.getPage(), queryDTO.getSize(), sort);
|
||||
Pageable pageable = PageRequest.of(queryDTO.getPage(), pageSize, sort);
|
||||
|
||||
Page<Order> orderPage;
|
||||
|
||||
@@ -402,6 +414,21 @@ public class OrderService {
|
||||
// 恢复库存
|
||||
productService.updateStock(order.getProductId(), order.getQuantity(), "increase");
|
||||
|
||||
// 秒杀订单额外恢复秒杀库存
|
||||
if (order.getOrderType() != null && order.getOrderType() == 2) {
|
||||
flashSaleService.restoreFlashSaleStock(order.getFlashSaleId(), order.getProductId(), order.getCreatedAt(),
|
||||
order.getUserId(), order.getQuantity());
|
||||
}
|
||||
|
||||
// 拼团订单额外处理
|
||||
if (order.getOrderType() != null && order.getOrderType() == 3 && order.getGroupBuyingGroupId() != null) {
|
||||
try {
|
||||
groupBuyingService.cancelMembership(order.getGroupBuyingGroupId(), order.getUserId());
|
||||
} catch (Exception e) {
|
||||
log.warn("拼团退出处理失败: orderId={}, error={}", orderId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 更新缓存
|
||||
cacheOrderInfo(order);
|
||||
|
||||
@@ -569,6 +596,7 @@ public class OrderService {
|
||||
orderMap.put("groupNo", order.getGroupNo() == null ? "" : order.getGroupNo());
|
||||
orderMap.put("userId", order.getUserId().toString());
|
||||
orderMap.put("productId", order.getProductId().toString());
|
||||
orderMap.put("flashSaleId", order.getFlashSaleId() == null ? "" : order.getFlashSaleId().toString());
|
||||
orderMap.put("quantity", order.getQuantity().toString());
|
||||
orderMap.put("totalPrice", order.getTotalPrice().toString());
|
||||
orderMap.put("status", order.getStatus().toString());
|
||||
@@ -603,6 +631,8 @@ public class OrderService {
|
||||
orderDTO.setGroupNo((String) orderMap.get("groupNo"));
|
||||
orderDTO.setUserId(Long.valueOf((String) orderMap.get("userId")));
|
||||
orderDTO.setProductId(Long.valueOf((String) orderMap.get("productId")));
|
||||
String flashSaleId = (String) orderMap.get("flashSaleId");
|
||||
if (flashSaleId != null && !flashSaleId.isEmpty()) { orderDTO.setFlashSaleId(Long.valueOf(flashSaleId)); }
|
||||
orderDTO.setQuantity(Integer.valueOf((String) orderMap.get("quantity")));
|
||||
orderDTO.setTotalPrice(new BigDecimal((String) orderMap.get("totalPrice")));
|
||||
orderDTO.setStatus(Integer.valueOf((String) orderMap.get("status")));
|
||||
@@ -849,6 +879,8 @@ public class OrderService {
|
||||
return "普通订单";
|
||||
case 2:
|
||||
return "秒杀订单";
|
||||
case 3:
|
||||
return "拼团订单";
|
||||
default:
|
||||
return "未知类型";
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@ 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.Product;
|
||||
import com.org.flashsalesystem.entity.ProductReview;
|
||||
import com.org.flashsalesystem.repository.OrderItemRepository;
|
||||
import com.org.flashsalesystem.repository.OrderRepository;
|
||||
import com.org.flashsalesystem.repository.ProductRepository;
|
||||
import com.org.flashsalesystem.repository.ProductReviewRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
@@ -15,6 +17,7 @@ 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
|
||||
@@ -30,6 +33,9 @@ public class ProductReviewService {
|
||||
@Autowired
|
||||
private OrderItemRepository orderItemRepository;
|
||||
|
||||
@Autowired
|
||||
private ProductRepository productRepository;
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@@ -68,7 +74,7 @@ public class ProductReviewService {
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
Double average = productReviewRepository.findAverageRatingByProductId(productId);
|
||||
Long total = productReviewRepository.countByProductId(productId);
|
||||
Long total = productReviewRepository.countByProductIdAndStatus(productId, 1);
|
||||
return new ProductReviewDTO.SummaryDTO(average == null ? 0.0 : average, total, reviews);
|
||||
}
|
||||
|
||||
@@ -89,12 +95,39 @@ public class ProductReviewService {
|
||||
return toDTO(review);
|
||||
}
|
||||
|
||||
public ProductReviewDTO.CheckDTO checkReviewStatus(Long orderId, Long productId) {
|
||||
ProductReviewDTO.CheckDTO checkDTO = new ProductReviewDTO.CheckDTO();
|
||||
Optional<ProductReview> review = productReviewRepository.findByOrderIdAndProductId(orderId, productId);
|
||||
checkDTO.setReviewed(review.isPresent());
|
||||
review.ifPresent(r -> checkDTO.setReview(toDTO(r)));
|
||||
return checkDTO;
|
||||
}
|
||||
|
||||
public List<ProductReviewDTO> getUserReviews(Long userId) {
|
||||
return productReviewRepository.findByUserIdOrderByCreatedAtDesc(userId)
|
||||
.stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public List<ProductReviewDTO> getOrderReviews(Long orderId) {
|
||||
return productReviewRepository.findByOrderId(orderId)
|
||||
.stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
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 ? "显示" : "隐藏");
|
||||
Optional<Product> product = productRepository.findById(review.getProductId());
|
||||
if (product.isPresent()) {
|
||||
dto.setProductName(product.get().getName());
|
||||
dto.setProductImage(product.get().getImageUrl());
|
||||
}
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,9 +120,11 @@ public class ProductService {
|
||||
return (Map<String, Object>) cachedResult;
|
||||
}
|
||||
|
||||
// 限制分页大小
|
||||
int pageSize = Math.min(queryDTO.getSize(), 100);
|
||||
// 构建分页和排序
|
||||
Sort sort = Sort.by(Sort.Direction.fromString(queryDTO.getSortDirection()), queryDTO.getSortBy());
|
||||
Pageable pageable = PageRequest.of(queryDTO.getPage(), queryDTO.getSize(), sort);
|
||||
Pageable pageable = PageRequest.of(queryDTO.getPage(), pageSize, sort);
|
||||
|
||||
Integer status = queryDTO.getStatus() != null ? queryDTO.getStatus() : 1;
|
||||
String keyword = queryDTO.getKeyword() != null && !queryDTO.getKeyword().trim().isEmpty()
|
||||
|
||||
Reference in New Issue
Block a user