feat(subscribe): add traffic limit rules and user traffic stats
- Add subscribe traffic_limit schema and migration\n- Support traffic_limit in admin create/update and list/details\n- Apply traffic_limit when building server user list speed limits\n- Add public user traffic stats API
This commit is contained in:
parent
17163486f6
commit
06a2425474
@ -28,22 +28,30 @@ type (
|
|||||||
}
|
}
|
||||||
// CreateNodeGroupRequest
|
// CreateNodeGroupRequest
|
||||||
CreateNodeGroupRequest {
|
CreateNodeGroupRequest {
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Sort int `json:"sort"`
|
Sort int `json:"sort"`
|
||||||
ForCalculation *bool `json:"for_calculation"`
|
ForCalculation *bool `json:"for_calculation"`
|
||||||
MinTrafficGB *int64 `json:"min_traffic_gb,omitempty"`
|
IsExpiredGroup *bool `json:"is_expired_group"`
|
||||||
MaxTrafficGB *int64 `json:"max_traffic_gb,omitempty"`
|
ExpiredDaysLimit *int `json:"expired_days_limit"`
|
||||||
|
MaxTrafficGBExpired *int64 `json:"max_traffic_gb_expired,omitempty"`
|
||||||
|
SpeedLimit *int `json:"speed_limit"`
|
||||||
|
MinTrafficGB *int64 `json:"min_traffic_gb,omitempty"`
|
||||||
|
MaxTrafficGB *int64 `json:"max_traffic_gb,omitempty"`
|
||||||
}
|
}
|
||||||
// UpdateNodeGroupRequest
|
// UpdateNodeGroupRequest
|
||||||
UpdateNodeGroupRequest {
|
UpdateNodeGroupRequest {
|
||||||
Id int64 `json:"id" validate:"required"`
|
Id int64 `json:"id" validate:"required"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Sort int `json:"sort"`
|
Sort int `json:"sort"`
|
||||||
ForCalculation *bool `json:"for_calculation"`
|
ForCalculation *bool `json:"for_calculation"`
|
||||||
MinTrafficGB *int64 `json:"min_traffic_gb,omitempty"`
|
IsExpiredGroup *bool `json:"is_expired_group"`
|
||||||
MaxTrafficGB *int64 `json:"max_traffic_gb,omitempty"`
|
ExpiredDaysLimit *int `json:"expired_days_limit"`
|
||||||
|
MaxTrafficGBExpired *int64 `json:"max_traffic_gb_expired,omitempty"`
|
||||||
|
SpeedLimit *int `json:"speed_limit"`
|
||||||
|
MinTrafficGB *int64 `json:"min_traffic_gb,omitempty"`
|
||||||
|
MaxTrafficGB *int64 `json:"max_traffic_gb,omitempty"`
|
||||||
}
|
}
|
||||||
// DeleteNodeGroupRequest
|
// DeleteNodeGroupRequest
|
||||||
DeleteNodeGroupRequest {
|
DeleteNodeGroupRequest {
|
||||||
|
|||||||
@ -50,6 +50,7 @@ type (
|
|||||||
NodeTags []string `json:"node_tags"`
|
NodeTags []string `json:"node_tags"`
|
||||||
NodeGroupIds []int64 `json:"node_group_ids,omitempty"`
|
NodeGroupIds []int64 `json:"node_group_ids,omitempty"`
|
||||||
NodeGroupId int64 `json:"node_group_id"`
|
NodeGroupId int64 `json:"node_group_id"`
|
||||||
|
TrafficLimit []TrafficLimit `json:"traffic_limit"`
|
||||||
Show *bool `json:"show"`
|
Show *bool `json:"show"`
|
||||||
Sell *bool `json:"sell"`
|
Sell *bool `json:"sell"`
|
||||||
DeductionRatio int64 `json:"deduction_ratio"`
|
DeductionRatio int64 `json:"deduction_ratio"`
|
||||||
@ -77,6 +78,7 @@ type (
|
|||||||
NodeTags []string `json:"node_tags"`
|
NodeTags []string `json:"node_tags"`
|
||||||
NodeGroupIds []int64 `json:"node_group_ids,omitempty"`
|
NodeGroupIds []int64 `json:"node_group_ids,omitempty"`
|
||||||
NodeGroupId int64 `json:"node_group_id"`
|
NodeGroupId int64 `json:"node_group_id"`
|
||||||
|
TrafficLimit []TrafficLimit `json:"traffic_limit"`
|
||||||
Show *bool `json:"show"`
|
Show *bool `json:"show"`
|
||||||
Sell *bool `json:"sell"`
|
Sell *bool `json:"sell"`
|
||||||
Sort int64 `json:"sort"`
|
Sort int64 `json:"sort"`
|
||||||
|
|||||||
@ -144,6 +144,22 @@ type (
|
|||||||
HistoryContinuousDays int64 `json:"history_continuous_days"`
|
HistoryContinuousDays int64 `json:"history_continuous_days"`
|
||||||
LongestSingleConnection int64 `json:"longest_single_connection"`
|
LongestSingleConnection int64 `json:"longest_single_connection"`
|
||||||
}
|
}
|
||||||
|
GetUserTrafficStatsRequest {
|
||||||
|
UserSubscribeId string `form:"user_subscribe_id" validate:"required"`
|
||||||
|
Days int `form:"days" validate:"required,oneof=7 30"`
|
||||||
|
}
|
||||||
|
DailyTrafficStats {
|
||||||
|
Date string `json:"date"`
|
||||||
|
Upload int64 `json:"upload"`
|
||||||
|
Download int64 `json:"download"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
}
|
||||||
|
GetUserTrafficStatsResponse {
|
||||||
|
List []DailyTrafficStats `json:"list"`
|
||||||
|
TotalUpload int64 `json:"total_upload"`
|
||||||
|
TotalDownload int64 `json:"total_download"`
|
||||||
|
TotalTraffic int64 `json:"total_traffic"`
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@server (
|
@server (
|
||||||
@ -271,6 +287,10 @@ service ppanel {
|
|||||||
@doc "Delete Current User Account"
|
@doc "Delete Current User Account"
|
||||||
@handler DeleteCurrentUserAccount
|
@handler DeleteCurrentUserAccount
|
||||||
delete /current_user_account
|
delete /current_user_account
|
||||||
|
|
||||||
|
@doc "Get User Traffic Statistics"
|
||||||
|
@handler GetUserTrafficStats
|
||||||
|
get /traffic_stats (GetUserTrafficStatsRequest) returns (GetUserTrafficStatsResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
@server (
|
@server (
|
||||||
|
|||||||
@ -211,6 +211,12 @@ type (
|
|||||||
Quantity int64 `json:"quantity"`
|
Quantity int64 `json:"quantity"`
|
||||||
Discount float64 `json:"discount"`
|
Discount float64 `json:"discount"`
|
||||||
}
|
}
|
||||||
|
TrafficLimit {
|
||||||
|
StatType string `json:"stat_type"`
|
||||||
|
StatValue int64 `json:"stat_value"`
|
||||||
|
TrafficUsage int64 `json:"traffic_usage"`
|
||||||
|
SpeedLimit int64 `json:"speed_limit"`
|
||||||
|
}
|
||||||
Subscribe {
|
Subscribe {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@ -229,6 +235,7 @@ type (
|
|||||||
NodeTags []string `json:"node_tags"`
|
NodeTags []string `json:"node_tags"`
|
||||||
NodeGroupIds []int64 `json:"node_group_ids,omitempty"`
|
NodeGroupIds []int64 `json:"node_group_ids,omitempty"`
|
||||||
NodeGroupId int64 `json:"node_group_id"`
|
NodeGroupId int64 `json:"node_group_id"`
|
||||||
|
TrafficLimit []TrafficLimit `json:"traffic_limit"`
|
||||||
Show bool `json:"show"`
|
Show bool `json:"show"`
|
||||||
Sell bool `json:"sell"`
|
Sell bool `json:"sell"`
|
||||||
Sort int64 `json:"sort"`
|
Sort int64 `json:"sort"`
|
||||||
@ -486,6 +493,7 @@ type (
|
|||||||
}
|
}
|
||||||
UserSubscribe {
|
UserSubscribe {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
|
IdStr string `json:"id_str"`
|
||||||
UserId int64 `json:"user_id"`
|
UserId int64 `json:"user_id"`
|
||||||
OrderId int64 `json:"order_id"`
|
OrderId int64 `json:"order_id"`
|
||||||
SubscribeId int64 `json:"subscribe_id"`
|
SubscribeId int64 `json:"subscribe_id"`
|
||||||
@ -882,16 +890,20 @@ type (
|
|||||||
// ===== 分组功能类型定义 =====
|
// ===== 分组功能类型定义 =====
|
||||||
// NodeGroup 节点组
|
// NodeGroup 节点组
|
||||||
NodeGroup {
|
NodeGroup {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Sort int `json:"sort"`
|
Sort int `json:"sort"`
|
||||||
ForCalculation bool `json:"for_calculation"`
|
ForCalculation bool `json:"for_calculation"`
|
||||||
MinTrafficGB int64 `json:"min_traffic_gb,omitempty"`
|
IsExpiredGroup bool `json:"is_expired_group"`
|
||||||
MaxTrafficGB int64 `json:"max_traffic_gb,omitempty"`
|
ExpiredDaysLimit int `json:"expired_days_limit"`
|
||||||
NodeCount int64 `json:"node_count,omitempty"`
|
MaxTrafficGBExpired int64 `json:"max_traffic_gb_expired,omitempty"`
|
||||||
CreatedAt int64 `json:"created_at"`
|
SpeedLimit int `json:"speed_limit"`
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
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 分组历史记录
|
||||||
GroupHistory {
|
GroupHistory {
|
||||||
|
|||||||
@ -0,0 +1,12 @@
|
|||||||
|
-- 回滚 user_subscribe 表的过期流量字段
|
||||||
|
ALTER TABLE `user_subscribe`
|
||||||
|
DROP COLUMN `expired_upload`,
|
||||||
|
DROP COLUMN `expired_download`;
|
||||||
|
|
||||||
|
-- 回滚 node_group 表的过期节点组字段
|
||||||
|
ALTER TABLE `node_group`
|
||||||
|
DROP INDEX `idx_is_expired_group`,
|
||||||
|
DROP COLUMN `speed_limit`,
|
||||||
|
DROP COLUMN `max_traffic_gb_expired`,
|
||||||
|
DROP COLUMN `expired_days_limit`,
|
||||||
|
DROP COLUMN `is_expired_group`;
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
-- 为 node_group 表添加过期节点组相关字段
|
||||||
|
ALTER TABLE `node_group`
|
||||||
|
ADD COLUMN `is_expired_group` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Is Expired Group: 0=normal, 1=expired group' AFTER `for_calculation`,
|
||||||
|
ADD COLUMN `expired_days_limit` int NOT NULL DEFAULT 7 COMMENT 'Expired days limit (days)' AFTER `is_expired_group`,
|
||||||
|
ADD COLUMN `max_traffic_gb_expired` bigint DEFAULT 0 COMMENT 'Max traffic for expired users (GB)' AFTER `expired_days_limit`,
|
||||||
|
ADD COLUMN `speed_limit` int NOT NULL DEFAULT 0 COMMENT 'Speed limit (KB/s)' AFTER `max_traffic_gb_expired`;
|
||||||
|
|
||||||
|
-- 添加索引
|
||||||
|
ALTER TABLE `node_group` ADD INDEX `idx_is_expired_group` (`is_expired_group`);
|
||||||
|
|
||||||
|
-- 为 user_subscribe 表添加过期流量统计字段
|
||||||
|
ALTER TABLE `user_subscribe`
|
||||||
|
ADD COLUMN `expired_download` bigint NOT NULL DEFAULT 0 COMMENT 'Expired period download traffic (bytes)' AFTER `upload`,
|
||||||
|
ADD COLUMN `expired_upload` bigint NOT NULL DEFAULT 0 COMMENT 'Expired period upload traffic (bytes)' AFTER `expired_download`;
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
-- Purpose: Rollback traffic_limit rules from subscribe
|
||||||
|
-- Author: Claude Code
|
||||||
|
-- Date: 2026-03-12
|
||||||
|
|
||||||
|
-- ===== Remove traffic_limit column from subscribe table =====
|
||||||
|
ALTER TABLE `subscribe` DROP COLUMN IF EXISTS `traffic_limit`;
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
-- Purpose: Add traffic_limit rules to subscribe
|
||||||
|
-- Author: Claude Code
|
||||||
|
-- Date: 2026-03-12
|
||||||
|
|
||||||
|
-- ===== Add traffic_limit column to subscribe table =====
|
||||||
|
SET @column_exists = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'subscribe'
|
||||||
|
AND COLUMN_NAME = 'traffic_limit'
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @sql = IF(
|
||||||
|
@column_exists = 0,
|
||||||
|
'ALTER TABLE `subscribe` ADD COLUMN `traffic_limit` TEXT NULL COMMENT ''Traffic Limit Rules (JSON)'' AFTER `node_group_id`',
|
||||||
|
'SELECT ''Column traffic_limit already exists in subscribe table'''
|
||||||
|
);
|
||||||
|
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
26
internal/handler/public/user/getUserTrafficStatsHandler.go
Normal file
26
internal/handler/public/user/getUserTrafficStatsHandler.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/perfect-panel/server/internal/logic/public/user"
|
||||||
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
|
"github.com/perfect-panel/server/internal/types"
|
||||||
|
"github.com/perfect-panel/server/pkg/result"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get User Traffic Statistics
|
||||||
|
func GetUserTrafficStatsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
var req types.GetUserTrafficStatsRequest
|
||||||
|
_ = c.ShouldBind(&req)
|
||||||
|
validateErr := svcCtx.Validate(&req)
|
||||||
|
if validateErr != nil {
|
||||||
|
result.ParamErrorResult(c, validateErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := user.NewGetUserTrafficStatsLogic(c.Request.Context(), svcCtx)
|
||||||
|
resp, err := l.GetUserTrafficStats(&req)
|
||||||
|
result.HttpResult(c, resp, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -955,6 +955,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
// Reset User Subscribe Token
|
// Reset User Subscribe Token
|
||||||
publicUserGroupRouter.PUT("/subscribe_token", publicUser.ResetUserSubscribeTokenHandler(serverCtx))
|
publicUserGroupRouter.PUT("/subscribe_token", publicUser.ResetUserSubscribeTokenHandler(serverCtx))
|
||||||
|
|
||||||
|
// Get User Traffic Statistics
|
||||||
|
publicUserGroupRouter.GET("/traffic_stats", publicUser.GetUserTrafficStatsHandler(serverCtx))
|
||||||
|
|
||||||
// Unbind Device
|
// Unbind Device
|
||||||
publicUserGroupRouter.PUT("/unbind_device", publicUser.UnbindDeviceHandler(serverCtx))
|
publicUserGroupRouter.PUT("/unbind_device", publicUser.UnbindDeviceHandler(serverCtx))
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package group
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/model/group"
|
"github.com/perfect-panel/server/internal/model/group"
|
||||||
@ -25,17 +26,51 @@ func NewCreateNodeGroupLogic(ctx context.Context, svcCtx *svc.ServiceContext) *C
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *CreateNodeGroupLogic) CreateNodeGroup(req *types.CreateNodeGroupRequest) error {
|
func (l *CreateNodeGroupLogic) CreateNodeGroup(req *types.CreateNodeGroupRequest) error {
|
||||||
|
// 验证:系统中只能有一个过期节点组
|
||||||
|
if req.IsExpiredGroup != nil && *req.IsExpiredGroup {
|
||||||
|
var count int64
|
||||||
|
err := l.svcCtx.DB.Model(&group.NodeGroup{}).
|
||||||
|
Where("is_expired_group = ?", true).
|
||||||
|
Count(&count).Error
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("failed to check expired group count: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
return errors.New("system already has an expired node group, cannot create multiple")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 创建节点组
|
// 创建节点组
|
||||||
nodeGroup := &group.NodeGroup{
|
nodeGroup := &group.NodeGroup{
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
Description: req.Description,
|
Description: req.Description,
|
||||||
Sort: req.Sort,
|
Sort: req.Sort,
|
||||||
ForCalculation: req.ForCalculation,
|
ForCalculation: req.ForCalculation,
|
||||||
MinTrafficGB: req.MinTrafficGB,
|
IsExpiredGroup: req.IsExpiredGroup,
|
||||||
MaxTrafficGB: req.MaxTrafficGB,
|
MaxTrafficGBExpired: req.MaxTrafficGBExpired,
|
||||||
CreatedAt: time.Now(),
|
MinTrafficGB: req.MinTrafficGB,
|
||||||
UpdatedAt: time.Now(),
|
MaxTrafficGB: req.MaxTrafficGB,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置过期节点组的默认值
|
||||||
|
if req.IsExpiredGroup != nil && *req.IsExpiredGroup {
|
||||||
|
// 过期节点组不参与分组计算
|
||||||
|
falseValue := false
|
||||||
|
nodeGroup.ForCalculation = &falseValue
|
||||||
|
|
||||||
|
if req.ExpiredDaysLimit != nil {
|
||||||
|
nodeGroup.ExpiredDaysLimit = *req.ExpiredDaysLimit
|
||||||
|
} else {
|
||||||
|
nodeGroup.ExpiredDaysLimit = 7 // 默认7天
|
||||||
|
}
|
||||||
|
if req.SpeedLimit != nil {
|
||||||
|
nodeGroup.SpeedLimit = *req.SpeedLimit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := l.svcCtx.DB.Create(nodeGroup).Error; err != nil {
|
if err := l.svcCtx.DB.Create(nodeGroup).Error; err != nil {
|
||||||
logger.Errorf("failed to create node group: %v", err)
|
logger.Errorf("failed to create node group: %v", err)
|
||||||
return err
|
return err
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/model/group"
|
"github.com/perfect-panel/server/internal/model/group"
|
||||||
|
"github.com/perfect-panel/server/internal/model/node"
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
"github.com/perfect-panel/server/internal/types"
|
"github.com/perfect-panel/server/internal/types"
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
"github.com/perfect-panel/server/pkg/logger"
|
||||||
@ -37,9 +38,9 @@ func (l *DeleteNodeGroupLogic) DeleteNodeGroup(req *types.DeleteNodeGroupRequest
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否有关联节点
|
// 检查是否有关联节点(使用JSON_CONTAINS查询node_group_ids数组)
|
||||||
var nodeCount int64
|
var nodeCount int64
|
||||||
if err := l.svcCtx.DB.Table("nodes").Where("node_group_id = ?", nodeGroup.Id).Count(&nodeCount).Error; err != nil {
|
if err := l.svcCtx.DB.Model(&node.Node{}).Where("JSON_CONTAINS(node_group_ids, ?)", fmt.Sprintf("[%d]", nodeGroup.Id)).Count(&nodeCount).Error; err != nil {
|
||||||
logger.Errorf("failed to count nodes in group: %v", err)
|
logger.Errorf("failed to count nodes in group: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/model/group"
|
"github.com/perfect-panel/server/internal/model/group"
|
||||||
|
"github.com/perfect-panel/server/internal/model/user"
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
"github.com/perfect-panel/server/internal/types"
|
"github.com/perfect-panel/server/internal/types"
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
"github.com/perfect-panel/server/pkg/logger"
|
||||||
@ -77,7 +78,7 @@ func (l *ExportGroupResultLogic) ExportGroupResult(req *types.ExportGroupResultR
|
|||||||
NodeGroupId int64 `json:"node_group_id"`
|
NodeGroupId int64 `json:"node_group_id"`
|
||||||
}
|
}
|
||||||
var userSubscribes []UserNodeGroupInfo
|
var userSubscribes []UserNodeGroupInfo
|
||||||
if err := l.svcCtx.DB.Table("user_subscribe").
|
if err := l.svcCtx.DB.Model(&user.Subscribe{}).
|
||||||
Select("DISTINCT user_id as id, node_group_id").
|
Select("DISTINCT user_id as id, node_group_id").
|
||||||
Where("node_group_id > ?", 0).
|
Where("node_group_id > ?", 0).
|
||||||
Find(&userSubscribes).Error; err != nil {
|
Find(&userSubscribes).Error; err != nil {
|
||||||
|
|||||||
@ -2,8 +2,10 @@ package group
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/model/group"
|
"github.com/perfect-panel/server/internal/model/group"
|
||||||
|
"github.com/perfect-panel/server/internal/model/node"
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
"github.com/perfect-panel/server/internal/types"
|
"github.com/perfect-panel/server/internal/types"
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
"github.com/perfect-panel/server/pkg/logger"
|
||||||
@ -46,9 +48,9 @@ func (l *GetNodeGroupListLogic) GetNodeGroupList(req *types.GetNodeGroupListRequ
|
|||||||
// 转换为响应格式
|
// 转换为响应格式
|
||||||
var list []types.NodeGroup
|
var list []types.NodeGroup
|
||||||
for _, ng := range nodeGroups {
|
for _, ng := range nodeGroups {
|
||||||
// 统计该组的节点数
|
// 统计该组的节点数(JSON数组查询)
|
||||||
var nodeCount int64
|
var nodeCount int64
|
||||||
l.svcCtx.DB.Table("nodes").Where("node_group_id = ?", ng.Id).Count(&nodeCount)
|
l.svcCtx.DB.Model(&node.Node{}).Where("JSON_CONTAINS(node_group_ids, ?)", fmt.Sprintf("[%d]", ng.Id)).Count(&nodeCount)
|
||||||
|
|
||||||
// 处理指针类型的字段
|
// 处理指针类型的字段
|
||||||
var forCalculation bool
|
var forCalculation bool
|
||||||
@ -58,25 +60,37 @@ func (l *GetNodeGroupListLogic) GetNodeGroupList(req *types.GetNodeGroupListRequ
|
|||||||
forCalculation = true // 默认值
|
forCalculation = true // 默认值
|
||||||
}
|
}
|
||||||
|
|
||||||
var minTrafficGB, maxTrafficGB int64
|
var isExpiredGroup bool
|
||||||
|
if ng.IsExpiredGroup != nil {
|
||||||
|
isExpiredGroup = *ng.IsExpiredGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
var minTrafficGB, maxTrafficGB, maxTrafficGBExpired int64
|
||||||
if ng.MinTrafficGB != nil {
|
if ng.MinTrafficGB != nil {
|
||||||
minTrafficGB = *ng.MinTrafficGB
|
minTrafficGB = *ng.MinTrafficGB
|
||||||
}
|
}
|
||||||
if ng.MaxTrafficGB != nil {
|
if ng.MaxTrafficGB != nil {
|
||||||
maxTrafficGB = *ng.MaxTrafficGB
|
maxTrafficGB = *ng.MaxTrafficGB
|
||||||
}
|
}
|
||||||
|
if ng.MaxTrafficGBExpired != nil {
|
||||||
|
maxTrafficGBExpired = *ng.MaxTrafficGBExpired
|
||||||
|
}
|
||||||
|
|
||||||
list = append(list, types.NodeGroup{
|
list = append(list, types.NodeGroup{
|
||||||
Id: ng.Id,
|
Id: ng.Id,
|
||||||
Name: ng.Name,
|
Name: ng.Name,
|
||||||
Description: ng.Description,
|
Description: ng.Description,
|
||||||
Sort: ng.Sort,
|
Sort: ng.Sort,
|
||||||
ForCalculation: forCalculation,
|
ForCalculation: forCalculation,
|
||||||
MinTrafficGB: minTrafficGB,
|
IsExpiredGroup: isExpiredGroup,
|
||||||
MaxTrafficGB: maxTrafficGB,
|
ExpiredDaysLimit: ng.ExpiredDaysLimit,
|
||||||
NodeCount: nodeCount,
|
MaxTrafficGBExpired: maxTrafficGBExpired,
|
||||||
CreatedAt: ng.CreatedAt.Unix(),
|
SpeedLimit: ng.SpeedLimit,
|
||||||
UpdatedAt: ng.UpdatedAt.Unix(),
|
MinTrafficGB: minTrafficGB,
|
||||||
|
MaxTrafficGB: maxTrafficGB,
|
||||||
|
NodeCount: nodeCount,
|
||||||
|
CreatedAt: ng.CreatedAt.Unix(),
|
||||||
|
UpdatedAt: ng.UpdatedAt.Unix(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -28,14 +28,14 @@ func NewGetSubscribeGroupMappingLogic(ctx context.Context, svcCtx *svc.ServiceCo
|
|||||||
func (l *GetSubscribeGroupMappingLogic) GetSubscribeGroupMapping(req *types.GetSubscribeGroupMappingRequest) (resp *types.GetSubscribeGroupMappingResponse, err error) {
|
func (l *GetSubscribeGroupMappingLogic) GetSubscribeGroupMapping(req *types.GetSubscribeGroupMappingRequest) (resp *types.GetSubscribeGroupMappingResponse, err error) {
|
||||||
// 1. 查询所有订阅套餐
|
// 1. 查询所有订阅套餐
|
||||||
var subscribes []subscribe.Subscribe
|
var subscribes []subscribe.Subscribe
|
||||||
if err := l.svcCtx.DB.Table("subscribe").Find(&subscribes).Error; err != nil {
|
if err := l.svcCtx.DB.Model(&subscribe.Subscribe{}).Find(&subscribes).Error; err != nil {
|
||||||
l.Errorw("[GetSubscribeGroupMapping] failed to query subscribes", logger.Field("error", err.Error()))
|
l.Errorw("[GetSubscribeGroupMapping] failed to query subscribes", logger.Field("error", err.Error()))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 查询所有节点组
|
// 2. 查询所有节点组
|
||||||
var nodeGroups []group.NodeGroup
|
var nodeGroups []group.NodeGroup
|
||||||
if err := l.svcCtx.DB.Table("node_group").Find(&nodeGroups).Error; err != nil {
|
if err := l.svcCtx.DB.Model(&group.NodeGroup{}).Find(&nodeGroups).Error; err != nil {
|
||||||
l.Errorw("[GetSubscribeGroupMapping] failed to query node groups", logger.Field("error", err.Error()))
|
l.Errorw("[GetSubscribeGroupMapping] failed to query node groups", logger.Field("error", err.Error()))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/perfect-panel/server/internal/model/group"
|
||||||
"github.com/perfect-panel/server/internal/model/node"
|
"github.com/perfect-panel/server/internal/model/node"
|
||||||
|
"github.com/perfect-panel/server/internal/model/subscribe"
|
||||||
|
"github.com/perfect-panel/server/internal/model/user"
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
"github.com/perfect-panel/server/internal/types"
|
"github.com/perfect-panel/server/internal/types"
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
"github.com/perfect-panel/server/pkg/logger"
|
||||||
@ -38,7 +41,7 @@ func (l *PreviewUserNodesLogic) PreviewUserNodes(req *types.PreviewUserNodesRequ
|
|||||||
NodeGroupId int64 // 用户订阅的 node_group_id(单个ID)
|
NodeGroupId int64 // 用户订阅的 node_group_id(单个ID)
|
||||||
}
|
}
|
||||||
var userSubscribes []UserSubscribe
|
var userSubscribes []UserSubscribe
|
||||||
err = l.svcCtx.DB.Table("user_subscribe").
|
err = l.svcCtx.DB.Model(&user.Subscribe{}).
|
||||||
Select("id, user_id, subscribe_id, node_group_id").
|
Select("id, user_id, subscribe_id, node_group_id").
|
||||||
Where("user_id = ? AND status IN ?", req.UserId, []int8{0, 1}).
|
Where("user_id = ? AND status IN ?", req.UserId, []int8{0, 1}).
|
||||||
Find(&userSubscribes).Error
|
Find(&userSubscribes).Error
|
||||||
@ -74,7 +77,7 @@ func (l *PreviewUserNodesLogic) PreviewUserNodes(req *types.PreviewUserNodesRequ
|
|||||||
NodeTags string // 节点标签
|
NodeTags string // 节点标签
|
||||||
}
|
}
|
||||||
var subscribeInfos []SubscribeInfo
|
var subscribeInfos []SubscribeInfo
|
||||||
err = l.svcCtx.DB.Table("subscribe").
|
err = l.svcCtx.DB.Model(&subscribe.Subscribe{}).
|
||||||
Select("id, node_group_id, node_group_ids, nodes, node_tags").
|
Select("id, node_group_id, node_group_ids, nodes, node_tags").
|
||||||
Where("id IN ?", subscribeIds).
|
Where("id IN ?", subscribeIds).
|
||||||
Find(&subscribeInfos).Error
|
Find(&subscribeInfos).Error
|
||||||
@ -149,15 +152,23 @@ func (l *PreviewUserNodesLogic) PreviewUserNodes(req *types.PreviewUserNodesRequ
|
|||||||
logger.Infof("[PreviewUserNodes] collected direct node_ids: %v", allDirectNodeIds)
|
logger.Infof("[PreviewUserNodes] collected direct node_ids: %v", allDirectNodeIds)
|
||||||
|
|
||||||
// 4. 判断分组功能是否启用
|
// 4. 判断分组功能是否启用
|
||||||
var groupEnabled string
|
type SystemConfig struct {
|
||||||
l.svcCtx.DB.Table("system").
|
Value string
|
||||||
|
}
|
||||||
|
var config SystemConfig
|
||||||
|
l.svcCtx.DB.Model(&struct {
|
||||||
|
Category string `gorm:"column:category"`
|
||||||
|
Key string `gorm:"column:key"`
|
||||||
|
Value string `gorm:"column:value"`
|
||||||
|
}{}).
|
||||||
|
Table("system").
|
||||||
Where("`category` = ? AND `key` = ?", "group", "enabled").
|
Where("`category` = ? AND `key` = ?", "group", "enabled").
|
||||||
Select("value").
|
Select("value").
|
||||||
Scan(&groupEnabled)
|
Scan(&config)
|
||||||
|
|
||||||
logger.Infof("[PreviewUserNodes] groupEnabled: %v", groupEnabled)
|
logger.Infof("[PreviewUserNodes] groupEnabled: %v", config.Value)
|
||||||
|
|
||||||
isGroupEnabled := groupEnabled == "true" || groupEnabled == "1"
|
isGroupEnabled := config.Value == "true" || config.Value == "1"
|
||||||
|
|
||||||
var filteredNodes []node.Node
|
var filteredNodes []node.Node
|
||||||
|
|
||||||
@ -177,7 +188,7 @@ func (l *PreviewUserNodesLogic) PreviewUserNodes(req *types.PreviewUserNodesRequ
|
|||||||
// 5. 查询所有启用的节点(只有当有节点组时才查询)
|
// 5. 查询所有启用的节点(只有当有节点组时才查询)
|
||||||
if len(allNodeGroupIds) > 0 {
|
if len(allNodeGroupIds) > 0 {
|
||||||
var dbNodes []node.Node
|
var dbNodes []node.Node
|
||||||
err = l.svcCtx.DB.Table("nodes").
|
err = l.svcCtx.DB.Model(&node.Node{}).
|
||||||
Where("enabled = ?", true).
|
Where("enabled = ?", true).
|
||||||
Find(&dbNodes).Error
|
Find(&dbNodes).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -238,7 +249,7 @@ func (l *PreviewUserNodesLogic) PreviewUserNodes(req *types.PreviewUserNodesRequ
|
|||||||
// 8. 查询所有启用的节点(只有当有 tags 时才查询)
|
// 8. 查询所有启用的节点(只有当有 tags 时才查询)
|
||||||
if len(allTags) > 0 {
|
if len(allTags) > 0 {
|
||||||
var dbNodes []node.Node
|
var dbNodes []node.Node
|
||||||
err = l.svcCtx.DB.Table("nodes").
|
err = l.svcCtx.DB.Model(&node.Node{}).
|
||||||
Where("enabled = ?", true).
|
Where("enabled = ?", true).
|
||||||
Find(&dbNodes).Error
|
Find(&dbNodes).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -370,7 +381,7 @@ func (l *PreviewUserNodesLogic) PreviewUserNodes(req *types.PreviewUserNodesRequ
|
|||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
var nodeGroupInfos []NodeGroupInfo
|
var nodeGroupInfos []NodeGroupInfo
|
||||||
err = l.svcCtx.DB.Table("node_group").
|
err = l.svcCtx.DB.Model(&group.NodeGroup{}).
|
||||||
Select("id, name").
|
Select("id, name").
|
||||||
Where("id IN ?", allGroupIds).
|
Where("id IN ?", allGroupIds).
|
||||||
Find(&nodeGroupInfos).Error
|
Find(&nodeGroupInfos).Error
|
||||||
@ -508,7 +519,7 @@ func (l *PreviewUserNodesLogic) PreviewUserNodes(req *types.PreviewUserNodesRequ
|
|||||||
if len(allDirectNodeIds) > 0 {
|
if len(allDirectNodeIds) > 0 {
|
||||||
// 查询直接分配的节点详情
|
// 查询直接分配的节点详情
|
||||||
var directNodes []node.Node
|
var directNodes []node.Node
|
||||||
err = l.svcCtx.DB.Table("nodes").
|
err = l.svcCtx.DB.Model(&node.Node{}).
|
||||||
Where("id IN ? AND enabled = ?", allDirectNodeIds, true).
|
Where("id IN ? AND enabled = ?", allDirectNodeIds, true).
|
||||||
Find(&directNodes).Error
|
Find(&directNodes).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -3,9 +3,13 @@ package group
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/model/group"
|
"github.com/perfect-panel/server/internal/model/group"
|
||||||
|
"github.com/perfect-panel/server/internal/model/node"
|
||||||
|
"github.com/perfect-panel/server/internal/model/subscribe"
|
||||||
|
"github.com/perfect-panel/server/internal/model/user"
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
"github.com/perfect-panel/server/internal/types"
|
"github.com/perfect-panel/server/internal/types"
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
"github.com/perfect-panel/server/pkg/logger"
|
||||||
@ -131,7 +135,7 @@ func (l *RecalculateGroupLogic) getUserEmail(tx *gorm.DB, userId int64) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var authMethod UserAuthMethod
|
var authMethod UserAuthMethod
|
||||||
if err := tx.Table("user_auth_methods").
|
if err := tx.Model(&user.AuthMethods{}).
|
||||||
Select("auth_identifier").
|
Select("auth_identifier").
|
||||||
Where("user_id = ? AND (auth_type = ? OR auth_type = ?)", userId, "email", "6").
|
Where("user_id = ? AND (auth_type = ? OR auth_type = ?)", userId, "email", "6").
|
||||||
First(&authMethod).Error; err != nil {
|
First(&authMethod).Error; err != nil {
|
||||||
@ -152,7 +156,7 @@ func (l *RecalculateGroupLogic) executeAverageGrouping(tx *gorm.DB, historyId in
|
|||||||
}
|
}
|
||||||
|
|
||||||
var userSubscribes []UserSubscribeInfo
|
var userSubscribes []UserSubscribeInfo
|
||||||
if err := tx.Table("user_subscribe").
|
if err := tx.Model(&user.Subscribe{}).
|
||||||
Select("id, user_id, subscribe_id").
|
Select("id, user_id, subscribe_id").
|
||||||
Where("group_locked = ? AND status IN (0, 1)", 0). // 只查询未锁定且有效的用户订阅
|
Where("group_locked = ? AND status IN (0, 1)", 0). // 只查询未锁定且有效的用户订阅
|
||||||
Scan(&userSubscribes).Error; err != nil {
|
Scan(&userSubscribes).Error; err != nil {
|
||||||
@ -168,7 +172,7 @@ func (l *RecalculateGroupLogic) executeAverageGrouping(tx *gorm.DB, historyId in
|
|||||||
|
|
||||||
// 1.5 查询所有参与计算的节点组ID
|
// 1.5 查询所有参与计算的节点组ID
|
||||||
var calculationNodeGroups []group.NodeGroup
|
var calculationNodeGroups []group.NodeGroup
|
||||||
if err := tx.Table("node_group").
|
if err := tx.Model(&group.NodeGroup{}).
|
||||||
Select("id").
|
Select("id").
|
||||||
Where("for_calculation = ?", true).
|
Where("for_calculation = ?", true).
|
||||||
Scan(&calculationNodeGroups).Error; err != nil {
|
Scan(&calculationNodeGroups).Error; err != nil {
|
||||||
@ -195,7 +199,7 @@ func (l *RecalculateGroupLogic) executeAverageGrouping(tx *gorm.DB, historyId in
|
|||||||
NodeGroupIds string `json:"node_group_ids"` // JSON string
|
NodeGroupIds string `json:"node_group_ids"` // JSON string
|
||||||
}
|
}
|
||||||
var subscribeInfos []SubscribeInfo
|
var subscribeInfos []SubscribeInfo
|
||||||
if err := tx.Table("subscribe").
|
if err := tx.Model(&subscribe.Subscribe{}).
|
||||||
Select("id, node_group_ids").
|
Select("id, node_group_ids").
|
||||||
Where("id IN ?", subscribeIds).
|
Where("id IN ?", subscribeIds).
|
||||||
Find(&subscribeInfos).Error; err != nil {
|
Find(&subscribeInfos).Error; err != nil {
|
||||||
@ -261,10 +265,10 @@ func (l *RecalculateGroupLogic) executeAverageGrouping(tx *gorm.DB, historyId in
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有节点组ID,跳过
|
// 如果没有节点组ID,跳过
|
||||||
if len(nodeGroupIds) == 0 {
|
if len(nodeGroupIds) == 0 {
|
||||||
l.Debugf("no valid node_group_ids for subscribe_id=%d, setting to 0", subInfo.Id)
|
l.Debugf("no valid node_group_ids for subscribe_id=%d, setting to 0", subInfo.Id)
|
||||||
if err := tx.Table("user_subscribe").
|
if err := tx.Model(&user.Subscribe{}).
|
||||||
Where("id = ?", us.Id).
|
Where("id = ?", us.Id).
|
||||||
Update("node_group_id", 0).Error; err != nil {
|
Update("node_group_id", 0).Error; err != nil {
|
||||||
l.Errorw("failed to update user_subscribe node_group_id",
|
l.Errorw("failed to update user_subscribe node_group_id",
|
||||||
@ -290,7 +294,7 @@ func (l *RecalculateGroupLogic) executeAverageGrouping(tx *gorm.DB, historyId in
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 更新 user_subscribe 的 node_group_id 字段(单个ID)
|
// 更新 user_subscribe 的 node_group_id 字段(单个ID)
|
||||||
if err := tx.Table("user_subscribe").
|
if err := tx.Model(&user.Subscribe{}).
|
||||||
Where("id = ?", us.Id).
|
Where("id = ?", us.Id).
|
||||||
Update("node_group_id", selectedNodeGroupId).Error; err != nil {
|
Update("node_group_id", selectedNodeGroupId).Error; err != nil {
|
||||||
l.Errorw("failed to update user_subscribe node_group_id",
|
l.Errorw("failed to update user_subscribe node_group_id",
|
||||||
@ -329,8 +333,8 @@ func (l *RecalculateGroupLogic) executeAverageGrouping(tx *gorm.DB, historyId in
|
|||||||
// 统计该节点组的节点数
|
// 统计该节点组的节点数
|
||||||
var nodeCount int64 = 0
|
var nodeCount int64 = 0
|
||||||
if nodeGroupId > 0 {
|
if nodeGroupId > 0 {
|
||||||
if err := tx.Table("nodes").
|
if err := tx.Model(&node.Node{}).
|
||||||
Where("JSON_CONTAINS(node_group_ids, ?)", nodeGroupId).
|
Where("JSON_CONTAINS(node_group_ids, ?)", fmt.Sprintf("[%d]", nodeGroupId)).
|
||||||
Count(&nodeCount).Error; err != nil {
|
Count(&nodeCount).Error; err != nil {
|
||||||
l.Errorw("failed to count nodes",
|
l.Errorw("failed to count nodes",
|
||||||
logger.Field("node_group_id", nodeGroupId),
|
logger.Field("node_group_id", nodeGroupId),
|
||||||
@ -383,7 +387,7 @@ func (l *RecalculateGroupLogic) executeSubscribeGrouping(tx *gorm.DB, historyId
|
|||||||
}
|
}
|
||||||
|
|
||||||
var userSubscribes []UserSubscribeInfo
|
var userSubscribes []UserSubscribeInfo
|
||||||
if err := tx.Table("user_subscribe").
|
if err := tx.Model(&user.Subscribe{}).
|
||||||
Select("id, user_id, subscribe_id").
|
Select("id, user_id, subscribe_id").
|
||||||
Where("group_locked = ? AND status IN (0, 1)", 0).
|
Where("group_locked = ? AND status IN (0, 1)", 0).
|
||||||
Scan(&userSubscribes).Error; err != nil {
|
Scan(&userSubscribes).Error; err != nil {
|
||||||
@ -400,7 +404,7 @@ func (l *RecalculateGroupLogic) executeSubscribeGrouping(tx *gorm.DB, historyId
|
|||||||
|
|
||||||
// 1.5 查询所有参与计算的节点组ID
|
// 1.5 查询所有参与计算的节点组ID
|
||||||
var calculationNodeGroups []group.NodeGroup
|
var calculationNodeGroups []group.NodeGroup
|
||||||
if err := tx.Table("node_group").
|
if err := tx.Model(&group.NodeGroup{}).
|
||||||
Select("id").
|
Select("id").
|
||||||
Where("for_calculation = ?", true).
|
Where("for_calculation = ?", true).
|
||||||
Scan(&calculationNodeGroups).Error; err != nil {
|
Scan(&calculationNodeGroups).Error; err != nil {
|
||||||
@ -427,7 +431,7 @@ func (l *RecalculateGroupLogic) executeSubscribeGrouping(tx *gorm.DB, historyId
|
|||||||
NodeGroupIds string `json:"node_group_ids"` // JSON string
|
NodeGroupIds string `json:"node_group_ids"` // JSON string
|
||||||
}
|
}
|
||||||
var subscribeInfos []SubscribeInfo
|
var subscribeInfos []SubscribeInfo
|
||||||
if err := tx.Table("subscribe").
|
if err := tx.Model(&subscribe.Subscribe{}).
|
||||||
Select("id, node_group_ids").
|
Select("id, node_group_ids").
|
||||||
Where("id IN ?", subscribeIds).
|
Where("id IN ?", subscribeIds).
|
||||||
Find(&subscribeInfos).Error; err != nil {
|
Find(&subscribeInfos).Error; err != nil {
|
||||||
@ -501,7 +505,7 @@ func (l *RecalculateGroupLogic) executeSubscribeGrouping(tx *gorm.DB, historyId
|
|||||||
us.Id, us.SubscribeId, selectedNodeGroupId, len(nodeGroupIds))
|
us.Id, us.SubscribeId, selectedNodeGroupId, len(nodeGroupIds))
|
||||||
|
|
||||||
// 更新 user_subscribe 的 node_group_id 字段
|
// 更新 user_subscribe 的 node_group_id 字段
|
||||||
if err := tx.Table("user_subscribe").
|
if err := tx.Model(&user.Subscribe{}).
|
||||||
Where("id = ?", us.Id).
|
Where("id = ?", us.Id).
|
||||||
Update("node_group_id", selectedNodeGroupId).Error; err != nil {
|
Update("node_group_id", selectedNodeGroupId).Error; err != nil {
|
||||||
l.Errorw("failed to update user_subscribe node_group_id",
|
l.Errorw("failed to update user_subscribe node_group_id",
|
||||||
@ -548,7 +552,7 @@ func (l *RecalculateGroupLogic) executeSubscribeGrouping(tx *gorm.DB, historyId
|
|||||||
expiredAffectedCount := 0
|
expiredAffectedCount := 0
|
||||||
for _, eu := range expiredUserSubscribes {
|
for _, eu := range expiredUserSubscribes {
|
||||||
// 更新 user_subscribe 表的 node_group_id 字段到 0
|
// 更新 user_subscribe 表的 node_group_id 字段到 0
|
||||||
if err := tx.Table("user_subscribe").
|
if err := tx.Model(&user.Subscribe{}).
|
||||||
Where("id = ?", eu.Id).
|
Where("id = ?", eu.Id).
|
||||||
Update("node_group_id", 0).Error; err != nil {
|
Update("node_group_id", 0).Error; err != nil {
|
||||||
l.Errorw("failed to update expired user subscribe node_group_id",
|
l.Errorw("failed to update expired user subscribe node_group_id",
|
||||||
@ -573,8 +577,8 @@ func (l *RecalculateGroupLogic) executeSubscribeGrouping(tx *gorm.DB, historyId
|
|||||||
// 统计该节点组的节点数
|
// 统计该节点组的节点数
|
||||||
var nodeCount int64 = 0
|
var nodeCount int64 = 0
|
||||||
if nodeGroupId > 0 {
|
if nodeGroupId > 0 {
|
||||||
if err := tx.Table("nodes").
|
if err := tx.Model(&node.Node{}).
|
||||||
Where("JSON_CONTAINS(node_group_ids, ?)", nodeGroupId).
|
Where("JSON_CONTAINS(node_group_ids, ?)", fmt.Sprintf("[%d]", nodeGroupId)).
|
||||||
Count(&nodeCount).Error; err != nil {
|
Count(&nodeCount).Error; err != nil {
|
||||||
l.Errorw("failed to count nodes",
|
l.Errorw("failed to count nodes",
|
||||||
logger.Field("node_group_id", nodeGroupId),
|
logger.Field("node_group_id", nodeGroupId),
|
||||||
@ -652,7 +656,7 @@ func (l *RecalculateGroupLogic) executeTrafficGrouping(tx *gorm.DB, historyId in
|
|||||||
}
|
}
|
||||||
|
|
||||||
var userSubscribes []UserSubscribeInfo
|
var userSubscribes []UserSubscribeInfo
|
||||||
if err := tx.Table("user_subscribe").
|
if err := tx.Model(&user.Subscribe{}).
|
||||||
Select("id, user_id, upload, download, (upload + download) as used_traffic").
|
Select("id, user_id, upload, download, (upload + download) as used_traffic").
|
||||||
Where("group_locked = ? AND status IN (0, 1)", 0). // 只查询有效且未锁定的用户订阅
|
Where("group_locked = ? AND status IN (0, 1)", 0). // 只查询有效且未锁定的用户订阅
|
||||||
Scan(&userSubscribes).Error; err != nil {
|
Scan(&userSubscribes).Error; err != nil {
|
||||||
@ -694,7 +698,7 @@ func (l *RecalculateGroupLogic) executeTrafficGrouping(tx *gorm.DB, historyId in
|
|||||||
// 如果没有匹配到任何范围,targetNodeGroupId 保持为 0(不分配节点组)
|
// 如果没有匹配到任何范围,targetNodeGroupId 保持为 0(不分配节点组)
|
||||||
|
|
||||||
// 更新 user_subscribe 的 node_group_id 字段
|
// 更新 user_subscribe 的 node_group_id 字段
|
||||||
if err := tx.Table("user_subscribe").
|
if err := tx.Model(&user.Subscribe{}).
|
||||||
Where("id = ?", us.Id).
|
Where("id = ?", us.Id).
|
||||||
Update("node_group_id", targetNodeGroupId).Error; err != nil {
|
Update("node_group_id", targetNodeGroupId).Error; err != nil {
|
||||||
l.Errorw("failed to update user subscribe node_group_id",
|
l.Errorw("failed to update user subscribe node_group_id",
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/model/group"
|
"github.com/perfect-panel/server/internal/model/group"
|
||||||
|
"github.com/perfect-panel/server/internal/model/subscribe"
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
"github.com/perfect-panel/server/internal/types"
|
"github.com/perfect-panel/server/internal/types"
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
"github.com/perfect-panel/server/pkg/logger"
|
||||||
@ -37,6 +38,34 @@ func (l *UpdateNodeGroupLogic) UpdateNodeGroup(req *types.UpdateNodeGroupRequest
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证:系统中只能有一个过期节点组
|
||||||
|
if req.IsExpiredGroup != nil && *req.IsExpiredGroup {
|
||||||
|
var count int64
|
||||||
|
err := l.svcCtx.DB.Model(&group.NodeGroup{}).
|
||||||
|
Where("is_expired_group = ? AND id != ?", true, req.Id).
|
||||||
|
Count(&count).Error
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("failed to check expired group count: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
return errors.New("system already has an expired node group, cannot create multiple")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证:被订阅商品设置为默认节点组的不能设置为过期节点组
|
||||||
|
var subscribeCount int64
|
||||||
|
err = l.svcCtx.DB.Model(&subscribe.Subscribe{}).
|
||||||
|
Where("node_group_id = ?", req.Id).
|
||||||
|
Count(&subscribeCount).Error
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("failed to check subscribe usage: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if subscribeCount > 0 {
|
||||||
|
return errors.New("this node group is used as default node group in subscription products, cannot set as expired group")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 构建更新数据
|
// 构建更新数据
|
||||||
updates := map[string]interface{}{
|
updates := map[string]interface{}{
|
||||||
"updated_at": time.Now(),
|
"updated_at": time.Now(),
|
||||||
@ -53,6 +82,22 @@ func (l *UpdateNodeGroupLogic) UpdateNodeGroup(req *types.UpdateNodeGroupRequest
|
|||||||
if req.ForCalculation != nil {
|
if req.ForCalculation != nil {
|
||||||
updates["for_calculation"] = *req.ForCalculation
|
updates["for_calculation"] = *req.ForCalculation
|
||||||
}
|
}
|
||||||
|
if req.IsExpiredGroup != nil {
|
||||||
|
updates["is_expired_group"] = *req.IsExpiredGroup
|
||||||
|
// 过期节点组不参与分组计算
|
||||||
|
if *req.IsExpiredGroup {
|
||||||
|
updates["for_calculation"] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.ExpiredDaysLimit != nil {
|
||||||
|
updates["expired_days_limit"] = *req.ExpiredDaysLimit
|
||||||
|
}
|
||||||
|
if req.MaxTrafficGBExpired != nil {
|
||||||
|
updates["max_traffic_gb_expired"] = *req.MaxTrafficGBExpired
|
||||||
|
}
|
||||||
|
if req.SpeedLimit != nil {
|
||||||
|
updates["speed_limit"] = *req.SpeedLimit
|
||||||
|
}
|
||||||
|
|
||||||
// 获取新的流量区间值
|
// 获取新的流量区间值
|
||||||
newMinTraffic := nodeGroup.MinTrafficGB
|
newMinTraffic := nodeGroup.MinTrafficGB
|
||||||
|
|||||||
@ -34,6 +34,12 @@ func (l *CreateSubscribeLogic) CreateSubscribe(req *types.CreateSubscribeRequest
|
|||||||
val, _ := json.Marshal(req.Discount)
|
val, _ := json.Marshal(req.Discount)
|
||||||
discount = string(val)
|
discount = string(val)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trafficLimit := ""
|
||||||
|
if len(req.TrafficLimit) > 0 {
|
||||||
|
val, _ := json.Marshal(req.TrafficLimit)
|
||||||
|
trafficLimit = string(val)
|
||||||
|
}
|
||||||
sub := &subscribe.Subscribe{
|
sub := &subscribe.Subscribe{
|
||||||
Id: 0,
|
Id: 0,
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
@ -52,6 +58,7 @@ func (l *CreateSubscribeLogic) CreateSubscribe(req *types.CreateSubscribeRequest
|
|||||||
NodeTags: tool.StringSliceToString(req.NodeTags),
|
NodeTags: tool.StringSliceToString(req.NodeTags),
|
||||||
NodeGroupIds: subscribe.JSONInt64Slice(req.NodeGroupIds),
|
NodeGroupIds: subscribe.JSONInt64Slice(req.NodeGroupIds),
|
||||||
NodeGroupId: req.NodeGroupId,
|
NodeGroupId: req.NodeGroupId,
|
||||||
|
TrafficLimit: trafficLimit,
|
||||||
Show: req.Show,
|
Show: req.Show,
|
||||||
Sell: req.Sell,
|
Sell: req.Sell,
|
||||||
Sort: 0,
|
Sort: 0,
|
||||||
|
|||||||
@ -42,6 +42,12 @@ func (l *GetSubscribeDetailsLogic) GetSubscribeDetails(req *types.GetSubscribeDe
|
|||||||
l.Logger.Error("[GetSubscribeDetailsLogic] JSON unmarshal failed: ", logger.Field("error", err.Error()), logger.Field("discount", sub.Discount))
|
l.Logger.Error("[GetSubscribeDetailsLogic] JSON unmarshal failed: ", logger.Field("error", err.Error()), logger.Field("discount", sub.Discount))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if sub.TrafficLimit != "" {
|
||||||
|
err = json.Unmarshal([]byte(sub.TrafficLimit), &resp.TrafficLimit)
|
||||||
|
if err != nil {
|
||||||
|
l.Logger.Error("[GetSubscribeDetailsLogic] JSON unmarshal failed: ", logger.Field("error", err.Error()), logger.Field("traffic_limit", sub.TrafficLimit))
|
||||||
|
}
|
||||||
|
}
|
||||||
resp.Nodes = tool.StringToInt64Slice(sub.Nodes)
|
resp.Nodes = tool.StringToInt64Slice(sub.Nodes)
|
||||||
resp.NodeTags = strings.Split(sub.NodeTags, ",")
|
resp.NodeTags = strings.Split(sub.NodeTags, ",")
|
||||||
return resp, nil
|
return resp, nil
|
||||||
|
|||||||
@ -62,6 +62,12 @@ func (l *GetSubscribeListLogic) GetSubscribeList(req *types.GetSubscribeListRequ
|
|||||||
l.Logger.Error("[GetSubscribeListLogic] JSON unmarshal failed: ", logger.Field("error", err.Error()), logger.Field("discount", item.Discount))
|
l.Logger.Error("[GetSubscribeListLogic] JSON unmarshal failed: ", logger.Field("error", err.Error()), logger.Field("discount", item.Discount))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if item.TrafficLimit != "" {
|
||||||
|
err = json.Unmarshal([]byte(item.TrafficLimit), &sub.TrafficLimit)
|
||||||
|
if err != nil {
|
||||||
|
l.Logger.Error("[GetSubscribeListLogic] JSON unmarshal failed: ", logger.Field("error", err.Error()), logger.Field("traffic_limit", item.TrafficLimit))
|
||||||
|
}
|
||||||
|
}
|
||||||
sub.Nodes = tool.StringToInt64Slice(item.Nodes)
|
sub.Nodes = tool.StringToInt64Slice(item.Nodes)
|
||||||
sub.NodeTags = strings.Split(item.NodeTags, ",")
|
sub.NodeTags = strings.Split(item.NodeTags, ",")
|
||||||
// Handle NodeGroupIds - convert from JSONInt64Slice to []int64
|
// Handle NodeGroupIds - convert from JSONInt64Slice to []int64
|
||||||
|
|||||||
@ -42,6 +42,12 @@ func (l *UpdateSubscribeLogic) UpdateSubscribe(req *types.UpdateSubscribeRequest
|
|||||||
val, _ := json.Marshal(req.Discount)
|
val, _ := json.Marshal(req.Discount)
|
||||||
discount = string(val)
|
discount = string(val)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trafficLimit := ""
|
||||||
|
if len(req.TrafficLimit) > 0 {
|
||||||
|
val, _ := json.Marshal(req.TrafficLimit)
|
||||||
|
trafficLimit = string(val)
|
||||||
|
}
|
||||||
sub := &subscribe.Subscribe{
|
sub := &subscribe.Subscribe{
|
||||||
Id: req.Id,
|
Id: req.Id,
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
@ -60,6 +66,7 @@ func (l *UpdateSubscribeLogic) UpdateSubscribe(req *types.UpdateSubscribeRequest
|
|||||||
NodeTags: tool.StringSliceToString(req.NodeTags),
|
NodeTags: tool.StringSliceToString(req.NodeTags),
|
||||||
NodeGroupIds: subscribe.JSONInt64Slice(req.NodeGroupIds),
|
NodeGroupIds: subscribe.JSONInt64Slice(req.NodeGroupIds),
|
||||||
NodeGroupId: req.NodeGroupId,
|
NodeGroupId: req.NodeGroupId,
|
||||||
|
TrafficLimit: trafficLimit,
|
||||||
Show: req.Show,
|
Show: req.Show,
|
||||||
Sell: req.Sell,
|
Sell: req.Sell,
|
||||||
Sort: req.Sort,
|
Sort: req.Sort,
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/perfect-panel/server/internal/model/group"
|
||||||
"github.com/perfect-panel/server/internal/model/node"
|
"github.com/perfect-panel/server/internal/model/node"
|
||||||
"github.com/perfect-panel/server/internal/model/user"
|
"github.com/perfect-panel/server/internal/model/user"
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
@ -88,7 +89,7 @@ func (l *QueryUserSubscribeNodeListLogic) QueryUserSubscribeNodeList() (resp *ty
|
|||||||
func (l *QueryUserSubscribeNodeListLogic) getServers(userSub *user.Subscribe) (userSubscribeNodes []*types.UserSubscribeNodeInfo, err error) {
|
func (l *QueryUserSubscribeNodeListLogic) getServers(userSub *user.Subscribe) (userSubscribeNodes []*types.UserSubscribeNodeInfo, err error) {
|
||||||
userSubscribeNodes = make([]*types.UserSubscribeNodeInfo, 0)
|
userSubscribeNodes = make([]*types.UserSubscribeNodeInfo, 0)
|
||||||
if l.isSubscriptionExpired(userSub) {
|
if l.isSubscriptionExpired(userSub) {
|
||||||
return l.createExpiredServers(), nil
|
return l.createExpiredServers(userSub), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if group management is enabled
|
// Check if group management is enabled
|
||||||
@ -312,8 +313,98 @@ func (l *QueryUserSubscribeNodeListLogic) isSubscriptionExpired(userSub *user.Su
|
|||||||
return userSub.ExpireTime.Unix() < time.Now().Unix() && userSub.ExpireTime.Unix() != 0
|
return userSub.ExpireTime.Unix() < time.Now().Unix() && userSub.ExpireTime.Unix() != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *QueryUserSubscribeNodeListLogic) createExpiredServers() []*types.UserSubscribeNodeInfo {
|
func (l *QueryUserSubscribeNodeListLogic) createExpiredServers(userSub *user.Subscribe) []*types.UserSubscribeNodeInfo {
|
||||||
return nil
|
// 1. 查询过期节点组
|
||||||
|
var expiredGroup group.NodeGroup
|
||||||
|
err := l.svcCtx.DB.Where("is_expired_group = ?", true).First(&expiredGroup).Error
|
||||||
|
if err != nil {
|
||||||
|
l.Debugw("no expired node group configured", logger.Field("error", err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查用户是否在过期天数限制内
|
||||||
|
expiredDays := int(time.Since(userSub.ExpireTime).Hours() / 24)
|
||||||
|
if expiredDays > expiredGroup.ExpiredDaysLimit {
|
||||||
|
l.Debugf("user subscription expired %d days, exceeds limit %d days", expiredDays, expiredGroup.ExpiredDaysLimit)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 检查用户已使用流量是否超过限制(仅使用过期期间的流量)
|
||||||
|
if expiredGroup.MaxTrafficGBExpired != nil && *expiredGroup.MaxTrafficGBExpired > 0 {
|
||||||
|
usedTrafficGB := (userSub.ExpiredDownload + userSub.ExpiredUpload) / (1024 * 1024 * 1024)
|
||||||
|
if usedTrafficGB >= *expiredGroup.MaxTrafficGBExpired {
|
||||||
|
l.Debugf("user expired traffic %d GB, exceeds expired group limit %d GB", usedTrafficGB, *expiredGroup.MaxTrafficGBExpired)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 查询过期节点组的节点
|
||||||
|
enable := true
|
||||||
|
_, nodes, err := l.svcCtx.NodeModel.FilterNodeList(l.ctx, &node.FilterNodeParams{
|
||||||
|
Page: 0,
|
||||||
|
Size: 1000,
|
||||||
|
NodeGroupIds: []int64{expiredGroup.Id},
|
||||||
|
Enabled: &enable,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
l.Errorw("failed to query expired group nodes", logger.Field("error", err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(nodes) == 0 {
|
||||||
|
l.Debug("no nodes found in expired group")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 查询服务器信息
|
||||||
|
var serverMapIds = make(map[int64]*node.Server)
|
||||||
|
for _, n := range nodes {
|
||||||
|
serverMapIds[n.ServerId] = nil
|
||||||
|
}
|
||||||
|
var serverIds []int64
|
||||||
|
for k := range serverMapIds {
|
||||||
|
serverIds = append(serverIds, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
servers, err := l.svcCtx.NodeModel.QueryServerList(l.ctx, serverIds)
|
||||||
|
if err != nil {
|
||||||
|
l.Errorw("failed to query servers", logger.Field("error", err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range servers {
|
||||||
|
serverMapIds[s.Id] = s
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 构建节点列表
|
||||||
|
userSubscribeNodes := make([]*types.UserSubscribeNodeInfo, 0, len(nodes))
|
||||||
|
for _, n := range nodes {
|
||||||
|
server := serverMapIds[n.ServerId]
|
||||||
|
if server == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
userSubscribeNode := &types.UserSubscribeNodeInfo{
|
||||||
|
Id: n.Id,
|
||||||
|
Name: n.Name,
|
||||||
|
Uuid: userSub.UUID,
|
||||||
|
Protocol: n.Protocol,
|
||||||
|
Protocols: server.Protocols,
|
||||||
|
Port: n.Port,
|
||||||
|
Address: n.Address,
|
||||||
|
Tags: strings.Split(n.Tags, ","),
|
||||||
|
Country: server.Country,
|
||||||
|
City: server.City,
|
||||||
|
Latitude: server.Latitude,
|
||||||
|
Longitude: server.Longitude,
|
||||||
|
LongitudeCenter: server.LongitudeCenter,
|
||||||
|
LatitudeCenter: server.LatitudeCenter,
|
||||||
|
CreatedAt: n.CreatedAt.Unix(),
|
||||||
|
}
|
||||||
|
userSubscribeNodes = append(userSubscribeNodes, userSubscribeNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Infof("returned %d nodes from expired group for user %d (expired %d days)", len(userSubscribeNodes), userSub.UserId, expiredDays)
|
||||||
|
return userSubscribeNodes
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *QueryUserSubscribeNodeListLogic) getFirstHostLine() string {
|
func (l *QueryUserSubscribeNodeListLogic) getFirstHostLine() string {
|
||||||
|
|||||||
138
internal/logic/public/user/getUserTrafficStatsLogic.go
Normal file
138
internal/logic/public/user/getUserTrafficStatsLogic.go
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/perfect-panel/server/internal/model/user"
|
||||||
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
|
"github.com/perfect-panel/server/internal/types"
|
||||||
|
"github.com/perfect-panel/server/pkg/constant"
|
||||||
|
"github.com/perfect-panel/server/pkg/logger"
|
||||||
|
"github.com/perfect-panel/server/pkg/xerr"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GetUserTrafficStatsLogic struct {
|
||||||
|
logger.Logger
|
||||||
|
ctx context.Context
|
||||||
|
svcCtx *svc.ServiceContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get User Traffic Statistics
|
||||||
|
func NewGetUserTrafficStatsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserTrafficStatsLogic {
|
||||||
|
return &GetUserTrafficStatsLogic{
|
||||||
|
Logger: logger.WithContext(ctx),
|
||||||
|
ctx: ctx,
|
||||||
|
svcCtx: svcCtx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *GetUserTrafficStatsLogic) GetUserTrafficStats(req *types.GetUserTrafficStatsRequest) (resp *types.GetUserTrafficStatsResponse, err error) {
|
||||||
|
// 获取当前用户
|
||||||
|
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
||||||
|
if !ok {
|
||||||
|
logger.Error("current user is not found in context")
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将字符串 ID 转换为 int64
|
||||||
|
userSubscribeId, err := strconv.ParseInt(req.UserSubscribeId, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
l.Errorw("[GetUserTrafficStats] Invalid User Subscribe ID:",
|
||||||
|
logger.Field("user_subscribe_id", req.UserSubscribeId),
|
||||||
|
logger.Field("err", err.Error()))
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid subscription ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证订阅归属权 - 直接查询 user_subscribe 表
|
||||||
|
var userSubscribe struct {
|
||||||
|
Id int64
|
||||||
|
UserId int64
|
||||||
|
}
|
||||||
|
err = l.svcCtx.DB.WithContext(l.ctx).
|
||||||
|
Table("user_subscribe").
|
||||||
|
Select("id, user_id").
|
||||||
|
Where("id = ?", userSubscribeId).
|
||||||
|
First(&userSubscribe).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
l.Errorw("[GetUserTrafficStats] User Subscribe Not Found:",
|
||||||
|
logger.Field("user_subscribe_id", userSubscribeId),
|
||||||
|
logger.Field("user_id", u.Id))
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Subscription not found")
|
||||||
|
}
|
||||||
|
l.Errorw("[GetUserTrafficStats] Query User Subscribe Error:", logger.Field("err", err.Error()))
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Subscribe Error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if userSubscribe.UserId != u.Id {
|
||||||
|
l.Errorw("[GetUserTrafficStats] User Subscribe Access Denied:",
|
||||||
|
logger.Field("user_subscribe_id", userSubscribeId),
|
||||||
|
logger.Field("subscribe_user_id", userSubscribe.UserId),
|
||||||
|
logger.Field("current_user_id", u.Id))
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算时间范围
|
||||||
|
now := time.Now()
|
||||||
|
startDate := now.AddDate(0, 0, -req.Days+1)
|
||||||
|
startDate = time.Date(startDate.Year(), startDate.Month(), startDate.Day(), 0, 0, 0, 0, time.Local)
|
||||||
|
|
||||||
|
// 初始化响应
|
||||||
|
resp = &types.GetUserTrafficStatsResponse{
|
||||||
|
List: make([]types.DailyTrafficStats, 0, req.Days),
|
||||||
|
TotalUpload: 0,
|
||||||
|
TotalDownload: 0,
|
||||||
|
TotalTraffic: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按天查询流量数据
|
||||||
|
for i := 0; i < req.Days; i++ {
|
||||||
|
currentDate := startDate.AddDate(0, 0, i)
|
||||||
|
dayStart := time.Date(currentDate.Year(), currentDate.Month(), currentDate.Day(), 0, 0, 0, 0, time.Local)
|
||||||
|
dayEnd := dayStart.Add(24 * time.Hour).Add(-time.Nanosecond)
|
||||||
|
|
||||||
|
// 查询当天流量
|
||||||
|
var dailyTraffic struct {
|
||||||
|
Upload int64
|
||||||
|
Download int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接使用 model 的查询方法
|
||||||
|
err := l.svcCtx.DB.WithContext(l.ctx).
|
||||||
|
Table("traffic_log").
|
||||||
|
Select("COALESCE(SUM(upload), 0) as upload, COALESCE(SUM(download), 0) as download").
|
||||||
|
Where("user_id = ? AND subscribe_id = ? AND timestamp BETWEEN ? AND ?",
|
||||||
|
u.Id, userSubscribeId, dayStart, dayEnd).
|
||||||
|
Scan(&dailyTraffic).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
l.Errorw("[GetUserTrafficStats] Query Daily Traffic Error:",
|
||||||
|
logger.Field("date", currentDate.Format("2006-01-02")),
|
||||||
|
logger.Field("err", err.Error()))
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query Traffic Error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加到结果列表
|
||||||
|
total := dailyTraffic.Upload + dailyTraffic.Download
|
||||||
|
resp.List = append(resp.List, types.DailyTrafficStats{
|
||||||
|
Date: currentDate.Format("2006-01-02"),
|
||||||
|
Upload: dailyTraffic.Upload,
|
||||||
|
Download: dailyTraffic.Download,
|
||||||
|
Total: total,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 累加总计
|
||||||
|
resp.TotalUpload += dailyTraffic.Upload
|
||||||
|
resp.TotalDownload += dailyTraffic.Download
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.TotalTraffic = resp.TotalUpload + resp.TotalDownload
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ package user
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/pkg/constant"
|
"github.com/perfect-panel/server/pkg/constant"
|
||||||
@ -52,6 +53,9 @@ func (l *QueryUserSubscribeLogic) QueryUserSubscribe() (resp *types.QueryUserSub
|
|||||||
var sub types.UserSubscribe
|
var sub types.UserSubscribe
|
||||||
tool.DeepCopy(&sub, item)
|
tool.DeepCopy(&sub, item)
|
||||||
|
|
||||||
|
// 填充 IdStr 字段,避免前端精度丢失
|
||||||
|
sub.IdStr = strconv.FormatInt(item.Id, 10)
|
||||||
|
|
||||||
// 解析Discount字段 避免在续订时只能续订一个月
|
// 解析Discount字段 避免在续订时只能续订一个月
|
||||||
if item.Subscribe != nil && item.Subscribe.Discount != "" {
|
if item.Subscribe != nil && item.Subscribe.Discount != "" {
|
||||||
var discounts []types.SubscribeDiscount
|
var discounts []types.SubscribeDiscount
|
||||||
|
|||||||
@ -4,10 +4,13 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/perfect-panel/server/internal/model/group"
|
||||||
"github.com/perfect-panel/server/internal/model/node"
|
"github.com/perfect-panel/server/internal/model/node"
|
||||||
"github.com/perfect-panel/server/internal/model/subscribe"
|
"github.com/perfect-panel/server/internal/model/subscribe"
|
||||||
|
"github.com/perfect-panel/server/internal/model/user"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
"github.com/perfect-panel/server/internal/types"
|
"github.com/perfect-panel/server/internal/types"
|
||||||
@ -133,7 +136,7 @@ func (l *GetServerUserListLogic) GetServerUserList(req *types.GetServerUserListR
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(subs) == 0 {
|
if len(subs) == 0 {
|
||||||
return &types.GetServerUserListResponse{
|
return &types.GetServerUserListResponse{
|
||||||
Users: []types.ServerUser{
|
Users: []types.ServerUser{
|
||||||
@ -151,14 +154,33 @@ func (l *GetServerUserListLogic) GetServerUserList(req *types.GetServerUserListR
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, datum := range data {
|
for _, datum := range data {
|
||||||
|
if !l.shouldIncludeServerUser(datum, nodeGroupIds) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算该用户的实际限速值(考虑按量限速规则)
|
||||||
|
effectiveSpeedLimit := l.calculateEffectiveSpeedLimit(sub, datum)
|
||||||
|
|
||||||
users = append(users, types.ServerUser{
|
users = append(users, types.ServerUser{
|
||||||
Id: datum.Id,
|
Id: datum.Id,
|
||||||
UUID: datum.UUID,
|
UUID: datum.UUID,
|
||||||
SpeedLimit: sub.SpeedLimit,
|
SpeedLimit: effectiveSpeedLimit,
|
||||||
DeviceLimit: sub.DeviceLimit,
|
DeviceLimit: sub.DeviceLimit,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理过期订阅用户:如果当前节点属于过期节点组,添加符合条件的过期用户
|
||||||
|
if len(nodeGroupIds) > 0 {
|
||||||
|
expiredUsers, expiredSpeedLimit := l.getExpiredUsers(nodeGroupIds)
|
||||||
|
for i := range expiredUsers {
|
||||||
|
if expiredSpeedLimit > 0 {
|
||||||
|
expiredUsers[i].SpeedLimit = expiredSpeedLimit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
users = append(users, expiredUsers...)
|
||||||
|
}
|
||||||
|
|
||||||
if len(users) == 0 {
|
if len(users) == 0 {
|
||||||
users = append(users, types.ServerUser{
|
users = append(users, types.ServerUser{
|
||||||
Id: 1,
|
Id: 1,
|
||||||
@ -181,3 +203,175 @@ func (l *GetServerUserListLogic) GetServerUserList(req *types.GetServerUserListR
|
|||||||
}
|
}
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *GetServerUserListLogic) shouldIncludeServerUser(userSub *user.Subscribe, serverNodeGroupIds []int64) bool {
|
||||||
|
if userSub == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if userSub.ExpireTime.Unix() == 0 || userSub.ExpireTime.After(time.Now()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return l.canUseExpiredNodeGroup(userSub, serverNodeGroupIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *GetServerUserListLogic) getExpiredUsers(serverNodeGroupIds []int64) ([]types.ServerUser, int64) {
|
||||||
|
var expiredGroup group.NodeGroup
|
||||||
|
if err := l.svcCtx.DB.Where("is_expired_group = ?", true).First(&expiredGroup).Error; err != nil {
|
||||||
|
return nil, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tool.Contains(serverNodeGroupIds, expiredGroup.Id) {
|
||||||
|
return nil, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var expiredSubs []*user.Subscribe
|
||||||
|
if err := l.svcCtx.DB.Where("status = ?", 3).Find(&expiredSubs).Error; err != nil {
|
||||||
|
l.Errorw("query expired subscriptions failed", logger.Field("error", err.Error()))
|
||||||
|
return nil, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
users := make([]types.ServerUser, 0)
|
||||||
|
seen := make(map[int64]bool)
|
||||||
|
for _, userSub := range expiredSubs {
|
||||||
|
if !l.checkExpiredUserEligibility(userSub, &expiredGroup) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if seen[userSub.Id] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[userSub.Id] = true
|
||||||
|
users = append(users, types.ServerUser{
|
||||||
|
Id: userSub.Id,
|
||||||
|
UUID: userSub.UUID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return users, int64(expiredGroup.SpeedLimit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *GetServerUserListLogic) checkExpiredUserEligibility(userSub *user.Subscribe, expiredGroup *group.NodeGroup) bool {
|
||||||
|
expiredDays := int(time.Since(userSub.ExpireTime).Hours() / 24)
|
||||||
|
if expiredDays > expiredGroup.ExpiredDaysLimit {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if expiredGroup.MaxTrafficGBExpired != nil && *expiredGroup.MaxTrafficGBExpired > 0 {
|
||||||
|
usedTrafficGB := (userSub.ExpiredDownload + userSub.ExpiredUpload) / (1024 * 1024 * 1024)
|
||||||
|
if usedTrafficGB >= *expiredGroup.MaxTrafficGBExpired {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *GetServerUserListLogic) canUseExpiredNodeGroup(userSub *user.Subscribe, serverNodeGroupIds []int64) bool {
|
||||||
|
var expiredGroup group.NodeGroup
|
||||||
|
if err := l.svcCtx.DB.Where("is_expired_group = ?", true).First(&expiredGroup).Error; err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tool.Contains(serverNodeGroupIds, expiredGroup.Id) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
expiredDays := int(time.Since(userSub.ExpireTime).Hours() / 24)
|
||||||
|
if expiredDays > expiredGroup.ExpiredDaysLimit {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if expiredGroup.MaxTrafficGBExpired != nil && *expiredGroup.MaxTrafficGBExpired > 0 {
|
||||||
|
usedTrafficGB := (userSub.ExpiredDownload + userSub.ExpiredUpload) / (1024 * 1024 * 1024)
|
||||||
|
if usedTrafficGB >= *expiredGroup.MaxTrafficGBExpired {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateEffectiveSpeedLimit 计算用户的实际限速值(考虑按量限速规则)
|
||||||
|
func (l *GetServerUserListLogic) calculateEffectiveSpeedLimit(sub *subscribe.Subscribe, userSub *user.Subscribe) int64 {
|
||||||
|
baseSpeedLimit := sub.SpeedLimit
|
||||||
|
|
||||||
|
// 解析 traffic_limit 规则
|
||||||
|
if sub.TrafficLimit == "" {
|
||||||
|
return baseSpeedLimit
|
||||||
|
}
|
||||||
|
|
||||||
|
var trafficLimitRules []types.TrafficLimit
|
||||||
|
if err := json.Unmarshal([]byte(sub.TrafficLimit), &trafficLimitRules); err != nil {
|
||||||
|
l.Errorw("[calculateEffectiveSpeedLimit] Failed to unmarshal traffic_limit",
|
||||||
|
logger.Field("error", err.Error()),
|
||||||
|
logger.Field("traffic_limit", sub.TrafficLimit))
|
||||||
|
return baseSpeedLimit
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(trafficLimitRules) == 0 {
|
||||||
|
return baseSpeedLimit
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询用户指定时段的流量使用情况
|
||||||
|
now := time.Now()
|
||||||
|
for _, rule := range trafficLimitRules {
|
||||||
|
var startTime, endTime time.Time
|
||||||
|
|
||||||
|
if rule.StatType == "hour" {
|
||||||
|
// 按小时统计:根据 StatValue 计算时间范围(往前推 N 小时)
|
||||||
|
if rule.StatValue <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 从当前时间往前推 StatValue 小时
|
||||||
|
startTime = now.Add(-time.Duration(rule.StatValue) * time.Hour)
|
||||||
|
endTime = now
|
||||||
|
} else if rule.StatType == "day" {
|
||||||
|
// 按天统计:根据 StatValue 计算时间范围(往前推 N 天)
|
||||||
|
if rule.StatValue <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 从当前时间往前推 StatValue 天
|
||||||
|
startTime = now.AddDate(0, 0, -int(rule.StatValue))
|
||||||
|
endTime = now
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询该时段的流量使用
|
||||||
|
var usedTraffic struct {
|
||||||
|
Upload int64
|
||||||
|
Download int64
|
||||||
|
}
|
||||||
|
err := l.svcCtx.DB.WithContext(l.ctx.Request.Context()).
|
||||||
|
Table("traffic_log").
|
||||||
|
Select("COALESCE(SUM(upload), 0) as upload, COALESCE(SUM(download), 0) as download").
|
||||||
|
Where("user_id = ? AND subscribe_id = ? AND timestamp >= ? AND timestamp < ?",
|
||||||
|
userSub.UserId, userSub.Id, startTime, endTime).
|
||||||
|
Scan(&usedTraffic).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
l.Errorw("[calculateEffectiveSpeedLimit] Failed to query traffic usage",
|
||||||
|
logger.Field("error", err.Error()),
|
||||||
|
logger.Field("user_id", userSub.UserId),
|
||||||
|
logger.Field("subscribe_id", userSub.Id))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算已使用流量(GB)
|
||||||
|
usedGB := float64(usedTraffic.Upload+usedTraffic.Download) / (1024 * 1024 * 1024)
|
||||||
|
|
||||||
|
// 如果已使用流量达到或超过阈值,应用限速
|
||||||
|
if usedGB >= float64(rule.TrafficUsage) {
|
||||||
|
// 如果规则限速大于0,应用该限速
|
||||||
|
if rule.SpeedLimit > 0 {
|
||||||
|
// 如果基础限速为0(无限速)或规则限速更严格,使用规则限速
|
||||||
|
if baseSpeedLimit == 0 || rule.SpeedLimit < baseSpeedLimit {
|
||||||
|
return rule.SpeedLimit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseSpeedLimit
|
||||||
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/perfect-panel/server/adapter"
|
"github.com/perfect-panel/server/adapter"
|
||||||
"github.com/perfect-panel/server/internal/model/client"
|
"github.com/perfect-panel/server/internal/model/client"
|
||||||
|
"github.com/perfect-panel/server/internal/model/group"
|
||||||
"github.com/perfect-panel/server/internal/model/log"
|
"github.com/perfect-panel/server/internal/model/log"
|
||||||
"github.com/perfect-panel/server/internal/model/node"
|
"github.com/perfect-panel/server/internal/model/node"
|
||||||
"github.com/perfect-panel/server/internal/report"
|
"github.com/perfect-panel/server/internal/report"
|
||||||
@ -206,6 +207,19 @@ func (l *SubscribeLogic) logSubscribeActivity(subscribeStatus bool, userSub *use
|
|||||||
|
|
||||||
func (l *SubscribeLogic) getServers(userSub *user.Subscribe) ([]*node.Node, error) {
|
func (l *SubscribeLogic) getServers(userSub *user.Subscribe) ([]*node.Node, error) {
|
||||||
if l.isSubscriptionExpired(userSub) {
|
if l.isSubscriptionExpired(userSub) {
|
||||||
|
// 尝试获取过期节点组的节点
|
||||||
|
expiredNodes, err := l.getExpiredGroupNodes(userSub)
|
||||||
|
if err != nil {
|
||||||
|
l.Errorw("[Generate Subscribe]get expired group nodes error", logger.Field("error", err.Error()))
|
||||||
|
return l.createExpiredServers(), nil
|
||||||
|
}
|
||||||
|
// 如果有符合条件的过期节点组节点,返回它们
|
||||||
|
if len(expiredNodes) > 0 {
|
||||||
|
l.Debugf("[Generate Subscribe]user %d can use expired node group, nodes count: %d", userSub.UserId, len(expiredNodes))
|
||||||
|
return expiredNodes, nil
|
||||||
|
}
|
||||||
|
// 否则返回假的过期节点
|
||||||
|
l.Debugf("[Generate Subscribe]user %d cannot use expired node group, return fake expired nodes", userSub.UserId)
|
||||||
return l.createExpiredServers(), nil
|
return l.createExpiredServers(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -422,3 +436,52 @@ func (l *SubscribeLogic) isGroupEnabled() bool {
|
|||||||
}
|
}
|
||||||
return value == "true" || value == "1"
|
return value == "true" || value == "1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getExpiredGroupNodes 获取过期节点组的节点
|
||||||
|
func (l *SubscribeLogic) getExpiredGroupNodes(userSub *user.Subscribe) ([]*node.Node, error) {
|
||||||
|
// 1. 查询过期节点组
|
||||||
|
var expiredGroup group.NodeGroup
|
||||||
|
err := l.svc.DB.Where("is_expired_group = ?", true).First(&expiredGroup).Error
|
||||||
|
if err != nil {
|
||||||
|
l.Debugw("[SubscribeLogic]no expired node group configured", logger.Field("error", err.Error()))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查用户是否在过期天数限制内
|
||||||
|
expiredDays := int(time.Since(userSub.ExpireTime).Hours() / 24)
|
||||||
|
if expiredDays > expiredGroup.ExpiredDaysLimit {
|
||||||
|
l.Debugf("[SubscribeLogic]user %d subscription expired %d days, exceeds limit %d days", userSub.UserId, expiredDays, expiredGroup.ExpiredDaysLimit)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 检查用户已使用流量是否超过限制(仅使用过期期间的流量)
|
||||||
|
if expiredGroup.MaxTrafficGBExpired != nil && *expiredGroup.MaxTrafficGBExpired > 0 {
|
||||||
|
usedTrafficGB := (userSub.ExpiredDownload + userSub.ExpiredUpload) / (1024 * 1024 * 1024)
|
||||||
|
if usedTrafficGB >= *expiredGroup.MaxTrafficGBExpired {
|
||||||
|
l.Debugf("[SubscribeLogic]user %d expired traffic %d GB, exceeds expired group limit %d GB", userSub.UserId, usedTrafficGB, *expiredGroup.MaxTrafficGBExpired)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 查询过期节点组的节点
|
||||||
|
enable := true
|
||||||
|
_, nodes, err := l.svc.NodeModel.FilterNodeList(l.ctx.Request.Context(), &node.FilterNodeParams{
|
||||||
|
Page: 0,
|
||||||
|
Size: 1000,
|
||||||
|
NodeGroupIds: []int64{expiredGroup.Id},
|
||||||
|
Enabled: &enable,
|
||||||
|
Preload: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
l.Errorw("[SubscribeLogic]failed to query expired group nodes", logger.Field("error", err.Error()))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(nodes) == 0 {
|
||||||
|
l.Debug("[SubscribeLogic]no nodes found in expired group")
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Infof("[SubscribeLogic]returned %d nodes from expired group for user %d (expired %d days)", len(nodes), userSub.UserId, expiredDays)
|
||||||
|
return nodes, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -8,15 +8,19 @@ import (
|
|||||||
|
|
||||||
// NodeGroup 节点组模型
|
// NodeGroup 节点组模型
|
||||||
type NodeGroup struct {
|
type NodeGroup struct {
|
||||||
Id int64 `gorm:"primaryKey"`
|
Id int64 `gorm:"primaryKey"`
|
||||||
Name string `gorm:"type:varchar(255);not null;comment:Name"`
|
Name string `gorm:"type:varchar(255);not null;comment:Name"`
|
||||||
Description string `gorm:"type:varchar(500);comment:Description"`
|
Description string `gorm:"type:varchar(500);comment:Description"`
|
||||||
Sort int `gorm:"default:0;index:idx_sort;comment:Sort Order"`
|
Sort int `gorm:"default:0;index:idx_sort;comment:Sort Order"`
|
||||||
ForCalculation *bool `gorm:"default:true;not null;comment:For Calculation: whether this node group participates in grouping calculation"`
|
ForCalculation *bool `gorm:"default:true;not null;comment:For Calculation: whether this node group participates in grouping calculation"`
|
||||||
MinTrafficGB *int64 `gorm:"default:0;comment:Minimum Traffic (GB) for this node group"`
|
IsExpiredGroup *bool `gorm:"default:false;not null;index:idx_is_expired_group;comment:Is Expired Group"`
|
||||||
MaxTrafficGB *int64 `gorm:"default:0;comment:Maximum Traffic (GB) for this node group"`
|
ExpiredDaysLimit int `gorm:"default:7;not null;comment:Expired days limit (days)"`
|
||||||
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
|
MaxTrafficGBExpired *int64 `gorm:"default:0;comment:Max traffic for expired users (GB)"`
|
||||||
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
SpeedLimit int `gorm:"default:0;not null;comment:Speed limit (KB/s)"`
|
||||||
|
MinTrafficGB *int64 `gorm:"default:0;comment:Minimum Traffic (GB) for this node group"`
|
||||||
|
MaxTrafficGB *int64 `gorm:"default:0;comment:Maximum Traffic (GB) for this node group"`
|
||||||
|
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
|
||||||
|
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName 指定表名
|
// TableName 指定表名
|
||||||
|
|||||||
@ -71,6 +71,7 @@ type Subscribe struct {
|
|||||||
NodeTags string `gorm:"type:varchar(255);comment:Node Tags"`
|
NodeTags string `gorm:"type:varchar(255);comment:Node Tags"`
|
||||||
NodeGroupIds JSONInt64Slice `gorm:"type:json;comment:Node Group IDs (JSON array, multiple groups)"`
|
NodeGroupIds JSONInt64Slice `gorm:"type:json;comment:Node Group IDs (JSON array, multiple groups)"`
|
||||||
NodeGroupId int64 `gorm:"default:0;index:idx_node_group_id;comment:Default Node Group ID (single ID)"`
|
NodeGroupId int64 `gorm:"default:0;index:idx_node_group_id;comment:Default Node Group ID (single ID)"`
|
||||||
|
TrafficLimit string `gorm:"type:text;comment:Traffic Limit Rules"`
|
||||||
Show *bool `gorm:"type:tinyint(1);not null;default:0;comment:Show portal page"`
|
Show *bool `gorm:"type:tinyint(1);not null;default:0;comment:Show portal page"`
|
||||||
Sell *bool `gorm:"type:tinyint(1);not null;default:0;comment:Sell"`
|
Sell *bool `gorm:"type:tinyint(1);not null;default:0;comment:Sell"`
|
||||||
Sort int64 `gorm:"type:int;not null;default:0;comment:Sort"`
|
Sort int64 `gorm:"type:int;not null;default:0;comment:Sort"`
|
||||||
|
|||||||
@ -82,7 +82,7 @@ type customUserLogicModel interface {
|
|||||||
FindOneSubscribeDetailsById(ctx context.Context, id int64) (*SubscribeDetails, error)
|
FindOneSubscribeDetailsById(ctx context.Context, id int64) (*SubscribeDetails, error)
|
||||||
FindOneUserSubscribe(ctx context.Context, id int64) (*SubscribeDetails, error)
|
FindOneUserSubscribe(ctx context.Context, id int64) (*SubscribeDetails, error)
|
||||||
FindUsersSubscribeBySubscribeId(ctx context.Context, subscribeId int64) ([]*Subscribe, error)
|
FindUsersSubscribeBySubscribeId(ctx context.Context, subscribeId int64) ([]*Subscribe, error)
|
||||||
UpdateUserSubscribeWithTraffic(ctx context.Context, id, download, upload int64, tx ...*gorm.DB) error
|
UpdateUserSubscribeWithTraffic(ctx context.Context, id, download, upload int64, isExpired bool, tx ...*gorm.DB) error
|
||||||
QueryResisterUserTotalByDate(ctx context.Context, date time.Time) (int64, error)
|
QueryResisterUserTotalByDate(ctx context.Context, date time.Time) (int64, error)
|
||||||
QueryResisterUserTotalByMonthly(ctx context.Context, date time.Time) (int64, error)
|
QueryResisterUserTotalByMonthly(ctx context.Context, date time.Time) (int64, error)
|
||||||
QueryResisterUserTotal(ctx context.Context) (int64, error)
|
QueryResisterUserTotal(ctx context.Context) (int64, error)
|
||||||
@ -181,7 +181,7 @@ func (m *customUserModel) BatchDeleteUser(ctx context.Context, ids []int64, tx .
|
|||||||
}, m.batchGetCacheKeys(users...)...)
|
}, m.batchGetCacheKeys(users...)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *customUserModel) UpdateUserSubscribeWithTraffic(ctx context.Context, id, download, upload int64, tx ...*gorm.DB) error {
|
func (m *customUserModel) UpdateUserSubscribeWithTraffic(ctx context.Context, id, download, upload int64, isExpired bool, tx ...*gorm.DB) error {
|
||||||
sub, err := m.FindOneSubscribe(ctx, id)
|
sub, err := m.FindOneSubscribe(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -198,10 +198,21 @@ func (m *customUserModel) UpdateUserSubscribeWithTraffic(ctx context.Context, id
|
|||||||
if len(tx) > 0 {
|
if len(tx) > 0 {
|
||||||
conn = tx[0]
|
conn = tx[0]
|
||||||
}
|
}
|
||||||
return conn.Model(&Subscribe{}).Where("id = ?", id).Updates(map[string]interface{}{
|
|
||||||
"download": gorm.Expr("download + ?", download),
|
// 根据订阅状态更新对应的流量字段
|
||||||
"upload": gorm.Expr("upload + ?", upload),
|
if isExpired {
|
||||||
}).Error
|
// 过期期间,更新过期流量字段
|
||||||
|
return conn.Model(&Subscribe{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||||
|
"expired_download": gorm.Expr("expired_download + ?", download),
|
||||||
|
"expired_upload": gorm.Expr("expired_upload + ?", upload),
|
||||||
|
}).Error
|
||||||
|
} else {
|
||||||
|
// 正常期间,更新正常流量字段
|
||||||
|
return conn.Model(&Subscribe{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||||
|
"download": gorm.Expr("download + ?", download),
|
||||||
|
"upload": gorm.Expr("upload + ?", upload),
|
||||||
|
}).Error
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -85,25 +85,27 @@ func (*User) TableName() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Subscribe struct {
|
type Subscribe struct {
|
||||||
Id int64 `gorm:"primaryKey"`
|
Id int64 `gorm:"primaryKey"`
|
||||||
UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"`
|
UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"`
|
||||||
User User `gorm:"foreignKey:UserId;references:Id"`
|
User User `gorm:"foreignKey:UserId;references:Id"`
|
||||||
OrderId int64 `gorm:"index:idx_order_id;not null;comment:Order ID"`
|
OrderId int64 `gorm:"index:idx_order_id;not null;comment:Order ID"`
|
||||||
SubscribeId int64 `gorm:"index:idx_subscribe_id;not null;comment:Subscription ID"`
|
SubscribeId int64 `gorm:"index:idx_subscribe_id;not null;comment:Subscription ID"`
|
||||||
NodeGroupId int64 `gorm:"index:idx_node_group_id;not null;default:0;comment:Node Group ID (single ID)"`
|
NodeGroupId int64 `gorm:"index:idx_node_group_id;not null;default:0;comment:Node Group ID (single ID)"`
|
||||||
GroupLocked *bool `gorm:"type:tinyint(1);not null;default:0;comment:Group Locked"`
|
GroupLocked *bool `gorm:"type:tinyint(1);not null;default:0;comment:Group Locked"`
|
||||||
StartTime time.Time `gorm:"default:CURRENT_TIMESTAMP(3);not null;comment:Subscription Start Time"`
|
StartTime time.Time `gorm:"default:CURRENT_TIMESTAMP(3);not null;comment:Subscription Start Time"`
|
||||||
ExpireTime time.Time `gorm:"default:NULL;comment:Subscription Expire Time"`
|
ExpireTime time.Time `gorm:"default:NULL;comment:Subscription Expire Time"`
|
||||||
FinishedAt *time.Time `gorm:"default:NULL;comment:Finished Time"`
|
FinishedAt *time.Time `gorm:"default:NULL;comment:Finished Time"`
|
||||||
Traffic int64 `gorm:"default:0;comment:Traffic"`
|
Traffic int64 `gorm:"default:0;comment:Traffic"`
|
||||||
Download int64 `gorm:"default:0;comment:Download Traffic"`
|
Download int64 `gorm:"default:0;comment:Download Traffic"`
|
||||||
Upload int64 `gorm:"default:0;comment:Upload Traffic"`
|
Upload int64 `gorm:"default:0;comment:Upload Traffic"`
|
||||||
Token string `gorm:"index:idx_token;unique;type:varchar(255);default:'';comment:Token"`
|
ExpiredDownload int64 `gorm:"default:0;comment:Expired period download traffic (bytes)"`
|
||||||
UUID string `gorm:"type:varchar(255);unique;index:idx_uuid;default:'';comment:UUID"`
|
ExpiredUpload int64 `gorm:"default:0;comment:Expired period upload traffic (bytes)"`
|
||||||
Status uint8 `gorm:"type:tinyint(1);default:0;comment:Subscription Status: 0: Pending 1: Active 2: Finished 3: Expired 4: Deducted 5: stopped"`
|
Token string `gorm:"index:idx_token;unique;type:varchar(255);default:'';comment:Token"`
|
||||||
Note string `gorm:"type:varchar(500);default:'';comment:User note for subscription"`
|
UUID string `gorm:"type:varchar(255);unique;index:idx_uuid;default:'';comment:UUID"`
|
||||||
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
|
Status uint8 `gorm:"type:tinyint(1);default:0;comment:Subscription Status: 0: Pending 1: Active 2: Finished 3: Expired 4: Deducted 5: stopped"`
|
||||||
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
Note string `gorm:"type:varchar(500);default:'';comment:User note for subscription"`
|
||||||
|
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
|
||||||
|
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*Subscribe) TableName() string {
|
func (*Subscribe) TableName() string {
|
||||||
|
|||||||
@ -321,12 +321,16 @@ type CreateDocumentRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreateNodeGroupRequest struct {
|
type CreateNodeGroupRequest struct {
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Sort int `json:"sort"`
|
Sort int `json:"sort"`
|
||||||
ForCalculation *bool `json:"for_calculation"`
|
ForCalculation *bool `json:"for_calculation"`
|
||||||
MinTrafficGB *int64 `json:"min_traffic_gb,omitempty"`
|
IsExpiredGroup *bool `json:"is_expired_group"`
|
||||||
MaxTrafficGB *int64 `json:"max_traffic_gb,omitempty"`
|
ExpiredDaysLimit *int `json:"expired_days_limit"`
|
||||||
|
MaxTrafficGBExpired *int64 `json:"max_traffic_gb_expired,omitempty"`
|
||||||
|
SpeedLimit *int `json:"speed_limit"`
|
||||||
|
MinTrafficGB *int64 `json:"min_traffic_gb,omitempty"`
|
||||||
|
MaxTrafficGB *int64 `json:"max_traffic_gb,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateNodeRequest struct {
|
type CreateNodeRequest struct {
|
||||||
@ -432,6 +436,7 @@ type CreateSubscribeRequest struct {
|
|||||||
NodeTags []string `json:"node_tags"`
|
NodeTags []string `json:"node_tags"`
|
||||||
NodeGroupIds []int64 `json:"node_group_ids,omitempty"`
|
NodeGroupIds []int64 `json:"node_group_ids,omitempty"`
|
||||||
NodeGroupId int64 `json:"node_group_id"`
|
NodeGroupId int64 `json:"node_group_id"`
|
||||||
|
TrafficLimit []TrafficLimit `json:"traffic_limit"`
|
||||||
Show *bool `json:"show"`
|
Show *bool `json:"show"`
|
||||||
Sell *bool `json:"sell"`
|
Sell *bool `json:"sell"`
|
||||||
DeductionRatio int64 `json:"deduction_ratio"`
|
DeductionRatio int64 `json:"deduction_ratio"`
|
||||||
@ -502,6 +507,13 @@ type CurrencyConfig struct {
|
|||||||
CurrencySymbol string `json:"currency_symbol"`
|
CurrencySymbol string `json:"currency_symbol"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DailyTrafficStats struct {
|
||||||
|
Date string `json:"date"`
|
||||||
|
Upload int64 `json:"upload"`
|
||||||
|
Download int64 `json:"download"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
type DeleteAdsRequest struct {
|
type DeleteAdsRequest struct {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
}
|
}
|
||||||
@ -1277,6 +1289,18 @@ type GetUserTicketListResponse struct {
|
|||||||
List []Ticket `json:"list"`
|
List []Ticket `json:"list"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GetUserTrafficStatsRequest struct {
|
||||||
|
UserSubscribeId string `form:"user_subscribe_id" validate:"required"`
|
||||||
|
Days int `form:"days" validate:"required,oneof=7 30"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetUserTrafficStatsResponse struct {
|
||||||
|
List []DailyTrafficStats `json:"list"`
|
||||||
|
TotalUpload int64 `json:"total_upload"`
|
||||||
|
TotalDownload int64 `json:"total_download"`
|
||||||
|
TotalTraffic int64 `json:"total_traffic"`
|
||||||
|
}
|
||||||
|
|
||||||
type GiftLog struct {
|
type GiftLog struct {
|
||||||
Type uint16 `json:"type"`
|
Type uint16 `json:"type"`
|
||||||
UserId int64 `json:"user_id"`
|
UserId int64 `json:"user_id"`
|
||||||
@ -1425,16 +1449,20 @@ type NodeDNS struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type NodeGroup struct {
|
type NodeGroup struct {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Sort int `json:"sort"`
|
Sort int `json:"sort"`
|
||||||
ForCalculation bool `json:"for_calculation"`
|
ForCalculation bool `json:"for_calculation"`
|
||||||
MinTrafficGB int64 `json:"min_traffic_gb,omitempty"`
|
IsExpiredGroup bool `json:"is_expired_group"`
|
||||||
MaxTrafficGB int64 `json:"max_traffic_gb,omitempty"`
|
ExpiredDaysLimit int `json:"expired_days_limit"`
|
||||||
NodeCount int64 `json:"node_count,omitempty"`
|
MaxTrafficGBExpired int64 `json:"max_traffic_gb_expired,omitempty"`
|
||||||
CreatedAt int64 `json:"created_at"`
|
SpeedLimit int `json:"speed_limit"`
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type NodeGroupItem struct {
|
type NodeGroupItem struct {
|
||||||
@ -2299,6 +2327,7 @@ type Subscribe struct {
|
|||||||
NodeTags []string `json:"node_tags"`
|
NodeTags []string `json:"node_tags"`
|
||||||
NodeGroupIds []int64 `json:"node_group_ids,omitempty"`
|
NodeGroupIds []int64 `json:"node_group_ids,omitempty"`
|
||||||
NodeGroupId int64 `json:"node_group_id"`
|
NodeGroupId int64 `json:"node_group_id"`
|
||||||
|
TrafficLimit []TrafficLimit `json:"traffic_limit"`
|
||||||
Show bool `json:"show"`
|
Show bool `json:"show"`
|
||||||
Sell bool `json:"sell"`
|
Sell bool `json:"sell"`
|
||||||
Sort int64 `json:"sort"`
|
Sort int64 `json:"sort"`
|
||||||
@ -2492,6 +2521,13 @@ type TosConfig struct {
|
|||||||
TosContent string `json:"tos_content"`
|
TosContent string `json:"tos_content"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TrafficLimit struct {
|
||||||
|
StatType string `json:"stat_type"`
|
||||||
|
StatValue int64 `json:"stat_value"`
|
||||||
|
TrafficUsage int64 `json:"traffic_usage"`
|
||||||
|
SpeedLimit int64 `json:"speed_limit"`
|
||||||
|
}
|
||||||
|
|
||||||
type TrafficLog struct {
|
type TrafficLog struct {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
ServerId int64 `json:"server_id"`
|
ServerId int64 `json:"server_id"`
|
||||||
@ -2629,13 +2665,17 @@ type UpdateGroupConfigRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UpdateNodeGroupRequest struct {
|
type UpdateNodeGroupRequest struct {
|
||||||
Id int64 `json:"id" validate:"required"`
|
Id int64 `json:"id" validate:"required"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Sort int `json:"sort"`
|
Sort int `json:"sort"`
|
||||||
ForCalculation *bool `json:"for_calculation"`
|
ForCalculation *bool `json:"for_calculation"`
|
||||||
MinTrafficGB *int64 `json:"min_traffic_gb,omitempty"`
|
IsExpiredGroup *bool `json:"is_expired_group"`
|
||||||
MaxTrafficGB *int64 `json:"max_traffic_gb,omitempty"`
|
ExpiredDaysLimit *int `json:"expired_days_limit"`
|
||||||
|
MaxTrafficGBExpired *int64 `json:"max_traffic_gb_expired,omitempty"`
|
||||||
|
SpeedLimit *int `json:"speed_limit"`
|
||||||
|
MinTrafficGB *int64 `json:"min_traffic_gb,omitempty"`
|
||||||
|
MaxTrafficGB *int64 `json:"max_traffic_gb,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateNodeRequest struct {
|
type UpdateNodeRequest struct {
|
||||||
@ -2727,6 +2767,7 @@ type UpdateSubscribeRequest struct {
|
|||||||
NodeTags []string `json:"node_tags"`
|
NodeTags []string `json:"node_tags"`
|
||||||
NodeGroupIds []int64 `json:"node_group_ids,omitempty"`
|
NodeGroupIds []int64 `json:"node_group_ids,omitempty"`
|
||||||
NodeGroupId int64 `json:"node_group_id"`
|
NodeGroupId int64 `json:"node_group_id"`
|
||||||
|
TrafficLimit []TrafficLimit `json:"traffic_limit"`
|
||||||
Show *bool `json:"show"`
|
Show *bool `json:"show"`
|
||||||
Sell *bool `json:"sell"`
|
Sell *bool `json:"sell"`
|
||||||
Sort int64 `json:"sort"`
|
Sort int64 `json:"sort"`
|
||||||
@ -2907,6 +2948,7 @@ type UserStatisticsResponse struct {
|
|||||||
|
|
||||||
type UserSubscribe struct {
|
type UserSubscribe struct {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
|
IdStr string `json:"id_str"`
|
||||||
UserId int64 `json:"user_id"`
|
UserId int64 `json:"user_id"`
|
||||||
OrderId int64 `json:"order_id"`
|
OrderId int64 `json:"order_id"`
|
||||||
SubscribeId int64 `json:"subscribe_id"`
|
SubscribeId int64 `json:"subscribe_id"`
|
||||||
|
|||||||
@ -351,18 +351,20 @@ func (l *ActivateOrderLogic) getSubscribeInfo(ctx context.Context, subscribeId i
|
|||||||
func (l *ActivateOrderLogic) createUserSubscription(ctx context.Context, orderInfo *order.Order, sub *subscribe.Subscribe) (*user.Subscribe, error) {
|
func (l *ActivateOrderLogic) createUserSubscription(ctx context.Context, orderInfo *order.Order, sub *subscribe.Subscribe) (*user.Subscribe, error) {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
userSub := &user.Subscribe{
|
userSub := &user.Subscribe{
|
||||||
UserId: orderInfo.UserId,
|
UserId: orderInfo.UserId,
|
||||||
OrderId: orderInfo.Id,
|
OrderId: orderInfo.Id,
|
||||||
SubscribeId: orderInfo.SubscribeId,
|
SubscribeId: orderInfo.SubscribeId,
|
||||||
StartTime: now,
|
StartTime: now,
|
||||||
ExpireTime: tool.AddTime(sub.UnitTime, orderInfo.Quantity, now),
|
ExpireTime: tool.AddTime(sub.UnitTime, orderInfo.Quantity, now),
|
||||||
Traffic: sub.Traffic,
|
Traffic: sub.Traffic,
|
||||||
Download: 0,
|
Download: 0,
|
||||||
Upload: 0,
|
Upload: 0,
|
||||||
Token: uuidx.SubscribeToken(orderInfo.OrderNo),
|
ExpiredDownload: 0,
|
||||||
UUID: uuid.New().String(),
|
ExpiredUpload: 0,
|
||||||
Status: 1,
|
Token: uuidx.SubscribeToken(orderInfo.OrderNo),
|
||||||
NodeGroupId: sub.NodeGroupId, // Inherit node_group_id from subscription plan
|
UUID: uuid.New().String(),
|
||||||
|
Status: 1,
|
||||||
|
NodeGroupId: sub.NodeGroupId, // Inherit node_group_id from subscription plan
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check quota limit before creating subscription (final safeguard)
|
// Check quota limit before creating subscription (final safeguard)
|
||||||
@ -650,6 +652,9 @@ func (l *ActivateOrderLogic) updateSubscriptionForRenewal(ctx context.Context, u
|
|||||||
|
|
||||||
userSub.ExpireTime = tool.AddTime(sub.UnitTime, orderInfo.Quantity, userSub.ExpireTime)
|
userSub.ExpireTime = tool.AddTime(sub.UnitTime, orderInfo.Quantity, userSub.ExpireTime)
|
||||||
userSub.Status = 1
|
userSub.Status = 1
|
||||||
|
// 续费时重置过期流量字段
|
||||||
|
userSub.ExpiredDownload = 0
|
||||||
|
userSub.ExpiredUpload = 0
|
||||||
|
|
||||||
if err := l.svc.UserModel.UpdateSubscribe(ctx, userSub); err != nil {
|
if err := l.svc.UserModel.UpdateSubscribe(ctx, userSub); err != nil {
|
||||||
logger.WithContext(ctx).Error("Update user subscribe failed", logger.Field("error", err.Error()))
|
logger.WithContext(ctx).Error("Update user subscribe failed", logger.Field("error", err.Error()))
|
||||||
@ -674,6 +679,8 @@ func (l *ActivateOrderLogic) ResetTraffic(ctx context.Context, orderInfo *order.
|
|||||||
// Reset traffic
|
// Reset traffic
|
||||||
userSub.Download = 0
|
userSub.Download = 0
|
||||||
userSub.Upload = 0
|
userSub.Upload = 0
|
||||||
|
userSub.ExpiredDownload = 0
|
||||||
|
userSub.ExpiredUpload = 0
|
||||||
userSub.Status = 1
|
userSub.Status = 1
|
||||||
|
|
||||||
if err := l.svc.UserModel.UpdateSubscribe(ctx, userSub); err != nil {
|
if err := l.svc.UserModel.UpdateSubscribe(ctx, userSub); err != nil {
|
||||||
|
|||||||
@ -98,11 +98,13 @@ func (l *TrafficStatisticsLogic) ProcessTask(ctx context.Context, task *asynq.Ta
|
|||||||
// update user subscribe with log
|
// update user subscribe with log
|
||||||
d := int64(float32(log.Download) * ratio * realTimeMultiplier)
|
d := int64(float32(log.Download) * ratio * realTimeMultiplier)
|
||||||
u := int64(float32(log.Upload) * ratio * realTimeMultiplier)
|
u := int64(float32(log.Upload) * ratio * realTimeMultiplier)
|
||||||
if err := l.svc.UserModel.UpdateUserSubscribeWithTraffic(ctx, sub.Id, d, u); err != nil {
|
isExpired := now.After(sub.ExpireTime)
|
||||||
|
if err := l.svc.UserModel.UpdateUserSubscribeWithTraffic(ctx, sub.Id, d, u, isExpired); err != nil {
|
||||||
logger.WithContext(ctx).Error("[TrafficStatistics] Update user subscribe with log failed",
|
logger.WithContext(ctx).Error("[TrafficStatistics] Update user subscribe with log failed",
|
||||||
logger.Field("sid", log.SID),
|
logger.Field("sid", log.SID),
|
||||||
logger.Field("download", float32(log.Download)*ratio),
|
logger.Field("download", float32(log.Download)*ratio),
|
||||||
logger.Field("upload", float32(log.Upload)*ratio),
|
logger.Field("upload", float32(log.Upload)*ratio),
|
||||||
|
logger.Field("is_expired", isExpired),
|
||||||
logger.Field("error", err.Error()),
|
logger.Field("error", err.Error()),
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user