定时任务流量上报

This commit is contained in:
lys 2026-03-23 17:08:48 +08:00
parent 07d74769df
commit a850899638
3 changed files with 331 additions and 8 deletions

View File

@ -17,18 +17,27 @@ dataSources:
rules:
- !SHARDING
tables: # 数据分片规则配置
# 表名
net_flow_record:
# 数据源 + 表名
actualDataNodes: net_flow_record.net_flow_record_${['00','01','02','03','04','05','06','07','08','09','10','11','12','13','14','15','16','17','18','19','20','21','22','23']}
# 节点流量记录表(按天分表)
net_node_flow_record:
actualDataNodes: net_flow_record.net_node_flow_record_${['20260323']}
tableStrategy:
standard:
shardingColumn: create_time
shardingAlgorithmName: hourly_sharding
shardingAlgorithmName: daily_sharding
# 用户流量记录表(按天分表)
net_user_flow_record:
actualDataNodes: net_flow_record.net_user_flow_record_${['20260323']}
tableStrategy:
standard:
shardingColumn: create_time
shardingAlgorithmName: daily_sharding
shardingAlgorithms:
hourly_sharding:
# 新增: 按天分表算法
daily_sharding:
type: CLASS_BASED
props:
strategy: standard
algorithmClassName: org.dromara.common.mybatis.sharding.HourlyShardingAlgorithm
algorithmClassName: org.dromara.common.mybatis.sharding.DailyShardingAlgorithm

View File

