重构提取逻辑,新增 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 ## TODO
- 日志记录
- 后台展示 mac, ip:port实际地区
上传文件平铺到 uploads不分子文件夹 上传文件平铺到 uploads不分子文件夹
错误提示增强,展示整链路信息 错误提示增强,展示整链路信息

View File

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

View File

@@ -46,6 +46,7 @@ services:
-api test:test@:9700 -api test:test@:9700
ports: ports:
- "9700:9700" - "9700:9700"
restart: unless-stopped
volumes: volumes:
postgres_data: 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.updated_at is '更新时间';
comment on column proxy.deleted_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 -- edge
drop table if exists edge cascade; drop table if exists edge cascade;
create table edge ( create table edge (
@@ -644,8 +669,7 @@ create table edge (
ip inet not null, ip inet not null,
port int, port int,
isp int not null, isp int not null,
prov text not null, area_id int not null,
city text not null,
status int not null default 0, status int not null default 0,
rtt int default 0, rtt int default 0,
loss 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 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_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_area_id on edge (area_id) where deleted_at is null;
create index idx_edge_city on edge (city) where deleted_at is null;
create index idx_edge_created_at on edge (created_at) where deleted_at is null; create index idx_edge_created_at on edge (created_at) where deleted_at is null;
-- edge表字段注释 -- 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.ip is '节点地址或 GOST chain addr 的 IP';
comment on column edge.port is 'GOST chain addr 的端口'; comment on column edge.port is 'GOST chain addr 的端口';
comment on column edge.isp is '运营商1-电信2-联通3-移动'; comment on column edge.isp is '运营商1-电信2-联通3-移动';
comment on column edge.prov is '省份'; comment on column edge.area_id is '城市地区ID';
comment on column edge.city is '城市';
comment on column edge.status is '节点状态0-离线1-正常'; comment on column edge.status is '节点状态0-离线1-正常';
comment on column edge.rtt is '最近平均延迟'; comment on column edge.rtt is '最近平均延迟';
comment on column edge.loss 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; add constraint fk_channel_user_id foreign key (user_id) references "user" (id) on delete cascade;
alter table channel alter table channel
add constraint fk_channel_proxy_id foreign key (proxy_id) references proxy (id) on delete set null; 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 alter table channel
add constraint fk_channel_edge_id foreign key (edge_id) references edge (id) on delete set null; add constraint fk_channel_edge_id foreign key (edge_id) references edge (id) on delete set null;
alter table channel alter table channel

View File

@@ -189,7 +189,7 @@ func (c *gostClient) request(method string, path string, payload any) ([]byte, e
return nil, err return nil, err
} }
if resp.StatusCode == http.StatusNotFound { if resp.StatusCode == http.StatusBadRequest {
return nil, fmt.Errorf("%w: %s", ErrGostNotFound, string(body)) return nil, fmt.Errorf("%w: %s", ErrGostNotFound, string(body))
} }
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { 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 { if err != nil {
return err return err
} }
areaID, err := s.Area.FindIdByFilter(req.Prov, req.City)
var isp *m.EdgeISP if err != nil {
return err
}
filter := &s.EdgeFilter{AreaID: areaID}
if req.Isp != nil { if req.Isp != nil {
isp = u.X(m.ToEdgeISP(*req.Isp)) filter.Isp = u.X(m.ToEdgeISP(*req.Isp))
} }
result, err := s.Channel.CreateChannels( result, err := s.Channel.CreateChannels(
ip, no, ip,
no,
req.AuthType == s.ChannelAuthTypeIp, req.AuthType == s.ChannelAuthTypeIp,
req.AuthType == s.ChannelAuthTypePass, req.AuthType == s.ChannelAuthTypePass,
req.Count, req.Count,
&s.EdgeFilter{ filter,
Isp: isp,
Prov: req.Prov,
City: req.City,
},
) )
if err != nil { if err != nil {
return err return err
} }
// 返回结果 // 返回结果
var resp = make([]*CreateChannelRespItem, len(result)) return c.JSON(buildCreateChannelResp(result, req.Protocol, req.AuthType))
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)
} }
type CreateChannelReq struct { 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 { if req.Isp != nil {
isp = u.X(m.ToEdgeISP(*req.Isp)) filter.Isp = u.X(m.ToEdgeISP(*req.Isp))
} }
result, err := s.Channel.CreateChannels( result, err := s.Channel.CreateChannels(
ip, ip,
@@ -179,31 +170,14 @@ func CreateChannelV2(c *fiber.Ctx) error {
req.AuthType == s.ChannelAuthTypeIp, req.AuthType == s.ChannelAuthTypeIp,
req.AuthType == s.ChannelAuthTypePass, req.AuthType == s.ChannelAuthTypePass,
req.Count, req.Count,
&s.EdgeFilter{ filter,
Isp: isp,
Prov: req.Prov,
City: req.City,
},
) )
if err != nil { if err != nil {
return err return err
} }
// 返回结果 // 返回结果
var resp = make([]*CreateChannelRespItem, len(result)) return c.JSON(buildCreateChannelResp(result, req.Protocol, req.AuthType))
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)
} }
type CreateChannelReqV2 struct { type CreateChannelReqV2 struct {
@@ -216,6 +190,63 @@ type CreateChannelReqV2 struct {
Isp *int `json:"isp"` 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 { type CreateChannelRespItem struct {
Proto int `json:"-"` Proto int `json:"-"`
Host string `json:"host"` Host string `json:"host"`

View File

@@ -5,11 +5,14 @@ import (
"platform/web/core" "platform/web/core"
g "platform/web/globals" g "platform/web/globals"
s "platform/web/services" s "platform/web/services"
"time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
// ====================
// admin 路由
// ====================
func PageProxyByAdmin(c *fiber.Ctx) error { func PageProxyByAdmin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyRead) _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyRead)
if err != nil { if err != nil {
@@ -102,6 +105,24 @@ func UpdateProxyStatus(c *fiber.Ctx) error {
return c.JSON(nil) 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 { func RemoveProxy(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyWrite) _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyWrite)
if err != nil { if err != nil {
@@ -119,347 +140,3 @@ func RemoveProxy(c *fiber.Ctx) error {
return c.JSON(nil) 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

@@ -14,11 +14,11 @@ type Edge struct {
IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // 节点地址或 GOST chain addr 的 IP 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 的端口 Port *uint16 `json:"port,omitempty" gorm:"column:port"` // GOST chain addr 的端口
ISP EdgeISP `json:"isp" gorm:"column:isp"` // 运营商0-未知1-电信2-联通3-移动 ISP EdgeISP `json:"isp" gorm:"column:isp"` // 运营商0-未知1-电信2-联通3-移动
Prov string `json:"prov" gorm:"column:prov"` // 省份 AreaID int32 `json:"area_id" gorm:"column:area_id"` // 城市地区ID
City string `json:"city" gorm:"column:city"` // 城市
Status EdgeStatus `json:"status" gorm:"column:status"` // 节点状态0-离线1-正常 Status EdgeStatus `json:"status" gorm:"column:status"` // 节点状态0-离线1-正常
RTT int32 `json:"rtt" gorm:"column:rtt"` // 最近平均延迟 RTT int32 `json:"rtt" gorm:"column:rtt"` // 最近平均延迟
Loss int32 `json:"loss" gorm:"column:loss"` // 最近丢包率 Loss int32 `json:"loss" gorm:"column:loss"` // 最近丢包率
Area *Area `json:"area,omitempty" gorm:"foreignKey:AreaID"` // 地区
} }
// EdgeType 节点类型枚举 // 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 { Edge struct {
field.RelationField field.RelationField
Area struct {
field.RelationField
Parent struct {
field.RelationField
}
}
} }
}{ }{
RelationField: field.NewRelation("Proxy.Channels", "models.Channel"), RelationField: field.NewRelation("Proxy.Channels", "models.Channel"),
@@ -238,8 +244,27 @@ func newChannel(db *gorm.DB, opts ...gen.DOOption) channel {
}, },
Edge: struct { Edge: struct {
field.RelationField field.RelationField
Area struct {
field.RelationField
Parent struct {
field.RelationField
}
}
}{ }{
RelationField: field.NewRelation("Proxy.Channels.Edge", "models.Edge"), 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 { Edge struct {
field.RelationField 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.IP = field.NewField(tableName, "ip")
_edge.Port = field.NewUint16(tableName, "port") _edge.Port = field.NewUint16(tableName, "port")
_edge.ISP = field.NewInt(tableName, "isp") _edge.ISP = field.NewInt(tableName, "isp")
_edge.Prov = field.NewString(tableName, "prov") _edge.AreaID = field.NewInt32(tableName, "area_id")
_edge.City = field.NewString(tableName, "city")
_edge.Status = field.NewInt(tableName, "status") _edge.Status = field.NewInt(tableName, "status")
_edge.RTT = field.NewInt32(tableName, "rtt") _edge.RTT = field.NewInt32(tableName, "rtt")
_edge.Loss = field.NewInt32(tableName, "loss") _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() _edge.fillFieldMap()
@@ -62,11 +71,11 @@ type edge struct {
IP field.Field IP field.Field
Port field.Uint16 Port field.Uint16
ISP field.Int ISP field.Int
Prov field.String AreaID field.Int32
City field.String
Status field.Int Status field.Int
RTT field.Int32 RTT field.Int32
Loss field.Int32 Loss field.Int32
Area edgeBelongsToArea
fieldMap map[string]field.Expr fieldMap map[string]field.Expr
} }
@@ -93,8 +102,7 @@ func (e *edge) updateTableName(table string) *edge {
e.IP = field.NewField(table, "ip") e.IP = field.NewField(table, "ip")
e.Port = field.NewUint16(table, "port") e.Port = field.NewUint16(table, "port")
e.ISP = field.NewInt(table, "isp") e.ISP = field.NewInt(table, "isp")
e.Prov = field.NewString(table, "prov") e.AreaID = field.NewInt32(table, "area_id")
e.City = field.NewString(table, "city")
e.Status = field.NewInt(table, "status") e.Status = field.NewInt(table, "status")
e.RTT = field.NewInt32(table, "rtt") e.RTT = field.NewInt32(table, "rtt")
e.Loss = field.NewInt32(table, "loss") e.Loss = field.NewInt32(table, "loss")
@@ -125,23 +133,111 @@ func (e *edge) fillFieldMap() {
e.fieldMap["ip"] = e.IP e.fieldMap["ip"] = e.IP
e.fieldMap["port"] = e.Port e.fieldMap["port"] = e.Port
e.fieldMap["isp"] = e.ISP e.fieldMap["isp"] = e.ISP
e.fieldMap["prov"] = e.Prov e.fieldMap["area_id"] = e.AreaID
e.fieldMap["city"] = e.City
e.fieldMap["status"] = e.Status e.fieldMap["status"] = e.Status
e.fieldMap["rtt"] = e.RTT e.fieldMap["rtt"] = e.RTT
e.fieldMap["loss"] = e.Loss e.fieldMap["loss"] = e.Loss
} }
func (e edge) clone(db *gorm.DB) edge { func (e edge) clone(db *gorm.DB) edge {
e.edgeDo.ReplaceConnPool(db.Statement.ConnPool) 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 return e
} }
func (e edge) replaceDB(db *gorm.DB) edge { func (e edge) replaceDB(db *gorm.DB) edge {
e.edgeDo.ReplaceDB(db) e.edgeDo.ReplaceDB(db)
e.Area.db = db.Session(&gorm.Session{})
return e 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 } type edgeDo struct{ gen.DO }
func (e edgeDo) Debug() *edgeDo { func (e edgeDo) Debug() *edgeDo {

View File

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

View File

@@ -262,8 +262,27 @@ func newProxy(db *gorm.DB, opts ...gen.DOOption) proxy {
}, },
Edge: struct { Edge: struct {
field.RelationField field.RelationField
Area struct {
field.RelationField
Parent struct {
field.RelationField
}
}
}{ }{
RelationField: field.NewRelation("Channels.Edge", "models.Edge"), 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 { Edge struct {
field.RelationField field.RelationField
Area struct {
field.RelationField
Parent struct {
field.RelationField
}
}
} }
} }

View File

@@ -1,11 +1,13 @@
package web package web
import ( import (
"fmt"
"platform/pkg/env" "platform/pkg/env"
auth2 "platform/web/auth" auth2 "platform/web/auth"
"platform/web/core" "platform/web/core"
"platform/web/globals" "platform/web/globals"
"platform/web/handlers" "platform/web/handlers"
"strings"
"time" "time"
q "platform/web/queries" q "platform/web/queries"
@@ -39,7 +41,6 @@ func ApplyRouters(app *fiber.App) {
debug.Get("/test/err", func(ctx *fiber.Ctx) error { debug.Get("/test/err", func(ctx *fiber.Ctx) error {
return core.NewBizErr("测试错误") return core.NewBizErr("测试错误")
}) })
debug.Get("/trade/status/:trade_no", func(ctx *fiber.Ctx) error { debug.Get("/trade/status/:trade_no", func(ctx *fiber.Ctx) error {
tradeNo := ctx.Params("trade_no") tradeNo := ctx.Params("trade_no")
resp, err := globals.SFTPay.QueryTrade(&globals.QueryTradeReq{ resp, err := globals.SFTPay.QueryTrade(&globals.QueryTradeReq{
@@ -50,6 +51,46 @@ func ApplyRouters(app *fiber.App) {
} }
return ctx.JSON(resp) 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 := client.Group("/proxy")
proxy.Post("/online", handlers.ProxyReportOnline) proxy.Post("/sync-pool", handlers.SyncProxyPool)
proxy.Post("/offline", handlers.ProxyReportOffline)
proxy.Post("/update", handlers.ProxyReportUpdate)
// 通道管理 // 通道管理
channel := client.Group("/channel") channel := client.Group("/channel")
@@ -139,6 +178,11 @@ func userRouter(api fiber.Router) {
channel.Post("/list", handlers.ListChannel) channel.Post("/list", handlers.ListChannel)
channel.Post("/create", handlers.CreateChannel) channel.Post("/create", handlers.CreateChannel)
channel.Post("/create/v2", handlers.CreateChannelV2) 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") 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) { 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() now := time.Now()
batchNo := ID.GenReadable("bat") batchNo := ID.GenReadable("bat")
var channels []*m.Channel var channels []*m.Channel
if edgeFilter == nil {
edgeFilter = &EdgeFilter{}
}
var whitelistText *string var whitelistText *string
err := g.Redsync.WithLock(lockChannelCreateKey(resourceNo), func() error { err := g.Redsync.WithLock(lockChannelCreateKey(resourceNo), func() error {
@@ -80,6 +89,7 @@ func (s *channelServer) CreateChannels(source netip.Addr, resourceNo string, aut
Expire: expire, Expire: expire,
Count: count, Count: count,
Filter: edgeFilter, Filter: edgeFilter,
Area: area,
AuthWhitelist: authWhitelist, AuthWhitelist: authWhitelist,
AuthPassword: authPassword, AuthPassword: authPassword,
Whitelists: whitelists, Whitelists: whitelists,
@@ -160,6 +170,7 @@ type channelCreateContext struct {
Expire time.Time Expire time.Time
Count int Count int
Filter *EdgeFilter Filter *EdgeFilter
Area *m.Area
AuthWhitelist bool AuthWhitelist bool
AuthPassword bool AuthPassword bool
Whitelists []string Whitelists []string
@@ -172,6 +183,7 @@ type channelCreateResult struct {
} }
func newBaseChannel(ctx *channelCreateContext, port uint16) *m.Channel { func newBaseChannel(ctx *channelCreateContext, port uint16) *m.Channel {
prov, city := areaProvinceCity(ctx.Area)
return &m.Channel{ return &m.Channel{
UserID: ctx.Resource.User.ID, UserID: ctx.Resource.User.ID,
ResourceID: ctx.Resource.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()), Host: u.Else(ctx.Proxy.Host, ctx.Proxy.IP.String()),
Port: port, Port: port,
FilterISP: ctx.Filter.Isp, FilterISP: ctx.Filter.Isp,
FilterProv: ctx.Filter.Prov, FilterProv: prov,
FilterCity: ctx.Filter.City, FilterCity: city,
ExpiredAt: ctx.Expire, ExpiredAt: ctx.Expire,
Proxy: ctx.Proxy, Proxy: ctx.Proxy,
} }
@@ -202,6 +214,7 @@ func applyChannelAuth(ctx *channelCreateContext, channel *m.Channel) (username s
} }
func persistChannelCreate(ctx *channelCreateContext, channels []*m.Channel) error { func persistChannelCreate(ctx *channelCreateContext, channels []*m.Channel) error {
prov, city := areaProvinceCity(ctx.Area)
return q.Q.Transaction(func(tx *q.Query) error { return q.Q.Transaction(func(tx *q.Query) error {
var ( var (
result gen.ResultInfo result gen.ResultInfo
@@ -252,8 +265,8 @@ func persistChannelCreate(ctx *channelCreateContext, channels []*m.Channel) erro
BatchNo: ctx.BatchNo, BatchNo: ctx.BatchNo,
Count: int32(ctx.Count), Count: int32(ctx.Count),
ISP: u.X(ctx.Filter.Isp.String()), ISP: u.X(ctx.Filter.Isp.String()),
Prov: ctx.Filter.Prov, Prov: prov,
City: ctx.Filter.City, City: city,
IP: orm.Inet{Addr: ctx.Source}, IP: orm.Inet{Addr: ctx.Source},
Time: ctx.Now, Time: ctx.Now,
}); err != nil { }); 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) { func findExpiredChannelBatches(proxyId int32, now time.Time) (map[string]struct{}, error) {
keys, err := g.Redis.Keys(context.Background(), usedChansKey(proxyId, "*")).Result() keys, err := g.Redis.Keys(context.Background(), usedChansKey(proxyId, "*")).Result()
if err != nil { if err != nil {
@@ -778,6 +822,20 @@ redis.call("DEL", batch_key)
return 1 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 ( var (
ErrResourceNotExist = core.NewBizErr("套餐不存在") ErrResourceNotExist = core.NewBizErr("套餐不存在")

View File

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

View File

@@ -9,12 +9,14 @@ import (
m "platform/web/models" m "platform/web/models"
q "platform/web/queries" q "platform/web/queries"
"strings" "strings"
"gorm.io/gen"
) )
type channelGostProvider struct{} type channelGostProvider struct{}
func (s *channelGostProvider) prepareCreate(ctx *channelCreateContext) (*channelCreateResult, error) { 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 { if err != nil {
return nil, err return nil, err
} }
@@ -131,26 +133,38 @@ func (s *channelGostProvider) selectProxy(count int) (*m.Proxy, error) {
return selectProxyByType(m.ProxyTypeGost, count) 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 { if filter == nil {
filter = &EdgeFilter{} filter = &EdgeFilter{}
} }
do := q.Edge.Where( conds := []gen.Condition{
q.Edge.Type.Eq(int(m.EdgeTypeGostChain)), q.Edge.Type.Eq(int(m.EdgeTypeGostChain)),
q.Edge.Status.Eq(int(m.EdgeStatusNormal)), 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 { 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 { if err != nil {
return nil, core.NewBizErr("查询可用节点失败", err) 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 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 { func (s *proxyService) Remove(id int32) error {
used, err := hasUsedChans(id) used, err := hasUsedChans(id)
if err != nil { if err != nil {