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 }