From 37cc2a729bc189f9e2954c66624cfce920bc1ca6 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Mon, 3 Nov 2025 17:49:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(push-notifications):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=8C=91=E6=88=98=E6=8F=90=E9=86=92=E5=AE=9A=E6=97=B6=E6=8E=A8?= =?UTF-8?q?=E9=80=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增每日定时推送系统,根据用户参与状态发送不同类型的挑战提醒: - 已参与用户:每日发送鼓励推送 - 未参与用户:隔天发送挑战邀请 - 匿名用户:隔天发送通用邀请 包含推送历史记录表、定时任务调度、多类型文案模板和防重复发送机制 --- package-lock.json | 43 +- package.json | 1 + .../push-reminder-history-table-create.sql | 16 + src/app.module.ts | 2 + .../challenge-reminder.service.ts | 384 ++++++++++++++++++ .../models/push-reminder-history.model.ts | 83 ++++ .../push-notifications.module.ts | 5 + .../templates/challenge-templates.ts | 127 ++++++ yarn.lock | 75 ++-- 9 files changed, 710 insertions(+), 26 deletions(-) create mode 100644 sql-scripts/push-reminder-history-table-create.sql create mode 100644 src/push-notifications/challenge-reminder.service.ts create mode 100644 src/push-notifications/models/push-reminder-history.model.ts create mode 100644 src/push-notifications/templates/challenge-templates.ts diff --git a/package-lock.json b/package-lock.json index 4724e54..54631b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@nestjs/core": "^11.0.1", "@nestjs/jwt": "^11.0.0", "@nestjs/platform-express": "^11.0.1", + "@nestjs/schedule": "^6.0.1", "@nestjs/sequelize": "^11.0.0", "@nestjs/swagger": "^11.1.0", "@parse/node-apn": "^5.0.0", @@ -2382,6 +2383,19 @@ "@nestjs/core": "^11.0.0" } }, + "node_modules/@nestjs/schedule": { + "version": "6.0.1", + "resolved": "https://mirrors.tencent.com/npm/@nestjs/schedule/-/schedule-6.0.1.tgz", + "integrity": "sha512-v3yO6cSPAoBSSyH67HWnXHzuhPhSNZhRmLY38JvCt2sqY8sPMOODpcU1D79iUMFf7k16DaMEbL4Mgx61ZhiC8Q==", + "license": "MIT", + "dependencies": { + "cron": "4.3.3" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0" + } + }, "node_modules/@nestjs/schematics": { "version": "11.0.2", "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.2.tgz", @@ -3452,6 +3466,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/luxon": { + "version": "3.7.1", + "resolved": "https://mirrors.tencent.com/npm/@types/luxon/-/luxon-3.7.1.tgz", + "integrity": "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==", + "license": "MIT" + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -5736,6 +5756,18 @@ "dev": true, "license": "MIT" }, + "node_modules/cron": { + "version": "4.3.3", + "resolved": "https://mirrors.tencent.com/npm/cron/-/cron-4.3.3.tgz", + "integrity": "sha512-B/CJj5yL3sjtlun6RtYHvoSB26EmQ2NUmhq9ZiJSyKIM4K/fqfh9aelDFlIayD2YMeFZqWLi9hHV+c+pq2Djkw==", + "dependencies": { + "@types/luxon": "~3.7.0", + "luxon": "~3.7.0" + }, + "engines": { + "node": ">=18.x" + } + }, "node_modules/croner": { "version": "4.1.97", "resolved": "https://mirrors.tencent.com/npm/croner/-/croner-4.1.97.tgz", @@ -9473,6 +9505,15 @@ "url": "https://github.com/sponsors/wellwelwel" } }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://mirrors.tencent.com/npm/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -13703,4 +13744,4 @@ } } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index f80beef..6e55e2c 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@nestjs/core": "^11.0.1", "@nestjs/jwt": "^11.0.0", "@nestjs/platform-express": "^11.0.1", + "@nestjs/schedule": "^6.0.1", "@nestjs/sequelize": "^11.0.0", "@nestjs/swagger": "^11.1.0", "@parse/node-apn": "^5.0.0", diff --git a/sql-scripts/push-reminder-history-table-create.sql b/sql-scripts/push-reminder-history-table-create.sql new file mode 100644 index 0000000..9a38388 --- /dev/null +++ b/sql-scripts/push-reminder-history-table-create.sql @@ -0,0 +1,16 @@ +-- 创建推送提醒历史记录表 +CREATE TABLE `t_push_reminder_history` ( + `id` char(36) NOT NULL DEFAULT (UUID()), + `user_id` varchar(64) DEFAULT NULL COMMENT '用户ID,可能为空', + `device_token` varchar(255) NOT NULL COMMENT '设备推送令牌', + `reminder_type` enum('challenge_encouragement','challenge_invitation','general_invitation') NOT NULL COMMENT '提醒类型', + `last_sent_at` datetime NOT NULL COMMENT '最后发送时间', + `sent_count` int NOT NULL DEFAULT '0' COMMENT '发送次数', + `next_available_at` datetime DEFAULT NULL COMMENT '下次可发送时间', + `is_active` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否激活', + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_user_device_type` (`user_id`,`device_token`,`reminder_type`), + KEY `idx_last_sent_at` (`last_sent_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='推送提醒历史记录表'; \ No newline at end of file diff --git a/src/app.module.ts b/src/app.module.ts index d7ca9a6..5f5afda 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -4,6 +4,7 @@ import { AppService } from "./app.service"; import { DatabaseModule } from "./database/database.module"; import { UsersModule } from "./users/users.module"; import { ConfigModule } from '@nestjs/config'; +import { ScheduleModule } from '@nestjs/schedule'; import { LoggerModule } from './common/logger/logger.module'; import { CheckinsModule } from './checkins/checkins.module'; import { AiCoachModule } from './ai-coach/ai-coach.module'; @@ -27,6 +28,7 @@ import { PushNotificationsModule } from './push-notifications/push-notifications isGlobal: true, envFilePath: '.env', }), + ScheduleModule.forRoot(), LoggerModule, DatabaseModule, UsersModule, diff --git a/src/push-notifications/challenge-reminder.service.ts b/src/push-notifications/challenge-reminder.service.ts new file mode 100644 index 0000000..09ef42b --- /dev/null +++ b/src/push-notifications/challenge-reminder.service.ts @@ -0,0 +1,384 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Cron } from '@nestjs/schedule'; +import { InjectModel } from '@nestjs/sequelize'; +import { Op } from 'sequelize'; +import { PushType } from 'apns2'; +import { PushNotificationsService } from './push-notifications.service'; +import { PushTokenService } from './push-token.service'; +import { UserPushToken } from './models/user-push-token.model'; +import { PushReminderHistory, ReminderType } from './models/push-reminder-history.model'; +import { ChallengesService } from '../challenges/challenges.service'; +import { Challenge } from '../challenges/models/challenge.model'; +import { ChallengeParticipant, ChallengeParticipantStatus } from '../challenges/models/challenge-participant.model'; +import { + getEncouragementTemplate, + getInvitationTemplate, + getGeneralInvitationTemplate +} from './templates/challenge-templates'; +import * as dayjs from 'dayjs'; + +@Injectable() +export class ChallengeReminderService { + private readonly logger = new Logger(ChallengeReminderService.name); + + constructor( + @InjectModel(UserPushToken) + private readonly pushTokenModel: typeof UserPushToken, + @InjectModel(PushReminderHistory) + private readonly reminderHistoryModel: typeof PushReminderHistory, + @InjectModel(ChallengeParticipant) + private readonly participantModel: typeof ChallengeParticipant, + private readonly pushNotificationsService: PushNotificationsService, + private readonly pushTokenService: PushTokenService, + private readonly challengesService: ChallengesService, + private readonly configService: ConfigService, + ) {} + + /** + * 每晚8点执行的挑战提醒定时任务 + */ + @Cron('0 20 * * *', { + name: 'challengeReminder', + timeZone: 'Asia/Shanghai', + }) + async handleChallengeReminder(): Promise { + this.logger.log('Starting daily challenge reminder task...'); + + try { + // 检查是否为主进程(NODE_APP_INSTANCE 为 0) + const nodeAppInstance = this.configService.get('NODE_APP_INSTANCE', 0); + if (Number(nodeAppInstance) !== 0) { + this.logger.log(`Not the primary process (instance: ${nodeAppInstance}). Skipping challenge reminder...`); + return; + } + + this.logger.log('Primary process detected. Running challenge reminder...'); + + // 1. 获取所有活跃的推送令牌 + const activeTokens = await this.getActivePushTokens(); + if (activeTokens.length === 0) { + this.logger.log('No active push tokens found'); + return; + } + + this.logger.log(`Found ${activeTokens.length} active push tokens`); + + // 2. 获取正在进行的挑战 + const ongoingChallenges = await this.getOngoingChallenges(); + if (ongoingChallenges.length === 0) { + this.logger.log('No ongoing challenges found'); + return; + } + + this.logger.log(`Found ${ongoingChallenges.length} ongoing challenges`); + + // 3. 获取参与挑战的活跃用户 + const activeParticipants = await this.getActiveParticipants(ongoingChallenges); + const participantUserIds = new Set(activeParticipants.map(p => p.userId)); + + this.logger.log(`Found ${activeParticipants.length} active participants`); + + // 4. 按用户类型分组处理 + await this.processTokensByUserType(activeTokens, participantUserIds, ongoingChallenges); + + } catch (error) { + this.logger.error(`Error during challenge reminder task: ${error.message}`, error); + } + } + + /** + * 获取所有活跃的推送令牌 + */ + private async getActivePushTokens(): Promise { + return await this.pushTokenModel.findAll({ + where: { + isActive: true, + }, + }); + } + + /** + * 获取正在进行中的挑战 + */ + private async getOngoingChallenges(): Promise { + const challenges = await this.challengesService['challengeModel'].findAll({ + order: [['startAt', 'ASC']], + }); + + // 过滤出真正进行中的挑战 + return challenges.filter(challenge => { + const start = dayjs(challenge.startAt); + const end = dayjs(challenge.endAt); + const current = dayjs(); + + return current.isAfter(start, 'minute') && current.isBefore(end, 'minute'); + }); + } + + /** + * 获取参与挑战的活跃用户 + */ + private async getActiveParticipants(challenges: Challenge[]): Promise { + const challengeIds = challenges.map(challenge => challenge.id); + + return await this.participantModel.findAll({ + where: { + challengeId: { + [Op.in]: challengeIds, + }, + status: ChallengeParticipantStatus.ACTIVE, + }, + }); + } + + /** + * 按用户类型分组处理推送令牌 + */ + private async processTokensByUserType( + tokens: UserPushToken[], + participantUserIds: Set, + challenges: Challenge[] + ): Promise { + // 分组:已参与挑战的用户、未参与挑战但有userId的用户、没有userId的用户 + const participatingTokens: UserPushToken[] = []; + const nonParticipatingTokens: UserPushToken[] = []; + const anonymousTokens: UserPushToken[] = []; + + for (const token of tokens) { + if (!token.userId) { + anonymousTokens.push(token); + } else if (participantUserIds.has(token.userId)) { + participatingTokens.push(token); + } else { + nonParticipatingTokens.push(token); + } + } + + this.logger.log(`Token groups - Participating: ${participatingTokens.length}, Non-participating: ${nonParticipatingTokens.length}, Anonymous: ${anonymousTokens.length}`); + + // 处理已参与挑战的用户 - 发送鼓励文案(每天) + await this.sendEncouragementReminders(participatingTokens); + + // 处理未参与挑战但有userId的用户 - 发送邀请文案(隔天) + await this.sendInvitationReminders(nonParticipatingTokens, challenges, ReminderType.CHALLENGE_INVITATION); + + // 处理没有userId的用户 - 发送通用邀请(隔天) + await this.sendInvitationReminders(anonymousTokens, challenges, ReminderType.GENERAL_INVITATION); + } + + /** + * 发送鼓励提醒给已参与挑战的用户 + */ + private async sendEncouragementReminders(tokens: UserPushToken[]): Promise { + if (tokens.length === 0) return; + + this.logger.log(`Sending encouragement reminders to ${tokens.length} participating users`); + + let totalSent = 0; + let totalFailed = 0; + + for (const token of tokens) { + try { + // 检查今天是否已经发送过鼓励推送 + const today = dayjs().startOf('day').toDate(); + const recentReminder = await this.reminderHistoryModel.findOne({ + where: { + userId: token.userId, + deviceToken: token.deviceToken, + reminderType: ReminderType.CHALLENGE_ENCOURAGEMENT, + lastSentAt: { + [Op.gte]: today, + }, + }, + }); + + if (recentReminder) { + this.logger.log(`User ${token.userId} already received encouragement reminder today`); + continue; + } + + // 获取用户参与的挑战信息 + const participant = await this.participantModel.findOne({ + where: { + userId: token.userId, + status: ChallengeParticipantStatus.ACTIVE, + }, + include: [{ + model: Challenge, + as: 'challenge', + }], + }); + + if (!participant || !participant.challenge) { + this.logger.warn(`No active challenge found for user ${token.userId}`); + continue; + } + + // 获取鼓励文案 + const template = getEncouragementTemplate(participant.challenge.type); + + // 发送推送 + const result = await this.pushNotificationsService.sendBatchNotificationToDevices({ + deviceTokens: [token.deviceToken], + title: template.title, + body: template.body, + pushType: PushType.alert, + }); + + if (result.code === 0) { + totalSent += result.data.successCount; + totalFailed += result.data.failedCount; + + // 记录推送历史 + await this.updateReminderHistory( + token.userId, + token.deviceToken, + ReminderType.CHALLENGE_ENCOURAGEMENT + ); + + this.logger.log(`Encouragement reminder sent to user ${token.userId}`); + } else { + totalFailed++; + this.logger.warn(`Failed to send encouragement reminder to user ${token.userId}: ${result.message}`); + } + + } catch (error) { + this.logger.error(`Error sending encouragement reminder to user ${token.userId}: ${error.message}`, error); + totalFailed++; + } + } + + this.logger.log(`Encouragement reminders completed. Sent: ${totalSent}, Failed: ${totalFailed}`); + } + + /** + * 发送邀请提醒给未参与挑战的用户 + */ + private async sendInvitationReminders( + tokens: UserPushToken[], + challenges: Challenge[], + reminderType: ReminderType + ): Promise { + if (tokens.length === 0 || challenges.length === 0) return; + + this.logger.log(`Sending invitation reminders to ${tokens.length} users`); + + let totalSent = 0; + let totalFailed = 0; + + // 随机选择一个挑战类型用于邀请 + const randomChallenge = challenges[Math.floor(Math.random() * challenges.length)]; + + for (const token of tokens) { + try { + // 检查是否可以发送(隔天推送) + const canSend = await this.canSendInvitation(token, reminderType); + if (!canSend) { + continue; + } + + // 获取邀请文案 + const template = reminderType === ReminderType.GENERAL_INVITATION + ? getGeneralInvitationTemplate() + : getInvitationTemplate(randomChallenge.type); + + // 发送推送 + const result = await this.pushNotificationsService.sendBatchNotificationToDevices({ + deviceTokens: [token.deviceToken], + title: template.title, + body: template.body, + pushType: PushType.alert, + }); + + if (result.code === 0) { + totalSent += result.data.successCount; + totalFailed += result.data.failedCount; + + // 记录推送历史,设置下次可发送时间为2天后 + await this.updateReminderHistory( + token.userId, + token.deviceToken, + reminderType, + 2 // 2天后可再次发送 + ); + + this.logger.log(`Invitation reminder sent to ${token.userId || 'anonymous user'}`); + } else { + totalFailed++; + this.logger.warn(`Failed to send invitation reminder: ${result.message}`); + } + + } catch (error) { + this.logger.error(`Error sending invitation reminder: ${error.message}`, error); + totalFailed++; + } + } + + this.logger.log(`Invitation reminders completed. Sent: ${totalSent}, Failed: ${totalFailed}`); + } + + /** + * 检查是否可以发送邀请(隔天推送逻辑) + */ + private async canSendInvitation(token: UserPushToken, reminderType: ReminderType): Promise { + const reminder = await this.reminderHistoryModel.findOne({ + where: { + userId: token.userId, + deviceToken: token.deviceToken, + reminderType, + isActive: true, + }, + }); + + if (!reminder) { + return true; // 没有记录,可以发送 + } + + // 检查是否到了下次可发送时间 + if (reminder.nextAvailableAt && dayjs().isBefore(reminder.nextAvailableAt)) { + return false; + } + + return true; + } + + /** + * 更新推送历史记录 + */ + private async updateReminderHistory( + userId: string | null, + deviceToken: string, + reminderType: ReminderType, + daysUntilNext = 1 + ): Promise { + const now = new Date(); + const nextAvailableAt = dayjs().add(daysUntilNext, 'day').toDate(); + + const [reminder, created] = await this.reminderHistoryModel.findOrCreate({ + where: { + userId, + deviceToken, + reminderType, + }, + defaults: { + userId, + deviceToken, + reminderType, + lastSentAt: now, + sentCount: 1, + nextAvailableAt, + isActive: true, + }, + }); + + if (!created) { + await reminder.update({ + lastSentAt: now, + sentCount: reminder.sentCount + 1, + nextAvailableAt, + isActive: true, + }); + } + } +} \ No newline at end of file diff --git a/src/push-notifications/models/push-reminder-history.model.ts b/src/push-notifications/models/push-reminder-history.model.ts new file mode 100644 index 0000000..370e24d --- /dev/null +++ b/src/push-notifications/models/push-reminder-history.model.ts @@ -0,0 +1,83 @@ +import { Table, Column, Model, DataType, Index } from 'sequelize-typescript'; + +export enum ReminderType { + CHALLENGE_ENCOURAGEMENT = 'challenge_encouragement', // 已参与挑战用户的鼓励推送 + CHALLENGE_INVITATION = 'challenge_invitation', // 未参与挑战用户的邀请推送 + GENERAL_INVITATION = 'general_invitation', // 无userId用户的通用邀请 +} + +@Table({ + tableName: 't_push_reminder_history', + underscored: true, +}) +export class PushReminderHistory extends Model { + @Column({ + type: DataType.UUID, + defaultValue: DataType.UUIDV4, + primaryKey: true, + }) + declare id: string; + + @Column({ + type: DataType.STRING, + allowNull: true, + comment: '用户ID,可能为空', + }) + declare userId: string | null; + + @Column({ + type: DataType.STRING, + allowNull: false, + comment: '设备推送令牌', + }) + declare deviceToken: string; + + @Column({ + type: DataType.ENUM(...Object.values(ReminderType)), + allowNull: false, + comment: '提醒类型', + }) + declare reminderType: ReminderType; + + @Column({ + type: DataType.DATE, + allowNull: false, + comment: '最后发送时间', + }) + declare lastSentAt: Date; + + @Column({ + type: DataType.INTEGER, + allowNull: false, + defaultValue: 0, + comment: '发送次数', + }) + declare sentCount: number; + + @Column({ + type: DataType.DATE, + allowNull: true, + comment: '下次可发送时间', + }) + declare nextAvailableAt: Date | null; + + @Column({ + type: DataType.BOOLEAN, + allowNull: false, + defaultValue: true, + comment: '是否激活', + }) + declare isActive: boolean; + + @Column({ + type: DataType.DATE, + defaultValue: DataType.NOW, + }) + declare createdAt: Date; + + @Column({ + type: DataType.DATE, + defaultValue: DataType.NOW, + }) + declare updatedAt: Date; +} \ No newline at end of file diff --git a/src/push-notifications/push-notifications.module.ts b/src/push-notifications/push-notifications.module.ts index 7f60f32..c7cde1c 100644 --- a/src/push-notifications/push-notifications.module.ts +++ b/src/push-notifications/push-notifications.module.ts @@ -8,9 +8,11 @@ import { PushTokenService } from './push-token.service'; import { PushTemplateService } from './push-template.service'; import { PushMessageService } from './push-message.service'; import { PushTestService } from './push-test.service'; +import { ChallengeReminderService } from './challenge-reminder.service'; import { UserPushToken } from './models/user-push-token.model'; import { PushMessage } from './models/push-message.model'; import { PushTemplate } from './models/push-template.model'; +import { PushReminderHistory } from './models/push-reminder-history.model'; import { ConfigModule } from '@nestjs/config'; import { DatabaseModule } from '../database/database.module'; import { UsersModule } from '../users/users.module'; @@ -27,6 +29,7 @@ import { ChallengeParticipant } from '../challenges/models/challenge-participant UserPushToken, PushMessage, PushTemplate, + PushReminderHistory, ChallengeParticipant, ]), ], @@ -41,6 +44,7 @@ import { ChallengeParticipant } from '../challenges/models/challenge-participant PushTemplateService, PushMessageService, PushTestService, + ChallengeReminderService, ], exports: [ ApnsProvider, @@ -49,6 +53,7 @@ import { ChallengeParticipant } from '../challenges/models/challenge-participant PushTemplateService, PushMessageService, PushTestService, + ChallengeReminderService, ], }) export class PushNotificationsModule { } \ No newline at end of file diff --git a/src/push-notifications/templates/challenge-templates.ts b/src/push-notifications/templates/challenge-templates.ts new file mode 100644 index 0000000..949039e --- /dev/null +++ b/src/push-notifications/templates/challenge-templates.ts @@ -0,0 +1,127 @@ +import { ChallengeType } from '../../challenges/models/challenge.model'; + +/** + * 挑战鼓励文案模板 - 针对已参与挑战的用户 + */ +export const ENCOURAGEMENT_TEMPLATES = { + [ChallengeType.WATER]: [ + { title: '饮水挑战进行中', body: '今天已有多人完成饮水目标!你也要记得多喝水哦!' }, + { title: '健康饮水提醒', body: '加入饮水挑战的小伙伴们今天都很棒!你今天喝水达标了吗?' }, + { title: '水分补充时间', body: '挑战者们都在坚持每日饮水目标,你也要跟上大家的步伐!' }, + { title: '饮水习惯养成', body: '看到很多挑战伙伴都养成了良好饮水习惯,你也是其中一员!' }, + { title: '团队饮水挑战', body: '今天挑战群里又有很多人完成了目标!别掉队,一起加油!' }, + ], + [ChallengeType.EXERCISE]: [ + { title: '运动挑战进行时', body: '今天已有多人完成运动目标!你也来动一动吧!' }, + { title: '活力运动提醒', body: '挑战伙伴们都在坚持运动,每一滴汗水都是进步的见证!' }, + { title: '运动习惯养成', body: '看到很多挑战者都养成了运动习惯,你也是其中一员!' }, + { title: '团队运动挑战', body: '今天运动挑战群里又有很多人完成了目标!别掉队,一起加油!' }, + { title: '运动生活分享', body: '挑战者们都在分享运动心得,你的今天运动了吗?' }, + ], + [ChallengeType.DIET]: [ + { title: '饮食挑战进行中', body: '今天已有多人记录了健康饮食!你也要记得合理搭配哦!' }, + { title: '营养均衡提醒', body: '挑战伙伴们都在坚持健康饮食,为身体提供充足营养!' }, + { title: '健康饮食分享', body: '看到很多挑战者都在分享健康餐,你今天吃了什么?' }, + { title: '团队饮食挑战', body: '今天饮食挑战群里又有很多人完成了目标!一起健康饮食!' }, + { title: '营养生活记录', body: '挑战者们都在记录饮食,关注健康,让每一餐都有意义!' }, + ], + [ChallengeType.MOOD]: [ + { title: '心情记录挑战', body: '今天已有多人记录了心情!你也来分享今天的感受吧!' }, + { title: '情绪管理提醒', body: '挑战伙伴们都在关注情绪变化,让心情更加愉悦平和!' }, + { title: '心情日记分享', body: '看到很多挑战者都在记录心情故事,你今天心情如何?' }, + { title: '团队心情挑战', body: '今天心情挑战群里又有很多人分享了感受!一起关注心理健康!' }, + { title: '情绪健康关怀', body: '挑战者们都在关爱自己的情绪,从记录心情开始!' }, + ], + [ChallengeType.SLEEP]: [ + { title: '睡眠挑战进行中', body: '今天已有多人保持了规律作息!你也要早睡早起哦!' }, + { title: '优质睡眠提醒', body: '挑战伙伴们都在保持良好睡眠质量,享受每一个宁静的夜晚!' }, + { title: '规律作息养成', body: '看到很多挑战者都养成了规律作息,你也是其中一员!' }, + { title: '团队睡眠挑战', body: '今天睡眠挑战群里又有很多人完成了目标!一起健康作息!' }, + { title: '休息时光分享', body: '挑战者们都在分享睡眠心得,你今天休息得好吗?' }, + ], + [ChallengeType.WEIGHT]: [ + { title: '体重管理挑战', body: '今天已有多人记录了体重变化!你也要坚持健康生活方式!' }, + { title: '健康体重提醒', body: '挑战伙伴们都在坚持体重管理,享受轻松自在的生活!' }, + { title: '体重记录分享', body: '看到很多挑战者都在记录体重变化,见证自己的进步!' }, + { title: '团队体重挑战', body: '今天体重挑战群里又有很多人完成了目标!一起健康生活!' }, + { title: '健康生活记录', body: '挑战者们都在坚持健康饮食+适量运动,你也是其中一员!' }, + ], +}; + +/** + * 挑战邀请文案模板 - 针对未参与挑战但有userId的用户 + */ +export const INVITATION_TEMPLATES = { + [ChallengeType.WATER]: [ + { title: '饮水挑战邀请', body: '加入21天饮水挑战,养成健康饮水习惯!' }, + { title: '健康饮水挑战', body: '每天适量饮水,让身体更健康。立即加入挑战!' }, + { title: '饮水习惯养成', body: '挑战自己,养成良好饮水习惯,收获健康!' }, + ], + [ChallengeType.EXERCISE]: [ + { title: '运动挑战邀请', body: '加入运动挑战,让身体充满活力!' }, + { title: '健身挑战', body: '21天运动挑战,塑造更好的自己!' }, + { title: '运动习惯养成', body: '坚持运动,收获健康体魄,立即加入!' }, + ], + [ChallengeType.DIET]: [ + { title: '饮食挑战邀请', body: '加入健康饮食挑战,培养良好饮食习惯!' }, + { title: '营养挑战', body: '21天饮食挑战,让营养均衡成为习惯!' }, + { title: '健康饮食计划', body: '科学饮食,健康生活,从挑战开始!' }, + ], + [ChallengeType.MOOD]: [ + { title: '心情记录挑战', body: '加入心情记录挑战,关注情绪健康!' }, + { title: '情绪管理挑战', body: '21天心情记录,让心灵更加宁静。' }, + { title: '心理健康挑战', body: '记录心情,关爱自己,从现在开始!' }, + ], + [ChallengeType.SLEEP]: [ + { title: '睡眠挑战邀请', body: '加入优质睡眠挑战,养成规律作息!' }, + { title: '作息规律挑战', body: '21天睡眠挑战,让身体得到充分休息。' }, + { title: '健康睡眠计划', body: '改善睡眠质量,享受每一个清晨!' }, + ], + [ChallengeType.WEIGHT]: [ + { title: '体重管理挑战', body: '加入体重管理挑战,收获健康理想体重!' }, + { title: '健康体重挑战', body: '21天体重管理,见证自己的变化!' }, + { title: '体重目标挑战', body: '科学管理体重,享受健康生活!' }, + ], +}; + +/** + * 通用邀请文案模板 - 针对没有userId的用户 + */ +export const GENERAL_INVITATION_TEMPLATES = [ + { title: '健康挑战邀请', body: '加入我们的健康挑战,开启健康生活新篇章!' }, + { title: '21天挑战', body: '21天养成健康习惯,你准备好了吗?' }, + { title: '健康生活', body: '追求健康生活,从参加挑战开始!' }, + { title: '挑战自我', body: '挑战自己,收获健康,让生活更美好!' }, + { title: '健康之旅', body: '开启健康之旅,遇见更好的自己!' }, +]; + +/** + * 随机选择一个模板 + */ +export function getRandomTemplate(templates: T[]): T { + const randomIndex = Math.floor(Math.random() * templates.length); + return templates[randomIndex]; +} + +/** + * 根据挑战类型获取鼓励文案 + */ +export function getEncouragementTemplate(challengeType: ChallengeType) { + const templates = ENCOURAGEMENT_TEMPLATES[challengeType] || ENCOURAGEMENT_TEMPLATES[ChallengeType.EXERCISE]; + return getRandomTemplate(templates); +} + +/** + * 根据挑战类型获取邀请文案 + */ +export function getInvitationTemplate(challengeType: ChallengeType) { + const templates = INVITATION_TEMPLATES[challengeType] || INVITATION_TEMPLATES[ChallengeType.EXERCISE]; + return getRandomTemplate(templates); +} + +/** + * 获取通用邀请文案 + */ +export function getGeneralInvitationTemplate() { + return getRandomTemplate(GENERAL_INVITATION_TEMPLATES); +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 1b10aa2..a787ebb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -884,12 +884,12 @@ "@napi-rs/nice-android-arm-eabi@1.0.1": version "1.0.1" - resolved "https://registry.yarnpkg.com/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.0.1.tgz#9a0cba12706ff56500df127d6f4caf28ddb94936" + resolved "https://mirrors.tencent.com/npm/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.0.1.tgz#9a0cba12706ff56500df127d6f4caf28ddb94936" integrity sha512-5qpvOu5IGwDo7MEKVqqyAxF90I6aLj4n07OzpARdgDRfz8UbBztTByBp0RC59r3J1Ij8uzYi6jI7r5Lws7nn6w== "@napi-rs/nice-android-arm64@1.0.1": version "1.0.1" - resolved "https://registry.yarnpkg.com/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.0.1.tgz#32fc32e9649bd759d2a39ad745e95766f6759d2f" + resolved "https://mirrors.tencent.com/npm/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.0.1.tgz#32fc32e9649bd759d2a39ad745e95766f6759d2f" integrity sha512-GqvXL0P8fZ+mQqG1g0o4AO9hJjQaeYG84FRfZaYjyJtZZZcMjXW5TwkL8Y8UApheJgyE13TQ4YNUssQaTgTyvA== "@napi-rs/nice-darwin-arm64@1.0.1": @@ -899,67 +899,67 @@ "@napi-rs/nice-darwin-x64@1.0.1": version "1.0.1" - resolved "https://registry.yarnpkg.com/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.0.1.tgz#f1b1365a8370c6a6957e90085a9b4873d0e6a957" + resolved "https://mirrors.tencent.com/npm/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.0.1.tgz#f1b1365a8370c6a6957e90085a9b4873d0e6a957" integrity sha512-jXnMleYSIR/+TAN/p5u+NkCA7yidgswx5ftqzXdD5wgy/hNR92oerTXHc0jrlBisbd7DpzoaGY4cFD7Sm5GlgQ== "@napi-rs/nice-freebsd-x64@1.0.1": version "1.0.1" - resolved "https://registry.yarnpkg.com/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.0.1.tgz#4280f081efbe0b46c5165fdaea8b286e55a8f89e" + resolved "https://mirrors.tencent.com/npm/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.0.1.tgz#4280f081efbe0b46c5165fdaea8b286e55a8f89e" integrity sha512-j+iJ/ezONXRQsVIB/FJfwjeQXX7A2tf3gEXs4WUGFrJjpe/z2KB7sOv6zpkm08PofF36C9S7wTNuzHZ/Iiccfw== "@napi-rs/nice-linux-arm-gnueabihf@1.0.1": version "1.0.1" - resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.0.1.tgz#07aec23a9467ed35eb7602af5e63d42c5d7bd473" + resolved "https://mirrors.tencent.com/npm/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.0.1.tgz#07aec23a9467ed35eb7602af5e63d42c5d7bd473" integrity sha512-G8RgJ8FYXYkkSGQwywAUh84m946UTn6l03/vmEXBYNJxQJcD+I3B3k5jmjFG/OPiU8DfvxutOP8bi+F89MCV7Q== "@napi-rs/nice-linux-arm64-gnu@1.0.1": version "1.0.1" - resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.0.1.tgz#038a77134cc6df3c48059d5a5e199d6f50fb9a90" + resolved "https://mirrors.tencent.com/npm/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.0.1.tgz#038a77134cc6df3c48059d5a5e199d6f50fb9a90" integrity sha512-IMDak59/W5JSab1oZvmNbrms3mHqcreaCeClUjwlwDr0m3BoR09ZiN8cKFBzuSlXgRdZ4PNqCYNeGQv7YMTjuA== "@napi-rs/nice-linux-arm64-musl@1.0.1": version "1.0.1" - resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.0.1.tgz#715d0906582ba0cff025109f42e5b84ea68c2bcc" + resolved "https://mirrors.tencent.com/npm/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.0.1.tgz#715d0906582ba0cff025109f42e5b84ea68c2bcc" integrity sha512-wG8fa2VKuWM4CfjOjjRX9YLIbysSVV1S3Kgm2Fnc67ap/soHBeYZa6AGMeR5BJAylYRjnoVOzV19Cmkco3QEPw== "@napi-rs/nice-linux-ppc64-gnu@1.0.1": version "1.0.1" - resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.0.1.tgz#ac1c8f781c67b0559fa7a1cd4ae3ca2299dc3d06" + resolved "https://mirrors.tencent.com/npm/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.0.1.tgz#ac1c8f781c67b0559fa7a1cd4ae3ca2299dc3d06" integrity sha512-lxQ9WrBf0IlNTCA9oS2jg/iAjQyTI6JHzABV664LLrLA/SIdD+I1i3Mjf7TsnoUbgopBcCuDztVLfJ0q9ubf6Q== "@napi-rs/nice-linux-riscv64-gnu@1.0.1": version "1.0.1" - resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.0.1.tgz#b0a430549acfd3920ffd28ce544e2fe17833d263" + resolved "https://mirrors.tencent.com/npm/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.0.1.tgz#b0a430549acfd3920ffd28ce544e2fe17833d263" integrity sha512-3xs69dO8WSWBb13KBVex+yvxmUeEsdWexxibqskzoKaWx9AIqkMbWmE2npkazJoopPKX2ULKd8Fm9veEn0g4Ig== "@napi-rs/nice-linux-s390x-gnu@1.0.1": version "1.0.1" - resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.0.1.tgz#5b95caf411ad72a965885217db378c4d09733e97" + resolved "https://mirrors.tencent.com/npm/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.0.1.tgz#5b95caf411ad72a965885217db378c4d09733e97" integrity sha512-lMFI3i9rlW7hgToyAzTaEybQYGbQHDrpRkg+1gJWEpH0PLAQoZ8jiY0IzakLfNWnVda1eTYYlxxFYzW8Rqczkg== "@napi-rs/nice-linux-x64-gnu@1.0.1": version "1.0.1" - resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.0.1.tgz#a98cdef517549f8c17a83f0236a69418a90e77b7" + resolved "https://mirrors.tencent.com/npm/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.0.1.tgz#a98cdef517549f8c17a83f0236a69418a90e77b7" integrity sha512-XQAJs7DRN2GpLN6Fb+ZdGFeYZDdGl2Fn3TmFlqEL5JorgWKrQGRUrpGKbgZ25UeZPILuTKJ+OowG2avN8mThBA== "@napi-rs/nice-linux-x64-musl@1.0.1": version "1.0.1" - resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.0.1.tgz#5e26843eafa940138aed437c870cca751c8a8957" + resolved "https://mirrors.tencent.com/npm/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.0.1.tgz#5e26843eafa940138aed437c870cca751c8a8957" integrity sha512-/rodHpRSgiI9o1faq9SZOp/o2QkKQg7T+DK0R5AkbnI/YxvAIEHf2cngjYzLMQSQgUhxym+LFr+UGZx4vK4QdQ== "@napi-rs/nice-win32-arm64-msvc@1.0.1": version "1.0.1" - resolved "https://registry.yarnpkg.com/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.0.1.tgz#bd62617d02f04aa30ab1e9081363856715f84cd8" + resolved "https://mirrors.tencent.com/npm/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.0.1.tgz#bd62617d02f04aa30ab1e9081363856715f84cd8" integrity sha512-rEcz9vZymaCB3OqEXoHnp9YViLct8ugF+6uO5McifTedjq4QMQs3DHz35xBEGhH3gJWEsXMUbzazkz5KNM5YUg== "@napi-rs/nice-win32-ia32-msvc@1.0.1": version "1.0.1" - resolved "https://registry.yarnpkg.com/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.0.1.tgz#b8b7aad552a24836027473d9b9f16edaeabecf18" + resolved "https://mirrors.tencent.com/npm/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.0.1.tgz#b8b7aad552a24836027473d9b9f16edaeabecf18" integrity sha512-t7eBAyPUrWL8su3gDxw9xxxqNwZzAqKo0Szv3IjVQd1GpXXVkb6vBBQUuxfIYaXMzZLwlxRQ7uzM2vdUE9ULGw== "@napi-rs/nice-win32-x64-msvc@1.0.1": version "1.0.1" - resolved "https://registry.yarnpkg.com/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.0.1.tgz#37d8718b8f722f49067713e9f1e85540c9a3dd09" + resolved "https://mirrors.tencent.com/npm/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.0.1.tgz#37d8718b8f722f49067713e9f1e85540c9a3dd09" integrity sha512-JlF+uDcatt3St2ntBG8H02F1mM45i5SF9W+bIKiReVE6wiy3o16oBP/yxt+RZ+N6LbCImJXJ6bXNO2kn9AXicg== "@napi-rs/nice@^1.0.1": @@ -1063,6 +1063,13 @@ path-to-regexp "8.2.0" tslib "2.8.1" +"@nestjs/schedule@^6.0.1": + version "6.0.1" + resolved "https://mirrors.tencent.com/npm/@nestjs/schedule/-/schedule-6.0.1.tgz" + integrity sha512-v3yO6cSPAoBSSyH67HWnXHzuhPhSNZhRmLY38JvCt2sqY8sPMOODpcU1D79iUMFf7k16DaMEbL4Mgx61ZhiC8Q== + dependencies: + cron "4.3.3" + "@nestjs/schematics@^11.0.0", "@nestjs/schematics@^11.0.1": version "11.0.2" resolved "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.2.tgz" @@ -1247,47 +1254,47 @@ "@swc/core-darwin-x64@1.11.13": version "1.11.13" - resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.11.13.tgz#9cad870d48ebff805e8946ddcbe3d8312182f70b" + resolved "https://mirrors.tencent.com/npm/@swc/core-darwin-x64/-/core-darwin-x64-1.11.13.tgz#9cad870d48ebff805e8946ddcbe3d8312182f70b" integrity sha512-uSA4UwgsDCIysUPfPS8OrQTH2h9spO7IYFd+1NB6dJlVGUuR6jLKuMBOP1IeLeax4cGHayvkcwSJ3OvxHwgcZQ== "@swc/core-linux-arm-gnueabihf@1.11.13": version "1.11.13" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.13.tgz#51839e5a850bfa300e2c838fee8379e4dba1de78" + resolved "https://mirrors.tencent.com/npm/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.13.tgz#51839e5a850bfa300e2c838fee8379e4dba1de78" integrity sha512-boVtyJzS8g30iQfe8Q46W5QE/cmhKRln/7NMz/5sBP/am2Lce9NL0d05NnFwEWJp1e2AMGHFOdRr3Xg1cDiPKw== "@swc/core-linux-arm64-gnu@1.11.13": version "1.11.13" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.13.tgz#4145f1e504bdfa92604aee883d777bc8c4fba5d7" + resolved "https://mirrors.tencent.com/npm/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.13.tgz#4145f1e504bdfa92604aee883d777bc8c4fba5d7" integrity sha512-+IK0jZ84zHUaKtwpV+T+wT0qIUBnK9v2xXD03vARubKF+eUqCsIvcVHXmLpFuap62dClMrhCiwW10X3RbXNlHw== "@swc/core-linux-arm64-musl@1.11.13": version "1.11.13" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.13.tgz#b1813ae2e99e386ca16fff5af6601ac45ef57c5b" + resolved "https://mirrors.tencent.com/npm/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.13.tgz#b1813ae2e99e386ca16fff5af6601ac45ef57c5b" integrity sha512-+ukuB8RHD5BHPCUjQwuLP98z+VRfu+NkKQVBcLJGgp0/+w7y0IkaxLY/aKmrAS5ofCNEGqKL+AOVyRpX1aw+XA== "@swc/core-linux-x64-gnu@1.11.13": version "1.11.13" - resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.13.tgz#13b89a0194c4033c01400e9c65d9c21c56a4a6cd" + resolved "https://mirrors.tencent.com/npm/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.13.tgz#13b89a0194c4033c01400e9c65d9c21c56a4a6cd" integrity sha512-q9H3WI3U3dfJ34tdv60zc8oTuWvSd5fOxytyAO9Pc5M82Hic3jjWaf2xBekUg07ubnMZpyfnv+MlD+EbUI3Llw== "@swc/core-linux-x64-musl@1.11.13": version "1.11.13" - resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.13.tgz#0d0e5aa889dd4da69723e2287c3c1714d9bfd8aa" + resolved "https://mirrors.tencent.com/npm/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.13.tgz#0d0e5aa889dd4da69723e2287c3c1714d9bfd8aa" integrity sha512-9aaZnnq2pLdTbAzTSzy/q8dr7Woy3aYIcQISmw1+Q2/xHJg5y80ZzbWSWKYca/hKonDMjIbGR6dp299I5J0aeA== "@swc/core-win32-arm64-msvc@1.11.13": version "1.11.13" - resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.13.tgz#ad7281f9467e3de09f52615afe2276a8ef738a9d" + resolved "https://mirrors.tencent.com/npm/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.13.tgz#ad7281f9467e3de09f52615afe2276a8ef738a9d" integrity sha512-n3QZmDewkHANcoHvtwvA6yJbmS4XJf0MBMmwLZoKDZ2dOnC9D/jHiXw7JOohEuzYcpLoL5tgbqmjxa3XNo9Oow== "@swc/core-win32-ia32-msvc@1.11.13": version "1.11.13" - resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.13.tgz#046f6dbddb5b69a29bbaa98de104090a46088b74" + resolved "https://mirrors.tencent.com/npm/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.13.tgz#046f6dbddb5b69a29bbaa98de104090a46088b74" integrity sha512-wM+Nt4lc6YSJFthCx3W2dz0EwFNf++j0/2TQ0Js9QLJuIxUQAgukhNDVCDdq8TNcT0zuA399ALYbvj5lfIqG6g== "@swc/core-win32-x64-msvc@1.11.13": version "1.11.13" - resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.13.tgz#0412620d8594a7d3e482d3e79d9e89d80f9a14c0" + resolved "https://mirrors.tencent.com/npm/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.13.tgz#0412620d8594a7d3e482d3e79d9e89d80f9a14c0" integrity sha512-+X5/uW3s1L5gK7wAo0E27YaAoidJDo51dnfKSfU7gF3mlEUuWH8H1bAy5OTt2mU4eXtfsdUMEVXSwhDlLtQkuA== "@swc/core@^1.10.7": @@ -1559,6 +1566,11 @@ resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz" integrity sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g== +"@types/luxon@~3.7.0": + version "3.7.1" + resolved "https://mirrors.tencent.com/npm/@types/luxon/-/luxon-3.7.1.tgz" + integrity sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg== + "@types/methods@^1.1.4": version "1.1.4" resolved "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz" @@ -2954,6 +2966,14 @@ create-require@^1.1.0: resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +cron@4.3.3: + version "4.3.3" + resolved "https://mirrors.tencent.com/npm/cron/-/cron-4.3.3.tgz" + integrity sha512-B/CJj5yL3sjtlun6RtYHvoSB26EmQ2NUmhq9ZiJSyKIM4K/fqfh9aelDFlIayD2YMeFZqWLi9hHV+c+pq2Djkw== + dependencies: + "@types/luxon" "~3.7.0" + luxon "~3.7.0" + croner@~4.1.92: version "4.1.97" resolved "https://mirrors.tencent.com/npm/croner/-/croner-4.1.97.tgz" @@ -2992,7 +3012,7 @@ data-uri-to-buffer@^6.0.2: dayjs@^1.11.18, dayjs@~1.11.13: version "1.11.18" - resolved "https://mirrors.tencent.com/npm/dayjs/-/dayjs-1.11.18.tgz" + resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz" integrity sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA== dayjs@~1.8.24: @@ -5223,6 +5243,11 @@ lru.min@^1.0.0: resolved "https://registry.npmjs.org/lru.min/-/lru.min-1.1.2.tgz" integrity sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg== +luxon@~3.7.0: + version "3.7.2" + resolved "https://mirrors.tencent.com/npm/luxon/-/luxon-3.7.2.tgz" + integrity sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew== + magic-string@0.30.17: version "0.30.17" resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz"