lty/qy_lty/docs/修改记录.md
pmc 46d72b8b39 docs(02-02): 两端修改记录互引 Phase 2 接口契约(qy_lty + qy-lty-admin)
- qy_lty/docs/修改记录.md 顶部新增 [2026-05-07] Phase 2 — 管理端通用凭据槽位 REST 接口(GET 脱敏 / PUT 覆写)条目;跨项目联动字段引用 qy-lty-admin 同期条目
- qy-lty-admin/docs/修改记录.md 顶部新增 [2026-05-07] Phase 2 — 锁定后端通用凭据槽位 REST 接口契约(消费方文档化)条目;服务端联动字段引用 qy_lty 同期条目
- 两端 grep 双向命中互引闭环;Phase 1 已有两条条目位置不变
- CLAUDE.md「跨项目联动两端各写一条互相引用」规则首次落地(Phase 2 是 Milestone v1.0 首次跨项目接口契约暴露)
2026-05-07 23:07:38 +08:00

343 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 服务器端代码修改记录
本文档记录每次对服务器端代码的修改,方便追踪变更历史。
---
## 修改格式说明
每次修改按以下格式记录:
```
### [日期] 修改简述
- **文件路径**: 相对于项目根目录的文件路径
- **修改类型**: 新增 / 修改 / 删除 / 重构 / 修复Bug
- **修改内容**: 具体修改了什么
- **修改原因**: 为什么要做这个修改
```
---
## 修改历史
<!-- 新的修改记录添加在此处下方,最新的在最前面 -->
### [2026-05-07] Phase 2 — 管理端通用凭据槽位 REST 接口GET 脱敏 / PUT 覆写)
配套 Phase[.planning/phases/02-admin-rest/](.planning/phases/02-admin-rest/)
覆盖需求CRED-03 + CRED-04
设计参考1:1 复刻 `aiapp.views.RTCChatHistoryAPIView``aiapp/views.py:434-555`)的单 URL 多方法 APIView 风格
- **文件路径**:
- `aiapp/serializers.py`(修改 — 顶部 import 追加 `CredentialSlot`,文件末尾追加 `CredentialSlotSerializer` ModelSerializer 类)
- `aiapp/views.py`(修改 — 顶部 import 追加 `CredentialSlot` / `CredentialSlotSerializer` / `mask_token` / `get_standardized_response_schema`;文件末尾追加 `CredentialSlotPutRequestSchema` swagger 请求体 + `_credential_slot_data_schema` 响应 data schema + `CredentialSlotAdminView` APIView 类)
- `userapp/admin_urls.py`(修改 — 追加 `from aiapp.views import CredentialSlotAdminView``path('credential-slot/', CredentialSlotAdminView.as_view(), name='admin_credential_slot')`
- **修改类型**: 新增
- **修改内容**:
- 暴露 `GET /api/v1/admin/credential-slot/`admin token 鉴权(`RedisTokenAuthentication` + 视图内 `is_staff` 二次校验,不发明 admin-only permission 类);返回 `{ success, code, message, data: { app_id, access_token: <末 4 位脱敏掩码>, updated_at } }`,脱敏由 view 层调 `common.utils.mask_token` 完成serializer 不参与脱敏,避免双重责任)
- 暴露 `PUT /api/v1/admin/credential-slot/`admin token 鉴权;接受 `{ app_id, access_token }` 全字段覆写;空记录场景自动走 `CredentialSlot.get_solo()``get_or_create(pk=1)`;写入后 `updated_at``auto_now=True` 自动刷新;响应同样脱敏 access_token避免运营在 admin UI 看到自己刚提交的明文回显)
- 鉴权拒绝矩阵:无 token → 401DRF NotAuthenticated → middleware 兜底标准壳层);持普通 user token非 staff→ 403 + `message="需要管理员权限"`
- Swagger / ReDoc 自动暴露method-level `@swagger_auto_schema` 装饰器;响应 schema 配 `common.swagger_utils.get_standardized_response_schema()`access_token 字段 description 显式标注「Access Token 末 4 位脱敏掩码(如 "*********1234")」,避免前端误解为明文
- 不引入新依赖(沿用 Django 4.2.13 + DRF + drf-yasg + Phase 1 落地的 `CredentialSlot.get_solo` / `mask_token`
- **修改原因**: Milestone v1.0「通用凭据槽位APP ID + Access Token」Phase 2 — 给管理后台前端qy-lty-admin暴露受控的凭据读写入口让运营无需进 Django Admin 也能管理凭据GET 与 PUT 响应均脱敏,避免明文经管理端 UI / 浏览器 devtools / 阿里云日志GET 响应体路径)泄露;为 Phase 3 客户端明文 GET 接口 + 阿里云日志 formatter 提供"接口已上线、凭据可写入"的稳定起点
- **跨项目联动**: 前端联动条目 [qy-lty-admin/docs/修改记录.md](../../qy-lty-admin/docs/修改记录.md) 同期 `[2026-05-07] Phase 2 — 锁定后端通用凭据槽位 REST 接口契约(消费方文档化)`。本 phase 是 Milestone v1.0 首次跨项目接口契约落地:本仓库(服务端)暴露 `/api/v1/admin/credential-slot/` GET/PUT前端 `qy-lty-admin` 后续 phase 将基于该契约写 API client含 React Hooks 调用 + 表单录入 UI。前后端各自维护独立修改记录本条与对方条目互相引用便于未来回查接口的双向上下游
### [2026-05-07] Phase 1 — Django Admin 注册凭据槽位(脱敏 + 单例约束 + 禁删)
配套 Phase[.planning/phases/01-credential-data-layer/](.planning/phases/01-credential-data-layer/)
覆盖需求CRED-02
- **文件路径**: `aiapp/admin.py`(修改 — 顶部 import 追加 `CredentialSlot``mask_token`,文件末尾追加 `CredentialSlotAdmin` 注册)
- **修改类型**: 新增
- **修改内容**:
- 注册 `CredentialSlotAdmin``list_display = ('id', 'app_id', 'access_token_masked', 'updated_at')`,其中 `access_token_masked` 是计算字段(调 `common.utils.mask_token` 仅显示末 4 位掩码)
- `fieldsets` 分「凭据信息」(`app_id` / `access_token` 明文可写)+「元数据」(`updated_at` 只读、可折叠)
- 重写 `has_add_permission`:已存在记录时返回 `False`Admin 列表页隐藏「增加」按钮,强制单例语义)
- 重写 `has_delete_permission`:永远返回 `False`(含批量动作;防运营误删丢失单例)
- 不修改既有 `BotAdmin` / `ChatMessageAdmin` 注册块
- **修改原因**: CRED-02 — 在 SimpleUI 后台为运营提供受控的凭据录入入口;列表 / 查看态脱敏防截图 / 录屏泄露;编辑态保留明文供录入;新增 / 删除按钮隐藏强制单例语义不被运营误操作破坏
- **跨项目联动**: 无 — qy-lty-admin 同期 v1.0 前端集成 milestone 已规划但未启动;待前端启动 phase 后由对方仓库写一条互引条目。本改动仅触及服务端 Django Admin运营访问 `/admin/aiapp/credentialslot/` 直接录入),与 `qy-lty-admin/`Web 管理后台前端)无 API 联动CLAUDE.md 跨项目规则下纯服务端改动不需要在 `qy-lty-admin/docs/修改记录.md` 写互引条目。Phase 2 暴露 `/api/v1/admin/credential-slot/` 接口时再做前后端联动。
### [2026-05-07] Phase 1 — 凭据槽位数据层CredentialSlot 单例模型 + 迁移 + mask_token 工具)
配套 Phase[.planning/phases/01-credential-data-layer/](.planning/phases/01-credential-data-layer/)
覆盖需求CRED-01
设计参考1:1 复刻 `userapp.models.AffinitySetting``userapp/models.py:247-314`)的 pk=1 + `save()` 钩子 + `get_solo()` 单例三件套
- **文件路径**:
- `common/utils.py`(新增 — `mask_token(token, visible_tail=4)` 工具函数,供本 Phase Admin 与 Phase 3 阿里云日志 formatter 共用)
- `aiapp/models.py`(修改 — 文件末尾追加 `CredentialSlot` 模型3 字段 + save 钩子 + `get_solo` 类方法)
- `aiapp/migrations/0004_credentialslot.py`(新增 — `python manage.py makemigrations aiapp` 自动生成)
- **修改类型**: 新增
- **修改内容**:
- 新增 `CredentialSlot` 模型aiapp app`app_id` CharField(128, blank=True, default='')、`access_token` CharField(512, blank=True, default='')、`updated_at` DateTimeField(auto_now=True)`save()` 钩子在已有记录时把新对象 pk 改为现有那条;`get_solo()` 类方法走 `get_or_create(pk=1)`
- 新增 `common.utils.mask_token(token, visible_tail=4, mask_char='*')`:空输入返回 `''`;短于 visible_tail 时全脱敏不暴露长度;其余保留末 N 位明文
- 自动生成迁移 `aiapp/migrations/0004_credentialslot.py``python manage.py migrate` 通过;首次访问 `CredentialSlot.objects.get_or_create(pk=1)` 拿到一条空记录
- **修改原因**: Milestone v1.0「通用凭据槽位APP ID + Access Token」Phase 1 — 在 DB 层落地全局单例的凭据存储槽位,为 Phase 2 管理端 REST、Phase 3 客户端 REST + 日志脱敏奠基mask_token 抽到 `common/` 让 Phase 3 阿里云日志 formatter 直接复用,避免重复实现
- **后续动作**: Phase 2 暴露 `/api/v1/admin/credential-slot/` GET脱敏 / PUT覆写Phase 3 暴露 `/api/credential-slot/` GET 明文 + 阿里云日志 formatter 用 `mask_token` 过滤 `access_token` 字段
- **跨项目联动**: 无 — qy-lty-admin 同期 v1.0 前端集成 milestone 已规划但未启动;待前端启动 phase 后由对方仓库写一条互引条目。本改动是纯数据层 + 工具函数,无任何 HTTP / WebSocket 接口暴露,`qy-lty-admin` 与 Unity 客户端均无感知;不需要在前端写互引条目。
### [2026-05-07] 引入 GSD 工作流并完成 brownfield 文档化初始化
- **文件路径**:
- `.planning/config.json`(新增)
- `.planning/PROJECT.md`(新增)
- `.planning/REQUIREMENTS.md`(新增)
- `.planning/STATE.md`(新增)
- `.planning/codebase/STACK.md` / `INTEGRATIONS.md` / `ARCHITECTURE.md` / `STRUCTURE.md` / `CONVENTIONS.md` / `TESTING.md` / `CONCERNS.md`(前序 commit `64a8cb8` 已建)
- **修改类型**: 新增
- **修改内容**: 在 `qy_lty/` 下引入 [GSDGet Shit Done](https://github.com/anthropics/get-shit-done) 工作流目录 `.planning/`,包含:
1. `.planning/codebase/` — 7 份 codebase 反向工程文档(栈 / 集成 / 架构 / 目录 / 规约 / 测试 / 隐患)
2. `.planning/PROJECT.md` — 项目愿景 + Core Value + 已交付能力Validated+ 关键决策记录
3. `.planning/REQUIREMENTS.md` — 把已上线能力拆为带 REQ-ID 的清单AUTH/AI/DEV/CARD/ACH/SUB/AFF/VI/INF/ADM/DEPActive 段留空待 `/gsd-new-milestone` 启动
4. `.planning/STATE.md` — 工作流状态机入口
5. `.planning/config.json` — 工作流偏好YOLO / Coarse / Parallel / 三类辅助 agent 全开 / Balanced 模型档)
- **修改原因**:
- 后续新功能 / 重构通过 GSD 走「discuss → plan → execute → verify」标准流程避免无规划的散弹式提交
- 反向梳理一遍现状形成文档基线,方便新成员(含 AI agent秒级进入上下文
- `.planning/` 锚定在 `qy_lty\` 而非父级 `Lila-Server\`,遵循 CLAUDE.md「`qy_lty``qy-lty-admin` 是独立项目」原则;通过预创建空 `.planning/` 目录强制锚定生效
- **后续动作**: 新功能开发使用 `/gsd-new-milestone` 启动候选优先级HIGH 项含成就条件校验缺失、SMS 限流、DEBUG/CORS 收紧、测试 MAC 后门移除、测试基础设施搭建)见 `.planning/REQUIREMENTS.md`
### [2026-05-07] CLAUDE.md 新增「沟通语言」规则 — 强制中文回复
- **文件路径**: `CLAUDE.md`
- **修改类型**: 新增
- **修改内容**: 在文件顶部(项目概述之前)新增 `## 沟通语言(重要 — 始终生效)` 章节明确所有面向用户的回复统一使用中文内部思考可用任意语言工具调用参数、commit message、代码注释保持项目原有约定此规则覆盖默认英文输出倾向仅在用户显式要求时切换。
- **修改原因**: 用户要求把"思考后的回答用中文显示"沉淀为本仓库长期生效的工作规则,避免每次会话重复声明,并让后续任何 Claude/Copilot 会话进入仓库即自动遵循。
### [2026-04-24] 好感度系统 P1 阶段 — 数据模型扩展 + 迁移 + seed 命令
配套设计文档:[docs/好感度系统功能与规则设计.md](好感度系统功能与规则设计.md)
配套任务清单:[docs/好感度系统-开发任务清单.md](好感度系统-开发任务清单.md)P1-01 ~ P1-10 全部完成)
本次改动把好感度系统的数据层从「用户级单值」(`ParadiseUser.favorability`)演进到「设备级独立计数」(`UserDevice.favorability`),并补齐规则、等级、配置、日志、计数器、奖励发放标记 6 类表,为后续 P2 service 层开发奠基。
#### P1-01 / P1-02 / P1-03 — AffinityRule、AffinityLevel 字段扩展
- **文件路径**: `userapp/models.py`
- **修改类型**: 重构
- **修改内容**:
- `AffinityRule` 新增字段:`rule_key`(代码标识)、`trigger_type`action / companion_time / decay`min_change` / `max_change`[min,max] 闭区间随机)、`single_cap` / `daily_cap` / `cooldown_seconds``is_negative` / `is_enabled` / `is_deleted``min_continuous_minutes` / `max_count_per_day`(陪伴时长专用)
- `AffinityLevel` 新增字段:`min_affinity` / `max_affinity`(区间)、`unlock_content``reward_type` / `reward_currency` / `reward_items``is_enabled` / `is_deleted`
- 旧字段 `points` / `daily_limit` / `is_active`Rule`required_points` / `rewards`Level保留作为兼容字段注释标记 "已弃用",下个版本删除
- **修改原因**:
- 旧字段无法满足设计文档 §4 / §6 的规则与等级配置维度(缺范围、缺冷却、缺奖励细分)
- 软删除字段 `is_deleted` 是 13.1-B1 默认方案的兜底,保留删除决策的可逆性
#### P1-04 — 新增 AffinitySetting单例表
- **文件路径**: `userapp/models.py`
- **修改类型**: 新增
- **修改内容**:
- 新增 `AffinitySetting` 模型,存全局参数:`initial_affinity``max_affinity``daily_cap`(全局日上限)、衰减相关 6 字段、通知开关、`timezone`(默认 Asia/Shanghai
- `save()` 强制单例:新增时若已有记录则覆盖到现有 pk
- `get_solo()` 类方法:取唯一实例,不存在则创建默认
- **修改原因**: 设计文档 §3.2 全局参数 + §5.1 衰减字段需要持久化配置,单例表是简单可靠的存储模式
#### P1-05 — 新增 AffinityLog变化日志
- **文件路径**: `userapp/models.py`
- **修改类型**: 新增
- **修改内容**:
- 新增 `AffinityLog` 模型:`user` / `device`(SET_NULL) / `rule`(SET_NULL) / `rule_key`(冗余文本)、`change_value` / `before_value` / `after_value``source`5 种来源)、`event_id`(幂等去重)、`operator_admin_id` + `reason`(管理员调整审计)、`metadata`(JSON)
- 索引:`(device, -created_at)` / `(user, -created_at)` / `(rule_key, -created_at)` / `(source, -created_at)`
- 部分唯一约束 `unique_affinity_event_id`:仅当 `event_id` 非空时唯一
- **修改原因**:
- 所有好感度变化必须可审计、可追溯(设计文档 §9.3 + §13 决策记录的 12 项)
- `event_id` 唯一约束实现服务端去重(决策 C9
#### P1-06 — 新增 UserAffinityDailyCounter每日计数器
- **文件路径**: `userapp/models.py`
- **修改类型**: 新增
- **修改内容**: 新增 `UserAffinityDailyCounter` 模型:`(device, rule, date)` 唯一,`accumulated_change` + `trigger_count`
- **修改原因**: 热路径走 Redis`daily:{device}:{rule}:{YYYYMMDD}`),数据库表作为审计兜底,每晚定时任务把 Redis 当日数据落库
#### P1-07 — 新增 UserLevelRewardGrant等级奖励发放标记
- **文件路径**: `userapp/models.py`
- **修改类型**: 新增
- **修改内容**: 新增 `UserLevelRewardGrant` 模型:`(device, level)` 唯一,`reward_snapshot` 保存发放时奖励快照
- **修改原因**:
- 决策 3 + 决策 11升级逐级发奖励永久幂等衰减回升后不补发
- `reward_snapshot` 防止 `AffinityLevel` 后续修改影响审计
#### P1-08 — UserDevice 加好感度字段(设备级模型核心)
- **文件路径**: `device_interaction/models.py`
- **修改类型**: 修改
- **修改内容**:
- `UserDevice` 新增 4 字段:`favorability`(默认 10`affinity_level`(默认 1`last_active_at`(带 db_index`is_active`(绑定有效软删除标记)
- 在 docstring 中说明:`UserDevice.is_active` 是绑定软删除标记,与 `Device.is_active`(设备激活态)不是同一概念
- **修改原因**: 决策 8 — 好感度归属为「设备级」,每条用户-设备绑定独立维护值、等级、解锁内容
#### P1 自动迁移文件 — 由 makemigrations 生成
- **文件路径**:
- `device_interaction/migrations/0003_userdevice_affinity_level_userdevice_favorability_and_more.py`
- `userapp/migrations/0005_affinitysetting_affinitylevel_is_deleted_and_more.py`
- **修改类型**: 新增
- **修改内容**: Django 自动生成的 schema 迁移,按依赖顺序处理跨应用 FKAffinityLog → UserDevice
- **修改原因**: P1-01 ~ P1-08 模型变更需要落库
#### P1-09 — 数据迁移ParadiseUser.favorability → UserDevice.favorability
- **文件路径**: `userapp/migrations/0006_migrate_favorability_to_userdevice.py`
- **修改类型**: 新增
- **修改内容**:
- 手写 RunPython 数据迁移:遍历所有 favorability > 0 的用户,写到主设备(无主设备则取最近绑定)
- 仅当目标 `UserDevice.favorability == 10`(默认值)时写入,避免覆盖业务层后续修改
- 提供 `migrate_favorability_backward` 回滚函数
-`ParadiseUser.favorability` 字段保留不删,由后续版本统一清理
- **修改原因**: 设备级模型上线时,存量用户的好感度数据不能丢失,需平滑迁移到主设备
#### P1-10 — seed 默认数据 management command
- **文件路径**:
- `userapp/management/__init__.py`(新建空文件)
- `userapp/management/commands/__init__.py`(新建空文件)
- `userapp/management/commands/seed_affinity.py`
- **修改类型**: 新增
- **修改内容**:
- 新增 `python manage.py seed_affinity` 命令:写入 AffinitySetting 单例 + 8 条默认规则 + 5 个默认等级
- 默认数据与设计文档 §4.2 / §6.2 一致;规则带 `rule_key``cooldown_seconds`chat=30stouch=10s其余 0等级带 `min_affinity`/`max_affinity` 闭区间
- 幂等:默认按 `rule_key` / `level` 查询,已存在则跳过;`--force` 模式下覆盖已存在记录
- **修改原因**: 提供一键初始化能力,避免管理员手工逐条添加,且保证默认值与文档一致
#### 后续步骤(不属于本次改动,留待用户确认后执行)
1. 在合适时机执行 `python manage.py migrate` 应用 schema 变更和数据迁移
2. 执行 `python manage.py seed_affinity` 写入默认规则/等级/配置
3. 进入 P2 阶段service 层 + 管理端 API见任务清单
---
### [2026-04-30] CLAUDE.md 新增"项目修改记录规则"段落
- **文件路径**: `CLAUDE.md`
- **修改类型**: 新增
- **修改内容**:
- 在文末追加"项目修改记录规则(重要 — 自动执行)"段落,明确要求每次代码改动后必须在同一会话内追加到 `docs/修改记录.md` 顶部
- 划清 `qy_lty``qy-lty-admin` 各自独立维护修改记录的边界,跨项目联动改动两端各写一条互相引用
- 列出适用范围:业务/配置/迁移/CI/k8s/Dockerfile/文档结构性改动必须记录typo / 临时调试脚本可省
- **修改原因**:
- 之前修改记录靠手工维护,部分改动遗漏未追加导致追踪历史中断
- 规则写进 CLAUDE.md 后Claude Code 在每次会话中可自动遵守,减少漏记风险
- 配套同步在 `qy-lty-admin/CLAUDE.md``qy-lty-admin/docs/修改记录.md` 建立独立修改记录骨架(详见 `qy-lty-admin/docs/修改记录.md` 同日条目)
---
### [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 增量拉取。
#### 修改 1strategy 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 仅向已实现处理的客户端透传)
#### 修改 2DeviceConsumer 加 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 调用会失败
#### 修改 3RTCChatHistoryAPIView 灰度期 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=<last>` 增量拉取替换队列里待修正的消息
#### 关联代码(手机端,仅记录依赖关系)
- 手机端 `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 revertstrategy B 主流程不受影响,只是不再 group_send手机端 UI 替换路径变为 5s 超时兜底
- 修改 2 revert与修改 1 必须同时 revert否则 group_send 收方为空报警
- 修改 3 revertPOST 不再去重(灰度期会出现双倒,需人工清理 DBGET 不再支持 since_id手机端兜底拉取无效
#### 待跟进 TODO
- 决策点 #3服务端区分手机端 / 设备端 RTC sessionmac 标记 / task_id 命名规则)→ `source_client` 字段填充真实值,让两端按需过滤
- 决策点 #5服务端验证打断时是否仍 flush 部分内容;如不 flush手机端打断分支应跳过入待替换队列以避免 5s 超时空触发
- Phase 0 步骤 1DB 双轨验证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保证唯一性。