SHA256
+2
-4
@@ -24,7 +24,7 @@ func main() {
|
||||
}
|
||||
fmt.Println("Database connected successfully")
|
||||
|
||||
// Create indexes in background (10M rows, may take minutes)
|
||||
// Ensure indexes exist (skip if already exist)
|
||||
go func() {
|
||||
time.Sleep(3 * time.Second)
|
||||
indexes := []string{
|
||||
@@ -34,9 +34,7 @@ func main() {
|
||||
"ALTER TABLE stock_daily ADD INDEX idx_sd_code_date_close (code, date, close)",
|
||||
}
|
||||
for _, idx := range indexes {
|
||||
if err := db.Exec(idx).Error; err != nil {
|
||||
fmt.Printf("index: %v (may already exist)\n", err)
|
||||
} else {
|
||||
if err := db.Exec(idx).Error; err == nil {
|
||||
fmt.Printf("index created: %s\n", idx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,20 +97,22 @@ func (r *StockRepo) GetSummaryByDate(date string) (*model.DashboardSummary, erro
|
||||
ORDER BY sd.pct_change ASC
|
||||
`, date).Scan(&summary.LimitDownList)
|
||||
|
||||
// 100-day new high: NOT EXISTS stops early when a higher close is found
|
||||
// 100-day new high: pre-aggregate max_close with covering index
|
||||
r.db.Raw(`
|
||||
SELECT sd.code, si.name, sd.pct_change, sd.close
|
||||
FROM stock_daily sd
|
||||
LEFT JOIN stock_info si ON sd.code = si.code
|
||||
WHERE sd.date = ? AND sd.close > 0
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM stock_daily sd2
|
||||
WHERE sd2.code = sd.code
|
||||
AND sd2.date BETWEEN DATE_SUB(?, INTERVAL 150 DAY) AND ?
|
||||
AND sd2.close > sd.close
|
||||
LIMIT 1
|
||||
)
|
||||
ORDER BY sd.pct_change DESC
|
||||
SELECT t.code, si.name, t.pct_change, t.close
|
||||
FROM (
|
||||
SELECT sd.code, sd.pct_change, sd.close
|
||||
FROM stock_daily sd
|
||||
JOIN (
|
||||
SELECT code, MAX(close) as max_close
|
||||
FROM stock_daily
|
||||
WHERE date BETWEEN DATE_SUB(?, INTERVAL 150 DAY) AND ?
|
||||
GROUP BY code
|
||||
) mh ON sd.code = mh.code AND sd.close = mh.max_close
|
||||
WHERE sd.date = ? AND sd.close > 0
|
||||
) t
|
||||
LEFT JOIN stock_info si ON t.code = si.code
|
||||
ORDER BY t.pct_change DESC
|
||||
`, date, date, date).Scan(&summary.High100List)
|
||||
|
||||
return summary, nil
|
||||
|
||||
@@ -3,28 +3,92 @@ package service
|
||||
import (
|
||||
"ashareview-server/model"
|
||||
"ashareview-server/repository"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type cacheEntry[T any] struct {
|
||||
data T
|
||||
expires time.Time
|
||||
}
|
||||
|
||||
type DashboardService struct {
|
||||
repo *repository.StockRepo
|
||||
repo *repository.StockRepo
|
||||
mu sync.RWMutex
|
||||
summary *cacheEntry[*model.DashboardSummary]
|
||||
daily map[int]*cacheEntry[[]model.DailySummary]
|
||||
consec *cacheEntry[[]model.ConsecutiveLimit]
|
||||
}
|
||||
|
||||
func NewDashboardService(repo *repository.StockRepo) *DashboardService {
|
||||
return &DashboardService{repo: repo}
|
||||
return &DashboardService{repo: repo, daily: make(map[int]*cacheEntry[[]model.DailySummary])}
|
||||
}
|
||||
|
||||
const cacheTTL = 3 * time.Minute
|
||||
|
||||
func (s *DashboardService) GetSummary() (*model.DashboardSummary, error) {
|
||||
s.mu.RLock()
|
||||
if s.summary != nil && time.Now().Before(s.summary.expires) {
|
||||
data := s.summary.data
|
||||
s.mu.RUnlock()
|
||||
return data, nil
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
|
||||
date, err := s.repo.GetLatestTradeDate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.repo.GetSummaryByDate(date)
|
||||
summary, err := s.repo.GetSummaryByDate(date)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.summary = &cacheEntry[*model.DashboardSummary]{data: summary, expires: time.Now().Add(cacheTTL)}
|
||||
s.mu.Unlock()
|
||||
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
func (s *DashboardService) GetDailySummary(days int) ([]model.DailySummary, error) {
|
||||
return s.repo.GetDailySummary(days)
|
||||
s.mu.RLock()
|
||||
if e, ok := s.daily[days]; ok && time.Now().Before(e.expires) {
|
||||
data := e.data
|
||||
s.mu.RUnlock()
|
||||
return data, nil
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
|
||||
data, err := s.repo.GetDailySummary(days)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.daily[days] = &cacheEntry[[]model.DailySummary]{data: data, expires: time.Now().Add(cacheTTL)}
|
||||
s.mu.Unlock()
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (s *DashboardService) GetConsecutiveLimits() ([]model.ConsecutiveLimit, error) {
|
||||
return s.repo.GetConsecutiveLimits()
|
||||
s.mu.RLock()
|
||||
if s.consec != nil && time.Now().Before(s.consec.expires) {
|
||||
data := s.consec.data
|
||||
s.mu.RUnlock()
|
||||
return data, nil
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
|
||||
data, err := s.repo.GetConsecutiveLimits()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.consec = &cacheEntry[[]model.ConsecutiveLimit]{data: data, expires: time.Now().Add(cacheTTL)}
|
||||
s.mu.Unlock()
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
*{box-sizing:border-box;margin:0;padding:0}html,body,#root{width:100%;height:100%;overflow:hidden}body{-webkit-font-smoothing:antialiased;font-family:system-ui,Segoe UI,Roboto,sans-serif}
|
||||
File diff suppressed because one or more lines are too long
@@ -5,8 +5,8 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>web</title>
|
||||
<script type="module" crossorigin src="/assets/index-Dq_0ARKI.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DGNrK5qb.css">
|
||||
<script type="module" crossorigin src="/assets/index-B7zDPCSd.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DJHqorWS.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Reference in New Issue
Block a user