Files
Neo-ZQYY/apps/backend/app/database.py
Neo 6f8f12314f feat: 累积功能变更 — 聊天集成、租户管理、小程序更新、ETL 增强、迁移脚本
包含多个会话的累积代码变更:
- backend: AI 聊天服务、触发器调度、认证增强、WebSocket、调度器最小间隔
- admin-web: ETL 状态页、任务管理、调度配置、登录优化
- miniprogram: 看板页面、聊天集成、UI 组件、导航更新
- etl: DWS 新任务(finance_area_daily/board_cache)、连接器增强
- tenant-admin: 项目初始化
- db: 19 个迁移脚本(etl_feiqiu 11 + zqyy_app 8)
- packages/shared: 枚举和工具函数更新
- tools: 数据库工具、报表生成、健康检查
- docs: PRD/架构/部署/合约文档更新

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 00:03:48 +08:00

145 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
数据库连接
使用 psycopg2 直连 PostgreSQL不引入 ORM。
连接参数从环境变量读取(经 config 模块加载)。
提供两类连接:
- get_connection()zqyy_app 读写连接(用户/队列/调度等业务数据)
- get_etl_readonly_connection(site_id)etl_feiqiu 只读连接(数据库查看器),
自动设置 RLS site_id 隔离
当 DEV_TRACE_ENABLED=true 且存在活跃 TraceContext 时,
get_connection() 返回 TracedConnection 包装,自动记录 DB_CONN / DB_QUERY / DB_CONN_RELEASE span。
"""
import time
import psycopg2
from psycopg2.extensions import connection as PgConnection
from app.config import (
APP_DB_NAME,
DB_HOST,
DB_PASSWORD,
DB_PORT,
DB_USER,
ETL_DB_HOST,
ETL_DB_NAME,
ETL_DB_PASSWORD,
ETL_DB_PORT,
ETL_DB_USER,
)
def get_connection() -> PgConnection:
"""
获取 zqyy_app 数据库连接。
调用方负责关闭连接(推荐配合 contextmanager 或 try/finally 使用)。
当 trace 启用且有活跃 TraceContext 时,返回 TracedConnection 包装,
自动记录 DB_CONN span连接获取耗时并拦截后续 SQL 执行。
"""
# CHANGE 2026-03-22 | task 8.2 | 集成 trace db_wrapper仅 trace 启用时包装
from app.trace.config import get_trace_config
from app.trace.context import SpanType, TraceSpan, get_current_trace
config = get_trace_config()
should_trace = config.enabled and get_current_trace() is not None
start = time.perf_counter() if should_trace else 0.0
conn = psycopg2.connect(
host=DB_HOST,
port=DB_PORT,
user=DB_USER,
password=DB_PASSWORD,
dbname=APP_DB_NAME,
)
if should_trace:
from datetime import datetime
from app.trace.db_wrapper import traced_connection
elapsed_ms = (time.perf_counter() - start) * 1000
ctx = get_current_trace()
# ctx 不为 None上面已检查
ctx.add_span(TraceSpan(
span_type=SpanType.DB_CONN,
module="app.database",
function="get_connection",
description_zh=f"获取数据库连接,耗时 {elapsed_ms:.1f}ms",
description_en=f"Acquired database connection in {elapsed_ms:.1f}ms",
params={},
result_summary=f"{elapsed_ms:.1f}ms",
duration_ms=elapsed_ms,
timestamp=datetime.now().isoformat(),
))
return traced_connection(conn)
return conn
def get_etl_global_readonly_connection() -> PgConnection:
"""
获取 ETL 数据库的全局只读连接(不设 RLS
用于系统管理后台等不需要门店隔离的场景(如 ETL 状态监控)。
"""
conn = psycopg2.connect(
host=ETL_DB_HOST,
port=ETL_DB_PORT,
user=ETL_DB_USER,
password=ETL_DB_PASSWORD,
dbname=ETL_DB_NAME,
)
try:
conn.autocommit = False
with conn.cursor() as cur:
cur.execute("SET default_transaction_read_only = on")
conn.commit()
except Exception:
conn.close()
raise
return conn
def get_etl_readonly_connection(site_id: int | str) -> PgConnection:
"""
获取 ETL 数据库etl_feiqiu的只读连接。
连接建立后自动执行:
1. SET default_transaction_read_only = on — 禁止写操作
2. SET LOCAL app.current_site_id = '{site_id}' — 启用 RLS 门店隔离
调用方负责关闭连接。典型用法::
conn = get_etl_readonly_connection(site_id)
try:
with conn.cursor() as cur:
cur.execute("SELECT ...")
finally:
conn.close()
"""
conn = psycopg2.connect(
host=ETL_DB_HOST,
port=ETL_DB_PORT,
user=ETL_DB_USER,
password=ETL_DB_PASSWORD,
dbname=ETL_DB_NAME,
)
try:
conn.autocommit = False
with conn.cursor() as cur:
# 会话级只读:防止任何写操作
cur.execute("SET default_transaction_read_only = on")
# 事务级 RLS 隔离:设置当前门店 ID
cur.execute(
"SET LOCAL app.current_site_id = %s", (str(site_id),)
)
conn.commit()
except Exception:
conn.close()
raise
return conn