重构提取逻辑,新增 area 表

This commit is contained in:
2026-06-10 14:32:45 +08:00
parent dd482dd6b0
commit ebac8042ea
26 changed files with 7939 additions and 666 deletions

View File

@@ -189,7 +189,7 @@ func (c *gostClient) request(method string, path string, payload any) ([]byte, e
return nil, err
}
if resp.StatusCode == http.StatusNotFound {
if resp.StatusCode == http.StatusBadRequest {
return nil, fmt.Errorf("%w: %s", ErrGostNotFound, string(body))
}
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {

View File

@@ -1,96 +0,0 @@
package globals
import (
"encoding/base64"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestGostClientCreateServiceUsesBasicAuthAndPathPrefix(t *testing.T) {
var (
gotPath string
gotAuth string
gotBody GostServiceConfig
)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
gotAuth = r.Header.Get("Authorization")
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
t.Fatalf("Decode failed: %v", err)
}
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
client := NewGost(server.URL, 0, "/api", "user", "pass")
err := client.CreateService(&GostServiceConfig{
Name: "svc-1",
Addr: ":10000",
Handler: GostHandlerConfig{
Type: "auto",
Chain: "chain-a",
Auther: "auther-a",
},
Listener: GostListenerConfig{Type: "tcp"},
})
if err != nil {
t.Fatalf("CreateService returned error: %v", err)
}
if gotPath != "/api/config/services" {
t.Fatalf("unexpected path: %s", gotPath)
}
if gotAuth != "Basic "+base64.StdEncoding.EncodeToString([]byte("user:pass")) {
t.Fatalf("unexpected auth header: %s", gotAuth)
}
if gotBody.Name != "svc-1" || gotBody.Handler.Type != "auto" || gotBody.Handler.Chain != "chain-a" {
t.Fatalf("unexpected request body: %+v", gotBody)
}
}
func TestGostClientDeleteServiceTreats404AsIdempotent(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r)
}))
defer server.Close()
client := NewGost(server.URL, 0, "", "user", "pass")
if err := client.DeleteService("svc-1"); !IsGostNotFound(err) {
t.Fatalf("expected gost not found error, got: %v", err)
}
}
func TestGostClientGetChainReadsTopLevelName(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/config/chains/chain-a" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
_, _ = w.Write([]byte(`{"name":"chain-a"}`))
}))
defer server.Close()
client := NewGost(server.URL, 0, "", "user", "pass")
chain, err := client.GetChain("chain-a")
if err != nil {
t.Fatalf("GetChain returned error: %v", err)
}
if chain.Name != "chain-a" {
t.Fatalf("unexpected chain: %+v", chain)
}
}
func TestNormalizeGostPathPrefix(t *testing.T) {
if got := normalizeGostPathPrefix("api/"); got != "/api" {
t.Fatalf("unexpected prefix: %s", got)
}
if got := normalizeGostPathPrefix(""); got != "" {
t.Fatalf("unexpected empty prefix: %s", got)
}
if !strings.HasPrefix(normalizeGostPathPrefix("/v1"), "/") {
t.Fatal("expected normalized prefix to start with slash")
}
}

29
web/handlers/area.go Normal file
View File

@@ -0,0 +1,29 @@
package handlers
import (
"platform/web/auth"
s "platform/web/services"
"github.com/gofiber/fiber/v2"
)
func ListArea(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitOfficialClient()
if err != nil {
return err
}
list, err := s.Area.ListAreas()
if err != nil {
return err
}
return c.JSON(list)
}
type ListAreaRespItem struct {
ID int32 `json:"id"`
Name string `json:"name"`
Level int `json:"level"`
ParentID *int32 `json:"parent_id,omitempty"`
}

View File

