42 KiB
IM 后端问题清单
2026-09-14 全面复核:本次对现有33条逐条重新核实(能实测的实测,能读代码的读代码,均标注核实方式),并把此前散落在各处(docs/IM-后端问题清单-2026-09-04.md、历次前端重写工程记录)但从未收录进本清单的问题一并归档,同时确认了几条此前记录、后来查明其实是前端自己的bug(已在下方"已排除"列出,不再作为后端问题)。这次复核期间发现后端正在把数据库从单机MySQL迁移到新k3s集群的Percona XtraDB Cluster(PXC)——server/meetingim.conf/tinode.conf的MySQL/Redis/Kafka地址已经从旧VM改成新集群地址,代码里也新增了专门处理PXC兼容性问题(命名锁、自增ID越界)的分支——这是今天新发现的几个MySQL相关问题(尤其是周期性的驱动层连接损坏)很可能与之相关的重要背景,供后续排查参考。
目录
P0 · 安全漏洞 / 核心功能完全不可用
暂无。
P1 · 数据完整性 / 权限失效 / 主要功能缺陷
| 编号 | 接口 | 一句话 |
|---|---|---|
| 问题36 | POST /messages/forward |
并发转发下响应说"失败"其实已经真实写入——2026-09-14用干净的12并发复测,仍有2/3的"失败"响应对应真实落库的消息,最近一次代码改动(926b4bb)改善了部分逻辑但未解决根本问题 |
| 新增80 | Bearer Token鉴权(影响所有需要登录态的接口) | 同一个token并发请求,20-45%概率随机返回401 AUTH_REQUIRED,鉴权层把"token真无效"和"Redis/DB瞬时报错"统一处理成同一个错误码且完全不打日志,无法从现有日志区分具体原因 |
| 新增81 | GET /v0/file/s/{id}(媒体文件服务) |
查询数据库里不存在的列mtg.recorder_user_id,25分钟观测窗口内命中这条SQL的请求243次全部失败 |
P2 · 数据一致性 / 体验缺口 / 校验缺失
| 编号 | 接口 | 一句话 |
|---|---|---|
| 问题25 | POST /messages/forward、/favorites/forward |
带附言转发时 head 声明类型与 content 实际结构对不上 |
| 问题27 | GET /reports/daily |
status 参数不做取值校验,传任何值都返回200+空列表 |
| 问题30 | GET /system-notifications |
summary/action结构不一致,且会议响应事件被写成两条重复通知 |
| 问题33 | GET /reports/daily |
permissions 四个字段恒为同一组硬编码值,与用户/参数无关 |
| 问题35 | GET /users/{id}/profile |
设了备注之后拿不到对方的原昵称(GET /friends一侧已修复) |
| 新增3 | PATCH /conversations/{topic}/settings |
单聊置顶没有批量查询接口,会话列表永远看不出置顶效果 |
| 新增8 | GET /friends |
完全不感知拉黑关系,与另外两个接口表现不一致 |
| 新增15 | POST /meetings/{id}/cancel |
完全不校验 version,乐观锁摆设 |
| 新增24 | GET /groups/{topic}、/settings |
群成员数存在两套互相独立、可永久失配的计数器 |
| 新增25 | 原生WS {pub} |
link类型消息协议层可构造,但没有真实用户可从UI触达的发送路径 |
| 新增30 | GET /settings/youth-mode |
开启后不对任何功能做实际限制,enforced 恒为硬编码false(前端已移除此功能,仅保留记录完整性) |
| 新增31 | POST /reports/abuse |
不校验 requires_description,可对必填原因提交空描述 |
| 新增32 | POST /system-notifications/{id}/read |
对已读通知重复标记,响应回显请求值但真实存储值不变 |
| 新增34 | POST /tenant-applications |
统一社会信用代码无去重机制,可无限重复提交 |
| 新增41 | POST /groups/{topic}/mutes |
无角色层级保护,管理员可禁言群主、任何角色可自我禁言 |
| 新增47 | POST /v0/file/u/ |
真实文件上传上限是32MB而非声明的100MB,且从未用于前端预校验 |
| 新增49 | POST /reports/daily、/monthly |
创建接口对日期只校验格式不校验范围,年份0000/2099均可创建 |
| 新增51 | POST /meetings |
不校验 attendee_user_ids 是否真实存在,可创建任意规模"幽灵参会人" |
| 新增54 | 日报/月报SUBMIT | 不校验必填内容,可提交完全空白报告 |
| 新增56 | POST /settings/youth-mode/enable |
policy_version 不做版本校验,传任意值即被无条件接受(前端已移除此功能) |
| 新增76 | GET /app/releases/latest?platform=web |
platform 只接受ios/android,web端完全不支持检查更新 |
| 新增77 | GET /groups/{topic}/announcements/latest |
响应没有已读人数字段,"已读N/M"这个设计要求的功能无法实现 |
| 新增78 | GET /topics/{topic}/messages/search、收藏/通话记录的相关字段 |
发送者/对方user_id被裁掉usr前缀,或返回内部hash而非可寻址id |
| 新增82 | cache group settings(内部缓存,影响群相关读接口的性能与正确性) |
群设置缓存反序列化失败,高频出现(242次/25分钟),覆盖大量不同群 |
| 新增83 | media serve(文件服务) |
死锁(1213/40001)没有捕获重试,命中的请求直接失败 |
| 新增84 | 内部后台任务"media gc" | 周期性(约每50秒一次)触发MySQL驱动层连接损坏("busy buffer"/协议序号错乱),与PXC迁移的时间点吻合,怀疑相关 |
| 新增85 | GET /groups/{topic}/members?role=admin 等按查询参数区分的缓存 |
写操作(如提升管理员)后,特定查询参数组合的结果有90秒以上的缓存滞后 |
| 新增86 | GET /calls |
响应字段结构与协议文档完全不同(无peer对象、无incoming布尔值、时间戳拆成4个字段、topic是内部hash) |
| 新增87 | 群二维码 POST /groups/{topic}/qrcodes vs POST /qrcodes/resolve |
铸造接口用group_snapshot字段名,解析接口对同样的数据用group字段名,两个接口不一致;解析接口另有一个协议文档完全没提过的status字段 |
| 新增88 | GET /topics/{topic}/messages/search |
部分明显应该命中的关键词搜索返回空结果(搜索召回不稳定,2026-09-14复测比更早的记录有改善但未完全解决) |
| 新增89 | im_file_review_tasks / 养成动态图片审核 |
IM 仅创建文件审核任务,没有执行 Worker,所有带图动态都会永久停在 reviewing/pending |
| (无编号,功能缺口非bug) | 无 | 整份协议文档没有"移出/踢出群成员"这个操作,03-group.dc.html里"移出"按钮因此没有对应接口可用——确认是协议本身没设计这个功能,不是遗漏 |
| (无编号,可能是设计而非缺口) | 无 | 无手机号查询/换绑接口、无登录设备列表接口;POST /security/challenges这个"敏感操作二次验证"机制目前只服务青少年模式两个场景,没有覆盖改密码等场景 |
本次复核确认已修复(不再是问题,供归档参考)
以下条目在2026-09-14用真实账号实测或直接读当前代码确认已经修复,从清单中移除:
- 问题22(无登出/Token撤销接口):已修复。
POST /v1/im/auth/logout现在是真实可用的接口(之前误判404的路径是没有走对,正确路径是这个)。实测:新token连续3次200 → 调用logout返回200 → 同一token之后5次全部401,确认真实吊销,不是假成功。 - 问题28(
purpose=user_avatar被拒):已修复。实测提交真实PNG文件,200成功。 - 新增16(总经理信箱超长内容返回500):已修复。代码确认
serveIMExecutiveMailboxSubmissions现在显式捕获ErrPolicy并返回400 VALIDATION_FAILED,不再落到default的500分支。 - 新增17(
writeIMRelationError把ErrNotFound错误映射成MALFORMED_REQUEST):已修复。代码确认现在ErrNotFound正确映射到404+RESOURCE_NOT_FOUND,reason与message一致。 - 新增27(工作台
unread_count恒为0):已修复(2026-09-10记录"只接入bootstrap,workbench本身还没接",现在确认serveIMWorkbench内部也调用了populateWorkbenchModuleUnreadCounts)。注:目前只有"会议"模块会计算真实值,其余模块代码上仍是原始SQL的0值,但这正好是原始bug报告实测覆盖的场景,已修复。 - 新增42(群公告按id查询恒定返回最新公告):已修复。代码确认现在正确解析路径里的
announcement_id并调用GetAnnouncement按id查询,不再无视这个参数。 - 新增43(群文件登记接口无长度/存在性校验):已修复。代码确认现在对
file_name(255)/mime_type(128)做长度校验,对file_size做负数校验,且会调用store.Files.Get验证file_id真实存在。 - 新增45(收藏/搜索/宫格分页off-by-one永久丢失边界记录):已修复。实测:24条真实收藏,用limit=10分3页拉取(10+10+4),合并后24条一条不少、无重复。
本次复核确认仍是历史误判(从未是真正的后端bug,不应再作为后端问题提出)
以下条目此前曾被记录为"疑似后端bug",后来(多为9月上中旬的复核)证实其实是前端自己调用错了接口/传错了参数,backend行为一直是对的——不要把这几条再当成后端问题反馈:
GET .../groups/{topic}/members/{user_id}/profile查自己恒定400——真相:有专门的members/me/profile接口可以正确查询,前端读操作一直在用错误的通用by-id接口,写操作反而一直用对了。已切换到正确接口,问题消失。POST .../groups/{topic}/mutes/batch恒定405——真相:真实的批量禁言接口是/mutes(不带/batch后缀),前端一直在调一个不存在的路径。已改用正确路径。GET /users/{id}/profile对好友usrW9ijv236hHA恒定403——真相:响应体里其实带着message:"profile is private",是对方自己设置了资料保护,属于设计内行为,不是bug。PATCH .../groups/{topic}/member-settings(置顶/免打扰)恒定400——真相:这个接口的请求体结构里根本没有version字段,后端解码器对未声明字段严格拒绝;前端一直在照抄别的接口的乐观锁写法多传了一个version字段。已去掉,问题消失。- 群设置
governance字段"被后端写死成默认值"——这是一次静态代码审查的猜测,当场用真实接口交叉验证后证明是错的:/settings和/governance返回一致,写入后能正确反映在读接口上。从未真正成立过。
问题 36(P1,2026-09-14复测:仍未修复,最近一次代码改动效果有限):并发转发到同一个会话时,响应说"失败"的条目里相当一部分其实已经成功写入,重试会造成真实内容重复
证据强度:✅ 实测确认(多轮独立复现,含2026-09-14最新一轮干净复测)+代码确认
2026-09-14复测方法与结果:git pull确认server/kafka_message_persist.go在9月11日有过一次专门的修复提交(926b4bb fix im forwarding and message list regressions),代码上看这次改动新增了Pending状态(区分"确定成功"/"确定失败"/"不确定,可能已经发出"三种情况,而不是原来的非成功即失败)。但不能只信代码改动就下结论,实际用真实群(grpDclMebv3MuI)、真实消息(seq 135)做了一次干净的12并发转发测试(每个请求用独立的幂等键,模拟"用户看到失败提示后重新点击"的真实场景):
- 12个请求里7个报告"sent"成功(seq 136/137/138/139/140/142/143),3个报告"failed"(
INTERNAL_ERROR),2个命中已知的401鉴权问题(新增80,未到达转发逻辑)。 - 测试前群里搜索"测试"能查到的最大seq是135;测试后变成130-144连续9个新seq(136-144)。
- 9个真实新消息 vs 7个确认成功——多出来的136-144里的141和144,对应的正是那3条报"failed"里的2条,它们实际上真的写入成功了,只是响应告诉客户端"失败"。
结论:本质问题没有解决——响应仍然不可信,"failed"不能被客户端安全地当作"真的没发生"来处理,重试依然会造成真实重复。9月11日的那次改动可能改善了部分场景(比如更细粒度的pending状态跟踪),但没有堵住这个具体的漏洞。
根因(此前已逐行核对,本次未见改变):开启Kafka时,saveMessageMaybeKafka先原子预留seq,再异步把消息发布到Kafka——如果produce调用本身报错(例如含糊的ack超时),HTTP handler判定失败并返回给客户端,但已预留的seq和可能已经在网络层提交成功、只是没收到ack的Kafka消息都回滚不了,只要Kafka消费者最终异步写入,"失败"响应背后就会真实落库。
接口:POST /v1/im/messages/forward
前端现状:批量转发的失败提示照原样展示后端返回的status/reason,无法感知也无法规避"响应可能撒谎"这个问题,需要后端修正响应准确性(例如:明确区分"确定失败可安全重试"和"结果不确定,建议先检查再决定是否重试"两种状态,而不是都归为同一种"failed")。
新增 80(P1,2026-09-14新发现):Bearer Token鉴权在并发请求下随机出现401,且无法从日志定位具体原因
证据强度:✅ 实测确认(自己独立复现,两个不同接口各自独立测试)+代码确认(完整调用链,含具体文件行号)
现象:用同一个有效token对GET /v1/im/groups/{topic}或GET /v1/im/me连续发起20个并发请求,大约15%-65%会随机返回401 {"code":30043,"reason":"AUTH_REQUIRED"},比例不稳定(同一token连续测两轮,一轮5/20失败,几秒后另一轮13/20失败)。对照组:同样并发打POST /v1/im/tenants/resolve(只需要API Key,不需要bearer token)20/20全部成功,证明问题只出在bearer token鉴权这一层,不是网络或网关层面的问题。
排除的可能性:进入当前所有正在运行的3个im-server Pod内部,对AUTH_TOKEN_KEY/MYSQL_DSN/REDIS_ADDR/REDIS_PASSWORD/TENANT_REGISTRY_DB/API_KEY_SALT/UID_ENCRYPTION_KEY共7个关键配置项,在每个Pod自己进程内部单独计算哈希值做比对(不在外部解密Secret),3个Pod完全一致——排除"多实例配置不一致"这个最初怀疑的方向;3个Pod是同一批次创建、同一个ReplicaSet,没有游离的旧实例。
根因(读当前im_be源码确认,文件行号):GET /v1/im/groups/{topic}等接口的鉴权链条是 authenticateIMHTTPBearer(server/im_http.go:7093) → resolveIMHTTPBearerToken(server/im_http.go:7111) → (*authenticator).Authenticate(server/auth/token/auth_token.go:103)。这个函数内部,token签名错误、过期、serial_num不匹配、Redis撤销状态查询报错(IsLoginTokenRevoked)、按租户路由的DB鉴权版本号查询报错(GetAuthVersion/TenantAdapterFor)——所有这些情况的错误全部被统一转换成同一个types.ErrPermissionDenied,最终一律报401,且这条失败路径上完全没有任何日志输出,导致即使线上真实发生了这类失败,事后也无法从日志区分具体是哪一种原因触发的。
其中GetAuthVersion背后的TenantAdapterFor(server/db/mysql/adapter.go:469)使用每个进程自己独立维护的懒加载租户连接缓存,某个租户如果还没被当前进程缓存过,一波并发请求会各自独立地去开一条新数据库连接并Ping(),这种"并发抢着建连接"的场景比"某个pod配置固定错误"更能解释"20个并发里随机几个失败、且失败比例还会波动"这个现象。
支持证据:查了同一时间窗口的k8s事件,3个pod中有2个在过去1小时内真实触发过健康检查失败(Unhealthy: context deadline exceeded,探测超时只给了1秒);同时机器上2/3节点内存占用在69%-81%(这是个多个服务共享的测试集群)。另外,MySQL的aborted_connects计数器非零(13),跟"存在瞬时连接失败"这个方向一致,但不是决定性证据。
重要背景:这次复核时发现后端仓库刚经历一次数据库基础设施迁移——server/meetingim.conf/tinode.conf里的MySQL/Redis/Kafka地址从旧VM换成了新k3s集群的PXC(Percona XtraDB Cluster)地址,且代码里新增了isPXCStrictModeNamedLockError/isIDOutOfRange两个专门处理PXC兼容性问题的错误分支,说明这个迁移目前还在持续排查兼容性问题的阶段。这个401问题的时间点与这次迁移吻合,虽然鉴权代码本身没有直接调用命名锁,但不排除是同一次基础设施变动带来的连锁不稳定,建议后端一并排查。
接口:影响所有需要bearer token鉴权的接口(穷举测试了GET /v1/im/groups/{topic}、GET /v1/im/me两个,机制上应该影响全部同类接口)
给后端的建议:(1)在这条失败路径上补充日志,区分"token确实无效"和"内部依赖(Redis/DB)报错"两种情况,不要两者都报同一个401;(2)审视TenantAdapterFor懒加载连接的并发安全性,避免同一租户的并发请求各自重复建连接。
新增 81(P1,2026-09-14新发现):文件服务查询数据库里不存在的列,几乎每次命中都失败
证据强度:✅ 实测确认(真实运行日志,25分钟窗口内3个pod合计243次)
现象:media serve(文件服务,/v0/file/s/{id})报错Error 1054 (42S22): Unknown column 'mtg.recorder_user_id' in 'where clause',25分钟的观测窗口里3个pod合计出现243次,是这次复核发现的所有问题里频率最高的一个——几乎所有命中这条SQL路径的真实请求都会失败。
推测:列名前缀mtg大概率是"会议(meeting)"相关表,可能是代码引用了一个数据库里还不存在的列(缺一次迁移),也可能是列名本身写错了。不确定具体原因,需要后端自己确认。
接口:GET /v0/file/s/{id}(原生Tinode大文件下载,命中会议关联文件时)
基础设施侧的态度:这类"代码查询数据库结构对不上"的问题,本身可能是缺一次迁移,但我们不建议自己去手动改数据库列(不清楚正确的类型/默认值/是否有对应的后端代码逻辑依赖这个列的具体语义),必须由后端确认。
问题 25(P2,最常见路径已修故降级):带附言转发/收藏转发时,head 声明的类型与 content 的实际结构对不上
证据强度:✅ 实测确认(四条路径全部真实构造验证)
现象:逐条转发带附言、收藏转发带附言两条路径,head仍声明原消息类型(如image/text),但content换成{forwarded,comment,snapshot}等包装对象,客户端按head类型解析content会得到空白/无效内容。逐条转发不带附言这条最常见路径已经修好(content原样透传)。
期望:附言作为独立的一条文本消息发送(对接文档本来就是这么描述的),转发的那条消息仍然原样透传原content。
接口:POST /v1/im/messages/forward;POST /v1/im/favorites/forward
问题 27(P2):status 参数不做取值校验,传任何值都返回200+空列表
证据强度:✅ 实测确认(2026-09-14重测,传status=totally_bogus_value_xyz依然200+items:[],未变)
现象:传完全不存在的status值也返回200,客户端无法区分"确实没数据"和"根本不支持"。
接口:GET /v1/im/reports/daily?status=(月报同构)
问题 30(P2):系统通知的 summary 要么是空串、要么是机器码,action 的结构因来源而异,且会议响应事件会被写成两条重复通知
证据强度:✅ 实测确认(三种summary情况+两条重复通知具体案例)+代码确认(重复通知根因)
现象:summary有时空串、有时机器码(如accepted/rejected)、有时真实数据;action是自由结构,不同来源放的键完全不同;会议响应更新会被两条独立代码路径各写一条内容几乎相同、结构不同的重复通知。
根因(重复通知):serveIMMeetingRespond同时调用受开关imReliableHTTPEventsEnabled控制的publishIMMeetingEvent(生成{meeting_id}形态)和无条件执行的publishIMWorkbenchAndAudit(经Kafka产生{module,resource_id}形态),该开关对标准请求头调用恒为true。
接口:GET /v1/im/system-notifications
前端现状:summary能在已知事件码表里查到就替换成中文,查不到就原样显示;跳转按钮按action里出现的键分派;重复通知没有做任何去重(不能安全启发式判断"内容相同就是重复")。
问题 33(P2):permissions 的四个字段在不同参数下恒为同一组值
证据强度:✅ 代码确认(2026-09-14重读源码,确认仍是硬编码map字面量,行号已随文件改动漂移到2614-2619行左右)
现象:can_create/can_remind_unsubmitted/can_view_managed/makeup_allowed_days换任何用户、scope、日期区间组合都恒为同一组值,因为响应体里这部分是Go代码写死的map字面量,完全不引用principal/reports/任何请求参数。
接口:GET /v1/im/reports/daily
问题 35(P2,GET /friends一侧已修复,范围收窄到GET /users/{id}/profile):设了备注之后,好友主页详情接口拿不到对方的原昵称
证据强度:✅ 实测确认(2026-09-14再次复核,问题依然存在)
现象:GET /v1/im/friends响应里带的嵌套peer对象(peer.display_name)能正确带对方的真实原始昵称,这一侧的诉求已经满足。但GET /v1/im/users/{id}/profile依然没有修:既没有remark字段,也没有独立的原昵称字段——保存备注后本地乐观更新正确显示新备注,但一旦触发这个详情接口的背景刷新重新拉取,会因为接口不带remark字段而把显示"冲"回"未设置",数据没有真的丢,只是这个详情页的显示会"看起来复原了"。
接口:GET /v1/im/users/{id}/profile(仍缺);对照已修复的GET /v1/im/friends
期望:GET /users/{id}/profile比照GET /friends已经落地的peer对象模式,补上对方的真实原始昵称字段。
新增 3(P2):单聊的"置顶聊天"没有任何办法让会话列表知道,因为没有批量查询会话设置的接口
证据强度:✅ 实测确认(对照群聊置顶排除法)
现象:群聊置顶写入订阅原生private字段能随meta同步下发;单聊置顶写入独立表且不写private字段,只有单条查询接口无批量接口,前端无法为几十上百个会话逐一查询,设置生效但用户看不出效果。
接口:PATCH /v1/im/conversations/{topic}/settings(写入);缺失批量查询接口
新增 8(P2):GET /friends 完全不感知拉黑关系,与另外两个接口表现不一致
证据强度:✅ 实测确认+代码确认
现象:拉黑后该好友仍显示relationship:"friend"且权限位全开,与GET /conversations/{topic}/context(正确显示blocked_by_me)、GET /users/{id}/profile(裸403)三者互不一致。
根因:friendListItemPayload及数据源types.FriendListItem从结构体到SQL全程未涉及黑名单表。
接口:GET /v1/im/friends;对照GET /v1/im/users/{id}/profile、GET /v1/im/conversations/{topic}/context
新增 15(P2):POST /meetings/{id}/cancel 完全不校验 version
证据强度:✅ 实测确认+代码确认(2026-09-14重读源码,serveIMMeetingCancel依然只解析Version int但从未在调用store.Meetings.Cancel时传入这个字段)
现象:故意传version:999也直接成功,与同资源my-response接口(版本校验正常409)形成对照——乐观锁形同虚设。
接口:POST /v1/im/meetings/{id}/cancel
新增 24(P2):群成员数存在两套互相独立、可永久失配的计数器
证据强度:✅ 实测确认(3个真实群交叉对照)+代码确认(2026-09-14重读源码确认未变)
现象:同一群同一时刻,GET /groups/{topic}与/settings显示的member_count,跟真实成员列表、qrcodes的group_snapshot可能不一致。
根因:业务层im_group_settings.member_count(只看im_group_members)与Tinode原生层im_topics.subcnt(只看im_subscriptions)是两套独立计数器,mergeGroupSettingsPayload(server/im_http.go:8012)无条件用业务层值覆盖了本来准确的subcnt值。
接口:GET /v1/im/groups/{topic};/settings;POST /groups/{topic}/qrcodes;GET /groups/{topic}/members
新增 25(P2,产品需求缺口):link类型消息——协议层面可构造,但没有任何真实用户可从webapp UI触达的路径
证据强度:✅ 实测确认(正反两方向均构造真实数据验证)
现象:WS{pub}可直接构造出content:{url:...}扁平结构被判定为link类型(可收藏可搜索),但im_webapp所有正常发送路径产出的content都不是这个形状,"我的收藏-链接"等tab对真实用户永远是空的。
接口:WS {pub}(消息发送),无特定REST路径
新增 30(P2):青少年模式开启后不对任何功能做实际限制,enforced 恒为硬编码false
证据强度:✅ 实测确认(2026-09-14重新实测,未变)+代码确认
现象:restriction_items三项(深夜免打扰/内容过滤/支付守护)的enforced恒为硬编码false,全仓库穷举确认没有任何消息发送/好友添加/群聊/支付相关接口会查询用户的青少年模式状态。
接口:GET /v1/im/settings/youth-mode
⚠️ 注:im_webapp已明确移除青少年模式这个前端功能(用户产品决策,非技术限制),这条对当前im_webapp已无实际影响,仅为完整性保留在清单里。
新增 31(P2):POST /reports/abuse 不校验 requires_description,可以对必填原因提交空描述
证据强度:✅ 实测确认+代码确认(2026-09-14重读源码确认未变)
现象:某原因码(如illegal/other)的requires_description为true,但提交该reason_code、description为空依然200成功——代码里只校验了description长度上限(500),从未检查对应reason_code是否要求必填。
接口:POST /v1/im/reports/abuse
新增 32(P2):系统通知已读接口对已有 read_at 的通知,响应会原样回显客户端提交的值,但真实存储值不变
证据强度:✅ 实测确认+代码确认(2026-09-14重读源码确认未变)
现象:对已有read_at的通知重复调用已读接口,响应200并原样回显本次提交的read_at(哪怕故意传明显不同值),但真实存储的read_at纹丝不动。
根因:TenantSystemNotificationMarkRead执行UPDATE...SET read_at=COALESCE(read_at,?),已非空时保留原值;serveIMSystemNotificationRead(server/im_http.go:3576)直接回显请求体局部变量readAt,从未回读真实值。
接口:POST /v1/im/system-notifications/{id}/read
新增 34(P2):企业注册申请接口对同一个统一社会信用代码没有任何去重/防重复提交机制
证据强度:✅ 实测确认+代码确认(2026-09-14重读源码确认未变)
现象:同一信用代码用三个不同幂等键连续提交三次,全部独立成功建单,与需求文档"应防止重复提交"矛盾。代码上serveIMTenantApplications直接调用CreateTenantApplication,没有先查询该信用代码是否已有pending申请。
接口:POST /v1/im/tenant-applications
新增 41(P2):群禁言接口没有任何角色层级保护——管理员可以禁言群主、任何角色都能自我禁言
证据强度:✅ 实测确认(代码逐行核对根因,2026-09-14重读源码确认未变)
现象:GET成员列表明确标注群主eligible:false/ineligible_reason:"owner",但POST /groups/{topic}/mutes对群主本人自我禁言、以及管理员禁言群主均返回200成功——代码只检查了操作者本人是否是管理员,从未检查目标成员的角色。
接口:POST /groups/{topic}/mutes;对照GET /groups/{topic}/members?mute_eligible=true
新增 47(P2):真实文件上传大小上限是32MB,不是仓库配置声明的100MB,且这个真实上限从未被前端用来做任何提前校验
证据强度:✅ 实测确认(2026-09-14重测GET /bootstrap,max_file_size依然是33554432即32MB,与之前记录一致)
现象:GET /v1/im/bootstrap声明的data.limits.max_file_size是33554432(32MB),但代码仓库tinode.conf/meetingim.conf里配置的是104857600(100MB)——声明值和真实生效值不一致,且这个字段从未被前端读取用于上传前的预校验。
接口:POST /v0/file/u/(Tinode原生大文件上传);GET /v1/im/bootstrap(data.limits.max_file_size)
⚠️ 说明:此前记录曾提到仓库配置文件已经改成一致的104857600,但线上实测一直是32MB——这次复核确认线上依然是32MB,怀疑部署环境的实际配置和仓库里能看到的配置文件不是同一份,或者中间有额外的网关/环境变量覆盖,建议后端确认线上部署实际生效的配置来源。
新增 49(P2):日报/月报创建接口对日期只校验格式不校验范围,年份0000、2099均可创建并成功提交
证据强度:✅ 实测确认+代码确认(2026-09-14重读源码确认未变)
现象:report_date:"2099-12-31"、"0000-01-01"均能创建、保存草稿、提交成功走完整流程。parseIMDate只用time.ParseInLocation("2006-01-02",...)校验格式,没有任何年份范围校验。
接口:POST /v1/im/reports/daily(report_date)、POST /v1/im/reports/monthly(month)及对应的/submit接口
新增 51(P2):会议创建接口不校验 attendee_user_ids 对应的用户是否真实存在,可创建任意规模的"幽灵参会人"
证据强度:✅ 实测确认+代码确认(2026-09-14重读源码确认未变)
现象:用50个语法合法但真实不存在的user_id+1个真实好友,一次性创建52人会议成功。parseIMUserIDList只做格式校验(types.ParseUserId)和去重,从未查询数据库确认这些用户真实存在。
接口:POST /v1/im/meetings
新增 54(P2):日报/月报SUBMIT接口不校验必填内容,可提交完全空白的报告,与响应自身声明的 validation.*_required:true 矛盾
证据强度:✅ 实测确认(现象)+代码确认(2026-09-14找到确切根因:TenantDailyReportSubmit的UPDATE语句只匹配report_id/author_id/version,完全不检查work_summary/next_plan等内容字段)
现象:work_summary/next_plan均为空字符串的月报草稿直接调用submit,返回200且status:"submitted",同时响应体自己声明这两个字段validation.*_required:true。
接口:POST /v1/im/reports/daily/{id}/submit、POST /v1/im/reports/monthly/{id}/submit
新增 56(P2):青少年模式启用接口 policy_version 不做版本校验,传任意正整数即被无条件接受并永久覆盖
证据强度:✅ 实测确认+代码确认(2026-09-14重读源码确认未变)
现象:代码只检查body.PolicyVersion <= 0(必须是正数),从未跟服务端已知的当前真实policy_version做比对,传任意大的正整数都会被接受并覆盖存储值。
接口:POST /v1/im/settings/youth-mode/enable
⚠️ 注:同新增30,im_webapp已移除此功能,仅为完整性保留记录。
新增 76(P2):GET /app/releases/latest 的 platform 参数只接受 ios/android,web端完全不支持检查更新
证据强度:✅ 代码确认(2026-09-14重读当前HEAD确认未变)
现象:GET /v1/im/app/releases/latest?platform=web恒定400 MALFORMED_REQUEST {"details":{"field":"platform"}}。
接口:GET /v1/im/app/releases/latest
前端现状:AboutPage.tsx捕获这个错误后显示"暂不支持检查更新",不会一直转圈或显示假的"已是最新版本"。
新增 77(P2):群公告接口响应里没有已读人数字段,设计要求的"已读N/M"功能无法实现
证据强度:✅ 实测确认+代码确认(2026-09-14重读groupAnnouncementPayload/groupAnnouncementPayloadWithPermissions确认未变)
现象:GET /groups/{topic}/announcements/latest真实响应确认没有任何"已读数"/"已读人数"相关字段,设计稿明确要求的"已读18/28"这类展示前端拿不到数据来源。
接口:GET /v1/im/groups/{topic}/announcements/latest
前端现状:不展示已读人数(没有编造假数据)。
新增 78(P2):消息搜索/收藏/通话记录里,涉及用户/话题id的字段有不一致的裁剪或哈希化处理
证据强度:✅ 实测确认(三处独立复现)
现象:
GET /topics/{topic}/messages/search的items[].from、收藏type:"text"的snapshot.from:真实user_id被裁掉usr前缀(如usrH5dvZ3ckWxo变成H5dvZ3ckWxo),直接拿这个值和当前用户完整id比较会永远为false。- 收藏的
source.topic(P2P来源)、通话记录GET /calls的topic字段:用的是服务端内部p2p...哈希形式,不是SDK寻址用的usr...形式,客户端没有办法把这个哈希反解回对方user_id。
接口:GET /v1/im/topics/{topic}/messages/search;收藏接口的snapshot.from/source.topic;GET /v1/im/calls的topic
前端现状:都补了对应的兜底逻辑(normalizeSender()补回usr前缀;p2p前缀的topic降级显示"单聊消息"通用文案),不会崩溃,只是丢失了直接寻址能力。
新增 82(P2,2026-09-14新发现):群设置缓存反序列化失败,高频出现,覆盖大量不同群
证据强度:✅ 实测确认(真实运行日志,25分钟窗口内3个pod合计242次,覆盖十几个不同真实群)
现象:cache group settings[{topic}]: read failed: Uid.UnmarshalJSON: invalid length,几乎是常态出现,不是个别数据问题——说明这条缓存读取逻辑本身有系统性bug,大概率是并发访问时缓存里的数据被写坏/读到了不完整的中间状态。其中至少一次复现(grpDclMebv3MuI)是被我们自己的并发请求触发的,其余多次是某个后台任务在循环访问一批固定的群时触发的。
接口:内部缓存逻辑,不直接对应某个REST接口,但会影响所有需要读取群设置的接口的正确性
新增 83(P2,2026-09-14新发现):media serve 死锁没有捕获重试
证据强度:✅ 实测确认(真实运行日志,25分钟窗口内3个pod合计82次)
现象:Error 1213 (40001): Deadlock found when trying to get lock; try restarting transaction——MySQL自己在错误信息里就写明了"try restarting transaction",这是标准的死锁处理方式,但代码里没有捕获这个特定错误码后自动重试,命中死锁的请求直接失败。
接口:GET /v0/file/s/{id}(文件服务)
新增 84(P2,2026-09-14新发现):内部周期性后台任务"media gc"约每50秒触发一次MySQL驱动层连接损坏
证据强度:✅ 实测确认(真实运行日志,从09:42到10:03连续观察,间隔稳定在46-67秒之间,与是否有外部请求流量无关)
现象:[mysql] connection.go:215 busy buffer紧接着packets.go:68 [warn] unexpected sequence nr: expected 0, got 5,然后media gc: N driver: bad connection——这是MySQL驱动协议层面的连接状态错乱,是同一个进程内部对同一条MySQL连接被并发误用导致的典型症状,不是网络问题。
与PXC迁移的关系:这个问题的出现时间与本次复核发现的数据库迁移(旧VM单机MySQL → 新k3s集群PXC)时间点吻合,怀疑与迁移相关,但没有直接证据(比如没有看到这个后台任务代码里调用了新增的PXC兼容分支),仅供后端排查参考。
接口:内部后台任务,不直接对应外部接口,但可能影响同一时间段内其它数据库操作的稳定性
新增 85(P2,历史遗留,曾用编号"新增7"记录在docs/IM-后端问题清单-2026-09-04.md但未被带入本清单,本次复核重新收录):按查询参数区分的缓存,写操作后特定参数组合的结果有明显滞后
证据强度:✅ 实测确认(提升管理员后90秒以上仍查不到);⚠️2026-09-14本次复核未重新实测(判断优先级较低,仅确认历史记录未遗漏)
现象:POST /groups/{topic}/admins/batch提升成功(version从1到2),但GET /groups/{topic}/members?role=admin反复查询超过90秒仍然是items:[]——用直连接口的方式排除了前端自己的客户端缓存,是服务端按role=admin这个具体参数组合单独缓存了旧的空结果,写操作没有使它失效。
接口:GET /v1/im/groups/{topic}/members?role=admin(及其它类似的按查询参数缓存的接口)
新增 86(P2):GET /calls 响应字段结构与协议文档完全不同
证据强度:✅ 实测确认(真实响应逐字段核对)
现象:协议文档描述的字段形状和真实响应完全对不上——没有peer对象,只有扁平的peer_user_id(真的是usr...形式);没有incoming布尔值,而是direction:"incoming"|"outgoing"字符串;时间戳拆成了created_at/started_at/finished_at/updated_at四个字段,没有单一的ts;topic字段是p2p...哈希形式(同新增78的毛病)。
接口:GET /v1/im/calls
前端现状:已按实测字段改正类型声明和渲染逻辑。
新增 87(P2):群二维码铸造/解析两个接口对同一份群数据用了不同的字段名,解析接口还多了一个文档没提到的字段
证据强度:✅ 实测确认(真实铸造+解析双向验证)
现象:POST /groups/{topic}/qrcodes(铸造)响应把群信息放在group_snapshot字段下;POST /qrcodes/resolve(解析,purpose:"join_group")响应把同样性质的数据放在group字段下——同一份数据两个接口用了不同字段名。另外resolve响应还带了一个协议文档完全没提过的status字段,扫描"自己已经在的群"时返回status:"joined";"真的还不是成员"这个场景下status具体是什么值没有实测到(当时没有第二个测试账号)。
接口:POST /v1/im/groups/{topic}/qrcodes;POST /v1/im/qrcodes/resolve
前端现状:做了双重兜底读取(同时尝试group_snapshot和group两个字段名);status按"不是joined就走提交申请"的宽松逻辑兜底处理。
新增 88(P2,未定论,需要后续继续观察):部分明显应该命中的关键词搜索返回空结果
证据强度:⚠️实测确认现象,但成因未定论
现象:在真实群里搜索"验证"/"链接"等明显应该命中历史消息的关键词,一度返回items:[],同一时间搜索"测试"能正常命中。2026-09-14复测:搜索"验证"这次能正确命中真实消息了,看起来比之前有改善;但"链接"依然返回空——不过这次没能确认这个群里是否真的存在包含"链接"二字的消息,不能算作确凿的回归证据。
接口:GET /v1/im/topics/{topic}/messages/search
建议:这条目前证据不够充分下定论(可能是搜索索引/召回策略的问题,也可能是测试数据本身的问题),建议后续有更多测试数据时再复核一次。
新增 89(P1,2026-09-24):文件审核任务无 Worker 消费,带图养成动态永久无法发布
证据强度:✅ 测试环境真实日志 + 数据库 + 代码确认
现象:用户发布带图动态后,动态保持 status=reviewing,audit_status=pending,scope=friends列表只查询 published 状态,因此列表返回空。实例动态 1577072518975639552 的两条图片审核任务(aliyun_green 和 aliyun_sas)均为 pending。
根因:文件上传时会无条件向 im_file_review_tasks 插入内容审核和恶意文件检测任务,但 IM 当前仅有阿里云提交/轮询客户端,没有实现读取、执行和回写这些任务的 Worker。测试库 meeting_im.im_file_review_tasks 共有 174 条任务,全部为 pending,attempts=0。
当前临时策略(2026-09-24 确认):图片审核暂时关闭并默认通过。cultivate_server 仍调用 IM 校验文件存在性、归属和上传完成状态,但不再因文件审核的 pending/risky 状态阻塞动态发布。文字内容审核保持原有逻辑。
待正式解决:
- 在 IM 中实现文件审核 Worker,完成任务抢占、阿里云提交/轮询、重试、超时、失败降级和结果回写。
- 区分内容审核与恶意文件检测是否真正启用,未启用的检测不得创建永久
pending的任务。 - Worker 稳定上线后恢复
cultivate_server的图片审核门禁,并确定现有 174 条历史任务的补偿或废弃策略。