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)) | ||||
|  | ||||
| 	if err := req.Valid(); err != nil { | ||||
| 		e.Error(500, err, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err = s.Insert(&req) | ||||
| 	if err != nil { | ||||
| 		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()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if err := req.Valid(); err != nil { | ||||
| 		e.Error(500, err, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	req.SetUpdateBy(user.GetUserId(c)) | ||||
| 	p := actions.GetPermissionFromContext(c) | ||||
|  | ||||
| @ -267,3 +278,74 @@ func (e LineApiUser) GetMainUser(c *gin.Context) { | ||||
| 	} | ||||
| 	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 ( | ||||
| 	"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" | ||||
| @ -193,6 +194,7 @@ func (e LinePriceLimit) Delete(c *gin.Context) { | ||||
| 	e.OK(req.GetId(), "删除成功") | ||||
| } | ||||
|  | ||||
| // aicoin数据同步 | ||||
| func (e LinePriceLimit) UpRange(c *gin.Context) { | ||||
| 	s := service.LinePriceLimit{} | ||||
| 	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 ( | ||||
| 	"go-admin/common/models" | ||||
|  | ||||
| 	"github.com/shopspring/decimal" | ||||
| ) | ||||
|  | ||||
| type LineApiUser struct { | ||||
| 	models.Model | ||||
|  | ||||
| 	ExchangeType string `json:"exchangeType" gorm:"type:varchar(20);comment:交易所类型(字典 exchange_type)"` | ||||
| 	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用户名"` | ||||
| 	ApiKey       string `json:"apiKey" gorm:"type:varchar(255);comment:apiKey"` | ||||
| 	ApiSecret    string `json:"apiSecret" gorm:"type:varchar(255);comment:apiSecret"` | ||||
| 	IpAddress    string `json:"ipAddress" gorm:"type:varchar(255);comment:代理地址"` | ||||
| 	UserPass     string `json:"userPass" gorm:"type:varchar(255);comment:代码账号密码"` | ||||
| 	AdminId      int64  `json:"adminId" gorm:"type:int unsigned;comment:管理员id"` | ||||
| 	Affiliation  int64  `json:"affiliation" gorm:"type:int;comment:归属:1=现货,2=合约,3=现货合约"` | ||||
| 	AdminShow    int64  `json:"adminShow" gorm:"type:int;comment:是否超管可见:1=是,0=否"` | ||||
| 	Site         string `json:"site" gorm:"type:enum('1','2','3');comment:允许下单的方向:1=多;2=空;3=多空"` | ||||
| 	Subordinate  string `json:"subordinate" gorm:"type:enum('0','1','2');comment:从属关系:0=未绑定关系;1=主账号;2=副帐号"` | ||||
| 	GroupId      int64  `json:"groupId" gorm:"type:int unsigned;comment:所属组id"` | ||||
| 	OpenStatus   int64  `json:"openStatus" gorm:"type:int unsigned;comment:开启状态 0=关闭 1=开启"` | ||||
|  | ||||
| 	SpotLastTime    string `json:"spotLastTime" gorm:"-"`    //现货websocket最后通信时间 | ||||
| 	FuturesLastTime string `json:"futuresLastTime" gorm:"-"` //合约websocket最后通信时间 | ||||
| 	ExchangeType    string          `json:"exchangeType" gorm:"type:varchar(20);comment:交易所类型(字典 exchange_type)"` | ||||
| 	UserId          int64           `json:"userId" gorm:"type:int unsigned;comment:用户id"` | ||||
| 	ApiName         string          `json:"apiName" gorm:"type:varchar(255);comment:api用户名"` | ||||
| 	ApiKey          string          `json:"apiKey" gorm:"type:varchar(255);comment:apiKey"` | ||||
| 	ApiSecret       string          `json:"apiSecret" gorm:"type:varchar(255);comment:apiSecret"` | ||||
| 	IpAddress       string          `json:"ipAddress" 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=现货合约"` | ||||
| 	AdminShow       int64           `json:"adminShow" gorm:"type:int;comment:是否超管可见:1=是,0=否"` | ||||
| 	Site            string          `json:"site" gorm:"type:enum('1','2','3');comment:允许下单的方向:1=多;2=空;3=多空"` | ||||
| 	Subordinate     string          `json:"subordinate" gorm:"type:enum('0','1','2');comment:从属关系:0=未绑定关系;1=主账号;2=副帐号"` | ||||
| 	GroupId         int64           `json:"groupId" gorm:"type:int unsigned;comment:所属组id"` | ||||
| 	OpenStatus      int64           `json:"openStatus" gorm:"type:int unsigned;comment:开启状态 0=关闭 1=开启"` | ||||
| 	ReverseStatus   int             `json:"reverseStatus" gorm:"type:tinyint unsigned;comment:反向开仓:1=开启;2=关闭"` | ||||
| 	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最后通信时间 | ||||
| 	FuturesLastTime string          `json:"futuresLastTime" gorm:"-"` //合约websocket最后通信时间 | ||||
| 	models.ModelTime | ||||
| 	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("getUser", api.GetUser)         //获取未绑定的用户 | ||||
| 		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 | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"go-admin/app/admin/models" | ||||
| 	"go-admin/common/dto" | ||||
| 	common "go-admin/common/models" | ||||
|  | ||||
| 	"github.com/shopspring/decimal" | ||||
| ) | ||||
|  | ||||
| type LineApiUserGetPageReq struct { | ||||
| @ -41,44 +44,66 @@ func (m *LineApiUserGetPageReq) GetNeedSearch() interface{} { | ||||
| } | ||||
|  | ||||
| type LineApiUserInsertReq struct { | ||||
| 	Id           int    `json:"-" comment:"id"` // id | ||||
| 	ExchangeType string `json:"exchangeType" comment:"交易所code"` | ||||
| 	UserId       int64  `json:"userId" comment:"用户id"` | ||||
| 	JysId        int64  `json:"jysId" comment:"关联交易所账号id"` | ||||
| 	ApiName      string `json:"apiName" comment:"api用户名"` | ||||
| 	ApiKey       string `json:"apiKey" comment:"apiKey"` | ||||
| 	ApiSecret    string `json:"apiSecret" comment:"apiSecret"` | ||||
| 	IpAddress    string `json:"ipAddress" comment:"代理地址"` | ||||
| 	UserPass     string `json:"userPass" comment:"代码账号密码"` | ||||
| 	AdminId      int64  `json:"adminId" comment:"管理员id"` | ||||
| 	Affiliation  int64  `json:"affiliation" comment:"归属:1=现货,2=合约,3=现货合约"` | ||||
| 	AdminShow    int64  `json:"adminShow" comment:"是否超管可见:1=是,0=否"` | ||||
| 	Site         string `json:"site" comment:"允许下单的方向:1=多;2=空;3=多空"` | ||||
| 	Subordinate  string `json:"subordinate" comment:"从属关系:0=未绑定关系;1=主账号;2=副帐号"` | ||||
| 	GroupId      int64  `json:"groupId" comment:"所属组id"` | ||||
| 	OpenStatus   int64  `json:"openStatus" comment:"开启状态 0=关闭 1=开启"` | ||||
| 	Id              int             `json:"-" comment:"id"` // id | ||||
| 	ExchangeType    string          `json:"exchangeType" comment:"交易所code"` | ||||
| 	UserId          int64           `json:"userId" comment:"用户id"` | ||||
| 	JysId           int64           `json:"jysId" comment:"关联交易所账号id"` | ||||
| 	ApiName         string          `json:"apiName" comment:"api用户名"` | ||||
| 	ApiKey          string          `json:"apiKey" comment:"apiKey"` | ||||
| 	ApiSecret       string          `json:"apiSecret" comment:"apiSecret"` | ||||
| 	IpAddress       string          `json:"ipAddress" comment:"代理地址"` | ||||
| 	UserPass        string          `json:"userPass" comment:"代码账号密码"` | ||||
| 	Affiliation     int64           `json:"affiliation" comment:"归属:1=现货,2=合约,3=现货合约"` | ||||
| 	AdminShow       int64           `json:"adminShow" comment:"是否超管可见:1=是,0=否"` | ||||
| 	Site            string          `json:"site" comment:"允许下单的方向:1=多;2=空;3=多空"` | ||||
| 	Subordinate     string          `json:"subordinate" comment:"从属关系:0=未绑定关系;1=主账号;2=副帐号"` | ||||
| 	GroupId         int64           `json:"groupId" comment:"所属组id"` | ||||
| 	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 | ||||
| } | ||||
|  | ||||
| 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) { | ||||
| 	if s.Id == 0 { | ||||
| 		model.Model = common.Model{Id: s.Id} | ||||
| 	} | ||||
| 	model.ExchangeType = s.ExchangeType | ||||
| 	model.UserId = s.UserId | ||||
| 	model.JysId = s.JysId | ||||
| 	model.ApiName = s.ApiName | ||||
| 	model.ApiKey = s.ApiKey | ||||
| 	model.ApiSecret = s.ApiSecret | ||||
| 	model.IpAddress = s.IpAddress | ||||
| 	model.UserPass = s.UserPass | ||||
| 	model.AdminId = s.AdminId | ||||
| 	model.Affiliation = s.Affiliation | ||||
| 	model.AdminShow = s.AdminShow | ||||
| 	model.Site = s.Site | ||||
| 	model.Subordinate = s.Subordinate | ||||
| 	model.GroupId = s.GroupId | ||||
| 	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 // 添加这而,需要记录是被谁创建的 | ||||
| } | ||||
|  | ||||
| @ -87,44 +112,66 @@ func (s *LineApiUserInsertReq) GetId() interface{} { | ||||
| } | ||||
|  | ||||
| type LineApiUserUpdateReq struct { | ||||
| 	Id           int    `uri:"id" comment:"id"` // id | ||||
| 	ExchangeType string `json:"exchangeType" comment:"交易所code"` | ||||
| 	UserId       int64  `json:"userId" comment:"用户id"` | ||||
| 	JysId        int64  `json:"jysId" comment:"关联交易所账号id"` | ||||
| 	ApiName      string `json:"apiName" comment:"api用户名"` | ||||
| 	ApiKey       string `json:"apiKey" comment:"apiKey"` | ||||
| 	ApiSecret    string `json:"apiSecret" comment:"apiSecret"` | ||||
| 	IpAddress    string `json:"ipAddress" comment:"代理地址"` | ||||
| 	UserPass     string `json:"userPass" comment:"代码账号密码"` | ||||
| 	AdminId      int64  `json:"adminId" comment:"管理员id"` | ||||
| 	Affiliation  int64  `json:"affiliation" comment:"归属:1=现货,2=合约,3=现货合约"` | ||||
| 	AdminShow    int64  `json:"adminShow" comment:"是否超管可见:1=是,0=否"` | ||||
| 	Site         string `json:"site" comment:"允许下单的方向:1=多;2=空;3=多空"` | ||||
| 	Subordinate  string `json:"subordinate" comment:"从属关系:0=未绑定关系;1=主账号;2=副帐号"` | ||||
| 	GroupId      int64  `json:"groupId" comment:"所属组id"` | ||||
| 	OpenStatus   int64  `json:"openStatus" comment:"开启状态 0=关闭 1=开启"` | ||||
| 	Id              int             `uri:"id" comment:"id"` // id | ||||
| 	ExchangeType    string          `json:"exchangeType" comment:"交易所code"` | ||||
| 	UserId          int64           `json:"userId" comment:"用户id"` | ||||
| 	JysId           int64           `json:"jysId" comment:"关联交易所账号id"` | ||||
| 	ApiName         string          `json:"apiName" comment:"api用户名"` | ||||
| 	ApiKey          string          `json:"apiKey" comment:"apiKey"` | ||||
| 	ApiSecret       string          `json:"apiSecret" comment:"apiSecret"` | ||||
| 	IpAddress       string          `json:"ipAddress" comment:"代理地址"` | ||||
| 	UserPass        string          `json:"userPass" comment:"代码账号密码"` | ||||
| 	Affiliation     int64           `json:"affiliation" comment:"归属:1=现货,2=合约,3=现货合约"` | ||||
| 	AdminShow       int64           `json:"adminShow" comment:"是否超管可见:1=是,0=否"` | ||||
| 	Site            string          `json:"site" comment:"允许下单的方向:1=多;2=空;3=多空"` | ||||
| 	Subordinate     string          `json:"subordinate" comment:"从属关系:0=未绑定关系;1=主账号;2=副帐号"` | ||||
| 	GroupId         int64           `json:"groupId" comment:"所属组id"` | ||||
| 	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 | ||||
| } | ||||
|  | ||||
| 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) { | ||||
| 	if s.Id == 0 { | ||||
| 		model.Model = common.Model{Id: s.Id} | ||||
| 	} | ||||
| 	model.ExchangeType = s.ExchangeType | ||||
| 	model.UserId = s.UserId | ||||
| 	model.JysId = s.JysId | ||||
| 	model.ApiName = s.ApiName | ||||
| 	model.ApiKey = s.ApiKey | ||||
| 	model.ApiSecret = s.ApiSecret | ||||
| 	model.IpAddress = s.IpAddress | ||||
| 	model.UserPass = s.UserPass | ||||
| 	model.AdminId = s.AdminId | ||||
| 	model.Affiliation = s.Affiliation | ||||
| 	model.AdminShow = s.AdminShow | ||||
| 	model.Site = s.Site | ||||
| 	model.Subordinate = s.Subordinate | ||||
| 	model.GroupId = s.GroupId | ||||
| 	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 // 添加这而,需要记录是被谁更新的 | ||||
| } | ||||
|  | ||||
| @ -159,3 +206,26 @@ type LineApiUserBindSubordinateReq struct { | ||||
| type GetMainUserReq struct { | ||||
| 	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/go-admin-team/go-admin-core/logger" | ||||
| 	"github.com/go-admin-team/go-admin-core/sdk/service" | ||||
| 	"github.com/go-redis/redis/v8" | ||||
| 	"gorm.io/gorm" | ||||
|  | ||||
| 	"go-admin/app/admin/models" | ||||
| @ -25,6 +26,43 @@ type LineApiUser struct { | ||||
| 	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列表 | ||||
| func (e *LineApiUser) GetPage(c *dto.LineApiUserGetPageReq, p *actions.DataPermission, list *[]models.LineApiUser, count *int64) error { | ||||
| 	var err error | ||||
| @ -97,13 +135,37 @@ func (e *LineApiUser) Insert(c *dto.LineApiUserInsertReq) error { | ||||
| 	var err error | ||||
| 	var data models.LineApiUser | ||||
| 	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 { | ||||
| 		e.Log.Errorf("LineApiUserService Insert error:%s \r\n", err) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	e.saveCache(data) | ||||
| 	if err2 := e.CacheRelation(); err2 != nil { | ||||
| 		return err2 | ||||
| 	} | ||||
|  | ||||
| 	val, _ := sonic.MarshalString(&data) | ||||
|  | ||||
| 	if val != "" { | ||||
| @ -131,7 +193,7 @@ func (e *LineApiUser) restartWebsocket(data models.LineApiUser) { | ||||
| 		fuSocket.Stop() | ||||
| 	} | ||||
|  | ||||
| 	e.saveCache(data) | ||||
| 	e.CacheRelation() | ||||
|  | ||||
| 	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订阅 | ||||
| */ | ||||
| @ -197,6 +276,7 @@ func (e *LineApiUser) Update(c *dto.LineApiUserUpdateReq, p *actions.DataPermiss | ||||
| 		actions.Permission(data.TableName(), p), | ||||
| 	).First(&data, c.GetId()) | ||||
| 	oldApiKey := data.ApiKey | ||||
| 	oldReverseApiId := data.ReverseApiId | ||||
|  | ||||
| 	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 { | ||||
| 		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) | ||||
| 		if err = db.Error; err != nil { | ||||
| 			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 | ||||
| 	} | ||||
|  | ||||
| 	e.saveCache(data) | ||||
| 	if err2 := e.CacheRelation(); err2 != nil { | ||||
| 		return err2 | ||||
| 	} | ||||
|  | ||||
| 	//旧key和新的key不一样,则关闭旧的websocket | ||||
| 	if oldApiKey != data.ApiKey { | ||||
| @ -289,10 +398,14 @@ func (e *LineApiUser) Remove(d *dto.LineApiUserDeleteReq, p *actions.DataPermiss | ||||
|  | ||||
| 	_, err := helper.DefaultRedis.BatchDeleteKeys(delKeys) | ||||
|  | ||||
| 	if err != nil { | ||||
| 	if err != nil && !errors.Is(err, redis.Nil) { | ||||
| 		e.Log.Error("批量删除api_user key失败", err) | ||||
| 	} | ||||
|  | ||||
| 	if err2 := e.CacheRelation(); err2 != nil { | ||||
| 		return err2 | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| @ -414,3 +527,44 @@ func (e *LineApiUser) GetActiveApis(apiIds []int) ([]int, error) { | ||||
|  | ||||
| 	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 ( | ||||
| 	"errors" | ||||
|  | ||||
|     "github.com/go-admin-team/go-admin-core/sdk/service" | ||||
| 	"github.com/go-admin-team/go-admin-core/sdk/service" | ||||
| 	"gorm.io/gorm" | ||||
|  | ||||
| 	"go-admin/app/admin/models" | ||||
| @ -59,9 +59,9 @@ func (e *LineUserSetting) Get(d *dto.LineUserSettingGetReq, p *actions.DataPermi | ||||
|  | ||||
| // Insert 创建LineUserSetting对象 | ||||
| func (e *LineUserSetting) Insert(c *dto.LineUserSettingInsertReq) error { | ||||
|     var err error | ||||
|     var data models.LineUserSetting | ||||
|     c.Generate(&data) | ||||
| 	var err error | ||||
| 	var data models.LineUserSetting | ||||
| 	c.Generate(&data) | ||||
| 	err = e.Orm.Create(&data).Error | ||||
| 	if err != nil { | ||||
| 		e.Log.Errorf("LineUserSettingService Insert error:%s \r\n", err) | ||||
| @ -72,22 +72,22 @@ func (e *LineUserSetting) Insert(c *dto.LineUserSettingInsertReq) error { | ||||
|  | ||||
| // Update 修改LineUserSetting对象 | ||||
| func (e *LineUserSetting) Update(c *dto.LineUserSettingUpdateReq, p *actions.DataPermission) error { | ||||
|     var err error | ||||
|     var data = models.LineUserSetting{} | ||||
|     e.Orm.Scopes( | ||||
|             actions.Permission(data.TableName(), p), | ||||
|         ).First(&data, c.GetId()) | ||||
|     c.Generate(&data) | ||||
| 	var err error | ||||
| 	var data = models.LineUserSetting{} | ||||
| 	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("LineUserSettingService Save error:%s \r\n", err) | ||||
|         return err | ||||
|     } | ||||
|     if db.RowsAffected == 0 { | ||||
|         return errors.New("无权更新该数据") | ||||
|     } | ||||
|     return nil | ||||
| 	db := e.Orm.Save(&data) | ||||
| 	if err = db.Error; err != nil { | ||||
| 		e.Log.Errorf("LineUserSettingService Save error:%s \r\n", err) | ||||
| 		return err | ||||
| 	} | ||||
| 	if db.RowsAffected == 0 { | ||||
| 		return errors.New("无权更新该数据") | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Remove 删除LineUserSetting | ||||
| @ -99,11 +99,23 @@ func (e *LineUserSetting) Remove(d *dto.LineUserSettingDeleteReq, p *actions.Dat | ||||
| 			actions.Permission(data.TableName(), p), | ||||
| 		).Delete(&data, d.GetId()) | ||||
| 	if err := db.Error; err != nil { | ||||
|         e.Log.Errorf("Service RemoveLineUserSetting error:%s \r\n", err) | ||||
|         return err | ||||
|     } | ||||
|     if db.RowsAffected == 0 { | ||||
|         return errors.New("无权删除该数据") | ||||
|     } | ||||
| 		e.Log.Errorf("Service RemoveLineUserSetting error:%s \r\n", err) | ||||
| 		return err | ||||
| 	} | ||||
| 	if db.RowsAffected == 0 { | ||||
| 		return errors.New("无权删除该数据") | ||||
| 	} | ||||
| 	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) | ||||
| 	}) | ||||
|  | ||||
| 	//自动重启websocket | ||||
| 	utility.SafeGo(func() { | ||||
| 		reconnect(ctx) | ||||
| 	}) | ||||
|  | ||||
| 	// 等待中断信号以优雅地关闭服务器(设置 5 秒的超时时间) | ||||
| 	quit := make(chan os.Signal, 1) | ||||
| 	signal.Notify(quit, os.Interrupt) | ||||
| @ -133,3 +138,33 @@ func clearLogJob(db *gorm.DB, ctx context.Context) { | ||||
| 		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/go-redis/redis/v8" | ||||
| 	"github.com/shopspring/decimal" | ||||
| ) | ||||
|  | ||||
| // RedisHelper 结构体封装了 Redis 客户端及上下文 | ||||
| @ -460,44 +461,76 @@ func (r *RedisHelper) SetNX(key string, value interface{}, expiration time.Durat | ||||
| 	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{}) | ||||
| 	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++ { | ||||
| 		field := typ.Field(i) | ||||
| 		tag := field.Tag.Get("redis") | ||||
| 		tag := field.Tag.Get("redis") // 获取 redis tag | ||||
| 		if tag != "" { | ||||
| 			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 { | ||||
| 					for j := 0; j < fieldVal.Len(); j++ { | ||||
| 						elem := fieldVal.Index(j).Interface() | ||||
| 						fields[fmt.Sprintf("%s_%d", tag, j)] = elem | ||||
| 					} | ||||
| 				} else if fieldVal.Kind() == reflect.Map { | ||||
| 					// 对于映射,使用键作为字段名 | ||||
| 					for _, key := range fieldVal.MapKeys() { | ||||
| 						elem := fieldVal.MapIndex(key).Interface() | ||||
| 						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() | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	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 | ||||
| 	return fields, nil | ||||
| } | ||||
|  | ||||
| // HSetField 设置哈希中的一个字段 | ||||
| @ -543,6 +576,82 @@ func (r *RedisHelper) HGetAllFields(key string) (map[string]string, error) { | ||||
| 	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 删除哈希中的某个字段 | ||||
| func (r *RedisHelper) HDelField(key, field string) error { | ||||
| 	_, err := r.client.HDel(r.ctx, key, field).Result() | ||||
|  | ||||
| @ -30,6 +30,24 @@ func BusinessInit(db *gorm.DB) { | ||||
| 		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) | ||||
| 	//初始化可缓存价格交易对 | ||||
|  | ||||
| @ -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/shopspring/decimal v1.2.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/gin-swagger v1.6.0 | ||||
| 	github.com/swaggo/swag v1.16.2 | ||||
| @ -82,6 +83,7 @@ require ( | ||||
| 	github.com/cloudwego/iasm v0.2.0 // indirect | ||||
| 	github.com/coreos/go-semver v0.3.1 // 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/fatih/color v1.15.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/nyaruka/phonenumbers v1.0.55 // 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/prometheus/client_model v0.5.0 // indirect | ||||
| 	github.com/prometheus/common v0.45.0 // indirect | ||||
|  | ||||
| @ -37,6 +37,10 @@ type TradeSet struct { | ||||
| 	E            int64   `json:"-"`                            //推送时间 | ||||
| } | ||||
|  | ||||
| func (e *TradeSet) GetSymbol() string { | ||||
| 	return e.Coin + e.Currency | ||||
| } | ||||
|  | ||||
| //CommissionType  int     `db:"commissiontype"`                 //手续费:1买,2卖,3双向 | ||||
| //DepositNum      float64 `db:"depositnum" json:"depositnum"`     //保证金规模(手) | ||||
| //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 { | ||||
| 	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 | ||||
| } | ||||
|  | ||||
| // 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账号信息 | ||||
| */ | ||||
|  | ||||
| @ -16,7 +16,7 @@ import ( | ||||
| ) | ||||
|  | ||||
| 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{}) | ||||
| 	var apiUserInfo models.LineApiUser | ||||
| 	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 | ||||
| } | ||||
|  | ||||
| // 回去反单默认配置 | ||||
| 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/futuresdto" | ||||
| 	"go-admin/pkg/httputils" | ||||
| 	"go-admin/pkg/retryhelper" | ||||
| 	"go-admin/pkg/utility" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| @ -598,6 +599,49 @@ func (e FutRestApi) GetHoldeData(apiInfo *DbModels.LineApiUser, symbol, side str | ||||
| 	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) { | ||||
| 	holdes, err := e.GetPositionV3(apiInfo, symbol) | ||||
| @ -715,6 +759,15 @@ func (e FutRestApi) ClosePosition(symbol string, orderSn string, quantity decima | ||||
| 	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 通过单个订单号取消合约委托 | ||||
| // symbol 交易对 | ||||
| // newClientOrderId 系统自定义订单号 | ||||
| @ -774,6 +827,27 @@ func (e FutRestApi) CancelAllFutOrder(apiUserInfo DbModels.LineApiUser, symbol s | ||||
| 	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 批量撤销订单 | ||||
| // symbol 交易对 | ||||
| // 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"] | ||||
| 	originOrderSn := mapData["C"] //取消操作 代表原始订单号 | ||||
| @ -61,34 +61,15 @@ func ChangeFutureOrder(mapData map[string]interface{}) { | ||||
| 	} | ||||
| 	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 { | ||||
| 		logger.Error("合约订单回调失败,查询订单失败:", orderSn, " err:", err) | ||||
| 		return | ||||
| 		logger.Errorf("合约订单回调失败,反单失败:%v", err) | ||||
| 	} | ||||
|  | ||||
| 	// 解析订单状态 | ||||
| 	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 { | ||||
| 	ApiId            int             `json:"api_id"`     //api用户id | ||||
| 	Symbol           string          `json:"symbol"`     //合约交易对 | ||||
| 	Side             string          `json:"side"`       //购买方向 | ||||
| 	Quantity         decimal.Decimal `json:"quantity"`   //数量 | ||||
| 	Price            decimal.Decimal `json:"price"`      //限价单价 | ||||
| 	SideType         string          `json:"side_type"`  //现价或者市价 | ||||
| 	OpenOrder        int             `json:"open_order"` //是否开启限价单止盈止损 | ||||
| 	Profit           decimal.Decimal `json:"profit"`     //止盈价格 | ||||
| 	StopPrice        decimal.Decimal `json:"stopprice"`  //止损价格 | ||||
| 	OrderType        string          `json:"order_type"` //订单类型,市价或限价MARKET(市价单) TAKE_PROFIT_MARKET(市价止盈) TAKE_PROFIT(限价止盈) STOP (限价止损)  STOP_MARKET(市价止损) | ||||
| 	ApiId            int             `json:"api_id"`        //api用户id | ||||
| 	Symbol           string          `json:"symbol"`        //合约交易对 | ||||
| 	Side             string          `json:"side"`          //购买方向 | ||||
| 	PositionSide     string          `json:"position_side"` //持仓方向 | ||||
| 	Quantity         decimal.Decimal `json:"quantity"`      //数量 | ||||
| 	Price            decimal.Decimal `json:"price"`         //限价单价 | ||||
| 	SideType         string          `json:"side_type"`     //现价或者市价 | ||||
| 	OpenOrder        int             `json:"open_order"`    //是否开启限价单止盈止损 | ||||
| 	Profit           decimal.Decimal `json:"profit"`        //止盈价格 | ||||
| 	StopPrice        decimal.Decimal `json:"stopprice"`     //止损价格 | ||||
| 	OrderType        string          `json:"order_type"`    //订单类型,市价或限价MARKET(市价单) TAKE_PROFIT_MARKET(市价止盈) TAKE_PROFIT(限价止盈) STOP (限价止损)  STOP_MARKET(市价止损) | ||||
| 	NewClientOrderId string          `json:"newClientOrderId"` | ||||
| 	ClosePosition    bool            `json:"closePosition"` //是否平仓 | ||||
| } | ||||
|  | ||||
| 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/go-admin-team/go-admin-core/logger" | ||||
| 	log "github.com/go-admin-team/go-admin-core/logger" | ||||
| 	"github.com/shopspring/decimal" | ||||
| 	"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"] | ||||
| 	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) | ||||
|  | ||||
| 	if ok, err := lock.AcquireWait(context.Background()); err != nil { | ||||
| 		log.Error("获取锁失败", err) | ||||
| 		logger.Error("获取锁失败", err) | ||||
| 		return | ||||
| 	} else if ok { | ||||
| 		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 | ||||
| } | ||||
|  | ||||
| // 获取反单配置 | ||||
| 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 消息内容 | ||||
|   - @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{} | ||||
| 	err = sonic.Unmarshal(msg, &dataMap) | ||||
|  | ||||
| @ -52,7 +52,7 @@ func ReceiveListen(msg []byte, listenType int) (reconnect bool, err error) { | ||||
| 			} | ||||
|  | ||||
| 			utility.SafeGo(func() { | ||||
| 				binanceservice.ChangeSpotOrder(mapData) | ||||
| 				binanceservice.ChangeSpotOrder(mapData, apiKey) | ||||
| 			}) | ||||
| 		} else { | ||||
| 			var data futuresdto.OrderTradeUpdate | ||||
| @ -64,7 +64,7 @@ func ReceiveListen(msg []byte, listenType int) (reconnect bool, err error) { | ||||
| 			} | ||||
|  | ||||
| 			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)) | ||||
|  | ||||
| 		if listenType == 0 { //现货 | ||||
| 			binanceservice.ChangeSpotOrder(dataMap) | ||||
| 			binanceservice.ChangeSpotOrder(dataMap, apiKey) | ||||
| 		} else if listenType == 1 { //合约 | ||||
| 			binanceservice.ChangeFutureOrder(dataMap) | ||||
| 			binanceservice.ChangeFutureOrder(dataMap, apiKey) | ||||
| 		} else { | ||||
| 			log.Error("executionReport 不支持的订阅类型", strconv.Itoa(listenType)) | ||||
| 		} | ||||
| @ -101,6 +101,7 @@ func ReceiveListen(msg []byte, listenType int) (reconnect bool, err error) { | ||||
|  | ||||
| 	case "eventStreamTerminated": | ||||
| 		log.Info("账户数据流被终止 type:", getWsTypeName(listenType)) | ||||
| 		return true, nil | ||||
| 	default: | ||||
| 		log.Info("未知事件 内容:", string(msg)) | ||||
| 		log.Info("未知事件", event) | ||||
|  | ||||
| @ -20,6 +20,7 @@ import ( | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"sync/atomic" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/bytedance/sonic" | ||||
| @ -42,7 +43,9 @@ type BinanceWebSocketManager struct { | ||||
| 	isStopped    bool       // 标记 WebSocket 是否已主动停止 | ||||
| 	mu           sync.Mutex // 用于控制并发访问 isStopped | ||||
| 	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() { | ||||
| 	utility.SafeGo(wm.run) | ||||
| 	// wm.run() | ||||
| @ -91,14 +98,33 @@ func (wm *BinanceWebSocketManager) Restart(apiKey, apiSecret, proxyType, proxyAd | ||||
| 		wm.isStopped = false | ||||
| 		utility.SafeGo(wm.run) | ||||
| 	} else { | ||||
| 		wm.reconnect <- struct{}{} | ||||
| 		log.Warnf("调用restart") | ||||
| 		wm.triggerReconnect(true) | ||||
| 	} | ||||
|  | ||||
| 	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) { | ||||
| 	wm.reconnect <- struct{}{} | ||||
| 	log.Warnf("调用restart") | ||||
| 	wm.triggerReconnect(true) | ||||
| } | ||||
| func (wm *BinanceWebSocketManager) run() { | ||||
| 	ctx, cancel := context.WithCancel(context.Background()) | ||||
| @ -201,6 +227,8 @@ func (wm *BinanceWebSocketManager) connect(ctx context.Context) error { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// 连接成功,更新连接时间 | ||||
| 	wm.ConnectTime = time.Now() | ||||
| 	log.Info(fmt.Sprintf("已连接到 Binance %s WebSocket【%s】 key:%s", getWsTypeName(wm.wsType), wm.apiKey, listenKey)) | ||||
|  | ||||
| 	// Ping处理 | ||||
| @ -222,10 +250,88 @@ func (wm *BinanceWebSocketManager) connect(ctx context.Context) error { | ||||
| 		return nil | ||||
| 	}) | ||||
|  | ||||
| 	// utility.SafeGoParam(wm.restartConnect, ctx) | ||||
| 	utility.SafeGo(func() { wm.startListenKeyRenewal2(ctx) }) | ||||
| 	utility.SafeGo(func() { wm.readMessages(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 | ||||
| } | ||||
| @ -251,6 +357,7 @@ func setLastTime(wm *BinanceWebSocketManager) { | ||||
| 	if val != "" { | ||||
| 		helper.DefaultRedis.SetString(subKey, val) | ||||
| 	} | ||||
|  | ||||
| } | ||||
|  | ||||
| func (wm *BinanceWebSocketManager) getDialer() (*websocket.Dialer, error) { | ||||
| @ -357,7 +464,8 @@ func (wm *BinanceWebSocketManager) readMessages(ctx context.Context) { | ||||
| 			_, msg, err := wm.ws.ReadMessage() | ||||
| 			if err != nil && strings.Contains(err.Error(), "websocket: close") { | ||||
| 				if !wm.isStopped { | ||||
| 					wm.reconnect <- struct{}{} | ||||
| 					log.Error("收到关闭消息", err.Error()) | ||||
| 					wm.triggerReconnect(false) | ||||
| 				} | ||||
|  | ||||
| 				log.Error("websocket 关闭") | ||||
| @ -375,11 +483,13 @@ func (wm *BinanceWebSocketManager) readMessages(ctx context.Context) { | ||||
| func (wm *BinanceWebSocketManager) handleOrderUpdate(msg []byte) { | ||||
| 	setLastTime(wm) | ||||
|  | ||||
| 	if reconnect, _ := ReceiveListen(msg, wm.wsType); reconnect { | ||||
| 		wm.reconnect <- struct{}{} | ||||
| 	if reconnect, _ := ReceiveListen(msg, wm.wsType, wm.apiKey); reconnect { | ||||
| 		log.Errorf("收到重连请求") | ||||
| 		wm.triggerReconnect(false) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Stop 安全停止 WebSocket | ||||
| func (wm *BinanceWebSocketManager) Stop() { | ||||
| 	wm.mu.Lock() | ||||
| 	defer wm.mu.Unlock() | ||||
| @ -387,9 +497,8 @@ func (wm *BinanceWebSocketManager) Stop() { | ||||
| 	if wm.isStopped { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	wm.isStopped = true | ||||
| 	// 关闭 stopChannel(确保已经关闭,避免 panic) | ||||
|  | ||||
| 	select { | ||||
| 	case <-wm.stopChannel: | ||||
| 	default: | ||||
| @ -398,107 +507,182 @@ func (wm *BinanceWebSocketManager) Stop() { | ||||
|  | ||||
| 	if wm.cancelFunc != nil { | ||||
| 		wm.cancelFunc() | ||||
| 		wm.cancelFunc = nil | ||||
| 	} | ||||
|  | ||||
| 	if wm.ws != nil { | ||||
| 		if err := wm.ws.Close(); err != nil { | ||||
| 			log.Error(fmt.Sprintf("key【%s】close失败", wm.apiKey), err) | ||||
| 		} else { | ||||
| 			log.Info(fmt.Sprintf("key【%s】close", wm.apiKey)) | ||||
| 			log.Errorf("WebSocket Close 错误 key:%s err:%v", wm.apiKey, err) | ||||
| 		} | ||||
| 		wm.ws = nil | ||||
| 	} | ||||
|  | ||||
| 	// **重新创建 stopChannel,避免 Restart() 时无效** | ||||
| 	wm.stopChannel = make(chan struct{}) | ||||
| 	log.Infof("WebSocket 已完全停止 key:%s", wm.apiKey) | ||||
| 	wm.stopChannel = make(chan struct{}, 10) | ||||
| } | ||||
|  | ||||
| // 重连机制 | ||||
| // handleReconnect 使用指数退避并保持永不退出 | ||||
| func (wm *BinanceWebSocketManager) handleReconnect(ctx context.Context) { | ||||
| 	maxRetries := 5 // 最大重试次数 | ||||
| 	const maxRetries = 100 | ||||
| 	baseDelay := time.Second * 2 | ||||
| 	retryCount := 0 | ||||
|  | ||||
| 	for { | ||||
| 		select { | ||||
| 		case <-ctx.Done(): | ||||
| 			log.Infof("handleReconnect context done: %s", wm.apiKey) | ||||
| 			return | ||||
|  | ||||
| 		case <-wm.reconnect: | ||||
| 			wm.mu.Lock() | ||||
| 			if wm.isStopped { | ||||
| 				wm.mu.Unlock() | ||||
| 				return | ||||
| 			} | ||||
| 			wm.mu.Unlock() | ||||
|  | ||||
| 			log.Warn("WebSocket 连接断开,尝试重连...") | ||||
|  | ||||
| 			if wm.ws != nil { | ||||
| 				wm.ws.Close() | ||||
| 			} | ||||
|  | ||||
| 			// 取消旧的上下文 | ||||
| 			if wm.cancelFunc != nil { | ||||
| 				wm.cancelFunc() | ||||
| 			} | ||||
| 			log.Warnf("WebSocket 连接断开,准备重连 key:%s", wm.apiKey) | ||||
|  | ||||
| 			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()) | ||||
| 				wm.cancelFunc = cancel // 更新 cancelFunc | ||||
| 				wm.mu.Lock() | ||||
| 				wm.cancelFunc = cancel | ||||
| 				wm.mu.Unlock() | ||||
|  | ||||
| 				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() | ||||
| 					retryCount++ | ||||
|  | ||||
| 					if retryCount >= maxRetries { | ||||
| 						log.Error("重连失败次数过多,退出重连逻辑") | ||||
| 						log.Errorf("❌ 重连失败次数过多,停止重连逻辑 key:%s", wm.apiKey) | ||||
| 						wm.reconnecting.Store(false) | ||||
| 						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 | ||||
| 				} | ||||
|  | ||||
| 				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 | ||||
| 			} | ||||
| 			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 | ||||
| func (wm *BinanceWebSocketManager) startListenKeyRenewal(ctx context.Context, listenKey string) { | ||||
| 	time.Sleep(30 * time.Minute) | ||||
| // func (wm *BinanceWebSocketManager) startListenKeyRenewal(ctx context.Context, listenKey string) { | ||||
| // 	time.Sleep(30 * time.Minute) | ||||
|  | ||||
| 	select { | ||||
| 	case <-ctx.Done(): | ||||
| 		return | ||||
| 	default: | ||||
| 		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{}{} | ||||
| 		} | ||||
| 	} | ||||
| // 	select { | ||||
| // 	case <-ctx.Done(): | ||||
| // 		return | ||||
| // 	default: | ||||
| // 		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.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) { | ||||
| @ -527,49 +711,34 @@ func (wm *BinanceWebSocketManager) startListenKeyRenewal2(ctx context.Context) { | ||||
| /* | ||||
| 删除listenkey | ||||
| */ | ||||
| func (wm *BinanceWebSocketManager) deleteListenKey(listenKey string) error { | ||||
| 	client, err := wm.createBinanceClient() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| // func (wm *BinanceWebSocketManager) deleteListenKey(listenKey string) error { | ||||
| // 	client, err := wm.createBinanceClient() | ||||
| // 	if err != nil { | ||||
| // 		return err | ||||
| // 	} | ||||
|  | ||||
| 	var resp []byte | ||||
| // 	var resp []byte | ||||
|  | ||||
| 	switch wm.wsType { | ||||
| 	case 0: | ||||
| 		path := fmt.Sprintf("/api/v3/userDataStream") | ||||
| 		params := map[string]interface{}{ | ||||
| 			"listenKey": listenKey, | ||||
| 		} | ||||
| 		resp, _, err = client.SendSpotRequestByKey(path, "DELETE", params) | ||||
| // 	switch wm.wsType { | ||||
| // 	case 0: | ||||
| // 		path := fmt.Sprintf("/api/v3/userDataStream") | ||||
| // 		params := map[string]interface{}{ | ||||
| // 			"listenKey": listenKey, | ||||
| // 		} | ||||
| // 		resp, _, err = client.SendSpotRequestByKey(path, "DELETE", params) | ||||
|  | ||||
| 		log.Debug(fmt.Sprintf("deleteListenKey resp: %s", string(resp))) | ||||
| 	case 1: | ||||
| 		resp, _, err = client.SendFuturesRequestByKey("/fapi/v1/listenKey", "DELETE", nil) | ||||
| 		log.Debug(fmt.Sprintf("deleteListenKey resp: %s", string(resp))) | ||||
| 	default: | ||||
| 		return errors.New("unknown ws type") | ||||
| 	} | ||||
| // 		log.Debug(fmt.Sprintf("deleteListenKey resp: %s", string(resp))) | ||||
| // 	case 1: | ||||
| // 		resp, _, err = client.SendFuturesRequestByKey("/fapi/v1/listenKey", "DELETE", nil) | ||||
| // 		log.Debug(fmt.Sprintf("deleteListenKey resp: %s", string(resp))) | ||||
| // 	default: | ||||
| // 		return errors.New("unknown ws type") | ||||
| // 	} | ||||
|  | ||||
| 	return err | ||||
| } | ||||
| // 	return err | ||||
| // } | ||||
|  | ||||
| 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() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
|  | ||||
		Reference in New Issue
	
	Block a user