256 lines
5.6 KiB
Go
256 lines
5.6 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, userId int32, 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(q *q.Query, resourceId int32, userId int32, count int, now time.Time) (*ResourceView, error) {
|
|
resource, err := q.Resource.
|
|
Preload(field.Associations).
|
|
Where(
|
|
q.Resource.ID.Eq(resourceId),
|
|
q.Resource.UserID.Eq(userId),
|
|
q.Resource.Active.Is(true),
|
|
).
|
|
Take()
|
|
if err != nil {
|
|
return nil, ErrResourceNotExist
|
|
}
|
|
|
|
var info = &ResourceView{
|
|
Id: resource.ID,
|
|
Active: resource.Active,
|
|
Type: resource.Type,
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 检查套餐使用情况
|
|
switch info.Mode {
|
|
default:
|
|
return nil, core.NewBizErr("不支持的套餐模式")
|
|
|
|
// 包时
|
|
case m.ResourceModeTime:
|
|
// 检查过期时间
|
|
if info.Expire.Before(now) {
|
|
return nil, ErrResourceExpired
|
|
}
|
|
// 检查每日限额
|
|
used := 0
|
|
if now.Format("2006-01-02") == info.DailyLast.Format("2006-01-02") {
|
|
used = int(info.DailyUsed)
|
|
}
|
|
excess := used+count > int(info.DailyLimit)
|
|
if excess {
|
|
return nil, ErrResourceDailyLimit
|
|
}
|
|
|
|
// 包量
|
|
case m.ResourceModeQuota:
|
|
// 检查可用配额
|
|
if int(info.Quota)-int(info.Used) < count {
|
|
return nil, ErrResourceExhausted
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func lockChans(batch string, count int, expire time.Time) ([]netip.AddrPort, error) {
|
|
results, err := g.Redis.Eval(
|
|
context.Background(),
|
|
RedisScriptLockChans,
|
|
[]string{"channel"},
|
|
batch,
|
|
count,
|
|
expire.Unix(),
|
|
).Result()
|
|
if err != nil {
|
|
return nil, core.NewBizErr("获取通道失败", err)
|
|
}
|
|
|
|
chans, ok := results.([]string)
|
|
if !ok {
|
|
return nil, core.NewServErr("转换通道数据失败")
|
|
}
|
|
|
|
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 key = KEYS[1]
|
|
local batch = ARGV[1]
|
|
local count = tonumber(ARGV[2])
|
|
local expire = tonumber(ARGV[3])
|
|
|
|
local chans_key = key .. ":chans"
|
|
local lease_key = key .. ":lease:" .. batch
|
|
|
|
if redis.call("SCARD", key) < count then
|
|
return nil
|
|
end
|
|
|
|
local ports = redis.call("SPOP", key, count)
|
|
|
|
redis.call("SET", lease_key, cjson.encode({
|
|
p = ports,
|
|
e = expire
|
|
}))
|
|
|
|
return ports
|
|
`
|
|
|
|
func freeChans(batch string, chans []string) error {
|
|
err := g.Redis.Eval(
|
|
context.Background(),
|
|
RedisScriptFreeChans,
|
|
[]string{"channel"},
|
|
batch,
|
|
chans,
|
|
).Err()
|
|
if err != nil {
|
|
return core.NewBizErr("释放通道失败", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var RedisScriptFreeChans = `
|
|
local key = KEYS[1]
|
|
local batch = ARGV[1]
|
|
local chans = ARGV[2]
|
|
|
|
local chans_key = key .. ":chans"
|
|
local lease_key = key .. ":lease:" .. batch
|
|
|
|
redis.call("SADD", chans_key, unpack(chans))
|
|
|
|
redis.call("DEL", lease_key)
|
|
|
|
return chans
|
|
`
|
|
|
|
// 错误信息
|
|
var (
|
|
ErrResourceNotExist = core.NewBizErr("套餐不存在")
|
|
ErrResourceInvalid = core.NewBizErr("套餐不可用")
|
|
ErrResourceExhausted = core.NewBizErr("套餐已用完")
|
|
ErrResourceExpired = core.NewBizErr("套餐已过期")
|
|
ErrResourceDailyLimit = core.NewBizErr("套餐每日配额已用完")
|
|
ErrEdgesNoAvailable = core.NewBizErr("没有可用的节点")
|
|
)
|