本文主要记录在 SQLite / PostgreSQL 双 Provider 与数据迁移场景下,因 Guid 文本大小写 引发的乐观锁失败、登录外键失败与进度上报 500 如何识别和收口。根因在 SQLite TEXT 等值比较大小写敏感,而不是业务权限本身。

同一根因会以不同表面症状出现:刮削批量写库报并发冲突;PG→SQLite 切库后无法登录;seek / pause 后客户端显示「认证失败 HTTP 500」(实际可能是进度 upsert 踩了 FK)。

两类 Guid 列

类型代表列存储读写约定
并发令牌实体上的 ConcurrencyTokenSQLite TEXT / PG varchar应用写出固定 小写 D 格式
主键 / 外键Users.IdAuthSessions.UserIdSQLite 多为 TEXT;PG 为 uuid文本落库也应统一小写

当前约定:所有 作为文本落库 的 Guid 使用小写 8-4-4-4-12Guid.ToString("D"))。乐观锁 UPDATE ... WHERE ConcurrencyToken = @original 比较的是 provider 侧字符串,库内必须与 converter 一致。

需要注意:PostgreSQL 上用户主键是 uuid 时,一般 不会 以「大小写不一致导致 FK 失败」的形式出现;ConcurrencyToken 仍是 varchar,乐观锁同样大小写敏感。

原因分析

SQLite TEXT 大小写敏感

"ABC...""abc..." 是两个不同值。ConcurrencyToken 与 SQLite 上的 Guid 键都以文本存储。

应用写出小写,ADO 默认绑参曾大写

  • EF 的 Guid 字符串 converter 使用 ToString("D").NET 固定小写
  • Microsoft.Data.SqliteGuid 参数绑定为 大写 TEXT(当前版本无稳定连接串开关可改)。
  • 库内 Users.Id 小写、插入会话仍绑大写 UserId → FK 失败。
  • 库内 ConcurrencyToken 大写、更新 WHERE 用小写 → 乐观锁失败。

历史迁移曾强制大写

有的迁移路径曾把写入 SQLite 的 Guid 统一 ToUpperInvariant(),动机是对齐 ADO 默认大写绑参。结果是:

  • 主键暂时与大写绑参「对上了」
  • ConcurrencyToken 也变大写 → 与 converter 小写冲突 → 扫描 / 刮削并发更新失败

收口方向是 全链路统一小写,而不是再在迁移里抬成大写。

raw ADO 绕过 EF 拦截器

若进度上报等路径用 GetDbConnection().CreateCommand() 直接执行 SQL,不会经过 EF 命令管线拦截器。此处若仍 DbType.Guid

  1. 绑出大写 TEXT UserId
  2. 库内 Users.Id 已是小写
  3. SQLite FK 失败 → 未处理异常 → HTTP 500
  4. 客户端可能误读成「认证失败」

硬规则:凡 SQLite 上对 Guid TEXT 列做 raw ADO 绑参,必须显式小写字符串 + DbType.String,禁止假设拦截器会兜底。

排查流程

先确认真正连哪套库

开发机 systemd 常通过环境变量覆盖 JSON 配置:

pid="$(systemctl show -p MainPID --value <service-name>)"
tr '\0' '\n' < "/proc/${pid}/environ" | rg '^Database__'

期望:Database__Provider 与健康检查里的 provider 一致;SQLite 时 Data Source 指向预期文件。

核对 ConcurrencyToken 是否全小写(SQLite)

DB="/path/to/app.db"

sqlite3 -header -column "$DB" "
SELECT 'MediaItems' AS t, COUNT(*) AS n,
  SUM(CASE WHEN ConcurrencyToken = lower(ConcurrencyToken) THEN 1 ELSE 0 END) AS lower_n,
  SUM(CASE WHEN ConcurrencyToken GLOB '*[A-F]*' THEN 1 ELSE 0 END) AS upper_hex
FROM MediaItems;
"
  • lower_n = nupper_hex = 0 → 形态正常
  • upper_hex > 0 → 库内仍有大写 token,乐观锁容易失败

核对 Users.Id 与会话 FK(SQLite)

sqlite3 -header -column "$DB" "
SELECT COUNT(*) AS n,
  SUM(CASE WHEN Id = lower(Id) THEN 1 ELSE 0 END) AS lower_n,
  SUM(CASE WHEN Id GLOB '*[A-F]*' THEN 1 ELSE 0 END) AS upper_hex
FROM Users;
"

sqlite3 "$DB" "
PRAGMA foreign_keys = ON;
SELECT COUNT(*) AS orphan_auth
FROM AuthSessions a
LEFT JOIN Users u ON u.Id = a.UserId
WHERE u.Id IS NULL;
"
库内 Users.Id应用绑参症状
全大写已改小写 interceptor新登录可能 FK 失败
全小写无 interceptor / 仍绑大写新登录可能 FK 失败
混写任意孤儿会话、间歇失败

PostgreSQL 侧

ConcurrencyToken 仍建议核对是否混有大写 hex。Users.Id 若是 uuid不要 按 SQLite TEXT 方式做 lower(Id) 数据修复。

处理办法

按优先级:

  1. 代码对齐:converter 小写;SQLite 注册参数 interceptor;迁移工具写小写;raw ADO 显式小写。
  2. 发布对齐:确认运行中的二进制已包含 interceptor(旧 publish 包会「代码看起来修了、进程还是旧的」)。
  3. 数据清洗(SQLite 历史脏数据):停服、备份后,把相关 TEXT Guid 列 lower() 到统一形态;再测登录与一次扫描写库。

不要在未备份时直接改 live 库。PostgreSQL 生产实例若主键已是 uuid 且仅 ConcurrencyToken 脏,应只修 varchar 列,避免误操作 uuid。

验证

检查期望
登录创建会话无 FK 失败
刮削 / 扫描批量更新无持续乐观锁风暴
播放进度 seek / pause / stop无因 Guid 绑参导致的 500
SQLite 抽检Guid 文本列 upper_hex = 0

相关阅读

注意事项

  • 症状在 SQLite 上最锋利;不要把「PG 上没复现」当成「没有大小写问题」。
  • 客户端报「认证失败」时,先看后端是否其实是进度 / 会话写库 500。
  • 双 Provider 项目里,凡新增 raw SQL 绑 Guid,都要按 SQLite 敏感比较再审一遍。