5 Commits

24 changed files with 8176 additions and 6864 deletions

View File

@@ -1,6 +1,11 @@
## TODO ## TODO
- 日志记录 后台交易备注
关闭支付弹窗
兜底手动操作
限速
- edge.area_id 可为空,代表节点无固定地区
- 后台展示 mac, ip:port实际地区 - 后台展示 mac, ip:port实际地区
上传文件平铺到 uploads不分子文件夹 上传文件平铺到 uploads不分子文件夹

View File

@@ -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:

View File

@@ -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]

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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)
} // }

View File

@@ -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
View 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")
}
}

View File

@@ -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
}

View File

@@ -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
} }

View File

@@ -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,

View File

@@ -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
} }

View File

@@ -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 // 移动

View File

@@ -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")

View File

@@ -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 不参与此流程。

View File

@@ -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 检查本地节点是否足够,如果不足从云端连入

View File

@@ -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
} }

View File

@@ -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
} }
// 新增产品 // 新增产品

View File

@@ -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 {

View File

@@ -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("交易状态已忽略")
) )

View File

@@ -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
} }

View File

@@ -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) {