2025-03-25 09:49:56 +08:00
|
|
|
|
package services
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2025-03-26 14:57:44 +08:00
|
|
|
|
"context"
|
2025-12-10 20:07:33 +08:00
|
|
|
|
"errors"
|
2025-12-08 14:22:30 +08:00
|
|
|
|
"fmt"
|
2026-06-08 17:24:55 +08:00
|
|
|
|
"log/slog"
|
2025-04-02 16:08:55 +08:00
|
|
|
|
"math/rand/v2"
|
2025-11-24 18:44:06 +08:00
|
|
|
|
"net/netip"
|
2025-12-10 20:07:33 +08:00
|
|
|
|
"platform/pkg/u"
|
2025-05-24 12:37:16 +08:00
|
|
|
|
"platform/web/core"
|
2026-06-08 17:24:55 +08:00
|
|
|
|
e "platform/web/events"
|
2025-04-16 14:01:30 +08:00
|
|
|
|
g "platform/web/globals"
|
2026-06-09 16:30:19 +08:00
|
|
|
|
"platform/web/globals/orm"
|
2025-05-08 19:02:07 +08:00
|
|
|
|
m "platform/web/models"
|
2025-03-25 09:49:56 +08:00
|
|
|
|
q "platform/web/queries"
|
2025-12-08 14:22:30 +08:00
|
|
|
|
"strconv"
|
2026-06-08 17:24:55 +08:00
|
|
|
|
"strings"
|
2025-03-26 14:57:44 +08:00
|
|
|
|
"time"
|
|
|
|
|
|
|
2026-06-08 17:24:55 +08:00
|
|
|
|
"github.com/hibiken/asynq"
|
2025-12-08 14:22:30 +08:00
|
|
|
|
"github.com/redis/go-redis/v9"
|
2026-06-09 16:30:19 +08:00
|
|
|
|
"gorm.io/gen"
|
2025-05-24 12:37:16 +08:00
|
|
|
|
"gorm.io/gen/field"
|
2025-03-25 09:49:56 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2025-11-24 18:44:06 +08:00
|
|
|
|
// 通道服务
|
2025-12-08 14:22:30 +08:00
|
|
|
|
var Channel = &channelServer{
|
2026-06-08 17:24:55 +08:00
|
|
|
|
provider: &channelGostProvider{},
|
2025-12-08 14:22:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 16:30:19 +08:00
|
|
|
|
type channelProvider interface {
|
|
|
|
|
|
selectProxy(count int) (*m.Proxy, error)
|
|
|
|
|
|
prepareCreate(ctx *channelCreateContext) (*channelCreateResult, error)
|
|
|
|
|
|
removeRemote(batchNo string, batch *usedChanBatch) error
|
2025-05-23 14:53:01 +08:00
|
|
|
|
}
|
2025-03-31 09:09:05 +08:00
|
|
|
|
|
2025-12-08 14:22:30 +08:00
|
|
|
|
type channelServer struct {
|
2026-06-09 16:30:19 +08:00
|
|
|
|
provider channelProvider
|
2025-12-08 14:22:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-23 13:50:52 +08:00
|
|
|
|
func (s *channelServer) CreateChannels(source netip.Addr, resourceNo string, authWhitelist bool, authPassword bool, count int, edgeFilter *EdgeFilter) ([]*m.Channel, error) {
|
2026-06-09 16:30:19 +08:00
|
|
|
|
now := time.Now()
|
|
|
|
|
|
batchNo := ID.GenReadable("bat")
|
|
|
|
|
|
var channels []*m.Channel
|
|
|
|
|
|
if edgeFilter == nil {
|
|
|
|
|
|
edgeFilter = &EdgeFilter{}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var whitelistText *string
|
|
|
|
|
|
err := g.Redsync.WithLock(lockChannelCreateKey(resourceNo), func() error {
|
|
|
|
|
|
resource, whitelists, err := ensure(now, source, resourceNo, authWhitelist, count)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if authWhitelist {
|
|
|
|
|
|
joined := strings.Join(whitelists, ",")
|
|
|
|
|
|
whitelistText = &joined
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
expire := now.Add(resource.Live)
|
|
|
|
|
|
proxy, err := s.provider.selectProxy(count)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ports, err := selectPorts(proxy.ID, batchNo, count, expire)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
createCtx := &channelCreateContext{
|
|
|
|
|
|
Now: now,
|
|
|
|
|
|
Source: source,
|
|
|
|
|
|
Resource: resource,
|
|
|
|
|
|
Proxy: proxy,
|
|
|
|
|
|
BatchNo: batchNo,
|
|
|
|
|
|
Ports: ports,
|
|
|
|
|
|
Expire: expire,
|
|
|
|
|
|
Count: count,
|
|
|
|
|
|
Filter: edgeFilter,
|
|
|
|
|
|
AuthWhitelist: authWhitelist,
|
|
|
|
|
|
AuthPassword: authPassword,
|
|
|
|
|
|
Whitelists: whitelists,
|
|
|
|
|
|
WhitelistText: whitelistText,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result, err := s.provider.prepareCreate(createCtx)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if result.applyRemote != nil {
|
|
|
|
|
|
if err := result.applyRemote(); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := persistChannelCreate(createCtx, result.Channels); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
channels = result.Channels
|
|
|
|
|
|
return nil
|
|
|
|
|
|
})
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return channels, nil
|
2025-12-08 14:22:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 16:17:57 +08:00
|
|
|
|
func (s *channelServer) RemoveChannels(batch string) error {
|
2026-06-09 16:30:19 +08:00
|
|
|
|
return g.Redsync.WithLock(lockChannelRemoveKey(batch), func() error {
|
|
|
|
|
|
start := time.Now()
|
|
|
|
|
|
|
|
|
|
|
|
usedBatch, err := findUsedChanBatch(batch)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if usedBatch == nil {
|
|
|
|
|
|
slog.Debug("通道为空,跳过清理", "batch", batch)
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err := s.provider.removeRemote(batch, usedBatch); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := freeChans(usedBatch.ProxyID, batch); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
slog.Debug("清除通道配置", "proxy", usedBatch.ProxyID, "batch", batch, "duration", time.Since(start).String())
|
|
|
|
|
|
return nil
|
|
|
|
|
|
})
|
2025-12-08 14:22:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 17:30:51 +08:00
|
|
|
|
func (s *channelServer) ClearExpiredChannels(proxyId int32) (int, error) {
|
2026-06-09 16:30:19 +08:00
|
|
|
|
batchSet, err := findExpiredChannelBatches(proxyId, time.Now())
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return 0, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
slog.Info("批量清理过期通道", "count", len(batchSet))
|
|
|
|
|
|
for batchNo := range batchSet {
|
|
|
|
|
|
if err := s.RemoveChannels(batchNo); err != nil {
|
|
|
|
|
|
slog.Error("清理过期通道失败", "batch", batchNo, "error", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return len(batchSet), nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type channelCreateContext struct {
|
|
|
|
|
|
Now time.Time
|
|
|
|
|
|
Source netip.Addr
|
|
|
|
|
|
Resource *ResourceView
|
|
|
|
|
|
Proxy *m.Proxy
|
|
|
|
|
|
BatchNo string
|
|
|
|
|
|
Ports []netip.AddrPort
|
|
|
|
|
|
Expire time.Time
|
|
|
|
|
|
Count int
|
|
|
|
|
|
Filter *EdgeFilter
|
|
|
|
|
|
AuthWhitelist bool
|
|
|
|
|
|
AuthPassword bool
|
|
|
|
|
|
Whitelists []string
|
|
|
|
|
|
WhitelistText *string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type channelCreateResult struct {
|
|
|
|
|
|
Channels []*m.Channel
|
|
|
|
|
|
applyRemote func() error
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func newBaseChannel(ctx *channelCreateContext, port uint16) *m.Channel {
|
|
|
|
|
|
return &m.Channel{
|
|
|
|
|
|
UserID: ctx.Resource.User.ID,
|
|
|
|
|
|
ResourceID: ctx.Resource.ID,
|
|
|
|
|
|
BatchNo: ctx.BatchNo,
|
|
|
|
|
|
ProxyID: ctx.Proxy.ID,
|
|
|
|
|
|
Host: u.Else(ctx.Proxy.Host, ctx.Proxy.IP.String()),
|
|
|
|
|
|
Port: port,
|
|
|
|
|
|
FilterISP: ctx.Filter.Isp,
|
|
|
|
|
|
FilterProv: ctx.Filter.Prov,
|
|
|
|
|
|
FilterCity: ctx.Filter.City,
|
|
|
|
|
|
ExpiredAt: ctx.Expire,
|
|
|
|
|
|
Proxy: ctx.Proxy,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func applyChannelAuth(ctx *channelCreateContext, channel *m.Channel) (username string, password string, ok bool) {
|
|
|
|
|
|
if ctx.AuthWhitelist {
|
|
|
|
|
|
channel.Whitelists = ctx.WhitelistText
|
|
|
|
|
|
}
|
|
|
|
|
|
if !ctx.AuthPassword {
|
|
|
|
|
|
return "", "", false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
username, password = genPassPair()
|
|
|
|
|
|
channel.Username = &username
|
|
|
|
|
|
channel.Password = &password
|
|
|
|
|
|
return username, password, true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func persistChannelCreate(ctx *channelCreateContext, channels []*m.Channel) error {
|
|
|
|
|
|
return q.Q.Transaction(func(tx *q.Query) error {
|
|
|
|
|
|
var (
|
|
|
|
|
|
result gen.ResultInfo
|
|
|
|
|
|
err error
|
|
|
|
|
|
)
|
|
|
|
|
|
switch ctx.Resource.Type {
|
|
|
|
|
|
case m.ResourceTypeShort:
|
|
|
|
|
|
result, err = tx.ResourceShort.
|
|
|
|
|
|
Where(
|
|
|
|
|
|
tx.ResourceShort.ID.Eq(*ctx.Resource.ShortId),
|
|
|
|
|
|
tx.ResourceShort.Used.Eq(ctx.Resource.Used),
|
|
|
|
|
|
tx.ResourceShort.Daily.Eq(ctx.Resource.Daily),
|
|
|
|
|
|
).
|
|
|
|
|
|
UpdateSimple(
|
|
|
|
|
|
tx.ResourceShort.Used.Add(int32(ctx.Count)),
|
|
|
|
|
|
tx.ResourceShort.Daily.Value(int32(ctx.Resource.Today+ctx.Count)),
|
|
|
|
|
|
tx.ResourceShort.LastAt.Value(ctx.Now),
|
|
|
|
|
|
)
|
|
|
|
|
|
case m.ResourceTypeLong:
|
|
|
|
|
|
result, err = tx.ResourceLong.
|
|
|
|
|
|
Where(
|
|
|
|
|
|
tx.ResourceLong.ID.Eq(*ctx.Resource.LongId),
|
|
|
|
|
|
tx.ResourceLong.Used.Eq(ctx.Resource.Used),
|
|
|
|
|
|
tx.ResourceLong.Daily.Eq(ctx.Resource.Daily),
|
|
|
|
|
|
).
|
|
|
|
|
|
UpdateSimple(
|
|
|
|
|
|
tx.ResourceLong.Used.Add(int32(ctx.Count)),
|
|
|
|
|
|
tx.ResourceLong.Daily.Value(int32(ctx.Resource.Today+ctx.Count)),
|
|
|
|
|
|
tx.ResourceLong.LastAt.Value(ctx.Now),
|
|
|
|
|
|
)
|
|
|
|
|
|
default:
|
|
|
|
|
|
return core.NewBizErr("套餐类型不正确,无法更新")
|
|
|
|
|
|
}
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return core.NewServErr("更新套餐使用记录失败", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if result.RowsAffected == 0 {
|
|
|
|
|
|
return core.NewBizErr("套餐状态已过期")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err := tx.Channel.Omit(field.AssociationFields).Create(channels...); err != nil {
|
|
|
|
|
|
return core.NewServErr("保存通道失败", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err := tx.LogsUserUsage.Create(&m.LogsUserUsage{
|
|
|
|
|
|
UserID: ctx.Resource.User.ID,
|
|
|
|
|
|
ResourceID: ctx.Resource.ID,
|
|
|
|
|
|
BatchNo: ctx.BatchNo,
|
|
|
|
|
|
Count: int32(ctx.Count),
|
|
|
|
|
|
ISP: u.X(ctx.Filter.Isp.String()),
|
|
|
|
|
|
Prov: ctx.Filter.Prov,
|
|
|
|
|
|
City: ctx.Filter.City,
|
|
|
|
|
|
IP: orm.Inet{Addr: ctx.Source},
|
|
|
|
|
|
Time: ctx.Now,
|
|
|
|
|
|
}); err != nil {
|
|
|
|
|
|
return core.NewServErr("保存用户使用记录失败", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func findExpiredChannelBatches(proxyId int32, now time.Time) (map[string]struct{}, error) {
|
|
|
|
|
|
keys, err := g.Redis.Keys(context.Background(), usedChansKey(proxyId, "*")).Result()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, core.NewServErr("查询使用中通道失败", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(keys) == 0 {
|
|
|
|
|
|
return map[string]struct{}{}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
batchList := make([]string, len(keys))
|
|
|
|
|
|
batchSet := make(map[string]struct{}, len(keys))
|
|
|
|
|
|
for i, key := range keys {
|
|
|
|
|
|
parsed, err := parseUsedChanKey(key)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
batchList[i] = parsed.BatchNo
|
|
|
|
|
|
batchSet[parsed.BatchNo] = struct{}{}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var batchQueried []struct{ BatchNo string }
|
|
|
|
|
|
err = q.Channel.
|
|
|
|
|
|
Select(q.Channel.BatchNo).
|
|
|
|
|
|
Where(
|
|
|
|
|
|
q.Channel.BatchNo.In(batchList...),
|
|
|
|
|
|
q.Channel.ExpiredAt.Gte(now.UTC()),
|
|
|
|
|
|
).
|
|
|
|
|
|
Group(q.Channel.BatchNo).
|
|
|
|
|
|
Scan(&batchQueried)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, core.NewServErr("查询过期通道失败", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, batch := range batchQueried {
|
|
|
|
|
|
delete(batchSet, batch.BatchNo)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return batchSet, nil
|
2026-05-07 14:58:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 17:24:55 +08:00
|
|
|
|
func lockChannelCreateKey(resourceNo string) string {
|
|
|
|
|
|
return fmt.Sprintf("platform:channel:create:%s", resourceNo)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func lockChannelRemoveKey(bid string) string {
|
|
|
|
|
|
return fmt.Sprintf("platform:batch:remove_expired:%s", bid)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func selectPorts(proxyId int32, batchNo string, count int, expire time.Time) ([]netip.AddrPort, error) {
|
|
|
|
|
|
chans, err := lockChans(proxyId, batchNo, count)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, core.NewBizErr("无可用通道,请稍后再试", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_, err = g.Asynq.Enqueue(
|
|
|
|
|
|
e.NewRemoveChannel(batchNo),
|
|
|
|
|
|
asynq.ProcessAt(expire),
|
|
|
|
|
|
)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, core.NewServErr("注册异步关闭通道任务失败", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return chans, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func selectProxyByType(proxyType m.ProxyType, count int) (*m.Proxy, error) {
|
|
|
|
|
|
proxies, err := q.Proxy.Where(
|
|
|
|
|
|
q.Proxy.Type.Eq(int(proxyType)),
|
|
|
|
|
|
q.Proxy.Status.Eq(int(m.ProxyStatusOnline)),
|
|
|
|
|
|
).Find()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, core.NewBizErr("获取可用代理失败", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(proxies) == 0 {
|
|
|
|
|
|
return nil, core.NewBizErr("无可用代理")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 16:30:19 +08:00
|
|
|
|
var bestProxy *m.Proxy
|
2026-06-08 17:24:55 +08:00
|
|
|
|
maxCount := -1
|
2026-06-09 16:30:19 +08:00
|
|
|
|
for _, proxy := range proxies {
|
|
|
|
|
|
idCount, err := g.Redis.SCard(context.Background(), freeChansKey(proxy.ID)).Result()
|
2026-06-08 17:24:55 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, core.NewServErr("查询可用通道数量失败", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if idCount > int64(maxCount) {
|
|
|
|
|
|
maxCount = int(idCount)
|
2026-06-09 16:30:19 +08:00
|
|
|
|
bestProxy = proxy
|
2026-06-08 17:24:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if maxCount < count {
|
|
|
|
|
|
return nil, core.NewBizErr("无可用代理")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 16:30:19 +08:00
|
|
|
|
return bestProxy, nil
|
2026-06-08 17:24:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 13:54:01 +08:00
|
|
|
|
func (s *channelServer) RefreshEdges() error {
|
|
|
|
|
|
|
2026-06-08 17:24:55 +08:00
|
|
|
|
// 仅白银网关支持边缘节点刷新,GOST 不参与此流程。
|
2026-05-18 13:54:01 +08:00
|
|
|
|
proxies, err := q.Proxy.Where(
|
2026-06-08 17:24:55 +08:00
|
|
|
|
q.Proxy.Type.Eq(int(m.ProxyTypeBaiYin)),
|
2026-05-18 13:54:01 +08:00
|
|
|
|
q.Proxy.Status.Eq(int(m.ProxyStatusOnline)),
|
|
|
|
|
|
).Find()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("查询网关失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for _, proxy := range proxies {
|
|
|
|
|
|
gateway, err := proxyGateway(proxy)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return core.NewServErr("创建代理网关失败", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 选取随机节点
|
|
|
|
|
|
edges, err := gateway.GatewayEdge(&g.GatewayEdgeReq{
|
|
|
|
|
|
Assigned: u.P(false),
|
|
|
|
|
|
Limit: u.P(1000),
|
|
|
|
|
|
})
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("获取边缘节点失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 提交断开配置
|
|
|
|
|
|
edgeIds := make([]string, 0, len(edges))
|
|
|
|
|
|
for id, _ := range edges {
|
|
|
|
|
|
edgeIds = append(edgeIds, id)
|
|
|
|
|
|
}
|
|
|
|
|
|
g.Cloud.CloudDisconnect(&g.CloudDisconnectReq{
|
|
|
|
|
|
Uuid: proxy.Mac,
|
|
|
|
|
|
Edge: &edgeIds,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-24 18:44:06 +08:00
|
|
|
|
// 授权方式
|
|
|
|
|
|
type ChannelAuthType int
|
2025-04-01 10:51:32 +08:00
|
|
|
|
|
2025-11-24 18:44:06 +08:00
|
|
|
|
const (
|
|
|
|
|
|
ChannelAuthTypeIp ChannelAuthType = iota + 1
|
|
|
|
|
|
ChannelAuthTypePass
|
|
|
|
|
|
)
|
2025-04-01 10:51:32 +08:00
|
|
|
|
|
2025-12-05 16:52:40 +08:00
|
|
|
|
// 生成用户名和密码对
|
2025-11-24 18:44:06 +08:00
|
|
|
|
func genPassPair() (string, string) {
|
|
|
|
|
|
var alphabet = []rune("abcdefghjkmnpqrstuvwxyz")
|
|
|
|
|
|
var numbers = []rune("23456789")
|
2025-04-01 10:51:32 +08:00
|
|
|
|
|
2025-11-24 18:44:06 +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
|
|
|
|
}
|
2025-11-24 18:44:06 +08:00
|
|
|
|
password[i] = numbers[rand.N(len(numbers))]
|
2025-05-23 14:53:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-24 18:44:06 +08:00
|
|
|
|
return string(username), string(password)
|
2025-03-28 10:03:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-23 13:50:52 +08:00
|
|
|
|
func FindResourceNoById(resourceId int32) (string, error) {
|
|
|
|
|
|
resource, err := q.Resource.
|
|
|
|
|
|
Select(q.Resource.ResourceNo).
|
|
|
|
|
|
Where(q.Resource.ID.Eq(resourceId)).
|
|
|
|
|
|
Take()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", ErrResourceNotExist
|
|
|
|
|
|
}
|
|
|
|
|
|
return u.Z(resource.ResourceNo), nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-05 16:52:40 +08:00
|
|
|
|
// 查找资源
|
2026-05-23 13:50:52 +08:00
|
|
|
|
func findResourceViewByNo(resourceNo string, now time.Time) (*ResourceView, error) {
|
2025-05-17 18:59:43 +08:00
|
|
|
|
resource, err := q.Resource.
|
2025-11-24 18:44:06 +08:00
|
|
|
|
Preload(field.Associations).
|
2025-05-08 19:02:07 +08:00
|
|
|
|
Where(
|
2026-05-23 13:50:52 +08:00
|
|
|
|
q.Resource.ResourceNo.Eq(resourceNo),
|
2025-11-24 18:44:06 +08:00
|
|
|
|
q.Resource.Active.Is(true),
|
2025-05-08 19:02:07 +08:00
|
|
|
|
).
|
2025-05-17 18:59:43 +08:00
|
|
|
|
Take()
|
2025-05-08 19:02:07 +08:00
|
|
|
|
if err != nil {
|
2025-05-17 18:59:43 +08:00
|
|
|
|
return nil, ErrResourceNotExist
|
|
|
|
|
|
}
|
2025-12-08 14:22:30 +08:00
|
|
|
|
if resource.User == nil {
|
|
|
|
|
|
return nil, ErrResourceNotExist
|
|
|
|
|
|
}
|
2025-11-24 18:44:06 +08:00
|
|
|
|
var info = &ResourceView{
|
2026-05-23 13:50:52 +08:00
|
|
|
|
ID: resource.ID,
|
2026-04-21 16:32:07 +08:00
|
|
|
|
User: *resource.User,
|
|
|
|
|
|
Active: resource.Active,
|
|
|
|
|
|
Type: resource.Type,
|
|
|
|
|
|
CheckIP: resource.CheckIP,
|
2025-05-17 18:59:43 +08:00
|
|
|
|
}
|
2025-05-26 10:57:39 +08:00
|
|
|
|
|
2025-11-24 18:44:06 +08:00
|
|
|
|
switch resource.Type {
|
|
|
|
|
|
case m.ResourceTypeShort:
|
2025-05-17 18:59:43 +08:00
|
|
|
|
var sub = resource.Short
|
2025-12-10 20:07:33 +08:00
|
|
|
|
info.ShortId = &sub.ID
|
|
|
|
|
|
info.ExpireAt = sub.ExpireAt
|
2026-04-15 16:56:24 +08:00
|
|
|
|
info.Live = time.Duration(sub.Live) * time.Minute
|
2025-12-10 20:07:33 +08:00
|
|
|
|
info.Mode = sub.Type
|
|
|
|
|
|
info.Quota = sub.Quota
|
2025-05-17 18:59:43 +08:00
|
|
|
|
info.Used = sub.Used
|
2025-12-10 20:07:33 +08:00
|
|
|
|
info.Daily = sub.Daily
|
|
|
|
|
|
info.LastAt = sub.LastAt
|
|
|
|
|
|
if sub.LastAt != nil && u.IsSameDate(*sub.LastAt, now) {
|
|
|
|
|
|
info.Today = int(sub.Daily)
|
|
|
|
|
|
}
|
2025-05-26 10:57:39 +08:00
|
|
|
|
|
2025-11-24 18:44:06 +08:00
|
|
|
|
case m.ResourceTypeLong:
|
2025-05-17 18:59:43 +08:00
|
|
|
|
var sub = resource.Long
|
2025-12-10 20:07:33 +08:00
|
|
|
|
info.LongId = &sub.ID
|
|
|
|
|
|
info.ExpireAt = sub.ExpireAt
|
2026-05-19 13:35:06 +08:00
|
|
|
|
info.Live = time.Duration(sub.Live) * time.Minute
|
2025-11-24 18:44:06 +08:00
|
|
|
|
info.Mode = sub.Type
|
2025-12-10 20:07:33 +08:00
|
|
|
|
info.Quota = sub.Quota
|
2025-05-17 18:59:43 +08:00
|
|
|
|
info.Used = sub.Used
|
2025-12-10 20:07:33 +08:00
|
|
|
|
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("检查套餐获取时间失败")
|
2025-03-28 10:03:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-17 18:59:43 +08:00
|
|
|
|
return info, nil
|
2025-03-28 10:03:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-24 18:44:06 +08:00
|
|
|
|
// ResourceView 套餐数据的简化视图,便于直接获取主要数据
|
|
|
|
|
|
type ResourceView struct {
|
2026-05-23 13:50:52 +08:00
|
|
|
|
ID int32
|
2025-12-10 20:07:33 +08:00
|
|
|
|
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 // 今日用量
|
2026-04-21 16:32:07 +08:00
|
|
|
|
CheckIP bool
|
2025-03-28 10:03:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-08 14:22:30 +08:00
|
|
|
|
// 检查用户是否可提取
|
2026-05-23 13:50:52 +08:00
|
|
|
|
func ensure(now time.Time, source netip.Addr, resourceNo string, authWhitelist bool, count int) (*ResourceView, []string, error) {
|
2025-12-08 14:22:30 +08:00
|
|
|
|
if count > 400 {
|
|
|
|
|
|
return nil, nil, core.NewBizErr("单次最多提取 400 个")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取用户套餐
|
2026-05-23 13:50:52 +08:00
|
|
|
|
resource, err := findResourceViewByNo(resourceNo, now)
|
2025-12-08 14:22:30 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-22 17:11:55 +08:00
|
|
|
|
if authWhitelist && len(whitelists) == 0 {
|
|
|
|
|
|
return nil, nil, core.NewBizErr("当前白名单为空,请先添加白名单")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-08 14:22:30 +08:00
|
|
|
|
ips := make([]string, len(whitelists))
|
|
|
|
|
|
pass := false
|
|
|
|
|
|
for i, item := range whitelists {
|
|
|
|
|
|
ips[i] = item.IP.String()
|
|
|
|
|
|
if item.IP.Addr == source {
|
|
|
|
|
|
pass = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-21 16:32:07 +08:00
|
|
|
|
if resource.CheckIP && !pass {
|
2025-12-08 14:22:30 +08:00
|
|
|
|
return nil, nil, core.NewBizErr(fmt.Sprintf("IP 地址 %s 不在白名单内", source.String()))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查套餐使用情况
|
|
|
|
|
|
switch resource.Mode {
|
|
|
|
|
|
default:
|
|
|
|
|
|
return nil, nil, core.NewBizErr("不支持的套餐模式")
|
|
|
|
|
|
|
|
|
|
|
|
// 包时
|
|
|
|
|
|
case m.ResourceModeTime:
|
|
|
|
|
|
// 检查过期时间
|
2025-12-10 20:07:33 +08:00
|
|
|
|
if resource.ExpireAt.Before(now) {
|
2025-12-08 14:22:30 +08:00
|
|
|
|
return nil, nil, ErrResourceExpired
|
|
|
|
|
|
}
|
|
|
|
|
|
// 检查每日限额
|
2025-12-10 20:07:33 +08:00
|
|
|
|
if count+resource.Today > int(resource.Quota) {
|
2025-12-08 14:22:30 +08:00
|
|
|
|
return nil, nil, ErrResourceDailyLimit
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 包量
|
|
|
|
|
|
case m.ResourceModeQuota:
|
|
|
|
|
|
// 检查可用配额
|
|
|
|
|
|
if int(resource.Quota)-int(resource.Used) < count {
|
|
|
|
|
|
return nil, nil, ErrResourceExhausted
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return resource, ips, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 17:30:51 +08:00
|
|
|
|
func freeChansKey(proxy int32) string {
|
|
|
|
|
|
return "channel:free:" + strconv.Itoa(int(proxy))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func usedChansKey(proxy int32, batch string) string {
|
|
|
|
|
|
return "channel:used:" + strconv.Itoa(int(proxy)) + ":" + batch
|
|
|
|
|
|
}
|
2025-12-05 16:52:40 +08:00
|
|
|
|
|
2026-06-08 17:24:55 +08:00
|
|
|
|
type usedChanBatch struct {
|
|
|
|
|
|
ProxyID int32
|
|
|
|
|
|
Chans []netip.AddrPort
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 16:30:19 +08:00
|
|
|
|
type usedChanKey struct {
|
|
|
|
|
|
ProxyID int32
|
|
|
|
|
|
BatchNo string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 17:24:55 +08:00
|
|
|
|
func findUsedChanBatch(batch string) (*usedChanBatch, error) {
|
|
|
|
|
|
keys, err := g.Redis.Keys(context.Background(), "channel:used:*:"+batch).Result()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, core.NewServErr("查询使用中通道失败", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
key, ok, err := selectUsedChanBatchKey(batch, keys)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return nil, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
chans, err := g.Redis.LRange(context.Background(), key, 0, -1).Result()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, core.NewServErr("查询使用中通道失败", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return parseUsedChanBatch(key, chans)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func selectUsedChanBatchKey(batch string, keys []string) (string, bool, error) {
|
|
|
|
|
|
switch len(keys) {
|
|
|
|
|
|
case 0:
|
|
|
|
|
|
return "", false, nil
|
|
|
|
|
|
case 1:
|
|
|
|
|
|
return keys[0], true, nil
|
|
|
|
|
|
default:
|
|
|
|
|
|
slog.Error("batchNo 全局唯一约束被破坏", "batch", batch, "keys", keys)
|
|
|
|
|
|
return "", false, core.NewServErr(
|
|
|
|
|
|
fmt.Sprintf("检测到重复 usedChans 键,batchNo 全局唯一被破坏: %s", batch),
|
|
|
|
|
|
fmt.Errorf("keys=%s", strings.Join(keys, ",")),
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func parseUsedChanBatch(key string, chans []string) (*usedChanBatch, error) {
|
2026-06-09 16:30:19 +08:00
|
|
|
|
parsed, err := parseUsedChanKey(key)
|
2026-06-08 17:24:55 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
addrs := make([]netip.AddrPort, len(chans))
|
|
|
|
|
|
for i, ch := range chans {
|
|
|
|
|
|
addr, err := netip.ParseAddrPort(ch)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, core.NewServErr(fmt.Sprintf("解析通道数据失败: %s", ch), err)
|
|
|
|
|
|
}
|
|
|
|
|
|
addrs[i] = addr
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return &usedChanBatch{
|
2026-06-09 16:30:19 +08:00
|
|
|
|
ProxyID: parsed.ProxyID,
|
2026-06-08 17:24:55 +08:00
|
|
|
|
Chans: addrs,
|
|
|
|
|
|
}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 16:30:19 +08:00
|
|
|
|
func parseUsedChanKey(key string) (*usedChanKey, error) {
|
2026-06-08 17:24:55 +08:00
|
|
|
|
parts := strings.Split(key, ":")
|
|
|
|
|
|
if len(parts) != 4 {
|
2026-06-09 16:30:19 +08:00
|
|
|
|
return nil, core.NewServErr(fmt.Sprintf("使用中通道键格式错误: %s", key), nil)
|
2026-06-08 17:24:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
proxyID, err := strconv.Atoi(parts[2])
|
|
|
|
|
|
if err != nil {
|
2026-06-09 16:30:19 +08:00
|
|
|
|
return nil, core.NewServErr(fmt.Sprintf("使用中通道键格式错误: %s", key), err)
|
2026-06-08 17:24:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 16:30:19 +08:00
|
|
|
|
return &usedChanKey{
|
|
|
|
|
|
ProxyID: int32(proxyID),
|
|
|
|
|
|
BatchNo: parts[3],
|
|
|
|
|
|
}, nil
|
2026-06-08 17:24:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-08 14:22:30 +08:00
|
|
|
|
// 扩容通道
|
|
|
|
|
|
func regChans(proxy int32, chans []netip.AddrPort) error {
|
|
|
|
|
|
strs := make([]any, len(chans))
|
|
|
|
|
|
for i, ch := range chans {
|
|
|
|
|
|
strs[i] = ch.String()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 17:30:51 +08:00
|
|
|
|
key := freeChansKey(proxy)
|
2025-12-08 14:22:30 +08:00
|
|
|
|
err := g.Redis.SAdd(context.Background(), key, strs...).Err()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("扩容通道失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 缩容通道
|
|
|
|
|
|
func remChans(proxy int32) error {
|
2026-05-08 17:30:51 +08:00
|
|
|
|
key := freeChansKey(proxy)
|
2026-04-17 16:27:29 +08:00
|
|
|
|
err := g.Redis.Del(context.Background(), key).Err()
|
2025-12-08 14:22:30 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("缩容通道失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-05 16:52:40 +08:00
|
|
|
|
// 取用通道
|
2025-12-08 14:22:30 +08:00
|
|
|
|
func lockChans(proxy int32, batch string, count int) ([]netip.AddrPort, error) {
|
|
|
|
|
|
chans, err := RedisScriptLockChans.Run(
|
2025-11-24 18:44:06 +08:00
|
|
|
|
context.Background(),
|
2025-12-08 14:22:30 +08:00
|
|
|
|
g.Redis,
|
2025-12-01 12:43:29 +08:00
|
|
|
|
[]string{
|
2026-05-08 17:30:51 +08:00
|
|
|
|
freeChansKey(proxy),
|
|
|
|
|
|
usedChansKey(proxy, batch),
|
2025-12-01 12:43:29 +08:00
|
|
|
|
},
|
2025-11-24 18:44:06 +08:00
|
|
|
|
count,
|
2025-12-01 12:42:51 +08:00
|
|
|
|
).StringSlice()
|
2025-05-08 19:02:07 +08:00
|
|
|
|
if err != nil {
|
2025-12-08 14:22:30 +08:00
|
|
|
|
return nil, fmt.Errorf("获取通道失败: %w", err)
|
2025-05-22 14:55:04 +08:00
|
|
|
|
}
|
2025-05-08 19:02:07 +08:00
|
|
|
|
|
2025-11-24 18:44:06 +08:00
|
|
|
|
addrs := make([]netip.AddrPort, len(chans))
|
|
|
|
|
|
for i, ch := range chans {
|
|
|
|
|
|
addr, err := netip.ParseAddrPort(ch)
|
|
|
|
|
|
if err != nil {
|
2025-12-08 14:22:30 +08:00
|
|
|
|
return nil, fmt.Errorf("解析通道数据失败: %w", err)
|
2025-05-19 11:03:38 +08:00
|
|
|
|
}
|
2025-11-24 18:44:06 +08:00
|
|
|
|
addrs[i] = addr
|
2025-05-19 11:03:38 +08:00
|
|
|
|
}
|
2025-05-17 18:59:43 +08:00
|
|
|
|
|
2025-11-24 18:44:06 +08:00
|
|
|
|
return addrs, nil
|
2025-05-08 19:02:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-08 14:22:30 +08:00
|
|
|
|
var RedisScriptLockChans = redis.NewScript(`
|
2026-04-17 16:27:29 +08:00
|
|
|
|
local free_key = KEYS[1]
|
2025-12-08 14:22:30 +08:00
|
|
|
|
local batch_key = KEYS[2]
|
2025-12-01 12:43:29 +08:00
|
|
|
|
local count = tonumber(ARGV[1])
|
2025-11-24 18:44:06 +08:00
|
|
|
|
|
2026-04-17 16:27:29 +08:00
|
|
|
|
local free_count = redis.call("SCARD", free_key)
|
|
|
|
|
|
if count <= 0 or free_count < count then
|
2025-11-24 18:44:06 +08:00
|
|
|
|
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))
|
2025-11-24 18:44:06 +08:00
|
|
|
|
|
|
|
|
|
|
return ports
|
2025-12-08 14:22:30 +08:00
|
|
|
|
`)
|
2025-11-24 18:44:06 +08:00
|
|
|
|
|
2025-12-05 16:52:40 +08:00
|
|
|
|
// 归还通道
|
2025-12-08 14:22:30 +08:00
|
|
|
|
func freeChans(proxy int32, batch string) error {
|
|
|
|
|
|
err := RedisScriptFreeChans.Run(
|
2025-11-24 18:44:06 +08:00
|
|
|
|
context.Background(),
|
2025-12-08 14:22:30 +08:00
|
|
|
|
g.Redis,
|
2025-12-01 12:43:29 +08:00
|
|
|
|
[]string{
|
2026-05-08 17:30:51 +08:00
|
|
|
|
freeChansKey(proxy),
|
|
|
|
|
|
usedChansKey(proxy, batch),
|
2025-12-01 12:43:29 +08:00
|
|
|
|
},
|
2025-11-24 18:44:06 +08:00
|
|
|
|
).Err()
|
2025-05-08 19:02:07 +08:00
|
|
|
|
if err != nil {
|
2025-11-24 18:44:06 +08:00
|
|
|
|
return core.NewBizErr("释放通道失败", err)
|
2025-05-08 19:02:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
2025-04-02 16:08:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-08 14:22:30 +08:00
|
|
|
|
var RedisScriptFreeChans = redis.NewScript(`
|
2026-04-17 16:27:29 +08:00
|
|
|
|
local free_key = KEYS[1]
|
2025-12-08 14:22:30 +08:00
|
|
|
|
local batch_key = KEYS[2]
|
2025-05-17 18:59:43 +08:00
|
|
|
|
|
2025-12-08 14:22:30 +08:00
|
|
|
|
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-08 14:22:30 +08:00
|
|
|
|
`)
|
2025-12-05 16:52:40 +08:00
|
|
|
|
|
2025-11-24 18:44:06 +08:00
|
|
|
|
// 错误信息
|
2025-05-24 12:37:16 +08:00
|
|
|
|
var (
|
|
|
|
|
|
ErrResourceNotExist = core.NewBizErr("套餐不存在")
|
|
|
|
|
|
ErrResourceExhausted = core.NewBizErr("套餐已用完")
|
|
|
|
|
|
ErrResourceExpired = core.NewBizErr("套餐已过期")
|
|
|
|
|
|
ErrResourceDailyLimit = core.NewBizErr("套餐每日配额已用完")
|
2025-05-08 19:02:07 +08:00
|
|
|
|
)
|