欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

SpringBoot實(shí)現(xiàn)簽到打卡功能的五種方案

 更新時(shí)間:2025年06月10日 08:40:03   作者:風(fēng)象南  
在現(xiàn)代應(yīng)用開發(fā)中,簽到打卡功能廣泛應(yīng)用于企業(yè)考勤管理、在線教育、社區(qū)運(yùn)營等多個(gè)領(lǐng)域,它不僅是一種記錄用戶行為的方式,也是提升用戶粘性和活躍度的重要手段,本文將介紹5種簽到打卡的實(shí)現(xiàn)方案,需要的朋友可以參考下

一、基于關(guān)系型數(shù)據(jù)庫的傳統(tǒng)簽到系統(tǒng)

1.1 基本原理

最直接的簽到系統(tǒng)實(shí)現(xiàn)方式是利用關(guān)系型數(shù)據(jù)庫(如MySQL、PostgreSQL)記錄每次簽到行為。

這種方案設(shè)計(jì)簡單,易于理解和實(shí)現(xiàn),適合大多數(shù)中小型應(yīng)用場景。

1.2 數(shù)據(jù)模型設(shè)計(jì)

-- 用戶表
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)
);

-- 簽到統(tǒng)計(jì)表
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 核心代碼實(shí)現(xiàn)

實(shí)體類設(shè)計(jì)

@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();
        
        // 檢查今天是否已經(jīng)簽到
        if (checkInMapper.existsByUserIdAndDateAndType(userId, today, type) > 0) {
            throw new RuntimeException("Already checked in today");
        }
        
        // 創(chuàng)建簽到記錄
        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);
        
        // 更新簽到統(tǒng)計(jì)
        updateCheckInStats(userId, today);
        
        return checkIn;
    }
    
    /**
     * 更新簽到統(tǒng)計(jì)信息
     */
    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 {
            // 更新總簽到天數(shù)
            stats.setTotalDays(stats.getTotalDays() + 1);
            
            // 更新連續(xù)簽到天數(shù)
            if (stats.getLastCheckInDate() != null) {
                if (today.minusDays(1).equals(stats.getLastCheckInDate())) {
                    // 連續(xù)簽到
                    stats.setContinuousDays(stats.getContinuousDays() + 1);
                } else if (today.equals(stats.getLastCheckInDate())) {
                    // 當(dāng)天重復(fù)簽到,不計(jì)算連續(xù)天數(shù)
                } else {
                    // 中斷連續(xù)簽到
                    stats.setContinuousDays(1);
                }
            }
            
            stats.setLastCheckInDate(today);
            
            checkInStatsMapper.updateById(stats);
        }
    }
    
    /**
     * 獲取用戶簽到統(tǒng)計(jì)
     */
    public CheckInStats getCheckInStats(Long userId) {
        CheckInStats stats = checkInStatsMapper.findByUserId(userId);
        if (stats == null) {
            throw new RuntimeException("Check-in stats not found");
        }
        return stats;
    }
    
    /**
     * 獲取用戶指定日期范圍內(nèi)的簽到記錄
     */
    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 優(yōu)缺點(diǎn)分析

優(yōu)點(diǎn):

  • 設(shè)計(jì)簡單直觀,易于理解和實(shí)現(xiàn)
  • 支持豐富的數(shù)據(jù)查詢和統(tǒng)計(jì)功能
  • 事務(wù)支持,確保數(shù)據(jù)一致性
  • 易于與現(xiàn)有系統(tǒng)集成

缺點(diǎn):

  • 數(shù)據(jù)量大時(shí)查詢性能可能下降
  • 連續(xù)簽到統(tǒng)計(jì)等復(fù)雜查詢邏輯實(shí)現(xiàn)相對繁瑣
  • 不適合高并發(fā)場景
  • 數(shù)據(jù)庫負(fù)載較高

1.5 適用場景

  • 中小型企業(yè)的員工考勤系統(tǒng)
  • 課程簽到系統(tǒng)
  • 會(huì)議簽到管理
  • 用戶量不大的社區(qū)簽到功能

二、基于Redis的高性能簽到系統(tǒng)

2.1 基本原理

利用Redis的高性能和豐富的數(shù)據(jù)結(jié)構(gòu),可以構(gòu)建一個(gè)響應(yīng)迅速的簽到系統(tǒng)。尤其是對于高并發(fā)場景和需要實(shí)時(shí)統(tǒng)計(jì)的應(yīng)用,Redis提供了顯著的性能優(yōu)勢。

2.2 系統(tǒng)設(shè)計(jì)

Redis中我們可以使用以下幾種數(shù)據(jù)結(jié)構(gòu)來實(shí)現(xiàn)簽到系統(tǒng):

  • String: 記錄用戶最后簽到時(shí)間和連續(xù)簽到天數(shù)
  • Hash: 存儲(chǔ)用戶當(dāng)天的簽到詳情
  • Sorted Set: 按簽到時(shí)間排序的用戶列表,便于排行榜等功能
  • Set: 記錄每天簽到的用戶集合

2.3 核心代碼實(shí)現(xiàn)

Redis配置

@Configuration
public class RedisConfig {
    
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        
        // 使用Jackson2JsonRedisSerializer序列化和反序列化redis的value值
        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
                ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        serializer.setObjectMapper(mapper);
        
        template.setValueSerializer(serializer);
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);
        template.afterPropertiesSet();
        
        return template;
    }
}

簽到服務(wù)實(shí)現(xiàn)

@Service
public class RedisCheckInService {
    
    private static final String USER_CHECKIN_KEY = "checkin:user:";
    private static final String DAILY_CHECKIN_KEY = "checkin:daily:";
    private static final String CHECKIN_RANK_KEY = "checkin:rank:";
    private static final String USER_STATS_KEY = "checkin:stats:";
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    /**
     * 用戶簽到
     */
    public boolean checkIn(Long userId, String location) {
        String today = LocalDate.now().format(DateTimeFormatter.ISO_DATE);
        String userKey = USER_CHECKIN_KEY + userId;
        String dailyKey = DAILY_CHECKIN_KEY + today;
        
        // 判斷用戶今天是否已經(jīng)簽到
        Boolean isMember = redisTemplate.opsForSet().isMember(dailyKey, userId);
        if (isMember != null && isMember) {
            return false; // 已經(jīng)簽到過了
        }
        
        // 記錄用戶簽到
        redisTemplate.opsForSet().add(dailyKey, userId);
        
        // 設(shè)置過期時(shí)間(35天后過期,以便統(tǒng)計(jì)連續(xù)簽到)
        redisTemplate.expire(dailyKey, 35, TimeUnit.DAYS);
        
        // 記錄用戶簽到詳情
        Map<String, String> checkInInfo = new HashMap<>();
        checkInInfo.put("time", LocalDateTime.now().toString());
        checkInInfo.put("location", location);
        redisTemplate.opsForHash().putAll(userKey + ":" + today, checkInInfo);
        
        // 更新簽到排行榜
        redisTemplate.opsForZSet().incrementScore(CHECKIN_RANK_KEY + today, userId, 1);
        
        // 更新用戶簽到統(tǒng)計(jì)
        updateUserCheckInStats(userId);
        
        return true;
    }
    
