如何实现每日签到送积分的逻辑
2025-08-04 23:56 SeektonYe 阅读(157) 评论(0) 收藏 举报如何实现每日签到送积分的逻辑:从数据库设计到业务实现
在现代 Web 应用中,用户留存是产品成功的关键指标之一。每日签到系统作为一种经典的用户激励机制,能够有效提升用户的日活跃度和粘性。本文将详细介绍如何在 Next.js + TypeScript + Drizzle ORM 的技术栈下,实现一个完整的每日签到送积分系统。
系统概述
我们要实现的每日签到系统具备以下核心功能:
- 用户每日只能签到一次
- 连续签到有额外奖励机制
- 积分自动发放到用户账户
- 完整的签到状态查询
- 防重复签到的安全机制
这套系统已经在 Monet Vision 平台上成功运行,为用户提供了稳定可靠的每日积分奖励服务。
数据库设计
核心表结构
首先,我们需要设计两个核心数据表:
1. 每日签到记录表 (daily_check_in)
CREATE TABLE `daily_check_in` (
`id` text PRIMARY KEY NOT NULL,
`userId` text NOT NULL,
`checkInDate` text NOT NULL,
`creditsEarned` integer DEFAULT 5 NOT NULL,
`consecutiveDays` integer DEFAULT 1 NOT NULL,
`createdAt` integer NOT NULL,
FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action
);
2. 使用记录表 (usage_log)
ALTER TABLE `usage_log` ADD `source` text;
索引优化
为了提升查询性能,我们创建了以下索引:
CREATE INDEX `daily_check_in_user_id_idx` ON `daily_check_in` (`userId`);
CREATE INDEX `daily_check_in_date_idx` ON `daily_check_in` (`checkInDate`);
CREATE INDEX `daily_check_in_user_date_idx` ON `daily_check_in` (`userId`,`checkInDate`);
CREATE INDEX `usage_log_source_idx` ON `usage_log` (`source`);
这些索引确保了:
- 按用户查询签到记录的高效性
- 按日期查询的快速响应
- 用户+日期的复合查询优化
- 积分来源的快速筛选
Drizzle ORM Schema 定义
使用 Drizzle ORM 定义数据表结构:
// 每日签到记录表
export const dailyCheckInTable = sqliteTable(
"daily_check_in",
{
id: text("id")
.primaryKey()
.$defaultFn(() => createId()),
userId: text("userId")
.notNull()
.references(() => userTable.id),
checkInDate: text("checkInDate").notNull(), // 格式: "2025-01-15"
creditsEarned: integer("creditsEarned").notNull().default(5),
consecutiveDays: integer("consecutiveDays").notNull().default(1),
createdAt: integer("createdAt", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
},
(table) => [
index("daily_check_in_user_id_idx").on(table.userId),
index("daily_check_in_date_idx").on(table.checkInDate),
index("daily_check_in_user_date_idx").on(table.userId, table.checkInDate),
],
);
核心业务逻辑实现
1. 签到状态检查
首先实现检查用户今日是否已签到的功能:
export async function getTodayCheckInStatus(userId: string): Promise<{
hasCheckedIn: boolean;
checkIn?: {
creditsEarned: number;
consecutiveDays: number;
};
}> {
const db = getDB();
const today = new Date().toISOString().split("T")[0]; // 格式: "2025-01-15"
const todayCheckIn = await db.query.dailyCheckInTable.findFirst({
where: and(
eq(dailyCheckInTable.userId, userId),
eq(dailyCheckInTable.checkInDate, today),
),
});
if (todayCheckIn) {
return {
hasCheckedIn: true,
checkIn: {
creditsEarned: todayCheckIn.creditsEarned,
consecutiveDays: todayCheckIn.consecutiveDays,
},
};
}
return { hasCheckedIn: false };
}
2. 连续签到天数计算
连续签到天数的计算是系统的核心逻辑之一:
async function calculateConsecutiveDays(userId: string): Promise<number> {
const db = getDB();
// 获取用户最近的签到记录,按日期倒序
const recentCheckIns = await db.query.dailyCheckInTable.findMany({
where: eq(dailyCheckInTable.userId, userId),
orderBy: [desc(dailyCheckInTable.checkInDate)],
limit: 100, // 最多查询100天
});
if (recentCheckIns.length === 0) {
return 1; // 首次签到
}
let consecutiveDays = 1; // 包括今天
const today = new Date();
// 从昨天开始检查连续性
for (let i = 1; i < 100; i++) {
const checkDate = new Date(today);
checkDate.setDate(checkDate.getDate() - i);
const dateString = checkDate.toISOString().split("T")[0];
const hasCheckIn = recentCheckIns.some(
(checkIn) => checkIn.checkInDate === dateString,
);
if (hasCheckIn) {
consecutiveDays++;
} else {
break; // 中断连续签到
}
}
return consecutiveDays;
}
3. 积分奖励计算
基于连续签到天数计算奖励积分:
function calculateCheckInCredits(consecutiveDays: number): number {
// 基础积分: 5
// 连续签到奖励:连续7天额外+5积分,最大总积分限制为10
const baseCredits = 5;
const bonusCredits = Math.min(Math.floor((consecutiveDays - 1) / 7) * 5, 5);
const totalCredits = baseCredits + bonusCredits;
return Math.min(totalCredits, 10); // 确保总积分不超过10
}
这个算法的设计思路:
- 基础奖励:每次签到获得 5 积分
- 连续奖励:每连续 7 天额外获得 5 积分
- 上限控制:单次签到最多获得 10 积分
4. 执行签到操作
完整的签到流程实现:
export async function performDailyCheckIn(): Promise<DailyCheckInResult> {
try {
const t = await getTranslations("user");
const session = await getSession();
const userId = session?.user?.id;
if (!userId) {
return {
success: false,
message: t("checkin_not_logged_in"),
};
}
// 检查今日是否已签到
const todayStatus = await getTodayCheckInStatus(userId);
if (todayStatus.hasCheckedIn) {
return {
success: false,
message: t("checkin_already_done"),
data: {
creditsEarned: todayStatus.checkIn!.creditsEarned,
consecutiveDays: todayStatus.checkIn!.consecutiveDays,
alreadyCheckedIn: true,
},
};
}
const db = getDB();
const today = new Date().toISOString().split("T")[0];
// 计算连续签到天数
const consecutiveDays = await calculateConsecutiveDays(userId);
// 计算奖励积分
const creditsEarned = calculateCheckInCredits(consecutiveDays);
// 记录签到
await db.insert(dailyCheckInTable).values({
userId,
checkInDate: today,
creditsEarned,
consecutiveDays,
createdAt: new Date(),
});
// 增加用户积分
const addCreditsSuccess = await addCreditsUsage(
userId,
creditsEarned,
"daily_checkin",
today,
);
if (!addCreditsSuccess) {
// 如果积分添加失败,删除签到记录
await db
.delete(dailyCheckInTable)
.where(
and(
eq(dailyCheckInTable.userId, userId),
eq(dailyCheckInTable.checkInDate, today),
),
);
return {
success: false,
message: t("checkin_error"),
};
}
return {
success: true,
message: t("checkin_success", {
credits: creditsEarned,
days: consecutiveDays,
}),
data: {
creditsEarned,
consecutiveDays,
alreadyCheckedIn: false,
},
};
} catch (error) {
console.error("Daily check-in error:", error);
const t = await getTranslations("user");
return {
success: false,
message: t("checkin_system_error"),
};
}
}
关键技术要点
1. 事务处理
在签到过程中,我们需要确保数据的一致性。如果积分添加失败,必须回滚签到记录:
// 增加用户积分
const addCreditsSuccess = await addCreditsUsage(
userId,
creditsEarned,
"daily_checkin",
today,
);
if (!addCreditsSuccess) {
// 如果积分添加失败,删除签到记录
await db
.delete(dailyCheckInTable)
.where(
and(
eq(dailyCheckInTable.userId, userId),
eq(dailyCheckInTable.checkInDate, today),
),
);
return { success: false, message: t("checkin_error") };
}
2. 日期处理
使用 ISO 日期格式确保跨时区的一致性:
const today = new Date().toISOString().split("T")[0]; // "2025-01-15"
3. 防重复机制
通过数据库索引和业务逻辑双重保障防止重复签到:
- 数据库层面:
daily_check_in_user_date_idx
复合索引 - 业务层面:签到前检查当日状态
4. 国际化支持
使用 next-intl 提供多语言支持:
const t = await getTranslations("user");
return {
success: true,
message: t("checkin_success", {
credits: creditsEarned,
days: consecutiveDays,
}),
};
性能优化策略
1. 查询优化
- 使用复合索引优化用户+日期查询
- 限制历史记录查询数量(limit: 100)
- 按需查询,避免全表扫描
2. 缓存策略
可以考虑在 Redis 或 Cloudflare KV 中缓存用户的签到状态:
// 伪代码示例
const cacheKey = `checkin:${userId}:${today}`;
const cached = await kv.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
3. 异步处理
对于积分发放等非核心流程,可以考虑异步处理以提升响应速度。
安全考虑
1. 身份验证
确保只有登录用户才能执行签到操作:
const session = await getSession();
const userId = session?.user?.id;
if (!userId) {
return { success: false, message: t("checkin_not_logged_in") };
}
2. 防刷机制
- 数据库约束防止重复签到
- 业务逻辑二次验证
- 可考虑添加 IP 限制或验证码
3. 数据完整性
使用外键约束确保数据关联的完整性:
FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action
扩展功能
1. 签到统计
可以添加用户签到统计功能:
export async function getUserCheckInStats(userId: string) {
const db = getDB();
const stats = await db
.select({
totalDays: count(),
totalCredits: sum(dailyCheckInTable.creditsEarned),
maxConsecutive: max(dailyCheckInTable.consecutiveDays),
})
.from(dailyCheckInTable)
.where(eq(dailyCheckInTable.userId, userId));
return stats[0];
}
2. 签到排行榜
实现用户签到排行榜功能:
export async function getCheckInLeaderboard(limit: number = 10) {
const db = getDB();
return await db
.select({
userId: dailyCheckInTable.userId,
maxConsecutive: max(dailyCheckInTable.consecutiveDays),
totalCredits: sum(dailyCheckInTable.creditsEarned),
})
.from(dailyCheckInTable)
.groupBy(dailyCheckInTable.userId)
.orderBy(desc(max(dailyCheckInTable.consecutiveDays)))
.limit(limit);
}
3. 特殊活动
可以根据特殊节日或活动调整奖励规则:
function calculateSpecialEventCredits(
consecutiveDays: number,
eventType?: string,
): number {
const baseCredits = calculateCheckInCredits(consecutiveDays);
switch (eventType) {
case "double_credits":
return baseCredits * 2;
case "new_year":
return baseCredits + 10;
default:
return baseCredits;
}
}
总结
通过以上的设计和实现,我们构建了一个完整、可靠的每日签到送积分系统。该系统具备以下特点:
- 数据一致性:通过事务处理确保数据的完整性
- 高性能:合理的索引设计和查询优化
- 可扩展性:模块化的设计便于功能扩展
- 安全性:完善的身份验证和防刷机制
- 用户体验:国际化支持和友好的错误提示
这套系统已经在 Monet Vision 等多个生产环境中稳定运行,为用户提供了良好的签到体验。通过合理的奖励机制设计,有效提升了用户的日活跃度和平台粘性。
在实际应用中,你可以根据具体的业务需求调整奖励规则、添加更多的统计功能,或者集成到现有的用户成长体系中。希望这篇文章能够帮助你快速实现自己的每日签到系统!