AI分析功能:LLM Key 改为数据库管理,支持管理员后台配置

This commit is contained in:
fish
2026-05-10 16:21:15 +08:00
parent ad9edf7ad4
commit 99c2a5bcbf
12 changed files with 814 additions and 23 deletions

View File

@@ -0,0 +1,134 @@
package store
import (
"database/sql"
"encoding/json"
"fmt"
)
// AnalysisContext 汇总一次 AI 分析所需的全部数据。
type AnalysisContext struct {
Score Score `json:"score"`
Candles []Candle `json:"candles"`
RecentScores []Score `json:"recent_scores"`
}
// GetAnalysisContext 拉取指定合约某日的 score + 近 60 日 K 线 + 近 10 日 scores。
func (s *FuturesStore) GetAnalysisContext(tsCode, tradeDate string) (*AnalysisContext, error) {
ctx := &AnalysisContext{}
// 1) 目标日 score含 detail_json
row := s.db.QueryRow(`SELECT id, ts_code, trade_date, close, oi, oi_chg, short_term, medium_term,
long_term, composite, signal, detail_json, created_at FROM scores
WHERE ts_code = $1 AND trade_date = $2`, tsCode, tradeDate)
var detail sql.NullString
if err := row.Scan(&ctx.Score.ID, &ctx.Score.TsCode, &ctx.Score.TradeDate,
&ctx.Score.Close, &ctx.Score.OI, &ctx.Score.OIChg,
&ctx.Score.ShortTerm, &ctx.Score.MediumTerm, &ctx.Score.LongTerm,
&ctx.Score.Composite, &ctx.Score.Signal, &detail, &ctx.Score.CreatedAt); err != nil {
return nil, fmt.Errorf("score not found: %w", err)
}
if detail.Valid {
ctx.Score.Detail = json.RawMessage(detail.String)
}
// 2) 近 60 日 K 线
rows, err := s.db.Query(`SELECT ts_code, trade_date,
COALESCE(NULLIF(open, 'NaN'::real), 0), COALESCE(NULLIF(high, 'NaN'::real), 0),
COALESCE(NULLIF(low, 'NaN'::real), 0), COALESCE(NULLIF(close, 'NaN'::real), 0),
COALESCE(NULLIF(vol, 'NaN'::real), 0), COALESCE(NULLIF(amount, 'NaN'::real), 0),
COALESCE(NULLIF(oi, 'NaN'::real), 0), COALESCE(NULLIF(oi_chg, 'NaN'::real), 0),
COALESCE(NULLIF(pre_close, 'NaN'::real), 0)
FROM candles WHERE ts_code = $1 AND trade_date <= $2
ORDER BY trade_date DESC LIMIT 60`, tsCode, tradeDate)
if err != nil {
return nil, fmt.Errorf("candles: %w", err)
}
defer rows.Close()
for rows.Next() {
var c Candle
if err := rows.Scan(&c.TsCode, &c.TradeDate, &c.Open, &c.High, &c.Low, &c.Close,
&c.Vol, &c.Amount, &c.OI, &c.OIChg, &c.PreClose); err != nil {
return nil, err
}
ctx.Candles = append(ctx.Candles, c)
}
// 反转回升序
for i, j := 0, len(ctx.Candles)-1; i < j; i, j = i+1, j-1 {
ctx.Candles[i], ctx.Candles[j] = ctx.Candles[j], ctx.Candles[i]
}
// 3) 近 10 日 scores不含 detail_json 以减体积)
scoreRows, err := s.db.Query(`SELECT id, ts_code, trade_date, close, oi, oi_chg, short_term, medium_term,
long_term, composite, signal, created_at FROM scores
WHERE ts_code = $1 AND trade_date <= $2
ORDER BY trade_date DESC LIMIT 10`, tsCode, tradeDate)
if err != nil {
return nil, fmt.Errorf("recent scores: %w", err)
}
defer scoreRows.Close()
for scoreRows.Next() {
var sc Score
if err := scoreRows.Scan(&sc.ID, &sc.TsCode, &sc.TradeDate, &sc.Close, &sc.OI, &sc.OIChg,
&sc.ShortTerm, &sc.MediumTerm, &sc.LongTerm, &sc.Composite, &sc.Signal, &sc.CreatedAt); err != nil {
return nil, err
}
ctx.RecentScores = append(ctx.RecentScores, sc)
}
// 反转回升序
for i, j := 0, len(ctx.RecentScores)-1; i < j; i, j = i+1, j-1 {
ctx.RecentScores[i], ctx.RecentScores[j] = ctx.RecentScores[j], ctx.RecentScores[i]
}
return ctx, nil
}
// LLMConfig 数据库中的 LLM 配置单例。
type LLMConfig struct {
BaseURL string `json:"base_url"`
APIKey string `json:"api_key"`
Model string `json:"model"`
}
// EnsureLLMConfigTable 建 llm_config 表(幂等)。
func (s *FuturesStore) EnsureLLMConfigTable() error {
_, err := s.db.Exec(`
CREATE TABLE IF NOT EXISTS llm_config (
id INTEGER PRIMARY KEY DEFAULT 1 CHECK(id = 1),
base_url TEXT NOT NULL DEFAULT 'https://api.deepseek.com/v1',
api_key TEXT NOT NULL DEFAULT '',
model TEXT NOT NULL DEFAULT 'deepseek-chat',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO llm_config (id) VALUES (1) ON CONFLICT DO NOTHING;
`)
return err
}
// GetLLMConfig 读取 LLM 配置,无记录返回零值。
func (s *FuturesStore) GetLLMConfig() (*LLMConfig, error) {
cfg := &LLMConfig{}
err := s.db.QueryRow(`SELECT base_url, api_key, model FROM llm_config WHERE id = 1`).
Scan(&cfg.BaseURL, &cfg.APIKey, &cfg.Model)
if err != nil {
if err.Error() == "sql: no rows in result set" {
return cfg, nil
}
return nil, err
}
return cfg, nil
}
// SaveLLMConfig 写入 LLM 配置upsert
func (s *FuturesStore) SaveLLMConfig(cfg *LLMConfig) error {
_, err := s.db.Exec(`
INSERT INTO llm_config (id, base_url, api_key, model, updated_at)
VALUES (1, $1, $2, $3, CURRENT_TIMESTAMP)
ON CONFLICT (id) DO UPDATE SET
base_url = EXCLUDED.base_url,
api_key = EXCLUDED.api_key,
model = EXCLUDED.model,
updated_at = CURRENT_TIMESTAMP
`, cfg.BaseURL, cfg.APIKey, cfg.Model)
return err
}