UserServiceImpl.java 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. package com.njuzr.eaibackend.service.impl;
  2. import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
  3. import com.baomidou.mybatisplus.core.metadata.IPage;
  4. import com.baomidou.mybatisplus.core.toolkit.StringUtils;
  5. import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
  6. import com.njuzr.eaibackend.dto.user.AdminRegisterDTO;
  7. import com.njuzr.eaibackend.dto.user.UserDTO;
  8. import com.njuzr.eaibackend.dto.user.UserRegisterDTO;
  9. import com.njuzr.eaibackend.dto.user.UserUpdateDTO;
  10. import com.njuzr.eaibackend.enums.Role;
  11. import com.njuzr.eaibackend.exception.MyException;
  12. import com.njuzr.eaibackend.mapper.UserMapper;
  13. import com.njuzr.eaibackend.po.User;
  14. import com.njuzr.eaibackend.service.EmailService;
  15. import com.njuzr.eaibackend.service.UserService;
  16. import com.njuzr.eaibackend.utils.ModelMapperUtil;
  17. import com.njuzr.eaibackend.utils.PageMapperUtil;
  18. import com.njuzr.eaibackend.vo.UserVO;
  19. import com.opencsv.CSVReader;
  20. import lombok.extern.slf4j.Slf4j;
  21. import org.apache.poi.ss.usermodel.*;
  22. import org.apache.poi.xssf.usermodel.XSSFWorkbook;
  23. import org.springframework.beans.factory.annotation.Autowired;
  24. import org.springframework.data.redis.core.RedisTemplate;
  25. import org.springframework.data.redis.core.ValueOperations;
  26. import org.springframework.http.HttpStatus;
  27. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  28. import org.springframework.security.crypto.password.PasswordEncoder;
  29. import org.springframework.stereotype.Service;
  30. import org.springframework.web.multipart.MultipartFile;
  31. import java.io.InputStreamReader;
  32. import java.util.*;
  33. import java.util.concurrent.TimeUnit;
  34. /**
  35. * @author: Leonezhurui
  36. * @Date: 2024/2/15 - 09:00
  37. * @Package: EAI-Backend
  38. */
  39. @Slf4j
  40. @Service
  41. public class UserServiceImpl implements UserService {
  42. private final UserMapper userMapper;
  43. private final RedisTemplate<String, Object> redisTemplate;
  44. private final EmailService emailService;
  45. private static final long SEND_INTERVAL = 60; // 允许再次发送的时间间隔,单位秒
  46. private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
  47. @Autowired
  48. public UserServiceImpl(UserMapper userMapper, RedisTemplate<String, Object> redisTemplate, EmailService emailService) {
  49. this.userMapper = userMapper;
  50. this.redisTemplate = redisTemplate;
  51. this.emailService = emailService;
  52. }
  53. @Override
  54. public IPage<UserVO> searchUsers(Page<User> page, Long id, String name, String officialNumber, Role role) {
  55. QueryWrapper<User> queryWrapper = new QueryWrapper<>(); // 设置查询条件
  56. if (role == Role.ADMIN) // 禁止访问查询ADMIN用户
  57. throw new MyException(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase() + ":" + "被拒绝");
  58. queryWrapper
  59. .eq(Objects.nonNull(id), "id", id)
  60. .like(StringUtils.isNotBlank(name), "name", name)
  61. .eq(StringUtils.isNotBlank(officialNumber), "official_number", officialNumber)
  62. .eq(Objects.nonNull(role), "role", role);
  63. IPage<User> targets = userMapper.selectPage(page, queryWrapper); // selectPage是内置的方法
  64. // targets.total如果为0,则说明没有符合条件的用户,但是不报错,由前端自行处理异常。
  65. return PageMapperUtil.convert(targets, record -> ModelMapperUtil.map(record, UserVO.class));
  66. }
  67. //TODO: 注册DEMO:学生老师都可自行注册账号,管理员那套不需要修改/弃用,学生注册这套已经有完整逻辑,直接使用
  68. @Override
  69. public UserVO createUser(UserRegisterDTO userDTO) throws MyException {
  70. // 从Redis中获取验证码
  71. final String key = "verifyCode:" + userDTO.getOfficialEmail();
  72. String storedCode = (String) redisTemplate.opsForValue().get(key);
  73. if (storedCode == null)
  74. throw new MyException(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase() + ":" + "验证码已过期");
  75. // 验证码校验
  76. if (storedCode.equals(userDTO.getVerifyCode())) {
  77. User targetUser = ModelMapperUtil.map(userDTO, User.class);
  78. targetUser.setPassword(new BCryptPasswordEncoder().encode(targetUser.getPassword())); // 密码加密存储
  79. targetUser.setCreateTime(new Date()); // 设置创建时间
  80. // 同一个邮箱、学号只能注册一次
  81. if (isOfficialNumberNotExists(targetUser.getOfficialNumber())
  82. && isEmailNotExists(targetUser.getOfficialEmail())) {
  83. int status = userMapper.insert(targetUser);
  84. if (status == 0) {
  85. log.error("createUser -- 数据库插入错误:");
  86. throw new MyException(501, "数据库插入错误");
  87. }
  88. }
  89. log.info("User创建完毕,User对象如下:" + targetUser);
  90. redisTemplate.delete(key); //注册成功后删除验证码
  91. return ModelMapperUtil.map(targetUser, UserVO.class);
  92. } else {
  93. throw new MyException(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase() + ":" + "验证码错误");
  94. }
  95. }
  96. @Override
  97. public void sendVerifyCode(String officialEmail) throws MyException {
  98. // 检验邮箱合法性,只允许南京大学邮箱
  99. if (!officialEmail.endsWith("@smail.nju.edu.cn") && !officialEmail.endsWith("@nju.edu.cn")) {
  100. throw new MyException(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase() + ":" + "只允许南京大学邮箱,请检查邮箱格式");
  101. }
  102. ValueOperations<String, Object> ops = redisTemplate.opsForValue();
  103. String intervalKey = "requestInterval:" + officialEmail;
  104. // 检查是否已经发送过验证码并且时间间隔未过
  105. if (ops.get(intervalKey) != null) {
  106. throw new MyException(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase() + ":" + "需等待" + SEND_INTERVAL + "秒才能再次发送验证码");
  107. }
  108. // 生成验证码
  109. String code = UUID.randomUUID().toString().substring(0, 6);
  110. // 存储验证码到Redis,设置5分钟过期
  111. int timeout = 5;
  112. final String key = "verifyCode:" + officialEmail;
  113. // 存储验证码到Redis并设置过期时间
  114. redisTemplate.opsForValue().set(key, code, timeout, TimeUnit.MINUTES);
  115. // 设置请求间隔标记,防止频繁请求
  116. redisTemplate.opsForValue().set(intervalKey, "requested", SEND_INTERVAL, TimeUnit.SECONDS);
  117. emailService.sendVerifyCodeEmail(officialEmail, code, timeout);
  118. }
  119. /**
  120. * 管理员创建用户,给出name、officialEmail、officialNumber、role,随机生成密码,将密码通过邮件服务发送到用户邮箱
  121. *
  122. * @param adminRegisterDTO
  123. * @return 1表示成功,0表示失败
  124. */
  125. @Override
  126. public void adminCreateUser(AdminRegisterDTO adminRegisterDTO) {
  127. Role role = adminRegisterDTO.getRole();
  128. if (role != Role.STUDENT && role != Role.TEACHER) { // 只能注册学生或老师账号
  129. throw new MyException(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase() + ":" + "被拒绝");
  130. }
  131. User targetUser = ModelMapperUtil.map(adminRegisterDTO, User.class);
  132. // 随机生成密码
  133. String randomPassword = UUID.randomUUID().toString();
  134. targetUser.setPassword(passwordEncoder.encode(randomPassword));
  135. targetUser.setCreateTime(new Date());
  136. // 同一个邮箱、学号只能注册一次
  137. if (isOfficialNumberNotExists(targetUser.getOfficialNumber())
  138. && isEmailNotExists(targetUser.getOfficialEmail())) {
  139. int code = userMapper.insert(targetUser);
  140. if (code == 0) {
  141. log.error("createUser -- 数据库插入错误:");
  142. throw new MyException(501, "数据库插入错误");
  143. }
  144. log.info("User创建完毕,User对象如下:" + targetUser);
  145. }
  146. // 发送初始密码
  147. emailService.sendInitialPasswordEmail(targetUser.getOfficialEmail(), targetUser.getName(), randomPassword);
  148. }
  149. /**
  150. * @param userDTO
  151. * @return
  152. */
  153. @Override
  154. public UserVO updateUser(Long id, UserUpdateDTO userDTO) {
  155. UserDTO opeUser = ModelMapperUtil.map(userDTO, UserDTO.class);
  156. return updateUser(id, opeUser);
  157. }
  158. @Override
  159. public UserVO updateUser(Long id, UserDTO userDTO) {
  160. User opeUser = ModelMapperUtil.map(userDTO, User.class);
  161. opeUser.setId(id);
  162. try {
  163. int status = userMapper.updateUser(opeUser);
  164. if (status > 0) {
  165. return ModelMapperUtil.map(userMapper.selectById(opeUser.getId()), UserVO.class);
  166. }
  167. } catch (Exception e) {
  168. log.error("数据库更新错误,错误如下:" + e.getMessage());
  169. throw new MyException(HttpStatus.INTERNAL_SERVER_ERROR.value(), HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase() + ":" + "数据库更新失败");
  170. }
  171. return null;
  172. }
  173. @Override
  174. public void updatePassword(Long id, String newPassword, String oldPassword) throws MyException {
  175. User user = userMapper.selectById(id);
  176. if (user == null) {
  177. log.error("用户不存在");
  178. throw new MyException(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase() + ":" + "找不到用户");
  179. }
  180. if (!passwordEncoder.matches(oldPassword, user.getPassword())) {
  181. log.error("用户就密码不正确");
  182. throw new MyException(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase() + ":" + "旧密码不正确");
  183. }
  184. String encodedNewPassword = passwordEncoder.encode(newPassword);
  185. int code = userMapper.updatePassword(id, encodedNewPassword);
  186. if (code == 1) {
  187. log.info("用户更新成功");
  188. }
  189. }
  190. /**
  191. * 管理员重置密码,并将重置密码发送到指定邮箱
  192. *
  193. * @param id
  194. */
  195. @Override
  196. public void resetPassword(Long id) {
  197. User targetUser = userMapper.selectById(id);
  198. if (targetUser == null)
  199. throw new MyException(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase() + ":" + "用户不存在");
  200. String targetName = targetUser.getName();
  201. String targetEmail = targetUser.getOfficialEmail();
  202. String randomPassword = UUID.randomUUID().toString();
  203. int code = userMapper.updatePassword(id, passwordEncoder.encode(randomPassword));
  204. if (code == 1) {
  205. log.info("用户更新成功");
  206. }
  207. emailService.sendResetPasswordEmail(targetEmail, targetName, randomPassword);
  208. }
  209. @Override
  210. public void deleteById(Long id) {
  211. if (isUserExists(id)) {
  212. int res = userMapper.deleteById(id);
  213. if (res == 0)
  214. throw new MyException(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase() + ":" + "删除失败");
  215. }
  216. }
  217. /**
  218. * 解析文件,批量创建用户。其中,文件必须包含姓名、邮箱、学号。
  219. *
  220. * @param file
  221. * @return
  222. */
  223. @Override
  224. public void batchCreateUsersFromExcel(MultipartFile file) throws Exception {
  225. Workbook workbook = new XSSFWorkbook(file.getInputStream());
  226. Sheet sheet = workbook.getSheetAt(0); // 默认只允许一张表
  227. Row headerRow = sheet.getRow(0);
  228. int nameCol = findColumnIndex(headerRow, "姓名");
  229. int numberCol = findColumnIndex(headerRow, "学号");
  230. int emailCol = findColumnIndex(headerRow, "邮箱");
  231. if (nameCol < 0 || numberCol < 0 || emailCol < 0)
  232. throw new MyException(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase() + ":" + "缺失必要字段信息");
  233. List<User> users = new ArrayList<>();
  234. for (Row row : sheet) {
  235. if (row.getRowNum() == 0) continue; // 跳过表头
  236. User user = new User();
  237. user.setName(stringifyCellValue(row.getCell(nameCol)));
  238. user.setOfficialNumber(stringifyCellValue(row.getCell(numberCol)));
  239. user.setOfficialEmail(stringifyCellValue(row.getCell(emailCol)));
  240. user.setRole(Role.STUDENT); // 批量创建只创建学生账号
  241. user.setCreateTime(new Date());
  242. users.add(user);
  243. }
  244. processUsers(users);
  245. }
  246. /**
  247. * 批量创建用户,从csv文件中读取数据
  248. *
  249. * @param file csv文件
  250. */
  251. @Override
  252. public void batchCreateUsersFromCsv(MultipartFile file) throws Exception {
  253. CSVReader csvReader = new CSVReader(new InputStreamReader(file.getInputStream()));
  254. String[] header = csvReader.readNext();
  255. int nameCol = findColumnIndex(header, "姓名");
  256. int numberCol = findColumnIndex(header, "学号");
  257. int emailCol = findColumnIndex(header, "邮箱");
  258. if (nameCol < 0 || numberCol < 0 || emailCol < 0)
  259. throw new MyException(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase() + ":" + "缺失必要字段信息");
  260. List<User> users = new ArrayList<>();
  261. String[] nextRecord;
  262. while ((nextRecord = csvReader.readNext()) != null) {
  263. String officialNumber = String.valueOf(nextRecord[numberCol]);
  264. String email = String.valueOf(nextRecord[emailCol]);
  265. if (isOfficialNumberNotExists(officialNumber)
  266. && isEmailNotExists(email)) {
  267. User user = new User();
  268. user.setName(String.valueOf(nextRecord[nameCol]));
  269. user.setOfficialNumber(officialNumber);
  270. user.setOfficialEmail(String.valueOf(email));
  271. user.setRole(Role.STUDENT); // 批量创建只创建学生账号
  272. user.setCreateTime(new Date());
  273. users.add(user);
  274. }
  275. }
  276. // 统一生成随机密码,批量创建用户,创建成功后,将初始密码发送至邮箱
  277. processUsers(users);
  278. }
  279. /**
  280. * 查看学号是否不存在
  281. *
  282. * @param officialNumber 学号
  283. * @return true表示不存在,false表示存在
  284. */
  285. private boolean isOfficialNumberNotExists(String officialNumber) throws MyException {
  286. QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  287. queryWrapper.eq("official_number", officialNumber);
  288. User existingUser = userMapper.selectOne(queryWrapper);
  289. if (existingUser != null) {
  290. log.error("用户已存在,学号{}重复", officialNumber);
  291. throw new MyException(400, "用户已存在,请检查学号信息");
  292. }
  293. return true;
  294. }
  295. /**
  296. * 查看邮箱是否不存在
  297. *
  298. * @param email 邮箱
  299. * @return true表示不存在,false表示存在
  300. */
  301. private boolean isEmailNotExists(String email) throws MyException {
  302. QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  303. queryWrapper.eq("official_email", email);
  304. User existingUser = userMapper.selectOne(queryWrapper);
  305. if (existingUser != null) {
  306. log.error("用户已存在,邮箱{}重复", email);
  307. throw new MyException(400, "用户已存在,请检查邮箱信息");
  308. }
  309. return true;
  310. }
  311. /**
  312. * 检查用户ID是否存在,如果存在则返回true;不存在则报错
  313. *
  314. * @param id
  315. * @return
  316. * @throws MyException
  317. */
  318. private boolean isUserExists(Long id) throws MyException {
  319. QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  320. queryWrapper.eq("id", id);
  321. User existingUser = userMapper.selectOne(queryWrapper);
  322. if (existingUser == null) {
  323. log.error("用户不存在,ID:{}", id);
  324. throw new MyException(400, "用户不存在,请检查ID信息");
  325. }
  326. return true;
  327. }
  328. /**
  329. * 在指定Excel Cell中找到具体列名的索引值
  330. * @param headerRow
  331. * @param columnName
  332. * @return
  333. */
  334. private int findColumnIndex(Row headerRow, String columnName) {
  335. for (Cell cell : headerRow) {
  336. if (cell.getStringCellValue().trim().equalsIgnoreCase(columnName)) {
  337. return cell.getColumnIndex();
  338. }
  339. }
  340. return -1;
  341. }
  342. private String stringifyCellValue(Cell cell) {
  343. String cellValue;
  344. if (cell.getCellType() == CellType.STRING) {
  345. cellValue = cell.getStringCellValue();
  346. } else if (cell.getCellType() == CellType.NUMERIC) {
  347. // 对于数值类型,可以将其转换为字符串
  348. // 注意:这里直接使用了Double.toString,你可能需要根据实际情况调整格式化方式
  349. cellValue = Double.toString(cell.getNumericCellValue());
  350. } else if (cell.getCellType() == CellType.BOOLEAN) {
  351. // 对于布尔类型,也可以转换为字符串
  352. cellValue = Boolean.toString(cell.getBooleanCellValue());
  353. } else {
  354. // 其他类型,根据需要处理或转换为字符串
  355. // 例如,对于公式类型,可以计算公式的值
  356. throw new MyException(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase() + ":" + "上传失败:请检查excel单元格的类型");
  357. }
  358. return cellValue;
  359. }
  360. /**
  361. * 在csv文件的header中找到具体列的索引值
  362. *
  363. * @param header
  364. * @param columnName
  365. * @return
  366. */
  367. private int findColumnIndex(String[] header, String columnName) {
  368. for (int i = 0; i < header.length; i++) {
  369. if (header[i].trim().equalsIgnoreCase(columnName)) {
  370. return i;
  371. }
  372. }
  373. return -1;
  374. }
  375. /**
  376. * 统一生成随机密码,批量创建用户,创建成功后,将初始密码发送至邮箱
  377. *
  378. * @param users
  379. */
  380. private void processUsers(List<User> users) {
  381. List<String> passwordStage = new ArrayList<>();
  382. for (User user : users) {
  383. // 随机生成密码,并使用BCryptPasswordEncoder加密密码
  384. String randomPassword = UUID.randomUUID().toString();
  385. passwordStage.add(randomPassword);
  386. String encodedPassword = passwordEncoder.encode(randomPassword);
  387. user.setPassword(encodedPassword);
  388. }
  389. int res = userMapper.batchInsert(users);
  390. if (res == 0)
  391. throw new MyException(HttpStatus.INTERNAL_SERVER_ERROR.value(), HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase() + ":" + "批量创建失败");
  392. for (int i = 0; i < users.size(); i++) {
  393. emailService.sendInitialPasswordEmail(users.get(i).getOfficialEmail(), users.get(i).getName(), passwordStage.get(i));
  394. }
  395. }
  396. }