1、新增首页合计详情页;
2、新增索引和缓存;
This commit is contained in:
@@ -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