- Node group CRUD operations with traffic-based filtering - Three grouping modes: average distribution, subscription-based, and traffic-based - Automatic and manual group recalculation with history tracking - Group assignment preview before applying changes - User subscription group locking to prevent automatic reassignment - Subscribe-to-group mapping configuration - Group calculation history and detailed reports - System configuration for group management (enabled/mode/auto_create) Database: - Add node_group table for group definitions - Add group_history and group_history_detail tables for tracking - Add node_group_ids (JSON) to nodes and subscribe tables - Add node_group_id and group_locked fields to user_subscribe table - Add migration files for schema changes
143 lines
4.7 KiB
Go
143 lines
4.7 KiB
Go
package user
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/perfect-panel/server/internal/logic/admin/group"
|
|
"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/logger"
|
|
"github.com/perfect-panel/server/pkg/uuidx"
|
|
"github.com/perfect-panel/server/pkg/xerr"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
type CreateUserSubscribeLogic struct {
|
|
logger.Logger
|
|
ctx context.Context
|
|
svcCtx *svc.ServiceContext
|
|
}
|
|
|
|
// Create user subcribe
|
|
func NewCreateUserSubscribeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateUserSubscribeLogic {
|
|
return &CreateUserSubscribeLogic{
|
|
Logger: logger.WithContext(ctx),
|
|
ctx: ctx,
|
|
svcCtx: svcCtx,
|
|
}
|
|
}
|
|
|
|
func (l *CreateUserSubscribeLogic) CreateUserSubscribe(req *types.CreateUserSubscribeRequest) error {
|
|
// validate user
|
|
userInfo, err := l.svcCtx.UserModel.FindOne(l.ctx, req.UserId)
|
|
if err != nil {
|
|
l.Errorw("FindOne error", logger.Field("error", err.Error()), logger.Field("userId", req.UserId))
|
|
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOne error: %v", err.Error())
|
|
}
|
|
subs, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, req.UserId)
|
|
if err != nil {
|
|
l.Errorw("QueryUserSubscribe error", logger.Field("error", err.Error()), logger.Field("userId", req.UserId))
|
|
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "QueryUserSubscribe error: %v", err.Error())
|
|
}
|
|
if len(subs) >= 1 && l.svcCtx.Config.Subscribe.SingleModel {
|
|
return errors.Wrapf(xerr.NewErrCode(xerr.SingleSubscribeModeExceedsLimit), "Single subscribe mode exceeds limit")
|
|
}
|
|
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, req.SubscribeId)
|
|
if err != nil {
|
|
l.Errorw("FindOne error", logger.Field("error", err.Error()), logger.Field("subscribeId", req.SubscribeId))
|
|
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOne error: %v", err.Error())
|
|
}
|
|
if req.Traffic == 0 {
|
|
req.Traffic = sub.Traffic
|
|
}
|
|
|
|
userSub := user.Subscribe{
|
|
UserId: req.UserId,
|
|
SubscribeId: req.SubscribeId,
|
|
StartTime: time.Now(),
|
|
ExpireTime: time.UnixMilli(req.ExpiredAt),
|
|
Traffic: req.Traffic,
|
|
Download: 0,
|
|
Upload: 0,
|
|
Token: uuidx.SubscribeToken(fmt.Sprintf("adminCreate:%d", time.Now().UnixMilli())),
|
|
UUID: uuid.New().String(),
|
|
NodeGroupId: sub.NodeGroupId,
|
|
Status: 1,
|
|
}
|
|
if err = l.svcCtx.UserModel.InsertSubscribe(l.ctx, &userSub); err != nil {
|
|
l.Errorw("InsertSubscribe error", logger.Field("error", err.Error()))
|
|
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "InsertSubscribe error: %v", err.Error())
|
|
}
|
|
|
|
// Trigger user group recalculation (runs in background)
|
|
go func() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
// Check if group management is enabled
|
|
var groupEnabled string
|
|
err := l.svcCtx.DB.Table("system").
|
|
Where("`category` = ? AND `key` = ?", "group", "enabled").
|
|
Select("value").
|
|
Scan(&groupEnabled).Error
|
|
if err != nil || groupEnabled != "true" && groupEnabled != "1" {
|
|
l.Debugf("Group management not enabled, skipping recalculation")
|
|
return
|
|
}
|
|
|
|
// Get the configured grouping mode
|
|
var groupMode string
|
|
err = l.svcCtx.DB.Table("system").
|
|
Where("`category` = ? AND `key` = ?", "group", "mode").
|
|
Select("value").
|
|
Scan(&groupMode).Error
|
|
if err != nil {
|
|
l.Errorw("Failed to get group mode", logger.Field("error", err.Error()))
|
|
return
|
|
}
|
|
|
|
// Validate group mode
|
|
if groupMode != "average" && groupMode != "subscribe" && groupMode != "traffic" {
|
|
l.Debugf("Invalid group mode (current: %s), skipping", groupMode)
|
|
return
|
|
}
|
|
|
|
// Trigger group recalculation with the configured mode
|
|
logic := group.NewRecalculateGroupLogic(ctx, l.svcCtx)
|
|
req := &types.RecalculateGroupRequest{
|
|
Mode: groupMode,
|
|
}
|
|
|
|
if err := logic.RecalculateGroup(req); err != nil {
|
|
l.Errorw("Failed to recalculate user group",
|
|
logger.Field("user_id", userInfo.Id),
|
|
logger.Field("error", err.Error()),
|
|
)
|
|
return
|
|
}
|
|
|
|
l.Infow("Successfully recalculated user group after admin created subscription",
|
|
logger.Field("user_id", userInfo.Id),
|
|
logger.Field("subscribe_id", userSub.Id),
|
|
logger.Field("mode", groupMode),
|
|
)
|
|
}()
|
|
|
|
err = l.svcCtx.UserModel.UpdateUserCache(l.ctx, userInfo)
|
|
if err != nil {
|
|
l.Errorw("UpdateUserCache error", logger.Field("error", err.Error()))
|
|
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "UpdateUserCache error: %v", err.Error())
|
|
}
|
|
|
|
err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, userSub.SubscribeId)
|
|
if err != nil {
|
|
logger.Errorw("ClearSubscribe error", logger.Field("error", err.Error()))
|
|
}
|
|
|
|
return nil
|
|
}
|