    /**
     * 更新用戶簽到統(tǒng)計(jì)
     */
    private void updateUserCheckInStats(Long userId) {
        String userStatsKey = USER_STATS_KEY + userId;
        
        // 獲取當(dāng)前日期
        LocalDate today = LocalDate.now();
        String todayStr = today.format(DateTimeFormatter.ISO_DATE);
        
        // 獲取用戶最后簽到日期
        String lastCheckInDate = (String) redisTemplate.opsForHash().get(userStatsKey, "lastCheckInDate");
        
        // 更新總簽到天數(shù)
        redisTemplate.opsForHash().increment(userStatsKey, "totalDays", 1);
        
        // 更新連續(xù)簽到天數(shù)
        if (lastCheckInDate != null) {
            LocalDate lastDate = LocalDate.parse(lastCheckInDate, DateTimeFormatter.ISO_DATE);
            
            if (today.minusDays(1).equals(lastDate)) {
                // 連續(xù)簽到
                redisTemplate.opsForHash().increment(userStatsKey, "continuousDays", 1);
            } else if (today.equals(lastDate)) {
                // 當(dāng)天重復(fù)簽到,不增加連續(xù)天數(shù)
            } else {
                // 中斷連續(xù)簽到
                redisTemplate.opsForHash().put(userStatsKey, "continuousDays", 1);
            }
        } else {
            // 第一次簽到
            redisTemplate.opsForHash().put(userStatsKey, "continuousDays", 1);
        }
        
        // 更新最后簽到日期
        redisTemplate.opsForHash().put(userStatsKey, "lastCheckInDate", todayStr);
    }
    
    /**
     * 獲取用戶簽到統(tǒng)計(jì)信息
     */
    public Map<Object, Object> getUserCheckInStats(Long userId) {
        String userStatsKey = USER_STATS_KEY + userId;
        return redisTemplate.opsForHash().entries(userStatsKey);
    }
    
    /**
     * 獲取用戶是否已簽到
     */
    public boolean isUserCheckedInToday(Long userId) {
        String today = LocalDate.now().format(DateTimeFormatter.ISO_DATE);
        String dailyKey = DAILY_CHECKIN_KEY + today;
        
        Boolean isMember = redisTemplate.opsForSet().isMember(dailyKey, userId);
        return isMember != null && isMember;
    }
    
    /**
     * 獲取今日簽到用戶數(shù)
     */
    public long getTodayCheckInCount() {
        String today = LocalDate.now().format(DateTimeFormatter.ISO_DATE);
        String dailyKey = DAILY_CHECKIN_KEY + today;
        
        Long size = redisTemplate.opsForSet().size(dailyKey);
        return size != null ? size : 0;
    }
    
    /**
     * 獲取簽到排行榜
     */
    public Set<ZSetOperations.TypedTuple<Object>> getCheckInRank(int limit) {
        String today = LocalDate.now().format(DateTimeFormatter.ISO_DATE);
        String rankKey = CHECKIN_RANK_KEY + today;
        
        return redisTemplate.opsForZSet().reverseRangeWithScores(rankKey, 0, limit - 1);
    }
    
    /**
     * 檢查用戶在指定日期是否簽到
     */
    public boolean checkUserSignedInDate(Long userId, LocalDate date) {
        String dateStr = date.format(DateTimeFormatter.ISO_DATE);
        String dailyKey = DAILY_CHECKIN_KEY + dateStr;
        
        Boolean isMember = redisTemplate.opsForSet().isMember(dailyKey, userId);
        return isMember != null && isMember;
    }
    
    /**
     * 獲取用戶指定月份的簽到情況
     */
    public List<String> getMonthlyCheckInStatus(Long userId, int year, int month) {
        List<String> result = new ArrayList<>();
        YearMonth yearMonth = YearMonth.of(year, month);
        
        // 獲取指定月份的第一天和最后一天
        LocalDate firstDay = yearMonth.atDay(1);
        LocalDate lastDay = yearMonth.atEndOfMonth();
        
        // 逐一檢查每一天是否簽到
        LocalDate currentDate = firstDay;
        while (!currentDate.isAfter(lastDay)) {
            if (checkUserSignedInDate(userId, currentDate)) {
                result.add(currentDate.format(DateTimeFormatter.ISO_DATE));
            }
            currentDate = currentDate.plusDays(1);
        }
        
        return result;
    }
}

控制器實(shí)現(xiàn)

@RestController
@RequestMapping("/api/redis-check-in")
public class RedisCheckInController {
    
    @Autowired
    private RedisCheckInService checkInService;
    
    @PostMapping
    public ResponseEntity<?> checkIn(@RequestBody RedisCheckInRequest request) {
        boolean success = checkInService.checkIn(request.getUserId(), request.getLocation());
        
        if (success) {
            return ResponseEntity.ok(Map.of("message", "Check-in successful"));
        } else {
            return ResponseEntity.badRequest().body(Map.of("message", "Already checked in today"));
        }
    }
    
    @GetMapping("/stats/{userId}")
    public ResponseEntity<Map<Object, Object>> getUserStats(@PathVariable Long userId) {
        Map<Object, Object> stats = checkInService.getUserCheckInStats(userId);
        return ResponseEntity.ok(stats);
    }
    
    @GetMapping("/status/{userId}")
    public ResponseEntity<Map<String, Object>> getCheckInStatus(@PathVariable Long userId) {
        boolean checkedIn = checkInService.isUserCheckedInToday(userId);
        long todayCount = checkInService.getTodayCheckInCount();
        
        Map<String, Object> response = new HashMap<>();
        response.put("checkedIn", checkedIn);
        response.put("todayCount", todayCount);
        
        return ResponseEntity.ok(response);
    }
    
    @GetMapping("/rank")
    public ResponseEntity<Set<ZSetOperations.TypedTuple<Object>>> getCheckInRank(
            @RequestParam(defaultValue = "10") int limit) {
        Set<ZSetOperations.TypedTuple<Object>> rank = checkInService.getCheckInRank(limit);
        return ResponseEntity.ok(rank);
    }
    
