feat: 实现订单退货全链路功能(申请、审核、物流、退款)
This commit is contained in:
52
flash-sale-frontend/src/api/modules/return.ts
Normal file
52
flash-sale-frontend/src/api/modules/return.ts
Normal file
@@ -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<ApiResponse<OrderReturn>> {
|
||||
return request.post('/api/return/create', data).then(normalizeResponse)
|
||||
},
|
||||
|
||||
getByOrderId(orderId: number): Promise<ApiResponse<OrderReturn | null>> {
|
||||
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<ApiResponse<any>> {
|
||||
return request.get('/api/return/my', params)
|
||||
},
|
||||
|
||||
ship(id: number, returnTracking: string): Promise<ApiResponse<OrderReturn>> {
|
||||
return request.post(`/api/return/${id}/ship`, { returnTracking }).then(normalizeResponse)
|
||||
},
|
||||
|
||||
cancel(id: number): Promise<ApiResponse<OrderReturn>> {
|
||||
return request.post(`/api/return/${id}/cancel`).then(normalizeResponse)
|
||||
},
|
||||
|
||||
adminReview(id: number, data: { status: number; rejectReason?: string; adminRemark?: string }): Promise<ApiResponse<OrderReturn>> {
|
||||
return request.post(`/api/return/${id}/review`, data).then(normalizeResponse)
|
||||
},
|
||||
|
||||
adminComplete(id: number, remark?: string): Promise<ApiResponse<OrderReturn>> {
|
||||
return request.post(`/api/return/${id}/complete`, { remark }).then(normalizeResponse)
|
||||
},
|
||||
|
||||
getAll(params?: { status?: number; page?: number; size?: number }): Promise<ApiResponse<any>> {
|
||||
return request.get('/api/return/all', params)
|
||||
},
|
||||
|
||||
getStatistics(): Promise<ApiResponse<any>> {
|
||||
return request.get('/api/return/statistics')
|
||||
},
|
||||
}
|
||||
|
||||
function normalizeResponse(res: any): any {
|
||||
return {
|
||||
...res,
|
||||
data: res.data ? normalizeOrderReturn(res.data) : res.data,
|
||||
}
|
||||
}
|
||||
105
flash-sale-frontend/src/components/business/ReturnDialog.vue
Normal file
105
flash-sale-frontend/src/components/business/ReturnDialog.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
title="申请退货"
|
||||
width="520px"
|
||||
@update:model-value="$emit('update:visible', $event)"
|
||||
>
|
||||
<el-form :model="form" label-width="80px">
|
||||
<el-form-item label="退款金额">
|
||||
<span class="text-lg font-bold text-red-500">¥{{ refundAmount }}</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="退货原因" required>
|
||||
<el-select v-model="form.reason" placeholder="请选择退货原因" style="width: 100%">
|
||||
<el-option label="质量问题" value="质量问题" />
|
||||
<el-option label="商品与描述不符" value="商品与描述不符" />
|
||||
<el-option label="发错商品" value="发错商品" />
|
||||
<el-option label="不想要了" value="不想要了" />
|
||||
<el-option label="其他" value="其他" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="详细描述">
|
||||
<el-input
|
||||
v-model="form.description"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请描述退货原因(选填)"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="$emit('update:visible', false)">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
:disabled="!form.reason"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
提交申请
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { returnApi } from '@/api/modules/return'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
orderId: number
|
||||
refundAmount: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean]
|
||||
success: []
|
||||
}>()
|
||||
|
||||
const submitting = ref(false)
|
||||
const form = reactive({
|
||||
reason: '',
|
||||
description: '',
|
||||
})
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!form.reason) {
|
||||
ElMessage.warning('请选择退货原因')
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const res = await returnApi.create({
|
||||
orderId: props.orderId,
|
||||
reason: form.reason,
|
||||
description: form.description || undefined,
|
||||
})
|
||||
if (res.success) {
|
||||
ElMessage.success('退货申请已提交')
|
||||
emit('success')
|
||||
emit('update:visible', false)
|
||||
} else {
|
||||
ElMessage.error(res.message || '申请失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
const msg = error?.response?.data?.message || error?.message || '申请失败'
|
||||
ElMessage.error(msg)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.visible, (val) => {
|
||||
if (val) {
|
||||
form.reason = ''
|
||||
form.description = ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
title="填写退货物流"
|
||||
width="460px"
|
||||
@update:model-value="$emit('update:visible', $event)"
|
||||
>
|
||||
<el-form :model="form" label-width="90px">
|
||||
<el-form-item label="物流单号" required>
|
||||
<el-input
|
||||
v-model="form.returnTracking"
|
||||
placeholder="请输入退货物流单号"
|
||||
maxlength="100"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="$emit('update:visible', false)">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
:disabled="!form.returnTracking.trim()"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
提交
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { returnApi } from '@/api/modules/return'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
returnId: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean]
|
||||
success: []
|
||||
}>()
|
||||
|
||||
const submitting = ref(false)
|
||||
const form = reactive({
|
||||
returnTracking: '',
|
||||
})
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!form.returnTracking.trim()) {
|
||||
ElMessage.warning('请输入物流单号')
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const res = await returnApi.ship(props.returnId, form.returnTracking.trim())
|
||||
if (res.success) {
|
||||
ElMessage.success('物流信息已提交')
|
||||
emit('success')
|
||||
emit('update:visible', false)
|
||||
} else {
|
||||
ElMessage.error(res.message || '提交失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
const msg = error?.response?.data?.message || error?.message || '提交失败'
|
||||
ElMessage.error(msg)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.visible, (val) => {
|
||||
if (val) {
|
||||
form.returnTracking = ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -48,6 +48,11 @@
|
||||
<template #title>用户管理</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="/admin/returns">
|
||||
<el-icon><RefreshLeft /></el-icon>
|
||||
<template #title>退货管理</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="/admin/reviews">
|
||||
<el-icon><ChatDotRound /></el-icon>
|
||||
<template #title>评价管理</template>
|
||||
@@ -161,6 +166,7 @@ const currentPageTitle = computed(() => {
|
||||
'/admin/groupbuying': '拼团管理',
|
||||
'/admin/orders': '订单管理',
|
||||
'/admin/users': '用户管理',
|
||||
'/admin/returns': '退货管理',
|
||||
'/admin/reviews': '评价管理',
|
||||
'/admin/favorites': '收藏管理',
|
||||
'/admin/monitor': '系统监控',
|
||||
|
||||
220
flash-sale-frontend/src/pages/admin/returns.vue
Normal file
220
flash-sale-frontend/src/pages/admin/returns.vue
Normal file
@@ -0,0 +1,220 @@
|
||||
<template>
|
||||
<div class="page-shell">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2 class="page-title">退货管理</h2>
|
||||
<p class="page-subtitle">处理用户退货申请、审核和退款</p>
|
||||
</div>
|
||||
<el-button @click="reloadData"><el-icon><Refresh /></el-icon>刷新</el-button>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="mini-stat orange"><div class="mini-stat__value">{{ stats.pendingCount }}</div><div class="mini-stat__label">待审核</div></div>
|
||||
<div class="mini-stat blue"><div class="mini-stat__value">{{ stats.approvedCount }}</div><div class="mini-stat__label">已同意</div></div>
|
||||
<div class="mini-stat purple"><div class="mini-stat__value">{{ stats.returningCount }}</div><div class="mini-stat__label">退货中</div></div>
|
||||
<div class="mini-stat green"><div class="mini-stat__value">{{ stats.completedCount }}</div><div class="mini-stat__label">已完成</div></div>
|
||||
</div>
|
||||
|
||||
<div class="panel-card filter-card">
|
||||
<el-select v-model="filters.status" placeholder="全部状态" clearable style="width: 150px" @change="loadReturns">
|
||||
<el-option label="全部" :value="undefined" />
|
||||
<el-option label="待审核" :value="1" />
|
||||
<el-option label="已同意" :value="2" />
|
||||
<el-option label="退货中" :value="3" />
|
||||
<el-option label="已完成" :value="4" />
|
||||
<el-option label="已拒绝" :value="5" />
|
||||
<el-option label="已取消" :value="6" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="loadReturns">查询</el-button>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<el-table v-loading="loading" :data="returns" stripe>
|
||||
<el-table-column prop="returnNo" label="退货单号" min-width="140" />
|
||||
<el-table-column prop="orderNo" label="订单号" min-width="140" />
|
||||
<el-table-column prop="username" label="用户" width="100" />
|
||||
<el-table-column prop="productName" label="商品" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="refundAmount" label="退款金额" width="100">
|
||||
<template #default="{ row }">¥{{ row.refundAmount }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="reason" label="退货原因" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="statusText" label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getReturnStatusType(row.status)" size="small">{{ row.statusText }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="申请时间" min-width="170">
|
||||
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status === 'PENDING'" text type="primary" @click="openReviewDialog(row)">审核</el-button>
|
||||
<el-button v-if="row.status === 'RETURNING'" text type="success" @click="handleComplete(row)">确认退款</el-button>
|
||||
<el-button text @click="openDetailDialog(row)">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="table-footer">
|
||||
<el-pagination v-model:current-page="page" v-model:page-size="size" :total="total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper" @current-change="loadReturns" @size-change="loadReturns" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 审核弹窗 -->
|
||||
<el-dialog v-model="reviewVisible" title="退货审核" width="500px">
|
||||
<div v-if="currentReturn" class="space-y-4">
|
||||
<div class="text-sm"><span class="text-gray-500">退货单号:</span>{{ currentReturn.returnNo }}</div>
|
||||
<div class="text-sm"><span class="text-gray-500">用户:</span>{{ currentReturn.username }}</div>
|
||||
<div class="text-sm"><span class="text-gray-500">退款金额:</span><span class="text-red-500 font-semibold">¥{{ currentReturn.refundAmount }}</span></div>
|
||||
<div class="text-sm"><span class="text-gray-500">退货原因:</span>{{ currentReturn.reason }}</div>
|
||||
<div v-if="currentReturn.description" class="text-sm"><span class="text-gray-500">详细描述:</span>{{ currentReturn.description }}</div>
|
||||
<el-divider />
|
||||
<el-radio-group v-model="reviewForm.status">
|
||||
<el-radio :label="2">同意退货</el-radio>
|
||||
<el-radio :label="5">拒绝退货</el-radio>
|
||||
</el-radio-group>
|
||||
<el-input v-if="reviewForm.status === 5" v-model="reviewForm.rejectReason" type="textarea" :rows="3" placeholder="请输入拒绝原因" />
|
||||
<el-input v-model="reviewForm.adminRemark" type="textarea" :rows="2" placeholder="管理员备注(选填)" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="reviewVisible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!reviewForm.status" @click="submitReview">确认</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<el-dialog v-model="detailVisible" title="退货详情" width="600px">
|
||||
<div v-if="currentReturn" class="space-y-3 text-sm">
|
||||
<div><span class="text-gray-500">退货单号:</span>{{ currentReturn.returnNo }}</div>
|
||||
<div><span class="text-gray-500">订单号:</span>{{ currentReturn.orderNo }}</div>
|
||||
<div><span class="text-gray-500">用户:</span>{{ currentReturn.username }}</div>
|
||||
<div><span class="text-gray-500">商品:</span>{{ currentReturn.productName }}</div>
|
||||
<div><span class="text-gray-500">退款金额:</span><span class="text-red-500 font-semibold">¥{{ currentReturn.refundAmount }}</span></div>
|
||||
<div><span class="text-gray-500">退货原因:</span>{{ currentReturn.reason }}</div>
|
||||
<div v-if="currentReturn.description"><span class="text-gray-500">详细描述:</span>{{ currentReturn.description }}</div>
|
||||
<div><span class="text-gray-500">状态:</span><el-tag :type="getReturnStatusType(currentReturn.status)" size="small">{{ currentReturn.statusText }}</el-tag></div>
|
||||
<div v-if="currentReturn.rejectReason"><span class="text-gray-500">拒绝原因:</span><span class="text-red-500">{{ currentReturn.rejectReason }}</span></div>
|
||||
<div v-if="currentReturn.returnTracking"><span class="text-gray-500">物流单号:</span>{{ currentReturn.returnTracking }}</div>
|
||||
<div v-if="currentReturn.adminRemark"><span class="text-gray-500">管理员备注:</span>{{ currentReturn.adminRemark }}</div>
|
||||
<div><span class="text-gray-500">申请时间:</span>{{ formatTime(currentReturn.createdAt) }}</div>
|
||||
<div v-if="currentReturn.reviewedAt"><span class="text-gray-500">审核时间:</span>{{ formatTime(currentReturn.reviewedAt) }}</div>
|
||||
<div v-if="currentReturn.shippedAt"><span class="text-gray-500">寄出时间:</span>{{ formatTime(currentReturn.shippedAt) }}</div>
|
||||
<div v-if="currentReturn.completedAt"><span class="text-gray-500">完成时间:</span>{{ formatTime(currentReturn.completedAt) }}</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { returnApi } from '@/api/modules/return'
|
||||
import { normalizeOrderReturn } from '@/utils/normalizers'
|
||||
import type { OrderReturn } from '@/types/api'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const loading = ref(false)
|
||||
const page = ref(1)
|
||||
const size = ref(10)
|
||||
const total = ref(0)
|
||||
const returns = ref<OrderReturn[]>([])
|
||||
const filters = reactive<{ status: number | undefined }>({ status: undefined })
|
||||
const stats = reactive({ pendingCount: 0, approvedCount: 0, returningCount: 0, completedCount: 0, rejectedCount: 0, cancelledCount: 0, totalCount: 0 })
|
||||
|
||||
const reviewVisible = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
const currentReturn = ref<OrderReturn | null>(null)
|
||||
const reviewForm = reactive({ status: 0 as number, rejectReason: '', adminRemark: '' })
|
||||
|
||||
const formatTime = (time: string) => dayjs(time).format('YYYY-MM-DD HH:mm:ss')
|
||||
const getReturnStatusType = (status: string) => ({ PENDING: 'warning', APPROVED: 'primary', RETURNING: 'primary', COMPLETED: 'success', REJECTED: 'danger', CANCELLED: 'info' }[status] || 'info')
|
||||
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
const res = await returnApi.getStatistics()
|
||||
if (res.success) Object.assign(stats, res.data)
|
||||
} catch (error) {
|
||||
console.error('加载统计失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const loadReturns = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await returnApi.getAll({
|
||||
status: filters.status,
|
||||
page: page.value - 1,
|
||||
size: size.value,
|
||||
})
|
||||
if (res.success) {
|
||||
returns.value = (res.data.content || []).map((item: any) => normalizeOrderReturn(item))
|
||||
total.value = res.data.totalElements || 0
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openReviewDialog = (row: OrderReturn) => {
|
||||
currentReturn.value = row
|
||||
reviewForm.status = 0
|
||||
reviewForm.rejectReason = ''
|
||||
reviewForm.adminRemark = ''
|
||||
reviewVisible.value = true
|
||||
}
|
||||
|
||||
const openDetailDialog = (row: OrderReturn) => {
|
||||
currentReturn.value = row
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
const submitReview = async () => {
|
||||
if (!currentReturn.value || !reviewForm.status) return
|
||||
if (reviewForm.status === 5 && !reviewForm.rejectReason.trim()) {
|
||||
ElMessage.warning('请输入拒绝原因')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await returnApi.adminReview(currentReturn.value.id, {
|
||||
status: reviewForm.status,
|
||||
rejectReason: reviewForm.status === 5 ? reviewForm.rejectReason : undefined,
|
||||
adminRemark: reviewForm.adminRemark || undefined,
|
||||
})
|
||||
ElMessage.success('审核完成')
|
||||
reviewVisible.value = false
|
||||
reloadData()
|
||||
} catch (error) {
|
||||
console.error('审核失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleComplete = async (row: OrderReturn) => {
|
||||
await ElMessageBox.confirm(`确认退款 ¥${row.refundAmount} 给用户 ${row.username}?`, '确认退款', { confirmButtonText: '确认退款', cancelButtonText: '取消', type: 'warning' })
|
||||
try {
|
||||
await returnApi.adminComplete(row.id)
|
||||
ElMessage.success('退款已完成')
|
||||
reloadData()
|
||||
} catch (error) {
|
||||
console.error('确认退款失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const reloadData = async () => { await Promise.all([loadStats(), loadReturns()]) }
|
||||
onMounted(() => { reloadData() })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-shell { display:flex; flex-direction:column; gap:20px; }
|
||||
.page-header { display:flex; justify-content:space-between; align-items:flex-start; gap:16px; }
|
||||
.page-title { @apply text-2xl font-bold text-slate-900; }
|
||||
.page-subtitle { @apply text-sm text-slate-500 mt-1; }
|
||||
.stats-grid { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:16px; }
|
||||
.mini-stat { @apply rounded-xl p-5 shadow-sm; background:#fffaf2; color:#171715; border:1px solid #d8cebf; box-shadow:0 10px 24px rgba(23,22,20,0.04); }
|
||||
.mini-stat__value { @apply text-3xl font-bold; }
|
||||
.mini-stat__label { @apply text-sm opacity-90 mt-2; }
|
||||
.panel-card { @apply bg-white rounded-xl p-5; border:1px solid #d8cebf; box-shadow:0 10px 24px rgba(23,22,20,0.04); }
|
||||
.filter-card { display:flex; gap:12px; align-items:center; }
|
||||
.table-footer { @apply flex justify-end mt-4; }
|
||||
</style>
|
||||
@@ -42,9 +42,17 @@
|
||||
<template v-else-if="order.status === 'COMPLETED'">
|
||||
<el-button v-if="allReviewed" @click="reviewDialogVisible = true">查看评价</el-button>
|
||||
<el-button v-else type="primary" @click="reviewDialogVisible = true">评价</el-button>
|
||||
<el-button type="warning" @click="returnDialogVisible = true">申请退货</el-button>
|
||||
<el-button @click="handleRebuy">再次购买</el-button>
|
||||
<el-button text type="danger" @click="handleDelete">删除订单</el-button>
|
||||
</template>
|
||||
<template v-else-if="order.status === 'REFUNDING'">
|
||||
<el-button v-if="orderReturn && orderReturn.status === 'APPROVED'" type="primary" @click="trackingDialogVisible = true">填写物流单号</el-button>
|
||||
<el-button v-if="orderReturn && (orderReturn.status === 'PENDING' || orderReturn.status === 'APPROVED')" @click="handleCancelReturn">取消退货</el-button>
|
||||
</template>
|
||||
<template v-else-if="order.status === 'REFUNDED'">
|
||||
<el-button text type="danger" @click="handleDelete">删除订单</el-button>
|
||||
</template>
|
||||
<template v-else-if="order.status === 'CANCELLED'">
|
||||
<el-button text type="danger" @click="handleDelete">删除订单</el-button>
|
||||
</template>
|
||||
@@ -95,6 +103,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 退货信息 -->
|
||||
<div v-if="orderReturn && (order.status === 'REFUNDING' || order.status === 'REFUNDED')" class="bg-white rounded-lg shadow-sm p-6 mb-6">
|
||||
<h3 class="text-lg font-semibold mb-4">退货信息</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||
<div><span class="text-gray-500">退货单号:</span><span>{{ orderReturn.returnNo }}</span></div>
|
||||
<div><span class="text-gray-500">退货状态:</span><el-tag :type="getReturnStatusType(orderReturn.status)" size="small">{{ orderReturn.statusText }}</el-tag></div>
|
||||
<div><span class="text-gray-500">退款金额:</span><span class="text-red-500 font-semibold">¥{{ orderReturn.refundAmount }}</span></div>
|
||||
<div><span class="text-gray-500">退货原因:</span><span>{{ orderReturn.reason }}</span></div>
|
||||
<div v-if="orderReturn.description" class="md:col-span-2"><span class="text-gray-500">详细描述:</span><span>{{ orderReturn.description }}</span></div>
|
||||
<div v-if="orderReturn.rejectReason" class="md:col-span-2"><span class="text-gray-500">拒绝原因:</span><span class="text-red-500">{{ orderReturn.rejectReason }}</span></div>
|
||||
<div v-if="orderReturn.returnTracking"><span class="text-gray-500">物流单号:</span><span>{{ orderReturn.returnTracking }}</span></div>
|
||||
<div v-if="orderReturn.adminRemark" class="md:col-span-2"><span class="text-gray-500">管理员备注:</span><span>{{ orderReturn.adminRemark }}</span></div>
|
||||
<div><span class="text-gray-500">申请时间:</span><span>{{ formatTime(orderReturn.createdAt) }}</span></div>
|
||||
<div v-if="orderReturn.completedAt"><span class="text-gray-500">完成时间:</span><span>{{ formatTime(orderReturn.completedAt) }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ReviewDialog
|
||||
v-if="order"
|
||||
v-model:visible="reviewDialogVisible"
|
||||
@@ -102,6 +127,21 @@
|
||||
:order-items="order.items"
|
||||
@success="checkAllReviewed"
|
||||
/>
|
||||
|
||||
<ReturnDialog
|
||||
v-if="order"
|
||||
v-model:visible="returnDialogVisible"
|
||||
:order-id="order.id"
|
||||
:refund-amount="order.paymentAmount"
|
||||
@success="onReturnSuccess"
|
||||
/>
|
||||
|
||||
<ReturnTrackingDialog
|
||||
v-if="orderReturn"
|
||||
v-model:visible="trackingDialogVisible"
|
||||
:return-id="orderReturn.id"
|
||||
@success="onReturnSuccess"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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<Order | null>(null)
|
||||
const reviewDialogVisible = ref(false)
|
||||
const allReviewed = ref(false)
|
||||
const returnDialogVisible = ref(false)
|
||||
const trackingDialogVisible = ref(false)
|
||||
const orderReturn = ref<OrderReturn | null>(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()])
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
<el-radio-button label="SHIPPED">待收货</el-radio-button>
|
||||
<el-radio-button label="COMPLETED">已完成</el-radio-button>
|
||||
<el-radio-button label="CANCELLED">已取消</el-radio-button>
|
||||
<el-radio-button label="REFUNDING">退货中</el-radio-button>
|
||||
<el-radio-button label="REFUNDED">已退货</el-radio-button>
|
||||
</el-radio-group>
|
||||
|
||||
<el-input v-model="filters.keyword" placeholder="搜索订单号或商品名称" style="width: 250px" clearable @keyup.enter="loadOrders">
|
||||
@@ -87,10 +89,19 @@
|
||||
<template v-else-if="order.status === 'COMPLETED'">
|
||||
<el-button v-if="orderReviewStatus[order.id]" size="small" @click="openReviewDialog(order)">查看评价</el-button>
|
||||
<el-button v-else type="primary" size="small" @click="openReviewDialog(order)">评价</el-button>
|
||||
<el-button type="warning" size="small" @click="openReturnDialog(order)">申请退货</el-button>
|
||||
<el-button size="small" @click="handleRebuy(order)">再次购买</el-button>
|
||||
<el-button text type="danger" size="small" @click="handleDelete(order)">删除订单</el-button>
|
||||
</template>
|
||||
|
||||
<template v-else-if="order.status === 'REFUNDING'">
|
||||
<el-button type="primary" size="small" @click="handleViewDetail(order.id)">查看退货进度</el-button>
|
||||
</template>
|
||||
|
||||
<template v-else-if="order.status === 'REFUNDED'">
|
||||
<el-button text type="danger" size="small" @click="handleDelete(order)">删除订单</el-button>
|
||||
</template>
|
||||
|
||||
<template v-else-if="order.status === 'CANCELLED'">
|
||||
<el-button text type="danger" size="small" @click="handleDelete(order)">删除订单</el-button>
|
||||
</template>
|
||||
@@ -110,6 +121,14 @@
|
||||
:order-items="currentReviewOrder.items"
|
||||
@success="onReviewSuccess"
|
||||
/>
|
||||
|
||||
<ReturnDialog
|
||||
v-if="currentReturnOrder"
|
||||
v-model:visible="returnDialogVisible"
|
||||
:order-id="currentReturnOrder.id"
|
||||
:refund-amount="currentReturnOrder.paymentAmount || currentReturnOrder.totalAmount"
|
||||
@success="onReturnSuccess"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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<Order | null>(null)
|
||||
const orderReviewStatus = ref<Record<number, boolean>>({})
|
||||
const returnDialogVisible = ref(false)
|
||||
const currentReturnOrder = ref<Order | null>(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
|
||||
|
||||
146
flash-sale-frontend/src/pages/user/returns.vue
Normal file
146
flash-sale-frontend/src/pages/user/returns.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div class="user-returns-page">
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<el-breadcrumb separator="/" class="mb-6">
|
||||
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
|
||||
<el-breadcrumb-item>我的退货</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
|
||||
<h1 class="text-3xl font-bold mb-6">我的退货</h1>
|
||||
|
||||
<div class="bg-white rounded-lg shadow-sm p-4 mb-6">
|
||||
<el-radio-group v-model="filters.status" @change="loadReturns">
|
||||
<el-radio-button label="">全部</el-radio-button>
|
||||
<el-radio-button label="1">待审核</el-radio-button>
|
||||
<el-radio-button label="2">已同意</el-radio-button>
|
||||
<el-radio-button label="3">退货中</el-radio-button>
|
||||
<el-radio-button label="4">已完成</el-radio-button>
|
||||
<el-radio-button label="5">已拒绝</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="text-center py-12">
|
||||
<el-icon :size="40" class="animate-spin"><Loading /></el-icon>
|
||||
<p class="mt-2 text-gray-500">加载中...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="returns.length === 0" class="bg-white rounded-lg shadow-sm p-12">
|
||||
<el-empty description="暂无退货记录" />
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
<div v-for="item in returns" :key="item.id" class="bg-white rounded-lg shadow-sm overflow-hidden">
|
||||
<div class="bg-gray-50 px-6 py-3 flex justify-between items-center">
|
||||
<div class="flex items-center gap-4 text-sm text-gray-600">
|
||||
<span>退货单号:{{ item.returnNo }}</span>
|
||||
<span>{{ formatTime(item.createdAt) }}</span>
|
||||
</div>
|
||||
<el-tag :type="getReturnStatusType(item.status)">{{ item.statusText }}</el-tag>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="flex gap-4">
|
||||
<SafeImage :src="item.productImage" :alt="item.productName" wrapper-class="w-20 h-20 rounded" img-class="w-20 h-20 object-cover rounded" />
|
||||
<div class="flex-1">
|
||||
<h4 class="font-semibold">{{ item.productName || '商品' }}</h4>
|
||||
<div class="text-sm text-gray-500 mt-1">订单号:{{ item.orderNo }}</div>
|
||||
<div class="text-sm text-gray-500 mt-1">退货原因:{{ item.reason }}</div>
|
||||
<div v-if="item.rejectReason" class="text-sm text-red-500 mt-1">拒绝原因:{{ item.rejectReason }}</div>
|
||||
<div v-if="item.returnTracking" class="text-sm text-gray-500 mt-1">物流单号:{{ item.returnTracking }}</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-sm text-gray-500">退款金额</div>
|
||||
<div class="text-lg font-bold text-red-500">¥{{ item.refundAmount }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t px-6 py-4 flex justify-end gap-2">
|
||||
<el-button text type="primary" @click="router.push(`/order/${item.orderId}`)">查看订单</el-button>
|
||||
<el-button v-if="item.status === 'APPROVED'" type="primary" size="small" @click="openTrackingDialog(item)">填写物流</el-button>
|
||||
<el-button v-if="item.status === 'PENDING' || item.status === 'APPROVED'" size="small" @click="handleCancel(item)">取消退货</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="pagination.total > 0" class="mt-8 flex justify-center">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.size" :total="pagination.total" :page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next" @size-change="loadReturns" @current-change="loadReturns" />
|
||||
</div>
|
||||
|
||||
<ReturnTrackingDialog
|
||||
v-if="currentReturn"
|
||||
v-model:visible="trackingDialogVisible"
|
||||
:return-id="currentReturn.id"
|
||||
@success="loadReturns"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { returnApi } from '@/api/modules/return'
|
||||
import { normalizeOrderReturn } from '@/utils/normalizers'
|
||||
import type { OrderReturn } from '@/types/api'
|
||||
import dayjs from 'dayjs'
|
||||
import SafeImage from '@/components/common/SafeImage.vue'
|
||||
import ReturnTrackingDialog from '@/components/business/ReturnTrackingDialog.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const returns = ref<OrderReturn[]>([])
|
||||
const filters = reactive({ status: '' })
|
||||
const pagination = reactive({ page: 1, size: 10, total: 0 })
|
||||
const trackingDialogVisible = ref(false)
|
||||
const currentReturn = ref<OrderReturn | null>(null)
|
||||
|
||||
const formatTime = (time: string) => dayjs(time).format('YYYY-MM-DD HH:mm:ss')
|
||||
const getReturnStatusType = (status: string) => ({ PENDING: 'warning', APPROVED: 'primary', RETURNING: 'primary', COMPLETED: 'success', REJECTED: 'danger', CANCELLED: 'info' }[status] || 'info')
|
||||
|
||||
const loadReturns = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await returnApi.getMyReturns({
|
||||
status: filters.status ? Number(filters.status) : undefined,
|
||||
page: pagination.page - 1,
|
||||
size: pagination.size,
|
||||
})
|
||||
if (res.success) {
|
||||
returns.value = (res.data.content || []).map((item: any) => normalizeOrderReturn(item))
|
||||
pagination.total = res.data.totalElements || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载退货列表失败:', error)
|
||||
ElMessage.error('加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openTrackingDialog = (item: OrderReturn) => {
|
||||
currentReturn.value = item
|
||||
trackingDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleCancel = async (item: OrderReturn) => {
|
||||
await ElMessageBox.confirm('确定要取消退货申请吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
|
||||
try {
|
||||
await returnApi.cancel(item.id)
|
||||
ElMessage.success('退货申请已取消')
|
||||
loadReturns()
|
||||
} catch (error) {
|
||||
console.error('取消退货失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => { loadReturns() })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.user-returns-page {
|
||||
min-height: calc(100vh - 60px);
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
@@ -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',
|
||||
|
||||
29
flash-sale-frontend/src/types/api.d.ts
vendored
29
flash-sale-frontend/src/types/api.d.ts
vendored
@@ -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
|
||||
|
||||
@@ -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<string, any>): 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<string, any>): 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<string, any>): GroupBuyingGroup => ({
|
||||
id: toNumber(group.id),
|
||||
groupNo: toString(group.groupNo),
|
||||
|
||||
@@ -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<Map<String, Object>> 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<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);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询订单退货信息
|
||||
*/
|
||||
@GetMapping("/order/{orderId}")
|
||||
public ResponseEntity<Map<String, Object>> getReturnByOrder(@PathVariable Long orderId,
|
||||
HttpServletRequest request) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return createUnauthorizedResponse();
|
||||
}
|
||||
|
||||
OrderReturnDTO result = orderReturnService.getReturnByOrderId(orderId);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", result);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户退货列表
|
||||
*/
|
||||
@GetMapping("/my")
|
||||
public ResponseEntity<Map<String, Object>> 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<String, Object> result = orderReturnService.getUserReturns(userId, queryDTO);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", result);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户填写物流单号
|
||||
*/
|
||||
@PostMapping("/{id}/ship")
|
||||
public ResponseEntity<Map<String, Object>> 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<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);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户取消退货
|
||||
*/
|
||||
@PostMapping("/{id}/cancel")
|
||||
public ResponseEntity<Map<String, Object>> cancelReturn(@PathVariable Long id,
|
||||
HttpServletRequest request) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return createUnauthorizedResponse();
|
||||
}
|
||||
|
||||
OrderReturnDTO result = orderReturnService.cancelReturn(userId, id);
|
||||
|
||||
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);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员审核退货
|
||||
*/
|
||||
@PostMapping("/{id}/review")
|
||||
public ResponseEntity<Map<String, Object>> adminReview(@PathVariable Long id,
|
||||
@Validated @RequestBody OrderReturnDTO.ReviewDTO reviewDTO) {
|
||||
try {
|
||||
OrderReturnDTO result = orderReturnService.adminReviewReturn(id, reviewDTO);
|
||||
|
||||
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);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员确认退款
|
||||
*/
|
||||
@PostMapping("/{id}/complete")
|
||||
public ResponseEntity<Map<String, Object>> adminComplete(@PathVariable Long id,
|
||||
@RequestBody(required = false) Map<String, String> body) {
|
||||
try {
|
||||
String remark = body != null ? body.get("remark") : null;
|
||||
OrderReturnDTO result = orderReturnService.adminCompleteReturn(id, remark);
|
||||
|
||||
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);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员查看全部退货
|
||||
*/
|
||||
@GetMapping("/all")
|
||||
public ResponseEntity<Map<String, Object>> 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<String, Object> result = orderReturnService.getAllReturns(queryDTO);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", result);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 退货统计
|
||||
*/
|
||||
@GetMapping("/statistics")
|
||||
public ResponseEntity<Map<String, Object>> getStatistics() {
|
||||
try {
|
||||
OrderReturnDTO.StatisticsDTO stats = orderReturnService.getReturnStatistics();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
121
src/main/java/com/org/flashsalesystem/entity/OrderReturn.java
Normal file
121
src/main/java/com/org/flashsalesystem/entity/OrderReturn.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<OrderReturn, Long> {
|
||||
|
||||
Optional<OrderReturn> findByOrderId(Long orderId);
|
||||
|
||||
boolean existsByOrderIdAndStatusIn(Long orderId, List<Integer> statuses);
|
||||
|
||||
Page<OrderReturn> findByUserIdOrderByCreatedAtDesc(Long userId, Pageable pageable);
|
||||
|
||||
Page<OrderReturn> findByUserIdAndStatusOrderByCreatedAtDesc(Long userId, Integer status, Pageable pageable);
|
||||
|
||||
Page<OrderReturn> findByStatusOrderByCreatedAtDesc(Integer status, Pageable pageable);
|
||||
|
||||
Page<OrderReturn> findAllByOrderByCreatedAtDesc(Pageable pageable);
|
||||
|
||||
long countByStatus(Integer status);
|
||||
}
|
||||
@@ -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<String, Object> 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<String, Object> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Order> 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<Integer> 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<Order> 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<String, Object> 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<OrderReturn> returnOpt = orderReturnRepository.findByOrderId(orderId);
|
||||
return returnOpt.map(this::buildReturnDTO).orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户退货列表
|
||||
*/
|
||||
public Map<String, Object> getUserReturns(Long userId, OrderReturnDTO.QueryDTO queryDTO) {
|
||||
Pageable pageable = PageRequest.of(queryDTO.getPage(), queryDTO.getSize());
|
||||
Page<OrderReturn> page;
|
||||
|
||||
if (queryDTO.getStatus() != null) {
|
||||
page = orderReturnRepository.findByUserIdAndStatusOrderByCreatedAtDesc(userId, queryDTO.getStatus(), pageable);
|
||||
} else {
|
||||
page = orderReturnRepository.findByUserIdOrderByCreatedAtDesc(userId, pageable);
|
||||
}
|
||||
|
||||
return buildPageResult(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员退货列表
|
||||
*/
|
||||
public Map<String, Object> getAllReturns(OrderReturnDTO.QueryDTO queryDTO) {
|
||||
Pageable pageable = PageRequest.of(queryDTO.getPage(), queryDTO.getSize());
|
||||
Page<OrderReturn> 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<Order> 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<String, Object> buildPageResult(Page<OrderReturn> page) {
|
||||
List<OrderReturnDTO> dtos = new ArrayList<>();
|
||||
for (OrderReturn orderReturn : page.getContent()) {
|
||||
dtos.add(buildReturnDTO(orderReturn));
|
||||
}
|
||||
|
||||
Map<String, Object> 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<String, Object> extraData) {
|
||||
Map<String, Object> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<Integer> 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 "未知状态";
|
||||
}
|
||||
|
||||
@@ -132,6 +132,11 @@ flashsale:
|
||||
# 秒杀活动缓存过期时间(分钟)
|
||||
flashsale-expire-minutes: 10
|
||||
|
||||
# 退货配置
|
||||
return:
|
||||
# 确认收货后可申请退货的天数
|
||||
max-days-after-completion: 7
|
||||
|
||||
# 消息队列配置
|
||||
mq:
|
||||
# 订单状态变更通知频道
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user