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(q *q.Query, resourceId int32, count int, 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 } 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 } func lockChans(batch string, count int, expire time.Time) ([]netip.AddrPort, error) { chans, err := g.Redis.Eval( context.Background(), RedisScriptLockChans, []string{ "channel:chans", "channel:lease:" + 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 chans_key = KEYS[1] local lease_key = KEYS[2] local count = tonumber(ARGV[1]) local expire = tonumber(ARGV[2]) if redis.call("SCARD", chans_key) < count then return nil end local ports = redis.call("SPOP", chans_key, count) redis.call("SET", lease_key, cjson.encode({ p = ports, e = expire })) 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{ "channel:chans", "channel:lease:" + batch, }, values..., ).Err() if err != nil { return core.NewBizErr("释放通道失败", err) } return nil } var RedisScriptFreeChans = ` local chans_key = KEYS[1] local lease_key = KEYS[2] local chans = ARGV redis.call("SADD", chans_key, unpack(chans)) redis.call("DEL", lease_key) return chans ` func registerChans(chans []netip.AddrPort) error { strs := make([]string, len(chans)) for i, ch := range chans { strs[i] = ch.String() } err := g.Redis.SAdd(context.Background(), "channel:chans", strs).Err() if err != nil { return core.NewBizErr("注册通道失败", err) } return nil } // 错误信息 var ( ErrResourceNotExist = core.NewBizErr("套餐不存在") ErrResourceInvalid = core.NewBizErr("套餐不可用") ErrResourceExhausted = core.NewBizErr("套餐已用完") ErrResourceExpired = core.NewBizErr("套餐已过期") ErrResourceDailyLimit = core.NewBizErr("套餐每日配额已用完") ErrEdgesNoAvailable = core.NewBizErr("没有可用的节点") )