Compare commits
6 Commits
main
...
Branch_反向做
| Author | SHA1 | Date | |
|---|---|---|---|
| b1cca5bec7 | |||
| 4b28684fe4 | |||
| 56a761e5ab | |||
| 771c617da4 | |||
| 3013486dd4 | |||
| e943a47121 |
@ -120,6 +120,11 @@ func (e LineApiUser) Insert(c *gin.Context) {
|
|||||||
// 设置创建人
|
// 设置创建人
|
||||||
req.SetCreateBy(user.GetUserId(c))
|
req.SetCreateBy(user.GetUserId(c))
|
||||||
|
|
||||||
|
if err := req.Valid(); err != nil {
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
err = s.Insert(&req)
|
err = s.Insert(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e.Error(500, err, fmt.Sprintf("创建api用户管理失败,\r\n失败信息 %s", err.Error()))
|
e.Error(500, err, fmt.Sprintf("创建api用户管理失败,\r\n失败信息 %s", err.Error()))
|
||||||
@ -153,6 +158,12 @@ func (e LineApiUser) Update(c *gin.Context) {
|
|||||||
e.Error(500, err, err.Error())
|
e.Error(500, err, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := req.Valid(); err != nil {
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
req.SetUpdateBy(user.GetUserId(c))
|
req.SetUpdateBy(user.GetUserId(c))
|
||||||
p := actions.GetPermissionFromContext(c)
|
p := actions.GetPermissionFromContext(c)
|
||||||
|
|
||||||
@ -267,3 +278,74 @@ func (e LineApiUser) GetMainUser(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
e.OK(list, "操作成功")
|
e.OK(list, "操作成功")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUnBindApiUser 获取未绑定下反单api用户
|
||||||
|
func (e LineApiUser) GetUnBindReverseApiUser(c *gin.Context) {
|
||||||
|
s := service.LineApiUser{}
|
||||||
|
req := dto.GetUnBindReverseReq{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req, binding.Query, binding.Form).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
datas := make([]dto.UnBindReverseResp, 0)
|
||||||
|
|
||||||
|
err = s.GetUnBindApiUser(&req, &datas)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("获取失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.OK(datas, "查询成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取反单api用户选项
|
||||||
|
func (e LineApiUser) GetReverseApiOptions(c *gin.Context) {
|
||||||
|
req := dto.GetReverseApiOptionsReq{}
|
||||||
|
s := service.LineApiUser{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
list := make([]models.LineApiUser, 0)
|
||||||
|
err = s.GetReverseApiOptions(&req, &list)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("获取失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.OK(list, "操作成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有启用的反单api用户
|
||||||
|
func (e LineApiUser) GetReverseApiOptionsAll(c *gin.Context) {
|
||||||
|
s := service.LineApiUser{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
list := make([]dto.LineApiUserOptionResp, 0)
|
||||||
|
err = s.GetReverseApiOptionsAll(&list)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("获取失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.OK(list, "操作成功")
|
||||||
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package apis
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/go-admin-team/go-admin-core/sdk/api"
|
"github.com/go-admin-team/go-admin-core/sdk/api"
|
||||||
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
|
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
|
||||||
@ -193,6 +194,7 @@ func (e LinePriceLimit) Delete(c *gin.Context) {
|
|||||||
e.OK(req.GetId(), "删除成功")
|
e.OK(req.GetId(), "删除成功")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// aicoin数据同步
|
||||||
func (e LinePriceLimit) UpRange(c *gin.Context) {
|
func (e LinePriceLimit) UpRange(c *gin.Context) {
|
||||||
s := service.LinePriceLimit{}
|
s := service.LinePriceLimit{}
|
||||||
err := e.MakeContext(c).
|
err := e.MakeContext(c).
|
||||||
|
|||||||
198
app/admin/apis/line_reverse_order.go
Normal file
198
app/admin/apis/line_reverse_order.go
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
package apis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk/api"
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
|
||||||
|
_ "github.com/go-admin-team/go-admin-core/sdk/pkg/response"
|
||||||
|
|
||||||
|
"go-admin/app/admin/models"
|
||||||
|
"go-admin/app/admin/service"
|
||||||
|
"go-admin/app/admin/service/dto"
|
||||||
|
"go-admin/common/actions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LineReverseOrder struct {
|
||||||
|
api.Api
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPage 获取反单下单列表列表
|
||||||
|
// @Summary 获取反单下单列表列表
|
||||||
|
// @Description 获取反单下单列表列表
|
||||||
|
// @Tags 反单下单列表
|
||||||
|
// @Param orderSn query string false "订单号"
|
||||||
|
// @Param orderId query string false "币安订单号"
|
||||||
|
// @Param followOrderSn query string false "跟随币安订单号"
|
||||||
|
// @Param orderType query int false "订单类型 0-主单 1-止损单 2-加仓 3-减仓"
|
||||||
|
// @Param positionSide query string false "持仓方向 LONG-多 SHORT-空"
|
||||||
|
// @Param side query string false "买卖方向 SELL-卖 BUY-买"
|
||||||
|
// @Param status query int false "状态 1-待下单 2-已下单 3-已成交 4-已平仓 5-已止损"
|
||||||
|
// @Param pageSize query int false "页条数"
|
||||||
|
// @Param pageIndex query int false "页码"
|
||||||
|
// @Success 200 {object} response.Response{data=response.Page{list=[]models.LineReverseOrder}} "{"code": 200, "data": [...]}"
|
||||||
|
// @Router /api/v1/line-reverse-order [get]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineReverseOrder) GetPage(c *gin.Context) {
|
||||||
|
req := dto.LineReverseOrderGetPageReq{}
|
||||||
|
s := service.LineReverseOrder{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p := actions.GetPermissionFromContext(c)
|
||||||
|
list := make([]models.LineReverseOrder, 0)
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
err = s.GetPage(&req, p, &list, &count)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("获取反单下单列表失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 获取反单下单列表
|
||||||
|
// @Summary 获取反单下单列表
|
||||||
|
// @Description 获取反单下单列表
|
||||||
|
// @Tags 反单下单列表
|
||||||
|
// @Param id path int false "id"
|
||||||
|
// @Success 200 {object} response.Response{data=models.LineReverseOrder} "{"code": 200, "data": [...]}"
|
||||||
|
// @Router /api/v1/line-reverse-order/{id} [get]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineReverseOrder) Get(c *gin.Context) {
|
||||||
|
req := dto.LineReverseOrderGetReq{}
|
||||||
|
s := service.LineReverseOrder{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var object models.LineReverseOrder
|
||||||
|
|
||||||
|
p := actions.GetPermissionFromContext(c)
|
||||||
|
err = s.Get(&req, p, &object)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("获取反单下单列表失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.OK( object, "查询成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert 创建反单下单列表
|
||||||
|
// @Summary 创建反单下单列表
|
||||||
|
// @Description 创建反单下单列表
|
||||||
|
// @Tags 反单下单列表
|
||||||
|
// @Accept application/json
|
||||||
|
// @Product application/json
|
||||||
|
// @Param data body dto.LineReverseOrderInsertReq true "data"
|
||||||
|
// @Success 200 {object} response.Response "{"code": 200, "message": "添加成功"}"
|
||||||
|
// @Router /api/v1/line-reverse-order [post]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineReverseOrder) Insert(c *gin.Context) {
|
||||||
|
req := dto.LineReverseOrderInsertReq{}
|
||||||
|
s := service.LineReverseOrder{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 设置创建人
|
||||||
|
req.SetCreateBy(user.GetUserId(c))
|
||||||
|
|
||||||
|
err = s.Insert(&req)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("创建反单下单列表失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.OK(req.GetId(), "创建成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 修改反单下单列表
|
||||||
|
// @Summary 修改反单下单列表
|
||||||
|
// @Description 修改反单下单列表
|
||||||
|
// @Tags 反单下单列表
|
||||||
|
// @Accept application/json
|
||||||
|
// @Product application/json
|
||||||
|
// @Param id path int true "id"
|
||||||
|
// @Param data body dto.LineReverseOrderUpdateReq true "body"
|
||||||
|
// @Success 200 {object} response.Response "{"code": 200, "message": "修改成功"}"
|
||||||
|
// @Router /api/v1/line-reverse-order/{id} [put]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineReverseOrder) Update(c *gin.Context) {
|
||||||
|
req := dto.LineReverseOrderUpdateReq{}
|
||||||
|
s := service.LineReverseOrder{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.SetUpdateBy(user.GetUserId(c))
|
||||||
|
p := actions.GetPermissionFromContext(c)
|
||||||
|
|
||||||
|
err = s.Update(&req, p)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("修改反单下单列表失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.OK( req.GetId(), "修改成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除反单下单列表
|
||||||
|
// @Summary 删除反单下单列表
|
||||||
|
// @Description 删除反单下单列表
|
||||||
|
// @Tags 反单下单列表
|
||||||
|
// @Param data body dto.LineReverseOrderDeleteReq true "body"
|
||||||
|
// @Success 200 {object} response.Response "{"code": 200, "message": "删除成功"}"
|
||||||
|
// @Router /api/v1/line-reverse-order [delete]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineReverseOrder) Delete(c *gin.Context) {
|
||||||
|
s := service.LineReverseOrder{}
|
||||||
|
req := dto.LineReverseOrderDeleteReq{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// req.SetUpdateBy(user.GetUserId(c))
|
||||||
|
p := actions.GetPermissionFromContext(c)
|
||||||
|
|
||||||
|
err = s.Remove(&req, p)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("删除反单下单列表失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.OK( req.GetId(), "删除成功")
|
||||||
|
}
|
||||||
282
app/admin/apis/line_reverse_position.go
Normal file
282
app/admin/apis/line_reverse_position.go
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
package apis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk/api"
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
|
||||||
|
_ "github.com/go-admin-team/go-admin-core/sdk/pkg/response"
|
||||||
|
|
||||||
|
"go-admin/app/admin/models"
|
||||||
|
"go-admin/app/admin/service"
|
||||||
|
"go-admin/app/admin/service/dto"
|
||||||
|
"go-admin/common/actions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LineReversePosition struct {
|
||||||
|
api.Api
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPage 获取反单管理-仓位列表
|
||||||
|
// @Summary 获取反单管理-仓位列表
|
||||||
|
// @Description 获取反单管理-仓位列表
|
||||||
|
// @Tags 反单管理-仓位
|
||||||
|
// @Param apiId query int64 false "api_id"
|
||||||
|
// @Param reverseApiId query int64 false "反单api_id"
|
||||||
|
// @Param side query string false "买卖方向 BUY SELL"
|
||||||
|
// @Param positionSide query string false "持仓方向 LONG SHORT"
|
||||||
|
// @Param symbol query string false "交易对"
|
||||||
|
// @Param pageSize query int false "页条数"
|
||||||
|
// @Param pageIndex query int false "页码"
|
||||||
|
// @Success 200 {object} response.Response{data=response.Page{list=[]models.LineReversePosition}} "{"code": 200, "data": [...]}"
|
||||||
|
// @Router /api/v1/line-reverse-position [get]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineReversePosition) GetPage(c *gin.Context) {
|
||||||
|
req := dto.LineReversePositionGetPageReq{}
|
||||||
|
s := service.LineReversePosition{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p := actions.GetPermissionFromContext(c)
|
||||||
|
list := make([]dto.LineReversePositionListResp, 0)
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
err = s.GetPage(&req, p, &list, &count)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("获取反单管理-仓位失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 获取反单管理-仓位
|
||||||
|
// @Summary 获取反单管理-仓位
|
||||||
|
// @Description 获取反单管理-仓位
|
||||||
|
// @Tags 反单管理-仓位
|
||||||
|
// @Param id path int false "id"
|
||||||
|
// @Success 200 {object} response.Response{data=models.LineReversePosition} "{"code": 200, "data": [...]}"
|
||||||
|
// @Router /api/v1/line-reverse-position/{id} [get]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineReversePosition) Get(c *gin.Context) {
|
||||||
|
req := dto.LineReversePositionGetReq{}
|
||||||
|
s := service.LineReversePosition{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var object models.LineReversePosition
|
||||||
|
|
||||||
|
p := actions.GetPermissionFromContext(c)
|
||||||
|
err = s.Get(&req, p, &object)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("获取反单管理-仓位失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.OK(object, "查询成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert 创建反单管理-仓位
|
||||||
|
// @Summary 创建反单管理-仓位
|
||||||
|
// @Description 创建反单管理-仓位
|
||||||
|
// @Tags 反单管理-仓位
|
||||||
|
// @Accept application/json
|
||||||
|
// @Product application/json
|
||||||
|
// @Param data body dto.LineReversePositionInsertReq true "data"
|
||||||
|
// @Success 200 {object} response.Response "{"code": 200, "message": "添加成功"}"
|
||||||
|
// @Router /api/v1/line-reverse-position [post]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineReversePosition) Insert(c *gin.Context) {
|
||||||
|
req := dto.LineReversePositionInsertReq{}
|
||||||
|
s := service.LineReversePosition{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 设置创建人
|
||||||
|
req.SetCreateBy(user.GetUserId(c))
|
||||||
|
|
||||||
|
err = s.Insert(&req)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("创建反单管理-仓位失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.OK(req.GetId(), "创建成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 修改反单管理-仓位
|
||||||
|
// @Summary 修改反单管理-仓位
|
||||||
|
// @Description 修改反单管理-仓位
|
||||||
|
// @Tags 反单管理-仓位
|
||||||
|
// @Accept application/json
|
||||||
|
// @Product application/json
|
||||||
|
// @Param id path int true "id"
|
||||||
|
// @Param data body dto.LineReversePositionUpdateReq true "body"
|
||||||
|
// @Success 200 {object} response.Response "{"code": 200, "message": "修改成功"}"
|
||||||
|
// @Router /api/v1/line-reverse-position/{id} [put]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineReversePosition) Update(c *gin.Context) {
|
||||||
|
req := dto.LineReversePositionUpdateReq{}
|
||||||
|
s := service.LineReversePosition{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.SetUpdateBy(user.GetUserId(c))
|
||||||
|
p := actions.GetPermissionFromContext(c)
|
||||||
|
|
||||||
|
err = s.Update(&req, p)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("修改反单管理-仓位失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.OK(req.GetId(), "修改成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除反单管理-仓位
|
||||||
|
// @Summary 删除反单管理-仓位
|
||||||
|
// @Description 删除反单管理-仓位
|
||||||
|
// @Tags 反单管理-仓位
|
||||||
|
// @Param data body dto.LineReversePositionDeleteReq true "body"
|
||||||
|
// @Success 200 {object} response.Response "{"code": 200, "message": "删除成功"}"
|
||||||
|
// @Router /api/v1/line-reverse-position [delete]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineReversePosition) Delete(c *gin.Context) {
|
||||||
|
s := service.LineReversePosition{}
|
||||||
|
req := dto.LineReversePositionDeleteReq{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// req.SetUpdateBy(user.GetUserId(c))
|
||||||
|
p := actions.GetPermissionFromContext(c)
|
||||||
|
|
||||||
|
err = s.Remove(&req, p)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("删除反单管理-仓位失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.OK(req.GetId(), "删除成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClosePosition 平仓
|
||||||
|
func (e LineReversePosition) ClosePosition(c *gin.Context) {
|
||||||
|
req := dto.LineReversePositionCloseReq{}
|
||||||
|
s := service.LineReversePosition{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userId := user.GetUserId(c)
|
||||||
|
p := actions.GetPermissionFromContext(c)
|
||||||
|
|
||||||
|
err = s.ClosePosition(&req, p, userId)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("平仓失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.OK(nil, "平仓成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClosePositionBatch 批量平仓
|
||||||
|
func (e LineReversePosition) ClosePositionBatch(c *gin.Context) {
|
||||||
|
req := dto.LineReversePositionCloseBatchReq{}
|
||||||
|
s := service.LineReversePosition{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userId := user.GetUserId(c)
|
||||||
|
p := actions.GetPermissionFromContext(c)
|
||||||
|
errs := make([]string, 0)
|
||||||
|
|
||||||
|
err = s.ClosePositionBatch(&req, p, userId, &errs)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("批量平仓失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) > 0 {
|
||||||
|
content := strings.Join(errs, "</br>")
|
||||||
|
|
||||||
|
e.OK(content, "批量平仓部分失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.OK(nil, "批量平仓成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除所有
|
||||||
|
func (e LineReversePosition) CleanAll(c *gin.Context) {
|
||||||
|
s := service.LineReversePosition{}
|
||||||
|
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userId := user.GetUserId(c)
|
||||||
|
p := actions.GetPermissionFromContext(c)
|
||||||
|
|
||||||
|
err = s.CleanAll(p, userId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("清除所有仓位失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.OK(nil, "清除所有仓位成功")
|
||||||
|
}
|
||||||
158
app/admin/apis/line_reverse_setting.go
Normal file
158
app/admin/apis/line_reverse_setting.go
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
package apis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk/api"
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
|
||||||
|
_ "github.com/go-admin-team/go-admin-core/sdk/pkg/response"
|
||||||
|
|
||||||
|
"go-admin/app/admin/models"
|
||||||
|
"go-admin/app/admin/service"
|
||||||
|
"go-admin/app/admin/service/dto"
|
||||||
|
"go-admin/common/actions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LineReverseSetting struct {
|
||||||
|
api.Api
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPage 获取反单下单配置列表
|
||||||
|
// @Summary 获取反单下单配置列表
|
||||||
|
// @Description 获取反单下单配置列表
|
||||||
|
// @Tags 反单下单配置
|
||||||
|
// @Param pageSize query int false "页条数"
|
||||||
|
// @Param pageIndex query int false "页码"
|
||||||
|
// @Success 200 {object} response.Response{data=response.Page{list=[]models.LineReverseSetting}} "{"code": 200, "data": [...]}"
|
||||||
|
// @Router /api/v1/line-reverse-setting [get]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineReverseSetting) GetPage(c *gin.Context) {
|
||||||
|
req := dto.LineReverseSettingGetPageReq{}
|
||||||
|
s := service.LineReverseSetting{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p := actions.GetPermissionFromContext(c)
|
||||||
|
list := make([]models.LineReverseSetting, 0)
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
err = s.GetPage(&req, p, &list, &count)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("获取反单下单配置失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 获取反单下单配置
|
||||||
|
// @Summary 获取反单下单配置
|
||||||
|
// @Description 获取反单下单配置
|
||||||
|
// @Tags 反单下单配置
|
||||||
|
// @Param id path int false "id"
|
||||||
|
// @Success 200 {object} response.Response{data=models.LineReverseSetting} "{"code": 200, "data": [...]}"
|
||||||
|
// @Router /api/v1/line-reverse-setting/{id} [get]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineReverseSetting) Get(c *gin.Context) {
|
||||||
|
req := dto.LineReverseSettingGetReq{}
|
||||||
|
s := service.LineReverseSetting{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var object models.LineReverseSetting
|
||||||
|
|
||||||
|
p := actions.GetPermissionFromContext(c)
|
||||||
|
err = s.Get(&req, p, &object)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("获取反单下单配置失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.OK(object, "查询成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert 创建反单下单配置
|
||||||
|
// @Summary 创建反单下单配置
|
||||||
|
// @Description 创建反单下单配置
|
||||||
|
// @Tags 反单下单配置
|
||||||
|
// @Accept application/json
|
||||||
|
// @Product application/json
|
||||||
|
// @Param data body dto.LineReverseSettingInsertReq true "data"
|
||||||
|
// @Success 200 {object} response.Response "{"code": 200, "message": "添加成功"}"
|
||||||
|
// @Router /api/v1/line-reverse-setting [post]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineReverseSetting) Insert(c *gin.Context) {
|
||||||
|
req := dto.LineReverseSettingInsertReq{}
|
||||||
|
s := service.LineReverseSetting{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 设置创建人
|
||||||
|
req.SetCreateBy(user.GetUserId(c))
|
||||||
|
|
||||||
|
err = s.Insert(&req)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("创建反单下单配置失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.OK(req.GetId(), "创建成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 修改反单下单配置
|
||||||
|
// @Summary 修改反单下单配置
|
||||||
|
// @Description 修改反单下单配置
|
||||||
|
// @Tags 反单下单配置
|
||||||
|
// @Accept application/json
|
||||||
|
// @Product application/json
|
||||||
|
// @Param id path int true "id"
|
||||||
|
// @Param data body dto.LineReverseSettingUpdateReq true "body"
|
||||||
|
// @Success 200 {object} response.Response "{"code": 200, "message": "修改成功"}"
|
||||||
|
// @Router /api/v1/line-reverse-setting/{id} [put]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineReverseSetting) Update(c *gin.Context) {
|
||||||
|
req := dto.LineReverseSettingUpdateReq{}
|
||||||
|
s := service.LineReverseSetting{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.SetUpdateBy(user.GetUserId(c))
|
||||||
|
p := actions.GetPermissionFromContext(c)
|
||||||
|
|
||||||
|
err = s.Update(&req, p)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("修改反单下单配置失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.OK(req.GetId(), "修改成功")
|
||||||
|
}
|
||||||
@ -2,29 +2,31 @@ package models
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"go-admin/common/models"
|
"go-admin/common/models"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LineApiUser struct {
|
type LineApiUser struct {
|
||||||
models.Model
|
models.Model
|
||||||
|
|
||||||
ExchangeType string `json:"exchangeType" gorm:"type:varchar(20);comment:交易所类型(字典 exchange_type)"`
|
ExchangeType string `json:"exchangeType" gorm:"type:varchar(20);comment:交易所类型(字典 exchange_type)"`
|
||||||
UserId int64 `json:"userId" gorm:"type:int unsigned;comment:用户id"`
|
UserId int64 `json:"userId" gorm:"type:int unsigned;comment:用户id"`
|
||||||
JysId int64 `json:"jysId" gorm:"type:int;comment:关联交易所账号id"`
|
ApiName string `json:"apiName" gorm:"type:varchar(255);comment:api用户名"`
|
||||||
ApiName string `json:"apiName" gorm:"type:varchar(255);comment:api用户名"`
|
ApiKey string `json:"apiKey" gorm:"type:varchar(255);comment:apiKey"`
|
||||||
ApiKey string `json:"apiKey" gorm:"type:varchar(255);comment:apiKey"`
|
ApiSecret string `json:"apiSecret" gorm:"type:varchar(255);comment:apiSecret"`
|
||||||
ApiSecret string `json:"apiSecret" gorm:"type:varchar(255);comment:apiSecret"`
|
IpAddress string `json:"ipAddress" gorm:"type:varchar(255);comment:代理地址"`
|
||||||
IpAddress string `json:"ipAddress" gorm:"type:varchar(255);comment:代理地址"`
|
UserPass string `json:"userPass" gorm:"type:varchar(255);comment:代码账号密码"`
|
||||||
UserPass string `json:"userPass" gorm:"type:varchar(255);comment:代码账号密码"`
|
Affiliation int64 `json:"affiliation" gorm:"type:int;comment:归属:1=现货,2=合约,3=现货合约"`
|
||||||
AdminId int64 `json:"adminId" gorm:"type:int unsigned;comment:管理员id"`
|
AdminShow int64 `json:"adminShow" gorm:"type:int;comment:是否超管可见:1=是,0=否"`
|
||||||
Affiliation int64 `json:"affiliation" gorm:"type:int;comment:归属:1=现货,2=合约,3=现货合约"`
|
Site string `json:"site" gorm:"type:enum('1','2','3');comment:允许下单的方向:1=多;2=空;3=多空"`
|
||||||
AdminShow int64 `json:"adminShow" gorm:"type:int;comment:是否超管可见:1=是,0=否"`
|
Subordinate string `json:"subordinate" gorm:"type:enum('0','1','2');comment:从属关系:0=未绑定关系;1=主账号;2=副帐号"`
|
||||||
Site string `json:"site" gorm:"type:enum('1','2','3');comment:允许下单的方向:1=多;2=空;3=多空"`
|
GroupId int64 `json:"groupId" gorm:"type:int unsigned;comment:所属组id"`
|
||||||
Subordinate string `json:"subordinate" gorm:"type:enum('0','1','2');comment:从属关系:0=未绑定关系;1=主账号;2=副帐号"`
|
OpenStatus int64 `json:"openStatus" gorm:"type:int unsigned;comment:开启状态 0=关闭 1=开启"`
|
||||||
GroupId int64 `json:"groupId" gorm:"type:int unsigned;comment:所属组id"`
|
ReverseStatus int `json:"reverseStatus" gorm:"type:tinyint unsigned;comment:反向开仓:1=开启;2=关闭"`
|
||||||
OpenStatus int64 `json:"openStatus" gorm:"type:int unsigned;comment:开启状态 0=关闭 1=开启"`
|
ReverseApiId int `json:"reverseApiId" gorm:"type:bigint;comment:反向apiId"`
|
||||||
|
OrderProportion decimal.Decimal `json:"orderProportion" gorm:"type:decimal(10,2);comment:委托比例"`
|
||||||
SpotLastTime string `json:"spotLastTime" gorm:"-"` //现货websocket最后通信时间
|
SpotLastTime string `json:"spotLastTime" gorm:"-"` //现货websocket最后通信时间
|
||||||
FuturesLastTime string `json:"futuresLastTime" gorm:"-"` //合约websocket最后通信时间
|
FuturesLastTime string `json:"futuresLastTime" gorm:"-"` //合约websocket最后通信时间
|
||||||
models.ModelTime
|
models.ModelTime
|
||||||
models.ControlBy
|
models.ControlBy
|
||||||
}
|
}
|
||||||
|
|||||||
52
app/admin/models/line_reverse_order.go
Normal file
52
app/admin/models/line_reverse_order.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go-admin/common/models"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LineReverseOrder struct {
|
||||||
|
models.Model
|
||||||
|
|
||||||
|
PositionId int `json:"positionId" gorm:"type:bigint;comment:仓位id"`
|
||||||
|
ApiId int `json:"apiId" gorm:"type:bigint;comment:api id"`
|
||||||
|
Category int `json:"category" gorm:"type:tinyint;comment:分类 0-原始订单 1-反单"`
|
||||||
|
|
||||||
|
OrderSn string `json:"orderSn" gorm:"type:varchar(50);comment:订单号 0-主单 1-止盈 2-止损"`
|
||||||
|
OrderId string `json:"orderId" gorm:"type:varchar(50);comment:币安订单号"`
|
||||||
|
FollowOrderSn string `json:"followOrderSn" gorm:"type:varchar(50);comment:跟随币安订单号"`
|
||||||
|
Symbol string `json:"symbol" gorm:"type:varchar(20);comment:交易对"`
|
||||||
|
Type string `json:"type" gorm:"type:varchar(20);comment:交易类型"`
|
||||||
|
OrderType int `json:"orderType" gorm:"type:tinyint;comment:订单类型 0-主单 1-止盈 2-止损 3-减仓单 4-平仓单"`
|
||||||
|
BuyPrice decimal.Decimal `json:"buyPrice" gorm:"type:decimal(18,8);comment:购买金额"`
|
||||||
|
Price decimal.Decimal `json:"price" gorm:"type:decimal(18,8);comment:委托价格"`
|
||||||
|
TotalNum decimal.Decimal `json:"totalNum" gorm:"type:decimal(18,8);comment:总成交数量"`
|
||||||
|
PriceU decimal.Decimal `json:"priceU" gorm:"type:decimal(18,8);comment:委托价格(U)"`
|
||||||
|
FinalPrice decimal.Decimal `json:"finalPrice" gorm:"type:decimal(18,8);comment:实际成交价"`
|
||||||
|
PositionSide string `json:"positionSide" gorm:"type:varchar(10);comment:持仓方向 LONG-多 SHORT-空"`
|
||||||
|
Side string `json:"side" gorm:"type:varchar(10);comment:买卖方向 SELL-卖 BUY-买"`
|
||||||
|
SignPrice decimal.Decimal `json:"signPrice" gorm:"type:decimal(18,8);comment:行情价"`
|
||||||
|
TriggerTime *time.Time `json:"triggerTime" gorm:"type:datetime;comment:触发时间"`
|
||||||
|
Status int `json:"status" gorm:"type:tinyint;comment:状态 1-待下单 2-已下单 3-已成交 6-已取消 7-已过期 8-已失败"`
|
||||||
|
IsAddPosition int `json:"isAddPosition" gorm:"type:tinyint;comment:是否增加仓位 1-否 2-是"`
|
||||||
|
IsReduce int `json:"isReduce" gorm:"type:tinyint;comment:是否减少仓位 1-否 2-是"`
|
||||||
|
Remark string `json:"remark" gorm:"type:varchar(255);comment:备注"`
|
||||||
|
models.ModelTime
|
||||||
|
models.ControlBy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (LineReverseOrder) TableName() string {
|
||||||
|
return "line_reverse_order"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *LineReverseOrder) Generate() models.ActiveRecord {
|
||||||
|
o := *e
|
||||||
|
return &o
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *LineReverseOrder) GetId() interface{} {
|
||||||
|
return e.Id
|
||||||
|
}
|
||||||
42
app/admin/models/line_reverse_position.go
Normal file
42
app/admin/models/line_reverse_position.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go-admin/common/models"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LineReversePosition struct {
|
||||||
|
models.Model
|
||||||
|
|
||||||
|
PositionNo string `json:"positionNo" gorm:"type:varchar(18);comment:仓位编号"`
|
||||||
|
ApiId int `json:"apiId" gorm:"type:bigint;comment:api_id"`
|
||||||
|
TotalAmount decimal.Decimal `json:"totalAmount" gorm:"type:decimal(18,8);comment:总仓位"`
|
||||||
|
Amount decimal.Decimal `json:"amount" gorm:"type:decimal(18,8);comment:仓位"`
|
||||||
|
ReverseApiId int `json:"reverseApiId" gorm:"type:bigint;comment:反单api_id"`
|
||||||
|
TotalReverseAmount decimal.Decimal `json:"totalReverseAmount" gorm:"type:decimal(18,8);comment:总反单仓位"`
|
||||||
|
ReverseAmount decimal.Decimal `json:"reverseAmount" gorm:"type:decimal(18,8);comment:反单仓位"`
|
||||||
|
Side string `json:"side" gorm:"type:varchar(10);comment:买卖方向 BUY SELL"`
|
||||||
|
PositionSide string `json:"positionSide" gorm:"type:varchar(10);comment:持仓方向 LONG SHORT"`
|
||||||
|
Symbol string `json:"symbol" gorm:"type:varchar(20);comment:交易对"`
|
||||||
|
Status int `json:"status" gorm:"type:tinyint;comment:仓位状态 1-已开仓 2-已平仓"`
|
||||||
|
ReverseStatus int `json:"reverseStatus" gorm:"type:tinyint;comment:反单仓位状态 1-已开仓 2-已平仓"`
|
||||||
|
AveragePrice decimal.Decimal `json:"averagePrice" gorm:"type:decimal(18,8);comment:主单平均价格"`
|
||||||
|
ReverseAveragePrice decimal.Decimal `json:"reverseAveragePrice" gorm:"type:decimal(18,8);comment:反单平均价格"`
|
||||||
|
Version int `json:"version" gorm:"type:int;default:0;comment:版本号,用于乐观锁控制"`
|
||||||
|
models.ModelTime
|
||||||
|
models.ControlBy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (LineReversePosition) TableName() string {
|
||||||
|
return "line_reverse_position"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *LineReversePosition) Generate() models.ActiveRecord {
|
||||||
|
o := *e
|
||||||
|
return &o
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *LineReversePosition) GetId() interface{} {
|
||||||
|
return e.Id
|
||||||
|
}
|
||||||
31
app/admin/models/line_reverse_setting.go
Normal file
31
app/admin/models/line_reverse_setting.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go-admin/common/models"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LineReverseSetting struct {
|
||||||
|
models.Model
|
||||||
|
|
||||||
|
ReverseOrderType string `json:"reverseOrderType" redis:"reverseOrderType" gorm:"type:varchar(10);comment:反单下单类型 LIMIT-限价 MARKET-市价"`
|
||||||
|
ReversePremiumRatio decimal.Decimal `json:"reversePremiumRatio" redis:"reversePremiumRatio" gorm:"type:decimal(10,2);comment:溢价百分比"`
|
||||||
|
TakeProfitRatio decimal.Decimal `json:"takeProfitRatio" redis:"takeProfitRatio" gorm:"type:decimal(10,2);comment:止盈百分比"`
|
||||||
|
StopLossRatio decimal.Decimal `json:"stopLossRatio" redis:"stopLossRatio" gorm:"type:decimal(10,2);comment:止损百分比"`
|
||||||
|
models.ModelTime
|
||||||
|
models.ControlBy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (LineReverseSetting) TableName() string {
|
||||||
|
return "line_reverse_setting"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *LineReverseSetting) Generate() models.ActiveRecord {
|
||||||
|
o := *e
|
||||||
|
return &o
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *LineReverseSetting) GetId() interface{} {
|
||||||
|
return e.Id
|
||||||
|
}
|
||||||
@ -27,5 +27,11 @@ func registerLineApiUserRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMi
|
|||||||
r.POST("bind", api.Bind) //绑定从属关系
|
r.POST("bind", api.Bind) //绑定从属关系
|
||||||
r.POST("getUser", api.GetUser) //获取未绑定的用户
|
r.POST("getUser", api.GetUser) //获取未绑定的用户
|
||||||
r.POST("getMainUser", api.GetMainUser) //获取获取主账号的用户
|
r.POST("getMainUser", api.GetMainUser) //获取获取主账号的用户
|
||||||
|
|
||||||
|
r.GET("unbind-reverse", api.GetUnBindReverseApiUser) //获取未绑定下反单用户
|
||||||
|
|
||||||
|
r.GET("reverse-options", api.GetReverseApiOptions) //获取可用反单api用户
|
||||||
|
|
||||||
|
r.GET("reverse-options-all", api.GetReverseApiOptionsAll) //获取全部启用的反单api用户
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
27
app/admin/router/line_reverse_order.go
Normal file
27
app/admin/router/line_reverse_order.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
|
||||||
|
|
||||||
|
"go-admin/app/admin/apis"
|
||||||
|
"go-admin/common/middleware"
|
||||||
|
"go-admin/common/actions"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
routerCheckRole = append(routerCheckRole, registerLineReverseOrderRouter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerLineReverseOrderRouter
|
||||||
|
func registerLineReverseOrderRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
|
||||||
|
api := apis.LineReverseOrder{}
|
||||||
|
r := v1.Group("/line-reverse-order").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole())
|
||||||
|
{
|
||||||
|
r.GET("", actions.PermissionAction(), api.GetPage)
|
||||||
|
r.GET("/:id", actions.PermissionAction(), api.Get)
|
||||||
|
r.POST("", api.Insert)
|
||||||
|
r.PUT("/:id", actions.PermissionAction(), api.Update)
|
||||||
|
r.DELETE("", api.Delete)
|
||||||
|
}
|
||||||
|
}
|
||||||
33
app/admin/router/line_reverse_position.go
Normal file
33
app/admin/router/line_reverse_position.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
|
||||||
|
|
||||||
|
"go-admin/app/admin/apis"
|
||||||
|
"go-admin/common/actions"
|
||||||
|
"go-admin/common/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
routerCheckRole = append(routerCheckRole, registerLineReversePositionRouter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerLineReversePositionRouter
|
||||||
|
func registerLineReversePositionRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
|
||||||
|
api := apis.LineReversePosition{}
|
||||||
|
r := v1.Group("/line-reverse-position").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole())
|
||||||
|
{
|
||||||
|
r.GET("", actions.PermissionAction(), api.GetPage)
|
||||||
|
r.GET("/:id", actions.PermissionAction(), api.Get)
|
||||||
|
r.POST("", api.Insert)
|
||||||
|
r.PUT("/:id", actions.PermissionAction(), api.Update)
|
||||||
|
|
||||||
|
//清理所有
|
||||||
|
r.DELETE("/clean-all", actions.PermissionAction(), api.CleanAll)
|
||||||
|
r.DELETE("", api.Delete)
|
||||||
|
|
||||||
|
r.PUT("close/:id", actions.PermissionAction(), api.ClosePosition)
|
||||||
|
r.PUT("close-batch", actions.PermissionAction(), api.ClosePositionBatch)
|
||||||
|
}
|
||||||
|
}
|
||||||
27
app/admin/router/line_reverse_setting.go
Normal file
27
app/admin/router/line_reverse_setting.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
|
||||||
|
|
||||||
|
"go-admin/app/admin/apis"
|
||||||
|
"go-admin/common/actions"
|
||||||
|
"go-admin/common/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
routerCheckRole = append(routerCheckRole, registerLineReverseSettingRouter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerLineReverseSettingRouter
|
||||||
|
func registerLineReverseSettingRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
|
||||||
|
api := apis.LineReverseSetting{}
|
||||||
|
r := v1.Group("/line-reverse-setting").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole())
|
||||||
|
{
|
||||||
|
r.GET("", actions.PermissionAction(), api.GetPage)
|
||||||
|
r.GET("/:id", actions.PermissionAction(), api.Get)
|
||||||
|
r.POST("", api.Insert)
|
||||||
|
r.PUT("/:id", actions.PermissionAction(), api.Update)
|
||||||
|
// r.DELETE("", api.Delete)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,9 +1,12 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"go-admin/app/admin/models"
|
"go-admin/app/admin/models"
|
||||||
"go-admin/common/dto"
|
"go-admin/common/dto"
|
||||||
common "go-admin/common/models"
|
common "go-admin/common/models"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LineApiUserGetPageReq struct {
|
type LineApiUserGetPageReq struct {
|
||||||
@ -41,44 +44,66 @@ func (m *LineApiUserGetPageReq) GetNeedSearch() interface{} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type LineApiUserInsertReq struct {
|
type LineApiUserInsertReq struct {
|
||||||
Id int `json:"-" comment:"id"` // id
|
Id int `json:"-" comment:"id"` // id
|
||||||
ExchangeType string `json:"exchangeType" comment:"交易所code"`
|
ExchangeType string `json:"exchangeType" comment:"交易所code"`
|
||||||
UserId int64 `json:"userId" comment:"用户id"`
|
UserId int64 `json:"userId" comment:"用户id"`
|
||||||
JysId int64 `json:"jysId" comment:"关联交易所账号id"`
|
JysId int64 `json:"jysId" comment:"关联交易所账号id"`
|
||||||
ApiName string `json:"apiName" comment:"api用户名"`
|
ApiName string `json:"apiName" comment:"api用户名"`
|
||||||
ApiKey string `json:"apiKey" comment:"apiKey"`
|
ApiKey string `json:"apiKey" comment:"apiKey"`
|
||||||
ApiSecret string `json:"apiSecret" comment:"apiSecret"`
|
ApiSecret string `json:"apiSecret" comment:"apiSecret"`
|
||||||
IpAddress string `json:"ipAddress" comment:"代理地址"`
|
IpAddress string `json:"ipAddress" comment:"代理地址"`
|
||||||
UserPass string `json:"userPass" comment:"代码账号密码"`
|
UserPass string `json:"userPass" comment:"代码账号密码"`
|
||||||
AdminId int64 `json:"adminId" comment:"管理员id"`
|
Affiliation int64 `json:"affiliation" comment:"归属:1=现货,2=合约,3=现货合约"`
|
||||||
Affiliation int64 `json:"affiliation" comment:"归属:1=现货,2=合约,3=现货合约"`
|
AdminShow int64 `json:"adminShow" comment:"是否超管可见:1=是,0=否"`
|
||||||
AdminShow int64 `json:"adminShow" comment:"是否超管可见:1=是,0=否"`
|
Site string `json:"site" comment:"允许下单的方向:1=多;2=空;3=多空"`
|
||||||
Site string `json:"site" comment:"允许下单的方向:1=多;2=空;3=多空"`
|
Subordinate string `json:"subordinate" comment:"从属关系:0=未绑定关系;1=主账号;2=副帐号"`
|
||||||
Subordinate string `json:"subordinate" comment:"从属关系:0=未绑定关系;1=主账号;2=副帐号"`
|
GroupId int64 `json:"groupId" comment:"所属组id"`
|
||||||
GroupId int64 `json:"groupId" comment:"所属组id"`
|
OpenStatus int64 `json:"openStatus" comment:"开启状态 0=关闭 1=开启"`
|
||||||
OpenStatus int64 `json:"openStatus" comment:"开启状态 0=关闭 1=开启"`
|
ReverseStatus int `json:"reverseStatus" comment:"反向下单状态 1-开启 2-关闭"`
|
||||||
|
ReverseApiId int `json:"reverseApiId" comment:"反向下单apiId"`
|
||||||
|
OrderProportion decimal.Decimal `json:"orderProportion" comment:"委托比例"`
|
||||||
common.ControlBy
|
common.ControlBy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *LineApiUserInsertReq) Valid() error {
|
||||||
|
if s.ReverseStatus == 1 {
|
||||||
|
if s.OrderProportion.Cmp(decimal.Zero) <= 0 {
|
||||||
|
return errors.New("委托比例必须大于0")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.ReverseStatus == 1 && s.ReverseApiId == 0 {
|
||||||
|
return errors.New("反向下单api不能为空")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *LineApiUserInsertReq) Generate(model *models.LineApiUser) {
|
func (s *LineApiUserInsertReq) Generate(model *models.LineApiUser) {
|
||||||
if s.Id == 0 {
|
if s.Id == 0 {
|
||||||
model.Model = common.Model{Id: s.Id}
|
model.Model = common.Model{Id: s.Id}
|
||||||
}
|
}
|
||||||
model.ExchangeType = s.ExchangeType
|
model.ExchangeType = s.ExchangeType
|
||||||
model.UserId = s.UserId
|
model.UserId = s.UserId
|
||||||
model.JysId = s.JysId
|
|
||||||
model.ApiName = s.ApiName
|
model.ApiName = s.ApiName
|
||||||
model.ApiKey = s.ApiKey
|
model.ApiKey = s.ApiKey
|
||||||
model.ApiSecret = s.ApiSecret
|
model.ApiSecret = s.ApiSecret
|
||||||
model.IpAddress = s.IpAddress
|
model.IpAddress = s.IpAddress
|
||||||
model.UserPass = s.UserPass
|
model.UserPass = s.UserPass
|
||||||
model.AdminId = s.AdminId
|
|
||||||
model.Affiliation = s.Affiliation
|
model.Affiliation = s.Affiliation
|
||||||
model.AdminShow = s.AdminShow
|
model.AdminShow = s.AdminShow
|
||||||
model.Site = s.Site
|
model.Site = s.Site
|
||||||
model.Subordinate = s.Subordinate
|
model.Subordinate = s.Subordinate
|
||||||
model.GroupId = s.GroupId
|
model.GroupId = s.GroupId
|
||||||
model.OpenStatus = s.OpenStatus
|
model.OpenStatus = s.OpenStatus
|
||||||
|
model.ReverseStatus = s.ReverseStatus
|
||||||
|
model.OrderProportion = s.OrderProportion
|
||||||
|
|
||||||
|
if model.ReverseStatus == 1 {
|
||||||
|
model.ReverseApiId = s.ReverseApiId
|
||||||
|
} else {
|
||||||
|
model.ReverseApiId = 0
|
||||||
|
}
|
||||||
|
|
||||||
model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的
|
model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,44 +112,66 @@ func (s *LineApiUserInsertReq) GetId() interface{} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type LineApiUserUpdateReq struct {
|
type LineApiUserUpdateReq struct {
|
||||||
Id int `uri:"id" comment:"id"` // id
|
Id int `uri:"id" comment:"id"` // id
|
||||||
ExchangeType string `json:"exchangeType" comment:"交易所code"`
|
ExchangeType string `json:"exchangeType" comment:"交易所code"`
|
||||||
UserId int64 `json:"userId" comment:"用户id"`
|
UserId int64 `json:"userId" comment:"用户id"`
|
||||||
JysId int64 `json:"jysId" comment:"关联交易所账号id"`
|
JysId int64 `json:"jysId" comment:"关联交易所账号id"`
|
||||||
ApiName string `json:"apiName" comment:"api用户名"`
|
ApiName string `json:"apiName" comment:"api用户名"`
|
||||||
ApiKey string `json:"apiKey" comment:"apiKey"`
|
ApiKey string `json:"apiKey" comment:"apiKey"`
|
||||||
ApiSecret string `json:"apiSecret" comment:"apiSecret"`
|
ApiSecret string `json:"apiSecret" comment:"apiSecret"`
|
||||||
IpAddress string `json:"ipAddress" comment:"代理地址"`
|
IpAddress string `json:"ipAddress" comment:"代理地址"`
|
||||||
UserPass string `json:"userPass" comment:"代码账号密码"`
|
UserPass string `json:"userPass" comment:"代码账号密码"`
|
||||||
AdminId int64 `json:"adminId" comment:"管理员id"`
|
Affiliation int64 `json:"affiliation" comment:"归属:1=现货,2=合约,3=现货合约"`
|
||||||
Affiliation int64 `json:"affiliation" comment:"归属:1=现货,2=合约,3=现货合约"`
|
AdminShow int64 `json:"adminShow" comment:"是否超管可见:1=是,0=否"`
|
||||||
AdminShow int64 `json:"adminShow" comment:"是否超管可见:1=是,0=否"`
|
Site string `json:"site" comment:"允许下单的方向:1=多;2=空;3=多空"`
|
||||||
Site string `json:"site" comment:"允许下单的方向:1=多;2=空;3=多空"`
|
Subordinate string `json:"subordinate" comment:"从属关系:0=未绑定关系;1=主账号;2=副帐号"`
|
||||||
Subordinate string `json:"subordinate" comment:"从属关系:0=未绑定关系;1=主账号;2=副帐号"`
|
GroupId int64 `json:"groupId" comment:"所属组id"`
|
||||||
GroupId int64 `json:"groupId" comment:"所属组id"`
|
OpenStatus int64 `json:"openStatus" comment:"开启状态 0=关闭 1=开启"`
|
||||||
OpenStatus int64 `json:"openStatus" comment:"开启状态 0=关闭 1=开启"`
|
ReverseStatus int `json:"reverseStatus" comment:"反向下单状态 1-开启 2-关闭"`
|
||||||
|
ReverseApiId int `json:"reverseApiId" comment:"反向下单apiId"`
|
||||||
|
OrderProportion decimal.Decimal `json:"orderProportion" comment:"委托比例"`
|
||||||
common.ControlBy
|
common.ControlBy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *LineApiUserUpdateReq) Valid() error {
|
||||||
|
if s.ReverseStatus == 1 {
|
||||||
|
if s.OrderProportion.Cmp(decimal.Zero) <= 0 {
|
||||||
|
return errors.New("委托比例必须大于0")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.ReverseStatus == 1 && s.ReverseApiId == 0 {
|
||||||
|
return errors.New("反向下单api不能为空")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *LineApiUserUpdateReq) Generate(model *models.LineApiUser) {
|
func (s *LineApiUserUpdateReq) Generate(model *models.LineApiUser) {
|
||||||
if s.Id == 0 {
|
if s.Id == 0 {
|
||||||
model.Model = common.Model{Id: s.Id}
|
model.Model = common.Model{Id: s.Id}
|
||||||
}
|
}
|
||||||
model.ExchangeType = s.ExchangeType
|
model.ExchangeType = s.ExchangeType
|
||||||
model.UserId = s.UserId
|
model.UserId = s.UserId
|
||||||
model.JysId = s.JysId
|
|
||||||
model.ApiName = s.ApiName
|
model.ApiName = s.ApiName
|
||||||
model.ApiKey = s.ApiKey
|
model.ApiKey = s.ApiKey
|
||||||
model.ApiSecret = s.ApiSecret
|
model.ApiSecret = s.ApiSecret
|
||||||
model.IpAddress = s.IpAddress
|
model.IpAddress = s.IpAddress
|
||||||
model.UserPass = s.UserPass
|
model.UserPass = s.UserPass
|
||||||
model.AdminId = s.AdminId
|
|
||||||
model.Affiliation = s.Affiliation
|
model.Affiliation = s.Affiliation
|
||||||
model.AdminShow = s.AdminShow
|
model.AdminShow = s.AdminShow
|
||||||
model.Site = s.Site
|
model.Site = s.Site
|
||||||
model.Subordinate = s.Subordinate
|
model.Subordinate = s.Subordinate
|
||||||
model.GroupId = s.GroupId
|
model.GroupId = s.GroupId
|
||||||
model.OpenStatus = s.OpenStatus
|
model.OpenStatus = s.OpenStatus
|
||||||
|
model.ReverseStatus = s.ReverseStatus
|
||||||
|
model.OrderProportion = s.OrderProportion
|
||||||
|
|
||||||
|
if model.ReverseStatus == 1 {
|
||||||
|
model.ReverseApiId = s.ReverseApiId
|
||||||
|
} else {
|
||||||
|
model.ReverseApiId = 0
|
||||||
|
}
|
||||||
|
|
||||||
model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的
|
model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,3 +206,26 @@ type LineApiUserBindSubordinateReq struct {
|
|||||||
type GetMainUserReq struct {
|
type GetMainUserReq struct {
|
||||||
ExchangeType string `json:"exchangeType"`
|
ExchangeType string `json:"exchangeType"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GetUnBindReverseReq struct {
|
||||||
|
ExchangeType string `json:"exchangeType" form:"exchangeType"`
|
||||||
|
ApiId int `json:"apiId" form:"apiId" comment:"当前apiId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未绑定反向下单查询返回结构体
|
||||||
|
type UnBindReverseResp struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
UserId int64 `json:"userId"`
|
||||||
|
Disabled bool `json:"disabled"`
|
||||||
|
ApiName string `json:"apiName"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetReverseApiOptionsReq struct {
|
||||||
|
Id int `json:"apiId" form:"id"`
|
||||||
|
ApiId int `json:"apiId" form:"apiId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineApiUserOptionResp struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|||||||
162
app/admin/service/dto/line_reverse_order.go
Normal file
162
app/admin/service/dto/line_reverse_order.go
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go-admin/app/admin/models"
|
||||||
|
"go-admin/common/dto"
|
||||||
|
common "go-admin/common/models"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LineReverseOrderGetPageReq struct {
|
||||||
|
dto.Pagination `search:"-"`
|
||||||
|
OrderSn string `form:"orderSn" search:"type:contains;column:order_sn;table:line_reverse_order" comment:"订单号"`
|
||||||
|
OrderId string `form:"orderId" search:"type:contains;column:order_id;table:line_reverse_order" comment:"币安订单号"`
|
||||||
|
FollowOrderSn string `form:"followOrderSn" search:"type:contains;column:follow_order_sn;table:line_reverse_order" comment:"跟随币安订单号"`
|
||||||
|
OrderType int `form:"orderType" search:"type:exact;column:order_type;table:line_reverse_order" comment:"订单类型 0-主单 1-止损单 2-加仓 3-减仓"`
|
||||||
|
PositionSide string `form:"positionSide" search:"type:exact;column:position_side;table:line_reverse_order" comment:"持仓方向 LONG-多 SHORT-空"`
|
||||||
|
Side string `form:"side" search:"type:exact;column:side;table:line_reverse_order" comment:"买卖方向 SELL-卖 BUY-买"`
|
||||||
|
Type string `form:"type" search:"type:exact;column:type;table:line_reverse_order" comment:"类型 LIMIT-限价 MARKET-市价 "`
|
||||||
|
Category int `form:"category" search:"-" comment:"类型 0-主单 1-反单"`
|
||||||
|
Status int `form:"status" search:"type:exact;column:status;table:line_reverse_order" comment:"状态 1-待下单 2-已下单 3-已成交 4-已平仓 5-已止损"`
|
||||||
|
PositionId int `form:"positionId" search:"type:exact;column:position_id;table:line_reverse_order" comment:"持仓id"`
|
||||||
|
LineReverseOrderOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineReverseOrderOrder struct {
|
||||||
|
Id string `form:"idOrder" search:"type:order;column:id;table:line_reverse_order"`
|
||||||
|
PId string `form:"pIdOrder" search:"type:order;column:p_id;table:line_reverse_order"`
|
||||||
|
OrderSn string `form:"orderSnOrder" search:"type:order;column:order_sn;table:line_reverse_order"`
|
||||||
|
OrderId string `form:"orderIdOrder" search:"type:order;column:order_id;table:line_reverse_order"`
|
||||||
|
FollowOrderSn string `form:"followOrderSnOrder" search:"type:order;column:follow_order_sn;table:line_reverse_order"`
|
||||||
|
Symbol string `form:"symbolOrder" search:"type:order;column:symbol;table:line_reverse_order"`
|
||||||
|
OrderType string `form:"orderTypeOrder" search:"type:order;column:order_type;table:line_reverse_order"`
|
||||||
|
BuyPrice string `form:"buyPriceOrder" search:"type:order;column:buy_price;table:line_reverse_order"`
|
||||||
|
Price string `form:"priceOrder" search:"type:order;column:price;table:line_reverse_order"`
|
||||||
|
PriceU string `form:"priceUOrder" search:"type:order;column:price_u;table:line_reverse_order"`
|
||||||
|
FinalPrice string `form:"finalPriceOrder" search:"type:order;column:final_price;table:line_reverse_order"`
|
||||||
|
PositionSide string `form:"positionSideOrder" search:"type:order;column:position_side;table:line_reverse_order"`
|
||||||
|
Side string `form:"sideOrder" search:"type:order;column:side;table:line_reverse_order"`
|
||||||
|
SignPrice string `form:"signPriceOrder" search:"type:order;column:sign_price;table:line_reverse_order"`
|
||||||
|
TriggerTime string `form:"triggerTimeOrder" search:"type:order;column:trigger_time;table:line_reverse_order"`
|
||||||
|
Status string `form:"statusOrder" search:"type:order;column:status;table:line_reverse_order"`
|
||||||
|
CreatedAt string `form:"createdAtOrder" search:"type:order;column:created_at;table:line_reverse_order"`
|
||||||
|
UpdatedAt string `form:"updatedAtOrder" search:"type:order;column:updated_at;table:line_reverse_order"`
|
||||||
|
DeletedAt string `form:"deletedAtOrder" search:"type:order;column:deleted_at;table:line_reverse_order"`
|
||||||
|
CreateBy string `form:"createByOrder" search:"type:order;column:create_by;table:line_reverse_order"`
|
||||||
|
UpdateBy string `form:"updateByOrder" search:"type:order;column:update_by;table:line_reverse_order"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *LineReverseOrderGetPageReq) GetNeedSearch() interface{} {
|
||||||
|
return *m
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineReverseOrderInsertReq struct {
|
||||||
|
Id int `json:"-" comment:"主键id"` // 主键id
|
||||||
|
PositionId int `json:"positionId" comment:"仓位id"`
|
||||||
|
OrderSn string `json:"orderSn" comment:"订单号"`
|
||||||
|
OrderId string `json:"orderId" comment:"币安订单号"`
|
||||||
|
FollowOrderSn string `json:"followOrderSn" comment:"跟随币安订单号"`
|
||||||
|
Symbol string `json:"symbol" comment:"交易对"`
|
||||||
|
OrderType int `json:"orderType" comment:"订单类型 0-主单 1-止损单 2-加仓 3-减仓"`
|
||||||
|
BuyPrice decimal.Decimal `json:"buyPrice" comment:"购买金额"`
|
||||||
|
Price decimal.Decimal `json:"price" comment:"委托价格"`
|
||||||
|
PriceU decimal.Decimal `json:"priceU" comment:"委托价格(U)"`
|
||||||
|
FinalPrice decimal.Decimal `json:"finalPrice" comment:"实际成交价"`
|
||||||
|
PositionSide string `json:"positionSide" comment:"持仓方向 LONG-多 SHORT-空"`
|
||||||
|
Side string `json:"side" comment:"买卖方向 SELL-卖 BUY-买"`
|
||||||
|
SignPrice decimal.Decimal `json:"signPrice" comment:"行情价"`
|
||||||
|
TriggerTime time.Time `json:"triggerTime" comment:"触发时间"`
|
||||||
|
Status int `json:"status" comment:"状态 1-待下单 2-已下单 3-已成交 4-已平仓 5-已止损"`
|
||||||
|
common.ControlBy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineReverseOrderInsertReq) Generate(model *models.LineReverseOrder) {
|
||||||
|
if s.Id == 0 {
|
||||||
|
model.Model = common.Model{Id: s.Id}
|
||||||
|
}
|
||||||
|
model.OrderSn = s.OrderSn
|
||||||
|
model.OrderId = s.OrderId
|
||||||
|
model.FollowOrderSn = s.FollowOrderSn
|
||||||
|
model.Symbol = s.Symbol
|
||||||
|
model.OrderType = s.OrderType
|
||||||
|
model.BuyPrice = s.BuyPrice
|
||||||
|
model.Price = s.Price
|
||||||
|
model.PriceU = s.PriceU
|
||||||
|
model.FinalPrice = s.FinalPrice
|
||||||
|
model.PositionSide = s.PositionSide
|
||||||
|
model.Side = s.Side
|
||||||
|
model.SignPrice = s.SignPrice
|
||||||
|
model.TriggerTime = &s.TriggerTime
|
||||||
|
model.Status = s.Status
|
||||||
|
model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineReverseOrderInsertReq) GetId() interface{} {
|
||||||
|
return s.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineReverseOrderUpdateReq struct {
|
||||||
|
Id int `uri:"id" comment:"主键id"` // 主键id
|
||||||
|
PositionId int `json:"positionId" comment:"仓位id"`
|
||||||
|
OrderSn string `json:"orderSn" comment:"订单号"`
|
||||||
|
OrderId string `json:"orderId" comment:"币安订单号"`
|
||||||
|
FollowOrderSn string `json:"followOrderSn" comment:"跟随币安订单号"`
|
||||||
|
Symbol string `json:"symbol" comment:"交易对"`
|
||||||
|
OrderType int `json:"orderType" comment:"订单类型 0-主单 1-止损单 2-加仓 3-减仓"`
|
||||||
|
BuyPrice decimal.Decimal `json:"buyPrice" comment:"购买金额"`
|
||||||
|
Price decimal.Decimal `json:"price" comment:"委托价格"`
|
||||||
|
PriceU decimal.Decimal `json:"priceU" comment:"委托价格(U)"`
|
||||||
|
FinalPrice decimal.Decimal `json:"finalPrice" comment:"实际成交价"`
|
||||||
|
PositionSide string `json:"positionSide" comment:"持仓方向 LONG-多 SHORT-空"`
|
||||||
|
Side string `json:"side" comment:"买卖方向 SELL-卖 BUY-买"`
|
||||||
|
SignPrice decimal.Decimal `json:"signPrice" comment:"行情价"`
|
||||||
|
TriggerTime time.Time `json:"triggerTime" comment:"触发时间"`
|
||||||
|
Status int `json:"status" comment:"状态 1-待下单 2-已下单 3-已成交 4-已平仓 5-已止损"`
|
||||||
|
common.ControlBy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineReverseOrderUpdateReq) Generate(model *models.LineReverseOrder) {
|
||||||
|
if s.Id == 0 {
|
||||||
|
model.Model = common.Model{Id: s.Id}
|
||||||
|
}
|
||||||
|
model.OrderSn = s.OrderSn
|
||||||
|
model.OrderId = s.OrderId
|
||||||
|
model.FollowOrderSn = s.FollowOrderSn
|
||||||
|
model.Symbol = s.Symbol
|
||||||
|
model.OrderType = s.OrderType
|
||||||
|
model.BuyPrice = s.BuyPrice
|
||||||
|
model.Price = s.Price
|
||||||
|
model.PriceU = s.PriceU
|
||||||
|
model.FinalPrice = s.FinalPrice
|
||||||
|
model.PositionSide = s.PositionSide
|
||||||
|
model.Side = s.Side
|
||||||
|
model.SignPrice = s.SignPrice
|
||||||
|
model.TriggerTime = &s.TriggerTime
|
||||||
|
model.Status = s.Status
|
||||||
|
model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineReverseOrderUpdateReq) GetId() interface{} {
|
||||||
|
return s.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineReverseOrderGetReq 功能获取请求参数
|
||||||
|
type LineReverseOrderGetReq struct {
|
||||||
|
Id int `uri:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineReverseOrderGetReq) GetId() interface{} {
|
||||||
|
return s.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineReverseOrderDeleteReq 功能删除请求参数
|
||||||
|
type LineReverseOrderDeleteReq struct {
|
||||||
|
Ids []int `json:"ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineReverseOrderDeleteReq) GetId() interface{} {
|
||||||
|
return s.Ids
|
||||||
|
}
|
||||||
155
app/admin/service/dto/line_reverse_position.go
Normal file
155
app/admin/service/dto/line_reverse_position.go
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go-admin/app/admin/models"
|
||||||
|
"go-admin/common/dto"
|
||||||
|
common "go-admin/common/models"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LineReversePositionGetPageReq struct {
|
||||||
|
dto.Pagination `search:"-"`
|
||||||
|
ApiId int64 `form:"apiId" search:"type:exact;column:api_id;table:line_reverse_position" comment:"api_id"`
|
||||||
|
ReverseApiId int64 `form:"reverseApiId" search:"type:exact;column:reverse_api_id;table:line_reverse_position" comment:"反单api_id"`
|
||||||
|
Side string `form:"side" search:"type:exact;column:side;table:line_reverse_position" comment:"买卖方向 BUY SELL"`
|
||||||
|
PositionSide string `form:"positionSide" search:"type:exact;column:position_side;table:line_reverse_position" comment:"持仓方向 LONG SHORT"`
|
||||||
|
Symbol string `form:"symbol" search:"type:contains;column:symbol;table:line_reverse_position" comment:"交易对"`
|
||||||
|
LineReversePositionOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineReversePositionOrder struct {
|
||||||
|
Id string `form:"idOrder" search:"type:order;column:id;table:line_reverse_position"`
|
||||||
|
ApiId string `form:"apiIdOrder" search:"type:order;column:api_id;table:line_reverse_position"`
|
||||||
|
ReverseApiId string `form:"reverseApiIdOrder" search:"type:order;column:reverse_api_id;table:line_reverse_position"`
|
||||||
|
ReverseAmount string `form:"reverseAmountOrder" search:"type:order;column:reverse_amount;table:line_reverse_position"`
|
||||||
|
Side string `form:"sideOrder" search:"type:order;column:side;table:line_reverse_position"`
|
||||||
|
PositionSide string `form:"positionSideOrder" search:"type:order;column:position_side;table:line_reverse_position"`
|
||||||
|
Symbol string `form:"symbolOrder" search:"type:order;column:symbol;table:line_reverse_position"`
|
||||||
|
Status string `form:"statusOrder" search:"type:order;column:status;table:line_reverse_position"`
|
||||||
|
CreatedAt string `form:"createdAtOrder" search:"type:order;column:created_at;table:line_reverse_position"`
|
||||||
|
UpdatedAt string `form:"updatedAtOrder" search:"type:order;column:updated_at;table:line_reverse_position"`
|
||||||
|
DeletedAt string `form:"deletedAtOrder" search:"type:order;column:deleted_at;table:line_reverse_position"`
|
||||||
|
CreateBy string `form:"createByOrder" search:"type:order;column:create_by;table:line_reverse_position"`
|
||||||
|
UpdateBy string `form:"updateByOrder" search:"type:order;column:update_by;table:line_reverse_position"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *LineReversePositionGetPageReq) GetNeedSearch() interface{} {
|
||||||
|
return *m
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineReversePositionInsertReq struct {
|
||||||
|
Id int `json:"-" comment:"主键"` // 主键
|
||||||
|
ApiId int `json:"apiId" comment:"api_id"`
|
||||||
|
ReverseApiId int `json:"reverseApiId" comment:"反单api_id"`
|
||||||
|
ReverseAmount decimal.Decimal `json:"reverseAmount" comment:"反单仓位"`
|
||||||
|
Side string `json:"side" comment:"买卖方向 BUY SELL"`
|
||||||
|
PositionSide string `json:"positionSide" comment:"持仓方向 LONG SHORT"`
|
||||||
|
Symbol string `json:"symbol" comment:"交易对"`
|
||||||
|
Status int `json:"status" comment:"仓位状态 1-已开仓 2-已平仓"`
|
||||||
|
common.ControlBy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineReversePositionInsertReq) Generate(model *models.LineReversePosition) {
|
||||||
|
if s.Id == 0 {
|
||||||
|
model.Model = common.Model{Id: s.Id}
|
||||||
|
}
|
||||||
|
model.ApiId = s.ApiId
|
||||||
|
model.ReverseApiId = s.ReverseApiId
|
||||||
|
model.ReverseAmount = s.ReverseAmount
|
||||||
|
model.Side = s.Side
|
||||||
|
model.PositionSide = s.PositionSide
|
||||||
|
model.Symbol = s.Symbol
|
||||||
|
model.Status = s.Status
|
||||||
|
model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineReversePositionInsertReq) GetId() interface{} {
|
||||||
|
return s.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineReversePositionUpdateReq struct {
|
||||||
|
Id int `uri:"id" comment:"主键"` // 主键
|
||||||
|
ApiId int `json:"apiId" comment:"api_id"`
|
||||||
|
ReverseApiId int `json:"reverseApiId" comment:"反单api_id"`
|
||||||
|
ReverseAmount decimal.Decimal `json:"reverseAmount" comment:"反单仓位"`
|
||||||
|
Side string `json:"side" comment:"买卖方向 BUY SELL"`
|
||||||
|
PositionSide string `json:"positionSide" comment:"持仓方向 LONG SHORT"`
|
||||||
|
Symbol string `json:"symbol" comment:"交易对"`
|
||||||
|
Status int `json:"status" comment:"仓位状态 1-已开仓 2-已平仓"`
|
||||||
|
common.ControlBy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineReversePositionUpdateReq) Generate(model *models.LineReversePosition) {
|
||||||
|
if s.Id == 0 {
|
||||||
|
model.Model = common.Model{Id: s.Id}
|
||||||
|
}
|
||||||
|
model.ApiId = s.ApiId
|
||||||
|
model.ReverseApiId = s.ReverseApiId
|
||||||
|
model.ReverseAmount = s.ReverseAmount
|
||||||
|
model.Side = s.Side
|
||||||
|
model.PositionSide = s.PositionSide
|
||||||
|
model.Symbol = s.Symbol
|
||||||
|
model.Status = s.Status
|
||||||
|
model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineReversePositionUpdateReq) GetId() interface{} {
|
||||||
|
return s.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineReversePositionGetReq 功能获取请求参数
|
||||||
|
type LineReversePositionGetReq struct {
|
||||||
|
Id int `uri:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineReversePositionGetReq) GetId() interface{} {
|
||||||
|
return s.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineReversePositionDeleteReq 功能删除请求参数
|
||||||
|
type LineReversePositionDeleteReq struct {
|
||||||
|
Ids []int `json:"ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineReversePositionDeleteReq) GetId() interface{} {
|
||||||
|
return s.Ids
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineReversePositionListResp struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
ApiName string `json:"apiName"`
|
||||||
|
ReverseApiName string `json:"reverseApiName"`
|
||||||
|
Amount decimal.Decimal `json:"amount"`
|
||||||
|
TotalAmount decimal.Decimal `json:"totalAmount"`
|
||||||
|
ReverseAmount decimal.Decimal `json:"reverseAmount"`
|
||||||
|
TotalReverseAmount decimal.Decimal `json:"totalReverseAmount"`
|
||||||
|
Side string `json:"side"`
|
||||||
|
PositionSide string `json:"positionSide"`
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
ReverseStatus int `json:"reverseStatus"`
|
||||||
|
AveragePrice decimal.Decimal `json:"averagePrice"`
|
||||||
|
ReverseAveragePrice decimal.Decimal `json:"reverseAveragePrice"`
|
||||||
|
CreatedAt string `json:"createdAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineReversePositionCloseReq struct {
|
||||||
|
PositionId int `uri:"id" form:"id" comment:"仓位id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineReversePositionCloseBatchReq struct {
|
||||||
|
PositionSide string `json:"positionSide"`
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
ReverseApiIds []int `json:"reverseApiIds" form:"reverseApiIds" comment:"反单api_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetPositionSymbolReq struct {
|
||||||
|
ReverseApiId int `form:"reverseApiId" comment:"反单api_id"`
|
||||||
|
PositionSide string `form:"positionSide" comment:"持仓方向 LONG SHORT"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PositionSymbolResp struct {
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
}
|
||||||
95
app/admin/service/dto/line_reverse_setting.go
Normal file
95
app/admin/service/dto/line_reverse_setting.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go-admin/app/admin/models"
|
||||||
|
"go-admin/common/dto"
|
||||||
|
common "go-admin/common/models"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LineReverseSettingGetPageReq struct {
|
||||||
|
dto.Pagination `search:"-"`
|
||||||
|
LineReverseSettingOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineReverseSettingOrder struct {
|
||||||
|
Id string `form:"idOrder" search:"type:order;column:id;table:line_reverse_setting"`
|
||||||
|
ReverseOrderType string `form:"reverseOrderTypeOrder" search:"type:order;column:reverse_order_type;table:line_reverse_setting"`
|
||||||
|
ReversePremiumRatio string `form:"reversePremiumRatioOrder" search:"type:order;column:reverse_premium_ratio;table:line_reverse_setting"`
|
||||||
|
CreatedAt string `form:"createdAtOrder" search:"type:order;column:created_at;table:line_reverse_setting"`
|
||||||
|
UpdatedAt string `form:"updatedAtOrder" search:"type:order;column:updated_at;table:line_reverse_setting"`
|
||||||
|
DeletedAt string `form:"deletedAtOrder" search:"type:order;column:deleted_at;table:line_reverse_setting"`
|
||||||
|
CreateBy string `form:"createByOrder" search:"type:order;column:create_by;table:line_reverse_setting"`
|
||||||
|
UpdateBy string `form:"updateByOrder" search:"type:order;column:update_by;table:line_reverse_setting"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *LineReverseSettingGetPageReq) GetNeedSearch() interface{} {
|
||||||
|
return *m
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineReverseSettingInsertReq struct {
|
||||||
|
Id int `json:"-" comment:"主键id"` // 主键id
|
||||||
|
ReverseOrderType string `json:"reverseOrderType" comment:"反单下单类型 LIMIT-限价 MARKET-市价"`
|
||||||
|
ReversePremiumRatio decimal.Decimal `json:"reversePremiumRatio" comment:"溢价百分比"`
|
||||||
|
TakeProfitRatio decimal.Decimal `json:"takeProfitRatio" comment:"止盈百分比"`
|
||||||
|
StopLossRatio decimal.Decimal `json:"stopLossRatio" comment:"止损百分比"`
|
||||||
|
common.ControlBy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineReverseSettingInsertReq) Generate(model *models.LineReverseSetting) {
|
||||||
|
if s.Id == 0 {
|
||||||
|
model.Model = common.Model{Id: s.Id}
|
||||||
|
}
|
||||||
|
model.ReverseOrderType = s.ReverseOrderType
|
||||||
|
model.ReversePremiumRatio = s.ReversePremiumRatio
|
||||||
|
model.StopLossRatio = s.StopLossRatio
|
||||||
|
model.TakeProfitRatio = s.TakeProfitRatio
|
||||||
|
model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineReverseSettingInsertReq) GetId() interface{} {
|
||||||
|
return s.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineReverseSettingUpdateReq struct {
|
||||||
|
Id int `uri:"id" comment:"主键id"` // 主键id
|
||||||
|
ReverseOrderType string `json:"reverseOrderType" comment:"反单下单类型 LIMIT-限价 MARKET-市价"`
|
||||||
|
ReversePremiumRatio decimal.Decimal `json:"reversePremiumRatio" comment:"溢价百分比"`
|
||||||
|
TakeProfitRatio decimal.Decimal `json:"takeProfitRatio" comment:"止盈百分比"`
|
||||||
|
StopLossRatio decimal.Decimal `json:"stopLossRatio" comment:"止损百分比"`
|
||||||
|
common.ControlBy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineReverseSettingUpdateReq) Generate(model *models.LineReverseSetting) {
|
||||||
|
if s.Id == 0 {
|
||||||
|
model.Model = common.Model{Id: s.Id}
|
||||||
|
}
|
||||||
|
model.ReverseOrderType = s.ReverseOrderType
|
||||||
|
model.ReversePremiumRatio = s.ReversePremiumRatio
|
||||||
|
model.StopLossRatio = s.StopLossRatio
|
||||||
|
model.TakeProfitRatio = s.TakeProfitRatio
|
||||||
|
model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineReverseSettingUpdateReq) GetId() interface{} {
|
||||||
|
return s.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineReverseSettingGetReq 功能获取请求参数
|
||||||
|
type LineReverseSettingGetReq struct {
|
||||||
|
Id int `uri:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineReverseSettingGetReq) GetId() interface{} {
|
||||||
|
return s.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineReverseSettingDeleteReq 功能删除请求参数
|
||||||
|
type LineReverseSettingDeleteReq struct {
|
||||||
|
Ids []int `json:"ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineReverseSettingDeleteReq) GetId() interface{} {
|
||||||
|
return s.Ids
|
||||||
|
}
|
||||||
@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
"github.com/go-admin-team/go-admin-core/logger"
|
"github.com/go-admin-team/go-admin-core/logger"
|
||||||
"github.com/go-admin-team/go-admin-core/sdk/service"
|
"github.com/go-admin-team/go-admin-core/sdk/service"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"go-admin/app/admin/models"
|
"go-admin/app/admin/models"
|
||||||
@ -25,6 +26,43 @@ type LineApiUser struct {
|
|||||||
service.Service
|
service.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取所有启用的反单api用户
|
||||||
|
func (e LineApiUser) GetReverseApiOptionsAll(user *[]dto.LineApiUserOptionResp) error {
|
||||||
|
var data models.LineApiUser
|
||||||
|
var datas []models.LineApiUser
|
||||||
|
|
||||||
|
if err := e.Orm.Model(&data).Where("open_status = 1 AND subordinate = '2'").Find(&datas).Error; err != nil {
|
||||||
|
e.Log.Errorf("LineApiUserService GetReverseApiOptionsAll error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range datas {
|
||||||
|
*user = append(*user, dto.LineApiUserOptionResp{
|
||||||
|
Id: item.Id,
|
||||||
|
Name: item.ApiName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取可以绑定的api列表
|
||||||
|
func (e LineApiUser) GetReverseApiOptions(req *dto.GetReverseApiOptionsReq, user *[]models.LineApiUser) error {
|
||||||
|
query := e.Orm.Model(models.LineApiUser{}).
|
||||||
|
Where("subordinate ='0' and id != ?", req.Id)
|
||||||
|
|
||||||
|
if req.ApiId > 0 {
|
||||||
|
query = query.Or(" id =?", req.ApiId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := query.Find(user).Error; err != nil {
|
||||||
|
e.Log.Errorf("LineApiUserService GetReverseApiOptions error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetPage 获取LineApiUser列表
|
// GetPage 获取LineApiUser列表
|
||||||
func (e *LineApiUser) GetPage(c *dto.LineApiUserGetPageReq, p *actions.DataPermission, list *[]models.LineApiUser, count *int64) error {
|
func (e *LineApiUser) GetPage(c *dto.LineApiUserGetPageReq, p *actions.DataPermission, list *[]models.LineApiUser, count *int64) error {
|
||||||
var err error
|
var err error
|
||||||
@ -97,13 +135,37 @@ func (e *LineApiUser) Insert(c *dto.LineApiUserInsertReq) error {
|
|||||||
var err error
|
var err error
|
||||||
var data models.LineApiUser
|
var data models.LineApiUser
|
||||||
c.Generate(&data)
|
c.Generate(&data)
|
||||||
err = e.Orm.Create(&data).Error
|
err = e.Orm.Transaction(func(tx *gorm.DB) error {
|
||||||
|
if c.ReverseApiId > 0 {
|
||||||
|
var count int64
|
||||||
|
if err2 := tx.Model(models.LineApiUser{}).Where("subordinate <> '0' AND id=?", c.ReverseApiId).Count(&count).Error; err2 != nil {
|
||||||
|
return err2
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
return errors.New("反向api已被使用")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err2 := tx.Model(models.LineApiUser{}).Where("id =?", c.ReverseApiId).Update("subordinate", '2').Error; err2 != nil {
|
||||||
|
return err2
|
||||||
|
}
|
||||||
|
data.Subordinate = "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
if err2 := tx.Create(&data).Error; err2 != nil {
|
||||||
|
return err2
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e.Log.Errorf("LineApiUserService Insert error:%s \r\n", err)
|
e.Log.Errorf("LineApiUserService Insert error:%s \r\n", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
e.saveCache(data)
|
if err2 := e.CacheRelation(); err2 != nil {
|
||||||
|
return err2
|
||||||
|
}
|
||||||
|
|
||||||
val, _ := sonic.MarshalString(&data)
|
val, _ := sonic.MarshalString(&data)
|
||||||
|
|
||||||
if val != "" {
|
if val != "" {
|
||||||
@ -131,7 +193,7 @@ func (e *LineApiUser) restartWebsocket(data models.LineApiUser) {
|
|||||||
fuSocket.Stop()
|
fuSocket.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
e.saveCache(data)
|
e.CacheRelation()
|
||||||
|
|
||||||
OpenUserBinanceWebsocket(data)
|
OpenUserBinanceWebsocket(data)
|
||||||
}
|
}
|
||||||
@ -149,6 +211,23 @@ func (e *LineApiUser) saveCache(data models.LineApiUser) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 缓存关系
|
||||||
|
// cacheAll 是否缓存所有关系
|
||||||
|
func (e *LineApiUser) CacheRelation() error {
|
||||||
|
var datas *[]models.LineApiUser
|
||||||
|
|
||||||
|
if err := e.Orm.Model(&models.LineApiUser{}).Where("open_status = 1").Find(&datas).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, data := range *datas {
|
||||||
|
// cacheStrs = append(cacheStrs, fmt.Sprintf(rediskey.API_USER, data.Id))
|
||||||
|
e.saveCache(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
打开用户websocket订阅
|
打开用户websocket订阅
|
||||||
*/
|
*/
|
||||||
@ -197,6 +276,7 @@ func (e *LineApiUser) Update(c *dto.LineApiUserUpdateReq, p *actions.DataPermiss
|
|||||||
actions.Permission(data.TableName(), p),
|
actions.Permission(data.TableName(), p),
|
||||||
).First(&data, c.GetId())
|
).First(&data, c.GetId())
|
||||||
oldApiKey := data.ApiKey
|
oldApiKey := data.ApiKey
|
||||||
|
oldReverseApiId := data.ReverseApiId
|
||||||
|
|
||||||
c.Generate(&data)
|
c.Generate(&data)
|
||||||
|
|
||||||
@ -204,6 +284,33 @@ func (e *LineApiUser) Update(c *dto.LineApiUserUpdateReq, p *actions.DataPermiss
|
|||||||
|
|
||||||
//事务
|
//事务
|
||||||
err = e.Orm.Transaction(func(tx *gorm.DB) error {
|
err = e.Orm.Transaction(func(tx *gorm.DB) error {
|
||||||
|
if data.ReverseApiId > 0 && data.ReverseApiId != oldReverseApiId {
|
||||||
|
var count int64
|
||||||
|
if err2 := tx.Model(models.LineApiUser{}).Where("subordinate <> '0' AND id=?", c.ReverseApiId).Count(&count).Error; err2 != nil {
|
||||||
|
return err2
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
return errors.New("反向api已被使用")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err2 := tx.Model(models.LineApiUser{}).Where("id =?", c.ReverseApiId).Update("subordinate", "2").Error; err2 != nil {
|
||||||
|
return err2
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Subordinate = "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
if oldReverseApiId > 0 && oldReverseApiId != data.ReverseApiId {
|
||||||
|
if err2 := tx.Model(models.LineApiUser{}).Where("id =?", oldReverseApiId).Update("subordinate", "0").Error; err2 != nil {
|
||||||
|
e.Log.Errorf("解绑反向api失败 api:%d err:%v", oldReverseApiId, err2)
|
||||||
|
return fmt.Errorf("解绑反向api失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.ReverseApiId == 0 {
|
||||||
|
data.Subordinate = "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
db := tx.Save(&data)
|
db := tx.Save(&data)
|
||||||
if err = db.Error; err != nil {
|
if err = db.Error; err != nil {
|
||||||
e.Log.Errorf("LineApiUserService Save error:%s \r\n", err)
|
e.Log.Errorf("LineApiUserService Save error:%s \r\n", err)
|
||||||
@ -226,7 +333,9 @@ func (e *LineApiUser) Update(c *dto.LineApiUserUpdateReq, p *actions.DataPermiss
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
e.saveCache(data)
|
if err2 := e.CacheRelation(); err2 != nil {
|
||||||
|
return err2
|
||||||
|
}
|
||||||
|
|
||||||
//旧key和新的key不一样,则关闭旧的websocket
|
//旧key和新的key不一样,则关闭旧的websocket
|
||||||
if oldApiKey != data.ApiKey {
|
if oldApiKey != data.ApiKey {
|
||||||
@ -289,10 +398,14 @@ func (e *LineApiUser) Remove(d *dto.LineApiUserDeleteReq, p *actions.DataPermiss
|
|||||||
|
|
||||||
_, err := helper.DefaultRedis.BatchDeleteKeys(delKeys)
|
_, err := helper.DefaultRedis.BatchDeleteKeys(delKeys)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil && !errors.Is(err, redis.Nil) {
|
||||||
e.Log.Error("批量删除api_user key失败", err)
|
e.Log.Error("批量删除api_user key失败", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err2 := e.CacheRelation(); err2 != nil {
|
||||||
|
return err2
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -414,3 +527,44 @@ func (e *LineApiUser) GetActiveApis(apiIds []int) ([]int, error) {
|
|||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUnBindApiUser 获取未绑定反向下单的的api用户
|
||||||
|
func (e *LineApiUser) GetUnBindApiUser(req *dto.GetUnBindReverseReq, list *[]dto.UnBindReverseResp) error {
|
||||||
|
var datas []models.LineApiUser
|
||||||
|
var data models.LineApiUser
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
if req.ApiId > 0 {
|
||||||
|
e.Orm.Model(data).Where("id =? AND subordinate = '2'", req.ApiId).Count(&count)
|
||||||
|
|
||||||
|
if count > 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query := e.Orm.Model(&data).
|
||||||
|
Where("open_status =1 AND exchange_type = ? ", req.ExchangeType)
|
||||||
|
|
||||||
|
if err := query.
|
||||||
|
Find(&datas).Error; err != nil {
|
||||||
|
e.Log.Error("获取未绑定用户失败:", err)
|
||||||
|
return fmt.Errorf("获取未绑定用户失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range datas {
|
||||||
|
if item.Id == req.ApiId {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
listItem := dto.UnBindReverseResp{
|
||||||
|
Id: item.Id,
|
||||||
|
UserId: item.UserId,
|
||||||
|
ApiName: item.ApiName,
|
||||||
|
Disabled: item.Subordinate != "0",
|
||||||
|
}
|
||||||
|
|
||||||
|
(*list) = append((*list), listItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
114
app/admin/service/line_reverse_order.go
Normal file
114
app/admin/service/line_reverse_order.go
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk/service"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"go-admin/app/admin/models"
|
||||||
|
"go-admin/app/admin/service/dto"
|
||||||
|
"go-admin/common/actions"
|
||||||
|
cDto "go-admin/common/dto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LineReverseOrder struct {
|
||||||
|
service.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPage 获取LineReverseOrder列表
|
||||||
|
func (e *LineReverseOrder) GetPage(c *dto.LineReverseOrderGetPageReq, p *actions.DataPermission, list *[]models.LineReverseOrder, count *int64) error {
|
||||||
|
var err error
|
||||||
|
var data models.LineReverseOrder
|
||||||
|
query := e.Orm.Model(&data).
|
||||||
|
Scopes(
|
||||||
|
cDto.MakeCondition(c.GetNeedSearch()),
|
||||||
|
cDto.Paginate(c.GetPageSize(), c.GetPageIndex()),
|
||||||
|
actions.Permission(data.TableName(), p),
|
||||||
|
)
|
||||||
|
|
||||||
|
if c.Category >= 0 {
|
||||||
|
query = query.Where("category =?", c.Category)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = query.
|
||||||
|
Find(list).Limit(-1).Offset(-1).
|
||||||
|
Count(count).Error
|
||||||
|
if err != nil {
|
||||||
|
e.Log.Errorf("LineReverseOrderService GetPage error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 获取LineReverseOrder对象
|
||||||
|
func (e *LineReverseOrder) Get(d *dto.LineReverseOrderGetReq, p *actions.DataPermission, model *models.LineReverseOrder) error {
|
||||||
|
var data models.LineReverseOrder
|
||||||
|
|
||||||
|
err := e.Orm.Model(&data).
|
||||||
|
Scopes(
|
||||||
|
actions.Permission(data.TableName(), p),
|
||||||
|
).
|
||||||
|
First(model, d.GetId()).Error
|
||||||
|
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
err = errors.New("查看对象不存在或无权查看")
|
||||||
|
e.Log.Errorf("Service GetLineReverseOrder error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
e.Log.Errorf("db error:%s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert 创建LineReverseOrder对象
|
||||||
|
func (e *LineReverseOrder) Insert(c *dto.LineReverseOrderInsertReq) error {
|
||||||
|
var err error
|
||||||
|
var data models.LineReverseOrder
|
||||||
|
c.Generate(&data)
|
||||||
|
err = e.Orm.Create(&data).Error
|
||||||
|
if err != nil {
|
||||||
|
e.Log.Errorf("LineReverseOrderService Insert error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 修改LineReverseOrder对象
|
||||||
|
func (e *LineReverseOrder) Update(c *dto.LineReverseOrderUpdateReq, p *actions.DataPermission) error {
|
||||||
|
var err error
|
||||||
|
var data = models.LineReverseOrder{}
|
||||||
|
e.Orm.Scopes(
|
||||||
|
actions.Permission(data.TableName(), p),
|
||||||
|
).First(&data, c.GetId())
|
||||||
|
c.Generate(&data)
|
||||||
|
|
||||||
|
db := e.Orm.Save(&data)
|
||||||
|
if err = db.Error; err != nil {
|
||||||
|
e.Log.Errorf("LineReverseOrderService Save error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if db.RowsAffected == 0 {
|
||||||
|
return errors.New("无权更新该数据")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove 删除LineReverseOrder
|
||||||
|
func (e *LineReverseOrder) Remove(d *dto.LineReverseOrderDeleteReq, p *actions.DataPermission) error {
|
||||||
|
var data models.LineReverseOrder
|
||||||
|
|
||||||
|
db := e.Orm.Model(&data).
|
||||||
|
Scopes(
|
||||||
|
actions.Permission(data.TableName(), p),
|
||||||
|
).Delete(&data, d.GetId())
|
||||||
|
if err := db.Error; err != nil {
|
||||||
|
e.Log.Errorf("Service RemoveLineReverseOrder error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if db.RowsAffected == 0 {
|
||||||
|
return errors.New("无权删除该数据")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
345
app/admin/service/line_reverse_position.go
Normal file
345
app/admin/service/line_reverse_position.go
Normal file
@ -0,0 +1,345 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk/service"
|
||||||
|
"github.com/jinzhu/copier"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"go-admin/app/admin/models"
|
||||||
|
"go-admin/app/admin/service/dto"
|
||||||
|
"go-admin/common/actions"
|
||||||
|
cDto "go-admin/common/dto"
|
||||||
|
"go-admin/common/global"
|
||||||
|
"go-admin/pkg/utility"
|
||||||
|
"go-admin/pkg/utility/snowflakehelper"
|
||||||
|
"go-admin/services/binanceservice"
|
||||||
|
"go-admin/services/cacheservice"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LineReversePosition struct {
|
||||||
|
service.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除仓位记录
|
||||||
|
func (e LineReversePosition) CleanAll(p *actions.DataPermission, userId int) error {
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
if err := e.Orm.Model(&models.LineReversePosition{}).
|
||||||
|
Where("status =1 or reverse_status = 1").Count(&count).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if count > 0 {
|
||||||
|
e.Log.Errorf("还有仓位无法清除")
|
||||||
|
return errors.New("有仓位正在进行中,不能清除所有")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := e.Orm.Exec("TRUNCATE TABLE line_reverse_position").Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量关闭仓位
|
||||||
|
func (e LineReversePosition) ClosePositionBatch(req *dto.LineReversePositionCloseBatchReq, p *actions.DataPermission, userId int, errs *[]string) error {
|
||||||
|
var positions []models.LineReversePosition
|
||||||
|
var entity models.LineReversePosition
|
||||||
|
query := e.Orm.Model(&entity).
|
||||||
|
Scopes(
|
||||||
|
actions.Permission(entity.TableName(), p),
|
||||||
|
).
|
||||||
|
Where("reverse_status = 1 and position_side =?", req.PositionSide)
|
||||||
|
|
||||||
|
if len(req.Symbol) > 0 {
|
||||||
|
query = query.Where("symbol =?", req.Symbol)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.ReverseApiIds) > 0 {
|
||||||
|
query = query.Where("reverse_api_id in (?)", req.ReverseApiIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := query.Find(&positions).Error; err != nil {
|
||||||
|
e.Log.Errorf("LineReversePositionService ClosePositionBatch error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(positions) == 0 {
|
||||||
|
return errors.New("没有需要关闭的仓位")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, position := range positions {
|
||||||
|
if err1 := e.Close(position); err1 != nil {
|
||||||
|
*errs = append(*errs, err1.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClosePosition 关闭单个仓位
|
||||||
|
func (e LineReversePosition) ClosePosition(req *dto.LineReversePositionCloseReq, p *actions.DataPermission, userId int) error {
|
||||||
|
var data models.LineReversePosition
|
||||||
|
err := e.Orm.Model(&data).
|
||||||
|
Scopes(
|
||||||
|
actions.Permission(data.TableName(), p),
|
||||||
|
).
|
||||||
|
Where("id =?", req.PositionId).First(&data).Error
|
||||||
|
if err != nil {
|
||||||
|
e.Log.Errorf("LineReversePositionService ClosePosition error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = e.Close(data)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *LineReversePosition) Close(data models.LineReversePosition) error {
|
||||||
|
if data.ReverseStatus != 1 {
|
||||||
|
return fmt.Errorf("%s-%s 仓位无法关闭", data.Symbol, data.PositionSide)
|
||||||
|
}
|
||||||
|
|
||||||
|
apiInfo, err := binanceservice.GetApiInfo(data.ReverseApiId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("api %d不存在", data.ReverseApiId)
|
||||||
|
}
|
||||||
|
futApi := binanceservice.FutRestApi{}
|
||||||
|
futApiV2 := binanceservice.FuturesResetV2{}
|
||||||
|
holdData := binanceservice.HoldeData{}
|
||||||
|
err = futApi.GetPositionData(&apiInfo, data.Symbol, data.PositionSide, &holdData)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("获取币安持仓失败 %v", err)
|
||||||
|
}
|
||||||
|
setting, err := cacheservice.GetReverseSetting(e.Orm)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("获取反单设置失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
symbol, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, data.Symbol, 0)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("获取%s的交易对信息失败", data.Symbol)
|
||||||
|
}
|
||||||
|
|
||||||
|
lastPrice, err := decimal.NewFromString(symbol.LastPrice)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("最新价格失败,%v", lastPrice)
|
||||||
|
}
|
||||||
|
side := ""
|
||||||
|
var price decimal.Decimal
|
||||||
|
|
||||||
|
if data.PositionSide == "LONG" {
|
||||||
|
side = "SELL"
|
||||||
|
price = decimal.NewFromInt(100).Sub(setting.ReversePremiumRatio).Div(decimal.NewFromInt(100)).Mul(lastPrice).Round(int32(symbol.PriceDigit))
|
||||||
|
} else {
|
||||||
|
side = "BUY"
|
||||||
|
price = decimal.NewFromInt(100).Add(setting.ReversePremiumRatio).Div(decimal.NewFromInt(100)).Mul(lastPrice).Round(int32(symbol.PriceDigit))
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
order := models.LineReverseOrder{
|
||||||
|
OrderSn: snowflakehelper.GetOrderNo(),
|
||||||
|
PositionId: data.Id,
|
||||||
|
PositionSide: data.PositionSide,
|
||||||
|
Symbol: data.Symbol,
|
||||||
|
TotalNum: holdData.TotalQuantity,
|
||||||
|
Category: 1,
|
||||||
|
OrderType: 4,
|
||||||
|
Side: side,
|
||||||
|
Price: price,
|
||||||
|
SignPrice: lastPrice,
|
||||||
|
Type: "LIMIT",
|
||||||
|
TriggerTime: &now,
|
||||||
|
Status: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
if holdData.TotalQuantity.Cmp(decimal.Zero) > 0 {
|
||||||
|
if err := e.Orm.Create(&order).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
params := binanceservice.FutOrderPlace{
|
||||||
|
ApiId: data.ReverseApiId,
|
||||||
|
Symbol: data.Symbol,
|
||||||
|
Side: order.Side,
|
||||||
|
PositionSide: order.PositionSide,
|
||||||
|
Quantity: order.TotalNum,
|
||||||
|
Price: order.Price,
|
||||||
|
SideType: order.Type,
|
||||||
|
OpenOrder: 0,
|
||||||
|
OrderType: "LIMIT",
|
||||||
|
NewClientOrderId: order.OrderSn,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := futApiV2.OrderPlaceLoop(&apiInfo, params); err != nil {
|
||||||
|
e.Log.Errorf("币安下单失败 symbol:%s custom:%s :%v", params.Symbol, order.OrderSn, err)
|
||||||
|
if err1 := e.Orm.Model(&order).Where("status = 1").Updates(map[string]interface{}{"status": 8, "remark": err.Error(), "updated_at": time.Now()}).Error; err1 != nil {
|
||||||
|
e.Log.Errorf("更新订单状态失败 symbol:%s custom:%s :%v", params.Symbol, order.OrderSn, err1)
|
||||||
|
return err1
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
order.Status = 8
|
||||||
|
order.Remark = "已经没有持仓"
|
||||||
|
|
||||||
|
err = e.Orm.Transaction(func(tx *gorm.DB) error {
|
||||||
|
if err1 := tx.Create(&order).Error; err1 != nil {
|
||||||
|
return err1
|
||||||
|
}
|
||||||
|
|
||||||
|
if err1 := tx.Model(&data).Where("reverse_status =1").Updates(map[string]interface{}{"reverse_status": 2, "updated_at": time.Now(), "reverse_amount": 0}).Error; err1 != nil {
|
||||||
|
return err1
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
e.Log.Errorf("修改失败 %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPage 获取LineReversePosition列表
|
||||||
|
func (e *LineReversePosition) GetPage(c *dto.LineReversePositionGetPageReq, p *actions.DataPermission, list *[]dto.LineReversePositionListResp, count *int64) error {
|
||||||
|
var err error
|
||||||
|
var data models.LineReversePosition
|
||||||
|
var datas []models.LineReversePosition
|
||||||
|
|
||||||
|
err = e.Orm.Model(&data).
|
||||||
|
Scopes(
|
||||||
|
cDto.MakeCondition(c.GetNeedSearch()),
|
||||||
|
cDto.Paginate(c.GetPageSize(), c.GetPageIndex()),
|
||||||
|
actions.Permission(data.TableName(), p),
|
||||||
|
).
|
||||||
|
Find(&datas).Limit(-1).Offset(-1).
|
||||||
|
Count(count).Error
|
||||||
|
if err != nil {
|
||||||
|
e.Log.Errorf("LineReversePositionService GetPage error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
userIds := make([]int, 0)
|
||||||
|
|
||||||
|
for _, item := range datas {
|
||||||
|
if !utility.ContainsInt(userIds, item.ApiId) {
|
||||||
|
userIds = append(userIds, item.ApiId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !utility.ContainsInt(userIds, item.ReverseApiId) {
|
||||||
|
userIds = append(userIds, item.ReverseApiId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userMap := make(map[int]string, 0)
|
||||||
|
|
||||||
|
if len(userIds) > 0 {
|
||||||
|
var users []models.LineApiUser
|
||||||
|
e.Orm.Model(&models.LineApiUser{}).
|
||||||
|
Where("id IN (?)", userIds).
|
||||||
|
Find(&users)
|
||||||
|
|
||||||
|
for _, item := range users {
|
||||||
|
userMap[int(item.Id)] = item.ApiName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range datas {
|
||||||
|
var resp dto.LineReversePositionListResp
|
||||||
|
copier.Copy(&resp, &item)
|
||||||
|
|
||||||
|
if userName, ok := userMap[item.ApiId]; ok {
|
||||||
|
resp.ApiName = userName
|
||||||
|
}
|
||||||
|
|
||||||
|
if userName, ok := userMap[item.ReverseApiId]; ok {
|
||||||
|
resp.ReverseApiName = userName
|
||||||
|
}
|
||||||
|
|
||||||
|
*list = append(*list, resp)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 获取LineReversePosition对象
|
||||||
|
func (e *LineReversePosition) Get(d *dto.LineReversePositionGetReq, p *actions.DataPermission, model *models.LineReversePosition) error {
|
||||||
|
var data models.LineReversePosition
|
||||||
|
|
||||||
|
err := e.Orm.Model(&data).
|
||||||
|
Scopes(
|
||||||
|
actions.Permission(data.TableName(), p),
|
||||||
|
).
|
||||||
|
First(model, d.GetId()).Error
|
||||||
|
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
err = errors.New("查看对象不存在或无权查看")
|
||||||
|
e.Log.Errorf("Service GetLineReversePosition error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
e.Log.Errorf("db error:%s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert 创建LineReversePosition对象
|
||||||
|
func (e *LineReversePosition) Insert(c *dto.LineReversePositionInsertReq) error {
|
||||||
|
var err error
|
||||||
|
var data models.LineReversePosition
|
||||||
|
c.Generate(&data)
|
||||||
|
err = e.Orm.Create(&data).Error
|
||||||
|
if err != nil {
|
||||||
|
e.Log.Errorf("LineReversePositionService Insert error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 修改LineReversePosition对象
|
||||||
|
func (e *LineReversePosition) Update(c *dto.LineReversePositionUpdateReq, p *actions.DataPermission) error {
|
||||||
|
var err error
|
||||||
|
var data = models.LineReversePosition{}
|
||||||
|
e.Orm.Scopes(
|
||||||
|
actions.Permission(data.TableName(), p),
|
||||||
|
).First(&data, c.GetId())
|
||||||
|
c.Generate(&data)
|
||||||
|
|
||||||
|
db := e.Orm.Save(&data)
|
||||||
|
if err = db.Error; err != nil {
|
||||||
|
e.Log.Errorf("LineReversePositionService Save error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if db.RowsAffected == 0 {
|
||||||
|
return errors.New("无权更新该数据")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove 删除LineReversePosition
|
||||||
|
func (e *LineReversePosition) Remove(d *dto.LineReversePositionDeleteReq, p *actions.DataPermission) error {
|
||||||
|
var data models.LineReversePosition
|
||||||
|
|
||||||
|
db := e.Orm.Model(&data).
|
||||||
|
Scopes(
|
||||||
|
actions.Permission(data.TableName(), p),
|
||||||
|
).Delete(&data, d.GetId())
|
||||||
|
if err := db.Error; err != nil {
|
||||||
|
e.Log.Errorf("Service RemoveLineReversePosition error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if db.RowsAffected == 0 {
|
||||||
|
return errors.New("无权删除该数据")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
136
app/admin/service/line_reverse_setting.go
Normal file
136
app/admin/service/line_reverse_setting.go
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk/service"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"go-admin/app/admin/models"
|
||||||
|
"go-admin/app/admin/service/dto"
|
||||||
|
"go-admin/common/actions"
|
||||||
|
"go-admin/common/const/rediskey"
|
||||||
|
cDto "go-admin/common/dto"
|
||||||
|
"go-admin/common/helper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LineReverseSetting struct {
|
||||||
|
service.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPage 获取LineReverseSetting列表
|
||||||
|
func (e *LineReverseSetting) GetPage(c *dto.LineReverseSettingGetPageReq, p *actions.DataPermission, list *[]models.LineReverseSetting, count *int64) error {
|
||||||
|
var err error
|
||||||
|
var data models.LineReverseSetting
|
||||||
|
|
||||||
|
err = e.Orm.Model(&data).
|
||||||
|
Scopes(
|
||||||
|
cDto.MakeCondition(c.GetNeedSearch()),
|
||||||
|
cDto.Paginate(c.GetPageSize(), c.GetPageIndex()),
|
||||||
|
actions.Permission(data.TableName(), p),
|
||||||
|
).
|
||||||
|
Find(list).Limit(-1).Offset(-1).
|
||||||
|
Count(count).Error
|
||||||
|
if err != nil {
|
||||||
|
e.Log.Errorf("LineReverseSettingService GetPage error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 获取LineReverseSetting对象
|
||||||
|
func (e *LineReverseSetting) Get(d *dto.LineReverseSettingGetReq, p *actions.DataPermission, model *models.LineReverseSetting) error {
|
||||||
|
var data models.LineReverseSetting
|
||||||
|
|
||||||
|
err := e.Orm.Model(&data).
|
||||||
|
Scopes(
|
||||||
|
actions.Permission(data.TableName(), p),
|
||||||
|
).
|
||||||
|
First(model, d.GetId()).Error
|
||||||
|
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
err = errors.New("查看对象不存在或无权查看")
|
||||||
|
e.Log.Errorf("Service GetLineReverseSetting error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
e.Log.Errorf("db error:%s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert 创建LineReverseSetting对象
|
||||||
|
func (e *LineReverseSetting) Insert(c *dto.LineReverseSettingInsertReq) error {
|
||||||
|
var err error
|
||||||
|
var data models.LineReverseSetting
|
||||||
|
c.Generate(&data)
|
||||||
|
err = e.Orm.Create(&data).Error
|
||||||
|
if err != nil {
|
||||||
|
e.Log.Errorf("LineReverseSettingService Insert error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = e.SaveCache(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存数据
|
||||||
|
func (e *LineReverseSetting) SaveCache(data models.LineReverseSetting) error {
|
||||||
|
if data.Id == 0 {
|
||||||
|
if err := e.Orm.First(&data).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := helper.DefaultRedis.SetHashWithTags(rediskey.ReverseSetting, &data); err != nil {
|
||||||
|
e.Log.Errorf("redis error:%s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 修改LineReverseSetting对象
|
||||||
|
func (e *LineReverseSetting) Update(c *dto.LineReverseSettingUpdateReq, p *actions.DataPermission) error {
|
||||||
|
var err error
|
||||||
|
var data = models.LineReverseSetting{}
|
||||||
|
e.Orm.Scopes(
|
||||||
|
actions.Permission(data.TableName(), p),
|
||||||
|
).First(&data, c.GetId())
|
||||||
|
c.Generate(&data)
|
||||||
|
|
||||||
|
db := e.Orm.Save(&data)
|
||||||
|
if err = db.Error; err != nil {
|
||||||
|
e.Log.Errorf("LineReverseSettingService Save error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if db.RowsAffected == 0 {
|
||||||
|
return errors.New("无权更新该数据")
|
||||||
|
}
|
||||||
|
if err = helper.DefaultRedis.SetHashWithTags(rediskey.ReverseSetting, data); err != nil {
|
||||||
|
e.Log.Errorf("redis error:%s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove 删除LineReverseSetting
|
||||||
|
func (e *LineReverseSetting) Remove(d *dto.LineReverseSettingDeleteReq, p *actions.DataPermission) error {
|
||||||
|
var data models.LineReverseSetting
|
||||||
|
|
||||||
|
db := e.Orm.Model(&data).
|
||||||
|
Scopes(
|
||||||
|
actions.Permission(data.TableName(), p),
|
||||||
|
).Delete(&data, d.GetId())
|
||||||
|
if err := db.Error; err != nil {
|
||||||
|
e.Log.Errorf("Service RemoveLineReverseSetting error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if db.RowsAffected == 0 {
|
||||||
|
return errors.New("无权删除该数据")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -3,7 +3,7 @@ package service
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/go-admin-team/go-admin-core/sdk/service"
|
"github.com/go-admin-team/go-admin-core/sdk/service"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"go-admin/app/admin/models"
|
"go-admin/app/admin/models"
|
||||||
@ -59,9 +59,9 @@ func (e *LineUserSetting) Get(d *dto.LineUserSettingGetReq, p *actions.DataPermi
|
|||||||
|
|
||||||
// Insert 创建LineUserSetting对象
|
// Insert 创建LineUserSetting对象
|
||||||
func (e *LineUserSetting) Insert(c *dto.LineUserSettingInsertReq) error {
|
func (e *LineUserSetting) Insert(c *dto.LineUserSettingInsertReq) error {
|
||||||
var err error
|
var err error
|
||||||
var data models.LineUserSetting
|
var data models.LineUserSetting
|
||||||
c.Generate(&data)
|
c.Generate(&data)
|
||||||
err = e.Orm.Create(&data).Error
|
err = e.Orm.Create(&data).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e.Log.Errorf("LineUserSettingService Insert error:%s \r\n", err)
|
e.Log.Errorf("LineUserSettingService Insert error:%s \r\n", err)
|
||||||
@ -72,22 +72,22 @@ func (e *LineUserSetting) Insert(c *dto.LineUserSettingInsertReq) error {
|
|||||||
|
|
||||||
// Update 修改LineUserSetting对象
|
// Update 修改LineUserSetting对象
|
||||||
func (e *LineUserSetting) Update(c *dto.LineUserSettingUpdateReq, p *actions.DataPermission) error {
|
func (e *LineUserSetting) Update(c *dto.LineUserSettingUpdateReq, p *actions.DataPermission) error {
|
||||||
var err error
|
var err error
|
||||||
var data = models.LineUserSetting{}
|
var data = models.LineUserSetting{}
|
||||||
e.Orm.Scopes(
|
e.Orm.Scopes(
|
||||||
actions.Permission(data.TableName(), p),
|
actions.Permission(data.TableName(), p),
|
||||||
).First(&data, c.GetId())
|
).First(&data, c.GetId())
|
||||||
c.Generate(&data)
|
c.Generate(&data)
|
||||||
|
|
||||||
db := e.Orm.Save(&data)
|
db := e.Orm.Save(&data)
|
||||||
if err = db.Error; err != nil {
|
if err = db.Error; err != nil {
|
||||||
e.Log.Errorf("LineUserSettingService Save error:%s \r\n", err)
|
e.Log.Errorf("LineUserSettingService Save error:%s \r\n", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if db.RowsAffected == 0 {
|
if db.RowsAffected == 0 {
|
||||||
return errors.New("无权更新该数据")
|
return errors.New("无权更新该数据")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove 删除LineUserSetting
|
// Remove 删除LineUserSetting
|
||||||
@ -99,11 +99,23 @@ func (e *LineUserSetting) Remove(d *dto.LineUserSettingDeleteReq, p *actions.Dat
|
|||||||
actions.Permission(data.TableName(), p),
|
actions.Permission(data.TableName(), p),
|
||||||
).Delete(&data, d.GetId())
|
).Delete(&data, d.GetId())
|
||||||
if err := db.Error; err != nil {
|
if err := db.Error; err != nil {
|
||||||
e.Log.Errorf("Service RemoveLineUserSetting error:%s \r\n", err)
|
e.Log.Errorf("Service RemoveLineUserSetting error:%s \r\n", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if db.RowsAffected == 0 {
|
if db.RowsAffected == 0 {
|
||||||
return errors.New("无权删除该数据")
|
return errors.New("无权删除该数据")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDefault 获取默认LineUserSetting对象
|
||||||
|
func (e *LineUserSetting) GetDefault() (models.LineUserSetting, error) {
|
||||||
|
var data models.LineUserSetting
|
||||||
|
|
||||||
|
if err := e.Orm.Model(&data).First(&data).Error; err != nil {
|
||||||
|
e.Log.Errorf("GetDefault LineUserSetting error:%s \r\n", err)
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -86,6 +86,11 @@ func run() error {
|
|||||||
clearLogJob(db, ctx)
|
clearLogJob(db, ctx)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
//自动重启websocket
|
||||||
|
utility.SafeGo(func() {
|
||||||
|
reconnect(ctx)
|
||||||
|
})
|
||||||
|
|
||||||
// 等待中断信号以优雅地关闭服务器(设置 5 秒的超时时间)
|
// 等待中断信号以优雅地关闭服务器(设置 5 秒的超时时间)
|
||||||
quit := make(chan os.Signal, 1)
|
quit := make(chan os.Signal, 1)
|
||||||
signal.Notify(quit, os.Interrupt)
|
signal.Notify(quit, os.Interrupt)
|
||||||
@ -133,3 +138,33 @@ func clearLogJob(db *gorm.DB, ctx context.Context) {
|
|||||||
fileservice.ClearLogs(db)
|
fileservice.ClearLogs(db)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 定时重连websocket
|
||||||
|
func reconnect(ctx context.Context) error {
|
||||||
|
ticker := time.NewTicker(time.Hour * 1)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
case <-ticker.C:
|
||||||
|
serverinit.RestartConnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartDeadCheck 假死检测
|
||||||
|
func StartDeadCheck(ctx context.Context) {
|
||||||
|
ticker := time.NewTicker(1 * time.Minute)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
serverinit.DeadCheck()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
23
common/const/binancecode/event.go
Normal file
23
common/const/binancecode/event.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package binancecode
|
||||||
|
|
||||||
|
// NEW
|
||||||
|
// CANCELED 已撤
|
||||||
|
// CALCULATED 订单 ADL 或爆仓
|
||||||
|
// EXPIRED 订单失效
|
||||||
|
// TRADE 交易
|
||||||
|
// AMENDMENT 订单修改
|
||||||
|
|
||||||
|
const (
|
||||||
|
//新开单
|
||||||
|
EVENT_NEW = "NEW"
|
||||||
|
//已撤单
|
||||||
|
EVENT_CANCELED = "CANCELED"
|
||||||
|
//订单 ADL 或爆仓
|
||||||
|
EVENT_CALCULATED = "CALCULATED"
|
||||||
|
//订单失效
|
||||||
|
EVENT_EXPIRED = "EXPIRED"
|
||||||
|
//交易中
|
||||||
|
EVENT_TRADE = "TRADE"
|
||||||
|
//订单修改
|
||||||
|
EVENT_AMENDMENT = "AMENDMENT"
|
||||||
|
)
|
||||||
20
common/const/binancecode/order_type.go
Normal file
20
common/const/binancecode/order_type.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package binancecode
|
||||||
|
|
||||||
|
const (
|
||||||
|
//限价单
|
||||||
|
ORDER_TYPE_LIMIT = "LIMIT"
|
||||||
|
//市价单
|
||||||
|
ORDER_TYPE_MARKET = "MARKET"
|
||||||
|
//止损限价单
|
||||||
|
ORDER_TYPE_STOP = "STOP"
|
||||||
|
//止损市价单
|
||||||
|
ORDER_TYPE_STOP_MARKET = "STOP_MARKET"
|
||||||
|
//止盈限价单
|
||||||
|
ORDER_TYPE_TAKE_PROFIT = "TAKE_PROFIT"
|
||||||
|
//止盈市价单
|
||||||
|
ORDER_TYPE_TAKE_PROFIT_MARKET = "TAKE_PROFIT_MARKET"
|
||||||
|
//跟踪止损单
|
||||||
|
ORDER_TYPE_TRAILING_STOP_MARKET = "TRAILING_STOP_MARKET"
|
||||||
|
//爆仓
|
||||||
|
ORDER_TYPE_LIQUIDATION = "LIQUIDATION"
|
||||||
|
)
|
||||||
6
common/const/binancecode/side.go
Normal file
6
common/const/binancecode/side.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package binancecode
|
||||||
|
|
||||||
|
const (
|
||||||
|
SideBuy = "BUY"
|
||||||
|
SideSell = "SELL"
|
||||||
|
)
|
||||||
6
common/const/rediskey/api_user.go
Normal file
6
common/const/rediskey/api_user.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package rediskey
|
||||||
|
|
||||||
|
const (
|
||||||
|
//主单api和反单关系 List {apiId}:{reverseApiId}
|
||||||
|
ApiReverseRelation = "api_reverse_relation"
|
||||||
|
)
|
||||||
11
common/const/rediskey/reverse_position.go
Normal file
11
common/const/rediskey/reverse_position.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package rediskey
|
||||||
|
|
||||||
|
const (
|
||||||
|
//反单仓位管理 {apiKey} hash key
|
||||||
|
ReversePositionKey = "reverse_position:%s"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
//反单配置 hash {order_type} {take_profit_ratio} {stop_loss_ratio} {reverse_premium_ratio}
|
||||||
|
ReverseSetting = "reverse_setting"
|
||||||
|
)
|
||||||
@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
"github.com/go-redis/redis/v8"
|
"github.com/go-redis/redis/v8"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RedisHelper 结构体封装了 Redis 客户端及上下文
|
// RedisHelper 结构体封装了 Redis 客户端及上下文
|
||||||
@ -460,44 +461,76 @@ func (r *RedisHelper) SetNX(key string, value interface{}, expiration time.Durat
|
|||||||
return result == "OK", nil
|
return result == "OK", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getFieldsFromStruct(obj interface{}) map[string]interface{} {
|
// SetHashWithTags 改进版:支持 struct 或 map 输入
|
||||||
|
func (r *RedisHelper) SetHashWithTags(key string, obj interface{}) error {
|
||||||
|
fields, err := getFieldsFromStruct(obj)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = r.client.HSet(r.ctx, key, fields).Result()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// getFieldsFromStruct 改进版:处理指针并进行类型检查,返回 error
|
||||||
|
func getFieldsFromStruct(obj interface{}) (map[string]interface{}, error) {
|
||||||
fields := make(map[string]interface{})
|
fields := make(map[string]interface{})
|
||||||
val := reflect.ValueOf(obj)
|
val := reflect.ValueOf(obj)
|
||||||
typ := reflect.TypeOf(obj)
|
|
||||||
|
// 如果 obj 是指针,则解引用
|
||||||
|
if val.Kind() == reflect.Ptr {
|
||||||
|
val = val.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保我们正在处理的是一个结构体
|
||||||
|
if val.Kind() != reflect.Struct {
|
||||||
|
return nil, fmt.Errorf("期望一个结构体或结构体指针,但得到的是 %s", val.Kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
typ := val.Type() // 在解引用后获取类型
|
||||||
|
|
||||||
for i := 0; i < val.NumField(); i++ {
|
for i := 0; i < val.NumField(); i++ {
|
||||||
field := typ.Field(i)
|
field := typ.Field(i)
|
||||||
tag := field.Tag.Get("redis")
|
tag := field.Tag.Get("redis") // 获取 redis tag
|
||||||
if tag != "" {
|
if tag != "" {
|
||||||
fieldVal := val.Field(i)
|
fieldVal := val.Field(i)
|
||||||
if fieldVal.Kind() == reflect.Slice || fieldVal.Kind() == reflect.Map {
|
// 检查字段是否可导出,不可导出的字段无法直接通过 Interface() 访问
|
||||||
|
if !fieldVal.CanInterface() {
|
||||||
|
continue // 跳过不可导出字段
|
||||||
|
}
|
||||||
|
|
||||||
|
switch fieldVal.Kind() {
|
||||||
|
case reflect.Slice, reflect.Map:
|
||||||
// 处理切片或映射类型
|
// 处理切片或映射类型
|
||||||
// 对于切片,使用索引作为字段名
|
|
||||||
if fieldVal.Kind() == reflect.Slice {
|
if fieldVal.Kind() == reflect.Slice {
|
||||||
for j := 0; j < fieldVal.Len(); j++ {
|
for j := 0; j < fieldVal.Len(); j++ {
|
||||||
elem := fieldVal.Index(j).Interface()
|
elem := fieldVal.Index(j).Interface()
|
||||||
fields[fmt.Sprintf("%s_%d", tag, j)] = elem
|
fields[fmt.Sprintf("%s_%d", tag, j)] = elem
|
||||||
}
|
}
|
||||||
} else if fieldVal.Kind() == reflect.Map {
|
} else if fieldVal.Kind() == reflect.Map {
|
||||||
// 对于映射,使用键作为字段名
|
|
||||||
for _, key := range fieldVal.MapKeys() {
|
for _, key := range fieldVal.MapKeys() {
|
||||||
elem := fieldVal.MapIndex(key).Interface()
|
elem := fieldVal.MapIndex(key).Interface()
|
||||||
fields[fmt.Sprintf("%s_%v", tag, key.Interface())] = elem
|
fields[fmt.Sprintf("%s_%v", tag, key.Interface())] = elem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
case reflect.Struct:
|
||||||
|
if fieldVal.Type() == reflect.TypeOf(decimal.Decimal{}) {
|
||||||
|
if decVal, ok := fieldVal.Interface().(decimal.Decimal); ok {
|
||||||
|
// 将 decimal.Decimal 直接转换为字符串来保留精度
|
||||||
|
fields[tag] = decVal.String()
|
||||||
|
} else {
|
||||||
|
// 理论上不应该发生,但作为回退
|
||||||
|
fields[tag] = fieldVal.Interface()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fields[tag] = fieldVal.Interface()
|
||||||
|
}
|
||||||
|
default:
|
||||||
fields[tag] = fieldVal.Interface()
|
fields[tag] = fieldVal.Interface()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return fields, nil
|
||||||
return fields
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RedisHelper) SetHashWithTags(key string, obj interface{}) error {
|
|
||||||
fields := getFieldsFromStruct(obj)
|
|
||||||
_, err := r.client.HSet(r.ctx, key, fields).Result()
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// HSetField 设置哈希中的一个字段
|
// HSetField 设置哈希中的一个字段
|
||||||
@ -543,6 +576,82 @@ func (r *RedisHelper) HGetAllFields(key string) (map[string]string, error) {
|
|||||||
return fields, nil
|
return fields, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HGetAsObject 获取哈希中所有字段的值并反序列化为对象
|
||||||
|
func (r *RedisHelper) HGetAsObject(key string, out interface{}) error {
|
||||||
|
data, err := r.HGetAllFields(key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
val := reflect.ValueOf(out)
|
||||||
|
if val.Kind() != reflect.Ptr || val.IsNil() {
|
||||||
|
return fmt.Errorf("output must be a non-nil pointer to a struct")
|
||||||
|
}
|
||||||
|
val = val.Elem()
|
||||||
|
typ := val.Type()
|
||||||
|
|
||||||
|
for i := 0; i < typ.NumField(); i++ {
|
||||||
|
field := typ.Field(i)
|
||||||
|
tag := field.Tag.Get("redis")
|
||||||
|
if tag == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
strVal, ok := data[tag]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldVal := val.Field(i)
|
||||||
|
if !fieldVal.CanSet() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch fieldVal.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
fieldVal.SetString(strVal)
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
if intVal, err := strconv.ParseInt(strVal, 10, 64); err == nil {
|
||||||
|
fieldVal.SetInt(intVal)
|
||||||
|
}
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
if uintVal, err := strconv.ParseUint(strVal, 10, 64); err == nil {
|
||||||
|
fieldVal.SetUint(uintVal)
|
||||||
|
}
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
if floatVal, err := strconv.ParseFloat(strVal, 64); err == nil {
|
||||||
|
fieldVal.SetFloat(floatVal)
|
||||||
|
}
|
||||||
|
case reflect.Bool:
|
||||||
|
if boolVal, err := strconv.ParseBool(strVal); err == nil {
|
||||||
|
fieldVal.SetBool(boolVal)
|
||||||
|
}
|
||||||
|
case reflect.Struct:
|
||||||
|
// 针对 time.Time 特别处理
|
||||||
|
if fieldVal.Type() == reflect.TypeOf(time.Time{}) {
|
||||||
|
if t, err := time.Parse(time.RFC3339, strVal); err == nil {
|
||||||
|
fieldVal.Set(reflect.ValueOf(t))
|
||||||
|
}
|
||||||
|
} else if fieldVal.Type() == reflect.TypeOf(decimal.Decimal{}) {
|
||||||
|
if t, err := decimal.NewFromString(strVal); err == nil {
|
||||||
|
fieldVal.Set(reflect.ValueOf(t))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 其他 struct 尝试用 json 反序列化
|
||||||
|
ptr := reflect.New(fieldVal.Type()).Interface()
|
||||||
|
if err := sonic.Unmarshal([]byte(strVal), ptr); err == nil {
|
||||||
|
fieldVal.Set(reflect.ValueOf(ptr).Elem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case reflect.Slice, reflect.Map:
|
||||||
|
ptr := reflect.New(fieldVal.Type()).Interface()
|
||||||
|
if err := sonic.Unmarshal([]byte(strVal), ptr); err == nil {
|
||||||
|
fieldVal.Set(reflect.ValueOf(ptr).Elem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// HDelField 删除哈希中的某个字段
|
// HDelField 删除哈希中的某个字段
|
||||||
func (r *RedisHelper) HDelField(key, field string) error {
|
func (r *RedisHelper) HDelField(key, field string) error {
|
||||||
_, err := r.client.HDel(r.ctx, key, field).Result()
|
_, err := r.client.HDel(r.ctx, key, field).Result()
|
||||||
|
|||||||
@ -30,6 +30,24 @@ func BusinessInit(db *gorm.DB) {
|
|||||||
os.Exit(-1)
|
os.Exit(-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lineApiService := service.LineApiUser{}
|
||||||
|
lineApiService.Orm = db
|
||||||
|
lineApiService.Log = logger.NewHelper(sdk.Runtime.GetLogger()).WithFields(map[string]interface{}{})
|
||||||
|
|
||||||
|
if err := lineApiService.CacheRelation(); err != nil {
|
||||||
|
lineApiService.Log.Errorf("初始化api关系失败 err:%v", err)
|
||||||
|
os.Exit(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
reverseSettingService := service.LineReverseSetting{}
|
||||||
|
reverseSettingService.Orm = db
|
||||||
|
reverseSettingService.Log = lineApiService.Log
|
||||||
|
|
||||||
|
if err := reverseSettingService.SaveCache(models.LineReverseSetting{}); err != nil {
|
||||||
|
reverseSettingService.Log.Errorf("初始化反单设置失败 err:%v", err)
|
||||||
|
os.Exit(-1)
|
||||||
|
}
|
||||||
|
|
||||||
//初始化参数配置
|
//初始化参数配置
|
||||||
cacheservice.InitConfigCache(db)
|
cacheservice.InitConfigCache(db)
|
||||||
//初始化可缓存价格交易对
|
//初始化可缓存价格交易对
|
||||||
|
|||||||
@ -84,3 +84,44 @@ func UserSubscribeInit(orm *gorm.DB, ctx context.Context) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 重启连接
|
||||||
|
func RestartConnect() error {
|
||||||
|
spotSockets := excservice.SpotSockets
|
||||||
|
futuresSockets := excservice.FutureSockets
|
||||||
|
timeOut := 22 * time.Hour
|
||||||
|
|
||||||
|
for _, item := range spotSockets {
|
||||||
|
//超过22小时,重新连接
|
||||||
|
if time.Since(item.ConnectTime) > timeOut {
|
||||||
|
if err := item.ReplaceConnection(); err != nil {
|
||||||
|
log.Errorf("现货重启连接失败 key:%s,error:%s", item.GetKey(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range futuresSockets {
|
||||||
|
//超过22小时,重新连接
|
||||||
|
if time.Since(item.ConnectTime) > timeOut {
|
||||||
|
if err := item.ReplaceConnection(); err != nil {
|
||||||
|
log.Errorf("合约重启连接失败 key:%s,error:%s", item.GetKey(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 假死重启
|
||||||
|
func DeadCheck() {
|
||||||
|
spotSockets := excservice.SpotSockets
|
||||||
|
futuresSockets := excservice.FutureSockets
|
||||||
|
|
||||||
|
for _, item := range spotSockets {
|
||||||
|
item.DeadCheck()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range futuresSockets {
|
||||||
|
item.DeadCheck()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
3
go.mod
3
go.mod
@ -39,6 +39,7 @@ require (
|
|||||||
github.com/shirou/gopsutil/v3 v3.23.10
|
github.com/shirou/gopsutil/v3 v3.23.10
|
||||||
github.com/shopspring/decimal v1.2.0
|
github.com/shopspring/decimal v1.2.0
|
||||||
github.com/spf13/cobra v1.7.0
|
github.com/spf13/cobra v1.7.0
|
||||||
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/swaggo/files v1.0.1
|
github.com/swaggo/files v1.0.1
|
||||||
github.com/swaggo/gin-swagger v1.6.0
|
github.com/swaggo/gin-swagger v1.6.0
|
||||||
github.com/swaggo/swag v1.16.2
|
github.com/swaggo/swag v1.16.2
|
||||||
@ -82,6 +83,7 @@ require (
|
|||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
github.com/coreos/go-semver v0.3.1 // indirect
|
github.com/coreos/go-semver v0.3.1 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/fatih/color v1.15.0 // indirect
|
github.com/fatih/color v1.15.0 // indirect
|
||||||
github.com/fatih/structs v1.1.0 // indirect
|
github.com/fatih/structs v1.1.0 // indirect
|
||||||
@ -143,6 +145,7 @@ require (
|
|||||||
github.com/nsqio/go-nsq v1.1.0 // indirect
|
github.com/nsqio/go-nsq v1.1.0 // indirect
|
||||||
github.com/nyaruka/phonenumbers v1.0.55 // indirect
|
github.com/nyaruka/phonenumbers v1.0.55 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||||
github.com/prometheus/client_model v0.5.0 // indirect
|
github.com/prometheus/client_model v0.5.0 // indirect
|
||||||
github.com/prometheus/common v0.45.0 // indirect
|
github.com/prometheus/common v0.45.0 // indirect
|
||||||
|
|||||||
@ -37,6 +37,10 @@ type TradeSet struct {
|
|||||||
E int64 `json:"-"` //推送时间
|
E int64 `json:"-"` //推送时间
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *TradeSet) GetSymbol() string {
|
||||||
|
return e.Coin + e.Currency
|
||||||
|
}
|
||||||
|
|
||||||
//CommissionType int `db:"commissiontype"` //手续费:1买,2卖,3双向
|
//CommissionType int `db:"commissiontype"` //手续费:1买,2卖,3双向
|
||||||
//DepositNum float64 `db:"depositnum" json:"depositnum"` //保证金规模(手)
|
//DepositNum float64 `db:"depositnum" json:"depositnum"` //保证金规模(手)
|
||||||
//ForceRate float64 `db:"forcerate" json:"forcerate"` //维持保证金率1%
|
//ForceRate float64 `db:"forcerate" json:"forcerate"` //维持保证金率1%
|
||||||
|
|||||||
46
pkg/maphelper/maphelper.go
Normal file
46
pkg/maphelper/maphelper.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package maphelper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetString(m map[string]interface{}, key string) (string, error) {
|
||||||
|
val, ok := m[key]
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("参数错误,缺少字段 %s", key)
|
||||||
|
}
|
||||||
|
strVal, ok := val.(string)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("字段 %s 类型错误", key)
|
||||||
|
}
|
||||||
|
return strVal, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFloat64(m map[string]interface{}, key string) (float64, error) {
|
||||||
|
val, ok := m[key]
|
||||||
|
if !ok {
|
||||||
|
return 0, fmt.Errorf("参数错误,缺少字段 %s", key)
|
||||||
|
}
|
||||||
|
f, ok := val.(float64)
|
||||||
|
if !ok {
|
||||||
|
return 0, fmt.Errorf("字段 %s 类型错误", key)
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetBool(m map[string]interface{}, key string) bool {
|
||||||
|
if val, ok := m[key].(bool); ok {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDecimal(m map[string]interface{}, key string) decimal.Decimal {
|
||||||
|
if val, ok := m[key].(string); ok {
|
||||||
|
d, _ := decimal.NewFromString(val)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
return decimal.Zero
|
||||||
|
}
|
||||||
53
pkg/retryhelper/retryhelper.go
Normal file
53
pkg/retryhelper/retryhelper.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package retryhelper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RetryOptions 定义重试的配置项
|
||||||
|
type RetryOptions struct {
|
||||||
|
MaxRetries int // 最大重试次数
|
||||||
|
InitialInterval time.Duration // 初始重试间隔
|
||||||
|
MaxInterval time.Duration // 最大重试间隔
|
||||||
|
BackoffFactor float64 // 指数退避的增长因子
|
||||||
|
RetryableErrFn func(error) bool // 用于判断是否为可重试错误的函数(可选)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultRetryOptions() RetryOptions {
|
||||||
|
return RetryOptions{
|
||||||
|
MaxRetries: 3,
|
||||||
|
InitialInterval: 150 * time.Millisecond,
|
||||||
|
MaxInterval: 3 * time.Second,
|
||||||
|
BackoffFactor: 2.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry 重试通用函数(用于没有返回值的函数)
|
||||||
|
func Retry(op func() error, opts RetryOptions) error {
|
||||||
|
_, err := RetryWithResult(func() (struct{}, error) {
|
||||||
|
return struct{}{}, op()
|
||||||
|
}, opts)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RetryWithResult 重试通用函数(用于带返回值的函数)
|
||||||
|
func RetryWithResult[T any](op func() (T, error), opts RetryOptions) (result T, err error) {
|
||||||
|
interval := opts.InitialInterval
|
||||||
|
for attempt := 0; attempt <= opts.MaxRetries; attempt++ {
|
||||||
|
result, err = op()
|
||||||
|
if err == nil {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.RetryableErrFn != nil && !opts.RetryableErrFn(err) {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if attempt < opts.MaxRetries {
|
||||||
|
time.Sleep(interval)
|
||||||
|
interval = time.Duration(math.Min(float64(opts.MaxInterval), float64(interval)*opts.BackoffFactor))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
@ -28,3 +28,7 @@ func init() {
|
|||||||
func GetOrderId() int64 {
|
func GetOrderId() int64 {
|
||||||
return snowNode.Generate().Int64()
|
return snowNode.Generate().Int64()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetOrderNo() string {
|
||||||
|
return fmt.Sprintf("%d", GetOrderId())
|
||||||
|
}
|
||||||
|
|||||||
@ -467,6 +467,21 @@ func GetApiInfo(apiId int) (DbModels.LineApiUser, error) {
|
|||||||
return api, nil
|
return api, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetReverseApiInfo 根据apiKey获取api用户信息
|
||||||
|
func GetApiInfoByKey(apiKey string) DbModels.LineApiUser {
|
||||||
|
result := DbModels.LineApiUser{}
|
||||||
|
keys, _ := helper.DefaultRedis.GetAllKeysAndValues(fmt.Sprintf(rediskey.API_USER, "*"))
|
||||||
|
|
||||||
|
for _, key := range keys {
|
||||||
|
if strings.Contains(key, apiKey) {
|
||||||
|
sonic.UnmarshalString(key, &result)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
根据A账户获取B账号信息
|
根据A账户获取B账号信息
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestCancelFutClosePosition(t *testing.T) {
|
func TestCancelFutClosePosition(t *testing.T) {
|
||||||
dsn := "root:root@tcp(192.168.123.216:3306)/gp-bian?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
|
dsn := "root:123456@tcp(127.0.0.1:3306)/gp-bian?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
|
||||||
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||||
var apiUserInfo models.LineApiUser
|
var apiUserInfo models.LineApiUser
|
||||||
db.Model(&models.LineApiUser{}).Where("id = 21").Find(&apiUserInfo)
|
db.Model(&models.LineApiUser{}).Where("id = 21").Find(&apiUserInfo)
|
||||||
|
|||||||
@ -479,3 +479,13 @@ func GetOpenOrderSns(db *gorm.DB, mainIds []int) ([]string, error) {
|
|||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 回去反单默认配置
|
||||||
|
func GetReverseSetting(db *gorm.DB) (DbModels.LineReverseSetting, error) {
|
||||||
|
var setting DbModels.LineReverseSetting
|
||||||
|
if err := db.Model(&DbModels.LineReverseSetting{}).First(&setting).Error; err != nil {
|
||||||
|
return setting, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return setting, nil
|
||||||
|
}
|
||||||
|
|||||||
358
services/binanceservice/config_optimized.go
Normal file
358
services/binanceservice/config_optimized.go
Normal file
@ -0,0 +1,358 @@
|
|||||||
|
package binanceservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OptimizedConfig 优化配置结构
|
||||||
|
type OptimizedConfig struct {
|
||||||
|
// 锁配置
|
||||||
|
LockConfig LockConfig `json:"lock_config"`
|
||||||
|
|
||||||
|
// 重试配置
|
||||||
|
RetryConfig RetryConfig `json:"retry_config"`
|
||||||
|
|
||||||
|
// 熔断器配置
|
||||||
|
CircuitBreakerConfig CircuitBreakerConfig `json:"circuit_breaker_config"`
|
||||||
|
|
||||||
|
// 同步配置
|
||||||
|
SyncConfig SyncConfig `json:"sync_config"`
|
||||||
|
|
||||||
|
// 性能配置
|
||||||
|
PerformanceConfig PerformanceConfig `json:"performance_config"`
|
||||||
|
|
||||||
|
// 监控配置
|
||||||
|
MonitorConfig MonitorConfig `json:"monitor_config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LockConfig 锁配置
|
||||||
|
type LockConfig struct {
|
||||||
|
// Redis分布式锁超时时间
|
||||||
|
RedisLockTimeout time.Duration `json:"redis_lock_timeout"`
|
||||||
|
|
||||||
|
// 获取锁等待时间
|
||||||
|
LockWaitTimeout time.Duration `json:"lock_wait_timeout"`
|
||||||
|
|
||||||
|
// 订单处理锁前缀
|
||||||
|
OrderLockPrefix string `json:"order_lock_prefix"`
|
||||||
|
|
||||||
|
// 持仓更新锁前缀
|
||||||
|
PositionLockPrefix string `json:"position_lock_prefix"`
|
||||||
|
|
||||||
|
// 止盈止损锁前缀
|
||||||
|
TakeProfitLockPrefix string `json:"take_profit_lock_prefix"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CircuitBreakerConfig 熔断器配置
|
||||||
|
type CircuitBreakerConfig struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
FailureThreshold int `json:"failure_threshold"`
|
||||||
|
SuccessThreshold int `json:"success_threshold"`
|
||||||
|
Timeout time.Duration `json:"timeout"`
|
||||||
|
HalfOpenMaxCalls int `json:"half_open_max_calls"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RetryConfig 重试配置
|
||||||
|
type RetryConfig struct {
|
||||||
|
// 最大重试次数
|
||||||
|
MaxRetries int `json:"max_retries"`
|
||||||
|
|
||||||
|
// 重试延迟
|
||||||
|
RetryDelay time.Duration `json:"retry_delay"`
|
||||||
|
|
||||||
|
// 指数退避因子
|
||||||
|
BackoffFactor float64 `json:"backoff_factor"`
|
||||||
|
|
||||||
|
// 最大重试延迟
|
||||||
|
MaxRetryDelay time.Duration `json:"max_retry_delay"`
|
||||||
|
|
||||||
|
// API调用重试次数
|
||||||
|
ApiRetryCount int `json:"api_retry_count"`
|
||||||
|
|
||||||
|
// 数据库操作重试次数
|
||||||
|
DbRetryCount int `json:"db_retry_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyncConfig 同步配置
|
||||||
|
type SyncConfig struct {
|
||||||
|
// 持仓同步检查间隔
|
||||||
|
PositionSyncInterval time.Duration `json:"position_sync_interval"`
|
||||||
|
|
||||||
|
// 持仓差异阈值
|
||||||
|
PositionDiffThreshold decimal.Decimal `json:"position_diff_threshold"`
|
||||||
|
|
||||||
|
// 强制同步阈值
|
||||||
|
ForceSyncThreshold decimal.Decimal `json:"force_sync_threshold"`
|
||||||
|
|
||||||
|
// 同步超时时间
|
||||||
|
SyncTimeout time.Duration `json:"sync_timeout"`
|
||||||
|
|
||||||
|
// 是否启用自动同步
|
||||||
|
AutoSyncEnabled bool `json:"auto_sync_enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PerformanceConfig 性能配置
|
||||||
|
type PerformanceConfig struct {
|
||||||
|
// 批量操作大小
|
||||||
|
BatchSize int `json:"batch_size"`
|
||||||
|
|
||||||
|
// 异步处理队列大小
|
||||||
|
AsyncQueueSize int `json:"async_queue_size"`
|
||||||
|
|
||||||
|
// 工作协程数量
|
||||||
|
WorkerCount int `json:"worker_count"`
|
||||||
|
|
||||||
|
// 数据库连接池大小
|
||||||
|
DbPoolSize int `json:"db_pool_size"`
|
||||||
|
|
||||||
|
// 缓存过期时间
|
||||||
|
CacheExpiration time.Duration `json:"cache_expiration"`
|
||||||
|
|
||||||
|
// 是否启用缓存
|
||||||
|
CacheEnabled bool `json:"cache_enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MonitorConfig 监控配置
|
||||||
|
type MonitorConfig struct {
|
||||||
|
// 是否启用监控
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
|
||||||
|
// 监控数据收集间隔
|
||||||
|
CollectInterval time.Duration `json:"collect_interval"`
|
||||||
|
|
||||||
|
// 告警阈值配置
|
||||||
|
AlertThresholds AlertThresholds `json:"alert_thresholds"`
|
||||||
|
|
||||||
|
// 日志级别
|
||||||
|
LogLevel string `json:"log_level"`
|
||||||
|
|
||||||
|
// 是否启用性能分析
|
||||||
|
ProfileEnabled bool `json:"profile_enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AlertThresholds 告警阈值配置
|
||||||
|
type AlertThresholds struct {
|
||||||
|
// 订单处理失败率阈值 (百分比)
|
||||||
|
OrderFailureRate float64 `json:"order_failure_rate"`
|
||||||
|
|
||||||
|
// 持仓同步失败率阈值 (百分比)
|
||||||
|
PositionSyncFailureRate float64 `json:"position_sync_failure_rate"`
|
||||||
|
|
||||||
|
// API调用失败率阈值 (百分比)
|
||||||
|
ApiFailureRate float64 `json:"api_failure_rate"`
|
||||||
|
|
||||||
|
// 响应时间阈值 (毫秒)
|
||||||
|
ResponseTimeThreshold time.Duration `json:"response_time_threshold"`
|
||||||
|
|
||||||
|
// 内存使用率阈值 (百分比)
|
||||||
|
MemoryUsageThreshold float64 `json:"memory_usage_threshold"`
|
||||||
|
|
||||||
|
// CPU使用率阈值 (百分比)
|
||||||
|
CpuUsageThreshold float64 `json:"cpu_usage_threshold"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaultOptimizedConfig 获取默认优化配置
|
||||||
|
func GetDefaultOptimizedConfig() *OptimizedConfig {
|
||||||
|
return &OptimizedConfig{
|
||||||
|
LockConfig: LockConfig{
|
||||||
|
RedisLockTimeout: 30 * time.Second,
|
||||||
|
LockWaitTimeout: 5 * time.Second,
|
||||||
|
OrderLockPrefix: "reverse_order_lock:",
|
||||||
|
PositionLockPrefix: "position_update_lock:",
|
||||||
|
TakeProfitLockPrefix: "take_profit_lock:",
|
||||||
|
},
|
||||||
|
RetryConfig: RetryConfig{
|
||||||
|
MaxRetries: 3,
|
||||||
|
RetryDelay: time.Second,
|
||||||
|
BackoffFactor: 2.0,
|
||||||
|
MaxRetryDelay: 10 * time.Second,
|
||||||
|
ApiRetryCount: 3,
|
||||||
|
DbRetryCount: 3,
|
||||||
|
},
|
||||||
|
CircuitBreakerConfig: CircuitBreakerConfig{
|
||||||
|
Enabled: true,
|
||||||
|
FailureThreshold: 5,
|
||||||
|
SuccessThreshold: 3,
|
||||||
|
Timeout: 60 * time.Second,
|
||||||
|
HalfOpenMaxCalls: 3,
|
||||||
|
},
|
||||||
|
SyncConfig: SyncConfig{
|
||||||
|
PositionSyncInterval: 30 * time.Second,
|
||||||
|
PositionDiffThreshold: decimal.NewFromFloat(0.001),
|
||||||
|
ForceSyncThreshold: decimal.NewFromFloat(0.01),
|
||||||
|
SyncTimeout: 10 * time.Second,
|
||||||
|
AutoSyncEnabled: true,
|
||||||
|
},
|
||||||
|
PerformanceConfig: PerformanceConfig{
|
||||||
|
BatchSize: 10,
|
||||||
|
AsyncQueueSize: 1000,
|
||||||
|
WorkerCount: 5,
|
||||||
|
DbPoolSize: 20,
|
||||||
|
CacheExpiration: 5 * time.Minute,
|
||||||
|
CacheEnabled: true,
|
||||||
|
},
|
||||||
|
MonitorConfig: MonitorConfig{
|
||||||
|
Enabled: true,
|
||||||
|
CollectInterval: time.Minute,
|
||||||
|
AlertThresholds: AlertThresholds{
|
||||||
|
OrderFailureRate: 5.0, // 5%
|
||||||
|
PositionSyncFailureRate: 2.0, // 2%
|
||||||
|
ApiFailureRate: 10.0, // 10%
|
||||||
|
ResponseTimeThreshold: 5 * time.Second,
|
||||||
|
MemoryUsageThreshold: 80.0, // 80%
|
||||||
|
CpuUsageThreshold: 70.0, // 70%
|
||||||
|
},
|
||||||
|
LogLevel: "info",
|
||||||
|
ProfileEnabled: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateConfig 验证配置有效性
|
||||||
|
func (c *OptimizedConfig) ValidateConfig() error {
|
||||||
|
// 验证锁配置
|
||||||
|
if c.LockConfig.RedisLockTimeout <= 0 {
|
||||||
|
return fmt.Errorf("Redis锁超时时间必须大于0")
|
||||||
|
}
|
||||||
|
if c.LockConfig.LockWaitTimeout <= 0 {
|
||||||
|
return fmt.Errorf("锁等待超时时间必须大于0")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证重试配置
|
||||||
|
if c.RetryConfig.MaxRetries < 0 {
|
||||||
|
return fmt.Errorf("最大重试次数不能为负数")
|
||||||
|
}
|
||||||
|
if c.RetryConfig.RetryDelay <= 0 {
|
||||||
|
return fmt.Errorf("重试延迟必须大于0")
|
||||||
|
}
|
||||||
|
if c.RetryConfig.BackoffFactor <= 1.0 {
|
||||||
|
return fmt.Errorf("退避因子必须大于1.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证同步配置
|
||||||
|
if c.SyncConfig.PositionSyncInterval <= 0 {
|
||||||
|
return fmt.Errorf("持仓同步间隔必须大于0")
|
||||||
|
}
|
||||||
|
if c.SyncConfig.PositionDiffThreshold.IsNegative() {
|
||||||
|
return fmt.Errorf("持仓差异阈值不能为负数")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证性能配置
|
||||||
|
if c.PerformanceConfig.BatchSize <= 0 {
|
||||||
|
return fmt.Errorf("批量操作大小必须大于0")
|
||||||
|
}
|
||||||
|
if c.PerformanceConfig.WorkerCount <= 0 {
|
||||||
|
return fmt.Errorf("工作协程数量必须大于0")
|
||||||
|
}
|
||||||
|
if c.PerformanceConfig.DbPoolSize <= 0 {
|
||||||
|
return fmt.Errorf("数据库连接池大小必须大于0")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证监控配置
|
||||||
|
if c.MonitorConfig.Enabled {
|
||||||
|
if c.MonitorConfig.CollectInterval <= 0 {
|
||||||
|
return fmt.Errorf("监控数据收集间隔必须大于0")
|
||||||
|
}
|
||||||
|
if c.MonitorConfig.AlertThresholds.OrderFailureRate < 0 || c.MonitorConfig.AlertThresholds.OrderFailureRate > 100 {
|
||||||
|
return fmt.Errorf("订单失败率阈值必须在0-100之间")
|
||||||
|
}
|
||||||
|
if c.MonitorConfig.AlertThresholds.MemoryUsageThreshold < 0 || c.MonitorConfig.AlertThresholds.MemoryUsageThreshold > 100 {
|
||||||
|
return fmt.Errorf("内存使用率阈值必须在0-100之间")
|
||||||
|
}
|
||||||
|
if c.MonitorConfig.AlertThresholds.CpuUsageThreshold < 0 || c.MonitorConfig.AlertThresholds.CpuUsageThreshold > 100 {
|
||||||
|
return fmt.Errorf("CPU使用率阈值必须在0-100之间")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLockKey 生成锁键名
|
||||||
|
func (c *OptimizedConfig) GetLockKey(lockType, identifier string) string {
|
||||||
|
switch lockType {
|
||||||
|
case "order":
|
||||||
|
return c.LockConfig.OrderLockPrefix + identifier
|
||||||
|
case "position":
|
||||||
|
return c.LockConfig.PositionLockPrefix + identifier
|
||||||
|
case "take_profit":
|
||||||
|
return c.LockConfig.TakeProfitLockPrefix + identifier
|
||||||
|
default:
|
||||||
|
return "unknown_lock:" + identifier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRetryableError 判断错误是否可重试
|
||||||
|
func (c *OptimizedConfig) IsRetryableError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
errStr := err.Error()
|
||||||
|
// 定义不可重试的错误类型
|
||||||
|
nonRetryableErrors := []string{
|
||||||
|
"余额不足",
|
||||||
|
"订单重复",
|
||||||
|
"API-key",
|
||||||
|
"无效",
|
||||||
|
"权限",
|
||||||
|
"签名",
|
||||||
|
"参数错误",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, nonRetryable := range nonRetryableErrors {
|
||||||
|
if contains(errStr, []string{nonRetryable}) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRetryDelay 计算重试延迟时间
|
||||||
|
func (c *OptimizedConfig) GetRetryDelay(attempt int) time.Duration {
|
||||||
|
if attempt <= 0 {
|
||||||
|
return c.RetryConfig.RetryDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
// 指数退避算法
|
||||||
|
delay := c.RetryConfig.RetryDelay
|
||||||
|
for i := 0; i < attempt; i++ {
|
||||||
|
delay = time.Duration(float64(delay) * c.RetryConfig.BackoffFactor)
|
||||||
|
if delay > c.RetryConfig.MaxRetryDelay {
|
||||||
|
return c.RetryConfig.MaxRetryDelay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return delay
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldSync 判断是否需要同步持仓
|
||||||
|
func (c *OptimizedConfig) ShouldSync(exchangePosition, systemPosition decimal.Decimal) bool {
|
||||||
|
diff := exchangePosition.Sub(systemPosition).Abs()
|
||||||
|
return diff.GreaterThan(c.SyncConfig.PositionDiffThreshold)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldForceSync 判断是否需要强制同步
|
||||||
|
func (c *OptimizedConfig) ShouldForceSync(exchangePosition, systemPosition decimal.Decimal) bool {
|
||||||
|
diff := exchangePosition.Sub(systemPosition).Abs()
|
||||||
|
return diff.GreaterThan(c.SyncConfig.ForceSyncThreshold)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局配置实例
|
||||||
|
var GlobalOptimizedConfig *OptimizedConfig
|
||||||
|
|
||||||
|
// InitOptimizedConfig 初始化优化配置
|
||||||
|
func InitOptimizedConfig() error {
|
||||||
|
GlobalOptimizedConfig = GetDefaultOptimizedConfig()
|
||||||
|
return GlobalOptimizedConfig.ValidateConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOptimizedConfig 获取全局优化配置
|
||||||
|
func GetOptimizedConfig() *OptimizedConfig {
|
||||||
|
if GlobalOptimizedConfig == nil {
|
||||||
|
InitOptimizedConfig()
|
||||||
|
}
|
||||||
|
return GlobalOptimizedConfig
|
||||||
|
}
|
||||||
676
services/binanceservice/error_handler_optimized.go
Normal file
676
services/binanceservice/error_handler_optimized.go
Normal file
@ -0,0 +1,676 @@
|
|||||||
|
package binanceservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-admin-team/go-admin-core/logger"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrorType 错误类型
|
||||||
|
type ErrorType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ErrorTypeNetwork ErrorType = "network"
|
||||||
|
ErrorTypeDatabase ErrorType = "database"
|
||||||
|
ErrorTypeBusiness ErrorType = "business"
|
||||||
|
ErrorTypeSystem ErrorType = "system"
|
||||||
|
ErrorTypeValidation ErrorType = "validation"
|
||||||
|
ErrorTypeTimeout ErrorType = "timeout"
|
||||||
|
ErrorTypeRateLimit ErrorType = "rate_limit"
|
||||||
|
ErrorTypeAuth ErrorType = "auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrorSeverity 错误严重程度
|
||||||
|
type ErrorSeverity string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SeverityLow ErrorSeverity = "low"
|
||||||
|
SeverityMedium ErrorSeverity = "medium"
|
||||||
|
SeverityHigh ErrorSeverity = "high"
|
||||||
|
SeverityCritical ErrorSeverity = "critical"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrorAction 错误处理动作
|
||||||
|
type ErrorAction string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ActionRetry ErrorAction = "retry"
|
||||||
|
ActionFallback ErrorAction = "fallback"
|
||||||
|
ActionAlert ErrorAction = "alert"
|
||||||
|
ActionIgnore ErrorAction = "ignore"
|
||||||
|
ActionCircuit ErrorAction = "circuit"
|
||||||
|
ActionDegrade ErrorAction = "degrade"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrorInfo 错误信息
|
||||||
|
type ErrorInfo struct {
|
||||||
|
Type ErrorType `json:"type"`
|
||||||
|
Severity ErrorSeverity `json:"severity"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
OriginalErr error `json:"-"`
|
||||||
|
Context string `json:"context"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
Retryable bool `json:"retryable"`
|
||||||
|
Action ErrorAction `json:"action"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RetryConfig 和 CircuitBreakerConfig 已在 config_optimized.go 中定义
|
||||||
|
|
||||||
|
// ErrorHandler 错误处理器
|
||||||
|
type ErrorHandler struct {
|
||||||
|
config *OptimizedConfig
|
||||||
|
metricsCollector *MetricsCollector
|
||||||
|
circuitBreakers map[string]*CircuitBreaker
|
||||||
|
}
|
||||||
|
|
||||||
|
// CircuitBreaker 熔断器
|
||||||
|
type CircuitBreaker struct {
|
||||||
|
name string
|
||||||
|
config CircuitBreakerConfig
|
||||||
|
state CircuitState
|
||||||
|
failureCount int
|
||||||
|
successCount int
|
||||||
|
lastFailTime time.Time
|
||||||
|
nextRetry time.Time
|
||||||
|
halfOpenCalls int
|
||||||
|
}
|
||||||
|
|
||||||
|
// CircuitState 熔断器状态
|
||||||
|
type CircuitState string
|
||||||
|
|
||||||
|
const (
|
||||||
|
StateClosed CircuitState = "closed"
|
||||||
|
StateOpen CircuitState = "open"
|
||||||
|
StateHalfOpen CircuitState = "half_open"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewErrorHandler 创建错误处理器
|
||||||
|
func NewErrorHandler(config *OptimizedConfig, metricsCollector *MetricsCollector) *ErrorHandler {
|
||||||
|
return &ErrorHandler{
|
||||||
|
config: config,
|
||||||
|
metricsCollector: metricsCollector,
|
||||||
|
circuitBreakers: make(map[string]*CircuitBreaker),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClassifyError 分类错误
|
||||||
|
func (eh *ErrorHandler) ClassifyError(err error, context string) *ErrorInfo {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
errorInfo := &ErrorInfo{
|
||||||
|
OriginalErr: err,
|
||||||
|
Message: err.Error(),
|
||||||
|
Context: context,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据错误类型分类
|
||||||
|
switch {
|
||||||
|
case isNetworkError(err):
|
||||||
|
errorInfo.Type = ErrorTypeNetwork
|
||||||
|
errorInfo.Severity = SeverityMedium
|
||||||
|
errorInfo.Retryable = true
|
||||||
|
errorInfo.Action = ActionRetry
|
||||||
|
|
||||||
|
case isDatabaseError(err):
|
||||||
|
errorInfo.Type = ErrorTypeDatabase
|
||||||
|
errorInfo.Severity = SeverityHigh
|
||||||
|
errorInfo.Retryable = isRetryableDatabaseError(err)
|
||||||
|
errorInfo.Action = ActionRetry
|
||||||
|
|
||||||
|
case isTimeoutError(err):
|
||||||
|
errorInfo.Type = ErrorTypeTimeout
|
||||||
|
errorInfo.Severity = SeverityMedium
|
||||||
|
errorInfo.Retryable = true
|
||||||
|
errorInfo.Action = ActionRetry
|
||||||
|
|
||||||
|
case isRateLimitError(err):
|
||||||
|
errorInfo.Type = ErrorTypeRateLimit
|
||||||
|
errorInfo.Severity = SeverityMedium
|
||||||
|
errorInfo.Retryable = true
|
||||||
|
errorInfo.Action = ActionRetry
|
||||||
|
|
||||||
|
case isAuthError(err):
|
||||||
|
errorInfo.Type = ErrorTypeAuth
|
||||||
|
errorInfo.Severity = SeverityHigh
|
||||||
|
errorInfo.Retryable = false
|
||||||
|
errorInfo.Action = ActionAlert
|
||||||
|
|
||||||
|
case isValidationError(err):
|
||||||
|
errorInfo.Type = ErrorTypeValidation
|
||||||
|
errorInfo.Severity = SeverityLow
|
||||||
|
errorInfo.Retryable = false
|
||||||
|
errorInfo.Action = ActionIgnore
|
||||||
|
|
||||||
|
case isBusinessError(err):
|
||||||
|
errorInfo.Type = ErrorTypeBusiness
|
||||||
|
errorInfo.Severity = SeverityMedium
|
||||||
|
errorInfo.Retryable = false
|
||||||
|
errorInfo.Action = ActionFallback
|
||||||
|
|
||||||
|
default:
|
||||||
|
errorInfo.Type = ErrorTypeSystem
|
||||||
|
errorInfo.Severity = SeverityHigh
|
||||||
|
errorInfo.Retryable = true
|
||||||
|
errorInfo.Action = ActionRetry
|
||||||
|
}
|
||||||
|
|
||||||
|
return errorInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleError 处理错误
|
||||||
|
func (eh *ErrorHandler) HandleError(err error, context string) *ErrorInfo {
|
||||||
|
errorInfo := eh.ClassifyError(err, context)
|
||||||
|
if errorInfo == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录错误指标
|
||||||
|
if eh.metricsCollector != nil {
|
||||||
|
eh.metricsCollector.RecordError(string(errorInfo.Type), errorInfo.Message, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据错误动作处理
|
||||||
|
switch errorInfo.Action {
|
||||||
|
case ActionAlert:
|
||||||
|
eh.sendAlert(errorInfo)
|
||||||
|
case ActionCircuit:
|
||||||
|
eh.triggerCircuitBreaker(context)
|
||||||
|
case ActionDegrade:
|
||||||
|
eh.enableDegradedMode(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录日志
|
||||||
|
eh.logError(errorInfo)
|
||||||
|
|
||||||
|
return errorInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// RetryWithBackoff 带退避的重试
|
||||||
|
func (eh *ErrorHandler) RetryWithBackoff(ctx context.Context, operation func() error, config RetryConfig, context string) error {
|
||||||
|
var lastErr error
|
||||||
|
delay := config.RetryDelay
|
||||||
|
|
||||||
|
for attempt := 1; attempt <= config.MaxRetries; attempt++ {
|
||||||
|
// 检查上下文是否已取消
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查熔断器状态
|
||||||
|
if !eh.canExecute(context) {
|
||||||
|
return fmt.Errorf("circuit breaker is open for %s", context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行操作
|
||||||
|
// 注意:config中没有TimeoutPerTry字段,这里暂时注释掉
|
||||||
|
// if config.TimeoutPerTry > 0 {
|
||||||
|
// var cancel context.CancelFunc
|
||||||
|
// operationCtx, cancel = context.WithTimeout(ctx, config.TimeoutPerTry)
|
||||||
|
// defer cancel()
|
||||||
|
// }
|
||||||
|
|
||||||
|
err := operation()
|
||||||
|
if err == nil {
|
||||||
|
// 成功,记录成功指标
|
||||||
|
if eh.metricsCollector != nil {
|
||||||
|
eh.metricsCollector.RecordRetry(true)
|
||||||
|
}
|
||||||
|
eh.recordSuccess(context)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lastErr = err
|
||||||
|
errorInfo := eh.ClassifyError(err, context)
|
||||||
|
|
||||||
|
// 记录失败
|
||||||
|
eh.recordFailure(context)
|
||||||
|
|
||||||
|
// 如果不可重试,直接返回
|
||||||
|
if errorInfo != nil && !errorInfo.Retryable {
|
||||||
|
logger.Warnf("不可重试的错误 [%s]: %v", context, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最后一次尝试,不再等待
|
||||||
|
if attempt == config.MaxRetries {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算下次重试延迟
|
||||||
|
nextDelay := eh.calculateDelay(delay, config)
|
||||||
|
logger.Infof("重试 %d/%d [%s] 在 %v 后,错误: %v", attempt, config.MaxRetries, context, nextDelay, err)
|
||||||
|
|
||||||
|
// 等待重试
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-time.After(nextDelay):
|
||||||
|
delay = nextDelay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录重试失败指标
|
||||||
|
if eh.metricsCollector != nil {
|
||||||
|
eh.metricsCollector.RecordRetry(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("重试 %d 次后仍然失败 [%s]: %w", config.MaxRetries, context, lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateDelay 计算重试延迟
|
||||||
|
func (eh *ErrorHandler) calculateDelay(currentDelay time.Duration, config RetryConfig) time.Duration {
|
||||||
|
nextDelay := time.Duration(float64(currentDelay) * config.BackoffFactor)
|
||||||
|
|
||||||
|
// 限制最大延迟
|
||||||
|
if nextDelay > config.MaxRetryDelay {
|
||||||
|
nextDelay = config.MaxRetryDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加10%抖动(简化版本,不依赖JitterEnabled字段)
|
||||||
|
jitter := time.Duration(float64(nextDelay) * 0.1)
|
||||||
|
if jitter > 0 {
|
||||||
|
nextDelay += time.Duration(time.Now().UnixNano() % int64(jitter))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
// canExecute 检查是否可以执行操作(熔断器检查)
|
||||||
|
func (eh *ErrorHandler) canExecute(context string) bool {
|
||||||
|
cb := eh.getCircuitBreaker(context)
|
||||||
|
if cb == nil || !cb.config.Enabled {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
switch cb.state {
|
||||||
|
case StateClosed:
|
||||||
|
return true
|
||||||
|
|
||||||
|
case StateOpen:
|
||||||
|
if now.After(cb.nextRetry) {
|
||||||
|
cb.state = StateHalfOpen
|
||||||
|
cb.halfOpenCalls = 0
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
|
||||||
|
case StateHalfOpen:
|
||||||
|
return cb.halfOpenCalls < cb.config.HalfOpenMaxCalls
|
||||||
|
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// recordSuccess 记录成功
|
||||||
|
func (eh *ErrorHandler) recordSuccess(context string) {
|
||||||
|
cb := eh.getCircuitBreaker(context)
|
||||||
|
if cb == nil || !cb.config.Enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch cb.state {
|
||||||
|
case StateHalfOpen:
|
||||||
|
cb.successCount++
|
||||||
|
if cb.successCount >= cb.config.SuccessThreshold {
|
||||||
|
cb.state = StateClosed
|
||||||
|
cb.failureCount = 0
|
||||||
|
cb.successCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
case StateClosed:
|
||||||
|
cb.failureCount = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// recordFailure 记录失败
|
||||||
|
func (eh *ErrorHandler) recordFailure(context string) {
|
||||||
|
cb := eh.getCircuitBreaker(context)
|
||||||
|
if cb == nil || !cb.config.Enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cb.failureCount++
|
||||||
|
cb.lastFailTime = time.Now()
|
||||||
|
|
||||||
|
switch cb.state {
|
||||||
|
case StateClosed:
|
||||||
|
if cb.failureCount >= cb.config.FailureThreshold {
|
||||||
|
cb.state = StateOpen
|
||||||
|
cb.nextRetry = time.Now().Add(cb.config.Timeout)
|
||||||
|
logger.Warnf("熔断器开启 [%s]: 失败次数 %d", context, cb.failureCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
case StateHalfOpen:
|
||||||
|
cb.state = StateOpen
|
||||||
|
cb.nextRetry = time.Now().Add(cb.config.Timeout)
|
||||||
|
cb.successCount = 0
|
||||||
|
logger.Warnf("熔断器重新开启 [%s]", context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCircuitBreaker 获取熔断器
|
||||||
|
func (eh *ErrorHandler) getCircuitBreaker(context string) *CircuitBreaker {
|
||||||
|
cb, exists := eh.circuitBreakers[context]
|
||||||
|
if !exists {
|
||||||
|
cb = &CircuitBreaker{
|
||||||
|
name: context,
|
||||||
|
config: eh.config.CircuitBreakerConfig,
|
||||||
|
state: StateClosed,
|
||||||
|
}
|
||||||
|
eh.circuitBreakers[context] = cb
|
||||||
|
}
|
||||||
|
return cb
|
||||||
|
}
|
||||||
|
|
||||||
|
// triggerCircuitBreaker 触发熔断器
|
||||||
|
func (eh *ErrorHandler) triggerCircuitBreaker(context string) {
|
||||||
|
cb := eh.getCircuitBreaker(context)
|
||||||
|
if cb != nil && cb.config.Enabled {
|
||||||
|
cb.state = StateOpen
|
||||||
|
cb.nextRetry = time.Now().Add(cb.config.Timeout)
|
||||||
|
logger.Warnf("手动触发熔断器 [%s]", context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendAlert 发送告警
|
||||||
|
func (eh *ErrorHandler) sendAlert(errorInfo *ErrorInfo) {
|
||||||
|
// 这里可以集成告警系统,如钉钉、邮件等
|
||||||
|
logger.Errorf("告警: [%s] %s - %s", errorInfo.Type, errorInfo.Context, errorInfo.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// enableDegradedMode 启用降级模式
|
||||||
|
func (eh *ErrorHandler) enableDegradedMode(context string) {
|
||||||
|
logger.Warnf("启用降级模式 [%s]", context)
|
||||||
|
// 这里可以实现具体的降级逻辑
|
||||||
|
}
|
||||||
|
|
||||||
|
// logError 记录错误日志
|
||||||
|
func (eh *ErrorHandler) logError(errorInfo *ErrorInfo) {
|
||||||
|
switch errorInfo.Severity {
|
||||||
|
case SeverityLow:
|
||||||
|
logger.Infof("[%s] %s: %s", errorInfo.Type, errorInfo.Context, errorInfo.Message)
|
||||||
|
case SeverityMedium:
|
||||||
|
logger.Warnf("[%s] %s: %s", errorInfo.Type, errorInfo.Context, errorInfo.Message)
|
||||||
|
case SeverityHigh, SeverityCritical:
|
||||||
|
logger.Errorf("[%s] %s: %s", errorInfo.Type, errorInfo.Context, errorInfo.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCircuitBreakerStatus 获取熔断器状态
|
||||||
|
func (eh *ErrorHandler) GetCircuitBreakerStatus() map[string]interface{} {
|
||||||
|
status := make(map[string]interface{})
|
||||||
|
for name, cb := range eh.circuitBreakers {
|
||||||
|
status[name] = map[string]interface{}{
|
||||||
|
"state": cb.state,
|
||||||
|
"failure_count": cb.failureCount,
|
||||||
|
"success_count": cb.successCount,
|
||||||
|
"last_fail_time": cb.lastFailTime,
|
||||||
|
"next_retry": cb.nextRetry,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误分类辅助函数
|
||||||
|
|
||||||
|
// isNetworkError 判断是否为网络错误
|
||||||
|
func isNetworkError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查网络相关错误
|
||||||
|
var netErr net.Error
|
||||||
|
if errors.As(err, &netErr) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查常见网络错误消息
|
||||||
|
errorMsg := strings.ToLower(err.Error())
|
||||||
|
networkKeywords := []string{
|
||||||
|
"connection refused",
|
||||||
|
"connection reset",
|
||||||
|
"connection timeout",
|
||||||
|
"network unreachable",
|
||||||
|
"no route to host",
|
||||||
|
"dns",
|
||||||
|
"socket",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, keyword := range networkKeywords {
|
||||||
|
if strings.Contains(errorMsg, keyword) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// isDatabaseError 判断是否为数据库错误
|
||||||
|
func isDatabaseError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查GORM错误
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) ||
|
||||||
|
errors.Is(err, gorm.ErrInvalidTransaction) ||
|
||||||
|
errors.Is(err, gorm.ErrNotImplemented) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查数据库相关错误消息
|
||||||
|
errorMsg := strings.ToLower(err.Error())
|
||||||
|
dbKeywords := []string{
|
||||||
|
"database",
|
||||||
|
"sql",
|
||||||
|
"mysql",
|
||||||
|
"postgres",
|
||||||
|
"connection pool",
|
||||||
|
"deadlock",
|
||||||
|
"constraint",
|
||||||
|
"duplicate key",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, keyword := range dbKeywords {
|
||||||
|
if strings.Contains(errorMsg, keyword) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// isRetryableDatabaseError 判断是否为可重试的数据库错误
|
||||||
|
func isRetryableDatabaseError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不可重试的错误
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMsg := strings.ToLower(err.Error())
|
||||||
|
nonRetryableKeywords := []string{
|
||||||
|
"constraint",
|
||||||
|
"duplicate key",
|
||||||
|
"foreign key",
|
||||||
|
"syntax error",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, keyword := range nonRetryableKeywords {
|
||||||
|
if strings.Contains(errorMsg, keyword) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// isTimeoutError 判断是否为超时错误
|
||||||
|
func isTimeoutError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查超时错误
|
||||||
|
var netErr net.Error
|
||||||
|
if errors.As(err, &netErr) && netErr.Timeout() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMsg := strings.ToLower(err.Error())
|
||||||
|
return strings.Contains(errorMsg, "timeout") ||
|
||||||
|
strings.Contains(errorMsg, "deadline exceeded")
|
||||||
|
}
|
||||||
|
|
||||||
|
// isRateLimitError 判断是否为限流错误
|
||||||
|
func isRateLimitError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMsg := strings.ToLower(err.Error())
|
||||||
|
rateLimitKeywords := []string{
|
||||||
|
"rate limit",
|
||||||
|
"too many requests",
|
||||||
|
"429",
|
||||||
|
"quota exceeded",
|
||||||
|
"throttle",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, keyword := range rateLimitKeywords {
|
||||||
|
if strings.Contains(errorMsg, keyword) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// isAuthError 判断是否为认证错误
|
||||||
|
func isAuthError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMsg := strings.ToLower(err.Error())
|
||||||
|
authKeywords := []string{
|
||||||
|
"unauthorized",
|
||||||
|
"authentication",
|
||||||
|
"invalid signature",
|
||||||
|
"api key",
|
||||||
|
"401",
|
||||||
|
"403",
|
||||||
|
"forbidden",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, keyword := range authKeywords {
|
||||||
|
if strings.Contains(errorMsg, keyword) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// isValidationError 判断是否为验证错误
|
||||||
|
func isValidationError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMsg := strings.ToLower(err.Error())
|
||||||
|
validationKeywords := []string{
|
||||||
|
"validation",
|
||||||
|
"invalid parameter",
|
||||||
|
"bad request",
|
||||||
|
"400",
|
||||||
|
"missing required",
|
||||||
|
"invalid format",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, keyword := range validationKeywords {
|
||||||
|
if strings.Contains(errorMsg, keyword) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// isBusinessError 判断是否为业务错误
|
||||||
|
func isBusinessError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMsg := strings.ToLower(err.Error())
|
||||||
|
businessKeywords := []string{
|
||||||
|
"insufficient balance",
|
||||||
|
"order not found",
|
||||||
|
"position not found",
|
||||||
|
"invalid order status",
|
||||||
|
"market closed",
|
||||||
|
"symbol not found",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, keyword := range businessKeywords {
|
||||||
|
if strings.Contains(errorMsg, keyword) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局错误处理器实例
|
||||||
|
var GlobalErrorHandler *ErrorHandler
|
||||||
|
|
||||||
|
// InitErrorHandler 初始化错误处理器
|
||||||
|
func InitErrorHandler(config *OptimizedConfig, metricsCollector *MetricsCollector) {
|
||||||
|
GlobalErrorHandler = NewErrorHandler(config, metricsCollector)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetErrorHandler 获取全局错误处理器
|
||||||
|
func GetErrorHandler() *ErrorHandler {
|
||||||
|
return GlobalErrorHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleErrorWithRetry 处理错误并重试的便捷函数
|
||||||
|
func HandleErrorWithRetry(ctx context.Context, operation func() error, context string) error {
|
||||||
|
if GlobalErrorHandler == nil {
|
||||||
|
return operation()
|
||||||
|
}
|
||||||
|
|
||||||
|
config := RetryConfig{
|
||||||
|
MaxRetries: 3,
|
||||||
|
RetryDelay: 100 * time.Millisecond,
|
||||||
|
MaxRetryDelay: 5 * time.Second,
|
||||||
|
BackoffFactor: 2.0,
|
||||||
|
ApiRetryCount: 3,
|
||||||
|
DbRetryCount: 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
return GlobalErrorHandler.RetryWithBackoff(ctx, operation, config, context)
|
||||||
|
}
|
||||||
125
services/binanceservice/futures_reset_v2.go
Normal file
125
services/binanceservice/futures_reset_v2.go
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
package binanceservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
DbModels "go-admin/app/admin/models"
|
||||||
|
"go-admin/pkg/retryhelper"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
|
log "github.com/go-admin-team/go-admin-core/logger"
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FuturesResetV2 struct {
|
||||||
|
service.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
// 带重试机制的合约下单
|
||||||
|
func (e *FuturesResetV2) OrderPlaceLoop(apiUserInfo *DbModels.LineApiUser, params FutOrderPlace) error {
|
||||||
|
opts := retryhelper.DefaultRetryOptions()
|
||||||
|
opts.RetryableErrFn = func(err error) bool {
|
||||||
|
if strings.Contains(err.Error(), "LOT_SIZE") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
//重试
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
err := retryhelper.Retry(func() error {
|
||||||
|
return e.OrderPlace(apiUserInfo, params)
|
||||||
|
}, opts)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合约下单
|
||||||
|
func (e *FuturesResetV2) OrderPlace(apiUserInfo *DbModels.LineApiUser, params FutOrderPlace) error {
|
||||||
|
if err := params.CheckParams(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
orderType := strings.ToUpper(params.OrderType)
|
||||||
|
side := strings.ToUpper(params.Side)
|
||||||
|
|
||||||
|
paramsMaps := map[string]string{
|
||||||
|
"symbol": params.Symbol,
|
||||||
|
"side": side,
|
||||||
|
"type": orderType,
|
||||||
|
"newClientOrderId": params.NewClientOrderId,
|
||||||
|
"positionSide": params.PositionSide,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置市价和限价等类型的参数
|
||||||
|
switch orderType {
|
||||||
|
case "LIMIT":
|
||||||
|
paramsMaps["price"] = params.Price.String()
|
||||||
|
paramsMaps["timeInForce"] = "GTC"
|
||||||
|
case "TAKE_PROFIT_MARKET":
|
||||||
|
paramsMaps["timeInForce"] = "GTC"
|
||||||
|
paramsMaps["stopprice"] = params.Profit.String()
|
||||||
|
paramsMaps["workingType"] = "MARK_PRICE"
|
||||||
|
|
||||||
|
if params.ClosePosition {
|
||||||
|
paramsMaps["closePosition"] = "true"
|
||||||
|
}
|
||||||
|
case "TAKE_PROFIT":
|
||||||
|
paramsMaps["price"] = params.Price.String()
|
||||||
|
paramsMaps["stopprice"] = params.Profit.String()
|
||||||
|
paramsMaps["timeInForce"] = "GTC"
|
||||||
|
case "STOP_MARKET":
|
||||||
|
paramsMaps["stopprice"] = params.StopPrice.String()
|
||||||
|
paramsMaps["workingType"] = "MARK_PRICE"
|
||||||
|
paramsMaps["timeInForce"] = "GTC"
|
||||||
|
|
||||||
|
if params.ClosePosition {
|
||||||
|
paramsMaps["closePosition"] = "true"
|
||||||
|
}
|
||||||
|
case "STOP":
|
||||||
|
paramsMaps["price"] = params.Price.String()
|
||||||
|
paramsMaps["stopprice"] = params.StopPrice.String()
|
||||||
|
paramsMaps["workingType"] = "MARK_PRICE"
|
||||||
|
paramsMaps["timeInForce"] = "GTC"
|
||||||
|
}
|
||||||
|
|
||||||
|
//不是平仓
|
||||||
|
if !params.ClosePosition {
|
||||||
|
paramsMaps["quantity"] = params.Quantity.String()
|
||||||
|
}
|
||||||
|
// 获取 API 信息和发送下单请求
|
||||||
|
client := GetClient(apiUserInfo)
|
||||||
|
_, statusCode, err := client.SendFuturesRequestAuth("/fapi/v1/order", "POST", paramsMaps)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return parseOrderError(err, paramsMaps, statusCode, apiUserInfo)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拆出的错误解析函数
|
||||||
|
func parseOrderError(err error, paramsMaps map[string]string, statusCode int, apiUserInfo *DbModels.LineApiUser) error {
|
||||||
|
var dataMap map[string]interface{}
|
||||||
|
|
||||||
|
if err2 := sonic.Unmarshal([]byte(err.Error()), &dataMap); err2 == nil {
|
||||||
|
if code, ok := dataMap["code"]; ok {
|
||||||
|
paramsVal, _ := sonic.MarshalString(¶msMaps)
|
||||||
|
log.Error("下单失败 参数:", paramsVal)
|
||||||
|
errContent := FutErrorMaps[code.(float64)]
|
||||||
|
if errContent == "" {
|
||||||
|
errContent, _ = dataMap["msg"].(string)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("api_id:%d 交易对:%s 下单失败:%s", apiUserInfo.Id, paramsMaps["symbol"], errContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 特殊处理保证金不足
|
||||||
|
if strings.Contains(err.Error(), "Margin is insufficient.") {
|
||||||
|
return fmt.Errorf("api_id:%d 交易对:%s 下单失败:%s", apiUserInfo.Id, paramsMaps["symbol"], FutErrorMaps[-2019])
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("api_id:%d 交易对:%s statusCode:%v 下单失败:%s", apiUserInfo.Id, paramsMaps["symbol"], statusCode, err.Error())
|
||||||
|
}
|
||||||
@ -12,6 +12,7 @@ import (
|
|||||||
"go-admin/models/binancedto"
|
"go-admin/models/binancedto"
|
||||||
"go-admin/models/futuresdto"
|
"go-admin/models/futuresdto"
|
||||||
"go-admin/pkg/httputils"
|
"go-admin/pkg/httputils"
|
||||||
|
"go-admin/pkg/retryhelper"
|
||||||
"go-admin/pkg/utility"
|
"go-admin/pkg/utility"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -598,6 +599,49 @@ func (e FutRestApi) GetHoldeData(apiInfo *DbModels.LineApiUser, symbol, side str
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取合约 持仓价格、数量
|
||||||
|
// symbol:交易对
|
||||||
|
// positionSide:持仓方向
|
||||||
|
// holdeData:持仓数据
|
||||||
|
func (e FutRestApi) GetPositionData(apiInfo *DbModels.LineApiUser, symbol, positionSide string, holdeData *HoldeData) error {
|
||||||
|
opts := retryhelper.DefaultRetryOptions()
|
||||||
|
opts.RetryableErrFn = func(err error) bool {
|
||||||
|
if strings.Contains(err.Error(), "LOT_SIZE") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
//重试
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
holdes, err := retryhelper.RetryWithResult(func() ([]PositionRisk, error) {
|
||||||
|
return e.GetPositionV3(apiInfo, symbol)
|
||||||
|
}, opts)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range holdes {
|
||||||
|
positionAmount, _ := decimal.NewFromString(item.PositionAmt)
|
||||||
|
if (positionSide == "LONG" && item.PositionSide == "BOTH" && positionAmount.Cmp(decimal.Zero) > 0) || item.PositionSide == positionSide { //多
|
||||||
|
holdeData.AveragePrice, _ = decimal.NewFromString(item.EntryPrice)
|
||||||
|
holdeData.TotalQuantity = positionAmount.Abs()
|
||||||
|
continue
|
||||||
|
} else if (positionSide == "SHORT" && item.PositionSide == "BOTH" && positionAmount.Cmp(decimal.Zero) < 0) || item.PositionSide == positionSide { //空
|
||||||
|
holdeData.AveragePrice, _ = decimal.NewFromString(item.EntryPrice)
|
||||||
|
holdeData.TotalQuantity = positionAmount.Abs()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if holdeData.AveragePrice.Cmp(decimal.Zero) == 0 {
|
||||||
|
holdesVal, _ := sonic.MarshalString(&holdes)
|
||||||
|
log.Error("均价错误 symbol:", symbol, " 数据:", holdesVal)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// 获取代币持仓信息
|
// 获取代币持仓信息
|
||||||
func getSymbolHolde(e FutRestApi, apiInfo *DbModels.LineApiUser, symbol string, side string, holdeData *HoldeData) ([]PositionRisk, error) {
|
func getSymbolHolde(e FutRestApi, apiInfo *DbModels.LineApiUser, symbol string, side string, holdeData *HoldeData) ([]PositionRisk, error) {
|
||||||
holdes, err := e.GetPositionV3(apiInfo, symbol)
|
holdes, err := e.GetPositionV3(apiInfo, symbol)
|
||||||
@ -715,6 +759,15 @@ func (e FutRestApi) ClosePosition(symbol string, orderSn string, quantity decima
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 带重试机制的取消订单
|
||||||
|
func (e FutRestApi) CancelFutOrderRetry(apiUserInfo DbModels.LineApiUser, symbol string, newClientOrderId string) error {
|
||||||
|
opts := retryhelper.DefaultRetryOptions()
|
||||||
|
|
||||||
|
return retryhelper.Retry(func() error {
|
||||||
|
return e.CancelFutOrder(apiUserInfo, symbol, newClientOrderId)
|
||||||
|
}, opts)
|
||||||
|
}
|
||||||
|
|
||||||
// CancelFutOrder 通过单个订单号取消合约委托
|
// CancelFutOrder 通过单个订单号取消合约委托
|
||||||
// symbol 交易对
|
// symbol 交易对
|
||||||
// newClientOrderId 系统自定义订单号
|
// newClientOrderId 系统自定义订单号
|
||||||
@ -774,6 +827,27 @@ func (e FutRestApi) CancelAllFutOrder(apiUserInfo DbModels.LineApiUser, symbol s
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 带重试的批量撤销订单
|
||||||
|
func (e FutRestApi) CancelBatchFutOrderLoop(apiUserInfo DbModels.LineApiUser, symbol string, newClientOrderIdList []string) error {
|
||||||
|
opts := retryhelper.DefaultRetryOptions()
|
||||||
|
opts.RetryableErrFn = func(err error) bool {
|
||||||
|
if strings.Contains(err.Error(), "LOT_SIZE") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
//重试
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
err := retryhelper.Retry(func() error {
|
||||||
|
return e.CancelBatchFutOrder(apiUserInfo, symbol, newClientOrderIdList)
|
||||||
|
}, opts)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// CancelBatchFutOrder 批量撤销订单
|
// CancelBatchFutOrder 批量撤销订单
|
||||||
// symbol 交易对
|
// symbol 交易对
|
||||||
// newClientOrderIdList 系统自定义的订单号, 最多支持10个订单
|
// newClientOrderIdList 系统自定义的订单号, 最多支持10个订单
|
||||||
|
|||||||
@ -27,7 +27,7 @@ import (
|
|||||||
/*
|
/*
|
||||||
修改订单信息
|
修改订单信息
|
||||||
*/
|
*/
|
||||||
func ChangeFutureOrder(mapData map[string]interface{}) {
|
func ChangeFutureOrder(mapData map[string]interface{}, apiKey string) {
|
||||||
// 检查订单号是否存在
|
// 检查订单号是否存在
|
||||||
orderSn, ok := mapData["c"]
|
orderSn, ok := mapData["c"]
|
||||||
originOrderSn := mapData["C"] //取消操作 代表原始订单号
|
originOrderSn := mapData["C"] //取消操作 代表原始订单号
|
||||||
@ -61,34 +61,15 @@ func ChangeFutureOrder(mapData map[string]interface{}) {
|
|||||||
}
|
}
|
||||||
defer lock.Release()
|
defer lock.Release()
|
||||||
|
|
||||||
// 查询订单
|
//反单逻辑
|
||||||
preOrder, err := getPreOrder(db, orderSn)
|
reverseService := ReverseService{}
|
||||||
|
reverseService.Orm = db
|
||||||
|
reverseService.Log = logger.NewHelper(logger.DefaultLogger)
|
||||||
|
_, err = reverseService.ReverseOrder(apiKey, mapData)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("合约订单回调失败,查询订单失败:", orderSn, " err:", err)
|
logger.Errorf("合约订单回调失败,反单失败:%v", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析订单状态
|
|
||||||
status, ok := mapData["X"].(string)
|
|
||||||
if !ok {
|
|
||||||
mapStr, _ := sonic.Marshal(&mapData)
|
|
||||||
logger.Error("订单回调失败,没有状态:", string(mapStr))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 更新订单状态
|
|
||||||
orderStatus, reason := parseOrderStatus(status, mapData)
|
|
||||||
|
|
||||||
if orderStatus == 0 {
|
|
||||||
logger.Error("订单回调失败,状态错误:", orderSn, " status:", status, " reason:", reason)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := updateOrderStatus(db, preOrder, orderStatus, reason, true, mapData); err != nil {
|
|
||||||
logger.Error("修改订单状态失败:", orderSn, " err:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
handleFutOrderByType(db, preOrder, orderStatus)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 合约回调
|
// 合约回调
|
||||||
|
|||||||
587
services/binanceservice/integration_test_optimized.go
Normal file
587
services/binanceservice/integration_test_optimized.go
Normal file
@ -0,0 +1,587 @@
|
|||||||
|
package binanceservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-admin-team/go-admin-core/logger"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OptimizedSystemTestSuite 优化系统测试套件
|
||||||
|
type OptimizedSystemTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
db *gorm.DB
|
||||||
|
redisClient *redis.Client
|
||||||
|
config *OptimizedConfig
|
||||||
|
metricsCollector *MetricsCollector
|
||||||
|
errorHandler *ErrorHandler
|
||||||
|
lockManager *LockManager
|
||||||
|
txManager *TransactionManager
|
||||||
|
reverseService *ReverseServiceOptimized
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupSuite 设置测试套件
|
||||||
|
func (suite *OptimizedSystemTestSuite) SetupSuite() {
|
||||||
|
// 初始化数据库连接(测试环境)
|
||||||
|
dsn := "root:password@tcp(localhost:3306)/test_exchange?charset=utf8mb4&parseTime=True&loc=Local"
|
||||||
|
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
suite.T().Skip("跳过集成测试:无法连接数据库")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
suite.db = db
|
||||||
|
|
||||||
|
// 初始化Redis连接(测试环境)
|
||||||
|
suite.redisClient = redis.NewClient(&redis.Options{
|
||||||
|
Addr: "localhost:6379",
|
||||||
|
Password: "",
|
||||||
|
DB: 1, // 使用测试数据库
|
||||||
|
})
|
||||||
|
|
||||||
|
// 测试Redis连接
|
||||||
|
ctx := context.Background()
|
||||||
|
if err := suite.redisClient.Ping(ctx).Err(); err != nil {
|
||||||
|
suite.T().Skip("跳过集成测试:无法连接Redis")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化配置
|
||||||
|
suite.config = GetDefaultOptimizedConfig()
|
||||||
|
suite.config.MonitorConfig.Enabled = true
|
||||||
|
|
||||||
|
// 初始化组件
|
||||||
|
suite.metricsCollector = NewMetricsCollector(suite.config)
|
||||||
|
suite.errorHandler = NewErrorHandler(suite.config, suite.metricsCollector)
|
||||||
|
suite.lockManager = NewLockManager(suite.config, suite.redisClient, suite.metricsCollector)
|
||||||
|
suite.txManager = NewTransactionManager(suite.db, suite.metricsCollector, suite.config)
|
||||||
|
|
||||||
|
// 启动指标收集
|
||||||
|
suite.metricsCollector.Start()
|
||||||
|
|
||||||
|
// 创建测试表
|
||||||
|
suite.createTestTables()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TearDownSuite 清理测试套件
|
||||||
|
func (suite *OptimizedSystemTestSuite) TearDownSuite() {
|
||||||
|
if suite.metricsCollector != nil {
|
||||||
|
suite.metricsCollector.Stop()
|
||||||
|
}
|
||||||
|
if suite.redisClient != nil {
|
||||||
|
suite.redisClient.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupTest 设置每个测试
|
||||||
|
func (suite *OptimizedSystemTestSuite) SetupTest() {
|
||||||
|
// 清理Redis测试数据
|
||||||
|
ctx := context.Background()
|
||||||
|
suite.redisClient.FlushDB(ctx)
|
||||||
|
|
||||||
|
// 清理数据库测试数据
|
||||||
|
suite.db.Exec("DELETE FROM test_orders")
|
||||||
|
suite.db.Exec("DELETE FROM test_positions")
|
||||||
|
}
|
||||||
|
|
||||||
|
// createTestTables 创建测试表
|
||||||
|
func (suite *OptimizedSystemTestSuite) createTestTables() {
|
||||||
|
// 创建测试订单表
|
||||||
|
suite.db.Exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS test_orders (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
order_id VARCHAR(100) UNIQUE NOT NULL,
|
||||||
|
user_id VARCHAR(50) NOT NULL,
|
||||||
|
symbol VARCHAR(20) NOT NULL,
|
||||||
|
side VARCHAR(10) NOT NULL,
|
||||||
|
quantity DECIMAL(20,8) NOT NULL,
|
||||||
|
price DECIMAL(20,8),
|
||||||
|
status VARCHAR(20) NOT NULL,
|
||||||
|
version INT DEFAULT 0,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_user_symbol (user_id, symbol),
|
||||||
|
INDEX idx_status (status),
|
||||||
|
INDEX idx_version (version)
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
|
||||||
|
// 创建测试持仓表
|
||||||
|
suite.db.Exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS test_positions (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
user_id VARCHAR(50) NOT NULL,
|
||||||
|
symbol VARCHAR(20) NOT NULL,
|
||||||
|
side VARCHAR(10) NOT NULL,
|
||||||
|
quantity DECIMAL(20,8) NOT NULL,
|
||||||
|
avg_price DECIMAL(20,8),
|
||||||
|
status VARCHAR(20) NOT NULL,
|
||||||
|
version INT DEFAULT 0,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY uk_user_symbol_side (user_id, symbol, side),
|
||||||
|
INDEX idx_version (version)
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLockManager 测试锁管理器
|
||||||
|
func (suite *OptimizedSystemTestSuite) TestLockManager() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// 测试分布式锁
|
||||||
|
suite.Run("DistributedLock", func() {
|
||||||
|
config := LockManagerConfig{
|
||||||
|
Type: LockTypeDistributed,
|
||||||
|
Scope: ScopeOrder,
|
||||||
|
Key: "test_order_123",
|
||||||
|
Expiration: 10 * time.Second,
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
RetryDelay: 100 * time.Millisecond,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取锁
|
||||||
|
lock, err := suite.lockManager.AcquireLock(ctx, config)
|
||||||
|
assert.NoError(suite.T(), err)
|
||||||
|
assert.NotNil(suite.T(), lock)
|
||||||
|
assert.True(suite.T(), lock.IsLocked())
|
||||||
|
|
||||||
|
// 尝试获取同一个锁(应该失败)
|
||||||
|
config.Timeout = 1 * time.Second
|
||||||
|
_, err = suite.lockManager.AcquireLock(ctx, config)
|
||||||
|
assert.Error(suite.T(), err)
|
||||||
|
|
||||||
|
// 释放锁
|
||||||
|
err = suite.lockManager.ReleaseLock(ctx, lock)
|
||||||
|
assert.NoError(suite.T(), err)
|
||||||
|
assert.False(suite.T(), lock.IsLocked())
|
||||||
|
})
|
||||||
|
|
||||||
|
// 测试本地锁
|
||||||
|
suite.Run("LocalLock", func() {
|
||||||
|
config := LockManagerConfig{
|
||||||
|
Type: LockTypeLocal,
|
||||||
|
Scope: ScopeUser,
|
||||||
|
Key: "test_user_456",
|
||||||
|
Expiration: 10 * time.Second,
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
RetryDelay: 100 * time.Millisecond,
|
||||||
|
}
|
||||||
|
|
||||||
|
lock, err := suite.lockManager.AcquireLock(ctx, config)
|
||||||
|
assert.NoError(suite.T(), err)
|
||||||
|
assert.NotNil(suite.T(), lock)
|
||||||
|
assert.True(suite.T(), lock.IsLocked())
|
||||||
|
|
||||||
|
err = suite.lockManager.ReleaseLock(ctx, lock)
|
||||||
|
assert.NoError(suite.T(), err)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 测试并发锁
|
||||||
|
suite.Run("ConcurrentLocks", func() {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
successCount := 0
|
||||||
|
var mu sync.Mutex
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(id int) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
config := LockManagerConfig{
|
||||||
|
Type: LockTypeDistributed,
|
||||||
|
Scope: ScopeGlobal,
|
||||||
|
Key: "concurrent_test",
|
||||||
|
Expiration: 2 * time.Second,
|
||||||
|
Timeout: 1 * time.Second,
|
||||||
|
RetryDelay: 50 * time.Millisecond,
|
||||||
|
}
|
||||||
|
|
||||||
|
lock, err := suite.lockManager.AcquireLock(ctx, config)
|
||||||
|
if err == nil {
|
||||||
|
mu.Lock()
|
||||||
|
successCount++
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
suite.lockManager.ReleaseLock(ctx, lock)
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
assert.Equal(suite.T(), 1, successCount, "只有一个goroutine应该获得锁")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTransactionManager 测试事务管理器
|
||||||
|
func (suite *OptimizedSystemTestSuite) TestTransactionManager() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// 测试基本事务
|
||||||
|
suite.Run("BasicTransaction", func() {
|
||||||
|
config := GetDefaultTransactionConfig()
|
||||||
|
result := suite.txManager.WithTransaction(ctx, config, func(txCtx *TransactionContext) error {
|
||||||
|
// 插入测试数据
|
||||||
|
return txCtx.Tx.Exec(`
|
||||||
|
INSERT INTO test_orders (order_id, user_id, symbol, side, quantity, status)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
`, "order_001", "user_001", "BTCUSDT", "BUY", 1.0, "NEW").Error
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.True(suite.T(), result.Success)
|
||||||
|
assert.NoError(suite.T(), result.Error)
|
||||||
|
|
||||||
|
// 验证数据已插入
|
||||||
|
var count int64
|
||||||
|
suite.db.Raw("SELECT COUNT(*) FROM test_orders WHERE order_id = ?", "order_001").Scan(&count)
|
||||||
|
assert.Equal(suite.T(), int64(1), count)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 测试事务回滚
|
||||||
|
suite.Run("TransactionRollback", func() {
|
||||||
|
config := GetDefaultTransactionConfig()
|
||||||
|
result := suite.txManager.WithTransaction(ctx, config, func(txCtx *TransactionContext) error {
|
||||||
|
// 插入数据
|
||||||
|
if err := txCtx.Tx.Exec(`
|
||||||
|
INSERT INTO test_orders (order_id, user_id, symbol, side, quantity, status)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
`, "order_002", "user_002", "ETHUSDT", "SELL", 2.0, "NEW").Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 故意返回错误以触发回滚
|
||||||
|
return fmt.Errorf("测试回滚")
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.False(suite.T(), result.Success)
|
||||||
|
assert.Error(suite.T(), result.Error)
|
||||||
|
|
||||||
|
// 验证数据未插入
|
||||||
|
var count int64
|
||||||
|
suite.db.Raw("SELECT COUNT(*) FROM test_orders WHERE order_id = ?", "order_002").Scan(&count)
|
||||||
|
assert.Equal(suite.T(), int64(0), count)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 测试乐观锁
|
||||||
|
suite.Run("OptimisticLock", func() {
|
||||||
|
// 先插入测试数据
|
||||||
|
suite.db.Exec(`
|
||||||
|
INSERT INTO test_positions (user_id, symbol, side, quantity, avg_price, status, version)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`, "user_003", "BTCUSDT", "LONG", 1.0, 50000.0, "OPEN", 0)
|
||||||
|
|
||||||
|
// 测试正常更新
|
||||||
|
updates := map[string]interface{}{
|
||||||
|
"quantity": 2.0,
|
||||||
|
}
|
||||||
|
err := suite.txManager.UpdateWithOptimisticLock(ctx, nil, updates,
|
||||||
|
"user_id = ? AND symbol = ? AND side = ? AND version = ?",
|
||||||
|
"user_003", "BTCUSDT", "LONG", 0)
|
||||||
|
assert.NoError(suite.T(), err)
|
||||||
|
|
||||||
|
// 测试版本冲突
|
||||||
|
updates["quantity"] = 3.0
|
||||||
|
err = suite.txManager.UpdateWithOptimisticLock(ctx, nil, updates,
|
||||||
|
"user_id = ? AND symbol = ? AND side = ? AND version = ?",
|
||||||
|
"user_003", "BTCUSDT", "LONG", 0) // 使用旧版本号
|
||||||
|
assert.Error(suite.T(), err)
|
||||||
|
assert.Contains(suite.T(), err.Error(), "乐观锁冲突")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestErrorHandler 测试错误处理器
|
||||||
|
func (suite *OptimizedSystemTestSuite) TestErrorHandler() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// 测试错误分类
|
||||||
|
suite.Run("ErrorClassification", func() {
|
||||||
|
testCases := []struct {
|
||||||
|
error error
|
||||||
|
expected ErrorType
|
||||||
|
}{
|
||||||
|
{fmt.Errorf("connection refused"), ErrorTypeNetwork},
|
||||||
|
{fmt.Errorf("database connection failed"), ErrorTypeDatabase},
|
||||||
|
{fmt.Errorf("context deadline exceeded"), ErrorTypeTimeout},
|
||||||
|
{fmt.Errorf("too many requests"), ErrorTypeRateLimit},
|
||||||
|
{fmt.Errorf("unauthorized access"), ErrorTypeAuth},
|
||||||
|
{fmt.Errorf("invalid parameter"), ErrorTypeValidation},
|
||||||
|
{fmt.Errorf("insufficient balance"), ErrorTypeBusiness},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
errorInfo := suite.errorHandler.ClassifyError(tc.error, "test")
|
||||||
|
assert.Equal(suite.T(), tc.expected, errorInfo.Type)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 测试重试机制
|
||||||
|
suite.Run("RetryMechanism", func() {
|
||||||
|
attempts := 0
|
||||||
|
config := RetryConfig{
|
||||||
|
MaxRetries: 3,
|
||||||
|
RetryDelay: 10 * time.Millisecond,
|
||||||
|
MaxRetryDelay: 100 * time.Millisecond,
|
||||||
|
BackoffFactor: 2.0,
|
||||||
|
ApiRetryCount: 3,
|
||||||
|
DbRetryCount: 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试重试成功
|
||||||
|
err := suite.errorHandler.RetryWithBackoff(ctx, func() error {
|
||||||
|
attempts++
|
||||||
|
if attempts < 3 {
|
||||||
|
return fmt.Errorf("temporary error")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, config, "test_retry")
|
||||||
|
|
||||||
|
assert.NoError(suite.T(), err)
|
||||||
|
assert.Equal(suite.T(), 3, attempts)
|
||||||
|
|
||||||
|
// 测试重试失败
|
||||||
|
attempts = 0
|
||||||
|
err = suite.errorHandler.RetryWithBackoff(ctx, func() error {
|
||||||
|
attempts++
|
||||||
|
return fmt.Errorf("persistent error")
|
||||||
|
}, config, "test_retry_fail")
|
||||||
|
|
||||||
|
assert.Error(suite.T(), err)
|
||||||
|
assert.Equal(suite.T(), 3, attempts)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMetricsCollector 测试指标收集器
|
||||||
|
func (suite *OptimizedSystemTestSuite) TestMetricsCollector() {
|
||||||
|
// 测试订单指标
|
||||||
|
suite.Run("OrderMetrics", func() {
|
||||||
|
// 记录订单处理
|
||||||
|
suite.metricsCollector.RecordOrderProcessed(true, 100*time.Millisecond)
|
||||||
|
suite.metricsCollector.RecordOrderProcessed(false, 200*time.Millisecond)
|
||||||
|
suite.metricsCollector.RecordOrderCanceled()
|
||||||
|
suite.metricsCollector.RecordReverseOrder(true)
|
||||||
|
|
||||||
|
metrics := suite.metricsCollector.GetMetrics()
|
||||||
|
orderMetrics := metrics["order_metrics"].(*OrderMetrics)
|
||||||
|
|
||||||
|
assert.Equal(suite.T(), int64(2), orderMetrics.TotalProcessed)
|
||||||
|
assert.Equal(suite.T(), int64(1), orderMetrics.SuccessfulOrders)
|
||||||
|
assert.Equal(suite.T(), int64(1), orderMetrics.FailedOrders)
|
||||||
|
assert.Equal(suite.T(), int64(1), orderMetrics.CanceledOrders)
|
||||||
|
assert.Equal(suite.T(), int64(1), orderMetrics.ReverseOrdersCreated)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 测试持仓指标
|
||||||
|
suite.Run("PositionMetrics", func() {
|
||||||
|
difference := decimal.NewFromFloat(0.1)
|
||||||
|
suite.metricsCollector.RecordPositionSync(true, difference)
|
||||||
|
suite.metricsCollector.RecordPositionSync(false, decimal.Zero)
|
||||||
|
suite.metricsCollector.RecordClosePosition(false)
|
||||||
|
suite.metricsCollector.RecordClosePosition(true)
|
||||||
|
|
||||||
|
metrics := suite.metricsCollector.GetMetrics()
|
||||||
|
positionMetrics := metrics["position_metrics"].(*PositionMetrics)
|
||||||
|
|
||||||
|
assert.Equal(suite.T(), int64(2), positionMetrics.SyncAttempts)
|
||||||
|
assert.Equal(suite.T(), int64(1), positionMetrics.SyncSuccesses)
|
||||||
|
assert.Equal(suite.T(), int64(1), positionMetrics.SyncFailures)
|
||||||
|
assert.Equal(suite.T(), int64(2), positionMetrics.ClosePositions)
|
||||||
|
assert.Equal(suite.T(), int64(1), positionMetrics.ForceClosePositions)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 测试性能指标
|
||||||
|
suite.Run("PerformanceMetrics", func() {
|
||||||
|
suite.metricsCollector.RecordLockOperation(true, 50*time.Millisecond)
|
||||||
|
suite.metricsCollector.RecordDbOperation(false, 30*time.Millisecond, nil)
|
||||||
|
suite.metricsCollector.RecordApiCall(100*time.Millisecond, nil, 1)
|
||||||
|
suite.metricsCollector.RecordConcurrency(5)
|
||||||
|
|
||||||
|
metrics := suite.metricsCollector.GetMetrics()
|
||||||
|
perfMetrics := metrics["performance_metrics"].(*PerformanceMetrics)
|
||||||
|
|
||||||
|
assert.Equal(suite.T(), int64(1), perfMetrics.LockAcquisitions)
|
||||||
|
assert.Equal(suite.T(), int64(1), perfMetrics.DbQueries)
|
||||||
|
assert.Equal(suite.T(), int64(1), perfMetrics.ApiCalls)
|
||||||
|
assert.Equal(suite.T(), int64(5), perfMetrics.ConcurrentRequests)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConcurrentOperations 测试并发操作
|
||||||
|
func (suite *OptimizedSystemTestSuite) TestConcurrentOperations() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
suite.Run("ConcurrentOrderProcessing", func() {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
successCount := 0
|
||||||
|
var mu sync.Mutex
|
||||||
|
|
||||||
|
// 并发处理订单
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(id int) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
// 使用锁保护订单处理
|
||||||
|
lockConfig := GetOrderLockConfig(fmt.Sprintf("order_%d", id))
|
||||||
|
err := suite.lockManager.WithLock(ctx, lockConfig, func() error {
|
||||||
|
// 模拟订单处理
|
||||||
|
config := GetDefaultTransactionConfig()
|
||||||
|
result := suite.txManager.WithTransaction(ctx, config, func(txCtx *TransactionContext) error {
|
||||||
|
return txCtx.Tx.Exec(`
|
||||||
|
INSERT INTO test_orders (order_id, user_id, symbol, side, quantity, status)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
`, fmt.Sprintf("concurrent_order_%d", id), fmt.Sprintf("user_%d", id),
|
||||||
|
"BTCUSDT", "BUY", 1.0, "NEW").Error
|
||||||
|
})
|
||||||
|
|
||||||
|
if result.Success {
|
||||||
|
mu.Lock()
|
||||||
|
successCount++
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Error
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("并发订单处理失败 [%d]: %v", id, err)
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
assert.Equal(suite.T(), 10, successCount, "所有并发订单都应该成功处理")
|
||||||
|
|
||||||
|
// 验证数据库中的记录数
|
||||||
|
var count int64
|
||||||
|
suite.db.Raw("SELECT COUNT(*) FROM test_orders WHERE order_id LIKE 'concurrent_order_%'").Scan(&count)
|
||||||
|
assert.Equal(suite.T(), int64(10), count)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSystemIntegration 测试系统集成
|
||||||
|
func (suite *OptimizedSystemTestSuite) TestSystemIntegration() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
suite.Run("CompleteOrderFlow", func() {
|
||||||
|
userID := "integration_user"
|
||||||
|
symbol := "BTCUSDT"
|
||||||
|
orderID := "integration_order_001"
|
||||||
|
|
||||||
|
// 步骤1:创建订单(使用锁和事务)
|
||||||
|
err := WithOrderLock(ctx, orderID, func() error {
|
||||||
|
return WithDefaultTransaction(ctx, func(txCtx *TransactionContext) error {
|
||||||
|
return txCtx.Tx.Exec(`
|
||||||
|
INSERT INTO test_orders (order_id, user_id, symbol, side, quantity, price, status)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`, orderID, userID, symbol, "BUY", 1.0, 50000.0, "NEW").Error
|
||||||
|
})
|
||||||
|
})
|
||||||
|
assert.NoError(suite.T(), err)
|
||||||
|
|
||||||
|
// 步骤2:更新订单状态(使用乐观锁)
|
||||||
|
updates := map[string]interface{}{
|
||||||
|
"status": "FILLED",
|
||||||
|
}
|
||||||
|
err = UpdateWithOptimisticLockGlobal(ctx, nil, updates,
|
||||||
|
"order_id = ? AND version = ?", orderID, 0)
|
||||||
|
assert.NoError(suite.T(), err)
|
||||||
|
|
||||||
|
// 步骤3:创建持仓(使用锁和事务)
|
||||||
|
err = WithPositionLock(ctx, userID, symbol, func() error {
|
||||||
|
return WithDefaultTransaction(ctx, func(txCtx *TransactionContext) error {
|
||||||
|
return txCtx.Tx.Exec(`
|
||||||
|
INSERT INTO test_positions (user_id, symbol, side, quantity, avg_price, status)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
quantity = quantity + VALUES(quantity),
|
||||||
|
avg_price = (avg_price * quantity + VALUES(avg_price) * VALUES(quantity)) / (quantity + VALUES(quantity)),
|
||||||
|
version = version + 1
|
||||||
|
`, userID, symbol, "LONG", 1.0, 50000.0, "OPEN").Error
|
||||||
|
})
|
||||||
|
})
|
||||||
|
assert.NoError(suite.T(), err)
|
||||||
|
|
||||||
|
// 验证最终状态
|
||||||
|
var orderStatus string
|
||||||
|
suite.db.Raw("SELECT status FROM test_orders WHERE order_id = ?", orderID).Scan(&orderStatus)
|
||||||
|
assert.Equal(suite.T(), "FILLED", orderStatus)
|
||||||
|
|
||||||
|
var positionQuantity float64
|
||||||
|
suite.db.Raw("SELECT quantity FROM test_positions WHERE user_id = ? AND symbol = ?",
|
||||||
|
userID, symbol).Scan(&positionQuantity)
|
||||||
|
assert.Equal(suite.T(), 1.0, positionQuantity)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPerformance 测试性能
|
||||||
|
func (suite *OptimizedSystemTestSuite) TestPerformance() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
suite.Run("LockPerformance", func() {
|
||||||
|
startTime := time.Now()
|
||||||
|
iterations := 1000
|
||||||
|
|
||||||
|
for i := 0; i < iterations; i++ {
|
||||||
|
lockConfig := GetOrderLockConfig(fmt.Sprintf("perf_order_%d", i))
|
||||||
|
lock, err := suite.lockManager.AcquireLock(ctx, lockConfig)
|
||||||
|
assert.NoError(suite.T(), err)
|
||||||
|
suite.lockManager.ReleaseLock(ctx, lock)
|
||||||
|
}
|
||||||
|
|
||||||
|
duration := time.Since(startTime)
|
||||||
|
avgDuration := duration / time.Duration(iterations)
|
||||||
|
logger.Infof("锁性能测试: %d 次操作耗时 %v,平均每次 %v", iterations, duration, avgDuration)
|
||||||
|
|
||||||
|
// 平均每次操作应该在合理范围内
|
||||||
|
assert.Less(suite.T(), avgDuration, 10*time.Millisecond)
|
||||||
|
})
|
||||||
|
|
||||||
|
suite.Run("TransactionPerformance", func() {
|
||||||
|
startTime := time.Now()
|
||||||
|
iterations := 100
|
||||||
|
|
||||||
|
for i := 0; i < iterations; i++ {
|
||||||
|
config := GetDefaultTransactionConfig()
|
||||||
|
result := suite.txManager.WithTransaction(ctx, config, func(txCtx *TransactionContext) error {
|
||||||
|
return txCtx.Tx.Exec(`
|
||||||
|
INSERT INTO test_orders (order_id, user_id, symbol, side, quantity, status)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
`, fmt.Sprintf("perf_order_%d", i), "perf_user", "BTCUSDT", "BUY", 1.0, "NEW").Error
|
||||||
|
})
|
||||||
|
assert.True(suite.T(), result.Success)
|
||||||
|
}
|
||||||
|
|
||||||
|
duration := time.Since(startTime)
|
||||||
|
avgDuration := duration / time.Duration(iterations)
|
||||||
|
logger.Infof("事务性能测试: %d 次操作耗时 %v,平均每次 %v", iterations, duration, avgDuration)
|
||||||
|
|
||||||
|
// 平均每次事务应该在合理范围内
|
||||||
|
assert.Less(suite.T(), avgDuration, 100*time.Millisecond)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSuite 运行测试套件
|
||||||
|
func TestOptimizedSystemSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(OptimizedSystemTestSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkLockManager 锁管理器基准测试
|
||||||
|
func BenchmarkLockManager(b *testing.B) {
|
||||||
|
// 这里可以添加基准测试
|
||||||
|
b.Skip("基准测试需要Redis连接")
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkTransactionManager 事务管理器基准测试
|
||||||
|
func BenchmarkTransactionManager(b *testing.B) {
|
||||||
|
// 这里可以添加基准测试
|
||||||
|
b.Skip("基准测试需要数据库连接")
|
||||||
|
}
|
||||||
575
services/binanceservice/lock_manager_optimized.go
Normal file
575
services/binanceservice/lock_manager_optimized.go
Normal file
@ -0,0 +1,575 @@
|
|||||||
|
package binanceservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-admin-team/go-admin-core/logger"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LockManagerConfig 锁管理器专用配置
|
||||||
|
type LockManagerConfig struct {
|
||||||
|
Type LockType `json:"type"`
|
||||||
|
Scope LockScope `json:"scope"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Expiration time.Duration `json:"expiration"`
|
||||||
|
Timeout time.Duration `json:"timeout"`
|
||||||
|
RetryDelay time.Duration `json:"retry_delay"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LockType 锁类型
|
||||||
|
type LockType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
LockTypeLocal LockType = "local"
|
||||||
|
LockTypeDistributed LockType = "distributed"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LockScope 锁范围
|
||||||
|
type LockScope string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ScopeOrder LockScope = "order"
|
||||||
|
ScopePosition LockScope = "position"
|
||||||
|
ScopeUser LockScope = "user"
|
||||||
|
ScopeSymbol LockScope = "symbol"
|
||||||
|
ScopeGlobal LockScope = "global"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Lock 锁接口
|
||||||
|
type Lock interface {
|
||||||
|
Lock(ctx context.Context) error
|
||||||
|
Unlock(ctx context.Context) error
|
||||||
|
TryLock(ctx context.Context) (bool, error)
|
||||||
|
IsLocked() bool
|
||||||
|
GetKey() string
|
||||||
|
GetExpiration() time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalLock 本地锁
|
||||||
|
type LocalLock struct {
|
||||||
|
key string
|
||||||
|
mu *sync.RWMutex
|
||||||
|
locked bool
|
||||||
|
expiration time.Duration
|
||||||
|
acquiredAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// DistributedLock 分布式锁
|
||||||
|
type DistributedLock struct {
|
||||||
|
key string
|
||||||
|
value string
|
||||||
|
expiration time.Duration
|
||||||
|
redisClient *redis.Client
|
||||||
|
locked bool
|
||||||
|
acquiredAt time.Time
|
||||||
|
refreshTicker *time.Ticker
|
||||||
|
stopRefresh chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LockManager 锁管理器
|
||||||
|
type LockManager struct {
|
||||||
|
config *OptimizedConfig
|
||||||
|
redisClient *redis.Client
|
||||||
|
localLocks map[string]*LocalLock
|
||||||
|
localMutex sync.RWMutex
|
||||||
|
metrics *MetricsCollector
|
||||||
|
}
|
||||||
|
|
||||||
|
// LockConfig 已在 config_optimized.go 中定义
|
||||||
|
|
||||||
|
// NewLockManager 创建锁管理器
|
||||||
|
func NewLockManager(config *OptimizedConfig, redisClient *redis.Client, metrics *MetricsCollector) *LockManager {
|
||||||
|
return &LockManager{
|
||||||
|
config: config,
|
||||||
|
redisClient: redisClient,
|
||||||
|
localLocks: make(map[string]*LocalLock),
|
||||||
|
metrics: metrics,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateLock 创建锁
|
||||||
|
func (lm *LockManager) CreateLock(config LockManagerConfig) Lock {
|
||||||
|
switch config.Type {
|
||||||
|
case LockTypeDistributed:
|
||||||
|
return lm.createDistributedLock(config)
|
||||||
|
case LockTypeLocal:
|
||||||
|
return lm.createLocalLock(config)
|
||||||
|
default:
|
||||||
|
// 默认使用分布式锁
|
||||||
|
return lm.createDistributedLock(config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// createLocalLock 创建本地锁
|
||||||
|
func (lm *LockManager) createLocalLock(config LockManagerConfig) *LocalLock {
|
||||||
|
lm.localMutex.Lock()
|
||||||
|
defer lm.localMutex.Unlock()
|
||||||
|
|
||||||
|
key := lm.buildLockKey(config.Scope, config.Key)
|
||||||
|
if lock, exists := lm.localLocks[key]; exists {
|
||||||
|
return lock
|
||||||
|
}
|
||||||
|
|
||||||
|
lock := &LocalLock{
|
||||||
|
key: key,
|
||||||
|
mu: &sync.RWMutex{},
|
||||||
|
expiration: config.Expiration,
|
||||||
|
}
|
||||||
|
|
||||||
|
lm.localLocks[key] = lock
|
||||||
|
return lock
|
||||||
|
}
|
||||||
|
|
||||||
|
// createDistributedLock 创建分布式锁
|
||||||
|
func (lm *LockManager) createDistributedLock(config LockManagerConfig) *DistributedLock {
|
||||||
|
key := lm.buildLockKey(config.Scope, config.Key)
|
||||||
|
value := fmt.Sprintf("%d_%s", time.Now().UnixNano(), generateRandomString(8))
|
||||||
|
|
||||||
|
return &DistributedLock{
|
||||||
|
key: key,
|
||||||
|
value: value,
|
||||||
|
expiration: config.Expiration,
|
||||||
|
redisClient: lm.redisClient,
|
||||||
|
stopRefresh: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildLockKey 构建锁键名
|
||||||
|
func (lm *LockManager) buildLockKey(scope LockScope, key string) string {
|
||||||
|
return fmt.Sprintf("lock:%s:%s", scope, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AcquireLock 获取锁(带超时和重试)
|
||||||
|
func (lm *LockManager) AcquireLock(ctx context.Context, config LockManagerConfig) (Lock, error) {
|
||||||
|
lock := lm.CreateLock(config)
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
|
// 设置超时上下文
|
||||||
|
lockCtx := ctx
|
||||||
|
if config.Timeout > 0 {
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
lockCtx, cancel = context.WithTimeout(ctx, config.Timeout)
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重试获取锁
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-lockCtx.Done():
|
||||||
|
waitTime := time.Since(startTime)
|
||||||
|
if lm.metrics != nil {
|
||||||
|
lm.metrics.RecordLockOperation(false, waitTime)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("获取锁超时: %s", config.Key)
|
||||||
|
|
||||||
|
default:
|
||||||
|
err := lock.Lock(lockCtx)
|
||||||
|
if err == nil {
|
||||||
|
waitTime := time.Since(startTime)
|
||||||
|
if lm.metrics != nil {
|
||||||
|
lm.metrics.RecordLockOperation(true, waitTime)
|
||||||
|
}
|
||||||
|
return lock, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待重试
|
||||||
|
select {
|
||||||
|
case <-lockCtx.Done():
|
||||||
|
return nil, lockCtx.Err()
|
||||||
|
case <-time.After(config.RetryDelay):
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReleaseLock 释放锁
|
||||||
|
func (lm *LockManager) ReleaseLock(ctx context.Context, lock Lock) error {
|
||||||
|
if lock == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return lock.Unlock(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLock 使用锁执行操作
|
||||||
|
func (lm *LockManager) WithLock(ctx context.Context, config LockManagerConfig, operation func() error) error {
|
||||||
|
lock, err := lm.AcquireLock(ctx, config)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("获取锁失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if unlockErr := lm.ReleaseLock(ctx, lock); unlockErr != nil {
|
||||||
|
logger.Errorf("释放锁失败 [%s]: %v", config.Key, unlockErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return operation()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanupExpiredLocks 清理过期的本地锁
|
||||||
|
func (lm *LockManager) CleanupExpiredLocks() {
|
||||||
|
lm.localMutex.Lock()
|
||||||
|
defer lm.localMutex.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
for key, lock := range lm.localLocks {
|
||||||
|
if lock.locked && lock.expiration > 0 && now.Sub(lock.acquiredAt) > lock.expiration {
|
||||||
|
lock.locked = false
|
||||||
|
logger.Warnf("本地锁已过期并被清理: %s", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLockStatus 获取锁状态
|
||||||
|
func (lm *LockManager) GetLockStatus() map[string]interface{} {
|
||||||
|
lm.localMutex.RLock()
|
||||||
|
defer lm.localMutex.RUnlock()
|
||||||
|
|
||||||
|
status := make(map[string]interface{})
|
||||||
|
localLocks := make(map[string]interface{})
|
||||||
|
|
||||||
|
for key, lock := range lm.localLocks {
|
||||||
|
localLocks[key] = map[string]interface{}{
|
||||||
|
"locked": lock.locked,
|
||||||
|
"acquired_at": lock.acquiredAt,
|
||||||
|
"expiration": lock.expiration,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
status["local_locks"] = localLocks
|
||||||
|
status["total_local_locks"] = len(lm.localLocks)
|
||||||
|
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalLock 实现
|
||||||
|
|
||||||
|
// Lock 获取本地锁
|
||||||
|
func (ll *LocalLock) Lock(ctx context.Context) error {
|
||||||
|
ll.mu.Lock()
|
||||||
|
defer ll.mu.Unlock()
|
||||||
|
|
||||||
|
if ll.locked {
|
||||||
|
return fmt.Errorf("锁已被占用: %s", ll.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
ll.locked = true
|
||||||
|
ll.acquiredAt = time.Now()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlock 释放本地锁
|
||||||
|
func (ll *LocalLock) Unlock(ctx context.Context) error {
|
||||||
|
ll.mu.Lock()
|
||||||
|
defer ll.mu.Unlock()
|
||||||
|
|
||||||
|
if !ll.locked {
|
||||||
|
return fmt.Errorf("锁未被占用: %s", ll.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
ll.locked = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TryLock 尝试获取本地锁
|
||||||
|
func (ll *LocalLock) TryLock(ctx context.Context) (bool, error) {
|
||||||
|
ll.mu.Lock()
|
||||||
|
defer ll.mu.Unlock()
|
||||||
|
|
||||||
|
if ll.locked {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ll.locked = true
|
||||||
|
ll.acquiredAt = time.Now()
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsLocked 检查本地锁是否被占用
|
||||||
|
func (ll *LocalLock) IsLocked() bool {
|
||||||
|
ll.mu.RLock()
|
||||||
|
defer ll.mu.RUnlock()
|
||||||
|
return ll.locked
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetKey 获取锁键名
|
||||||
|
func (ll *LocalLock) GetKey() string {
|
||||||
|
return ll.key
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetExpiration 获取过期时间
|
||||||
|
func (ll *LocalLock) GetExpiration() time.Duration {
|
||||||
|
return ll.expiration
|
||||||
|
}
|
||||||
|
|
||||||
|
// DistributedLock 实现
|
||||||
|
|
||||||
|
// Lock 获取分布式锁
|
||||||
|
func (dl *DistributedLock) Lock(ctx context.Context) error {
|
||||||
|
// 使用 SET key value EX seconds NX 命令
|
||||||
|
result := dl.redisClient.SetNX(ctx, dl.key, dl.value, dl.expiration)
|
||||||
|
if result.Err() != nil {
|
||||||
|
return fmt.Errorf("获取分布式锁失败: %w", result.Err())
|
||||||
|
}
|
||||||
|
|
||||||
|
if !result.Val() {
|
||||||
|
return fmt.Errorf("分布式锁已被占用: %s", dl.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
dl.locked = true
|
||||||
|
dl.acquiredAt = time.Now()
|
||||||
|
|
||||||
|
// 启动锁续期
|
||||||
|
dl.startRefresh()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlock 释放分布式锁
|
||||||
|
func (dl *DistributedLock) Unlock(ctx context.Context) error {
|
||||||
|
if !dl.locked {
|
||||||
|
return fmt.Errorf("分布式锁未被占用: %s", dl.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止续期
|
||||||
|
dl.stopRefreshProcess()
|
||||||
|
|
||||||
|
// 使用 Lua 脚本确保只删除自己的锁
|
||||||
|
luaScript := `
|
||||||
|
if redis.call("get", KEYS[1]) == ARGV[1] then
|
||||||
|
return redis.call("del", KEYS[1])
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
`
|
||||||
|
|
||||||
|
result := dl.redisClient.Eval(ctx, luaScript, []string{dl.key}, dl.value)
|
||||||
|
if result.Err() != nil {
|
||||||
|
return fmt.Errorf("释放分布式锁失败: %w", result.Err())
|
||||||
|
}
|
||||||
|
|
||||||
|
dl.locked = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TryLock 尝试获取分布式锁
|
||||||
|
func (dl *DistributedLock) TryLock(ctx context.Context) (bool, error) {
|
||||||
|
result := dl.redisClient.SetNX(ctx, dl.key, dl.value, dl.expiration)
|
||||||
|
if result.Err() != nil {
|
||||||
|
return false, fmt.Errorf("尝试获取分布式锁失败: %w", result.Err())
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Val() {
|
||||||
|
dl.locked = true
|
||||||
|
dl.acquiredAt = time.Now()
|
||||||
|
dl.startRefresh()
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsLocked 检查分布式锁是否被占用
|
||||||
|
func (dl *DistributedLock) IsLocked() bool {
|
||||||
|
return dl.locked
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetKey 获取锁键名
|
||||||
|
func (dl *DistributedLock) GetKey() string {
|
||||||
|
return dl.key
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetExpiration 获取过期时间
|
||||||
|
func (dl *DistributedLock) GetExpiration() time.Duration {
|
||||||
|
return dl.expiration
|
||||||
|
}
|
||||||
|
|
||||||
|
// startRefresh 启动锁续期
|
||||||
|
func (dl *DistributedLock) startRefresh() {
|
||||||
|
if dl.expiration <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每隔过期时间的1/3进行续期
|
||||||
|
refreshInterval := dl.expiration / 3
|
||||||
|
dl.refreshTicker = time.NewTicker(refreshInterval)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer dl.refreshTicker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-dl.stopRefresh:
|
||||||
|
return
|
||||||
|
case <-dl.refreshTicker.C:
|
||||||
|
if !dl.locked {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 Lua 脚本续期
|
||||||
|
luaScript := `
|
||||||
|
if redis.call("get", KEYS[1]) == ARGV[1] then
|
||||||
|
return redis.call("expire", KEYS[1], ARGV[2])
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
`
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
result := dl.redisClient.Eval(ctx, luaScript, []string{dl.key}, dl.value, int(dl.expiration.Seconds()))
|
||||||
|
if result.Err() != nil {
|
||||||
|
logger.Errorf("分布式锁续期失败 [%s]: %v", dl.key, result.Err())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Val().(int64) == 0 {
|
||||||
|
logger.Warnf("分布式锁已被其他进程占用,停止续期 [%s]", dl.key)
|
||||||
|
dl.locked = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// stopRefreshProcess 停止续期进程
|
||||||
|
func (dl *DistributedLock) stopRefreshProcess() {
|
||||||
|
if dl.refreshTicker != nil {
|
||||||
|
close(dl.stopRefresh)
|
||||||
|
dl.refreshTicker.Stop()
|
||||||
|
dl.refreshTicker = nil
|
||||||
|
dl.stopRefresh = make(chan struct{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数
|
||||||
|
|
||||||
|
// generateRandomString 生成随机字符串
|
||||||
|
func generateRandomString(length int) string {
|
||||||
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
b := make([]byte, length)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = charset[time.Now().UnixNano()%int64(len(charset))]
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预定义的锁配置
|
||||||
|
|
||||||
|
// GetOrderLockConfig 获取订单锁配置
|
||||||
|
func GetOrderLockConfig(orderID string) LockManagerConfig {
|
||||||
|
return LockManagerConfig{
|
||||||
|
Type: LockTypeDistributed,
|
||||||
|
Scope: ScopeOrder,
|
||||||
|
Key: orderID,
|
||||||
|
Expiration: 30 * time.Second,
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
RetryDelay: 100 * time.Millisecond,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPositionLockConfig 获取持仓锁配置
|
||||||
|
func GetPositionLockConfig(userID, symbol string) LockManagerConfig {
|
||||||
|
return LockManagerConfig{
|
||||||
|
Type: LockTypeDistributed,
|
||||||
|
Scope: ScopePosition,
|
||||||
|
Key: fmt.Sprintf("%s_%s", userID, symbol),
|
||||||
|
Expiration: 60 * time.Second,
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
RetryDelay: 200 * time.Millisecond,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserLockConfig 获取用户锁配置
|
||||||
|
func GetUserLockConfig(userID string) LockManagerConfig {
|
||||||
|
return LockManagerConfig{
|
||||||
|
Type: LockTypeLocal,
|
||||||
|
Scope: ScopeUser,
|
||||||
|
Key: userID,
|
||||||
|
Expiration: 30 * time.Second,
|
||||||
|
Timeout: 3 * time.Second,
|
||||||
|
RetryDelay: 50 * time.Millisecond,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSymbolLockConfig 获取交易对锁配置
|
||||||
|
func GetSymbolLockConfig(symbol string) LockManagerConfig {
|
||||||
|
return LockManagerConfig{
|
||||||
|
Type: LockTypeLocal,
|
||||||
|
Scope: ScopeSymbol,
|
||||||
|
Key: symbol,
|
||||||
|
Expiration: 15 * time.Second,
|
||||||
|
Timeout: 2 * time.Second,
|
||||||
|
RetryDelay: 25 * time.Millisecond,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGlobalLockConfig 获取全局锁配置
|
||||||
|
func GetGlobalLockConfig(operation string) LockManagerConfig {
|
||||||
|
return LockManagerConfig{
|
||||||
|
Type: LockTypeDistributed,
|
||||||
|
Scope: ScopeGlobal,
|
||||||
|
Key: operation,
|
||||||
|
Expiration: 120 * time.Second,
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
RetryDelay: 500 * time.Millisecond,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局锁管理器实例
|
||||||
|
var GlobalLockManager *LockManager
|
||||||
|
|
||||||
|
// InitLockManager 初始化锁管理器
|
||||||
|
func InitLockManager(config *OptimizedConfig, redisClient *redis.Client, metrics *MetricsCollector) {
|
||||||
|
GlobalLockManager = NewLockManager(config, redisClient, metrics)
|
||||||
|
|
||||||
|
// 启动清理过期锁的定时任务
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(time.Minute)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
GlobalLockManager.CleanupExpiredLocks()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLockManager 获取全局锁管理器
|
||||||
|
func GetLockManager() *LockManager {
|
||||||
|
return GlobalLockManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithOrderLock 使用订单锁执行操作
|
||||||
|
func WithOrderLock(ctx context.Context, orderID string, operation func() error) error {
|
||||||
|
if GlobalLockManager == nil {
|
||||||
|
return operation()
|
||||||
|
}
|
||||||
|
|
||||||
|
config := GetOrderLockConfig(orderID)
|
||||||
|
return GlobalLockManager.WithLock(ctx, config, operation)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPositionLock 使用持仓锁执行操作
|
||||||
|
func WithPositionLock(ctx context.Context, userID, symbol string, operation func() error) error {
|
||||||
|
if GlobalLockManager == nil {
|
||||||
|
return operation()
|
||||||
|
}
|
||||||
|
|
||||||
|
config := GetPositionLockConfig(userID, symbol)
|
||||||
|
return GlobalLockManager.WithLock(ctx, config, operation)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithUserLock 使用用户锁执行操作
|
||||||
|
func WithUserLock(ctx context.Context, userID string, operation func() error) error {
|
||||||
|
if GlobalLockManager == nil {
|
||||||
|
return operation()
|
||||||
|
}
|
||||||
|
|
||||||
|
config := GetUserLockConfig(userID)
|
||||||
|
return GlobalLockManager.WithLock(ctx, config, operation)
|
||||||
|
}
|
||||||
@ -61,17 +61,19 @@ type EntryPriceResult struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FutOrderPlace struct {
|
type FutOrderPlace struct {
|
||||||
ApiId int `json:"api_id"` //api用户id
|
ApiId int `json:"api_id"` //api用户id
|
||||||
Symbol string `json:"symbol"` //合约交易对
|
Symbol string `json:"symbol"` //合约交易对
|
||||||
Side string `json:"side"` //购买方向
|
Side string `json:"side"` //购买方向
|
||||||
Quantity decimal.Decimal `json:"quantity"` //数量
|
PositionSide string `json:"position_side"` //持仓方向
|
||||||
Price decimal.Decimal `json:"price"` //限价单价
|
Quantity decimal.Decimal `json:"quantity"` //数量
|
||||||
SideType string `json:"side_type"` //现价或者市价
|
Price decimal.Decimal `json:"price"` //限价单价
|
||||||
OpenOrder int `json:"open_order"` //是否开启限价单止盈止损
|
SideType string `json:"side_type"` //现价或者市价
|
||||||
Profit decimal.Decimal `json:"profit"` //止盈价格
|
OpenOrder int `json:"open_order"` //是否开启限价单止盈止损
|
||||||
StopPrice decimal.Decimal `json:"stopprice"` //止损价格
|
Profit decimal.Decimal `json:"profit"` //止盈价格
|
||||||
OrderType string `json:"order_type"` //订单类型,市价或限价MARKET(市价单) TAKE_PROFIT_MARKET(市价止盈) TAKE_PROFIT(限价止盈) STOP (限价止损) STOP_MARKET(市价止损)
|
StopPrice decimal.Decimal `json:"stopprice"` //止损价格
|
||||||
|
OrderType string `json:"order_type"` //订单类型,市价或限价MARKET(市价单) TAKE_PROFIT_MARKET(市价止盈) TAKE_PROFIT(限价止盈) STOP (限价止损) STOP_MARKET(市价止损)
|
||||||
NewClientOrderId string `json:"newClientOrderId"`
|
NewClientOrderId string `json:"newClientOrderId"`
|
||||||
|
ClosePosition bool `json:"closePosition"` //是否平仓
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s FutOrderPlace) CheckParams() error {
|
func (s FutOrderPlace) CheckParams() error {
|
||||||
|
|||||||
594
services/binanceservice/monitor_optimized.go
Normal file
594
services/binanceservice/monitor_optimized.go
Normal file
@ -0,0 +1,594 @@
|
|||||||
|
package binanceservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-admin-team/go-admin-core/logger"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MetricsCollector 指标收集器
|
||||||
|
type MetricsCollector struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
|
||||||
|
// 业务指标
|
||||||
|
OrderMetrics *OrderMetrics
|
||||||
|
PositionMetrics *PositionMetrics
|
||||||
|
TakeProfitMetrics *TakeProfitMetrics
|
||||||
|
|
||||||
|
// 技术指标
|
||||||
|
PerformanceMetrics *PerformanceMetrics
|
||||||
|
ErrorMetrics *ErrorMetrics
|
||||||
|
|
||||||
|
// 系统指标
|
||||||
|
SystemMetrics *SystemMetrics
|
||||||
|
|
||||||
|
// 配置
|
||||||
|
config *OptimizedConfig
|
||||||
|
|
||||||
|
// 控制
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
startTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// OrderMetrics 订单相关指标
|
||||||
|
type OrderMetrics struct {
|
||||||
|
// 订单处理统计
|
||||||
|
TotalProcessed int64 `json:"total_processed"`
|
||||||
|
SuccessfulOrders int64 `json:"successful_orders"`
|
||||||
|
FailedOrders int64 `json:"failed_orders"`
|
||||||
|
CanceledOrders int64 `json:"canceled_orders"`
|
||||||
|
|
||||||
|
// 反向订单统计
|
||||||
|
ReverseOrdersCreated int64 `json:"reverse_orders_created"`
|
||||||
|
ReverseOrdersFailed int64 `json:"reverse_orders_failed"`
|
||||||
|
|
||||||
|
// 响应时间统计
|
||||||
|
TotalResponseTime time.Duration `json:"total_response_time"`
|
||||||
|
MaxResponseTime time.Duration `json:"max_response_time"`
|
||||||
|
MinResponseTime time.Duration `json:"min_response_time"`
|
||||||
|
|
||||||
|
// 最近更新时间
|
||||||
|
LastUpdated time.Time `json:"last_updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PositionMetrics 持仓相关指标
|
||||||
|
type PositionMetrics struct {
|
||||||
|
// 持仓同步统计
|
||||||
|
SyncAttempts int64 `json:"sync_attempts"`
|
||||||
|
SyncSuccesses int64 `json:"sync_successes"`
|
||||||
|
SyncFailures int64 `json:"sync_failures"`
|
||||||
|
|
||||||
|
// 持仓差异统计
|
||||||
|
PositionMismatches int64 `json:"position_mismatches"`
|
||||||
|
MaxDifference decimal.Decimal `json:"max_difference"`
|
||||||
|
TotalDifference decimal.Decimal `json:"total_difference"`
|
||||||
|
|
||||||
|
// 平仓统计
|
||||||
|
ClosePositions int64 `json:"close_positions"`
|
||||||
|
ForceClosePositions int64 `json:"force_close_positions"`
|
||||||
|
|
||||||
|
LastUpdated time.Time `json:"last_updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TakeProfitMetrics 止盈止损相关指标
|
||||||
|
type TakeProfitMetrics struct {
|
||||||
|
// 止盈止损创建统计
|
||||||
|
TakeProfitCreated int64 `json:"take_profit_created"`
|
||||||
|
StopLossCreated int64 `json:"stop_loss_created"`
|
||||||
|
|
||||||
|
// 止盈止损执行统计
|
||||||
|
TakeProfitExecuted int64 `json:"take_profit_executed"`
|
||||||
|
StopLossExecuted int64 `json:"stop_loss_executed"`
|
||||||
|
|
||||||
|
// 取消统计
|
||||||
|
OrdersCanceled int64 `json:"orders_canceled"`
|
||||||
|
BatchCancelAttempts int64 `json:"batch_cancel_attempts"`
|
||||||
|
BatchCancelSuccesses int64 `json:"batch_cancel_successes"`
|
||||||
|
|
||||||
|
LastUpdated time.Time `json:"last_updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PerformanceMetrics 性能相关指标
|
||||||
|
type PerformanceMetrics struct {
|
||||||
|
// 并发统计
|
||||||
|
ConcurrentRequests int64 `json:"concurrent_requests"`
|
||||||
|
MaxConcurrency int64 `json:"max_concurrency"`
|
||||||
|
|
||||||
|
// 锁统计
|
||||||
|
LockAcquisitions int64 `json:"lock_acquisitions"`
|
||||||
|
LockWaitTime time.Duration `json:"lock_wait_time"`
|
||||||
|
LockTimeouts int64 `json:"lock_timeouts"`
|
||||||
|
|
||||||
|
// 数据库操作统计
|
||||||
|
DbQueries int64 `json:"db_queries"`
|
||||||
|
DbTransactions int64 `json:"db_transactions"`
|
||||||
|
DbQueryTime time.Duration `json:"db_query_time"`
|
||||||
|
DbErrors int64 `json:"db_errors"`
|
||||||
|
|
||||||
|
// API调用统计
|
||||||
|
ApiCalls int64 `json:"api_calls"`
|
||||||
|
ApiCallTime time.Duration `json:"api_call_time"`
|
||||||
|
ApiErrors int64 `json:"api_errors"`
|
||||||
|
ApiRetries int64 `json:"api_retries"`
|
||||||
|
|
||||||
|
LastUpdated time.Time `json:"last_updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorMetrics 错误相关指标
|
||||||
|
type ErrorMetrics struct {
|
||||||
|
// 错误分类统计
|
||||||
|
NetworkErrors int64 `json:"network_errors"`
|
||||||
|
DatabaseErrors int64 `json:"database_errors"`
|
||||||
|
BusinessErrors int64 `json:"business_errors"`
|
||||||
|
SystemErrors int64 `json:"system_errors"`
|
||||||
|
|
||||||
|
// 重试统计
|
||||||
|
RetryAttempts int64 `json:"retry_attempts"`
|
||||||
|
RetrySuccesses int64 `json:"retry_successes"`
|
||||||
|
RetryFailures int64 `json:"retry_failures"`
|
||||||
|
|
||||||
|
// 最近错误
|
||||||
|
RecentErrors []ErrorRecord `json:"recent_errors"`
|
||||||
|
|
||||||
|
LastUpdated time.Time `json:"last_updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SystemMetrics 系统相关指标
|
||||||
|
type SystemMetrics struct {
|
||||||
|
// 内存使用
|
||||||
|
MemoryUsage uint64 `json:"memory_usage"`
|
||||||
|
MemoryPercent float64 `json:"memory_percent"`
|
||||||
|
|
||||||
|
// CPU使用
|
||||||
|
CpuPercent float64 `json:"cpu_percent"`
|
||||||
|
|
||||||
|
// Goroutine统计
|
||||||
|
GoroutineCount int `json:"goroutine_count"`
|
||||||
|
|
||||||
|
// GC统计
|
||||||
|
GcCount uint32 `json:"gc_count"`
|
||||||
|
GcPauseMs uint64 `json:"gc_pause_ms"`
|
||||||
|
|
||||||
|
LastUpdated time.Time `json:"last_updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorRecord 错误记录
|
||||||
|
type ErrorRecord struct {
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Context string `json:"context"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMetricsCollector 创建新的指标收集器
|
||||||
|
func NewMetricsCollector(config *OptimizedConfig) *MetricsCollector {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
return &MetricsCollector{
|
||||||
|
OrderMetrics: &OrderMetrics{
|
||||||
|
MinResponseTime: time.Hour, // 初始化为一个大值
|
||||||
|
LastUpdated: time.Now(),
|
||||||
|
},
|
||||||
|
PositionMetrics: &PositionMetrics{
|
||||||
|
MaxDifference: decimal.Zero,
|
||||||
|
TotalDifference: decimal.Zero,
|
||||||
|
LastUpdated: time.Now(),
|
||||||
|
},
|
||||||
|
TakeProfitMetrics: &TakeProfitMetrics{
|
||||||
|
LastUpdated: time.Now(),
|
||||||
|
},
|
||||||
|
PerformanceMetrics: &PerformanceMetrics{
|
||||||
|
LastUpdated: time.Now(),
|
||||||
|
},
|
||||||
|
ErrorMetrics: &ErrorMetrics{
|
||||||
|
RecentErrors: make([]ErrorRecord, 0, 100),
|
||||||
|
LastUpdated: time.Now(),
|
||||||
|
},
|
||||||
|
SystemMetrics: &SystemMetrics{
|
||||||
|
LastUpdated: time.Now(),
|
||||||
|
},
|
||||||
|
config: config,
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
startTime: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start 启动指标收集
|
||||||
|
func (mc *MetricsCollector) Start() {
|
||||||
|
if !mc.config.MonitorConfig.Enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("启动指标收集器")
|
||||||
|
|
||||||
|
// 启动定期收集
|
||||||
|
go mc.collectLoop()
|
||||||
|
|
||||||
|
// 启动告警检查
|
||||||
|
go mc.alertLoop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop 停止指标收集
|
||||||
|
func (mc *MetricsCollector) Stop() {
|
||||||
|
logger.Info("停止指标收集器")
|
||||||
|
mc.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectLoop 收集循环
|
||||||
|
func (mc *MetricsCollector) collectLoop() {
|
||||||
|
ticker := time.NewTicker(mc.config.MonitorConfig.CollectInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-mc.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
mc.collectSystemMetrics()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// alertLoop 告警检查循环
|
||||||
|
func (mc *MetricsCollector) alertLoop() {
|
||||||
|
ticker := time.NewTicker(time.Minute) // 每分钟检查一次
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-mc.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
mc.checkAlerts()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordOrderProcessed 记录订单处理
|
||||||
|
func (mc *MetricsCollector) RecordOrderProcessed(success bool, responseTime time.Duration) {
|
||||||
|
mc.mu.Lock()
|
||||||
|
defer mc.mu.Unlock()
|
||||||
|
|
||||||
|
atomic.AddInt64(&mc.OrderMetrics.TotalProcessed, 1)
|
||||||
|
|
||||||
|
if success {
|
||||||
|
atomic.AddInt64(&mc.OrderMetrics.SuccessfulOrders, 1)
|
||||||
|
} else {
|
||||||
|
atomic.AddInt64(&mc.OrderMetrics.FailedOrders, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新响应时间统计
|
||||||
|
mc.OrderMetrics.TotalResponseTime += responseTime
|
||||||
|
if responseTime > mc.OrderMetrics.MaxResponseTime {
|
||||||
|
mc.OrderMetrics.MaxResponseTime = responseTime
|
||||||
|
}
|
||||||
|
if responseTime < mc.OrderMetrics.MinResponseTime {
|
||||||
|
mc.OrderMetrics.MinResponseTime = responseTime
|
||||||
|
}
|
||||||
|
|
||||||
|
mc.OrderMetrics.LastUpdated = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordOrderCanceled 记录订单取消
|
||||||
|
func (mc *MetricsCollector) RecordOrderCanceled() {
|
||||||
|
atomic.AddInt64(&mc.OrderMetrics.CanceledOrders, 1)
|
||||||
|
mc.OrderMetrics.LastUpdated = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordReverseOrder 记录反向订单
|
||||||
|
func (mc *MetricsCollector) RecordReverseOrder(success bool) {
|
||||||
|
if success {
|
||||||
|
atomic.AddInt64(&mc.OrderMetrics.ReverseOrdersCreated, 1)
|
||||||
|
} else {
|
||||||
|
atomic.AddInt64(&mc.OrderMetrics.ReverseOrdersFailed, 1)
|
||||||
|
}
|
||||||
|
mc.OrderMetrics.LastUpdated = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordPositionSync 记录持仓同步
|
||||||
|
func (mc *MetricsCollector) RecordPositionSync(success bool, difference decimal.Decimal) {
|
||||||
|
mc.mu.Lock()
|
||||||
|
defer mc.mu.Unlock()
|
||||||
|
|
||||||
|
atomic.AddInt64(&mc.PositionMetrics.SyncAttempts, 1)
|
||||||
|
|
||||||
|
if success {
|
||||||
|
atomic.AddInt64(&mc.PositionMetrics.SyncSuccesses, 1)
|
||||||
|
} else {
|
||||||
|
atomic.AddInt64(&mc.PositionMetrics.SyncFailures, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新差异统计
|
||||||
|
if !difference.IsZero() {
|
||||||
|
atomic.AddInt64(&mc.PositionMetrics.PositionMismatches, 1)
|
||||||
|
mc.PositionMetrics.TotalDifference = mc.PositionMetrics.TotalDifference.Add(difference.Abs())
|
||||||
|
|
||||||
|
if difference.Abs().GreaterThan(mc.PositionMetrics.MaxDifference) {
|
||||||
|
mc.PositionMetrics.MaxDifference = difference.Abs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mc.PositionMetrics.LastUpdated = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordClosePosition 记录平仓
|
||||||
|
func (mc *MetricsCollector) RecordClosePosition(forced bool) {
|
||||||
|
atomic.AddInt64(&mc.PositionMetrics.ClosePositions, 1)
|
||||||
|
if forced {
|
||||||
|
atomic.AddInt64(&mc.PositionMetrics.ForceClosePositions, 1)
|
||||||
|
}
|
||||||
|
mc.PositionMetrics.LastUpdated = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordTakeProfitCreated 记录止盈止损创建
|
||||||
|
func (mc *MetricsCollector) RecordTakeProfitCreated(orderType string) {
|
||||||
|
switch orderType {
|
||||||
|
case "TAKE_PROFIT", "TAKE_PROFIT_MARKET":
|
||||||
|
atomic.AddInt64(&mc.TakeProfitMetrics.TakeProfitCreated, 1)
|
||||||
|
case "STOP", "STOP_MARKET":
|
||||||
|
atomic.AddInt64(&mc.TakeProfitMetrics.StopLossCreated, 1)
|
||||||
|
}
|
||||||
|
mc.TakeProfitMetrics.LastUpdated = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordTakeProfitExecuted 记录止盈止损执行
|
||||||
|
func (mc *MetricsCollector) RecordTakeProfitExecuted(orderType string) {
|
||||||
|
switch orderType {
|
||||||
|
case "TAKE_PROFIT", "TAKE_PROFIT_MARKET":
|
||||||
|
atomic.AddInt64(&mc.TakeProfitMetrics.TakeProfitExecuted, 1)
|
||||||
|
case "STOP", "STOP_MARKET":
|
||||||
|
atomic.AddInt64(&mc.TakeProfitMetrics.StopLossExecuted, 1)
|
||||||
|
}
|
||||||
|
mc.TakeProfitMetrics.LastUpdated = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordOrdersCanceled 记录订单取消
|
||||||
|
func (mc *MetricsCollector) RecordOrdersCanceled(count int) {
|
||||||
|
atomic.AddInt64(&mc.TakeProfitMetrics.OrdersCanceled, int64(count))
|
||||||
|
mc.TakeProfitMetrics.LastUpdated = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordBatchCancel 记录批量取消
|
||||||
|
func (mc *MetricsCollector) RecordBatchCancel(success bool) {
|
||||||
|
atomic.AddInt64(&mc.TakeProfitMetrics.BatchCancelAttempts, 1)
|
||||||
|
if success {
|
||||||
|
atomic.AddInt64(&mc.TakeProfitMetrics.BatchCancelSuccesses, 1)
|
||||||
|
}
|
||||||
|
mc.TakeProfitMetrics.LastUpdated = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordLockOperation 记录锁操作
|
||||||
|
func (mc *MetricsCollector) RecordLockOperation(acquired bool, waitTime time.Duration) {
|
||||||
|
atomic.AddInt64(&mc.PerformanceMetrics.LockAcquisitions, 1)
|
||||||
|
mc.PerformanceMetrics.LockWaitTime += waitTime
|
||||||
|
|
||||||
|
if !acquired {
|
||||||
|
atomic.AddInt64(&mc.PerformanceMetrics.LockTimeouts, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
mc.PerformanceMetrics.LastUpdated = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordDbOperation 记录数据库操作
|
||||||
|
func (mc *MetricsCollector) RecordDbOperation(isTransaction bool, duration time.Duration, err error) {
|
||||||
|
if isTransaction {
|
||||||
|
atomic.AddInt64(&mc.PerformanceMetrics.DbTransactions, 1)
|
||||||
|
} else {
|
||||||
|
atomic.AddInt64(&mc.PerformanceMetrics.DbQueries, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
mc.PerformanceMetrics.DbQueryTime += duration
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
atomic.AddInt64(&mc.PerformanceMetrics.DbErrors, 1)
|
||||||
|
mc.RecordError("database", err.Error(), "db_operation")
|
||||||
|
}
|
||||||
|
|
||||||
|
mc.PerformanceMetrics.LastUpdated = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordApiCall 记录API调用
|
||||||
|
func (mc *MetricsCollector) RecordApiCall(duration time.Duration, err error, retryCount int) {
|
||||||
|
atomic.AddInt64(&mc.PerformanceMetrics.ApiCalls, 1)
|
||||||
|
mc.PerformanceMetrics.ApiCallTime += duration
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
atomic.AddInt64(&mc.PerformanceMetrics.ApiErrors, 1)
|
||||||
|
mc.RecordError("api", err.Error(), "api_call")
|
||||||
|
}
|
||||||
|
|
||||||
|
if retryCount > 0 {
|
||||||
|
atomic.AddInt64(&mc.PerformanceMetrics.ApiRetries, int64(retryCount))
|
||||||
|
}
|
||||||
|
|
||||||
|
mc.PerformanceMetrics.LastUpdated = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordConcurrency 记录并发数
|
||||||
|
func (mc *MetricsCollector) RecordConcurrency(current int64) {
|
||||||
|
atomic.StoreInt64(&mc.PerformanceMetrics.ConcurrentRequests, current)
|
||||||
|
|
||||||
|
if current > atomic.LoadInt64(&mc.PerformanceMetrics.MaxConcurrency) {
|
||||||
|
atomic.StoreInt64(&mc.PerformanceMetrics.MaxConcurrency, current)
|
||||||
|
}
|
||||||
|
|
||||||
|
mc.PerformanceMetrics.LastUpdated = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordError 记录错误
|
||||||
|
func (mc *MetricsCollector) RecordError(errorType, message, context string) {
|
||||||
|
mc.mu.Lock()
|
||||||
|
defer mc.mu.Unlock()
|
||||||
|
|
||||||
|
// 分类统计
|
||||||
|
switch errorType {
|
||||||
|
case "network":
|
||||||
|
atomic.AddInt64(&mc.ErrorMetrics.NetworkErrors, 1)
|
||||||
|
case "database":
|
||||||
|
atomic.AddInt64(&mc.ErrorMetrics.DatabaseErrors, 1)
|
||||||
|
case "business":
|
||||||
|
atomic.AddInt64(&mc.ErrorMetrics.BusinessErrors, 1)
|
||||||
|
default:
|
||||||
|
atomic.AddInt64(&mc.ErrorMetrics.SystemErrors, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录最近错误
|
||||||
|
errorRecord := ErrorRecord{
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Type: errorType,
|
||||||
|
Message: message,
|
||||||
|
Context: context,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保持最近100个错误
|
||||||
|
if len(mc.ErrorMetrics.RecentErrors) >= 100 {
|
||||||
|
mc.ErrorMetrics.RecentErrors = mc.ErrorMetrics.RecentErrors[1:]
|
||||||
|
}
|
||||||
|
mc.ErrorMetrics.RecentErrors = append(mc.ErrorMetrics.RecentErrors, errorRecord)
|
||||||
|
|
||||||
|
mc.ErrorMetrics.LastUpdated = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordRetry 记录重试
|
||||||
|
func (mc *MetricsCollector) RecordRetry(success bool) {
|
||||||
|
atomic.AddInt64(&mc.ErrorMetrics.RetryAttempts, 1)
|
||||||
|
if success {
|
||||||
|
atomic.AddInt64(&mc.ErrorMetrics.RetrySuccesses, 1)
|
||||||
|
} else {
|
||||||
|
atomic.AddInt64(&mc.ErrorMetrics.RetryFailures, 1)
|
||||||
|
}
|
||||||
|
mc.ErrorMetrics.LastUpdated = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectSystemMetrics 收集系统指标
|
||||||
|
func (mc *MetricsCollector) collectSystemMetrics() {
|
||||||
|
mc.mu.Lock()
|
||||||
|
defer mc.mu.Unlock()
|
||||||
|
|
||||||
|
// 获取内存统计
|
||||||
|
var m runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
|
||||||
|
mc.SystemMetrics.MemoryUsage = m.Alloc
|
||||||
|
mc.SystemMetrics.GoroutineCount = runtime.NumGoroutine()
|
||||||
|
mc.SystemMetrics.GcCount = m.NumGC
|
||||||
|
mc.SystemMetrics.GcPauseMs = uint64(m.PauseNs[(m.NumGC+255)%256]) / 1000000
|
||||||
|
|
||||||
|
mc.SystemMetrics.LastUpdated = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkAlerts 检查告警
|
||||||
|
func (mc *MetricsCollector) checkAlerts() {
|
||||||
|
thresholds := mc.config.MonitorConfig.AlertThresholds
|
||||||
|
|
||||||
|
// 检查订单失败率
|
||||||
|
if mc.OrderMetrics.TotalProcessed > 0 {
|
||||||
|
failureRate := float64(mc.OrderMetrics.FailedOrders) / float64(mc.OrderMetrics.TotalProcessed) * 100
|
||||||
|
if failureRate > thresholds.OrderFailureRate {
|
||||||
|
logger.Warnf("订单失败率告警: %.2f%% (阈值: %.2f%%)", failureRate, thresholds.OrderFailureRate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查持仓同步失败率
|
||||||
|
if mc.PositionMetrics.SyncAttempts > 0 {
|
||||||
|
syncFailureRate := float64(mc.PositionMetrics.SyncFailures) / float64(mc.PositionMetrics.SyncAttempts) * 100
|
||||||
|
if syncFailureRate > thresholds.PositionSyncFailureRate {
|
||||||
|
logger.Warnf("持仓同步失败率告警: %.2f%% (阈值: %.2f%%)", syncFailureRate, thresholds.PositionSyncFailureRate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查API失败率
|
||||||
|
if mc.PerformanceMetrics.ApiCalls > 0 {
|
||||||
|
apiFailureRate := float64(mc.PerformanceMetrics.ApiErrors) / float64(mc.PerformanceMetrics.ApiCalls) * 100
|
||||||
|
if apiFailureRate > thresholds.ApiFailureRate {
|
||||||
|
logger.Warnf("API失败率告警: %.2f%% (阈值: %.2f%%)", apiFailureRate, thresholds.ApiFailureRate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查内存使用率
|
||||||
|
if mc.SystemMetrics.MemoryPercent > thresholds.MemoryUsageThreshold {
|
||||||
|
logger.Warnf("内存使用率告警: %.2f%% (阈值: %.2f%%)", mc.SystemMetrics.MemoryPercent, thresholds.MemoryUsageThreshold)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查CPU使用率
|
||||||
|
if mc.SystemMetrics.CpuPercent > thresholds.CpuUsageThreshold {
|
||||||
|
logger.Warnf("CPU使用率告警: %.2f%% (阈值: %.2f%%)", mc.SystemMetrics.CpuPercent, thresholds.CpuUsageThreshold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMetrics 获取所有指标
|
||||||
|
func (mc *MetricsCollector) GetMetrics() map[string]interface{} {
|
||||||
|
mc.mu.RLock()
|
||||||
|
defer mc.mu.RUnlock()
|
||||||
|
|
||||||
|
return map[string]interface{}{
|
||||||
|
"order_metrics": mc.OrderMetrics,
|
||||||
|
"position_metrics": mc.PositionMetrics,
|
||||||
|
"take_profit_metrics": mc.TakeProfitMetrics,
|
||||||
|
"performance_metrics": mc.PerformanceMetrics,
|
||||||
|
"error_metrics": mc.ErrorMetrics,
|
||||||
|
"system_metrics": mc.SystemMetrics,
|
||||||
|
"uptime": time.Since(mc.startTime).String(),
|
||||||
|
"collected_at": time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSummary 获取指标摘要
|
||||||
|
func (mc *MetricsCollector) GetSummary() map[string]interface{} {
|
||||||
|
mc.mu.RLock()
|
||||||
|
defer mc.mu.RUnlock()
|
||||||
|
|
||||||
|
summary := make(map[string]interface{})
|
||||||
|
|
||||||
|
// 订单摘要
|
||||||
|
if mc.OrderMetrics.TotalProcessed > 0 {
|
||||||
|
successRate := float64(mc.OrderMetrics.SuccessfulOrders) / float64(mc.OrderMetrics.TotalProcessed) * 100
|
||||||
|
avgResponseTime := mc.OrderMetrics.TotalResponseTime / time.Duration(mc.OrderMetrics.TotalProcessed)
|
||||||
|
|
||||||
|
summary["order_success_rate"] = fmt.Sprintf("%.2f%%", successRate)
|
||||||
|
summary["avg_response_time"] = avgResponseTime.String()
|
||||||
|
summary["total_orders"] = mc.OrderMetrics.TotalProcessed
|
||||||
|
}
|
||||||
|
|
||||||
|
// 持仓摘要
|
||||||
|
if mc.PositionMetrics.SyncAttempts > 0 {
|
||||||
|
syncSuccessRate := float64(mc.PositionMetrics.SyncSuccesses) / float64(mc.PositionMetrics.SyncAttempts) * 100
|
||||||
|
summary["position_sync_rate"] = fmt.Sprintf("%.2f%%", syncSuccessRate)
|
||||||
|
summary["position_mismatches"] = mc.PositionMetrics.PositionMismatches
|
||||||
|
}
|
||||||
|
|
||||||
|
// 性能摘要
|
||||||
|
summary["current_concurrency"] = mc.PerformanceMetrics.ConcurrentRequests
|
||||||
|
summary["max_concurrency"] = mc.PerformanceMetrics.MaxConcurrency
|
||||||
|
summary["total_api_calls"] = mc.PerformanceMetrics.ApiCalls
|
||||||
|
summary["total_db_queries"] = mc.PerformanceMetrics.DbQueries
|
||||||
|
|
||||||
|
// 系统摘要
|
||||||
|
summary["memory_usage_mb"] = mc.SystemMetrics.MemoryUsage / 1024 / 1024
|
||||||
|
summary["goroutine_count"] = mc.SystemMetrics.GoroutineCount
|
||||||
|
summary["uptime"] = time.Since(mc.startTime).String()
|
||||||
|
|
||||||
|
return summary
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局指标收集器实例
|
||||||
|
var GlobalMetricsCollector *MetricsCollector
|
||||||
|
|
||||||
|
// InitMetricsCollector 初始化指标收集器
|
||||||
|
func InitMetricsCollector(config *OptimizedConfig) {
|
||||||
|
GlobalMetricsCollector = NewMetricsCollector(config)
|
||||||
|
GlobalMetricsCollector.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMetricsCollector 获取全局指标收集器
|
||||||
|
func GetMetricsCollector() *MetricsCollector {
|
||||||
|
return GlobalMetricsCollector
|
||||||
|
}
|
||||||
1074
services/binanceservice/reverse_service.go
Normal file
1074
services/binanceservice/reverse_service.go
Normal file
File diff suppressed because it is too large
Load Diff
1121
services/binanceservice/reverse_service_optimized.go
Normal file
1121
services/binanceservice/reverse_service_optimized.go
Normal file
File diff suppressed because it is too large
Load Diff
34
services/binanceservice/reverse_service_test.go
Normal file
34
services/binanceservice/reverse_service_test.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package binanceservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go-admin/common/helper"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
|
"github.com/go-admin-team/go-admin-core/logger"
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk"
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initConfig() *gorm.DB {
|
||||||
|
dsn := "root:123456@tcp(127.0.0.1:3306)/go_exchange_single?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
|
||||||
|
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||||
|
sdk.Runtime.SetDb("default", db)
|
||||||
|
helper.InitDefaultRedis("127.0.0.1:6379", "", 2)
|
||||||
|
helper.InitLockRedisConn("127.0.0.1:6379", "", "2")
|
||||||
|
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
func TestReverseOrder(t *testing.T) {
|
||||||
|
db := initConfig()
|
||||||
|
mapData := make(map[string]interface{})
|
||||||
|
content := `{"s":"ADAUSDT","c":"439301585084878848","S":"SELL","o":"LIMIT","f":"GTC","q":"8","p":"0.7781","ap":"0.7843","sp":"0","x":"TRADE","X":"FILLED","i":56468022240,"l":"8","z":"8","L":"0.7843","n":"0.0031372","N":"USDT","T":1753950025820,"t":1642816051,"b":"0","a":"0","m":false,"R":false,"wt":"CONTRACT_PRICE","ot":"LIMIT","ps":"SHORT","cp":false,"rp":"0","pP":false,"si":0,"ss":0,"V":"EXPIRE_MAKER","pm":"NONE","gtd":0}`
|
||||||
|
sonic.Unmarshal([]byte(content), &mapData)
|
||||||
|
|
||||||
|
service := ReverseService{}
|
||||||
|
service.Orm = db
|
||||||
|
service.Log = logger.NewHelper(logger.DefaultLogger)
|
||||||
|
|
||||||
|
service.ReverseOrder("c8ej2vxXzNUIlCjQCmU1iavK8LG78uZaXY3CIT4kz0PuhnXycg44HsVAbsqupHTw", mapData)
|
||||||
|
}
|
||||||
@ -21,7 +21,6 @@ import (
|
|||||||
|
|
||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
"github.com/go-admin-team/go-admin-core/logger"
|
"github.com/go-admin-team/go-admin-core/logger"
|
||||||
log "github.com/go-admin-team/go-admin-core/logger"
|
|
||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@ -29,7 +28,7 @@ import (
|
|||||||
/*
|
/*
|
||||||
订单回调
|
订单回调
|
||||||
*/
|
*/
|
||||||
func ChangeSpotOrder(mapData map[string]interface{}) {
|
func ChangeSpotOrder(mapData map[string]interface{}, apiKey string) {
|
||||||
// 检查订单号是否存在
|
// 检查订单号是否存在
|
||||||
orderSn, ok := mapData["c"]
|
orderSn, ok := mapData["c"]
|
||||||
originOrderSn := mapData["C"] //取消操作 代表原始订单号
|
originOrderSn := mapData["C"] //取消操作 代表原始订单号
|
||||||
@ -193,7 +192,7 @@ func handleMainReduceFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) {
|
|||||||
lock := helper.NewRedisLock(fmt.Sprintf(rediskey.SpotReduceCallback, preOrder.ApiId, preOrder.Symbol), 120, 20, 100*time.Millisecond)
|
lock := helper.NewRedisLock(fmt.Sprintf(rediskey.SpotReduceCallback, preOrder.ApiId, preOrder.Symbol), 120, 20, 100*time.Millisecond)
|
||||||
|
|
||||||
if ok, err := lock.AcquireWait(context.Background()); err != nil {
|
if ok, err := lock.AcquireWait(context.Background()); err != nil {
|
||||||
log.Error("获取锁失败", err)
|
logger.Error("获取锁失败", err)
|
||||||
return
|
return
|
||||||
} else if ok {
|
} else if ok {
|
||||||
defer lock.Release()
|
defer lock.Release()
|
||||||
|
|||||||
457
services/binanceservice/transaction_manager_optimized.go
Normal file
457
services/binanceservice/transaction_manager_optimized.go
Normal file
@ -0,0 +1,457 @@
|
|||||||
|
package binanceservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-admin-team/go-admin-core/logger"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TransactionManager 事务管理器
|
||||||
|
type TransactionManager struct {
|
||||||
|
db *gorm.DB
|
||||||
|
metrics *MetricsCollector
|
||||||
|
config *OptimizedConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransactionConfig 事务配置
|
||||||
|
type TransactionConfig struct {
|
||||||
|
Timeout time.Duration `json:"timeout"`
|
||||||
|
RetryAttempts int `json:"retry_attempts"`
|
||||||
|
RetryDelay time.Duration `json:"retry_delay"`
|
||||||
|
IsolationLevel string `json:"isolation_level"`
|
||||||
|
ReadOnly bool `json:"read_only"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransactionContext 事务上下文
|
||||||
|
type TransactionContext struct {
|
||||||
|
Tx *gorm.DB
|
||||||
|
StartTime time.Time
|
||||||
|
Config TransactionConfig
|
||||||
|
Context context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransactionResult 事务结果
|
||||||
|
type TransactionResult struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Duration time.Duration `json:"duration"`
|
||||||
|
Error error `json:"error"`
|
||||||
|
Retries int `json:"retries"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTransactionManager 创建事务管理器
|
||||||
|
func NewTransactionManager(db *gorm.DB, metrics *MetricsCollector, config *OptimizedConfig) *TransactionManager {
|
||||||
|
return &TransactionManager{
|
||||||
|
db: db,
|
||||||
|
metrics: metrics,
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTransaction 在事务中执行操作
|
||||||
|
func (tm *TransactionManager) WithTransaction(ctx context.Context, config TransactionConfig, operation func(*TransactionContext) error) *TransactionResult {
|
||||||
|
startTime := time.Now()
|
||||||
|
result := &TransactionResult{
|
||||||
|
Timestamp: startTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置默认配置
|
||||||
|
if config.Timeout == 0 {
|
||||||
|
config.Timeout = 30 * time.Second // 默认事务超时时间
|
||||||
|
}
|
||||||
|
if config.RetryAttempts == 0 {
|
||||||
|
config.RetryAttempts = tm.config.RetryConfig.MaxRetries
|
||||||
|
}
|
||||||
|
if config.RetryDelay == 0 {
|
||||||
|
config.RetryDelay = tm.config.RetryConfig.RetryDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行事务(带重试)
|
||||||
|
for attempt := 1; attempt <= config.RetryAttempts; attempt++ {
|
||||||
|
err := tm.executeTransaction(ctx, config, operation)
|
||||||
|
if err == nil {
|
||||||
|
result.Success = true
|
||||||
|
result.Duration = time.Since(startTime)
|
||||||
|
result.Retries = attempt - 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Error = err
|
||||||
|
result.Retries = attempt
|
||||||
|
|
||||||
|
// 检查是否可重试
|
||||||
|
if !tm.isRetryableError(err) || attempt == config.RetryAttempts {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待重试
|
||||||
|
logger.Warnf("事务执行失败,第 %d 次重试: %v", attempt, err)
|
||||||
|
time.Sleep(config.RetryDelay * time.Duration(attempt))
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Duration = time.Since(startTime)
|
||||||
|
|
||||||
|
// 记录指标
|
||||||
|
if tm.metrics != nil {
|
||||||
|
tm.metrics.RecordDbOperation(true, result.Duration, result.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeTransaction 执行单次事务
|
||||||
|
func (tm *TransactionManager) executeTransaction(ctx context.Context, config TransactionConfig, operation func(*TransactionContext) error) error {
|
||||||
|
// 设置事务超时
|
||||||
|
txCtx := ctx
|
||||||
|
if config.Timeout > 0 {
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
txCtx, cancel = context.WithTimeout(ctx, config.Timeout)
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始事务
|
||||||
|
tx := tm.db.WithContext(txCtx).Begin()
|
||||||
|
if tx.Error != nil {
|
||||||
|
return fmt.Errorf("开始事务失败: %w", tx.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置事务隔离级别
|
||||||
|
if config.IsolationLevel != "" {
|
||||||
|
if err := tx.Exec(fmt.Sprintf("SET TRANSACTION ISOLATION LEVEL %s", config.IsolationLevel)).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return fmt.Errorf("设置事务隔离级别失败: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置只读模式
|
||||||
|
if config.ReadOnly {
|
||||||
|
if err := tx.Exec("SET TRANSACTION READ ONLY").Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return fmt.Errorf("设置只读事务失败: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建事务上下文
|
||||||
|
txContext := &TransactionContext{
|
||||||
|
Tx: tx,
|
||||||
|
StartTime: time.Now(),
|
||||||
|
Config: config,
|
||||||
|
Context: txCtx,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行操作
|
||||||
|
err := operation(txContext)
|
||||||
|
if err != nil {
|
||||||
|
// 回滚事务
|
||||||
|
if rollbackErr := tx.Rollback().Error; rollbackErr != nil {
|
||||||
|
logger.Errorf("事务回滚失败: %v", rollbackErr)
|
||||||
|
return fmt.Errorf("操作失败且回滚失败: %w, 回滚错误: %v", err, rollbackErr)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("事务操作失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交事务
|
||||||
|
if commitErr := tx.Commit().Error; commitErr != nil {
|
||||||
|
return fmt.Errorf("事务提交失败: %w", commitErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithOptimisticLock 使用乐观锁执行操作
|
||||||
|
func (tm *TransactionManager) WithOptimisticLock(ctx context.Context, config TransactionConfig, operation func(*TransactionContext) error) *TransactionResult {
|
||||||
|
return tm.WithTransaction(ctx, config, func(txCtx *TransactionContext) error {
|
||||||
|
// 在乐观锁模式下,我们需要在操作中检查版本号
|
||||||
|
return operation(txCtx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveWithOptimisticLock 使用乐观锁保存数据
|
||||||
|
func (tm *TransactionManager) SaveWithOptimisticLock(ctx context.Context, model interface{}, condition string, args ...interface{}) error {
|
||||||
|
config := TransactionConfig{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
RetryAttempts: 3,
|
||||||
|
RetryDelay: 100 * time.Millisecond,
|
||||||
|
}
|
||||||
|
|
||||||
|
result := tm.WithTransaction(ctx, config, func(txCtx *TransactionContext) error {
|
||||||
|
// 使用 WHERE 条件和版本号进行更新
|
||||||
|
result := txCtx.Tx.Where(condition, args...).Save(model)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有行被更新
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return fmt.Errorf("乐观锁冲突: 数据已被其他进程修改")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateWithOptimisticLock 使用乐观锁更新数据
|
||||||
|
func (tm *TransactionManager) UpdateWithOptimisticLock(ctx context.Context, model interface{}, updates map[string]interface{}, condition string, args ...interface{}) error {
|
||||||
|
config := TransactionConfig{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
RetryAttempts: 3,
|
||||||
|
RetryDelay: 100 * time.Millisecond,
|
||||||
|
}
|
||||||
|
|
||||||
|
result := tm.WithTransaction(ctx, config, func(txCtx *TransactionContext) error {
|
||||||
|
// 增加版本号
|
||||||
|
if _, hasVersion := updates["version"]; !hasVersion {
|
||||||
|
updates["version"] = gorm.Expr("version + 1")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行更新
|
||||||
|
result := txCtx.Tx.Model(model).Where(condition, args...).Updates(updates)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有行被更新
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return fmt.Errorf("乐观锁冲突: 数据已被其他进程修改")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchOperation 批量操作
|
||||||
|
func (tm *TransactionManager) BatchOperation(ctx context.Context, config TransactionConfig, operations []func(*TransactionContext) error) *TransactionResult {
|
||||||
|
return tm.WithTransaction(ctx, config, func(txCtx *TransactionContext) error {
|
||||||
|
for i, operation := range operations {
|
||||||
|
if err := operation(txCtx); err != nil {
|
||||||
|
return fmt.Errorf("批量操作第 %d 步失败: %w", i+1, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateSavepoint 创建保存点
|
||||||
|
func (tm *TransactionManager) CreateSavepoint(txCtx *TransactionContext, name string) error {
|
||||||
|
return txCtx.Tx.Exec(fmt.Sprintf("SAVEPOINT %s", name)).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// RollbackToSavepoint 回滚到保存点
|
||||||
|
func (tm *TransactionManager) RollbackToSavepoint(txCtx *TransactionContext, name string) error {
|
||||||
|
return txCtx.Tx.Exec(fmt.Sprintf("ROLLBACK TO SAVEPOINT %s", name)).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReleaseSavepoint 释放保存点
|
||||||
|
func (tm *TransactionManager) ReleaseSavepoint(txCtx *TransactionContext, name string) error {
|
||||||
|
return txCtx.Tx.Exec(fmt.Sprintf("RELEASE SAVEPOINT %s", name)).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// isRetryableError 判断错误是否可重试
|
||||||
|
func (tm *TransactionManager) isRetryableError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMsg := err.Error()
|
||||||
|
|
||||||
|
// 可重试的错误类型
|
||||||
|
retryableErrors := []string{
|
||||||
|
"deadlock",
|
||||||
|
"lock wait timeout",
|
||||||
|
"connection",
|
||||||
|
"timeout",
|
||||||
|
"temporary",
|
||||||
|
"乐观锁冲突",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, retryableError := range retryableErrors {
|
||||||
|
if strings.Contains(errorMsg, retryableError) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTransactionStats 获取事务统计信息
|
||||||
|
func (tm *TransactionManager) GetTransactionStats() map[string]interface{} {
|
||||||
|
stats := make(map[string]interface{})
|
||||||
|
|
||||||
|
// 从指标收集器获取数据库相关统计
|
||||||
|
if tm.metrics != nil {
|
||||||
|
stats["total_transactions"] = tm.metrics.PerformanceMetrics.DbTransactions
|
||||||
|
stats["total_queries"] = tm.metrics.PerformanceMetrics.DbQueries
|
||||||
|
stats["total_errors"] = tm.metrics.PerformanceMetrics.DbErrors
|
||||||
|
stats["avg_query_time"] = tm.metrics.PerformanceMetrics.DbQueryTime
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预定义的事务配置
|
||||||
|
|
||||||
|
// GetDefaultTransactionConfig 获取默认事务配置
|
||||||
|
func GetDefaultTransactionConfig() TransactionConfig {
|
||||||
|
return TransactionConfig{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
RetryAttempts: 3,
|
||||||
|
RetryDelay: 100 * time.Millisecond,
|
||||||
|
IsolationLevel: "READ COMMITTED",
|
||||||
|
ReadOnly: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetReadOnlyTransactionConfig 获取只读事务配置
|
||||||
|
func GetReadOnlyTransactionConfig() TransactionConfig {
|
||||||
|
return TransactionConfig{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
RetryAttempts: 1,
|
||||||
|
RetryDelay: 0,
|
||||||
|
IsolationLevel: "READ COMMITTED",
|
||||||
|
ReadOnly: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLongRunningTransactionConfig 获取长时间运行事务配置
|
||||||
|
func GetLongRunningTransactionConfig() TransactionConfig {
|
||||||
|
return TransactionConfig{
|
||||||
|
Timeout: 300 * time.Second, // 5分钟
|
||||||
|
RetryAttempts: 5,
|
||||||
|
RetryDelay: 500 * time.Millisecond,
|
||||||
|
IsolationLevel: "REPEATABLE READ",
|
||||||
|
ReadOnly: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCriticalTransactionConfig 获取关键事务配置
|
||||||
|
func GetCriticalTransactionConfig() TransactionConfig {
|
||||||
|
return TransactionConfig{
|
||||||
|
Timeout: 60 * time.Second,
|
||||||
|
RetryAttempts: 5,
|
||||||
|
RetryDelay: 200 * time.Millisecond,
|
||||||
|
IsolationLevel: "SERIALIZABLE",
|
||||||
|
ReadOnly: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局事务管理器实例
|
||||||
|
var GlobalTransactionManager *TransactionManager
|
||||||
|
|
||||||
|
// InitTransactionManager 初始化事务管理器
|
||||||
|
func InitTransactionManager(db *gorm.DB, metrics *MetricsCollector, config *OptimizedConfig) {
|
||||||
|
GlobalTransactionManager = NewTransactionManager(db, metrics, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTransactionManager 获取全局事务管理器
|
||||||
|
func GetTransactionManager() *TransactionManager {
|
||||||
|
return GlobalTransactionManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDefaultTransaction 使用默认配置执行事务
|
||||||
|
func WithDefaultTransaction(ctx context.Context, operation func(*TransactionContext) error) error {
|
||||||
|
if GlobalTransactionManager == nil {
|
||||||
|
return fmt.Errorf("事务管理器未初始化")
|
||||||
|
}
|
||||||
|
|
||||||
|
config := GetDefaultTransactionConfig()
|
||||||
|
result := GlobalTransactionManager.WithTransaction(ctx, config, operation)
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithReadOnlyTransaction 使用只读配置执行事务
|
||||||
|
func WithReadOnlyTransaction(ctx context.Context, operation func(*TransactionContext) error) error {
|
||||||
|
if GlobalTransactionManager == nil {
|
||||||
|
return fmt.Errorf("事务管理器未初始化")
|
||||||
|
}
|
||||||
|
|
||||||
|
config := GetReadOnlyTransactionConfig()
|
||||||
|
result := GlobalTransactionManager.WithTransaction(ctx, config, operation)
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithCriticalTransaction 使用关键事务配置执行事务
|
||||||
|
func WithCriticalTransaction(ctx context.Context, operation func(*TransactionContext) error) error {
|
||||||
|
if GlobalTransactionManager == nil {
|
||||||
|
return fmt.Errorf("事务管理器未初始化")
|
||||||
|
}
|
||||||
|
|
||||||
|
config := GetCriticalTransactionConfig()
|
||||||
|
result := GlobalTransactionManager.WithTransaction(ctx, config, operation)
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveWithOptimisticLockGlobal 全局乐观锁保存
|
||||||
|
func SaveWithOptimisticLockGlobal(ctx context.Context, model interface{}, condition string, args ...interface{}) error {
|
||||||
|
if GlobalTransactionManager == nil {
|
||||||
|
return fmt.Errorf("事务管理器未初始化")
|
||||||
|
}
|
||||||
|
|
||||||
|
return GlobalTransactionManager.SaveWithOptimisticLock(ctx, model, condition, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateWithOptimisticLockGlobal 全局乐观锁更新
|
||||||
|
func UpdateWithOptimisticLockGlobal(ctx context.Context, model interface{}, updates map[string]interface{}, condition string, args ...interface{}) error {
|
||||||
|
if GlobalTransactionManager == nil {
|
||||||
|
return fmt.Errorf("事务管理器未初始化")
|
||||||
|
}
|
||||||
|
|
||||||
|
return GlobalTransactionManager.UpdateWithOptimisticLock(ctx, model, updates, condition, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransactionHelper 事务辅助函数集合
|
||||||
|
type TransactionHelper struct {
|
||||||
|
tm *TransactionManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTransactionHelper 创建事务辅助器
|
||||||
|
func NewTransactionHelper(tm *TransactionManager) *TransactionHelper {
|
||||||
|
return &TransactionHelper{tm: tm}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SavePosition 保存持仓(带乐观锁)
|
||||||
|
func (th *TransactionHelper) SavePosition(ctx context.Context, position interface{}, userID, symbol string, version int) error {
|
||||||
|
condition := "user_id = ? AND symbol = ? AND version = ?"
|
||||||
|
return th.tm.SaveWithOptimisticLock(ctx, position, condition, userID, symbol, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveOrder 保存订单(带乐观锁)
|
||||||
|
func (th *TransactionHelper) SaveOrder(ctx context.Context, order interface{}, orderID string, version int) error {
|
||||||
|
condition := "order_id = ? AND version = ?"
|
||||||
|
return th.tm.SaveWithOptimisticLock(ctx, order, condition, orderID, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateOrderStatus 更新订单状态(带乐观锁)
|
||||||
|
func (th *TransactionHelper) UpdateOrderStatus(ctx context.Context, orderID, newStatus string, version int) error {
|
||||||
|
updates := map[string]interface{}{
|
||||||
|
"status": newStatus,
|
||||||
|
"updated_at": time.Now(),
|
||||||
|
}
|
||||||
|
condition := "order_id = ? AND version = ?"
|
||||||
|
return th.tm.UpdateWithOptimisticLock(ctx, nil, updates, condition, orderID, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatePositionQuantity 更新持仓数量(带乐观锁)
|
||||||
|
func (th *TransactionHelper) UpdatePositionQuantity(ctx context.Context, userID, symbol string, quantity float64, version int) error {
|
||||||
|
updates := map[string]interface{}{
|
||||||
|
"quantity": quantity,
|
||||||
|
"updated_at": time.Now(),
|
||||||
|
}
|
||||||
|
condition := "user_id = ? AND symbol = ? AND version = ?"
|
||||||
|
return th.tm.UpdateWithOptimisticLock(ctx, nil, updates, condition, userID, symbol, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGlobalTransactionHelper 获取全局事务辅助器
|
||||||
|
func GetGlobalTransactionHelper() *TransactionHelper {
|
||||||
|
if GlobalTransactionManager == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return NewTransactionHelper(GlobalTransactionManager)
|
||||||
|
}
|
||||||
23
services/cacheservice/confg_server_test.go
Normal file
23
services/cacheservice/confg_server_test.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package cacheservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go-admin/common/helper"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetReverseSetting(t *testing.T) {
|
||||||
|
dsn := "root:123456@tcp(127.0.0.1:3306)/go_exchange_single?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
|
||||||
|
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||||
|
helper.InitDefaultRedis("127.0.0.1:6379", "", 2)
|
||||||
|
|
||||||
|
setting, err := GetReverseSetting(db)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log(setting)
|
||||||
|
}
|
||||||
@ -130,3 +130,22 @@ func ResetSystemSetting(db *gorm.DB) (models.LineSystemSetting, error) {
|
|||||||
}
|
}
|
||||||
return models.LineSystemSetting{}, nil
|
return models.LineSystemSetting{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取反单配置
|
||||||
|
func GetReverseSetting(db *gorm.DB) (models.LineReverseSetting, error) {
|
||||||
|
setting := models.LineReverseSetting{}
|
||||||
|
|
||||||
|
helper.DefaultRedis.HGetAsObject(rediskey.ReverseSetting, &setting)
|
||||||
|
|
||||||
|
if setting.ReverseOrderType == "" {
|
||||||
|
if err := db.Model(&setting).First(&setting).Error; err != nil {
|
||||||
|
logger.Error("获取反单配置失败", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := helper.DefaultRedis.SetHashWithTags(rediskey.ReverseSetting, &setting); err != nil {
|
||||||
|
logger.Error("redis添加反单配置失败", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return setting, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import (
|
|||||||
- @msg 消息内容
|
- @msg 消息内容
|
||||||
- @listenType 订阅类型 0-现货 1-合约
|
- @listenType 订阅类型 0-现货 1-合约
|
||||||
*/
|
*/
|
||||||
func ReceiveListen(msg []byte, listenType int) (reconnect bool, err error) {
|
func ReceiveListen(msg []byte, listenType int, apiKey string) (reconnect bool, err error) {
|
||||||
var dataMap map[string]interface{}
|
var dataMap map[string]interface{}
|
||||||
err = sonic.Unmarshal(msg, &dataMap)
|
err = sonic.Unmarshal(msg, &dataMap)
|
||||||
|
|
||||||
@ -52,7 +52,7 @@ func ReceiveListen(msg []byte, listenType int) (reconnect bool, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
utility.SafeGo(func() {
|
utility.SafeGo(func() {
|
||||||
binanceservice.ChangeSpotOrder(mapData)
|
binanceservice.ChangeSpotOrder(mapData, apiKey)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
var data futuresdto.OrderTradeUpdate
|
var data futuresdto.OrderTradeUpdate
|
||||||
@ -64,7 +64,7 @@ func ReceiveListen(msg []byte, listenType int) (reconnect bool, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
utility.SafeGo(func() {
|
utility.SafeGo(func() {
|
||||||
binanceservice.ChangeFutureOrder(data.OrderDetails)
|
binanceservice.ChangeFutureOrder(data.OrderDetails, apiKey)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
//订单更新
|
//订单更新
|
||||||
@ -72,9 +72,9 @@ func ReceiveListen(msg []byte, listenType int) (reconnect bool, err error) {
|
|||||||
log.Info("executionReport 推送:", string(msg))
|
log.Info("executionReport 推送:", string(msg))
|
||||||
|
|
||||||
if listenType == 0 { //现货
|
if listenType == 0 { //现货
|
||||||
binanceservice.ChangeSpotOrder(dataMap)
|
binanceservice.ChangeSpotOrder(dataMap, apiKey)
|
||||||
} else if listenType == 1 { //合约
|
} else if listenType == 1 { //合约
|
||||||
binanceservice.ChangeFutureOrder(dataMap)
|
binanceservice.ChangeFutureOrder(dataMap, apiKey)
|
||||||
} else {
|
} else {
|
||||||
log.Error("executionReport 不支持的订阅类型", strconv.Itoa(listenType))
|
log.Error("executionReport 不支持的订阅类型", strconv.Itoa(listenType))
|
||||||
}
|
}
|
||||||
@ -101,6 +101,7 @@ func ReceiveListen(msg []byte, listenType int) (reconnect bool, err error) {
|
|||||||
|
|
||||||
case "eventStreamTerminated":
|
case "eventStreamTerminated":
|
||||||
log.Info("账户数据流被终止 type:", getWsTypeName(listenType))
|
log.Info("账户数据流被终止 type:", getWsTypeName(listenType))
|
||||||
|
return true, nil
|
||||||
default:
|
default:
|
||||||
log.Info("未知事件 内容:", string(msg))
|
log.Info("未知事件 内容:", string(msg))
|
||||||
log.Info("未知事件", event)
|
log.Info("未知事件", event)
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
@ -42,7 +43,9 @@ type BinanceWebSocketManager struct {
|
|||||||
isStopped bool // 标记 WebSocket 是否已主动停止
|
isStopped bool // 标记 WebSocket 是否已主动停止
|
||||||
mu sync.Mutex // 用于控制并发访问 isStopped
|
mu sync.Mutex // 用于控制并发访问 isStopped
|
||||||
cancelFunc context.CancelFunc
|
cancelFunc context.CancelFunc
|
||||||
listenKey string // 新增字段
|
listenKey string // 新增字段
|
||||||
|
reconnecting atomic.Bool // 防止重复重连
|
||||||
|
ConnectTime time.Time // 当前连接建立时间
|
||||||
}
|
}
|
||||||
|
|
||||||
// 已有连接
|
// 已有连接
|
||||||
@ -72,6 +75,10 @@ func NewBinanceWebSocketManager(wsType int, apiKey, apiSecret, proxyType, proxyA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (wm *BinanceWebSocketManager) GetKey() string {
|
||||||
|
return wm.apiKey
|
||||||
|
}
|
||||||
|
|
||||||
func (wm *BinanceWebSocketManager) Start() {
|
func (wm *BinanceWebSocketManager) Start() {
|
||||||
utility.SafeGo(wm.run)
|
utility.SafeGo(wm.run)
|
||||||
// wm.run()
|
// wm.run()
|
||||||
@ -91,14 +98,33 @@ func (wm *BinanceWebSocketManager) Restart(apiKey, apiSecret, proxyType, proxyAd
|
|||||||
wm.isStopped = false
|
wm.isStopped = false
|
||||||
utility.SafeGo(wm.run)
|
utility.SafeGo(wm.run)
|
||||||
} else {
|
} else {
|
||||||
wm.reconnect <- struct{}{}
|
log.Warnf("调用restart")
|
||||||
|
wm.triggerReconnect(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
return wm
|
return wm
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 触发重连
|
||||||
|
func (wm *BinanceWebSocketManager) triggerReconnect(force bool) {
|
||||||
|
if force {
|
||||||
|
wm.reconnecting.Store(false) // 强制重置标志位
|
||||||
|
}
|
||||||
|
|
||||||
|
if wm.reconnecting.CompareAndSwap(false, true) {
|
||||||
|
log.Warnf("准备重连 key: %s wsType: %v", wm.apiKey, wm.wsType)
|
||||||
|
// 发送信号触发重连协程
|
||||||
|
select {
|
||||||
|
case wm.reconnect <- struct{}{}:
|
||||||
|
default:
|
||||||
|
// 防止阻塞,如果通道满了就跳过
|
||||||
|
log.Debugf("reconnect 信号已存在,跳过 key:%s", wm.apiKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
func Restart(wm *BinanceWebSocketManager) {
|
func Restart(wm *BinanceWebSocketManager) {
|
||||||
wm.reconnect <- struct{}{}
|
log.Warnf("调用restart")
|
||||||
|
wm.triggerReconnect(true)
|
||||||
}
|
}
|
||||||
func (wm *BinanceWebSocketManager) run() {
|
func (wm *BinanceWebSocketManager) run() {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
@ -201,6 +227,8 @@ func (wm *BinanceWebSocketManager) connect(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 连接成功,更新连接时间
|
||||||
|
wm.ConnectTime = time.Now()
|
||||||
log.Info(fmt.Sprintf("已连接到 Binance %s WebSocket【%s】 key:%s", getWsTypeName(wm.wsType), wm.apiKey, listenKey))
|
log.Info(fmt.Sprintf("已连接到 Binance %s WebSocket【%s】 key:%s", getWsTypeName(wm.wsType), wm.apiKey, listenKey))
|
||||||
|
|
||||||
// Ping处理
|
// Ping处理
|
||||||
@ -222,10 +250,88 @@ func (wm *BinanceWebSocketManager) connect(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
// utility.SafeGoParam(wm.restartConnect, ctx)
|
|
||||||
utility.SafeGo(func() { wm.startListenKeyRenewal2(ctx) })
|
utility.SafeGo(func() { wm.startListenKeyRenewal2(ctx) })
|
||||||
utility.SafeGo(func() { wm.readMessages(ctx) })
|
utility.SafeGo(func() { wm.readMessages(ctx) })
|
||||||
utility.SafeGo(func() { wm.handleReconnect(ctx) })
|
utility.SafeGo(func() { wm.handleReconnect(ctx) })
|
||||||
|
utility.SafeGo(func() { wm.startPingLoop(ctx) })
|
||||||
|
// utility.SafeGo(func() { wm.startDeadCheck(ctx) })
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceConnection 创建新连接并关闭旧连接,实现无缝连接替换
|
||||||
|
func (wm *BinanceWebSocketManager) ReplaceConnection() error {
|
||||||
|
wm.mu.Lock()
|
||||||
|
if wm.isStopped {
|
||||||
|
wm.mu.Unlock()
|
||||||
|
return errors.New("WebSocket 已停止")
|
||||||
|
}
|
||||||
|
oldCtxCancel := wm.cancelFunc
|
||||||
|
oldConn := wm.ws
|
||||||
|
wm.mu.Unlock()
|
||||||
|
|
||||||
|
log.Infof("🔄 正在替换连接: %s", wm.apiKey)
|
||||||
|
|
||||||
|
// 步骤 1:先获取新的 listenKey 和连接
|
||||||
|
newListenKey, err := wm.getListenKey()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("获取新 listenKey 失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dialer, err := wm.getDialer()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newURL := fmt.Sprintf("%s/%s", wm.url, newListenKey)
|
||||||
|
newConn, _, err := dialer.Dial(newURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("新连接 Dial 失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 步骤 2:创建新的上下文并启动协程
|
||||||
|
newCtx, newCancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
// 设置 ping handler
|
||||||
|
newConn.SetPingHandler(func(appData string) error {
|
||||||
|
log.Infof("收到 Ping(新连接) key:%s msg:%s", wm.apiKey, appData)
|
||||||
|
for x := 0; x < 5; x++ {
|
||||||
|
if err := newConn.WriteControl(websocket.PongMessage, []byte(appData), time.Now().Add(10*time.Second)); err != nil {
|
||||||
|
log.Errorf("Pong 失败 %d 次 err:%v", x, err)
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
setLastTime(wm)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// 步骤 3:安全切换连接
|
||||||
|
wm.mu.Lock()
|
||||||
|
wm.ws = newConn
|
||||||
|
wm.listenKey = newListenKey
|
||||||
|
wm.ConnectTime = time.Now()
|
||||||
|
wm.cancelFunc = newCancel
|
||||||
|
wm.mu.Unlock()
|
||||||
|
|
||||||
|
log.Infof("✅ 替换连接成功: %s listenKey: %s", wm.apiKey, newListenKey)
|
||||||
|
|
||||||
|
// 步骤 4:启动新连接协程
|
||||||
|
go wm.startListenKeyRenewal2(newCtx)
|
||||||
|
go wm.readMessages(newCtx)
|
||||||
|
go wm.handleReconnect(newCtx)
|
||||||
|
go wm.startPingLoop(newCtx)
|
||||||
|
// go wm.startDeadCheck(newCtx)
|
||||||
|
|
||||||
|
// 步骤 5:关闭旧连接、取消旧协程
|
||||||
|
if oldCtxCancel != nil {
|
||||||
|
oldCtxCancel()
|
||||||
|
}
|
||||||
|
if oldConn != nil {
|
||||||
|
_ = oldConn.Close()
|
||||||
|
log.Infof("🔒 旧连接已关闭: %s", wm.apiKey)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -251,6 +357,7 @@ func setLastTime(wm *BinanceWebSocketManager) {
|
|||||||
if val != "" {
|
if val != "" {
|
||||||
helper.DefaultRedis.SetString(subKey, val)
|
helper.DefaultRedis.SetString(subKey, val)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wm *BinanceWebSocketManager) getDialer() (*websocket.Dialer, error) {
|
func (wm *BinanceWebSocketManager) getDialer() (*websocket.Dialer, error) {
|
||||||
@ -357,7 +464,8 @@ func (wm *BinanceWebSocketManager) readMessages(ctx context.Context) {
|
|||||||
_, msg, err := wm.ws.ReadMessage()
|
_, msg, err := wm.ws.ReadMessage()
|
||||||
if err != nil && strings.Contains(err.Error(), "websocket: close") {
|
if err != nil && strings.Contains(err.Error(), "websocket: close") {
|
||||||
if !wm.isStopped {
|
if !wm.isStopped {
|
||||||
wm.reconnect <- struct{}{}
|
log.Error("收到关闭消息", err.Error())
|
||||||
|
wm.triggerReconnect(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Error("websocket 关闭")
|
log.Error("websocket 关闭")
|
||||||
@ -375,11 +483,13 @@ func (wm *BinanceWebSocketManager) readMessages(ctx context.Context) {
|
|||||||
func (wm *BinanceWebSocketManager) handleOrderUpdate(msg []byte) {
|
func (wm *BinanceWebSocketManager) handleOrderUpdate(msg []byte) {
|
||||||
setLastTime(wm)
|
setLastTime(wm)
|
||||||
|
|
||||||
if reconnect, _ := ReceiveListen(msg, wm.wsType); reconnect {
|
if reconnect, _ := ReceiveListen(msg, wm.wsType, wm.apiKey); reconnect {
|
||||||
wm.reconnect <- struct{}{}
|
log.Errorf("收到重连请求")
|
||||||
|
wm.triggerReconnect(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop 安全停止 WebSocket
|
||||||
func (wm *BinanceWebSocketManager) Stop() {
|
func (wm *BinanceWebSocketManager) Stop() {
|
||||||
wm.mu.Lock()
|
wm.mu.Lock()
|
||||||
defer wm.mu.Unlock()
|
defer wm.mu.Unlock()
|
||||||
@ -387,9 +497,8 @@ func (wm *BinanceWebSocketManager) Stop() {
|
|||||||
if wm.isStopped {
|
if wm.isStopped {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
wm.isStopped = true
|
wm.isStopped = true
|
||||||
// 关闭 stopChannel(确保已经关闭,避免 panic)
|
|
||||||
select {
|
select {
|
||||||
case <-wm.stopChannel:
|
case <-wm.stopChannel:
|
||||||
default:
|
default:
|
||||||
@ -398,107 +507,182 @@ func (wm *BinanceWebSocketManager) Stop() {
|
|||||||
|
|
||||||
if wm.cancelFunc != nil {
|
if wm.cancelFunc != nil {
|
||||||
wm.cancelFunc()
|
wm.cancelFunc()
|
||||||
|
wm.cancelFunc = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if wm.ws != nil {
|
if wm.ws != nil {
|
||||||
if err := wm.ws.Close(); err != nil {
|
if err := wm.ws.Close(); err != nil {
|
||||||
log.Error(fmt.Sprintf("key【%s】close失败", wm.apiKey), err)
|
log.Errorf("WebSocket Close 错误 key:%s err:%v", wm.apiKey, err)
|
||||||
} else {
|
|
||||||
log.Info(fmt.Sprintf("key【%s】close", wm.apiKey))
|
|
||||||
}
|
}
|
||||||
|
wm.ws = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// **重新创建 stopChannel,避免 Restart() 时无效**
|
log.Infof("WebSocket 已完全停止 key:%s", wm.apiKey)
|
||||||
wm.stopChannel = make(chan struct{})
|
wm.stopChannel = make(chan struct{}, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重连机制
|
// handleReconnect 使用指数退避并保持永不退出
|
||||||
func (wm *BinanceWebSocketManager) handleReconnect(ctx context.Context) {
|
func (wm *BinanceWebSocketManager) handleReconnect(ctx context.Context) {
|
||||||
maxRetries := 5 // 最大重试次数
|
const maxRetries = 100
|
||||||
|
baseDelay := time.Second * 2
|
||||||
retryCount := 0
|
retryCount := 0
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
log.Infof("handleReconnect context done: %s", wm.apiKey)
|
||||||
return
|
return
|
||||||
|
|
||||||
case <-wm.reconnect:
|
case <-wm.reconnect:
|
||||||
|
wm.mu.Lock()
|
||||||
if wm.isStopped {
|
if wm.isStopped {
|
||||||
|
wm.mu.Unlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
wm.mu.Unlock()
|
||||||
|
|
||||||
log.Warn("WebSocket 连接断开,尝试重连...")
|
log.Warnf("WebSocket 连接断开,准备重连 key:%s", wm.apiKey)
|
||||||
|
|
||||||
if wm.ws != nil {
|
|
||||||
wm.ws.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 取消旧的上下文
|
|
||||||
if wm.cancelFunc != nil {
|
|
||||||
wm.cancelFunc()
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
wm.mu.Lock()
|
||||||
|
if wm.ws != nil {
|
||||||
|
_ = wm.ws.Close()
|
||||||
|
wm.ws = nil
|
||||||
|
}
|
||||||
|
if wm.cancelFunc != nil {
|
||||||
|
wm.cancelFunc()
|
||||||
|
wm.cancelFunc = nil
|
||||||
|
}
|
||||||
|
wm.mu.Unlock()
|
||||||
|
|
||||||
newCtx, cancel := context.WithCancel(context.Background())
|
newCtx, cancel := context.WithCancel(context.Background())
|
||||||
wm.cancelFunc = cancel // 更新 cancelFunc
|
wm.mu.Lock()
|
||||||
|
wm.cancelFunc = cancel
|
||||||
|
wm.mu.Unlock()
|
||||||
|
|
||||||
if err := wm.connect(newCtx); err != nil {
|
if err := wm.connect(newCtx); err != nil {
|
||||||
log.Errorf("重连失败: %v", err)
|
log.Errorf("🔌 重连失败(%d/%d)key:%s,err: %v", retryCount+1, maxRetries, wm.apiKey, err)
|
||||||
cancel()
|
cancel()
|
||||||
retryCount++
|
retryCount++
|
||||||
|
|
||||||
if retryCount >= maxRetries {
|
if retryCount >= maxRetries {
|
||||||
log.Error("重连失败次数过多,退出重连逻辑")
|
log.Errorf("❌ 重连失败次数过多,停止重连逻辑 key:%s", wm.apiKey)
|
||||||
|
wm.reconnecting.Store(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(5 * time.Second)
|
delay := baseDelay * time.Duration(1<<retryCount)
|
||||||
|
if delay > time.Minute*5 {
|
||||||
|
delay = time.Minute * 5
|
||||||
|
}
|
||||||
|
log.Warnf("等待 %v 后重试...", delay)
|
||||||
|
time.Sleep(delay)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Infof("✅ 重连成功 key:%s", wm.apiKey)
|
||||||
|
retryCount = 0
|
||||||
|
wm.reconnecting.Store(false)
|
||||||
|
|
||||||
|
// ✅ 重连成功后开启假死检测
|
||||||
|
utility.SafeGo(func() { wm.startDeadCheck(newCtx) })
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// startDeadCheck 替代 Start 中的定时器,绑定连接生命周期
|
||||||
|
func (wm *BinanceWebSocketManager) startDeadCheck(ctx context.Context) {
|
||||||
|
ticker := time.NewTicker(1 * time.Minute)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
if wm.isStopped {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
wm.DeadCheck()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 假死检测
|
||||||
|
func (wm *BinanceWebSocketManager) DeadCheck() {
|
||||||
|
subKey := fmt.Sprintf(global.USER_SUBSCRIBE, wm.apiKey)
|
||||||
|
val, _ := helper.DefaultRedis.GetString(subKey)
|
||||||
|
if val == "" {
|
||||||
|
log.Warnf("没有订阅信息,无法进行假死检测")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var data binancedto.UserSubscribeState
|
||||||
|
_ = sonic.Unmarshal([]byte(val), &data)
|
||||||
|
|
||||||
|
var lastTime *time.Time
|
||||||
|
if wm.wsType == 0 {
|
||||||
|
lastTime = data.SpotLastTime
|
||||||
|
} else {
|
||||||
|
lastTime = data.FuturesLastTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义最大静默时间(超出视为假死)
|
||||||
|
var timeout time.Duration
|
||||||
|
if wm.wsType == 0 {
|
||||||
|
timeout = 40 * time.Second // Spot 每 20s ping,40s 足够
|
||||||
|
} else {
|
||||||
|
timeout = 6 * time.Minute // Futures 每 3 分钟 ping
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastTime != nil && time.Since(*lastTime) > timeout {
|
||||||
|
log.Warnf("检测到假死连接 key:%s type:%v, 距离上次通信: %v, 触发重连", wm.apiKey, wm.wsType, time.Since(*lastTime))
|
||||||
|
wm.triggerReconnect(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主动心跳发送机制
|
||||||
|
func (wm *BinanceWebSocketManager) startPingLoop(ctx context.Context) {
|
||||||
|
ticker := time.NewTicker(1 * time.Minute)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
if wm.isStopped {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err := wm.ws.WriteMessage(websocket.PingMessage, []byte("ping"))
|
||||||
|
if err != nil {
|
||||||
|
log.Error("主动 Ping Binance 失败:", err)
|
||||||
|
wm.triggerReconnect(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 定期删除listenkey 并重启ws
|
// 定期删除listenkey 并重启ws
|
||||||
func (wm *BinanceWebSocketManager) startListenKeyRenewal(ctx context.Context, listenKey string) {
|
// func (wm *BinanceWebSocketManager) startListenKeyRenewal(ctx context.Context, listenKey string) {
|
||||||
time.Sleep(30 * time.Minute)
|
// time.Sleep(30 * time.Minute)
|
||||||
|
|
||||||
select {
|
// select {
|
||||||
case <-ctx.Done():
|
// case <-ctx.Done():
|
||||||
return
|
// return
|
||||||
default:
|
// default:
|
||||||
if err := wm.deleteListenKey(listenKey); err != nil {
|
// if err := wm.deleteListenKey(listenKey); err != nil {
|
||||||
log.Error("Failed to renew listenKey: ,type:%v key: %s", wm.wsType, wm.apiKey, err)
|
// log.Error("Failed to renew listenKey: ,type:%v key: %s", wm.wsType, wm.apiKey, err)
|
||||||
} else {
|
// } else {
|
||||||
log.Debug("Successfully delete listenKey")
|
// log.Debug("Successfully delete listenKey")
|
||||||
wm.reconnect <- struct{}{}
|
// wm.triggerReconnect()
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// ticker := time.NewTicker(5 * time.Minute)
|
// }
|
||||||
// defer ticker.Stop()
|
|
||||||
|
|
||||||
// for {
|
|
||||||
// select {
|
|
||||||
// case <-ticker.C:
|
|
||||||
// if wm.isStopped {
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if err := wm.deleteListenKey(listenKey); err != nil {
|
|
||||||
// log.Error("Failed to renew listenKey: ,type:%v key: %s", wm.wsType, wm.apiKey, err)
|
|
||||||
// } else {
|
|
||||||
// log.Debug("Successfully delete listenKey")
|
|
||||||
// wm.reconnect <- struct{}{}
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// case <-ctx.Done():
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定时续期
|
// 定时续期
|
||||||
func (wm *BinanceWebSocketManager) startListenKeyRenewal2(ctx context.Context) {
|
func (wm *BinanceWebSocketManager) startListenKeyRenewal2(ctx context.Context) {
|
||||||
@ -527,49 +711,34 @@ func (wm *BinanceWebSocketManager) startListenKeyRenewal2(ctx context.Context) {
|
|||||||
/*
|
/*
|
||||||
删除listenkey
|
删除listenkey
|
||||||
*/
|
*/
|
||||||
func (wm *BinanceWebSocketManager) deleteListenKey(listenKey string) error {
|
// func (wm *BinanceWebSocketManager) deleteListenKey(listenKey string) error {
|
||||||
client, err := wm.createBinanceClient()
|
// client, err := wm.createBinanceClient()
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return err
|
// return err
|
||||||
}
|
// }
|
||||||
|
|
||||||
var resp []byte
|
// var resp []byte
|
||||||
|
|
||||||
switch wm.wsType {
|
// switch wm.wsType {
|
||||||
case 0:
|
// case 0:
|
||||||
path := fmt.Sprintf("/api/v3/userDataStream")
|
// path := fmt.Sprintf("/api/v3/userDataStream")
|
||||||
params := map[string]interface{}{
|
// params := map[string]interface{}{
|
||||||
"listenKey": listenKey,
|
// "listenKey": listenKey,
|
||||||
}
|
// }
|
||||||
resp, _, err = client.SendSpotRequestByKey(path, "DELETE", params)
|
// resp, _, err = client.SendSpotRequestByKey(path, "DELETE", params)
|
||||||
|
|
||||||
log.Debug(fmt.Sprintf("deleteListenKey resp: %s", string(resp)))
|
// log.Debug(fmt.Sprintf("deleteListenKey resp: %s", string(resp)))
|
||||||
case 1:
|
// case 1:
|
||||||
resp, _, err = client.SendFuturesRequestByKey("/fapi/v1/listenKey", "DELETE", nil)
|
// resp, _, err = client.SendFuturesRequestByKey("/fapi/v1/listenKey", "DELETE", nil)
|
||||||
log.Debug(fmt.Sprintf("deleteListenKey resp: %s", string(resp)))
|
// log.Debug(fmt.Sprintf("deleteListenKey resp: %s", string(resp)))
|
||||||
default:
|
// default:
|
||||||
return errors.New("unknown ws type")
|
// return errors.New("unknown ws type")
|
||||||
}
|
// }
|
||||||
|
|
||||||
return err
|
// return err
|
||||||
}
|
// }
|
||||||
|
|
||||||
func (wm *BinanceWebSocketManager) renewListenKey(listenKey string) error {
|
func (wm *BinanceWebSocketManager) renewListenKey(listenKey string) error {
|
||||||
// payloadParam := map[string]interface{}{
|
|
||||||
// "listenKey": listenKey,
|
|
||||||
// "apiKey": wm.apiKey,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// params := map[string]interface{}{
|
|
||||||
// "id": getUUID(),
|
|
||||||
// "method": "userDataStream.ping",
|
|
||||||
// "params": payloadParam,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if err := wm.ws.WriteJSON(params); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// wm.ws.WriteJSON()
|
|
||||||
client, err := wm.createBinanceClient()
|
client, err := wm.createBinanceClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
Reference in New Issue
Block a user