Files
crawler-plugin/backend/utils/db.py
T
huangzd1997 228d481211 perf(全链路): 连接池/事务边界/轮询与 IO 效率收口
Java:
- LLM 180s 读超时不再被全局 call-timeout 静默截断成 90s(长思考请求被掐断→重试→付费网关二次计费)
- 代理 HttpClient 缓存改有界 LRU(jikip 每次提取新 IP,无界缓存持续泄漏 selector 线程与连接池)
- 12 个 service 的 Redis 任务锁移出 @Transactional(自旋最坏 10s 白占 DB 连接,池仅 30),远端对象删/传改 afterCommit
- 结果文件 Job 闸门拒绝时不再回退内联执行(改重新入队,避免把背压转嫁给 MQ 消费线程)
- imagevideo 每秒扫描加列投影、过期清理加 LIMIT;权限页整表查询改列投影(不再拉回密码哈希)
- 哈希改 HexFormat;补 5 处"不能改"的技术依据注释(批量插入会丢回填主键、流式丢模板与图片等)

前端:4 个工具页轮询改轻量端点(带 fallback);PriceTrack 快照节流写盘;候选店铺表分页;页面隐藏时停表

客户端:HTTP 连接池按出口复用(Session 仍每请求新建,保持无跨请求状态);品牌检测 WIPO 逐请求握手;
代理配置按 mtime 缓存;串行任务改专属池;异常降级为标签页重连;紫鸟启动改端口轮询;模板编译缓存;
日志上报连接与落盘收口;Flask 版本 API 改按请求复用连接
2026-09-15 23:01:59 +08:00

175 lines
5.9 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.
"""
数据库连接与初始化
请求级连接复用:pymysql 每次 connect 都要做一次完整的鉴权握手,而本进程
15124)只服务 /api/version、/api/version/latest 这类轻量查询,握手开销
占了单次请求的大头。连接因此挂在 flask.g 上按请求复用,请求结束由
close_db(注册在 app.teardown_appcontext)统一释放。
"""
import re
import pymysql
try:
from flask import g, has_app_context
except ImportError: # 非 Flask 环境(脚本/离线核查)下退化为每次新建连接
g = None
def has_app_context() -> bool:
return False
# 请求级连接挂在 flask.g 上的属性名(避免与业务字段冲突)
_CONN_ATTR = "_shufu_db_conn"
try:
from config import mysql_host as config_mysql_host
from config import mysql_user as config_mysql_user
from config import mysql_password as config_mysql_password
from config import mysql_database as config_mysql_database
from config import mysql_host_source as config_mysql_host_source
from config import mysql_user_source as config_mysql_user_source
from config import mysql_database_source as config_mysql_database_source
except ImportError:
config_mysql_host = 'localhost'
config_mysql_user = 'root'
config_mysql_password = ''
config_mysql_database = 'maixiang_ai'
config_mysql_host_source = 'fallback.default'
config_mysql_user_source = 'fallback.default'
config_mysql_database_source = 'fallback.default'
mysql_host = config_mysql_host
mysql_user = config_mysql_user
mysql_password = config_mysql_password
mysql_database = config_mysql_database
mysql_host_source = config_mysql_host_source
mysql_user_source = config_mysql_user_source
mysql_database_source = config_mysql_database_source
def _safe_identifier(name):
"""仅允许字母、数字、下划线的数据库/表名片段,防止注入 DDL 片段。"""
if not re.fullmatch(r'[A-Za-z0-9_]+', name or ''):
raise ValueError(f'非法标识符: {name!r}')
return name
def describe_db_target():
return (
f"{mysql_user}@{mysql_host}/{mysql_database} "
f"(host={mysql_host_source}, user={mysql_user_source}, database={mysql_database_source})"
)
def _connect():
return pymysql.connect(
host=mysql_host,
user=mysql_user,
password=mysql_password,
database=mysql_database,
charset='utf8mb4',
cursorclass=pymysql.cursors.DictCursor
)
def _is_alive(conn) -> bool:
"""连接可用性探测:已关闭直接判死,否则发一次 COM_PING。
COM_PING 一次往返远小于一次完整鉴权握手;不用 ping(reconnect=True)
(该参数在新版 pymysql 已废弃,且失败时会自行重连、语义不可控)。
"""
try:
if not conn.open:
return False
conn.ping(reconnect=False)
return True
except Exception:
return False
def _close_quietly(conn) -> None:
"""静默关闭连接(可能为 None / 已关闭 / 断连)。"""
if conn is None:
return
try:
conn.close()
except Exception:
pass
def get_db():
"""取当前请求的数据库连接(同一请求内复用,避免每次请求都做鉴权握手)。
连接挂在 flask.g 上,请求结束由 close_db 释放;被服务端 wait_timeout
断开、或调用方提前 close 过,这里都会静默换成新连接,调用方无感知。
无 Flask 应用上下文时(脚本调用)退回每次新建,保持旧行为。
"""
ctx = g if has_app_context() else None
conn = getattr(ctx, _CONN_ATTR, None) if ctx is not None else None
if conn is not None:
if _is_alive(conn):
return conn
# 断连/已被关闭:丢弃旧连接后重建(不把坏连接交给业务查询)
_close_quietly(conn)
try:
delattr(ctx, _CONN_ATTR)
except Exception:
pass
conn = _connect()
if ctx is not None:
setattr(ctx, _CONN_ATTR, conn)
return conn
def close_db(exc=None) -> None:
"""释放当前请求的连接(注册为 app.teardown_appcontext 回调)。
异常路径同样会走 teardown,因此查询抛错时连接不会泄漏到下一次请求。
"""
ctx = g if has_app_context() else None
if ctx is None:
return
conn = getattr(ctx, _CONN_ATTR, None)
if conn is None:
return
try:
delattr(ctx, _CONN_ATTR)
except Exception:
pass
_close_quietly(conn)
def init_db():
"""确保版本公开 API 依赖的最小表结构存在。
本进程(15124)只服务 /api/version、/api/version/latest,仅依赖 web_config 表。
历史遗留说明(2026-09 全维度审查后删除):这里原本还会创建 users 表、执行 role 角色
迁移、并在查不到 super_admin 时用 ADMIN_PASSWORD(默认 admin123)插入一个超管。
管理后台早已迁到 Javausers 表结构由 Flyway 迁移管理,本进程再写会与之冲突;
其中「无超管即用默认密码建超管」在生产等同于后门(删除超管或改 role 枚举后本进程
重启就会静默重建一个密码已知的超管),故整段移除。
"""
conn = pymysql.connect(
host=mysql_host,
user=mysql_user,
password=mysql_password,
charset='utf8mb4'
)
try:
db_name = _safe_identifier(mysql_database)
with conn.cursor() as cur:
cur.execute(f"CREATE DATABASE IF NOT EXISTS `{db_name}` DEFAULT CHARSET utf8mb4")
cur.execute(f"USE `{db_name}`")
cur.execute("""
CREATE TABLE IF NOT EXISTS web_config (
id INT AUTO_INCREMENT PRIMARY KEY,
version VARCHAR(64) NOT NULL,
file_url VARCHAR(1024) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
""")
conn.commit()
finally:
conn.close()