228d481211
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 改按请求复用连接
175 lines
5.9 KiB
Python
175 lines
5.9 KiB
Python
"""
|
||
数据库连接与初始化
|
||
|
||
请求级连接复用: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)插入一个超管。
|
||
管理后台早已迁到 Java,users 表结构由 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()
|