# Changelog: 修复场景"景别"字段存储位置 **日期**: 2026-02-09 **类型**: Bug 修复 **影响范围**: 前端 - 场景信息编辑保存 --- ## 问题描述 场景信息编辑时,"景别"字段(locationType)被错误地存储到数据库的 `meta_data.location_type` 中,而不是正确的 `location` 顶层字段。 ## 根因分析 **前端字段映射错误**: ```typescript // ❌ 错误: usePreviewActions.ts - handleUpdateLocation const metaData: Record = {}; if (data.locationType !== undefined) metaData.location_type = data.locationType; // 错误存入 meta_data // 顶层字段 if (data.setting !== undefined) payload.location = data.setting; // setting 字段已不再使用 ``` **数据库模型定义**: ```python class ProjectLocation(SQLModel, table=True): location_id: UUID name: str location: Optional[str] # ← 这是"景别"应该存储的字段 description: Optional[str] meta_data: Dict[str, Any] # ← 不应该存储 location_type ``` ## 解决方案 ### 1. 前端保存逻辑修复 修改前端 `handleUpdateLocation` 函数,将 `locationType` 正确映射到数据库的 `location` 字段。 **`client/src/components/features/preview/hooks/usePreviewActions.ts`** ```typescript // ✅ 修复后 try { // 构建 metadata:timeOfDay, weather, atmosphere, realWorldLocation const metaData: Record = {}; // locationType 不再存入 metaData if (data.timeOfDay !== undefined) metaData.time_of_day = data.timeOfDay; if (data.weather !== undefined) metaData.weather = data.weather; if (data.atmosphere !== undefined) metaData.atmosphere = data.atmosphere; if (data.realWorldLocation !== undefined) metaData.real_world_location = data.realWorldLocation; // 构建顶层字段 const payload: Record = {}; if (data.name !== undefined) payload.name = data.name; if (data.description !== undefined) payload.description = data.description; // ✅ locationType(景别) 正确存入数据库的 location 字段 if (data.locationType !== undefined) payload.location = data.locationType; // 合并 meta_data if (Object.keys(metaData).length > 0) { payload.meta_data = metaData; } // ... } ``` ### 2. 后端 meta_data 更新策略修复 修改后端更新逻辑,使用**完全替换**而非**合并**策略,避免保留旧字段。 **`server/app/services/project_element_service.py`** ```python # ❌ 修复前: 合并策略 current_meta = dict(location.meta_data or {}) # 保留旧数据 current_meta.update(meta_updates) # 合并新数据 # 结果: 旧的 location_type 被保留 # ✅ 修复后: 替换策略 location.meta_data = dict(meta_updates) # 完全替换 # 结果: 只保留前端发送的字段 ``` ### 3. 前端数据读取逻辑修复 修改 `usePreviewData.ts` 中的场景数据映射,正确从 `location` 字段读取景别值。 **`client/src/components/features/preview/hooks/usePreviewData.ts`** ```typescript // ❌ 修复前 return tags.map((tag, index) => ({ // ... setting: locationDetail.location, // 错误映射 locationType: metadata.location_type as string | undefined, // 从不存在的 meta_data 读取 // ... })); // ✅ 修复后: 从顶层直接读取 return tags.map((tag, index) => ({ // ... locationType: locationDetail.location, timeOfDay: locationDetail.time_of_day, // 从顶层读取 weather: locationDetail.weather, atmosphere: locationDetail.atmosphere, realWorldLocation: locationDetail.real_world_location, // ... })); ``` ### 4. 后端 Schema 字段提升(方案2实施) 为 `ProjectLocationResponse` 和 `ProjectPropResponse` 添加字段提升,与 `ProjectCharacterResponse` 保持一致。 **`server/app/schemas/project_element.py`** ```python class ProjectLocationResponse(BaseModel): """项目场景响应""" # ... 现有字段 ... # 场景详细字段(从 meta_data 提升到顶层) time_of_day: Optional[str] = Field(None, description="时间") weather: Optional[str] = Field(None, description="天气") atmosphere: Optional[str] = Field(None, description="氛围") real_world_location: Optional[str] = Field(None, description="现实取景地参考") meta_data: Dict[str, Any] = Field(..., description="元数据") def model_post_init(self, __context: Any) -> None: """从 meta_data 中提取字段到顶层""" if self.meta_data: if not self.time_of_day and 'time_of_day' in self.meta_data: self.time_of_day = self.meta_data.get('time_of_day') # ... 其他字段同理 class ProjectPropResponse(BaseModel): """项目道具响应""" # ... 现有字段 ... # 道具详细字段(从 meta_data 提升到顶层) prop_type: Optional[str] = Field(None, description="道具类型") usage: Optional[str] = Field(None, description="使用方法/关键功能") meta_data: Dict[str, Any] = Field(..., description="元数据") def model_post_init(self, __context: Any) -> None: """从 meta_data 中提取字段到顶层""" if self.meta_data: if not self.prop_type and 'prop_type' in self.meta_data: self.prop_type = self.meta_data.get('prop_type') # ... ``` ## 字段映射对照表 ### 场景(Location)字段映射 | 前端字段 | API 响应位置 | 数据库存储位置 | 说明 | |---------|-------------|---------------|-----| | `name` | 顶层 | `name` (顶层) | 场景名称 | | `locationType` | 顶层 `location` | `location` (顶层) | **景别**(外景/内景) | | `description` | 顶层 | `description` (顶层) | 场景描述 | | `timeOfDay` | **顶层 `time_of_day`** | `meta_data.time_of_day` | 时间(白天/夜晚) | | `weather` | **顶层 `weather`** | `meta_data.weather` | 天气 | | `atmosphere` | **顶层 `atmosphere`** | `meta_data.atmosphere` | 氛围 | | `realWorldLocation` | **顶层 `real_world_location`** | `meta_data.real_world_location` | 现实取景地参考 | ### 道具(Prop)字段映射 | 前端字段 | API 响应位置 | 数据库存储位置 | 说明 | |---------|-------------|---------------|-----| | `name` | 顶层 | `name` (顶层) | 道具名称 | | `description` | 顶层 | `description` (顶层) | 道具描述 | | `propType` | **顶层 `prop_type`** | `meta_data.prop_type` | 道具类型 | | `usage` | **顶层 `usage`** | `meta_data.usage` | 使用方法/关键功能 | ### 字段提升说明 **方案**: 采用与角色(`ProjectCharacterResponse`)一致的字段提升模式 **API 响应示例**: ```json { "location_id": "xxx", "name": "街道", "location": "外景", "time_of_day": "白天", // ✅ 提升到顶层 "weather": "晴天", // ✅ 提升到顶层 "atmosphere": "温馨", // ✅ 提升到顶层 "real_world_location": "横店", // ✅ 提升到顶层 "meta_data": { "time_of_day": "白天", // 同时保留在 meta_data "weather": "晴天" } } ``` ### 废弃字段 - ~~`setting`~~ → 已移除,改用 `locationType` → `location` ## 数据流 ``` 前端展示 ↑ locationType: locationDetail.location usePreviewData.ts (读取) ↑ API: GET /projects/{id}/locations/{id} ↑ 数据库: project_locations.location = '外景' ↓ API: PUT /projects/{id}/locations/{id} ↓ payload.location = data.locationType handleUpdateLocation() (保存) ↓ locationType: '外景' 前端编辑 ``` ### 完整的数据循环 1. **前端展示**: `locationDetail.location` → `locationType` → 显示在"景别"字段 2. **前端编辑**: 用户修改"景别" → `data.locationType` 3. **前端保存**: `data.locationType` → `payload.location` → 发送到后端 4. **后端保存**: `payload.location` → `location.location` → 存入数据库 5. **前端读取**: 数据库 `location` → API 响应 `locationDetail.location` → 映射到 `locationType` ## 影响范围 - ✅ **修复**: 场景"景别"字段正确存储到 `location` 字段 - ✅ **无破坏性变更**: 仅修改字段映射逻辑 - ⚠️ **历史数据**: 如果之前有场景数据将 location_type 存入 meta_data,需要手动迁移 ## 测试验证 ### 手动测试步骤 1. 打开场景详情面板 2. 编辑"景别"字段(如:外景/内景) 3. 点击"保存" 4. 验证数据库: ```sql SELECT location_id, name, location, meta_data FROM project_locations ORDER BY updated_at DESC LIMIT 5; ``` ### 预期结果 - `location` 字段包含"景别"值(如 "外景") - `meta_data` 不包含 `location_type` 键 - `meta_data` 正确包含 `time_of_day`、`weather`、`atmosphere`、`real_world_location` ## 相关文件 **前端**: - `client/src/components/features/preview/hooks/usePreviewActions.ts` - 前端保存逻辑 - `client/src/components/features/preview/hooks/usePreviewData.ts` - 前端数据读取与映射 - `client/src/components/features/preview/types.ts` - 类型定义 - `client/src/components/features/preview/PreviewInfoPanel.tsx` - 前端编辑表单 **后端**: - `server/app/models/project_location.py` - 数据库模型 - `server/app/services/project_element_service.py` - 后端服务层 ## 额外修复 同时修复了场景和道具的 `update_location` 和 `update_prop` 方法,应用与 `update_character` 相同的 JSONB 变更检测修复,确保 `meta_data` 字段正确更新到数据库。 参见: [2026-02-09-character-metadata-response-fix.md](../../server/changelogs/2026-02-09-character-metadata-response-fix.md)