Files

328 lines
7.5 KiB
Go
Raw Permalink Normal View History

2025-03-25 09:49:56 +08:00
package services
import (
"context"
"errors"
"fmt"
"math/rand/v2"
"net/netip"
"platform/pkg/u"
"platform/web/core"
g "platform/web/globals"
2025-05-08 19:02:07 +08:00
m "platform/web/models"
2025-03-25 09:49:56 +08:00
q "platform/web/queries"
"strconv"
"time"
"github.com/redis/go-redis/v9"
"gorm.io/gen/field"
2025-03-25 09:49:56 +08:00
)
// 通道服务
var Channel = &channelServer{
2025-12-18 14:22:56 +08:00
provider: &channelBaiyinProvider{},
}
2025-12-18 14:22:56 +08:00
type ChannelServiceProvider interface {
2026-04-17 16:27:29 +08:00
CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter *EdgeFilter) ([]*m.Channel, error)
RemoveChannels(batch string) error
}
type channelServer struct {
2025-12-18 14:22:56 +08:00
provider ChannelServiceProvider
}
2026-04-17 16:27:29 +08:00
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
2025-04-01 10:51:32 +08:00
const (
ChannelAuthTypeIp ChannelAuthType = iota + 1
ChannelAuthTypePass
)
2025-04-01 10:51:32 +08:00
2025-12-05 16:52:40 +08:00
// 生成用户名和密码对
func genPassPair() (string, string) {
var alphabet = []rune("abcdefghjkmnpqrstuvwxyz")
var numbers = []rune("23456789")
2025-04-01 10:51:32 +08:00
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))]
2025-03-29 11:13:10 +08:00
}
password[i] = numbers[rand.N(len(numbers))]
}
return string(username), string(password)
}
2025-12-05 16:52:40 +08:00
// 查找资源
func findResource(resourceId int32, now time.Time) (*ResourceView, error) {
resource, err := q.Resource.
Preload(field.Associations).
2025-05-08 19:02:07 +08:00
Where(
q.Resource.ID.Eq(resourceId),
q.Resource.Active.Is(true),
2025-05-08 19:02:07 +08:00
).
Take()
2025-05-08 19:02:07 +08:00
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
2026-04-15 16:56:24 +08:00
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
}
2025-12-05 16:52:40 +08:00
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))
2026-04-17 16:27:29 +08:00
err := g.Redis.Del(context.Background(), key).Err()
if err != nil {
return fmt.Errorf("缩容通道失败: %w", err)
}
return nil
}
2025-12-05 16:52:40 +08:00
// 取用通道
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,
2025-12-01 12:42:51 +08:00
).StringSlice()
2025-05-08 19:02:07 +08:00
if err != nil {
return nil, fmt.Errorf("获取通道失败: %w", err)
}
2025-05-08 19:02:07 +08:00
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
2025-05-08 19:02:07 +08:00
}
var RedisScriptLockChans = redis.NewScript(`
2026-04-17 16:27:29 +08:00
local free_key = KEYS[1]
local batch_key = KEYS[2]
local count = tonumber(ARGV[1])
2026-04-17 16:27:29 +08:00
local free_count = redis.call("SCARD", free_key)
if count <= 0 or free_count < count then
return nil
end
2025-12-05 16:52:40 +08:00
local ports = redis.call("SPOP", free_key, count)
redis.call("RPUSH", batch_key, unpack(ports))
return ports
`)
2025-12-05 16:52:40 +08:00
// 归还通道
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()
2025-05-08 19:02:07 +08:00
if err != nil {
return core.NewBizErr("释放通道失败", err)
2025-05-08 19:02:07 +08:00
}
return nil
}
var RedisScriptFreeChans = redis.NewScript(`
2026-04-17 16:27:29 +08:00
local free_key = KEYS[1]
local batch_key = KEYS[2]
local chans = redis.call("LRANGE", batch_key, 0, -1)
2026-04-17 16:27:29 +08:00
if #chans == 0 then
return 1
2025-12-05 18:57:52 +08:00
end
2025-12-05 16:52:40 +08:00
2026-04-17 16:27:29 +08:00
redis.call("SADD", free_key, unpack(chans))
redis.call("DEL", batch_key)
2025-12-05 16:52:40 +08:00
return 1
`)
2025-12-05 16:52:40 +08:00
// 错误信息
var (
ErrResourceNotExist = core.NewBizErr("套餐不存在")
ErrResourceInvalid = core.NewBizErr("套餐不可用")
ErrResourceExhausted = core.NewBizErr("套餐已用完")
ErrResourceExpired = core.NewBizErr("套餐已过期")
ErrResourceDailyLimit = core.NewBizErr("套餐每日配额已用完")
ErrEdgesNoAvailable = core.NewBizErr("没有可用的节点")
2025-05-08 19:02:07 +08:00
)