# 服务器端代码修改记录 本文档记录每次对服务器端代码的修改,方便追踪变更历史。 --- ## 修改格式说明 每次修改按以下格式记录: ``` ### [日期] 修改简述 - **文件路径**: 相对于项目根目录的文件路径 - **修改类型**: 新增 / 修改 / 删除 / 重构 / 修复Bug - **修改内容**: 具体修改了什么 - **修改原因**: 为什么要做这个修改 ``` --- ## 修改历史 ### [2026-04-29] strategy B group_send 推回消息体新增 timestamp_unix 字段 配套手机端记录:`LTY_App_Project_URP/docs/修改记录.md` 同日"修复 B' 双倒真正根因:时间戳时区解析"条目。 手机端实测 B' 方案出现 UI 双倒,根因定位为:服务端 `chat_msg.timestamp.isoformat()` 输出 UTC 带时区的 ISO8601(如 `+00:00`),客户端 Unity Mono `DateTime.TryParse` 对此处理不稳定,可能丢失时区信息导致与本地时间戳比较时差 8 小时 → 替换匹配窗口(15s)永远不命中 → 走"作为新消息插入"兜底分支 → 双倒。 服务端最稳妥的修复方式:在 group_send payload 多附一个无时区歧义的 unix 秒级时间戳,让客户端优先使用。 #### 修改:strategy B 落库后 group_send payload 新增 timestamp_unix - **文件路径**: `device_interaction/views.py` - **修改类型**: 增强 - **修改内容**: - `conversation_status` action 内字幕落库分支(约 L1438 附近)的 `channel_layer.group_send` payload 新增字段: ```python 'timestamp_unix': int(chat_msg.timestamp.timestamp()), ``` - 保留原 `timestamp` 字段(ISO8601)兼容老客户端,不破坏现有约定 - **修改原因**: - Unix 秒级时间戳是绝对值,跨语言跨时区零歧义 - 客户端 `DateTimeOffset.FromUnixTimeSeconds(...).LocalDateTime` 转换可靠 - 服务端代价极小(一次 `.timestamp()` 调用),收益是消除一类隐性双倒 bug #### 客户端配套改动(仅记录依赖关系) - `Assets/Scripts/AI/ChatLogManager.cs` 的 `ServerPersistedData` 结构体新增 `long timestamp_unix` 字段 - `OnServerChatPersisted` 时间戳解析改为:优先 `timestamp_unix` > 0 → fallback `DateTimeOffset.TryParse(timestamp)` → fallback `DateTime.Now` - `LoadChatHistoryFromServer` 同步改用 `DateTimeOffset.TryParse`(GET 接口暂未提供 unix 字段) #### 验证 服务端部署后,客户端 Console 应能在 `[匹配诊断]` 日志中看到 delta 缩小到秒级(之前是 ~28800s)。修复确认后客户端会删除诊断日志。 #### 待跟进 - `aiapp/views.py` 的 `RTCChatHistoryAPIView.get` 也可在响应里加 `timestamp_unix` 字段进一步收紧(非必须,因为 GET 路径双倒不直接受影响 —— 走的是覆盖式拉取) --- ### [2026-04-29] 手机端聊天记录切换服务端字幕落库(B' 方案 服务端部分) 配套手机端方案文档:`LTY_App_Project_URP/docs/手机端聊天记录_切换服务端字幕落库方案.md`。手机端已实施 B'(本地 ASR 实时显示 + 服务端 webhook 静默替换),服务端需要补三件事:strategy B 落库后 group_send 推回客户端、DeviceConsumer 加 handler、RTCChatHistoryAPIView 灰度期去重 + since_id 增量拉取。 #### 修改 1:strategy B 落库成功后 group_send 转推 - **文件路径**: `device_interaction/views.py` - **修改类型**: 新增功能 - **修改内容**: - 在 `conversation_status` action 内字幕落库分支(约 L1414 `ChatMessage.objects.create(...)` 处): - 把 `create()` 返回值赋给变量 `chat_msg`,落库成功 log 加上 `id` 字段 - 落库成功后追加 `channel_layer.group_send` 调用,向 `device_{paradise_user_id}` 群组发送 `type='chat_message_persisted'` 消息,payload 含 `id` / `sender` / `message` / `timestamp` / `source_client` - 用独立 `try/except` 包住,转推失败仅 warning 日志,不影响主落库流程 - `source_client` 暂传 `'unknown'`(决策点 #3 落定后改为 `'phone'` / `'device'`) - **修改原因**: - 手机端 B' 方案需要服务端在字幕入库后通过 WebSocket 把"权威 LLM 原始版本"推回客户端 - 手机端按 `chat_msg.id` 去重 + 按 `(sender, timestamp ±10s)` 匹配本地待替换队列做静默替换,达到 UI 与 DB 字符级一致 - 不影响设备端:设备端不订阅 `chat_message_persisted` 类型即可(DeviceConsumer handler 仅向已实现处理的客户端透传) #### 修改 2:DeviceConsumer 加 chat_message_persisted handler - **文件路径**: `device_interaction/consumers.py` - **修改类型**: 新增功能 - **修改内容**: - 在 `conversation_subtitle` handler 之后新增 `chat_message_persisted` handler - 接收 group_send 事件后通过 `self.send` 把 JSON 推到 WebSocket 客户端 - 日志记录 `id` / `sender` / `source_client` 用于后续排查 - **修改原因**: - Channels 协议要求 group_send 的 `type` 字段值在 Consumer 上有同名方法处理,否则消息被丢弃且报警 - 必须与修改 1 同步部署,否则 strategy B 的 group_send 调用会失败 #### 修改 3:RTCChatHistoryAPIView 灰度期 POST 去重 + GET since_id 支持 - **文件路径**: `aiapp/views.py` - **修改类型**: 新增功能 + 增强 - **修改内容**: - `RTCChatHistoryAPIView.post()` 入口加去重判定:同一 `(user, bot, sender, message)` 在 `±2s` 时间窗内已存在则跳过 `create`,返回 `deduplicated: true` - `RTCChatHistoryAPIView.get()` 支持 `since_id` query 参数:传入则返回 `id > since_id` 的消息(升序,最多 page_size 条),未传则保持原最近 page_size 条逻辑 - **修改原因**: - **灰度期双倒保护**:手机端 App 发版到用户手里需要时间,老版仍走 POST 落库;strategy B webhook 此时也在落库 → 同一对话产生重复行。POST 去重让两条路径并存而不致脏库 - **重放保护**:strategy B 自身被火山重试或客户端重连补提时,去重也能挡住 - **WebSocket 漏推兜底**:B' 方案手机端 5s 超时未收到 `chat_message_persisted` 时调 `GET ?since_id=` 增量拉取替换队列里待修正的消息 #### 关联代码(手机端,仅记录依赖关系) - 手机端 `Assets/Scripts/Manager/WebSocketNetworking.cs` 已新增 `chat_message_persisted` 类型分发分支 - 手机端 `Assets/Scripts/AI/ChatLogManager.cs` 已新增 `OnServerChatPersisted` 方法、`_pendingReplaceQueue` 与 5s 超时兜底 - 手机端 `Assets/Scripts/AI/getJson.cs` 已加 `Config.SubtitleConfig.SubtitleMode=1` #### 部署顺序与回滚 - **部署顺序**:服务端先部署(修改 1+2+3 三处一同上线)→ 验证 group_send 通道工作 → 手机端再发版 - **回滚**:三处改动都用独立 try/except 包住,可独立 git revert - 修改 1 revert:strategy B 主流程不受影响,只是不再 group_send,手机端 UI 替换路径变为 5s 超时兜底 - 修改 2 revert:与修改 1 必须同时 revert,否则 group_send 收方为空报警 - 修改 3 revert:POST 不再去重(灰度期会出现双倒,需人工清理 DB);GET 不再支持 since_id(手机端兜底拉取无效) #### 待跟进 TODO - 决策点 #3:服务端区分手机端 / 设备端 RTC session(mac 标记 / task_id 命名规则)→ `source_client` 字段填充真实值,让两端按需过滤 - 决策点 #5:服务端验证打断时是否仍 flush 部分内容;如不 flush,手机端打断分支应跳过入待替换队列以避免 5s 超时空触发 - Phase 0 步骤 1:DB 双轨验证(SQL 见 `LTY_App_Project_URP/docs/手机端聊天记录_切换服务端字幕落库方案.md`) - Phase 0 步骤 4:清理历史脏数据(如发现) --- ### [2026-03-17] 修复手机号登录时 IntegrityError - **文件路径**: `userapp/views.py` - **修改类型**: 修复Bug - **修改内容**: `PhoneLoginView.post()` 中 `get_or_create` 新增 `defaults={'username': phone_number}` - **修改原因**: 新用户首次通过手机号登录时,`get_or_create` 未设置 `username` 字段,导致 `username=""` 与数据库中已有空 username 记录冲突,触发 `IntegrityError: duplicate key value violates unique constraint "userapp_paradiseuser_username_key"`。改为用手机号作为默认 username,保证唯一性。