init
continuous-integration/drone Build is passing

This commit is contained in:
曾志威
2026-05-10 21:31:57 +08:00
parent 1b6a382970
commit f2c75c89f1
10 changed files with 783 additions and 133 deletions
+2 -4
View File
@@ -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)
}
}
+15 -13
View File
@@ -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
+69 -5
View File
@@ -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
+1
View File
@@ -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
+2 -2
View File
@@ -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>