19 KiB
服务器端代码修改记录
本文档记录每次对服务器端代码的修改,方便追踪变更历史。
修改格式说明
每次修改按以下格式记录:
### [日期] 修改简述
- **文件路径**: 相对于项目根目录的文件路径
- **修改类型**: 新增 / 修改 / 删除 / 重构 / 修复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(前序 commit64a8cb8已建)
- 修改类型: 新增
- 修改内容: 在
qy_lty/下引入 GSD(Get Shit Done) 工作流目录.planning/,包含:.planning/codebase/— 7 份 codebase 反向工程文档(栈 / 集成 / 架构 / 目录 / 规约 / 测试 / 隐患).planning/PROJECT.md— 项目愿景 + Core Value + 已交付能力(Validated)+ 关键决策记录.planning/REQUIREMENTS.md— 把已上线能力拆为带 REQ-ID 的清单(AUTH/AI/DEV/CARD/ACH/SUB/AFF/VI/INF/ADM/DEP),Active 段留空待/gsd-new-milestone启动.planning/STATE.md— 工作流状态机入口.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 配套任务清单:docs/好感度系统-开发任务清单.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()强制单例:新增时若已有记录则覆盖到现有 pkget_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.pyuserapp/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模式下覆盖已存在记录
- 新增
- 修改原因: 提供一键初始化能力,避免管理员手工逐条添加,且保证默认值与文档一致
后续步骤(不属于本次改动,留待用户确认后执行)
- 在合适时机执行
python manage.py migrate应用 schema 变更和数据迁移 - 执行
python manage.py seed_affinity写入默认规则/等级/配置 - 进入 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_statusaction 内字幕落库分支(约 L1438 附近)的channel_layer.group_sendpayload 新增字段:'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 → fallbackDateTimeOffset.TryParse(timestamp)→ fallbackDateTime.NowLoadChatHistoryFromServer同步改用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_statusaction 内字幕落库分支(约 L1414ChatMessage.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_subtitlehandler 之后新增chat_message_persistedhandler - 接收 group_send 事件后通过
self.send把 JSON 推到 WebSocket 客户端 - 日志记录
id/sender/source_client用于后续排查
- 在
- 修改原因:
- Channels 协议要求 group_send 的
type字段值在 Consumer 上有同名方法处理,否则消息被丢弃且报警 - 必须与修改 1 同步部署,否则 strategy B 的 group_send 调用会失败
- Channels 协议要求 group_send 的
修改 3:RTCChatHistoryAPIView 灰度期 POST 去重 + GET since_id 支持
- 文件路径:
aiapp/views.py - 修改类型: 新增功能 + 增强
- 修改内容:
RTCChatHistoryAPIView.post()入口加去重判定:同一(user, bot, sender, message)在±2s时间窗内已存在则跳过create,返回deduplicated: trueRTCChatHistoryAPIView.get()支持since_idquery 参数:传入则返回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,保证唯一性。