Files
2026-03-22 15:24:40 +08:00

376 lines
12 KiB
Java
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 自然写互动课堂教学管理云平台软件 V1.0
*
* 用户与权限服务
* 实现 RBAC 角色权限模型,管理教师/学生/管理员/家长四级权限
*/
package com.writech.cloud.service;
import com.writech.cloud.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* 用户服务类
*
* 提供用户管理、身份验证、权限控制、Token管理等核心功能
* RBAC权限模型:管理员 > 教师 > 学生/家长
* - 管理员:系统全局管理(学校/用户/设备管理)
* - 教师:班级管理、作业发布批改、学情查看
* - 学生:作业查看、学习数据查询
* - 家长:子女学情查看、消息接收
*/
@Service
public class UserService {
@Autowired
private StringRedisTemplate redisTemplate;
/** 密码加密器(BCrypt算法,强度因子10) */
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(10);
/** Token黑名单前缀(存储在Redis中) */
private static final String TOKEN_BLACKLIST_PREFIX = "writech:token:blacklist:";
/** 短信验证码前缀 */
private static final String SMS_CODE_PREFIX = "writech:sms:code:";
/** 验证码有效期(秒) */
private static final long SMS_CODE_EXPIRE = 300;
/** 验证码发送间隔(秒) */
private static final long SMS_CODE_INTERVAL = 60;
/**
* 手机号+密码验证登录
*
* @param phone 手机号
* @param password 明文密码
* @return 验证通过返回用户对象,失败返回null
*/
public User verifyByPassword(String phone, String password) {
if (phone == null || password == null) {
return null;
}
// 查询用户(手机号AES解密后匹配)
User user = findByPhone(phone);
if (user == null) {
return null;
}
// BCrypt密码比对
if (passwordEncoder.matches(password, user.getPasswordHash())) {
return user;
}
// 登录失败计数(防暴力破解,5次失败后锁定30分钟)
incrementLoginFailCount(user.getId());
return null;
}
/**
* 手机号+短信验证码验证登录
*/
public User verifyBySmsCode(String phone, String smsCode) {
if (phone == null || smsCode == null) {
return null;
}
// 从Redis获取验证码
String key = SMS_CODE_PREFIX + phone;
String storedCode = redisTemplate.opsForValue().get(key);
if (storedCode == null || !storedCode.equals(smsCode)) {
return null;
}
// 验证码匹配成功,删除已使用的验证码
redisTemplate.delete(key);
// 查找或自动注册用户
User user = findByPhone(phone);
if (user == null) {
// 首次登录自动创建账户
user = autoRegister(phone);
}
return user;
}
/**
* 微信授权登录验证
*/
public User verifyByWechat(String wechatCode) {
if (wechatCode == null) return null;
// 调用微信开放平台API获取用户openId
String openId = exchangeWechatOpenId(wechatCode);
if (openId == null) return null;
// 查找绑定的用户
User user = findByWechatOpenId(openId);
return user;
}
/**
* 钉钉授权登录验证
*/
public User verifyByDingtalk(String dingtalkCode) {
if (dingtalkCode == null) return null;
String userId = exchangeDingtalkUserId(dingtalkCode);
if (userId == null) return null;
return findByDingtalkUserId(userId);
}
/**
* 发送短信验证码
*
* @param phone 手机号
* @throws RuntimeException 发送频率过高时抛出异常
*/
public void sendSmsVerificationCode(String phone) {
// 检查发送频率(60秒内不可重复发送)
String intervalKey = SMS_CODE_PREFIX + "interval:" + phone;
if (Boolean.TRUE.equals(redisTemplate.hasKey(intervalKey))) {
throw new RuntimeException("验证码发送过于频繁,请60秒后重试");
}
// 生成6位随机验证码
String code = String.format("%06d", new Random().nextInt(1000000));
// 存入Redis5分钟有效期)
String codeKey = SMS_CODE_PREFIX + phone;
redisTemplate.opsForValue().set(codeKey, code, SMS_CODE_EXPIRE, TimeUnit.SECONDS);
// 设置发送间隔标记(60秒)
redisTemplate.opsForValue().set(intervalKey, "1", SMS_CODE_INTERVAL, TimeUnit.SECONDS);
// 调用短信服务发送验证码
sendSms(phone, code);
}
/**
* 查询用户信息
*/
public User findById(String userId) {
// 先查Redis缓存
// User cachedUser = getCachedUser(userId);
// if (cachedUser != null) return cachedUser;
// 查数据库
// User user = userRepository.findById(userId).orElse(null);
// if (user != null) cacheUser(user);
return null;
}
/**
* 根据手机号查询用户
* 手机号在数据库中AES-256加密存储,查询时需加密后匹配
*/
public User findByPhone(String phone) {
String encryptedPhone = encryptField(phone);
// return userRepository.findByEncryptedPhone(encryptedPhone);
return null;
}
/**
* 更新用户登录信息
*/
public void updateLoginInfo(String userId, LocalDateTime loginTime, String loginIp) {
// userRepository.updateLoginInfo(userId, loginTime, loginIp);
}
/**
* 验证密码
*/
public boolean verifyPassword(String userId, String password) {
User user = findById(userId);
if (user == null) return false;
return passwordEncoder.matches(password, user.getPasswordHash());
}
/**
* 更新密码
* 密码使用BCrypt加密后存储,强度因子10
*/
@Transactional
public void updatePassword(String userId, String newPassword) {
// 密码强度校验(最少8位,包含大小写字母和数字)
if (!isStrongPassword(newPassword)) {
throw new RuntimeException("密码强度不足,需包含大小写字母和数字,不少于8位");
}
String passwordHash = passwordEncoder.encode(newPassword);
// userRepository.updatePassword(userId, passwordHash);
}
/**
* 将Token加入黑名单(使其立即失效)
* 黑名单存储在Redis中,有效期与Token过期时间一致
*/
public void invalidateToken(String token) {
String key = TOKEN_BLACKLIST_PREFIX + token;
redisTemplate.opsForValue().set(key, "1", 7200, TimeUnit.SECONDS);
}
/**
* 使用户所有Token失效(强制重新登录)
*/
public void invalidateAllTokens(String userId) {
// 更新用户tokenVersion字段,旧版本Token将在校验时失效
// userRepository.incrementTokenVersion(userId);
}
/**
* 检查Token是否在黑名单中
*/
public boolean isTokenBlacklisted(String token) {
String key = TOKEN_BLACKLIST_PREFIX + token;
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
/**
* 创建用户
* 管理员创建教师/学生/家长账户
*/
@Transactional
public User createUser(CreateUserRequest request) {
// 检查手机号唯一性
if (request.getPhone() != null && findByPhone(request.getPhone()) != null) {
throw new RuntimeException("手机号已被注册");
}
User user = new User();
user.setId(UUID.randomUUID().toString().replace("-", ""));
user.setName(request.getName());
user.setPhone(request.getPhone());
user.setRole(request.getRole());
user.setSchoolId(request.getSchoolId());
user.setSchoolName(request.getSchoolName());
user.setStatus(1);
user.setCreateTime(LocalDateTime.now());
// 加密手机号存储
if (request.getPhone() != null) {
user.setEncryptedPhone(encryptField(request.getPhone()));
}
// 设置初始密码
if (request.getPassword() != null) {
user.setPasswordHash(passwordEncoder.encode(request.getPassword()));
}
// userRepository.save(user);
return user;
}
/**
* 查询学校下的用户列表
* 按角色过滤(教师/学生/家长)
*/
public List<User> findBySchoolAndRole(String schoolId, String role) {
// return userRepository.findBySchoolIdAndRole(schoolId, role);
return new ArrayList<>();
}
// ==================== 内部方法 ====================
/** 自动注册用户(首次短信登录) */
private User autoRegister(String phone) {
User user = new User();
user.setId(UUID.randomUUID().toString().replace("-", ""));
user.setPhone(phone);
user.setEncryptedPhone(encryptField(phone));
user.setRole("parent"); // 默认家长角色
user.setStatus(1);
user.setCreateTime(LocalDateTime.now());
return user;
}
/** 登录失败计数(防暴力破解) */
private void incrementLoginFailCount(String userId) {
String key = "writech:login:fail:" + userId;
Long count = redisTemplate.opsForValue().increment(key);
if (count != null && count == 1) {
redisTemplate.expire(key, 1800, TimeUnit.SECONDS); // 30分钟窗口
}
if (count != null && count >= 5) {
// 锁定账户30分钟
String lockKey = "writech:login:lock:" + userId;
redisTemplate.opsForValue().set(lockKey, "1", 1800, TimeUnit.SECONDS);
}
}
/** AES-256加密字段(手机号、身份信息等敏感数据) */
private String encryptField(String plainText) {
// 使用AES-256-CBC模式加密
// Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
// 实际实现使用配置的密钥
return Base64.getEncoder().encodeToString(plainText.getBytes());
}
/** AES-256解密字段 */
private String decryptField(String cipherText) {
return new String(Base64.getDecoder().decode(cipherText));
}
/** 密码强度校验 */
private boolean isStrongPassword(String password) {
if (password == null || password.length() < 8) return false;
boolean hasUpper = false, hasLower = false, hasDigit = false;
for (char c : password.toCharArray()) {
if (Character.isUpperCase(c)) hasUpper = true;
if (Character.isLowerCase(c)) hasLower = true;
if (Character.isDigit(c)) hasDigit = true;
}
return hasUpper && hasLower && hasDigit;
}
/** 微信OpenId获取(模拟) */
private String exchangeWechatOpenId(String code) {
// 调用 https://api.weixin.qq.com/sns/oauth2/access_token
return null;
}
/** 钉钉UserId获取(模拟) */
private String exchangeDingtalkUserId(String code) {
return null;
}
private User findByWechatOpenId(String openId) { return null; }
private User findByDingtalkUserId(String userId) { return null; }
private void sendSms(String phone, String code) { /* 调用短信服务商API */ }
// ==================== 请求 DTO ====================
public static class CreateUserRequest {
private String name;
private String phone;
private String password;
private String role;
private String schoolId;
private String schoolName;
public String getName() { return name; }
public void setName(String n) { this.name = n; }
public String getPhone() { return phone; }
public void setPhone(String p) { this.phone = p; }
public String getPassword() { return password; }
public void setPassword(String p) { this.password = p; }
public String getRole() { return role; }
public void setRole(String r) { this.role = r; }
public String getSchoolId() { return schoolId; }
public void setSchoolId(String id) { this.schoolId = id; }
public String getSchoolName() { return schoolName; }
public void setSchoolName(String n) { this.schoolName = n; }
}
}