Files
platform/web/services/channel.go

308 lines
6.3 KiB
Go

package services
import (
"context"
"math/rand/v2"
"net/netip"
"platform/web/core"
g "platform/web/globals"
m "platform/web/models"
q "platform/web/queries"
"time"
"gorm.io/gen/field"
)
var Channel ChannelService = &channelBaiyinService{}
// 通道服务
type ChannelService interface {
CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter ...EdgeFilter) ([]*m.Channel, error)
RemoveChannels(batch string, ids []int32) error
}
// 授权方式
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) (*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
}
var info = &ResourceView{
Id: resource.ID,
Active: resource.Active,
Type: resource.Type,
User: resource.User,
}
switch resource.Type {
case m.ResourceTypeShort:
var sub = resource.Short
var dailyLast = time.Time{}
if sub.DailyLast != nil {
dailyLast = time.Time(*sub.DailyLast)
}
var expire = time.Time{}
if sub.Expire != nil {
expire = time.Time(*sub.Expire)
}
var quota int32
if sub.Quota != nil {
quota = *sub.Quota
}
info.Mode = sub.Type
info.Live = time.Duration(sub.Live) * time.Second
info.DailyLimit = sub.DailyLimit
info.DailyUsed = sub.DailyUsed
info.DailyLast = dailyLast
info.Expire = expire
info.Quota = quota
info.Used = sub.Used
case m.ResourceTypeLong:
var sub = resource.Long
var dailyLast = time.Time{}
if sub.DailyLast != nil {
dailyLast = time.Time(*sub.DailyLast)
}
var expire = time.Time{}
if sub.Expire != nil {
expire = time.Time(*sub.Expire)
}
var quota int32
if sub.Quota != nil {
quota = *sub.Quota
}
info.Mode = sub.Type
info.Live = time.Duration(sub.Live) * time.Hour * 24
info.DailyLimit = sub.DailyLimit
info.DailyUsed = sub.DailyUsed
info.DailyLast = dailyLast
info.Expire = expire
info.Quota = quota
info.Used = sub.Used
}
return info, nil
}
// ResourceView 套餐数据的简化视图,便于直接获取主要数据
type ResourceView struct {
Id int32
Active bool
Type m.ResourceType
Mode m.ResourceMode
Live time.Duration
DailyLimit int32
DailyUsed int32
DailyLast time.Time
Quota int32
Used int32
Expire time.Time
User m.User
}
var (
allChansKey = "channel:all"
freeChansKey = "channel:free"
usedChansKey = "channel:used"
)
// 取用通道
func lockChans(batch string, count int, expire time.Time) ([]netip.AddrPort, error) {
chans, err := g.Redis.Eval(
context.Background(),
RedisScriptLockChans,
[]string{
freeChansKey,
usedChansKey,
usedChansKey + ":" + batch,
},
count,
expire.Unix(),
).StringSlice()
if err != nil {
return nil, core.NewBizErr("获取通道失败", err)
}
addrs := make([]netip.AddrPort, len(chans))
for i, ch := range chans {
addr, err := netip.ParseAddrPort(ch)
if err != nil {
return nil, core.NewServErr("解析通道数据失败", err)
}
addrs[i] = addr
}
return addrs, nil
}
var RedisScriptLockChans = `
local free_key = KEYS[1]
local used_key = KEYS[2]
local batch_key = KEYS[3]
local count = tonumber(ARGV[1])
local expire = tonumber(ARGV[2])
if redis.call("SCARD", free_key) < count then
return nil
end
local ports = redis.call("SPOP", free_key, count)
redis.call("ZADD", used_key, expire, batch_key)
redis.call("RPUSH", batch_key, unpack(ports))
return ports
`
// 归还通道
func freeChans(batch string, chans []string) error {
values := make([]any, len(chans))
for i, ch := range chans {
values[i] = ch
}
err := g.Redis.Eval(
context.Background(),
RedisScriptFreeChans,
[]string{
freeChansKey,
usedChansKey,
usedChansKey + ":" + batch,
allChansKey,
},
values...,
).Err()
if err != nil {
return core.NewBizErr("释放通道失败", err)
}
return nil
}
var RedisScriptFreeChans = `
local free_key = KEYS[1]
local used_key = KEYS[2]
local batch_key = KEYS[3]
local all_key = KEYS[4]
local chans = ARGV
local count = 0
for i, chan in ipairs(chans) do
if redis.call("SISMEMBER", all_key, chan) == 1 then
redis.call("SADD", free_key, chan)
count = count + 1
end
end
redis.call("ZREM", used_key, batch_key)
redis.call("DEL", batch_key)
return count
`
// 扩容通道
func addChans(chans []netip.AddrPort) error {
strs := make([]string, len(chans))
for i, ch := range chans {
strs[i] = ch.String()
}
err := g.Redis.Eval(
context.Background(),
RedisScriptAddChans,
[]string{
freeChansKey,
allChansKey,
},
strs,
).Err()
if err != nil {
return core.NewBizErr("扩容通道失败", err)
}
return nil
}
var RedisScriptAddChans = `
local free_key = KEYS[1]
local all_key = KEYS[2]
local chans = ARGV
redis.call("SADD", free_key, unpack(chans))
redis.call("SADD", all_key, unpack(chans))
return 1
`
// 缩容通道
func removeChans(chans []string) error {
err := g.Redis.Eval(
context.Background(),
RedisScriptRemoveChans,
[]string{
freeChansKey,
allChansKey,
},
chans,
).Err()
if err != nil {
return core.NewBizErr("缩容通道失败", err)
}
return nil
}
var RedisScriptRemoveChans = `
local free_key = KEYS[1]
local all_key = KEYS[2]
local chans = ARGV
redis.call("SREM", free_key, unpack(chans))
redis.call("SREM", all_key, unpack(chans))
return 1
`
// 错误信息
var (
ErrResourceNotExist = core.NewBizErr("套餐不存在")
ErrResourceInvalid = core.NewBizErr("套餐不可用")
ErrResourceExhausted = core.NewBizErr("套餐已用完")
ErrResourceExpired = core.NewBizErr("套餐已过期")
ErrResourceDailyLimit = core.NewBizErr("套餐每日配额已用完")
ErrEdgesNoAvailable = core.NewBizErr("没有可用的节点")
)