Compare commits
5 Commits
ebac8042ea
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5bb65216a9 | |||
| 9c6b07ad8c | |||
| 49d2df54f5 | |||
| 25cacf0bca | |||
| 513fe78815 |
@@ -1,6 +1,11 @@
|
|||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
- 日志记录
|
后台交易备注
|
||||||
|
关闭支付弹窗
|
||||||
|
兜底手动操作
|
||||||
|
限速
|
||||||
|
|
||||||
|
- edge.area_id 可为空,代表节点无固定地区
|
||||||
- 后台展示 mac, ip:port,实际地区
|
- 后台展示 mac, ip:port,实际地区
|
||||||
|
|
||||||
上传文件平铺到 uploads,不分子文件夹
|
上传文件平铺到 uploads,不分子文件夹
|
||||||
|
|||||||
@@ -40,14 +40,6 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
|
|
||||||
gost:
|
|
||||||
image: gogost/gost
|
|
||||||
command: >
|
|
||||||
-api test:test@:9700
|
|
||||||
ports:
|
|
||||||
- "9700:9700"
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
redis_data:
|
redis_data:
|
||||||
|
|||||||
@@ -5010,6 +5010,42 @@ paths:
|
|||||||
default:
|
default:
|
||||||
$ref: "#/components/responses/PlainTextError"
|
$ref: "#/components/responses/PlainTextError"
|
||||||
|
|
||||||
|
/api/admin/proxy/sync/ports:
|
||||||
|
post:
|
||||||
|
tags: [admin/proxy]
|
||||||
|
summary: 重建端口池
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/IdRequest"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: 成功,无响应体
|
||||||
|
default:
|
||||||
|
$ref: "#/components/responses/PlainTextError"
|
||||||
|
|
||||||
|
/api/admin/proxy/sync/chains:
|
||||||
|
post:
|
||||||
|
tags: [admin/proxy]
|
||||||
|
summary: 重建代理链
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/IdRequest"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: 成功,无响应体
|
||||||
|
default:
|
||||||
|
$ref: "#/components/responses/PlainTextError"
|
||||||
|
|
||||||
/api/admin/proxy/remove:
|
/api/admin/proxy/remove:
|
||||||
post:
|
post:
|
||||||
tags: [admin/proxy]
|
tags: [admin/proxy]
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ 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 = '浙江')),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -614,8 +614,8 @@ create table proxy (
|
|||||||
updated_at timestamptz default current_timestamp,
|
updated_at timestamptz default current_timestamp,
|
||||||
deleted_at timestamptz
|
deleted_at timestamptz
|
||||||
);
|
);
|
||||||
create unique index udx_proxy_mac on proxy (mac) where deleted_at is null;
|
create index idx_proxy_mac on proxy (mac) where deleted_at is null;
|
||||||
create unique index udx_proxy_ip on proxy (ip) where deleted_at is null;
|
create index idx_proxy_ip on proxy (ip) where deleted_at is null;
|
||||||
create index idx_proxy_created_at on proxy (created_at) where deleted_at is null;
|
create index idx_proxy_created_at on proxy (created_at) where deleted_at is null;
|
||||||
|
|
||||||
-- proxy表字段注释
|
-- proxy表字段注释
|
||||||
@@ -669,7 +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,
|
||||||
area_id int not null,
|
area_id int,
|
||||||
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,
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ func ErrorHandler(c *fiber.Ctx, err error) error {
|
|||||||
|
|
||||||
case errors.As(err, &servErr):
|
case errors.As(err, &servErr):
|
||||||
code = fiber.StatusInternalServerError
|
code = fiber.StatusInternalServerError
|
||||||
message = err.Error()
|
slog.Warn("服务端错误", slog.String("error", servErr.Error()))
|
||||||
|
message = "服务端错误"
|
||||||
|
|
||||||
case errors.As(err, &timeErr):
|
case errors.As(err, &timeErr):
|
||||||
code = fiber.StatusBadRequest
|
code = fiber.StatusBadRequest
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
package events
|
package events
|
||||||
|
|
||||||
import "github.com/hibiken/asynq"
|
|
||||||
|
|
||||||
const RefreshEdge = "edge:refresh"
|
const RefreshEdge = "edge:refresh"
|
||||||
|
|
||||||
func NewRefreshEdge() *asynq.Task {
|
// func NewRefreshEdge() *asynq.Task {
|
||||||
return asynq.NewTask(RefreshEdge, nil)
|
// return asynq.NewTask(RefreshEdge, nil)
|
||||||
}
|
// }
|
||||||
|
|||||||
@@ -19,7 +19,11 @@ func IsGostNotFound(err error) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GostClient interface {
|
type GostClient interface {
|
||||||
|
ListChains() ([]*GostChainConfig, error)
|
||||||
GetChain(name string) (*GostChainConfig, error)
|
GetChain(name string) (*GostChainConfig, error)
|
||||||
|
CreateChain(chain *GostChainConfig) error
|
||||||
|
DeleteChain(name string) error
|
||||||
|
SaveConfig() error
|
||||||
CreateService(service *GostServiceConfig) error
|
CreateService(service *GostServiceConfig) error
|
||||||
DeleteService(name string) error
|
DeleteService(name string) error
|
||||||
CreateAuther(auther *GostAutherConfig) error
|
CreateAuther(auther *GostAutherConfig) error
|
||||||
@@ -38,7 +42,7 @@ type gostClient struct {
|
|||||||
var GostInitializer = func(host string, port int, pathPrefix, username, password string) GostClient {
|
var GostInitializer = func(host string, port int, pathPrefix, username, password string) GostClient {
|
||||||
baseURL := strings.TrimSpace(host)
|
baseURL := strings.TrimSpace(host)
|
||||||
if !strings.Contains(baseURL, "://") {
|
if !strings.Contains(baseURL, "://") {
|
||||||
baseURL = fmt.Sprintf("http://%s:%d", baseURL, port)
|
baseURL = fmt.Sprintf("https://%s", baseURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &gostClient{
|
return &gostClient{
|
||||||
@@ -54,15 +58,38 @@ func NewGost(host string, port int, pathPrefix, username, password string) GostC
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GostChainConfig struct {
|
type GostChainConfig struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
Hops []GostHopConfig `json:"hops,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GostHopConfig struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Nodes []GostNodeConfig `json:"nodes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GostNodeConfig struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Addr string `json:"addr"`
|
||||||
|
Connector GostConnectorConfig `json:"connector"`
|
||||||
|
Dialer GostDialerConfig `json:"dialer"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GostConnectorConfig struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GostDialerConfig struct {
|
||||||
|
Type string `json:"type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GostServiceConfig struct {
|
type GostServiceConfig struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Addr string `json:"addr"`
|
Addr string `json:"addr"`
|
||||||
Admission string `json:"admission,omitempty"`
|
Admission string `json:"admission,omitempty"`
|
||||||
Handler GostHandlerConfig `json:"handler"`
|
Handler GostHandlerConfig `json:"handler"`
|
||||||
Listener GostListenerConfig `json:"listener"`
|
Listener GostListenerConfig `json:"listener"`
|
||||||
|
Recorders []GostRecorderConfig `json:"recorders,omitempty"`
|
||||||
|
Limiter string `json:"limiter,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GostHandlerConfig struct {
|
type GostHandlerConfig struct {
|
||||||
@@ -75,6 +102,11 @@ type GostListenerConfig struct {
|
|||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GostRecorderConfig struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Record string `json:"record"`
|
||||||
|
}
|
||||||
|
|
||||||
type GostAutherConfig struct {
|
type GostAutherConfig struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Auths []GostAuthConfig `json:"auths"`
|
Auths []GostAuthConfig `json:"auths"`
|
||||||
@@ -115,6 +147,40 @@ func (c *gostClient) GetChain(name string) (*GostChainConfig, error) {
|
|||||||
return &GostChainConfig{Name: name}, nil
|
return &GostChainConfig{Name: name}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *gostClient) ListChains() ([]*GostChainConfig, error) {
|
||||||
|
body, err := c.get("/config/chains")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(body) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp struct {
|
||||||
|
Data struct {
|
||||||
|
Count int `json:"count"`
|
||||||
|
List []*GostChainConfig `json:"list"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &resp); err != nil {
|
||||||
|
return nil, fmt.Errorf("parse gost chain list failed %s: %w", string(body), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.Data.List, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *gostClient) CreateChain(chain *GostChainConfig) error {
|
||||||
|
return c.create("/config/chains", chain)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *gostClient) DeleteChain(name string) error {
|
||||||
|
return c.delete("/config/chains/" + url.PathEscape(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *gostClient) SaveConfig() error {
|
||||||
|
return c.create("/config", nil)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *gostClient) CreateService(service *GostServiceConfig) error {
|
func (c *gostClient) CreateService(service *GostServiceConfig) error {
|
||||||
return c.create("/config/services", service)
|
return c.create("/config/services", service)
|
||||||
}
|
}
|
||||||
|
|||||||
107
web/globals/gost_test.go
Normal file
107
web/globals/gost_test.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package globals
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGostClientChainOperations(t *testing.T) {
|
||||||
|
var (
|
||||||
|
created *GostChainConfig
|
||||||
|
deleted []string
|
||||||
|
saved bool
|
||||||
|
)
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
username, password, ok := r.BasicAuth()
|
||||||
|
if !ok || username != "user" || password != "pass" {
|
||||||
|
t.Errorf("unexpected auth: ok=%v username=%q password=%q", ok, username, password)
|
||||||
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case r.Method == http.MethodGet && r.URL.Path == "/api/config/chains":
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"count": 2,
|
||||||
|
"list": []map[string]any{
|
||||||
|
{"name": "old-a"},
|
||||||
|
{"name": "old-b"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
case r.Method == http.MethodPost && r.URL.Path == "/api/config/chains":
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&created); err != nil {
|
||||||
|
t.Errorf("Decode chain failed: %v", err)
|
||||||
|
http.Error(w, "bad request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = w.Write([]byte(`{}`))
|
||||||
|
case r.Method == http.MethodDelete && r.URL.Path == "/api/config/chains/old-a":
|
||||||
|
deleted = append(deleted, "old-a")
|
||||||
|
_, _ = w.Write([]byte(`{}`))
|
||||||
|
case r.Method == http.MethodDelete && r.URL.Path == "/api/config/chains/old-b":
|
||||||
|
deleted = append(deleted, "old-b")
|
||||||
|
_, _ = w.Write([]byte(`{}`))
|
||||||
|
case r.Method == http.MethodPost && r.URL.Path == "/api/config":
|
||||||
|
saved = true
|
||||||
|
_, _ = w.Write([]byte(`{}`))
|
||||||
|
default:
|
||||||
|
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := NewGost(server.URL, 9700, "/api", "user", "pass")
|
||||||
|
|
||||||
|
chains, err := client.ListChains()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListChains returned error: %v", err)
|
||||||
|
}
|
||||||
|
if len(chains) != 2 || chains[0].Name != "old-a" || chains[1].Name != "old-b" {
|
||||||
|
t.Fatalf("unexpected chains: %#v", chains)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.DeleteChain(chains[0].Name); err != nil {
|
||||||
|
t.Fatalf("DeleteChain old-a returned error: %v", err)
|
||||||
|
}
|
||||||
|
if err := client.DeleteChain(chains[1].Name); err != nil {
|
||||||
|
t.Fatalf("DeleteChain old-b returned error: %v", err)
|
||||||
|
}
|
||||||
|
if len(deleted) != 2 {
|
||||||
|
t.Fatalf("unexpected deleted chains: %#v", deleted)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = client.CreateChain(&GostChainConfig{
|
||||||
|
Name: "edge-a",
|
||||||
|
Hops: []GostHopConfig{{
|
||||||
|
Nodes: []GostNodeConfig{{
|
||||||
|
Addr: "192.0.2.1:1080",
|
||||||
|
Connector: GostConnectorConfig{Type: "socks5"},
|
||||||
|
Dialer: GostDialerConfig{Type: "tcp"},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateChain returned error: %v", err)
|
||||||
|
}
|
||||||
|
if created == nil || created.Name != "edge-a" {
|
||||||
|
t.Fatalf("unexpected created chain: %#v", created)
|
||||||
|
}
|
||||||
|
if len(created.Hops) != 1 || len(created.Hops[0].Nodes) != 1 {
|
||||||
|
t.Fatalf("unexpected created chain hops: %#v", created.Hops)
|
||||||
|
}
|
||||||
|
node := created.Hops[0].Nodes[0]
|
||||||
|
if node.Addr != "192.0.2.1:1080" || node.Connector.Type != "socks5" || node.Dialer.Type != "tcp" {
|
||||||
|
t.Fatalf("unexpected created node: %#v", node)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.SaveConfig(); err != nil {
|
||||||
|
t.Fatalf("SaveConfig returned error: %v", err)
|
||||||
|
}
|
||||||
|
if !saved {
|
||||||
|
t.Fatal("expected SaveConfig request")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -230,23 +230,6 @@ type CreateChannelReqV3 struct {
|
|||||||
Isp *int `json:"isp"`
|
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"`
|
||||||
@@ -474,3 +457,20 @@ type SyncChannelClearExpiredByAdminReq struct {
|
|||||||
type SyncChannelClearExpiredByAdminResp struct {
|
type SyncChannelClearExpiredByAdminResp struct {
|
||||||
Count int `json:"count"`
|
Count int `json:"count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ func UpdateProxyStatus(c *fiber.Ctx) error {
|
|||||||
return c.JSON(nil)
|
return c.JSON(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SyncProxyPool(c *fiber.Ctx) error {
|
func SyncProxyPorts(c *fiber.Ctx) error {
|
||||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyWrite)
|
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyWrite)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -116,7 +116,25 @@ func SyncProxyPool(c *fiber.Ctx) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Proxy.SyncPool(req.Id); err != nil {
|
if err := s.Proxy.SyncPorts(req.Id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SyncProxyChains(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.SyncChains(req.Id); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -422,7 +422,7 @@ func PageResourceShortOfUserByAdmin(c *fiber.Ctx) error {
|
|||||||
do = do.Where(q.Resource.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
|
do = do.Where(q.Resource.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
|
||||||
}
|
}
|
||||||
|
|
||||||
list, total, err := q.Resource.Debug().
|
list, total, err := q.Resource.
|
||||||
Joins(q.Resource.User, q.Resource.Short, q.Resource.Short.Sku).
|
Joins(q.Resource.User, q.Resource.Short, q.Resource.Short.Sku).
|
||||||
Select(
|
Select(
|
||||||
q.Resource.ALL,
|
q.Resource.ALL,
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"platform/pkg/env"
|
|
||||||
"platform/web/auth"
|
"platform/web/auth"
|
||||||
"platform/web/core"
|
"platform/web/core"
|
||||||
g "platform/web/globals"
|
g "platform/web/globals"
|
||||||
@@ -14,7 +11,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/valyala/fasthttp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// PageTradeByAdmin 分页查询所有订单
|
// PageTradeByAdmin 分页查询所有订单
|
||||||
@@ -221,6 +217,36 @@ type TradeCreateReq struct {
|
|||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// 更新订单备注
|
||||||
|
func TradeUpdateRemarkByAdmin(c *fiber.Ctx) error {
|
||||||
|
// 检查权限
|
||||||
|
_, err := auth.GetAuthCtx(c).PermitAdmin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析请求参数
|
||||||
|
var req TradeUpdateRemarkReq
|
||||||
|
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新订单备注
|
||||||
|
err = s.Trade.UpdateRemark(req.TradeNo, req.Remark)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.SendStatus(fiber.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
type TradeUpdateRemarkReq struct {
|
||||||
|
TradeNo string `json:"trade_no" validate:"required"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
// 完成订单
|
// 完成订单
|
||||||
func TradeComplete(c *fiber.Ctx) error {
|
func TradeComplete(c *fiber.Ctx) error {
|
||||||
// 检查权限
|
// 检查权限
|
||||||
@@ -276,6 +302,29 @@ func TradeCompleteByAdmin(c *fiber.Ctx) error {
|
|||||||
return c.SendStatus(fiber.StatusNoContent)
|
return c.SendStatus(fiber.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 订单补余额
|
||||||
|
func TradeConvertByAdmin(c *fiber.Ctx) error {
|
||||||
|
// 检查权限
|
||||||
|
authCtx, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeTradeWrite)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析请求参数
|
||||||
|
var req s.TradeRef
|
||||||
|
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订单补余额
|
||||||
|
err = s.Trade.ConvertTradeToBalance(authCtx.Admin, &req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.SendStatus(fiber.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
// 取消订单
|
// 取消订单
|
||||||
@@ -287,14 +336,14 @@ func TradeCancel(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 解析请求参数
|
// 解析请求参数
|
||||||
req := new(TradeCancelReq)
|
req := new(s.TradeRef)
|
||||||
if err := g.Validator.ParseBody(c, req); err != nil {
|
if err := g.Validator.ParseBody(c, req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 取消交易
|
// 取消交易
|
||||||
err = s.Trade.CancelTrade(&req.TradeRef)
|
err = s.Trade.CancelTrade(req)
|
||||||
if err != nil {
|
if err != nil && err != s.ErrTradeStatusIgnored {
|
||||||
slog.Error("取消交易失败", "trade_no", req.TradeNo, "error", err)
|
slog.Error("取消交易失败", "trade_no", req.TradeNo, "error", err)
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "取消交易失败"})
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "取消交易失败"})
|
||||||
}
|
}
|
||||||
@@ -302,62 +351,63 @@ func TradeCancel(c *fiber.Ctx) error {
|
|||||||
return c.SendStatus(fiber.StatusNoContent)
|
return c.SendStatus(fiber.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
type TradeCancelReq struct {
|
// ============================================================
|
||||||
s.TradeRef
|
|
||||||
|
// 结束订单:完成或取消订单
|
||||||
|
func TradeFinish(c *fiber.Ctx) error {
|
||||||
|
// 检查权限
|
||||||
|
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析请求参数
|
||||||
|
var req s.TradeRef
|
||||||
|
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试取消交易
|
||||||
|
err = s.Trade.CancelTrade(&req)
|
||||||
|
if err == s.ErrTradeStatusIgnored {
|
||||||
|
// 尝试完成交易
|
||||||
|
err = s.Trade.CompleteTrade(authCtx.User, &req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(map[string]m.TradeStatus{
|
||||||
|
"status": m.TradeStatusSuccess,
|
||||||
|
})
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(map[string]m.TradeStatus{
|
||||||
|
"status": m.TradeStatusCanceled,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
// 检查订单
|
// 检查订单
|
||||||
func TradeCheck(c *fiber.Ctx) error {
|
func TradeCheck(c *fiber.Ctx) error {
|
||||||
// 检查权限:sse 接口暂时不检查权限
|
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeTradeRead)
|
||||||
|
if err != nil {
|
||||||
// 解析请求参数
|
|
||||||
req := new(TradeCheckReq)
|
|
||||||
if err := g.Validator.ParseQuery(c, req); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Set(fiber.HeaderContentType, "text/event-stream")
|
// 解析请求参数
|
||||||
c.Set(fiber.HeaderCacheControl, "no-cache")
|
var req s.TradeRef
|
||||||
c.Set(fiber.HeaderConnection, "keep-alive")
|
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||||
c.Set(fiber.HeaderTransferEncoding, "chunked")
|
return err
|
||||||
c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) {
|
}
|
||||||
|
|
||||||
expire := env.TradeExpire
|
// 检查订单状态
|
||||||
interval := 5
|
result, err := s.Trade.CheckTrade(&req)
|
||||||
for range expire / interval {
|
if err != nil {
|
||||||
// 检查订单状态
|
return err
|
||||||
result, err := s.Trade.CheckTrade(&req.TradeRef)
|
}
|
||||||
if err != nil {
|
|
||||||
slog.Error("检查订单状态失败", "trade_no", req.TradeNo, "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 写入订单状态
|
return c.JSON(result)
|
||||||
_, err = fmt.Fprintf(w, "data: %d\n\n", result.Status)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("写入订单状态失败", "trade_no", req.TradeNo, "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = w.Flush()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 当订单离开支付状态后结束查询
|
|
||||||
if result.Status != m.TradeStatusPending {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Duration(interval) * time.Second)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type TradeCheckReq struct {
|
|
||||||
s.TradeRef
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ 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-移动
|
||||||
AreaID int32 `json:"area_id" gorm:"column:area_id"` // 城市地区ID
|
AreaID *int32 `json:"area_id,omitempty" gorm:"column:area_id"` // 城市地区ID
|
||||||
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"` // 最近丢包率
|
||||||
@@ -41,6 +41,7 @@ const (
|
|||||||
type EdgeISP int
|
type EdgeISP int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
EdgeISPUnknown EdgeISP = 0 // 未知/任意
|
||||||
EdgeISPTelecom EdgeISP = 1 // 电信
|
EdgeISPTelecom EdgeISP = 1 // 电信
|
||||||
EdgeISPUnicom EdgeISP = 2 // 联通
|
EdgeISPUnicom EdgeISP = 2 // 联通
|
||||||
EdgeISPMobile EdgeISP = 3 // 移动
|
EdgeISPMobile EdgeISP = 3 // 移动
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
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"
|
||||||
@@ -51,46 +49,6 @@ 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())
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,10 +66,6 @@ func publicRouter(api fiber.Router) {
|
|||||||
resource := api.Group("/resource")
|
resource := api.Group("/resource")
|
||||||
resource.Post("/price", handlers.ResourcePrice)
|
resource.Post("/price", handlers.ResourcePrice)
|
||||||
|
|
||||||
// 交易
|
|
||||||
trade := api.Group("/trade")
|
|
||||||
trade.Get("/check", handlers.TradeCheck)
|
|
||||||
|
|
||||||
// 前台
|
// 前台
|
||||||
inquiry := api.Group("/inquiry")
|
inquiry := api.Group("/inquiry")
|
||||||
inquiry.Post("/create", handlers.CreateInquiry)
|
inquiry.Post("/create", handlers.CreateInquiry)
|
||||||
@@ -129,9 +83,6 @@ func clientRouter(api fiber.Router) {
|
|||||||
client.Post("/verify/sms", handlers.SendSmsCode)
|
client.Post("/verify/sms", handlers.SendSmsCode)
|
||||||
|
|
||||||
// 网关
|
// 网关
|
||||||
proxy := client.Group("/proxy")
|
|
||||||
proxy.Post("/sync-pool", handlers.SyncProxyPool)
|
|
||||||
|
|
||||||
// 通道管理
|
// 通道管理
|
||||||
channel := client.Group("/channel")
|
channel := client.Group("/channel")
|
||||||
channel.Post("/remove", handlers.RemoveChannels)
|
channel.Post("/remove", handlers.RemoveChannels)
|
||||||
@@ -189,6 +140,7 @@ func userRouter(api fiber.Router) {
|
|||||||
trade.Post("/create", handlers.TradeCreate)
|
trade.Post("/create", handlers.TradeCreate)
|
||||||
trade.Post("/complete", handlers.TradeComplete)
|
trade.Post("/complete", handlers.TradeComplete)
|
||||||
trade.Post("/cancel", handlers.TradeCancel)
|
trade.Post("/cancel", handlers.TradeCancel)
|
||||||
|
trade.Post("/finish", handlers.TradeFinish)
|
||||||
|
|
||||||
// 账单
|
// 账单
|
||||||
bill := api.Group("/bill")
|
bill := api.Group("/bill")
|
||||||
@@ -277,6 +229,8 @@ func adminRouter(api fiber.Router) {
|
|||||||
proxy.Post("/create", handlers.CreateProxy)
|
proxy.Post("/create", handlers.CreateProxy)
|
||||||
proxy.Post("/update", handlers.UpdateProxy)
|
proxy.Post("/update", handlers.UpdateProxy)
|
||||||
proxy.Post("/update/status", handlers.UpdateProxyStatus)
|
proxy.Post("/update/status", handlers.UpdateProxyStatus)
|
||||||
|
proxy.Post("/sync/ports", handlers.SyncProxyPorts)
|
||||||
|
proxy.Post("/sync/chains", handlers.SyncProxyChains)
|
||||||
proxy.Post("/remove", handlers.RemoveProxy)
|
proxy.Post("/remove", handlers.RemoveProxy)
|
||||||
|
|
||||||
// trade 交易
|
// trade 交易
|
||||||
@@ -284,6 +238,8 @@ func adminRouter(api fiber.Router) {
|
|||||||
trade.Post("/page", handlers.PageTradeByAdmin)
|
trade.Post("/page", handlers.PageTradeByAdmin)
|
||||||
trade.Post("/page/of-user", handlers.PageTradeOfUserByAdmin)
|
trade.Post("/page/of-user", handlers.PageTradeOfUserByAdmin)
|
||||||
trade.Post("/complete", handlers.TradeCompleteByAdmin)
|
trade.Post("/complete", handlers.TradeCompleteByAdmin)
|
||||||
|
trade.Post("/update/remark", handlers.TradeUpdateRemarkByAdmin)
|
||||||
|
trade.Post("/check", handlers.TradeCheck)
|
||||||
|
|
||||||
// bill 账单
|
// bill 账单
|
||||||
var bill = api.Group("/bill")
|
var bill = api.Group("/bill")
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ 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) {
|
||||||
|
if edgeFilter == nil {
|
||||||
|
edgeFilter = &EdgeFilter{}
|
||||||
|
}
|
||||||
|
|
||||||
var area *m.Area
|
var area *m.Area
|
||||||
if edgeFilter.AreaID != nil {
|
if edgeFilter.AreaID != nil {
|
||||||
var err error
|
var err error
|
||||||
@@ -132,7 +136,12 @@ func (s *channelServer) RemoveChannels(batch string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.provider.removeRemote(batch, usedBatch); err != nil {
|
provider, err := channelProviderByProxyID(usedBatch.ProxyID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := provider.removeRemote(batch, usedBatch); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := freeChans(usedBatch.ProxyID, batch); err != nil {
|
if err := freeChans(usedBatch.ProxyID, batch); err != nil {
|
||||||
@@ -265,8 +274,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: prov,
|
Prov: u.Ternary(ctx.Filter.AreaID != nil, prov, nil),
|
||||||
City: city,
|
City: u.Ternary(ctx.Filter.AreaID != nil, city, nil),
|
||||||
IP: orm.Inet{Addr: ctx.Source},
|
IP: orm.Inet{Addr: ctx.Source},
|
||||||
Time: ctx.Now,
|
Time: ctx.Now,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
@@ -397,12 +406,28 @@ func selectProxyByType(proxyType m.ProxyType, count int) (*m.Proxy, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if maxCount < count {
|
if maxCount < count {
|
||||||
return nil, core.NewBizErr("无可用代理")
|
return nil, core.NewBizErr("无空闲代理")
|
||||||
}
|
}
|
||||||
|
|
||||||
return bestProxy, nil
|
return bestProxy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func channelProviderByProxyID(proxyID int32) (channelProvider, error) {
|
||||||
|
proxy, err := q.Proxy.Where(q.Proxy.ID.Eq(proxyID)).Take()
|
||||||
|
if err != nil {
|
||||||
|
return nil, core.NewServErr("获取代理数据失败", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch proxy.Type {
|
||||||
|
case m.ProxyTypeGost:
|
||||||
|
return &channelGostProvider{}, nil
|
||||||
|
case m.ProxyTypeBaiYin:
|
||||||
|
return &channelBaiyinProvider{}, nil
|
||||||
|
default:
|
||||||
|
return nil, core.NewBizErr(fmt.Sprintf("不支持的代理类型: %d", proxy.Type))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *channelServer) RefreshEdges() error {
|
func (s *channelServer) RefreshEdges() error {
|
||||||
|
|
||||||
// 仅白银网关支持边缘节点刷新,GOST 不参与此流程。
|
// 仅白银网关支持边缘节点刷新,GOST 不参与此流程。
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"platform/web/core"
|
"platform/web/core"
|
||||||
g "platform/web/globals"
|
g "platform/web/globals"
|
||||||
m "platform/web/models"
|
m "platform/web/models"
|
||||||
q "platform/web/queries"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type channelBaiyinProvider struct{}
|
type channelBaiyinProvider struct{}
|
||||||
@@ -68,30 +67,31 @@ func (s *channelBaiyinProvider) prepareCreate(ctx *channelCreateContext) (*chann
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *channelBaiyinProvider) removeRemote(_ string, batch *usedChanBatch) error {
|
func (s *channelBaiyinProvider) removeRemote(_ string, batch *usedChanBatch) error {
|
||||||
configs := make([]*g.PortConfigsReq, len(batch.Chans))
|
|
||||||
for i, ch := range batch.Chans {
|
|
||||||
configs[i] = &g.PortConfigsReq{
|
|
||||||
Port: int(ch.Port()),
|
|
||||||
Edge: &[]string{},
|
|
||||||
AutoEdgeConfig: &g.AutoEdgeConfig{Count: u.P(0)},
|
|
||||||
Status: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
proxy, err := q.Proxy.Where(q.Proxy.ID.Eq(batch.ProxyID)).Take()
|
|
||||||
if err != nil {
|
|
||||||
return core.NewServErr("获取代理数据失败", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
gateway, err := proxyGateway(proxy)
|
|
||||||
if err != nil {
|
|
||||||
return core.NewServErr("创建代理网关失败", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = gateway.GatewayPortConfigs(configs); err != nil {
|
|
||||||
return core.NewServErr(fmt.Sprintf("清空代理 %s 端口配置失败", proxy.IP.String()), err)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
|
// configs := make([]*g.PortConfigsReq, len(batch.Chans))
|
||||||
|
// for i, ch := range batch.Chans {
|
||||||
|
// configs[i] = &g.PortConfigsReq{
|
||||||
|
// Port: int(ch.Port()),
|
||||||
|
// Edge: &[]string{},
|
||||||
|
// AutoEdgeConfig: &g.AutoEdgeConfig{Count: u.P(0)},
|
||||||
|
// Status: false,
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// proxy, err := q.Proxy.Where(q.Proxy.ID.Eq(batch.ProxyID)).Take()
|
||||||
|
// if err != nil {
|
||||||
|
// return core.NewServErr("获取代理数据失败", err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// gateway, err := proxyGateway(proxy)
|
||||||
|
// if err != nil {
|
||||||
|
// return core.NewServErr("创建代理网关失败", err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if err = gateway.GatewayPortConfigs(configs); err != nil {
|
||||||
|
// return core.NewServErr(fmt.Sprintf("清空代理 %s 端口配置失败", proxy.IP.String()), err)
|
||||||
|
// }
|
||||||
|
// return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensureEdges 检查本地节点是否足够,如果不足从云端连入
|
// ensureEdges 检查本地节点是否足够,如果不足从云端连入
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
q "platform/web/queries"
|
q "platform/web/queries"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gorm.io/gen"
|
"gorm.io/gen/field"
|
||||||
)
|
)
|
||||||
|
|
||||||
type channelGostProvider struct{}
|
type channelGostProvider struct{}
|
||||||
@@ -37,7 +37,7 @@ func (s *channelGostProvider) prepareCreate(ctx *channelCreateContext) (*channel
|
|||||||
serviceName := gostServiceName(ctx.BatchNo, port)
|
serviceName := gostServiceName(ctx.BatchNo, port)
|
||||||
channel := newBaseChannel(ctx, port)
|
channel := newBaseChannel(ctx, port)
|
||||||
channel.EdgeID = u.P(edge.ID)
|
channel.EdgeID = u.P(edge.ID)
|
||||||
channel.EdgeRef = u.P(serviceName)
|
channel.EdgeRef = u.P(edge.Mac)
|
||||||
channel.IP = u.P(edge.IP)
|
channel.IP = u.P(edge.IP)
|
||||||
|
|
||||||
service := &g.GostServiceConfig{
|
service := &g.GostServiceConfig{
|
||||||
@@ -50,6 +50,10 @@ func (s *channelGostProvider) prepareCreate(ctx *channelCreateContext) (*channel
|
|||||||
Listener: g.GostListenerConfig{
|
Listener: g.GostListenerConfig{
|
||||||
Type: "tcp",
|
Type: "tcp",
|
||||||
},
|
},
|
||||||
|
Recorders: []g.GostRecorderConfig{
|
||||||
|
{Name: "record-http-otel", Record: "recorder.service.handler"},
|
||||||
|
},
|
||||||
|
Limiter: "limiter-8m",
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.AuthWhitelist {
|
if ctx.AuthWhitelist {
|
||||||
@@ -138,40 +142,34 @@ func (s *channelGostProvider) selectEdge(filter *EdgeFilter, area *m.Area, count
|
|||||||
filter = &EdgeFilter{}
|
filter = &EdgeFilter{}
|
||||||
}
|
}
|
||||||
|
|
||||||
conds := []gen.Condition{
|
do := q.Edge.Where(
|
||||||
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 filter.Isp != nil {
|
||||||
|
do = do.Where(q.Edge.ISP.In(int(m.EdgeISPUnknown), int(*filter.Isp)))
|
||||||
}
|
}
|
||||||
if isp := u.X(filter.Isp.String()); isp != nil {
|
|
||||||
conds = append(conds, q.Edge.ISP.Eq(int(*filter.Isp)))
|
|
||||||
}
|
|
||||||
|
|
||||||
query := q.Edge.Where(conds...)
|
|
||||||
if area != nil {
|
if area != nil {
|
||||||
switch area.Level {
|
switch area.Level {
|
||||||
case m.AreaLevelProvince:
|
case m.AreaLevelProvince:
|
||||||
edgeArea := q.Area.As("EdgeArea")
|
edgeArea := q.Area.As("EdgeArea")
|
||||||
query = query.
|
do = do.
|
||||||
Join(edgeArea, edgeArea.ID.EqCol(q.Edge.AreaID)).
|
Where(edgeArea.ParentID.Eq(area.ID)).
|
||||||
Where(edgeArea.ParentID.Eq(area.ID))
|
Join(edgeArea, edgeArea.ID.EqCol(q.Edge.AreaID))
|
||||||
case m.AreaLevelCity:
|
case m.AreaLevelCity:
|
||||||
query = query.Where(q.Edge.AreaID.Eq(area.ID))
|
do = do.Where(q.Edge.AreaID.Eq(area.ID))
|
||||||
default:
|
default:
|
||||||
return nil, core.NewBizErr("地区层级不支持")
|
return nil, core.NewBizErr("地区层级不支持")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
edges, err := query.
|
edges, err := do.
|
||||||
Order(q.Edge.ID).
|
Order(field.NewUnsafeFieldRaw("random()")).
|
||||||
Limit(count).
|
Limit(count).
|
||||||
Find()
|
Find()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, core.NewBizErr("查询可用节点失败", err)
|
return nil, core.NewBizErr("查询可用节点失败", err)
|
||||||
}
|
}
|
||||||
return expandGostEdges(edges, count)
|
|
||||||
}
|
|
||||||
|
|
||||||
func expandGostEdges(edges []*m.Edge, count int) ([]*m.Edge, error) {
|
|
||||||
if len(edges) == 0 {
|
if len(edges) == 0 {
|
||||||
return nil, core.NewBizErr("地区可用节点数量不足")
|
return nil, core.NewBizErr("地区可用节点数量不足")
|
||||||
}
|
}
|
||||||
@@ -180,6 +178,7 @@ func expandGostEdges(edges []*m.Edge, count int) ([]*m.Edge, error) {
|
|||||||
for i := range count {
|
for i := range count {
|
||||||
result[i] = edges[i%len(edges)]
|
result[i] = edges[i%len(edges)]
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
q "platform/web/queries"
|
q "platform/web/queries"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
"gorm.io/gen/field"
|
"gorm.io/gen/field"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ func (s *productService) AllProducts() ([]*m.Product, error) {
|
|||||||
Find()
|
Find()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *productService) AllProductSaleInfos() ([]*m.Product, error) {
|
func (s *productService) AllProductSaleInfos() ([]any, error) {
|
||||||
products, err := q.Product.
|
products, err := q.Product.
|
||||||
Select(
|
Select(
|
||||||
q.Product.ID,
|
q.Product.ID,
|
||||||
@@ -43,7 +44,17 @@ func (s *productService) AllProductSaleInfos() ([]*m.Product, error) {
|
|||||||
pids[i] = p.ID
|
pids[i] = p.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
skus, err := q.ProductSku.
|
type SkuInfo struct {
|
||||||
|
ID int32 `json:"id"`
|
||||||
|
ProductID int32 `json:"product_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
Price decimal.Decimal `json:"price"`
|
||||||
|
CountMin decimal.Decimal `json:"count_min"`
|
||||||
|
Discount float64 `json:"discount"`
|
||||||
|
}
|
||||||
|
var skus []*SkuInfo
|
||||||
|
err = q.ProductSku.
|
||||||
Select(
|
Select(
|
||||||
q.ProductSku.ID,
|
q.ProductSku.ID,
|
||||||
q.ProductSku.ProductID,
|
q.ProductSku.ProductID,
|
||||||
@@ -51,29 +62,47 @@ func (s *productService) AllProductSaleInfos() ([]*m.Product, error) {
|
|||||||
q.ProductSku.Code,
|
q.ProductSku.Code,
|
||||||
q.ProductSku.Price,
|
q.ProductSku.Price,
|
||||||
q.ProductSku.CountMin,
|
q.ProductSku.CountMin,
|
||||||
|
q.ProductDiscount.Discount,
|
||||||
).
|
).
|
||||||
Where(
|
Where(
|
||||||
q.ProductSku.ProductID.In(pids...),
|
q.ProductSku.ProductID.In(pids...),
|
||||||
q.ProductSku.Status.Eq(int32(m.SkuStatusEnabled)),
|
q.ProductSku.Status.Eq(int32(m.SkuStatusEnabled)),
|
||||||
).
|
).
|
||||||
|
LeftJoin(q.ProductDiscount, q.ProductDiscount.ID.EqCol(q.ProductSku.DiscountId)).
|
||||||
Order(q.ProductSku.Sort).
|
Order(q.ProductSku.Sort).
|
||||||
Find()
|
Scan(&skus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
pmap := make(map[int32]*m.Product, len(products))
|
type ProductInfo struct {
|
||||||
|
m.Product
|
||||||
|
Skus []*SkuInfo `json:"skus,omitempty"`
|
||||||
|
}
|
||||||
|
pmap := make(map[int32]*ProductInfo, len(products))
|
||||||
for _, p := range products {
|
for _, p := range products {
|
||||||
pmap[p.ID] = p
|
pmap[p.ID] = &ProductInfo{Product: *p, Skus: make([]*SkuInfo, 0)}
|
||||||
p.Skus = make([]*m.ProductSku, 0)
|
|
||||||
}
|
}
|
||||||
for _, s := range skus {
|
for _, s := range skus {
|
||||||
if p, ok := pmap[s.ProductID]; ok {
|
if p, ok := pmap[s.ProductID]; ok {
|
||||||
p.Skus = append(p.Skus, s)
|
p.Skus = append(p.Skus, &SkuInfo{
|
||||||
|
ID: s.ID,
|
||||||
|
ProductID: s.ProductID,
|
||||||
|
Name: s.Name,
|
||||||
|
Code: s.Code,
|
||||||
|
Price: s.Price,
|
||||||
|
CountMin: s.CountMin,
|
||||||
|
Discount: s.Discount,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return products, nil
|
plist := make([]any, 0, len(pmap))
|
||||||
|
for _, p := range pmap {
|
||||||
|
plist = append(plist, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
return plist, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增产品
|
// 新增产品
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"platform/pkg/u"
|
"platform/pkg/u"
|
||||||
@@ -14,6 +15,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gorm.io/gen/field"
|
"gorm.io/gen/field"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Proxy = &proxyService{}
|
var Proxy = &proxyService{}
|
||||||
@@ -23,11 +25,20 @@ type proxyService struct{}
|
|||||||
func hasUsedChans(proxyID int32) (bool, error) {
|
func hasUsedChans(proxyID int32) (bool, error) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
pattern := usedChansKey(proxyID, "*")
|
pattern := usedChansKey(proxyID, "*")
|
||||||
keys, _, err := g.Redis.Scan(ctx, 0, pattern, 1).Result()
|
var cursor uint64
|
||||||
if err != nil {
|
for {
|
||||||
return false, err
|
keys, next, err := g.Redis.Scan(ctx, cursor, pattern, 100).Result()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if len(keys) > 0 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
if next == 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
cursor = next
|
||||||
}
|
}
|
||||||
return len(keys) > 0, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func rebuildFreeChans(proxyID int32, addr netip.Addr) error {
|
func rebuildFreeChans(proxyID int32, addr netip.Addr) error {
|
||||||
@@ -161,17 +172,117 @@ func (s *proxyService) Update(update *UpdateProxy) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *proxyService) SyncPool(id int32) error {
|
func (s *proxyService) SyncPorts(id int32) error {
|
||||||
proxy, err := q.Proxy.Where(q.Proxy.ID.Eq(id)).Select(q.Proxy.ID, q.Proxy.IP).First()
|
proxy, err := findOfflineProxy(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return core.NewServErr("获取代理数据失败", err)
|
return err
|
||||||
}
|
}
|
||||||
if proxy == nil {
|
|
||||||
return core.NewBizErr("代理不存在")
|
used, err := hasUsedChans(id)
|
||||||
|
if err != nil {
|
||||||
|
return core.NewServErr("检查代理通道状态失败", err)
|
||||||
}
|
}
|
||||||
|
if used {
|
||||||
|
return core.NewBizErr("代理存在未关闭通道,禁止重建端口池")
|
||||||
|
}
|
||||||
|
|
||||||
return rebuildFreeChans(id, proxy.IP.Addr)
|
return rebuildFreeChans(id, proxy.IP.Addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *proxyService) SyncChains(id int32) error {
|
||||||
|
proxy, err := findOfflineProxy(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if proxy.Type != m.ProxyTypeGost {
|
||||||
|
return core.NewBizErr("仅 GOST 代理支持重建代理链")
|
||||||
|
}
|
||||||
|
|
||||||
|
chains, err := buildGostChainsFromEdges()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := proxyGost(proxy)
|
||||||
|
if err != nil {
|
||||||
|
return core.NewServErr("创建 GOST 客户端失败", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
oldChains, err := client.ListChains()
|
||||||
|
if err != nil {
|
||||||
|
return core.NewServErr("查询 GOST chains 失败", err)
|
||||||
|
}
|
||||||
|
for _, chain := range oldChains {
|
||||||
|
if err := client.DeleteChain(chain.Name); err != nil {
|
||||||
|
return core.NewServErr(fmt.Sprintf("删除 GOST chain 失败: %s", chain.Name), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, chain := range chains {
|
||||||
|
if err := client.CreateChain(chain); err != nil {
|
||||||
|
return core.NewServErr(fmt.Sprintf("创建 GOST chain 失败: %s", chain.Name), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.SaveConfig(); err != nil {
|
||||||
|
return core.NewServErr("保存 GOST 配置失败", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findOfflineProxy(id int32) (*m.Proxy, error) {
|
||||||
|
proxy, err := q.Proxy.Where(q.Proxy.ID.Eq(id)).Take()
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, core.NewBizErr("代理不存在")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, core.NewServErr("获取代理数据失败", err)
|
||||||
|
}
|
||||||
|
if proxy.Status != m.ProxyStatusOffline {
|
||||||
|
return nil, core.NewBizErr("代理未下线,禁止同步")
|
||||||
|
}
|
||||||
|
return proxy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildGostChainsFromEdges() ([]*g.GostChainConfig, error) {
|
||||||
|
edges, err := q.Edge.
|
||||||
|
Where(q.Edge.Type.Eq(int(m.EdgeTypeGostChain))).
|
||||||
|
Order(q.Edge.ID).
|
||||||
|
Find()
|
||||||
|
if err != nil {
|
||||||
|
return nil, core.NewServErr("查询 GOST edge 数据失败", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
chains := make([]*g.GostChainConfig, len(edges))
|
||||||
|
for i, edge := range edges {
|
||||||
|
if strings.TrimSpace(edge.Mac) == "" {
|
||||||
|
return nil, core.NewBizErr(fmt.Sprintf("GOST edge %d chain 名称为空", edge.ID))
|
||||||
|
}
|
||||||
|
if !edge.IP.Addr.IsValid() {
|
||||||
|
return nil, core.NewBizErr(fmt.Sprintf("GOST edge %s IP 无效", edge.Mac))
|
||||||
|
}
|
||||||
|
if edge.Port == nil || *edge.Port == 0 {
|
||||||
|
return nil, core.NewBizErr(fmt.Sprintf("GOST edge %s 端口为空", edge.Mac))
|
||||||
|
}
|
||||||
|
|
||||||
|
chains[i] = &g.GostChainConfig{
|
||||||
|
Name: edge.Mac,
|
||||||
|
Hops: []g.GostHopConfig{{
|
||||||
|
Nodes: []g.GostNodeConfig{{
|
||||||
|
Addr: netip.AddrPortFrom(edge.IP.Addr, *edge.Port).String(),
|
||||||
|
Connector: g.GostConnectorConfig{
|
||||||
|
Type: "socks5",
|
||||||
|
},
|
||||||
|
Dialer: g.GostDialerConfig{
|
||||||
|
Type: "tcp",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chains, nil
|
||||||
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
|||||||
@@ -204,12 +204,7 @@ func (s *tradeService) Create(user *m.User, tradeData *CreateTradeData, productD
|
|||||||
// 缓存产品数据
|
// 缓存产品数据
|
||||||
w := bytes.Buffer{}
|
w := bytes.Buffer{}
|
||||||
gob.NewEncoder(&w).Encode(detail)
|
gob.NewEncoder(&w).Encode(detail)
|
||||||
err = g.Redis.Set(
|
err = g.Redis.Set(context.Background(), tradeProductKey(tradeNo), w.Bytes(), 0).Err()
|
||||||
context.Background(),
|
|
||||||
tradeProductKey(tradeNo),
|
|
||||||
w.Bytes(),
|
|
||||||
expireIn,
|
|
||||||
).Err()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, core.NewServErr("保存购买信息失败", err)
|
return nil, core.NewServErr("保存购买信息失败", err)
|
||||||
}
|
}
|
||||||
@@ -271,7 +266,8 @@ func (s *tradeService) OnCompleteTrade(user *m.User, interNo string, outerNo str
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 恢复购买信息;如果反序列化失败,检查开头 init 函数中是否注册了对应的 struct 类型
|
// 恢复购买信息;如果反序列化失败,检查开头 init 函数中是否注册了对应的 struct 类型
|
||||||
detailBytes, err := g.Redis.Get(context.Background(), tradeProductKey(interNo)).Bytes()
|
tradeKey := tradeProductKey(interNo)
|
||||||
|
detailBytes, err := g.Redis.Get(context.Background(), tradeKey).Bytes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return core.NewServErr("恢复购买信息失败", err)
|
return core.NewServErr("恢复购买信息失败", err)
|
||||||
}
|
}
|
||||||
@@ -344,6 +340,12 @@ func (s *tradeService) OnCompleteTrade(user *m.User, interNo string, outerNo str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 删除缓存
|
||||||
|
err = g.Redis.Del(context.Background(), tradeKey).Err()
|
||||||
|
if err != nil {
|
||||||
|
return core.NewServErr("删除缓存失败", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -353,6 +355,23 @@ func (s *tradeService) OnCompleteTrade(user *m.User, interNo string, outerNo str
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 转换交易
|
||||||
|
func (s *tradeService) ConvertTradeToBalance(admin *m.Admin, ref *TradeRef) error {
|
||||||
|
trade, err := q.Trade.Where(q.Trade.InnerNo.Eq(ref.TradeNo)).First()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := q.User.Where(q.User.ID.Eq(trade.UserID)).First()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return q.Q.Transaction(func(q *q.Query) error {
|
||||||
|
return User.UpdateBalance(q, user, trade.Payment, "管理员订单补余额", &admin.ID, nil)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 取消交易
|
// 取消交易
|
||||||
func (s *tradeService) CancelTrade(ref *TradeRef) error {
|
func (s *tradeService) CancelTrade(ref *TradeRef) error {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
@@ -394,7 +413,7 @@ func (s *tradeService) CancelTrade(ref *TradeRef) error {
|
|||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Debug(fmt.Sprintf("订单无需关闭: %s", err.Error()))
|
slog.Debug(fmt.Sprintf("订单无需关闭: %s", err.Error()))
|
||||||
return nil
|
return ErrTradeStatusIgnored
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -575,6 +594,12 @@ func (s *tradeService) CheckTrade(ref *TradeRef) (*CheckTradeResult, error) {
|
|||||||
return &result, nil
|
return &result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新备注
|
||||||
|
func (s *tradeService) UpdateRemark(tradeNo string, remark string) error {
|
||||||
|
_, err := q.Trade.Where(q.Trade.InnerNo.Eq(tradeNo)).UpdateColumn(q.Trade.Remark, remark)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func tradeProductKey(no string) string {
|
func tradeProductKey(no string) string {
|
||||||
return fmt.Sprintf("trade:%s:product", no)
|
return fmt.Sprintf("trade:%s:product", no)
|
||||||
}
|
}
|
||||||
@@ -639,4 +664,5 @@ func (e TradeErr) Error() string {
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
ErrTransactionNotSupported = core.NewBizErr("不支持的支付方式")
|
ErrTransactionNotSupported = core.NewBizErr("不支持的支付方式")
|
||||||
|
ErrTradeStatusIgnored = core.NewBizErr("交易状态已忽略")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ package tasks
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"platform/web/events"
|
"platform/web/events"
|
||||||
q "platform/web/queries"
|
m "platform/web/models"
|
||||||
s "platform/web/services"
|
s "platform/web/services"
|
||||||
|
|
||||||
"github.com/hibiken/asynq"
|
"github.com/hibiken/asynq"
|
||||||
@@ -24,19 +25,30 @@ func HandleCompleteTrade(_ context.Context, task *asynq.Task) error {
|
|||||||
Method: event.Method,
|
Method: event.Method,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试完成交易
|
// 关闭交易
|
||||||
user, err := s.User.Get(q.Q, event.UserId)
|
err := s.Trade.CancelTrade(&data)
|
||||||
if err != nil {
|
if errors.Is(err, s.ErrTradeStatusIgnored) {
|
||||||
return fmt.Errorf("获取用户失败: %w", err)
|
result, err := s.Trade.CheckTrade(&data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("检查交易状态失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch result.Status {
|
||||||
|
case m.TradeStatusSuccess:
|
||||||
|
if err := s.Trade.UpdateRemark(data.TradeNo, "已付款"); err != nil {
|
||||||
|
slog.Error("添加备注失败", "err", err)
|
||||||
|
}
|
||||||
|
case m.TradeStatusCanceled:
|
||||||
|
slog.Debug("交易已取消", "status", result.Status)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("意外交易状态: %v", result.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Trade.CompleteTrade(user, &data); err != nil {
|
if err != nil {
|
||||||
slog.Debug("结束交易失败:完成交易失败", "err", err)
|
return fmt.Errorf("结束交易失败:取消交易失败: %w", err)
|
||||||
|
|
||||||
// 交易无法完成,关闭交易
|
|
||||||
if err := s.Trade.CancelTrade(&data); err != nil {
|
|
||||||
return fmt.Errorf("结束交易失败:取消交易失败: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -56,9 +68,9 @@ func HandleRemoveChannel(_ context.Context, task *asynq.Task) (err error) {
|
|||||||
func HandleRefreshEdges(_ context.Context, task *asynq.Task) (err error) {
|
func HandleRefreshEdges(_ context.Context, task *asynq.Task) (err error) {
|
||||||
slog.Info("[event]刷新边缘节点")
|
slog.Info("[event]刷新边缘节点")
|
||||||
|
|
||||||
err = s.Channel.RefreshEdges()
|
// err = s.Channel.RefreshEdges()
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return fmt.Errorf("刷新边缘节点失败: %w", err)
|
// return fmt.Errorf("刷新边缘节点失败: %w", err)
|
||||||
}
|
// }
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
27
web/web.go
27
web/web.go
@@ -42,10 +42,6 @@ func RunApp(pCtx context.Context) error {
|
|||||||
return RunTask(ctx)
|
return RunTask(ctx)
|
||||||
})
|
})
|
||||||
|
|
||||||
g.Go(func() error {
|
|
||||||
return RunCron(ctx)
|
|
||||||
})
|
|
||||||
|
|
||||||
return g.Wait()
|
return g.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,29 +107,6 @@ func RunTask(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunCron(ctx context.Context) error {
|
|
||||||
cron := asynq.NewSchedulerFromRedisClient(deps.Redis, &asynq.SchedulerOpts{
|
|
||||||
Logger: &AppAsynqLogger{},
|
|
||||||
Location: time.Local,
|
|
||||||
})
|
|
||||||
|
|
||||||
cron.Register("0/10 * * * *", events.NewRefreshEdge())
|
|
||||||
|
|
||||||
// 停止服务
|
|
||||||
go func() {
|
|
||||||
<-ctx.Done()
|
|
||||||
cron.Shutdown()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 启动服务
|
|
||||||
err := cron.Run()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("定时任务服务运行失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type AppAsynqLogger struct{}
|
type AppAsynqLogger struct{}
|
||||||
|
|
||||||
func (l *AppAsynqLogger) Debug(args ...any) {
|
func (l *AppAsynqLogger) Debug(args ...any) {
|
||||||
|
|||||||
Reference in New Issue
Block a user