Files
cpzs-backend/src/main/java/com/xy/xyaicpzs/service/impl/UserServiceImpl.java
2025-11-04 17:18:21 +08:00

569 lines
24 KiB
Java
Raw 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.

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<UserMapper, User>
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<User> 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<User> 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<User> accountQueryWrapper = new QueryWrapper<>();
accountQueryWrapper.eq("userAccount", userAccount);
long accountCount = this.baseMapper.selectCount(accountQueryWrapper);
if (accountCount > 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号已存在");
}
// 手机号不能重复
QueryWrapper<User> 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<User> 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<User> 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<User> recentUsers = this.list(queryWrapper);
List<UserVO> 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<User> 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<User> recentVips = this.list(queryWrapper);
List<UserVO> recentVipVOs = recentVips.stream().map(user -> {
UserVO userVO = new UserVO();
BeanUtils.copyProperties(user, userVO);
return userVO;
}).collect(Collectors.toList());
// 计算会员转化率(该时间段内新增会员 / 新增用户总数)
QueryWrapper<User> 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<User> queryWrapper = new QueryWrapper<>();
queryWrapper.between("createTime", startDate + " 00:00:00", endDate + " 23:59:59");
queryWrapper.orderByAsc("createTime");
List<User> users = this.list(queryWrapper);
Map<String, Long> trendData = new HashMap<>();
Map<String, Long> 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<UserVO> getExpiringVips(Integer days, Long current, Long pageSize) {
try {
// 计算目标日期范围
Date now = new Date();
Date futureDate = new Date(now.getTime() + days * 24 * 60 * 60 * 1000L);
// 构建查询条件
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.ne("isVip", 0); // 是会员
queryWrapper.isNotNull("vipExpire"); // 有到期时间
queryWrapper.between("vipExpire", now, futureDate); // 在即将到期的时间范围内
queryWrapper.orderByAsc("vipExpire"); // 按到期时间升序
Page<User> userPage = this.page(new Page<>(current, pageSize), queryWrapper);
// 转换为UserVO
Page<UserVO> userVOPage = new Page<>(userPage.getCurrent(), userPage.getSize(), userPage.getTotal());
List<UserVO> 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<User> normalWrapper = new QueryWrapper<>();
normalWrapper.and(wrapper -> wrapper.eq("isVip", 0).or().isNull("isVip"));
long normalUsers = this.count(normalWrapper);
// 有效会员数isVip != 0 且 vipExpire > now
QueryWrapper<User> 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<User> 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<User> 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
}