    @GetMapping("/monthly/{userId}")
    public ResponseEntity<List<String>> getMonthlyStatus(
            @PathVariable Long userId,
            @RequestParam int year,
            @RequestParam int month) {
        List<String> checkInDays = checkInService.getMonthlyCheckInStatus(userId, year, month);
        return ResponseEntity.ok(checkInDays);
    }
}

@Data
public class RedisCheckInRequest {
    private Long userId;
    private String location;
}

2.4 優(yōu)缺點(diǎn)分析

優(yōu)點(diǎn):

  • 極高的性能,支持高并發(fā)場景
  • 豐富的數(shù)據(jù)結(jié)構(gòu)支持多種簽到功能(排行榜、簽到日歷等)
  • 內(nèi)存數(shù)據(jù)庫,響應(yīng)速度快
  • 適合實(shí)時(shí)統(tǒng)計(jì)和分析

缺點(diǎn):

  • 數(shù)據(jù)持久性不如關(guān)系型數(shù)據(jù)庫
  • 復(fù)雜查詢能力有限
  • 內(nèi)存成本較高

2.5 適用場景

  • 大型社區(qū)或應(yīng)用的簽到功能
  • 實(shí)時(shí)性要求高的簽到系統(tǒng)
  • 高并發(fā)場景下的打卡功能
  • 需要簽到排行榜、簽到日歷等交互功能的應(yīng)用

三、基于位圖(Bitmap)的連續(xù)簽到統(tǒng)計(jì)系統(tǒng)

3.1 基本原理

Redis的Bitmap是一種非常節(jié)省空間的數(shù)據(jù)結(jié)構(gòu),它可以用來記錄簽到狀態(tài),每個(gè)bit位代表一天的簽到狀態(tài)(0表示未簽到,1表示已簽到)。利用Bitmap可以高效地實(shí)現(xiàn)連續(xù)簽到統(tǒng)計(jì)、月度簽到日歷等功能,同時(shí)極大地節(jié)省內(nèi)存使用。

3.2 系統(tǒng)設(shè)計(jì)

主要使用Redis的Bitmap操作來記錄和統(tǒng)計(jì)用戶簽到情況:

  • 每個(gè)用戶每個(gè)月的簽到記錄使用一個(gè)Bitmap
  • Bitmap的每一位代表當(dāng)月的一天(1-31)
  • 通過位操作可以高效地統(tǒng)計(jì)簽到天數(shù)、連續(xù)簽到等信息

3.3 核心代碼實(shí)現(xiàn)

@Service
public class BitmapCheckInService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    /**
     * 用戶簽到
     */
    public boolean checkIn(Long userId) {
        LocalDate today = LocalDate.now();
        int day = today.getDayOfMonth(); // 獲取當(dāng)月的第幾天
        String key = buildSignKey(userId, today);
        
        // 檢查今天是否已經(jīng)簽到
        Boolean isSigned = redisTemplate.opsForValue().getBit(key, day - 1);
        if (isSigned != null && isSigned) {
            return false; // 已經(jīng)簽到
        }
        
        // 設(shè)置簽到標(biāo)記
        redisTemplate.opsForValue().setBit(key, day - 1, true);
        
        // 設(shè)置過期時(shí)間(確保數(shù)據(jù)不會(huì)永久保存)
        redisTemplate.expire(key, 100, TimeUnit.DAYS);
        
        // 更新連續(xù)簽到記錄
        updateContinuousSignDays(userId);
        
        return true;
    }
    
    /**
     * 更新連續(xù)簽到天數(shù)
     */
    private void updateContinuousSignDays(Long userId) {
        LocalDate today = LocalDate.now();
        String continuousKey = "user:sign:continuous:" + userId;
        
        // 判斷昨天是否簽到
        boolean yesterdayChecked = isSignedIn(userId, today.minusDays(1));
        
        if (yesterdayChecked) {
            // 昨天簽到了,連續(xù)簽到天數(shù)+1
            redisTemplate.opsForValue().increment(continuousKey);
        } else {
            // 昨天沒簽到,重置連續(xù)簽到天數(shù)為1
            redisTemplate.opsForValue().set(continuousKey, "1");
        }
    }
    
    /**
     * 判斷用戶指定日期是否簽到
     */
    public boolean isSignedIn(Long userId, LocalDate date) {
        int day = date.getDayOfMonth();
        String key = buildSignKey(userId, date);
        
        Boolean isSigned = redisTemplate.opsForValue().getBit(key, day - 1);
        return isSigned != null && isSigned;
    }
    
    /**
     * 獲取用戶連續(xù)簽到天數(shù)
     */
    public int getContinuousSignDays(Long userId) {
        String continuousKey = "user:sign:continuous:" + userId;
        String value = redisTemplate.opsForValue().get(continuousKey);
        return value != null ? Integer.parseInt(value) : 0;
    }
    
    /**
     * 獲取用戶當(dāng)月簽到次數(shù)
     */
    public long getMonthSignCount(Long userId, LocalDate date) {
        String key = buildSignKey(userId, date);
        int dayOfMonth = date.lengthOfMonth(); // 當(dāng)月總天數(shù)
        
        return redisTemplate.execute((RedisCallback<Long>) con -> {
            return con.bitCount(key.getBytes());
        });
    }
    
    /**
     * 獲取用戶當(dāng)月簽到情況
     */
    public List<Integer> getMonthSignData(Long userId, LocalDate date) {
        List<Integer> result = new ArrayList<>();
        String key = buildSignKey(userId, date);
        int dayOfMonth = date.lengthOfMonth(); // 當(dāng)月總天數(shù)
        
        for (int i = 0; i < dayOfMonth; i++) {
            Boolean isSigned = redisTemplate.opsForValue().getBit(key, i);
            result.add(isSigned != null && isSigned ? 1 : 0);
        }
        
        return result;
    }
    
    /**
     * 獲取用戶當(dāng)月首次簽到時(shí)間
     */
    public int getFirstSignDay(Long userId, LocalDate date) {
        String key = buildSignKey(userId, date);
        int dayOfMonth = date.lengthOfMonth(); // 當(dāng)月總天數(shù)
        
        for (int i = 0; i < dayOfMonth; i++) {
            Boolean isSigned = redisTemplate.opsForValue().getBit(key, i);
            if (isSigned != null && isSigned) {
                return i + 1; // 返回第一次簽到的日期
            }
        }
        
        return -1; // 本月沒有簽到記錄
    }
    
    /**
     * 構(gòu)建簽到Key
     */
    private String buildSignKey(Long userId, LocalDate date) {
        return String.format("user:sign:%d:%d%02d", userId, date.getYear(), date.getMonthValue());
    }
}

控制器實(shí)現(xiàn)

