package com.ruoyi.system.service; import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.ruoyi.common.core.redis.RedisCache; /** * 学生端(Miniapp)会话 token 管理 —— Redis UUID token。 * * key 与 MiniappController 既有内联实现保持一致: * miniapp:token:{token} = studentId(24h) * 额外维护反向索引 miniapp:tokens:{studentId} = 该生全部 token 集合, * 供"改密/重置密码/强制下线"作废一个学生的所有在线会话。 * * @author ruoyi */ @Service public class MiniappTokenService { public static final String TOKEN_PREFIX = "miniapp:token:"; public static final String TOKEN_SET_PREFIX = "miniapp:tokens:"; /** 学生端登录密码错误计数 key 前缀(管理端重置密码时同步清除) */ public static final String PWD_ERR_PREFIX = "miniapp_pwd_err:"; /** 自助重置失败计数 key 前缀(管理端修正手机号/重置时同步清除) */ public static final String RESET_ERR_PREFIX = "miniapp_reset_err:"; /** 登录失败锁 key:按 学号+姓名 组合(学号仅班级内唯一,纯学号键会跨班互锁) */ public static String pwdErrKey(String studentNo, String name) { return PWD_ERR_PREFIX + studentNo + ":" + name; } /** 自助重置失败锁 key(同上组合粒度) */ public static String resetErrKey(String studentNo, String name) { return RESET_ERR_PREFIX + studentNo + ":" + name; } private static final int EXPIRE_HOURS = 24; @Autowired private RedisCache redisCache; /** 生成一个新 token(不落库,配合 setCacheObject 使用) */ public String newToken() { return UUID.randomUUID().toString().replace("-", ""); } /** 把一个已签发的 token 登记进学生反向索引(老链路签发的 token 也应补登记,才能被强制下线覆盖) */ @SuppressWarnings({ "unchecked", "rawtypes" }) public void registerSession(Long studentId, String token) { redisCache.redisTemplate.opsForSet().add(TOKEN_SET_PREFIX + studentId, token); redisCache.redisTemplate.expire(TOKEN_SET_PREFIX + studentId, EXPIRE_HOURS + 1, TimeUnit.HOURS); } /** 签发 token + 写会话缓存 + 登记反向索引,一步到位 */ public String createSession(Long studentId) { String token = newToken(); redisCache.setCacheObject(TOKEN_PREFIX + token, studentId, EXPIRE_HOURS, TimeUnit.HOURS); registerSession(studentId, token); return token; } /** 作废某学生全部会话(改密后/重置密码后/管理端强制下线) */ @SuppressWarnings({ "unchecked", "rawtypes" }) public void invalidateAll(Long studentId) { Set tokens = redisCache.redisTemplate.opsForSet().members(TOKEN_SET_PREFIX + studentId); if (tokens != null) { for (Object t : tokens) { redisCache.deleteObject(TOKEN_PREFIX + t); } } redisCache.deleteObject(TOKEN_SET_PREFIX + studentId); } }