1、新增首页合计详情页;
2、新增索引和缓存;
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
"level_go/internal/api"
|
||||
@@ -12,6 +13,8 @@ import (
|
||||
|
||||
// main 是服务启动入口,负责完成配置加载、数据库连接、依赖组装和 HTTP 路由启动。
|
||||
func main() {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
|
||||
// 初始化配置文件,后续数据库 DSN 和服务端口都从全局配置中读取。
|
||||
config.InitConfig()
|
||||
|
||||
@@ -21,6 +24,12 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatal("数据库连接失败:", err)
|
||||
}
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
log.Fatal("获取数据库连接池失败:", err)
|
||||
}
|
||||
sqlDB.SetMaxIdleConns(20)
|
||||
sqlDB.SetMaxOpenConns(100)
|
||||
|
||||
// 依赖注入:Service 持有数据库连接,Handler 持有对应 Service。
|
||||
commonSrv := &service.CommonService{DB: db}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE `stock_limit_combined_data`
|
||||
ADD INDEX `idx_ts_code_date_status` (`ts_code`, `trade_date`, `final_status`),
|
||||
ADD INDEX `idx_trade_status_code` (`trade_date`, `final_status`, `ts_code`);
|
||||
@@ -110,5 +110,7 @@ CREATE TABLE `stock_limit_combined_data` (
|
||||
UNIQUE KEY `uniq_date_code` (`trade_date`,`ts_code`),
|
||||
KEY `idx_trade_date` (`trade_date`),
|
||||
KEY `idx_symbol` (`symbol`),
|
||||
KEY `idx_trade_date_status` (`trade_date`,`final_status`)
|
||||
KEY `idx_trade_date_status` (`trade_date`,`final_status`),
|
||||
KEY `idx_ts_code_date_status` (`ts_code`,`trade_date`,`final_status`),
|
||||
KEY `idx_trade_status_code` (`trade_date`,`final_status`,`ts_code`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=167035 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
@@ -63,6 +63,10 @@ GET /api/v1/limit-board/second-cal?tradeStartDate=2026-05-07&tradeEndDate=2026-0
|
||||
|
||||
### 4.2 list[].stocks 明细字段
|
||||
|
||||
`list[].stocks` 保留当日全部二板明细。
|
||||
|
||||
`list[].total_stocks` 为当日涨停和炸板合并后的明细列表,字段结构与 `stocks` 相同;该列表会剔除 `first_limit_time` 为空的数据,并按 `first_limit_time` 升序排序,时间相同时按 `ts_code` 升序排序。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `trade_date` | `string` | T 日 |
|
||||
@@ -121,6 +125,31 @@ GET /api/v1/limit-board/second-cal?tradeStartDate=2026-05-07&tradeEndDate=2026-0
|
||||
"break_next_data_count": 3,
|
||||
"break_next_profit_count": 3,
|
||||
"break_next_profit_rate": 100,
|
||||
"total_stocks": [
|
||||
{
|
||||
"trade_date": "2026-05-07",
|
||||
"previous_trade_date": "2026-05-06",
|
||||
"previous_two_trade_date": "2026-04-30",
|
||||
"next_trade_date": "2026-05-08",
|
||||
"symbol": "000001",
|
||||
"ts_code": "000001.SZ",
|
||||
"name": "示例股份",
|
||||
"market": "主板",
|
||||
"industry": "银行",
|
||||
"previous_close": 10,
|
||||
"limit_price": 11,
|
||||
"t_high": 11,
|
||||
"t_close": 11,
|
||||
"t_pct_chg": 10,
|
||||
"status": "limit",
|
||||
"first_limit_time": "09:35:12",
|
||||
"open_times": 0,
|
||||
"next_high": 11.2,
|
||||
"next_close": 11.1,
|
||||
"next_intraday_profitable": true,
|
||||
"next_high_profit_pct": 1.82
|
||||
}
|
||||
],
|
||||
"stocks": [
|
||||
{
|
||||
"trade_date": "2026-05-07",
|
||||
|
||||
@@ -261,6 +261,7 @@ type SecondCalDay struct {
|
||||
BreakNextProfitCount int `json:"break_next_profit_count"` // 当日炸板股 T+1 盘中盈利数量。
|
||||
BreakNextProfitRate float64 `json:"break_next_profit_rate"` // 当日炸板股 T+1 盘中盈利概率,百分比。
|
||||
Stocks []SecondCalStock `json:"stocks"` // 当日二板股票明细。
|
||||
TotalStocks []SecondCalStock `json:"total_stocks"` // 涨停和炸板合并后按首次涨停时间排序的明细。
|
||||
}
|
||||
|
||||
// SecondCalResponse 是二板日历接口响应。
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
responseCacheTTL = 5 * time.Minute
|
||||
responseCacheMaxBody = 4 << 20
|
||||
responseCacheMaxKeys = 512
|
||||
)
|
||||
|
||||
type responseCacheEntry struct {
|
||||
status int
|
||||
contentType string
|
||||
body []byte
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
type responseCacheStore struct {
|
||||
mu sync.RWMutex
|
||||
items map[string]responseCacheEntry
|
||||
}
|
||||
|
||||
func newResponseCacheStore() *responseCacheStore {
|
||||
return &responseCacheStore{items: make(map[string]responseCacheEntry)}
|
||||
}
|
||||
|
||||
func (s *responseCacheStore) get(key string) (responseCacheEntry, bool) {
|
||||
now := time.Now()
|
||||
|
||||
s.mu.RLock()
|
||||
entry, ok := s.items[key]
|
||||
s.mu.RUnlock()
|
||||
if !ok {
|
||||
return responseCacheEntry{}, false
|
||||
}
|
||||
if now.After(entry.expiresAt) {
|
||||
s.mu.Lock()
|
||||
if current, exists := s.items[key]; exists && now.After(current.expiresAt) {
|
||||
delete(s.items, key)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return responseCacheEntry{}, false
|
||||
}
|
||||
return entry, true
|
||||
}
|
||||
|
||||
func (s *responseCacheStore) set(key string, entry responseCacheEntry) {
|
||||
s.mu.Lock()
|
||||
if len(s.items) >= responseCacheMaxKeys {
|
||||
now := time.Now()
|
||||
for cacheKey, current := range s.items {
|
||||
if now.After(current.expiresAt) || len(s.items) >= responseCacheMaxKeys {
|
||||
delete(s.items, cacheKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
s.items[key] = entry
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
type cachedResponseWriter struct {
|
||||
gin.ResponseWriter
|
||||
body *bytes.Buffer
|
||||
}
|
||||
|
||||
func (w cachedResponseWriter) Write(data []byte) (int, error) {
|
||||
w.body.Write(data)
|
||||
return w.ResponseWriter.Write(data)
|
||||
}
|
||||
|
||||
func cacheableGETResponseMiddleware(store *responseCacheStore) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if c.Request.Method != http.MethodGet || strings.HasPrefix(c.Request.URL.Path, "/api/v1/download/") {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
key := c.Request.URL.RequestURI()
|
||||
if entry, ok := store.get(key); ok {
|
||||
c.Header("Content-Type", entry.contentType)
|
||||
c.Data(entry.status, entry.contentType, entry.body)
|
||||
return
|
||||
}
|
||||
|
||||
body := bytes.NewBuffer(nil)
|
||||
writer := cachedResponseWriter{ResponseWriter: c.Writer, body: body}
|
||||
c.Writer = writer
|
||||
c.Next()
|
||||
|
||||
status := c.Writer.Status()
|
||||
if status != http.StatusOK || body.Len() == 0 || body.Len() > responseCacheMaxBody {
|
||||
return
|
||||
}
|
||||
|
||||
contentType := c.Writer.Header().Get("Content-Type")
|
||||
if !strings.Contains(contentType, "application/json") {
|
||||
return
|
||||
}
|
||||
|
||||
store.set(key, responseCacheEntry{
|
||||
status: status,
|
||||
contentType: contentType,
|
||||
body: append([]byte(nil), body.Bytes()...),
|
||||
expiresAt: time.Now().Add(responseCacheTTL),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,8 @@ import (
|
||||
//
|
||||
// 函数集中注册跨域中间件和 v1 版本接口,调用方只需要注入各业务 Handler。
|
||||
func NewRouter(commonApi *api.CommonHandler, stockApi *api.StockHandler, limitBoardApi *api.LimitBoardHandler) *gin.Engine {
|
||||
r := gin.Default()
|
||||
r := gin.New()
|
||||
r.Use(gin.Recovery())
|
||||
|
||||
// 跨域中间件允许前端从任意来源访问接口,并暴露 CSV 下载需要的 Content-Disposition。
|
||||
r.Use(func(c *gin.Context) {
|
||||
@@ -23,6 +24,7 @@ func NewRouter(commonApi *api.CommonHandler, stockApi *api.StockHandler, limitBo
|
||||
}
|
||||
c.Next()
|
||||
})
|
||||
r.Use(cacheableGETResponseMiddleware(newResponseCacheStore()))
|
||||
|
||||
v1 := r.Group("/api/v1")
|
||||
{
|
||||
|
||||
@@ -2,6 +2,8 @@ package service
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"level_go/internal/model"
|
||||
)
|
||||
@@ -57,6 +59,7 @@ func (s *LimitBoardService) GetSecondCal(req model.SecondCalRequest) (model.Seco
|
||||
PreviousTwoTradeDate: day.PreviousTwoTradeDate,
|
||||
NextTradeDate: day.NextTradeDate,
|
||||
Stocks: []model.SecondCalStock{},
|
||||
TotalStocks: []model.SecondCalStock{},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -298,10 +301,19 @@ func accumulateSecondCalDay(day *model.SecondCalDay, item model.SecondCalStock)
|
||||
}
|
||||
}
|
||||
day.Stocks = append(day.Stocks, item)
|
||||
if strings.TrimSpace(item.FirstLimitTime) != "" {
|
||||
day.TotalStocks = append(day.TotalStocks, item)
|
||||
}
|
||||
}
|
||||
|
||||
// finalizeSecondCalDay 计算单个交易日的概率字段。
|
||||
func finalizeSecondCalDay(day *model.SecondCalDay) {
|
||||
sort.SliceStable(day.TotalStocks, func(i, j int) bool {
|
||||
if day.TotalStocks[i].FirstLimitTime == day.TotalStocks[j].FirstLimitTime {
|
||||
return day.TotalStocks[i].TsCode < day.TotalStocks[j].TsCode
|
||||
}
|
||||
return day.TotalStocks[i].FirstLimitTime < day.TotalStocks[j].FirstLimitTime
|
||||
})
|
||||
day.NextIntradayProfitRate = secondCalRate(day.NextIntradayProfitCount, day.NextTradeDataCount)
|
||||
day.LimitNextProfitRate = secondCalRate(day.LimitNextProfitCount, day.LimitNextDataCount)
|
||||
day.BreakNextProfitRate = secondCalRate(day.BreakNextProfitCount, day.BreakNextDataCount)
|
||||
|
||||
Reference in New Issue
Block a user