|
|
@@ -0,0 +1,304 @@
|
|
|
+import { DataSource, Repository } from 'typeorm';
|
|
|
+import { Campaign } from './campaign.entity';
|
|
|
+import { logger } from '@/server/utils/logger';
|
|
|
+import { z } from 'zod';
|
|
|
+import { CampaignSchema } from './campaign.entity';
|
|
|
+
|
|
|
+// 定义查询参数接口
|
|
|
+interface CampaignQueryParams {
|
|
|
+ page?: number;
|
|
|
+ pageSize?: number;
|
|
|
+ status?: number;
|
|
|
+ keyword?: string;
|
|
|
+ startDate?: Date;
|
|
|
+ endDate?: Date;
|
|
|
+}
|
|
|
+
|
|
|
+// 定义创建和更新活动的接口
|
|
|
+type CreateCampaignDto = z.infer<typeof CampaignSchema>;
|
|
|
+type UpdateCampaignDto = Partial<CreateCampaignDto>;
|
|
|
+
|
|
|
+// 定义活动状态枚举
|
|
|
+export enum CampaignStatus {
|
|
|
+ NOT_STARTED = 0,
|
|
|
+ IN_PROGRESS = 1,
|
|
|
+ ENDED = 2,
|
|
|
+ CANCELLED = 3
|
|
|
+}
|
|
|
+
|
|
|
+// 定义活动参与人接口
|
|
|
+interface CampaignParticipant {
|
|
|
+ campaignId: number;
|
|
|
+ userId: number;
|
|
|
+ joinedAt: Date;
|
|
|
+}
|
|
|
+
|
|
|
+// 定义活动效果分析结果接口
|
|
|
+interface CampaignEffectiveness {
|
|
|
+ participationRate: number;
|
|
|
+ conversionRate: number;
|
|
|
+ roi: number;
|
|
|
+ totalParticipants: number;
|
|
|
+ totalConversions: number;
|
|
|
+ totalRevenue: number;
|
|
|
+}
|
|
|
+
|
|
|
+export class CampaignService {
|
|
|
+ private campaignRepository: Repository<Campaign>;
|
|
|
+
|
|
|
+ constructor(private dataSource: DataSource) {
|
|
|
+ this.campaignRepository = this.dataSource.getRepository(Campaign);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取活动列表(分页)
|
|
|
+ */
|
|
|
+ async findAll(params: CampaignQueryParams): Promise<{ data: Campaign[], total: number }> {
|
|
|
+ const { page = 1, pageSize = 10, status, keyword, startDate, endDate } = params;
|
|
|
+
|
|
|
+ const query = this.campaignRepository.createQueryBuilder('campaign')
|
|
|
+ .where('campaign.isDeleted = 0');
|
|
|
+
|
|
|
+ // 条件筛选
|
|
|
+ if (status !== undefined) {
|
|
|
+ query.andWhere('campaign.status = :status', { status });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (keyword) {
|
|
|
+ query.andWhere('(campaign.name LIKE :keyword OR campaign.description LIKE :keyword)', {
|
|
|
+ keyword: `%${keyword}%`
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (startDate) {
|
|
|
+ query.andWhere('campaign.startDate >= :startDate', { startDate });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (endDate) {
|
|
|
+ query.andWhere('campaign.endDate <= :endDate', { endDate });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 分页
|
|
|
+ query.skip((page - 1) * pageSize)
|
|
|
+ .take(pageSize)
|
|
|
+ .orderBy('campaign.createdAt', 'DESC');
|
|
|
+
|
|
|
+ const [data, total] = await query.getManyAndCount();
|
|
|
+
|
|
|
+ logger.api(`获取活动列表: 第${page}页, 共${total}条`);
|
|
|
+ return { data, total };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取活动详情
|
|
|
+ */
|
|
|
+ async findOne(id: number): Promise<Campaign | null> {
|
|
|
+ const campaign = await this.campaignRepository.findOne({
|
|
|
+ where: { id, isDeleted: 0 }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!campaign) {
|
|
|
+ logger.error(`活动不存在: ID=${id}`);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ logger.api(`获取活动详情: ID=${id}`);
|
|
|
+ return campaign;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建活动
|
|
|
+ */
|
|
|
+ async create(createCampaignDto: CreateCampaignDto): Promise<Campaign> {
|
|
|
+ // 验证数据
|
|
|
+ const validatedData = CampaignSchema.parse(createCampaignDto);
|
|
|
+
|
|
|
+ const campaign = this.campaignRepository.create({
|
|
|
+ ...validatedData,
|
|
|
+ createdAt: new Date(),
|
|
|
+ updatedAt: new Date(),
|
|
|
+ isDeleted: 0
|
|
|
+ });
|
|
|
+
|
|
|
+ const result = await this.campaignRepository.save(campaign);
|
|
|
+ logger.api(`创建活动: ID=${result.id}, 名称=${result.name}`);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 更新活动
|
|
|
+ */
|
|
|
+ async update(id: number, updateCampaignDto: UpdateCampaignDto): Promise<Campaign | null> {
|
|
|
+ const campaign = await this.findOne(id);
|
|
|
+ if (!campaign) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证数据
|
|
|
+ const validatedData = CampaignSchema.partial().parse(updateCampaignDto);
|
|
|
+
|
|
|
+ Object.assign(campaign, validatedData);
|
|
|
+ campaign.updatedAt = new Date();
|
|
|
+
|
|
|
+ const result = await this.campaignRepository.save(campaign);
|
|
|
+ logger.api(`更新活动: ID=${id}, 名称=${result.name}`);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 删除活动(软删除)
|
|
|
+ */
|
|
|
+ async remove(id: number): Promise<boolean> {
|
|
|
+ const campaign = await this.findOne(id);
|
|
|
+ if (!campaign) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ campaign.isDeleted = 1;
|
|
|
+ campaign.updatedAt = new Date();
|
|
|
+
|
|
|
+ await this.campaignRepository.save(campaign);
|
|
|
+ logger.api(`删除活动: ID=${id}`);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 更新活动状态
|
|
|
+ */
|
|
|
+ async updateStatus(id: number, status: CampaignStatus): Promise<Campaign | null> {
|
|
|
+ const campaign = await this.findOne(id);
|
|
|
+ if (!campaign) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 状态变更验证
|
|
|
+ if (!Object.values(CampaignStatus).includes(status)) {
|
|
|
+ logger.error(`无效的活动状态: ${status}`);
|
|
|
+ throw new Error('无效的活动状态');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 业务逻辑验证:已结束的活动不能再改为进行中
|
|
|
+ if (campaign.status === CampaignStatus.ENDED && status === CampaignStatus.IN_PROGRESS) {
|
|
|
+ logger.error(`活动状态变更失败: 已结束的活动不能再改为进行中, ID=${id}`);
|
|
|
+ throw new Error('已结束的活动不能再改为进行中');
|
|
|
+ }
|
|
|
+
|
|
|
+ campaign.status = status;
|
|
|
+ campaign.updatedAt = new Date();
|
|
|
+
|
|
|
+ // 如果活动状态改为已结束,更新结束日期
|
|
|
+ if (status === CampaignStatus.ENDED && !campaign.endDate) {
|
|
|
+ campaign.endDate = new Date();
|
|
|
+ }
|
|
|
+
|
|
|
+ const result = await this.campaignRepository.save(campaign);
|
|
|
+ logger.api(`更新活动状态: ID=${id}, 状态=${status}`);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 添加活动参与人
|
|
|
+ */
|
|
|
+ async addParticipant(campaignId: number, userId: number): Promise<CampaignParticipant> {
|
|
|
+ const campaign = await this.findOne(campaignId);
|
|
|
+ if (!campaign) {
|
|
|
+ throw new Error('活动不存在');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查活动状态
|
|
|
+ if (campaign.status === CampaignStatus.ENDED || campaign.status === CampaignStatus.CANCELLED) {
|
|
|
+ throw new Error('无法添加参与人到已结束或已取消的活动');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查用户是否已参与
|
|
|
+ // 使用类型断言处理类型问题
|
|
|
+ const participantRepository = this.dataSource.getRepository('campaign_participant') as unknown as Repository<CampaignParticipant>;
|
|
|
+ const existingParticipant = await participantRepository.findOne({
|
|
|
+ where: { campaignId, userId }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (existingParticipant) {
|
|
|
+ throw new Error('用户已参与该活动');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加参与人
|
|
|
+ const participant = participantRepository.create({
|
|
|
+ campaignId,
|
|
|
+ userId,
|
|
|
+ joinedAt: new Date()
|
|
|
+ });
|
|
|
+
|
|
|
+ const result = await participantRepository.save(participant);
|
|
|
+ logger.api(`添加活动参与人: 活动ID=${campaignId}, 用户ID=${userId}`);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取活动参与人列表
|
|
|
+ */
|
|
|
+ async getParticipants(campaignId: number): Promise<CampaignParticipant[]> {
|
|
|
+ const campaign = await this.findOne(campaignId);
|
|
|
+ if (!campaign) {
|
|
|
+ throw new Error('活动不存在');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 使用类型断言处理类型问题
|
|
|
+ const participantRepository = this.dataSource.getRepository('campaign_participant') as unknown as Repository<CampaignParticipant>;
|
|
|
+ const participants = await participantRepository.find({
|
|
|
+ where: { campaignId }
|
|
|
+ });
|
|
|
+
|
|
|
+ logger.api(`获取活动参与人列表: 活动ID=${campaignId}, 参与人数=${participants.length}`);
|
|
|
+ return participants;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 分析活动效果
|
|
|
+ */
|
|
|
+ async analyzeEffectiveness(campaignId: number): Promise<CampaignEffectiveness> {
|
|
|
+ const campaign = await this.findOne(campaignId);
|
|
|
+ if (!campaign) {
|
|
|
+ throw new Error('活动不存在');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取参与人数
|
|
|
+ const participantRepository = this.dataSource.getRepository('campaign_participant');
|
|
|
+ const totalParticipants = await participantRepository.count({
|
|
|
+ where: { campaignId }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 获取转化人数(假设有一个conversions表记录转化数据)
|
|
|
+ const conversionRepository = this.dataSource.getRepository('campaign_conversion');
|
|
|
+ const totalConversions = await conversionRepository.count({
|
|
|
+ where: { campaignId }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 获取总收入(假设每个转化有一个revenue字段)
|
|
|
+ const conversionSum = await conversionRepository
|
|
|
+ .createQueryBuilder('conversion')
|
|
|
+ .select('SUM(conversion.revenue)', 'totalRevenue')
|
|
|
+ .where('conversion.campaignId = :campaignId', { campaignId })
|
|
|
+ .getRawOne();
|
|
|
+
|
|
|
+ const totalRevenue = conversionSum.totalRevenue || 0;
|
|
|
+
|
|
|
+ // 计算参与率、转化率和ROI
|
|
|
+ const participationRate = totalParticipants > 0 ? (totalParticipants / 100) : 0; // 假设目标受众为100人,实际应从活动设置中获取
|
|
|
+ const conversionRate = totalParticipants > 0 ? (totalConversions / totalParticipants) : 0;
|
|
|
+ // 处理budget可能为null的情况
|
|
|
+ const budget = campaign.budget || 0;
|
|
|
+ const roi = budget > 0 ? (totalRevenue - budget) / budget : 0;
|
|
|
+
|
|
|
+ const effectiveness: CampaignEffectiveness = {
|
|
|
+ participationRate,
|
|
|
+ conversionRate,
|
|
|
+ roi,
|
|
|
+ totalParticipants,
|
|
|
+ totalConversions,
|
|
|
+ totalRevenue
|
|
|
+ };
|
|
|
+
|
|
|
+ logger.api(`分析活动效果: 活动ID=${campaignId}, ROI=${roi}`);
|
|
|
+ return effectiveness;
|
|
|
+ }
|
|
|
+}
|