feat: 删除JSP视图层,完善评价和通知系统,新增拼团模块

- 删除所有 JSP 页面(20个文件),前端完全迁移至 Vue 3 SPA
- 完善评价系统:ReviewDialog 组件、用户评价历史页、评价状态检查API
- 新增通知系统:Notification 实体/仓库/服务/控制器,NotificationCenter 接入真实API
- 新增拼团模块:GroupBuying 全套后端和前端页面
- 修复 review check API 参数双重包装导致请求格式错误
- 修复通知 API 路径缺少 /api 前缀和响应格式处理
- MessageListenerService 集成 NotificationService 创建持久化通知

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-14 16:40:26 +08:00
parent b684ea38d4
commit c4582655d9
115 changed files with 5968 additions and 12623 deletions

View File

@@ -128,10 +128,22 @@
</el-col>
</el-row>
<el-form-item label="开始时间" prop="startTime">
<el-date-picker v-model="form.startTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" class="w-full" />
<el-date-picker
v-model="form.startTime"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
:disabled-date="disablePastDate"
class="w-full"
/>
</el-form-item>
<el-form-item label="结束时间" prop="endTime">
<el-date-picker v-model="form.endTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" class="w-full" />
<el-date-picker
v-model="form.endTime"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
:disabled-date="disablePastDate"
class="w-full"
/>
</el-form-item>
</el-form>
<template #footer>
@@ -187,6 +199,9 @@ const formRef = ref<FormInstance>()
const flashSales = ref<FlashSale[]>([])
const currentItem = ref<FlashSale | null>(null)
const productOptions = ref<AdminProductRow[]>([])
const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'
const CREATE_START_LEAD_MINUTES = 5
const CREATE_DURATION_DAYS = 1
const query = reactive({
keyword: '',
@@ -206,13 +221,16 @@ const stats = reactive<AdminFlashSaleStats>({
endedFlashSales: 0,
})
const buildDefaultStartTime = () => dayjs().add(CREATE_START_LEAD_MINUTES, 'minute').startOf('minute').format(TIME_FORMAT)
const buildDefaultEndTime = (startTime = buildDefaultStartTime()) => dayjs(startTime).add(CREATE_DURATION_DAYS, 'day').format(TIME_FORMAT)
const form = reactive({
id: 0,
productId: undefined as number | undefined,
flashPrice: 0.01,
flashStock: 1,
startTime: '',
endTime: '',
startTime: buildDefaultStartTime(),
endTime: buildDefaultEndTime(),
})
const rules: FormRules = {
@@ -254,13 +272,38 @@ const getStockRate = (item: FlashSale) => {
return Math.round((item.remainingStock / item.flashStock) * 100)
}
const disablePastDate = (date: Date) => dayjs(date).endOf('day').isBefore(dayjs())
const validateTimeRange = () => {
const now = dayjs()
const startTime = dayjs(form.startTime)
const endTime = dayjs(form.endTime)
if (!startTime.isValid() || !endTime.isValid()) {
ElMessage.error('开始时间或结束时间格式无效')
return false
}
if (!startTime.isAfter(now)) {
ElMessage.error('开始时间必须晚于当前时间')
return false
}
if (!endTime.isAfter(startTime)) {
ElMessage.error('结束时间必须晚于开始时间')
return false
}
return true
}
const resetForm = () => {
form.id = 0
form.productId = undefined
form.flashPrice = 0.01
form.flashStock = 1
form.startTime = ''
form.endTime = ''
form.startTime = buildDefaultStartTime()
form.endTime = buildDefaultEndTime(form.startTime)
}
const loadStats = async () => {
@@ -315,6 +358,7 @@ const submitForm = async () => {
await formRef.value.validate(async (valid) => {
if (!valid) return
if (!validateTimeRange()) return
saving.value = true
try {
@@ -435,19 +479,20 @@ onMounted(() => {
}
.mini-stat {
@apply rounded-xl text-white p-5 shadow-sm;
&.purple { background: linear-gradient(135deg, #8b5cf6, #7c3aed); }
&.red { background: linear-gradient(135deg, #ef4444, #dc2626); }
&.orange { background: linear-gradient(135deg, #f59e0b, #ea580c); }
&.gray { background: linear-gradient(135deg, #64748b, #475569); }
@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);
&__value { @apply text-3xl font-bold; }
&__label { @apply text-sm opacity-90 mt-2; }
}
.panel-card {
@apply bg-white rounded-xl shadow-sm p-5;
@apply bg-white rounded-xl p-5;
border: 1px solid #d8cebf;
box-shadow: 0 10px 24px rgba(23, 22, 20, 0.04);
}
.filter-card {
@@ -468,7 +513,7 @@ onMounted(() => {
height: 56px;
object-fit: cover;
border-radius: 12px;
border: 1px solid #e2e8f0;
border: 1px solid #d8cebf;
}
.detail-image {
@@ -499,7 +544,8 @@ onMounted(() => {
}
.flash-price {
@apply text-3xl font-bold text-rose-500;
@apply text-3xl font-bold;
color: #171715;
}
.origin-price {