software copyright

This commit is contained in:
jiahong
2026-03-22 15:24:40 +08:00
parent e303bb868a
commit 60f336e345
155 changed files with 127262 additions and 0 deletions
@@ -0,0 +1,375 @@
/**
* 自然写互动课堂教学管理云平台软件 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; }
}
}