Features:

- Node group CRUD operations with traffic-based filtering
  - Three grouping modes: average distribution, subscription-based, and traffic-based
  - Automatic and manual group recalculation with history tracking
  - Group assignment preview before applying changes
  - User subscription group locking to prevent automatic reassignment
  - Subscribe-to-group mapping configuration
  - Group calculation history and detailed reports
  - System configuration for group management (enabled/mode/auto_create)

  Database:
  - Add node_group table for group definitions
  - Add group_history and group_history_detail tables for tracking
  - Add node_group_ids (JSON) to nodes and subscribe tables
  - Add node_group_id and group_locked fields to user_subscribe table
  - Add migration files for schema changes
This commit is contained in:
EUForest
2026-03-08 23:22:38 +08:00
parent 7d46b31866
commit 39310d5b9a
72 changed files with 4682 additions and 282 deletions
+138 -6
View File
@@ -215,14 +215,133 @@ func (l *SubscribeLogic) getServers(userSub *user.Subscribe) ([]*node.Node, erro
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe details error: %v", err.Error())
}
// 判断是否使用分组模式
isGroupMode := l.isGroupEnabled()
if isGroupMode {
// === 分组模式:使用 node_group_id 获取节点 ===
// 按优先级获取 node_group_iduser_subscribe.node_group_id > subscribe.node_group_id > subscribe.node_group_ids[0]
nodeGroupId := int64(0)
source := ""
// 优先级1: user_subscribe.node_group_id
if userSub.NodeGroupId != 0 {
nodeGroupId = userSub.NodeGroupId
source = "user_subscribe.node_group_id"
} else {
// 优先级2 & 3: 从 subscribe 表获取
if subDetails.NodeGroupId != 0 {
nodeGroupId = subDetails.NodeGroupId
source = "subscribe.node_group_id"
} else if len(subDetails.NodeGroupIds) > 0 {
// 优先级3: subscribe.node_group_ids[0]
nodeGroupId = subDetails.NodeGroupIds[0]
source = "subscribe.node_group_ids[0]"
}
}
l.Debugf("[Generate Subscribe]group mode, using %s: %v", source, nodeGroupId)
// 根据 node_group_id 获取节点
enable := true
// 1. 获取分组节点
var groupNodes []*node.Node
if nodeGroupId > 0 {
params := &node.FilterNodeParams{
Page: 0,
Size: 1000,
NodeGroupIds: []int64{nodeGroupId},
Enabled: &enable,
Preload: true,
}
_, groupNodes, err = l.svc.NodeModel.FilterNodeList(l.ctx.Request.Context(), params)
if err != nil {
l.Errorw("[Generate Subscribe]filter nodes by group error", logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "filter nodes by group error: %v", err.Error())
}
l.Debugf("[Generate Subscribe]found %d nodes for node_group_id=%d", len(groupNodes), nodeGroupId)
}
// 2. 获取公共节点(NodeGroupIds 为空的节点)
_, allNodes, err := l.svc.NodeModel.FilterNodeList(l.ctx.Request.Context(), &node.FilterNodeParams{
Page: 0,
Size: 1000,
Enabled: &enable,
Preload: true,
})
if err != nil {
l.Errorw("[Generate Subscribe]filter all nodes error", logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "filter all nodes error: %v", err.Error())
}
// 过滤出公共节点
var publicNodes []*node.Node
for _, n := range allNodes {
if len(n.NodeGroupIds) == 0 {
publicNodes = append(publicNodes, n)
}
}
l.Debugf("[Generate Subscribe]found %d public nodes (node_group_ids is empty)", len(publicNodes))
// 3. 合并分组节点和公共节点
nodesMap := make(map[int64]*node.Node)
for _, n := range groupNodes {
nodesMap[n.Id] = n
}
for _, n := range publicNodes {
if _, exists := nodesMap[n.Id]; !exists {
nodesMap[n.Id] = n
}
}
// 转换为切片
var result []*node.Node
for _, n := range nodesMap {
result = append(result, n)
}
l.Debugf("[Generate Subscribe]total nodes (group + public): %d (group: %d, public: %d)", len(result), len(groupNodes), len(publicNodes))
// 查询节点组信息,获取节点组名称(仅当用户有分组时)
if nodeGroupId > 0 {
type NodeGroupInfo struct {
Id int64
Name string
}
var nodeGroupInfo NodeGroupInfo
err = l.svc.DB.Table("node_group").Select("id, name").Where("id = ?", nodeGroupId).First(&nodeGroupInfo).Error
if err != nil {
l.Infow("[Generate Subscribe]node group not found", logger.Field("nodeGroupId", nodeGroupId), logger.Field("error", err.Error()))
}
// 如果节点组信息存在,为没有 tag 的分组节点设置节点组名称为 tag
if nodeGroupInfo.Id != 0 && nodeGroupInfo.Name != "" {
for _, n := range result {
// 只为分组节点设置 tag,公共节点不设置
if n.Tags == "" && len(n.NodeGroupIds) > 0 {
n.Tags = nodeGroupInfo.Name
l.Debugf("[Generate Subscribe]set node_group name as tag for node %d: %s", n.Id, nodeGroupInfo.Name)
}
}
}
}
return result, nil
}
// === 标签模式:使用 node_ids 和 tags 获取节点 ===
nodeIds := tool.StringToInt64Slice(subDetails.Nodes)
tags := tool.RemoveStringElement(strings.Split(subDetails.NodeTags, ","), "")
l.Debugf("[Generate Subscribe]nodes: %v, NodeTags: %v", len(nodeIds), len(tags))
l.Debugf("[Generate Subscribe]tag mode, nodes: %v, NodeTags: %v", len(nodeIds), len(tags))
if len(nodeIds) == 0 && len(tags) == 0 {
logger.Infow("[Generate Subscribe]no subscribe nodes")
logger.Infow("[Generate Subscribe]no subscribe nodes configured")
return []*node.Node{}, nil
}
enable := true
var nodes []*node.Node
_, nodes, err = l.svc.NodeModel.FilterNodeList(l.ctx.Request.Context(), &node.FilterNodeParams{
@@ -231,16 +350,15 @@ func (l *SubscribeLogic) getServers(userSub *user.Subscribe) ([]*node.Node, erro
NodeId: nodeIds,
Tag: tool.RemoveDuplicateElements(tags...),
Preload: true,
Enabled: &enable, // Only get enabled nodes
Enabled: &enable,
})
l.Debugf("[Query Subscribe]found servers: %v", len(nodes))
if err != nil {
l.Errorw("[Generate Subscribe]find server details error: %v", logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find server details error: %v", err.Error())
}
logger.Debugf("[Generate Subscribe]found servers: %v", len(nodes))
l.Debugf("[Generate Subscribe]found %d nodes in tag mode", len(nodes))
return nodes, nil
}
@@ -290,3 +408,17 @@ func (l *SubscribeLogic) getFirstHostLine() string {
}
return host
}
// isGroupEnabled 判断分组功能是否启用
func (l *SubscribeLogic) isGroupEnabled() bool {
var value string
err := l.svc.DB.Table("system").
Where("`category` = ? AND `key` = ?", "group", "enabled").
Select("value").
Scan(&value).Error
if err != nil {
l.Debugf("[SubscribeLogic]check group enabled failed: %v", err)
return false
}
return value == "true" || value == "1"
}