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("没有可用的节点") )