162 lines
4.7 KiB
Go
162 lines
4.7 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"time"
|
|
|
|
"stock-user-system/internal/models"
|
|
"stock-user-system/internal/services"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
var coreIndexSymbols = map[string]bool{
|
|
"000001.SH": true,
|
|
"399001.SZ": true,
|
|
"399006.SZ": true,
|
|
"000688.SH": true,
|
|
}
|
|
|
|
// StockHandler 处理股票看板与同步接口。
|
|
type StockHandler struct {
|
|
DB *gorm.DB
|
|
Svc *services.StockSyncService
|
|
}
|
|
|
|
type stockRow struct {
|
|
Symbol string `json:"symbol"`
|
|
Name string `json:"name"`
|
|
Close float64 `json:"close"`
|
|
ChangePct float64 `json:"change_pct"`
|
|
Amount float64 `json:"amount,omitempty"`
|
|
}
|
|
|
|
type overviewResponse struct {
|
|
LatestTradeDate string `json:"latest_trade_date"`
|
|
Counts marketCounts `json:"counts"`
|
|
AvgChangePct float64 `json:"avg_change_pct"`
|
|
Indices []stockRow `json:"indices"`
|
|
TopGainers []stockRow `json:"top_gainers"`
|
|
TopLosers []stockRow `json:"top_losers"`
|
|
TurnoverLeaders []stockRow `json:"turnover_leaders"`
|
|
}
|
|
|
|
type marketCounts struct {
|
|
Up int64 `json:"up"`
|
|
Down int64 `json:"down"`
|
|
Flat int64 `json:"flat"`
|
|
Total int64 `json:"total"`
|
|
}
|
|
|
|
// Overview 返回最新交易日的市场概览。
|
|
func (h *StockHandler) Overview(c *gin.Context) {
|
|
ctx := context.Background()
|
|
|
|
var latestDate time.Time
|
|
if err := h.DB.WithContext(ctx).Model(&models.StockDailyQuote{}).
|
|
Select("MAX(trade_date)").Scan(&latestDate).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "查询最新交易日失败"})
|
|
return
|
|
}
|
|
if latestDate.IsZero() {
|
|
c.JSON(http.StatusOK, overviewResponse{})
|
|
return
|
|
}
|
|
|
|
var counts marketCounts
|
|
if err := h.DB.WithContext(ctx).Model(&models.StockDailyQuote{}).
|
|
Select(`
|
|
COUNT(*) AS total,
|
|
SUM(CASE WHEN change_pct > 0 THEN 1 ELSE 0 END) AS up,
|
|
SUM(CASE WHEN change_pct < 0 THEN 1 ELSE 0 END) AS down,
|
|
SUM(CASE WHEN change_pct = 0 THEN 1 ELSE 0 END) AS flat
|
|
`).
|
|
Where("trade_date = ? AND symbol NOT IN ?", latestDate, indexSymbolsList()).
|
|
Scan(&counts).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "统计涨跌失败"})
|
|
return
|
|
}
|
|
|
|
var avgChange float64
|
|
h.DB.WithContext(ctx).Model(&models.StockDailyQuote{}).
|
|
Select("COALESCE(AVG(change_pct), 0)").
|
|
Where("trade_date = ? AND symbol NOT IN ?", latestDate, indexSymbolsList()).
|
|
Scan(&avgChange)
|
|
|
|
indices := h.topRows(ctx, latestDate, "symbol IN ?", indexSymbolsList(), 10, false)
|
|
gainers := h.topRows(ctx, latestDate, "symbol NOT IN ?", indexSymbolsList(), 10, true)
|
|
losers := h.topRows(ctx, latestDate, "symbol NOT IN ?", indexSymbolsList(), 10, false)
|
|
turnover := h.topRowsByAmount(ctx, latestDate)
|
|
|
|
c.JSON(http.StatusOK, overviewResponse{
|
|
LatestTradeDate: latestDate.Format("2006-01-02"),
|
|
Counts: counts,
|
|
AvgChangePct: avgChange,
|
|
Indices: indices,
|
|
TopGainers: gainers,
|
|
TopLosers: losers,
|
|
TurnoverLeaders: turnover,
|
|
})
|
|
}
|
|
|
|
func (h *StockHandler) topRows(ctx context.Context, tradeDate time.Time, condition string, args any, limit int, desc bool) []stockRow {
|
|
var rows []stockRow
|
|
order := "change_pct ASC"
|
|
if desc {
|
|
order = "change_pct DESC"
|
|
}
|
|
h.DB.WithContext(ctx).Model(&models.StockDailyQuote{}).
|
|
Select("symbol, name, close, change_pct").
|
|
Where("trade_date = ?", tradeDate).
|
|
Where(condition, args).
|
|
Order(order).
|
|
Limit(limit).
|
|
Scan(&rows)
|
|
return rows
|
|
}
|
|
|
|
func (h *StockHandler) topRowsByAmount(ctx context.Context, tradeDate time.Time) []stockRow {
|
|
var rows []stockRow
|
|
h.DB.WithContext(ctx).Model(&models.StockDailyQuote{}).
|
|
Select("symbol, name, close, change_pct, amount").
|
|
Where("trade_date = ? AND symbol NOT IN ?", tradeDate, indexSymbolsList()).
|
|
Order("amount DESC").
|
|
Limit(10).
|
|
Scan(&rows)
|
|
return rows
|
|
}
|
|
|
|
// SyncStatus 返回最近一次同步任务状态。
|
|
func (h *StockHandler) SyncStatus(c *gin.Context) {
|
|
job, err := h.Svc.LatestJob(c.Request.Context())
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "查询同步状态失败"})
|
|
return
|
|
}
|
|
if job == nil {
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": nil})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": job})
|
|
}
|
|
|
|
// TriggerSync 手动触发同步(管理员)。
|
|
func (h *StockHandler) TriggerSync(c *gin.Context) {
|
|
count, err := h.Svc.Sync(c.Request.Context(), "manual")
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "records_count": count})
|
|
}
|
|
|
|
func indexSymbolsList() []string {
|
|
list := make([]string, 0, len(coreIndexSymbols))
|
|
for s := range coreIndexSymbols {
|
|
list = append(list, s)
|
|
}
|
|
return list
|
|
}
|