迁移 psycopg3 并修复 Postgres 18 兼容性问题

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
fish
2026-05-03 15:29:08 +08:00
parent 961ab8224e
commit d3ec1de275
5 changed files with 24 additions and 17 deletions

View File

@@ -1,12 +1,13 @@
services: services:
postgres: postgres:
image: postgres:18.3-alpine3.23 image: postgres:18.3-alpine3.23
container_name: trade-postgres
environment: environment:
POSTGRES_USER: trade POSTGRES_USER: trade
POSTGRES_PASSWORD: trade POSTGRES_PASSWORD: trade
POSTGRES_DB: futures POSTGRES_DB: futures
volumes: volumes:
- pgdata:/var/lib/postgresql/data - pgdata:/var/lib/postgresql
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U trade -d futures"] test: ["CMD-SHELL", "pg_isready -U trade -d futures"]
interval: 5s interval: 5s
@@ -15,6 +16,7 @@ services:
tushare: tushare:
build: ./tushare build: ./tushare
container_name: trade-tushare
env_file: ./tushare/.env env_file: ./tushare/.env
environment: environment:
- DATABASE_URL=postgresql://trade:trade@postgres:5432/futures - DATABASE_URL=postgresql://trade:trade@postgres:5432/futures
@@ -29,6 +31,7 @@ services:
build: build:
context: ./web context: ./web
dockerfile: backend/Dockerfile dockerfile: backend/Dockerfile
container_name: trade-web
env_file: ./web/backend/.env env_file: ./web/backend/.env
environment: environment:
- LISTEN_ADDR=:8080 - LISTEN_ADDR=:8080

View File

@@ -9,8 +9,8 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
WORKDIR /app WORKDIR /app
# 运行时依赖 + 时区 + libpq(psycopg2) # 时区(psycopg[binary] wheel 自带 libpq,不再需要系统装 libpq)
RUN apk add --no-cache tzdata libpq \ RUN apk add --no-cache tzdata \
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone && echo "Asia/Shanghai" > /etc/timezone

View File

@@ -3,4 +3,4 @@ pandas>=2.2.0
requests>=2.31.0 requests>=2.31.0
fastapi>=0.115.0 fastapi>=0.115.0
uvicorn[standard]>=0.34.0 uvicorn[standard]>=0.34.0
psycopg2-binary>=2.9.10 psycopg[binary]>=3.2.0

View File

