SHA256
Generated
+10
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GoImports">
|
||||
<option name="excludedPackages">
|
||||
<array>
|
||||
<option value="golang.org/x/net/context" />
|
||||
</array>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
+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>
|
||||
|
||||
+3
-3
@@ -24,8 +24,8 @@ function AppLayout() {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<Layout style={{ minHeight: '100vh' }}>
|
||||
<Header style={{ display: 'flex', alignItems: 'center', padding: '0 24px' }}>
|
||||
<Layout style={{ height: '100vh' }}>
|
||||
<Header style={{ display: 'flex', alignItems: 'center', padding: '0 24px', flexShrink: 0 }}>
|
||||
<div style={{ color: '#fff', fontSize: 18, fontWeight: 'bold', marginRight: 40 }}>
|
||||
AShareView
|
||||
</div>
|
||||
@@ -38,7 +38,7 @@ function AppLayout() {
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
</Header>
|
||||
<Content style={{ padding: 24, background: '#f5f5f5' }}>
|
||||
<Content style={{ padding: 16, background: '#f5f5f5', overflow: 'auto' }}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/consecutive" element={<ConsecutivePage />} />
|
||||
|
||||
+11
-106
@@ -1,111 +1,16 @@
|
||||
:root {
|
||||
--text: #6b6375;
|
||||
--text-h: #08060d;
|
||||
--bg: #fff;
|
||||
--border: #e5e4e7;
|
||||
--code-bg: #f4f3ec;
|
||||
--accent: #aa3bff;
|
||||
--accent-bg: rgba(170, 59, 255, 0.1);
|
||||
--accent-border: rgba(170, 59, 255, 0.5);
|
||||
--social-bg: rgba(244, 243, 236, 0.5);
|
||||
--shadow:
|
||||
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
|
||||
|
||||
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
|
||||
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
|
||||
--mono: ui-monospace, Consolas, monospace;
|
||||
|
||||
font: 18px/145% var(--sans);
|
||||
letter-spacing: 0.18px;
|
||||
color-scheme: light dark;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--text: #9ca3af;
|
||||
--text-h: #f3f4f6;
|
||||
--bg: #16171d;
|
||||
--border: #2e303a;
|
||||
--code-bg: #1f2028;
|
||||
--accent: #c084fc;
|
||||
--accent-bg: rgba(192, 132, 252, 0.15);
|
||||
--accent-border: rgba(192, 132, 252, 0.5);
|
||||
--social-bg: rgba(47, 48, 58, 0.5);
|
||||
--shadow:
|
||||
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
|
||||
}
|
||||
|
||||
#social .button-icon {
|
||||
filter: invert(1) brightness(2);
|
||||
}
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 1126px;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
border-inline: 1px solid var(--border);
|
||||
min-height: 100svh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
font-family: var(--heading);
|
||||
font-weight: 500;
|
||||
color: var(--text-h);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 56px;
|
||||
letter-spacing: -1.68px;
|
||||
margin: 32px 0;
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 36px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
}
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
line-height: 118%;
|
||||
letter-spacing: -0.24px;
|
||||
margin: 0 0 8px;
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
code,
|
||||
.counter {
|
||||
font-family: var(--mono);
|
||||
display: inline-flex;
|
||||
border-radius: 4px;
|
||||
color: var(--text-h);
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 15px;
|
||||
line-height: 135%;
|
||||
padding: 4px 8px;
|
||||
background: var(--code-bg);
|
||||
font-family: system-ui, 'Segoe UI', Roboto, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user