package api import ( "errors" "net/http" "strconv" "strings" "github.com/gin-gonic/gin" "level_go/internal/model" "level_go/internal/service" ) // LimitBoardHandler 承载涨停板相关 HTTP 接口。 type LimitBoardHandler struct { Service *service.LimitBoardService // 涨停板服务,封装统计、分组和导出逻辑。 } // NewLimitBoardHandler 创建涨停板接口 Handler。 func NewLimitBoardHandler(s *service.LimitBoardService) *LimitBoardHandler { return &LimitBoardHandler{Service: s} } // GetOverview 返回指定交易日的涨停股连板概览。 func (h *LimitBoardHandler) GetOverview(c *gin.Context) { var req model.LimitBoardRequest if err := c.ShouldBindQuery(&req); err != nil || req.TradeDate == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "trade_date 参数不能为空"}) return } resp, err := h.Service.GetLimitBoardOverview(req.TradeDate) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, resp) } // GetOpenLowerGroups 返回昨日涨停且今日继续涨停股票的开盘对比分组。 func (h *LimitBoardHandler) GetOpenLowerGroups(c *gin.Context) { var req model.LimitBoardRequest if err := c.ShouldBindQuery(&req); err != nil || req.TradeDate == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "trade_date 参数不能为空"}) return } resp, err := h.Service.GetLimitOpenCompareGroups(req.TradeDate) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, resp) } // GetBreakNextPerformance 返回首板后次日炸板股票的 T+1 表现分页结果。 func (h *LimitBoardHandler) GetBreakNextPerformance(c *gin.Context) { req, err := parseLimitBreakNextPerformanceRequest(c, true) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } resp, err := h.Service.GetLimitBreakNextPerformance(req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, resp) } // GetSecondCal 返回日期范围内的二板日历统计。 func (h *LimitBoardHandler) GetSecondCal(c *gin.Context) { req, err := parseSecondCalRequest(c) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } resp, err := h.Service.GetSecondCal(req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, resp) } // parseSecondCalRequest 解析二板日历接口的日期范围参数。 func parseSecondCalRequest(c *gin.Context) (model.SecondCalRequest, error) { req := model.SecondCalRequest{ TradeStartDate: strings.TrimSpace(c.DefaultQuery("tradeStartDate", c.Query("start_date"))), TradeEndDate: strings.TrimSpace(c.DefaultQuery("tradeEndDate", c.Query("end_date"))), } if req.TradeStartDate == "" { return req, errors.New("tradeStartDate 参数不能为空") } if req.TradeEndDate == "" { return req, errors.New("tradeEndDate 参数不能为空") } if err := validateSecondProfitDateRange(req.TradeStartDate, req.TradeEndDate); err != nil { return req, err } return req, nil } // parseLimitBreakNextPerformanceRequest 解析炸板次日表现接口的查询参数。 // // includePagination 为 false 时跳过分页参数,供 CSV 导出复用同一套日期和过滤条件解析逻辑。 func parseLimitBreakNextPerformanceRequest(c *gin.Context, includePagination bool) (model.LimitBreakNextPerformanceRequest, error) { req := model.LimitBreakNextPerformanceRequest{ TradeStartDate: strings.TrimSpace(c.DefaultQuery("tradeStartDate", c.Query("start_date"))), TradeEndDate: strings.TrimSpace(c.DefaultQuery("tradeEndDate", c.Query("end_date"))), Page: 1, PageSize: 20, } if req.TradeStartDate == "" { return req, errors.New("tradeStartDate 参数不能为空") } if req.TradeEndDate == "" { return req, errors.New("tradeEndDate 参数不能为空") } if err := validateSecondProfitDateRange(req.TradeStartDate, req.TradeEndDate); err != nil { return req, err } if includePagination { pageText := strings.TrimSpace(c.DefaultQuery("page", "1")) page, err := strconv.Atoi(pageText) if err != nil || page <= 0 { return req, errors.New("page 必须为正整数") } req.Page = page pageSizeText := strings.TrimSpace(c.DefaultQuery("pageSize", "20")) pageSize, err := strconv.Atoi(pageSizeText) if err != nil || pageSize <= 0 { return req, errors.New("pageSize 必须为正整数") } req.PageSize = pageSize } tOpenGainLt101, err := parseOptionalBoolQuery(c, "tOpenGainLt101", "tOpenLessThan101", "t_open_gain_lt_101") if err != nil { return req, errors.New("tOpenGainLt101 必须为布尔值") } req.TOpenGainLt101 = tOpenGainLt101 breakTimeBefore1100, err := parseOptionalBoolQuery(c, "breakTimeBefore1100", "tBreakTimeBefore1100", "break_time_before_1100") if err != nil { return req, errors.New("breakTimeBefore1100 必须为布尔值") } req.BreakTimeBefore1100 = breakTimeBefore1100 return req, nil } // parseOptionalBoolQuery 从多个兼容参数名中读取可选布尔值。 // // 返回 nil 表示用户没有传入该过滤项;返回 *bool 表示需要按 true/false 过滤。 func parseOptionalBoolQuery(c *gin.Context, keys ...string) (*bool, error) { for _, key := range keys { valueText, exists := c.GetQuery(key) if !exists { continue } valueText = strings.TrimSpace(valueText) if valueText == "" { continue } value, err := strconv.ParseBool(valueText) if err != nil { return nil, err } return &value, nil } return nil, nil } // GetOpenThresholdGroups 返回昨日涨停股今日开盘是否低于前收 101% 的分组。 func (h *LimitBoardHandler) GetOpenThresholdGroups(c *gin.Context) { var req model.LimitBoardRequest if err := c.ShouldBindQuery(&req); err != nil || req.TradeDate == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "trade_date 参数不能为空"}) return } resp, err := h.Service.GetLimitOpenThresholdGroups(req.TradeDate) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, resp) }