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
+10
View File
@@ -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
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)
}
}
+13 -11
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
SELECT t.code, si.name, t.pct_change, t.close
FROM (
SELECT sd.code, sd.pct_change, sd.close
FROM stock_daily sd
LEFT JOIN stock_info si ON sd.code = si.code
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
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
) 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
+68 -4
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
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>
+3 -3
View File
@@ -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
View File
@@ -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;
}