package service // 声明 service 包,保持与涨停板服务层一致。 import ( // 引入本文件需要的标准库和项目包。 "strings" // 用于清理请求中的股票代码空格。 "level_go/internal/model" // 引入接口请求和响应模型。 ) const ( // 定义二板获利缓存接口使用的常量。 secondBoardProfitStartTime = "09:31:00" // 固定的二板首次涨停时间筛选起点。 secondBoardProfitEndTime = "09:59:59" // 固定的二板首次涨停时间筛选终点。 ) type secondBoardProfitCacheRange struct { // 表示明细缓存表当前覆盖的交易日期范围。 StartDate string `gorm:"column:start_date"` // 缓存明细最早交易日,格式 YYYY-MM-DD。 EndDate string `gorm:"column:end_date"` // 缓存明细最晚交易日,格式 YYYY-MM-DD。 } type secondBoardProfitDetailCacheRow struct { // 表示从二板获利详情缓存表扫描出的单行数据。 TradeDate string `gorm:"column:trade_date"` // T 日交易日。 PreviousTradeDate string `gorm:"column:previous_trade_date"` // T-1 交易日。 PreviousTwoTradeDate string `gorm:"column:previous_two_trade_date"` // T-2 交易日。 NextTradeDate string `gorm:"column:next_trade_date"` // T+1 交易日。 Symbol string `gorm:"column:symbol"` // 股票代码。 TsCode string `gorm:"column:ts_code"` // TuShare 股票代码。 Name string `gorm:"column:name"` // 股票简称。 Market string `gorm:"column:market"` // 所属板块。 Industry string `gorm:"column:industry"` // 所属行业。 PreviousClose *float64 `gorm:"column:previous_close"` // T-1 收盘价。 LimitPrice *float64 `gorm:"column:limit_price"` // T 日涨停价。 THigh *float64 `gorm:"column:t_high"` // T 日最高价。 TClose *float64 `gorm:"column:t_close"` // T 日收盘价。 TPctChg *float64 `gorm:"column:t_pct_chg"` // T 日涨跌幅。 Status string `gorm:"column:status"` // 二板状态,limit 表示封板,break 表示炸板。 FirstLimitTime string `gorm:"column:first_limit_time"` // T 日二板首次涨停时间。 OpenTimes *int `gorm:"column:open_times"` // T 日开板次数。 NextHigh *float64 `gorm:"column:next_high"` // T+1 最高价。 NextClose *float64 `gorm:"column:next_close"` // T+1 收盘价。 NextIntradayProfitable *int `gorm:"column:next_intraday_profitable"` // T+1 盘中是否盈利,数据库中为 0/1/null。 NextHighProfitPct *float64 `gorm:"column:next_high_profit_pct"` // T+1 最大获利百分比。 } // GetSecondBoardProfitSummary 返回二板获利综合数据。 func (s *LimitBoardService) GetSecondBoardProfitSummary(req model.SecondBoardProfitRequest) (model.SecondBoardProfitSummaryResponse, error) { // 定义二板获利综合服务方法。 resp := model.SecondBoardProfitSummaryResponse{ // 初始化响应结构。 TradeStartDate: req.TradeStartDate, // 回显查询起始交易日。 TradeEndDate: req.TradeEndDate, // 回显查询结束交易日。 StartTime: secondBoardProfitStartTime, // 回显固定时间筛选起点。 EndTime: secondBoardProfitEndTime, // 回显固定时间筛选终点。 List: []model.SecondBoardProfitSummaryItem{}, // 初始化空列表,避免 JSON 返回 null。 } useSummaryCache, err := s.shouldUseSecondBoardProfitSummaryCache(req) // 判断当前日期范围是否可以直接使用汇总缓存表。 if err != nil { // 判断缓存范围查询是否失败。 return resp, err // 返回已初始化响应和数据库错误。 } if useSummaryCache { // 请求范围覆盖整张明细缓存表时,直接读取预聚合汇总缓存表。 return s.loadSecondBoardProfitSummaryCache(resp) // 从 summary cache 加载结果并返回。 } return s.loadSecondBoardProfitSummaryFromDetail(req, resp) // 窄日期范围从 detail cache 动态聚合,保持日期筛选语义。 } // GetSecondBoardProfitDetail 返回单只股票二板获利详情数据。 func (s *LimitBoardService) GetSecondBoardProfitDetail(req model.SecondBoardProfitRequest) (model.SecondBoardProfitDetailResponse, error) { // 定义个股二板详情服务方法。 resp := model.SecondBoardProfitDetailResponse{ // 初始化响应结构。 TradeStartDate: req.TradeStartDate, // 回显查询起始交易日。 TradeEndDate: req.TradeEndDate, // 回显查询结束交易日。 StartTime: secondBoardProfitStartTime, // 回显固定时间筛选起点。 EndTime: secondBoardProfitEndTime, // 回显固定时间筛选终点。 Symbol: strings.TrimSpace(req.Symbol), // 回显请求中的股票代码。 TsCode: strings.TrimSpace(req.TsCode), // 回显请求中的 TuShare 股票代码。 List: []model.SecondCalStock{}, // 初始化空列表,避免 JSON 返回 null。 } rows, err := s.loadSecondBoardProfitDetailRows(req) // 从详情缓存表读取个股明细行。 if err != nil { // 判断数据库查询是否失败。 return resp, err // 返回已初始化响应和数据库错误。 } for _, row := range rows { // 遍历缓存明细行并构建响应。 stock := buildSecondBoardProfitStockFromCache(row) // 将缓存行转换为原接口复用的股票明细结构。 if resp.SecondBoardCount == 0 { // 第一条命中记录用于补齐个股基础信息。 resp.Symbol = stock.Symbol // 设置响应股票代码。 resp.TsCode = stock.TsCode // 设置响应 TuShare 股票代码。 resp.Name = stock.Name // 设置响应股票简称。 resp.Market = stock.Market // 设置响应所属板块。 resp.Industry = stock.Industry // 设置响应所属行业。 } resp.SecondBoardCount++ // 累加命中的二板次数。 if stock.NextHighProfitPct != nil { // 只统计具备 T+1 最大获利百分比的数据。 resp.ProfitDataCount++ // 累加具备收益数据的次数。 resp.TotalNextHighProfitPct = roundSecondCalPct(resp.TotalNextHighProfitPct + *stock.NextHighProfitPct) // 累加最大获利百分比。 } resp.List = append(resp.List, stock) // 追加单次二板详情。 } if resp.ProfitDataCount > 0 { // 判断是否存在可计算平均值的数据。 resp.AverageNextHighProfitPct = roundSecondCalPct(resp.TotalNextHighProfitPct / float64(resp.ProfitDataCount)) // 计算平均最大获利百分比。 } return resp, nil // 返回详情响应。 } // shouldUseSecondBoardProfitSummaryCache 判断是否可以使用预聚合汇总缓存表。 func (s *LimitBoardService) shouldUseSecondBoardProfitSummaryCache(req model.SecondBoardProfitRequest) (bool, error) { // 定义汇总缓存使用判断函数。 cacheRange, err := s.loadSecondBoardProfitCacheRange() // 查询明细缓存表当前覆盖日期范围。 if err != nil { // 判断缓存范围查询是否失败。 return false, err // 查询失败时返回错误。 } if cacheRange.StartDate == "" || cacheRange.EndDate == "" { // 判断缓存表是否为空。 return false, nil // 缓存为空时不能使用汇总缓存。 } return req.TradeStartDate <= cacheRange.StartDate && req.TradeEndDate >= cacheRange.EndDate, nil // 请求覆盖缓存全范围时使用汇总缓存。 } // loadSecondBoardProfitCacheRange 查询二板获利详情缓存表的日期覆盖范围。 func (s *LimitBoardService) loadSecondBoardProfitCacheRange() (secondBoardProfitCacheRange, error) { // 定义缓存日期范围查询函数。 // 定义查询缓存范围 SQL。 query := ` SELECT COALESCE(DATE_FORMAT(MIN(trade_date), '%Y-%m-%d'), '') AS start_date, COALESCE(DATE_FORMAT(MAX(trade_date), '%Y-%m-%d'), '') AS end_date FROM second_board_profit_detail_cache` // 从明细缓存表读取最小和最大交易日。 var cacheRange secondBoardProfitCacheRange // 声明缓存范围结果变量。 err := s.DB.Raw(query).Scan(&cacheRange).Error // 执行 SQL 并扫描结果。 return cacheRange, err // 返回缓存范围和可能的错误。 } // loadSecondBoardProfitSummaryCache 从预聚合汇总缓存表读取完整汇总数据。 func (s *LimitBoardService) loadSecondBoardProfitSummaryCache(resp model.SecondBoardProfitSummaryResponse) (model.SecondBoardProfitSummaryResponse, error) { // 定义汇总缓存读取函数。 // 定义汇总缓存查询 SQL。 query := ` SELECT symbol, ts_code, name, market, industry, second_board_count, limit_count, break_count, profit_data_count, total_next_high_profit_pct, average_next_high_profit_pct, COALESCE(DATE_FORMAT(first_trade_date, '%Y-%m-%d'), '') AS first_trade_date, COALESCE(DATE_FORMAT(last_trade_date, '%Y-%m-%d'), '') AS last_trade_date FROM second_board_profit_summary_cache ORDER BY total_next_high_profit_pct DESC, profit_data_count DESC, ts_code ASC` // 按累计获利、数据次数和股票代码稳定排序。 if err := s.DB.Raw(query).Scan(&resp.List).Error; err != nil { // 执行汇总缓存查询。 return resp, err // 查询失败时返回错误。 } resp.Total = len(resp.List) // 设置返回股票数量。 return resp, nil // 返回汇总响应。 } // loadSecondBoardProfitSummaryFromDetail 从明细缓存表按日期范围动态聚合汇总数据。 func (s *LimitBoardService) loadSecondBoardProfitSummaryFromDetail(req model.SecondBoardProfitRequest, resp model.SecondBoardProfitSummaryResponse) (model.SecondBoardProfitSummaryResponse, error) { // 定义明细聚合函数。 // 定义日期范围聚合 SQL。 query := ` SELECT MAX(symbol) AS symbol, ts_code, MAX(name) AS name, MAX(market) AS market, MAX(industry) AS industry, COUNT(*) AS second_board_count, SUM(CASE WHEN status = 'limit' THEN 1 ELSE 0 END) AS limit_count, SUM(CASE WHEN status = 'break' THEN 1 ELSE 0 END) AS break_count, COUNT(next_high_profit_pct) AS profit_data_count, ROUND(SUM(COALESCE(next_high_profit_pct, 0)), 2) AS total_next_high_profit_pct, COALESCE(ROUND(SUM(COALESCE(next_high_profit_pct, 0)) / NULLIF(COUNT(next_high_profit_pct), 0), 2), 0) AS average_next_high_profit_pct, COALESCE(DATE_FORMAT(MIN(trade_date), '%Y-%m-%d'), '') AS first_trade_date, COALESCE(DATE_FORMAT(MAX(trade_date), '%Y-%m-%d'), '') AS last_trade_date FROM second_board_profit_detail_cache WHERE trade_date BETWEEN ? AND ? GROUP BY ts_code ORDER BY total_next_high_profit_pct DESC, profit_data_count DESC, ts_code ASC` // 在明细缓存表内按请求日期范围聚合。 if err := s.DB.Raw(query, req.TradeStartDate, req.TradeEndDate).Scan(&resp.List).Error; err != nil { // 执行聚合查询。 return resp, err // 查询失败时返回错误。 } resp.Total = len(resp.List) // 设置返回股票数量。 return resp, nil // 返回汇总响应。 } // loadSecondBoardProfitDetailRows 从明细缓存表读取单只股票详情。 func (s *LimitBoardService) loadSecondBoardProfitDetailRows(req model.SecondBoardProfitRequest) ([]secondBoardProfitDetailCacheRow, error) { // 定义明细缓存查询函数。 tsCode := strings.TrimSpace(req.TsCode) // 清理 TuShare 股票代码参数。 symbol := strings.TrimSpace(req.Symbol) // 清理市场股票代码参数。 // 定义明细缓存查询 SQL。 query := ` SELECT DATE_FORMAT(trade_date, '%Y-%m-%d') AS trade_date, DATE_FORMAT(previous_trade_date, '%Y-%m-%d') AS previous_trade_date, DATE_FORMAT(previous_two_trade_date, '%Y-%m-%d') AS previous_two_trade_date, COALESCE(DATE_FORMAT(next_trade_date, '%Y-%m-%d'), '') AS next_trade_date, symbol, ts_code, name, market, industry, previous_close, limit_price, t_high, t_close, t_pct_chg, status, first_limit_time, open_times, next_high, next_close, next_intraday_profitable, next_high_profit_pct FROM second_board_profit_detail_cache WHERE trade_date BETWEEN ? AND ?` // 先按日期范围过滤明细缓存。 args := []interface{}{req.TradeStartDate, req.TradeEndDate} // 初始化 SQL 参数为日期范围。 if tsCode != "" { // 优先按 TuShare 股票代码查询。 query += " AND ts_code = ?" // 追加 TuShare 股票代码条件。 args = append(args, tsCode) // 追加 TuShare 股票代码参数。 } else { // 未传 TuShare 股票代码时使用市场股票代码。 query += " AND symbol = ?" // 追加市场股票代码条件。 args = append(args, symbol) // 追加市场股票代码参数。 } query += " ORDER BY trade_date ASC, ts_code ASC" // 追加稳定排序条件。 var rows []secondBoardProfitDetailCacheRow // 声明明细缓存扫描结果。 err := s.DB.Raw(query, args...).Scan(&rows).Error // 执行 SQL 并扫描结果。 return rows, err // 返回明细行和可能的错误。 } // buildSecondBoardProfitStockFromCache 将缓存行转换为接口复用的二板股票明细模型。 func buildSecondBoardProfitStockFromCache(row secondBoardProfitDetailCacheRow) model.SecondCalStock { // 定义缓存行到响应模型的转换函数。 item := model.SecondCalStock{ // 初始化二板股票明细。 TradeDate: row.TradeDate, // 设置 T 日交易日。 PreviousTradeDate: row.PreviousTradeDate, // 设置 T-1 交易日。 PreviousTwoTradeDate: row.PreviousTwoTradeDate, // 设置 T-2 交易日。 NextTradeDate: row.NextTradeDate, // 设置 T+1 交易日。 Symbol: row.Symbol, // 设置股票代码。 TsCode: row.TsCode, // 设置 TuShare 股票代码。 Name: row.Name, // 设置股票简称。 Market: row.Market, // 设置所属板块。 Industry: row.Industry, // 设置所属行业。 PreviousClose: row.PreviousClose, // 设置 T-1 收盘价。 LimitPrice: row.LimitPrice, // 设置 T 日涨停价。 THigh: row.THigh, // 设置 T 日最高价。 TClose: row.TClose, // 设置 T 日收盘价。 TPctChg: row.TPctChg, // 设置 T 日涨跌幅。 Status: row.Status, // 设置二板状态。 FirstLimitTime: row.FirstLimitTime, // 设置首次涨停时间。 OpenTimes: row.OpenTimes, // 设置开板次数。 NextHigh: row.NextHigh, // 设置 T+1 最高价。 NextClose: row.NextClose, // 设置 T+1 收盘价。 NextHighProfitPct: row.NextHighProfitPct, // 设置 T+1 最大获利百分比。 } if row.NextIntradayProfitable != nil { // 判断缓存中是否存在盈利布尔数据。 nextIntradayProfitable := *row.NextIntradayProfitable != 0 // 将 0/1 转换为 bool。 item.NextIntradayProfitable = &nextIntradayProfitable // 设置 T+1 盘中是否盈利。 } return item // 返回转换后的股票明细。 }