mirror of
https://github.com/TeaOSLab/EdgeAPI.git
synced 2025-11-08 03:00:26 +08:00
智能DNS初步支持搜索引擎线路
This commit is contained in:
98
internal/db/models/clients/client_agent_dao.go
Normal file
98
internal/db/models/clients/client_agent_dao.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package clients
|
||||
|
||||
import (
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
)
|
||||
|
||||
type ClientAgentDAO dbs.DAO
|
||||
|
||||
func NewClientAgentDAO() *ClientAgentDAO {
|
||||
return dbs.NewDAO(&ClientAgentDAO{
|
||||
DAOObject: dbs.DAOObject{
|
||||
DB: Tea.Env,
|
||||
Table: "edgeClientAgents",
|
||||
Model: new(ClientAgent),
|
||||
PkName: "id",
|
||||
},
|
||||
}).(*ClientAgentDAO)
|
||||
}
|
||||
|
||||
var SharedClientAgentDAO *ClientAgentDAO
|
||||
|
||||
func init() {
|
||||
dbs.OnReady(func() {
|
||||
SharedClientAgentDAO = NewClientAgentDAO()
|
||||
})
|
||||
}
|
||||
|
||||
// FindClientAgentName 根据主键查找名称
|
||||
func (this *ClientAgentDAO) FindClientAgentName(tx *dbs.Tx, id int64) (string, error) {
|
||||
return this.Query(tx).
|
||||
Pk(id).
|
||||
Result("name").
|
||||
FindStringCol("")
|
||||
}
|
||||
|
||||
// FindAgent 查找Agent
|
||||
func (this *ClientAgentDAO) FindAgent(tx *dbs.Tx, agentId int64) (*ClientAgent, error) {
|
||||
if agentId <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
one, err := this.Query(tx).
|
||||
Pk(agentId).
|
||||
Find()
|
||||
if err != nil || one == nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return one.(*ClientAgent), nil
|
||||
}
|
||||
|
||||
// FindAgentIdWithCode 根据代号查找ID
|
||||
func (this *ClientAgentDAO) FindAgentIdWithCode(tx *dbs.Tx, code string) (int64, error) {
|
||||
return this.Query(tx).
|
||||
ResultPk().
|
||||
Attr("code", code).
|
||||
FindInt64Col(0)
|
||||
}
|
||||
|
||||
// FindAgentNameWithCode 根据代号查找Agent名称
|
||||
func (this *ClientAgentDAO) FindAgentNameWithCode(tx *dbs.Tx, code string) (string, error) {
|
||||
return this.Query(tx).
|
||||
Result("name").
|
||||
Attr("code", code).
|
||||
FindStringCol("")
|
||||
}
|
||||
|
||||
// UpdateAgentCountIPs 修改Agent拥有的IP数量
|
||||
func (this *ClientAgentDAO) UpdateAgentCountIPs(tx *dbs.Tx, agentId int64, countIPs int64) error {
|
||||
return this.Query(tx).
|
||||
Pk(agentId).
|
||||
Set("countIPs", countIPs).
|
||||
UpdateQuickly()
|
||||
}
|
||||
|
||||
// FindAllAgents 查找所有Agents
|
||||
func (this *ClientAgentDAO) FindAllAgents(tx *dbs.Tx) (result []*ClientAgent, err error) {
|
||||
_, err = this.Query(tx).
|
||||
Desc("order").
|
||||
AscPk().
|
||||
Slice(&result).
|
||||
FindAll()
|
||||
return
|
||||
}
|
||||
|
||||
// FindAllNSAgents 查找所有DNS可以使用的Agents
|
||||
func (this *ClientAgentDAO) FindAllNSAgents(tx *dbs.Tx) (result []*ClientAgent, err error) {
|
||||
// 注意:允许NS使用所有的Agent,不管有没有IP数据
|
||||
_, err = this.Query(tx).
|
||||
Result("id", "name", "code").
|
||||
Desc("order").
|
||||
AscPk().
|
||||
Slice(&result).
|
||||
FindAll()
|
||||
return
|
||||
}
|
||||
6
internal/db/models/clients/client_agent_dao_test.go
Normal file
6
internal/db/models/clients/client_agent_dao_test.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package clients_test
|
||||
|
||||
import (
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/iwind/TeaGo/bootstrap"
|
||||
)
|
||||
105
internal/db/models/clients/client_agent_ip_dao.go
Normal file
105
internal/db/models/clients/client_agent_ip_dao.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package clients
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
)
|
||||
|
||||
// TODO 需要定时对所有IP的PTR进行检查,剔除已经变更的IP
|
||||
|
||||
type ClientAgentIPDAO dbs.DAO
|
||||
|
||||
func NewClientAgentIPDAO() *ClientAgentIPDAO {
|
||||
return dbs.NewDAO(&ClientAgentIPDAO{
|
||||
DAOObject: dbs.DAOObject{
|
||||
DB: Tea.Env,
|
||||
Table: "edgeClientAgentIPs",
|
||||
Model: new(ClientAgentIP),
|
||||
PkName: "id",
|
||||
},
|
||||
}).(*ClientAgentIPDAO)
|
||||
}
|
||||
|
||||
var SharedClientAgentIPDAO *ClientAgentIPDAO
|
||||
|
||||
func init() {
|
||||
dbs.OnReady(func() {
|
||||
SharedClientAgentIPDAO = NewClientAgentIPDAO()
|
||||
})
|
||||
}
|
||||
|
||||
// CreateIP 写入IP
|
||||
func (this *ClientAgentIPDAO) CreateIP(tx *dbs.Tx, agentId int64, ip string, ptr string) error {
|
||||
// 检查数据有效性
|
||||
if agentId <= 0 || len(ip) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 限制ptr长度
|
||||
if len(ptr) > 100 {
|
||||
ptr = ptr[:100]
|
||||
}
|
||||
|
||||
// 检查是否存在
|
||||
exists, err := this.Query(tx).
|
||||
Attr("agentId", agentId).
|
||||
Attr("ip", ip).
|
||||
Exist()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
var op = NewClientAgentIPOperator()
|
||||
op.AgentId = agentId
|
||||
op.IP = ip
|
||||
op.Ptr = ptr
|
||||
err = this.Save(tx, op)
|
||||
if err != nil {
|
||||
// 忽略duplicate错误
|
||||
if models.CheckSQLDuplicateErr(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新Agent IP数量
|
||||
countIPs, err := this.CountAgentIPs(tx, agentId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = SharedClientAgentDAO.UpdateAgentCountIPs(tx, agentId, countIPs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListIPsAfterId 列出某个ID之后的IP
|
||||
func (this *ClientAgentIPDAO) ListIPsAfterId(tx *dbs.Tx, id int64, size int64) (result []*ClientAgentIP, err error) {
|
||||
if id < 0 {
|
||||
id = 0
|
||||
}
|
||||
|
||||
_, err = this.Query(tx).
|
||||
Result("id", "ip", "agentId").
|
||||
Gt("id", id).
|
||||
AscPk().
|
||||
Limit(size). // 限制单次读取个数
|
||||
Slice(&result).
|
||||
FindAll()
|
||||
return
|
||||
}
|
||||
|
||||
// CountAgentIPs 计算Agent IP数量
|
||||
func (this *ClientAgentIPDAO) CountAgentIPs(tx *dbs.Tx, agentId int64) (int64, error) {
|
||||
return this.Query(tx).
|
||||
Attr("agentId", agentId).
|
||||
Count()
|
||||
}
|
||||
16
internal/db/models/clients/client_agent_ip_dao_test.go
Normal file
16
internal/db/models/clients/client_agent_ip_dao_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package clients_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/db/models/clients"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/iwind/TeaGo/bootstrap"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClientAgentIPDAO_CreateIP(t *testing.T) {
|
||||
var dao = clients.NewClientAgentIPDAO()
|
||||
err := dao.CreateIP(nil, 1, "127.0.0.1", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
20
internal/db/models/clients/client_agent_ip_model.go
Normal file
20
internal/db/models/clients/client_agent_ip_model.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package clients
|
||||
|
||||
// ClientAgentIP Agent IP
|
||||
type ClientAgentIP struct {
|
||||
Id uint64 `field:"id"` // ID
|
||||
AgentId uint32 `field:"agentId"` // Agent ID
|
||||
IP string `field:"ip"` // IP地址
|
||||
Ptr string `field:"ptr"` // PTR值
|
||||
}
|
||||
|
||||
type ClientAgentIPOperator struct {
|
||||
Id any // ID
|
||||
AgentId any // Agent ID
|
||||
IP any // IP地址
|
||||
Ptr any // PTR值
|
||||
}
|
||||
|
||||
func NewClientAgentIPOperator() *ClientAgentIPOperator {
|
||||
return &ClientAgentIPOperator{}
|
||||
}
|
||||
1
internal/db/models/clients/client_agent_ip_model_ext.go
Normal file
1
internal/db/models/clients/client_agent_ip_model_ext.go
Normal file
@@ -0,0 +1 @@
|
||||
package clients
|
||||
24
internal/db/models/clients/client_agent_model.go
Normal file
24
internal/db/models/clients/client_agent_model.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package clients
|
||||
|
||||
// ClientAgent Agent库
|
||||
type ClientAgent struct {
|
||||
Id uint32 `field:"id"` // ID
|
||||
Name string `field:"name"` // 名称
|
||||
Code string `field:"code"` // 代号
|
||||
Description string `field:"description"` // 介绍
|
||||
Order uint32 `field:"order"` // 排序
|
||||
CountIPs uint32 `field:"countIPs"` // IP数量
|
||||
}
|
||||
|
||||
type ClientAgentOperator struct {
|
||||
Id any // ID
|
||||
Name any // 名称
|
||||
Code any // 代号
|
||||
Description any // 介绍
|
||||
Order any // 排序
|
||||
CountIPs any // IP数量
|
||||
}
|
||||
|
||||
func NewClientAgentOperator() *ClientAgentOperator {
|
||||
return &ClientAgentOperator{}
|
||||
}
|
||||
6
internal/db/models/clients/client_agent_model_ext.go
Normal file
6
internal/db/models/clients/client_agent_model_ext.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package clients
|
||||
|
||||
// NSRouteCode NS线路代号
|
||||
func (this *ClientAgent) NSRouteCode() string {
|
||||
return "agent:" + this.Code
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
package clients_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/db/models/clients"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClientBrowserDAO_CreateBrowser(t *testing.T) {
|
||||
var dao = models.NewClientBrowserDAO()
|
||||
var dao = clients.NewClientBrowserDAO()
|
||||
err := dao.CreateBrowserIfNotExists(nil, "Hello")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -25,7 +25,7 @@ func TestClientBrowserDAO_CreateBrowser(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestClientBrowserDAO_Clean(t *testing.T) {
|
||||
var dao = models.NewClientBrowserDAO()
|
||||
var dao = clients.NewClientBrowserDAO()
|
||||
err := dao.Clean(nil, 30)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package clients_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/db/models/clients"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClientSystemDAO_CreateSystemIfNotExists(t *testing.T) {
|
||||
var dao = models.NewClientSystemDAO()
|
||||
var dao = clients.NewClientSystemDAO()
|
||||
{
|
||||
err := dao.CreateSystemIfNotExists(nil, "Mac OS X")
|
||||
if err != nil {
|
||||
@@ -23,7 +23,7 @@ func TestClientSystemDAO_CreateSystemIfNotExists(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestClientSystemDAO_Clean(t *testing.T) {
|
||||
var dao = models.NewClientSystemDAO()
|
||||
var dao = clients.NewClientSystemDAO()
|
||||
err := dao.Clean(nil, 30)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -23,6 +23,7 @@ type NSCluster struct {
|
||||
Answer dbs.JSON `field:"answer"` // 应答设置
|
||||
SoaSerial uint64 `field:"soaSerial"` // SOA序列号
|
||||
Email string `field:"email"` // 管理员邮箱
|
||||
DetectAgents bool `field:"detectAgents"` // 是否监测Agents
|
||||
}
|
||||
|
||||
type NSClusterOperator struct {
|
||||
@@ -45,6 +46,7 @@ type NSClusterOperator struct {
|
||||
Answer any // 应答设置
|
||||
SoaSerial any // SOA序列号
|
||||
Email any // 管理员邮箱
|
||||
DetectAgents any // 是否监测Agents
|
||||
}
|
||||
|
||||
func NewNSClusterOperator() *NSClusterOperator {
|
||||
|
||||
@@ -72,3 +72,11 @@ func CheckSQLErrCode(err error, code uint16) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// CheckSQLDuplicateErr 检查Duplicate错误
|
||||
func CheckSQLDuplicateErr(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
return CheckSQLErrCode(err, 1062)
|
||||
}
|
||||
|
||||
@@ -206,6 +206,23 @@ func (this *EdgeDNSAPIProvider) GetRoutes(domain string) (routes []*dnstypes.Rou
|
||||
}
|
||||
}
|
||||
|
||||
// Agent
|
||||
{
|
||||
var routesResp = &edgeapi.FindAllNSRoutesResponse{}
|
||||
err = this.doAPI("/NSRouteService/FindAllAgentNSRoutes", map[string]any{}, routesResp)
|
||||
if err != nil {
|
||||
// 忽略错误,因为老版本的EdgeDNS没有提供这个接口
|
||||
err = nil
|
||||
} else {
|
||||
for _, route := range routesResp.Data.NSRoutes {
|
||||
routes = append(routes, &dnstypes.Route{
|
||||
Name: route.Name,
|
||||
Code: route.Code,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义
|
||||
{
|
||||
var routesResp = &edgeapi.FindAllNSRoutesResponse{}
|
||||
|
||||
@@ -414,6 +414,16 @@ func (this *APINode) registerServices(server *grpc.Server) {
|
||||
pb.RegisterFormalClientBrowserServiceServer(server, instance)
|
||||
this.rest(instance)
|
||||
}
|
||||
{
|
||||
var instance = this.serviceInstance(&clients.ClientAgentIPService{}).(*clients.ClientAgentIPService)
|
||||
pb.RegisterClientAgentIPServiceServer(server, instance)
|
||||
this.rest(instance)
|
||||
}
|
||||
{
|
||||
var instance = this.serviceInstance(&clients.ClientAgentService{}).(*clients.ClientAgentService)
|
||||
pb.RegisterClientAgentServiceServer(server, instance)
|
||||
this.rest(instance)
|
||||
}
|
||||
{
|
||||
var instance = this.serviceInstance(&services.ServerClientSystemMonthlyStatService{}).(*services.ServerClientSystemMonthlyStatService)
|
||||
pb.RegisterServerClientSystemMonthlyStatServiceServer(server, instance)
|
||||
|
||||
40
internal/rpc/services/clients/service_client_agent.go
Normal file
40
internal/rpc/services/clients/service_client_agent.go
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package clients
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/db/models/clients"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
)
|
||||
|
||||
// ClientAgentService Agent服务
|
||||
type ClientAgentService struct {
|
||||
services.BaseService
|
||||
}
|
||||
|
||||
// FindAllClientAgents 查找所有Agent
|
||||
func (this *ClientAgentService) FindAllClientAgents(ctx context.Context, req *pb.FindAllClientAgentsRequest) (*pb.FindAllClientAgentsResponse, error) {
|
||||
_, err := this.ValidateAdmin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tx = this.NullTx()
|
||||
agents, err := clients.SharedClientAgentDAO.FindAllAgents(tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var pbAgents = []*pb.ClientAgent{}
|
||||
for _, agent := range agents {
|
||||
pbAgents = append(pbAgents, &pb.ClientAgent{
|
||||
Id: int64(agent.Id),
|
||||
Name: agent.Name,
|
||||
Code: agent.Code,
|
||||
Description: agent.Description,
|
||||
CountIPs: int64(agent.CountIPs),
|
||||
})
|
||||
}
|
||||
return &pb.FindAllClientAgentsResponse{ClientAgents: pbAgents}, nil
|
||||
}
|
||||
97
internal/rpc/services/clients/service_client_agent_ip.go
Normal file
97
internal/rpc/services/clients/service_client_agent_ip.go
Normal file
@@ -0,0 +1,97 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package clients
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/db/models/clients"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
)
|
||||
|
||||
// ClientAgentIPService Agent IP服务
|
||||
type ClientAgentIPService struct {
|
||||
services.BaseService
|
||||
}
|
||||
|
||||
// CreateClientAgentIPs 创建一组IP
|
||||
func (this *ClientAgentIPService) CreateClientAgentIPs(ctx context.Context, req *pb.CreateClientAgentIPsRequest) (*pb.RPCSuccess, error) {
|
||||
// 先不支持网站服务节点,避免影响普通用户
|
||||
_, err := this.ValidateNSNode(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(req.AgentIPs) == 0 {
|
||||
return this.Success()
|
||||
}
|
||||
|
||||
var tx = this.NullTx()
|
||||
for _, agentIP := range req.AgentIPs {
|
||||
agentId, err := clients.SharedClientAgentDAO.FindAgentIdWithCode(tx, agentIP.AgentCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if agentId <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
err = clients.SharedClientAgentIPDAO.CreateIP(tx, agentId, agentIP.Ip, agentIP.Ptr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return this.Success()
|
||||
}
|
||||
|
||||
// ListClientAgentIPsAfterId 查询最新的IP
|
||||
func (this *ClientAgentIPService) ListClientAgentIPsAfterId(ctx context.Context, req *pb.ListClientAgentIPsAfterIdRequest) (*pb.ListClientAgentIPsAfterIdResponse, error) {
|
||||
_, err := this.ValidateNSNode(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if req.Size <= 0 {
|
||||
req.Size = 10000
|
||||
}
|
||||
|
||||
var tx = this.NullTx()
|
||||
var agentMap = map[int64]*clients.ClientAgent{} // agentId => agentCode
|
||||
agentIPs, err := clients.SharedClientAgentIPDAO.ListIPsAfterId(tx, req.Id, req.Size)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var pbIPs = []*pb.ClientAgentIP{}
|
||||
for _, agentIP := range agentIPs {
|
||||
var agentId = int64(agentIP.AgentId)
|
||||
agent, ok := agentMap[agentId]
|
||||
if !ok {
|
||||
agent, err = clients.SharedClientAgentDAO.FindAgent(tx, agentId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if agent == nil {
|
||||
continue
|
||||
}
|
||||
agentMap[agentId] = agent
|
||||
}
|
||||
|
||||
pbIPs = append(pbIPs, &pb.ClientAgentIP{
|
||||
Id: int64(agentIP.Id),
|
||||
Ip: agentIP.IP,
|
||||
Ptr: "",
|
||||
ClientAgent: &pb.ClientAgent{
|
||||
Id: agentId,
|
||||
Name: "",
|
||||
Code: agent.Code,
|
||||
Description: "",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return &pb.ListClientAgentIPsAfterIdResponse{
|
||||
ClientAgentIPs: pbIPs,
|
||||
}, nil
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -49,6 +49,14 @@ var recordsTables = []*SQLRecordsTable{
|
||||
TableName: "edgeFormalClientBrowsers",
|
||||
UniqueFields: []string{"dataId"},
|
||||
},
|
||||
{
|
||||
TableName: "edgeClientAgents",
|
||||
UniqueFields: []string{"code"},
|
||||
},
|
||||
{
|
||||
TableName: "edgeClientAgentIPs",
|
||||
UniqueFields: []string{"agentId", "ip"},
|
||||
},
|
||||
}
|
||||
|
||||
type sqlItem struct {
|
||||
|
||||
Reference in New Issue
Block a user