package services import ( "context" "errors" "fmt" "math/rand/v2" "net/netip" "platform/pkg/u" "platform/web/core" g "platform/web/globals" m "platform/web/models" q "platform/web/queries" "strconv" "time" "github.com/redis/go-redis/v9" "gorm.io/gen/field" ) // 通道服务 var Channel = &channelServer{ provider: &channelBaiyinProvider{}, } type ChannelServiceProvider interface { CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter *EdgeFilter) ([]*m.Channel, error) RemoveChannels(batch string) error } type channelServer struct { provider ChannelServiceProvider } func (s *channelServer) CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter *EdgeFilter) ([]*m.Channel, error) { return s.provider.CreateChannels(source, resourceId, authWhitelist, authPassword, count, edgeFilter) } func (s *channelServer) RemoveChannels(batch string) error { return s.provider.RemoveChannels(batch) } // 授权方式 type ChannelAuthType int const ( ChannelAuthTypeIp ChannelAuthType = iota + 1 ChannelAuthTypePass ) // 生成用户名和密码对 func genPassPair() (string, string) { var alphabet = []rune("abcdefghjkmnpqrstuvwxyz") var numbers = []rune("23456789") var username = make([]rune, 6) var password = make([]rune, 6) for i := range 6 { if i < 2 { username[i] = alphabet[rand.N(len(alphabet))] } else { username[i] = numbers[rand.N(len(numbers))] } password[i] = numbers[rand.N(len(numbers))] } return string(username), string(password) } // 查找资源 func findResource(resourceId int32, now time.Time) (*ResourceView, error) { resource, err := q.Resource. Preload(field.Associations). Where( q.Resource.ID.Eq(resourceId), q.Resource.Active.Is(true), ). Take() if err != nil { return nil, ErrResourceNotExist } if resource.User == nil { return nil, ErrResourceNotExist } var info = &ResourceView{ Id: resource.ID, User: *resource.User, Active: resource.Active, Type: resource.Type, } switch resource.Type { case m.ResourceTypeShort: var sub = resource.Short info.ShortId = &sub.ID info.ExpireAt = sub.ExpireAt info.Live = time.Duration(sub.Live) * time.Minute info.Mode = sub.Type info.Quota = sub.Quota info.Used = sub.Used info.Daily = sub.Daily info.LastAt = sub.LastAt if sub.LastAt != nil && u.IsSameDate(*sub.LastAt, now) { info.Today = int(sub.Daily) } case m.ResourceTypeLong: var sub = resource.Long info.LongId = &sub.ID info.ExpireAt = sub.ExpireAt info.Live = time.Duration(sub.Live) * time.Hour info.Mode = sub.Type info.Quota = sub.Quota info.Used = sub.Used info.Daily = sub.Daily info.LastAt = sub.LastAt if sub.LastAt != nil && u.IsSameDate(*sub.LastAt, now) { info.Today = int(sub.Daily) } } if info.Mode == m.ResourceModeTime && info.ExpireAt == nil { return nil, errors.New("检查套餐获取时间失败") } return info, nil } // ResourceView 套餐数据的简化视图,便于直接获取主要数据 type ResourceView struct { Id int32 User m.User Active bool Type m.ResourceType ShortId *int32 LongId *int32 Live time.Duration Mode m.ResourceMode Quota int32 ExpireAt *time.Time Used int32 Daily int32 LastAt *time.Time Today int // 今日用量 } // 检查用户是否可提取 func ensure(now time.Time, source netip.Addr, resourceId int32, count int) (*ResourceView, []string, error) { if count > 400 { return nil, nil, core.NewBizErr("单次最多提取 400 个") } // 获取用户套餐 resource, err := findResource(resourceId, now) if err != nil { return nil, nil, err } // 检查用户 user := resource.User if user.IDToken == nil || *user.IDToken == "" { return nil, nil, core.NewBizErr("账号未实名") } // 获取用户白名单并检查用户 ip 地址 whitelists, err := q.Whitelist.Where( q.Whitelist.UserID.Eq(user.ID), ).Find() if err != nil { return nil, nil, err } ips := make([]string, len(whitelists)) pass := false for i, item := range whitelists { ips[i] = item.IP.String() if item.IP.Addr == source { pass = true } } if !pass { return nil, nil, core.NewBizErr(fmt.Sprintf("IP 地址 %s 不在白名单内", source.String())) } // 检查套餐使用情况 switch resource.Mode { default: return nil, nil, core.NewBizErr("不支持的套餐模式") // 包时 case m.ResourceModeTime: // 检查过期时间 if resource.ExpireAt.Before(now) { return nil, nil, ErrResourceExpired } // 检查每日限额 if count+resource.Today > int(resource.Quota) { return nil, nil, ErrResourceDailyLimit } // 包量 case m.ResourceModeQuota: // 检查可用配额 if int(resource.Quota)-int(resource.Used) < count { return nil, nil, ErrResourceExhausted } } return resource, ips, nil } var ( freeChansKey = "channel:free" usedChansKey = "channel:used" ) // 扩容通道 func regChans(proxy int32, chans []netip.AddrPort) error { strs := make([]any, len(chans)) for i, ch := range chans { strs[i] = ch.String() } key := freeChansKey + ":" + strconv.Itoa(int(proxy)) err := g.Redis.SAdd(context.Background(), key, strs...).Err() if err != nil { return fmt.Errorf("扩容通道失败: %w", err) } return nil } // 缩容通道 func remChans(proxy int32) error { key := freeChansKey + ":" + strconv.Itoa(int(proxy)) err := g.Redis.Del(context.Background(), key).Err() if err != nil { return fmt.Errorf("缩容通道失败: %w", err) } return nil } // 取用通道 func lockChans(proxy int32, batch string, count int) ([]netip.AddrPort, error) { pid := strconv.Itoa(int(proxy)) chans, err := RedisScriptLockChans.Run( context.Background(), g.Redis, []string{ freeChansKey + ":" + pid, usedChansKey + ":" + pid + ":" + batch, }, count, ).StringSlice() if err != nil { return nil, fmt.Errorf("获取通道失败: %w", err) } addrs := make([]netip.AddrPort, len(chans)) for i, ch := range chans { addr, err := netip.ParseAddrPort(ch) if err != nil { return nil, fmt.Errorf("解析通道数据失败: %w", err) } addrs[i] = addr } return addrs, nil } var RedisScriptLockChans = redis.NewScript(` local free_key = KEYS[1] local batch_key = KEYS[2] local count = tonumber(ARGV[1]) local free_count = redis.call("SCARD", free_key) if count <= 0 or free_count < count then return nil end local ports = redis.call("SPOP", free_key, count) redis.call("RPUSH", batch_key, unpack(ports)) return ports `) // 归还通道 func freeChans(proxy int32, batch string) error { pid := strconv.Itoa(int(proxy)) err := RedisScriptFreeChans.Run( context.Background(), g.Redis, []string{ freeChansKey + ":" + pid, usedChansKey + ":" + pid + ":" + batch, }, ).Err() if err != nil { return core.NewBizErr("释放通道失败", err) } return nil } var RedisScriptFreeChans = redis.NewScript(` local free_key = KEYS[1] local batch_key = KEYS[2] local chans = redis.call("LRANGE", batch_key, 0, -1) if #chans == 0 then return 1 end redis.call("SADD", free_key, unpack(chans)) redis.call("DEL", batch_key) return 1 `) // 错误信息 var ( ErrResourceNotExist = core.NewBizErr("套餐不存在") ErrResourceInvalid = core.NewBizErr("套餐不可用") ErrResourceExhausted = core.NewBizErr("套餐已用完") ErrResourceExpired = core.NewBizErr("套餐已过期") ErrResourceDailyLimit = core.NewBizErr("套餐每日配额已用完") ErrEdgesNoAvailable = core.NewBizErr("没有可用的节点") )