@RestController
@RequestMapping("/api/bitmap-check-in")
public class BitmapCheckInController {
    
    @Autowired
    private BitmapCheckInService checkInService;
    
    @PostMapping("/{userId}")
    public ResponseEntity<?> checkIn(@PathVariable Long userId) {
        boolean success = checkInService.checkIn(userId);
        
        if (success) {
            Map<String, Object> response = new HashMap<>();
            response.put("success", true);
            response.put("continuousDays", checkInService.getContinuousSignDays(userId));
            
            return ResponseEntity.ok(response);
        } else {
            return ResponseEntity.badRequest().body(Map.of("message", "Already checked in today"));
        }
    }
    
    @GetMapping("/{userId}/status")
    public ResponseEntity<?> checkInStatus(
            @PathVariable Long userId,
            @RequestParam(required = false) 
                @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) {
        
        if (date == null) {
            date = LocalDate.now();
        }
        
        Map<String, Object> response = new HashMap<>();
        response.put("signedToday", checkInService.isSignedIn(userId, date));
        response.put("continuousDays", checkInService.getContinuousSignDays(userId));
        response.put("monthSignCount", checkInService.getMonthSignCount(userId, date));
        response.put("monthSignData", checkInService.getMonthSignData(userId, date));
        
        return ResponseEntity.ok(response);
    }
    
    @GetMapping("/{userId}/first-sign-day")
    public ResponseEntity<Integer> getFirstSignDay(
            @PathVariable Long userId,
            @RequestParam(required = false) 
                @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) {
        
        if (date == null) {
            date = LocalDate.now();
        }
        
        int firstDay = checkInService.getFirstSignDay(userId, date);
        return ResponseEntity.ok(firstDay);
    }
}

3.4 優(yōu)缺點(diǎn)分析

優(yōu)點(diǎn):

  • 極其節(jié)省存儲(chǔ)空間,一個(gè)月的簽到記錄僅需要4字節(jié)
  • 位操作性能高,適合大規(guī)模用戶
  • 統(tǒng)計(jì)操作(如計(jì)算簽到天數(shù))非常高效
  • 適合實(shí)現(xiàn)簽到日歷和連續(xù)簽到統(tǒng)計(jì)

缺點(diǎn):

  • 不能存儲(chǔ)簽到的詳細(xì)信息(如簽到時(shí)間、地點(diǎn)等)
  • 僅適合簡單的簽到/未簽到二元狀態(tài)記錄
  • 復(fù)雜的簽到業(yè)務(wù)邏輯實(shí)現(xiàn)較困難
  • 歷史數(shù)據(jù)查詢相對復(fù)雜

3.5 適用場景

  • 需要節(jié)省存儲(chǔ)空間的大規(guī)模用戶簽到系統(tǒng)
  • 社區(qū)/電商平臺(tái)的每日簽到獎(jiǎng)勵(lì)功能
  • 需要高效計(jì)算連續(xù)簽到天數(shù)的應(yīng)用
  • 移動(dòng)應(yīng)用的簽到日歷功能

四、基于地理位置的簽到打卡系統(tǒng)

4.1 基本原理

地理位置簽到系統(tǒng)利用用戶的GPS定位信息,驗(yàn)證用戶是否在指定區(qū)域內(nèi)進(jìn)行簽到,常用于企業(yè)考勤、學(xué)校上課點(diǎn)名、實(shí)地活動(dòng)簽到等場景。

該方案結(jié)合了關(guān)系型數(shù)據(jù)庫存儲(chǔ)簽到記錄和Redis的GEO功能進(jìn)行位置驗(yàn)證。

4.2 數(shù)據(jù)模型設(shè)計(jì)

