SignIn-With-Redis
在现代应用开发中,签到打卡功能广泛应用于企业考勤管理、在线教育、社区运营等多个领域。它不仅是一种记录用户行为的方式,也是提升用户粘性和活跃度的重要手段。本文将介绍5种签到打卡的实现方案。
一、基于关系型数据库的传统签到系统
1.1 基本原理
最直接的签到系统实现方式是利用关系型数据库(如MySQL、PostgreSQL)记录每次签到行为。这种方案设计简单,易于理解和实现,适合大多数中小型应用场景。
1.2 数据模型设计
-- 用户表
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 签到记录表
CREATE TABLE check_ins (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
check_in_time TIMESTAMP NOT NULL,
check_in_date DATE NOT NULL,
check_in_type VARCHAR(20) NOT NULL, -- 'DAILY', 'COURSE', 'MEETING' 等
location VARCHAR(255),
device_info VARCHAR(255),
remark VARCHAR(255),
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE KEY unique_user_date (user_id, check_in_date, check_in_type)
);
-- 签到统计表
CREATE TABLE check_in_stats (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
total_days INT DEFAULT 0,
continuous_days INT DEFAULT 0,
last_check_in_date DATE,
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE KEY unique_user (user_id)
);
1.3 核心代码实现实体类设计
@Data
@TableName("check_ins")
public class CheckIn {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@TableField("user_id")
private Long userId;
@TableField("check_in_time")
private LocalDateTime checkInTime;
@TableField("check_in_date")
private LocalDate checkInDate;
@TableField("check_in_type")
private String checkInType;
private String location;
@TableField("device_info")
private String deviceInfo;
private String remark;
}
@Data
@TableName("check_in_stats")
public class CheckInStats {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@TableField("user_id")
private Long userId;
@TableField("total_days")
private Integer totalDays = 0;
@TableField("continuous_days")
private Integer continuousDays = 0;
@TableField("last_check_in_date")
private LocalDate lastCheckInDate;
}
@Data
@TableName("users")
public class User {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
private String username;
private String password;
private String email;
@TableField("created_at")
private LocalDateTime createdAt;
}
Mapper层
@Mapper
public interface CheckInMapper extends BaseMapper<CheckIn> {
@Select("SELECT COUNT(*) FROM check_ins WHERE user_id = #{userId} AND check_in_type = #{type}")
int countByUserIdAndType(@Param("userId") Long userId, @Param("type") String type);
@Select("SELECT * FROM check_ins WHERE user_id = #{userId} AND check_in_date BETWEEN #{startDate} AND #{endDate} ORDER BY check_in_date ASC")
List<CheckIn> findByUserIdAndDateBetween(
@Param("userId") Long userId,
@Param("startDate") LocalDate startDate,
@Param("endDate") LocalDate endDate);
@Select("SELECT COUNT(*) FROM check_ins WHERE user_id = #{userId} AND check_in_date = #{date} AND check_in_type = #{type}")
int existsByUserIdAndDateAndType(
@Param("userId") Long userId,
@Param("date") LocalDate date,
@Param("type") String type);
}
@Mapper
public interface CheckInStatsMapper extends BaseMapper<CheckInStats> {
@Select("SELECT * FROM check_in_stats WHERE user_id = #{userId}")
CheckInStats findByUserId(@Param("userId") Long userId);
}
@Mapper
public interface UserMapper extends BaseMapper<User> {
@Select("SELECT * FROM users WHERE username = #{username}")
User findByUsername(@Param("username") String username);
}
Service层
@Service
@Transactional
public class CheckInService {
@Autowired
private CheckInMapper checkInMapper;
@Autowired
private CheckInStatsMapper checkInStatsMapper;
@Autowired
private UserMapper userMapper;
/**
* 用户签到
*/
public CheckIn checkIn(Long userId, String type, String location, String deviceInfo, String remark) {
// 检查用户是否存在
User user = userMapper.selectById(userId);
if (user == null) {
throw new RuntimeException("User not found");
}
LocalDate today = LocalDate.now();
// 检查今天是否已经签到
if (checkInMapper.existsByUserIdAndDateAndType(userId, today, type) > 0) {
thrownew RuntimeException("Already checked in today");
}
// 创建签到记录
CheckIn checkIn = new CheckIn();
checkIn.setUserId(userId);
checkIn.setCheckInTime(LocalDateTime.now());
checkIn.setCheckInDate(today);
checkIn.setCheckInType(type);
checkIn.setLocation(location);
checkIn.setDeviceInfo(deviceInfo);
checkIn.setRemark(remark);
checkInMapper.insert(checkIn);
// 更新签到统计
updateCheckInStats(userId, today);
return checkIn;
}
/**
* 更新签到统计信息
*/
private void updateCheckInStats(Long userId, LocalDate today) {
CheckInStats stats = checkInStatsMapper.findByUserId(userId);
if (stats == null) {
stats = new CheckInStats();
stats.setUserId(userId);
stats.setTotalDays(1);
stats.setContinuousDays(1);
stats.setLastCheckInDate(today);
checkInStatsMapper.insert(stats);
} else {
// 更新总签到天数
stats.setTotalDays(stats.getTotalDays() + 1);
// 更新连续签到天数
if (stats.getLastCheckInDate() != null) {
if (today.minusDays(1).equals(stats.getLastCheckInDate())) {
// 连续签到
stats.setContinuousDays(stats.getContinuousDays() + 1);
} elseif (today.equals(stats.getLastCheckInDate())) {
// 当天重复签到,不计算连续天数
} else {
// 中断连续签到
stats.setContinuousDays(1);
}
}
stats.setLastCheckInDate(today);
checkInStatsMapper.updateById(stats);
}
}
/**
* 获取用户签到统计
*/
public CheckInStats getCheckInStats(Long userId) {
CheckInStats stats = checkInStatsMapper.findByUserId(userId);
if (stats == null) {
throw new RuntimeException("Check-in stats not found");
}
return stats;
}
/**
* 获取用户指定日期范围内的签到记录
*/
public List<CheckIn> getCheckInHistory(Long userId, LocalDate startDate, LocalDate endDate) {
return checkInMapper.findByUserIdAndDateBetween(userId, startDate, endDate);
}
}
Controller层
@RestController
@RequestMapping("/api/check-ins")
public class CheckInController {
@Autowired
private CheckInService checkInService;
@PostMapping
public ResponseEntity<CheckIn> checkIn(@RequestBody CheckInRequest request) {
CheckIn checkIn = checkInService.checkIn(
request.getUserId(),
request.getType(),
request.getLocation(),
request.getDeviceInfo(),
request.getRemark()
);
return ResponseEntity.ok(checkIn);
}
@GetMapping("/stats/{userId}")
public ResponseEntity<CheckInStats> getStats(@PathVariable Long userId) {
CheckInStats stats = checkInService.getCheckInStats(userId);
return ResponseEntity.ok(stats);
}
@GetMapping("/history/{userId}")
public ResponseEntity<List<CheckIn>> getHistory(
@PathVariable Long userId,
@RequestParam@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
@RequestParam@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
List<CheckIn> history = checkInService.getCheckInHistory(userId, startDate, endDate);
return ResponseEntity.ok(history);
}
}
@Data
public class CheckInRequest {
private Long userId;
private String type;
private String location;
private String deviceInfo;
private String remark;
}
1.4 优缺点分析
优点:
- 设计简单直观,易于理解和实现
- 支持丰富的数据查询和统计功能
- 事务支持,确保数据一致性
- 易于与现有系统集成
缺点:
- 数据量大时查询性能可能下降
- 连续签到统计等复杂查询逻辑实现相对繁琐
- 不适合高并发场景
- 数据库负载较高