From 28f41754d0d734f949a29f188de1b11dff110d96 Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Tue, 17 Mar 2026 00:08:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(groupbuying):=20=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E6=8B=BC=E5=9B=A2=E8=AE=A2=E5=8D=95=E5=85=A8=E9=93=BE=E8=B7=AF?= =?UTF-8?q?=E5=8F=8A=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 拼团订单发货校验:必须成团后才能发货 - 限购校验:跨团组统计用户参与次数,超限拒绝 - 成团/失败自动通知所有成员(Redis Pub/Sub) - 拼团详情页区分"进行中"和"已成团"团组展示 - 订单类型支持三态(普通/秒杀/拼团)前后端联调 - 400错误只显示业务消息,不再重复弹出状态码 - 响应式导航栏适配及UI优化 - 新增历史数据修复SQL脚本 --- flash-sale-frontend/src/api/request.ts | 35 +++-- .../src/components/business/ReviewDialog.vue | 14 +- .../src/components/common/AppHeader.vue | 144 +++++++++++++++--- .../components/common/NotificationCenter.vue | 12 +- .../src/layouts/MainLayout.vue | 2 +- .../src/pages/admin/orders.vue | 18 ++- .../src/pages/groupbuying/detail.vue | 55 +++++-- .../src/pages/groupbuying/group.vue | 9 +- flash-sale-frontend/src/pages/order/index.vue | 6 +- flash-sale-frontend/src/styles/index.scss | 1 + flash-sale-frontend/src/types/admin.ts | 2 + flash-sale-frontend/src/types/api.d.ts | 1 + flash-sale-frontend/src/utils/normalizers.ts | 11 ++ .../controller/ProductReviewController.java | 10 +- .../com/org/flashsalesystem/dto/OrderDTO.java | 3 + .../GroupBuyingMemberRepository.java | 7 + .../flashsalesystem/service/AdminService.java | 2 + .../service/GroupBuyingService.java | 56 +++++++ .../service/MessageListenerService.java | 73 +++++++++ .../flashsalesystem/service/OrderService.java | 32 ++++ .../service/ProductReviewService.java | 4 +- .../resources/sql/fix-groupbuying-data.sql | 132 ++++++++++++++++ 22 files changed, 571 insertions(+), 58 deletions(-) create mode 100644 src/main/resources/sql/fix-groupbuying-data.sql diff --git a/flash-sale-frontend/src/api/request.ts b/flash-sale-frontend/src/api/request.ts index b398e67..ddbf5e8 100644 --- a/flash-sale-frontend/src/api/request.ts +++ b/flash-sale-frontend/src/api/request.ts @@ -85,34 +85,47 @@ service.interceptors.response.use( }, (error) => { console.error('响应错误:', error) - + + // 提取后端返回的业务错误消息 + const bizMessage = error.response?.data?.message + let displayMessage = '' + if (error.response) { switch (error.response.status) { + case 400: + // 业务错误(如"该团组已满员"),只显示后端返回的消息 + displayMessage = bizMessage || '请求参数错误' + break case 401: - ElMessage.error('未授权,请登录') + displayMessage = '未授权,请登录' break case 403: - ElMessage.error('拒绝访问') + displayMessage = '拒绝访问' break case 404: - ElMessage.error('请求地址不存在') + displayMessage = '请求地址不存在' break case 429: - ElMessage.error('请求过于频繁,请稍后再试') + displayMessage = '请求过于频繁,请稍后再试' break case 500: - ElMessage.error('服务器内部错误') + displayMessage = '服务器内部错误' break default: - ElMessage.error(error.response.data?.message || '请求失败') + displayMessage = bizMessage || '请求失败' } } else if (error.request) { - ElMessage.error('网络错误,请检查网络连接') + displayMessage = '网络错误,请检查网络连接' } else { - ElMessage.error('请求配置错误') + displayMessage = '请求配置错误' } - - return Promise.reject(error) + + ElMessage.error(displayMessage) + + // 用业务消息替换原始 axios error message,避免组件 catch 显示 "Request failed with status code 400" + const rejectError = new Error(displayMessage) + ;(rejectError as any)._handled = true + return Promise.reject(rejectError) } ) diff --git a/flash-sale-frontend/src/components/business/ReviewDialog.vue b/flash-sale-frontend/src/components/business/ReviewDialog.vue index ac5f61b..237b5b6 100644 --- a/flash-sale-frontend/src/components/business/ReviewDialog.vue +++ b/flash-sale-frontend/src/components/business/ReviewDialog.vue @@ -107,7 +107,10 @@ const reviewedItems = computed(() => items.value.filter(i => i.reviewed)) const canSubmit = computed(() => reviewableItems.value.some(i => i.content.trim())) const loadReviewStatus = async () => { - if (!props.orderId || !props.orderItems.length) return + if (!props.orderId || !props.orderItems.length) { + items.value = [] + return + } checkLoading.value = true try { const list: ReviewableItem[] = props.orderItems.map(item => ({ @@ -177,5 +180,14 @@ const handleSubmit = async () => { watch(() => props.visible, (val) => { if (val) loadReviewStatus() + if (!val) items.value = [] }) + +watch( + () => [props.orderId, props.orderItems], + () => { + if (props.visible) loadReviewStatus() + }, + { immediate: true } +) diff --git a/flash-sale-frontend/src/components/common/AppHeader.vue b/flash-sale-frontend/src/components/common/AppHeader.vue index 9a5ecf1..09553d6 100644 --- a/flash-sale-frontend/src/components/common/AppHeader.vue +++ b/flash-sale-frontend/src/components/common/AppHeader.vue @@ -1,9 +1,8 @@ - - + @@ -126,7 +158,7 @@ import { ref, computed, onMounted } from 'vue' import { useRoute, useRouter } from 'vue-router' import { ElMessage } from 'element-plus' -import { Loading } from '@element-plus/icons-vue' +import { Loading, CircleCheckFilled } from '@element-plus/icons-vue' import type { GroupBuying, GroupBuyingGroup } from '@/types/api' import { groupbuyingApi } from '@/api/modules/groupbuying' import SafeImage from '@/components/common/SafeImage.vue' @@ -141,6 +173,9 @@ const groups = ref([]) const id = computed(() => Number(route.params.id)) +const formingGroups = computed(() => groups.value.filter(g => g.status === 'FORMING')) +const completedGroups = computed(() => groups.value.filter(g => g.status === 'SUCCESS')) + const statusType = computed(() => { switch (detail.value?.status) { case 'UPCOMING': return 'warning' @@ -175,7 +210,7 @@ const loadDetail = async () => { const loadGroups = async () => { try { const res = await groupbuyingApi.getGroups(id.value, { page: 0, size: 50 }) - groups.value = res.data.content.filter(g => g.status === 'FORMING') + groups.value = res.data.content } catch (e) { console.error('加载团组列表失败', e) } @@ -187,8 +222,8 @@ const handleCreateGroup = async () => { const res = await groupbuyingApi.joinGroup({ groupBuyingId: id.value }) ElMessage.success(res.data.message || '开团成功') router.push(`/groupbuying/group/${res.data.groupId}`) - } catch (e: any) { - ElMessage.error(e.message || '开团失败') + } catch { + // 全局拦截器已处理错误提示 } finally { joining.value = false } @@ -200,8 +235,8 @@ const handleJoinGroup = async (groupId: number) => { const res = await groupbuyingApi.joinGroup({ groupBuyingId: id.value, groupId }) ElMessage.success(res.data.message || '加入成功') router.push(`/groupbuying/group/${res.data.groupId}`) - } catch (e: any) { - ElMessage.error(e.message || '加入失败') + } catch { + // 全局拦截器已处理错误提示 } finally { joining.value = false } diff --git a/flash-sale-frontend/src/pages/groupbuying/group.vue b/flash-sale-frontend/src/pages/groupbuying/group.vue index c42b8b5..d5a8cbe 100644 --- a/flash-sale-frontend/src/pages/groupbuying/group.vue +++ b/flash-sale-frontend/src/pages/groupbuying/group.vue @@ -127,8 +127,8 @@ const handleJoin = async () => { }) ElMessage.success(res.data.message || '加入成功') await loadGroup() - } catch (e: any) { - ElMessage.error(e.message || '加入失败') + } catch { + // 全局拦截器已处理错误提示 } finally { joining.value = false } @@ -146,9 +146,10 @@ const handleCancel = async () => { ElMessage.success('已退出团组') await loadGroup() } catch (e: any) { - if (e !== 'cancel') { - ElMessage.error(e.message || '退出失败') + if (e !== 'cancel' && !(e instanceof Error)) { + // ElMessageBox 取消操作,忽略 } + // API 错误由全局拦截器处理 } finally { cancelling.value = false } diff --git a/flash-sale-frontend/src/pages/order/index.vue b/flash-sale-frontend/src/pages/order/index.vue index 9319900..47a3f0a 100644 --- a/flash-sale-frontend/src/pages/order/index.vue +++ b/flash-sale-frontend/src/pages/order/index.vue @@ -55,7 +55,11 @@ 订单号:{{ order.orderNo }} {{ formatTime(order.createdAt) }} - {{ getStatusText(order.status) }} +
+ 秒杀 + 拼团 + {{ getStatusText(order.status) }} +
diff --git a/flash-sale-frontend/src/styles/index.scss b/flash-sale-frontend/src/styles/index.scss index f2d47ae..61159ae 100644 --- a/flash-sale-frontend/src/styles/index.scss +++ b/flash-sale-frontend/src/styles/index.scss @@ -3,6 +3,7 @@ @tailwind utilities; :root { + --app-header-height: 72px; --tone-0: #fffdf8; --tone-50: #f7f2ea; --tone-100: #efe7dc; diff --git a/flash-sale-frontend/src/types/admin.ts b/flash-sale-frontend/src/types/admin.ts index f59c128..2c8193b 100644 --- a/flash-sale-frontend/src/types/admin.ts +++ b/flash-sale-frontend/src/types/admin.ts @@ -47,6 +47,7 @@ export interface AdminRecentOrderRow { quantity: number totalAmount: number status: string + orderType: 'NORMAL' | 'FLASH_SALE' | 'GROUP_BUYING' createdAt: string isFlashSale: boolean } @@ -81,6 +82,7 @@ export interface AdminOrderRow { quantity: number totalAmount: number status: string + orderType: 'NORMAL' | 'FLASH_SALE' | 'GROUP_BUYING' createdAt: string isFlashSale: boolean } diff --git a/flash-sale-frontend/src/types/api.d.ts b/flash-sale-frontend/src/types/api.d.ts index f7b644c..b61baa4 100644 --- a/flash-sale-frontend/src/types/api.d.ts +++ b/flash-sale-frontend/src/types/api.d.ts @@ -113,6 +113,7 @@ export interface Order { paymentAmount: number paymentMethod?: string status: 'PENDING' | 'PAID' | 'SHIPPED' | 'COMPLETED' | 'CANCELLED' | 'REFUNDING' | 'REFUNDED' + orderType?: 'NORMAL' | 'FLASH_SALE' | 'GROUP_BUYING' items: OrderItem[] address?: OrderAddress remark?: string diff --git a/flash-sale-frontend/src/utils/normalizers.ts b/flash-sale-frontend/src/utils/normalizers.ts index fb952b5..970faf2 100644 --- a/flash-sale-frontend/src/utils/normalizers.ts +++ b/flash-sale-frontend/src/utils/normalizers.ts @@ -57,6 +57,14 @@ export const mapOrderStatus = (status: number | string): Order['status'] => { return 'PENDING' } +export const mapOrderType = (orderType: number | string | undefined, isFlashSale?: boolean): 'NORMAL' | 'FLASH_SALE' | 'GROUP_BUYING' => { + const value = typeof orderType === 'string' ? orderType : toNumber(orderType) + if (value === 'FLASH_SALE' || value === 2) return 'FLASH_SALE' + if (value === 'GROUP_BUYING' || value === 3) return 'GROUP_BUYING' + if (isFlashSale) return 'FLASH_SALE' + return 'NORMAL' +} + export const mapFlashSaleStatus = (status: number | string): FlashSale['status'] => { const value = typeof status === 'string' ? status : toNumber(status) if (value === 'UPCOMING' || value === 1) return 'UPCOMING' @@ -186,6 +194,7 @@ export const normalizeOrder = (order: Record): Order => { paymentAmount: totalAmount, paymentMethod: toString(order.paymentMethod) || (status === 'PENDING' ? undefined : 'ONLINE'), status, + orderType: mapOrderType(order.orderType, order.isFlashSale), items, address: buildOrderAddress(order), remark: toString(order.remark), @@ -237,6 +246,7 @@ export const normalizeAdminRecentOrder = (order: Record): AdminRece quantity: toNumber(order.quantity, 1), totalAmount: toNumber(order.totalAmount ?? order.totalPrice), status: mapOrderStatus(order.status), + orderType: mapOrderType(order.orderType, order.isFlashSale), createdAt: toIsoLikeString(order.createdAt), isFlashSale: Boolean(order.isFlashSale), }) @@ -271,6 +281,7 @@ export const normalizeAdminOrder = (order: Record): AdminOrderRow = quantity: toNumber(order.quantity, 1), totalAmount: toNumber(order.totalAmount), status: mapOrderStatus(order.status), + orderType: mapOrderType(order.orderType, order.isFlashSale), createdAt: toIsoLikeString(order.createdAt), isFlashSale: Boolean(order.isFlashSale), }) diff --git a/src/main/java/com/org/flashsalesystem/controller/ProductReviewController.java b/src/main/java/com/org/flashsalesystem/controller/ProductReviewController.java index 687b44b..a54e870 100644 --- a/src/main/java/com/org/flashsalesystem/controller/ProductReviewController.java +++ b/src/main/java/com/org/flashsalesystem/controller/ProductReviewController.java @@ -57,10 +57,16 @@ public class ProductReviewController { @GetMapping("/check") public ResponseEntity> checkReview(@RequestParam Long orderId, - @RequestParam Long productId) { + @RequestParam Long productId, + HttpServletRequest request) { + Long userId = getCurrentUserId(request); + if (userId == null) { + return unauthorized(); + } + Map response = new HashMap<>(); response.put("success", true); - response.put("data", productReviewService.checkReviewStatus(orderId, productId)); + response.put("data", productReviewService.checkReviewStatus(userId, orderId, productId)); return ResponseEntity.ok(response); } diff --git a/src/main/java/com/org/flashsalesystem/dto/OrderDTO.java b/src/main/java/com/org/flashsalesystem/dto/OrderDTO.java index 5bca457..d0847f4 100644 --- a/src/main/java/com/org/flashsalesystem/dto/OrderDTO.java +++ b/src/main/java/com/org/flashsalesystem/dto/OrderDTO.java @@ -170,9 +170,12 @@ public class OrderDTO { private Long shippedOrders; private Long completedOrders; private Long cancelledOrders; + private Long refundingOrders; + private Long refundedOrders; private BigDecimal totalAmount; private BigDecimal todayAmount; private Long flashSaleOrders; private Long normalOrders; + private Long groupBuyingOrders; } } diff --git a/src/main/java/com/org/flashsalesystem/repository/GroupBuyingMemberRepository.java b/src/main/java/com/org/flashsalesystem/repository/GroupBuyingMemberRepository.java index cfefd97..0c3ccc3 100644 --- a/src/main/java/com/org/flashsalesystem/repository/GroupBuyingMemberRepository.java +++ b/src/main/java/com/org/flashsalesystem/repository/GroupBuyingMemberRepository.java @@ -26,4 +26,11 @@ public interface GroupBuyingMemberRepository extends JpaRepository= gb.getMaxPerUser()) { + throw new RuntimeException("您已达到该活动的限购数量(" + gb.getMaxPerUser() + " 件)"); + } + // 2. 获取商品信息 Product product = productRepository.findById(gb.getProductId()) .orElseThrow(() -> new RuntimeException("商品不存在")); @@ -365,6 +374,17 @@ public class GroupBuyingService { order.setGroupBuyingGroupId(group.getId()); order = orderRepository.save(order); + // 5.1 创建订单明细 + OrderItem orderItem = new OrderItem(); + orderItem.setOrderId(order.getId()); + orderItem.setProductId(product.getId()); + orderItem.setProductName(product.getName()); + orderItem.setProductImageUrl(product.getImageUrl()); + orderItem.setPrice(gb.getGroupPrice()); + orderItem.setQuantity(1); + orderItem.setSubtotal(gb.getGroupPrice()); + orderItemRepository.save(orderItem); + // 6. 创建成员记录 GroupBuyingMember member = new GroupBuyingMember(); member.setGroupId(group.getId()); @@ -385,6 +405,10 @@ public class GroupBuyingService { // Update all members status to SUCCESS groupBuyingMemberRepository.updateStatusByGroupId(group.getId(), 2); log.info("拼团成功: groupId={}, groupNo={}", group.getId(), group.getGroupNo()); + + // 发布拼团成功通知给所有成员 + publishGroupStatusChange(group.getId(), group.getGroupNo(), activityId, + gb.getProductId(), product.getName(), "success"); } // Add to Redis members set @@ -502,6 +526,14 @@ public class GroupBuyingService { // Clean Redis redisService.delete(GB_MEMBERS_PREFIX + group.getId()); + // 发布拼团失败通知 + GroupBuying gb = groupBuyingRepository.findById(group.getGroupBuyingId()).orElse(null); + if (gb != null) { + Product product = productRepository.findById(gb.getProductId()).orElse(null); + publishGroupStatusChange(group.getId(), group.getGroupNo(), group.getGroupBuyingId(), + gb.getProductId(), product != null ? product.getName() : "未知商品", "failed"); + } + log.info("超时团组处理完成: groupId={}", group.getId()); } @@ -649,4 +681,28 @@ public class GroupBuyingService { default: return "未知"; } } + + /** + * 发布拼团状态变更消息(通知所有成员) + */ + private void publishGroupStatusChange(Long groupId, String groupNo, Long activityId, + Long productId, String productName, String action) { + List members = groupBuyingMemberRepository.findByGroupId(groupId); + List memberUserIds = members.stream() + .map(GroupBuyingMember::getUserId) + .collect(Collectors.toList()); + + Map message = new HashMap<>(); + message.put("groupId", groupId); + message.put("groupNo", groupNo); + message.put("activityId", activityId); + message.put("productId", productId); + message.put("productName", productName); + message.put("action", action); + message.put("memberUserIds", memberUserIds); + message.put("timestamp", System.currentTimeMillis()); + + redisService.publish("groupbuying:status:change", message); + log.info("发布拼团状态变更消息: groupId={}, action={}, members={}", groupId, action, memberUserIds.size()); + } } diff --git a/src/main/java/com/org/flashsalesystem/service/MessageListenerService.java b/src/main/java/com/org/flashsalesystem/service/MessageListenerService.java index b20756c..bc5ac1b 100644 --- a/src/main/java/com/org/flashsalesystem/service/MessageListenerService.java +++ b/src/main/java/com/org/flashsalesystem/service/MessageListenerService.java @@ -10,6 +10,8 @@ import org.springframework.data.redis.listener.RedisMessageListenerContainer; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; +import java.util.ArrayList; +import java.util.List; import java.util.Map; /** @@ -64,6 +66,12 @@ public class MessageListenerService { new ChannelTopic("return:status:change") ); + // 拼团状态变更监听 + redisMessageListenerContainer.addMessageListener( + new GroupBuyingStatusChangeListener(), + new ChannelTopic("groupbuying:status:change") + ); + log.info("Redis消息监听器初始化完成"); } @@ -231,6 +239,48 @@ public class MessageListenerService { log.info("退货状态变更通知已创建: 订单ID={}, 操作={}", orderId, action); } + /** + * 处理拼团状态变更 + */ + @SuppressWarnings("unchecked") + private void handleGroupBuyingStatusChange(String action, Map data) { + String groupNo = data.get("groupNo") != null ? data.get("groupNo").toString() : ""; + String productName = data.get("productName") != null ? data.get("productName").toString() : "商品"; + Long activityId = extractLongValue(data.get("activityId")); + + List memberUserIds = new ArrayList<>(); + Object memberObj = data.get("memberUserIds"); + if (memberObj instanceof List) { + for (Object item : (List) memberObj) { + Long uid = extractLongValue(item); + if (uid != null) memberUserIds.add(uid); + } + } + + String title; + String message; + String link = "/groupbuying/" + (activityId != null ? activityId : ""); + + switch (action) { + case "success": + title = "拼团成功"; + message = "您参与的「" + productName + "」拼团已成功!请尽快完成支付。"; + break; + case "failed": + title = "拼团失败"; + message = "很遗憾,您参与的「" + productName + "」拼团未能成团,订单已自动取消。"; + break; + default: + log.info("未知拼团状态变更: {}", action); + return; + } + + for (Long userId : memberUserIds) { + notificationService.createNotification(userId, "groupbuying", title, message, link); + } + log.info("拼团状态变更通知已创建: groupNo={}, action={}, 通知人数={}", groupNo, action, memberUserIds.size()); + } + /** * 检查库存预警 */ @@ -417,4 +467,27 @@ public class MessageListenerService { } } } + + /** + * 拼团状态变更监听器 + */ + private class GroupBuyingStatusChangeListener implements MessageListener { + @Override + public void onMessage(Message message, byte[] pattern) { + try { + String messageBody = new String(message.getBody()); + log.debug("接收到拼团状态变更消息: {}", messageBody); + + Map data = parseRedissonMessage(messageBody); + String action = data.get("action").toString(); + + log.info("拼团状态变更: action={}", action); + + handleGroupBuyingStatusChange(action, data); + + } catch (Exception e) { + log.error("处理拼团状态变更消息失败", e); + } + } + } } diff --git a/src/main/java/com/org/flashsalesystem/service/OrderService.java b/src/main/java/com/org/flashsalesystem/service/OrderService.java index 851ed03..d90ac20 100644 --- a/src/main/java/com/org/flashsalesystem/service/OrderService.java +++ b/src/main/java/com/org/flashsalesystem/service/OrderService.java @@ -3,9 +3,11 @@ package com.org.flashsalesystem.service; import com.org.flashsalesystem.dto.OrderDTO; import com.org.flashsalesystem.dto.ProductDTO; import com.org.flashsalesystem.dto.UserDTO; +import com.org.flashsalesystem.entity.GroupBuyingGroup; import com.org.flashsalesystem.entity.Order; import com.org.flashsalesystem.entity.OrderItem; import com.org.flashsalesystem.entity.UserAddress; +import com.org.flashsalesystem.repository.GroupBuyingGroupRepository; import com.org.flashsalesystem.repository.OrderItemRepository; import com.org.flashsalesystem.repository.OrderRepository; import com.org.flashsalesystem.repository.ProductRepository; @@ -56,6 +58,8 @@ public class OrderService { private FlashSaleService flashSaleService; @Autowired private GroupBuyingService groupBuyingService; + @Autowired + private GroupBuyingGroupRepository groupBuyingGroupRepository; /** * 创建普通订单 @@ -321,6 +325,17 @@ public class OrderService { throw new RuntimeException("无效的状态转换"); } + // 拼团订单发货校验:必须成团后才能发货 + if (newStatus == 3 && order.getOrderType() != null && order.getOrderType() == 3) { + if (order.getGroupBuyingGroupId() == null) { + throw new RuntimeException("拼团订单数据异常,缺少团组信息"); + } + GroupBuyingGroup group = groupBuyingGroupRepository.findById(order.getGroupBuyingGroupId()).orElse(null); + if (group == null || group.getStatus() != 2) { + throw new RuntimeException("拼团尚未成团,不能发货"); + } + } + // 更新状态 order.setStatus(newStatus); if (remark != null && !remark.trim().isEmpty()) { @@ -371,6 +386,20 @@ public class OrderService { throw new RuntimeException("订单状态不正确,无法支付"); } + // 拼团订单校验:团组必须存在且未过期/未失败 + if (order.getOrderType() != null && order.getOrderType() == 3 && order.getGroupBuyingGroupId() != null) { + GroupBuyingGroup group = groupBuyingGroupRepository.findById(order.getGroupBuyingGroupId()).orElse(null); + if (group == null) { + throw new RuntimeException("拼团团组不存在"); + } + if (group.getStatus() == 3) { + throw new RuntimeException("拼团已失败,无法支付"); + } + if (group.getStatus() == 1 && LocalDateTime.now().isAfter(group.getExpireTime())) { + throw new RuntimeException("拼团已过期,无法支付"); + } + } + order.setStatus(2); order.setPaymentMethod(paymentMethod); order.setPaidAt(LocalDateTime.now()); @@ -532,10 +561,13 @@ public class OrderService { statistics.setShippedOrders(orderRepository.countByStatus(3)); statistics.setCompletedOrders(orderRepository.countByStatus(4)); statistics.setCancelledOrders(orderRepository.countByStatus(5)); + statistics.setRefundingOrders(orderRepository.countByStatus(6)); + statistics.setRefundedOrders(orderRepository.countByStatus(7)); // 订单类型统计 statistics.setNormalOrders(orderRepository.countByOrderType(1)); statistics.setFlashSaleOrders(orderRepository.countByOrderType(2)); + statistics.setGroupBuyingOrders(orderRepository.countByOrderType(3)); // 金额统计(这里简化处理,实际应该从数据库聚合查询) List allOrders = orderRepository.findAll(); diff --git a/src/main/java/com/org/flashsalesystem/service/ProductReviewService.java b/src/main/java/com/org/flashsalesystem/service/ProductReviewService.java index 03f8d12..3be3fba 100644 --- a/src/main/java/com/org/flashsalesystem/service/ProductReviewService.java +++ b/src/main/java/com/org/flashsalesystem/service/ProductReviewService.java @@ -95,9 +95,9 @@ public class ProductReviewService { return toDTO(review); } - public ProductReviewDTO.CheckDTO checkReviewStatus(Long orderId, Long productId) { + public ProductReviewDTO.CheckDTO checkReviewStatus(Long userId, Long orderId, Long productId) { ProductReviewDTO.CheckDTO checkDTO = new ProductReviewDTO.CheckDTO(); - Optional review = productReviewRepository.findByOrderIdAndProductId(orderId, productId); + Optional review = productReviewRepository.findByOrderIdAndUserIdAndProductId(orderId, userId, productId); checkDTO.setReviewed(review.isPresent()); review.ifPresent(r -> checkDTO.setReview(toDTO(r))); return checkDTO; diff --git a/src/main/resources/sql/fix-groupbuying-data.sql b/src/main/resources/sql/fix-groupbuying-data.sql new file mode 100644 index 0000000..3dda8a9 --- /dev/null +++ b/src/main/resources/sql/fix-groupbuying-data.sql @@ -0,0 +1,132 @@ +-- ============================================================ +-- 拼团历史数据修复脚本 +-- 修复内容: +-- 1. 已满员但状态仍为 FORMING(1) 的团组 → 更新为 SUCCESS(2) +-- 2. 已成团团组的成员状态从 已加入(1) → 已成团(2) +-- 3. 拼团订单的 order_type 修正为 3(拼团订单) +-- 4. 已过期且未满员的 FORMING 团组 → 更新为 FAILED(3) +-- 5. 已失败团组的成员状态更新 +-- 使用方式:mysql -u root -p flash_sale_db < fix-groupbuying-data.sql +-- ============================================================ + +-- 先查看当前数据状况(仅查询,不修改) +SELECT '====== 修复前数据概况 ======' AS info; + +SELECT '满员但仍为FORMING的团组' AS category, COUNT(*) AS cnt +FROM group_buying_group +WHERE status = 1 AND current_members >= required_members; + +SELECT '已过期但仍为FORMING的团组' AS category, COUNT(*) AS cnt +FROM group_buying_group +WHERE status = 1 AND expire_time < NOW(); + +SELECT '拼团订单但order_type不是3的订单' AS category, COUNT(*) AS cnt +FROM orders +WHERE group_buying_group_id IS NOT NULL AND order_type != 3; + +SELECT '缺少order_items记录的拼团订单' AS category, COUNT(*) AS cnt +FROM orders o +WHERE o.group_buying_group_id IS NOT NULL + AND NOT EXISTS (SELECT 1 FROM order_items oi WHERE oi.order_id = o.id); + +-- ============================================================ +-- 修复1: 已满员的团组 → SUCCESS(2) +-- ============================================================ +UPDATE group_buying_group +SET status = 2, + completed_at = COALESCE(completed_at, NOW()) +WHERE status = 1 + AND current_members >= required_members; + +SELECT CONCAT('修复1完成: ', ROW_COUNT(), ' 个满员团组已更新为已成团') AS result; + +-- ============================================================ +-- 修复2: 已成团(status=2)团组的成员状态 → 已成团(2) +-- ============================================================ +UPDATE group_buying_member m + INNER JOIN group_buying_group g ON m.group_id = g.id +SET m.status = 2 +WHERE g.status = 2 + AND m.status = 1; + +SELECT CONCAT('修复2完成: ', ROW_COUNT(), ' 个成员状态已更新为已成团') AS result; + +-- ============================================================ +-- 修复3: 已过期且未满员的团组 → FAILED(3) +-- ============================================================ +UPDATE group_buying_group +SET status = 3 +WHERE status = 1 + AND expire_time < NOW() + AND current_members < required_members; + +SELECT CONCAT('修复3完成: ', ROW_COUNT(), ' 个过期团组已更新为已失败') AS result; + +-- ============================================================ +-- 修复4: 已失败(status=3)团组的成员状态 → 已退出(3) +-- ============================================================ +UPDATE group_buying_member m + INNER JOIN group_buying_group g ON m.group_id = g.id +SET m.status = 3 +WHERE g.status = 3 + AND m.status = 1; + +SELECT CONCAT('修复4完成: ', ROW_COUNT(), ' 个失败团组成员已更新为已退出') AS result; + +-- ============================================================ +-- 修复5: 拼团关联订单的 order_type 修正为 3 +-- ============================================================ +UPDATE orders +SET order_type = 3 +WHERE group_buying_group_id IS NOT NULL + AND order_type != 3; + +SELECT CONCAT('修复5完成: ', ROW_COUNT(), ' 个拼团订单的order_type已修正') AS result; + +-- ============================================================ +-- 修复6: 补充缺失的 order_items 记录(拼团订单) +-- ============================================================ +INSERT INTO order_items (order_id, product_id, product_name, product_image_url, price, quantity, subtotal, created_at) +SELECT o.id, + o.product_id, + COALESCE(p.name, '未知商品'), + p.image_url, + o.total_price / o.quantity, + o.quantity, + o.total_price, + o.created_at +FROM orders o + LEFT JOIN products p ON o.product_id = p.id +WHERE o.group_buying_group_id IS NOT NULL + AND NOT EXISTS (SELECT 1 FROM order_items oi WHERE oi.order_id = o.id); + +SELECT CONCAT('修复6完成: ', ROW_COUNT(), ' 条缺失的订单明细已补充') AS result; + +-- ============================================================ +-- 修复7: 已失败团组关联的待支付订单 → 取消(5) +-- ============================================================ +UPDATE orders o + INNER JOIN group_buying_group g ON o.group_buying_group_id = g.id +SET o.status = 5 +WHERE g.status = 3 + AND o.status = 1; + +SELECT CONCAT('修复7完成: ', ROW_COUNT(), ' 个失败团组的待支付订单已取消') AS result; + +-- 修复后数据验证 +SELECT '====== 修复后数据验证 ======' AS info; + +SELECT status, + CASE status WHEN 1 THEN '拼团中' WHEN 2 THEN '已成团' WHEN 3 THEN '已失败' END AS status_desc, + COUNT(*) AS cnt +FROM group_buying_group +GROUP BY status +ORDER BY status; + +SELECT '仍有异常的团组(满员但非SUCCESS)' AS category, COUNT(*) AS cnt +FROM group_buying_group +WHERE current_members >= required_members AND status != 2; + +SELECT '仍有异常的团组(过期但仍FORMING)' AS category, COUNT(*) AS cnt +FROM group_buying_group +WHERE expire_time < NOW() AND status = 1;