2025-08-01 19:09:57 +08:00
|
|
|
|
package com.xy.xyaicpzs.service.impl;
|
|
|
|
|
|
|
|
|
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
2025-11-04 17:18:21 +08:00
|
|
|
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
2025-08-01 19:09:57 +08:00
|
|
|
|
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;
|
2025-11-04 17:18:21 +08:00
|
|
|
|
import com.xy.xyaicpzs.domain.vo.*;
|
2025-08-01 19:09:57 +08:00
|
|
|
|
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;
|
2025-11-04 17:18:21 +08:00
|
|
|
|
import org.springframework.beans.BeanUtils;
|
2025-08-01 19:09:57 +08:00
|
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
|
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
import org.springframework.util.DigestUtils;
|
|
|
|
|
|
|
|
|
|
|
|
import jakarta.servlet.http.HttpServletRequest;
|
2025-11-04 17:18:21 +08:00
|
|
|
|
import java.util.*;
|
|
|
|
|
|
import java.util.stream.Collectors;
|
2025-08-01 19:09:57 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @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());
|
|
|
|
|
|
}
|
2025-11-04 17:18:21 +08:00
|
|
|
|
|
|
|
|
|
|
// 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
|
2025-08-01 19:09:57 +08:00
|
|
|
|
}
|