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
+207
View File
@@ -0,0 +1,207 @@
syntax = "v1"
info (
title: "Group API"
desc: "API for user group and node group management"
author: "Tension"
email: "tension@ppanel.com"
version: "0.0.1"
)
import (
"../types.api"
"./server.api"
)
type (
// ===== 节点组管理 =====
// GetNodeGroupListRequest
GetNodeGroupListRequest {
Page int `form:"page"`
Size int `form:"size"`
GroupId string `form:"group_id,omitempty"`
}
// GetNodeGroupListResponse
GetNodeGroupListResponse {
Total int64 `json:"total"`
List []NodeGroup `json:"list"`
}
// CreateNodeGroupRequest
CreateNodeGroupRequest {
Name string `json:"name" validate:"required"`
Description string `json:"description"`
Sort int `json:"sort"`
ForCalculation *bool `json:"for_calculation"`
MinTrafficGB *int64 `json:"min_traffic_gb,omitempty"`
MaxTrafficGB *int64 `json:"max_traffic_gb,omitempty"`
}
// UpdateNodeGroupRequest
UpdateNodeGroupRequest {
Id int64 `json:"id" validate:"required"`
Name string `json:"name"`
Description string `json:"description"`
Sort int `json:"sort"`
ForCalculation *bool `json:"for_calculation"`
MinTrafficGB *int64 `json:"min_traffic_gb,omitempty"`
MaxTrafficGB *int64 `json:"max_traffic_gb,omitempty"`
}
// DeleteNodeGroupRequest
DeleteNodeGroupRequest {
Id int64 `json:"id" validate:"required"`
}
// ===== 分组配置管理 =====
// GetGroupConfigRequest
GetGroupConfigRequest {
Keys []string `form:"keys,omitempty"`
}
// GetGroupConfigResponse
GetGroupConfigResponse {
Enabled bool `json:"enabled"`
Mode string `json:"mode"`
Config map[string]interface{} `json:"config"`
State RecalculationState `json:"state"`
}
// UpdateGroupConfigRequest
UpdateGroupConfigRequest {
Enabled bool `json:"enabled"`
Mode string `json:"mode"`
Config map[string]interface{} `json:"config"`
}
// RecalculationState
RecalculationState {
State string `json:"state"`
Progress int `json:"progress"`
Total int `json:"total"`
}
// ===== 分组操作 =====
// RecalculateGroupRequest
RecalculateGroupRequest {
Mode string `json:"mode" validate:"required"`
TriggerType string `json:"trigger_type"` // "manual" or "scheduled"
}
// GetGroupHistoryRequest
GetGroupHistoryRequest {
Page int `form:"page"`
Size int `form:"size"`
GroupMode string `form:"group_mode,omitempty"`
TriggerType string `form:"trigger_type,omitempty"`
}
// GetGroupHistoryResponse
GetGroupHistoryResponse {
Total int64 `json:"total"`
List []GroupHistory `json:"list"`
}
// GetGroupHistoryDetailRequest
GetGroupHistoryDetailRequest {
Id int64 `form:"id" validate:"required"`
}
// GetGroupHistoryDetailResponse
GetGroupHistoryDetailResponse {
GroupHistoryDetail
}
// PreviewUserNodesRequest
PreviewUserNodesRequest {
UserId int64 `form:"user_id" validate:"required"`
}
// PreviewUserNodesResponse
PreviewUserNodesResponse {
UserId int64 `json:"user_id"`
NodeGroups []NodeGroupItem `json:"node_groups"`
}
// NodeGroupItem
NodeGroupItem {
Id int64 `json:"id"`
Name string `json:"name"`
Nodes []Node `json:"nodes"`
}
// ExportGroupResultRequest
ExportGroupResultRequest {
HistoryId *int64 `form:"history_id,omitempty"`
}
// ===== Reset Groups =====
// ResetGroupsRequest
ResetGroupsRequest {
Confirm bool `json:"confirm" validate:"required"`
}
// ===== 套餐分组映射 =====
// SubscribeGroupMappingItem
SubscribeGroupMappingItem {
SubscribeName string `json:"subscribe_name"`
NodeGroupName string `json:"node_group_name"`
}
// GetSubscribeGroupMappingRequest
GetSubscribeGroupMappingRequest {}
// GetSubscribeGroupMappingResponse
GetSubscribeGroupMappingResponse {
List []SubscribeGroupMappingItem `json:"list"`
}
)
@server (
prefix: v1/admin/group
group: admin/group
jwt: JwtAuth
middleware: AuthMiddleware
)
service ppanel {
// ===== 节点组管理 =====
@doc "Get node group list"
@handler GetNodeGroupList
get /node/list (GetNodeGroupListRequest) returns (GetNodeGroupListResponse)
@doc "Create node group"
@handler CreateNodeGroup
post /node (CreateNodeGroupRequest)
@doc "Update node group"
@handler UpdateNodeGroup
put /node (UpdateNodeGroupRequest)
@doc "Delete node group"
@handler DeleteNodeGroup
delete /node (DeleteNodeGroupRequest)
// ===== 分组配置管理 =====
@doc "Get group config"
@handler GetGroupConfig
get /config (GetGroupConfigRequest) returns (GetGroupConfigResponse)
@doc "Update group config"
@handler UpdateGroupConfig
put /config (UpdateGroupConfigRequest)
// ===== 分组操作 =====
@doc "Recalculate group"
@handler RecalculateGroup
post /recalculate (RecalculateGroupRequest)
@doc "Get recalculation status"
@handler GetRecalculationStatus
get /recalculation/status returns (RecalculationState)
@doc "Get group history"
@handler GetGroupHistory
get /history (GetGroupHistoryRequest) returns (GetGroupHistoryResponse)
@doc "Export group result"
@handler ExportGroupResult
get /export (ExportGroupResultRequest)
// Routes with query parameters
@doc "Get group history detail"
@handler GetGroupHistoryDetail
get /history/detail (GetGroupHistoryDetailRequest) returns (GetGroupHistoryDetailResponse)
@doc "Preview user nodes"
@handler PreviewUserNodes
get /preview (PreviewUserNodesRequest) returns (PreviewUserNodesResponse)
@doc "Reset all groups"
@handler ResetGroups
post /reset (ResetGroupsRequest)
@doc "Get subscribe group mapping"
@handler GetSubscribeGroupMapping
get /subscribe/mapping (GetSubscribeGroupMappingRequest) returns (GetSubscribeGroupMappingResponse)
}
+3 -2
View File
@@ -27,8 +27,8 @@ type (
Status int64 `json:"status,omitempty" validate:"omitempty,oneof=0 1"`
}
ToggleRedemptionCodeStatusRequest {
Id int64 `json:"id" validate:"required"`
Status int64 `json:"status" validate:"oneof=0 1"`
Id int64 `json:"id" validate:"required"`
Status int64 `json:"status" validate:"oneof=0 1"`
}
DeleteRedemptionCodeRequest {
Id int64 `json:"id" validate:"required"`
@@ -93,3 +93,4 @@ service ppanel {
@handler GetRedemptionRecordList
get /record/list (GetRedemptionRecordListRequest) returns (GetRedemptionRecordListResponse)
}
+34 -29
View File
@@ -80,36 +80,40 @@ type (
Protocols []Protocol `json:"protocols"`
}
Node {
Id int64 `json:"id"`
Name string `json:"name"`
Tags []string `json:"tags"`
Port uint16 `json:"port"`
Address string `json:"address"`
ServerId int64 `json:"server_id"`
Protocol string `json:"protocol"`
Enabled *bool `json:"enabled"`
Sort int `json:"sort,omitempty"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
Id int64 `json:"id"`
Name string `json:"name"`
Tags []string `json:"tags"`
Port uint16 `json:"port"`
Address string `json:"address"`
ServerId int64 `json:"server_id"`
Protocol string `json:"protocol"`
Enabled *bool `json:"enabled"`
Sort int `json:"sort,omitempty"`
NodeGroupId int64 `json:"node_group_id,omitempty"`
NodeGroupIds []int64 `json:"node_group_ids,omitempty"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
CreateNodeRequest {
Name string `json:"name"`
Tags []string `json:"tags,omitempty"`
Port uint16 `json:"port"`
Address string `json:"address"`
ServerId int64 `json:"server_id"`
Protocol string `json:"protocol"`
Enabled *bool `json:"enabled"`
Name string `json:"name"`
Tags []string `json:"tags,omitempty"`
Port uint16 `json:"port"`
Address string `json:"address"`
ServerId int64 `json:"server_id"`
Protocol string `json:"protocol"`
Enabled *bool `json:"enabled"`
NodeGroupIds []int64 `json:"node_group_ids,omitempty"`
}
UpdateNodeRequest {
Id int64 `json:"id"`
Name string `json:"name"`
Tags []string `json:"tags,omitempty"`
Port uint16 `json:"port"`
Address string `json:"address"`
ServerId int64 `json:"server_id"`
Protocol string `json:"protocol"`
Enabled *bool `json:"enabled"`
Id int64 `json:"id"`
Name string `json:"name"`
Tags []string `json:"tags,omitempty"`
Port uint16 `json:"port"`
Address string `json:"address"`
ServerId int64 `json:"server_id"`
Protocol string `json:"protocol"`
Enabled *bool `json:"enabled"`
NodeGroupIds []int64 `json:"node_group_ids,omitempty"`
}
ToggleNodeStatusRequest {
Id int64 `json:"id"`
@@ -119,9 +123,10 @@ type (
Id int64 `json:"id"`
}
FilterNodeListRequest {
Page int `form:"page"`
Size int `form:"size"`
Search string `form:"search,omitempty"`
Page int `form:"page"`
Size int `form:"size"`
Search string `form:"search,omitempty"`
NodeGroupId *int64 `form:"node_group_id,omitempty"`
}
FilterNodeListResponse {
Total int64 `json:"total"`
+10 -4
View File
@@ -48,6 +48,8 @@ type (
Quota int64 `json:"quota"`
Nodes []int64 `json:"nodes"`
NodeTags []string `json:"node_tags"`
NodeGroupIds []int64 `json:"node_group_ids,omitempty"`
NodeGroupId int64 `json:"node_group_id"`
Show *bool `json:"show"`
Sell *bool `json:"sell"`
DeductionRatio int64 `json:"deduction_ratio"`
@@ -55,6 +57,7 @@ type (
ResetCycle int64 `json:"reset_cycle"`
RenewalReset *bool `json:"renewal_reset"`
ShowOriginalPrice bool `json:"show_original_price"`
AutoCreateGroup bool `json:"auto_create_group"`
}
UpdateSubscribeRequest {
Id int64 `json:"id" validate:"required"`
@@ -72,6 +75,8 @@ type (
Quota int64 `json:"quota"`
Nodes []int64 `json:"nodes"`
NodeTags []string `json:"node_tags"`
NodeGroupIds []int64 `json:"node_group_ids,omitempty"`
NodeGroupId int64 `json:"node_group_id"`
Show *bool `json:"show"`
Sell *bool `json:"sell"`
Sort int64 `json:"sort"`
@@ -85,10 +90,11 @@ type (
Sort []SortItem `json:"sort"`
}
GetSubscribeListRequest {
Page int64 `form:"page" validate:"required"`
Size int64 `form:"size" validate:"required"`
Language string `form:"language,omitempty"`
Search string `form:"search,omitempty"`
Page int64 `form:"page" validate:"required"`
Size int64 `form:"size" validate:"required"`
Language string `form:"language,omitempty"`
Search string `form:"search,omitempty"`
NodeGroupId int64 `form:"node_group_id,omitempty"`
}
SubscribeItem {
Subscribe
+2
View File
@@ -78,6 +78,8 @@ type (
OrderId int64 `json:"order_id"`
SubscribeId int64 `json:"subscribe_id"`
Subscribe Subscribe `json:"subscribe"`
NodeGroupId int64 `json:"node_group_id"`
GroupLocked bool `json:"group_locked"`
StartTime int64 `json:"start_time"`
ExpireTime int64 `json:"expire_time"`
ResetTime int64 `json:"reset_time"`
+1
View File
@@ -30,3 +30,4 @@ service ppanel {
@handler RedeemCode
post / (RedeemCodeRequest) returns (RedeemCodeResponse)
}
+42 -45
View File
@@ -14,48 +14,45 @@ type (
QuerySubscribeListRequest {
Language string `form:"language"`
}
QueryUserSubscribeNodeListResponse {
List []UserSubscribeInfo `json:"list"`
}
UserSubscribeInfo {
Id int64 `json:"id"`
UserId int64 `json:"user_id"`
OrderId int64 `json:"order_id"`
SubscribeId int64 `json:"subscribe_id"`
StartTime int64 `json:"start_time"`
ExpireTime int64 `json:"expire_time"`
FinishedAt int64 `json:"finished_at"`
ResetTime int64 `json:"reset_time"`
Traffic int64 `json:"traffic"`
Download int64 `json:"download"`
Upload int64 `json:"upload"`
Token string `json:"token"`
Status uint8 `json:"status"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
IsTryOut bool `json:"is_try_out"`
Nodes []*UserSubscribeNodeInfo `json:"nodes"`
}
UserSubscribeNodeInfo{
Id int64 `json:"id"`
Name string `json:"name"`
Uuid string `json:"uuid"`
Protocol string `json:"protocol"`
Protocols string `json:"protocols"`
Port uint16 `json:"port"`
Address string `json:"address"`
Tags []string `json:"tags"`
Country string `json:"country"`
City string `json:"city"`
Longitude string `json:"longitude"`
Latitude string `json:"latitude"`
LatitudeCenter string `json:"latitude_center"`
LongitudeCenter string `json:"longitude_center"`
CreatedAt int64 `json:"created_at"`
}
QueryUserSubscribeNodeListResponse {
List []UserSubscribeInfo `json:"list"`
}
UserSubscribeInfo {
Id int64 `json:"id"`
UserId int64 `json:"user_id"`
OrderId int64 `json:"order_id"`
SubscribeId int64 `json:"subscribe_id"`
StartTime int64 `json:"start_time"`
ExpireTime int64 `json:"expire_time"`
FinishedAt int64 `json:"finished_at"`
ResetTime int64 `json:"reset_time"`
Traffic int64 `json:"traffic"`
Download int64 `json:"download"`
Upload int64 `json:"upload"`
Token string `json:"token"`
Status uint8 `json:"status"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
IsTryOut bool `json:"is_try_out"`
Nodes []*UserSubscribeNodeInfo `json:"nodes"`
}
UserSubscribeNodeInfo {
Id int64 `json:"id"`
Name string `json:"name"`
Uuid string `json:"uuid"`
Protocol string `json:"protocol"`
Protocols string `json:"protocols"`
Port uint16 `json:"port"`
Address string `json:"address"`
Tags []string `json:"tags"`
Country string `json:"country"`
City string `json:"city"`
Longitude string `json:"longitude"`
Latitude string `json:"latitude"`
LatitudeCenter string `json:"latitude_center"`
LongitudeCenter string `json:"longitude_center"`
CreatedAt int64 `json:"created_at"`
}
)
@server (
@@ -68,8 +65,8 @@ service ppanel {
@handler QuerySubscribeList
get /list (QuerySubscribeListRequest) returns (QuerySubscribeListResponse)
@doc "Get user subscribe node info"
@handler QueryUserSubscribeNodeList
get /node/list returns (QueryUserSubscribeNodeListResponse)
@doc "Get user subscribe node info"
@handler QueryUserSubscribeNodeList
get /node/list returns (QueryUserSubscribeNodeListResponse)
}
+39 -47
View File
@@ -66,7 +66,6 @@ type (
UnbindOAuthRequest {
Method string `json:"method"`
}
GetLoginLogRequest {
Page int `form:"page"`
Size int `form:"size"`
@@ -95,21 +94,17 @@ type (
Email string `json:"email" validate:"required"`
Code string `json:"code" validate:"required"`
}
GetDeviceListResponse {
List []UserDevice `json:"list"`
Total int64 `json:"total"`
}
UnbindDeviceRequest {
Id int64 `json:"id" validate:"required"`
}
GetDeviceListResponse {
List []UserDevice `json:"list"`
Total int64 `json:"total"`
}
UnbindDeviceRequest {
Id int64 `json:"id" validate:"required"`
}
UpdateUserSubscribeNoteRequest {
UserSubscribeId int64 `json:"user_subscribe_id" validate:"required"`
Note string `json:"note" validate:"max=500"`
}
UpdateUserRulesRequest {
Rules []string `json:"rules" validate:"required"`
}
@@ -135,23 +130,20 @@ type (
List []WithdrawalLog `json:"list"`
Total int64 `json:"total"`
}
GetDeviceOnlineStatsResponse {
WeeklyStats []WeeklyStat `json:"weekly_stats"`
ConnectionRecords ConnectionRecords `json:"connection_records"`
}
WeeklyStat {
Day int `json:"day"`
DayName string `json:"day_name"`
Hours float64 `json:"hours"`
}
ConnectionRecords {
CurrentContinuousDays int64 `json:"current_continuous_days"`
HistoryContinuousDays int64 `json:"history_continuous_days"`
LongestSingleConnection int64 `json:"longest_single_connection"`
}
GetDeviceOnlineStatsResponse {
WeeklyStats []WeeklyStat `json:"weekly_stats"`
ConnectionRecords ConnectionRecords `json:"connection_records"`
}
WeeklyStat {
Day int `json:"day"`
DayName string `json:"day_name"`
Hours float64 `json:"hours"`
}
ConnectionRecords {
CurrentContinuousDays int64 `json:"current_continuous_days"`
HistoryContinuousDays int64 `json:"history_continuous_days"`
LongestSingleConnection int64 `json:"longest_single_connection"`
}
)
@server (
@@ -248,9 +240,9 @@ service ppanel {
@handler UpdateBindEmail
put /bind_email (UpdateBindEmailRequest)
@doc "Get Device List"
@handler GetDeviceList
get /devices returns (GetDeviceListResponse)
@doc "Get Device List"
@handler GetDeviceList
get /devices returns (GetDeviceListResponse)
@doc "Unbind Device"
@handler UnbindDevice
@@ -272,23 +264,23 @@ service ppanel {
@handler QueryWithdrawalLog
get /withdrawal_log (QueryWithdrawalLogListRequest) returns (QueryWithdrawalLogListResponse)
@doc "Device Online Statistics"
@handler DeviceOnlineStatistics
get /device_online_statistics returns (GetDeviceOnlineStatsResponse)
@doc "Delete Current User Account"
@handler DeleteCurrentUserAccount
delete /current_user_account
@doc "Device Online Statistics"
@handler DeviceOnlineStatistics
get /device_online_statistics returns (GetDeviceOnlineStatsResponse)
@doc "Delete Current User Account"
@handler DeleteCurrentUserAccount
delete /current_user_account
}
@server(
prefix: v1/public/user
group: public/user/ws
middleware: AuthMiddleware
@server (
prefix: v1/public/user
group: public/user/ws
middleware: AuthMiddleware
)
service ppanel {
@doc "Webosocket Device Connect"
@handler DeviceWsConnect
get /device_ws_connect
@doc "Webosocket Device Connect"
@handler DeviceWsConnect
get /device_ws_connect
}
+38 -2
View File
@@ -206,7 +206,7 @@ type (
CurrencySymbol string `json:"currency_symbol"`
}
SubscribeDiscount {
Quantity int64 `json:"quantity"`
Quantity int64 `json:"quantity"`
Discount float64 `json:"discount"`
}
Subscribe {
@@ -225,6 +225,8 @@ type (
Quota int64 `json:"quota"`
Nodes []int64 `json:"nodes"`
NodeTags []string `json:"node_tags"`
NodeGroupIds []int64 `json:"node_group_ids,omitempty"`
NodeGroupId int64 `json:"node_group_id"`
Show bool `json:"show"`
Sell bool `json:"sell"`
Sort int64 `json:"sort"`
@@ -486,6 +488,8 @@ type (
OrderId int64 `json:"order_id"`
SubscribeId int64 `json:"subscribe_id"`
Subscribe Subscribe `json:"subscribe"`
NodeGroupId int64 `json:"node_group_id"`
GroupLocked bool `json:"group_locked"`
StartTime int64 `json:"start_time"`
ExpireTime int64 `json:"expire_time"`
FinishedAt int64 `json:"finished_at"`
@@ -697,7 +701,6 @@ type (
List []SubscribeGroup `json:"list"`
Total int64 `json:"total"`
}
GetUserSubscribeTrafficLogsRequest {
Page int `form:"page"`
Size int `form:"size"`
@@ -874,5 +877,38 @@ type (
ResetUserSubscribeTokenRequest {
UserSubscribeId int64 `json:"user_subscribe_id"`
}
// ===== 分组功能类型定义 =====
// NodeGroup 节点组
NodeGroup {
Id int64 `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Sort int `json:"sort"`
ForCalculation bool `json:"for_calculation"`
MinTrafficGB int64 `json:"min_traffic_gb,omitempty"`
MaxTrafficGB int64 `json:"max_traffic_gb,omitempty"`
NodeCount int64 `json:"node_count,omitempty"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
// GroupHistory 分组历史记录
GroupHistory {
Id int64 `json:"id"`
GroupMode string `json:"group_mode"`
TriggerType string `json:"trigger_type"`
TotalUsers int `json:"total_users"`
SuccessCount int `json:"success_count"`
FailedCount int `json:"failed_count"`
StartTime *int64 `json:"start_time,omitempty"`
EndTime *int64 `json:"end_time,omitempty"`
Operator string `json:"operator,omitempty"`
ErrorLog string `json:"error_log,omitempty"`
CreatedAt int64 `json:"created_at"`
}
// GroupHistoryDetail 分组历史详情
GroupHistoryDetail {
GroupHistory
ConfigSnapshot map[string]interface{} `json:"config_snapshot,omitempty"`
}
)