-
-
@@ -111,7 +133,7 @@
\ No newline at end of file
+
+.page-icon {
+ color: #44443f;
+}
+
+.category-tag {
+ font-size: 14px;
+ padding: 6px 16px;
+ border-radius: 999px;
+ transition: all 0.2s;
+
+ &:hover {
+ transform: translateY(-1px);
+ }
+}
+
diff --git a/flash-sale-frontend/src/pages/user/favorites.vue b/flash-sale-frontend/src/pages/user/favorites.vue
index bd6b56f..09a2b04 100644
--- a/flash-sale-frontend/src/pages/user/favorites.vue
+++ b/flash-sale-frontend/src/pages/user/favorites.vue
@@ -94,7 +94,7 @@ onMounted(() => {
diff --git a/flash-sale-frontend/src/pages/user/notifications.vue b/flash-sale-frontend/src/pages/user/notifications.vue
new file mode 100644
index 0000000..d76e30c
--- /dev/null
+++ b/flash-sale-frontend/src/pages/user/notifications.vue
@@ -0,0 +1,184 @@
+
+
+
+
+ 首页
+ 消息通知
+
+
+
+
+
消息通知
+
+
+ 全部已读
+
+
+ 清空全部
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.title }}
+ 未读
+ {{ getTypeLabel(item.type) }}
+
+
{{ item.message }}
+
{{ formatTime(item.createdAt) }}
+
+
+ 标记已读
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flash-sale-frontend/src/pages/user/profile.vue b/flash-sale-frontend/src/pages/user/profile.vue
index 299d3bf..4e5f8ee 100644
--- a/flash-sale-frontend/src/pages/user/profile.vue
+++ b/flash-sale-frontend/src/pages/user/profile.vue
@@ -326,16 +326,15 @@ onMounted(async () => {
\ No newline at end of file
+
+.page-mark {
+ color: #171715;
+}
+
+.register-panel {
+ border: 1px solid #d8cebf;
+ border-radius: 24px;
+ background: #fffaf2;
+ box-shadow: 0 14px 34px rgba(23, 22, 20, 0.06);
+}
+
diff --git a/flash-sale-frontend/src/pages/user/reviews.vue b/flash-sale-frontend/src/pages/user/reviews.vue
new file mode 100644
index 0000000..c32a61f
--- /dev/null
+++ b/flash-sale-frontend/src/pages/user/reviews.vue
@@ -0,0 +1,122 @@
+
+
+
+
+ 首页
+ 我的评价
+
+
+
我的评价
+
+
+
+
+
+ 去购物
+
+
+
+
+
+
+
+
+
+
+
+ {{ review.productName || '商品' }}
+
+
+ 订单 #{{ review.orderId }}
+
+
+
{{ formatTime(review.createdAt) }}
+
+
+
{{ review.content }}
+
+
+
商家回复
+
{{ review.adminReply }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flash-sale-frontend/src/router/index.ts b/flash-sale-frontend/src/router/index.ts
index 8caf1f2..31538e9 100644
--- a/flash-sale-frontend/src/router/index.ts
+++ b/flash-sale-frontend/src/router/index.ts
@@ -80,6 +80,36 @@ const routes: RouteRecordRaw[] = [
component: () => import('@/pages/user/favorites.vue'),
meta: { title: '我的收藏', requiresAuth: true }
},
+ {
+ path: 'reviews',
+ name: 'MyReviews',
+ component: () => import('@/pages/user/reviews.vue'),
+ meta: { title: '我的评价', requiresAuth: true }
+ },
+ {
+ path: 'notifications',
+ name: 'Notifications',
+ component: () => import('@/pages/user/notifications.vue'),
+ meta: { title: '消息通知', requiresAuth: true }
+ },
+ {
+ path: 'groupbuying',
+ name: 'GroupBuying',
+ component: () => import('@/pages/groupbuying/index.vue'),
+ meta: { title: '拼团活动' }
+ },
+ {
+ path: 'groupbuying/:id',
+ name: 'GroupBuyingDetail',
+ component: () => import('@/pages/groupbuying/detail.vue'),
+ meta: { title: '拼团详情' }
+ },
+ {
+ path: 'groupbuying/group/:id',
+ name: 'GroupBuyingGroupDetail',
+ component: () => import('@/pages/groupbuying/group.vue'),
+ meta: { title: '团组详情', requiresAuth: true }
+ },
{
path: 'addresses',
name: 'Addresses',
@@ -123,6 +153,12 @@ const routes: RouteRecordRaw[] = [
component: () => import('@/pages/admin/flashsales.vue'),
meta: { title: '秒杀管理' }
},
+ {
+ path: 'groupbuying',
+ name: 'AdminGroupBuying',
+ component: () => import('@/pages/admin/groupbuying.vue'),
+ meta: { title: '拼团管理' }
+ },
{
path: 'orders',
name: 'AdminOrders',
diff --git a/flash-sale-frontend/src/stores/user.ts b/flash-sale-frontend/src/stores/user.ts
index b2f23d6..ce3c1dd 100644
--- a/flash-sale-frontend/src/stores/user.ts
+++ b/flash-sale-frontend/src/stores/user.ts
@@ -34,16 +34,32 @@ export const useUserStore = defineStore('user', () => {
if (res.success) {
token.value = res.data.token
user.value = res.data.user
-
- // 保存到localStorage
+
localStorage.setItem('token', token.value)
localStorage.setItem('user', JSON.stringify(user.value))
+
+ try {
+ const profile = await userApi.getInfo()
+ if (profile.success) {
+ user.value = {
+ ...profile.data,
+ avatar: profile.data.avatar || user.value?.avatar || '',
+ }
+ localStorage.setItem('user', JSON.stringify(user.value))
+ }
+ } catch (sessionError) {
+ console.error('登录成功但会话校验失败:', sessionError)
+ user.value = null
+ token.value = ''
+ localStorage.removeItem('token')
+ localStorage.removeItem('user')
+ ElMessage.error('登录成功但会话未建立,请检查 Cookie / 代理配置')
+ return false
+ }
ElMessage.success('登录成功')
-
- // 跳转到之前的页面或首页
const redirect = router.currentRoute.value.query.redirect as string
- router.push(redirect || '/')
+ await router.push(redirect || '/')
return true
}
diff --git a/flash-sale-frontend/src/styles/index.scss b/flash-sale-frontend/src/styles/index.scss
index 7105cb7..f2d47ae 100644
--- a/flash-sale-frontend/src/styles/index.scss
+++ b/flash-sale-frontend/src/styles/index.scss
@@ -2,49 +2,120 @@
@tailwind components;
@tailwind utilities;
-// 自定义变量
:root {
- --primary-color: #ef4444;
- --success-color: #10b981;
- --warning-color: #f59e0b;
- --danger-color: #ef4444;
- --info-color: #3b82f6;
+ --tone-0: #fffdf8;
+ --tone-50: #f7f2ea;
+ --tone-100: #efe7dc;
+ --tone-200: #d8cebf;
+ --tone-300: #c4b7a4;
+ --tone-400: #9a8b76;
+ --tone-500: #746855;
+ --tone-600: #5c5346;
+ --tone-700: #433d34;
+ --tone-800: #2d2a25;
+ --tone-900: #171614;
+ --surface-muted: #f4ede4;
+ --surface-raised: #fffaf2;
+ --line-soft: #d8cebf;
+ --line-strong: #171614;
+ --shadow-soft: 0 14px 34px rgba(23, 22, 20, 0.06);
+ --shadow-strong: 0 18px 40px rgba(23, 22, 20, 0.1);
+ --radius-xl: 24px;
+ --radius-lg: 20px;
+ --radius-md: 16px;
+
+ --primary-color: var(--tone-900);
+ --success-color: var(--tone-700);
+ --warning-color: var(--tone-600);
+ --danger-color: var(--tone-900);
+ --info-color: var(--tone-500);
+
+ --el-color-primary: var(--tone-900);
+ --el-color-primary-light-3: var(--tone-700);
+ --el-color-primary-light-5: var(--tone-600);
+ --el-color-primary-light-7: var(--tone-400);
+ --el-color-primary-light-8: var(--tone-300);
+ --el-color-primary-light-9: var(--tone-100);
+ --el-color-primary-dark-2: #0f0f0d;
+ --el-color-success: var(--tone-700);
+ --el-color-success-light-9: var(--tone-100);
+ --el-color-warning: var(--tone-600);
+ --el-color-warning-light-9: var(--tone-100);
+ --el-color-danger: var(--tone-900);
+ --el-color-danger-light-9: var(--tone-100);
+ --el-color-info: var(--tone-500);
+ --el-color-info-light-9: var(--tone-100);
+ --el-border-color: var(--line-soft);
+ --el-border-color-light: var(--line-soft);
+ --el-border-color-lighter: var(--tone-100);
+ --el-fill-color-light: var(--surface-muted);
+ --el-fill-color-blank: var(--tone-0);
+ --el-bg-color: var(--tone-0);
+ --el-bg-color-page: var(--tone-50);
+ --el-text-color-primary: var(--tone-900);
+ --el-text-color-regular: var(--tone-700);
+ --el-text-color-secondary: var(--tone-500);
+ --el-text-color-placeholder: var(--tone-400);
+ --el-mask-color: rgba(23, 23, 21, 0.52);
+ --el-box-shadow-light: var(--shadow-soft);
}
-// 全局样式重置
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
-body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
- 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
+html {
+ background: var(--tone-50);
+}
+
+body {
+ font-family: 'Avenir Next', 'Segoe UI Variable', 'PingFang SC', 'Hiragino Sans GB',
+ 'Microsoft YaHei', sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ color: var(--tone-900);
+ background:
+ radial-gradient(circle at top, rgba(255, 253, 248, 0.88), rgba(255, 253, 248, 0) 26%),
+ linear-gradient(180deg, var(--tone-50) 0%, #f2ebdf 100%);
+}
+
+#app {
+ min-height: 100vh;
+ background: transparent;
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+}
+
+button,
+input,
+textarea,
+select {
+ font: inherit;
}
-// 滚动条样式
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
- background: #f1f1f1;
+ background: var(--tone-50);
}
::-webkit-scrollbar-thumb {
- background: #888;
+ background: #b8ab90;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
- background: #555;
+ background: #8c7e6b;
}
-// 动画类
@keyframes shake {
0%, 100% {
transform: translateX(0);
@@ -61,34 +132,383 @@ body {
animation: shake 0.5s;
}
-// 工具类
.text-gradient {
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
+ color: var(--tone-900);
}
.card-shadow {
- box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
- transition: all 0.3s;
-
+ border: 1px solid var(--line-soft);
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow-soft);
+ transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease;
+
&:hover {
- box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.15);
+ border-color: var(--line-strong);
+ box-shadow: var(--shadow-strong);
transform: translateY(-2px);
}
}
-// Element Plus 样式覆盖
+:where(.el-button, .el-input__wrapper, .el-select__wrapper, .el-textarea__inner, .el-dialog,
+ .el-card, .el-popover, .el-message-box, .el-notification, .el-alert, .el-tag, .el-table,
+ .el-empty, .el-menu-item, .el-sub-menu__title, .el-radio-button__inner, .el-pagination button) {
+ transition: all 0.2s ease;
+}
+
+.el-button {
+ border-radius: 999px !important;
+ font-weight: 600;
+ box-shadow: none !important;
+ border-width: 1px !important;
+ border-style: solid !important;
+ border-color: var(--line-strong) !important;
+ background: var(--surface-raised) !important;
+ color: var(--tone-900) !important;
+}
+
+.el-button--primary,
.el-button--danger {
- background-color: var(--primary-color) !important;
- border-color: var(--primary-color) !important;
+ background-color: var(--surface-raised) !important;
+ border-color: var(--line-strong) !important;
+ color: var(--tone-900) !important;
}
-.el-message-box {
- border-radius: 8px;
+.el-button:hover,
+.el-button:focus,
+.el-button--primary:hover,
+.el-button--primary:focus,
+.el-button--danger:hover,
+.el-button--danger:focus {
+ background-color: var(--tone-900) !important;
+ border-color: var(--tone-900) !important;
+ color: #ffffff !important;
}
+.el-button.is-text,
+.el-button--text {
+ color: var(--tone-900) !important;
+ background: transparent !important;
+ border-color: transparent !important;
+ padding-left: 4px !important;
+ padding-right: 4px !important;
+}
+
+.el-button.is-text:hover,
+.el-button--text:hover {
+ background: transparent !important;
+ color: var(--tone-700) !important;
+}
+
+.el-button--default:hover,
+.el-button--default:focus {
+ background: var(--tone-50) !important;
+ color: var(--tone-900) !important;
+ border-color: var(--line-strong) !important;
+}
+
+.el-input__wrapper,
+.el-select__wrapper,
+.el-textarea__inner,
+.el-date-editor.el-input__wrapper,
+.el-date-editor .el-input__wrapper {
+ background: var(--surface-raised) !important;
+ border-radius: 14px !important;
+ box-shadow: inset 0 0 0 1px var(--line-soft) !important;
+}
+
+.el-input__wrapper.is-focus,
+.el-select__wrapper.is-focused,
+.el-textarea__inner:focus {
+ box-shadow: inset 0 0 0 1px var(--line-strong) !important;
+}
+
+.el-radio-button__inner {
+ border-radius: 14px !important;
+ border-color: var(--line-soft) !important;
+ color: var(--tone-900) !important;
+ background: var(--surface-raised) !important;
+ box-shadow: none !important;
+}
+
+.el-radio-button__original-radio:checked + .el-radio-button__inner {
+ background: var(--tone-0) !important;
+ border-color: var(--line-strong) !important;
+ color: var(--tone-900) !important;
+ box-shadow: inset 0 0 0 1px var(--tone-900) !important;
+}
+
+.el-card,
+.el-dialog,
+.el-popover,
+.el-message-box,
.el-notification {
- border-radius: 8px;
-}
\ No newline at end of file
+ border: 1px solid var(--line-soft) !important;
+ border-radius: var(--radius-lg) !important;
+ box-shadow: var(--shadow-soft) !important;
+ background: var(--surface-raised) !important;
+ overflow: hidden;
+}
+
+.el-dialog__header,
+.el-message-box__header {
+ margin: 0;
+ padding-bottom: 12px;
+}
+
+.el-dialog__body {
+ color: var(--tone-700);
+}
+
+.el-table {
+ --el-table-border-color: var(--line-soft);
+ --el-table-header-bg-color: var(--surface-muted);
+ --el-table-row-hover-bg-color: var(--tone-50);
+ overflow: hidden;
+ border-radius: var(--radius-md);
+ border: 1px solid var(--line-soft);
+ background: var(--surface-raised);
+}
+
+.el-table th.el-table__cell {
+ background: var(--surface-muted);
+ color: var(--tone-900);
+ font-weight: 600;
+}
+
+.el-table tr {
+ color: var(--tone-800);
+}
+
+.el-tag,
+.el-tag--success,
+.el-tag--warning,
+.el-tag--danger,
+.el-tag--info,
+.el-tag--primary {
+ background: var(--surface-raised) !important;
+ border-color: var(--line-soft) !important;
+ color: var(--tone-700) !important;
+ border-radius: 999px !important;
+}
+
+.el-alert,
+.el-alert--success,
+.el-alert--warning,
+.el-alert--error,
+.el-alert--info {
+ background: var(--surface-raised) !important;
+ border: 1px solid var(--line-soft) !important;
+ color: var(--tone-900) !important;
+}
+
+.el-tabs__item {
+ color: var(--tone-500) !important;
+}
+
+.el-tabs__item.is-active,
+.el-tabs__item:hover {
+ color: var(--tone-900) !important;
+}
+
+.el-tabs__active-bar {
+ background: var(--tone-900) !important;
+}
+
+.el-progress-bar__outer {
+ background: var(--surface-muted) !important;
+ box-shadow: inset 0 0 0 1px var(--line-soft) !important;
+}
+
+.el-progress-bar__inner {
+ background: var(--tone-900) !important;
+}
+
+.el-menu {
+ --el-menu-bg-color: transparent;
+ --el-menu-hover-bg-color: var(--surface-raised);
+ --el-menu-active-color: var(--tone-900);
+ border-right: none !important;
+}
+
+.el-sub-menu__title:hover,
+.el-menu-item:hover {
+ background: var(--surface-muted) !important;
+}
+
+.el-menu-item.is-active {
+ background: var(--surface-raised) !important;
+ color: var(--tone-900) !important;
+ box-shadow: inset 0 0 0 1px var(--line-strong);
+}
+
+.el-pagination {
+ --el-pagination-button-bg-color: transparent;
+ --el-pagination-hover-color: var(--tone-900);
+}
+
+.el-pagination .btn-prev,
+.el-pagination .btn-next,
+.el-pagination .el-pager li {
+ border-radius: 999px;
+ border: 1px solid var(--line-soft);
+ background: var(--surface-raised);
+}
+
+.el-pagination .el-pager li.is-active {
+ background: var(--surface-raised) !important;
+ color: var(--tone-900) !important;
+ font-weight: 700;
+ border-color: var(--line-strong);
+}
+
+.el-badge__content {
+ background: var(--surface-raised) !important;
+ border-color: var(--line-strong) !important;
+ color: var(--tone-900) !important;
+}
+
+.el-breadcrumb__inner.is-link,
+.el-breadcrumb__inner a {
+ color: var(--tone-500) !important;
+}
+
+.el-breadcrumb__inner {
+ color: var(--tone-700) !important;
+}
+
+.el-step__title.is-process,
+.el-step__title.is-finish,
+.el-step__icon.is-process,
+.el-step__icon.is-finish {
+ color: var(--tone-900) !important;
+ border-color: var(--tone-900) !important;
+}
+
+.el-step__head.is-process,
+.el-step__head.is-finish,
+.el-step__line-inner {
+ border-color: var(--tone-900) !important;
+ background-color: var(--tone-900) !important;
+}
+
+.el-empty__description p {
+ color: var(--tone-500) !important;
+}
+
+.el-rate__icon.is-active {
+ color: var(--tone-800) !important;
+}
+
+.text-red-500,
+.text-blue-500,
+.text-green-500,
+.text-orange-500,
+.text-purple-500,
+.text-pink-500,
+.text-rose-500,
+.text-yellow-500,
+.text-emerald-500,
+.text-blue-700,
+.text-blue-900 {
+ color: var(--tone-700) !important;
+}
+
+.bg-red-50,
+.bg-blue-50,
+.bg-yellow-50,
+.bg-orange-50,
+.bg-green-50,
+.bg-purple-50,
+.bg-pink-50,
+.bg-rose-50,
+.bg-emerald-50,
+.bg-blue-100,
+.bg-emerald-100,
+.bg-orange-100,
+.bg-rose-100 {
+ background-color: var(--surface-muted) !important;
+}
+
+.from-red-500,
+.from-orange-400,
+.from-green-400,
+.from-purple-400,
+.from-yellow-400,
+.from-blue-500,
+.from-blue-400,
+.from-pink-500 {
+ --tw-gradient-from: #ffffff var(--tw-gradient-from-position) !important;
+ --tw-gradient-to: rgb(255 255 255 / 0) var(--tw-gradient-to-position) !important;
+ --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to) !important;
+}
+
+.to-red-500,
+.to-blue-500,
+.to-pink-500,
+.to-orange-500,
+.to-blue-400,
+.to-orange-400 {
+ --tw-gradient-to: #ffffff var(--tw-gradient-to-position) !important;
+}
+
+.border-primary-500 {
+ border-color: var(--tone-900) !important;
+}
+
+.hover\:text-primary-500:hover,
+.hover\:text-blue-500:hover,
+.hover\:text-red-500:hover {
+ color: var(--tone-900) !important;
+}
+
+.mini-stat,
+.stat-card,
+.panel-card,
+.feature-card,
+.price-card,
+.note-card,
+.rules-card,
+.service-row,
+.business-item,
+.log-row,
+.brand-icon,
+.brand-tag,
+.cart-link,
+.user-trigger,
+.notification-center .notification-trigger,
+.discount-badge,
+.discount-pill,
+.time-block {
+ background: var(--surface-raised) !important;
+ color: var(--tone-900) !important;
+ border: 1px solid var(--line-soft) !important;
+ box-shadow: var(--shadow-soft) !important;
+ border-radius: var(--radius-lg) !important;
+ overflow: hidden;
+}
+
+.mini-stat__value,
+.mini-stat__label,
+.stat-value,
+.stat-label,
+.stat-desc,
+.panel-title,
+.panel-subtitle {
+ color: var(--tone-900) !important;
+ opacity: 1 !important;
+}
+
+.stat-icon {
+ background: var(--surface-muted) !important;
+ color: var(--tone-900) !important;
+ border: 1px solid var(--line-soft) !important;
+}
+
+.log-time {
+ color: var(--tone-500) !important;
+}
+
+.search-highlight {
+ color: var(--tone-900);
+ font-weight: 700;
+}
diff --git a/flash-sale-frontend/src/types/api.d.ts b/flash-sale-frontend/src/types/api.d.ts
index 73a52dd..750f910 100644
--- a/flash-sale-frontend/src/types/api.d.ts
+++ b/flash-sale-frontend/src/types/api.d.ts
@@ -155,4 +155,65 @@ export interface Statistics {
todaySales: number
activeFlashSales: number
onlineUsers: number
+}
+
+// 拼团活动类型
+export interface GroupBuying {
+ id: number
+ productId: number
+ productName: string
+ productImageUrl: string
+ productPrice: number
+ groupPrice: number
+ requiredMembers: number
+ durationMinutes: number
+ totalStock: number
+ remainingStock: number
+ maxPerUser: number
+ status: 'DRAFT' | 'UPCOMING' | 'ACTIVE' | 'ENDED'
+ statusDescription: string
+ startTime: string
+ endTime: string
+ createdAt: string
+ updatedAt: string
+ activeGroupCount: number
+ discount: number
+}
+
+// 拼团团组类型
+export interface GroupBuyingGroup {
+ id: number
+ groupNo: string
+ groupBuyingId: number
+ leaderUserId: number
+ leaderUsername: string
+ requiredMembers: number
+ currentMembers: number
+ status: 'FORMING' | 'SUCCESS' | 'FAILED'
+ statusDescription: string
+ expireTime: string
+ createdAt: string
+ completedAt?: string
+ members: GroupBuyingMember[]
+ groupBuying?: GroupBuying
+}
+
+// 拼团成员类型
+export interface GroupBuyingMember {
+ id: number
+ userId: number
+ username: string
+ avatar?: string
+ orderId?: number
+ status: number
+ joinedAt: string
+}
+
+// 拼团统计
+export interface GroupBuyingStatistics {
+ totalActivities: number
+ activeActivities: number
+ myGroups: number
+ successGroups: number
+ totalSaved: number
}
\ No newline at end of file
diff --git a/flash-sale-frontend/src/utils/normalizers.ts b/flash-sale-frontend/src/utils/normalizers.ts
index fdb4d51..02d261a 100644
--- a/flash-sale-frontend/src/utils/normalizers.ts
+++ b/flash-sale-frontend/src/utils/normalizers.ts
@@ -1,6 +1,8 @@
import type {
CartItem,
FlashSale,
+ GroupBuying,
+ GroupBuyingGroup,
Order,
OrderAddress,
PageResponse,
@@ -285,3 +287,69 @@ export const normalizeAdminProduct = (product: Record
): AdminProduc
viewCount: toNumber(product.viewCount),
rating: toNumber(product.rating),
})
+
+export const mapGroupBuyingStatus = (status: number | string): GroupBuying['status'] => {
+ const value = typeof status === 'string' ? status : toNumber(status)
+ if (value === 'DRAFT' || value === 0) return 'DRAFT'
+ if (value === 'UPCOMING' || value === 1) return 'UPCOMING'
+ if (value === 'ACTIVE' || value === 2) return 'ACTIVE'
+ if (value === 'ENDED' || value === 3) return 'ENDED'
+ return 'DRAFT'
+}
+
+export const mapGroupStatus = (status: number | string): GroupBuyingGroup['status'] => {
+ const value = typeof status === 'string' ? status : toNumber(status)
+ if (value === 'FORMING' || value === 1) return 'FORMING'
+ if (value === 'SUCCESS' || value === 2) return 'SUCCESS'
+ if (value === 'FAILED' || value === 3) return 'FAILED'
+ return 'FORMING'
+}
+
+export const normalizeGroupBuying = (gb: Record): GroupBuying => ({
+ id: toNumber(gb.id),
+ productId: toNumber(gb.productId),
+ productName: toString(gb.productName),
+ productImageUrl: resolveImageUrl(toString(gb.productImageUrl, '')),
+ productPrice: toNumber(gb.productPrice),
+ groupPrice: toNumber(gb.groupPrice),
+ requiredMembers: toNumber(gb.requiredMembers, 2),
+ durationMinutes: toNumber(gb.durationMinutes, 1440),
+ totalStock: toNumber(gb.totalStock),
+ remainingStock: toNumber(gb.remainingStock),
+ maxPerUser: toNumber(gb.maxPerUser, 1),
+ status: mapGroupBuyingStatus(gb.status),
+ statusDescription: toString(gb.statusDescription),
+ startTime: toIsoLikeString(gb.startTime),
+ endTime: toIsoLikeString(gb.endTime),
+ createdAt: toIsoLikeString(gb.createdAt),
+ updatedAt: toIsoLikeString(gb.updatedAt || gb.createdAt),
+ activeGroupCount: toNumber(gb.activeGroupCount),
+ discount: toNumber(gb.discount),
+})
+
+export const normalizeGroupBuyingGroup = (group: Record): GroupBuyingGroup => ({
+ id: toNumber(group.id),
+ groupNo: toString(group.groupNo),
+ groupBuyingId: toNumber(group.groupBuyingId),
+ leaderUserId: toNumber(group.leaderUserId),
+ leaderUsername: toString(group.leaderUsername),
+ requiredMembers: toNumber(group.requiredMembers, 2),
+ currentMembers: toNumber(group.currentMembers, 1),
+ status: mapGroupStatus(group.status),
+ statusDescription: toString(group.statusDescription),
+ expireTime: toIsoLikeString(group.expireTime),
+ createdAt: toIsoLikeString(group.createdAt),
+ completedAt: group.completedAt ? toIsoLikeString(group.completedAt) : undefined,
+ members: Array.isArray(group.members)
+ ? group.members.map((m: Record) => ({
+ id: toNumber(m.id),
+ userId: toNumber(m.userId),
+ username: toString(m.username),
+ avatar: resolveImageUrl(toString(m.avatar, '')),
+ orderId: m.orderId ? toNumber(m.orderId) : undefined,
+ status: toNumber(m.status),
+ joinedAt: toIsoLikeString(m.joinedAt),
+ }))
+ : [],
+ groupBuying: group.groupBuying ? normalizeGroupBuying(group.groupBuying) : undefined,
+})
diff --git a/flash-sale-frontend/tailwind.config.js b/flash-sale-frontend/tailwind.config.js
index f35f4ae..9c70ba1 100644
--- a/flash-sale-frontend/tailwind.config.js
+++ b/flash-sale-frontend/tailwind.config.js
@@ -8,16 +8,16 @@ export default {
extend: {
colors: {
primary: {
- 50: '#fef2f2',
- 100: '#fee2e2',
- 200: '#fecaca',
- 300: '#fca5a5',
- 400: '#f87171',
- 500: '#ef4444',
- 600: '#dc2626',
- 700: '#b91c1c',
- 800: '#991b1b',
- 900: '#7f1d1d',
+ 50: '#f7f7f6',
+ 100: '#efefed',
+ 200: '#dfdfdc',
+ 300: '#c6c6c2',
+ 400: '#9f9f99',
+ 500: '#7b7b74',
+ 600: '#5e5e58',
+ 700: '#44443f',
+ 800: '#2b2b27',
+ 900: '#171715',
},
},
animation: {
@@ -26,4 +26,4 @@ export default {
},
},
plugins: [],
-}
\ No newline at end of file
+}
diff --git a/flash-sale-frontend/vite.config.ts b/flash-sale-frontend/vite.config.ts
index d941e0b..2439f8a 100644
--- a/flash-sale-frontend/vite.config.ts
+++ b/flash-sale-frontend/vite.config.ts
@@ -31,6 +31,13 @@ export default defineConfig({
},
},
},
+ css: {
+ preprocessorOptions: {
+ scss: {
+ api: 'modern-compiler',
+ },
+ },
+ },
build: {
rollupOptions: {
output: {
diff --git a/src/main/java/com/org/flashsalesystem/FlashSaleSystemApplication.java b/src/main/java/com/org/flashsalesystem/FlashSaleSystemApplication.java
index 06d8c58..ad5f1ef 100644
--- a/src/main/java/com/org/flashsalesystem/FlashSaleSystemApplication.java
+++ b/src/main/java/com/org/flashsalesystem/FlashSaleSystemApplication.java
@@ -2,8 +2,10 @@ package com.org.flashsalesystem;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
+@EnableScheduling
public class FlashSaleSystemApplication {
public static void main(String[] args) {
diff --git a/src/main/java/com/org/flashsalesystem/config/RedissonConfig.java b/src/main/java/com/org/flashsalesystem/config/RedissonConfig.java
index 9f4b555..93a2bc8 100644
--- a/src/main/java/com/org/flashsalesystem/config/RedissonConfig.java
+++ b/src/main/java/com/org/flashsalesystem/config/RedissonConfig.java
@@ -292,4 +292,16 @@ public class RedissonConfig {
log.info("加载购物车操作Lua脚本");
return script;
}
+
+ /**
+ * 拼团库存扣减Lua脚本
+ */
+ @Bean
+ public DefaultRedisScript groupBuyingStockScript() {
+ DefaultRedisScript script = new DefaultRedisScript<>();
+ script.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/groupbuying_stock.lua")));
+ script.setResultType(Long.class);
+ log.info("加载拼团库存扣减Lua脚本");
+ return script;
+ }
}
diff --git a/src/main/java/com/org/flashsalesystem/controller/FlashSaleController.java b/src/main/java/com/org/flashsalesystem/controller/FlashSaleController.java
index 5dde93a..95f916f 100644
--- a/src/main/java/com/org/flashsalesystem/controller/FlashSaleController.java
+++ b/src/main/java/com/org/flashsalesystem/controller/FlashSaleController.java
@@ -150,6 +150,31 @@ public class FlashSaleController {
}
}
+ /**
+ * 获取秒杀活动统计信息
+ */
+ @GetMapping("/statistics")
+ public ResponseEntity