- 阿里云 Redis 实例 10054 RST 导致 /api/v1/admin/login/ 等接口全线 500,切到火山实例 (db /3, user zyc)
- CHANNEL_LAYERS hosts 由手工拼接 redis://:{pwd}@{host} 改为直接消费 REDIS_LOCATION,支持 ACL username
- .gitignore 恢复 qy_lty/.env 忽略,git rm --cached 移除跟踪;历史中旧密钥仍在,需另行 rotate
- 详见 qy_lty/docs/修改记录.md 2026-05-18 条目
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
54 KiB
服务器端代码修改记录
本文档记录每次对服务器端代码的修改,方便追踪变更历史。
修改格式说明
每次修改按以下格式记录:
### [日期] 修改简述
- **文件路径**: 相对于项目根目录的文件路径
- **修改类型**: 新增 / 修改 / 删除 / 重构 / 修复Bug
- **修改内容**: 具体修改了什么
- **修改原因**: 为什么要做这个修改
修改历史
[2026-05-18] Redis 切换为火山引擎实例 + 修复 CHANNEL_LAYERS 不支持 ACL username
原阿里云 Redis 实例(r-7xvat0vez5clwbzk5vpd.redis.rds.aliyuncs.com:6379)连接被远端 RST(10054),导致 /api/v1/admin/login/ 等所有依赖 token / 缓存的接口报 ConnectionError。改用火山引擎 Redis 实例(redis-shzlsczo52dft8mia.redis.volces.com:6379/3,用户 zyc)。
切换过程中发现 CHANNEL_LAYERS 配置硬拼 URL 时只放了 password、没有 username,导致带 ACL username 的 Redis(如本次的火山实例)会拼出畸形 URL(redis://:pwd@host 缺 user)。改为直接消费完整 REDIS_LOCATION(已含 zyc:Zyc188208@),后续切换实例只改 .env 即可。
- 文件路径:
.env(修改 — 旧 REDIS_LOCATION/PASSWORD 注释保留作为回滚参考;新 REDIS_LOCATION 含完整zyc:Zyc188208@凭据;REDIS_PASSWORD 仍保留Zyc188208以兼容django-redisOPTIONS.PASSWORD)qy_lty/settings.py(修改 — 第 519 行 CHANNEL_LAYERS hosts 由f"redis://:{config('REDIS_PASSWORD')}@{config('REDIS_LOCATION').replace('redis://', '')}"改为[config('REDIS_LOCATION')])
- 修改类型: 配置切换 + 修复Bug
- 修改内容:
- Redis 实例 URL 切换到火山引擎,库号由
/0改为/3 - CHANNEL_LAYERS hosts 不再二次拼接,直接读取完整 URL;不再丢失 ACL username
- Redis 实例 URL 切换到火山引擎,库号由
- 修改原因:
- 阿里云 Redis 不可用(白名单 / 实例状态 / 网络出口任一原因均会触发 10054 RST),登录 / token / 缓存全线 500
- channels_redis 接受标准 redis URL(含
user:pass@),原硬拼方式只能表达 password-only,遇到 ACL 模式实例无法登录通道层
- 验证: 重启 daphne 后启动日志
Cache Status: OK,延迟 296.67ms;HTTP 8000 监听正常 - 回滚: 取消
.env中两行火山配置注释 → 取消阿里云两行注释;CHANNEL_LAYERS 改回原拼接(若回到不带 username 的实例可保留新写法,更通用)
[2026-05-13] 好感度系统 P2 阶段 — Service 层 + 管理端 API 落地
配套设计文档:../../docs/好感度系统功能与规则设计.md 配套任务清单:../../docs/好感度系统-开发任务清单.md(P2-01 ~ P2-12 全部完成)
本次完成「好感度系统服务层 + 管理端 API」整体落地,所有好感度变化收敛到 AffinityService.apply() 单一入口,管理端 admin 后台具备完整 CRUD + 数据统计 + 设备查询 + 手动调整能力。Service 层 6 项 smoke test 全 PASS(含正常 apply / event_id 去重 / 冷却拦截 / admin_adjust / 钳位)。
- 文件路径:
userapp/affinity/counters.py(新建 — P2-02 Redis 计数器:冷却 / 单规则日上限 / 全局日上限 / event_id 去重;Asia/Shanghai 自然日基准;TTL 48h;用 django-redis cache.add+incr 原子语义)userapp/affinity/levels.py(新建 — P2-03 等级映射:map_value_to_level / progress_to_next_level / update_device_level;区间匹配按 -level desc 优先,重叠场景配合 P1 clean() 校验拦截)userapp/affinity/ws.py(新建 — P2-05 WS 推送:push_affinity_update / push_level_up / push_level_down;asgiref async_to_sync 包装 channel_layer.group_send,向 device_{user_id} 分组广播;故障 fire-and-forget 不阻塞主流程)userapp/affinity/rewards.py(新建 — P2-04 跨级奖励发放,A3 方案 B:每级独立事务,UserLevelRewardGrant 唯一约束保证幂等;外部派发 hook _dispatch_reward_to_external_systems 暂为 STUB,P3/P4 接虚拟货币/道具 app 时实现)userapp/affinity/services.py(新建 — P2-01 AffinityService.apply() 主入口:10 步流水线 [event_id 去重 → 取规则 → 冷却 → 取 UserDevice → 计算变化 + single_cap 钳位 → 规则日上限 → 全局日上限 → 原子写 favorability + log + counter + 等级缓存 → Redis 计数器累加 → 奖励发放 → WS 推送];admin_adjust 专用入口绕过 rule 但仍走钳位 + 日志 + 等级 + 奖励 + WS)userapp/affinity/serializers.py(新建 — 9 个序列化器:Rule/Level/Setting 三个 ModelSerializer 含跨字段校验,AffinityLogSerializer 只读 + 关联字段展开,UserDeviceAffinitySerializer 含 device_code/mac/level_name,AffinityAdjust 与 AffinityAdjustBatch 用 Serializer 而非 ModelSerializer)userapp/affinity/permissions.py(新建 — IsAdminUserStaff 复用 IsAuthenticated 并加 is_staff 检查)userapp/affinity/views.py(新建 — 7 个视图:AffinityRuleAdminViewSet / AffinityLevelAdminViewSet ModelViewSet + 软删 perform_destroy + restore action;AffinitySettingView APIView 单例 GET/PUT/PATCH;AffinityLogListView 含 user/device/rule/source/date_range/分页过滤;AffinityStatsView 聚合 avg/max/top_count/active_7d/today_interactions/rule_freq_top/level_distribution;UserAffinityDevicesView 按 user_id 展开设备列表,CR-001 默认仅返回 is_bound=True;AffinityAdjustView + AffinityAdjustBatchView 委托 AffinityService.admin_adjust)userapp/affinity/urls.py(新建 — DRF DefaultRouter 注册 rules/levels CRUD + 5 个独立 path 挂 settings/logs/stats/devices/adjust*)userapp/admin_urls.py(修改 — 引入 include 并新增path('affinity/', include('userapp.affinity.urls')))
- 修改类型: 新增 + 重构
- 修改内容:
- P2-01 Service 层骨架:唯一写入入口
AffinityService.apply(user_id, device_id, rule_key, source, event_id, metadata, operator_admin_id, reason)+admin_adjust(user_id, device_id, delta, operator_admin_id, reason, batch);返回ApplyResultdataclass 含 outcome 枚举 + change/before/after/level 信息 - P2-02 Redis 计数器:6 类操作(is_in_cooldown / set_cooldown / get/incr_rule_daily / get/incr_global_daily / event_already_processed / mark_event_processed),用
cache.add+incr实现 set-if-not-exists+atomic-increment 语义;Asia/Shanghai 时区自然日字符串通过zoneinfo.ZoneInfo计算 - P2-03 等级映射:
map_value_to_level(value)按 (min, max) 区间匹配;update_device_level(user_device)仅在 level 变化时调 save(update_fields=['affinity_level']) - P2-04 跨级奖励发放:
grant_levels(user_device, from_level, to_level)逐级独立事务 + UniqueConstraint 防重;返回 RewardGrantResult(granted, skipped_duplicate, failed);失败的级别不影响其他级别(A3 方案 B 核心特性) - P2-05 WS 推送:3 类事件(affinity_update / level_up / level_down);channel_layer 故障静默吞掉但日志记录
- P2-06 AffinityRule admin CRUD:默认列表过滤
is_deleted=False,?include_deleted=true 显示全集;DELETE 走软删is_deleted=True+is_enabled=False;POST/restore 自定义 action 恢复软删 - P2-07 AffinityLevel admin CRUD:同上软删;serializer 跨字段校验区间重叠(与启用中其他等级不冲突)
- P2-08 AffinitySetting GET/PUT/PATCH:单例,pk=1 硬约束;跨字段校验衰减区间 + 初始/上限关系
- P2-09 AffinityLog 查询:select_related user/rule/device.device 避免 N+1;过滤 user_id/device_id/rule_key/source/date_from/date_to;自实现分页(page/page_size,page_size 上限 200)
- P2-10 stats:所有指标基于
UserDevice.active(is_bound=True)聚合;今日数据按 AffinitySetting.timezone 取 local date;rule_freq_top 取近 7 日 Top 10 - P2-11 devices:?user_id= 必传 + 404 校验;?include_unbound=true 才返回历史;默认按 is_primary desc, bound_at desc 排序
- P2-12 adjust / adjust-batch:单台调整必传 user_id+device_id+delta+reason;批量调整对 user 名下所有 active 设备各调一次,逐台独立调用 service,返回 per-device 结果数组
- 挂载位置:
/api/v1/admin/affinity/{rules,levels,settings,logs,stats,devices,adjust,adjust-batch}/;旧的/api/user/affinity-rules/与/affinity-levels/暂保留兼容(前端切到 admin 后即可清理)
- P2-01 Service 层骨架:唯一写入入口
- 修改原因: P1 数据层就绪后必须落地服务层 + admin API,否则数据模型只是空壳;管理后台前端(P3)需要这套 admin API 才能拆 mock 接通;触发点埋点(P4)和客户端 API(P5)都依赖 service 层的 apply() 入口
- 跨项目联动:
- 管理后台前端 P3 阶段接入:
lib/api/affinity.ts需要切到/api/v1/admin/affinity/...新路径并对齐新字段集(cooldown_seconds, min_continuous_minutes 等) - 设备/手机端 P4 阶段在
device_interaction/consumers.py的 chat_message / sing / dance / touch / conversation_status 处调AffinityService.apply(rule_key=...);事件需带event_id(UUID) - 客户端 P5 阶段查询
/api/user/me/affinity/暂未实现(P5 任务),P2 仅落地 admin 端
- 管理后台前端 P3 阶段接入:
[2026-05-13] 好感度系统 P1 审查修复 D — WR-002WR-009 + IN-001IN-006 综合改进
配套审查报告:docs/REVIEW-affinity-P1.md(WR-002 ~ WR-009 + IN-001 ~ IN-006) 配套修复报告:docs/REVIEW-affinity-P1-FIX-REPORT.md
- 文件路径:
userapp/models.py(修改 — 多处:AffinityLog 索引精简 / event_id null=True /__str__用 pk 兜底;UserLevelRewardGrant SET_NULL + device_snapshot_id + conditional unique;AffinitySetting daily_cap → global_daily_cap;description 显式 default='';弃用字段加 [DEPRECATED] 版本标记;ParadiseUser.favorability 标记 (已弃用))userapp/serializers.py(修改 —UserInfoSerializer移除favorability字段暴露,WR-008)userapp/affinity/__init__.py(新建 — affinity 业务包入口)userapp/affinity/defaults.py(新建 — DEFAULT_RULES / DEFAULT_LEVELS / DEFAULT_SETTING 常量从 management command 抽出;新增 1 条 companion_30min 规则;所有 description 显式填写)userapp/management/commands/seed_affinity.py(重写 — 从 affinity.defaults 导入常量;去掉全局@transaction.atomic,改为每条 spec 独立with transaction.atomic();新增 failed 计数与不同 style 输出,部分失败可重跑)userapp/migrations/0009_affinity_p1_polish.py(新建 — 手工修正 makemigrations 自动生成版本:把daily_cap → global_daily_cap改为 RenameField(保留数据,不是 Remove+Add);event_id''→ NULL 数据兜底 RunPython;UserLevelRewardGrant on_delete + conditional unique;索引精简;弃用字段 help_text 升级)
- 修改类型: 修复Bug + 重构
- 修改内容:
- WR-002:UserLevelRewardGrant.device
on_delete=CASCADE→SET_NULL,加device_snapshot_id冗余字段(save 时自动填充原 pk),unique_together=[('device','level')]→UniqueConstraint(fields=['device','level'], condition=Q(device__isnull=False))保证 device 已删的历史记录不参与唯一性 - WR-003:AffinityLog 删除 3 个低价值索引(user/rule_key/source 各 -created_at 复合),仅保留 (device, -created_at) 与 event_id partial unique
- WR-004:AffinityLog.event_id
null=True;partial unique 条件event_id__gt=''→event_id__isnull=False;RunPython 把现有''改为 NULL - WR-005:DEFAULT_RULES 新增 1 条
companion_30min(trigger_type=companion_time, min_continuous_minutes=30, max_count_per_day=4,min/max change 1~2) - WR-006:AffinityRule/AffinityLevel.description 显式
default='';DEFAULT_LEVELS 所有 entry 补 description - WR-007:seed_affinity 每条 spec 独立事务,部分失败不影响其他记录(去掉 handle 上的 @transaction.atomic)
- WR-008:ParadiseUser.favorability 字段保留(避免 0006 backward 失效)+ verbose_name 加 (已弃用);help_text 标 [DEPRECATED — P2 后删除];UserInfoSerializer 移除字段暴露。未做 property 改造:Model field 与 property 同名冲突,必须先 RenameField 才能上 property,本次只做软标记 + 序列化器清理(详见 FIX-REPORT 风险说明)
- WR-009:(已在 Commit B 的 AffinityLevel.clean() + save() 中实现 — DB 跨行约束 PG 表达不出,应用层多层兜底)
- IN-001:5 个弃用字段(AffinityRule.points / daily_limit / is_active;AffinityLevel.required_points / rewards)help_text 加
[DEPRECATED — 计划于 P2 完成后删除]显式版本标记 - IN-002:DEFAULT_RULES / DEFAULT_LEVELS / DEFAULT_SETTING 抽到
userapp/affinity/defaults.py,供 seed / 单元测试 / P2 服务层复用 - IN-003:AffinitySetting.daily_cap → global_daily_cap RenameField(与 AffinityRule.daily_cap 区分);模型 / 校验 / 约束 / DEFAULT_SETTING 全部同步
- IN-004:AffinityLog.__str__ 用
self.pk or 'new'替代self.id,未保存对象显示#new而非#None - IN-005:(已在 Commit A 完成 — is_active → is_bound 改名)
- IN-006:(已在 Commit C 完成 — 0006 print 前缀改为
[migration 0006_migrate_favorability])
- WR-002:UserLevelRewardGrant.device
- 修改原因: P1 数据层审查中除 3 个 Critical 外的全部剩余项(9 Warning + 6 Info)一次性收尾,避免遗留到 P2 服务层动工后修复成本上升;同时保持每个修复项可独立追溯(commit message + 修改记录条目)
- 跨项目联动: 管理后台前端如已读取 AffinitySetting.daily_cap,需要同步改为 global_daily_cap(仅当 admin UI 暴露该字段时);UserInfoSerializer 不再返回 favorability,前端如有使用需改为查询 UserDevice 列表。其他改动对外接口无破坏
[2026-05-13] 好感度系统 P1 审查修复 C — 0006 数据迁移幂等性修正(CR-003)
配套审查报告:docs/REVIEW-affinity-P1.md(CR-003) 配套修复报告:docs/REVIEW-affinity-P1-FIX-REPORT.md
- 文件路径:
userapp/migrations/0006_migrate_favorability_to_userdevice.py(重写 — 完整替换 forward/backward 函数内容,改用 AffinityLog source='data_migration' 标记做幂等)userapp/models.py(修改 —AffinityLog.SOURCE_CHOICES追加('data_migration', '数据迁移')选项)userapp/migrations/0008_alter_affinitylog_source_choices.py(新建 — 由 makemigrations 自动生成,更新 source 字段的 choices)
- 修改类型: 修复Bug + 重构
- 修改内容:
- CR-003(Critical):旧 0006 forward 用
target.favorability == 10做幂等判断,因 10 既是初始值也是衰减下限附近常见值,重跑会覆盖合法数据;backward 同样用!= 10反向判断会丢失衰减回 10 的数据 - 新实现 forward:用
AffinityLog.objects.filter(device_id=target.id, source='data_migration').exists()做幂等标记,同时写入审计 metadata(from_user_favorability/before_device_favorability/migration文件名) - 新实现 backward:遍历 source='data_migration' 的 AffinityLog 反向恢复 ParadiseUser.favorability,并删除标记保证可循环
- 模型层补
'data_migration'到 SOURCE_CHOICES(Python 校验,0008 AlterField 同步)
- CR-003(Critical):旧 0006 forward 用
- 已知风险(option B 选择说明):
- 选择直接重写 0006(而非追加 0007 补偿)是因为 dev DB 已执行过一次但 migrate_count=0,等于未动数据
- django_migrations 表已记录 0006 完成,重写后不会自动重跑;如需对意外脏数据强制重跑,需手工
python manage.py migrate userapp 0005 --fake后再migrate userapp 0006 - 生产环境部署前必须确认 prod 还未跑过 0006,否则需走 fake-reverse 流程
- 修改原因: P1 审查指出(详见 REVIEW-affinity-P1.md CR-003)旧幂等条件
favorability == 10在 P3/P4 衰减跑起来后会变成"定时炸弹",重跑迁移会覆盖正常业务数据;backward 会丢数据 - 跨项目联动: 无 — 仅服务端迁移逻辑变更
[2026-05-13] 好感度系统 P1 审查修复 B — Affinity 模型 DB CHECK 约束 + WR-001 单例硬约束(CR-002 + WR-001)
配套审查报告:docs/REVIEW-affinity-P1.md(CR-002 + WR-001) 配套修复报告:docs/REVIEW-affinity-P1-FIX-REPORT.md
- 文件路径:
userapp/models.py(修改 —AffinityRule.Meta.constraints加 5 条 CheckConstraint +clean();AffinityLevel.Meta.constraints加 2 条 CheckConstraint +clean()+save()自动 full_clean;AffinitySetting.Meta.constraints加 6 条 CheckConstraint(含 pk=1 单例硬约束)+clean()+save()强制 pk=1;imports 段补 CheckConstraint / F / ValidationError)userapp/migrations/0007_add_affinity_check_constraints.py(新建 — 由 makemigrations 自动生成,13 条 AddConstraint)
- 修改类型: 新增 + 修复Bug
- 修改内容:
- CR-002(Critical):
- AffinityRule:
min_change ≤ max_change/cooldown_seconds ≥ 0/single_cap > 0/daily_cap > 0/ 陪伴时长规则必须设置min_continuous_minutes > 0 ∧ max_count_per_day > 0 - AffinityLevel:
min_affinity ≤ max_affinity/reward_currency ≥ 0 - AffinitySetting:
decay_min_decay ≤ decay_max_decay ≤ decay_cap/initial_affinity ≤ max_affinity/decay_min_floor ≤ max_affinity/daily_cap > 0
- AffinityRule:
- WR-001(Warning):AffinitySetting 加
pk=1单例硬约束 + save() 强制 pk=1,配合形成事实单例(CHECK 约束跨行不可,但能阻止任何非 1 主键的写入) - 所有模型
clean()提供 Python 级兜底,给 DRF / admin 友好错误信息(DB 级 CheckConstraint 是最终防线) - AffinityLevel.save() 自动调 full_clean 触发跨等级区间不重叠校验(WR-009 多层兜底,详见 Commit D);提供
skip_clean=True后门给迁移 / fixture 场景
- CR-002(Critical):
- 修改原因: P1 审查指出(详见 REVIEW-affinity-P1.md CR-002)模型字段对管理后台 / shell / 直 SQL 写入毫无保护,P2 服务层
random.randint(rule.min_change, rule.max_change)等运算会被脏数据击穿(ValueError 抛出、冷却永久解锁、上限永远命中等);同时审查中 WR-001 指出 AffinitySetting 单例保证在并发下脆弱 - 跨项目联动: 无 — 仅服务端 DB 约束 + 模型校验层;管理后台前端在写入前应捕获 ValidationError 显示给运营,但接口契约本身未动
[2026-05-13] 好感度系统 P1 审查修复 A — UserDevice 软删语义修正(CR-001 + IN-005)
配套审查报告:docs/REVIEW-affinity-P1.md(CR-001 + IN-005) 配套修复报告:docs/REVIEW-affinity-P1-FIX-REPORT.md
- 文件路径:
device_interaction/models.py(修改 — 新增ActiveUserDeviceManager;UserDevice.is_active改名为is_bound;新增双 managerobjects/active;Meta.base_manager_name = 'objects'保证 admin 默认 queryset 不受 active 过滤影响)device_interaction/migrations/0004_rename_userdevice_is_active_is_bound.py(新建 —RenameField+AlterField更新 help_text)device_interaction/migrations/0005_alter_userdevice_options.py(新建 — 由 makemigrations 自动生成,记录base_manager_name='objects'的 Meta 变更)userapp/views.py(修改 — MAC 登录第 120 行UserDevice.objects.filter(...)→UserDevice.active.filter(...))device_interaction/views.py(修改 — 4 处调用点切换到UserDevice.active:bind_status第 462 行、绑定 endpoint 第 694/702 行两处、RTC token 第 1158 行)device_interaction/serializers.py(修改 — 第 125 行绑定校验切到UserDevice.active)qy_lty/CLAUDE.md(修改 — § "设备绑定与控制权" 新增硬规则:所有控制权解析查询必须使用UserDevice.active.filter(...);解释is_bound改名背景)
- 修改类型: 重构 + 修复Bug
- 修改内容:
- CR-001(Critical):4 处控制权解析调用点全部加
is_bound=True过滤,通过ActiveUserDeviceManager强制语义;避免 P2 软删(解绑设 is_bound=False)后旧绑定者被签发 user-token / 路由到错 user_id 的 WS 分组 / RTC 房间 - IN-005:
UserDevice.is_active→is_bound改名,消除与Device.is_active(设备激活态)的命名冲突 RenameField在 PostgreSQL 上是元数据级 ALTER COLUMN RENAME(O(1) 锁),无数据风险base_manager_name='objects'保证 Django admin 与反向关系(user.devices.all()、device.users.all())依然返回全集,仅UserDevice.active.filter()才过滤
- CR-001(Critical):4 处控制权解析调用点全部加
- 修改原因: P1 数据层代码审查指出(详见 REVIEW-affinity-P1.md CR-001)现有 4 处"按 MAC 取最新绑定者"的代码路径未过滤
is_active(P1-08 引入),一旦 P2 实现解绑=软删,会签发已解绑用户的 user-token、WS 路由到前主人频道等安全 / 越权风险。IN-005 与之共因(两个同名 is_active 字段语义截然不同),审查报告显式建议合并修复 - 跨项目联动: 无 — 仅服务端 ORM 层 + view 调用点变更,对客户端 / 管理后台无外显接口变化(响应 schema 未动)
[2026-05-08] Phase 3 — 客户端凭据槽位 GET 接口 + 阿里云日志 access_token 脱敏
配套 Phase:.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 +CredentialSlotClientViewAPIView 类,仅 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/
覆盖需求:CRED-03 + CRED-04
设计参考:1:1 复刻 aiapp.views.RTCChatHistoryAPIView(aiapp/views.py:434-555)的单 URL 多方法 APIView 风格
- 文件路径:
aiapp/serializers.py(修改 — 顶部 import 追加CredentialSlot,文件末尾追加CredentialSlotSerializerModelSerializer 类)aiapp/views.py(修改 — 顶部 import 追加CredentialSlot/CredentialSlotSerializer/mask_token/get_standardized_response_schema;文件末尾追加CredentialSlotPutRequestSchemaswagger 请求体 +_credential_slot_data_schema响应 data schema +CredentialSlotAdminViewAPIView 类)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 同期
[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/ 覆盖需求: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/
覆盖需求: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_idCharField(128, blank=True, default='')、access_tokenCharField(512, blank=True, default='')、updated_atDateTimeField(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(前序 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,保证唯一性。