新增接口及接口文档

This commit is contained in:
2026-04-27 00:32:09 +08:00
parent 7a7e28684c
commit ee1707c17f
10 changed files with 830 additions and 2 deletions
+33
View File
@@ -0,0 +1,33 @@
package api
import (
"net/http"
"github.com/gin-gonic/gin"
"level_go/internal/model"
"level_go/internal/service"
)
type LimitBoardHandler struct {
Service *service.LimitBoardService
}
func NewLimitBoardHandler(s *service.LimitBoardService) *LimitBoardHandler {
return &LimitBoardHandler{Service: s}
}
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)
}
+44
View File
@@ -0,0 +1,44 @@
package model
type LimitBoardRequest struct {
TradeDate string `form:"trade_date" json:"trade_date"`
}
type LimitBoardStock struct {
Symbol string `json:"symbol"`
TsCode string `json:"ts_code"`
Name string `json:"name"`
Market string `json:"market"`
TradeDate string `json:"trade_date"`
Close float64 `json:"close"`
LimitPrice float64 `json:"limit_price"`
PctChg float64 `json:"pct_chg"`
BoardCount int `json:"board_count"`
BoardLabel string `json:"board_label"`
DisplayStatus string `json:"display_status"`
}
type LimitBoardGroup struct {
BoardCount int `json:"board_count"`
Label string `json:"label"`
Count int `json:"count"`
Stocks []LimitBoardStock `json:"stocks"`
}
type LimitBoardSummary struct {
MaxBoardCount int `json:"max_board_count"`
LimitCount int `json:"limit_count"`
GroupCount int `json:"group_count"`
}
type LimitBoardDailyMax struct {
TradeDate string `json:"trade_date"`
MaxBoardCount int `json:"max_board_count"`
}
type LimitBoardResponse struct {
TradeDate string `json:"trade_date"`
Summary LimitBoardSummary `json:"summary"`
RecentMaxBoardStats []LimitBoardDailyMax `json:"recent_max_board_stats"`
Groups []LimitBoardGroup `json:"groups"`
}
+2 -1
View File
@@ -5,7 +5,7 @@ import (
"level_go/internal/api"
)
func NewRouter(commonApi *api.CommonHandler, stockApi *api.StockHandler) *gin.Engine {
func NewRouter(commonApi *api.CommonHandler, stockApi *api.StockHandler, limitBoardApi *api.LimitBoardHandler) *gin.Engine {
r := gin.Default()
// 跨域中间件 (根据需要开启)
@@ -25,6 +25,7 @@ func NewRouter(commonApi *api.CommonHandler, stockApi *api.StockHandler) *gin.En
v1.GET("/common/trade-calendar", commonApi.GetTradeCalendar)
v1.POST("/stock/analyze", stockApi.Analyze)
v1.GET("/stock/kline/:code", stockApi.GetKlineData)
v1.GET("/limit-board/overview", limitBoardApi.GetOverview)
}
return r
}
+271
View File
@@ -0,0 +1,271 @@
package service
import (
"fmt"
"sort"
"gorm.io/gorm"
"level_go/internal/model"
)
type LimitBoardService struct {
DB *gorm.DB
}
type limitBoardHistoryRow struct {
TsCode string `gorm:"column:ts_code"`
TradeDate string `gorm:"column:trade_date"`
}
func boardLabel(boardCount int) string {
switch boardCount {
case 1:
return "首板"
case 2:
return "二板"
case 3:
return "三板"
case 4:
return "四板"
case 5:
return "五板"
case 6:
return "六板"
case 7:
return "七板"
case 8:
return "八板"
case 9:
return "九板"
default:
return fmt.Sprintf("%d板", boardCount)
}
}
func (s *LimitBoardService) GetLimitBoardOverview(tradeDate string) (model.LimitBoardResponse, error) {
resp := model.LimitBoardResponse{TradeDate: tradeDate}
tradeDates, dateIndex, err := s.loadTradeCalendar(tradeDate)
if err != nil {
return resp, err
}
if dateIndex == -1 {
return resp, nil
}
resp.RecentMaxBoardStats, err = s.buildRecentMaxBoardStats(tradeDates, dateIndex)
if err != nil {
return resp, err
}
limitStocks, err := s.loadCurrentLimitStocks(tradeDate)
if err != nil {
return resp, err
}
if len(limitStocks) == 0 {
return resp, nil
}
historyMap, err := s.loadLimitHistory(limitStocks, tradeDate)
if err != nil {
return resp, err
}
groupMap, maxBoardCount := buildLimitBoardGroups(limitStocks, tradeDates, dateIndex, historyMap)
resp.Groups = sortLimitBoardGroups(groupMap)
resp.Summary = model.LimitBoardSummary{
MaxBoardCount: maxBoardCount,
LimitCount: len(limitStocks),
GroupCount: len(resp.Groups),
}
return resp, nil
}
func (s *LimitBoardService) buildRecentMaxBoardStats(tradeDates []string, dateIndex int) ([]model.LimitBoardDailyMax, error) {
startIndex := 0
if dateIndex >= 29 {
startIndex = dateIndex - 29
}
windowDates := tradeDates[startIndex : dateIndex+1]
if len(windowDates) == 0 {
return []model.LimitBoardDailyMax{}, nil
}
var history []limitBoardHistoryRow
if err := s.DB.Table("stock_limit_data").
Select("ts_code, DATE_FORMAT(trade_date, '%Y-%m-%d') AS trade_date").
Where("trade_date BETWEEN ? AND ? AND status = ?", windowDates[0], windowDates[len(windowDates)-1], 1).
Order("ts_code ASC, trade_date ASC").
Scan(&history).Error; err != nil {
return nil, err
}
dateByCode := make(map[string]map[string]struct{})
for _, item := range history {
if _, ok := dateByCode[item.TsCode]; !ok {
dateByCode[item.TsCode] = make(map[string]struct{})
}
dateByCode[item.TsCode][item.TradeDate] = struct{}{}
}
maxBoardByDate := make(map[string]int, len(windowDates))
for _, tradeDate := range windowDates {
maxBoardByDate[tradeDate] = 0
}
for _, limitDates := range dateByCode {
for idx, tradeDate := range windowDates {
if _, ok := limitDates[tradeDate]; !ok {
continue
}
boardCount := 0
for j := idx; j >= 0; j-- {
if _, ok := limitDates[windowDates[j]]; !ok {
break
}
boardCount++
}
if boardCount > maxBoardByDate[tradeDate] {
maxBoardByDate[tradeDate] = boardCount
}
}
}
stats := make([]model.LimitBoardDailyMax, 0, len(windowDates))
for _, tradeDate := range windowDates {
stats = append(stats, model.LimitBoardDailyMax{
TradeDate: tradeDate,
MaxBoardCount: maxBoardByDate[tradeDate],
})
}
return stats, nil
}
func (s *LimitBoardService) loadTradeCalendar(tradeDate string) ([]string, int, error) {
var tradeDates []string
if err := s.DB.Table("trade_calendar").
Select("DATE_FORMAT(cal_date, '%Y-%m-%d') AS trade_date").
Where("exchange = ? AND is_open = ? AND cal_date <= ?", "SSE", 1, tradeDate).
Order("cal_date ASC").
Scan(&tradeDates).Error; err != nil {
return nil, -1, err
}
dateIndex := -1
for i, d := range tradeDates {
if d == tradeDate {
dateIndex = i
break
}
}
return tradeDates, dateIndex, nil
}
func (s *LimitBoardService) loadCurrentLimitStocks(tradeDate string) ([]model.LimitBoardStock, error) {
var stocks []model.LimitBoardStock
query := `SELECT
l.symbol,
l.ts_code,
l.name,
COALESCE(l.market, '') AS market,
DATE_FORMAT(l.trade_date, '%Y-%m-%d') AS trade_date,
COALESCE(d.close, l.close, 0) AS close,
COALESCE(l.limit_price, 0) AS limit_price,
COALESCE(d.pct_chg, 0) AS pct_chg
FROM stock_limit_data l
LEFT JOIN stock_daily d
ON d.ts_code = l.ts_code AND d.trade_date = l.trade_date
WHERE l.trade_date = ? AND l.status = 1
ORDER BY d.pct_chg DESC, l.ts_code ASC`
err := s.DB.Raw(query, tradeDate).Scan(&stocks).Error
return stocks, err
}
func (s *LimitBoardService) loadLimitHistory(stocks []model.LimitBoardStock, tradeDate string) (map[string]map[string]struct{}, error) {
codes := make([]string, 0, len(stocks))
for _, stock := range stocks {
codes = append(codes, stock.TsCode)
}
var history []limitBoardHistoryRow
if err := s.DB.Table("stock_limit_data").
Select("ts_code, DATE_FORMAT(trade_date, '%Y-%m-%d') AS trade_date").
Where("ts_code IN ? AND trade_date <= ? AND status = ?", codes, tradeDate, 1).
Scan(&history).Error; err != nil {
return nil, err
}
historyMap := make(map[string]map[string]struct{}, len(codes))
for _, item := range history {
if _, ok := historyMap[item.TsCode]; !ok {
historyMap[item.TsCode] = make(map[string]struct{})
}
historyMap[item.TsCode][item.TradeDate] = struct{}{}
}
return historyMap, nil
}
func buildLimitBoardGroups(stocks []model.LimitBoardStock, tradeDates []string, dateIndex int, historyMap map[string]map[string]struct{}) (map[int][]model.LimitBoardStock, int) {
groupMap := make(map[int][]model.LimitBoardStock)
maxBoardCount := 0
for i := range stocks {
boardCount := calcBoardCount(tradeDates, dateIndex, historyMap[stocks[i].TsCode])
stocks[i].BoardCount = boardCount
stocks[i].BoardLabel = boardLabel(boardCount)
stocks[i].DisplayStatus = boardLabel(boardCount)
if boardCount > maxBoardCount {
maxBoardCount = boardCount
}
groupMap[boardCount] = append(groupMap[boardCount], stocks[i])
}
return groupMap, maxBoardCount
}
func calcBoardCount(tradeDates []string, dateIndex int, limitDates map[string]struct{}) int {
boardCount := 0
for i := dateIndex; i >= 0; i-- {
if _, ok := limitDates[tradeDates[i]]; !ok {
break
}
boardCount++
}
return boardCount
}
func sortLimitBoardGroups(groupMap map[int][]model.LimitBoardStock) []model.LimitBoardGroup {
boardCounts := make([]int, 0, len(groupMap))
for boardCount := range groupMap {
boardCounts = append(boardCounts, boardCount)
}
sort.Sort(sort.Reverse(sort.IntSlice(boardCounts)))
groups := make([]model.LimitBoardGroup, 0, len(boardCounts))
for _, boardCount := range boardCounts {
stocks := groupMap[boardCount]
sort.Slice(stocks, func(i, j int) bool {
if stocks[i].PctChg == stocks[j].PctChg {
return stocks[i].TsCode < stocks[j].TsCode
}
return stocks[i].PctChg > stocks[j].PctChg
})
groups = append(groups, model.LimitBoardGroup{
BoardCount: boardCount,
Label: boardLabel(boardCount),
Count: len(stocks),
Stocks: stocks,
})
}
return groups
}