# 服务器端代码修改记录

本文档记录每次对服务器端代码的修改，方便追踪变更历史。

---

## 修改格式说明

每次修改按以下格式记录：

```
### [日期] 修改简述

- **文件路径**: 相对于项目根目录的文件路径
- **修改类型**: 新增 / 修改 / 删除 / 重构 / 修复Bug
- **修改内容**: 具体修改了什么
- **修改原因**: 为什么要做这个修改
```

---

## 修改历史

<!-- 新的修改记录添加在此处下方，最新的在最前面 -->

### [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/` 下引入 [GSD（Get 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/DEP），Active 段留空待 `/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 迁移，按依赖顺序处理跨应用 FK（AffinityLog → 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=30s，touch=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 增量拉取。

#### 修改 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=<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 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，保证唯一性。

