相信自己,终会成功
目录
B端用户管理
C端用户代码
发送验证码:
验证验证码
退出登录
登录用户信息功能
用户详情与用户编辑
用户竞赛接口
用户报名竞赛
用户竞赛报名接口查询
用户信息列表
ThreadLocalUtil
Hutool工具库
常用功能介绍
B端用户管理
进行列表显示与用户状态修改(拉黑操作)
用户列表显示主要运用了分页管理给前端传输数据(UserVO)从而在页面上显示
拉黑操作,根据前端传送过来的数据,在数据库中根据用户id搜索此用户,失败返回异常
成功执行user.setStatus(userDTO.getStatus()); 作用是把 userDTO 里的状态值赋给 user 对象
userCacheManager.updateStatus(user.getUserId(), userDTO.getStatus()); 则是让缓存中的用户状态与新状态保持一致,执行 updateStatus 函数,先刷新用户缓存,
@Override
public List<UserVO> list(UserQueryDTO userQueryDTO) {
PageHelper.startPage(userQueryDTO.getPageNum(),userQueryDTO.getPageSize());
return userMapper.selectUserList(userQueryDTO);
}
@Override
public int updateStatus(UserDTO userDTO) {
User user = userMapper.selectById(userDTO.getUserId());
if(user==null){
throw new ServiceException(ResultCode.FAILED_USER_NOT_EXISTS);
}
//这里是从前端拿到的值,假设前端把用户拉黑,返回的就是0,设置也就是0
user.setStatus(userDTO.getStatus());
userCacheManager.updateStatus(user.getUserId(),userDTO.getStatus());
return userMapper.updateById(user);
}
public void updateStatus(Long userId,Integer status) {
//刷新用户缓存
String userKey = getUserKey(userId);
User user = redisService.getCacheObject(userKey, User.class);
if(user==null){
return;
}
user.setStatus(status);
redisService.setCacheObject(userKey, user);
//设置用户缓存有效期为10分钟
redisService.expire(userKey, CacheConstants.USER_EXP, TimeUnit.MINUTES);
}
getCacheObiect中从Redis缓存中获取对象并转换为指定类型
从redis缓存中获取指定key的对象,并将其转换为指定类型的示例
/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public <T> T getCacheObject(final String key, Class<T> clazz) {
// 获取 Redis 的 Value 操作对象
ValueOperations<String, T> operation = redisTemplate.opsForValue();
// 从缓存中获取对象
T t = operation.get(key);
// 如果缓存值是 String 类型且目标类型也是 String,则直接返回
if (t instanceof String) {
return t;
}
// 否则尝试将对象转换为 JSON 字符串并解析为目标类型
return JSON.parseObject(String.valueOf(t), clazz);
}
C端用户代码
发送验证码:
从前端拿到数据,获取到用户手机号码,
进入validatePhone(phone);用于判断phone是否为空(StrUtil.isBlank(phone),isStrUtil.isBlank是Hutool工具类下的方法Hutool工具类下方有介绍),为空执行异常,不为空判断手机号格式是否正确,如果正确,则正常进行频率检查
checkDailyRequestLimit()检查每日请求限制
生成Redis存储的key(格式示例:c:t:13800138000),从redis获取已发送次数(String类型),转化发送次数为Long类型(若为空则默认为0),给sendTimes赋值,与每日限制次数(sendLimit)进行对比,结果为true则抛出异常
@Override
public boolean sendCode(UserDTO userDTO) {
// 1. 参数校验
String phone = userDTO.getPhone();
validatePhone(phone);
// 2. 频率控制检查
checkRequestFrequency(phone);
// 3. 每日限额检查
checkDailyRequestLimit(phone);
// 4. 生成并存储验证码
String code = generateVerificationCode(phone);
// 5. 发送验证码
sendVerificationCode(phone, code);
System.out.println("code: "+code);
return true;
}
private void validatePhone(String phone) {
if (StrUtil.isBlank(phone)) {
throw new ServiceException(ResultCode.FAILED_USER_PHONE_EMPTY);
}
// 中国手机号正则(11位,1开头)
String regex = "^1[3-9]\\\\d{9}$";
if (!Pattern.matches(regex, phone)) {
throw new ServiceException(ResultCode.FAILED_USER_PHONE_INVALID);
}
}
private void checkRequestFrequency(String phone) {
// getExpire()获取当前 Key 的剩余生存时间
String phoneCodeKey = getPhoneCodeKey(phone);
Long expire = redisService.getExpire(phoneCodeKey, TimeUnit.SECONDS);
// 如果上一次验证码的发送时间距离现在 不足1分钟(即 总有效期 – 剩余时间 < 60秒),则拒绝新请求。
if (expire != null && (emailCodeExpiration * 60 – expire) < 60) {
throw new ServiceException(ResultCode.FAILED_FREQUENT);
}
}
private void checkDailyRequestLimit(String phone) {
// 每天的验证码获取次数有一个限制 50 次 , 第二天 计数清0 重新开始计数
// 计数 怎么存,存在哪里 ?
// 操作次数数据频繁,不需要存储, 记录的次数 有效时间(当天有效) redis String key: c:t:
// 1.获取已经请求的次数 和 50 进行比较
String codeTimeKey = getCodeTimeKey(phone);
String sendTimesStr = redisService.getCacheObject(codeTimeKey, String.class);
Long sendTimes = sendTimesStr != null ? Long.parseLong(sendTimesStr) : 0L;
// 如果大于限制,抛出异常
// 如果不大于限制,正常执行后续逻辑,并且将获取计数+1
if (sendTimes >= sendLimit) {
throw new ServiceException(ResultCode.FAILED_TIME_LIMIT);
}
}
验证验证码
对比验证码是否正确,如果正确,根据输入的phone去数据库中查询
如果user为null则在数据库中添加一个新用户,添加手机号,用户状态
最后生成专属令牌,用于后续验证
@Override
public String codeLogin(String phone, String code) {
//判断验证码是否正确
CheckCode(phone,code);
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhone, phone));
if(user==null){ //新用户
//注册逻辑 1.先完成验证码的比对->成功,往系统中增加用户
user=new User();
user.setPhone(phone);
user.setStatus(UserStatus.Normal.getValue());
userMapper.insert(user);
}
return tokenService.createToken(user.getUserId(),secret, UserIdentity.ORDINARY.getValue(),user.getNickName(),user.getHeadImage());
private void CheckCode(String phone, String code) {
String phoneCodeKey = getPhoneCodeKey(phone);
String cacheCode = redisService.getCacheObject(phoneCodeKey,String.class);
if(StrUtil.isEmpty(cacheCode)){
throw new ServiceException(ResultCode.FAILED_INVALID_CODE);
}
if(!cacheCode.equals(code)){
throw new ServiceException(ResultCode.FAILED_CODE_MISMATCH);
}
redisService.deleteObject(phoneCodeKey);
}
退出登录
最开始检测token是否为空这步是一个保障,防止没有生成令牌
public static final String PREFIX = "Bearer ";为令牌前缀,如果存在令牌前缀则把前缀替换成空
最后删除令牌
@Override
public boolean logout(String token) {
//检查token是否为空
if (StrUtil.isEmpty(token)) {
throw new ServiceException(ResultCode.TOKEN_IS_DEFAULT);
}
//再处理前缀
if (token.startsWith(HttpConstants.PREFIX)) {
token = token.replaceFirst(HttpConstants.PREFIX,StrUtil.EMPTY);
}
return tokenService.deleteLoginUser(token,secret);
}
登录用户信息功能
LoginUserVO,返回给前端的数据(包括头像和昵称)
根据token获取登录用户信息返回,包含用户基本信息的VO对象
用户头像字段需要拼接下载地址前缀,使用StrUtil.isNotEmpty确保头像字段非空
@Override
public R<LoginUserVO> info(String token) {
// if (StrUtil.isNotEmpty(token) && token.startsWith(HttpConstants.PREFIX)) {
// token = token.replaceFirst(HttpConstants.PREFIX, StrUtil.EMPTY);
// }
// 先检查token是否为空
if (StrUtil.isEmpty(token)) {
throw new ServiceException(ResultCode.TOKEN_IS_DEFAULT);
}
// 再处理前缀
if (token.startsWith(HttpConstants.PREFIX)) {
token = token.substring(HttpConstants.PREFIX.length());
}
LoginUser loginUser = tokenService.getLoginUser(token, secret);
if(loginUser==null){
return R.fail();
}
LoginUserVO loginUserVO=new LoginUserVO();
loginUserVO.setNickName(loginUser.getNickName());
if(StrUtil.isNotEmpty(loginUser.getHeadImage())){
loginUserVO.setHeadImage(download+loginUser.getHeadImage());
}
return R.ok(loginUserVO);
}
用户详情与用户编辑
用户详情
从线程池中获取userId的值
定义一个userVO对象,从数据库中根据userId查询内容赋值给userVO
如果用户存在,设置头像,存入userVO,最后返回给前端
用户编辑
先把用户获取出来,根据前端输入的内容去跟新数据库中的值
最后更新缓存
extracted(),刷新用户缓存和登录状态信息
UserCacheManager .refreshUser: 用户缓存刷新接口
TokenService.refreshLoginUser: Token信息刷新接口
@Override
public UserVO detail() {
Long userId= ThreadLocalUtil.get(Constants.USER_ID,Long.class);
if(userId==null){
throw new ServiceException(ResultCode.FAILED_USER_NOT_EXISTS);
}
UserVO userVO;
userVO=userCacheManager.getUserById(userId);
if(userVO==null){
throw new ServiceException(ResultCode.FAILED_USER_NOT_EXISTS);
}
if(StrUtil.isNotEmpty(userVO.getHeadImage())){
userVO.setHeadImage(download+userVO.getHeadImage());
}
return userVO;
}
@Override
public int edit(UserUpdateDTO userUpdateDTO) {
User user = getUser();
user.setNickName(userUpdateDTO.getNickName());
user.setSex(userUpdateDTO.getSex());
user.setSchoolName(userUpdateDTO.getSchoolName());
user.setMajorName(userUpdateDTO.getMajorName());
user.setPhone(userUpdateDTO.getPhone());
user.setEmail(userUpdateDTO.getEmail());
user.setWechat(userUpdateDTO.getWechat());
user.setIntroduce(userUpdateDTO.getIntroduce());
//更新用户缓存
extracted(user);
return userMapper.updateById(user);
}
private void extracted(User user) {
userCacheManager.refreshUser(user);
tokenService.refreshLoginUser(user.getNickName(), user.getHeadImage(),
ThreadLocalUtil.get(Constants.USER_KEY, String.class));
}
用户竞赛接口
用户报名竞赛
获取当前用户信息,筛选用户是否符合条件(1.用户处于登录状态 2.不能报名不存在的竞赛 3.不能重复报名 4.已经开赛的禁止报名)
条件符合之后,往用户竞赛表中添加数据(底层使用list结构存储数据(头插法))
@Override
public int enter(String token, Long examId) {
// 获取当前用户的信息 status
Long userId = ThreadLocalUtil.get(Constants.USER_ID, Long.class);
// UserVO user = userCacheManager.getUserById(userId);
// if (user.getStatus()==0){
// throw new ServiceException(ResultCode.FAILED_USER_BANNED);
// }
//使用spring aop
//报名是否符合条件 (1.用户处于登录状态 2.不能报名不存在的竞赛 3.不能重复报名 4.已经开赛的禁止报名)
Exam exam = examMapper.selectById(examId);
if(exam==null){
throw new ServiceException(ResultCode.EXAM_RESULT_NOT_EXIST);
}
if(exam.getStartTime().isBefore(LocalDateTime.now())){
throw new ServiceException(ResultCode.EXAM_IS_START);
}
// Long userId= tokenService.getUserId(token,secret);
// Long userId = userId;
UserExam userExam = userExamMapper.selectOne(new LambdaQueryWrapper<UserExam>()
.eq(UserExam::getExamId, examId)
.eq(UserExam::getUserId, userId));
if(userExam!=null){
throw new ServiceException(ResultCode.USER_EXAM_HAS_ENTER);
}
examCacheManager.addUserExamCache(userId, examId);
userExam=new UserExam();
userExam.setExamId(examId);
userExam.setUserId(userId);
return userExamMapper.insert(userExam);
}
用户竞赛报名接口查询
type :0:代表未完赛 1:代表历史竞赛
先查询用户信息,把type的值设置为2,
userExamMapper.selectUserExamList(userId)根据userId获取指定类型的考试列表长度
判断type和userId是否为空
不为空生成redis唯一标识符,返回一个json形式的数据
@Override
public TableDataInfo list(ExamQueryDTO examQueryDTO) {
Long userId = ThreadLocalUtil.get(Constants.USER_ID, Long.class);
examQueryDTO.setType(ExamListType.USER_EXAM_LIST.getValue());
Long total = examCacheManager.getListSize(ExamListType.USER_EXAM_LIST.getValue(),userId);
List<ExamVO> examVOList;
if(total == null || total <= 0){//缓存不存在时的处理:
//从数据库中查询我的竞赛列表
PageHelper.startPage(examQueryDTO.getPageNum(),examQueryDTO.getPageSize());
examVOList = userExamMapper.selectUserExamList(userId);
//将数据库中的数据同步给缓存
examCacheManager.refreshCache(examQueryDTO.getType(),userId);
//从数据库中查询
total=new PageInfo<>(examVOList).getTotal();
}else{//缓存存在时的处理:
examVOList = examCacheManager.getExamVOList(examQueryDTO,userId);
//从redis中查询,在查询时,出现异常情况,可能会重新刷新数据,这次查询用户数据的更新
// 0:代表未完赛 1:表示开始竞赛
total = examCacheManager.getListSize(examQueryDTO.getType(),userId);
}
// 空结果处理:
if (CollectionUtil.isEmpty(examVOList)){
return TableDataInfo.empty();
}
//获取符合查询条件的数据总数
//从数据库中查询数据,不是从redis中查询数据
return TableDataInfo.success(examVOList,total);
}
public Long getListSize(Integer examListType, Long userId) {
// 1. 参数校验(示例,根据实际需求调整)
if (examListType == null || userId == null) {
throw new IllegalArgumentException("参数不能为null");
}
// 2. 生成业务唯一的Redis键
String examListKey = getExamListKey(examListType, userId);
// 3. 获取Redis列表长度(返回Long类型,可能为0)
return redisService.getListSize(examListKey);
}
//未查出任何数据时调用
public static TableDataInfo empty() {
TableDataInfo rspData = new TableDataInfo();
rspData.setCode(ResultCode.SUCCESS.getCode());
rspData.setRows(new ArrayList<>());
rspData.setMsg(ResultCode.SUCCESS.getMsg());
rspData.setTotal(0);
return rspData;
}
//查出数据时调用
public static TableDataInfo success(List<?> list,long total) {
TableDataInfo rspData = new TableDataInfo();
rspData.setCode(ResultCode.SUCCESS.getCode());
rspData.setRows(list);
rspData.setMsg(ResultCode.SUCCESS.getMsg());
rspData.setTotal(total);
return rspData;
}
用户信息列表
获取用户消息列表(带分页,优先从缓存读取)
从ThreadLocal获取当前用户ID 检查缓存中是否存在消息列表
缓存不存在,为空时查询数据库并刷新缓存
返回分页格式的统一响应
public TableDataInfo list(PageQueryDTO dto) {
// 1. 获取当前用户ID(从线程上下文)
Long userId = ThreadLocalUtil.get(Constants.USER_ID, Long.class);
if (userId == null) {
throw new ServiceException("用户未登录");
}
// 2. 检查缓存是否存在有效数据
Long total = messageCacheManager.getListSize(userId);
List<MessageTextVO> messageTextVOList;
// 3. 缓存不存在时走数据库查询
if (total == null || total <= 0) {
// 3.1 启用分页查询
PageHelper.startPage(dto.getPageNum(), dto.getPageSize());
messageTextVOList = messageTextMapper.selectUserMsgList(userId);
// 3.2 刷新缓存
messageCacheManager.refreshCache(userId);
total = new PageInfo<>(messageTextVOList).getTotal();
}
// 4. 缓存存在时直接读取
else {
messageTextVOList = messageCacheManager.getMsgTextVOList(dto, userId);
}
// 5. 处理空结果
if (CollectionUtil.isEmpty(messageTextVOList)) {
return TableDataInfo.empty();
}
// 6. 返回统一分页响应
return TableDataInfo.success(messageTextVOList, total);
}
ThreadLocalUtil
ThreadLocalUtil是一个工具类,封装了Java的ThreadLocal操作,ThreadLocal工具类(基于TransmittableThreadLocal实现)
封装线程隔离的变量存储功能,解决原生ThreadLocal在线程池场景下的数据污染问题
使用TransmittableThreadLocal替代原生ThreadLocal,支持线程池环境
每个线程独立维护ConcurrentHashMap存储数据,线程安全
自动处理null值存储(转为空字符串)
必须显式调用remove()避免内存泄漏详情看注释
//ThreadLocalUtil是一个工具类,封装了Java的ThreadLocal操作,
//用于实现线程隔离的变量存储(每个线程独立存取数据,互不干扰)。
public class ThreadLocalUtil {
// 使用 TransmittableThreadLocal(而非原生 ThreadLocal),
// 解决原生 ThreadLocal 在线程池中线程复用导致的数据错乱问题。
private static final TransmittableThreadLocal<Map<String, Object>>
THREAD_LOCAL = new TransmittableThreadLocal<>();
/**
* 存储键值对到当前线程上下文
* @param key 键(非空)
* @param value 值(自动转换null为空字符串)
*/
public static void set(String key, Object value) {
Map<String, Object> map = getLocalMap();
map.put(key, value == null ? StrUtil.EMPTY : value);
}
/**
* 从当前线程上下文获取值(带类型转换)
* @param key 键
* @param clazz 目标类型Class对象
* @return 值(不存在时返回null)
* @param <T> 返回值泛型
*/
public static <T> T get(String key, Class<T> clazz) {
Map<String, Object> map = getLocalMap();
return (T) map.getOrDefault(key, null);
}
/**
* 获取当前线程的存储Map(不存在时自动初始化)
*
* 注意:使用ConcurrentHashMap保证线程安全</p>
*
* @return 当前线程关联的Map(永不为null)
*/
// 每个线程独立维护一个 Map<String, Object>,
// 通过键值对(Key-Value)存储数据,线程间数据互不干扰。
public static Map<String, Object> getLocalMap() {
Map<String, Object> map = THREAD_LOCAL.get();
if (map == null) {
map = new ConcurrentHashMap<String, Object>();
THREAD_LOCAL.set(map);
}
return map;
}
/**
* 清除当前线程的存储Map(
*
* 使用场景:
* 线程池场景必须调用,避免内存泄漏
* 请求处理结束时建议调用
*/
//清除当前线程的 Map,防止内存泄漏(尤其在线程池场景中必须调用)
public static void remove() {
THREAD_LOCAL.remove();
}
}
Hutool工具库
官方网站:
Hutool🍬一个功能丰富且易用的Java工具库,涵盖了字符串、数字、集合、编码、日期、文件、IO、加密、数据库JDBC、JSON、HTTP客户端等功能。
导入依赖
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>5.8.16</version> <!– 以最新版本为准 –>
</dependency>
常用功能介绍
1.1 字符串工具(StrUtil)
import cn.hutool.core.util.StrUtil;
// 判空
boolean isBlank = StrUtil.isBlank(" "); // true(空或空白字符)
boolean isEmpty = StrUtil.isEmpty(""); // true(仅空字符串)
// 格式化字符串
String formatted = StrUtil.format("Hello, {}!", "Hutool"); // "Hello, Hutool!"
// 字符串截取、填充、反转
String sub = StrUtil.sub("HelloWorld", 2, 5); // "llo"
String padded = StrUtil.padPre("123", 5, '0'); // "00123"
String reversed = StrUtil.reverse("ABC"); // "CBA"
1.2 数字工具(NumberUtil)
import cn.hutool.core.util.NumberUtil;
// 数学计算
int sum = NumberUtil.add(1, 2, 3); // 6
double div = NumberUtil.div(10, 3, 2); // 3.33(保留2位小数)
// 数字判断
boolean isNumber = NumberUtil.isNumber("123.45"); // true
boolean isInteger = NumberUtil.isInteger("100"); // true
1.3 日期时间工具(DateUtil)
import cn.hutool.core.date.DateUtil;
// 日期解析与格式化
Date date = DateUtil.parse("2023-10-01"); // String → Date
String dateStr = DateUtil.format(date, "yyyy/MM/dd"); // "2023/10/01"
// 日期计算
Date tomorrow = DateUtil.offsetDay(new Date(), 1); // 明天
long betweenDays = DateUtil.between(date, new Date(), DateUnit.DAY); // 相差天数
// 获取时间部分
int year = DateUtil.year(date); // 2023
int month = DateUtil.month(date) + 1; // 10(月份从0开始)
1.4 集合工具(CollUtil)
import cn.hutool.core.collection.CollUtil;
List<String> list = CollUtil.newArrayList("A", "B", "C");
// 判空
boolean isEmpty = CollUtil.isEmpty(list); // false
// 集合操作
List<String> reversedList = CollUtil.reverse(list); // ["C", "B", "A"]
String joined = CollUtil.join(list, ","); // "A,B,C"
2.1 文件工具(FileUtil)
import cn.hutool.core.io.FileUtil;
// 读取文件内容
String content = FileUtil.readUtf8String("test.txt");
// 写入文件
FileUtil.writeUtf8String("Hello, Hutool!", "output.txt");
// 文件操作
boolean exists = FileUtil.exist("test.txt"); // 检查文件是否存在
FileUtil.copy("src.txt", "dest.txt", true); // 复制文件(覆盖)
FileUtil.del("temp_dir"); // 删除目录
2.2 流工具(IoUtil)
import cn.hutool.core.io.IoUtil;
// 流拷贝(自动关闭流)
try (InputStream in = new FileInputStream("src.txt");
OutputStream out = new FileOutputStream("dest.txt")) {
IoUtil.copy(in, out, IoUtil.DEFAULT_BUFFER_SIZE);
}
3.1 摘要加密(DigestUtil)
import cn.hutool.crypto.digest.DigestUtil;
// MD5
String md5 = DigestUtil.md5Hex("123456"); // "e10adc3949ba59abbe56e057f20f883e"
// SHA-256
String sha256 = DigestUtil.sha256Hex("123456");
// 加盐加密(BCrypt)
String hashed = BCrypt.hashpw("password");
boolean isMatch = BCrypt.checkpw("password", hashed); // 校验
3.2 对称加密(AES、DES)
import cn.hutool.crypto.SecureUtil;
// AES 加密
String key = "1234567890abcdef"; // 16/24/32位密钥
String encrypted = SecureUtil.aes(key.getBytes()).encryptHex("Hello");
String decrypted = SecureUtil.aes(key.getBytes()).decryptStr(encrypted);
hutool-core | 字符串、日期、集合、数字 | StrUtil, DateUtil, CollUtil |
hutool-http | HTTP 请求 | HttpUtil |
hutool-crypto | 加密解密(MD5、AES、BCrypt) | DigestUtil, SecureUtil |
hutool-json | JSON/XML 处理 | JSONUtil, XmlUtil |
hutool-extra | 文件、IO、邮件等 | FileUtil, MailUtil |
评论前必须登录!
注册