Files
stock/stock-html/db.py
T
freedakgmail 40ce519188 fix: 修复6个数据和代码问题
1. 修复 _generate_plain_summary position key 不匹配 (20d→d20, pct→range_pct)
2. 修复 mairui_api.py 日期解析不一致及 float(None) 崩溃风险
3. 修复 fund_flow_analyzer.py 麦蕊回退数据 close_price=0 导致量价背离误判
4. 修复 db_get_fund_flow_history 缺少完整字段和日期过滤
5. 修复 scheduler.py 连接池泄漏 (conn.close() → put_db(conn))
6. 修复多处北交所股票代码映射缺失 (8/9开头→bj)
2026-07-22 07:20:44 +08:00

641 lines
20 KiB
Python

"""
数据库连接和用户认证
"""
import psycopg2
from psycopg2 import pool
from psycopg2.extras import RealDictCursor
from werkzeug.security import generate_password_hash, check_password_hash
from flask import session
import functools
from config import Config
_pool = None
def init_db_pool():
"""初始化数据库连接池(应用启动时调用一次)"""
global _pool
if _pool is not None:
return
try:
_pool = pool.ThreadedConnectionPool(
minconn=2,
maxconn=20,
host=Config.DB_HOST,
port=Config.DB_PORT,
database=Config.DB_NAME,
user=Config.DB_USER,
password=Config.DB_PASSWORD, sslmode='disable',
)
print(f"[DB] 连接池初始化成功 (min=2, max=20)")
except Exception as e:
print(f"[DB] 连接池初始化失败,降级为逐次建连: {e}")
_pool = None
def get_db():
"""从连接池获取数据库连接(用完必须调用 put_db(conn) 归还)"""
global _pool
if _pool is not None:
try:
conn = _pool.getconn()
if conn and not conn.closed:
conn.autocommit = False
return conn
except Exception as e:
print(f"[DB] 连接池获取失败: {e}")
try:
conn = psycopg2.connect(
host=Config.DB_HOST,
port=Config.DB_PORT,
database=Config.DB_NAME,
user=Config.DB_USER,
password=Config.DB_PASSWORD, sslmode="disable"
)
return conn
except Exception as e:
print(f"数据库连接失败: {e}")
return None
def put_db(conn):
"""归还连接到连接池"""
if conn is None:
return
global _pool
if _pool is not None:
try:
_pool.putconn(conn)
return
except Exception:
pass
try:
conn.close()
except Exception:
pass
def login_required(f):
"""登录验证装饰器"""
@functools.wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
from flask import jsonify
return jsonify({'success': False, 'error': '请先登录'}), 401
return f(*args, **kwargs)
return decorated_function
def get_current_user_id():
"""获取当前登录用户ID"""
return session.get('user_id')
def get_current_username():
"""获取当前登录用户名"""
return session.get('username')
# ========== 可用资金操作 ==========
def db_get_available_cash(user_id):
"""获取用户可用资金"""
conn = get_db()
if not conn:
return 0
try:
cur = conn.cursor(cursor_factory=RealDictCursor)
cur.execute("SELECT available_cash FROM users WHERE id = %s", (user_id,))
result = cur.fetchone()
return float(result['available_cash'] or 0) if result else 0
finally:
put_db(conn)
def db_update_available_cash(user_id, amount):
"""更新用户可用资金"""
conn = get_db()
if not conn:
return False, '数据库连接失败'
try:
cur = conn.cursor()
cur.execute("UPDATE users SET available_cash = %s WHERE id = %s", (amount, user_id))
conn.commit()
return True, None
except Exception as e:
conn.rollback()
return False, str(e)
finally:
put_db(conn)
# ========== 用户操作 ==========
def create_user(email, password):
"""创建用户(使用邮箱)"""
conn = get_db()
if not conn:
return None, '数据库连接失败'
try:
cur = conn.cursor(cursor_factory=RealDictCursor)
# 检查邮箱是否已存在
cur.execute("SELECT id FROM users WHERE email = %s OR username = %s", (email, email))
if cur.fetchone():
return None, '该邮箱已注册'
# 创建用户(username和email都存邮箱)
password_hash = generate_password_hash(password)
cur.execute(
"INSERT INTO users (username, email, password_hash) VALUES (%s, %s, %s) RETURNING id, username, email",
(email, email, password_hash)
)
user = cur.fetchone()
conn.commit()
return user, None
except Exception as e:
conn.rollback()
return None, str(e)
finally:
put_db(conn)
def verify_user(email, password):
"""验证用户登录(使用邮箱)"""
conn = get_db()
if not conn:
return None, '数据库连接失败'
try:
cur = conn.cursor(cursor_factory=RealDictCursor)
# 同时检查email和username字段(兼容旧数据)
cur.execute("SELECT * FROM users WHERE email = %s OR username = %s", (email, email))
user = cur.fetchone()
if not user or not check_password_hash(user['password_hash'], password):
return None, '邮箱或密码错误'
return {'id': user['id'], 'username': user.get('email') or user['username']}, None
finally:
put_db(conn)
def change_user_password(user_id, old_password, new_password):
"""修改用户密码"""
conn = get_db()
if not conn:
return False, '数据库连接失败'
try:
cur = conn.cursor(cursor_factory=RealDictCursor)
cur.execute("SELECT * FROM users WHERE id = %s", (user_id,))
user = cur.fetchone()
if not user:
return False, '用户不存在'
if not check_password_hash(user['password_hash'], old_password):
return False, '当前密码错误'
new_hash = generate_password_hash(new_password)
cur.execute("UPDATE users SET password_hash = %s WHERE id = %s", (new_hash, user_id))
conn.commit()
return True, None
except Exception as e:
return False, str(e)
finally:
put_db(conn)
# ========== 交易记录操作(数据库版) ==========
def db_get_trades(user_id):
"""从数据库获取用户交易记录"""
conn = get_db()
if not conn:
return []
try:
cur = conn.cursor(cursor_factory=RealDictCursor)
cur.execute("""
SELECT id, stock_code, stock_name, trade_type, price, quantity,
trade_date::text, reason, result, profit_amount, stop_loss_price, notes,
created_at::text
FROM trades
WHERE user_id = %s
ORDER BY trade_date DESC, created_at DESC
""", (user_id,))
return cur.fetchall()
finally:
put_db(conn)
def db_get_trade(user_id, trade_id):
"""获取单条交易记录"""
conn = get_db()
if not conn:
return None
try:
cur = conn.cursor(cursor_factory=RealDictCursor)
cur.execute("""
SELECT id, stock_code, stock_name, trade_type, price, quantity,
trade_date::text, reason, result, profit_amount, stop_loss_price, notes,
created_at::text
FROM trades WHERE id = %s AND user_id = %s
""", (trade_id, user_id))
return cur.fetchone()
finally:
put_db(conn)
def db_add_trade(user_id, data):
"""添加交易记录到数据库"""
conn = get_db()
if not conn:
return None, '数据库连接失败'
try:
cur = conn.cursor(cursor_factory=RealDictCursor)
cur.execute("""
INSERT INTO trades (user_id, stock_code, stock_name, trade_type, price,
quantity, trade_date, reason, result, profit_amount,
stop_loss_price, notes)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING id, stock_code, stock_name, trade_type, price, quantity,
trade_date::text, reason, result, profit_amount, stop_loss_price,
notes, created_at::text
""", (
user_id,
data.get('stock_code'),
data.get('stock_name'),
data.get('trade_type'),
data.get('price'),
data.get('quantity'),
data.get('trade_date'),
data.get('reason'),
data.get('result'),
data.get('profit_amount'),
data.get('stop_loss_price'),
data.get('notes')
))
trade = cur.fetchone()
conn.commit()
return trade, None
except Exception as e:
conn.rollback()
return None, str(e)
finally:
put_db(conn)
def db_update_trade(user_id, trade_id, data):
"""更新交易记录"""
conn = get_db()
if not conn:
return None, '数据库连接失败'
try:
cur = conn.cursor(cursor_factory=RealDictCursor)
cur.execute("""
UPDATE trades SET
stock_code = COALESCE(%s, stock_code),
stock_name = COALESCE(%s, stock_name),
trade_type = COALESCE(%s, trade_type),
price = COALESCE(%s, price),
quantity = COALESCE(%s, quantity),
trade_date = COALESCE(%s, trade_date),
reason = COALESCE(%s, reason),
result = COALESCE(%s, result),
profit_amount = COALESCE(%s, profit_amount),
stop_loss_price = COALESCE(%s, stop_loss_price),
notes = COALESCE(%s, notes)
WHERE id = %s AND user_id = %s
RETURNING id, stock_code, stock_name, trade_type, price, quantity,
trade_date::text, reason, result, profit_amount, stop_loss_price,
notes, created_at::text
""", (
data.get('stock_code'),
data.get('stock_name'),
data.get('trade_type'),
data.get('price'),
data.get('quantity'),
data.get('trade_date'),
data.get('reason'),
data.get('result'),
data.get('profit_amount'),
data.get('stop_loss_price'),
data.get('notes'),
trade_id,
user_id
))
trade = cur.fetchone()
conn.commit()
return trade, None
except Exception as e:
conn.rollback()
return None, str(e)
finally:
put_db(conn)
def db_delete_trade(user_id, trade_id):
"""删除交易记录"""
conn = get_db()
if not conn:
return False
try:
cur = conn.cursor()
cur.execute("DELETE FROM trades WHERE id = %s AND user_id = %s", (trade_id, user_id))
conn.commit()
return cur.rowcount > 0
finally:
put_db(conn)
# ========== 关注列表操作(数据库版) ==========
def db_get_watchlist(user_id):
"""从数据库获取用户关注列表"""
conn = get_db()
if not conn:
return []
try:
cur = conn.cursor(cursor_factory=RealDictCursor)
cur.execute("""
SELECT stock_code as code, stock_name as name, added_time::text
FROM watchlist
WHERE user_id = %s
ORDER BY added_time DESC
""", (user_id,))
return cur.fetchall()
finally:
put_db(conn)
def db_add_to_watchlist(user_id, code, name):
"""添加到关注列表"""
conn = get_db()
if not conn:
return None, '数据库连接失败'
try:
cur = conn.cursor(cursor_factory=RealDictCursor)
cur.execute("""
INSERT INTO watchlist (user_id, stock_code, stock_name)
VALUES (%s, %s, %s)
ON CONFLICT (user_id, stock_code) DO NOTHING
RETURNING stock_code as code, stock_name as name
""", (user_id, code, name))
conn.commit()
return db_get_watchlist(user_id), None
except Exception as e:
conn.rollback()
return None, str(e)
finally:
put_db(conn)
def db_remove_from_watchlist(user_id, code):
"""从关注列表移除"""
conn = get_db()
if not conn:
return None
try:
cur = conn.cursor()
cur.execute("DELETE FROM watchlist WHERE user_id = %s AND stock_code = %s", (user_id, code))
conn.commit()
return db_get_watchlist(user_id)
finally:
put_db(conn)
# ========== 分析缓存操作(数据库版) ==========
def db_get_alerts_cache(user_id):
"""从数据库获取分析缓存"""
conn = get_db()
if not conn:
return None
try:
cur = conn.cursor(cursor_factory=RealDictCursor)
cur.execute("""
SELECT data, updated_at::text as lastUpdate
FROM alerts_cache
WHERE user_id = %s
""", (user_id,))
result = cur.fetchone()
if result:
return {
'alerts': result['data'] or [],
'lastUpdate': result['lastupdate']
}
return {'alerts': [], 'lastUpdate': None}
finally:
put_db(conn)
def db_save_alerts_cache(user_id, alerts):
"""保存分析缓存到数据库"""
conn = get_db()
if not conn:
return False
try:
import json
cur = conn.cursor()
cur.execute("""
INSERT INTO alerts_cache (user_id, data, updated_at)
VALUES (%s, %s, NOW())
ON CONFLICT (user_id) DO UPDATE SET
data = EXCLUDED.data,
updated_at = NOW()
""", (user_id, json.dumps(alerts)))
conn.commit()
return True
except Exception as e:
conn.rollback()
print(f"保存分析缓存失败: {e}")
return False
finally:
put_db(conn)
# ========== 基本面数据操作(数据库版) ==========
def db_get_fundamental(code):
"""从数据库获取基本面数据(当日缓存)"""
conn = get_db()
if not conn:
return None
try:
from datetime import date
cur = conn.cursor(cursor_factory=RealDictCursor)
cur.execute("""
SELECT code, name, pe, pb, total_market_cap, industry,
latest_price, change_pct, update_date::text, updated_at::text,
roe, eps, bps, revenue_yoy, profit_yoy, gross_margin, net_margin
FROM stock_fundamental
WHERE code = %s AND update_date = %s
""", (code, date.today()))
return cur.fetchone()
finally:
put_db(conn)
def db_save_fundamental(code, data):
"""保存基本面数据到数据库"""
conn = get_db()
if not conn:
return False
try:
from datetime import date
cur = conn.cursor()
cur.execute("""
INSERT INTO stock_fundamental
(code, name, pe, pb, total_market_cap, industry, latest_price, change_pct, update_date,
roe, eps, bps, revenue_yoy, profit_yoy, gross_margin, net_margin)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (code) DO UPDATE SET
name = EXCLUDED.name,
pe = EXCLUDED.pe,
pb = EXCLUDED.pb,
total_market_cap = EXCLUDED.total_market_cap,
industry = EXCLUDED.industry,
latest_price = EXCLUDED.latest_price,
change_pct = EXCLUDED.change_pct,
update_date = EXCLUDED.update_date,
roe = EXCLUDED.roe,
eps = EXCLUDED.eps,
bps = EXCLUDED.bps,
revenue_yoy = EXCLUDED.revenue_yoy,
profit_yoy = EXCLUDED.profit_yoy,
gross_margin = EXCLUDED.gross_margin,
net_margin = EXCLUDED.net_margin,
updated_at = NOW()
""", (
code,
data.get('name') or data.get('stock_name'),
data.get('pe') or data.get('pe_ttm'),
data.get('pb'),
data.get('total_market_cap'),
data.get('industry'),
data.get('latest_price'),
data.get('change_pct'),
date.today(),
data.get('roe'),
data.get('eps'),
data.get('bps'),
data.get('revenue_yoy'),
data.get('profit_yoy'),
data.get('gross_margin'),
data.get('net_margin'),
))
conn.commit()
return True
except Exception as e:
conn.rollback()
print(f"保存基本面数据失败: {e}")
return False
finally:
put_db(conn)
# ========== 资金流向历史数据操作(数据库版) ==========
def db_get_fund_flow_history(code, limit=60):
"""获取股票的资金流向历史数据"""
conn = get_db()
if not conn:
return None, None
try:
cur = conn.cursor(cursor_factory=RealDictCursor)
cur.execute("""
SELECT code, trade_date::text, close_price, change_pct,
main_net_inflow, main_net_inflow_pct,
super_net_inflow, super_net_inflow_pct,
big_net_inflow, big_net_inflow_pct,
mid_net_inflow, mid_net_inflow_pct,
small_net_inflow, small_net_inflow_pct
FROM stock_fund_flow_history
WHERE code = %s
ORDER BY trade_date DESC
LIMIT %s
""", (code, limit))
rows = cur.fetchall()
# 获取最新日期
latest_date = rows[0]['trade_date'] if rows else None
return [dict(row) for row in rows], latest_date
finally:
put_db(conn)
def db_save_fund_flow_history(code, records):
"""保存资金流向历史数据到数据库"""
conn = get_db()
if not conn:
return False
try:
cur = conn.cursor()
for r in records:
cur.execute("""
INSERT INTO stock_fund_flow_history
(code, trade_date, close_price, change_pct,
main_net_inflow, main_net_inflow_pct,
super_net_inflow, super_net_inflow_pct,
big_net_inflow, big_net_inflow_pct,
mid_net_inflow, mid_net_inflow_pct,
small_net_inflow, small_net_inflow_pct)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (code, trade_date) DO UPDATE SET
close_price = EXCLUDED.close_price,
change_pct = EXCLUDED.change_pct,
main_net_inflow = EXCLUDED.main_net_inflow,
main_net_inflow_pct = EXCLUDED.main_net_inflow_pct,
super_net_inflow = EXCLUDED.super_net_inflow,
super_net_inflow_pct = EXCLUDED.super_net_inflow_pct,
big_net_inflow = EXCLUDED.big_net_inflow,
big_net_inflow_pct = EXCLUDED.big_net_inflow_pct,
mid_net_inflow = EXCLUDED.mid_net_inflow,
mid_net_inflow_pct = EXCLUDED.mid_net_inflow_pct,
small_net_inflow = EXCLUDED.small_net_inflow,
small_net_inflow_pct = EXCLUDED.small_net_inflow_pct,
updated_at = NOW()
""", (
code,
r.get('日期') or r.get('trade_date'),
r.get('收盘价') or r.get('close_price'),
r.get('涨跌幅') or r.get('change_pct'),
r.get('主力净流入-净额') or r.get('main_net_inflow'),
r.get('主力净流入-净占比') or r.get('main_net_inflow_pct'),
r.get('超大单净流入-净额') or r.get('super_net_inflow'),
r.get('超大单净流入-净占比') or r.get('super_net_inflow_pct'),
r.get('大单净流入-净额') or r.get('big_net_inflow'),
r.get('大单净流入-净占比') or r.get('big_net_inflow_pct'),
r.get('中单净流入-净额') or r.get('mid_net_inflow'),
r.get('中单净流入-净占比') or r.get('mid_net_inflow_pct'),
r.get('小单净流入-净额') or r.get('small_net_inflow'),
r.get('小单净流入-净占比') or r.get('small_net_inflow_pct')
))
conn.commit()
return True
except Exception as e:
conn.rollback()
print(f"保存资金流向历史失败: {e}")
return False
finally:
put_db(conn)