@@ -106,41 +106,28 @@ func CreateChannel(c *fiber.Ctx) error {
if err != nil {
return err
}
var isp *m.EdgeISP
areaID, err := s.Area.FindIdByFilter(req.Prov, req.City)
if err != nil {
return err
}
filter := &s.EdgeFilter{AreaID: areaID}
if req.Isp != nil {
isp = u.X(m.ToEdgeISP(*req.Isp))
filter.Isp = u.X(m.ToEdgeISP(*req.Isp))
}
result, err := s.Channel.CreateChannels(
ip, no,
ip,
no,
req.AuthType == s.ChannelAuthTypeIp,
req.AuthType == s.ChannelAuthTypePass,
req.Count,
&s.EdgeFilter{
Isp: isp,
Prov: req.Prov,
City: req.City,
},
filter,
)
if err != nil {
return err
}
// 返回结果
var resp = make([]*CreateChannelRespItem, len(result))
for i, channel := range result {
resp[i] = &CreateChannelRespItem{
Proto: req.Protocol,
Host: channel.Host,
IP: channel.Proxy.IP.String(),
Port: channel.Port,
}
if req.AuthType == s.ChannelAuthTypePass {
resp[i].Username = channel.Username
resp[i].Password = channel.Password
}
}
return c.JSON(resp)
return c.JSON(buildCreateChannelResp(result, req.Protocol, req.AuthType))
}
type CreateChannelReq struct {
@@ -169,9 +156,13 @@ func CreateChannelV2(c *fiber.Ctx) error {
}
// 创建通道
var isp *m.EdgeISP
areaID, err := s.Area.FindIdByFilter(req.Prov, req.City)
if err != nil {
return err
}
filter := &s.EdgeFilter{AreaID: areaID}
if req.Isp != nil {
isp = u.X(m.ToEdgeISP(*req.Isp))
filter.Isp = u.X(m.ToEdgeISP(*req.Isp))
}
result, err := s.Channel.CreateChannels(
ip,
@@ -179,31 +170,14 @@ func CreateChannelV2(c *fiber.Ctx) error {
req.AuthType == s.ChannelAuthTypeIp,
req.AuthType == s.ChannelAuthTypePass,
req.Count,
&s.EdgeFilter{
Isp: isp,
Prov: req.Prov,
City: req.City,
},
filter,
)
if err != nil {
return err
}
// 返回结果
var resp = make([]*CreateChannelRespItem, len(result))
for i, channel := range result {
resp[i] = &CreateChannelRespItem{
Proto: req.Protocol,
Host: channel.Host,
IP: channel.Proxy.IP.String(),
Port: channel.Port,
}
if req.AuthType == s.ChannelAuthTypePass {
resp[i].Username = channel.Username
resp[i].Password = channel.Password
}
}
return c.JSON(resp)
return c.JSON(buildCreateChannelResp(result, req.Protocol, req.AuthType))
}
type CreateChannelReqV2 struct {
@@ -216,6 +190,63 @@ type CreateChannelReqV2 struct {
Isp *int `json:"isp"`
}
// CreateChannelV3 创建新通道 v3使用 resource_no + area_id
func CreateChannelV3(c *fiber.Ctx) error {
req := new(CreateChannelReqV3)
if err := g.Validator.ParseBody(c, req); err != nil {
return core.NewBizErr("解析参数失败", err)
}
ip, err := netip.ParseAddr(c.IP())
if err != nil {
return core.NewBizErr("获取客户端地址失败", err)
}
filter := &s.EdgeFilter{AreaID: req.AreaID}
if req.Isp != nil {
filter.Isp = u.X(m.ToEdgeISP(*req.Isp))
}
result, err := s.Channel.CreateChannels(
ip,
req.ResourceNo,
req.AuthType == s.ChannelAuthTypeIp,
req.AuthType == s.ChannelAuthTypePass,
req.Count,
filter,
)
if err != nil {
return err
}
return c.JSON(buildCreateChannelResp(result, req.Protocol, req.AuthType))
}
type CreateChannelReqV3 struct {
ResourceNo string `json:"resource_no" validate:"required"`
AuthType s.ChannelAuthType `json:"auth_type" validate:"required"`
Protocol int `json:"protocol" validate:"required"`
Count int `json:"count" validate:"required"`
AreaID *int32 `json:"area_id"`
Isp *int `json:"isp"`
}
func buildCreateChannelResp(result []*m.Channel, protocol int, authType s.ChannelAuthType) []*CreateChannelRespItem {
resp := make([]*CreateChannelRespItem, len(result))
for i, channel := range result {
resp[i] = &CreateChannelRespItem{
Proto: protocol,
Host: channel.Host,
IP: channel.Proxy.IP.String(),
Port: channel.Port,
}
if authType == s.ChannelAuthTypePass {
resp[i].Username = channel.Username
resp[i].Password = channel.Password
}
}
return resp
}
type CreateChannelRespItem struct {
Proto int `json:"-"`
Host string `json:"host"`

View File

@@ -5,11 +5,14 @@ import (
"platform/web/core"
g "platform/web/globals"
s "platform/web/services"
"time"
"github.com/gofiber/fiber/v2"
)
// ====================
// admin 路由
// ====================
func PageProxyByAdmin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyRead)
if err != nil {
@@ -102,6 +105,24 @@ func UpdateProxyStatus(c *fiber.Ctx) error {
return c.JSON(nil)
}
func SyncProxyPool(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyWrite)
if err != nil {
return err
}
var req core.IdReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
if err := s.Proxy.SyncPool(req.Id); err != nil {
return err
}
return c.JSON(nil)
}
func RemoveProxy(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyWrite)
if err != nil {
@@ -119,347 +140,3 @@ func RemoveProxy(c *fiber.Ctx) error {
return c.JSON(nil)
}
// ====================
// region 报告上线
func ProxyReportOnline(c *fiber.Ctx) (err error) {
return c.JSON(map[string]any{
"error": "接口暂不可用",
})
// // 检查接口权限
// _, err = auth2.GetAuthCtx(c).PermitSecretClient()
// if err != nil {
// return err
// }
// // 验证请求参数
// var req = new(ProxyReportOnlineReq)
// err = g.Validator.Validate(c, req)
// if err != nil {
// return err
// }
// // 创建代理
// var ip = c.Context().RemoteIP()
// var secretBytes = make([]byte, 16)
// if _, err := rand.Read(secretBytes); err != nil {
// return err
// }
// var secret = base32.StdEncoding.
// WithPadding(base32.NoPadding).
// EncodeToString(secretBytes)
// slog.Debug("生成随机密钥", "ip", ip, "secret", secret)
// var proxy = &m.Proxy{
// Mac: req.Name,
// Version: int32(req.Version),
// Type: m.ProxyTypeSelfHosted,
// IP: ip,
// Secret: &secret,
// Status: 1,
// }
// err = q.Proxy.
// Clauses(clause.OnConflict{
// UpdateAll: true,
// Columns: []clause.Column{
// {Name: q.Proxy.Mac.ColumnName().String()},
// },
// }).
// Create(proxy)
// if err != nil {
// return err
// }
// // 获取边缘节点信息
// data, err := q.Edge.Where(
// q.Edge.ProxyID.Eq(proxy.ID),
// ).Find()
// if err != nil {
// return err
// }
// edges := make([]*ProxyEdge, len(data))
// for i, edge := range data {
// edges[i] = &ProxyEdge{
// Id: edge.ID,
// Port: edge.ProxyPort,
// Prov: &edge.Prov,
// City: &edge.City,
// Isp: u.P(edge2.ISP(edge.Isp).String()),
// Status: &edge.Status,
// Loss: edge.Loss,
// Rtt: edge.Rtt,
// }
// }
// // 获取许可配置
// channels, err := q.Channel.Where(
// q.Channel.ProxyID.Eq(proxy.ID),
// q.Channel.Expiration.Gt(orm.LocalDateTime(time.Now())),
// ).Find()
// if err != nil {
// return err
// }
// var permits = make([]*ProxyPermit, len(channels))
// for i, channel := range channels {
// if channel.EdgeID == nil {
// return core.NewBizErr(fmt.Sprintf("权限解析异常通道缺少边缘节点ID %d", channel.ID))
// }
// permits[i] = &ProxyPermit{
// Id: *channel.EdgeID,
// Expire: time.Time(channel.Expiration),
// Whitelists: u.P(strings.Split(u.Z(channel.Whitelists), ",")),
// Username: channel.Username,
// Password: channel.Password,
// }
// }
// slog.Debug("注册转发服务", "ip", ip, "id", proxy.ID)
// return c.JSON(&ProxyReportOnlineResp{
// Id: proxy.ID,
// Secret: secret,
// Edges: edges,
// Permits: permits,
// })
}
type ProxyReportOnlineReq struct {
Name string `json:"name" validate:"required"`
Version int `json:"version" validate:"required"`
}
type ProxyReportOnlineResp struct {
Id int32 `json:"id"`
Secret string `json:"secret"`
Permits []*ProxyPermit `json:"permits"`
Edges []*ProxyEdge `json:"edges"`
}
// region 报告下线
func ProxyReportOffline(c *fiber.Ctx) (err error) {
return c.JSON(map[string]any{
"error": "接口暂不可用",
})
// // 检查接口权限
// _, err = auth2.GetAuthCtx(c).PermitSecretClient()
// if err != nil {
// return err
// }
// // 验证请求参数
// var req = new(ProxyReportOfflineReq)
// err = g.Validator.Validate(c, req)
// if err != nil {
// return err
// }
// // 下线转发服务
// _, err = q.Proxy.
// Where(q.Proxy.ID.Eq(req.Id)).
// UpdateSimple(q.Proxy.Status.Value(0))
// if err != nil {
// return err
// }
// // 下线所有相关的边缘节点
// _, err = q.Edge.
// Where(q.Edge.ProxyID.Eq(req.Id)).
// UpdateSimple(q.Edge.Status.Value(0))
// if err != nil {
// return err
// }
// return nil
}
type ProxyReportOfflineReq struct {
Id int32 `json:"id" validate:"required"`
}
// region 报告更新
func ProxyReportUpdate(c *fiber.Ctx) (err error) {
return c.JSON(map[string]any{
"error": "接口暂不可用",
})
// // 检查接口权限
// _, err = auth2.GetAuthCtx(c).PermitSecretClient()
// if err != nil {
// return err
// }
// // 验证请求参数
// var req = new(ProxyReportUpdateReq)
// err = g.Validator.Validate(c, req)
// if err != nil {
// return err
// }
// // 更新节点信息
// var idsActive = make([]int32, 0, len(req.Edges))
// var idsInactive = make([]int32, 0, len(req.Edges))
// var idsIspUnknown = make([]int32, 0, len(req.Edges))
// var idsIspTelecom = make([]int32, 0, len(req.Edges))
// var idsIspUnicom = make([]int32, 0, len(req.Edges))
// var idsIspMobile = make([]int32, 0, len(req.Edges))
// var otherEdges = make([]*ProxyEdge, 0, len(req.Edges))
// for _, edge := range req.Edges {
// // 检查更新ISP
// if edge.Isp != nil {
// switch edge2.ISPFromStr(*edge.Isp) {
// case edge2.IspUnknown:
// idsIspUnknown = append(idsIspUnknown, edge.Id)
// case edge2.IspChinaTelecom:
// idsIspTelecom = append(idsIspTelecom, edge.Id)
// case edge2.IspChinaUnicom:
// idsIspUnicom = append(idsIspUnicom, edge.Id)
// case edge2.IspChinaMobile:
// idsIspMobile = append(idsIspMobile, edge.Id)
// }
// }
// // 检查更新状态
// if edge.Status != nil {
// if *edge.Status == 1 {
// idsActive = append(idsActive, edge.Id)
// } else {
// idsInactive = append(idsInactive, edge.Id)
// }
// }
// // 无法分类更新
// if edge.Host != nil || edge.Port != nil || edge.Prov != nil || edge.City != nil {
// otherEdges = append(otherEdges, edge)
// continue
// }
// }
// slog.Debug("更新边缘节点信息",
// "active", len(idsActive),
// "inactive", len(idsInactive),
// "isp_unknown", len(idsIspUnknown),
// "isp_telecom", len(idsIspTelecom),
// "isp_unicom", len(idsIspUnicom),
// "isp_mobile", len(idsIspMobile),
// "other_edges", len(otherEdges),
// )
// err = q.Q.Transaction(func(q *q.Query) error {
// // 更新边缘节点状态
// if len(idsActive) > 0 {
// _, err = q.Edge.Debug().
// Where(q.Edge.ID.In(idsActive...)).
// UpdateSimple(q.Edge.Status.Value(1))
// if err != nil {
// return err
// }
// }
// if len(idsInactive) > 0 {
// _, err = q.Edge.Debug().
// Where(q.Edge.ID.In(idsInactive...)).
// UpdateSimple(q.Edge.Status.Value(0))
// if err != nil {
// return err
// }
// }
// // 更新边缘节点ISP
// if len(idsIspUnknown) > 0 {
// _, err = q.Edge.Debug().
// Where(q.Edge.ID.In(idsIspUnknown...)).
// UpdateSimple(q.Edge.Isp.Value(int32(edge2.IspUnknown)))
// if err != nil {
// return err
// }
// }
// if len(idsIspTelecom) > 0 {
// _, err = q.Edge.Debug().
// Where(q.Edge.ID.In(idsIspTelecom...)).
// UpdateSimple(q.Edge.Isp.Value(int32(edge2.IspChinaTelecom)))
// if err != nil {
// return err
// }
// }
// if len(idsIspUnicom) > 0 {
// _, err = q.Edge.Debug().
// Where(q.Edge.ID.In(idsIspUnicom...)).
// UpdateSimple(q.Edge.Isp.Value(int32(edge2.IspChinaUnicom)))
// if err != nil {
// return err
// }
// }
// if len(idsIspMobile) > 0 {
// _, err = q.Edge.Debug().
// Where(q.Edge.ID.In(idsIspMobile...)).
// UpdateSimple(q.Edge.Isp.Value(int32(edge2.IspChinaMobile)))
// if err != nil {
// return err
// }
// }
// // 更新其他边缘节点信息
// for _, edge := range otherEdges {
// do := q.Edge.Debug().Where(q.Edge.ID.Eq(edge.Id))
// var assigns = make([]field.AssignExpr, 0, 5)
// if edge.Host != nil {
// assigns = append(assigns, q.Edge.Host.Value(*edge.Host))
// }
// if edge.Port != nil {
// assigns = append(assigns, q.Edge.ProxyPort.Value(*edge.Port))
// }
// if edge.Prov != nil {
// assigns = append(assigns, q.Edge.Prov.Value(*edge.Prov))
// }
// if edge.City != nil {
// assigns = append(assigns, q.Edge.City.Value(*edge.City))
// }
// // 更新边缘节点
// _, err := do.UpdateSimple(assigns...)
// if err != nil {
// return fmt.Errorf("更新边缘节点 %d 失败: %w", edge.Id, err)
// }
// }
// return nil
// })
// if err != nil {
// return err
// }
// return nil
}
type ProxyReportUpdateReq struct {
Id int32 `json:"id" validate:"required"`
Edges []*ProxyEdge `json:"edges" validate:"required"`
}
type ProxyPermit struct {
Id int32 `json:"id"`
Expire time.Time `json:"expire"`
Whitelists *[]string `json:"whitelists"`
Username *string `json:"username"`
Password *string `json:"password"`
}
type ProxyEdge struct {
Id int32 `json:"id"`
Host *string `json:"host,omitempty"` // 边缘节点地址
Port *int32 `json:"port,omitempty"` // 边缘节点代理端口
Prov *string `json:"prov,omitempty"`
City *string `json:"city,omitempty"`
Isp *string `json:"isp,omitempty"`
Status *int32 `json:"status,omitempty"`
Loss *int32 `json:"loss,omitempty"` // 丢包率
Rtt *int32 `json:"latency,omitempty"` // 延迟
}

20
web/models/area.go Normal file
View File

@@ -0,0 +1,20 @@
package models
import "platform/web/core"
// Area 地区表
type Area struct {
core.Model
Name string `json:"name" gorm:"column:name"` // 地区名称
Level AreaLevel `json:"level" gorm:"column:level"` // 地区层级1-省2-市
ParentID *int32 `json:"parent_id,omitempty" gorm:"column:parent_id"` // 父级地区ID
Parent *Area `json:"parent,omitempty" gorm:"foreignKey:ParentID"`
}
// AreaLevel 地区层级枚举
type AreaLevel int
const (
AreaLevelProvince AreaLevel = 1 // 省
AreaLevelCity AreaLevel = 2 // 市
)

View File

@@ -8,17 +8,17 @@ import (
// Edge 节点表
type Edge struct {
core.Model
Type EdgeType `json:"type" gorm:"column:type"` // 节点类型1-自建2-GOST chain
Version int32 `json:"version" gorm:"column:version"` // 节点版本
Mac string `json:"mac" gorm:"column:mac"` // 节点 mac 地址或 GOST chain 名称
IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // 节点地址或 GOST chain addr 的 IP
Port *uint16 `json:"port,omitempty" gorm:"column:port"` // GOST chain addr 的端口
ISP EdgeISP `json:"isp" gorm:"column:isp"` // 运营商0-未知1-电信2-联通3-移动
Prov string `json:"prov" gorm:"column:prov"` // 省份
City string `json:"city" gorm:"column:city"` // 城市
Status EdgeStatus `json:"status" gorm:"column:status"` // 节点状态0-离线1-正常
RTT int32 `json:"rtt" gorm:"column:rtt"` // 最近平均延迟
Loss int32 `json:"loss" gorm:"column:loss"` // 最近丢包率
Type EdgeType `json:"type" gorm:"column:type"` // 节点类型1-自建2-GOST chain
Version int32 `json:"version" gorm:"column:version"` // 节点版本
Mac string `json:"mac" gorm:"column:mac"` // 节点 mac 地址或 GOST chain 名称
IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // 节点地址或 GOST chain addr 的 IP
Port *uint16 `json:"port,omitempty" gorm:"column:port"` // GOST chain addr 的端口
ISP EdgeISP `json:"isp" gorm:"column:isp"` // 运营商0-未知1-电信2-联通3-移动
AreaID int32 `json:"area_id" gorm:"column:area_id"` // 城市地区ID
Status EdgeStatus `json:"status" gorm:"column:status"` // 节点状态0-离线1-正常
RTT int32 `json:"rtt" gorm:"column:rtt"` // 最近平均延迟
Loss int32 `json:"loss" gorm:"column:loss"` // 最近丢包率
Area *Area `json:"area,omitempty" gorm:"foreignKey:AreaID"` // 地区
}
// EdgeType 节点类型枚举

443
web/queries/area.gen.go Normal file
View File

@@ -0,0 +1,443 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package queries
import (
"context"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/schema"
"gorm.io/gen"
"gorm.io/gen/field"
"gorm.io/plugin/dbresolver"
"platform/web/models"
)
func newArea(db *gorm.DB, opts ...gen.DOOption) area {
_area := area{}
_area.areaDo.UseDB(db, opts...)
_area.areaDo.UseModel(&models.Area{})
tableName := _area.areaDo.TableName()
_area.ALL = field.NewAsterisk(tableName)
_area.ID = field.NewInt32(tableName, "id")
_area.CreatedAt = field.NewTime(tableName, "created_at")
_area.UpdatedAt = field.NewTime(tableName, "updated_at")
_area.DeletedAt = field.NewField(tableName, "deleted_at")
_area.Name = field.NewString(tableName, "name")
_area.Level = field.NewInt(tableName, "level")
_area.ParentID = field.NewInt32(tableName, "parent_id")
_area.Parent = areaBelongsToParent{
db: db.Session(&gorm.Session{}),
RelationField: field.NewRelation("Parent", "models.Area"),
Parent: struct {
field.RelationField
}{
RelationField: field.NewRelation("Parent.Parent", "models.Area"),
},
}
_area.fillFieldMap()
return _area
}
type area struct {
areaDo
ALL field.Asterisk
ID field.Int32
CreatedAt field.Time
UpdatedAt field.Time
DeletedAt field.Field
Name field.String
Level field.Int
ParentID field.Int32
Parent areaBelongsToParent
fieldMap map[string]field.Expr
}
func (a area) Table(newTableName string) *area {
a.areaDo.UseTable(newTableName)
return a.updateTableName(newTableName)
}
func (a area) As(alias string) *area {
a.areaDo.DO = *(a.areaDo.As(alias).(*gen.DO))
return a.updateTableName(alias)
}
func (a *area) updateTableName(table string) *area {
a.ALL = field.NewAsterisk(table)
a.ID = field.NewInt32(table, "id")
a.CreatedAt = field.NewTime(table, "created_at")
a.UpdatedAt = field.NewTime(table, "updated_at")
a.DeletedAt = field.NewField(table, "deleted_at")
a.Name = field.NewString(table, "name")
a.Level = field.NewInt(table, "level")
a.ParentID = field.NewInt32(table, "parent_id")
a.fillFieldMap()
return a
}
func (a *area) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
_f, ok := a.fieldMap[fieldName]
if !ok || _f == nil {
return nil, false
}
_oe, ok := _f.(field.OrderExpr)
return _oe, ok
}
func (a *area) fillFieldMap() {
a.fieldMap = make(map[string]field.Expr, 8)
a.fieldMap["id"] = a.ID
a.fieldMap["created_at"] = a.CreatedAt
a.fieldMap["updated_at"] = a.UpdatedAt
a.fieldMap["deleted_at"] = a.DeletedAt
a.fieldMap["name"] = a.Name
a.fieldMap["level"] = a.Level
a.fieldMap["parent_id"] = a.ParentID
}
func (a area) clone(db *gorm.DB) area {
a.areaDo.ReplaceConnPool(db.Statement.ConnPool)
a.Parent.db = db.Session(&gorm.Session{Initialized: true})
a.Parent.db.Statement.ConnPool = db.Statement.ConnPool
return a
}
func (a area) replaceDB(db *gorm.DB) area {
a.areaDo.ReplaceDB(db)
a.Parent.db = db.Session(&gorm.Session{})
return a
}
type areaBelongsToParent struct {
db *gorm.DB
field.RelationField
Parent struct {
field.RelationField
}
}
func (a areaBelongsToParent) Where(conds ...field.Expr) *areaBelongsToParent {
if len(conds) == 0 {
return &a
}
exprs := make([]clause.Expression, 0, len(conds))
for _, cond := range conds {
exprs = append(exprs, cond.BeCond().(clause.Expression))
}
a.db = a.db.Clauses(clause.Where{Exprs: exprs})
return &a
}
func (a areaBelongsToParent) WithContext(ctx context.Context) *areaBelongsToParent {
a.db = a.db.WithContext(ctx)
return &a
}
func (a areaBelongsToParent) Session(session *gorm.Session) *areaBelongsToParent {
a.db = a.db.Session(session)
return &a
}
func (a areaBelongsToParent) Model(m *models.Area) *areaBelongsToParentTx {
return &areaBelongsToParentTx{a.db.Model(m).Association(a.Name())}
}
func (a areaBelongsToParent) Unscoped() *areaBelongsToParent {
a.db = a.db.Unscoped()
return &a
}
type areaBelongsToParentTx struct{ tx *gorm.Association }
func (a areaBelongsToParentTx) Find() (result *models.Area, err error) {
return result, a.tx.Find(&result)
}
func (a areaBelongsToParentTx) Append(values ...*models.Area) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Append(targetValues...)
}
func (a areaBelongsToParentTx) Replace(values ...*models.Area) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Replace(targetValues...)
}
func (a areaBelongsToParentTx) Delete(values ...*models.Area) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Delete(targetValues...)
}
func (a areaBelongsToParentTx) Clear() error {
return a.tx.Clear()
}
func (a areaBelongsToParentTx) Count() int64 {
return a.tx.Count()
}
func (a areaBelongsToParentTx) Unscoped() *areaBelongsToParentTx {
a.tx = a.tx.Unscoped()
return &a
}
type areaDo struct{ gen.DO }
func (a areaDo) Debug() *areaDo {
return a.withDO(a.DO.Debug())
}
func (a areaDo) WithContext(ctx context.Context) *areaDo {
return a.withDO(a.DO.WithContext(ctx))
}
func (a areaDo) ReadDB() *areaDo {
return a.Clauses(dbresolver.Read)
}
func (a areaDo) WriteDB() *areaDo {
return a.Clauses(dbresolver.Write)
}
func (a areaDo) Session(config *gorm.Session) *areaDo {
return a.withDO(a.DO.Session(config))
}
func (a areaDo) Clauses(conds ...clause.Expression) *areaDo {
return a.withDO(a.DO.Clauses(conds...))
}
func (a areaDo) Returning(value interface{}, columns ...string) *areaDo {
return a.withDO(a.DO.Returning(value, columns...))
}
func (a areaDo) Not(conds ...gen.Condition) *areaDo {
return a.withDO(a.DO.Not(conds...))
}
func (a areaDo) Or(conds ...gen.Condition) *areaDo {
return a.withDO(a.DO.Or(conds...))
}
func (a areaDo) Select(conds ...field.Expr) *areaDo {
return a.withDO(a.DO.Select(conds...))
}
func (a areaDo) Where(conds ...gen.Condition) *areaDo {
return a.withDO(a.DO.Where(conds...))
}
func (a areaDo) Order(conds ...field.Expr) *areaDo {
return a.withDO(a.DO.Order(conds...))
}
func (a areaDo) Distinct(cols ...field.Expr) *areaDo {
return a.withDO(a.DO.Distinct(cols...))
}
func (a areaDo) Omit(cols ...field.Expr) *areaDo {
return a.withDO(a.DO.Omit(cols...))
}
func (a areaDo) Join(table schema.Tabler, on ...field.Expr) *areaDo {
return a.withDO(a.DO.Join(table, on...))
}
func (a areaDo) LeftJoin(table schema.Tabler, on ...field.Expr) *areaDo {
return a.withDO(a.DO.LeftJoin(table, on...))
}
func (a areaDo) RightJoin(table schema.Tabler, on ...field.Expr) *areaDo {
return a.withDO(a.DO.RightJoin(table, on...))
}
func (a areaDo) Group(cols ...field.Expr) *areaDo {
return a.withDO(a.DO.Group(cols...))
}
func (a areaDo) Having(conds ...gen.Condition) *areaDo {
return a.withDO(a.DO.Having(conds...))
}
func (a areaDo) Limit(limit int) *areaDo {
return a.withDO(a.DO.Limit(limit))
}
func (a areaDo) Offset(offset int) *areaDo {
return a.withDO(a.DO.Offset(offset))
}
func (a areaDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *areaDo {
return a.withDO(a.DO.Scopes(funcs...))
}
func (a areaDo) Unscoped() *areaDo {
return a.withDO(a.DO.Unscoped())
}
func (a areaDo) Create(values ...*models.Area) error {
if len(values) == 0 {
return nil
}
return a.DO.Create(values)
}
func (a areaDo) CreateInBatches(values []*models.Area, batchSize int) error {
return a.DO.CreateInBatches(values, batchSize)
}
// Save : !!! underlying implementation is different with GORM
// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
func (a areaDo) Save(values ...*models.Area) error {
if len(values) == 0 {
return nil
}
return a.DO.Save(values)
}
func (a areaDo) First() (*models.Area, error) {
if result, err := a.DO.First(); err != nil {
return nil, err
} else {
return result.(*models.Area), nil
}
}
func (a areaDo) Take() (*models.Area, error) {
if result, err := a.DO.Take(); err != nil {
return nil, err
} else {
return result.(*models.Area), nil
}
}
func (a areaDo) Last() (*models.Area, error) {
if result, err := a.DO.Last(); err != nil {
return nil, err
} else {
return result.(*models.Area), nil
}
}
func (a areaDo) Find() ([]*models.Area, error) {
result, err := a.DO.Find()
return result.([]*models.Area), err
}
func (a areaDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*models.Area, err error) {
buf := make([]*models.Area, 0, batchSize)
err = a.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
defer func() { results = append(results, buf...) }()
return fc(tx, batch)
})
return results, err
}
func (a areaDo) FindInBatches(result *[]*models.Area, batchSize int, fc func(tx gen.Dao, batch int) error) error {
return a.DO.FindInBatches(result, batchSize, fc)
}
func (a areaDo) Attrs(attrs ...field.AssignExpr) *areaDo {
return a.withDO(a.DO.Attrs(attrs...))
}
func (a areaDo) Assign(attrs ...field.AssignExpr) *areaDo {
return a.withDO(a.DO.Assign(attrs...))
}
func (a areaDo) Joins(fields ...field.RelationField) *areaDo {
for _, _f := range fields {
a = *a.withDO(a.DO.Joins(_f))
}
return &a
}
func (a areaDo) Preload(fields ...field.RelationField) *areaDo {
for _, _f := range fields {
a = *a.withDO(a.DO.Preload(_f))
}
return &a
}
func (a areaDo) FirstOrInit() (*models.Area, error) {
if result, err := a.DO.FirstOrInit(); err != nil {
return nil, err
} else {
return result.(*models.Area), nil
}
}
func (a areaDo) FirstOrCreate() (*models.Area, error) {
if result, err := a.DO.FirstOrCreate(); err != nil {
return nil, err
} else {
return result.(*models.Area), nil
}
}
func (a areaDo) FindByPage(offset int, limit int) (result []*models.Area, count int64, err error) {
result, err = a.Offset(offset).Limit(limit).Find()
if err != nil {
return
}
if size := len(result); 0 < limit && 0 < size && size < limit {
count = int64(size + offset)
return
}
count, err = a.Offset(-1).Limit(-1).Count()
return
}
func (a areaDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
count, err = a.Count()
if err != nil {
return
}
err = a.Offset(offset).Limit(limit).Scan(result)
return
}
func (a areaDo) Scan(result interface{}) (err error) {
return a.DO.Scan(result)
}
func (a areaDo) Delete(models ...*models.Area) (result gen.ResultInfo, err error) {
return a.DO.Delete(models)
}
func (a *areaDo) withDO(do gen.Dao) *areaDo {
a.DO = *do.(*gen.DO)
return a
}

