重构提取逻辑,新增 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

@@ -1,5 +1,8 @@
## TODO
- 日志记录
- 后台展示 mac, ip:port实际地区
上传文件平铺到 uploads不分子文件夹
错误提示增强,展示整链路信息

View File

@@ -35,6 +35,7 @@ func main() {
m.Admin{},
m.AdminRole{},
m.Announcement{},
m.Area{},
m.Article{},
m.ArticleGroup{},
m.Bill{},

View File

@@ -46,6 +46,7 @@ services:
-api test:test@:9700
ports:
- "9700:9700"
restart: unless-stopped
volumes:
postgres_data:

258
scripts/sql/fill-area.sql Normal file
View File

@@ -0,0 +1,258 @@
insert into area
(name, level)
values
('上海',1),
('云南',1),
('内蒙古',1),
('北京',1),
('吉林',1),
('四川',1),
('天津',1),
('宁夏',1),
('安徽',1),
('山东',1),
('山西',1),
('广东',1),
('广西',1),
('新疆',1),
('江苏',1),
('江西',1),
('河北',1),
('河南',1),
('浙江',1),
('海南',1),
('湖北',1),
('湖南',1),
('甘肃',1),
('福建',1),
('贵州',1),
('辽宁',1),
('重庆',1),
('陕西',1),
('黑龙江',1)
;
insert into area
(name, level, parent_id)
values
('上海', 2, (select id from area where name = '上海')),
('昆明', 2, (select id from area where name = '云南')),
('包头', 2, (select id from area where name = '内蒙古')),
('呼伦贝尔', 2, (select id from area where name = '内蒙古')),
('呼和浩特', 2, (select id from area where name = '内蒙古')),
('赤峰', 2, (select id from area where name = '内蒙古')),
('通辽', 2, (select id from area where name = '内蒙古')),
('鄂尔多斯', 2, (select id from area where name = '内蒙古')),
('北京', 2, (select id from area where name = '北京')),
('四平', 2, (select id from area where name = '吉林')),
('延边朝鲜族自治州', 2, (select id from area where name = '吉林')),
('松原', 2, (select id from area where name = '吉林')),
('白山', 2, (select id from area where name = '吉林')),
('通化', 2, (select id from area where name = '吉林')),
('长春', 2, (select id from area where name = '吉林')),
('乐山', 2, (select id from area where name = '四川')),
('内江', 2, (select id from area where name = '四川')),
('南充', 2, (select id from area where name = '四川')),
('宜宾', 2, (select id from area where name = '四川')),
('广元', 2, (select id from area where name = '四川')),
('德阳', 2, (select id from area where name = '四川')),
('成都', 2, (select id from area where name = '四川')),
('攀枝花', 2, (select id from area where name = '四川')),
('泸州', 2, (select id from area where name = '四川')),
('绵阳', 2, (select id from area where name = '四川')),
('自贡', 2, (select id from area where name = '四川')),
('达州', 2, (select id from area where name = '四川')),
('天津', 2, (select id from area where name = '天津')),
('银川', 2, (select id from area where name = '宁夏')),
('亳州', 2, (select id from area where name = '安徽')),
('六安', 2, (select id from area where name = '安徽')),
('合肥', 2, (select id from area where name = '安徽')),
('安庆', 2, (select id from area where name = '安徽')),
('宣城', 2, (select id from area where name = '安徽')),
('宿州', 2, (select id from area where name = '安徽')),
('池州', 2, (select id from area where name = '安徽')),
('淮北', 2, (select id from area where name = '安徽')),
('淮南', 2, (select id from area where name = '安徽')),
('滁州', 2, (select id from area where name = '安徽')),
('芜湖', 2, (select id from area where name = '安徽')),
('蚌埠', 2, (select id from area where name = '安徽')),
('铜陵', 2, (select id from area where name = '安徽')),
('阜阳', 2, (select id from area where name = '安徽')),
('马鞍山', 2, (select id from area where name = '安徽')),
('黄山', 2, (select id from area where name = '安徽')),
('东营', 2, (select id from area where name = '山东')),
('临沂', 2, (select id from area where name = '山东')),
('威海', 2, (select id from area where name = '山东')),
('德州', 2, (select id from area where name = '山东')),
('日照', 2, (select id from area where name = '山东')),
('枣庄', 2, (select id from area where name = '山东')),
('泰安', 2, (select id from area where name = '山东')),
('济南', 2, (select id from area where name = '山东')),
('济宁', 2, (select id from area where name = '山东')),
('淄博', 2, (select id from area where name = '山东')),
('滨州', 2, (select id from area where name = '山东')),
('潍坊', 2, (select id from area where name = '山东')),
('烟台', 2, (select id from area where name = '山东')),
('聊城', 2, (select id from area where name = '山东')),
('菏泽', 2, (select id from area where name = '山东')),
('青岛', 2, (select id from area where name = '山东')),
('临汾', 2, (select id from area where name = '山西')),
('吕梁', 2, (select id from area where name = '山西')),
('大同', 2, (select id from area where name = '山西')),
('太原', 2, (select id from area where name = '山西')),
('忻州', 2, (select id from area where name = '山西')),
('晋城', 2, (select id from area where name = '山西')),
('朔州', 2, (select id from area where name = '山西')),
('运城', 2, (select id from area where name = '山西')),
('长治', 2, (select id from area where name = '山西')),
('阳泉', 2, (select id from area where name = '山西')),
('东莞', 2, (select id from area where name = '广东')),
('中山', 2, (select id from area where name = '广东')),
('云浮', 2, (select id from area where name = '广东')),
('佛山', 2, (select id from area where name = '广东')),
('广州', 2, (select id from area where name = '广东')),
('惠州', 2, (select id from area where name = '广东')),
('揭阳', 2, (select id from area where name = '广东')),
('梅州', 2, (select id from area where name = '广东')),
('汕头', 2, (select id from area where name = '广东')),
('汕尾', 2, (select id from area where name = '广东')),
('江门', 2, (select id from area where name = '广东')),
('河源', 2, (select id from area where name = '广东')),
('深圳', 2, (select id from area where name = '广东')),
('清远', 2, (select id from area where name = '广东')),
('湛江', 2, (select id from area where name = '广东')),
('潮州', 2, (select id from area where name = '广东')),
('珠海', 2, (select id from area where name = '广东')),
('肇庆', 2, (select id from area where name = '广东')),
('茂名', 2, (select id from area where name = '广东')),
('阳江', 2, (select id from area where name = '广东')),
('韶关', 2, (select id from area where name = '广东')),
('北海', 2, (select id from area where name = '广西')),
('南宁', 2, (select id from area where name = '广西')),
('柳州', 2, (select id from area where name = '广西')),
('桂林', 2, (select id from area where name = '广西')),
('玉林', 2, (select id from area where name = '广西')),
('贵港', 2, (select id from area where name = '广西')),
('钦州', 2, (select id from area where name = '广西')),
('乌鲁木齐', 2, (select id from area where name = '新疆')),
('南京', 2, (select id from area where name = '江苏')),
('南通', 2, (select id from area where name = '江苏')),
('宿迁', 2, (select id from area where name = '江苏')),
('常州', 2, (select id from area where name = '江苏')),
('徐州', 2, (select id from area where name = '江苏')),
('扬州', 2, (select id from area where name = '江苏')),
('无锡', 2, (select id from area where name = '江苏')),
('泰州', 2, (select id from area where name = '江苏')),
('淮安', 2, (select id from area where name = '江苏')),
('盐城', 2, (select id from area where name = '江苏')),
('苏州', 2, (select id from area where name = '江苏')),
('连云港', 2, (select id from area where name = '江苏')),
('镇江', 2, (select id from area where name = '江苏')),
('上饶', 2, (select id from area where name = '江西')),
('九江', 2, (select id from area where name = '江西')),
('南昌', 2, (select id from area where name = '江西')),
('吉安', 2, (select id from area where name = '江西')),
('宜春', 2, (select id from area where name = '江西')),
('抚州', 2, (select id from area where name = '江西')),
('新余', 2, (select id from area where name = '江西')),
('景德镇', 2, (select id from area where name = '江西')),
('萍乡', 2, (select id from area where name = '江西')),
('赣州', 2, (select id from area where name = '江西')),
('鹰潭', 2, (select id from area where name = '江西')),
('保定', 2, (select id from area where name = '河北')),
('唐山', 2, (select id from area where name = '河北')),
('廊坊', 2, (select id from area where name = '河北')),
('张家口', 2, (select id from area where name = '河北')),
('承德', 2, (select id from area where name = '河北')),
('沧州', 2, (select id from area where name = '河北')),
('石家庄', 2, (select id from area where name = '河北')),
('秦皇岛', 2, (select id from area where name = '河北')),
('衡水', 2, (select id from area where name = '河北')),
('邢台', 2, (select id from area where name = '河北')),
('邯郸', 2, (select id from area where name = '河北')),
('信阳', 2, (select id from area where name = '河南')),
('南阳', 2, (select id from area where name = '河南')),
('周口', 2, (select id from area where name = '河南')),
('商丘', 2, (select id from area where name = '河南')),
('安阳', 2, (select id from area where name = '河南')),
('开封', 2, (select id from area where name = '河南')),
('新乡', 2, (select id from area where name = '河南')),
('洛阳', 2, (select id from area where name = '河南')),
('漯河', 2, (select id from area where name = '河南')),
('焦作', 2, (select id from area where name = '河南')),
('许昌', 2, (select id from area where name = '河南')),
('郑州', 2, (select id from area where name = '河南')),
('驻马店', 2, (select id from area where name = '河南')),
('鹤壁', 2, (select id from area where name = '河南')),
('丽水', 2, (select id from area where name = '浙江')),
('台州', 2, (select id from area where name = '浙江')),
('嘉兴', 2, (select id from area where name = '浙江')),
('宁波', 2, (select id from area where name = '浙江')),
('杭州', 2, (select id from area where name = '浙江')),
('温州', 2, (select id from area where name = '浙江')),
('湖州', 2, (select id from area where name = '浙江')),
('绍兴', 2, (select id from area where name = '浙江')),
('舟山', 2, (select id from area where name = '浙江')),
('衢州', 2, (select id from area where name = '浙江')),
('金华', 2, (select id from area where name = '浙江')),
('三亚', 2, (select id from area where name = '海南')),
('文昌', 2, (select id from area where name = '海南')),
('海口', 2, (select id from area where name = '海南')),
('咸宁', 2, (select id from area where name = '湖北')),
('孝感', 2, (select id from area where name = '湖北')),
('宜昌', 2, (select id from area where name = '湖北')),
('武汉', 2, (select id from area where name = '湖北')),
('荆州', 2, (select id from area where name = '湖北')),
('荆门', 2, (select id from area where name = '湖北')),
('襄阳', 2, (select id from area where name = '湖北')),
('黄冈', 2, (select id from area where name = '湖北')),
('黄石', 2, (select id from area where name = '湖北')),
('岳阳', 2, (select id from area where name = '湖南')),
('株洲', 2, (select id from area where name = '湖南')),
('湘潭', 2, (select id from area where name = '湖南')),
('衡阳', 2, (select id from area where name = '湖南')),
('邵阳', 2, (select id from area where name = '湖南')),
('郴州', 2, (select id from area where name = '湖南')),
('长沙', 2, (select id from area where name = '湖南')),
('兰州', 2, (select id from area where name = '甘肃')),
('三明', 2, (select id from area where name = '福建')),
('南平', 2, (select id from area where name = '福建')),
('厦门', 2, (select id from area where name = '福建')),
('宁德', 2, (select id from area where name = '福建')),
('泉州', 2, (select id from area where name = '福建')),
('福州', 2, (select id from area where name = '福建')),
('莆田', 2, (select id from area where name = '福建')),
('龙岩', 2, (select id from area where name = '福建')),
('六盘水', 2, (select id from area where name = '贵州')),
('贵阳', 2, (select id from area where name = '贵州')),
('遵义', 2, (select id from area where name = '贵州')),
('铜仁', 2, (select id from area where name = '贵州')),
('黔东南苗族侗族自治州', 2, (select id from area where name = '贵州')),
('大连', 2, (select id from area where name = '辽宁')),
('抚顺', 2, (select id from area where name = '辽宁')),
('朝阳', 2, (select id from area where name = '辽宁')),
('沈阳', 2, (select id from area where name = '辽宁')),
('盘锦', 2, (select id from area where name = '辽宁')),
('营口', 2, (select id from area where name = '辽宁')),
('葫芦岛', 2, (select id from area where name = '辽宁')),
('铁岭', 2, (select id from area where name = '辽宁')),
('鞍山', 2, (select id from area where name = '辽宁')),
('重庆', 2, (select id from area where name = '重庆')),
('咸阳', 2, (select id from area where name = '陕西')),
('宝鸡', 2, (select id from area where name = '陕西')),
('渭南', 2, (select id from area where name = '陕西')),
('西安', 2, (select id from area where name = '陕西')),
('铜川', 2, (select id from area where name = '陕西')),
('七台河', 2, (select id from area where name = '黑龙江')),
('伊春', 2, (select id from area where name = '黑龙江')),
('佳木斯', 2, (select id from area where name = '黑龙江')),
('双鸭山', 2, (select id from area where name = '黑龙江')),
('哈尔滨', 2, (select id from area where name = '黑龙江')),
('大庆', 2, (select id from area where name = '黑龙江')),
('牡丹江', 2, (select id from area where name = '黑龙江')),
('绥化', 2, (select id from area where name = '黑龙江')),
('鸡西', 2, (select id from area where name = '黑龙江')),
('鹤岗', 2, (select id from area where name = '黑龙江')),
('黑河', 2, (select id from area where name = '黑龙江')),
('齐齐哈尔', 2, (select id from area where name = '黑龙江'))

6601
scripts/sql/fill-edge.sql Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -634,6 +634,31 @@ comment on column proxy.created_at is '创建时间';
comment on column proxy.updated_at is '更新时间';
comment on column proxy.deleted_at is '删除时间';
-- area
drop table if exists area cascade;
create table area (
id int generated by default as identity primary key,
name text not null,
level int not null,
parent_id int,
created_at timestamptz default current_timestamp,
updated_at timestamptz default current_timestamp,
deleted_at timestamptz
);
create index idx_area_level on area (level) where deleted_at is null;
create index idx_area_parent_id on area (parent_id) where deleted_at is null;
create index idx_area_created_at on area (created_at) where deleted_at is null;
-- area表字段注释
comment on table area is '地区表';
comment on column area.id is '地区ID';
comment on column area.name is '地区名称';
comment on column area.level is '地区层级1-省2-市';
comment on column area.parent_id is '父级地区ID';
comment on column area.created_at is '创建时间';
comment on column area.updated_at is '更新时间';
comment on column area.deleted_at is '删除时间';
-- edge
drop table if exists edge cascade;
create table edge (
@@ -644,8 +669,7 @@ create table edge (
ip inet not null,
port int,
isp int not null,
prov text not null,
city text not null,
area_id int not null,
status int not null default 0,
rtt int default 0,
loss int default 0,
@@ -655,8 +679,7 @@ create table edge (
);
create unique index udx_edge_mac on edge (mac) where deleted_at is null;
create index idx_edge_isp on edge (isp) where deleted_at is null;
create index idx_edge_prov on edge (prov) where deleted_at is null;
create index idx_edge_city on edge (city) where deleted_at is null;
create index idx_edge_area_id on edge (area_id) where deleted_at is null;
create index idx_edge_created_at on edge (created_at) where deleted_at is null;
-- edge表字段注释
@@ -668,8 +691,7 @@ comment on column edge.mac is '节点 mac 地址或 GOST chain 名称';
comment on column edge.ip is '节点地址或 GOST chain addr 的 IP';
comment on column edge.port is 'GOST chain addr 的端口';
comment on column edge.isp is '运营商1-电信2-联通3-移动';
comment on column edge.prov is '省份';
comment on column edge.city is '城市';
comment on column edge.area_id is '城市地区ID';
comment on column edge.status is '节点状态0-离线1-正常';
comment on column edge.rtt is '最近平均延迟';
comment on column edge.loss is '最近丢包率';
@@ -1237,6 +1259,10 @@ alter table channel
add constraint fk_channel_user_id foreign key (user_id) references "user" (id) on delete cascade;
alter table channel
add constraint fk_channel_proxy_id foreign key (proxy_id) references proxy (id) on delete set null;
alter table area
add constraint fk_area_parent_id foreign key (parent_id) references area (id) on delete set null;
alter table edge
add constraint fk_edge_area_id foreign key (area_id) references area (id) on delete restrict;
alter table channel
add constraint fk_channel_edge_id foreign key (edge_id) references edge (id) on delete set null;
alter table channel

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 {