From 32c1113d4ac9a75e84767bee7e7eae902f896da6 Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Mon, 16 Mar 2026 23:13:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E8=AE=A2=E5=8D=95?= =?UTF-8?q?=E9=80=80=E8=B4=A7=E5=85=A8=E9=93=BE=E8=B7=AF=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=88=E7=94=B3=E8=AF=B7=E3=80=81=E5=AE=A1=E6=A0=B8=E3=80=81?= =?UTF-8?q?=E7=89=A9=E6=B5=81=E3=80=81=E9=80=80=E6=AC=BE=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- flash-sale-frontend/src/api/modules/return.ts | 52 +++ .../src/components/business/ReturnDialog.vue | 105 +++++ .../business/ReturnTrackingDialog.vue | 82 ++++ .../src/layouts/AdminLayout.vue | 6 + .../src/pages/admin/returns.vue | 220 ++++++++++ .../src/pages/order/detail.vue | 86 +++- flash-sale-frontend/src/pages/order/index.vue | 36 +- .../src/pages/user/returns.vue | 146 +++++++ flash-sale-frontend/src/router/index.ts | 12 + flash-sale-frontend/src/types/api.d.ts | 29 +- flash-sale-frontend/src/utils/normalizers.ts | 40 ++ .../controller/OrderReturnController.java | 287 +++++++++++++ .../flashsalesystem/dto/OrderReturnDTO.java | 95 +++++ .../com/org/flashsalesystem/entity/Order.java | 4 +- .../flashsalesystem/entity/OrderReturn.java | 121 ++++++ .../repository/OrderReturnRepository.java | 28 ++ .../service/MessageListenerService.java | 82 ++++ .../service/OrderReturnService.java | 393 ++++++++++++++++++ .../flashsalesystem/service/OrderService.java | 23 +- src/main/resources/application.yml | 5 + src/main/resources/sql/schema.sql | 30 +- 21 files changed, 1870 insertions(+), 12 deletions(-) create mode 100644 flash-sale-frontend/src/api/modules/return.ts create mode 100644 flash-sale-frontend/src/components/business/ReturnDialog.vue create mode 100644 flash-sale-frontend/src/components/business/ReturnTrackingDialog.vue create mode 100644 flash-sale-frontend/src/pages/admin/returns.vue create mode 100644 flash-sale-frontend/src/pages/user/returns.vue create mode 100644 src/main/java/com/org/flashsalesystem/controller/OrderReturnController.java create mode 100644 src/main/java/com/org/flashsalesystem/dto/OrderReturnDTO.java create mode 100644 src/main/java/com/org/flashsalesystem/entity/OrderReturn.java create mode 100644 src/main/java/com/org/flashsalesystem/repository/OrderReturnRepository.java create mode 100644 src/main/java/com/org/flashsalesystem/service/OrderReturnService.java diff --git a/flash-sale-frontend/src/api/modules/return.ts b/flash-sale-frontend/src/api/modules/return.ts new file mode 100644 index 0000000..a97eeb0 --- /dev/null +++ b/flash-sale-frontend/src/api/modules/return.ts @@ -0,0 +1,52 @@ +import { request } from '../request' +import type { ApiResponse, OrderReturn } from '@/types/api' +import { normalizeOrderReturn } from '@/utils/normalizers' + +export const returnApi = { + create(data: { orderId: number; reason: string; description?: string; images?: string }): Promise> { + return request.post('/api/return/create', data).then(normalizeResponse) + }, + + getByOrderId(orderId: number): Promise> { + return request.get(`/api/return/order/${orderId}`) + .then((res: any) => ({ + ...res, + data: res.data ? normalizeOrderReturn(res.data) : null, + })) + }, + + getMyReturns(params?: { status?: number; page?: number; size?: number }): Promise> { + return request.get('/api/return/my', params) + }, + + ship(id: number, returnTracking: string): Promise> { + return request.post(`/api/return/${id}/ship`, { returnTracking }).then(normalizeResponse) + }, + + cancel(id: number): Promise> { + return request.post(`/api/return/${id}/cancel`).then(normalizeResponse) + }, + + adminReview(id: number, data: { status: number; rejectReason?: string; adminRemark?: string }): Promise> { + return request.post(`/api/return/${id}/review`, data).then(normalizeResponse) + }, + + adminComplete(id: number, remark?: string): Promise> { + return request.post(`/api/return/${id}/complete`, { remark }).then(normalizeResponse) + }, + + getAll(params?: { status?: number; page?: number; size?: number }): Promise> { + return request.get('/api/return/all', params) + }, + + getStatistics(): Promise> { + return request.get('/api/return/statistics') + }, +} + +function normalizeResponse(res: any): any { + return { + ...res, + data: res.data ? normalizeOrderReturn(res.data) : res.data, + } +} diff --git a/flash-sale-frontend/src/components/business/ReturnDialog.vue b/flash-sale-frontend/src/components/business/ReturnDialog.vue new file mode 100644 index 0000000..d3ebc21 --- /dev/null +++ b/flash-sale-frontend/src/components/business/ReturnDialog.vue @@ -0,0 +1,105 @@ + + + diff --git a/flash-sale-frontend/src/components/business/ReturnTrackingDialog.vue b/flash-sale-frontend/src/components/business/ReturnTrackingDialog.vue new file mode 100644 index 0000000..8b830d6 --- /dev/null +++ b/flash-sale-frontend/src/components/business/ReturnTrackingDialog.vue @@ -0,0 +1,82 @@ + + + diff --git a/flash-sale-frontend/src/layouts/AdminLayout.vue b/flash-sale-frontend/src/layouts/AdminLayout.vue index 485d3fc..1eed7c4 100644 --- a/flash-sale-frontend/src/layouts/AdminLayout.vue +++ b/flash-sale-frontend/src/layouts/AdminLayout.vue @@ -48,6 +48,11 @@ + + + + + @@ -161,6 +166,7 @@ const currentPageTitle = computed(() => { '/admin/groupbuying': '拼团管理', '/admin/orders': '订单管理', '/admin/users': '用户管理', + '/admin/returns': '退货管理', '/admin/reviews': '评价管理', '/admin/favorites': '收藏管理', '/admin/monitor': '系统监控', diff --git a/flash-sale-frontend/src/pages/admin/returns.vue b/flash-sale-frontend/src/pages/admin/returns.vue new file mode 100644 index 0000000..8ffce98 --- /dev/null +++ b/flash-sale-frontend/src/pages/admin/returns.vue @@ -0,0 +1,220 @@ + + + + + diff --git a/flash-sale-frontend/src/pages/order/detail.vue b/flash-sale-frontend/src/pages/order/detail.vue index 355eaa4..ddf9447 100644 --- a/flash-sale-frontend/src/pages/order/detail.vue +++ b/flash-sale-frontend/src/pages/order/detail.vue @@ -42,9 +42,17 @@ + + @@ -95,6 +103,23 @@ + +
+

退货信息

+
+
退货单号:{{ orderReturn.returnNo }}
+
退货状态:{{ orderReturn.statusText }}
+
退款金额:¥{{ orderReturn.refundAmount }}
+
退货原因:{{ orderReturn.reason }}
+
详细描述:{{ orderReturn.description }}
+
拒绝原因:{{ orderReturn.rejectReason }}
+
物流单号:{{ orderReturn.returnTracking }}
+
管理员备注:{{ orderReturn.adminRemark }}
+
申请时间:{{ formatTime(orderReturn.createdAt) }}
+
完成时间:{{ formatTime(orderReturn.completedAt) }}
+
+
+ + + + + @@ -117,6 +157,10 @@ import type { Order } from '@/types/api' import dayjs from 'dayjs' import SafeImage from '@/components/common/SafeImage.vue' import ReviewDialog from '@/components/business/ReviewDialog.vue' +import ReturnDialog from '@/components/business/ReturnDialog.vue' +import ReturnTrackingDialog from '@/components/business/ReturnTrackingDialog.vue' +import { returnApi } from '@/api/modules/return' +import type { OrderReturn } from '@/types/api' const route = useRoute() const router = useRouter() @@ -126,10 +170,13 @@ const loading = ref(false) const order = ref(null) const reviewDialogVisible = ref(false) const allReviewed = ref(false) +const returnDialogVisible = ref(false) +const trackingDialogVisible = ref(false) +const orderReturn = ref(null) const formatTime = (time: string) => dayjs(time).format('YYYY-MM-DD HH:mm:ss') -const getStatusType = (status: string) => ({ PENDING: 'warning', PAID: 'primary', SHIPPED: 'primary', COMPLETED: 'success', CANCELLED: 'info', REFUNDED: 'danger' }[status] || 'info') -const getStatusText = (status: string) => ({ PENDING: '待付款', PAID: '待发货', SHIPPED: '待收货', COMPLETED: '已完成', CANCELLED: '已取消', REFUNDED: '已退款' }[status] || status) +const getStatusType = (status: string) => ({ PENDING: 'warning', PAID: 'primary', SHIPPED: 'primary', COMPLETED: 'success', CANCELLED: 'info', REFUNDING: 'warning', REFUNDED: 'danger' }[status] || 'info') +const getStatusText = (status: string) => ({ PENDING: '待付款', PAID: '待发货', SHIPPED: '待收货', COMPLETED: '已完成', CANCELLED: '已取消', REFUNDING: '退货中', REFUNDED: '已退货' }[status] || status) const getPaymentMethodText = (method: string) => ({ ONLINE: '在线支付', ALIPAY: '支付宝', WECHAT: '微信支付', CASH: '货到付款', default: '默认支付' }[method] || method) const getActiveStep = () => { @@ -192,6 +239,39 @@ const checkAllReviewed = async () => { } } +const getReturnStatusType = (status: string) => ({ PENDING: 'warning', APPROVED: 'primary', RETURNING: 'primary', COMPLETED: 'success', REJECTED: 'danger', CANCELLED: 'info' }[status] || 'info') + +const loadReturnInfo = async () => { + if (!order.value) return + if (order.value.status !== 'REFUNDING' && order.value.status !== 'REFUNDED') return + try { + const res = await returnApi.getByOrderId(order.value.id) + if (res.success && res.data) { + orderReturn.value = res.data + } + } catch (error) { + console.error('加载退货信息失败:', error) + } +} + +const onReturnSuccess = () => { + loadOrderDetail() + loadReturnInfo() +} + +const handleCancelReturn = async () => { + if (!orderReturn.value) return + await ElMessageBox.confirm('确定要取消退货申请吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }) + try { + await returnApi.cancel(orderReturn.value.id) + ElMessage.success('退货申请已取消') + loadOrderDetail() + loadReturnInfo() + } catch (error) { + console.error('取消退货失败:', error) + } +} + const handleRebuy = async () => { if (!order.value) return const firstItem = order.value.items[0] @@ -208,7 +288,7 @@ const handleDelete = async () => { onMounted(async () => { await loadOrderDetail() - await checkAllReviewed() + await Promise.all([checkAllReviewed(), loadReturnInfo()]) }) diff --git a/flash-sale-frontend/src/pages/order/index.vue b/flash-sale-frontend/src/pages/order/index.vue index c6c2ec1..9319900 100644 --- a/flash-sale-frontend/src/pages/order/index.vue +++ b/flash-sale-frontend/src/pages/order/index.vue @@ -25,6 +25,8 @@ 待收货 已完成 已取消 + 退货中 + 已退货 @@ -87,10 +89,19 @@ + + + + @@ -110,6 +121,14 @@ :order-items="currentReviewOrder.items" @success="onReviewSuccess" /> + + @@ -125,6 +144,7 @@ import type { Order } from '@/types/api' import dayjs from 'dayjs' import SafeImage from '@/components/common/SafeImage.vue' import ReviewDialog from '@/components/business/ReviewDialog.vue' +import ReturnDialog from '@/components/business/ReturnDialog.vue' const router = useRouter() const cartStore = useCartStore() @@ -137,6 +157,8 @@ const pagination = reactive({ page: 1, size: 10, total: 0 }) const reviewDialogVisible = ref(false) const currentReviewOrder = ref(null) const orderReviewStatus = ref>({}) +const returnDialogVisible = ref(false) +const currentReturnOrder = ref(null) const orderStats = ref([ { key: '', label: '全部', count: 0, icon: 'List', color: 'text-gray-500' }, @@ -149,8 +171,8 @@ const orderStats = ref([ const formatTime = (time: string) => dayjs(time).format('YYYY-MM-DD HH:mm:ss') -const getStatusType = (status: string) => ({ PENDING: 'warning', PAID: 'primary', SHIPPED: 'primary', COMPLETED: 'success', CANCELLED: 'info', REFUNDED: 'danger' }[status] || 'info') -const getStatusText = (status: string) => ({ PENDING: '待付款', PAID: '待发货', SHIPPED: '待收货', COMPLETED: '已完成', CANCELLED: '已取消', REFUNDED: '已退款' }[status] || status) +const getStatusType = (status: string) => ({ PENDING: 'warning', PAID: 'primary', SHIPPED: 'primary', COMPLETED: 'success', CANCELLED: 'info', REFUNDING: 'warning', REFUNDED: 'danger' }[status] || 'info') +const getStatusText = (status: string) => ({ PENDING: '待付款', PAID: '待发货', SHIPPED: '待收货', COMPLETED: '已完成', CANCELLED: '已取消', REFUNDING: '退货中', REFUNDED: '已退货' }[status] || status) const loadOrders = async () => { loading.value = true @@ -231,6 +253,16 @@ const onReviewSuccess = () => { } } +const openReturnDialog = (order: Order) => { + currentReturnOrder.value = order + returnDialogVisible.value = true +} + +const onReturnSuccess = () => { + loadOrders() + loadStatistics() +} + const handleRebuy = async (order: Order) => { const firstItem = order.items[0] if (!firstItem) return diff --git a/flash-sale-frontend/src/pages/user/returns.vue b/flash-sale-frontend/src/pages/user/returns.vue new file mode 100644 index 0000000..447e8ff --- /dev/null +++ b/flash-sale-frontend/src/pages/user/returns.vue @@ -0,0 +1,146 @@ + + + + + diff --git a/flash-sale-frontend/src/router/index.ts b/flash-sale-frontend/src/router/index.ts index 31538e9..355772e 100644 --- a/flash-sale-frontend/src/router/index.ts +++ b/flash-sale-frontend/src/router/index.ts @@ -86,6 +86,12 @@ const routes: RouteRecordRaw[] = [ component: () => import('@/pages/user/reviews.vue'), meta: { title: '我的评价', requiresAuth: true } }, + { + path: 'returns', + name: 'MyReturns', + component: () => import('@/pages/user/returns.vue'), + meta: { title: '我的退货', requiresAuth: true } + }, { path: 'notifications', name: 'Notifications', @@ -177,6 +183,12 @@ const routes: RouteRecordRaw[] = [ component: () => import('@/pages/admin/reviews.vue'), meta: { title: '评价管理' } }, + { + path: 'returns', + name: 'AdminReturns', + component: () => import('@/pages/admin/returns.vue'), + meta: { title: '退货管理' } + }, { path: 'favorites', name: 'AdminFavorites', diff --git a/flash-sale-frontend/src/types/api.d.ts b/flash-sale-frontend/src/types/api.d.ts index 8520b18..f7b644c 100644 --- a/flash-sale-frontend/src/types/api.d.ts +++ b/flash-sale-frontend/src/types/api.d.ts @@ -112,7 +112,7 @@ export interface Order { totalAmount: number paymentAmount: number paymentMethod?: string - status: 'PENDING' | 'PAID' | 'SHIPPED' | 'COMPLETED' | 'CANCELLED' | 'REFUNDED' + status: 'PENDING' | 'PAID' | 'SHIPPED' | 'COMPLETED' | 'CANCELLED' | 'REFUNDING' | 'REFUNDED' items: OrderItem[] address?: OrderAddress remark?: string @@ -157,6 +157,33 @@ export interface Statistics { onlineUsers: number } +// 退货类型 +export interface OrderReturn { + id: number + returnNo: string + orderId: number + orderNo: string + userId: number + username: string + refundAmount: number + reason: string + description?: string + images?: string + status: 'PENDING' | 'APPROVED' | 'RETURNING' | 'COMPLETED' | 'REJECTED' | 'CANCELLED' + statusText: string + rejectReason?: string + adminRemark?: string + returnTracking?: string + productName?: string + productImage?: string + reviewedAt?: string + shippedAt?: string + completedAt?: string + cancelledAt?: string + createdAt: string + updatedAt: string +} + // 拼团活动类型 export interface GroupBuying { id: number diff --git a/flash-sale-frontend/src/utils/normalizers.ts b/flash-sale-frontend/src/utils/normalizers.ts index 28e46b1..fb952b5 100644 --- a/flash-sale-frontend/src/utils/normalizers.ts +++ b/flash-sale-frontend/src/utils/normalizers.ts @@ -5,6 +5,7 @@ import type { GroupBuyingGroup, Order, OrderAddress, + OrderReturn, PageResponse, Product, User, @@ -51,6 +52,8 @@ export const mapOrderStatus = (status: number | string): Order['status'] => { if (value === 'SHIPPED' || value === 3) return 'SHIPPED' if (value === 'COMPLETED' || value === 4) return 'COMPLETED' if (value === 'CANCELLED' || value === 5) return 'CANCELLED' + if (value === 'REFUNDING' || value === 6) return 'REFUNDING' + if (value === 'REFUNDED' || value === 7) return 'REFUNDED' return 'PENDING' } @@ -328,6 +331,43 @@ export const normalizeGroupBuying = (gb: Record): GroupBuying => ({ discount: toNumber(gb.discount), }) +export const mapReturnStatus = (status: number | string): OrderReturn['status'] => { + const value = typeof status === 'string' ? status : toNumber(status) + if (value === 'PENDING' || value === 1) return 'PENDING' + if (value === 'APPROVED' || value === 2) return 'APPROVED' + if (value === 'RETURNING' || value === 3) return 'RETURNING' + if (value === 'COMPLETED' || value === 4) return 'COMPLETED' + if (value === 'REJECTED' || value === 5) return 'REJECTED' + if (value === 'CANCELLED' || value === 6) return 'CANCELLED' + return 'PENDING' +} + +export const normalizeOrderReturn = (ret: Record): OrderReturn => ({ + id: toNumber(ret.id), + returnNo: toString(ret.returnNo), + orderId: toNumber(ret.orderId), + orderNo: toString(ret.orderNo), + userId: toNumber(ret.userId), + username: toString(ret.username), + refundAmount: toNumber(ret.refundAmount), + reason: toString(ret.reason), + description: toString(ret.description), + images: toString(ret.images), + status: mapReturnStatus(ret.status), + statusText: toString(ret.statusText), + rejectReason: toString(ret.rejectReason), + adminRemark: toString(ret.adminRemark), + returnTracking: toString(ret.returnTracking), + productName: toString(ret.productName), + productImage: resolveImageUrl(toString(ret.productImage, '')), + reviewedAt: ret.reviewedAt ? toIsoLikeString(ret.reviewedAt) : undefined, + shippedAt: ret.shippedAt ? toIsoLikeString(ret.shippedAt) : undefined, + completedAt: ret.completedAt ? toIsoLikeString(ret.completedAt) : undefined, + cancelledAt: ret.cancelledAt ? toIsoLikeString(ret.cancelledAt) : undefined, + createdAt: toIsoLikeString(ret.createdAt), + updatedAt: toIsoLikeString(ret.updatedAt || ret.createdAt), +}) + export const normalizeGroupBuyingGroup = (group: Record): GroupBuyingGroup => ({ id: toNumber(group.id), groupNo: toString(group.groupNo), diff --git a/src/main/java/com/org/flashsalesystem/controller/OrderReturnController.java b/src/main/java/com/org/flashsalesystem/controller/OrderReturnController.java new file mode 100644 index 0000000..fde5fbb --- /dev/null +++ b/src/main/java/com/org/flashsalesystem/controller/OrderReturnController.java @@ -0,0 +1,287 @@ +package com.org.flashsalesystem.controller; + +import com.org.flashsalesystem.dto.OrderReturnDTO; +import com.org.flashsalesystem.dto.UserDTO; +import com.org.flashsalesystem.service.OrderReturnService; +import com.org.flashsalesystem.service.UserService; +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/return") +@Slf4j +public class OrderReturnController { + + @Autowired + private OrderReturnService orderReturnService; + + @Autowired + private UserService userService; + + /** + * 用户申请退货 + */ + @PostMapping("/create") + public ResponseEntity> createReturn(@Validated @RequestBody OrderReturnDTO.CreateDTO createDTO, + HttpServletRequest request) { + try { + Long userId = getCurrentUserId(request); + if (userId == null) { + return createUnauthorizedResponse(); + } + + OrderReturnDTO result = orderReturnService.createReturn(userId, createDTO); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "退货申请已提交"); + response.put("data", result); + return ResponseEntity.ok(response); + } catch (Exception e) { + log.error("申请退货失败", e); + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", e.getMessage()); + return ResponseEntity.badRequest().body(response); + } + } + + /** + * 查询订单退货信息 + */ + @GetMapping("/order/{orderId}") + public ResponseEntity> getReturnByOrder(@PathVariable Long orderId, + HttpServletRequest request) { + try { + Long userId = getCurrentUserId(request); + if (userId == null) { + return createUnauthorizedResponse(); + } + + OrderReturnDTO result = orderReturnService.getReturnByOrderId(orderId); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", result); + return ResponseEntity.ok(response); + } catch (Exception e) { + log.error("查询退货信息失败", e); + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", e.getMessage()); + return ResponseEntity.badRequest().body(response); + } + } + + /** + * 用户退货列表 + */ + @GetMapping("/my") + public ResponseEntity> getMyReturns(@RequestParam(required = false) Integer status, + @RequestParam(defaultValue = "0") Integer page, + @RequestParam(defaultValue = "10") Integer size, + HttpServletRequest request) { + try { + Long userId = getCurrentUserId(request); + if (userId == null) { + return createUnauthorizedResponse(); + } + + OrderReturnDTO.QueryDTO queryDTO = new OrderReturnDTO.QueryDTO(); + queryDTO.setStatus(status); + queryDTO.setPage(page); + queryDTO.setSize(size); + + Map result = orderReturnService.getUserReturns(userId, queryDTO); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", result); + return ResponseEntity.ok(response); + } catch (Exception e) { + log.error("获取退货列表失败", e); + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", e.getMessage()); + return ResponseEntity.badRequest().body(response); + } + } + + /** + * 用户填写物流单号 + */ + @PostMapping("/{id}/ship") + public ResponseEntity> shipReturn(@PathVariable Long id, + @Validated @RequestBody OrderReturnDTO.ShipDTO shipDTO, + HttpServletRequest request) { + try { + Long userId = getCurrentUserId(request); + if (userId == null) { + return createUnauthorizedResponse(); + } + + OrderReturnDTO result = orderReturnService.userShipReturn(userId, id, shipDTO); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "物流信息已提交"); + response.put("data", result); + return ResponseEntity.ok(response); + } catch (Exception e) { + log.error("提交物流信息失败", e); + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", e.getMessage()); + return ResponseEntity.badRequest().body(response); + } + } + + /** + * 用户取消退货 + */ + @PostMapping("/{id}/cancel") + public ResponseEntity> cancelReturn(@PathVariable Long id, + HttpServletRequest request) { + try { + Long userId = getCurrentUserId(request); + if (userId == null) { + return createUnauthorizedResponse(); + } + + OrderReturnDTO result = orderReturnService.cancelReturn(userId, id); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "退货申请已取消"); + response.put("data", result); + return ResponseEntity.ok(response); + } catch (Exception e) { + log.error("取消退货失败", e); + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", e.getMessage()); + return ResponseEntity.badRequest().body(response); + } + } + + /** + * 管理员审核退货 + */ + @PostMapping("/{id}/review") + public ResponseEntity> adminReview(@PathVariable Long id, + @Validated @RequestBody OrderReturnDTO.ReviewDTO reviewDTO) { + try { + OrderReturnDTO result = orderReturnService.adminReviewReturn(id, reviewDTO); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "审核完成"); + response.put("data", result); + return ResponseEntity.ok(response); + } catch (Exception e) { + log.error("审核退货失败", e); + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", e.getMessage()); + return ResponseEntity.badRequest().body(response); + } + } + + /** + * 管理员确认退款 + */ + @PostMapping("/{id}/complete") + public ResponseEntity> adminComplete(@PathVariable Long id, + @RequestBody(required = false) Map body) { + try { + String remark = body != null ? body.get("remark") : null; + OrderReturnDTO result = orderReturnService.adminCompleteReturn(id, remark); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "退款已完成"); + response.put("data", result); + return ResponseEntity.ok(response); + } catch (Exception e) { + log.error("确认退款失败", e); + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", e.getMessage()); + return ResponseEntity.badRequest().body(response); + } + } + + /** + * 管理员查看全部退货 + */ + @GetMapping("/all") + public ResponseEntity> getAllReturns(@RequestParam(required = false) Integer status, + @RequestParam(defaultValue = "0") Integer page, + @RequestParam(defaultValue = "10") Integer size) { + try { + OrderReturnDTO.QueryDTO queryDTO = new OrderReturnDTO.QueryDTO(); + queryDTO.setStatus(status); + queryDTO.setPage(page); + queryDTO.setSize(size); + + Map result = orderReturnService.getAllReturns(queryDTO); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", result); + return ResponseEntity.ok(response); + } catch (Exception e) { + log.error("获取退货列表失败", e); + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", e.getMessage()); + return ResponseEntity.badRequest().body(response); + } + } + + /** + * 退货统计 + */ + @GetMapping("/statistics") + public ResponseEntity> getStatistics() { + try { + OrderReturnDTO.StatisticsDTO stats = orderReturnService.getReturnStatistics(); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", stats); + return ResponseEntity.ok(response); + } catch (Exception e) { + log.error("获取退货统计失败", e); + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", e.getMessage()); + return ResponseEntity.badRequest().body(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> createUnauthorizedResponse() { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "用户未登录或登录已过期"); + return ResponseEntity.status(401).body(response); + } +} diff --git a/src/main/java/com/org/flashsalesystem/dto/OrderReturnDTO.java b/src/main/java/com/org/flashsalesystem/dto/OrderReturnDTO.java new file mode 100644 index 0000000..29c7a6d --- /dev/null +++ b/src/main/java/com/org/flashsalesystem/dto/OrderReturnDTO.java @@ -0,0 +1,95 @@ +package com.org.flashsalesystem.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OrderReturnDTO { + private Long id; + private String returnNo; + private Long orderId; + private String orderNo; + private Long userId; + private String username; + private BigDecimal refundAmount; + private String reason; + private String description; + private String images; + private Integer status; + private String statusText; + private String rejectReason; + private String adminRemark; + private String returnTracking; + private String productName; + private String productImage; + private LocalDateTime reviewedAt; + private LocalDateTime shippedAt; + private LocalDateTime completedAt; + private LocalDateTime cancelledAt; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class CreateDTO { + @NotNull(message = "订单ID不能为空") + private Long orderId; + + @NotBlank(message = "退货原因不能为空") + private String reason; + + private String description; + + private String images; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class ReviewDTO { + @NotNull(message = "审核结果不能为空") + private Integer status; + + private String rejectReason; + private String adminRemark; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class ShipDTO { + @NotBlank(message = "物流单号不能为空") + private String returnTracking; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class QueryDTO { + private Integer status; + private Integer page = 0; + private Integer size = 10; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class StatisticsDTO { + private Long pendingCount; + private Long approvedCount; + private Long returningCount; + private Long completedCount; + private Long rejectedCount; + private Long cancelledCount; + private Long totalCount; + } +} diff --git a/src/main/java/com/org/flashsalesystem/entity/Order.java b/src/main/java/com/org/flashsalesystem/entity/Order.java index 931221a..c83660c 100644 --- a/src/main/java/com/org/flashsalesystem/entity/Order.java +++ b/src/main/java/com/org/flashsalesystem/entity/Order.java @@ -127,7 +127,9 @@ public class Order { PAID(2, "已支付"), SHIPPED(3, "已发货"), COMPLETED(4, "已完成"), - CANCELLED(5, "已取消"); + CANCELLED(5, "已取消"), + REFUNDING(6, "退货中"), + REFUNDED(7, "已退货"); private final int code; private final String description; diff --git a/src/main/java/com/org/flashsalesystem/entity/OrderReturn.java b/src/main/java/com/org/flashsalesystem/entity/OrderReturn.java new file mode 100644 index 0000000..3b14a11 --- /dev/null +++ b/src/main/java/com/org/flashsalesystem/entity/OrderReturn.java @@ -0,0 +1,121 @@ +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; + +/** + * 订单退货实体类 + * 对应数据库order_returns表 + */ +@Entity +@Table(name = "order_returns") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OrderReturn { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "return_no", nullable = false, unique = true, length = 64) + private String returnNo; + + @Column(name = "order_id", nullable = false) + private Long orderId; + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Column(name = "refund_amount", nullable = false, precision = 10, scale = 2) + private BigDecimal refundAmount; + + @Column(nullable = false, length = 500) + private String reason; + + @Column(columnDefinition = "TEXT") + private String description; + + @Column(length = 2000) + private String images; + + /** + * 退货状态:1-待审核 2-已同意 3-退货中 4-已完成 5-已拒绝 6-已取消 + */ + @Column(nullable = false) + private Integer status = 1; + + @Column(name = "reject_reason", length = 500) + private String rejectReason; + + @Column(name = "admin_remark", length = 500) + private String adminRemark; + + @Column(name = "return_tracking", length = 100) + private String returnTracking; + + @Column(name = "reviewed_at") + private LocalDateTime reviewedAt; + + @Column(name = "shipped_at") + private LocalDateTime shippedAt; + + @Column(name = "completed_at") + private LocalDateTime completedAt; + + @Column(name = "cancelled_at") + private LocalDateTime cancelledAt; + + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @PrePersist + protected void onCreate() { + createdAt = LocalDateTime.now(); + updatedAt = LocalDateTime.now(); + if (returnNo == null || returnNo.trim().isEmpty()) { + returnNo = "RT" + System.currentTimeMillis(); + } + } + + @PreUpdate + protected void onUpdate() { + updatedAt = LocalDateTime.now(); + } + + /** + * 退货状态枚举 + */ + public enum ReturnStatus { + PENDING(1, "待审核"), + APPROVED(2, "已同意"), + RETURNING(3, "退货中"), + COMPLETED(4, "已完成"), + REJECTED(5, "已拒绝"), + CANCELLED(6, "已取消"); + + private final int code; + private final String description; + + ReturnStatus(int code, String description) { + this.code = code; + this.description = description; + } + + public int getCode() { + return code; + } + + public String getDescription() { + return description; + } + } +} diff --git a/src/main/java/com/org/flashsalesystem/repository/OrderReturnRepository.java b/src/main/java/com/org/flashsalesystem/repository/OrderReturnRepository.java new file mode 100644 index 0000000..801441e --- /dev/null +++ b/src/main/java/com/org/flashsalesystem/repository/OrderReturnRepository.java @@ -0,0 +1,28 @@ +package com.org.flashsalesystem.repository; + +import com.org.flashsalesystem.entity.OrderReturn; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface OrderReturnRepository extends JpaRepository { + + Optional findByOrderId(Long orderId); + + boolean existsByOrderIdAndStatusIn(Long orderId, List statuses); + + Page findByUserIdOrderByCreatedAtDesc(Long userId, Pageable pageable); + + Page findByUserIdAndStatusOrderByCreatedAtDesc(Long userId, Integer status, Pageable pageable); + + Page findByStatusOrderByCreatedAtDesc(Integer status, Pageable pageable); + + Page findAllByOrderByCreatedAtDesc(Pageable pageable); + + long countByStatus(Integer status); +} diff --git a/src/main/java/com/org/flashsalesystem/service/MessageListenerService.java b/src/main/java/com/org/flashsalesystem/service/MessageListenerService.java index 8d08c92..b20756c 100644 --- a/src/main/java/com/org/flashsalesystem/service/MessageListenerService.java +++ b/src/main/java/com/org/flashsalesystem/service/MessageListenerService.java @@ -58,6 +58,12 @@ public class MessageListenerService { new ChannelTopic("user:action") ); + // 退货状态变更监听 + redisMessageListenerContainer.addMessageListener( + new ReturnStatusChangeListener(), + new ChannelTopic("return:status:change") + ); + log.info("Redis消息监听器初始化完成"); } @@ -175,6 +181,56 @@ public class MessageListenerService { } } + /** + * 处理退货状态变更 + */ + private void handleReturnStatusChange(Long userId, Long orderId, String action, Map data) { + if (userId == null) { + log.warn("退货状态变更缺少用户ID: orderId={}", orderId); + return; + } + + String title; + String message; + String link = "/order/" + orderId; + + switch (action) { + case "created": + title = "退货申请已提交"; + message = "您的订单 #" + orderId + " 退货申请已提交,请等待审核"; + break; + case "approved": + title = "退货申请已通过"; + message = "您的订单 #" + orderId + " 退货申请已通过,请尽快寄回商品"; + break; + case "rejected": + String rejectReason = data.get("reason") != null ? data.get("reason").toString() : ""; + title = "退货申请已被拒绝"; + message = "您的订单 #" + orderId + " 退货申请已被拒绝" + (rejectReason.isEmpty() ? "" : ":" + rejectReason); + break; + case "returning": + title = "退货商品已寄出"; + message = "您的订单 #" + orderId + " 退货商品已寄出,等待商家确认"; + break; + case "completed": + Object amountObj = data.get("amount"); + String amount = amountObj != null ? amountObj.toString() : ""; + title = "退款已完成"; + message = "您的订单 #" + orderId + " 退款已完成" + (amount.isEmpty() ? "" : ",¥" + amount + " 已退回"); + break; + case "cancelled": + title = "退货申请已取消"; + message = "您的订单 #" + orderId + " 退货申请已取消"; + break; + default: + log.info("未知退货状态变更: {}", action); + return; + } + + notificationService.createNotification(userId, "return", title, message, link); + log.info("退货状态变更通知已创建: 订单ID={}, 操作={}", orderId, action); + } + /** * 检查库存预警 */ @@ -335,4 +391,30 @@ public class MessageListenerService { } } } + + /** + * 退货状态变更监听器 + */ + private class ReturnStatusChangeListener implements MessageListener { + @Override + public void onMessage(Message message, byte[] pattern) { + try { + String messageBody = new String(message.getBody()); + log.debug("接收到退货状态变更消息: {}", messageBody); + + Map data = parseRedissonMessage(messageBody); + + Long userId = extractLongValue(data.get("userId")); + Long orderId = extractLongValue(data.get("orderId")); + String action = data.get("action").toString(); + + log.info("退货状态变更: 用户ID={}, 订单ID={}, 操作={}", userId, orderId, action); + + handleReturnStatusChange(userId, orderId, action, data); + + } catch (Exception e) { + log.error("处理退货状态变更消息失败", e); + } + } + } } diff --git a/src/main/java/com/org/flashsalesystem/service/OrderReturnService.java b/src/main/java/com/org/flashsalesystem/service/OrderReturnService.java new file mode 100644 index 0000000..98d839a --- /dev/null +++ b/src/main/java/com/org/flashsalesystem/service/OrderReturnService.java @@ -0,0 +1,393 @@ +package com.org.flashsalesystem.service; + +import com.org.flashsalesystem.dto.OrderReturnDTO; +import com.org.flashsalesystem.dto.OrderDTO; +import com.org.flashsalesystem.dto.ProductDTO; +import com.org.flashsalesystem.dto.UserDTO; +import com.org.flashsalesystem.entity.Order; +import com.org.flashsalesystem.entity.OrderReturn; +import com.org.flashsalesystem.repository.OrderRepository; +import com.org.flashsalesystem.repository.OrderReturnRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.*; + +@Service +@Slf4j +public class OrderReturnService { + + @Value("${flashsale.return.max-days-after-completion:7}") + private int maxDaysAfterCompletion; + + @Autowired + private OrderReturnRepository orderReturnRepository; + + @Autowired + private OrderRepository orderRepository; + + @Autowired + private OrderService orderService; + + @Autowired + private ProductService productService; + + @Autowired + private UserService userService; + + @Autowired + private RedisService redisService; + + /** + * 用户申请退货 + */ + @Transactional + public OrderReturnDTO createReturn(Long userId, OrderReturnDTO.CreateDTO createDTO) { + log.info("用户申请退货: userId={}, orderId={}", userId, createDTO.getOrderId()); + + Optional orderOpt = orderRepository.findById(createDTO.getOrderId()); + if (!orderOpt.isPresent()) { + throw new RuntimeException("订单不存在"); + } + + Order order = orderOpt.get(); + + // 校验订单归属 + if (!order.getUserId().equals(userId)) { + throw new RuntimeException("无权限操作此订单"); + } + + // 校验订单状态必须为已完成 + if (order.getStatus() != 4) { + throw new RuntimeException("只有已完成的订单可以申请退货"); + } + + // 校验7天窗口期 + if (order.getCompletedAt() != null) { + LocalDateTime deadline = order.getCompletedAt().plusDays(maxDaysAfterCompletion); + if (LocalDateTime.now().isAfter(deadline)) { + throw new RuntimeException("已超过退货期限(确认收货后" + maxDaysAfterCompletion + "天内可申请)"); + } + } + + // 校验是否有活跃的退货申请 + List activeStatuses = Arrays.asList(1, 2, 3); + if (orderReturnRepository.existsByOrderIdAndStatusIn(createDTO.getOrderId(), activeStatuses)) { + throw new RuntimeException("该订单已有退货申请正在处理中"); + } + + // 创建退货单 + OrderReturn orderReturn = new OrderReturn(); + orderReturn.setOrderId(order.getId()); + orderReturn.setUserId(userId); + orderReturn.setRefundAmount(order.getTotalPrice()); + orderReturn.setReason(createDTO.getReason()); + orderReturn.setDescription(createDTO.getDescription()); + orderReturn.setImages(createDTO.getImages()); + orderReturn.setStatus(1); + + orderReturn = orderReturnRepository.save(orderReturn); + + // 更新订单状态为退货中 + orderService.updateOrderStatus(order.getId(), 6, "用户申请退货"); + + // 发布退货状态变更消息 + publishReturnStatusChange(orderReturn, "created"); + + log.info("退货申请创建成功: returnId={}", orderReturn.getId()); + return buildReturnDTO(orderReturn); + } + + /** + * 管理员审核退货 + */ + @Transactional + public OrderReturnDTO adminReviewReturn(Long returnId, OrderReturnDTO.ReviewDTO reviewDTO) { + log.info("管理员审核退货: returnId={}, status={}", returnId, reviewDTO.getStatus()); + + OrderReturn orderReturn = orderReturnRepository.findById(returnId) + .orElseThrow(() -> new RuntimeException("退货单不存在")); + + if (orderReturn.getStatus() != 1) { + throw new RuntimeException("只有待审核的退货单可以审核"); + } + + if (reviewDTO.getStatus() != 2 && reviewDTO.getStatus() != 5) { + throw new RuntimeException("审核结果只能是同意(2)或拒绝(5)"); + } + + orderReturn.setStatus(reviewDTO.getStatus()); + orderReturn.setAdminRemark(reviewDTO.getAdminRemark()); + orderReturn.setReviewedAt(LocalDateTime.now()); + + if (reviewDTO.getStatus() == 5) { + // 拒绝:恢复订单状态 + orderReturn.setRejectReason(reviewDTO.getRejectReason()); + orderService.updateOrderStatus(orderReturn.getOrderId(), 4, "退货申请被拒绝"); + publishReturnStatusChange(orderReturn, "rejected"); + } else { + // 同意 + publishReturnStatusChange(orderReturn, "approved"); + } + + orderReturn = orderReturnRepository.save(orderReturn); + log.info("退货审核完成: returnId={}, status={}", returnId, reviewDTO.getStatus()); + return buildReturnDTO(orderReturn); + } + + /** + * 用户填写物流单号 + */ + @Transactional + public OrderReturnDTO userShipReturn(Long userId, Long returnId, OrderReturnDTO.ShipDTO shipDTO) { + log.info("用户填写退货物流: userId={}, returnId={}", userId, returnId); + + OrderReturn orderReturn = orderReturnRepository.findById(returnId) + .orElseThrow(() -> new RuntimeException("退货单不存在")); + + if (!orderReturn.getUserId().equals(userId)) { + throw new RuntimeException("无权限操作此退货单"); + } + + if (orderReturn.getStatus() != 2) { + throw new RuntimeException("只有已同意的退货单可以填写物流信息"); + } + + orderReturn.setReturnTracking(shipDTO.getReturnTracking()); + orderReturn.setStatus(3); + orderReturn.setShippedAt(LocalDateTime.now()); + + orderReturn = orderReturnRepository.save(orderReturn); + + publishReturnStatusChange(orderReturn, "returning"); + + log.info("退货物流信息已更新: returnId={}", returnId); + return buildReturnDTO(orderReturn); + } + + /** + * 管理员确认退款 + */ + @Transactional + public OrderReturnDTO adminCompleteReturn(Long returnId, String remark) { + log.info("管理员确认退款: returnId={}", returnId); + + OrderReturn orderReturn = orderReturnRepository.findById(returnId) + .orElseThrow(() -> new RuntimeException("退货单不存在")); + + if (orderReturn.getStatus() != 3) { + throw new RuntimeException("只有退货中的退货单可以确认退款"); + } + + orderReturn.setStatus(4); + orderReturn.setCompletedAt(LocalDateTime.now()); + if (remark != null && !remark.trim().isEmpty()) { + orderReturn.setAdminRemark(remark); + } + + orderReturn = orderReturnRepository.save(orderReturn); + + // 更新订单状态为已退货 + orderService.updateOrderStatus(orderReturn.getOrderId(), 7, "退货退款已完成"); + + // 恢复商品库存(普通订单) + Optional orderOpt = orderRepository.findById(orderReturn.getOrderId()); + if (orderOpt.isPresent()) { + Order order = orderOpt.get(); + if (order.getOrderType() == 1) { + productService.updateStock(order.getProductId(), order.getQuantity(), "increase"); + log.info("退货库存已恢复: productId={}, quantity={}", order.getProductId(), order.getQuantity()); + } + } + + // 发布退货完成消息 + Map extraData = new HashMap<>(); + extraData.put("amount", orderReturn.getRefundAmount().toString()); + publishReturnStatusChange(orderReturn, "completed", extraData); + + log.info("退货退款完成: returnId={}", returnId); + return buildReturnDTO(orderReturn); + } + + /** + * 用户取消退货 + */ + @Transactional + public OrderReturnDTO cancelReturn(Long userId, Long returnId) { + log.info("用户取消退货: userId={}, returnId={}", userId, returnId); + + OrderReturn orderReturn = orderReturnRepository.findById(returnId) + .orElseThrow(() -> new RuntimeException("退货单不存在")); + + if (!orderReturn.getUserId().equals(userId)) { + throw new RuntimeException("无权限操作此退货单"); + } + + if (orderReturn.getStatus() != 1 && orderReturn.getStatus() != 2) { + throw new RuntimeException("当前状态不允许取消退货"); + } + + orderReturn.setStatus(6); + orderReturn.setCancelledAt(LocalDateTime.now()); + orderReturn = orderReturnRepository.save(orderReturn); + + // 恢复订单状态为已完成 + orderService.updateOrderStatus(orderReturn.getOrderId(), 4, "用户取消退货申请"); + + publishReturnStatusChange(orderReturn, "cancelled"); + + log.info("退货申请已取消: returnId={}", returnId); + return buildReturnDTO(orderReturn); + } + + /** + * 查询订单退货信息 + */ + public OrderReturnDTO getReturnByOrderId(Long orderId) { + Optional returnOpt = orderReturnRepository.findByOrderId(orderId); + return returnOpt.map(this::buildReturnDTO).orElse(null); + } + + /** + * 用户退货列表 + */ + public Map getUserReturns(Long userId, OrderReturnDTO.QueryDTO queryDTO) { + Pageable pageable = PageRequest.of(queryDTO.getPage(), queryDTO.getSize()); + Page page; + + if (queryDTO.getStatus() != null) { + page = orderReturnRepository.findByUserIdAndStatusOrderByCreatedAtDesc(userId, queryDTO.getStatus(), pageable); + } else { + page = orderReturnRepository.findByUserIdOrderByCreatedAtDesc(userId, pageable); + } + + return buildPageResult(page); + } + + /** + * 管理员退货列表 + */ + public Map getAllReturns(OrderReturnDTO.QueryDTO queryDTO) { + Pageable pageable = PageRequest.of(queryDTO.getPage(), queryDTO.getSize()); + Page page; + + if (queryDTO.getStatus() != null) { + page = orderReturnRepository.findByStatusOrderByCreatedAtDesc(queryDTO.getStatus(), pageable); + } else { + page = orderReturnRepository.findAllByOrderByCreatedAtDesc(pageable); + } + + return buildPageResult(page); + } + + /** + * 退货统计 + */ + public OrderReturnDTO.StatisticsDTO getReturnStatistics() { + OrderReturnDTO.StatisticsDTO stats = new OrderReturnDTO.StatisticsDTO(); + stats.setPendingCount(orderReturnRepository.countByStatus(1)); + stats.setApprovedCount(orderReturnRepository.countByStatus(2)); + stats.setReturningCount(orderReturnRepository.countByStatus(3)); + stats.setCompletedCount(orderReturnRepository.countByStatus(4)); + stats.setRejectedCount(orderReturnRepository.countByStatus(5)); + stats.setCancelledCount(orderReturnRepository.countByStatus(6)); + stats.setTotalCount(orderReturnRepository.count()); + return stats; + } + + private OrderReturnDTO buildReturnDTO(OrderReturn orderReturn) { + OrderReturnDTO dto = new OrderReturnDTO(); + dto.setId(orderReturn.getId()); + dto.setReturnNo(orderReturn.getReturnNo()); + dto.setOrderId(orderReturn.getOrderId()); + dto.setUserId(orderReturn.getUserId()); + dto.setRefundAmount(orderReturn.getRefundAmount()); + dto.setReason(orderReturn.getReason()); + dto.setDescription(orderReturn.getDescription()); + dto.setImages(orderReturn.getImages()); + dto.setStatus(orderReturn.getStatus()); + dto.setStatusText(getReturnStatusText(orderReturn.getStatus())); + dto.setRejectReason(orderReturn.getRejectReason()); + dto.setAdminRemark(orderReturn.getAdminRemark()); + dto.setReturnTracking(orderReturn.getReturnTracking()); + dto.setReviewedAt(orderReturn.getReviewedAt()); + dto.setShippedAt(orderReturn.getShippedAt()); + dto.setCompletedAt(orderReturn.getCompletedAt()); + dto.setCancelledAt(orderReturn.getCancelledAt()); + dto.setCreatedAt(orderReturn.getCreatedAt()); + dto.setUpdatedAt(orderReturn.getUpdatedAt()); + + // 关联订单信息 + Optional orderOpt = orderRepository.findById(orderReturn.getOrderId()); + if (orderOpt.isPresent()) { + Order order = orderOpt.get(); + dto.setOrderNo(order.getOrderNo()); + ProductDTO product = productService.getProductById(order.getProductId()); + if (product != null) { + dto.setProductName(product.getName()); + dto.setProductImage(product.getImageUrl()); + } + } + + // 用户信息 + UserDTO user = userService.getUserById(orderReturn.getUserId()); + if (user != null) { + dto.setUsername(user.getUsername()); + } + + return dto; + } + + private String getReturnStatusText(Integer status) { + switch (status) { + case 1: return "待审核"; + case 2: return "已同意"; + case 3: return "退货中"; + case 4: return "已完成"; + case 5: return "已拒绝"; + case 6: return "已取消"; + default: return "未知状态"; + } + } + + private Map buildPageResult(Page page) { + List dtos = new ArrayList<>(); + for (OrderReturn orderReturn : page.getContent()) { + dtos.add(buildReturnDTO(orderReturn)); + } + + Map result = new HashMap<>(); + result.put("content", dtos); + result.put("totalElements", page.getTotalElements()); + result.put("totalPages", page.getTotalPages()); + result.put("currentPage", page.getNumber()); + result.put("size", page.getSize()); + return result; + } + + private void publishReturnStatusChange(OrderReturn orderReturn, String action) { + publishReturnStatusChange(orderReturn, action, null); + } + + private void publishReturnStatusChange(OrderReturn orderReturn, String action, Map extraData) { + Map message = new HashMap<>(); + message.put("returnId", orderReturn.getId()); + message.put("orderId", orderReturn.getOrderId()); + message.put("userId", orderReturn.getUserId()); + message.put("status", orderReturn.getStatus()); + message.put("action", action); + message.put("timestamp", System.currentTimeMillis()); + if (extraData != null) { + message.putAll(extraData); + } + + redisService.publish("return:status:change", message); + } +} diff --git a/src/main/java/com/org/flashsalesystem/service/OrderService.java b/src/main/java/com/org/flashsalesystem/service/OrderService.java index 01923de..851ed03 100644 --- a/src/main/java/com/org/flashsalesystem/service/OrderService.java +++ b/src/main/java/com/org/flashsalesystem/service/OrderService.java @@ -507,8 +507,8 @@ public class OrderService { throw new RuntimeException("无权限删除此订单"); } - if (order.getStatus() != 4 && order.getStatus() != 5) { - throw new RuntimeException("只有已完成或已取消的订单允许删除"); + if (order.getStatus() != 4 && order.getStatus() != 5 && order.getStatus() != 7) { + throw new RuntimeException("只有已完成、已取消或已退货的订单允许删除"); } orderRepository.deleteById(orderId); @@ -764,8 +764,10 @@ public class OrderService { validTransitions.put(1, Arrays.asList(2, 5)); // 待支付 -> 已支付/已取消 validTransitions.put(2, Arrays.asList(3, 5)); // 已支付 -> 已发货/已取消 validTransitions.put(3, Collections.singletonList(4)); // 已发货 -> 已完成 - validTransitions.put(4, Collections.emptyList()); // 已完成 -> 无 + validTransitions.put(4, Collections.singletonList(6)); // 已完成 -> 退货中 validTransitions.put(5, Collections.emptyList()); // 已取消 -> 无 + validTransitions.put(6, Arrays.asList(4, 7)); // 退货中 -> 已完成(拒绝/取消)/已退货 + validTransitions.put(7, Collections.emptyList()); // 已退货 -> 无 List allowedTransitions = validTransitions.get(fromStatus); return allowedTransitions != null && allowedTransitions.contains(toStatus); @@ -796,7 +798,16 @@ public class OrderService { // 取消 if (newStatus == 5) { log.info("订单已取消: 订单ID={}", order.getId()); - // 这里可以添加订单取消后的业务逻辑 + } + + // 退货中 + if (newStatus == 6) { + log.info("订单退货中: 订单ID={}", order.getId()); + } + + // 已退货 + if (newStatus == 7) { + log.info("订单已退货: 订单ID={}", order.getId()); } } @@ -865,6 +876,10 @@ public class OrderService { return "已完成"; case 5: return "已取消"; + case 6: + return "退货中"; + case 7: + return "已退货"; default: return "未知状态"; } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 1758d9d..8be3838 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -132,6 +132,11 @@ flashsale: # 秒杀活动缓存过期时间(分钟) flashsale-expire-minutes: 10 + # 退货配置 + return: + # 确认收货后可申请退货的天数 + max-days-after-completion: 7 + # 消息队列配置 mq: # 订单状态变更通知频道 diff --git a/src/main/resources/sql/schema.sql b/src/main/resources/sql/schema.sql index bf9bfc8..847a90a 100644 --- a/src/main/resources/sql/schema.sql +++ b/src/main/resources/sql/schema.sql @@ -277,7 +277,35 @@ CREATE TABLE IF NOT EXISTS group_buying_member ( COLLATE = utf8mb4_unicode_ci COMMENT ='拼团成员表'; -- ================================ --- 12. 视图 +-- 12. 退货表 +-- ================================ +CREATE TABLE IF NOT EXISTS order_returns ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + return_no VARCHAR(64) NOT NULL UNIQUE COMMENT '退货单号', + order_id BIGINT NOT NULL COMMENT '订单ID', + user_id BIGINT NOT NULL COMMENT '用户ID', + refund_amount DECIMAL(10,2) NOT NULL COMMENT '退款金额', + reason VARCHAR(500) NOT NULL COMMENT '退货原因', + description TEXT COMMENT '详细描述', + images VARCHAR(2000) COMMENT '图片URL(逗号分隔)', + status TINYINT NOT NULL DEFAULT 1 COMMENT '1-待审核 2-已同意 3-退货中 4-已完成 5-已拒绝 6-已取消', + reject_reason VARCHAR(500) COMMENT '拒绝原因', + admin_remark VARCHAR(500) COMMENT '管理员备注', + return_tracking VARCHAR(100) COMMENT '退货物流单号', + reviewed_at TIMESTAMP NULL, + shipped_at TIMESTAMP NULL, + completed_at TIMESTAMP NULL, + cancelled_at TIMESTAMP NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_order_returns_order FOREIGN KEY (order_id) REFERENCES orders(id), + CONSTRAINT fk_order_returns_user FOREIGN KEY (user_id) REFERENCES users(id), + INDEX idx_returns_user_id (user_id), + INDEX idx_returns_status (status), + INDEX idx_returns_created_at (created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单退货表'; + +-- 13. 视图 -- ================================ CREATE OR REPLACE VIEW active_flash_sales AS SELECT fs.id,