@@ -3,8 +3,8 @@ import os
from typing import Optional from typing import Optional
import pandas as pd import pandas as pd
import psycopg2 import psycopg
from psycopg2.extras import RealDictCursor from psycopg.rows import dict_row
from .models import ScoreResult from .models import ScoreResult
@@ -12,7 +12,7 @@ DEFAULT_DB_URL = os.environ.get("DATABASE_URL", "postgresql://trade:trade@postgr
def _get_conn(db_url: str = DEFAULT_DB_URL): def _get_conn(db_url: str = DEFAULT_DB_URL):
return psycopg2.connect(db_url) return psycopg.connect(db_url)
def init_db(db_url: str = DEFAULT_DB_URL): def init_db(db_url: str = DEFAULT_DB_URL):
@@ -38,7 +38,7 @@ def init_db(db_url: str = DEFAULT_DB_URL):
""") """)
cur.execute(""" cur.execute("""
CREATE TABLE IF NOT EXISTS scores ( CREATE TABLE IF NOT EXISTS scores (
id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY, id UUID DEFAULT uuidv7() PRIMARY KEY,
ts_code TEXT NOT NULL, ts_code TEXT NOT NULL,
trade_date TEXT NOT NULL, trade_date TEXT NOT NULL,
close REAL, close REAL,
@@ -143,7 +143,7 @@ def get_latest_score(ts_code: str, db_url: str = DEFAULT_DB_URL) -> Optional[dic
"""查询最新打分记录。""" """查询最新打分记录。"""
conn = _get_conn(db_url) conn = _get_conn(db_url)
try: try:
with conn.cursor(cursor_factory=RealDictCursor) as cur: with conn.cursor(row_factory=dict_row) as cur:
cur.execute( cur.execute(
"SELECT * FROM scores WHERE ts_code = %s ORDER BY trade_date DESC LIMIT 1", "SELECT * FROM scores WHERE ts_code = %s ORDER BY trade_date DESC LIMIT 1",
(ts_code,), (ts_code,),

View File

@@ -55,23 +55,25 @@ func (s *FuturesStore) ListScores(f ScoreFilter) ([]Score, error) {
q := `SELECT id, ts_code, trade_date, close, oi, oi_chg, short_term, medium_term, long_term, q := `SELECT id, ts_code, trade_date, close, oi, oi_chg, short_term, medium_term, long_term,
composite, signal, created_at FROM scores WHERE 1=1` composite, signal, created_at FROM scores WHERE 1=1`
args := []any{} args := []any{}
n := 0
next := func() string { n++; return fmt.Sprintf("$%d", n) }
if f.TsCode != "" { if f.TsCode != "" {
q += " AND ts_code = ?" q += " AND ts_code = " + next()
args = append(args, f.TsCode) args = append(args, f.TsCode)
} }
if f.Start != "" { if f.Start != "" {
q += " AND trade_date >= ?" q += " AND trade_date >= " + next()
args = append(args, f.Start) args = append(args, f.Start)
} }
if f.End != "" { if f.End != "" {
q += " AND trade_date <= ?" q += " AND trade_date <= " + next()
args = append(args, f.End) args = append(args, f.End)
} }
q += " ORDER BY trade_date DESC, id DESC" q += " ORDER BY trade_date DESC, id DESC"
if f.Limit <= 0 || f.Limit > 500 { if f.Limit <= 0 || f.Limit > 500 {
f.Limit = 200 f.Limit = 200
} }
q += " LIMIT ?" q += " LIMIT " + next()
args = append(args, f.Limit) args = append(args, f.Limit)
rows, err := s.db.Query(q, args...) rows, err := s.db.Query(q, args...)
@@ -93,7 +95,7 @@ func (s *FuturesStore) ListScores(f ScoreFilter) ([]Score, error) {
func (s *FuturesStore) GetScore(id string) (*Score, error) { func (s *FuturesStore) GetScore(id string) (*Score, error) {
row := s.db.QueryRow(`SELECT id, ts_code, trade_date, close, oi, oi_chg, short_term, medium_term, 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 id = ?`, id) long_term, composite, signal, detail_json, created_at FROM scores WHERE id = $1`, id)
var x Score var x Score
var detail sql.NullString var detail sql.NullString
if err := row.Scan(&x.ID, &x.TsCode, &x.TradeDate, &x.Close, &x.OI, &x.OIChg, if err := row.Scan(&x.ID, &x.TsCode, &x.TradeDate, &x.Close, &x.OI, &x.OIChg,
@@ -145,14 +147,16 @@ func (s *FuturesStore) ListCandles(tsCode, start, end string) ([]Candle, error)
COALESCE(open, 0), COALESCE(high, 0), COALESCE(low, 0), COALESCE(close, 0), COALESCE(open, 0), COALESCE(high, 0), COALESCE(low, 0), COALESCE(close, 0),
COALESCE(vol, 0), COALESCE(amount, 0), COALESCE(vol, 0), COALESCE(amount, 0),
COALESCE(oi, 0), COALESCE(oi_chg, 0), COALESCE(pre_close, 0) COALESCE(oi, 0), COALESCE(oi_chg, 0), COALESCE(pre_close, 0)
FROM candles WHERE ts_code = ?` FROM candles WHERE ts_code = $1`
args := []any{tsCode} args := []any{tsCode}
n := 1
next := func() string { n++; return fmt.Sprintf("$%d", n) }
if start != "" { if start != "" {
q += " AND trade_date >= ?" q += " AND trade_date >= " + next()
args = append(args, start) args = append(args, start)
} }
if end != "" { if end != "" {
q += " AND trade_date <= ?" q += " AND trade_date <= " + next()
args = append(args, end) args = append(args, end)
} }
q += " ORDER BY trade_date ASC LIMIT 1000" q += " ORDER BY trade_date ASC LIMIT 1000"