feat(deploy): 完成数据迁移与MySQL全链路(14表698k消息100%一致+5方言补丁+bind mount)
- migrate_sqlite_to_mysql.py: 逐表清空重导(幂等),复用repos强转,JSON列 解析,67万消息按msg_id键集分批,467s跑完14表全一致 - 修4个MySQL实测bug: NULLS LAST语法(compat.py编译补丁)、message.text TEXT→MEDIUMTEXT(64KB字节上限,实测20.8万字符车源清单)、URL列 String(1024)→Text(分享链接超1KB)、垃圾超长member_id源头清理 - compose: media改bind mount(不占C盘Docker虚拟磁盘,实测2.5GB卷把 C盘撑爆引擎500);mysql不映射宿主机3306 - 前端Dockerfile: npm ci→install(本地/容器npm版本锁漂移) - 五服务实测:nginx→backend→MySQL全链路返回真实数据(客户6859/A**杰25条 /搜索56条),媒体49536文件bind mount直读 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ from sqlalchemy.ext.asyncio import (
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
from app.config import get_settings
|
||||
from app.db import compat # noqa: F401,I100 — 注册 MySQL NULLS LAST 编译补丁(导入即生效)
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
"""MySQL 方言兼容补丁(2026-08-22 部署实测发现)。
|
||||
|
||||
MySQL 不支持 ``NULLS LAST`` / ``NULLS FIRST`` 语法(SQLite/PostgreSQL 有)。
|
||||
本项目所有 ``ORDER BY x DESC NULLS LAST`` 在 MySQL 下语义天然一致——
|
||||
MySQL 的 DESC 排序 NULL 本来就排最后——所以编译时直接剥掉该子句。
|
||||
ASC + NULLS LAST 的组合如果将来出现,需要改为 ``ORDER BY x IS NULL, x``
|
||||
(MySQL 惯用法),当前代码库没有这种用法。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy.ext.compiler import compiles
|
||||
from sqlalchemy.sql.elements import UnaryExpression
|
||||
|
||||
|
||||
@compiles(UnaryExpression, "mysql")
|
||||
def _mysql_strip_nullslast(element, compiler, **kw):
|
||||
"""MySQL: 剥掉 NULLS LAST/NULLS FIRST。
|
||||
|
||||
``sa.nullslast(x.desc())`` 编译为 ``UnaryExpression(modifier=NullsLast)``;
|
||||
MySQL DESC 下 NULL 天然在末尾(语义等价),直接输出内层表达式。
|
||||
非 NULLS 修饰的 UnaryExpression(如普通 -x)不受影响——modifier 为 None
|
||||
时走原编译路径。
|
||||
"""
|
||||
from sqlalchemy.sql.operators import nulls_last_op
|
||||
|
||||
if element.modifier is not nulls_last_op:
|
||||
return compiler.visit_unary(element, **kw)
|
||||
return compiler.process(element.element, **kw)
|
||||
@@ -45,6 +45,7 @@ import datetime as dt
|
||||
from typing import Any
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.mysql import MEDIUMTEXT # noqa: E402
|
||||
from sqlalchemy import BigInteger, Boolean, DateTime, Index, Integer, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy.types import JSON
|
||||
@@ -358,10 +359,14 @@ class Message(Base):
|
||||
type_raw: Mapped[int | None] = mapped_column(
|
||||
Integer, nullable=True
|
||||
) # original upstream type code (e.g. WeChat 1=text, -2=divider)
|
||||
text: Mapped[str | None] = mapped_column(Text, nullable=True) # content/text
|
||||
file_url: Mapped[str | None] = mapped_column(String(1024), nullable=True)
|
||||
file_th_url: Mapped[str | None] = mapped_column(String(1024), nullable=True)
|
||||
file_hd_url: Mapped[str | None] = mapped_column(String(1024), nullable=True)
|
||||
# MEDIUMTEXT(16MB):MySQL TEXT 上限 64KB 字节,实测车源清单消息最长
|
||||
# 20.8万字符(≈800KB utf8mb4)超限(2026-08-21 迁移踩坑)
|
||||
text: Mapped[str | None] = mapped_column(
|
||||
Text().with_variant(MEDIUMTEXT(), "mysql"), nullable=True
|
||||
) # content/text
|
||||
file_url: Mapped[str | None] = mapped_column(sa.Text, nullable=True) # 分享链接可超1KB(实测,2026-08-21)
|
||||
file_th_url: Mapped[str | None] = mapped_column(sa.Text, nullable=True)
|
||||
file_hd_url: Mapped[str | None] = mapped_column(sa.Text, nullable=True)
|
||||
md5: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||
md5_th: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||
md5_hd: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||
@@ -428,7 +433,7 @@ class Media(Base):
|
||||
media_type: Mapped[str | None] = mapped_column(
|
||||
String(32), nullable=True
|
||||
) # image|voice|video|file|...
|
||||
original_url: Mapped[str | None] = mapped_column(String(1024), nullable=True)
|
||||
original_url: Mapped[str | None] = mapped_column(sa.Text, nullable=True)
|
||||
original_th_url: Mapped[str | None] = mapped_column(String(1024), nullable=True)
|
||||
original_hd_url: Mapped[str | None] = mapped_column(String(1024), nullable=True)
|
||||
local_path: Mapped[str | None] = mapped_column(String(1024), nullable=True)
|
||||
|
||||
Reference in New Issue
Block a user