优化内容
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>社区生鲜团购系统</title>
|
||||
<meta name="description" content="社区生鲜团购系统,支持分布式锁、接口限流、库存预热等核心功能">
|
||||
<meta name="keywords" content="秒杀,抢购,电商,flash sale">
|
||||
<meta content="限时,抢购,电商,community group buying" name="keywords">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -2,17 +2,17 @@ import request from './request'
|
||||
import type { FlashSale, FlashSaleParams } from '@/types/flashsale'
|
||||
|
||||
export const flashSaleApi = {
|
||||
// 获取秒杀活动列表
|
||||
// 获取限时活动列表
|
||||
getList(params?: FlashSaleParams) {
|
||||
return request.get<any, { list: FlashSale[], total: number }>('/api/flashsales', { params })
|
||||
},
|
||||
|
||||
// 获取秒杀活动详情
|
||||
// 获取限时活动详情
|
||||
getDetail(id: number) {
|
||||
return request.get<any, FlashSale>(`/api/flashsales/${id}`)
|
||||
},
|
||||
|
||||
// 参与秒杀
|
||||
// 参与限时
|
||||
participate(flashSaleId: number, quantity: number = 1) {
|
||||
return request.post('/api/flashsales/participate', {
|
||||
flashSaleId,
|
||||
@@ -20,12 +20,12 @@ export const flashSaleApi = {
|
||||
})
|
||||
},
|
||||
|
||||
// 获取正在进行的秒杀活动
|
||||
// 获取正在进行的限时活动
|
||||
getActive() {
|
||||
return request.get<any, FlashSale[]>('/api/flashsales/active')
|
||||
},
|
||||
|
||||
// 获取即将开始的秒杀活动
|
||||
// 获取即将开始的限时活动
|
||||
getUpcoming() {
|
||||
return request.get<any, FlashSale[]>('/api/flashsales/upcoming')
|
||||
}
|
||||
|
||||
@@ -67,6 +67,9 @@ export const adminApi = {
|
||||
},
|
||||
}))
|
||||
},
|
||||
deleteUser(id: number): Promise<ApiResponse> {
|
||||
return request.delete(`/api/admin/users/${id}`)
|
||||
},
|
||||
getOrders(params: { page: number; size: number; keyword?: string; status?: string | '' }): Promise<ApiResponse<{ orders: AdminOrderRow[]; total: number; totalPages: number; currentPage: number; size: number }>> {
|
||||
const query = { page: params.page, size: params.size, keyword: params.keyword, status: params.status === '' ? undefined : params.status }
|
||||
return request.get<ApiResponse<Record<string, any>>>('/api/admin/orders', query).then((res) => ({
|
||||
|
||||
@@ -17,12 +17,12 @@ const flashSaleSortField = (sort?: string) => {
|
||||
}
|
||||
|
||||
export const flashsaleApi = {
|
||||
// 获取秒杀活动统计信息(即将开始/正在进行/我的参与/抢购成功)
|
||||
// 获取限时活动统计信息(即将开始/正在进行/我的参与/抢购成功)
|
||||
getStatistics(): Promise<ApiResponse<{ upcoming: number; active: number; participated: number; success: number }>> {
|
||||
return request.get('/api/flashsale/statistics')
|
||||
},
|
||||
|
||||
// 获取秒杀活动列表
|
||||
// 获取限时活动列表
|
||||
getList(params?: PageParams & { status?: string }): Promise<ApiResponse<PageResponse<FlashSale>>> {
|
||||
return request.post<ApiResponse<Record<string, any>>>('/api/flashsale/list', {
|
||||
status: flashSaleStatusToCode(params?.status),
|
||||
@@ -35,8 +35,8 @@ export const flashsaleApi = {
|
||||
data: normalizePage(res.data, normalizeFlashSale),
|
||||
}))
|
||||
},
|
||||
|
||||
// 获取正在进行的秒杀活动
|
||||
|
||||
// 获取正在进行的限时活动
|
||||
getActive(limit?: number): Promise<ApiResponse<FlashSale[]>> {
|
||||
return request.get<ApiResponse<any[]>>('/api/flashsale/active').then((res) => ({
|
||||
...res,
|
||||
@@ -45,16 +45,16 @@ export const flashsaleApi = {
|
||||
.slice(0, limit ?? Number.MAX_SAFE_INTEGER),
|
||||
}))
|
||||
},
|
||||
|
||||
// 获取秒杀活动详情
|
||||
|
||||
// 获取限时活动详情
|
||||
getDetail(id: number): Promise<ApiResponse<FlashSale>> {
|
||||
return request.get<ApiResponse<any>>(`/api/flashsale/${id}`).then((res) => ({
|
||||
...res,
|
||||
data: normalizeFlashSale(res.data),
|
||||
}))
|
||||
},
|
||||
|
||||
// 参与秒杀
|
||||
|
||||
// 参与限时
|
||||
participate(data: {
|
||||
flashSaleId: number;
|
||||
quantity: number;
|
||||
|
||||
@@ -69,7 +69,8 @@ const statusType = computed(() => {
|
||||
const statusText = computed(() => {
|
||||
switch (props.data.status) {
|
||||
case 'UPCOMING': return '即将开始'
|
||||
case 'ACTIVE': return '秒杀中'
|
||||
case 'ACTIVE':
|
||||
return '进行中'
|
||||
case 'ENDED': return '已结束'
|
||||
case 'PAUSED': return '已暂停'
|
||||
default: return '未知'
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold mb-4">关于我们</h3>
|
||||
<p class="text-gray-600 text-sm">
|
||||
社区生鲜团购系统,支持分布式锁、接口限流、库存预热等核心功能。
|
||||
社区生鲜团购平台,提供商品浏览、拼团下单和订单管理服务。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/flashsales" class="footer-link">
|
||||
秒杀活动
|
||||
<router-link class="footer-link" to="/groupbuying">
|
||||
拼团活动
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
@@ -51,7 +51,7 @@
|
||||
<div class="space-y-2 text-gray-600">
|
||||
<p class="flex items-center">
|
||||
<el-icon class="mr-2"><Message /></el-icon>
|
||||
contact@flashsale.com
|
||||
service@freshgroup.com
|
||||
</p>
|
||||
<p class="flex items-center">
|
||||
<el-icon class="mr-2"><Phone /></el-icon>
|
||||
@@ -62,7 +62,7 @@
|
||||
</div>
|
||||
|
||||
<div class="border-t mt-8 pt-8 text-center text-gray-500 text-sm">
|
||||
<p>© 2024 社区生鲜团购系统. All rights reserved.</p>
|
||||
<p>© 社区生鲜团购平台. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -8,9 +8,6 @@
|
||||
<Lightning />
|
||||
</el-icon>
|
||||
<span class="brand-title">社区生鲜团购系统</span>
|
||||
<span class="brand-tag">
|
||||
FLASH SALE
|
||||
</span>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
@@ -19,10 +16,6 @@
|
||||
<el-icon><HomeFilled /></el-icon>
|
||||
首页
|
||||
</router-link>
|
||||
<router-link to="/flashsales" class="nav-link">
|
||||
<el-icon><Lightning /></el-icon>
|
||||
秒杀活动
|
||||
</router-link>
|
||||
<el-dropdown trigger="hover" @command="handleCategoryCommand">
|
||||
<router-link to="/products" class="nav-link">
|
||||
<el-icon><ShoppingBag /></el-icon>
|
||||
@@ -57,7 +50,6 @@
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="/">首页</el-dropdown-item>
|
||||
<el-dropdown-item command="/flashsales">秒杀活动</el-dropdown-item>
|
||||
<el-dropdown-item command="/products">商品列表</el-dropdown-item>
|
||||
<el-dropdown-item command="/groupbuying">拼团</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
@@ -68,9 +60,9 @@
|
||||
<SearchComponent />
|
||||
</div>
|
||||
|
||||
<NotificationCenter v-if="userStore.isLoggedIn" />
|
||||
<NotificationCenter v-if="userStore.isLoggedIn && !userStore.isAdmin"/>
|
||||
|
||||
<router-link to="/cart" class="cart-link relative">
|
||||
<router-link v-if="!userStore.isAdmin" class="cart-link relative" to="/cart">
|
||||
<el-badge :value="cartCount" :hidden="cartCount === 0" class="cart-badge">
|
||||
<el-icon :size="20"><ShoppingCart /></el-icon>
|
||||
</el-badge>
|
||||
@@ -86,23 +78,23 @@
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="router.push('/profile')">
|
||||
<el-dropdown-item v-if="!userStore.isAdmin" @click="router.push('/profile')">
|
||||
<el-icon><User /></el-icon>
|
||||
个人中心
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="router.push('/orders')">
|
||||
<el-dropdown-item v-if="!userStore.isAdmin" @click="router.push('/orders')">
|
||||
<el-icon><List /></el-icon>
|
||||
我的订单
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="router.push('/favorites')">
|
||||
<el-dropdown-item v-if="!userStore.isAdmin" @click="router.push('/favorites')">
|
||||
<el-icon><Star /></el-icon>
|
||||
我的收藏
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="router.push('/reviews')">
|
||||
<el-dropdown-item v-if="!userStore.isAdmin" @click="router.push('/reviews')">
|
||||
<el-icon><ChatDotRound /></el-icon>
|
||||
我的评价
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="router.push('/notifications')">
|
||||
<el-dropdown-item v-if="!userStore.isAdmin" @click="router.push('/notifications')">
|
||||
<el-icon><Bell /></el-icon>
|
||||
消息通知
|
||||
</el-dropdown-item>
|
||||
@@ -186,7 +178,7 @@ const handleLogout = async () => {
|
||||
|
||||
// 更新购物车数量
|
||||
const updateCartCount = async () => {
|
||||
if (userStore.isLoggedIn) {
|
||||
if (userStore.isLoggedIn && !userStore.isAdmin) {
|
||||
cartCount.value = await cartStore.getCartCount()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,8 +61,8 @@
|
||||
<el-empty v-if="allNotifications.length === 0" description="暂无消息" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="秒杀" name="flashsale">
|
||||
|
||||
<el-tab-pane label="限时" name="flashsale">
|
||||
<div class="notification-list">
|
||||
<div
|
||||
v-for="item in flashsaleNotifications"
|
||||
@@ -80,8 +80,8 @@
|
||||
<div class="time">{{ formatTime(item.createdAt) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="flashsaleNotifications.length === 0" description="暂无秒杀消息" />
|
||||
|
||||
<el-empty v-if="flashsaleNotifications.length === 0" description="暂无限时消息"/>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<template #reference>
|
||||
<el-input
|
||||
v-model="searchQuery"
|
||||
placeholder="搜索商品、秒杀活动..."
|
||||
placeholder="搜索商品、限时活动..."
|
||||
class="search-input"
|
||||
@keyup.enter="handleQuickSearch"
|
||||
@focus="handleFocus"
|
||||
@@ -88,7 +88,7 @@
|
||||
<div class="content">
|
||||
<div class="name" v-html="highlightKeyword(item.name)"></div>
|
||||
<div class="info">
|
||||
<span class="type">{{ item.type === 'product' ? '商品' : '秒杀' }}</span>
|
||||
<span class="type">{{ item.type === 'product' ? '商品' : '限时' }}</span>
|
||||
<span class="price">¥{{ item.price }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -133,7 +133,7 @@
|
||||
<el-form-item label="搜索类型">
|
||||
<el-checkbox-group v-model="advancedForm.types">
|
||||
<el-checkbox label="product">商品</el-checkbox>
|
||||
<el-checkbox label="flashsale">秒杀</el-checkbox>
|
||||
<el-checkbox label="flashsale">限时</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
@@ -172,7 +172,7 @@ const searchHistory = ref<string[]>([])
|
||||
const hotSearches = ref([
|
||||
'iPhone 15',
|
||||
'MacBook Pro',
|
||||
'秒杀活动',
|
||||
'限时活动',
|
||||
'AirPods',
|
||||
'限时特价',
|
||||
'新品上市'
|
||||
|
||||
@@ -80,8 +80,8 @@ class WebSocketService {
|
||||
switch (message.type) {
|
||||
case 'FLASH_SALE_START':
|
||||
ElNotification({
|
||||
title: '秒杀开始',
|
||||
message: `${message.data.productName} 秒杀活动已开始!`,
|
||||
title: '限时开始',
|
||||
message: `${message.data.productName} 限时活动已开始!`,
|
||||
type: 'success',
|
||||
duration: 5000
|
||||
})
|
||||
@@ -89,8 +89,8 @@ class WebSocketService {
|
||||
|
||||
case 'FLASH_SALE_END':
|
||||
ElNotification({
|
||||
title: '秒杀结束',
|
||||
message: `${message.data.productName} 秒杀活动已结束`,
|
||||
title: '限时结束',
|
||||
message: `${message.data.productName} 限时活动已结束`,
|
||||
type: 'info',
|
||||
duration: 3000
|
||||
})
|
||||
|
||||
@@ -28,11 +28,6 @@
|
||||
<template #title>商品管理</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="/admin/flashsales">
|
||||
<el-icon><Lightning /></el-icon>
|
||||
<template #title>秒杀管理</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="/admin/groupbuying">
|
||||
<el-icon><Connection /></el-icon>
|
||||
<template #title>拼团管理</template>
|
||||
@@ -113,15 +108,7 @@
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="router.push('/')">
|
||||
<el-icon><HomeFilled /></el-icon>
|
||||
返回前台
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="router.push('/profile')">
|
||||
<el-icon><User /></el-icon>
|
||||
个人中心
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided @click="handleLogout">
|
||||
<el-dropdown-item @click="handleLogout">
|
||||
<el-icon><SwitchButton /></el-icon>
|
||||
退出登录
|
||||
</el-dropdown-item>
|
||||
@@ -162,7 +149,6 @@ const currentPageTitle = computed(() => {
|
||||
const titles: Record<string, string> = {
|
||||
'/admin': '',
|
||||
'/admin/products': '商品管理',
|
||||
'/admin/flashsales': '秒杀管理',
|
||||
'/admin/groupbuying': '拼团管理',
|
||||
'/admin/orders': '订单管理',
|
||||
'/admin/users': '用户管理',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="admin-flashsales page-shell">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2 class="page-title">秒杀管理</h2>
|
||||
<h2 class="page-title">限时管理</h2>
|
||||
<p class="page-subtitle">覆盖 JSP 的活动列表、发布、暂停、恢复、结束与详情查看</p>
|
||||
</div>
|
||||
<div class="page-actions">
|
||||
@@ -12,7 +12,7 @@
|
||||
</el-button>
|
||||
<el-button type="primary" @click="openCreateDialog">
|
||||
<el-icon><Plus /></el-icon>
|
||||
创建秒杀
|
||||
创建限时
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -67,7 +67,7 @@
|
||||
<el-table-column prop="originalPrice" label="原价" width="110">
|
||||
<template #default="{ row }">¥{{ formatCurrency(row.originalPrice) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="flashPrice" label="秒杀价" width="110">
|
||||
<el-table-column label="活动价" prop="flashPrice" width="110">
|
||||
<template #default="{ row }">¥{{ formatCurrency(row.flashPrice) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="flashStock" label="总库存" width="100" />
|
||||
@@ -109,7 +109,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="formVisible" :title="formMode === 'create' ? '创建秒杀活动' : '编辑秒杀活动'" width="760px">
|
||||
<el-dialog v-model="formVisible" :title="formMode === 'create' ? '创建限时活动' : '编辑限时活动'" width="760px">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
|
||||
<el-form-item label="关联商品" prop="productId">
|
||||
<el-select v-model="form.productId" filterable :disabled="formMode === 'edit'" placeholder="请选择商品">
|
||||
@@ -118,12 +118,12 @@
|
||||
</el-form-item>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="秒杀价格" prop="flashPrice">
|
||||
<el-form-item label="活动价格" prop="flashPrice">
|
||||
<el-input-number v-model="form.flashPrice" :min="0.01" :precision="2" class="w-full" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="秒杀库存" prop="flashStock">
|
||||
<el-form-item label="活动库存" prop="flashStock">
|
||||
<el-input-number v-model="form.flashStock" :min="1" class="w-full" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -153,7 +153,7 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="detailVisible" title="秒杀详情" width="760px">
|
||||
<el-dialog v-model="detailVisible" title="限时详情" width="760px">
|
||||
<div v-if="currentItem" class="detail-layout">
|
||||
<SafeImage :src="currentItem.productImageUrl" :alt="currentItem.productName" wrapper-class="detail-image" img-class="detail-image" />
|
||||
<div class="detail-content">
|
||||
@@ -236,8 +236,8 @@ const form = reactive({
|
||||
|
||||
const rules: FormRules = {
|
||||
productId: [{ required: true, message: '请选择商品', trigger: 'change' }],
|
||||
flashPrice: [{ required: true, message: '请输入秒杀价格', trigger: 'change' }],
|
||||
flashStock: [{ required: true, message: '请输入秒杀库存', trigger: 'change' }],
|
||||
flashPrice: [{required: true, message: '请输入活动价格', trigger: 'change'}],
|
||||
flashStock: [{required: true, message: '请输入活动库存', trigger: 'change'}],
|
||||
startTime: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
|
||||
endTime: [{ required: true, message: '请选择结束时间', trigger: 'change' }],
|
||||
}
|
||||
@@ -377,7 +377,7 @@ const submitForm = async () => {
|
||||
|
||||
if (formMode.value === 'create') {
|
||||
await flashsaleApi.create(payload)
|
||||
ElMessage.success('秒杀活动创建成功')
|
||||
ElMessage.success('限时活动创建成功')
|
||||
} else {
|
||||
await flashsaleApi.update(form.id, {
|
||||
flashPrice: form.flashPrice,
|
||||
@@ -385,7 +385,7 @@ const submitForm = async () => {
|
||||
startTime: form.startTime,
|
||||
endTime: form.endTime,
|
||||
})
|
||||
ElMessage.success('秒杀活动更新成功')
|
||||
ElMessage.success('限时活动更新成功')
|
||||
}
|
||||
|
||||
formVisible.value = false
|
||||
|
||||
@@ -86,7 +86,12 @@
|
||||
<template #default="{ row }">
|
||||
<el-button text type="primary" @click="openEditDialog(row)">编辑</el-button>
|
||||
<el-button v-if="row.status === 'DRAFT'" text type="success" @click="publishActivity(row)">发布</el-button>
|
||||
<el-button text type="danger" @click="removeActivity(row)">删除</el-button>
|
||||
<el-tooltip :disabled="row.status !== 'ACTIVE'" content="进行中的活动不能删除" placement="top">
|
||||
<span>
|
||||
<el-button :disabled="row.status === 'ACTIVE'" text type="danger"
|
||||
@click="removeActivity(row)">删除</el-button>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
@@ -93,8 +93,8 @@
|
||||
<div class="panel-card">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h3 class="panel-title">秒杀业务监控</h3>
|
||||
<p class="panel-subtitle">承接 JSP 中秒杀活动监控区域</p>
|
||||
<h3 class="panel-title">活动监控</h3>
|
||||
<p class="panel-subtitle">承接 JSP 中限时活动监控区域</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="business-metrics">
|
||||
|
||||
@@ -204,7 +204,7 @@ const getStatusType = (status: string) => {
|
||||
}
|
||||
|
||||
const getOrderTypeText = (orderType: string) => {
|
||||
const map: Record<string, string> = { NORMAL: '普通订单', FLASH_SALE: '秒杀订单', GROUP_BUYING: '拼团订单' }
|
||||
const map: Record<string, string> = {NORMAL: '普通订单', FLASH_SALE: '限时订单', GROUP_BUYING: '拼团订单'}
|
||||
return map[orderType] || '普通订单'
|
||||
}
|
||||
|
||||
|
||||
@@ -75,9 +75,10 @@
|
||||
<el-table-column prop="lastLogin" label="最后登录" min-width="170">
|
||||
<template #default="{ row }">{{ row.lastLogin ? formatTime(row.lastLogin) : '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<el-table-column fixed="right" label="操作" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-button text type="primary" @click="viewUser(row)">查看</el-button>
|
||||
<el-button v-if="row.role !== 'ADMIN'" text type="danger" @click="removeUser(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -114,6 +115,7 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import {ElMessage, ElMessageBox} from 'element-plus'
|
||||
import { adminApi } from '@/api/modules/admin'
|
||||
import type { AdminUserRow, AdminUserStats } from '@/types/admin'
|
||||
|
||||
@@ -168,6 +170,17 @@ const viewUser = (row: AdminUserRow) => {
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
const removeUser = async (row: AdminUserRow) => {
|
||||
await ElMessageBox.confirm(`确定删除用户“${row.username}”吗?该操作会同步清理该用户的订单、拼团、评价、收藏等数据。`, '删除确认', {type: 'warning'})
|
||||
try {
|
||||
await adminApi.deleteUser(row.id)
|
||||
ElMessage.success('用户已删除')
|
||||
await reloadData()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
loadUsers()
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<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 :to="{ path: '/flashsale' }">秒杀活动</el-breadcrumb-item>
|
||||
<el-breadcrumb-item :to="{ path: '/flashsale' }">限时活动</el-breadcrumb-item>
|
||||
<el-breadcrumb-item>{{ flashSale?.productName || '详情' }}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
</div>
|
||||
|
||||
<div v-else-if="!flashSale" class="text-center py-12">
|
||||
<el-empty description="秒杀活动不存在" />
|
||||
<el-button type="primary" @click="router.push('/flashsale')">返回秒杀列表</el-button>
|
||||
<el-empty description="限时活动不存在"/>
|
||||
<el-button type="primary" @click="router.push('/flashsale')">返回限时列表</el-button>
|
||||
</div>
|
||||
|
||||
<div v-else class="bg-white rounded-lg shadow-lg overflow-hidden">
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
<div class="price-card rounded-lg p-6 mb-6">
|
||||
<div class="flex items-end mb-2">
|
||||
<span class="text-sm text-gray-500 mr-2">秒杀价</span>
|
||||
<span class="text-sm text-gray-500 mr-2">活动价</span>
|
||||
<span class="detail-price">¥{{ flashSale.flashPrice }}</span>
|
||||
<span class="ml-4 text-lg text-gray-400 line-through">¥{{ flashSale.originalPrice }}</span>
|
||||
<span class="discount-pill">{{ discountPercent }}% OFF</span>
|
||||
@@ -88,7 +88,7 @@
|
||||
<div class="rules-card mt-8 p-4 rounded-lg">
|
||||
<h3 class="font-semibold mb-2">抢购说明</h3>
|
||||
<ul class="text-sm text-gray-600 space-y-1">
|
||||
<li>• 秒杀商品数量有限,先到先得</li>
|
||||
<li>• 限时商品数量有限,先到先得</li>
|
||||
<li>• 每个用户限购{{ flashSale.limitPerUser }}件</li>
|
||||
<li>• 下单后请在30分钟内完成支付</li>
|
||||
<li>• 商品一经售出,非质量问题不支持退换</li>
|
||||
@@ -141,7 +141,8 @@ const statusText = computed(() => {
|
||||
if (!flashSale.value) return ''
|
||||
switch (flashSale.value.status) {
|
||||
case 'UPCOMING': return '即将开始'
|
||||
case 'ACTIVE': return '秒杀中'
|
||||
case 'ACTIVE':
|
||||
return '进行中'
|
||||
case 'ENDED': return '已结束'
|
||||
default: return ''
|
||||
}
|
||||
@@ -187,7 +188,7 @@ const loadDetail = async () => {
|
||||
const res = await flashsaleApi.getDetail(Number(route.params.id))
|
||||
if (res.success) flashSale.value = res.data
|
||||
} catch (error) {
|
||||
console.error('加载秒杀详情失败:', error)
|
||||
console.error('加载限时详情失败:', error)
|
||||
ElMessage.error('加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -202,7 +203,7 @@ const handleParticipate = async () => {
|
||||
}
|
||||
if (!flashSale.value || !canParticipate.value) return
|
||||
|
||||
await ElMessageBox.confirm(`确定要抢购该商品吗?\n秒杀价:¥${flashSale.value.flashPrice}`, '抢购确认', {
|
||||
await ElMessageBox.confirm(`确定要抢购该商品吗?\n活动价:¥${flashSale.value.flashPrice}`, '抢购确认', {
|
||||
confirmButtonText: '立即抢购',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
@@ -216,7 +217,7 @@ const handleParticipate = async () => {
|
||||
router.push(`/order/${res.data.orderId}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('参与秒杀失败:', error)
|
||||
console.error('参与限时失败:', error)
|
||||
} finally {
|
||||
participating.value = false
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold mb-2 flex items-center">
|
||||
<el-icon class="page-icon mr-2"><Lightning /></el-icon>
|
||||
秒杀活动
|
||||
限时活动
|
||||
</h1>
|
||||
<p class="text-gray-600">限时抢购,先到先得</p>
|
||||
</div>
|
||||
@@ -80,15 +80,15 @@
|
||||
<el-icon :size="30" class="stat-icon"><SuccessFilled /></el-icon>
|
||||
</div>
|
||||
</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="flashSales.length === 0" class="text-center py-12">
|
||||
<el-empty description="暂无秒杀活动" />
|
||||
<el-empty description="暂无限时活动"/>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
@@ -157,7 +157,7 @@ const statistics = reactive({
|
||||
success: 0
|
||||
})
|
||||
|
||||
// 加载秒杀活动
|
||||
// 加载限时活动
|
||||
const loadFlashSales = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -172,7 +172,7 @@ const loadFlashSales = async () => {
|
||||
pagination.total = res.data.totalElements
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载秒杀活动失败:', error)
|
||||
console.error('加载限时活动失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -193,7 +193,7 @@ const loadStatistics = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 参与秒杀
|
||||
// 参与限时
|
||||
const handleParticipate = async (flashSaleId: number) => {
|
||||
if (!userStore.isLoggedIn) {
|
||||
ElMessage.warning('请先登录')
|
||||
@@ -207,7 +207,7 @@ const handleParticipate = async (flashSaleId: number) => {
|
||||
if (res.success && res.data.eligible) {
|
||||
// 确认对话框
|
||||
await ElMessageBox.confirm(
|
||||
'确定要参与这个秒杀活动吗?',
|
||||
'确定要参与这个限时活动吗?',
|
||||
'提示',
|
||||
{
|
||||
confirmButtonText: '立即抢购',
|
||||
|
||||
@@ -65,12 +65,12 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 正在秒杀 -->
|
||||
<!-- 限时活动 -->
|
||||
<section class="mb-12">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-2xl font-bold flex items-center">
|
||||
<el-icon class="section-icon mr-2"><Lightning /></el-icon>
|
||||
正在秒杀
|
||||
限时活动
|
||||
</h2>
|
||||
<el-button text @click="router.push('/flashsale')">
|
||||
查看全部
|
||||
@@ -84,7 +84,7 @@
|
||||
</div>
|
||||
|
||||
<div v-else-if="activeFlashSales.length === 0" class="text-center py-8">
|
||||
<el-empty description="暂无进行中的秒杀活动" />
|
||||
<el-empty description="暂无进行中的限时活动"/>
|
||||
</div>
|
||||
|
||||
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
@@ -135,8 +135,8 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div class="feature-card">
|
||||
<el-icon :size="40" class="feature-icon mb-4"><Lightning /></el-icon>
|
||||
<h3 class="text-lg font-semibold mb-2">秒杀抢购</h3>
|
||||
<p class="text-gray-600">社区生鲜团购系统,支持大量用户同时抢购</p>
|
||||
<h3 class="text-lg font-semibold mb-2">限时优惠</h3>
|
||||
<p class="text-gray-600">社区生鲜团购系统,支持热门商品集中促销</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<el-icon :size="40" class="feature-icon mb-4"><Lock /></el-icon>
|
||||
@@ -181,8 +181,8 @@ const banners = [
|
||||
id: 1,
|
||||
title: '社区生鲜团购系统',
|
||||
subtitle: '社区生鲜团购系统,新鲜直达您身边',
|
||||
buttonText: '立即抢购',
|
||||
link: '/flashsales',
|
||||
buttonText: '查看拼团',
|
||||
link: '/groupbuying',
|
||||
bgColor: '#ffffff',
|
||||
icon: 'Lightning'
|
||||
},
|
||||
@@ -191,7 +191,7 @@ const banners = [
|
||||
title: '防超卖机制',
|
||||
subtitle: '采用分布式锁和Lua脚本,确保数据一致性',
|
||||
buttonText: '了解更多',
|
||||
link: '/flashsales',
|
||||
link: '/groupbuying',
|
||||
bgColor: '#ffffff',
|
||||
icon: 'Lock'
|
||||
},
|
||||
@@ -246,7 +246,7 @@ const loadCategories = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载秒杀活动
|
||||
// 加载限时活动
|
||||
const loadFlashSales = async () => {
|
||||
loadingFlashSales.value = true
|
||||
try {
|
||||
@@ -255,7 +255,7 @@ const loadFlashSales = async () => {
|
||||
activeFlashSales.value = res.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载秒杀活动失败:', error)
|
||||
console.error('加载限时活动失败:', error)
|
||||
} finally {
|
||||
loadingFlashSales.value = false
|
||||
}
|
||||
@@ -276,15 +276,15 @@ const loadProducts = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 参与秒杀
|
||||
// 参与限时
|
||||
const handleParticipate = async (flashSaleId: number) => {
|
||||
if (!userStore.isLoggedIn) {
|
||||
ElMessage.warning('请先登录')
|
||||
router.push('/login')
|
||||
return
|
||||
}
|
||||
|
||||
// 跳转到秒杀详情页
|
||||
|
||||
// 跳转到限时详情页
|
||||
router.push(`/flashsale/${flashSaleId}`)
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
<span>{{ formatTime(order.createdAt) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<el-tag v-if="order.orderType === 'FLASH_SALE'" type="danger" size="small">秒杀</el-tag>
|
||||
<el-tag v-if="order.orderType === 'FLASH_SALE'" size="small" type="danger">限时</el-tag>
|
||||
<el-tag v-else-if="order.orderType === 'GROUP_BUYING'" type="success" size="small">拼团</el-tag>
|
||||
<el-tag :type="getStatusType(order.status)">{{ getStatusText(order.status) }}</el-tag>
|
||||
</div>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<!-- 标签筛选 -->
|
||||
<el-tabs v-model="activeType" @tab-change="loadNotifications">
|
||||
<el-tab-pane label="全部" name="all" />
|
||||
<el-tab-pane label="秒杀" name="flashsale" />
|
||||
<el-tab-pane label="限时" name="flashsale"/>
|
||||
<el-tab-pane label="订单" name="order" />
|
||||
<el-tab-pane label="系统" name="system" />
|
||||
</el-tabs>
|
||||
@@ -171,7 +171,7 @@ const getIconColor = (type: string) => {
|
||||
|
||||
const getTypeLabel = (type: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
flashsale: '秒杀',
|
||||
flashsale: '限时',
|
||||
order: '订单',
|
||||
system: '系统'
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</div>
|
||||
<div class="stat-card orange">
|
||||
<div class="stat-value">{{ profileStats.flashSaleSuccess }}</div>
|
||||
<div class="stat-label">秒杀成功</div>
|
||||
<div class="stat-label">活动成功</div>
|
||||
</div>
|
||||
<div class="stat-card purple">
|
||||
<div class="stat-value">{{ profileStats.favoriteCount }}</div>
|
||||
|
||||
@@ -30,6 +30,22 @@ export function setupGuards(router: Router) {
|
||||
next('/')
|
||||
return
|
||||
}
|
||||
|
||||
const adminBlockedFrontPaths = [
|
||||
'/cart',
|
||||
'/orders',
|
||||
'/favorites',
|
||||
'/reviews',
|
||||
'/returns',
|
||||
'/notifications',
|
||||
'/addresses',
|
||||
'/profile',
|
||||
]
|
||||
const isAdminFrontPath = adminBlockedFrontPaths.some((path) => to.path === path || to.path.startsWith(`${path}/`))
|
||||
if (userStore.isAdmin && isAdminFrontPath) {
|
||||
next('/admin')
|
||||
return
|
||||
}
|
||||
|
||||
// 已登录用户访问登录/注册页面
|
||||
if ((to.path === '/login' || to.path === '/register') && userStore.isLoggedIn) {
|
||||
|
||||
@@ -18,7 +18,7 @@ const routes: RouteRecordRaw[] = [
|
||||
path: 'flashsale',
|
||||
name: 'FlashSale',
|
||||
component: () => import('@/pages/flashsale/index.vue'),
|
||||
meta: { title: '秒杀活动' }
|
||||
meta: {title: '限时活动'}
|
||||
},
|
||||
{
|
||||
path: 'flashsales',
|
||||
@@ -28,7 +28,7 @@ const routes: RouteRecordRaw[] = [
|
||||
path: 'flashsale/:id',
|
||||
name: 'FlashSaleDetail',
|
||||
component: () => import('@/pages/flashsale/detail.vue'),
|
||||
meta: { title: '秒杀详情' }
|
||||
meta: {title: '限时详情'}
|
||||
},
|
||||
{
|
||||
path: 'products',
|
||||
@@ -157,7 +157,7 @@ const routes: RouteRecordRaw[] = [
|
||||
path: 'flashsales',
|
||||
name: 'AdminFlashSales',
|
||||
component: () => import('@/pages/admin/flashsales.vue'),
|
||||
meta: { title: '秒杀管理' }
|
||||
meta: {title: '限时管理'}
|
||||
},
|
||||
{
|
||||
path: 'groupbuying',
|
||||
|
||||
2
flash-sale-frontend/src/types/api.d.ts
vendored
2
flash-sale-frontend/src/types/api.d.ts
vendored
@@ -71,7 +71,7 @@ export interface Product {
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
// 秒杀活动类型
|
||||
// 限时活动类型
|
||||
export interface FlashSale {
|
||||
id: number
|
||||
productId: number
|
||||
|
||||
@@ -244,6 +244,31 @@ public class AdminController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除普通用户
|
||||
*/
|
||||
@Operation(summary = "删除普通用户")
|
||||
@DeleteMapping("/users/{id}")
|
||||
public ResponseEntity<Map<String, Object>> deleteUser(@PathVariable Long id) {
|
||||
try {
|
||||
adminService.deleteUser(id);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "用户删除成功");
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单列表
|
||||
*/
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -25,8 +26,12 @@ public interface GroupBuyingGroupRepository extends JpaRepository<GroupBuyingGro
|
||||
|
||||
List<GroupBuyingGroup> findByLeaderUserId(Long userId);
|
||||
|
||||
List<GroupBuyingGroup> findByLeaderUserIdAndStatus(Long userId, Integer status);
|
||||
|
||||
Page<GroupBuyingGroup> findByGroupBuyingId(Long groupBuyingId, Pageable pageable);
|
||||
|
||||
List<GroupBuyingGroup> findByGroupBuyingId(Long groupBuyingId);
|
||||
|
||||
@Query("SELECT g FROM GroupBuyingGroup g WHERE g.id IN " +
|
||||
"(SELECT m.groupId FROM GroupBuyingMember m WHERE m.userId = :userId AND m.status != 3)")
|
||||
Page<GroupBuyingGroup> findByMemberUserId(@Param("userId") Long userId, Pageable pageable);
|
||||
@@ -44,4 +49,8 @@ public interface GroupBuyingGroupRepository extends JpaRepository<GroupBuyingGro
|
||||
@Modifying
|
||||
@Query("UPDATE GroupBuyingGroup g SET g.status = :status, g.completedAt = :completedAt WHERE g.id = :id")
|
||||
int updateStatusAndCompletedAt(@Param("id") Long id, @Param("status") Integer status, @Param("completedAt") LocalDateTime completedAt);
|
||||
|
||||
@Modifying
|
||||
@Query("DELETE FROM GroupBuyingGroup g WHERE g.id IN :ids")
|
||||
int deleteByIdIn(@Param("ids") Collection<Long> ids);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -33,4 +34,10 @@ public interface GroupBuyingMemberRepository extends JpaRepository<GroupBuyingMe
|
||||
@Query("SELECT COUNT(m) FROM GroupBuyingMember m WHERE m.userId = :userId AND m.status != 3 " +
|
||||
"AND m.groupId IN (SELECT g.id FROM GroupBuyingGroup g WHERE g.groupBuyingId = :activityId)")
|
||||
long countActiveByUserIdAndActivityId(@Param("userId") Long userId, @Param("activityId") Long activityId);
|
||||
|
||||
@Modifying
|
||||
@Query("DELETE FROM GroupBuyingMember m WHERE m.groupId IN :groupIds")
|
||||
int deleteByGroupIdIn(@Param("groupIds") Collection<Long> groupIds);
|
||||
|
||||
void deleteByUserId(Long userId);
|
||||
}
|
||||
|
||||
@@ -27,4 +27,6 @@ public interface NotificationRepository extends JpaRepository<Notification, Long
|
||||
int markAsRead(@Param("id") Long id, @Param("userId") Long userId);
|
||||
|
||||
void deleteByUserId(Long userId);
|
||||
|
||||
void deleteByLinkIn(List<String> links);
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@ package com.org.flashsalesystem.repository;
|
||||
|
||||
import com.org.flashsalesystem.entity.OrderItem;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
@@ -19,4 +21,8 @@ public interface OrderItemRepository extends JpaRepository<OrderItem, Long> {
|
||||
|
||||
@Query("SELECT COALESCE(SUM(i.subtotal), 0) FROM OrderItem i JOIN Order o ON i.orderId = o.id WHERE i.productId = :productId AND o.status IN (2,3,4)")
|
||||
BigDecimal sumSubtotalByProductId(@Param("productId") Long productId);
|
||||
|
||||
@Modifying
|
||||
@Query("DELETE FROM OrderItem i WHERE i.orderId IN :orderIds")
|
||||
int deleteByOrderIdIn(@Param("orderIds") Collection<Long> orderIds);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -167,4 +168,12 @@ public interface OrderRepository extends JpaRepository<Order, Long> {
|
||||
BigDecimal sumTotalPriceByProductId(@Param("productId") Long productId);
|
||||
|
||||
List<Order> findByGroupNoOrderByCreatedAtAsc(String groupNo);
|
||||
|
||||
List<Order> findByGroupBuyingGroupIdIn(Collection<Long> groupIds);
|
||||
|
||||
@Modifying
|
||||
@Query("DELETE FROM Order o WHERE o.id IN :orderIds")
|
||||
int deleteByIdIn(@Param("orderIds") Collection<Long> orderIds);
|
||||
|
||||
void deleteByUserId(Long userId);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,12 @@ 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.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -25,4 +29,10 @@ public interface OrderReturnRepository extends JpaRepository<OrderReturn, Long>
|
||||
Page<OrderReturn> findAllByOrderByCreatedAtDesc(Pageable pageable);
|
||||
|
||||
long countByStatus(Integer status);
|
||||
|
||||
@Modifying
|
||||
@Query("DELETE FROM OrderReturn r WHERE r.orderId IN :orderIds")
|
||||
int deleteByOrderIdIn(@Param("orderIds") Collection<Long> orderIds);
|
||||
|
||||
void deleteByUserId(Long userId);
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ package com.org.flashsalesystem.repository;
|
||||
|
||||
import com.org.flashsalesystem.entity.ProductReview;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -30,4 +32,10 @@ public interface ProductReviewRepository extends JpaRepository<ProductReview, Lo
|
||||
boolean existsByOrderIdAndProductId(Long orderId, Long productId);
|
||||
|
||||
Optional<ProductReview> findByOrderIdAndProductId(Long orderId, Long productId);
|
||||
|
||||
@Modifying
|
||||
@Query("DELETE FROM ProductReview r WHERE r.orderId IN :orderIds")
|
||||
int deleteByOrderIdIn(@Param("orderIds") Collection<Long> orderIds);
|
||||
|
||||
void deleteByUserId(Long userId);
|
||||
}
|
||||
|
||||
@@ -12,4 +12,6 @@ public interface UserAddressRepository extends JpaRepository<UserAddress, Long>
|
||||
List<UserAddress> findByUserIdOrderByIsDefaultDescUpdatedAtDesc(Long userId);
|
||||
Optional<UserAddress> findByUserIdAndIsDefaultTrue(Long userId);
|
||||
Optional<UserAddress> findByIdAndUserId(Long id, Long userId);
|
||||
|
||||
void deleteByUserId(Long userId);
|
||||
}
|
||||
|
||||
@@ -14,4 +14,6 @@ public interface UserFavoriteRepository extends JpaRepository<UserFavorite, Long
|
||||
boolean existsByUserIdAndProductId(Long userId, Long productId);
|
||||
long countByUserId(Long userId);
|
||||
void deleteByUserIdAndProductId(Long userId, Long productId);
|
||||
|
||||
void deleteByUserId(Long userId);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import com.org.flashsalesystem.dto.UserDTO;
|
||||
import com.org.flashsalesystem.entity.Order;
|
||||
import com.org.flashsalesystem.entity.Product;
|
||||
import com.org.flashsalesystem.entity.ProductReview;
|
||||
import com.org.flashsalesystem.entity.User;
|
||||
import com.org.flashsalesystem.entity.UserFavorite;
|
||||
import com.org.flashsalesystem.repository.FlashSaleRepository;
|
||||
import com.org.flashsalesystem.repository.OrderItemRepository;
|
||||
import com.org.flashsalesystem.repository.OrderRepository;
|
||||
import com.org.flashsalesystem.repository.ProductRepository;
|
||||
import com.org.flashsalesystem.repository.ProductReviewRepository;
|
||||
import com.org.flashsalesystem.repository.UserFavoriteRepository;
|
||||
import com.org.flashsalesystem.repository.UserRepository;
|
||||
import com.org.flashsalesystem.entity.*;
|
||||
import com.org.flashsalesystem.repository.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -21,6 +11,7 @@ import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.io.File;
|
||||
@@ -60,6 +51,21 @@ public class AdminService {
|
||||
@Autowired
|
||||
private FlashSaleRepository flashSaleRepository;
|
||||
|
||||
@Autowired
|
||||
private OrderReturnRepository orderReturnRepository;
|
||||
|
||||
@Autowired
|
||||
private UserAddressRepository userAddressRepository;
|
||||
|
||||
@Autowired
|
||||
private NotificationRepository notificationRepository;
|
||||
|
||||
@Autowired
|
||||
private GroupBuyingGroupRepository groupBuyingGroupRepository;
|
||||
|
||||
@Autowired
|
||||
private GroupBuyingMemberRepository groupBuyingMemberRepository;
|
||||
|
||||
@Autowired
|
||||
private RedisService redisService;
|
||||
|
||||
@@ -404,6 +410,71 @@ public class AdminService {
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteUser(Long id) {
|
||||
User user = userRepository.findById(id)
|
||||
.orElseThrow(() -> new RuntimeException("用户不存在"));
|
||||
|
||||
String role = user.getRole() == null ? "" : user.getRole();
|
||||
if ("ADMIN".equalsIgnoreCase(role) || "admin".equalsIgnoreCase(user.getUsername())) {
|
||||
throw new RuntimeException("管理员账号不能删除");
|
||||
}
|
||||
|
||||
List<GroupBuyingGroup> formingLedGroups = groupBuyingGroupRepository.findByLeaderUserIdAndStatus(id, 1);
|
||||
if (!formingLedGroups.isEmpty()) {
|
||||
throw new RuntimeException("该用户是进行中团组的团长,不能删除");
|
||||
}
|
||||
|
||||
List<Long> ledGroupIds = groupBuyingGroupRepository.findByLeaderUserId(id).stream()
|
||||
.map(GroupBuyingGroup::getId)
|
||||
.collect(Collectors.toList());
|
||||
deleteGroupData(ledGroupIds);
|
||||
|
||||
List<Long> orderIds = orderRepository.findByUserId(id).stream()
|
||||
.map(Order::getId)
|
||||
.collect(Collectors.toList());
|
||||
deleteOrderData(orderIds);
|
||||
|
||||
groupBuyingMemberRepository.deleteByUserId(id);
|
||||
notificationRepository.deleteByUserId(id);
|
||||
userFavoriteRepository.deleteByUserId(id);
|
||||
userAddressRepository.deleteByUserId(id);
|
||||
productReviewRepository.deleteByUserId(id);
|
||||
orderReturnRepository.deleteByUserId(id);
|
||||
|
||||
orderRepository.deleteByUserId(id);
|
||||
redisService.sRem("online_users", id.toString());
|
||||
userRepository.deleteById(id);
|
||||
}
|
||||
|
||||
private void deleteGroupData(List<Long> groupIds) {
|
||||
if (groupIds == null || groupIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<Long> groupOrderIds = orderRepository.findByGroupBuyingGroupIdIn(groupIds).stream()
|
||||
.map(Order::getId)
|
||||
.collect(Collectors.toList());
|
||||
deleteOrderData(groupOrderIds);
|
||||
groupBuyingMemberRepository.deleteByGroupIdIn(groupIds);
|
||||
groupBuyingGroupRepository.deleteByIdIn(groupIds);
|
||||
}
|
||||
|
||||
private void deleteOrderData(List<Long> orderIds) {
|
||||
if (orderIds == null || orderIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> orderLinks = orderIds.stream()
|
||||
.map(orderId -> "/order/" + orderId)
|
||||
.collect(Collectors.toList());
|
||||
notificationRepository.deleteByLinkIn(orderLinks);
|
||||
productReviewRepository.deleteByOrderIdIn(orderIds);
|
||||
orderReturnRepository.deleteByOrderIdIn(orderIds);
|
||||
orderItemRepository.deleteByOrderIdIn(orderIds);
|
||||
orderRepository.deleteByIdIn(orderIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单列表
|
||||
*/
|
||||
|
||||
@@ -79,6 +79,14 @@ public class FlashSaleService {
|
||||
|
||||
Product product = productOpt.get();
|
||||
|
||||
if (createDTO.getStartTime() != null && createDTO.getStartTime().isBefore(LocalDateTime.now())) {
|
||||
throw new RuntimeException("开始时间不能早于当前时间");
|
||||
}
|
||||
if (createDTO.getStartTime() != null && createDTO.getEndTime() != null
|
||||
&& !createDTO.getEndTime().isAfter(createDTO.getStartTime())) {
|
||||
throw new RuntimeException("结束时间必须晚于开始时间");
|
||||
}
|
||||
|
||||
// 创建秒杀活动
|
||||
FlashSale flashSale = new FlashSale();
|
||||
BeanUtils.copyProperties(createDTO, flashSale);
|
||||
|
||||
@@ -56,6 +56,15 @@ public class GroupBuyingService {
|
||||
@Autowired
|
||||
private OrderItemRepository orderItemRepository;
|
||||
|
||||
@Autowired
|
||||
private ProductReviewRepository productReviewRepository;
|
||||
|
||||
@Autowired
|
||||
private OrderReturnRepository orderReturnRepository;
|
||||
|
||||
@Autowired
|
||||
private NotificationRepository notificationRepository;
|
||||
|
||||
@Autowired
|
||||
private RedisService redisService;
|
||||
|
||||
@@ -132,15 +141,49 @@ public class GroupBuyingService {
|
||||
GroupBuying gb = groupBuyingRepository.findById(id)
|
||||
.orElseThrow(() -> new RuntimeException("拼团活动不存在"));
|
||||
|
||||
if (gb.getStatus() == 2) {
|
||||
if (isEffectivelyActive(gb)) {
|
||||
throw new RuntimeException("进行中的活动不能删除");
|
||||
}
|
||||
|
||||
groupBuyingRepository.deleteById(id);
|
||||
List<GroupBuyingGroup> groups = groupBuyingGroupRepository.findByGroupBuyingId(id);
|
||||
List<Long> groupIds = groups.stream()
|
||||
.map(GroupBuyingGroup::getId)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!groupIds.isEmpty()) {
|
||||
List<Long> orderIds = orderRepository.findByGroupBuyingGroupIdIn(groupIds).stream()
|
||||
.map(Order::getId)
|
||||
.collect(Collectors.toList());
|
||||
deleteOrderData(orderIds);
|
||||
groupBuyingMemberRepository.deleteByGroupIdIn(groupIds);
|
||||
groupBuyingGroupRepository.deleteByIdIn(groupIds);
|
||||
}
|
||||
|
||||
groupBuyingRepository.delete(gb);
|
||||
redisService.delete(GB_STOCK_PREFIX + id);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isEffectivelyActive(GroupBuying gb) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
return Integer.valueOf(2).equals(gb.getStatus()) && !now.isBefore(gb.getStartTime()) && now.isBefore(gb.getEndTime());
|
||||
}
|
||||
|
||||
private void deleteOrderData(List<Long> orderIds) {
|
||||
if (orderIds == null || orderIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> orderLinks = orderIds.stream()
|
||||
.map(orderId -> "/order/" + orderId)
|
||||
.collect(Collectors.toList());
|
||||
notificationRepository.deleteByLinkIn(orderLinks);
|
||||
productReviewRepository.deleteByOrderIdIn(orderIds);
|
||||
orderReturnRepository.deleteByOrderIdIn(orderIds);
|
||||
orderItemRepository.deleteByOrderIdIn(orderIds);
|
||||
orderRepository.deleteByIdIn(orderIds);
|
||||
}
|
||||
|
||||
// ========== 查询操作 ==========
|
||||
|
||||
public Map<String, Object> getGroupBuyingList(int page, int size, Integer status) {
|
||||
@@ -584,8 +627,9 @@ public class GroupBuyingService {
|
||||
dto.setTotalStock(gb.getTotalStock());
|
||||
dto.setRemainingStock(gb.getRemainingStock());
|
||||
dto.setMaxPerUser(gb.getMaxPerUser());
|
||||
dto.setStatus(gb.getStatus());
|
||||
dto.setStatusDescription(getStatusDescription(gb.getStatus()));
|
||||
int effectiveStatus = getEffectiveStatus(gb);
|
||||
dto.setStatus(effectiveStatus);
|
||||
dto.setStatusDescription(getStatusDescription(effectiveStatus));
|
||||
dto.setStartTime(gb.getStartTime());
|
||||
dto.setEndTime(gb.getEndTime());
|
||||
dto.setCreatedAt(gb.getCreatedAt());
|
||||
@@ -668,6 +712,17 @@ public class GroupBuyingService {
|
||||
}
|
||||
}
|
||||
|
||||
private int getEffectiveStatus(GroupBuying gb) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (Integer.valueOf(2).equals(gb.getStatus()) && !now.isBefore(gb.getEndTime())) {
|
||||
return 3;
|
||||
}
|
||||
if (Integer.valueOf(1).equals(gb.getStatus()) && !now.isBefore(gb.getStartTime()) && now.isBefore(gb.getEndTime())) {
|
||||
return 2;
|
||||
}
|
||||
return gb.getStatus() == null ? 0 : gb.getStatus();
|
||||
}
|
||||
|
||||
private String getGroupStatusDescription(Integer status) {
|
||||
if (status == null) return "未知";
|
||||
switch (status) {
|
||||
|
||||
Reference in New Issue
Block a user