文件
shanghaipeiwan/server/src/core/activity/activity.service.ts

343 行
10 KiB
TypeScript

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<string, Activity>();
private participants = new Map<string, Participant[]>(); // 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,
};
}
}