package com.xy.xyaicpzs.service.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.xy.xyaicpzs.common.ErrorCode; import com.xy.xyaicpzs.constant.UserConstant; import com.xy.xyaicpzs.domain.dto.user.UserPhoneLoginRequest; import com.xy.xyaicpzs.domain.dto.user.UserPhoneRegisterRequest; import com.xy.xyaicpzs.domain.entity.User; import com.xy.xyaicpzs.domain.vo.*; import com.xy.xyaicpzs.exception.BusinessException; import com.xy.xyaicpzs.mapper.UserMapper; import com.xy.xyaicpzs.service.SmsService; import com.xy.xyaicpzs.service.UserService; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.DigestUtils; import jakarta.servlet.http.HttpServletRequest; import java.util.*; import java.util.stream.Collectors; /** * @author XY003 * @description 针对表【user(用户表)】的数据库操作Service实现 * @createDate 2025-06-14 09:48:10 */ @Service @Slf4j public class UserServiceImpl extends ServiceImpl implements UserService{ /** * 盐值,混淆密码 */ private static final String SALT = "xy"; @Autowired private SmsService smsService; @Override public long userRegister(String userAccount, String userName, String userPassword, String checkPassword) { // 1. 校验 if (StringUtils.isAnyBlank(userAccount, userName, userPassword, checkPassword)) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数为空"); } if (userAccount.length() < 4) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户账号过短"); } if (userName.length() > 40) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户名过长"); } if (userPassword.length() < 8 || checkPassword.length() < 8) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户密码过短"); } // 密码和校验密码相同 if (!userPassword.equals(checkPassword)) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "两次输入的密码不一致"); } synchronized (userAccount.intern()) { // 账户不能重复 QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("userAccount", userAccount); long count = this.baseMapper.selectCount(queryWrapper); if (count > 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号重复"); } // 2. 加密 String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes()); // 3. 插入数据 User user = new User(); user.setUserAccount(userAccount); user.setUserName(userName); user.setUserPassword(encryptPassword); user.setCreateTime(new Date()); user.setUpdateTime(new Date()); // 设置为VIP用户,有效期10天 user.setIsVip(1); Date vipExpireDate = new Date(System.currentTimeMillis() + 10L * 24 * 60 * 60 * 1000); user.setVipExpire(vipExpireDate); boolean saveResult = this.save(user); if (!saveResult) { throw new BusinessException(ErrorCode.SYSTEM_ERROR, "注册失败,数据库错误"); } return user.getId(); } } @Override public User userLogin(String userAccount, String userPassword, HttpServletRequest request) { // 1. 校验 if (StringUtils.isAnyBlank(userAccount, userPassword)) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数为空"); } if (userAccount.length() < 4) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号错误"); } if (userPassword.length() < 8) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码错误"); } // 2. 加密 String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes()); // 查询用户是否存在 QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("userAccount", userAccount); queryWrapper.eq("userPassword", encryptPassword); User user = this.baseMapper.selectOne(queryWrapper); // 用户不存在 if (user == null) { log.info("user login failed, userAccount cannot match userPassword"); throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户不存在或密码错误"); } // 3. 记录用户的登录态 request.getSession().setAttribute(UserConstant.USER_LOGIN_STATE, user); return getSafetyUser(user); } /** * 获取当前登录用户 * * @param request * @return */ @Override public User getLoginUser(HttpServletRequest request) { // 先判断是否已登录 Object userObj = request.getSession().getAttribute(UserConstant.USER_LOGIN_STATE); User currentUser = (User) userObj; if (currentUser == null || currentUser.getId() == null) { throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR); } // 从数据库查询(追求性能的话可以注释,直接走缓存) long userId = currentUser.getId(); currentUser = this.getById(userId); if (currentUser == null) { throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR); } return currentUser; } /** * 用户注销 * * @param request */ @Override public boolean userLogout(HttpServletRequest request) { if (request.getSession().getAttribute(UserConstant.USER_LOGIN_STATE) == null) { throw new BusinessException(ErrorCode.OPERATION_ERROR, "未登录"); } // 移除登录态 request.getSession().removeAttribute(UserConstant.USER_LOGIN_STATE); return true; } @Override public User getSafetyUser(User originUser) { if (originUser == null) { return null; } User safetyUser = new User(); safetyUser.setId(originUser.getId()); safetyUser.setUserName(originUser.getUserName()); safetyUser.setUserAccount(originUser.getUserAccount()); safetyUser.setUserAvatar(originUser.getUserAvatar()); safetyUser.setUserRole(originUser.getUserRole()); safetyUser.setCreateTime(originUser.getCreateTime()); safetyUser.setUpdateTime(originUser.getUpdateTime()); return safetyUser; } @Override public boolean isAdmin(HttpServletRequest request) { // 仅管理员可查询 Object userObj = request.getSession().getAttribute(UserConstant.USER_LOGIN_STATE); User user = (User) userObj; return isAdmin(user); } @Override public boolean isAdmin(User user) { return user != null && (UserConstant.ADMIN_ROLE.equals(user.getUserRole()) || UserConstant.SUPER_ADMIN_ROLE.equals(user.getUserRole())); } @Override public long userPhoneRegister(UserPhoneRegisterRequest userPhoneRegisterRequest) { if (userPhoneRegisterRequest == null) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数为空"); } String userAccount = userPhoneRegisterRequest.getUserAccount(); String userPassword = userPhoneRegisterRequest.getUserPassword(); String checkPassword = userPhoneRegisterRequest.getCheckPassword(); String phone = userPhoneRegisterRequest.getPhone(); String code = userPhoneRegisterRequest.getCode(); String userName = userPhoneRegisterRequest.getUserName(); // 1. 校验 if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword, phone, code)) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数为空"); } // 用户名可以为空,如果为空则默认使用账号 if (StringUtils.isBlank(userName)) { userName = userAccount; } if (userAccount.length() < 4) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户账号过短"); } if (userPassword.length() < 8 || checkPassword.length() < 8) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户密码过短"); } // 密码和校验密码相同 if (!userPassword.equals(checkPassword)) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "两次输入的密码不一致"); } // 验证手机号格式 String phoneRegex = "^1[3-9]\\d{9}$"; if (!phone.matches(phoneRegex)) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "手机号格式错误"); } // 验证短信验证码 boolean isCodeValid = smsService.verifyCode(phone, code); if (!isCodeValid) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "验证码错误或已过期"); } synchronized (userAccount.intern()) { // 账户不能重复 QueryWrapper accountQueryWrapper = new QueryWrapper<>(); accountQueryWrapper.eq("userAccount", userAccount); long accountCount = this.baseMapper.selectCount(accountQueryWrapper); if (accountCount > 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号已存在"); } // 手机号不能重复 QueryWrapper phoneQueryWrapper = new QueryWrapper<>(); phoneQueryWrapper.eq("phone", phone); long phoneCount = this.baseMapper.selectCount(phoneQueryWrapper); if (phoneCount > 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "手机号已注册"); } // 2. 加密 String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes()); // 3. 插入数据 User user = new User(); user.setUserAccount(userAccount); user.setUserPassword(encryptPassword); user.setPhone(phone); // 设置用户名 user.setUserName(userName); user.setCreateTime(new Date()); user.setUpdateTime(new Date()); // 设置为VIP用户,有效期10天 user.setIsVip(0); Date vipExpireDate = new Date(System.currentTimeMillis() + 10L * 24 * 60 * 60 * 1000); user.setVipExpire(vipExpireDate); boolean saveResult = this.save(user); if (!saveResult) { throw new BusinessException(ErrorCode.SYSTEM_ERROR, "注册失败,数据库错误"); } return user.getId(); } } @Override public User userPhoneLogin(UserPhoneLoginRequest userPhoneLoginRequest, HttpServletRequest request) { if (userPhoneLoginRequest == null) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数为空"); } String phone = userPhoneLoginRequest.getPhone(); String code = userPhoneLoginRequest.getCode(); // 1. 校验参数 if (StringUtils.isAnyBlank(phone, code)) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数为空"); } // 验证手机号格式 String phoneRegex = "^1[3-9]\\d{9}$"; if (!phone.matches(phoneRegex)) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "手机号格式错误"); } // 验证短信验证码 boolean isCodeValid = smsService.verifyCode(phone, code); if (!isCodeValid) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "验证码错误或已过期"); } // 查询用户是否存在 QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("phone", phone); User user = this.baseMapper.selectOne(queryWrapper); // 用户不存在 if (user == null) { log.info("user login failed, phone number not registered"); throw new BusinessException(ErrorCode.PARAMS_ERROR, "手机号未注册"); } // 3. 记录用户的登录态 request.getSession().setAttribute(UserConstant.USER_LOGIN_STATE, user); return getSafetyUser(user); } @Override public String encryptPassword(String password) { if (StringUtils.isBlank(password)) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码不能为空"); } return DigestUtils.md5DigestAsHex((SALT + password).getBytes()); } // region 统计相关方法实现 @Override public UserStatisticsVO getNewUsersStatistics(String startDate, String endDate) { try { // 构建查询条件 QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.between("createTime", startDate + " 00:00:00", endDate + " 23:59:59"); // 获取新增用户总数 long totalNewUsers = this.count(queryWrapper); // 获取新增用户列表(最近20个) queryWrapper.orderByDesc("createTime"); queryWrapper.last("LIMIT 20"); List recentUsers = this.list(queryWrapper); List recentUserVOs = recentUsers.stream().map(user -> { UserVO userVO = new UserVO(); BeanUtils.copyProperties(user, userVO); return userVO; }).collect(Collectors.toList()); return UserStatisticsVO.builder() .totalNewUsers(totalNewUsers) .startDate(startDate) .endDate(endDate) .recentUsers(recentUserVOs) .build(); } catch (Exception e) { log.error("获取新增用户统计失败:{}", e.getMessage()); throw new BusinessException(ErrorCode.SYSTEM_ERROR, "获取统计数据失败"); } } @Override public VipStatisticsVO getNewVipsStatistics(String startDate, String endDate) { try { // 构建查询条件 - 新增会员(isVip != 0 且在时间范围内) QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.ne("isVip", 0); queryWrapper.between("createTime", startDate + " 00:00:00", endDate + " 23:59:59"); // 获取新增会员总数 long totalNewVips = this.count(queryWrapper); // 获取新增会员列表(最近20个) queryWrapper.orderByDesc("createTime"); queryWrapper.last("LIMIT 20"); List recentVips = this.list(queryWrapper); List recentVipVOs = recentVips.stream().map(user -> { UserVO userVO = new UserVO(); BeanUtils.copyProperties(user, userVO); return userVO; }).collect(Collectors.toList()); // 计算会员转化率(该时间段内新增会员 / 新增用户总数) QueryWrapper allUsersWrapper = new QueryWrapper<>(); allUsersWrapper.between("createTime", startDate + " 00:00:00", endDate + " 23:59:59"); long totalNewUsers = this.count(allUsersWrapper); double conversionRate = totalNewUsers > 0 ? (double) totalNewVips / totalNewUsers * 100 : 0.0; return VipStatisticsVO.builder() .totalNewVips(totalNewVips) .totalNewUsers(totalNewUsers) .conversionRate(Math.round(conversionRate * 100.0) / 100.0) .startDate(startDate) .endDate(endDate) .recentVips(recentVipVOs) .build(); } catch (Exception e) { log.error("获取新增会员统计失败:{}", e.getMessage()); throw new BusinessException(ErrorCode.SYSTEM_ERROR, "获取统计数据失败"); } } @Override public RegistrationTrendVO getRegistrationTrend(String startDate, String endDate, String granularity) { try { // 这里简化处理,实际应该使用SQL的GROUP BY和DATE_FORMAT函数 // 由于MyBatis-Plus的限制,我们先获取所有数据然后在Java中分组 QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.between("createTime", startDate + " 00:00:00", endDate + " 23:59:59"); queryWrapper.orderByAsc("createTime"); List users = this.list(queryWrapper); Map trendData = new HashMap<>(); Map vipTrendData = new HashMap<>(); // 在Java中按指定粒度分组统计 for (User user : users) { String key = formatDateByGranularity(user.getCreateTime(), granularity); trendData.put(key, trendData.getOrDefault(key, 0L) + 1); // 同时统计会员趋势 if (user.getIsVip() != null && user.getIsVip() != 0) { vipTrendData.put(key, vipTrendData.getOrDefault(key, 0L) + 1); } } return RegistrationTrendVO.builder() .startDate(startDate) .endDate(endDate) .granularity(granularity) .userTrend(trendData) .vipTrend(vipTrendData) .totalUsers(users.size()) .totalVips(users.stream().mapToLong(u -> u.getIsVip() != null && u.getIsVip() != 0 ? 1 : 0).sum()) .build(); } catch (Exception e) { log.error("获取用户注册趋势失败:{}", e.getMessage()); throw new BusinessException(ErrorCode.SYSTEM_ERROR, "获取趋势数据失败"); } } @Override public Page getExpiringVips(Integer days, Long current, Long pageSize) { try { // 计算目标日期范围 Date now = new Date(); Date futureDate = new Date(now.getTime() + days * 24 * 60 * 60 * 1000L); // 构建查询条件 QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.ne("isVip", 0); // 是会员 queryWrapper.isNotNull("vipExpire"); // 有到期时间 queryWrapper.between("vipExpire", now, futureDate); // 在即将到期的时间范围内 queryWrapper.orderByAsc("vipExpire"); // 按到期时间升序 Page userPage = this.page(new Page<>(current, pageSize), queryWrapper); // 转换为UserVO Page userVOPage = new Page<>(userPage.getCurrent(), userPage.getSize(), userPage.getTotal()); List userVOList = userPage.getRecords().stream().map(user -> { UserVO userVO = new UserVO(); BeanUtils.copyProperties(user, userVO); return userVO; }).collect(Collectors.toList()); userVOPage.setRecords(userVOList); return userVOPage; } catch (Exception e) { log.error("获取即将到期会员失败:{}", e.getMessage()); throw new BusinessException(ErrorCode.SYSTEM_ERROR, "获取数据失败"); } } @Override public VipDistributionVO getVipDistribution() { try { Date now = new Date(); // 总用户数 long totalUsers = this.count(); // 普通用户数(isVip = 0 或 null) QueryWrapper normalWrapper = new QueryWrapper<>(); normalWrapper.and(wrapper -> wrapper.eq("isVip", 0).or().isNull("isVip")); long normalUsers = this.count(normalWrapper); // 有效会员数(isVip != 0 且 vipExpire > now) QueryWrapper activeVipWrapper = new QueryWrapper<>(); activeVipWrapper.ne("isVip", 0) .and(wrapper -> wrapper.isNull("vipExpire").or().gt("vipExpire", now)); long activeVips = this.count(activeVipWrapper); // 过期会员数(isVip != 0 且 vipExpire <= now) QueryWrapper expiredVipWrapper = new QueryWrapper<>(); expiredVipWrapper.ne("isVip", 0) .isNotNull("vipExpire") .le("vipExpire", now); long expiredVips = this.count(expiredVipWrapper); // 即将到期会员数(7天内到期) Date sevenDaysLater = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000L); QueryWrapper soonExpireWrapper = new QueryWrapper<>(); soonExpireWrapper.ne("isVip", 0) .between("vipExpire", now, sevenDaysLater); long soonExpireVips = this.count(soonExpireWrapper); // 计算百分比 double normalPercentage = totalUsers > 0 ? (double) normalUsers / totalUsers * 100 : 0; double activeVipPercentage = totalUsers > 0 ? (double) activeVips / totalUsers * 100 : 0; double expiredVipPercentage = totalUsers > 0 ? (double) expiredVips / totalUsers * 100 : 0; return VipDistributionVO.builder() .totalUsers(totalUsers) .normalUsers(normalUsers) .normalPercentage(Math.round(normalPercentage * 100.0) / 100.0) .activeVips(activeVips) .activeVipPercentage(Math.round(activeVipPercentage * 100.0) / 100.0) .expiredVips(expiredVips) .expiredVipPercentage(Math.round(expiredVipPercentage * 100.0) / 100.0) .soonExpireVips(soonExpireVips) .statisticsTime(now) .build(); } catch (Exception e) { log.error("获取会员分布统计失败:{}", e.getMessage()); throw new BusinessException(ErrorCode.SYSTEM_ERROR, "获取统计数据失败"); } } /** * 根据时间粒度格式化日期 */ private String formatDateByGranularity(Date date, String granularity) { if (date == null) return ""; java.time.LocalDateTime localDateTime = date.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDateTime(); java.time.format.DateTimeFormatter formatter; switch (granularity) { case "week": // 获取年份和周数 int year = localDateTime.getYear(); int week = localDateTime.get(java.time.temporal.WeekFields.of(java.util.Locale.getDefault()).weekOfYear()); return year + "-W" + String.format("%02d", week); case "month": formatter = java.time.format.DateTimeFormatter.ofPattern("yyyy-MM"); break; default: formatter = java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd"); break; } return localDateTime.format(formatter); } // endregion }