import { UserRoleFlag } from '../auth/auth-role.manager'; export enum ActivityPayType { AA_PAY = 1, // AA制付费 PROJECT_PAY = 2, // 项目制付费 } export interface Activity { id: string; creatorId: number; title: string; gameCategory: string; location?: string; payType: ActivityPayType; totalFeeCents?: number; aaFeeCents?: number; perPersonFeeCents?: number; allowDynamicPrice?: boolean; priceAdjustRule?: 'UNIFIED_HIGH' | 'TIERED'; priceAdjustHistory?: Array<{ oldPriceCents: number; newPriceCents: number; adjustedAt: Date; operatorId: number; }>; maxParticipants: number; currentParticipants: number; status?: 'RECRUITING' | 'FULL' | 'IN_PROGRESS' | 'COMPLETED' | 'CANCELLED'; } export interface Participant { id: string; activityId: string; userId: number; payAmountCents: number; verifyCode: string; // 4位纯数字核销码 checkinStatus: boolean; verifiedAt?: Date; verifiedBy?: number; } export class ActivityService { private activities = new Map(); private participants = new Map(); // activityId -> participants createActivity(params: { creatorId: number; title: string; gameCategory: string; location?: string; payType: ActivityPayType; totalFeeCents?: number; aaFeeCents?: number; perPersonFeeCents?: number; allowDynamicPrice?: boolean; priceAdjustRule?: 'UNIFIED_HIGH' | 'TIERED'; maxParticipants: number; roleManager?: any; }): Activity { if (params.roleManager && typeof params.roleManager.grantRole === 'function') { params.roleManager.grantRole(params.creatorId, UserRoleFlag.HOST); } const id = `act_${Date.now()}_${Math.floor(Math.random() * 1000)}`; const activity: Activity = { id, creatorId: params.creatorId, title: params.title, gameCategory: params.gameCategory, location: params.location, payType: params.payType, totalFeeCents: params.totalFeeCents, aaFeeCents: params.aaFeeCents, perPersonFeeCents: params.perPersonFeeCents, allowDynamicPrice: params.allowDynamicPrice ?? true, priceAdjustRule: params.priceAdjustRule || 'UNIFIED_HIGH', priceAdjustHistory: [], maxParticipants: params.maxParticipants, currentParticipants: 0, status: 'RECRUITING', }; this.activities.set(id, activity); this.participants.set(id, []); return activity; } joinActivity(activityId: string, params: { userId: number; payAmountCents: number }): Participant { const activity = this.activities.get(activityId); if (!activity) { throw new Error('活动不存在'); } const currentList = this.participants.get(activityId) || []; if (activity.maxParticipants > 0 && currentList.length >= activity.maxParticipants) { throw new Error('活动报名人数已满'); } const alreadyJoined = currentList.some((p) => p.userId === params.userId); if (alreadyJoined) { throw new Error('用户已报名该活动'); } // 生成当前活动内唯一的4位数字核销码 (1000-9999) const existingCodes = new Set(currentList.map((p) => p.verifyCode)); let verifyCode = ''; for (let i = 0; i < 100; i++) { const code = String(Math.floor(1000 + Math.random() * 9000)); if (!existingCodes.has(code)) { verifyCode = code; break; } } if (!verifyCode) { throw new Error('核销码生成失败,池已满'); } const participant: Participant = { id: `p_${Date.now()}_${Math.floor(Math.random() * 1000)}`, activityId, userId: params.userId, payAmountCents: params.payAmountCents, verifyCode, checkinStatus: false, }; currentList.push(participant); activity.currentParticipants = currentList.length; return participant; } verifyCheckin(params: { activityId: string; operatorId: number; verifyCode: string; }): { success: boolean; participant: Participant } { const activity = this.activities.get(params.activityId); if (!activity) { throw new Error('活动不存在'); } if (activity.creatorId !== params.operatorId) { throw new Error('无权核销该活动的核销码'); } const currentList = this.participants.get(params.activityId) || []; const participant = currentList.find((p) => p.verifyCode === params.verifyCode); if (!participant) { throw new Error('无效或不存在的核销码'); } if (participant.checkinStatus) { throw new Error('该核销码已被核销,不可重复使用'); } participant.checkinStatus = true; participant.verifiedAt = new Date(); participant.verifiedBy = params.operatorId; return { success: true, participant }; } adjustActivityPrice(params: { activityId: string; operatorId: number; newPriceCents: number; adjustRule?: 'UNIFIED_HIGH' | 'TIERED'; }): { activity: Activity; priceDiffCents: number; affectedParticipantsCount: number } { const activity = this.activities.get(params.activityId); if (!activity) { throw new Error('活动不存在'); } if (activity.creatorId !== params.operatorId) { throw new Error('只有发起人有权调整活动出价'); } if (activity.allowDynamicPrice === false) { throw new Error('该活动未开启动态调价权限'); } if (params.newPriceCents <= 0) { throw new Error('调整后的价格必须大于0'); } const currentPriceCents = activity.perPersonFeeCents || activity.aaFeeCents || 0; const priceDiffCents = params.newPriceCents - currentPriceCents; const rule = params.adjustRule || activity.priceAdjustRule || 'UNIFIED_HIGH'; const currentList = this.participants.get(params.activityId) || []; let affectedParticipantsCount = 0; if (rule === 'UNIFIED_HIGH') { for (const p of currentList) { p.payAmountCents = params.newPriceCents; } affectedParticipantsCount = currentList.length; } if (!activity.priceAdjustHistory) { activity.priceAdjustHistory = []; } activity.priceAdjustHistory.push({ oldPriceCents: currentPriceCents, newPriceCents: params.newPriceCents, adjustedAt: new Date(), operatorId: params.operatorId, }); if (activity.payType === ActivityPayType.AA_PAY) { activity.aaFeeCents = params.newPriceCents; activity.totalFeeCents = params.newPriceCents * Math.max(1, activity.maxParticipants); } else { activity.perPersonFeeCents = params.newPriceCents; activity.aaFeeCents = params.newPriceCents; activity.totalFeeCents = params.newPriceCents * Math.max(1, activity.maxParticipants); } return { activity, priceDiffCents, affectedParticipantsCount }; } getAllActivities(): Activity[] { return Array.from(this.activities.values()); } getActivity(id: string): Activity | undefined { return this.activities.get(id); } getParticipants(activityId: string): Participant[] { return this.participants.get(activityId) || []; } cancelActivity(params: { activityId: string; operatorId: number; reason?: string; }): { success: boolean; status: string; refundedCount: number; totalRefundCents: number; refundDetails: Array<{ userId: number; refundAmountCents: number }>; } { const activity = this.activities.get(params.activityId); if (!activity) { throw new Error('活动不存在'); } if (activity.creatorId !== params.operatorId) { throw new Error('只有发起人有权解散此组局'); } const currentList = this.participants.get(params.activityId) || []; const hasCheckedIn = currentList.some((p) => p.checkinStatus); if (hasCheckedIn) { throw new Error('聚会已有成员到场核销,不可随意解散'); } const refundDetails = currentList.map((p) => ({ userId: p.userId, refundAmountCents: p.payAmountCents, })); const totalRefundCents = refundDetails.reduce((sum, r) => sum + r.refundAmountCents, 0); activity.status = 'CANCELLED'; return { success: true, status: 'CANCELLED', refundedCount: refundDetails.length, totalRefundCents, refundDetails, }; } leaveActivity(params: { activityId: string; userId: number; }): { success: boolean; refundAmountCents: number; } { const activity = this.activities.get(params.activityId); if (!activity) { throw new Error('活动不存在'); } const currentList = this.participants.get(params.activityId) || []; const ptIndex = currentList.findIndex((p) => p.userId === params.userId); if (ptIndex === -1) { throw new Error('用户未报名该活动'); } const pt = currentList[ptIndex]; if (pt.checkinStatus) { throw new Error('已核销到场,无法申请退席'); } const refundAmountCents = pt.payAmountCents; currentList.splice(ptIndex, 1); activity.currentParticipants = Math.max(0, activity.currentParticipants - 1); return { success: true, refundAmountCents, }; } adminForceCancelActivity(params: { activityId: string; operatorAdminId: number; reason?: string; roleManager: any; }): { success: boolean; status: string; refundedCount: number; totalRefundCents: number; refundDetails: Array<{ userId: number; refundAmountCents: number }>; } { if (!params.roleManager || !params.roleManager.isAdmin(params.operatorAdminId)) { throw new Error('只有平台管理员有权执行平台强制解散'); } const activity = this.activities.get(params.activityId); if (!activity) { throw new Error('活动不存在'); } const currentList = this.participants.get(params.activityId) || []; const refundDetails = currentList.map((p) => ({ userId: p.userId, refundAmountCents: p.payAmountCents, })); const totalRefundCents = refundDetails.reduce((sum, r) => sum + r.refundAmountCents, 0); activity.status = 'CANCELLED'; return { success: true, status: 'CANCELLED', refundedCount: refundDetails.length, totalRefundCents, refundDetails, }; } }