AuthService.java 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. package com.xuekairui.user.service;
  2. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  3. import com.xuekairui.common.BusinessException;
  4. import com.xuekairui.common.ErrorCode;
  5. import com.xuekairui.common.ValidateUtil;
  6. import com.xuekairui.user.dto.*;
  7. import com.xuekairui.user.entity.User;
  8. import com.xuekairui.user.enums.MembershipLevel;
  9. import com.xuekairui.user.enums.OperationType;
  10. import com.xuekairui.user.event.UserRegisteredEvent;
  11. import com.xuekairui.user.mapper.UserMapper;
  12. import com.xuekairui.user.util.JwtUtil;
  13. import com.xuekairui.user.util.PasswordUtil;
  14. import io.jsonwebtoken.Claims;
  15. import lombok.RequiredArgsConstructor;
  16. import lombok.extern.slf4j.Slf4j;
  17. import org.springframework.context.ApplicationEventPublisher;
  18. import org.springframework.stereotype.Service;
  19. import org.springframework.transaction.annotation.Transactional;
  20. import java.time.LocalDateTime;
  21. import java.time.temporal.ChronoUnit;
  22. /**
  23. * 认证服务
  24. */
  25. @Slf4j
  26. @Service
  27. @RequiredArgsConstructor
  28. public class AuthService {
  29. private final UserMapper userMapper;
  30. private final VerificationCodeService verificationCodeService;
  31. private final WechatService wechatService;
  32. private final JwtUtil jwtUtil;
  33. private final ApplicationEventPublisher eventPublisher;
  34. private final PasswordUtil passwordUtil;
  35. private final OperationAuditLogService auditLogService;
  36. private final MembershipService membershipService;
  37. private final UserLevelService userLevelService;
  38. /**
  39. * 发送验证码
  40. */
  41. public void sendVerificationCode(SendCodeRequest request, String ip) {
  42. verificationCodeService.sendCode(request.getPhone(), request.getScene(), ip);
  43. }
  44. /**
  45. * 验证码登录/注册
  46. * 如果手机号已注册则登录,否则自动注册
  47. */
  48. @Transactional
  49. public LoginResponse smsLogin(SmsLoginRequest request, String ip) {
  50. // 1. 验证验证码(兼容 scene=login 和 scene=register,统一用 login key)
  51. verificationCodeService.verifyCode(request.getPhone(), request.getCode());
  52. // 2. 查找或创建用户
  53. User user = userMapper.selectOne(
  54. new LambdaQueryWrapper<User>().eq(User::getPhone, request.getPhone()));
  55. boolean isNewUser = false;
  56. if (user == null) {
  57. isNewUser = true;
  58. // 自动注册新用户
  59. user = User.builder()
  60. .phone(request.getPhone())
  61. .nickname("用户" + request.getPhone().substring(7))
  62. .membershipLevel(MembershipLevel.PLUS)
  63. .loginSource("SMS")
  64. .status(1)
  65. .build();
  66. userMapper.insert(user);
  67. // 发布用户注册事件(邀请模块监听处理邀请码,赠送会员奖励)
  68. eventPublisher.publishEvent(new UserRegisteredEvent(this, user.getId(), request.getInviteCode()));
  69. // 普通会员(PLUS)免费开放,注册即拥有,无需额外发放
  70. }
  71. // 3. 更新登录信息
  72. user.setLastLoginTime(LocalDateTime.now());
  73. user.setLastLoginIp(ip);
  74. userMapper.updateById(user);
  75. // 4. 生成Token
  76. LoginResponse response = buildLoginResponse(user);
  77. response.setUserId(user.getId());
  78. response.setNewUser(isNewUser);
  79. // 记录审计日志
  80. String roleCode = user.getRole() != null ? user.getRole().getCode() : "USER";
  81. if (isNewUser) {
  82. auditLogService.logSuccess(user.getId(), user.getNickname(), roleCode,
  83. OperationType.USER_REGISTER, "USER:" + user.getId(), user.getId());
  84. }
  85. auditLogService.logSuccess(user.getId(), user.getNickname(), roleCode,
  86. OperationType.USER_LOGIN, "USER:" + user.getId(), user.getId());
  87. return response;
  88. }
  89. /**
  90. * 微信扫码登录
  91. */
  92. @Transactional
  93. public LoginResponse wechatLogin(WechatLoginRequest request, String ip) {
  94. // 1. 获取微信用户信息
  95. WechatService.WechatUserInfo wechatInfo = wechatService.getUserInfo(request.getCode());
  96. // 2. 通过UnionID查找用户
  97. User user = null;
  98. if (wechatInfo.getUnionId() != null) {
  99. user = userMapper.selectOne(
  100. new LambdaQueryWrapper<User>().eq(User::getWechatUnionId, wechatInfo.getUnionId()));
  101. }
  102. // 3. 如果用户存在且已绑定手机号,直接登录
  103. if (user != null && user.getPhone() != null) {
  104. user.setLastLoginTime(LocalDateTime.now());
  105. user.setLastLoginIp(ip);
  106. userMapper.updateById(user);
  107. return buildLoginResponse(user);
  108. }
  109. // 4. 如果用户存在但未绑定手机号,或新用户,返回需要绑定手机号
  110. if (user == null) {
  111. // 创建新用户(未绑定手机号)
  112. user = User.builder()
  113. .wechatOpenId(wechatInfo.getOpenId())
  114. .wechatUnionId(wechatInfo.getUnionId())
  115. .nickname(wechatInfo.getNickname())
  116. .avatar(wechatInfo.getHeadImgUrl())
  117. .membershipLevel(MembershipLevel.PLUS)
  118. .loginSource("WECHAT")
  119. .status(1)
  120. .build();
  121. userMapper.insert(user);
  122. } else {
  123. // 更新微信信息
  124. user.setWechatOpenId(wechatInfo.getOpenId());
  125. user.setWechatUnionId(wechatInfo.getUnionId());
  126. if (user.getNickname() == null) {
  127. user.setNickname(wechatInfo.getNickname());
  128. }
  129. if (user.getAvatar() == null) {
  130. user.setAvatar(wechatInfo.getHeadImgUrl());
  131. }
  132. user.setLastLoginTime(LocalDateTime.now());
  133. user.setLastLoginIp(ip);
  134. userMapper.updateById(user);
  135. }
  136. // 5. 生成临时Token,要求绑定手机号
  137. String tempToken = jwtUtil.generateTempToken(wechatInfo.getOpenId(), wechatInfo.getUnionId());
  138. return LoginResponse.builder()
  139. .needBindPhone(true)
  140. .tempToken(tempToken)
  141. .userInfo(convertToUserInfoResponse(user))
  142. .build();
  143. }
  144. /**
  145. * 微信绑定手机号
  146. */
  147. @Transactional
  148. public LoginResponse wechatBindPhone(WechatBindRequest request, String ip) {
  149. // 1. 验证临时Token
  150. Claims claims = jwtUtil.parseToken(request.getTempToken());
  151. String tokenType = claims.get("type", String.class);
  152. if (!"temp".equals(tokenType)) {
  153. throw new BusinessException(ErrorCode.TOKEN_INVALID);
  154. }
  155. String openId = claims.get("openId", String.class);
  156. String unionId = claims.get("unionId", String.class);
  157. // 2. 验证验证码
  158. verificationCodeService.verifyCode(request.getPhone(), request.getCode());
  159. // 3. 检查手机号是否已被其他用户绑定
  160. User existUser = userMapper.selectOne(
  161. new LambdaQueryWrapper<User>().eq(User::getPhone, request.getPhone()));
  162. User wechatUser = userMapper.selectOne(
  163. new LambdaQueryWrapper<User>().eq(User::getWechatOpenId, openId));
  164. if (wechatUser == null) {
  165. throw new BusinessException(ErrorCode.WECHAT_LOGIN_FAILED);
  166. }
  167. if (existUser != null && !existUser.getId().equals(wechatUser.getId())) {
  168. // 手机号已被其他账号绑定,合并账号
  169. existUser.setWechatOpenId(openId);
  170. existUser.setWechatUnionId(unionId);
  171. existUser.setLastLoginTime(LocalDateTime.now());
  172. existUser.setLastLoginIp(ip);
  173. userMapper.updateById(existUser);
  174. // 删除旧的微信用户
  175. userMapper.deleteById(wechatUser.getId());
  176. wechatUser = existUser;
  177. } else if (existUser == null) {
  178. // 绑定手机号到微信用户(首次完成注册)
  179. wechatUser.setPhone(request.getPhone());
  180. wechatUser.setLoginSource("WECHAT");
  181. wechatUser.setLastLoginTime(LocalDateTime.now());
  182. wechatUser.setLastLoginIp(ip);
  183. userMapper.updateById(wechatUser);
  184. // 发布用户注册事件,邀请模块可据此建立邀请关系并发放奖励
  185. eventPublisher.publishEvent(new UserRegisteredEvent(this, wechatUser.getId(), request.getInviteCode()));
  186. // 普通会员(PLUS)免费开放,注册即拥有,无需额外发放
  187. }
  188. // 4. 生成正式Token
  189. return buildLoginResponse(wechatUser);
  190. }
  191. /**
  192. * 获取用户信息
  193. */
  194. public UserInfoResponse getUserInfo(Long userId) {
  195. User user = userMapper.selectById(userId);
  196. if (user == null) {
  197. throw new BusinessException(ErrorCode.USER_NOT_FOUND);
  198. }
  199. return convertToUserInfoResponse(user);
  200. }
  201. /**
  202. * 刷新Token
  203. */
  204. public LoginResponse refreshToken(String refreshToken) {
  205. // 验证刷新Token
  206. String tokenType = jwtUtil.getTokenType(refreshToken);
  207. if (!"refresh".equals(tokenType)) {
  208. throw new BusinessException(ErrorCode.REFRESH_TOKEN_INVALID);
  209. }
  210. Long userId = jwtUtil.getUserId(refreshToken);
  211. User user = userMapper.selectById(userId);
  212. if (user == null) {
  213. throw new BusinessException(ErrorCode.USER_NOT_FOUND);
  214. }
  215. return buildLoginResponse(user);
  216. }
  217. /**
  218. * 构建登录响应
  219. */
  220. private LoginResponse buildLoginResponse(User user) {
  221. String roleCode = user.getRole() != null ? user.getRole().getCode() : "USER";
  222. String accessToken = jwtUtil.generateAccessToken(user.getId(), user.getPhone(), roleCode);
  223. String refreshToken = jwtUtil.generateRefreshToken(user.getId());
  224. return LoginResponse.builder()
  225. .userId(user.getId())
  226. .accessToken(accessToken)
  227. .refreshToken(refreshToken)
  228. .expiresIn(jwtUtil.getExpirationInSeconds())
  229. .userInfo(convertToUserInfoResponse(user))
  230. .needBindPhone(false)
  231. .newUser(false)
  232. .role(roleCode)
  233. .build();
  234. }
  235. /**
  236. * 转换用户信息响应
  237. */
  238. private UserInfoResponse convertToUserInfoResponse(User user) {
  239. UserLevelResponse levelInfo = null;
  240. if (user != null && user.getId() != null) {
  241. try {
  242. levelInfo = userLevelService.getUserLevel(user.getId());
  243. } catch (Exception e) {
  244. log.warn("获取用户等级信息失败: userId={}, error={}", user.getId(), e.getMessage());
  245. }
  246. // 填充会员过期信息
  247. if (levelInfo != null) {
  248. try {
  249. MembershipResponse membership = membershipService.getMyMembership(user.getId());
  250. if (membership != null && membership.getEffectiveTo() != null) {
  251. levelInfo.setMembershipExpireAt(membership.getEffectiveTo());
  252. long daysRemaining = ChronoUnit.DAYS.between(LocalDateTime.now(), membership.getEffectiveTo());
  253. levelInfo.setMembershipDaysRemaining(daysRemaining > 0 ? (int) daysRemaining : 0);
  254. } else {
  255. // 永久会员或无会员记录
  256. levelInfo.setMembershipExpireAt(null);
  257. levelInfo.setMembershipDaysRemaining(-1);
  258. }
  259. } catch (Exception e) {
  260. log.warn("获取用户会员信息失败: userId={}, error={}", user.getId(), e.getMessage());
  261. }
  262. }
  263. }
  264. return UserInfoResponse.builder()
  265. .id(user.getId())
  266. .phone(ValidateUtil.maskPhone(user.getPhone()))
  267. .nickname(user.getNickname())
  268. .avatar(user.getAvatar())
  269. .wechatBound(user.getWechatOpenId() != null)
  270. .levelInfo(levelInfo)
  271. .createTime(user.getCreateTime())
  272. .loginSource(user.getLoginSource() != null ? user.getLoginSource() : "UNKNOWN")
  273. .pharmacyName(user.getPharmacyName())
  274. .province(user.getProvince())
  275. .city(user.getCity())
  276. .district(user.getDistrict())
  277. .role(user.getRole() != null ? user.getRole().getCode() : "USER")
  278. .membershipLevel(levelInfo != null ? levelInfo.getLevelCode() : "PLUS")
  279. .membershipLevelName(levelInfo != null ? levelInfo.getLevelName() : "普通会员")
  280. .build();
  281. }
  282. /**
  283. * 密码登录
  284. * 支持手机号或用户名登录:phone 和 username 至少填一个
  285. */
  286. @Transactional
  287. public LoginResponse passwordLogin(PasswordLoginRequest request, String ip) {
  288. // 1. 校验:phone 和 username 至少填一个
  289. boolean hasPhone = request.getPhone() != null && !request.getPhone().isBlank();
  290. boolean hasUsername = request.getUsername() != null && !request.getUsername().isBlank();
  291. if (!hasPhone && !hasUsername) {
  292. throw new BusinessException(ErrorCode.PARAM_ERROR, "手机号和用户名至少填写一个");
  293. }
  294. // 2. 查找用户(优先手机号,其次用户名/昵称)
  295. User user;
  296. if (hasPhone) {
  297. user = userMapper.selectOne(
  298. new LambdaQueryWrapper<User>().eq(User::getPhone, request.getPhone()));
  299. } else {
  300. user = userMapper.selectOne(
  301. new LambdaQueryWrapper<User>().eq(User::getNickname, request.getUsername()));
  302. }
  303. if (user == null) {
  304. throw new BusinessException(ErrorCode.USER_NOT_FOUND);
  305. }
  306. // 2. 检查是否设置了密码
  307. if (user.getPassword() == null) {
  308. throw new BusinessException(ErrorCode.PASSWORD_NOT_SET);
  309. }
  310. // 3. 验证密码
  311. if (!passwordUtil.matches(request.getPassword(), user.getPassword())) {
  312. throw new BusinessException(ErrorCode.PASSWORD_INCORRECT);
  313. }
  314. // 4. 检查用户状态
  315. if (user.getStatus() != null && user.getStatus() == 0) {
  316. throw new BusinessException(ErrorCode.USER_DISABLED);
  317. }
  318. // 5. 更新登录信息
  319. user.setLastLoginTime(LocalDateTime.now());
  320. user.setLastLoginIp(ip);
  321. userMapper.updateById(user);
  322. // 6. 生成Token
  323. LoginResponse response = buildLoginResponse(user);
  324. String roleCode = user.getRole() != null ? user.getRole().getCode() : "USER";
  325. auditLogService.logSuccess(user.getId(), user.getNickname(), roleCode,
  326. OperationType.USER_LOGIN, "USER:" + user.getId(), user.getId());
  327. return response;
  328. }
  329. /**
  330. * 设置/修改密码
  331. */
  332. @Transactional
  333. public void setPassword(Long userId, SetPasswordRequest request) {
  334. User user = userMapper.selectById(userId);
  335. if (user == null) {
  336. throw new BusinessException(ErrorCode.USER_NOT_FOUND);
  337. }
  338. // 如果已有密码,需要验证旧密码
  339. if (user.getPassword() != null && request.getOldPassword() != null) {
  340. if (!passwordUtil.matches(request.getOldPassword(), user.getPassword())) {
  341. throw new BusinessException(ErrorCode.OLD_PASSWORD_INCORRECT);
  342. }
  343. } else if (user.getPassword() != null) {
  344. throw new BusinessException(ErrorCode.OLD_PASSWORD_REQUIRED);
  345. }
  346. // 加密并更新密码
  347. user.setPassword(passwordUtil.encode(request.getNewPassword()));
  348. userMapper.updateById(user);
  349. log.info("用户{}设置/修改密码成功", userId);
  350. String roleCode = user.getRole() != null ? user.getRole().getCode() : "USER";
  351. auditLogService.logSuccess(userId, user.getNickname(), roleCode,
  352. OperationType.USER_UPDATE, "PASSWORD:" + userId, userId);
  353. }
  354. /**
  355. * 修改手机号
  356. */
  357. @Transactional
  358. public void changePhone(Long userId, ChangePhoneRequest request) {
  359. User user = userMapper.selectById(userId);
  360. if (user == null) {
  361. throw new BusinessException(ErrorCode.USER_NOT_FOUND);
  362. }
  363. // 1. 验证验证码(使用新手机号和change场景)
  364. verificationCodeService.verifyCode(request.getNewPhone(), request.getCode());
  365. // 2. 检查新手机号是否已被其他用户占用
  366. User existUser = userMapper.selectOne(
  367. new LambdaQueryWrapper<User>().eq(User::getPhone, request.getNewPhone()));
  368. if (existUser != null && !existUser.getId().equals(userId)) {
  369. throw new BusinessException(ErrorCode.PHONE_ALREADY_EXISTS);
  370. }
  371. // 3. 更新手机号
  372. String oldPhone = user.getPhone();
  373. user.setPhone(request.getNewPhone());
  374. userMapper.updateById(user);
  375. log.info("用户{}修改手机号成功,旧手机号:{},新手机号:{}", userId, oldPhone, request.getNewPhone());
  376. String roleCode = user.getRole() != null ? user.getRole().getCode() : "USER";
  377. auditLogService.logSuccess(userId, user.getNickname(), roleCode,
  378. OperationType.USER_UPDATE, "PHONE:" + userId, userId);
  379. }
  380. }