From 4a5ae4cd6c8636d9505c7d1868d9baccd068515cc34b8fdc5b240ab1b7f60e62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9B=BE=E5=BF=97=E5=A8=81?= Date: Tue, 12 May 2026 00:02:27 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/handler/dashboard.go | 28 ++++++++------ server/repository/stock_repo.go | 55 ++++++++++++++++++-------- server/service/dashboard_svc.go | 39 +++++++++++++------ web/src/api/dashboard.ts | 24 ++++++------ web/src/pages/ConsecutivePage.tsx | 38 ++++++++++-------- web/src/pages/Dashboard.tsx | 18 ++++++--- web/src/pages/High100Page.tsx | 64 +++++++++++++++++-------------- web/src/pages/TrendPage.tsx | 33 +++++++++------- 8 files changed, 184 insertions(+), 115 deletions(-) diff --git a/server/handler/dashboard.go b/server/handler/dashboard.go index d09edee..4c924cd 100644 --- a/server/handler/dashboard.go +++ b/server/handler/dashboard.go @@ -18,10 +18,11 @@ func NewDashboardHandler(svc *service.DashboardService) *DashboardHandler { return &DashboardHandler{svc: svc} } -// GetSummary 获取最新交易日的市场概览数据 -// GET /api/dashboard/summary +// GetSummary 获取市场概览数据,支持可选 date 参数查询历史数据 +// GET /api/dashboard/summary?date=2024-01-15 func (h *DashboardHandler) GetSummary(c *gin.Context) { - summary, err := h.svc.GetSummary() + date := c.Query("date") // 空字符串表示查最新交易日 + summary, err := h.svc.GetSummary(date) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -29,14 +30,15 @@ func (h *DashboardHandler) GetSummary(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"data": summary}) } -// GetDailySummary 获取近 N 天的每日市场统计数据 -// GET /api/dashboard/daily?days=30 +// GetDailySummary 获取近 N 天的每日市场统计数据,支持可选 end_date 参数 +// GET /api/dashboard/daily?days=30&end_date=2024-01-15 func (h *DashboardHandler) GetDailySummary(c *gin.Context) { days, _ := strconv.Atoi(c.DefaultQuery("days", "30")) if days <= 0 || days > 365 { days = 30 } - data, err := h.svc.GetDailySummary(days) + endDate := c.Query("end_date") + data, err := h.svc.GetDailySummary(days, endDate) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -44,10 +46,11 @@ func (h *DashboardHandler) GetDailySummary(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"data": data}) } -// GetConsecutiveLimits 获取最新交易日的连板股票统计 -// GET /api/dashboard/consecutive +// GetConsecutiveLimits 获取连板股票统计,支持可选 date 参数查询历史数据 +// GET /api/dashboard/consecutive?date=2024-01-15 func (h *DashboardHandler) GetConsecutiveLimits(c *gin.Context) { - data, err := h.svc.GetConsecutiveLimits() + date := c.Query("date") + data, err := h.svc.GetConsecutiveLimits(date) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -55,10 +58,11 @@ func (h *DashboardHandler) GetConsecutiveLimits(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"data": data}) } -// GetHigh100 获取百日新高股票列表 -// GET /api/dashboard/high100 +// GetHigh100 获取百日新高股票列表,支持可选 date 参数查询历史数据 +// GET /api/dashboard/high100?date=2024-01-15 func (h *DashboardHandler) GetHigh100(c *gin.Context) { - data, err := h.svc.GetHigh100() + date := c.Query("date") + data, err := h.svc.GetHigh100(date) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return diff --git a/server/repository/stock_repo.go b/server/repository/stock_repo.go index 2059aba..0edd9f9 100644 --- a/server/repository/stock_repo.go +++ b/server/repository/stock_repo.go @@ -128,9 +128,17 @@ func (r *StockRepo) GetSummaryByDate(date string) (*model.DashboardSummary, erro // GetHigh100 获取最新交易日创百日新高的股票列表 func (r *StockRepo) GetHigh100() ([]model.StockRank, error) { - date, err := r.getLatestDate() - if err != nil { - return nil, err + return r.GetHigh100ByDate("") +} + +// GetHigh100ByDate 获取指定日期创百日新高的股票列表,date 为空时使用最新交易日 +func (r *StockRepo) GetHigh100ByDate(date string) ([]model.StockRank, error) { + var err error + if date == "" { + date, err = r.getLatestDate() + if err != nil { + return nil, err + } } var list []model.StockRank r.db.Raw(` @@ -154,10 +162,16 @@ func (r *StockRepo) GetHigh100() ([]model.StockRank, error) { // GetDailySummary 获取近 N 天的每日市场统计 // 包含: 涨跌数量、成交额、涨停跌停数量、连板数量 -func (r *StockRepo) GetDailySummary(days int) ([]model.DailySummary, error) { - latest, err := r.getLatestDate() - if err != nil { - return nil, err +func (r *StockRepo) GetDailySummary(days int, endDate string) ([]model.DailySummary, error) { + var latest string + var err error + if endDate != "" { + latest = endDate + } else { + latest, err = r.getLatestDate() + if err != nil { + return nil, err + } } var result []model.DailySummary @@ -175,10 +189,10 @@ func (r *StockRepo) GetDailySummary(days int) ([]model.DailySummary, error) { SUM(CASE WHEN pct_change >= 19.5 THEN 1 ELSE 0 END) as limit_up_20, SUM(CASE WHEN pct_change <= -19.5 THEN 1 ELSE 0 END) as limit_down_20 FROM stock_daily - WHERE date >= DATE_SUB(?, INTERVAL ? DAY) + WHERE date >= DATE_SUB(?, INTERVAL ? DAY) AND date <= ? GROUP BY date ORDER BY date ASC - `, latest, days-1).Scan(&result) + `, latest, days-1, latest).Scan(&result) // 计算连板数量: 获取相邻交易日对,批量查询当日和前一日都涨停的股票数量 type datePair struct { @@ -188,10 +202,10 @@ func (r *StockRepo) GetDailySummary(days int) ([]model.DailySummary, error) { var pairs []datePair r.db.Raw(` SELECT d1.date AS cur_date, MAX(d2.date) AS prev_date - FROM (SELECT DISTINCT date FROM stock_daily WHERE date >= DATE_SUB(?, INTERVAL ? DAY)) d1 - JOIN (SELECT DISTINCT date FROM stock_daily WHERE date >= DATE_SUB(?, INTERVAL ? DAY)) d2 ON d2.date < d1.date + FROM (SELECT DISTINCT date FROM stock_daily WHERE date >= DATE_SUB(?, INTERVAL ? DAY) AND date <= ?) d1 + JOIN (SELECT DISTINCT date FROM stock_daily WHERE date >= DATE_SUB(?, INTERVAL ? DAY) AND date <= ?) d2 ON d2.date < d1.date GROUP BY d1.date - `, latest, days-1, latest, days).Scan(&pairs) + `, latest, days-1, latest, latest, days, latest).Scan(&pairs) type consecRow struct { Date string `gorm:"column:date"` @@ -229,9 +243,20 @@ func (r *StockRepo) GetDailySummary(days int) ([]model.DailySummary, error) { // GetConsecutiveLimits 获取最新交易日连板股票列表 // 查询近 30 天的涨停记录,计算当日涨停股票的连续涨停天数 func (r *StockRepo) GetConsecutiveLimits() ([]model.ConsecutiveLimit, error) { - latest, err := r.getLatestDate() - if err != nil { - return nil, err + return r.GetConsecutiveLimitsByDate("") +} + +// GetConsecutiveLimitsByDate 获取指定日期的连板股票列表,date 为空时使用最新交易日 +func (r *StockRepo) GetConsecutiveLimitsByDate(date string) ([]model.ConsecutiveLimit, error) { + var latest string + var err error + if date != "" { + latest = date + } else { + latest, err = r.getLatestDate() + if err != nil { + return nil, err + } } if latest == "" { return nil, nil diff --git a/server/service/dashboard_svc.go b/server/service/dashboard_svc.go index 776931f..9702374 100644 --- a/server/service/dashboard_svc.go +++ b/server/service/dashboard_svc.go @@ -35,8 +35,13 @@ func NewDashboardService(repo *repository.StockRepo) *DashboardService { const cacheTTL = 3 * time.Minute -// GetSummary 获取最新交易日市场概览(涨跌数量、涨停跌停、百日新高等) -func (s *DashboardService) GetSummary() (*model.DashboardSummary, error) { +// GetSummary 获取市场概览,date 为空时查最新交易日(走缓存),指定日期时跳过缓存直接查询 +func (s *DashboardService) GetSummary(date string) (*model.DashboardSummary, error) { + // 指定历史日期:直接查询,不走缓存 + if date != "" { + return s.repo.GetSummaryByDate(date) + } + s.mu.RLock() if s.summary != nil && time.Now().Before(s.summary.expires) { data := s.summary.data @@ -45,11 +50,11 @@ func (s *DashboardService) GetSummary() (*model.DashboardSummary, error) { } s.mu.RUnlock() - date, err := s.repo.GetLatestTradeDate() + latestDate, err := s.repo.GetLatestTradeDate() if err != nil { return nil, err } - summary, err := s.repo.GetSummaryByDate(date) + summary, err := s.repo.GetSummaryByDate(latestDate) if err != nil { return nil, err } @@ -61,8 +66,12 @@ func (s *DashboardService) GetSummary() (*model.DashboardSummary, error) { return summary, nil } -// GetDailySummary 获取近 N 天每日市场统计 -func (s *DashboardService) GetDailySummary(days int) ([]model.DailySummary, error) { +// GetDailySummary 获取近 N 天每日市场统计,endDate 为空时查到最新交易日(走缓存) +func (s *DashboardService) GetDailySummary(days int, endDate string) ([]model.DailySummary, error) { + if endDate != "" { + return s.repo.GetDailySummary(days, endDate) + } + s.mu.RLock() if e, ok := s.daily[days]; ok && time.Now().Before(e.expires) { data := e.data @@ -71,7 +80,7 @@ func (s *DashboardService) GetDailySummary(days int) ([]model.DailySummary, erro } s.mu.RUnlock() - data, err := s.repo.GetDailySummary(days) + data, err := s.repo.GetDailySummary(days, "") if err != nil { return nil, err } @@ -83,8 +92,12 @@ func (s *DashboardService) GetDailySummary(days int) ([]model.DailySummary, erro return data, nil } -// GetConsecutiveLimits 获取最新交易日连板股票(连续多日涨停) -func (s *DashboardService) GetConsecutiveLimits() ([]model.ConsecutiveLimit, error) { +// GetConsecutiveLimits 获取连板股票,date 为空时查最新交易日(走缓存),指定日期时跳过缓存 +func (s *DashboardService) GetConsecutiveLimits(date string) ([]model.ConsecutiveLimit, error) { + if date != "" { + return s.repo.GetConsecutiveLimitsByDate(date) + } + s.mu.RLock() if s.consec != nil && time.Now().Before(s.consec.expires) { data := s.consec.data @@ -105,8 +118,12 @@ func (s *DashboardService) GetConsecutiveLimits() ([]model.ConsecutiveLimit, err return data, nil } -// GetHigh100 获取最新交易日创百日新高的股票列表 -func (s *DashboardService) GetHigh100() ([]model.StockRank, error) { +// GetHigh100 获取百日新高股票,date 为空时查最新交易日(走缓存),指定日期时跳过缓存 +func (s *DashboardService) GetHigh100(date string) ([]model.StockRank, error) { + if date != "" { + return s.repo.GetHigh100ByDate(date) + } + s.mu.RLock() if s.high100 != nil && time.Now().Before(s.high100.expires) { data := s.high100.data diff --git a/web/src/api/dashboard.ts b/web/src/api/dashboard.ts index ad2fca0..caeea1a 100644 --- a/web/src/api/dashboard.ts +++ b/web/src/api/dashboard.ts @@ -45,15 +45,15 @@ export interface DailySummary { consecutive_limit_count: number; // 连板股票数量 } -/** 获取市场概览数据 */ -export async function getDashboardSummary(): Promise { - const res = await api.get('/dashboard/summary'); +/** 获取市场概览数据,date 为空时返回最新交易日数据 */ +export async function getDashboardSummary(date?: string): Promise { + const res = await api.get('/dashboard/summary', { params: date ? { date } : undefined }); return res.data.data; } -/** 获取近 N 天每日市场统计 */ -export async function getDailySummary(days = 30): Promise { - const res = await api.get('/dashboard/daily', { params: { days } }); +/** 获取近 N 天每日市场统计,endDate 为空时截止到最新交易日 */ +export async function getDailySummary(days = 30, endDate?: string): Promise { + const res = await api.get('/dashboard/daily', { params: { days, ...(endDate ? { end_date: endDate } : {}) } }); return res.data.data; } @@ -66,15 +66,15 @@ export interface ConsecutiveLimit { pct_change: number; } -/** 获取连板股票统计 */ -export async function getConsecutiveLimits(): Promise { - const res = await api.get('/dashboard/consecutive'); +/** 获取连板股票统计,date 为空时返回最新交易日数据 */ +export async function getConsecutiveLimits(date?: string): Promise { + const res = await api.get('/dashboard/consecutive', { params: date ? { date } : undefined }); return res.data.data; } -/** 获取百日新高股票列表 */ -export async function getHigh100(): Promise { - const res = await api.get('/dashboard/high100'); +/** 获取百日新高股票列表,date 为空时返回最新交易日数据 */ +export async function getHigh100(date?: string): Promise { + const res = await api.get('/dashboard/high100', { params: date ? { date } : undefined }); return res.data.data; } diff --git a/web/src/pages/ConsecutivePage.tsx b/web/src/pages/ConsecutivePage.tsx index fe4f670..07596b2 100644 --- a/web/src/pages/ConsecutivePage.tsx +++ b/web/src/pages/ConsecutivePage.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; import { Card, Spin, Table, Tag, Typography } from 'antd'; import { getConsecutiveLimits, type ConsecutiveLimit } from '../api/dashboard'; +import DateNav from '../components/DateNav'; const { Title } = Typography; @@ -39,20 +40,14 @@ const columns = [ export default function ConsecutivePage() { const [data, setData] = useState([]); const [loading, setLoading] = useState(true); + const [date, setDate] = useState(undefined); useEffect(() => { - getConsecutiveLimits() - .then(setData) + setLoading(true); + getConsecutiveLimits(date) + .then((res) => setData(res || [])) .finally(() => setLoading(false)); - }, []); - - if (loading) { - return ( -
- -
- ); - } + }, [date]); // Group by streak const grouped = data.reduce>((acc, item) => { @@ -65,14 +60,23 @@ export default function ConsecutivePage() { return (
- - - 今日涨停 <span style={{ color: '#cf1322' }}>{data.length}</span> 只, - 最高 <span style={{ color: '#722ed1', fontSize: 28 }}>{maxStreak}</span> 连板 - + + {loading ? ( +
+ +
+ ) : ( +
+ + 涨停 <span style={{ color: '#cf1322' }}>{data.length}</span> 只, + 最高 <span style={{ color: '#722ed1', fontSize: 28 }}>{maxStreak}</span> 连板 + + +
+ )}
- {Object.keys(grouped) + {!loading && Object.keys(grouped) .sort((a, b) => Number(b) - Number(a)) .map((streak) => ( (null); const [loading, setLoading] = useState(true); + const [date, setDate] = useState(undefined); useEffect(() => { - getDashboardSummary() - .then(setSummary) + setLoading(true); + getDashboardSummary(date) + .then((res) => setSummary(res || null)) .finally(() => setLoading(false)); - }, []); + }, [date]); if (loading) { return ( @@ -54,9 +57,14 @@ export default function Dashboard() { return (
- + +
+ +
+ +
-
+
([]); const [loading, setLoading] = useState(true); + const [date, setDate] = useState(undefined); useEffect(() => { - getHigh100() - .then(setData) + setLoading(true); + getHigh100(date) + .then((res) => setData(res || [])) .finally(() => setLoading(false)); - }, []); - - if (loading) { - return ( -
- -
- ); - } + }, [date]); const limitUp = data.filter((s) => s.pct_change >= 9.5).length; return (
- - - 今日创150日新高 <span style={{ color: '#cf1322', fontSize: 28 }}>{data.length}</span> 只 - {limitUp > 0 && ( - <span style={{ color: '#722ed1', marginLeft: 16 }}> - 其中涨停 {limitUp} 只 - </span> - )} - + + {loading ? ( +
+ +
+ ) : ( +
+ + 创150日新高 <span style={{ color: '#cf1322', fontSize: 28 }}>{data.length}</span> 只 + {limitUp > 0 && ( + <span style={{ color: '#722ed1', marginLeft: 16 }}> + 其中涨停 {limitUp} 只 + </span> + )} + + +
+ )}
- -
- + {!loading && ( + +
+ + )} ); } diff --git a/web/src/pages/TrendPage.tsx b/web/src/pages/TrendPage.tsx index 18e0188..9882482 100644 --- a/web/src/pages/TrendPage.tsx +++ b/web/src/pages/TrendPage.tsx @@ -2,35 +2,40 @@ import { useEffect, useState } from 'react'; import { Card, Segmented, Spin } from 'antd'; import { getDailySummary, type DailySummary } from '../api/dashboard'; import DailyChart from '../components/DailyChart'; +import DateNav from '../components/DateNav'; export default function TrendPage() { const [daily, setDaily] = useState([]); const [loading, setLoading] = useState(true); const [days, setDays] = useState(30); + const [endDate, setEndDate] = useState(undefined); useEffect(() => { setLoading(true); - getDailySummary(days) - .then(setDaily) + getDailySummary(days, endDate) + .then((res) => setDaily(res || [])) .finally(() => setLoading(false)); - }, [days]); + }, [days, endDate]); return (
- setDays(v as number)} - options={[ - { label: '7天', value: 7 }, - { label: '30天', value: 30 }, - { label: '90天', value: 90 }, - { label: '180天', value: 180 }, - ]} - /> +
+ + setDays(v as number)} + options={[ + { label: '7天', value: 7 }, + { label: '30天', value: 30 }, + { label: '90天', value: 90 }, + { label: '180天', value: 180 }, + ]} + /> +
} > {loading ? (