- 5 处文件改动汇总: aiapp/views.py + qy_lty/urls.py + common/logging/__init__.py + common/logging/filters.py + qy_lty/settings.py - 修改类型: 新增 - 修改内容: 客户端 GET 接口明文返回 + AccessTokenMaskFilter 4 正则脱敏 + LOGGING 注册 - 修改原因: Milestone v1.0 收尾 phase, 客户端读取 + 日志防御性兜底 - 跨项目联动: 无 — 客户端给 Unity (LTY_Project / LTY_App_Project_URP) 用, 那两个 repo 各自维护; qy-lty-admin 不消费此接口 - qy-lty-admin/docs/修改记录.md mtime 验证未变, 不写互引
366 lines
33 KiB
Markdown
366 lines
33 KiB
Markdown
# 服务器端代码修改记录
|
||
|
||
本文档记录每次对服务器端代码的修改,方便追踪变更历史。
|
||
|
||
---
|
||
|
||
## 修改格式说明
|
||
|
||
每次修改按以下格式记录:
|
||
|
||
```
|
||
### [日期] 修改简述
|
||
|
||
- **文件路径**: 相对于项目根目录的文件路径
|
||
- **修改类型**: 新增 / 修改 / 删除 / 重构 / 修复Bug
|
||
- **修改内容**: 具体修改了什么
|
||
- **修改原因**: 为什么要做这个修改
|
||
```
|
||
|
||
---
|
||
|
||
## 修改历史
|
||
|
||
<!-- 新的修改记录添加在此处下方,最新的在最前面 -->
|
||
|
||
### [2026-05-08] Phase 3 — 客户端凭据槽位 GET 接口 + 阿里云日志 access_token 脱敏
|
||
|
||
配套 Phase:[.planning/phases/03-client-and-log-mask/](.planning/phases/03-client-and-log-mask/)
|
||
覆盖需求:CRED-05 + CRED-06
|
||
设计参考:1:1 复刻 `aiapp.views.CredentialSlotAdminView` 的 GET 部分(删 `_ensure_admin` / `_build_response_data` / PUT 三处),实现明文返回客户端 view;新建 `common/logging/filters.py:AccessTokenMaskFilter` 作为 LOGGING.handlers 层防御性兜底
|
||
|
||
- **文件路径**:
|
||
- `aiapp/views.py`(修改 — 文件末尾追加 `_credential_slot_client_data_schema` 客户端响应 schema + `CredentialSlotClientView` APIView 类,仅 GET,明文返回;imports 段未动;Phase 2 既有 `CredentialSlotAdminView` 未动)
|
||
- `qy_lty/urls.py`(修改 — imports 段追加 `from aiapp.views import CredentialSlotClientView`;`api_urlpatterns` 列表中追加 `path('credential-slot/', CredentialSlotClientView.as_view(), name='client_credential_slot')`,注册位置:`common/upload/` 之后、`v1/admin/` 之前)
|
||
- `common/logging/__init__.py`(**新建** — 空文件,让 `common.logging` 成为可 import 的 Python 包)
|
||
- `common/logging/filters.py`(**新建** — `AccessTokenMaskFilter(logging.Filter)` 类 + 4 个 regex 模式(JSON / Python dict repr / URL query / 等号或冒号兜底)+ `filter()` 方法重写 `record.msg` 与 `record.args` 中的 access_token 字段值为 `mask_token(value)` 输出)
|
||
- `qy_lty/settings.py`(修改 — `LOGGING` 字典新增 `'filters'` 段(用 `'()': 'common.logging.filters.AccessTokenMaskFilter'` dictConfig 工厂语法);`'handlers'.aliyun` 与 `'handlers'.console` 各追加 `'filters': ['access_token_mask']`;loggers 段 5 条 logger 完全未动)
|
||
- **修改类型**: 新增
|
||
- **修改内容**:
|
||
- 暴露 `GET /api/credential-slot/`(路径与管理端 `/api/v1/admin/credential-slot/` **完全分开**,客户端走 `/api/` 一级命名空间不进 `v1/admin/` 子路径):`RedisTokenAuthentication` + `IsAuthenticated`,**不**做 is_staff 二次校验(admin / user token 都允许;admin 用户是手机用户超集,CONTEXT 锁定决策);返回 `{ success, code, message, data: { app_id, access_token: <**明文**>, updated_at } }`,Access Token 直接返回 `serializer.data`(不调 `mask_token`),供手机端(LTY_App_Project_URP)/ 设备端(LTY_Project)实际调用阿里云 / 火山 / 腾讯第三方服务
|
||
- 新建 `AccessTokenMaskFilter`:4 个正则模式覆盖 JSON 字符串(`"access_token":"VALUE"`)、Python dict repr(`'access_token':'VALUE'`)、URL query(`access_token=VALUE`)、等号或冒号兜底(`access_token: VALUE`)共 4 种序列化形态;filter 同时改 `record.msg` 与 `record.args`(避免 Formatter 阶段再用 `%` 拼接出明文,per RESEARCH Pitfall 2);只匹配 `access_token` 字段名为前缀锚点,**不**误伤 `Authorization header:` / `Bearer` / 裸 user token(per RESEARCH Pitfall 3);filter 永远 `return True` 不丢弃 record(per RESEARCH Pitfall 1)
|
||
- LOGGING dictConfig 注册:filter 段用 `'()': '...'` 工厂语法(不是 `'class'`,per RESEARCH Pitfall 5);filter 挂在 `handlers.aliyun` / `handlers.console` 两个 handler 上(**不**挂 loggers 段,per RESEARCH Pitfall 1 — 挂 logger 仅过滤直接通过该 logger 的 record,挂 handler 才统一覆盖所有 logger → handler 路径);既有 5 条 logger 配置完全未动
|
||
- Swagger / ReDoc 自动暴露:method-level `@swagger_auto_schema` 装饰器;响应 data schema 用独立 `_credential_slot_client_data_schema`,access_token 字段 description 显式标注「明文 Access Token,供手机/设备端实际调用第三方服务(管理端同接口会脱敏返回末 4 位)」,避免前端误解明文 / 脱敏
|
||
- 不引入新依赖(沿用 Django 4.2.13 + DRF + drf-yasg + Phase 1/2 落地的 `CredentialSlot.get_solo` / `CredentialSlotSerializer` / `mask_token`)
|
||
- **修改原因**: Milestone v1.0「通用凭据槽位(APP ID + Access Token)」Phase 3 收尾 phase — 同时落地客户端读取(CRED-05)与日志脱敏(CRED-06)。客户端读取需要明文(手机/设备端 Unity 调阿里云 / 火山 / 腾讯 SDK 时第三方 API 校验 token 字符级一致),所以 view 层不脱敏;但「明文走 view」会让任何后续开发者写 `logger.info(f"PUT body: {request.data}")` 类代码立即把 access_token 打到阿里云日志服务,所以新增 LOGGING.handlers 层 filter 作为防御性兜底。RESEARCH 已实证:当前仓库**没有**任何代码 logger 输出 `CredentialSlot.access_token` 明文(`StandardResponseMiddleware` 不打日志、view 不显式 logger 字段、Django 默认 access log 不含 body),所以 CRED-06 的端到端验证靠**单元测试**伪造 LogRecord 验证 filter 行为(4 种序列化形态 + 不误伤 Authorization 字段)+ 1 条端到端 logger.info 真实输出脱敏验证,不靠端到端找泄露路径。这是 CRED-06 的真实价值 — 防御性兜底,让未来代码改动天然安全
|
||
- **跨项目联动**: 无 — 客户端 GET `/api/credential-slot/` 给 Unity 客户端(`LTY_Project` / `LTY_App_Project_URP`)使用,那两个 repo 各自维护修改记录,不在本仓库范畴;`qy-lty-admin`(Web 管理后台前端)**不消费**此接口(管理端走 Phase 2 落地的 `/api/v1/admin/credential-slot/`,由 admin token 鉴权 + 脱敏返回)。CLAUDE.md 跨项目规则下:本 phase 既不影响 qy-lty-admin 也不与 Unity 客户端在同一仓库,故不在 qy-lty-admin/docs/修改记录.md 写互引条目;Unity 客户端改动由 LTY_Project / LTY_App_Project_URP 在自身仓库各自记录
|
||
- **后续动作**: Milestone v1.0 至此完成;下一周期 milestone 候选见 `.planning/REQUIREMENTS.md` 「候选优先级」段(HIGH:ACH-02 / SMS 频率限制 / DEBUG 收紧 / 测试基础设施 / 测试 MAC 硬编码;MEDIUM:好感度 P2-P4 / Python 版本升级 / device_interaction 拆分)
|
||
|
||
### [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 → 401(DRF 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/` 下引入 [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,保证唯一性。
|
||
|