liu
2026-09-22 0837c3ff473499d8a267924125c776b34cc8b517
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
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<Object> 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);
    }
}