彻底解决 sql 兼容性问题

This commit is contained in:
2026-05-13 21:26:22 +08:00
parent 61cc29d7b3
commit 50b10fa880
13 changed files with 109 additions and 559 deletions
+12 -2
View File
@@ -112,8 +112,8 @@ func parseSecondCalRequest(c *gin.Context) (model.SecondCalRequest, error) {
// includePagination 为 false 时跳过分页参数,供 CSV 导出复用同一套日期和过滤条件解析逻辑。
func parseLimitBreakNextPerformanceRequest(c *gin.Context, includePagination bool) (model.LimitBreakNextPerformanceRequest, error) {
req := model.LimitBreakNextPerformanceRequest{
TradeStartDate: strings.TrimSpace(c.DefaultQuery("tradeStartDate", c.Query("start_date"))),
TradeEndDate: strings.TrimSpace(c.DefaultQuery("tradeEndDate", c.Query("end_date"))),
TradeStartDate: firstNonEmptyQuery(c, "tradeStartDate", "trade_start_date", "start_date"),
TradeEndDate: firstNonEmptyQuery(c, "tradeEndDate", "trade_end_date", "end_date"),
Page: 1,
PageSize: 20,
}
@@ -159,6 +159,16 @@ func parseLimitBreakNextPerformanceRequest(c *gin.Context, includePagination boo
return req, nil
}
// firstNonEmptyQuery 按优先级读取第一个非空查询参数。
func firstNonEmptyQuery(c *gin.Context, keys ...string) string {
for _, key := range keys {
if value := strings.TrimSpace(c.Query(key)); value != "" {
return value
}
}
return ""
}
// parseOptionalBoolQuery 从多个兼容参数名中读取可选布尔值。
//
// 返回 nil 表示用户没有传入该过滤项;返回 *bool 表示需要按 true/false 过滤。
+22 -18
View File
@@ -11,7 +11,6 @@ import (
"time"
"github.com/gin-gonic/gin"
"level_go/internal/dbcompat"
)
const (
@@ -337,16 +336,21 @@ func validateSecondProfitDateRange(startDate, endDate string) error {
// SQL 先找出“前一日涨停、前两日未涨停、当日继续涨停”的股票,再补齐 T+1 行情;
// Go 代码负责应用动态过滤条件并计算开盘、最高、最低、收盘相对 T 日收盘的收益率。
func (h *LimitBoardHandler) loadSecondProfitRows(req secondProfitQuery) ([]secondProfitCSVRow, error) {
seeds, err := dbcompat.LoadSSEOpenCalendarSeeds(h.Service.DB, req.TradeStartDate, req.TradeEndDate)
if err != nil {
return nil, err
}
if len(seeds) == 0 {
return []secondProfitCSVRow{}, nil
}
calendarTable, calendarArgs := dbcompat.CalendarSeedTable(seeds)
query := `
WITH calendar AS (
SELECT
cal_date AS trade_date,
LAG(cal_date) OVER (ORDER BY cal_date) AS previous_trade_date,
LAG(cal_date, 2) OVER (ORDER BY cal_date) AS previous_two_trade_date,
LEAD(cal_date) OVER (ORDER BY cal_date) AS next_trade_date
FROM trade_calendar
WHERE exchange = 'SSE' AND is_open = 1
),
calendar_window AS (
SELECT *
FROM calendar
WHERE trade_date BETWEEN ? AND ?
)
SELECT
DATE_FORMAT(c.trade_date, '%Y-%m-%d') AS trade_date,
DATE_FORMAT(c.previous_trade_date, '%Y-%m-%d') AS previous_trade_date,
@@ -374,28 +378,28 @@ SELECT
nd.high AS next_high,
nd.low AS next_low,
nd.close AS next_close
FROM (` + calendarTable + `) c
FROM calendar_window c
INNER JOIN stock_limit_combined_data l
ON l.trade_date = c.trade_date AND l.final_status = 1
INNER JOIN stock_limit_combined_data pl
ON ` + dbcompat.TsCodeEqual("pl.ts_code", "l.ts_code") + ` AND pl.trade_date = c.previous_trade_date AND pl.final_status = 1
ON pl.ts_code = l.ts_code AND pl.trade_date = c.previous_trade_date AND pl.final_status = 1
LEFT JOIN stock_limit_combined_data ppl
ON ` + dbcompat.TsCodeEqual("ppl.ts_code", "l.ts_code") + ` AND ppl.trade_date = c.previous_two_trade_date AND ppl.final_status = 1
ON ppl.ts_code = l.ts_code AND ppl.trade_date = c.previous_two_trade_date AND ppl.final_status = 1
LEFT JOIN stock_daily d
ON ` + dbcompat.TsCodeEqual("l.ts_code", "d.ts_code") + ` AND d.trade_date = l.trade_date
ON l.ts_code = d.ts_code AND d.trade_date = l.trade_date
LEFT JOIN stock_daily pd
ON ` + dbcompat.TsCodeEqual("l.ts_code", "pd.ts_code") + ` AND pd.trade_date = c.previous_trade_date
ON l.ts_code = pd.ts_code AND pd.trade_date = c.previous_trade_date
LEFT JOIN stock_daily ppd
ON ` + dbcompat.TsCodeEqual("l.ts_code", "ppd.ts_code") + ` AND ppd.trade_date = c.previous_two_trade_date
ON l.ts_code = ppd.ts_code AND ppd.trade_date = c.previous_two_trade_date
LEFT JOIN stock_daily nd
ON ` + dbcompat.TsCodeEqual("l.ts_code", "nd.ts_code") + ` AND nd.trade_date = c.next_trade_date
ON l.ts_code = nd.ts_code AND nd.trade_date = c.next_trade_date
WHERE c.previous_trade_date IS NOT NULL
AND c.previous_two_trade_date IS NOT NULL
AND ppl.ts_code IS NULL
ORDER BY c.trade_date ASC, l.ts_code ASC`
var rawRows []secondProfitCSVRow
if err := h.Service.DB.Raw(query, calendarArgs...).Scan(&rawRows).Error; err != nil {
if err := h.Service.DB.Raw(query, req.TradeStartDate, req.TradeEndDate).Scan(&rawRows).Error; err != nil {
return nil, err
}
-147
View File
@@ -1,147 +0,0 @@
package dbcompat
import (
"strings"
"gorm.io/gorm"
)
// CalendarSeed is a MySQL-version-neutral replacement for LAG/LEAD over the
// trade calendar. Keeping the adjacent dates in Go avoids depending on MySQL 8.
type CalendarSeed struct {
TradeDate string
PreviousTradeDate string
PreviousTwoTradeDate string
NextTradeDate string
}
type calendarDateRow struct {
TradeDate string `gorm:"column:trade_date"`
}
type columnCollationRow struct {
CollationName string `gorm:"column:collation_name"`
}
// LoadSSEOpenCalendarSeeds loads all SSE open dates and builds the date window
// needed by range queries. The returned seeds are limited to [startDate, endDate].
func LoadSSEOpenCalendarSeeds(db *gorm.DB, startDate, endDate string) ([]CalendarSeed, error) {
var rows []calendarDateRow
if err := db.Table("trade_calendar").
Select("DATE_FORMAT(cal_date, '%Y-%m-%d') AS trade_date").
Where("exchange = ? AND is_open = ?", "SSE", 1).
Order("cal_date ASC").
Scan(&rows).Error; err != nil {
return nil, err
}
seeds := make([]CalendarSeed, 0)
for i, row := range rows {
if row.TradeDate < startDate || row.TradeDate > endDate {
continue
}
seed := CalendarSeed{TradeDate: row.TradeDate}
if i > 0 {
seed.PreviousTradeDate = rows[i-1].TradeDate
}
if i > 1 {
seed.PreviousTwoTradeDate = rows[i-2].TradeDate
}
if i+1 < len(rows) {
seed.NextTradeDate = rows[i+1].TradeDate
}
seeds = append(seeds, seed)
}
return seeds, nil
}
// CalendarSeedTable converts calendar seeds into a derived table that works on
// MySQL 5.7, MySQL 8, and MariaDB without CTE/window-function support.
func CalendarSeedTable(seeds []CalendarSeed) (string, []any) {
if len(seeds) == 0 {
return `SELECT
CAST(NULL AS DATE) AS trade_date,
CAST(NULL AS DATE) AS previous_trade_date,
CAST(NULL AS DATE) AS previous_two_trade_date,
CAST(NULL AS DATE) AS next_trade_date
WHERE 1 = 0`, nil
}
parts := make([]string, 0, len(seeds))
args := make([]any, 0, len(seeds)*4)
for _, seed := range seeds {
parts = append(parts, `SELECT
CAST(? AS DATE) AS trade_date,
CAST(? AS DATE) AS previous_trade_date,
CAST(? AS DATE) AS previous_two_trade_date,
CAST(? AS DATE) AS next_trade_date`)
args = append(args,
nullableDate(seed.TradeDate),
nullableDate(seed.PreviousTradeDate),
nullableDate(seed.PreviousTwoTradeDate),
nullableDate(seed.NextTradeDate),
)
}
return strings.Join(parts, "\nUNION ALL\n"), args
}
// TsCodeEqual keeps joins free of hard-coded COLLATE clauses. This avoids
// binding SQL to MySQL-8-only collations while preserving index-friendly joins.
func TsCodeEqual(left, right string) string {
return left + " = " + right
}
// ColumnCollation returns the current database collation for one column.
func ColumnCollation(db *gorm.DB, tableName, columnName string) (string, error) {
var row columnCollationRow
err := db.Raw(`
SELECT COLLATION_NAME AS collation_name
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = ?
AND COLUMN_NAME = ?
LIMIT 1`, tableName, columnName).Scan(&row).Error
if err != nil {
return "", err
}
return row.CollationName, nil
}
// TsCodeEqualUnicode compares a utf8mb4_unicode_ci column with a column that
// may be utf8mb4_0900_ai_ci. utf8mb4_unicode_ci exists on both MySQL 5.7 and 8.
func TsCodeEqualUnicode(left, right string) string {
return left + " = " + right + " COLLATE utf8mb4_unicode_ci"
}
// TsCodeEqualRightCollation compares two stock-code columns using a collation
// known to exist on the connected database. This avoids MySQL-version-specific
// literals while preserving the fast side of joins such as stock_daily.ts_code.
func TsCodeEqualRightCollation(left, right, collation string) string {
if !safeCollationName(collation) {
return TsCodeEqual(left, right)
}
return left + " = " + right + " COLLATE " + collation
}
func safeCollationName(collation string) bool {
if collation == "" {
return false
}
for _, r := range collation {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {
continue
}
return false
}
return true
}
func nullableDate(date string) any {
if strings.TrimSpace(date) == "" {
return nil
}
return date
}
@@ -5,7 +5,6 @@ import (
"strconv"
"strings"
"level_go/internal/dbcompat"
"level_go/internal/model"
)
@@ -105,16 +104,21 @@ func (s *LimitBoardService) ListLimitBreakNextPerformanceStocks(req model.LimitB
//
// 口径:前一交易日涨停、前两交易日未涨停、当前交易日 final_status = 2。
func (s *LimitBoardService) loadLimitBreakNextPerformanceRows(startDate, endDate string) ([]limitBreakNextPerformanceRow, error) {
seeds, err := dbcompat.LoadSSEOpenCalendarSeeds(s.DB, startDate, endDate)
if err != nil {
return nil, err
}
if len(seeds) == 0 {
return []limitBreakNextPerformanceRow{}, nil
}
calendarTable, calendarArgs := dbcompat.CalendarSeedTable(seeds)
query := `
WITH calendar AS (
SELECT
cal_date AS trade_date,
LAG(cal_date) OVER (ORDER BY cal_date) AS previous_trade_date,
LAG(cal_date, 2) OVER (ORDER BY cal_date) AS previous_two_trade_date,
LEAD(cal_date) OVER (ORDER BY cal_date) AS next_trade_date
FROM trade_calendar
WHERE exchange = 'SSE' AND is_open = 1
),
calendar_window AS (
SELECT *
FROM calendar
WHERE trade_date BETWEEN ? AND ?
)
SELECT *
FROM (
SELECT
@@ -143,21 +147,21 @@ FROM (
nd.high AS next_high,
nd.low AS next_low,
nd.close AS next_close
FROM (` + calendarTable + `) c
FROM calendar_window c
INNER JOIN stock_limit_combined_data l
ON l.trade_date = c.trade_date AND l.final_status = 2
INNER JOIN stock_limit_combined_data pl
ON ` + dbcompat.TsCodeEqual("pl.ts_code", "l.ts_code") + ` AND pl.trade_date = c.previous_trade_date AND pl.final_status = 1
ON pl.ts_code = l.ts_code AND pl.trade_date = c.previous_trade_date AND pl.final_status = 1
LEFT JOIN stock_limit_combined_data ppl
ON ` + dbcompat.TsCodeEqual("ppl.ts_code", "l.ts_code") + ` AND ppl.trade_date = c.previous_two_trade_date AND ppl.final_status = 1
ON ppl.ts_code = l.ts_code AND ppl.trade_date = c.previous_two_trade_date AND ppl.final_status = 1
LEFT JOIN stock_daily d
ON ` + dbcompat.TsCodeEqual("d.ts_code", "l.ts_code") + ` AND d.trade_date = c.trade_date
ON d.ts_code = l.ts_code AND d.trade_date = c.trade_date
LEFT JOIN stock_daily pd
ON ` + dbcompat.TsCodeEqual("pd.ts_code", "l.ts_code") + ` AND pd.trade_date = c.previous_trade_date
ON pd.ts_code = l.ts_code AND pd.trade_date = c.previous_trade_date
LEFT JOIN stock_daily nd
ON ` + dbcompat.TsCodeEqual("nd.ts_code", "l.ts_code") + ` AND nd.trade_date = c.next_trade_date
ON nd.ts_code = l.ts_code AND nd.trade_date = c.next_trade_date
LEFT JOIN stock_basic sb
ON ` + dbcompat.TsCodeEqualUnicode("sb.ts_code", "l.ts_code") + `
ON sb.ts_code = l.ts_code
WHERE c.previous_trade_date IS NOT NULL
AND c.previous_two_trade_date IS NOT NULL
AND ppl.ts_code IS NULL
@@ -167,7 +171,7 @@ WHERE previous_close IS NOT NULL
ORDER BY trade_date ASC, ts_code ASC`
var rows []limitBreakNextPerformanceRow
err = s.DB.Raw(query, calendarArgs...).Scan(&rows).Error
err := s.DB.Raw(query, startDate, endDate).Scan(&rows).Error
return rows, err
}
@@ -6,7 +6,6 @@ import (
"strconv"
"strings"
"level_go/internal/dbcompat"
"level_go/internal/model"
)
@@ -147,7 +146,7 @@ func (s *LimitBoardService) loadLimitOpenCompareStocks(tradeDate, previousTradeD
(? <> '' AND NOT EXISTS (
SELECT 1
FROM stock_limit_combined_data ppl
WHERE ` + dbcompat.TsCodeEqual("ppl.ts_code", "l.ts_code") + ` AND ppl.trade_date = ? AND ppl.final_status = 1
WHERE ppl.ts_code = l.ts_code AND ppl.trade_date = ? AND ppl.final_status = 1
)) AS is_first_to_second_board,
COALESCE(l.calc_limit_price, 0) AS limit_price,
COALESCE(l.tushare_pct_chg, d.pct_chg, 0) AS pct_chg,
@@ -165,11 +164,11 @@ func (s *LimitBoardService) loadLimitOpenCompareStocks(tradeDate, previousTradeD
l.calc_vol AS vol
FROM stock_limit_combined_data l
LEFT JOIN stock_daily d
ON ` + dbcompat.TsCodeEqual("l.ts_code", "d.ts_code") + ` AND d.trade_date = l.trade_date
ON l.ts_code = d.ts_code AND d.trade_date = l.trade_date
LEFT JOIN stock_daily pd
ON ` + dbcompat.TsCodeEqual("l.ts_code", "pd.ts_code") + ` AND pd.trade_date = ?
ON l.ts_code = pd.ts_code AND pd.trade_date = ?
INNER JOIN stock_limit_combined_data pl
ON ` + dbcompat.TsCodeEqual("pl.ts_code", "l.ts_code") + ` AND pl.trade_date = ? AND pl.final_status = 1
ON pl.ts_code = l.ts_code AND pl.trade_date = ? AND pl.final_status = 1
WHERE l.trade_date = ? AND l.final_status = 1
ORDER BY d.pct_chg DESC, l.ts_code ASC`
@@ -3,7 +3,6 @@ package service
import (
"sort"
"level_go/internal/dbcompat"
"level_go/internal/model"
)
@@ -94,11 +93,11 @@ func (s *LimitBoardService) loadLimitOpenThresholdStocks(tradeDate, previousTrad
l.calc_vol AS vol
FROM stock_limit_combined_data l
LEFT JOIN stock_daily d
ON ` + dbcompat.TsCodeEqual("l.ts_code", "d.ts_code") + ` AND d.trade_date = l.trade_date
ON l.ts_code = d.ts_code AND d.trade_date = l.trade_date
LEFT JOIN stock_daily pd
ON ` + dbcompat.TsCodeEqual("l.ts_code", "pd.ts_code") + ` AND pd.trade_date = ?
ON l.ts_code = pd.ts_code AND pd.trade_date = ?
INNER JOIN stock_limit_combined_data pl
ON ` + dbcompat.TsCodeEqual("pl.ts_code", "l.ts_code") + ` AND pl.trade_date = ? AND pl.final_status = 1
ON pl.ts_code = l.ts_code AND pl.trade_date = ? AND pl.final_status = 1
WHERE l.trade_date = ? AND l.final_status = 1
ORDER BY d.pct_chg DESC, l.ts_code ASC`
@@ -3,7 +3,6 @@ package service
import (
"sort"
"level_go/internal/dbcompat"
"level_go/internal/model"
)
@@ -147,7 +146,7 @@ func (s *LimitBoardService) loadCurrentLimitStocks(tradeDate string) ([]model.Li
l.calc_vol AS vol
FROM stock_limit_combined_data l
LEFT JOIN stock_daily d
ON ` + dbcompat.TsCodeEqual("l.ts_code", "d.ts_code") + ` AND d.trade_date = l.trade_date
ON l.ts_code = d.ts_code AND d.trade_date = l.trade_date
WHERE l.trade_date = ? AND l.final_status = 1
ORDER BY d.pct_chg DESC, l.ts_code ASC`
+43 -33
View File
@@ -3,7 +3,6 @@ package service
import (
"math"
"level_go/internal/dbcompat"
"level_go/internal/model"
)
@@ -88,19 +87,29 @@ func (s *LimitBoardService) GetSecondCal(req model.SecondCalRequest) (model.Seco
// loadSecondCalDays 查询日期范围内的开市交易日和相邻交易日。
func (s *LimitBoardService) loadSecondCalDays(startDate, endDate string) ([]secondCalDaySeed, map[string]int, error) {
seeds, err := dbcompat.LoadSSEOpenCalendarSeeds(s.DB, startDate, endDate)
if err != nil {
query := `
WITH calendar AS (
SELECT
cal_date AS trade_date,
LAG(cal_date) OVER (ORDER BY cal_date) AS previous_trade_date,
LAG(cal_date, 2) OVER (ORDER BY cal_date) AS previous_two_trade_date,
LEAD(cal_date) OVER (ORDER BY cal_date) AS next_trade_date
FROM trade_calendar
WHERE exchange = 'SSE' AND is_open = 1
)
SELECT
DATE_FORMAT(trade_date, '%Y-%m-%d') AS trade_date,
COALESCE(DATE_FORMAT(previous_trade_date, '%Y-%m-%d'), '') AS previous_trade_date,
COALESCE(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
FROM calendar
WHERE trade_date BETWEEN ? AND ?
ORDER BY trade_date ASC`
var days []secondCalDaySeed
if err := s.DB.Raw(query, startDate, endDate).Scan(&days).Error; err != nil {
return nil, nil, err
}
days := make([]secondCalDaySeed, 0, len(seeds))
for _, seed := range seeds {
days = append(days, secondCalDaySeed{
TradeDate: seed.TradeDate,
PreviousTradeDate: seed.PreviousTradeDate,
PreviousTwoTradeDate: seed.PreviousTwoTradeDate,
NextTradeDate: seed.NextTradeDate,
})
}
// 构建交易日到响应数组下标的映射,后续聚合明细时可以 O(1) 定位。
dayIndex := make(map[string]int, len(days))
@@ -112,20 +121,21 @@ func (s *LimitBoardService) loadSecondCalDays(startDate, endDate string) ([]seco
// loadSecondCalRows 查询 T-1 涨停、T-2 未涨停、T 日再次触及涨停价的二板股票。
func (s *LimitBoardService) loadSecondCalRows(startDate, endDate string) ([]secondCalRow, error) {
seeds, err := dbcompat.LoadSSEOpenCalendarSeeds(s.DB, startDate, endDate)
if err != nil {
return nil, err
}
if len(seeds) == 0 {
return []secondCalRow{}, nil
}
calendarTable, calendarArgs := dbcompat.CalendarSeedTable(seeds)
stockDailyTsCodeCollation, err := dbcompat.ColumnCollation(s.DB, "stock_daily", "ts_code")
if err != nil {
return nil, err
}
query := `
WITH calendar AS (
SELECT
cal_date AS trade_date,
LAG(cal_date) OVER (ORDER BY cal_date) AS previous_trade_date,
LAG(cal_date, 2) OVER (ORDER BY cal_date) AS previous_two_trade_date,
LEAD(cal_date) OVER (ORDER BY cal_date) AS next_trade_date
FROM trade_calendar
WHERE exchange = 'SSE' AND is_open = 1
),
calendar_window AS (
SELECT *
FROM calendar
WHERE trade_date BETWEEN ? AND ?
)
SELECT
DATE_FORMAT(trade_date, '%Y-%m-%d') AS trade_date,
DATE_FORMAT(previous_trade_date, '%Y-%m-%d') AS previous_trade_date,
@@ -190,21 +200,21 @@ FROM (
END AS open_times,
nd.high AS next_high,
nd.close AS next_close
FROM (` + calendarTable + `) c
FROM calendar_window c
INNER JOIN limit_list_d pl
ON pl.trade_date = c.previous_trade_date AND pl.` + "`limit`" + ` = 'U'
LEFT JOIN limit_list_d ppl
ON ` + dbcompat.TsCodeEqual("ppl.ts_code", "pl.ts_code") + ` AND ppl.trade_date = c.previous_two_trade_date AND ppl.` + "`limit`" + ` = 'U'
ON ppl.ts_code = pl.ts_code AND ppl.trade_date = c.previous_two_trade_date AND ppl.` + "`limit`" + ` = 'U'
INNER JOIN stock_daily d
ON ` + dbcompat.TsCodeEqualRightCollation("d.ts_code", "pl.ts_code", stockDailyTsCodeCollation) + ` AND d.trade_date = c.trade_date
ON d.ts_code = pl.ts_code AND d.trade_date = c.trade_date
LEFT JOIN stock_daily nd
ON ` + dbcompat.TsCodeEqualRightCollation("nd.ts_code", "pl.ts_code", stockDailyTsCodeCollation) + ` AND nd.trade_date = c.next_trade_date
ON nd.ts_code = pl.ts_code AND nd.trade_date = c.next_trade_date
LEFT JOIN limit_list_d tlu
ON ` + dbcompat.TsCodeEqual("tlu.ts_code", "pl.ts_code") + ` AND tlu.trade_date = c.trade_date AND tlu.` + "`limit`" + ` = 'U'
ON tlu.ts_code = pl.ts_code AND tlu.trade_date = c.trade_date AND tlu.` + "`limit`" + ` = 'U'
LEFT JOIN limit_list_d tlz
ON ` + dbcompat.TsCodeEqual("tlz.ts_code", "pl.ts_code") + ` AND tlz.trade_date = c.trade_date AND tlz.` + "`limit`" + ` = 'Z'
ON tlz.ts_code = pl.ts_code AND tlz.trade_date = c.trade_date AND tlz.` + "`limit`" + ` = 'Z'
LEFT JOIN stock_basic sb
ON ` + dbcompat.TsCodeEqual("sb.ts_code", "pl.ts_code") + `
ON sb.ts_code = pl.ts_code
WHERE c.previous_trade_date IS NOT NULL
AND c.previous_two_trade_date IS NOT NULL
AND ppl.ts_code IS NULL
@@ -219,7 +229,7 @@ WHERE previous_close IS NOT NULL
ORDER BY trade_date ASC, ts_code ASC`
var rows []secondCalRow
err = s.DB.Raw(query, calendarArgs...).Scan(&rows).Error
err := s.DB.Raw(query, startDate, endDate).Scan(&rows).Error
return rows, err
}
+1 -2
View File
@@ -5,7 +5,6 @@ import (
"sort"
"gorm.io/gorm"
"level_go/internal/dbcompat"
"level_go/internal/model"
)
@@ -46,7 +45,7 @@ func (s *StockService) AnalyzePerformance(startDate, endDate string) ([]model.An
// 查询 startDate 当日涨停股,并优先使用涨停合并表中的计算最高价。
query := `SELECT l.ts_code, l.name, d.open, COALESCE(l.calc_high, d.high, 0) AS high FROM stock_limit_combined_data l
LEFT JOIN stock_daily d ON ` + dbcompat.TsCodeEqual("l.ts_code", "d.ts_code") + ` AND l.trade_date = d.trade_date
LEFT JOIN stock_daily d ON l.ts_code = d.ts_code AND l.trade_date = d.trade_date
WHERE l.trade_date = ? AND l.final_status = 1`
if err := s.DB.Raw(query, startDate).Scan(&limits).Error; err != nil {