package services import ( "context" "fmt" "log/slog" "net/netip" "platform/pkg/env" "platform/pkg/u" "platform/web/core" g "platform/web/globals" "platform/web/globals/orm" m "platform/web/models" q "platform/web/queries" "strings" "time" "gorm.io/gen" "gorm.io/gen/field" ) type channelGostProvider struct{} func (s *channelGostProvider) CreateChannels(source netip.Addr, resourceNo string, authWhitelist bool, authPassword bool, count int, filter *EdgeFilter) ([]*m.Channel, error) { now := time.Now() batchNo := ID.GenReadable("bat") channels := make([]*m.Channel, count) if filter == nil { filter = &EdgeFilter{} } err := g.Redsync.WithLock(lockChannelCreateKey(resourceNo), func() error { resource, whitelists, err := ensure(now, source, resourceNo, authWhitelist, count) if err != nil { return err } user := resource.User expire := now.Add(resource.Live) proxy, err := s.selectProxy(count) if err != nil { return err } chans, err := selectPorts(proxy.ID, batchNo, count, expire) if err != nil { return err } edges, err := s.selectEdge(filter, count) if err != nil { return err } client, err := proxyGost(proxy) if err != nil { return err } admissions := make([]*g.GostAdmissionConfig, 0, count) authers := make([]*g.GostAutherConfig, 0, count) services := make([]*g.GostServiceConfig, count) for i := range count { ch := chans[i] edge := edges[i] port := ch.Port() host := u.Else(proxy.Host, proxy.IP.String()) serviceName := gostServiceName(batchNo, port) channel := &m.Channel{ UserID: user.ID, ResourceID: resource.ID, BatchNo: batchNo, ProxyID: proxy.ID, Host: host, Port: port, EdgeID: u.P(edge.ID), EdgeRef: u.P(serviceName), FilterISP: filter.Isp, FilterProv: filter.Prov, FilterCity: filter.City, IP: u.P(edge.IP), ExpiredAt: expire, Proxy: proxy, } service := &g.GostServiceConfig{ Name: serviceName, Addr: fmt.Sprintf(":%d", port), Handler: g.GostHandlerConfig{ Type: "auto", Chain: edge.Mac, }, Listener: g.GostListenerConfig{ Type: "tcp", }, } if authWhitelist { channel.Whitelists = u.P(strings.Join(whitelists, ",")) service.Admission = gostAdmissionName(batchNo, port) admission := &g.GostAdmissionConfig{ Name: service.Admission, Whitelist: true, Matchers: whitelists, } admissions = append(admissions, admission) } if authPassword { username, password := genPassPair() channel.Username = &username channel.Password = &password service.Handler.Auther = gostAutherName(batchNo, port) auther := &g.GostAutherConfig{ Name: service.Handler.Auther, Auths: []g.GostAuthConfig{{ Username: username, Password: password, }}, } authers = append(authers, auther) } services[i] = service channels[i] = channel } for _, admission := range admissions { if err := client.CreateAdmission(admission); err != nil { return core.NewServErr(fmt.Sprintf("创建 GOST admission 失败: %s", admission.Name), err) } } for _, auther := range authers { if err := client.CreateAuther(auther); err != nil { return core.NewServErr(fmt.Sprintf("创建 GOST auther 失败: %s", auther.Name), err) } } for _, service := range services { if err := client.CreateService(service); err != nil { return core.NewServErr(fmt.Sprintf("创建 GOST service 失败: %s", service.Name), err) } } err = q.Q.Transaction(func(tx *q.Query) error { var result gen.ResultInfo var err error switch resource.Type { case m.ResourceTypeShort: result, err = tx.ResourceShort. Where( tx.ResourceShort.ID.Eq(*resource.ShortId), tx.ResourceShort.Used.Eq(resource.Used), tx.ResourceShort.Daily.Eq(resource.Daily), ). UpdateSimple( tx.ResourceShort.Used.Add(int32(count)), tx.ResourceShort.Daily.Value(int32(resource.Today+count)), tx.ResourceShort.LastAt.Value(now), ) case m.ResourceTypeLong: result, err = tx.ResourceLong. Where( tx.ResourceLong.ID.Eq(*resource.LongId), tx.ResourceLong.Used.Eq(resource.Used), tx.ResourceLong.Daily.Eq(resource.Daily), ). UpdateSimple( tx.ResourceLong.Used.Add(int32(count)), tx.ResourceLong.Daily.Value(int32(resource.Today+count)), tx.ResourceLong.LastAt.Value(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: user.ID, ResourceID: resource.ID, BatchNo: batchNo, Count: int32(count), ISP: u.X(filter.Isp.String()), Prov: filter.Prov, City: filter.City, IP: orm.Inet{Addr: source}, Time: now, }); err != nil { return core.NewServErr("保存用户使用记录失败", err) } return nil }) if err != nil { return err } return nil }) if err != nil { return nil, err } return channels, nil } func (s *channelGostProvider) RemoveChannels(batchNo string) error { return g.Redsync.WithLock(lockChannelRemoveKey(batchNo), func() error { start := time.Now() batch, err := findUsedChanBatch(batchNo) if err != nil { return err } if batch == nil { slog.Debug("通道为空,跳过清理", "batch", batchNo) return nil } if env.RunMode == env.RunModeProd { proxy, err := q.Proxy.Where(q.Proxy.ID.Eq(batch.ProxyID)).Take() if err != nil { return core.NewServErr("获取代理数据失败", err) } client, err := proxyGost(proxy) if err != nil { return core.NewServErr("创建 GOST 客户端失败", err) } var deleteErrs []error for _, ch := range batch.Chans { port := ch.Port() serviceName := gostServiceName(batchNo, port) deleteErrs = append(deleteErrs, deleteGostResource("service", serviceName, func() error { return client.DeleteService(serviceName) })) autherName := gostAutherName(batchNo, port) deleteErrs = append(deleteErrs, deleteGostResource("auther", autherName, func() error { return client.DeleteAuther(autherName) })) admissionName := gostAdmissionName(batchNo, port) deleteErrs = append(deleteErrs, deleteGostResource("admission", admissionName, func() error { return client.DeleteAdmission(admissionName) })) } if err := u.CombineErrors(deleteErrs); err != nil { return err } } if err := freeChans(batch.ProxyID, batchNo); err != nil { return err } slog.Debug("清除 GOST 端口配置", "proxy", batch.ProxyID, "batch", batchNo, "duration", time.Since(start).String()) return nil }) } func (s *channelGostProvider) ClearExpiredChannels(proxyId int32) (int, error) { now := time.Now() keys, err := g.Redis.Keys(context.Background(), usedChansKey(proxyId, "*")).Result() if err != nil { return 0, core.NewServErr("查询使用中通道失败", err) } if len(keys) == 0 { return 0, nil } batchList := make([]string, len(keys)) batchSet := make(map[string]struct{}, len(keys)) for i, key := range keys { parts := strings.Split(key, ":") if len(parts) != 4 { return 0, core.NewServErr(fmt.Sprintf("使用中通道键格式错误: %s", key), nil) } batchList[i] = parts[3] batchSet[parts[3]] = 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 0, core.NewServErr("查询过期通道失败", err) } for _, batch := range batchQueried { delete(batchSet, batch.BatchNo) } slog.Info("批量清理过期 GOST 通道", "count", len(batchSet)) for batchNo := range batchSet { if err := s.RemoveChannels(batchNo); err != nil { slog.Error("清理过期 GOST 通道失败", "batch", batchNo, "error", err) } } return len(batchSet), nil } func (s *channelGostProvider) selectProxy(count int) (*m.Proxy, error) { return selectProxyByType(m.ProxyTypeGost, count) } func (s *channelGostProvider) selectEdge(filter *EdgeFilter, count int) ([]*m.Edge, error) { if filter == nil { filter = &EdgeFilter{} } do := q.Edge.Where( q.Edge.Type.Eq(int(m.EdgeTypeGostChain)), q.Edge.Status.Eq(int(m.EdgeStatusNormal)), ) if prov := u.N(filter.Prov); prov != nil { do = do.Where(q.Edge.Prov.Eq(*prov)) } if city := u.N(filter.City); city != nil { do = do.Where(q.Edge.City.Eq(*city)) } if isp := u.X(filter.Isp.String()); isp != nil { do = do.Where(q.Edge.ISP.Eq(int(*filter.Isp))) } edges, err := q.Edge.Where(do).Order(q.Edge.ID).Limit(count).Find() if err != nil { return nil, core.NewBizErr("查询可用节点失败", err) } return expandGostEdges(edges, count) } func expandGostEdges(edges []*m.Edge, count int) ([]*m.Edge, error) { if len(edges) == 0 { return nil, core.NewBizErr("地区可用节点数量不足") } result := make([]*m.Edge, count) for i := range count { result[i] = edges[i%len(edges)] } return result, nil } func proxyGost(proxy *m.Proxy) (g.GostClient, error) { secret := strings.Split(u.Z(proxy.Secret), ":") if len(secret) != 2 { return nil, core.NewServErr(fmt.Sprintf("代理 %s 密钥格式错误", proxy.IP.String()), nil) } host := u.Else(proxy.Host, proxy.IP.String()) return g.NewGost(host, env.GostApiPort, env.GostApiPathPrefix, secret[0], secret[1]), nil } func deleteGostResource(kind string, name string, deleteFn func() error) error { if err := deleteFn(); err != nil && !g.IsGostNotFound(err) { return core.NewServErr(fmt.Sprintf("删除 GOST %s 配置失败: %s", kind, name), err) } return nil } func gostServiceName(batchNo string, port uint16) string { return fmt.Sprintf("gost-svc-%s-%d", batchNo, port) } func gostAutherName(batchNo string, port uint16) string { return fmt.Sprintf("gost-auther-%s-%d", batchNo, port) } func gostAdmissionName(batchNo string, port uint16) string { return fmt.Sprintf("gost-adm-%s-%d", batchNo, port) }