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")
|
fmt.Println("Database connected successfully")
|
||||||
|
|
||||||
// Create indexes in background (10M rows, may take minutes)
|
// Ensure indexes exist (skip if already exist)
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(3 * time.Second)
|
time.Sleep(3 * time.Second)
|
||||||
indexes := []string{
|
indexes := []string{
|
||||||
@@ -34,9 +34,7 @@ func main() {
|
|||||||
"ALTER TABLE stock_daily ADD INDEX idx_sd_code_date_close (code, date, close)",
|
"ALTER TABLE stock_daily ADD INDEX idx_sd_code_date_close (code, date, close)",
|
||||||
}
|
}
|
||||||
for _, idx := range indexes {
|
for _, idx := range indexes {
|
||||||
if err := db.Exec(idx).Error; err != nil {
|
if err := db.Exec(idx).Error; err == nil {
|
||||||
fmt.Printf("index: %v (may already exist)\n", err)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("index created: %s\n", idx)
|
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
|
ORDER BY sd.pct_change ASC
|
||||||
`, date).Scan(&summary.LimitDownList)
|
`, 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(`
|
r.db.Raw(`
|
||||||
SELECT sd.code, si.name, sd.pct_change, sd.close
|
SELECT t.code, si.name, t.pct_change, t.close
|
||||||
FROM stock_daily sd
|
FROM (
|
||||||
LEFT JOIN stock_info si ON sd.code = si.code
|
SELECT sd.code, sd.pct_change, sd.close
|
||||||
WHERE sd.date = ? AND sd.close > 0
|
FROM stock_daily sd
|
||||||
AND NOT EXISTS (
|
JOIN (
|
||||||
SELECT 1 FROM stock_daily sd2
|
SELECT code, MAX(close) as max_close
|
||||||
WHERE sd2.code = sd.code
|
FROM stock_daily
|
||||||
AND sd2.date BETWEEN DATE_SUB(?, INTERVAL 150 DAY) AND ?
|
WHERE date BETWEEN DATE_SUB(?, INTERVAL 150 DAY) AND ?
|
||||||
AND sd2.close > sd.close
|
GROUP BY code
|
||||||
LIMIT 1
|
) mh ON sd.code = mh.code AND sd.close = mh.max_close
|
||||||
)
|
WHERE sd.date = ? AND sd.close > 0
|
||||||
ORDER BY sd.pct_change DESC
|
) t
|
||||||
|
LEFT JOIN stock_info si ON t.code = si.code
|
||||||
|
ORDER BY t.pct_change DESC
|
||||||
`, date, date, date).Scan(&summary.High100List)
|
`, date, date, date).Scan(&summary.High100List)
|
||||||
|
|
||||||
return summary, nil
|
return summary, nil
|
||||||
|
|||||||
@@ -3,28 +3,92 @@ package service
|
|||||||
import (
|
import (
|
||||||
"ashareview-server/model"
|
"ashareview-server/model"
|
||||||
"ashareview-server/repository"
|
"ashareview-server/repository"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type cacheEntry[T any] struct {
|
||||||
|
data T
|
||||||
|
expires time.Time
|
||||||
|
}
|
||||||
|
|
||||||
type DashboardService struct {
|
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 {
|
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) {
|
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()
|
date, err := s.repo.GetLatestTradeDate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
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) {
|
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" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>web</title>
|
<title>web</title>
|
||||||
<script type="module" crossorigin src="/assets/index-Dq_0ARKI.js"></script>
|
<script type="module" crossorigin src="/assets/index-B7zDPCSd.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-DGNrK5qb.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DJHqorWS.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
+3
-3
@@ -24,8 +24,8 @@ function AppLayout() {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout style={{ minHeight: '100vh' }}>
|
<Layout style={{ height: '100vh' }}>
|
||||||
<Header style={{ display: 'flex', alignItems: 'center', padding: '0 24px' }}>
|
<Header style={{ display: 'flex', alignItems: 'center', padding: '0 24px', flexShrink: 0 }}>
|
||||||
<div style={{ color: '#fff', fontSize: 18, fontWeight: 'bold', marginRight: 40 }}>
|
<div style={{ color: '#fff', fontSize: 18, fontWeight: 'bold', marginRight: 40 }}>
|
||||||
AShareView
|
AShareView
|
||||||
</div>
|
</div>
|
||||||
@@ -38,7 +38,7 @@ function AppLayout() {
|
|||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
/>
|
/>
|
||||||
</Header>
|
</Header>
|
||||||
<Content style={{ padding: 24, background: '#f5f5f5' }}>
|
<Content style={{ padding: 16, background: '#f5f5f5', overflow: 'auto' }}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Dashboard />} />
|
<Route path="/" element={<Dashboard />} />
|
||||||
<Route path="/consecutive" element={<ConsecutivePage />} />
|
<Route path="/consecutive" element={<ConsecutivePage />} />
|
||||||
|
|||||||
+11
-106
@@ -1,111 +1,16 @@
|
|||||||
:root {
|
* {
|
||||||
--text: #6b6375;
|
margin: 0;
|
||||||
--text-h: #08060d;
|
padding: 0;
|
||||||
--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;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html, body, #root {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
font-family: system-ui, 'Segoe UI', Roboto, sans-serif;
|
||||||
}
|
-webkit-font-smoothing: antialiased;
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user