-- 簽到位置表
CREATE TABLE check_in_locations (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    latitude DOUBLE NOT NULL,
    longitude DOUBLE NOT NULL,
    radius DOUBLE NOT NULL, -- 有效半徑(米)
    address VARCHAR(255),
    location_type VARCHAR(50),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 地理位置簽到記錄表
CREATE TABLE geo_check_ins (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL,
    location_id BIGINT NOT NULL,
    check_in_time TIMESTAMP NOT NULL,
    latitude DOUBLE NOT NULL,
    longitude DOUBLE NOT NULL,
    accuracy DOUBLE, -- 定位精度(米)
    is_valid BOOLEAN DEFAULT TRUE, -- 是否有效簽到
    device_info VARCHAR(255),
    FOREIGN KEY (user_id) REFERENCES users(id),
    FOREIGN KEY (location_id) REFERENCES check_in_locations(id)
);

4.3 核心代碼實(shí)現(xiàn)

實(shí)體類設(shè)計(jì)

@Data
@TableName("check_in_locations")
public class CheckInLocation {
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    
    private String name;
    
    private Double latitude;
    
    private Double longitude;
    
    private Double radius; // 單位:米
    
    private String address;
    
    @TableField("location_type")
    private String locationType;
    
    @TableField("created_at")
    private LocalDateTime createdAt;
}

@Data
@TableName("geo_check_ins")
public class GeoCheckIn {
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    
    @TableField("user_id")
    private Long userId;
    
    @TableField("location_id")
    private Long locationId;
    
    @TableField("check_in_time")
    private LocalDateTime checkInTime;
    
    private Double latitude;
    
    private Double longitude;
    
    private Double accuracy;
    
    @TableField("is_valid")
    private Boolean isValid = true;
    
    @TableField("device_info")
    private String deviceInfo;
}

Mapper層

@Mapper
public interface CheckInLocationMapper extends BaseMapper<CheckInLocation> {
    
    @Select("SELECT * FROM check_in_locations WHERE location_type = #{locationType}")
    List<CheckInLocation> findByLocationType(@Param("locationType") String locationType);
}

@Mapper
public interface GeoCheckInMapper extends BaseMapper<GeoCheckIn> {
    
    @Select("SELECT * FROM geo_check_ins WHERE user_id = #{userId} AND check_in_time BETWEEN #{startTime} AND #{endTime}")
    List<GeoCheckIn> findByUserIdAndCheckInTimeBetween(
            @Param("userId") Long userId, 
            @Param("startTime") LocalDateTime startTime, 
            @Param("endTime") LocalDateTime endTime);
    
    @Select("SELECT * FROM geo_check_ins WHERE user_id = #{userId} AND location_id = #{locationId} " +
            "AND DATE(check_in_time) = DATE(#{date})")
    GeoCheckIn findByUserIdAndLocationIdAndDate(
            @Param("userId") Long userId, 
            @Param("locationId") Long locationId, 
            @Param("date") LocalDateTime date);
}

Service層

@Service
@Transactional
public class GeoCheckInService {
    
    @Autowired
    private CheckInLocationMapper locationMapper;
    
    @Autowired
    private GeoCheckInMapper geoCheckInMapper;
    
    @Autowired
    private UserMapper userMapper;
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    private static final String GEO_KEY = "geo:locations";
    
    @PostConstruct
    public void init() {
        // 將所有簽到位置加載到Redis GEO中
        List<CheckInLocation> locations = locationMapper.selectList(null);
        
        if (!locations.isEmpty()) {
            Map<String, Point> locationPoints = new HashMap<>();
            
            for (CheckInLocation location : locations) {
                locationPoints.put(location.getId().toString(), 
                        new Point(location.getLongitude(), location.getLatitude()));
            }
            
            redisTemplate.opsForGeo().add(GEO_KEY, locationPoints);
        }
    }
    
    /**
     * 添加新的簽到地點(diǎn)
     */
    public CheckInLocation addCheckInLocation(CheckInLocation location) {
        location.setCreatedAt(LocalDateTime.now());
        locationMapper.insert(location);
        
        // 添加到Redis GEO
        redisTemplate.opsForGeo().add(GEO_KEY, 
                new Point(location.getLongitude(), location.getLatitude()), 
                location.getId().toString());
        
        return location;
    }
    
    /**
     * 用戶地理位置簽到
     */
    public GeoCheckIn checkIn(Long userId, Long locationId, Double latitude, 
                              Double longitude, Double accuracy, String deviceInfo) {
        // 驗(yàn)證用戶和位置是否存在
        User user = userMapper.selectById(userId);
        if (user == null) {
            throw new RuntimeException("User not found");
        }
        
        CheckInLocation location = locationMapper.selectById(locationId);
        if (location == null) {
            throw new RuntimeException("Check-in location not found");
        }
        
        // 檢查今天是否已經(jīng)在該位置簽到
        LocalDateTime now = LocalDateTime.now();
        GeoCheckIn existingCheckIn = geoCheckInMapper
                .findByUserIdAndLocationIdAndDate(userId, locationId, now);
        
        if (existingCheckIn != null) {
            throw new RuntimeException("Already checked in at this location today");
        }
        
        // 驗(yàn)證用戶是否在簽到范圍內(nèi)
        boolean isWithinRange = isWithinCheckInRange(
                latitude, longitude, location.getLatitude(), location.getLongitude(), location.getRadius());
        
        // 創(chuàng)建簽到記錄
        GeoCheckIn checkIn = new GeoCheckIn();
        checkIn.setUserId(userId);
        checkIn.setLocationId(locationId);
        checkIn.setCheckInTime(now);
        checkIn.setLatitude(latitude);
        checkIn.setLongitude(longitude);
        checkIn.setAccuracy(accuracy);
        checkIn.setIsValid(isWithinRange);
        checkIn.setDeviceInfo(deviceInfo);
        
        geoCheckInMapper.insert(checkIn);
        
        return checkIn;
    }
    
    /**
     * 檢查用戶是否在簽到范圍內(nèi)
     */
    private boolean isWithinCheckInRange(Double userLat, Double userLng, 
                                        Double locationLat, Double locationLng, Double radius) {
        // 使用Redis GEO計(jì)算距離
        Distance distance = redisTemplate.opsForGeo().distance(
                GEO_KEY,
                locationLat + "," + locationLng,
                userLat + "," + userLng,
                Metrics.METERS
        );
        
        return distance != null && distance.getValue() <= radius;
    }
    
    /**
     * 查找附近的簽到地點(diǎn)
     */
    public List<GeoResult<RedisGeoCommands.GeoLocation<String>>> findNearbyLocations(
            Double latitude, Double longitude, Double radius) {
        
        Circle circle = new Circle(new Point(longitude, latitude), new Distance(radius, Metrics.METERS));
        RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs
                .newGeoRadiusArgs().includeDistance().sortAscending();
        
        GeoResults<RedisGeoCommands.GeoLocation<String>> results = redisTemplate.opsForGeo()
                .radius(GEO_KEY, circle, args);
        
        return results != null ? results.getContent() : Collections.emptyList();
    }
    
    /**
     * 獲取用戶簽到歷史
     */
    public List<GeoCheckIn> getUserCheckInHistory(Long userId, LocalDate startDate, LocalDate endDate) {
        LocalDateTime startDateTime = startDate.atStartOfDay();
        LocalDateTime endDateTime = endDate.atTime(23, 59, 59);
        
        return geoCheckInMapper.findByUserIdAndCheckInTimeBetween(
                userId, startDateTime, endDateTime);
    }
}

Controller層

@RestController
@RequestMapping("/api/geo-check-in")
public class GeoCheckInController {
    
    @Autowired
    private GeoCheckInService geoCheckInService;
    
    @PostMapping("/locations")
    public ResponseEntity<CheckInLocation> addLocation(@RequestBody CheckInLocation location) {
        CheckInLocation savedLocation = geoCheckInService.addCheckInLocation(location);
        return ResponseEntity.ok(savedLocation);
    }
    
    @PostMapping
    public ResponseEntity<?> checkIn(@RequestBody GeoCheckInRequest request) {
        try {
            GeoCheckIn checkIn = geoCheckInService.checkIn(
                    request.getUserId(),
                    request.getLocationId(),
                    request.getLatitude(),
                    request.getLongitude(),
                    request.getAccuracy(),
                    request.getDeviceInfo()
            );
            
            Map<String, Object> response = new HashMap<>();
            response.put("id", checkIn.getId());
            response.put("checkInTime", checkIn.getCheckInTime());
            response.put("isValid", checkIn.getIsValid());
            
            if (!checkIn.getIsValid()) {
                response.put("message", "You are not within the valid check-in range");
            }
            
            return ResponseEntity.ok(response);
        } catch (RuntimeException e) {
            return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
        }
    }
    
    @GetMapping("/nearby")
    public ResponseEntity<?> findNearbyLocations(
            @RequestParam Double latitude,
            @RequestParam Double longitude,
            @RequestParam(defaultValue = "500") Double radius) {
        
        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> locations = 
                geoCheckInService.findNearbyLocations(latitude, longitude, radius);
        
        return ResponseEntity.ok(locations);
    }
    
    @GetMapping("/history/{userId}")
    public ResponseEntity<List<GeoCheckIn>> getUserHistory(
            @PathVariable Long userId,
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
        
        List<GeoCheckIn> history = geoCheckInService.getUserCheckInHistory(userId, startDate, endDate);
        return ResponseEntity.ok(history);
    }
}

@Data
public class GeoCheckInRequest {
    private Long userId;
    private Long locationId;
    private Double latitude;
    private Double longitude;
    private Double accuracy;
    private String deviceInfo;
}

4.4 優(yōu)缺點(diǎn)分析

優(yōu)點(diǎn):

  • 利用地理位置驗(yàn)證提高簽到真實(shí)性
  • 支持多地點(diǎn)簽到和附近地點(diǎn)查找
  • 結(jié)合了關(guān)系型數(shù)據(jù)庫和Redis的優(yōu)勢
  • 適合需要物理位置驗(yàn)證的場景

缺點(diǎn):

  • 依賴用戶設(shè)備的GPS定位精度
  • 可能受到GPS欺騙工具的影響
  • 室內(nèi)定位精度可能不足
  • 系統(tǒng)復(fù)雜度較高

4.5 適用場景

  • 企業(yè)員工考勤系統(tǒng)
  • 外勤人員簽到打卡
  • 學(xué)校課堂點(diǎn)名
  • 實(shí)地活動(dòng)簽到驗(yàn)證
  • 外賣/快遞配送簽收系統(tǒng)

五、基于二維碼的簽到打卡系統(tǒng)

5.1 基本原理

二維碼簽到系統(tǒng)通過動(dòng)態(tài)生成帶有時(shí)間戳和簽名的二維碼,用戶通過掃描二維碼完成簽到。

這種方式適合會(huì)議、課程、活動(dòng)等場景,可有效防止代簽,同時(shí)簡化簽到流程。

5.2 數(shù)據(jù)模型設(shè)計(jì)

-- 簽到活動(dòng)表
CREATE TABLE check_in_events (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    title VARCHAR(100) NOT NULL,
    description VARCHAR(500),
    start_time TIMESTAMP NOT NULL,
    end_time TIMESTAMP NOT NULL,
    location VARCHAR(255),
    organizer_id BIGINT,
    qr_code_refresh_interval INT DEFAULT 60, -- 二維碼刷新間隔(秒)
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (organizer_id) REFERENCES users(id)
);

-- 二維碼簽到記錄表
CREATE TABLE qr_check_ins (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    event_id BIGINT NOT NULL,
    user_id BIGINT NOT NULL,
    check_in_time TIMESTAMP NOT NULL,
    qr_code_token VARCHAR(100) NOT NULL,
    ip_address VARCHAR(50),
    device_info VARCHAR(255),
    FOREIGN KEY (event_id) REFERENCES check_in_events(id),
    FOREIGN KEY (user_id) REFERENCES users(id),
    UNIQUE KEY unique_event_user (event_id, user_id)
);

5.3 核心代碼實(shí)現(xiàn)

實(shí)體類設(shè)計(jì)

@Data
@TableName("check_in_events")
public class CheckInEvent {
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    
    private String title;
    
    private String description;
    
    @TableField("start_time")
    private LocalDateTime startTime;
    
    @TableField("end_time")
    private LocalDateTime endTime;
    
    private String location;
    
    @TableField("organizer_id")
    private Long organizerId;
    
    @TableField("qr_code_refresh_interval")
    private Integer qrCodeRefreshInterval = 60; // 默認(rèn)60秒
    
    @TableField("created_at")
    private LocalDateTime createdAt;
    
    @TableField(exist = false)
    private String currentQrCode;
}

@Data
@TableName("qr_check_ins")
public class QrCheckIn {
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    
    @TableField("event_id")
    private Long eventId;
    
    @TableField("user_id")
    private Long userId;
    
    @TableField("check_in_time")
    private LocalDateTime checkInTime;
    
    @TableField("qr_code_token")
    private String qrCodeToken;
    
    @TableField("ip_address")
    private String ipAddress;
    
    @TableField("device_info")
    private String deviceInfo;
}

QR碼服務(wù)和校驗(yàn)

@Service
public class QrCodeService {
    
    @Value("${qrcode.secret:defaultSecretKey}")
    private String secretKey;
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    /**
     * 生成帶簽名的二維碼內(nèi)容
     */
    public String generateQrCodeContent(Long eventId) {
        long timestamp = System.currentTimeMillis();
        String content = eventId + ":" + timestamp;
        String signature = generateSignature(content);
        
        // 創(chuàng)建完整的二維碼內(nèi)容
        String qrCodeContent = content + ":" + signature;
        
        // 保存到Redis,設(shè)置過期時(shí)間
        String redisKey = "qrcode:event:" + eventId + ":" + timestamp;
        redisTemplate.opsForValue().set(redisKey, qrCodeContent, 5, TimeUnit.MINUTES);
        
        return qrCodeContent;
    }
    
    /**
     * 驗(yàn)證二維碼內(nèi)容
     */
    public boolean validateQrCode(String qrCodeContent) {
        String[] parts = qrCodeContent.split(":");
        if (parts.length != 3) {
            return false;
        }
        
        String eventId = parts[0];
        String timestamp = parts[1];
        String providedSignature = parts[2];
        
        // 驗(yàn)證簽名
        String content = eventId + ":" + timestamp;
        String expectedSignature = generateSignature(content);
        
        if (!expectedSignature.equals(providedSignature)) {
            return false;
        }
        
        // 驗(yàn)證二維碼是否在Redis中存在(防止重復(fù)使用)
        String redisKey = "qrcode:event:" + eventId + ":" + timestamp;
        Boolean exists = redisTemplate.hasKey(redisKey);
        
        return exists != null && exists;
    }
    
    /**
     * 生成二維碼圖片
     */
    public byte[] generateQrCodeImage(String content, int width, int height) throws Exception {
        QRCodeWriter qrCodeWriter = new QRCodeWriter();
        BitMatrix bitMatrix = qrCodeWriter.encode(content, BarcodeFormat.QR_CODE, width, height);
        
        ByteArrayOutputStream pngOutputStream = new ByteArrayOutputStream();
        MatrixToImageWriter.writeToStream(bitMatrix, "PNG", pngOutputStream);
        
        return pngOutputStream.toByteArray();
    }
    
    /**
     * 生成內(nèi)容簽名
     */
    private String generateSignature(String content) {
        try {
            Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
            SecretKeySpec secret_key = new SecretKeySpec(secretKey.getBytes(), "HmacSHA256");
            sha256_HMAC.init(secret_key);
            
            byte[] hash = sha256_HMAC.doFinal(content.getBytes());
            return Base64.getEncoder().encodeToString(hash);
        } catch (Exception e) {
            throw new RuntimeException("Failed to generate signature", e);
        }
    }
}

服務(wù)層實(shí)現(xiàn)

@Service
@Transactional
public class QrCheckInService {
    
    @Autowired
    private CheckInEventMapper eventMapper;
    
    @Autowired
    private QrCheckInMapper qrCheckInMapper;
    
    @Autowired
    private UserMapper userMapper;
    
    @Autowired
    private QrCodeService qrCodeService;
    
    /**
     * 創(chuàng)建簽到活動(dòng)
     */
    public CheckInEvent createEvent(CheckInEvent event) {
        event.setCreatedAt(LocalDateTime.now());
        eventMapper.insert(event);
        return event;
    }
    
    /**
     * 獲取活動(dòng)信息,包括當(dāng)前二維碼
     */
    public CheckInEvent getEventWithQrCode(Long eventId) {
        CheckInEvent event = eventMapper.selectById(eventId);
        if (event == null) {
            throw new RuntimeException("Event not found");
        }
        
        // 檢查活動(dòng)是否在有效期內(nèi)
        LocalDateTime now = LocalDateTime.now();
        if (now.isBefore(event.getStartTime()) || now.isAfter(event.getEndTime())) {
            throw new RuntimeException("Event is not active");
        }
        
        // 生成當(dāng)前二維碼
        String qrCodeContent = qrCodeService.generateQrCodeContent(eventId);
        event.setCurrentQrCode(qrCodeContent);
        
        return event;
    }
    
    /**
     * 用戶通過二維碼簽到
     */
    public QrCheckIn checkIn(String qrCodeContent, Long userId, String ipAddress, String deviceInfo) {
        // 驗(yàn)證二維碼
        if (!qrCodeService.validateQrCode(qrCodeContent)) {
            throw new RuntimeException("Invalid QR code");
        }
        
        // 解析二維碼內(nèi)容
        String[] parts = qrCodeContent.split(":");
        Long eventId = Long.parseLong(parts[0]);
        
        // 驗(yàn)證活動(dòng)和用戶
        CheckInEvent event = eventMapper.selectById(eventId);
        if (event == null) {
            throw new RuntimeException("Event not found");
        }
        
        User user = userMapper.selectById(userId);
        if (user == null) {
            throw new RuntimeException("User not found");
        }
        
        // 檢查活動(dòng)是否在有效期內(nèi)
        LocalDateTime now = LocalDateTime.now();
        if (now.isBefore(event.getStartTime()) || now.isAfter(event.getEndTime())) {
            throw new RuntimeException("Event is not active");
        }
        
        // 檢查用戶是否已經(jīng)簽到
        if (qrCheckInMapper.existsByEventIdAndUserId(eventId, userId) > 0) {
            throw new RuntimeException("User already checked in for this event");
        }
        
        // 創(chuàng)建簽到記錄
        QrCheckIn checkIn = new QrCheckIn();
        checkIn.setEventId(eventId);
        checkIn.setUserId(userId);
        checkIn.setCheckInTime(now);
        checkIn.setQrCodeToken(qrCodeContent);
        checkIn.setIpAddress(ipAddress);
        checkIn.setDeviceInfo(deviceInfo);
        
        qrCheckInMapper.insert(checkIn);
        return checkIn;
    }
    
    /**
     * 獲取活動(dòng)簽到列表
     */
    public List<QrCheckIn> getEventCheckIns(Long eventId) {
        return qrCheckInMapper.findByEventIdOrderByCheckInTimeDesc(eventId);
    }
    
    /**
     * 獲取用戶簽到歷史
     */
    public List<QrCheckIn> getUserCheckIns(Long userId) {
        return qrCheckInMapper.findByUserIdOrderByCheckInTimeDesc(userId);
    }
    
    /**
     * 獲取活動(dòng)簽到統(tǒng)計(jì)
     */
    public Map<String, Object> getEventStatistics(Long eventId) {
        CheckInEvent event = eventMapper.selectById(eventId);
        if (event == null) {
            throw new RuntimeException("Event not found");
        }
        
        long totalCheckIns = qrCheckInMapper.countByEventId(eventId);
        
        Map<String, Object> statistics = new HashMap<>();
        statistics.put("eventId", eventId);
        statistics.put("title", event.getTitle());
        statistics.put("startTime", event.getStartTime());
        statistics.put("endTime", event.getEndTime());
        statistics.put("totalCheckIns", totalCheckIns);
        
        return statistics;
    }
}

控制器實(shí)現(xiàn)

@RestController
@RequestMapping("/api/qr-check-in")
public class QrCheckInController {
    
    @Autowired
    private QrCheckInService checkInService;
    
    @Autowired
    private QrCodeService qrCodeService;
    
    @PostMapping("/events")
    public ResponseEntity<CheckInEvent> createEvent(@RequestBody CheckInEvent event) {
        CheckInEvent createdEvent = checkInService.createEvent(event);
        return ResponseEntity.ok(createdEvent);
    }
    
    @GetMapping("/events/{eventId}")
    public ResponseEntity<CheckInEvent> getEvent(@PathVariable Long eventId) {
        CheckInEvent event = checkInService.getEventWithQrCode(eventId);
        return ResponseEntity.ok(event);
    }
    
    @GetMapping("/events/{eventId}/qrcode")
    public ResponseEntity<?> getEventQrCode(
            @PathVariable Long eventId,
            @RequestParam(defaultValue = "300") int width,
            @RequestParam(defaultValue = "300") int height) {
        
        try {
            CheckInEvent event = checkInService.getEventWithQrCode(eventId);
            byte[] qrCodeImage = qrCodeService.generateQrCodeImage(
                    event.getCurrentQrCode(), width, height);
            
            return ResponseEntity.ok()
                    .contentType(MediaType.IMAGE_PNG)
                    .body(qrCodeImage);
        } catch (Exception e) {
            return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
        }
    }
    
    @PostMapping("/check-in")
    public ResponseEntity<?> checkIn(@RequestBody QrCheckInRequest request, 
                                     HttpServletRequest httpRequest) {
        try {
            String ipAddress = httpRequest.getRemoteAddr();
            
            QrCheckIn checkIn = checkInService.checkIn(
                    request.getQrCodeContent(),
                    request.getUserId(),
                    ipAddress,
                    request.getDeviceInfo()
            );
            
            return ResponseEntity.ok(checkIn);
        } catch (RuntimeException e) {
            return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
        }
    }
    
    @GetMapping("/events/{eventId}/check-ins")
    public ResponseEntity<List<QrCheckIn>> getEventCheckIns(@PathVariable Long eventId) {
        List<QrCheckIn> checkIns = checkInService.getEventCheckIns(eventId);
        return ResponseEntity.ok(checkIns);
    }
    
    @GetMapping("/users/{userId}/check-ins")
    public ResponseEntity<List<QrCheckIn>> getUserCheckIns(@PathVariable Long userId) {
        List<QrCheckIn> checkIns = checkInService.getUserCheckIns(userId);
        return ResponseEntity.ok(checkIns);
    }
    
    @GetMapping("/events/{eventId}/statistics")
    public ResponseEntity<Map<String, Object>> getEventStatistics(@PathVariable Long eventId) {
        Map<String, Object> statistics = checkInService.getEventStatistics(eventId);
        return ResponseEntity.ok(statistics);
    }
}

@Data
public class QrCheckInRequest {
    private String qrCodeContent;
    private Long userId;
    private String deviceInfo;
}

5.4 優(yōu)缺點(diǎn)分析

優(yōu)點(diǎn):

  • 簽到過程簡單快捷,用戶體驗(yàn)好
  • 適合集中式簽到場景(會(huì)議、課程等)

缺點(diǎn):

  • 需要組織者提前設(shè)置簽到活動(dòng)
  • 需要現(xiàn)場展示二維碼(投影、打印等)
  • 可能出現(xiàn)二維碼被拍照傳播的風(fēng)險(xiǎn)

5.5 適用場景

  • 會(huì)議、研討會(huì)簽到
  • 課堂點(diǎn)名
  • 活動(dòng)入場簽到
  • 培訓(xùn)簽到
  • 需要現(xiàn)場確認(rèn)的簽到場景

六、各方案對比與選擇指南

6.1 功能對比

功能特性關(guān)系型數(shù)據(jù)庫RedisBitmap地理位置二維碼
實(shí)現(xiàn)復(fù)雜度
系統(tǒng)性能極高
存儲(chǔ)效率極高
用戶體驗(yàn)
開發(fā)成本
維護(hù)成本

6.2 適用場景對比

方案最佳適用場景不適合場景
關(guān)系型數(shù)據(jù)庫中小型企業(yè)考勤、簡單簽到系統(tǒng)高并發(fā)、大規(guī)模用戶場景
Redis高并發(fā)社區(qū)簽到、連續(xù)簽到獎(jiǎng)勵(lì)需要復(fù)雜查詢和報(bào)表統(tǒng)計(jì)
Bitmap大規(guī)模用戶的每日簽到、連續(xù)簽到統(tǒng)計(jì)需要詳細(xì)簽到信息記錄
地理位置外勤人員打卡、實(shí)地活動(dòng)簽到室內(nèi)或GPS信號(hào)弱的環(huán)境
二維碼會(huì)議、課程、活動(dòng)簽到遠(yuǎn)程辦公、分散式簽到

七、總結(jié)

在實(shí)際應(yīng)用中,可以根據(jù)具體需求、用戶規(guī)模、安全要求和預(yù)算等因素選擇最合適的方案,也可以將多種方案結(jié)合使用,構(gòu)建更加完善的簽到打卡系統(tǒng)。

以上就是SpringBoot實(shí)現(xiàn)簽到打卡功能的五種方案的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot簽到打卡的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • MyBatis攔截器的實(shí)現(xiàn)原理

    MyBatis攔截器的實(shí)現(xiàn)原理

    這篇文章主要介紹了MyBatis攔截器的實(shí)現(xiàn)原理,Mybatis攔截器并不是每個(gè)對象里面的方法都可以被攔截的,其具體內(nèi)容感興趣的小伙伴課題參考一下下面文章內(nèi)容
    2022-08-08
  • 深入學(xué)習(xí)Java同步機(jī)制中的底層實(shí)現(xiàn)

    深入學(xué)習(xí)Java同步機(jī)制中的底層實(shí)現(xiàn)

    在多線程編程中我們會(huì)遇到很多需要使用線程同步機(jī)制去解決的并發(fā)問題,這些同步機(jī)制是如何實(shí)現(xiàn)的呢?下面和小編來一起學(xué)習(xí)吧
    2019-05-05
  • 使用idea+gradle編譯spring5.x.x源碼分析

    使用idea+gradle編譯spring5.x.x源碼分析

    這篇文章主要介紹了idea?+?gradle編譯spring5.x.x源碼,在編譯spring5源碼時(shí)需要將項(xiàng)目導(dǎo)入idea中然后編譯配置,本文給大家講解的非常詳細(xì),需要的朋友可以參考下
    2022-04-04
  • hadoop?切片機(jī)制分析與應(yīng)用

    hadoop?切片機(jī)制分析與應(yīng)用

    切片這個(gè)詞對于做過python開發(fā)的同學(xué)一定不陌生,但是與hadoop中的切片有所區(qū)別,hadoop中的切片是為了優(yōu)化hadoop的job在處理過程中MapTask階段的性能達(dá)到最優(yōu)而言
    2022-02-02
  • 基于Java并發(fā)容器ConcurrentHashMap#put方法解析

    基于Java并發(fā)容器ConcurrentHashMap#put方法解析

    下面小編就為大家?guī)硪黄贘ava并發(fā)容器ConcurrentHashMap#put方法解析。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2017-06-06
  • spring boot+mybatis搭建一個(gè)后端restfull服務(wù)的實(shí)例詳解

    spring boot+mybatis搭建一個(gè)后端restfull服務(wù)的實(shí)例詳解

    這篇文章主要介紹了spring boot+mybatis搭建一個(gè)后端restfull服務(wù),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2020-11-11
  • SpringBoot實(shí)現(xiàn)yml配置文件為變量賦值

    SpringBoot實(shí)現(xiàn)yml配置文件為變量賦值

    這篇文章主要介紹了SpringBoot實(shí)現(xiàn)yml配置文件為變量賦值,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-02-02
  • Java數(shù)據(jù)結(jié)構(gòu)常見幾大排序梳理

    Java數(shù)據(jù)結(jié)構(gòu)常見幾大排序梳理

    Java常見的排序算法有:直接插入排序、希爾排序、選擇排序、冒泡排序、歸并排序、快速排序、堆排序等。本文詳解介紹它們的實(shí)現(xiàn)以及圖解,需要的可以參考一下
    2022-03-03
  • 詳析Spring中依賴注入的三種方式

    詳析Spring中依賴注入的三種方式

    在開發(fā)的過程中突然對Spring的依賴注入幾種方式出現(xiàn)混交,打算做個(gè)簡單的小結(jié),方便大家和自己以后參考借鑒,如有總結(jié)不對的地方,請大家不吝指教!下面來一起看看吧。
    2016-09-09
  • Java合并集合幾種常見方式總結(jié)(List、Set、Map)

    Java合并集合幾種常見方式總結(jié)(List、Set、Map)

    這篇文章主要介紹了Java中合并List、Set、Map的多種方法,包括addAll()、Stream.concat()、Stream.of()+flatMap()、List.copyOf()、putAll()、merge()、compute()和StreamAPI等,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2025-03-03

最新評論