View File

@@ -218,6 +218,12 @@ func newChannel(db *gorm.DB, opts ...gen.DOOption) channel {
}
Edge struct {
field.RelationField
Area struct {
field.RelationField
Parent struct {
field.RelationField
}
}
}
}{
RelationField: field.NewRelation("Proxy.Channels", "models.Channel"),
@@ -238,8 +244,27 @@ func newChannel(db *gorm.DB, opts ...gen.DOOption) channel {
},
Edge: struct {
field.RelationField
Area struct {
field.RelationField
Parent struct {
field.RelationField
}
}
}{
RelationField: field.NewRelation("Proxy.Channels.Edge", "models.Edge"),
Area: struct {
field.RelationField
Parent struct {
field.RelationField
}
}{
RelationField: field.NewRelation("Proxy.Channels.Edge.Area", "models.Area"),
Parent: struct {
field.RelationField
}{
RelationField: field.NewRelation("Proxy.Channels.Edge.Area.Parent", "models.Area"),
},
},
},
},
}
@@ -617,6 +642,12 @@ type channelBelongsToProxy struct {
}
Edge struct {
field.RelationField
Area struct {
field.RelationField
Parent struct {
field.RelationField
}
}
}
}
}

View File

@@ -37,11 +37,20 @@ func newEdge(db *gorm.DB, opts ...gen.DOOption) edge {
_edge.IP = field.NewField(tableName, "ip")
_edge.Port = field.NewUint16(tableName, "port")
_edge.ISP = field.NewInt(tableName, "isp")
_edge.Prov = field.NewString(tableName, "prov")
_edge.City = field.NewString(tableName, "city")
_edge.AreaID = field.NewInt32(tableName, "area_id")
_edge.Status = field.NewInt(tableName, "status")
_edge.RTT = field.NewInt32(tableName, "rtt")
_edge.Loss = field.NewInt32(tableName, "loss")
_edge.Area = edgeBelongsToArea{
db: db.Session(&gorm.Session{}),
RelationField: field.NewRelation("Area", "models.Area"),
Parent: struct {
field.RelationField
}{
RelationField: field.NewRelation("Area.Parent", "models.Area"),
},
}
_edge.fillFieldMap()
@@ -62,11 +71,11 @@ type edge struct {
IP field.Field
Port field.Uint16
ISP field.Int
Prov field.String
City field.String
AreaID field.Int32
Status field.Int
RTT field.Int32
Loss field.Int32
Area edgeBelongsToArea
fieldMap map[string]field.Expr
}
@@ -93,8 +102,7 @@ func (e *edge) updateTableName(table string) *edge {
e.IP = field.NewField(table, "ip")
e.Port = field.NewUint16(table, "port")
e.ISP = field.NewInt(table, "isp")
e.Prov = field.NewString(table, "prov")
e.City = field.NewString(table, "city")
e.AreaID = field.NewInt32(table, "area_id")
e.Status = field.NewInt(table, "status")
e.RTT = field.NewInt32(table, "rtt")
e.Loss = field.NewInt32(table, "loss")
@@ -125,23 +133,111 @@ func (e *edge) fillFieldMap() {
e.fieldMap["ip"] = e.IP
e.fieldMap["port"] = e.Port
e.fieldMap["isp"] = e.ISP
e.fieldMap["prov"] = e.Prov
e.fieldMap["city"] = e.City
e.fieldMap["area_id"] = e.AreaID
e.fieldMap["status"] = e.Status
e.fieldMap["rtt"] = e.RTT
e.fieldMap["loss"] = e.Loss
}
func (e edge) clone(db *gorm.DB) edge {
e.edgeDo.ReplaceConnPool(db.Statement.ConnPool)
e.Area.db = db.Session(&gorm.Session{Initialized: true})
e.Area.db.Statement.ConnPool = db.Statement.ConnPool
return e
}
func (e edge) replaceDB(db *gorm.DB) edge {
e.edgeDo.ReplaceDB(db)
e.Area.db = db.Session(&gorm.Session{})
return e
}
type edgeBelongsToArea struct {
db *gorm.DB
field.RelationField
Parent struct {
field.RelationField
}
}
func (a edgeBelongsToArea) Where(conds ...field.Expr) *edgeBelongsToArea {
if len(conds) == 0 {
return &a
}
exprs := make([]clause.Expression, 0, len(conds))
for _, cond := range conds {
exprs = append(exprs, cond.BeCond().(clause.Expression))
}
a.db = a.db.Clauses(clause.Where{Exprs: exprs})
return &a
}
func (a edgeBelongsToArea) WithContext(ctx context.Context) *edgeBelongsToArea {
a.db = a.db.WithContext(ctx)
return &a
}
func (a edgeBelongsToArea) Session(session *gorm.Session) *edgeBelongsToArea {
a.db = a.db.Session(session)
return &a
}
func (a edgeBelongsToArea) Model(m *models.Edge) *edgeBelongsToAreaTx {
return &edgeBelongsToAreaTx{a.db.Model(m).Association(a.Name())}
}
func (a edgeBelongsToArea) Unscoped() *edgeBelongsToArea {
a.db = a.db.Unscoped()
return &a
}
type edgeBelongsToAreaTx struct{ tx *gorm.Association }
func (a edgeBelongsToAreaTx) Find() (result *models.Area, err error) {
return result, a.tx.Find(&result)
}
func (a edgeBelongsToAreaTx) Append(values ...*models.Area) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Append(targetValues...)
}
func (a edgeBelongsToAreaTx) Replace(values ...*models.Area) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Replace(targetValues...)
}
func (a edgeBelongsToAreaTx) Delete(values ...*models.Area) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Delete(targetValues...)
}
func (a edgeBelongsToAreaTx) Clear() error {
return a.tx.Clear()
}
func (a edgeBelongsToAreaTx) Count() int64 {
return a.tx.Count()
}
func (a edgeBelongsToAreaTx) Unscoped() *edgeBelongsToAreaTx {
a.tx = a.tx.Unscoped()
return &a
}
type edgeDo struct{ gen.DO }
func (e edgeDo) Debug() *edgeDo {

View File

@@ -20,6 +20,7 @@ var (
Admin *admin
AdminRole *adminRole
Announcement *announcement
Area *area
Article *article
ArticleGroup *articleGroup
BalanceActivity *balanceActivity
@@ -61,6 +62,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
Admin = &Q.Admin
AdminRole = &Q.AdminRole
Announcement = &Q.Announcement
Area = &Q.Area
Article = &Q.Article
ArticleGroup = &Q.ArticleGroup
BalanceActivity = &Q.BalanceActivity
@@ -103,6 +105,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
Admin: newAdmin(db, opts...),
AdminRole: newAdminRole(db, opts...),
Announcement: newAnnouncement(db, opts...),
Area: newArea(db, opts...),
Article: newArticle(db, opts...),
ArticleGroup: newArticleGroup(db, opts...),
BalanceActivity: newBalanceActivity(db, opts...),
@@ -146,6 +149,7 @@ type Query struct {
Admin admin
AdminRole adminRole
Announcement announcement
Area area
Article article
ArticleGroup articleGroup
BalanceActivity balanceActivity
@@ -190,6 +194,7 @@ func (q *Query) clone(db *gorm.DB) *Query {
Admin: q.Admin.clone(db),
AdminRole: q.AdminRole.clone(db),
Announcement: q.Announcement.clone(db),
Area: q.Area.clone(db),
Article: q.Article.clone(db),
ArticleGroup: q.ArticleGroup.clone(db),
BalanceActivity: q.BalanceActivity.clone(db),
@@ -241,6 +246,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
Admin: q.Admin.replaceDB(db),
AdminRole: q.AdminRole.replaceDB(db),
Announcement: q.Announcement.replaceDB(db),
Area: q.Area.replaceDB(db),
Article: q.Article.replaceDB(db),
ArticleGroup: q.ArticleGroup.replaceDB(db),
BalanceActivity: q.BalanceActivity.replaceDB(db),
@@ -282,6 +288,7 @@ type queryCtx struct {
Admin *adminDo
AdminRole *adminRoleDo
Announcement *announcementDo
Area *areaDo
Article *articleDo
ArticleGroup *articleGroupDo
BalanceActivity *balanceActivityDo
@@ -323,6 +330,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
Admin: q.Admin.WithContext(ctx),
AdminRole: q.AdminRole.WithContext(ctx),
Announcement: q.Announcement.WithContext(ctx),
Area: q.Area.WithContext(ctx),
Article: q.Article.WithContext(ctx),
ArticleGroup: q.ArticleGroup.WithContext(ctx),
BalanceActivity: q.BalanceActivity.WithContext(ctx),

View File

@@ -262,8 +262,27 @@ func newProxy(db *gorm.DB, opts ...gen.DOOption) proxy {
},
Edge: struct {
field.RelationField
Area struct {
field.RelationField
Parent struct {
field.RelationField
}
}
}{
RelationField: field.NewRelation("Channels.Edge", "models.Edge"),
Area: struct {
field.RelationField
Parent struct {
field.RelationField
}
}{
RelationField: field.NewRelation("Channels.Edge.Area", "models.Area"),
Parent: struct {
field.RelationField
}{
RelationField: field.NewRelation("Channels.Edge.Area.Parent", "models.Area"),
},
},
},
}
@@ -435,6 +454,12 @@ type proxyHasManyChannels struct {
}
Edge struct {
field.RelationField
Area struct {
field.RelationField
Parent struct {
field.RelationField
}
}
}
}

View File

@@ -1,11 +1,13 @@
package web
import (
"fmt"
"platform/pkg/env"
auth2 "platform/web/auth"
"platform/web/core"
"platform/web/globals"
"platform/web/handlers"
"strings"
"time"
q "platform/web/queries"
@@ -39,7 +41,6 @@ func ApplyRouters(app *fiber.App) {
debug.Get("/test/err", func(ctx *fiber.Ctx) error {
return core.NewBizErr("测试错误")
})
debug.Get("/trade/status/:trade_no", func(ctx *fiber.Ctx) error {
tradeNo := ctx.Params("trade_no")
resp, err := globals.SFTPay.QueryTrade(&globals.QueryTradeReq{
@@ -50,6 +51,46 @@ func ApplyRouters(app *fiber.App) {
}
return ctx.JSON(resp)
})
debug.Get("/gen-edge", func(ctx *fiber.Ctx) error {
areas, err := q.Area.Where(q.Area.Level.Eq(2)).Find()
if err != nil {
return err
}
sb := strings.Builder{}
sb.WriteString("INSERT INTO edge (type, version, mac, ip, port, isp, area_id, status) VALUES\n")
for i, area := range areas {
// jh edges
for j := range 20 {
fmt.Fprintf(&sb, "(2, 1, 'jh-%d-%d-%d', '192.168.50.%d', %d, 0, %d, 1)", area.ID, j+1, i+44001, j+2, i+44001, area.ID)
sb.WriteString(",\n")
}
// jg edges
for j := range 10 {
var ip string
var n int
if i < 100 {
ip = "192.168.0.232"
n = 1
} else if i < 200 {
ip = "192.168.59.236"
n = 2
} else {
ip = "192.168.59.237"
n = 3
}
fmt.Fprintf(&sb, "(2, 1, 'jg-%d-%d-%d', '%s', %d, 0, %d, 1)", area.ID, n, i*10+j+20001, ip, i*10+j+20001, area.ID)
if i < len(areas)-1 || j < 9 {
sb.WriteString(",\n")
}
}
}
sb.WriteString(";\n")
return ctx.SendString(sb.String())
})
}
}
@@ -89,9 +130,7 @@ func clientRouter(api fiber.Router) {
// 网关
proxy := client.Group("/proxy")
proxy.Post("/online", handlers.ProxyReportOnline)
proxy.Post("/offline", handlers.ProxyReportOffline)
proxy.Post("/update", handlers.ProxyReportUpdate)
proxy.Post("/sync-pool", handlers.SyncProxyPool)
// 通道管理
channel := client.Group("/channel")
@@ -139,6 +178,11 @@ func userRouter(api fiber.Router) {
channel.Post("/list", handlers.ListChannel)
channel.Post("/create", handlers.CreateChannel)
channel.Post("/create/v2", handlers.CreateChannelV2)
channel.Post("/create/v3", handlers.CreateChannelV3)
// 地区
area := api.Group("/area")
area.Post("/list", handlers.ListArea)
// 交易
trade := api.Group("/trade")

112
web/services/area.go Normal file
View File

@@ -0,0 +1,112 @@
package services
import (
"errors"
"platform/pkg/u"
"platform/web/core"
m "platform/web/models"
q "platform/web/queries"
"gorm.io/gorm"
)
var Area = &areaService{}
type areaService struct{}
func (s *areaService) ListAreas() ([]*m.Area, error) {
areas, err := q.Area.
Order(q.Area.Level, q.Area.ParentID, q.Area.ID).
Find()
if err != nil {
return nil, core.NewServErr("查询地区失败", err)
}
return areas, nil
}
func (s *areaService) FindIdByFilter(prov *string, city *string) (*int32, error) {
prov = u.N(prov)
city = u.N(city)
if prov == nil && city == nil {
return nil, nil
}
switch {
case prov != nil && city == nil:
area, err := q.Area.
Where(
q.Area.Level.Eq(int(m.AreaLevelProvince)),
q.Area.Name.Eq(*prov),
).
Order(q.Area.ID).
Take()
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrAreaNotExist
}
if err != nil {
return nil, core.NewServErr("查询地区失败", err)
}
return u.P(area.ID), nil
case prov == nil && city != nil:
area, err := q.Area.
Where(
q.Area.Level.Eq(int(m.AreaLevelCity)),
q.Area.Name.Eq(*city),
).
Order(q.Area.ID).
Take()
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrAreaNotExist
}
if err != nil {
return nil, core.NewServErr("查询地区失败", err)
}
return u.P(area.ID), nil
default:
province, err := q.Area.
Where(
q.Area.Level.Eq(int(m.AreaLevelProvince)),
q.Area.Name.Eq(*prov),
).
Order(q.Area.ID).
Take()
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrAreaNotExist
}
if err != nil {
return nil, core.NewServErr("查询地区失败", err)
}
area, err := q.Area.
Where(
q.Area.ParentID.Eq(province.ID),
q.Area.Level.Eq(int(m.AreaLevelCity)),
q.Area.Name.Eq(*city),
).
Order(q.Area.ID).
Take()
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrAreaNotExist
}
if err != nil {
return nil, core.NewServErr("查询地区失败", err)
}
return u.P(area.ID), nil
}
}
func (s *areaService) Get(id int32) (*m.Area, error) {
area, err := q.Area.
Preload(q.Area.Parent).
Where(q.Area.ID.Eq(id)).
Take()
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrAreaNotExist
}
if err != nil {
return nil, core.NewServErr("查询地区失败", err)
}
return area, nil
}
var ErrAreaNotExist = core.NewBizErr("地区不存在")

View File

@@ -40,12 +40,21 @@ type channelServer struct {
}
func (s *channelServer) CreateChannels(source netip.Addr, resourceNo string, authWhitelist bool, authPassword bool, count int, edgeFilter *EdgeFilter) ([]*m.Channel, error) {
var area *m.Area
if edgeFilter.AreaID != nil {
var err error
area, err = Area.Get(*edgeFilter.AreaID)
if err != nil {
return nil, err
}
if err := validateChannelArea(area); err != nil {
return nil, err
}
}
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 {
@@ -80,6 +89,7 @@ func (s *channelServer) CreateChannels(source netip.Addr, resourceNo string, aut
Expire: expire,
Count: count,
Filter: edgeFilter,
Area: area,
AuthWhitelist: authWhitelist,
AuthPassword: authPassword,
Whitelists: whitelists,
@@ -160,6 +170,7 @@ type channelCreateContext struct {
Expire time.Time
Count int
Filter *EdgeFilter
Area *m.Area
AuthWhitelist bool
AuthPassword bool
Whitelists []string
@@ -172,6 +183,7 @@ type channelCreateResult struct {
}
func newBaseChannel(ctx *channelCreateContext, port uint16) *m.Channel {
prov, city := areaProvinceCity(ctx.Area)
return &m.Channel{
UserID: ctx.Resource.User.ID,
ResourceID: ctx.Resource.ID,
@@ -180,8 +192,8 @@ func newBaseChannel(ctx *channelCreateContext, port uint16) *m.Channel {
Host: u.Else(ctx.Proxy.Host, ctx.Proxy.IP.String()),
Port: port,
FilterISP: ctx.Filter.Isp,
FilterProv: ctx.Filter.Prov,
FilterCity: ctx.Filter.City,
FilterProv: prov,
FilterCity: city,
ExpiredAt: ctx.Expire,
Proxy: ctx.Proxy,
}
@@ -202,6 +214,7 @@ func applyChannelAuth(ctx *channelCreateContext, channel *m.Channel) (username s
}
func persistChannelCreate(ctx *channelCreateContext, channels []*m.Channel) error {
prov, city := areaProvinceCity(ctx.Area)
return q.Q.Transaction(func(tx *q.Query) error {
var (
result gen.ResultInfo
@@ -252,8 +265,8 @@ func persistChannelCreate(ctx *channelCreateContext, channels []*m.Channel) erro
BatchNo: ctx.BatchNo,
Count: int32(ctx.Count),
ISP: u.X(ctx.Filter.Isp.String()),
Prov: ctx.Filter.Prov,
City: ctx.Filter.City,
Prov: prov,
City: city,
IP: orm.Inet{Addr: ctx.Source},
Time: ctx.Now,
}); err != nil {
@@ -264,6 +277,37 @@ func persistChannelCreate(ctx *channelCreateContext, channels []*m.Channel) erro
})
}
func validateChannelArea(area *m.Area) error {
if area == nil {
return nil
}
switch area.Level {
case m.AreaLevelProvince:
return nil
case m.AreaLevelCity:
if area.ParentID == nil || area.Parent == nil {
return core.NewServErr("地区数据异常", nil)
}
return nil
default:
return core.NewBizErr("地区层级不支持")
}
}
func areaProvinceCity(area *m.Area) (prov *string, city *string) {
if area == nil {
return nil, nil
}
switch area.Level {
case m.AreaLevelProvince:
return u.P(area.Name), nil
case m.AreaLevelCity:
return u.P(area.Parent.Name), u.P(area.Name)
default:
return nil, 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 {
@@ -778,6 +822,20 @@ redis.call("DEL", batch_key)
return 1
`)
// 节点筛选条件
type EdgeFilter struct {
Isp *m.EdgeISP `json:"isp"`
AreaID *int32 `json:"area_id"`
}
func (f *EdgeFilter) IsEmpty() bool {
if f == nil {
return true
}
return u.X(f.Isp.String()) == nil && f.AreaID == nil
}
// 错误信息
var (
ErrResourceNotExist = core.NewBizErr("套餐不存在")

View File

@@ -21,6 +21,7 @@ func (s *channelBaiyinProvider) prepareCreate(ctx *channelCreateContext) (*chann
if err != nil {
return nil, core.NewServErr("创建代理网关失败", err)
}
prov, city := areaProvinceCity(ctx.Area)
channels := make([]*m.Channel, len(ctx.Ports))
chanConfigs := make([]*g.PortConfigsReq, len(ctx.Ports))
@@ -30,8 +31,8 @@ func (s *channelBaiyinProvider) prepareCreate(ctx *channelCreateContext) (*chann
Port: int(portRef.Port()),
Status: true,
AutoEdgeConfig: &g.AutoEdgeConfig{
Province: u.Z(ctx.Filter.Prov),
City: u.Z(ctx.Filter.City),
Province: u.Z(prov),
City: u.Z(city),
Isp: ctx.Filter.Isp.String(),
Count: u.P(1),
},
@@ -52,7 +53,7 @@ func (s *channelBaiyinProvider) prepareCreate(ctx *channelCreateContext) (*chann
Channels: channels,
applyRemote: func() error {
slog.Debug("提交代理端口配置", "proxy", ctx.Proxy.IP.String(), "total_count", len(chanConfigs))
if err := ensureEdges(ctx.Proxy, gateway, ctx.Filter, ctx.Count); err != nil {
if err := ensureEdges(ctx.Proxy, gateway, ctx.Area, ctx.Filter.Isp, ctx.Count); err != nil {
slog.Warn("ensureEdges 失败", "err", err)
}
if len(chanConfigs) > 0 {
@@ -96,16 +97,17 @@ func (s *channelBaiyinProvider) removeRemote(_ string, batch *usedChanBatch) err
// ensureEdges 检查本地节点是否足够,如果不足从云端连入
// 本地节点通过 Assigned = false 排除已分配节点
// 云端节点通过 NoRepeat = true 排除已分配节点
func ensureEdges(proxy *m.Proxy, gateway g.GatewayClient, filter *EdgeFilter, count int) error {
if filter.IsEmpty() {
func ensureEdges(proxy *m.Proxy, gateway g.GatewayClient, area *m.Area, isp *m.EdgeISP, count int) error {
prov, city := areaProvinceCity(area)
if prov == nil && city == nil && u.X(isp.String()) == nil {
return nil // 没有过滤条件,直接返回空,避免无意义的查询
}
// 先查本地
localEdges, err := gateway.GatewayEdge(&g.GatewayEdgeReq{
Province: filter.Prov,
City: filter.City,
Isp: u.X(filter.Isp.String()),
Province: prov,
City: city,
Isp: u.X(isp.String()),
Limit: &count,
Assigned: u.P(false),
})
@@ -119,9 +121,9 @@ func ensureEdges(proxy *m.Proxy, gateway g.GatewayClient, filter *EdgeFilter, co
// 再查云端
remaining := count - len(localEdges)
cloudEdges, err := g.Cloud.CloudEdges(&g.CloudEdgesReq{
Province: filter.Prov,
City: filter.City,
Isp: u.X(filter.Isp.String()),
Province: prov,
City: city,
Isp: u.X(isp.String()),
Limit: &remaining,
NoRepeat: u.P(true),
ActiveTime: u.P(3600),

View File

@@ -9,12 +9,14 @@ import (
m "platform/web/models"
q "platform/web/queries"
"strings"
"gorm.io/gen"
)
type channelGostProvider struct{}
func (s *channelGostProvider) prepareCreate(ctx *channelCreateContext) (*channelCreateResult, error) {
edges, err := s.selectEdge(ctx.Filter, ctx.Count)
edges, err := s.selectEdge(ctx.Filter, ctx.Area, ctx.Count)
if err != nil {
return nil, err
}
@@ -131,26 +133,38 @@ 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) {
func (s *channelGostProvider) selectEdge(filter *EdgeFilter, area *m.Area, count int) ([]*m.Edge, error) {
if filter == nil {
filter = &EdgeFilter{}
}
do := q.Edge.Where(
conds := []gen.Condition{
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)))
conds = append(conds, q.Edge.ISP.Eq(int(*filter.Isp)))
}
edges, err := q.Edge.Where(do).Order(q.Edge.ID).Limit(count).Find()
query := q.Edge.Where(conds...)
if area != nil {
switch area.Level {
case m.AreaLevelProvince:
edgeArea := q.Area.As("EdgeArea")
query = query.
Join(edgeArea, edgeArea.ID.EqCol(q.Edge.AreaID)).
Where(edgeArea.ParentID.Eq(area.ID))
case m.AreaLevelCity:
query = query.Where(q.Edge.AreaID.Eq(area.ID))
default:
return nil, core.NewBizErr("地区层级不支持")
}
}
edges, err := query.
Order(q.Edge.ID).
Limit(count).
Find()
if err != nil {
return nil, core.NewBizErr("查询可用节点失败", err)
}

View File

@@ -1,74 +0,0 @@
package services
import (
"testing"
m "platform/web/models"
)
func TestExpandGostEdgesRejectsEmpty(t *testing.T) {
_, err := expandGostEdges(nil, 1)
if err == nil {
t.Fatal("expected error, got nil")
}
}
func TestExpandGostEdgesReusesWhenInsufficient(t *testing.T) {
edges := []*m.Edge{
{Mac: "chain-a"},
{Mac: "chain-b"},
}
result, err := expandGostEdges(edges, 5)
if err != nil {
t.Fatalf("expandGostEdges returned error: %v", err)
}
if len(result) != 5 {
t.Fatalf("unexpected edge count: %d", len(result))
}
expected := []string{"chain-a", "chain-b", "chain-a", "chain-b", "chain-a"}
for i, edge := range result {
if edge.Mac != expected[i] {
t.Fatalf("unexpected edge at %d: %s", i, edge.Mac)
}
}
}
func TestEdgeFilterIsEmpty(t *testing.T) {
if !(*EdgeFilter)(nil).IsEmpty() {
t.Fatal("nil filter should be empty")
}
if (&EdgeFilter{}).IsEmpty() != true {
t.Fatal("empty filter should be empty")
}
if (&EdgeFilter{Prov: strPtr("")}).IsEmpty() != true {
t.Fatal("filter with empty province should be empty")
}
if (&EdgeFilter{City: strPtr("")}).IsEmpty() != true {
t.Fatal("filter with empty city should be empty")
}
if (&EdgeFilter{Isp: ispPtr(m.ToEdgeISP(0))}).IsEmpty() != true {
t.Fatal("filter with zero ISP should be empty")
}
if (&EdgeFilter{Isp: ispPtr(m.ToEdgeISP(99))}).IsEmpty() != true {
t.Fatal("filter with invalid ISP should be empty")
}
prov := "江苏"
if (&EdgeFilter{Prov: &prov}).IsEmpty() {
t.Fatal("filter with province should not be empty")
}
isp := m.EdgeISPTelecom
if (&EdgeFilter{Isp: &isp}).IsEmpty() {
t.Fatal("filter with valid ISP should not be empty")
}
}
func strPtr(v string) *string {
return &v
}
func ispPtr(v m.EdgeISP) *m.EdgeISP {
return &v
}

View File

@@ -1,48 +0,0 @@
package services
import (
"platform/pkg/u"
m "platform/web/models"
q "platform/web/queries"
)
var Edge = &edgeService{}
type edgeService struct{}
func (s *edgeService) AllEdges(count int, filter EdgeFilter) ([]*m.Edge, error) {
do := q.Edge.Where(q.Edge.Type.Eq(int(m.EdgeTypeSelfBuilt)))
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)))
}
if count > 0 {
do = do.Limit(count)
}
edges, err := q.Edge.Where(do).Find()
if err != nil {
return nil, err
}
return edges, nil
}
type EdgeFilter struct {
Isp *m.EdgeISP `json:"isp"`
Prov *string `json:"prov"`
City *string `json:"city"`
}
func (f *EdgeFilter) IsEmpty() bool {
if f == nil {
return true
}
return u.X(f.Isp.String()) == nil && u.N(f.Prov) == nil && u.N(f.City) == nil
}

View File

@@ -161,6 +161,17 @@ func (s *proxyService) Update(update *UpdateProxy) error {
return nil
}
func (s *proxyService) SyncPool(id int32) error {
proxy, err := q.Proxy.Where(q.Proxy.ID.Eq(id)).Select(q.Proxy.ID, q.Proxy.IP).First()
if err != nil {
return core.NewServErr("获取代理数据失败", err)
}
if proxy == nil {
return core.NewBizErr("代理不存在")
}
return rebuildFreeChans(id, proxy.IP.Addr)
}
func (s *proxyService) Remove(id int32) error {
used, err := hasUsedChans(id)
if err != nil {