Compare commits

...

3 Commits

Author SHA1 Message Date
浅念 682249a3fd perf(management): message补无渠道前缀索引——修驾驶舱channel=all超时34s→1.3s
该回未回决赛圈点查(owner+talker定位末条消息)在channel=None时无渠道
前缀条件,现有索引全以channel打头用不上,服务器100万行实测dashboard
34~37s撞前端15s超时(ECONNABORTED);补(owner,talker,timestamp)索引
后实测1.3~1.8s。服务器已先行手工建索引止血,本迁移幂等兜底对齐。
模型+迁移双声明防autogenerate反DROP
2026-09-10 15:26:25 +08:00
浅念 80b721b84f fix(groups): 字节序排序按方言——MySQL CAST AS BINARY / SQLite COLLATE BINARY
collate("BINARY") 被 SQLAlchemy 反引号成标识符致 MySQL 500;SQLite 的
CAST AS BINARY 语义不符(排序键失效退化 room_id 序,测试失去判别力)。
按 session.bind.dialect 分支,两库都用真字节序。

726 测试绿。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 14:37:07 +08:00
浅念 acd6997bf0 fix(groups): 排序终判——owner 二进制字符串序(云客逐位吻合),弃 list_order
上版按 list_order 排不对:云客员工列表序 ≠ 群统计页员工序。真机对照
切换点员工终判:群统计员工顺序 = owner(微信号/wxid 混存)的**区分大小写
二进制字符串升序**(Nancyjade < csf7169708 < wxid_01gv…,大写在小写前,
逐位吻合),组内 room_id 字符串升序。COLLATE BINARY 双库通用,且无需
JOIN wechat_account。

726 测试绿。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 14:23:43 +08:00
4 changed files with 71 additions and 15 deletions
+15 -11
View File
@@ -102,18 +102,22 @@ async def list_groups(
if conditions: if conditions:
list_stmt = list_stmt.where(*conditions) list_stmt = list_stmt.where(*conditions)
if (channel or "wechat") == "wechat": if (channel or "wechat") == "wechat":
# 云客「微信群统计」同款排序(2026-09-10 真机判别):员工分组—— # 云客「微信群统计」同款排序(2026-09-10 真机终判):owner_account_id
# 按云客员工列表序(wechat_account.list_order,NULL 垫底按 wx_id)、 # (=微信号/wxid 混存,如 Nancyjade < csf7169708 < wxid_01gv…——大写
# 组内按 room_id(chatRoomId)字符串升序。WA 保持最近活跃优先。 # 在小写前=区分大小写)**二进制字节序升序**分组,组内 room_id 字符串
# 升序。字节序按方言:MySQL 用 CAST(col AS BINARY)(collate("BINARY")
# 会被 SQLAlchemy 反引号成标识符);SQLite 用 COLLATE BINARY(其 CAST
# AS BINARY 语义不符)。WA 保持最近活跃优先。
dialect = session.bind.dialect.name if session.bind is not None else ""
if dialect == "mysql":
owner_order = sa.literal_column(
"CAST(chat_group.owner_account_id AS BINARY)"
).asc()
else:
owner_order = ChatGroup.owner_account_id.collate("BINARY").asc()
list_stmt = ( list_stmt = (
list_stmt.outerjoin( list_stmt.order_by(
WechatAccount, owner_order,
WechatAccount.wx_id == ChatGroup.owner_account_id,
)
.order_by(
WechatAccount.list_order.is_(None),
WechatAccount.list_order.asc(),
ChatGroup.owner_account_id.asc(),
ChatGroup.room_id.asc(), ChatGroup.room_id.asc(),
) )
.limit(page_size) .limit(page_size)
+12
View File
@@ -537,6 +537,18 @@ class Message(Base):
"ix_message_fetched_at", "ix_message_fetched_at",
"fetched_at", "fetched_at",
), ),
# 该回未回决赛圈点查(owner+talker 定位末条消息)在 channel=None
# (全平台计数——管理驾驶舱 action_counts 复用)时无渠道前缀条件,
# 上面全部以 channel 打头的索引用不上,逐候选退化扫表+filesort:
# 2026-09-10 服务器实测 dashboard 34~37s 撞前端 15s 超时
# (ECONNABORTED);补无渠道前缀索引后 1.3~1.8s(100 万行在线建
# 索引 49s)。「模型+迁移双声明」防 alembic autogenerate 反 DROP。
Index(
"ix_message_owner_talker_time",
"owner_account_id",
"talker_id",
"timestamp",
),
) )
@@ -0,0 +1,41 @@
"""message 补无渠道前缀索引 (owner_account_id, talker_id, timestamp)
Revision ID: d8e9f0a1b2c3
Revises: c9d0e1f2a3b4
Create Date: 2026-09-10
背景:该回未回决赛圈点查在 channel=None(管理驾驶舱 action_counts)时
无渠道前缀条件,现有索引全部以 channel 打头用不上——服务器 100 万行
实测 dashboard 34~37s 撞前端 15s 超时;索引落地后 1.3~1.8s。
幂等:sa.inspect 先查,存在即跳过(服务器已先行手工建同名索引,本迁移
兜底对齐 schema 版本)。「模型+迁移双声明」防 autogenerate 反 DROP。
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "d8e9f0a1b2c3"
down_revision: str | Sequence[str] | None = "c9d0e1f2a3b4"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
_IDX = "ix_message_owner_talker_time"
def _has_index(bind) -> bool:
insp = sa.inspect(bind)
if not insp.has_table("message"):
return False
return _IDX in {ix["name"] for ix in insp.get_indexes("message")}
def upgrade() -> None:
if not _has_index(op.get_bind()):
op.create_index(_IDX, "message",
["owner_account_id", "talker_id", "timestamp"])
def downgrade() -> None:
if _has_index(op.get_bind()):
op.drop_index(_IDX, table_name="message")
+3 -4
View File
@@ -64,10 +64,9 @@ async def _seed(session: AsyncSession) -> None:
async def test_default_list_returns_all_ordered_desc(make_client): async def test_default_list_returns_all_ordered_desc(make_client):
"""No filters -> 3 groups;微信排序=员工分组(list_order NULL 按 owner asc) """No filters -> 3 groups;微信排序=owner 二进制字符串升序分组+room_id
+组内 room_id 字符串升序(云客群统计同款,2026-09-10)。 升序(云客群统计同款,2026-09-10 终判:区分大小写,大写排小写前)。
seed 无 wechat_account 行 → list_order 全 NULL → owner 字符串序: owner 序:wa_owner_1 < wx_owner_1 < wx_owner_2 → [wa_room_x, room_a, room_b]。
wa_owner_1 < wx_owner_1 < wx_owner_2 → [wa_room_x, room_a, room_b]。
""" """
client = await make_client(_seed) client = await make_client(_seed)
r = await client.get("/api/groups") r = await client.get("/api/groups")