@ -0,0 +1,314 @@
package org.dromara.job.snailjob;
import com.aizuda.snailjob.client.job.core.annotation.JobExecutor;
import com.aizuda.snailjob.client.job.core.dto.JobArgs;
import com.aizuda.snailjob.common.core.util.JsonUtil;
import com.aizuda.snailjob.common.log.SnailJobLog;
import com.aizuda.snailjob.model.dto.ExecuteResult;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.enums.NetNodeSoftTypeEnum;
import org.dromara.common.mybatis.utils.IdGeneratorUtil;
import org.dromara.common.tenant.helper.TenantHelper;
import org.dromara.net.domain.NetNode;
import org.dromara.net.domain.NetNodeConfig;
import org.dromara.net.domain.NetNodeFlowRecord;
import org.dromara.net.domain.NetUserFlowRecord;
import org.dromara.net.dto.NodeUserStatsRequest;
import org.dromara.net.dto.NodeUserStatsResponse;
import org.dromara.net.mapper.NetNodeConfigMapper;
import org.dromara.net.mapper.NetNodeFlowRecordMapper;
import org.dromara.net.mapper.NetNodeMapper;
import org.dromara.net.mapper.NetUserFlowRecordMapper;
import org.dromara.net.util.RemnaNodeHttpClient;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Remna节点用户流量统计定时任务
* <p>
* 功能说明
* 1. 查询开启的或状态正常的REMNAWAVE节点
* 2. 调用节点接口 /node/stats/get-users-stats 获取用户流量信息reset=true 重置统计
* 3. 将流量信息写入 net_node_flow_record net_user_flow_record
* </p>
*
* @author lys
* @date 2025-03-23
*/
@Slf4j
@Component
@RequiredArgsConstructor
@JobExecutor(name = "remnaNodeUserStatsJob")
public class RemnaNodeUserStatsJob {
private final NetNodeMapper netNodeMapper;
private final NetNodeConfigMapper netNodeConfigMapper;
private final NetNodeFlowRecordMapper netNodeFlowRecordMapper;
private final NetUserFlowRecordMapper netUserFlowRecordMapper;
/**
* 请求超时时间毫秒
*/
private static final int REQUEST_TIMEOUT = 10000;
/**
* 用户流量统计接口路径
*/
private static final String USER_STATS_PATH = "/node/stats/get-users-stats";
/**
* 节点状态超时阈值5分钟与NodeStatusFilterHandler保持一致
*/
private static final long NODE_STATUS_TIMEOUT_MS = 5 * 60 * 1000L;
public ExecuteResult jobExecute(JobArgs jobArgs) {
SnailJobLog.LOCAL.info("remnaNodeUserStatsJob 开始执行. jobArgs:{}", JsonUtil.toJsonString(jobArgs));
SnailJobLog.REMOTE.info("remnaNodeUserStatsJob 开始执行. jobArgs:{}", JsonUtil.toJsonString(jobArgs));
// 1. 查询所有可用的REMNAWAVE节点
List<NetNode> nodes = queryAvailableNodes();
SnailJobLog.LOCAL.info("remnaNodeUserStatsJob 找到 {} 个可用RemnaWave节点", nodes.size());
SnailJobLog.REMOTE.info("remnaNodeUserStatsJob 找到 {} 个可用RemnaWave节点", nodes.size());
if (nodes.isEmpty()) {
return ExecuteResult.success("没有可用的RemnaWave节点");
}
// 2. 用于汇总用户级别的流量跨节点汇总
Map<Long, UserFlowAggregate> userFlowMap = new HashMap<>();
int successCount = 0;
int failCount = 0;
Date recordTime = new Date();
// 3. 遍历节点获取流量数据
for (NetNode node : nodes) {
try {
// 获取节点配置
NetNodeConfig config = TenantHelper.ignore(() -> {
LambdaQueryWrapper<NetNodeConfig> configQueryWrapper = new LambdaQueryWrapper<>();
configQueryWrapper.eq(NetNodeConfig::getNodeId, node.getId());
return netNodeConfigMapper.selectOne(configQueryWrapper);
});
if (config == null) {
SnailJobLog.LOCAL.warn("remnaNodeUserStatsJob 节点 {} ({}) 配置不存在,跳过",
node.getId(), node.getNodeName());
SnailJobLog.REMOTE.warn("remnaNodeUserStatsJob 节点 {} ({}) 配置不存在,跳过",
node.getId(), node.getNodeName());
failCount++;
continue;
}
// 构建请求URL
String nodeUrl = RemnaNodeHttpClient.buildNodeUrl(node.getNodeIp(), node.getServerPort());
String statsUrl = nodeUrl + USER_STATS_PATH;
// 构建请求体reset=true 重置节点统计
NodeUserStatsRequest request = new NodeUserStatsRequest(true);
// 调用接口获取用户流量统计
NodeUserStatsResponse statsResponse = RemnaNodeHttpClient.postAndParse(
config, statsUrl, request, NodeUserStatsResponse.class, REQUEST_TIMEOUT);
if (statsResponse != null && statsResponse.getResponse() != null
&& statsResponse.getResponse().getUsers() != null) {
List<NodeUserStatsResponse.UserFlowData> users = statsResponse.getResponse().getUsers();
// 计算节点级别的流量汇总
long nodeUpTotal = 0L;
long nodeDownTotal = 0L;
for (NodeUserStatsResponse.UserFlowData userData : users) {
Long uplink = userData.getUplink() != null ? userData.getUplink() : 0L;
Long downlink = userData.getDownlink() != null ? userData.getDownlink() : 0L;
nodeUpTotal += uplink;
nodeDownTotal += downlink;
// 解析用户IDusername 就是用户ID 的字符串形式
Long userId = parseUserId(userData.getUsername());
if (userId != null) {
// 汇总用户流量跨节点累加
userFlowMap.compute(userId, (k, v) -> {
if (v == null) {
return new UserFlowAggregate(uplink, downlink);
} else {
v.addUplink(uplink);
v.addDownlink(downlink);
return v;
}
});
}
}
// 保存节点流量记录
saveNodeFlowRecord(node.getId(), nodeUpTotal, nodeDownTotal, recordTime);
successCount++;
SnailJobLog.LOCAL.info("remnaNodeUserStatsJob 节点 {} ({}) 流量采集成功, 用户数={}, 上传={}, 下载={}",
node.getId(), node.getNodeName(), users.size(), nodeUpTotal, nodeDownTotal);
} else {
failCount++;
SnailJobLog.LOCAL.warn("remnaNodeUserStatsJob 节点 {} ({}) 流量采集失败:响应为空",
node.getId(), node.getNodeName());
SnailJobLog.REMOTE.warn("remnaNodeUserStatsJob 节点 {} ({}) 流量采集失败:响应为空",
node.getId(), node.getNodeName());
}
} catch (Exception e) {
failCount++;
SnailJobLog.LOCAL.error("remnaNodeUserStatsJob 节点 {} ({}) 流量采集异常: {}",
node.getId(), node.getNodeName(), e.getMessage());
SnailJobLog.REMOTE.error("remnaNodeUserStatsJob 节点 {} ({}) 流量采集异常: {}",
node.getId(), node.getNodeName(), e.getMessage());
}
}
// 4. 保存用户级别的流量汇总记录
int userRecordCount = saveUserFlowRecords(userFlowMap, recordTime);
String result = String.format("执行完成, 节点成功: %d, 节点失败: %d, 用户记录数: %d",
successCount, failCount, userRecordCount);
SnailJobLog.LOCAL.info("remnaNodeUserStatsJob {}", result);
SnailJobLog.REMOTE.info("remnaNodeUserStatsJob {}", result);
return ExecuteResult.success(result);
}
/**
* 查询可用的REMNAWAVE节点
* 条件
* 1. nodeSoftType = REMNAWAVE
* 2. enableFlag = 1 (开启状态)
* 3. reportStatusTime 在超时阈值内状态正常
*
* @return 可用节点列表
*/
private List<NetNode> queryAvailableNodes() {
return TenantHelper.ignore(() -> {
LambdaQueryWrapper<NetNode> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(NetNode::getNodeSoftType, NetNodeSoftTypeEnum.REMNAWAVE.getNodeValue())
.eq(NetNode::getEnableFlag, 1)
.isNotNull(NetNode::getReportStatusTime);
List<NetNode> nodes = netNodeMapper.selectList(queryWrapper);
// 过滤掉状态超时的节点
Date timeoutThreshold = new Date(System.currentTimeMillis() - NODE_STATUS_TIMEOUT_MS);
nodes.removeIf(node ->
node.getReportStatusTime() != null &&
node.getReportStatusTime().before(timeoutThreshold)
);
return nodes;
});
}
/**
* 解析用户ID
* username 是用户ID的字符串形式
*
* @param username 用户名字符串
* @return 用户ID解析失败返回null
*/
private Long parseUserId(String username) {
if (username == null || username.isEmpty()) {
return null;
}
try {
return Long.parseLong(username);
} catch (NumberFormatException e) {
SnailJobLog.LOCAL.warn("remnaNodeUserStatsJob 解析用户ID失败: {}", username);
return null;
}
}
/**
* 保存节点流量记录
*
* @param nodeId 节点ID
* @param upNum 上传流量
* @param downNum 下载流量
* @param recordTime 记录时间
*/
private void saveNodeFlowRecord(Long nodeId, Long upNum, Long downNum, Date recordTime) {
NetNodeFlowRecord record = new NetNodeFlowRecord();
record.setId(IdGeneratorUtil.nextLongId());
record.setNodeId(nodeId);
record.setUpNum(upNum);
record.setDownNum(downNum);
record.setTotalNum(upNum + downNum);
record.setCreateTime(recordTime);
netNodeFlowRecordMapper.insert(record);
}
/**
* 批量保存用户流量记录
*
* @param userFlowMap 用户流量汇总Map
* @param recordTime 记录时间
* @return 保存的记录数
*/
private int saveUserFlowRecords(Map<Long, UserFlowAggregate> userFlowMap, Date recordTime) {
if (userFlowMap.isEmpty()) {
return 0;
}
List<NetUserFlowRecord> records = new ArrayList<>();
for (Map.Entry<Long, UserFlowAggregate> entry : userFlowMap.entrySet()) {
Long userId = entry.getKey();
UserFlowAggregate flow = entry.getValue();
// 只记录有流量的用户
if (flow.getUplink() > 0 || flow.getDownlink() > 0) {
NetUserFlowRecord record = new NetUserFlowRecord();
record.setId(IdGeneratorUtil.nextLongId());
record.setUserId(userId);
record.setUpNum(flow.getUplink());
record.setDownNum(flow.getDownlink());
record.setTotalNum(flow.getUplink() + flow.getDownlink());
record.setCreateTime(recordTime);
records.add(record);
}
}
// 批量插入
if (!records.isEmpty()) {
netUserFlowRecordMapper.insert(records, 1000);
}
return records.size();
}
/**
* 用户流量汇总内部类
*/
@Data
private static class UserFlowAggregate {
private Long uplink;
private Long downlink;
public UserFlowAggregate(Long uplink, Long downlink) {
this.uplink = uplink;
this.downlink = downlink;
}
public void addUplink(Long value) {
this.uplink += value;
}
public void addDownlink(Long value) {
this.downlink += value;
}
}
}

View File

@ -121,7 +121,7 @@ public class NetNodeConfigServiceImpl implements INetNodeConfigService {
private XrayConfig.Inbound buildUserInbound(NetNodeVo node, NetNodeConfigVo config) {
XrayConfig.Inbound inbound = new XrayConfig.Inbound();
inbound.setTag(node.getProtocolType() + "-inbound");
inbound.setPort(node.getServerPort());
inbound.setPort(node.getConnectionPort());
inbound.setProtocol(node.getProtocolType().toLowerCase());
// 设置协议配置