343 行
10 KiB
TypeScript
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,
|
|
};
|
|
}
|
|
}
|
|
|