diff --git a/.deploy-tmp/check-fk.js b/.deploy-tmp/check-fk.js
new file mode 100644
index 0000000..fafc567
--- /dev/null
+++ b/.deploy-tmp/check-fk.js
@@ -0,0 +1,15 @@
+// One-off diagnostic — list FK constraints on orders.flash_sale_id
+// Run from packages/server so .env is loaded.
+const { PrismaClient } = require('@prisma/client')
+const p = new PrismaClient()
+;(async () => {
+ const rows = await p.$queryRawUnsafe(`
+ SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME IN ('orders', 'flash_sales', 'flash_sale_orders')
+ ORDER BY TABLE_NAME, ORDINAL_POSITION
+ `)
+ console.log('FK rows:', JSON.stringify(rows, null, 2))
+ await p.$disconnect()
+})().catch((e) => { console.error(e); process.exit(1) })
\ No newline at end of file
diff --git a/.deploy-tmp/resolve-migration.js b/.deploy-tmp/resolve-migration.js
new file mode 100644
index 0000000..eb27431
--- /dev/null
+++ b/.deploy-tmp/resolve-migration.js
@@ -0,0 +1,33 @@
+// Mark the failed drop_flash_sale migration as rolled_back so deploy can retry.
+const { PrismaClient } = require('@prisma/client')
+const p = new PrismaClient()
+const dump = (x) => JSON.stringify(x, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2)
+;(async () => {
+ const before = await p.$queryRawUnsafe(
+ `SELECT migration_name, finished_at, rolled_back_at, applied_steps_count
+ FROM _prisma_migrations
+ WHERE migration_name = ?`,
+ '20260910030338_drop_flash_sale'
+ )
+ console.log('before:', dump(before))
+
+ const result = await p.$executeRawUnsafe(
+ `UPDATE _prisma_migrations
+ SET rolled_back_at = NOW()
+ WHERE migration_name = ?
+ AND finished_at IS NULL
+ AND rolled_back_at IS NULL`,
+ '20260910030338_drop_flash_sale'
+ )
+ console.log('rows updated:', result)
+
+ const after = await p.$queryRawUnsafe(
+ `SELECT migration_name, finished_at, rolled_back_at, applied_steps_count
+ FROM _prisma_migrations
+ WHERE migration_name = ?`,
+ '20260910030338_drop_flash_sale'
+ )
+ console.log('after:', dump(after))
+
+ await p.$disconnect()
+})().catch((e) => { console.error(e); process.exit(1) })
\ No newline at end of file
diff --git a/.deploy-tmp/verify-migration.js b/.deploy-tmp/verify-migration.js
new file mode 100644
index 0000000..c109c88
--- /dev/null
+++ b/.deploy-tmp/verify-migration.js
@@ -0,0 +1,27 @@
+// Verify migration result on production
+const { PrismaClient } = require('@prisma/client')
+const p = new PrismaClient()
+;(async () => {
+ const tables = await p.$queryRawUnsafe(`
+ SELECT TABLE_NAME
+ FROM information_schema.TABLES
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME LIKE '%flash%'
+ `)
+ const ordersCol = await p.$queryRawUnsafe(`
+ SELECT COLUMN_NAME
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'orders'
+ AND COLUMN_NAME = 'flash_sale_id'
+ `)
+ const mig = await p.$queryRawUnsafe(`
+ SELECT migration_name, finished_at, rolled_back_at
+ FROM _prisma_migrations
+ WHERE migration_name = '20260910030338_drop_flash_sale'
+ `)
+ console.log('flash tables:', tables)
+ console.log('orders.flash_sale_id col:', ordersCol)
+ console.log('migration row:', mig)
+ await p.$disconnect()
+})().catch((e) => { console.error(e); process.exit(1) })
\ No newline at end of file
diff --git a/docs/flash-sale-removal.md b/docs/flash-sale-removal.md
new file mode 100644
index 0000000..bd39464
--- /dev/null
+++ b/docs/flash-sale-removal.md
@@ -0,0 +1,41 @@
+# 移除秒杀功能
+
+按业务调整下线限时秒杀活动。前后端代码、共享类型、Prisma schema 与迁移均已同步清理。
+
+## 清理范围
+
+| 层 | 内容 |
+| --- | --- |
+| 后端模块 | `packages/server/src/flash-sale/`(service、controller、admin controller、dto、测试) |
+| 调度任务 | `SchedulerService.handleExpireFlashSaleReservations` 及其依赖注入 |
+| 支付流程 | `PaymentService` 中 `flashSaleOrder` 标记为 PAID 的分支 |
+| 前端页面 | `pages/flash-sale/detail.vue`、`pages/admin/flash-sales.vue` |
+| 前端组件 | `components/FlashSaleSection.vue` |
+| 前端状态 | `stores/flash-sale.ts` |
+| 前端入口 | `pages/admin/index.vue` 秒杀管理菜单与对应样式 |
+| 首页 | `pages/home/index.vue` 中 `FlashSaleSection` 引用 |
+| 共享类型 | `shared/src/types/flash-sale.ts`,`enums.ts` 中 `FlashSaleStatus`、`FlashSaleOrderStatus`,以及 `index.ts` 的 re-export |
+| 数据库 | Prisma 删除 `FlashSale`、`FlashSaleOrder` 模型、相关枚举与 `Order.flash_sale_id` 字段 |
+
+## 数据库迁移
+
+新增 `packages/server/prisma/migrations/20260910030338_drop_flash_sale/migration.sql`。
+执行 `pnpm prisma migrate deploy` 后以下内容会被移除:
+
+- `flash_sales`、`flash_sale_orders` 两张表
+- `orders.flash_sale_id` 列与外键
+- `FlashSaleStatus`、`FlashSaleOrderStatus` 枚举
+
+迁移以 MySQL 方言编写;SQLite 开发环境由 Prisma shadow database 自动重建。
+
+## 验证
+
+```bash
+pnpm build:shared
+pnpm build:server
+pnpm test # 包含 payment / scheduler 套件
+```
+
+## 回退说明
+
+回退需要还原 schema 与源码,本项目不保留 git 自动恢复外的额外兜底。如需重新启用秒杀,按 git 历史恢复即可,并运行 `pnpm prisma migrate reset` 重新初始化数据库。
\ No newline at end of file
diff --git a/packages/app/src/components/FlashSaleSection.vue b/packages/app/src/components/FlashSaleSection.vue
deleted file mode 100644
index 807954f..0000000
--- a/packages/app/src/components/FlashSaleSection.vue
+++ /dev/null
@@ -1,185 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ phaseLabel(sale.phase) }}
-
-
-
-
- 距开始
- 剩余
-
- {{ getSaleCountdown(sale).h }}
- :
- {{ getSaleCountdown(sale).m }}
- :
- {{ getSaleCountdown(sale).s }}
-
-
-
-
-
-
- {{ sale.title }}
- {{ sale.cardType.name }}
-
-
-
-
- ¥
- {{ formatPrice(invite.price(sale.flashPrice)) }}
-
- 好友 95 折
- ¥{{ formatPrice(sale.originalPrice) }}
-
-
-
-
-
-
-
-
- {{ sale.phase === FlashSalePhase.SOLD_OUT ? '已售罄' : `剩 ${sale.remainingStock}/${sale.totalStock}` }}
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/app/src/pages.json b/packages/app/src/pages.json
index 9a6f730..7d5bec2 100644
--- a/packages/app/src/pages.json
+++ b/packages/app/src/pages.json
@@ -75,12 +75,6 @@
"navigationStyle": "custom"
}
},
- {
- "path": "pages/flash-sale/detail",
- "style": {
- "navigationStyle": "custom"
- }
- },
{
"path": "pages/profile/progress",
"style": {
@@ -171,12 +165,6 @@
"navigationStyle": "custom"
}
},
- {
- "path": "flash-sales",
- "style": {
- "navigationStyle": "custom"
- }
- },
{
"path": "member-progress",
"style": {
diff --git a/packages/app/src/pages/admin/flash-sales.vue b/packages/app/src/pages/admin/flash-sales.vue
deleted file mode 100644
index ccfaa47..0000000
--- a/packages/app/src/pages/admin/flash-sales.vue
+++ /dev/null
@@ -1,863 +0,0 @@
-
-
-
-
-
-
- 共 {{ total }} 个秒杀活动
-
- + 新建秒杀
-
-
-
-
-
-
-
-
-
-
- ◈
- 暂无秒杀活动,点击右上角新建
-
-
-
-
-
-
-
-
-
-
-
- 关联卡种: {{ item.cardType.name }}
-
-
-
-
- 秒杀价
- ¥{{ formatPrice(item.flashPrice) }}
-
-
- 原价
- ¥{{ formatPrice(item.originalPrice) }}
-
-
- 库存
- {{ item.soldCount }}/{{ item.totalStock }}
-
-
-
-
-
-
-
-
-
- {{ formatDateTime(item.startTime) }} — {{ formatDateTime(item.endTime) }}
-
-
-
-
-
-
- 编辑
-
-
- 上线
-
-
- 结束
-
-
- 删除
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 关联卡种
-
-
- {{ cardTypeOptions[form.cardTypeIdx]?.label || '请选择' }}
- ›
-
-
-
-
-
- 活动标题
-
-
-
-
- 原价(元)
-
-
-
-
- 秒杀价(元)
-
-
-
-
- 库存数量
-
-
-
-
- 开始时间
-
-
- {{ form.startDate || '选择日期' }}
-
-
- {{ form.startTimeStr || '选择时间' }}
-
-
-
-
-
- 结束时间
-
-
- {{ form.endDate || '选择日期' }}
-
-
- {{ form.endTimeStr || '选择时间' }}
-
-
-
-
-
- 排序值
-
-
-
-
- 活动说明
-
-
-
-
-
-
-
- 取消
-
-
- {{ submitting ? '保存中...' : '确认保存' }}
-
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/app/src/pages/admin/index.vue b/packages/app/src/pages/admin/index.vue
index b9ff792..c742d8f 100644
--- a/packages/app/src/pages/admin/index.vue
+++ b/packages/app/src/pages/admin/index.vue
@@ -127,21 +127,6 @@
›
-
-
-
-
- ◈
-
-
- 秒杀管理
- 创建和管理限时秒杀活动
-
-
-
- ›
-
-
@@ -386,7 +371,6 @@ onMounted(() => {
.icon--members { background: linear-gradient(135deg, $primary-color, $primary-dark); }
.icon--orders { background: linear-gradient(135deg, #7E9EC4, #6E8EB4); }
.icon--card { background: linear-gradient(135deg, #C48E7E, #B47E6E); }
-.icon--flash-sale { background: linear-gradient(135deg, #D4A59A, #C08B7E); }
.icon--studio { background: linear-gradient(135deg, #9E9E7E, #8E8E6E); }
.icon--subscribe { background: linear-gradient(135deg, #5D8C8A, #476D72); }
diff --git a/packages/app/src/pages/admin/stores/admin.ts b/packages/app/src/pages/admin/stores/admin.ts
index efc1cdd..181f455 100644
--- a/packages/app/src/pages/admin/stores/admin.ts
+++ b/packages/app/src/pages/admin/stores/admin.ts
@@ -15,9 +15,6 @@ import type {
PaginatedData,
ScheduleSlotPreview,
PublishDaySlotsDto,
- FlashSaleAdminItem,
- CreateFlashSaleDto,
- UpdateFlashSaleDto,
CreateStudioUploadCredentialDto,
StudioUploadCredential,
AdminMemberSummary,
@@ -272,26 +269,7 @@ export const useAdminStore = defineStore('admin', () => {
return get('/admin/stats')
}
- // ── Flash sales ─────────────────────────────────────────────────
- async function fetchFlashSales(params?: {
- page?: number
- limit?: number
- }): Promise> {
- return get>('/admin/flash-sales', params as Record)
- }
-
- async function createFlashSale(dto: CreateFlashSaleDto): Promise {
- return post('/admin/flash-sales', dto as unknown as Record)
- }
-
- async function updateFlashSale(id: string, dto: UpdateFlashSaleDto): Promise {
- return put(`/admin/flash-sales/${id}`, dto as unknown as Record)
- }
-
- async function deleteFlashSale(id: string): Promise<{ deleted: boolean }> {
- return del<{ deleted: boolean }>(`/admin/flash-sales/${id}`)
- }
-
+ // ── Teaching analytics ─────────────────────────────────────────
async function fetchTeachingAnalytics(month: string): Promise {
return get('/admin/teaching-analytics', { month })
}
@@ -339,10 +317,5 @@ export const useAdminStore = defineStore('admin', () => {
publishDaySlots,
// Stats
fetchDashboardStats,
- // Flash sales
- fetchFlashSales,
- createFlashSale,
- updateFlashSale,
- deleteFlashSale,
}
})
diff --git a/packages/app/src/pages/flash-sale/detail.vue b/packages/app/src/pages/flash-sale/detail.vue
deleted file mode 100644
index 67ea2e2..0000000
--- a/packages/app/src/pages/flash-sale/detail.vue
+++ /dev/null
@@ -1,848 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ◈
- 活动信息加载失败
-
- 点击重试
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ phaseLabel }}
-
-
-
- {{ detail.title }}
-
-
-
- ¥
- 好友 95 折
- {{ formatPrice(invite.price(detail.flashPrice)) }}
-
- 原价
- ¥{{ formatPrice(detail.originalPrice) }}
-
-
-
-
-
- 立省 ¥{{ formatPrice(detail.originalPrice - invite.price(detail.flashPrice)) }}
-
-
-
-
-
- {{ detail.phase === FlashSalePhase.UPCOMING ? '距开始' : '距结束' }}
-
-
- {{ countdown.h }}
- :
- {{ countdown.m }}
- :
- {{ countdown.s }}
-
-
-
-
-
-
-
- 抢购进度
-
- {{ detail.phase === FlashSalePhase.SOLD_OUT ? '已售罄' : `已抢 ${detail.soldCount}/${detail.totalStock}` }}
-
-
-
-
-
-
-
-
-
-
- 📱
-
- 提前授权手机号
- 授权后抢购更快,也方便馆主联系您
-
-
-
-
-
-
-
-
-
-
-
- {{ detail.cardType.name }}
- 卡种
-
-
- {{ detail.cardType.totalTimes }}
- 课时次数
-
-
- {{ detail.cardType.durationDays }}
- 有效天数
-
-
-
-
-
-
-
- {{ detail.description }}
-
-
-
-
-
-
- •
- 每位用户同一秒杀活动仅限参与一次
-
-
- •
- 购买后立即生效,有效期 {{ detail.cardType.durationDays }} 天
-
-
- •
- 共 {{ detail.cardType.totalTimes }} 次课时,可灵活预约
-
-
- •
- 需登录并授权手机号后方可参与秒杀
-
-
- •
- 建议提前完善账号信息及手机号授权,方便馆主联系
-
-
- •
- 秒杀卡不可退款,到期或课时用完后自动失效
-
-
- •
- 支持微信支付,安全便捷
-
-
- * 本活动最终解释权归普拉提馆所有
-
-
-
-
-
-
-
- 秒杀价
-
- ¥
- {{ formatPrice(invite.price(detail.flashPrice)) }}
-
-
-
- {{ actionBtnText }}
-
-
-
-
-
-
-
-
-
diff --git a/packages/app/src/pages/home/index.vue b/packages/app/src/pages/home/index.vue
index 2d32fff..0d40d66 100644
--- a/packages/app/src/pages/home/index.vue
+++ b/packages/app/src/pages/home/index.vue
@@ -13,7 +13,6 @@
-
@@ -33,7 +32,6 @@ import ReviewSummaryCard from '../../components/ReviewSummaryCard.vue'
import StudioInfo from '../../components/StudioInfo.vue'
import QuickEntry from '../../components/QuickEntry.vue'
import UpcomingBooking from '../../components/UpcomingBooking.vue'
-import FlashSaleSection from '../../components/FlashSaleSection.vue'
import CardShop from '../../components/CardShop.vue'
import AboutSection from '../../components/AboutSection.vue'
@@ -64,7 +62,6 @@ onShareTimeline(() => {
// ─── Layout ───────────────────────────────────────────────
const refreshing = ref(false)
const cardShopRef = ref | null>(null)
-const flashSaleRef = ref | null>(null)
const cardShopAnchorId = 'card-shop-anchor'
const scrollTarget = ref('')
const pageHeight = ref(`${uni.getWindowInfo().windowHeight}px`)
@@ -105,10 +102,9 @@ async function refreshData() {
await Promise.allSettled(tasks)
- // Also refresh card shop and flash sales
+ // Also refresh card shop
await Promise.allSettled([
cardShopRef.value?.fetchCardTypes(),
- flashSaleRef.value?.fetchFlashSales(),
])
}
diff --git a/packages/app/src/stores/flash-sale.ts b/packages/app/src/stores/flash-sale.ts
deleted file mode 100644
index d13269e..0000000
--- a/packages/app/src/stores/flash-sale.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { defineStore } from 'pinia'
-import { ref } from 'vue'
-import type {
- FlashSaleListItem,
- FlashSaleDetail,
- FlashSalePurchaseResponse,
-} from '@mp-pilates/shared'
-import { get, post } from '../utils/request'
-
-export const useFlashSaleStore = defineStore('flash-sale', () => {
- const flashSales = ref([])
- const loading = ref(false)
-
- async function fetchFlashSales(): Promise {
- loading.value = true
- try {
- const data = await get('/flash-sales')
- flashSales.value = [...data]
- return data
- } catch {
- flashSales.value = []
- return []
- } finally {
- loading.value = false
- }
- }
-
- async function fetchDetail(id: string): Promise {
- return get(`/flash-sales/${id}`)
- }
-
- async function purchase(id: string): Promise {
- return post(`/flash-sales/${id}/purchase`)
- }
-
- return {
- flashSales,
- loading,
- fetchFlashSales,
- fetchDetail,
- purchase,
- }
-})
diff --git a/packages/app/src/utils/format.ts b/packages/app/src/utils/format.ts
index 05685d5..a73eaaf 100644
--- a/packages/app/src/utils/format.ts
+++ b/packages/app/src/utils/format.ts
@@ -1,5 +1,4 @@
import { CardTypeCategory } from '@mp-pilates/shared'
-import { FlashSalePhase } from '@mp-pilates/shared'
/** Minimal membership shape needed by progress/usage helpers. */
interface MembershipLike {
@@ -128,17 +127,6 @@ export function getCountdownParts(targetTime: string): { readonly h: string; rea
}
}
-/** 秒杀阶段中文标签 */
-export function getFlashSalePhaseLabel(phase: FlashSalePhase): string {
- const map: Record = {
- [FlashSalePhase.UPCOMING]: '即将开始',
- [FlashSalePhase.ONGOING]: '抢购中',
- [FlashSalePhase.SOLD_OUT]: '已售罄',
- [FlashSalePhase.ENDED]: '已结束',
- }
- return map[phase]
-}
-
/** 库存已售比例 */
export function getStockRatio(soldCount: number, totalStock: number): number {
if (totalStock === 0) return 0
diff --git a/packages/server/prisma/migrations/20260910030338_drop_flash_sale/migration.sql b/packages/server/prisma/migrations/20260910030338_drop_flash_sale/migration.sql
new file mode 100644
index 0000000..1baf126
--- /dev/null
+++ b/packages/server/prisma/migrations/20260910030338_drop_flash_sale/migration.sql
@@ -0,0 +1,12 @@
+-- Drop flash sale feature
+-- See docs/flash-sale-removal.md for context
+--
+-- NOTE: production MySQL has no FK on orders.flash_sale_id, so we drop the
+-- column directly without a prior DROP FOREIGN KEY.
+
+-- Drop tables first
+DROP TABLE IF EXISTS `flash_sale_orders`;
+DROP TABLE IF EXISTS `flash_sales`;
+
+-- Remove order.flash_sale_id column
+ALTER TABLE `orders` DROP COLUMN `flash_sale_id`;
\ No newline at end of file
diff --git a/packages/server/prisma/schema.prisma b/packages/server/prisma/schema.prisma
index 195950a..f5d6800 100644
--- a/packages/server/prisma/schema.prisma
+++ b/packages/server/prisma/schema.prisma
@@ -51,18 +51,6 @@ enum OrderStatus {
REFUNDED
}
-enum FlashSaleStatus {
- DRAFT
- ACTIVE
- ENDED
-}
-
-enum FlashSaleOrderStatus {
- RESERVED
- PAID
- EXPIRED
-}
-
enum InviteReferralStatus {
REGISTERED
TRIAL_PURCHASED
@@ -92,7 +80,6 @@ model User {
memberships Membership[]
bookings Booking[]
orders Order[]
- flashSaleOrders FlashSaleOrder[]
subscriptionMessageConsents SubscriptionMessageConsent[]
sentInviteReferrals InviteReferral[] @relation("InviteReferralInviter")
receivedInviteReferral InviteReferral[] @relation("InviteReferralInvitee")
@@ -143,7 +130,6 @@ model CardType {
memberships Membership[]
orders Order[]
- flashSales FlashSale[]
@@map("card_types")
}
@@ -269,14 +255,12 @@ model Order {
status OrderStatus @default(PENDING)
wxTransactionId String? @map("wx_transaction_id")
paidAt DateTime? @map("paid_at")
- flashSaleId String? @map("flash_sale_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id])
cardType CardType @relation(fields: [cardTypeId], references: [id])
membership Membership? @relation(fields: [membershipId], references: [id])
- flashSaleOrder FlashSaleOrder?
inviteReferrals InviteReferral[]
@@index([userId])
@@ -342,51 +326,6 @@ model StudioConfig {
@@map("studio_config")
}
-model FlashSale {
- id String @id @default(uuid())
- cardTypeId String @map("card_type_id")
- title String
- originalPrice Decimal @map("original_price") @db.Decimal(10, 0)
- flashPrice Decimal @map("flash_price") @db.Decimal(10, 0)
- totalStock Int @map("total_stock")
- soldCount Int @default(0) @map("sold_count")
- startTime DateTime @map("start_time")
- endTime DateTime @map("end_time")
- status FlashSaleStatus @default(DRAFT)
- description String? @db.Text
- sortOrder Int @default(0) @map("sort_order")
- createdAt DateTime @default(now()) @map("created_at")
- updatedAt DateTime @updatedAt @map("updated_at")
-
- cardType CardType @relation(fields: [cardTypeId], references: [id])
- orders FlashSaleOrder[]
-
- @@index([status, startTime, endTime])
- @@map("flash_sales")
-}
-
-model FlashSaleOrder {
- id String @id @default(uuid())
- flashSaleId String @map("flash_sale_id")
- userId String @map("user_id")
- orderId String? @unique @map("order_id")
- status FlashSaleOrderStatus @default(RESERVED)
- reservedAt DateTime @default(now()) @map("reserved_at")
- paidAt DateTime? @map("paid_at")
- expiredAt DateTime? @map("expired_at")
- createdAt DateTime @default(now()) @map("created_at")
- updatedAt DateTime @updatedAt @map("updated_at")
-
- flashSale FlashSale @relation(fields: [flashSaleId], references: [id])
- user User @relation(fields: [userId], references: [id])
- order Order? @relation(fields: [orderId], references: [id])
-
- @@unique([flashSaleId, userId])
- @@index([userId])
- @@index([status])
- @@map("flash_sale_orders")
-}
-
// Historical totals without fabricated scheduled dates.
model LessonSupplement {
id String @id @default(uuid())
diff --git a/packages/server/src/app.module.ts b/packages/server/src/app.module.ts
index 71f7aef..f8f1727 100644
--- a/packages/server/src/app.module.ts
+++ b/packages/server/src/app.module.ts
@@ -11,7 +11,6 @@ import { BookingModule } from './booking/booking.module'
import { SchedulerModule } from './scheduler/scheduler.module'
import { PaymentModule } from './payment/payment.module'
import { AdminModule } from './admin/admin.module'
-import { FlashSaleModule } from './flash-sale/flash-sale.module'
import { InviteModule } from './invite/invite.module'
@Module({
@@ -30,7 +29,6 @@ import { InviteModule } from './invite/invite.module'
SchedulerModule,
PaymentModule,
AdminModule,
- FlashSaleModule,
InviteModule,
],
controllers: [AppController],
diff --git a/packages/server/src/flash-sale/__tests__/flash-sale-invite.spec.ts b/packages/server/src/flash-sale/__tests__/flash-sale-invite.spec.ts
deleted file mode 100644
index 80e2e5e..0000000
--- a/packages/server/src/flash-sale/__tests__/flash-sale-invite.spec.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { FlashSaleService } from '../flash-sale.service'
-import { PrismaService } from '../../prisma/prisma.service'
-import { WechatPayService } from '../../payment/wechat-pay.service'
-
-describe('Flash sale referral pricing', () => {
- it.each([true, false])('snapshots referral and category with eligible=%s', async (eligible) => {
- const prisma = {
- user: { findUnique: jest.fn().mockResolvedValue({ id: 'buyer', phone: '123', openid: 'openid' }) },
- flashSale: {
- findUnique: jest.fn().mockResolvedValue({ id: 'sale', title: 'sale', cardTypeId: 'card', cardType: { type: 'TIMES' }, status: 'ACTIVE', startTime: new Date(0), endTime: new Date(Date.now() + 60000), totalStock: 10, flashPrice: 999 }),
- updateMany: jest.fn().mockResolvedValue({ count: 1 }),
- },
- inviteReferral: { findUnique: jest.fn().mockResolvedValue(eligible ? { inviterId: 'friend' } : null) },
- order: { create: jest.fn().mockImplementation(({ data }) => ({ ...data, id: 'order' })) },
- flashSaleOrder: { create: jest.fn().mockResolvedValue({ id: 'reserved' }) },
- $transaction: jest.fn(),
- }
- prisma.$transaction.mockImplementation((fn) => fn(prisma))
- const pay = { createUnifiedOrder: jest.fn().mockResolvedValue({}) }
- const service = new FlashSaleService(prisma as unknown as PrismaService, pay as unknown as WechatPayService)
- await service.purchase('sale', 'buyer')
- expect(prisma.order.create).toHaveBeenCalledWith({ data: expect.objectContaining({ amount: eligible ? 949 : 999, inviteInviterId: eligible ? 'friend' : undefined, purchasedCategory: 'TIMES' }) })
- expect(pay.createUnifiedOrder).toHaveBeenCalledWith(expect.objectContaining({ amount: eligible ? 949 : 999 }))
- })
-})
diff --git a/packages/server/src/flash-sale/dto/create-flash-sale.dto.ts b/packages/server/src/flash-sale/dto/create-flash-sale.dto.ts
deleted file mode 100644
index a9da993..0000000
--- a/packages/server/src/flash-sale/dto/create-flash-sale.dto.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { IsUUID, IsString, IsInt, IsDateString, IsOptional, Min, IsNumber } from 'class-validator'
-
-export class CreateFlashSaleDto {
- @IsUUID()
- cardTypeId!: string
-
- @IsString()
- title!: string
-
- @IsNumber()
- @Min(1)
- originalPrice!: number
-
- @IsNumber()
- @Min(1)
- flashPrice!: number
-
- @IsInt()
- @Min(1)
- totalStock!: number
-
- @IsDateString()
- startTime!: string
-
- @IsDateString()
- endTime!: string
-
- @IsOptional()
- @IsString()
- description?: string
-
- @IsOptional()
- @IsInt()
- sortOrder?: number
-}
diff --git a/packages/server/src/flash-sale/dto/update-flash-sale.dto.ts b/packages/server/src/flash-sale/dto/update-flash-sale.dto.ts
deleted file mode 100644
index a2c2a22..0000000
--- a/packages/server/src/flash-sale/dto/update-flash-sale.dto.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { IsString, IsInt, IsDateString, IsOptional, Min, IsNumber, IsEnum } from 'class-validator'
-import { FlashSaleStatus } from '@mp-pilates/shared'
-
-export class UpdateFlashSaleDto {
- @IsOptional()
- @IsString()
- title?: string
-
- @IsOptional()
- @IsNumber()
- @Min(1)
- originalPrice?: number
-
- @IsOptional()
- @IsNumber()
- @Min(1)
- flashPrice?: number
-
- @IsOptional()
- @IsInt()
- @Min(1)
- totalStock?: number
-
- @IsOptional()
- @IsDateString()
- startTime?: string
-
- @IsOptional()
- @IsDateString()
- endTime?: string
-
- @IsOptional()
- @IsString()
- description?: string
-
- @IsOptional()
- @IsEnum(FlashSaleStatus)
- status?: FlashSaleStatus
-
- @IsOptional()
- @IsInt()
- sortOrder?: number
-}
diff --git a/packages/server/src/flash-sale/flash-sale-admin.controller.ts b/packages/server/src/flash-sale/flash-sale-admin.controller.ts
deleted file mode 100644
index b4706c9..0000000
--- a/packages/server/src/flash-sale/flash-sale-admin.controller.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import {
- Body,
- Controller,
- Delete,
- Get,
- Param,
- Post,
- Put,
- Query,
- UseGuards,
- ValidationPipe,
-} from '@nestjs/common'
-import { UserRole } from '@mp-pilates/shared'
-import { JwtAuthGuard } from '../auth/jwt-auth.guard'
-import { RolesGuard } from '../auth/roles.guard'
-import { Roles } from '../auth/roles.decorator'
-import { FlashSaleService } from './flash-sale.service'
-import { CreateFlashSaleDto } from './dto/create-flash-sale.dto'
-import { UpdateFlashSaleDto } from './dto/update-flash-sale.dto'
-
-@Controller('admin/flash-sales')
-@UseGuards(JwtAuthGuard, RolesGuard)
-@Roles(UserRole.ADMIN)
-export class FlashSaleAdminController {
- constructor(private readonly flashSaleService: FlashSaleService) {}
-
- /** POST /admin/flash-sales — create */
- @Post()
- create(
- @Body(new ValidationPipe({ whitelist: true })) dto: CreateFlashSaleDto,
- ) {
- return this.flashSaleService.createFlashSale(dto)
- }
-
- /** GET /admin/flash-sales — list (paginated) */
- @Get()
- list(
- @Query('page') page?: string,
- @Query('limit') limit?: string,
- ) {
- return this.flashSaleService.getAdminFlashSales(
- page ? parseInt(page, 10) : 1,
- limit ? parseInt(limit, 10) : 20,
- )
- }
-
- /** GET /admin/flash-sales/:id — detail */
- @Get(':id')
- detail(@Param('id') id: string) {
- return this.flashSaleService.getFlashSaleDetail(id)
- }
-
- /** PUT /admin/flash-sales/:id — update */
- @Put(':id')
- update(
- @Param('id') id: string,
- @Body(new ValidationPipe({ whitelist: true })) dto: UpdateFlashSaleDto,
- ) {
- return this.flashSaleService.updateFlashSale(id, dto)
- }
-
- /** DELETE /admin/flash-sales/:id — delete (DRAFT only) */
- @Delete(':id')
- remove(@Param('id') id: string) {
- return this.flashSaleService.deleteFlashSale(id)
- }
-}
diff --git a/packages/server/src/flash-sale/flash-sale.controller.ts b/packages/server/src/flash-sale/flash-sale.controller.ts
deleted file mode 100644
index 744c807..0000000
--- a/packages/server/src/flash-sale/flash-sale.controller.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import {
- Controller,
- Get,
- Param,
- Post,
- UseGuards,
-} from '@nestjs/common'
-import { JwtAuthGuard } from '../auth/jwt-auth.guard'
-import { CurrentUser } from '../common/decorators/current-user.decorator'
-import { FlashSaleService } from './flash-sale.service'
-
-@Controller('flash-sales')
-export class FlashSaleController {
- constructor(private readonly flashSaleService: FlashSaleService) {}
-
- /** GET /flash-sales — list active/upcoming (public) */
- @Get()
- getActiveFlashSales() {
- return this.flashSaleService.getActiveFlashSales()
- }
-
- /** GET /flash-sales/:id — detail (optionally authenticated) */
- @Get(':id')
- getFlashSaleDetail(
- @Param('id') id: string,
- @CurrentUser('sub') userId?: string,
- ) {
- return this.flashSaleService.getFlashSaleDetail(id, userId)
- }
-
- /** POST /flash-sales/:id/purchase — requires auth */
- @Post(':id/purchase')
- @UseGuards(JwtAuthGuard)
- purchase(
- @Param('id') flashSaleId: string,
- @CurrentUser('sub') userId: string,
- ) {
- return this.flashSaleService.purchase(flashSaleId, userId)
- }
-}
diff --git a/packages/server/src/flash-sale/flash-sale.module.ts b/packages/server/src/flash-sale/flash-sale.module.ts
deleted file mode 100644
index b5906fb..0000000
--- a/packages/server/src/flash-sale/flash-sale.module.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { Module } from '@nestjs/common'
-import { PrismaModule } from '../prisma/prisma.module'
-import { PaymentModule } from '../payment/payment.module'
-import { FlashSaleService } from './flash-sale.service'
-import { FlashSaleController } from './flash-sale.controller'
-import { FlashSaleAdminController } from './flash-sale-admin.controller'
-
-@Module({
- imports: [PrismaModule, PaymentModule],
- controllers: [FlashSaleController, FlashSaleAdminController],
- providers: [FlashSaleService],
- exports: [FlashSaleService],
-})
-export class FlashSaleModule {}
diff --git a/packages/server/src/flash-sale/flash-sale.service.ts b/packages/server/src/flash-sale/flash-sale.service.ts
deleted file mode 100644
index 5d455b5..0000000
--- a/packages/server/src/flash-sale/flash-sale.service.ts
+++ /dev/null
@@ -1,412 +0,0 @@
-import {
- BadRequestException,
- ConflictException,
- Injectable,
- Logger,
- NotFoundException,
-} from '@nestjs/common'
-import { Prisma } from '@prisma/client'
-import {
- FlashSaleStatus,
- FlashSaleOrderStatus,
- MembershipStatus,
- OrderStatus,
-} from '@mp-pilates/shared'
-import { FlashSalePhase } from '@mp-pilates/shared'
-import { PrismaService } from '../prisma/prisma.service'
-import { WechatPayService, WxPaymentParams } from '../payment/wechat-pay.service'
-import { CreateFlashSaleDto } from './dto/create-flash-sale.dto'
-import { UpdateFlashSaleDto } from './dto/update-flash-sale.dto'
-
-// ── Helpers ─────────────────────────────────────────────────
-function computePhase(sale: {
- startTime: Date
- endTime: Date
- soldCount: number
- totalStock: number
- status: string
-}): FlashSalePhase {
- if (sale.status === FlashSaleStatus.ENDED) return FlashSalePhase.ENDED
- const now = new Date()
- if (now < sale.startTime) return FlashSalePhase.UPCOMING
- if (now > sale.endTime) return FlashSalePhase.ENDED
- if (sale.soldCount >= sale.totalStock) return FlashSalePhase.SOLD_OUT
- return FlashSalePhase.ONGOING
-}
-
-function toNumber(val: Prisma.Decimal | number): number {
- return typeof val === 'number' ? val : Number(val)
-}
-
-// ── Service ─────────────────────────────────────────────────
-
-@Injectable()
-export class FlashSaleService {
- private readonly logger = new Logger(FlashSaleService.name)
-
- constructor(
- private readonly prisma: PrismaService,
- private readonly wechatPayService: WechatPayService,
- ) {}
-
- // ═══════════════════════════════════════════════════════════
- // USER: List active/upcoming flash sales
- // ═══════════════════════════════════════════════════════════
- async getActiveFlashSales() {
- const sales = await this.prisma.flashSale.findMany({
- where: {
- status: FlashSaleStatus.ACTIVE,
- endTime: { gt: new Date() },
- },
- include: {
- cardType: {
- select: { name: true, type: true, totalTimes: true, durationDays: true },
- },
- },
- orderBy: [{ sortOrder: 'asc' }, { startTime: 'asc' }],
- })
-
- return sales.map((s) => ({
- ...s,
- originalPrice: toNumber(s.originalPrice),
- flashPrice: toNumber(s.flashPrice),
- phase: computePhase(s),
- remainingStock: s.totalStock - s.soldCount,
- cardType: s.cardType,
- }))
- }
-
- // ═══════════════════════════════════════════════════════════
- // USER: Get detail (with participation check)
- // ═══════════════════════════════════════════════════════════
- async getFlashSaleDetail(id: string, userId?: string) {
- const sale = await this.prisma.flashSale.findUnique({
- where: { id },
- include: {
- cardType: {
- select: {
- name: true,
- type: true,
- totalTimes: true,
- durationDays: true,
- description: true,
- },
- },
- },
- })
- if (!sale) throw new NotFoundException('秒杀活动不存在')
-
- let hasParticipated = false
- let userOrderStatus: FlashSaleOrderStatus | null = null
-
- if (userId) {
- const existing = await this.prisma.flashSaleOrder.findUnique({
- where: { flashSaleId_userId: { flashSaleId: id, userId } },
- })
- if (existing && existing.status !== FlashSaleOrderStatus.EXPIRED) {
- hasParticipated = true
- userOrderStatus = existing.status as FlashSaleOrderStatus
- }
- }
-
- return {
- ...sale,
- originalPrice: toNumber(sale.originalPrice),
- flashPrice: toNumber(sale.flashPrice),
- phase: computePhase(sale),
- remainingStock: sale.totalStock - sale.soldCount,
- cardType: { ...sale.cardType },
- hasParticipated,
- userOrderStatus,
- serverTime: new Date().toISOString(),
- }
- }
-
- // ═══════════════════════════════════════════════════════════
- // PURCHASE — Atomic stock deduction
- // ═══════════════════════════════════════════════════════════
- async purchase(flashSaleId: string, userId: string) {
- // ① Pre-validate (fast-fail before transaction)
- const user = await this.prisma.user.findUnique({ where: { id: userId } })
- if (!user) throw new NotFoundException('用户不存在')
- if (!user.phone) throw new BadRequestException('请先授权手机号后再参与秒杀')
-
- const sale = await this.prisma.flashSale.findUnique({
- where: { id: flashSaleId },
- include: { cardType: true },
- })
- if (!sale) throw new NotFoundException('秒杀活动不存在')
- if (sale.status !== FlashSaleStatus.ACTIVE) {
- throw new BadRequestException('秒杀活动未上线')
- }
-
- const now = new Date()
- if (now < sale.startTime) throw new BadRequestException('秒杀尚未开始')
- if (now > sale.endTime) throw new BadRequestException('秒杀已结束')
-
- // ② Atomic transaction: reserve stock + create FlashSaleOrder + create Order
- let result: { order: { id: string; orderNo: string; amount: Prisma.Decimal }; flashSaleOrderId: string }
-
- try {
- result = await this.prisma.$transaction(async (tx) => {
- // ②-a: CAS optimistic lock stock deduction
- const updated = await tx.flashSale.updateMany({
- where: {
- id: flashSaleId,
- soldCount: { lt: sale.totalStock },
- },
- data: {
- soldCount: { increment: 1 },
- },
- })
-
- if (updated.count === 0) {
- throw new BadRequestException('手慢了,已售罄')
- }
-
- // ②-b: Create Order with flash sale price
- const orderNo = `FS${Date.now()}${Math.random().toString(36).substring(2, 8)}`
-
- const referral = await tx.inviteReferral.findUnique({ where: { inviteeId: userId } })
- const order = await tx.order.create({
- data: {
- userId,
- cardTypeId: sale.cardTypeId,
- orderNo,
- amount: referral ? Math.round(Number(sale.flashPrice) * 95 / 100) : sale.flashPrice,
- inviteInviterId: referral?.inviterId,
- purchasedCategory: sale.cardType.type,
- status: OrderStatus.PENDING,
- flashSaleId,
- },
- })
-
- // ②-c: Create FlashSaleOrder (unique constraint prevents duplicate)
- const flashSaleOrder = await tx.flashSaleOrder.create({
- data: {
- flashSaleId,
- userId,
- orderId: order.id,
- status: FlashSaleOrderStatus.RESERVED,
- },
- })
-
- return {
- order: { id: order.id, orderNo: order.orderNo, amount: order.amount },
- flashSaleOrderId: flashSaleOrder.id,
- }
- })
- } catch (err) {
- // Handle unique constraint violation (user already participated)
- if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === 'P2002') {
- throw new ConflictException('您已参与过此秒杀活动')
- }
- throw err
- }
-
- // ③ Create WeChat unified order (outside transaction — network call)
- const paymentParams = await this.wechatPayService.createUnifiedOrder({
- orderNo: result.order.orderNo,
- amount: toNumber(result.order.amount),
- openid: user.openid,
- description: `秒杀-${sale.title}`,
- })
-
- return {
- flashSaleOrderId: result.flashSaleOrderId,
- order: {
- id: result.order.id,
- orderNo: result.order.orderNo,
- amount: toNumber(result.order.amount),
- },
- paymentParams,
- }
- }
-
- // ═══════════════════════════════════════════════════════════
- // ADMIN: Create flash sale
- // ═══════════════════════════════════════════════════════════
- async createFlashSale(dto: CreateFlashSaleDto) {
- const cardType = await this.prisma.cardType.findUnique({
- where: { id: dto.cardTypeId },
- })
- if (!cardType) throw new NotFoundException('卡种不存在')
-
- const startTime = new Date(dto.startTime)
- const endTime = new Date(dto.endTime)
- if (endTime <= startTime) {
- throw new BadRequestException('结束时间必须晚于开始时间')
- }
-
- const sale = await this.prisma.flashSale.create({
- data: {
- cardTypeId: dto.cardTypeId,
- title: dto.title,
- originalPrice: dto.originalPrice,
- flashPrice: dto.flashPrice,
- totalStock: dto.totalStock,
- startTime,
- endTime,
- description: dto.description ?? null,
- sortOrder: dto.sortOrder ?? 0,
- status: FlashSaleStatus.DRAFT,
- },
- include: {
- cardType: { select: { name: true, type: true } },
- },
- })
-
- return {
- ...sale,
- originalPrice: toNumber(sale.originalPrice),
- flashPrice: toNumber(sale.flashPrice),
- phase: computePhase(sale),
- cardType: { ...sale.cardType },
- }
- }
-
- // ═══════════════════════════════════════════════════════════
- // ADMIN: Update flash sale
- // ═══════════════════════════════════════════════════════════
- async updateFlashSale(id: string, dto: UpdateFlashSaleDto) {
- const existing = await this.prisma.flashSale.findUnique({ where: { id } })
- if (!existing) throw new NotFoundException('秒杀活动不存在')
-
- const data: Record = {}
- if (dto.title !== undefined) data.title = dto.title
- if (dto.originalPrice !== undefined) data.originalPrice = dto.originalPrice
- if (dto.flashPrice !== undefined) data.flashPrice = dto.flashPrice
- if (dto.totalStock !== undefined) {
- if (dto.totalStock < existing.soldCount) {
- throw new BadRequestException('库存不能小于已售数量')
- }
- data.totalStock = dto.totalStock
- }
- if (dto.startTime !== undefined) data.startTime = new Date(dto.startTime)
- if (dto.endTime !== undefined) data.endTime = new Date(dto.endTime)
- if (dto.description !== undefined) data.description = dto.description
- if (dto.status !== undefined) data.status = dto.status
- if (dto.sortOrder !== undefined) data.sortOrder = dto.sortOrder
-
- const sale = await this.prisma.flashSale.update({
- where: { id },
- data,
- include: {
- cardType: { select: { name: true, type: true } },
- },
- })
-
- return {
- ...sale,
- originalPrice: toNumber(sale.originalPrice),
- flashPrice: toNumber(sale.flashPrice),
- phase: computePhase(sale),
- cardType: { ...sale.cardType },
- }
- }
-
- // ═══════════════════════════════════════════════════════════
- // ADMIN: Delete flash sale (only DRAFT)
- // ═══════════════════════════════════════════════════════════
- async deleteFlashSale(id: string) {
- const existing = await this.prisma.flashSale.findUnique({ where: { id } })
- if (!existing) throw new NotFoundException('秒杀活动不存在')
-
- if (existing.soldCount > 0) {
- throw new BadRequestException('已有用户参与,无法删除,请结束活动')
- }
-
- await this.prisma.flashSale.delete({ where: { id } })
- return { deleted: true }
- }
-
- // ═══════════════════════════════════════════════════════════
- // ADMIN: List all flash sales (paginated)
- // ═══════════════════════════════════════════════════════════
- async getAdminFlashSales(page = 1, limit = 20) {
- const skip = (page - 1) * limit
-
- const [data, total] = await Promise.all([
- this.prisma.flashSale.findMany({
- include: {
- cardType: { select: { name: true, type: true } },
- },
- orderBy: { createdAt: 'desc' },
- skip,
- take: limit,
- }),
- this.prisma.flashSale.count(),
- ])
-
- return {
- data: data.map((s) => ({
- ...s,
- originalPrice: toNumber(s.originalPrice),
- flashPrice: toNumber(s.flashPrice),
- phase: computePhase(s),
- cardType: s.cardType,
- })),
- total,
- page,
- limit,
- }
- }
-
- // ═══════════════════════════════════════════════════════════
- // SCHEDULER: Expire unpaid reservations (release stock)
- // ═══════════════════════════════════════════════════════════
- async expireUnpaidReservations(expireMinutes = 15): Promise {
- const cutoff = new Date(Date.now() - expireMinutes * 60_000)
-
- const expiredOrders = await this.prisma.flashSaleOrder.findMany({
- where: {
- status: FlashSaleOrderStatus.RESERVED,
- reservedAt: { lt: cutoff },
- },
- })
-
- if (expiredOrders.length === 0) return 0
-
- // Group by flashSaleId to batch stock release
- const stockDecrements = new Map()
- const orderIds: string[] = []
- const flashSaleOrderIds: string[] = []
-
- for (const fo of expiredOrders) {
- flashSaleOrderIds.push(fo.id)
- stockDecrements.set(fo.flashSaleId, (stockDecrements.get(fo.flashSaleId) ?? 0) + 1)
- if (fo.orderId) orderIds.push(fo.orderId)
- }
-
- try {
- await this.prisma.$transaction([
- // Batch mark all as expired
- this.prisma.flashSaleOrder.updateMany({
- where: { id: { in: flashSaleOrderIds } },
- data: { status: FlashSaleOrderStatus.EXPIRED, expiredAt: new Date() },
- }),
- // Release stock per flash sale
- ...Array.from(stockDecrements.entries()).map(([flashSaleId, count]) =>
- this.prisma.flashSale.update({
- where: { id: flashSaleId },
- data: { soldCount: { decrement: count } },
- }),
- ),
- // Cancel associated payment orders
- ...(orderIds.length > 0
- ? [
- this.prisma.order.updateMany({
- where: { id: { in: orderIds } },
- data: { status: OrderStatus.REFUNDED },
- }),
- ]
- : []),
- ])
- } catch (err) {
- this.logger.error('Failed to batch-expire flash sale orders', err)
- return 0
- }
-
- return expiredOrders.length
- }
-}
diff --git a/packages/server/src/payment/__tests__/payment.service.spec.ts b/packages/server/src/payment/__tests__/payment.service.spec.ts
index b3c9898..af3536b 100644
--- a/packages/server/src/payment/__tests__/payment.service.spec.ts
+++ b/packages/server/src/payment/__tests__/payment.service.spec.ts
@@ -90,9 +90,6 @@ function buildPrismaMock() {
findFirst: jest.fn(),
update: jest.fn(),
},
- flashSaleOrder: {
- updateMany: jest.fn(),
- },
$transaction: jest.fn(),
}
}
diff --git a/packages/server/src/payment/payment.service.ts b/packages/server/src/payment/payment.service.ts
index fbf9d9e..b1ccf1c 100644
--- a/packages/server/src/payment/payment.service.ts
+++ b/packages/server/src/payment/payment.service.ts
@@ -5,7 +5,7 @@ import {
NotFoundException,
} from '@nestjs/common'
import { CardType, Order } from '@prisma/client'
-import { OrderStatus, FlashSaleOrderStatus } from '@mp-pilates/shared'
+import { OrderStatus } from '@mp-pilates/shared'
import { PrismaService } from '../prisma/prisma.service'
import { WechatPayService, WxPaymentParams } from './wechat-pay.service'
import { InviteService } from '../invite/invite.service'
@@ -154,21 +154,6 @@ export class PaymentService {
this.logger.log(`Order PAID and membership granted: orderNo=${notification.orderNo}`)
- // ── Flash sale order: mark as PAID ──
- if (existingOrder.flashSaleId) {
- await this.prisma.flashSaleOrder.updateMany({
- where: {
- orderId: existingOrder.id,
- status: FlashSaleOrderStatus.RESERVED,
- },
- data: {
- status: FlashSaleOrderStatus.PAID,
- paidAt: now,
- },
- })
- this.logger.log(`Flash sale order marked PAID for orderNo=${notification.orderNo}`)
- }
-
return this.buildSuccessXml()
}
diff --git a/packages/server/src/scheduler/__tests__/scheduler.service.spec.ts b/packages/server/src/scheduler/__tests__/scheduler.service.spec.ts
index ab64af8..2f5318f 100644
--- a/packages/server/src/scheduler/__tests__/scheduler.service.spec.ts
+++ b/packages/server/src/scheduler/__tests__/scheduler.service.spec.ts
@@ -1,4 +1,3 @@
-import { FlashSaleService } from '../../flash-sale/flash-sale.service'
import { Test, TestingModule } from '@nestjs/testing'
import { Logger } from '@nestjs/common'
import { SchedulerService } from '../scheduler.service'
@@ -34,7 +33,6 @@ describe('SchedulerService', () => {
providers: [
SchedulerService,
{ provide: SlotGeneratorService, useValue: mockSlotGenerator },
- { provide: FlashSaleService, useValue: { expireUnpaidReservations: jest.fn() } },
],
}).compile()
diff --git a/packages/server/src/scheduler/scheduler.module.ts b/packages/server/src/scheduler/scheduler.module.ts
index 01a0186..8c6c37c 100644
--- a/packages/server/src/scheduler/scheduler.module.ts
+++ b/packages/server/src/scheduler/scheduler.module.ts
@@ -4,7 +4,6 @@ import { ReviewReminderService } from './review-reminder.service'
import { Module } from '@nestjs/common'
import { ScheduleModule } from '@nestjs/schedule'
import { TimeSlotModule } from '../time-slot/time-slot.module'
-import { FlashSaleModule } from '../flash-sale/flash-sale.module'
import { SchedulerService } from './scheduler.service'
@Module({
@@ -12,7 +11,6 @@ import { SchedulerService } from './scheduler.service'
ScheduleModule.forRoot(),
UserModule, ConfigModule,
TimeSlotModule,
- FlashSaleModule,
],
providers: [SchedulerService, ReviewReminderService],
})
diff --git a/packages/server/src/scheduler/scheduler.service.ts b/packages/server/src/scheduler/scheduler.service.ts
index 3dd647f..2299464 100644
--- a/packages/server/src/scheduler/scheduler.service.ts
+++ b/packages/server/src/scheduler/scheduler.service.ts
@@ -1,7 +1,6 @@
import { Injectable, Logger } from '@nestjs/common'
import { Cron } from '@nestjs/schedule'
import { SlotGeneratorService } from '../time-slot/slot-generator.service'
-import { FlashSaleService } from '../flash-sale/flash-sale.service'
@Injectable()
export class SchedulerService {
@@ -9,7 +8,6 @@ export class SchedulerService {
constructor(
private readonly slotGenerator: SlotGeneratorService,
- private readonly flashSaleService: FlashSaleService,
) {}
/** 02:00 daily — generate slots 14 days ahead from week templates */
@@ -55,17 +53,4 @@ export class SchedulerService {
this.logger.error('[handleCompleteBookings] Failed to complete bookings', err)
}
}
-
- /** Every 5 min — expire unpaid flash sale reservations older than 15min */
- @Cron('*/5 * * * *')
- async handleExpireFlashSaleReservations(): Promise {
- try {
- const count = await this.flashSaleService.expireUnpaidReservations(15)
- if (count > 0) {
- this.logger.log(`[handleExpireFlashSaleReservations] Expired ${count} unpaid reservations`)
- }
- } catch (err) {
- this.logger.error('[handleExpireFlashSaleReservations] Failed', err)
- }
- }
}
diff --git a/packages/shared/src/enums.ts b/packages/shared/src/enums.ts
index e1c5612..a61ae77 100644
--- a/packages/shared/src/enums.ts
+++ b/packages/shared/src/enums.ts
@@ -46,19 +46,6 @@ export enum OrderStatus {
REFUNDED = 'REFUNDED',
}
-// ===== FlashSale =====
-export enum FlashSaleStatus {
- DRAFT = 'DRAFT',
- ACTIVE = 'ACTIVE',
- ENDED = 'ENDED',
-}
-
-export enum FlashSaleOrderStatus {
- RESERVED = 'RESERVED',
- PAID = 'PAID',
- EXPIRED = 'EXPIRED',
-}
-
// ===== Invite =====
export enum InviteReferralStatus {
REGISTERED = 'REGISTERED',
diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts
index 762199e..983221b 100644
--- a/packages/shared/src/index.ts
+++ b/packages/shared/src/index.ts
@@ -7,8 +7,6 @@ export {
TimeSlotSource,
BookingStatus,
OrderStatus,
- FlashSaleStatus,
- FlashSaleOrderStatus,
InviteReferralStatus,
SubscriptionMessageScene,
} from './enums'
@@ -104,13 +102,6 @@ export type {
PaginatedData,
PaginatedResponse,
PaginationQuery,
- FlashSale,
- FlashSaleListItem,
- FlashSaleDetail,
- FlashSaleAdminItem,
- CreateFlashSaleDto,
- UpdateFlashSaleDto,
- FlashSalePurchaseResponse,
InviteActivityReferral,
InviteRewardGrantRecord,
InviteActivitySummary,
@@ -122,6 +113,4 @@ export type {
SubscriptionMessageConsentSummary,
} from './types/index'
-export { FlashSalePhase } from './types/index'
-
export * from './types/member-care'
diff --git a/packages/shared/src/types/flash-sale.ts b/packages/shared/src/types/flash-sale.ts
deleted file mode 100644
index 834126d..0000000
--- a/packages/shared/src/types/flash-sale.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-import { FlashSaleStatus, FlashSaleOrderStatus, CardTypeCategory } from '../enums'
-
-// ── Computed sale phase (derived from server time) ──────────
-export enum FlashSalePhase {
- UPCOMING = 'UPCOMING',
- ONGOING = 'ONGOING',
- SOLD_OUT = 'SOLD_OUT',
- ENDED = 'ENDED',
-}
-
-// ── Core entity ─────────────────────────────────────────────
-export interface FlashSale {
- readonly id: string
- readonly cardTypeId: string
- readonly title: string
- readonly originalPrice: number
- readonly flashPrice: number
- readonly totalStock: number
- readonly soldCount: number
- readonly startTime: string
- readonly endTime: string
- readonly status: FlashSaleStatus
- readonly description: string | null
- readonly sortOrder: number
- readonly createdAt: string
- readonly updatedAt: string
-}
-
-// ── User-facing list item (home page) ───────────────────────
-export interface FlashSaleListItem extends FlashSale {
- readonly phase: FlashSalePhase
- readonly remainingStock: number
- readonly cardType: {
- readonly name: string
- readonly type: CardTypeCategory
- readonly totalTimes: number | null
- readonly durationDays: number
- }
-}
-
-// ── Detail (includes user participation status) ─────────────
-export interface FlashSaleDetail extends FlashSaleListItem {
- readonly hasParticipated: boolean
- readonly userOrderStatus: FlashSaleOrderStatus | null
- readonly serverTime: string
-}
-
-// ── Admin DTOs ──────────────────────────────────────────────
-export interface CreateFlashSaleDto {
- readonly cardTypeId: string
- readonly title: string
- readonly originalPrice: number
- readonly flashPrice: number
- readonly totalStock: number
- readonly startTime: string
- readonly endTime: string
- readonly description?: string
- readonly sortOrder?: number
-}
-
-export interface UpdateFlashSaleDto {
- readonly title?: string
- readonly originalPrice?: number
- readonly flashPrice?: number
- readonly totalStock?: number
- readonly startTime?: string
- readonly endTime?: string
- readonly description?: string
- readonly status?: FlashSaleStatus
- readonly sortOrder?: number
-}
-
-// ── Purchase response ───────────────────────────────────────
-export interface FlashSalePurchaseResponse {
- readonly flashSaleOrderId: string
- readonly order: {
- readonly id: string
- readonly orderNo: string
- readonly amount: number
- }
- readonly paymentParams: {
- readonly timeStamp: string
- readonly nonceStr: string
- readonly package: string
- readonly signType: string
- readonly paySign: string
- }
-}
-
-// ── Admin list item ─────────────────────────────────────────
-export interface FlashSaleAdminItem extends FlashSale {
- readonly phase: FlashSalePhase
- readonly cardType: {
- readonly name: string
- readonly type: CardTypeCategory
- }
-}
diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts
index 3877c7d..351be93 100644
--- a/packages/shared/src/types/index.ts
+++ b/packages/shared/src/types/index.ts
@@ -44,20 +44,10 @@ export type {
StudioUploadCredential,
} from './studio'
export type { ApiResponse, PaginatedData, PaginatedResponse, PaginationQuery } from './api'
-export type {
- FlashSale,
- FlashSaleListItem,
- FlashSaleDetail,
- FlashSaleAdminItem,
- CreateFlashSaleDto,
- UpdateFlashSaleDto,
- FlashSalePurchaseResponse,
-} from './flash-sale'
export type {
InviteActivityReferral,
InviteRewardGrantRecord,
InviteActivitySummary,
} from './invite'
-export { FlashSalePhase } from './flash-sale'
export type { TeachingAnalytics, TeachingAnalyticsRecord, TeachingAnalyticsSummary } from './teaching-analytics'