diff --git a/docker-compose.yml b/docker-compose.yml index 369973e..f7fd990 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,12 +1,13 @@ services: postgres: image: postgres:18.3-alpine3.23 + container_name: trade-postgres environment: POSTGRES_USER: trade POSTGRES_PASSWORD: trade POSTGRES_DB: futures volumes: - - pgdata:/var/lib/postgresql/data + - pgdata:/var/lib/postgresql healthcheck: test: ["CMD-SHELL", "pg_isready -U trade -d futures"] interval: 5s @@ -15,6 +16,7 @@ services: tushare: build: ./tushare + container_name: trade-tushare env_file: ./tushare/.env environment: - DATABASE_URL=postgresql://trade:trade@postgres:5432/futures @@ -29,6 +31,7 @@ services: build: context: ./web dockerfile: backend/Dockerfile + container_name: trade-web env_file: ./web/backend/.env environment: - LISTEN_ADDR=:8080 diff --git a/tushare/Dockerfile b/tushare/Dockerfile index ad931dc..14bad48 100644 --- a/tushare/Dockerfile +++ b/tushare/Dockerfile @@ -9,8 +9,8 @@ ENV PYTHONDONTWRITEBYTECODE=1 \ WORKDIR /app -# 运行时依赖 + 时区 + libpq(psycopg2) -RUN apk add --no-cache tzdata libpq \ +# 时区(psycopg[binary] wheel 自带 libpq,不再需要系统装 libpq) +RUN apk add --no-cache tzdata \ && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && echo "Asia/Shanghai" > /etc/timezone diff --git a/tushare/requirements.txt b/tushare/requirements.txt index d54b57d..00ff912 100644 --- a/tushare/requirements.txt +++ b/tushare/requirements.txt @@ -3,4 +3,4 @@ pandas>=2.2.0 requests>=2.31.0 fastapi>=0.115.0 uvicorn[standard]>=0.34.0 -psycopg2-binary>=2.9.10 +psycopg[binary]>=3.2.0 diff --git a/tushare/src/storage.py b/tushare/src/storage.py index 1fb2435..0363824 100644 --- a/tushare/src/storage.py +++ b/tushare/src/storage.py @@ -3,8 +3,8 @@ import os from typing import Optional import pandas as pd -import psycopg2 -from psycopg2.extras import RealDictCursor +import psycopg +from psycopg.rows import dict_row 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): - return psycopg2.connect(db_url) + return psycopg.connect(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(""" 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, trade_date TEXT NOT NULL, 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) try: - with conn.cursor(cursor_factory=RealDictCursor) as cur: + with conn.cursor(row_factory=dict_row) as cur: cur.execute( "SELECT * FROM scores WHERE ts_code = %s ORDER BY trade_date DESC LIMIT 1", (ts_code,), diff --git a/web/backend/internal/store/futures.go b/web/backend/internal/store/futures.go index 9a1c16a..b96cca8 100644 --- a/web/backend/internal/store/futures.go +++ b/web/backend/internal/store/futures.go @@ -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, composite, signal, created_at FROM scores WHERE 1=1` args := []any{} + n := 0 + next := func() string { n++; return fmt.Sprintf("$%d", n) } if f.TsCode != "" { - q += " AND ts_code = ?" + q += " AND ts_code = " + next() args = append(args, f.TsCode) } if f.Start != "" { - q += " AND trade_date >= ?" + q += " AND trade_date >= " + next() args = append(args, f.Start) } if f.End != "" { - q += " AND trade_date <= ?" + q += " AND trade_date <= " + next() args = append(args, f.End) } q += " ORDER BY trade_date DESC, id DESC" if f.Limit <= 0 || f.Limit > 500 { f.Limit = 200 } - q += " LIMIT ?" + q += " LIMIT " + next() args = append(args, f.Limit) 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) { 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 detail sql.NullString 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(vol, 0), COALESCE(amount, 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} + n := 1 + next := func() string { n++; return fmt.Sprintf("$%d", n) } if start != "" { - q += " AND trade_date >= ?" + q += " AND trade_date >= " + next() args = append(args, start) } if end != "" { - q += " AND trade_date <= ?" + q += " AND trade_date <= " + next() args = append(args, end) } q += " ORDER BY trade_date ASC LIMIT 1000"