You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

9.3 KiB

Changelog: 修复场景"景别"字段存储位置

日期: 2026-02-09
类型: Bug 修复
影响范围: 前端 - 场景信息编辑保存


问题描述

场景信息编辑时,"景别"字段(locationType)被错误地存储到数据库的 meta_data.location_type 中,而不是正确的 location 顶层字段。

根因分析

前端字段映射错误:

// ❌ 错误: usePreviewActions.ts - handleUpdateLocation
const metaData: Record<string, unknown> = {};
if (data.locationType !== undefined) 
  metaData.location_type = data.locationType;  // 错误存入 meta_data

// 顶层字段
if (data.setting !== undefined) 
  payload.location = data.setting;  // setting 字段已不再使用

数据库模型定义:

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

// ✅ 修复后
try {
  // 构建 metadata:timeOfDay, weather, atmosphere, realWorldLocation
  const metaData: Record<string, unknown> = {};
  // 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<string, unknown> = {};
  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

# ❌ 修复前: 合并策略
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

// ❌ 修复前
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实施)

ProjectLocationResponseProjectPropResponse 添加字段提升,与 ProjectCharacterResponse 保持一致。

server/app/schemas/project_element.py

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 响应示例:

{
  "location_id": "xxx",
  "name": "街道",
  "location": "外景",
  "time_of_day": "白天",      //  提升到顶层
  "weather": "晴天",          //  提升到顶层
  "atmosphere": "温馨",       //  提升到顶层
  "real_world_location": "横店", //  提升到顶层
  "meta_data": {
    "time_of_day": "白天",    // 同时保留在 meta_data
    "weather": "晴天"
  }
}

废弃字段

  • setting → 已移除,改用 locationTypelocation

数据流

前端展示
  ↑ 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.locationlocationType → 显示在"景别"字段
  2. 前端编辑: 用户修改"景别" → data.locationType
  3. 前端保存: data.locationTypepayload.location → 发送到后端
  4. 后端保存: payload.locationlocation.location → 存入数据库
  5. 前端读取: 数据库 location → API 响应 locationDetail.location → 映射到 locationType

影响范围

  • 修复: 场景"景别"字段正确存储到 location 字段
  • 无破坏性变更: 仅修改字段映射逻辑
  • ⚠️ 历史数据: 如果之前有场景数据将 location_type 存入 meta_data,需要手动迁移

测试验证

手动测试步骤

  1. 打开场景详情面板
  2. 编辑"景别"字段(如:外景/内景)
  3. 点击"保存"
  4. 验证数据库:
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_dayweatheratmospherereal_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_locationupdate_prop 方法,应用与 update_character 相同的 JSONB 变更检测修复,确保 meta_data 字段正确更新到数据库。

参见: 2026-02-09-character-metadata-response-fix.md