mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-29 16:06:36 +08:00
增加登陆验证码,24小时流量统计
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 验证码验证请求DTO
|
||||
*/
|
||||
@Data
|
||||
public class CaptchaVerifyDto {
|
||||
|
||||
private String id;
|
||||
|
||||
private ImageCaptchaTrack data;
|
||||
|
||||
}
|
||||
@@ -14,4 +14,7 @@ public class LoginDto {
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String password;
|
||||
|
||||
private String captchaId;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import com.admin.entity.StatisticsFlow;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
@@ -24,6 +25,11 @@ public class UserPackageDto {
|
||||
* 用户转发列表
|
||||
*/
|
||||
private List<UserForwardDetailDto> forwards;
|
||||
|
||||
/**
|
||||
* 用户转发列表
|
||||
*/
|
||||
private List<StatisticsFlow> statisticsFlows;
|
||||
|
||||
/**
|
||||
* 用户基本信息
|
||||
|
||||
@@ -48,7 +48,7 @@ public class ResetFlowAsync {
|
||||
* - 不会影响DelayQueueManager的到期任务对status等其他字段的更新
|
||||
* - 避免了并发修改导致的数据覆盖问题
|
||||
*/
|
||||
@Scheduled(cron = "0 0 0 * * ?")
|
||||
@Scheduled(cron = "5 0 0 * * ?")
|
||||
public void reset_flow(){
|
||||
log.info("开始执行流量重置任务");
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.admin.common.task;
|
||||
|
||||
|
||||
import com.admin.entity.StatisticsFlow;
|
||||
import com.admin.entity.User;
|
||||
import com.admin.service.StatisticsFlowService;
|
||||
import com.admin.service.UserService;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.Resource;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@EnableScheduling
|
||||
public class StatisticsFlowAsync {
|
||||
|
||||
@Resource
|
||||
UserService userService;
|
||||
|
||||
@Resource
|
||||
StatisticsFlowService statisticsFlowService;
|
||||
|
||||
@Scheduled(cron = "0 0 * * * ?")
|
||||
public void statistics_flow() {
|
||||
LocalDateTime currentHour = LocalDateTime.now().withMinute(0).withSecond(0).withNano(0);
|
||||
String hourString = currentHour.format(DateTimeFormatter.ofPattern("HH:mm"));
|
||||
|
||||
List<User> list = userService.list();
|
||||
List<StatisticsFlow> statisticsFlowList = new ArrayList<>();
|
||||
|
||||
for (User user : list) {
|
||||
long currentFlow = user.getInFlow() + user.getOutFlow();
|
||||
|
||||
// 从数据库获取上一次记录
|
||||
StatisticsFlow lastFlowRecord = statisticsFlowService.getOne(
|
||||
new LambdaQueryWrapper<StatisticsFlow>()
|
||||
.eq(StatisticsFlow::getUserId, user.getId())
|
||||
.orderByDesc(StatisticsFlow::getId)
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
|
||||
long currentTotalFlow = currentFlow;
|
||||
long incrementFlow = currentTotalFlow;
|
||||
|
||||
if (lastFlowRecord != null) {
|
||||
long lastTotalFlow = lastFlowRecord.getTotalFlow();
|
||||
incrementFlow = currentTotalFlow - lastTotalFlow;
|
||||
|
||||
if (incrementFlow < 0) {
|
||||
incrementFlow = currentTotalFlow;
|
||||
}
|
||||
}
|
||||
|
||||
StatisticsFlow statisticsFlow = new StatisticsFlow();
|
||||
statisticsFlow.setUserId(user.getId());
|
||||
statisticsFlow.setFlow(incrementFlow);
|
||||
statisticsFlow.setTotalFlow(currentTotalFlow);
|
||||
statisticsFlow.setTime(hourString);
|
||||
|
||||
statisticsFlowList.add(statisticsFlow);
|
||||
}
|
||||
|
||||
statisticsFlowService.saveBatch(statisticsFlowList);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.admin.config;
|
||||
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.resource.FontCache;
|
||||
import cloud.tianai.captcha.resource.ResourceStore;
|
||||
import cloud.tianai.captcha.resource.common.model.dto.Resource;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CaptchaResourceConfiguration {
|
||||
|
||||
private final ResourceStore resourceStore;
|
||||
|
||||
|
||||
private static final String[] OPTIONS = {
|
||||
"SLIDER", "WORD_IMAGE_CLICK", "ROTATE", "CONCAT"
|
||||
};
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
|
||||
// 添加自定义背景图片
|
||||
for (String option : OPTIONS) {
|
||||
for (int i = 1; i <= 26; i++) {
|
||||
resourceStore.addResource(option, new Resource("classpath", "bgimages/"+i + ".jpg", "default"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//添加自定义字体
|
||||
resourceStore.addResource(FontCache.FONT_TYPE,new Resource("classpath", "fonts/SIMSUN.TTC", "default"));
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,7 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
.excludePathPatterns("/flow/**")
|
||||
.excludePathPatterns("/api/v1/open_api/**")
|
||||
.excludePathPatterns("/api/v1/config/get")
|
||||
.excludePathPatterns("/api/v1/user/login");
|
||||
.excludePathPatterns("/api/v1/user/login")
|
||||
.excludePathPatterns("/api/v1/captcha/**");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.admin.controller;
|
||||
|
||||
import cloud.tianai.captcha.application.ImageCaptchaApplication;
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.common.response.ApiResponse;
|
||||
import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
|
||||
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
|
||||
import cloud.tianai.captcha.validator.common.model.dto.MatchParam;
|
||||
import com.admin.common.dto.CaptchaVerifyDto;
|
||||
import com.admin.common.lang.R;
|
||||
import com.admin.entity.ViteConfig;
|
||||
import com.admin.service.ViteConfigService;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import cloud.tianai.captcha.application.vo.CaptchaResponse;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.validation.Valid;
|
||||
import java.util.Collections;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
* 验证码控制器
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@CrossOrigin
|
||||
@RequestMapping("/api/v1/captcha")
|
||||
public class CaptchaController extends BaseController {
|
||||
|
||||
@Resource
|
||||
ImageCaptchaApplication application;
|
||||
|
||||
private static final String[] OPTIONS = {
|
||||
"SLIDER", "WORD_IMAGE_CLICK", "ROTATE", "CONCAT"
|
||||
};
|
||||
|
||||
@PostMapping("/check")
|
||||
public R check() {
|
||||
ViteConfig viteConfig = viteConfigService.getOne(new QueryWrapper<ViteConfig>().eq("name", "captcha_enabled"));
|
||||
if (viteConfig == null) return R.ok(0);
|
||||
if (!Objects.equals(viteConfig.getValue(), "true")) return R.ok(0);
|
||||
return R.ok(1);
|
||||
}
|
||||
|
||||
|
||||
@PostMapping("/generate")
|
||||
public CaptchaResponse<ImageCaptchaVO> genCaptcha() {
|
||||
ViteConfig viteConfig = viteConfigService.getOne(new QueryWrapper<ViteConfig>().eq("name", "captcha_type"));
|
||||
String captchaType;
|
||||
if (viteConfig == null || Objects.equals(viteConfig.getValue(), "RANDOM")) {
|
||||
captchaType = getRandomOption();
|
||||
}else {
|
||||
captchaType = viteConfig.getValue();
|
||||
}
|
||||
return application.generateCaptcha(captchaType);
|
||||
}
|
||||
|
||||
|
||||
@PostMapping("/verify")
|
||||
public ApiResponse<?> verify(@Valid @RequestBody CaptchaVerifyDto verifyDto) {
|
||||
ApiResponse<?> response = application.matching(verifyDto.getId(), verifyDto.getData());
|
||||
if (response.isSuccess()) {
|
||||
return ApiResponse.ofSuccess(Collections.singletonMap("validToken", verifyDto.getId()));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
public static String getRandomOption() {
|
||||
int index = ThreadLocalRandom.current().nextInt(OPTIONS.length);
|
||||
return OPTIONS[index];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.admin.entity;
|
||||
|
||||
import com.admin.entity.BaseEntity;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
*
|
||||
* </p>
|
||||
*
|
||||
* @author QAQ
|
||||
* @since 2025-08-14
|
||||
*/
|
||||
@Data
|
||||
public class StatisticsFlow {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long userId;
|
||||
|
||||
private Long flow;
|
||||
|
||||
private Long totalFlow;
|
||||
|
||||
private String time;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.admin.mapper;
|
||||
|
||||
import com.admin.entity.StatisticsFlow;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Mapper 接口
|
||||
* </p>
|
||||
*
|
||||
* @author QAQ
|
||||
* @since 2025-08-14
|
||||
*/
|
||||
public interface StatisticsFlowMapper extends BaseMapper<StatisticsFlow> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.admin.service;
|
||||
|
||||
import com.admin.entity.StatisticsFlow;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 服务类
|
||||
* </p>
|
||||
*
|
||||
* @author QAQ
|
||||
* @since 2025-08-14
|
||||
*/
|
||||
public interface StatisticsFlowService extends IService<StatisticsFlow> {
|
||||
|
||||
}
|
||||
@@ -7,10 +7,12 @@ import com.admin.common.dto.NodeUpdateDto;
|
||||
import com.admin.common.lang.R;
|
||||
import com.admin.entity.Node;
|
||||
import com.admin.entity.Tunnel;
|
||||
import com.admin.entity.ViteConfig;
|
||||
import com.admin.mapper.NodeMapper;
|
||||
import com.admin.mapper.TunnelMapper;
|
||||
import com.admin.service.NodeService;
|
||||
import com.admin.service.TunnelService;
|
||||
import com.admin.service.ViteConfigService;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
@@ -69,8 +71,9 @@ public class NodeServiceImpl extends ServiceImpl<NodeMapper, Node> implements No
|
||||
@Lazy
|
||||
private TunnelService tunnelService;
|
||||
|
||||
@Value("${server-addr}")
|
||||
private String serverAddr;
|
||||
@Resource
|
||||
ViteConfigService viteConfigService;
|
||||
|
||||
|
||||
// ========== 公共接口实现 ==========
|
||||
|
||||
@@ -321,9 +324,7 @@ public class NodeServiceImpl extends ServiceImpl<NodeMapper, Node> implements No
|
||||
}
|
||||
|
||||
// 2. 构建安装命令
|
||||
String installCommand = buildInstallCommand(node);
|
||||
|
||||
return R.ok(installCommand);
|
||||
return buildInstallCommand(node);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -332,7 +333,10 @@ public class NodeServiceImpl extends ServiceImpl<NodeMapper, Node> implements No
|
||||
* @param node 节点对象
|
||||
* @return 格式化的安装命令
|
||||
*/
|
||||
private String buildInstallCommand(Node node) {
|
||||
private R buildInstallCommand(Node node) {
|
||||
ViteConfig viteConfig = viteConfigService.getOne(new QueryWrapper<ViteConfig>().eq("name", "ip"));
|
||||
if (viteConfig == null) return R.err("请先前往网站配置中设置ip");
|
||||
|
||||
StringBuilder command = new StringBuilder();
|
||||
|
||||
// 第一部分:下载安装脚本
|
||||
@@ -340,14 +344,14 @@ public class NodeServiceImpl extends ServiceImpl<NodeMapper, Node> implements No
|
||||
.append(" -o ./install.sh && chmod +x ./install.sh && ");
|
||||
|
||||
// 处理服务器地址,如果是IPv6需要添加方括号
|
||||
String processedServerAddr = processServerAddress(serverAddr);
|
||||
String processedServerAddr = processServerAddress(viteConfig.getValue());
|
||||
|
||||
// 第二部分:执行安装脚本(去掉-u参数)
|
||||
command.append("./install.sh")
|
||||
.append(" -a ").append(processedServerAddr) // 服务器地址
|
||||
.append(" -s ").append(node.getSecret()); // 节点密钥
|
||||
|
||||
return command.toString();
|
||||
return R.ok(command.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.admin.service.impl;
|
||||
|
||||
import com.admin.entity.StatisticsFlow;
|
||||
import com.admin.mapper.StatisticsFlowMapper;
|
||||
import com.admin.service.StatisticsFlowService;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 服务实现类
|
||||
* </p>
|
||||
*
|
||||
* @author QAQ
|
||||
* @since 2025-08-14
|
||||
*/
|
||||
@Service
|
||||
public class StatisticsFlowServiceImpl extends ServiceImpl<StatisticsFlowMapper, StatisticsFlow> implements StatisticsFlowService {
|
||||
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.admin.service.impl;
|
||||
|
||||
import cloud.tianai.captcha.application.ImageCaptchaApplication;
|
||||
import cloud.tianai.captcha.spring.plugins.secondary.SecondaryVerificationApplication;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.admin.common.dto.*;
|
||||
@@ -7,28 +9,24 @@ import com.admin.common.lang.R;
|
||||
import com.admin.common.utils.GostUtil;
|
||||
import com.admin.common.utils.JwtUtil;
|
||||
import com.admin.common.utils.Md5Util;
|
||||
import com.admin.entity.Forward;
|
||||
import com.admin.entity.Node;
|
||||
import com.admin.entity.Tunnel;
|
||||
import com.admin.entity.User;
|
||||
import com.admin.entity.UserTunnel;
|
||||
import com.admin.entity.*;
|
||||
import com.admin.mapper.ForwardMapper;
|
||||
import com.admin.mapper.UserMapper;
|
||||
import com.admin.mapper.UserTunnelMapper;
|
||||
import com.admin.service.NodeService;
|
||||
import com.admin.service.TunnelService;
|
||||
import com.admin.service.UserService;
|
||||
import com.admin.service.UserTunnelService;
|
||||
import com.admin.service.*;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
@@ -77,8 +75,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
private static final String ERROR_GET_PACKAGE_INFO_FAILED = "获取套餐信息失败";
|
||||
private static final String ERROR_CURRENT_PASSWORD_WRONG = "当前密码错误";
|
||||
private static final String ERROR_PASSWORD_NOT_MATCH = "新密码和确认密码不匹配";
|
||||
private static final String SUCCESS_PASSWORD_UPDATE = "密码修改成功";
|
||||
|
||||
|
||||
/** 默认账号密码 */
|
||||
private static final String DEFAULT_USERNAME = "admin_user";
|
||||
private static final String DEFAULT_PASSWORD = "admin_user";
|
||||
@@ -112,28 +109,48 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
@Resource
|
||||
UserTunnelService userTunnelService;
|
||||
|
||||
@Resource
|
||||
ViteConfigService viteConfigService;
|
||||
|
||||
@Resource
|
||||
StatisticsFlowService statisticsFlowService;
|
||||
|
||||
@Resource
|
||||
private ImageCaptchaApplication application;
|
||||
|
||||
// ========== 公共接口实现 ==========
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
* 验证用户名密码,检查账户状态,生成JWT令牌
|
||||
* 验证验证码、用户名密码,检查账户状态,生成JWT令牌
|
||||
*
|
||||
* @param loginDto 登录数据传输对象
|
||||
* @return 登录结果响应,包含令牌和用户信息
|
||||
*/
|
||||
@Override
|
||||
public R login(LoginDto loginDto) {
|
||||
// 1. 验证用户凭据
|
||||
|
||||
// 1. 验证验证码
|
||||
ViteConfig viteConfig = viteConfigService.getOne(new QueryWrapper<ViteConfig>().eq("name", "captcha_enabled"));
|
||||
if (viteConfig != null && Objects.equals(viteConfig.getValue(), "true")) {
|
||||
if (StringUtils.isBlank(loginDto.getCaptchaId())) return R.err("验证码校验失败");
|
||||
boolean valid = ((SecondaryVerificationApplication) application).secondaryVerification(loginDto.getCaptchaId());
|
||||
if (!valid) return R.err("验证码校验失败");
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 2. 验证用户凭据
|
||||
LoginValidationResult validationResult = validateUserCredentials(loginDto);
|
||||
if (validationResult.isHasError()) {
|
||||
return R.err(validationResult.getErrorMessage());
|
||||
}
|
||||
|
||||
// 2. 生成令牌并返回用户信息
|
||||
// 3. 生成令牌并返回用户信息
|
||||
User user = validationResult.getUser();
|
||||
String token = JwtUtil.generateToken(user);
|
||||
|
||||
// 3. 检查是否使用默认账号密码
|
||||
// 4. 检查是否使用默认账号密码
|
||||
boolean requirePasswordChange = isDefaultCredentials(loginDto.getUsername(), loginDto.getPassword());
|
||||
|
||||
return R.ok(MapUtil.builder()
|
||||
@@ -238,6 +255,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
try {
|
||||
// 2. 级联删除用户相关数据
|
||||
deleteUserRelatedData(id);
|
||||
statisticsFlowService.remove(new QueryWrapper<StatisticsFlow>().eq("user_id", id));
|
||||
// 3. 删除用户
|
||||
boolean result = this.removeById(id);
|
||||
return result ? R.ok(SUCCESS_DELETE_MSG) : R.err(ERROR_DELETE_FAILED);
|
||||
@@ -345,6 +363,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
|
||||
// ========== 私有辅助方法 ==========
|
||||
|
||||
|
||||
/**
|
||||
* 验证用户登录凭据
|
||||
*
|
||||
@@ -655,12 +674,16 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
|
||||
// 3. 获取转发详情
|
||||
List<UserPackageDto.UserForwardDetailDto> forwards = userMapper.getUserForwardDetails(user.getId().intValue());
|
||||
|
||||
// 4. 查询最近24小时流量信息,没有的补0
|
||||
List<StatisticsFlow> statisticsFlows = getLast24HoursFlowStatistics(user.getId());
|
||||
|
||||
// 4. 构造返回结果
|
||||
// 5. 构造返回结果
|
||||
UserPackageDto packageDto = new UserPackageDto();
|
||||
packageDto.setUserInfo(userInfo);
|
||||
packageDto.setTunnelPermissions(tunnelPermissions);
|
||||
packageDto.setForwards(forwards);
|
||||
packageDto.setStatisticsFlows(statisticsFlows);
|
||||
|
||||
return packageDto;
|
||||
}
|
||||
@@ -697,6 +720,104 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
return userMapper.getUserTunnelDetails(userId.intValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户最近24小时的流量统计数据,没有数据的时间点补0
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 最近24小时流量统计列表
|
||||
*/
|
||||
private List<StatisticsFlow> getLast24HoursFlowStatistics(Long userId) {
|
||||
try {
|
||||
// 按ID倒序查最近24条记录(ID越大越新,时间就是23:00, 22:00, 21:00...这样倒序)
|
||||
List<StatisticsFlow> recentFlows = statisticsFlowService.list(
|
||||
new QueryWrapper<StatisticsFlow>()
|
||||
.eq("user_id", userId)
|
||||
.orderByDesc("id")
|
||||
.last("LIMIT 24")
|
||||
);
|
||||
|
||||
List<StatisticsFlow> result = new ArrayList<>(recentFlows);
|
||||
|
||||
// 如果查出来的记录不足24条,需要补0和对应的时间
|
||||
if (result.size() < 24) {
|
||||
// 获取最早记录的时间,继续往前推
|
||||
int startHour = getCurrentHour();
|
||||
if (!result.isEmpty()) {
|
||||
// 从最后一条记录的时间继续往前推
|
||||
String lastTime = result.get(result.size() - 1).getTime();
|
||||
startHour = parseHour(lastTime) - 1;
|
||||
}
|
||||
|
||||
// 补0到24条
|
||||
while (result.size() < 24) {
|
||||
if (startHour < 0) startHour = 23; // 跨天处理
|
||||
|
||||
StatisticsFlow emptyFlow = new StatisticsFlow();
|
||||
emptyFlow.setUserId(userId);
|
||||
emptyFlow.setFlow(0L);
|
||||
emptyFlow.setTotalFlow(0L);
|
||||
emptyFlow.setTime(String.format("%02d:00", startHour));
|
||||
result.add(emptyFlow);
|
||||
|
||||
startHour--;
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("用户 {} 获取到 {} 条实际记录,补齐为 {} 条24小时记录", userId, recentFlows.size(), result.size());
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取用户 {} 最近24小时流量统计失败", userId, e);
|
||||
// 返回24条全0数据,时间从当前小时往前推
|
||||
return generateEmpty24HourData(userId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前小时(0-23)
|
||||
*/
|
||||
private int getCurrentHour() {
|
||||
return java.time.LocalDateTime.now().getHour();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析时间字符串获取小时数
|
||||
*/
|
||||
private int parseHour(String timeStr) {
|
||||
try {
|
||||
if (timeStr != null && timeStr.contains(":")) {
|
||||
return Integer.parseInt(timeStr.split(":")[0]);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 解析失败,返回当前小时
|
||||
}
|
||||
return getCurrentHour();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成24小时全0数据(异常情况使用)
|
||||
*/
|
||||
private List<StatisticsFlow> generateEmpty24HourData(Long userId) {
|
||||
List<StatisticsFlow> result = new ArrayList<>();
|
||||
int currentHour = getCurrentHour();
|
||||
|
||||
for (int i = 0; i < 24; i++) {
|
||||
StatisticsFlow emptyFlow = new StatisticsFlow();
|
||||
emptyFlow.setUserId(userId);
|
||||
emptyFlow.setFlow(0L);
|
||||
emptyFlow.setTotalFlow(0L);
|
||||
emptyFlow.setTime(String.format("%02d:00", currentHour));
|
||||
result.add(emptyFlow);
|
||||
|
||||
currentHour--;
|
||||
if (currentHour < 0) currentHour = 23;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user