瀏覽代碼

优化素材管理模块v1.1及开发文档调整

yawuga 2 周之前
父節點
當前提交
4c9102ac31

+ 53 - 57
src/views/base/mediAasset/index.vue

@@ -151,7 +151,6 @@
                 <template #tip>
                   <div class="el-upload__tip">
                     {{ form.id ? '如需替换素材文件,请重新上传;不上传则保留原文件。' : '支持 jpg/png/webp 图片和 mp4 视频,文件信息由系统自动解析。' }}
-                    <span class="upload-api-tip">当前上传功能需后端完成 /base/mediAasset/upload 接口后可用。</span>
                   </div>
                 </template>
               </el-upload>
@@ -202,9 +201,19 @@
     <!-- 素材预览 -->
     <el-dialog title="素材预览" v-model="previewOpen" width="820px" append-to-body @closed="previewAsset = {}">
       <div class="preview-box">
-        <img v-if="previewAsset.assetType === 'image'" :src="previewAsset.fileUrl" class="preview-image" />
-        <video v-else-if="previewAsset.assetType === 'video'" :src="previewAsset.fileUrl" class="preview-video"
-          controls />
+        <img
+          v-if="previewAsset.assetType === 'image'"
+          :src="previewAsset.fileUrl"
+          class="preview-image"
+          @error="handlePreviewError"
+        />
+        <video
+          v-else-if="previewAsset.assetType === 'video'"
+          :src="previewAsset.fileUrl"
+          class="preview-video"
+          controls
+          @error="handlePreviewError"
+        />
         <el-empty v-else description="暂无可预览内容" />
       </div>
     </el-dialog>
@@ -385,7 +394,7 @@ function submitForm() {
         })
       } else {
         addMediAasset(payload).then(() => {
-          proxy.$modal.msgSuccess("新增成功")
+          proxy.$modal.msgSuccess("上传成功")
           open.value = false
           getList()
         })
@@ -450,7 +459,7 @@ function formatDuration(seconds) {
 function beforeUpload(file) {
   const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'video/mp4']
   const isAllowed = allowedTypes.includes(file.type)
-  const isLt500M = file.size / 1024 / 1024 < 500
+  const isLt500M = file.size / 1024 / 1024 <= 500
   if (!isAllowed) {
     proxy.$modal.msgError('仅支持 jpg、png、webp 图片和 mp4 视频')
     return false
@@ -464,82 +473,71 @@ function beforeUpload(file) {
 
 
 function handleUploadSuccess(response) {
-  // 打印完整返回值以便调试
-  console.log('上传素材接口返回值:', response)
-
   const data = response.data || response || {}
   const isSuccess = response.code === 200 || response.code === undefined
-  
+
   if (!isSuccess) {
     proxy.$modal.msgError(response.msg || '上传失败')
     return
   }
-  
-  // --- 开始修改:重置文件相关属性,防止编辑时旧数据残留 ---
-  // 当重新上传文件时,应清除原有的文件特征信息,以新上传的文件为准
+
   form.value.assetType = null
   form.value.fileFormat = null
   form.value.mimeType = null
   form.value.durationSeconds = null
   form.value.resolution = null
   form.value.fileSize = null
-  // 注意:fileUrl 和 thumbnailUrl 稍后赋值,这里不重置,以免闪烁
-  // --- 结束修改 ---
 
-  // 1. 基础字段赋值
-  let rawName = data.assetName || data.originalFilename || data.fileName || data.newFileName || ''
-  
-  // 如果存在文件名,且当前表单中没有手动输入过名称(或者允许覆盖),则处理名称
+  const rawName = data.assetName || data.originalFilename || data.fileName || data.newFileName || ''
   if (rawName) {
-    // 使用正则表达式去除最后一个 '.' 及其后面的内容作为后缀
     const nameWithoutExt = rawName.replace(/\.[^/.]+$/, "")
-    // 只有在 form.value.assetName 为空或者我们希望强制覆盖时才赋值
-    // 通常上传成功后回显,我们直接赋值处理后的名称
-    // 如果用户之前修改过名称,这里可能会覆盖。如果希望保留用户修改的名称,可以加判断:
-    // if (!form.value.assetName || form.value.assetName === oldRawName) { ... }
-    // 但通常上传新文件意味着这是一个新素材,覆盖名称是预期行为
-    form.value.assetName = nameWithoutExt
-  } else {
-    // 如果没有文件名,保持原样或清空
-    form.value.assetName = form.value.assetName || ''
+    if (!form.value.assetName) {
+      form.value.assetName = nameWithoutExt
+    }
   }
 
   form.value.fileUrl = data.fileUrl || data.url || form.value.fileUrl
-  form.value.thumbnailUrl = data.thumbnailUrl || data.fileUrl || data.url || form.value.thumbnailUrl
-  
-  // 直接赋值后端返回的新值,如果后端没返回,则保持上面重置后的 null/undefined,模板中会显示 '-'
-  form.value.fileSize = data.fileSize
-  form.value.fileFormat = data.fileFormat
-  form.value.mimeType = data.mimeType
-  form.value.durationSeconds = data.durationSeconds
-  form.value.resolution = data.resolution
-  
-  // 2. 【新增逻辑】判断 assetType (image 或 video)
-  // 如果后端没有直接返回 assetType,则根据 mimeType 或文件格式推断
-  if (!form.value.assetType) {
+
+  if (data.assetType) {
+    form.value.assetType = String(data.assetType)
+  } else {
     const mimeType = (data.mimeType || '').toLowerCase()
     const fileFormat = (data.fileFormat || '').toLowerCase()
-    const fileName = (data.originalFilename || data.fileName || '').toLowerCase()
-    
-    if (mimeType.startsWith('video/') || fileFormat === 'mp4' || fileFormat === 'avi' || fileFormat === 'mov' || fileName.endsWith('.mp4')) {
+    const fileName = (data.originalFilename || data.fileName || data.newFileName || '').toLowerCase()
+    if (mimeType.startsWith('video/') || fileFormat === 'mp4' || fileName.endsWith('.mp4')) {
       form.value.assetType = 'video'
-    } else if (mimeType.startsWith('image/') || ['jpg', 'jpeg', 'png', 'webp', 'gif'].includes(fileFormat)) {
+    } else if (mimeType.startsWith('image/') || ['jpg', 'jpeg', 'png', 'webp'].includes(fileFormat) || /\.(jpg|jpeg|png|webp)$/.test(fileName)) {
       form.value.assetType = 'image'
     } else {
-      // 默认或者无法识别时,可以根据业务需求设定默认值
-      // form.value.assetType = 'image' 
+      form.value.assetType = null
     }
+  }
+
+  const nextAssetType = data.assetType ? String(data.assetType) : form.value.assetType
+  if (data.thumbnailUrl) {
+    form.value.thumbnailUrl = data.thumbnailUrl
+  } else if (nextAssetType === 'image') {
+    form.value.thumbnailUrl = data.fileUrl || data.url || form.value.fileUrl
   } else {
-    // 如果后端返回了 assetType,确保格式统一
-    form.value.assetType = String(data.assetType)
+    form.value.thumbnailUrl = null
   }
 
+  form.value.id = data.id || form.value.id
+  form.value.fileSize = data.fileSize
+  form.value.fileFormat = data.fileFormat
+  form.value.mimeType = data.mimeType
+  form.value.durationSeconds = data.durationSeconds
+  form.value.resolution = data.resolution
+  form.value.status = data.status != null ? String(data.status) : (form.value.status || '1')
+  form.value.quotedFlag = data.quotedFlag != null ? String(data.quotedFlag) : (form.value.quotedFlag || '0')
+
   proxy.$modal.msgSuccess('上传成功')
   proxy.$refs["mediAassetRef"]?.validateField?.("fileUrl")
 }
 
-function handleUploadError() {
-  proxy.$modal.msgError('素材上传接口暂不可用,请后端完成 /base/mediAasset/upload 后再联调')
+function handleUploadError(error) {
+  console.error('素材上传失败:', error)
+  proxy.$modal.msgError('素材上传失败,请检查文件格式、文件大小或稍后重试')
 }
 
 function handlePreview(row) {
@@ -558,6 +556,10 @@ function handlePreview(row) {
   previewOpen.value = true
 }
 
+function handlePreviewError() {
+  proxy.$modal.msgWarning('素材文件暂无法预览,请检查文件地址是否有效。')
+}
+
 function handleStatusChange(row, status) {
   const actionText = status === '1' ? '启用' : '停用'
   proxy.$modal.confirm('确认' + actionText + '素材“' + row.assetName + '”吗?').then(() => {
@@ -634,12 +636,6 @@ getList()
   margin-bottom: 12px;
 }
 
-.upload-api-tip {
-  display: block;
-  margin-top: 4px;
-  color: #e6a23c;
-}
-
 .file-url-link {
   max-width: 100%;
   word-break: break-all;

+ 125 - 46
迎宾巡逻安防机器人运维端Web管理系统详细设计开发文档_V2.1.html

@@ -78,7 +78,7 @@
   <div class="section" id="s5"><h2>5. 菜单结构设计</h2>
     <table><thead><tr><th>一级菜单</th><th>二级菜单</th><th>页面职责</th><th>一期优先级</th></tr></thead><tbody>
       <tr><td>首页</td><td>首页总览</td><td>展示机器人实时状态、摘要统计、异常告警与快捷操作入口。</td><td>P0</td></tr>
-      <tr><td rowspan="7">内容管理</td><td>欢迎语配置</td><td>维护机器人默认欢迎语和触发控制参数。</td><td>P0</td></tr><tr><td>问答库管理</td><td>维护 FAQ 问答数据,支持字典分类、相似问、导入导出;问答分类使用 RuoYi 字典,不单独建设问答分类管理菜单。</td><td>P0</td></tr><tr><td>素材管理</td><td>维护图片、视频素材。</td><td>P0</td></tr><tr><td>播放方案管理</td><td>维护素材播放编排关系、时长、顺序、默认方案。</td><td>P0</td></tr><tr><td>播报内容管理</td><td>维护可被播报任务引用的播报文本模板。</td><td>P0</td></tr><tr><td>播报任务管理</td><td>维护播报时间策略、频率、启停状态。</td><td>P0</td></tr><tr><td>展示主题配置</td><td>维护机器人对外展示界面的品牌与主题风格。</td><td>P1</td></tr>
+      <tr><td rowspan="7">内容管理</td><td>欢迎语配置</td><td>维护机器人默认欢迎语和触发控制参数。</td><td>P0</td></tr><tr><td>问答库管理</td><td>维护 FAQ 问答数据,支持字典分类、相似问、导出;一期暂不支持导入。问答分类使用 RuoYi 字典,不单独建设问答分类管理菜单。</td><td>P0</td></tr><tr><td>素材管理</td><td>维护图片、视频素材。</td><td>P0</td></tr><tr><td>播放方案管理</td><td>维护素材播放编排关系、时长、顺序、默认方案。</td><td>P0</td></tr><tr><td>播报内容管理</td><td>维护可被播报任务引用的播报文本模板。</td><td>P0</td></tr><tr><td>播报任务管理</td><td>维护播报时间策略、频率、启停状态。</td><td>P0</td></tr><tr><td>展示主题配置</td><td>维护机器人对外展示界面的品牌与主题风格。</td><td>P1</td></tr>
       <tr><td rowspan="3">访客管理</td><td>访客记录</td><td>查看访客登记记录,支持查询、详情、导出。</td><td>P1</td></tr><tr><td>预约记录</td><td>查看主控平台同步的预约记录。</td><td>P1</td></tr><tr><td>白名单管理</td><td>维护人员白名单数据,支持通过人脸照片、身份证号、手机号进行白名单匹配。</td><td>P1</td></tr>
       <tr><td rowspan="4">监控管理</td><td>视频预览</td><td>查看机器人摄像头实时画面。</td><td>P1</td></tr><tr><td>远程喊话</td><td>下发喊话内容、查看执行结果。</td><td>P1</td></tr><tr><td>对话日志</td><td>查看人机交互日志。</td><td>P1</td></tr><tr><td>安防告警日志</td><td>查看机器人侧安防告警记录。</td><td>P1</td></tr>
       <tr><td rowspan="6">运维管理</td><td>设备状态</td><td>查看详细设备状态、资源占用、模块状态。</td><td>P0</td></tr><tr><td>设备控制</td><td>提供一键充电、停止充电、重启、关机、服务重启等操作。</td><td>P0</td></tr><tr><td>运行参数配置</td><td>动态读取参数分组与字段,支持编辑与保存。</td><td>P0</td></tr><tr><td>系统诊断</td><td>查看诊断检查结果、自检结果与关键资源状态。</td><td>P1</td></tr><tr><td>日志中心</td><td>统一查看系统、设备、升级、操作等日志。</td><td>P0</td></tr><tr><td>软件版本 / OTA 升级</td><td>查看版本、上传安装包、执行升级、查看升级记录。</td><td>P0</td></tr>
@@ -100,9 +100,9 @@
       <tbody>
         <tr><td>首页</td><td>首页总览</td><td>定制开发</td><td>否</td><td>首页涉及机器人实时状态、统计卡片、告警摘要、快捷操作入口,属于聚合看板页面,不能直接按单表 CRUD 生成。</td></tr>
         <tr><td rowspan="7">内容管理</td><td>欢迎语配置</td><td>定制开发</td><td>不建议生成前端列表页</td><td>欢迎语配置为单配置页,不是多条数据 CRUD 页面。数据库通过 config_key=default 定位默认配置;前端建议自定义表单页,直接加载和保存默认配置,不提供列表、新增、删除。</td></tr>
-        <tr><td>问答库管理</td><td>RuoYi 主子表生成后定制</td><td>部分适合</td><td>可基于 robot_ops_faq、robot_ops_faq_similar 生成基础 CRUD;问题分类使用 RuoYi 字典 robot_faq_category,不单独生成问答分类管理页面;前端需将主子表明细表格调整为"相似问多行输入,一行一个"的交互方式;sortNo 作为保留字段,不在页面展示和编辑;启用/停用、导出、分类字典回显需按业务微调。一期暂不支持问答库导入,后续如运营需要批量维护再扩展。</td></tr>
-        <tr><td>素材管理</td><td>RuoYi 生成后定制</td><td>部分适合</td><td>可基于 robot_ops_media_asset 生成基础列表、查询、详情、编辑、删除接口和页面;上传素材、自动解析文件信息、缩略图/视频封面、图片/视频预览、引用状态维护、删除引用保护需要二次定制。前端只允许用户维护素材名称、启用状态和备注,文件信息由上传接口自动生成或解析。</td></tr>
-        <tr><td>播放方案管理</td><td>定制开发</td><td>否</td><td>涉及主子表、素材选择、拖拽排序、播放时长、复制方案、预览方案等复杂交互,建议 Cursor 或开发人员手工实现。</td></tr>
+        <tr><td>问答库管理</td><td>RuoYi 主子表生成后定制</td><td>部分适合</td><td>可基于 robot_ops_faq、robot_ops_faq_similar 生成基础 CRUD;问题分类使用 RuoYi 字典 robot_faq_category,不单独生成问答分类管理页面;前端需将主子表明细表格调整为“相似问多行输入,一行一个”的交互方式;sortNo 作为保留字段,不在页面展示和编辑;启用/停用、导出、分类字典回显需按业务微调。一期暂不支持问答库导入,后续如运营需要批量维护再扩展。</td></tr>
+        <tr><td>素材管理</td><td>RuoYi 生成后定制</td><td>部分适合</td><td>可基于 robot_ops_media_asset 生成基础列表、查询、详情、编辑、删除、导出接口和页面;当前前端已基于 RuoYi 接口和 /common/uploadMediaFile 上传接口完成定制。前端只允许用户维护素材名称、启用状态和备注,文件信息由上传接口返回或系统维护;缩略图展示、图片/视频预览、引用状态展示、删除引用保护提示均需定制。</td></tr>
+        <tr><td>播放方案管理</td><td>RuoYi 主子表生成后定制</td><td>部分适合</td><td>播放方案由主表 robot_ops_play_plan 和子表 robot_ops_play_plan_item 组成,适合先使用 RuoYi 主子表生成基础列表、表单、Controller、Service、Mapper,再进行定制。前端需将原始子表表格调整为“选择素材 + 素材编排”交互,支持素材选择、顺序调整、图片停留时长、视频默认播完切换、设为默认、启用/停用、复制方案和预览方案。默认方案唯一性、素材 quotedFlag 维护、删除默认方案限制等需由后端补充业务逻辑。</td></tr>
         <tr><td>播报内容管理</td><td>RuoYi 生成后微调</td><td>适合</td><td>典型单表 CRUD,可基于 robot_ops_broadcast_content 生成,再补充测试播报按钮。</td></tr>
         <tr><td>播报任务管理</td><td>RuoYi 生成后定制</td><td>部分适合</td><td>基础 CRUD 可生成;时间段、频率、循环规则、复制任务等需要定制表单校验和交互。</td></tr>
         <tr><td>展示主题配置</td><td>RuoYi 生成后定制</td><td>部分适合</td><td>基础 CRUD 可生成;Logo/背景上传、颜色选择器、主题预览、设为启用需要定制。</td></tr>
@@ -156,18 +156,39 @@
 <tr><td>查询条件</td><td>素材名称、素材类型、启用状态、上传时间范围。</td></tr>
 <tr><td>列表字段</td><td>缩略图(thumbnailUrl)、素材名称(assetName)、素材类型(assetType)、文件格式(fileFormat)、文件大小(fileSize)、视频时长(durationSeconds)、分辨率(resolution)、上传时间(createTime)、引用状态(quotedFlag)、启用状态(status)、操作。</td></tr>
 <tr><td>操作按钮</td><td>上传素材、预览、编辑、删除、启用/停用、批量删除。</td></tr>
-<tr><td>编辑字段</td><td>素材名称(assetName)、启用状态(status)、备注(remark)。文件地址、缩略图、文件大小、文件格式、MIME 类型、视频时长、分辨率等信息由上传接口自动生成或解析,前端只读展示,不允许用户手动填写。</td></tr>
+<tr><td>编辑字段</td><td>用户只允许维护素材名称(assetName)、启用状态(status)、备注(remark)。文件地址、缩略图、文件大小、文件格式、MIME 类型、视频时长、分辨率、引用状态等信息由上传接口返回或系统维护,前端只读展示,不允许用户手动填写。</td></tr>
 <tr><td>素材类型</td><td>素材类型 assetType 为业务类型,建议使用 RuoYi 字典 media_asset_type,字典项:image=图片,video=视频。素材类型可由上传接口根据文件 MIME 类型或后缀自动判断。</td></tr>
-<tr><td>文件信息</td><td>fileUrl 为素材文件访问地址;thumbnailUrl 为缩略图地址,图片可等于 fileUrl,视频可为视频封面图;fileSize 为文件大小,单位字节;fileFormat 为文件格式/后缀;mimeType 为文件 MIME 类型;resolution 为分辨率;durationSeconds 为视频时长,图片素材为空。</td></tr>
-<tr><td>上传规则</td><td>图片支持 jpg/png/webp;视频支持 mp4;单文件大小默认上限 500MB。上传接口至少应返回 fileUrl、fileSize、fileFormat、mimeType、assetType;thumbnailUrl、resolution、durationSeconds 可根据后端解析能力逐步完善,前端需做好空值兜底展示。</td></tr>
+<tr><td>文件信息</td><td>fileUrl 为素材文件访问地址;thumbnailUrl 为缩略图地址,图片可等于 fileUrl,视频只有后端返回 thumbnailUrl 时才展示封面,否则前端显示默认视频图标;fileSize 为文件大小,单位字节;fileFormat 为文件格式/后缀;mimeType 为文件 MIME 类型;resolution 为分辨率;durationSeconds 为视频时长,图片素材为空。resolution、durationSeconds 允许为空。</td></tr>
+<tr><td>上传规则</td><td>图片支持 jpg/png/webp;视频支持 mp4;单文件大小默认上限 500MB。当前前端实际调用 /common/uploadMediaFile 上传素材文件,上传成功后回填 fileUrl、thumbnailUrl、fileSize、fileFormat、mimeType、durationSeconds、resolution、assetType 等字段。若部分字段为空,前端按默认图标或“-”兜底展示。</td></tr>
 <tr><td>引用状态</td><td>quotedFlag 表示素材是否被播放方案引用,由系统维护,前端只读展示,不允许编辑。删除素材时,后端必须检查播放方案明细表中的实际引用关系,不应仅依赖 quotedFlag。</td></tr>
-<tr><td>引用保护</td><td>被播放方案引用的素材不可直接删除,需先解除引用。接口应返回明确提示,例如"该素材已被 2 个播放方案引用,请先解除引用后再删除"。</td></tr>
+<tr><td>引用保护</td><td>被播放方案引用的素材不可直接删除,需先解除引用。接口应返回明确提示,例如该素材已被 2 个播放方案引用,请先解除引用后再删除"。</td></tr>
 <tr><td>预览规则</td><td>图片弹窗预览;视频弹窗播放器预览;视频无缩略图时,列表展示默认视频图标;无法播放时提示格式不支持。</td></tr>
 <tr><td>业务规则</td><td>启用状态为启用的素材可被新播放方案选择;停用素材不可被新播放方案选择,但历史播放方案已引用的停用素材仍需支持回显。</td></tr>
 </tbody></table>
-    <h4>6.3.4 播放方案管理页面</h4><table><thead><tr><th>模块</th><th>详细设计</th></tr></thead><tbody><tr><td>查询条件</td><td>方案名称、是否默认、启用状态。</td></tr><tr><td>列表字段</td><td>方案名称、素材数、循环方式、默认方案、启用状态、更新时间、操作。</td></tr><tr><td>编辑页面字段</td><td>方案名称(planName)、循环方式(loopMode)、是否默认(isDefault)、启用状态(status)、备注(remark)、素材明细列表(itemList)。</td></tr><tr><td>素材明细字段</td><td>素材 ID(assetId)、素材名称(assetName,关联素材表展示字段)、素材类型(assetType,关联素材表展示字段)、播放顺序(playOrder)、停留时长(staySeconds)、转场方式(transitionType)。</td></tr><tr><td>操作按钮</td><td>新增、编辑、复制、删除、设为默认、启用/停用、预览。</td></tr><tr><td>交互规则</td><td>支持拖拽排序;图片素材必须填写停留时长;视频素材默认播完切换。</td></tr></tbody></table>
+    <h4>6.3.4 播放方案管理页面</h4>
+<table><thead><tr><th>模块</th><th>详细设计</th></tr></thead><tbody>
+<tr><td>页面目标</td><td>维护机器人屏幕播放使用的素材编排方案。一个播放方案由主表信息和多个素材明细组成,素材来源于素材管理模块。</td></tr>
+<tr><td>开发方式</td><td>基于 RuoYi 主子表生成基础页面和接口,主表 robot_ops_play_plan,子表 robot_ops_play_plan_item,关联字段 plan_id。生成后前端需要定制为“选择素材 + 素材编排”的交互方式。</td></tr>
+<tr><td>查询条件</td><td>方案名称、默认方案标识(defaultFlag)、启用状态(status)。</td></tr>
+<tr><td>列表字段</td><td>方案名称(planName)、素材数量(assetCount)、循环方式(loopMode)、默认方案标识(defaultFlag)、启用状态(status)、更新时间(updateTime)、操作。</td></tr>
+<tr><td>主表编辑字段</td><td>方案名称(planName)、循环方式(loopMode)、默认方案标识(defaultFlag)、启用状态(status)、备注(remark)。</td></tr>
+<tr><td>素材明细字段</td><td>素材ID(assetId)、播放顺序(playOrder)、停留时长(staySeconds)、转场方式(transitionType,预留字段,默认 none)。素材名称、素材类型、文件地址、缩略图、视频时长、分辨率等展示字段不在明细表中冗余保存,由后端根据 assetId 关联 robot_ops_media_asset 返回。</td></tr>
+<tr><td>素材选择</td><td>新增素材明细时,通过“选择素材”弹窗从素材库选择启用状态的素材。前端保存时只提交 assetId、playOrder、staySeconds、transitionType 等播放编排字段;素材名称、素材类型、文件地址、缩略图、视频时长等信息由后端查询详情或预览时关联素材表返回。</td></tr>
+<tr><td>素材编排</td><td>播放方案明细支持上移、下移或拖拽排序。保存时根据当前顺序重新生成 playOrder。</td></tr>
+<tr><td>图片播放规则</td><td>当 assetType=image 时,staySeconds 必填,建议默认 10 秒,取值范围 1-3600 秒。</td></tr>
+<tr><td>视频播放规则</td><td>当 assetType=video 时,staySeconds 可为空,表示按视频自身播放。一期不做视频截断播放逻辑。</td></tr>
+<tr><td>转场方式</td><td>transitionType 作为预留字段,一期前端不展示、不编辑,默认值为 none。后续车端屏幕支持转场效果后,再开放配置,例如 none=无转场、fade=淡入淡出。</td></tr>
+<tr><td>操作按钮</td><td>新增、编辑、复制、删除、设为默认、启用/停用、预览。</td></tr>
+<tr><td>默认方案规则</td><td>同一时间仅允许一个播放方案 defaultFlag=1。只有启用状态的方案允许设为默认。设为默认时,后端应在事务中将其他方案 defaultFlag 置为 0,将当前方案 defaultFlag 置为 1。</td></tr>
+<tr><td>删除规则</td><td>默认方案不允许直接删除,需先设其他方案为默认或取消默认。删除方案时需要同步删除其素材明细,并重新计算相关素材的 quotedFlag。</td></tr>
+<tr><td>素材引用规则</td><td>播放方案新增、编辑、删除后,后端需要重新计算相关素材的 quotedFlag。只要素材被任意播放方案明细引用,则 quotedFlag=1;不再被任何播放方案引用,则 quotedFlag=0。</td></tr>
+<tr><td>预览规则</td><td>预览方案时按 playOrder 顺序展示素材;图片按 staySeconds 停留;视频使用 fileUrl 播放,默认播完后切换到下一个素材。若视频时长为空,不影响预览。</td></tr>
+<tr><td>业务规则</td><td>新增/编辑时至少选择一个素材。停用素材不可被新播放方案选择,但历史方案中已引用的停用素材仍需支持回显。</td></tr>
+<tr><td>素材信息回显</td><td>播放方案详情、编辑回显和预览接口需要关联 robot_ops_media_asset 返回素材展示信息,包括 assetName、assetType、fileUrl、thumbnailUrl、durationSeconds、resolution、assetStatus 等。明细表本身不保存这些快照字段。</td></tr>
+</tbody></table>
+    <div class="note">字典建议:loopMode 可使用 RuoYi 字典 play_plan_loop_mode,字典项:loop=循环播放,once=播放一次。transitionType 为预留字段,一期不开放配置。</div>
     <h4>6.3.5 播报内容管理页面</h4><table><thead><tr><th>模块</th><th>详细设计</th></tr></thead><tbody><tr><td>查询条件</td><td>内容名称、内容分类、启用状态。</td></tr><tr><td>列表字段</td><td>内容名称、内容分类、播报文本摘要、启用状态、更新时间、操作。</td></tr><tr><td>编辑字段</td><td>内容名称(contentName)、内容分类(contentType)、播报文本(broadcastText)、启用状态(status)、备注(remark)。</td></tr><tr><td>操作按钮</td><td>新增、编辑、删除、启用/停用、测试播报。</td></tr><tr><td>内容分类</td><td>通知、宣传、提示、安防提醒、自定义。</td></tr></tbody></table>
-    <h4>6.3.6 播报任务管理页面</h4><table><thead><tr><th>模块</th><th>详细设计</th></tr></thead><tbody><tr><td>查询条件</td><td>任务名称、循环类型、启用状态。</td></tr><tr><td>列表字段</td><td>任务名称、关联内容名称、开始时间、结束时间、播报频率(分钟)、循环类型、循环取值、启用状态、操作。</td></tr><tr><td>编辑字段</td><td>任务名称(taskName)、关联播报内容(contentId,页面显示内容名称)、开始时间(startTime)、结束时间(endTime)、播报频率分钟数(frequencyMinutes,单位:分钟)、循环类型(cycleType)、循环取值(cycleValue)、启用状态(status)、备注(remark)。</td></tr><tr><td>cycleType</td><td>使用 RuoYi 字典 <code class="inline">broadcast_task_cycle_type</code>,字典值:1=按星期,2=按日期。</td></tr><tr><td>cycleValue</td><td>当 cycleType=1(按星期)时,保存星期值,1=星期一、2=星期二、3=星期三、4=星期四、5=星期五、6=星期六、7=星期日,多个值用英文逗号分隔,例如 1,2,3,4,5。当 cycleType=2(按日期)时,保存指定日期,多个日期用英文逗号分隔,例如 2026-03-20,2026-03-21。</td></tr><tr><td>操作按钮</td><td>新增、编辑、复制、删除、启用/停用。</td></tr><tr><td>校验规则</td><td>结束时间必须大于开始时间;frequencyMinutes 必须大于 0,单位为分钟;当循环类型为按星期时,至少选择一个星期;当循环类型为按日期时,至少选择一个指定日期。</td></tr><tr><td>交互规则</td><td>关联播报内容列表显示内容名称;新增/编辑时仅允许选择启用状态的播报内容;历史任务关联的播报内容如已停用,编辑时仍需可回显,并显示"已停用"提示,但不可重新选择停用内容。</td></tr></tbody></table>
+    <h4>6.3.6 播报任务管理页面</h4><table><thead><tr><th>模块</th><th>详细设计</th></tr></thead><tbody><tr><td>查询条件</td><td>任务名称、循环类型、启用状态。</td></tr><tr><td>列表字段</td><td>任务名称、关联内容名称、开始时间、结束时间、播报频率(分钟)、循环类型、循环取值、启用状态、操作。</td></tr><tr><td>编辑字段</td><td>任务名称(taskName)、关联播报内容(contentId,页面显示内容名称)、开始时间(startTime)、结束时间(endTime)、播报频率分钟数(frequencyMinutes,单位:分钟)、循环类型(cycleType)、循环取值(cycleValue)、启用状态(status)、备注(remark)。</td></tr><tr><td>cycleType</td><td>使用 RuoYi 字典 <code class="inline">broadcast_task_cycle_type</code>,字典值:1=按星期,2=按日期。</td></tr><tr><td>cycleValue</td><td>当 cycleType=1(按星期)时,保存星期值,1=星期一、2=星期二、3=星期三、4=星期四、5=星期五、6=星期六、7=星期日,多个值用英文逗号分隔,例如 1,2,3,4,5。当 cycleType=2(按日期)时,保存指定日期,多个日期用英文逗号分隔,例如 2026-03-20,2026-03-21。</td></tr><tr><td>操作按钮</td><td>新增、编辑、复制、删除、启用/停用。</td></tr><tr><td>校验规则</td><td>结束时间必须大于开始时间;frequencyMinutes 必须大于 0,单位为分钟;当循环类型为按星期时,至少选择一个星期;当循环类型为按日期时,至少选择一个指定日期。</td></tr><tr><td>交互规则</td><td>关联播报内容列表显示内容名称;新增/编辑时仅允许选择启用状态的播报内容;历史任务关联的播报内容如已停用,编辑时仍需可回显,并显示“已停用”提示,但不可重新选择停用内容。</td></tr></tbody></table>
     <h4>6.3.7 展示主题配置页面</h4><table><thead><tr><th>模块</th><th>详细设计</th></tr></thead><tbody><tr><td>查询条件</td><td>主题名称、启用状态。</td></tr><tr><td>列表字段</td><td>主题名称、Logo、背景资源、主色、启用状态、当前启用标识、更新时间、操作。</td></tr><tr><td>编辑字段</td><td>主题名称(themeName)、Logo 地址(logoUrl)、背景类型(backgroundType)、背景资源地址(backgroundUrl)、主色(primaryColor)、辅助色(secondaryColor)、欢迎标题(welcomeTitle)、欢迎副标题(welcomeSubTitle)、启用状态(status)。</td></tr><tr><td>操作按钮</td><td>新增、编辑、删除、设为启用、预览。</td></tr><tr><td>业务规则</td><td>同一时刻仅允许一个主题处于“当前启用”状态。</td></tr></tbody></table>
 
     <h3>6.4 访客管理</h3>
@@ -195,8 +216,8 @@
 <tr><td>页面目标</td><td>维护可被机器人识别为可信人员的白名单数据,支持通过人脸照片、身份证号、手机号等信息进行白名单匹配。</td></tr>
 <tr><td>查询条件</td><td>姓名、手机号、身份证号、人员类型、来源类型、启用状态。</td></tr>
 <tr><td>列表字段</td><td>姓名、手机号、身份证号、人员类型、是否有人脸照片、来源类型、有效期、启用状态、更新时间、操作。</td></tr>
-<tr><td>编辑字段</td><td>姓名(name)、人员类型(whitelistType,页面显示为"人员类型")、手机号(mobile)、身份证号(idCardNo)、人脸照片(faceImageUrl)、来源类型(sourceType,只读展示,不允许人工修改)、有效开始时间(validStartTime)、有效结束时间(validEndTime)、启用状态(status)、备注(remark)。</td></tr>
-<tr><td>人员类型</td><td>人员类型用于描述白名单人员身份,建议字典项为:内部人员、访客、VIP、其他。不建议将"人脸白名单"作为人员类型,因为人脸识别属于匹配方式,不属于人员身份类型。</td></tr>
+<tr><td>编辑字段</td><td>姓名(name)、人员类型(whitelistType,页面显示为“人员类型”)、手机号(mobile)、身份证号(idCardNo)、人脸照片(faceImageUrl)、来源类型(sourceType,只读展示,不允许人工修改)、有效开始时间(validStartTime)、有效结束时间(validEndTime)、启用状态(status)、备注(remark)。</td></tr>
+<tr><td>人员类型</td><td>人员类型用于描述白名单人员身份,建议字典项为:内部人员、访客、VIP、其他。不建议将“人脸白名单”作为人员类型,因为人脸识别属于匹配方式,不属于人员身份类型。</td></tr>
 <tr><td>操作按钮</td><td>新增、编辑、删除、导入、导出、启用/停用。</td></tr>
 <tr><td>表单校验</td><td>姓名、人员类型、来源类型、启用状态必填;手机号、身份证号、人脸照片三者至少填写一种;手机号填写时需符合大陆手机号格式;身份证号填写时需符合 18 位身份证基础格式;有效结束时间如填写,必须大于有效开始时间。</td></tr>
 <tr><td>人脸照片规则</td><td>一期仅保存人脸照片地址(faceImageUrl),不保存人脸特征ID。机器人侧按照片进行人脸比对,建议上传清晰正脸照片;前端上传限制为 2MB,支持 png/jpg/jpeg 格式。</td></tr>
@@ -237,9 +258,11 @@
     <h3>7.0 通用文件上传接口</h3>
     <table><thead><tr><th>接口</th><th>方法</th><th>说明</th><th>请求参数</th><th>返回字段</th><th>适用场景</th></tr></thead><tbody>
       <tr><td>/robot-ops/common/file/upload</td><td>POST</td><td>通用文件上传</td><td>文件(file)、业务类型(bizType)</td><td>文件地址(fileUrl)、文件名称(fileName)、文件大小(fileSize)、文件格式(fileFormat)</td><td>主题Logo、主题背景资源,普通附件等。素材库文件仍优先使用素材上传接口。</td></tr>
+      <tr><td>/common/uploadMediaFile</td><td>POST</td><td>素材文件上传</td><td>文件(file)</td><td>文件地址(fileUrl/url)、素材类型(assetType)、缩略图地址(thumbnailUrl)、文件大小(fileSize)、文件格式(fileFormat)、MIME 类型(mimeType)、视频时长(durationSeconds)、分辨率(resolution)等;具体字段以后端实际返回为准</td><td>当前素材管理前端实际使用的上传接口。上传后前端回填文件信息,再通过素材新增/编辑接口保存素材记录。</td></tr>
     </tbody></table>
     <div class="note">通用文件上传接口默认不单独建设文件记录表,上传后的 fileUrl 由具体业务表保存;如后续需要统一文件管理,再扩展附件表。</div>
-    <div class="note">素材库文件上传优先使用素材管理专用上传接口 /robot-ops/content/media/upload。素材上传接口除保存文件外,还需要生成或解析 assetType、fileSize、fileFormat、mimeType、thumbnailUrl、durationSeconds、resolution 等素材字段;通用文件上传接口不承担素材入库和素材元数据解析职责。</div>
+    <div class="note">当前素材管理前端实际使用 <code class="inline">/common/uploadMediaFile</code> 上传素材文件。上传接口负责保存文件并返回素材文件信息,前端回填 fileUrl、thumbnailUrl、fileSize、fileFormat、mimeType、durationSeconds、resolution、assetType 等字段;用户确认后再通过 RuoYi 生成的素材新增/编辑接口保存素材记录。</div>
+    <div class="note">开发设计中的 <code class="inline">/robot-ops/content/media/upload</code> 属于后续统一接口规划路径;本期前端已按当前项目实际接口 <code class="inline">/common/uploadMediaFile</code> 落地。</div>
 
 
     <h3>7.1 通用接口规范</h3>
@@ -351,7 +374,7 @@
       <tr><td>/robot-ops/content/welcome-config/test</td><td>POST</td><td>测试欢迎语播报</td><td>请求:配置标识(configKey,固定 default)、欢迎语文本(welcomeText)、启用欢迎语状态(status)、语音播报开关(voiceEnabled)。仅下发测试播报,不新增或修改配置数据。</td><td>不新增业务表,可写入 robot_ops_operate_log</td></tr>
     </tbody></table>
     <div class="note">欢迎语配置为单配置页,前端当前会在保存和测试播报时携带 configKey='default';后端也应支持不传 configKey 时默认按 default 处理。</div>
-    <div class="note">保存欢迎语配置时,后端建议按 config_key='default' 执行"有则更新,无则新增"的逻辑,避免初始化数据缺失时保存失败。</div>
+    <div class="note">保存欢迎语配置时,后端建议按 config_key='default' 执行“有则更新,无则新增”的逻辑,避免初始化数据缺失时保存失败。</div>
     <div class="note">恢复默认由前端本地重置表单完成,不单独调用后端恢复默认接口;用户点击“保存配置”后才真正写入数据库。</div>
     <div class="note">status 控制欢迎语功能整体是否启用;voiceEnabled 控制欢迎语触发时是否进行语音播报;cooldownSeconds 表示语音播报冷却时间,用于避免短时间内重复语音播报。</div>
 
@@ -373,38 +396,84 @@
 
     <h4>7.4.3 素材管理接口</h4>
 <table><thead><tr><th>接口</th><th>方法</th><th>说明</th><th>请求参数</th><th>返回/处理字段</th><th>数据库表</th></tr></thead><tbody>
-<tr><td>/robot-ops/content/media/page</td><td>GET</td><td>素材分页</td><td>素材名称(assetName)、素材类型(assetType)、启用状态(status)、上传时间范围、pageNum、pageSize</td><td>id、assetName、assetType、fileFormat、fileSize、durationSeconds、resolution、thumbnailUrl、quotedFlag、status、createTime</td><td>robot_ops_media_asset</td></tr>
-<tr><td>/robot-ops/content/media/{id}</td><td>GET</td><td>素材详情</td><td>素材ID(id)</td><td>id、assetName、assetType、fileUrl、thumbnailUrl、fileSize、fileFormat、mimeType、durationSeconds、resolution、quotedFlag、status、remark、createTime、updateTime</td><td>robot_ops_media_asset</td></tr>
-<tr><td>/robot-ops/content/media/upload</td><td>POST</td><td>上传素材</td><td>文件(file)、素材名称(assetName,可选,未填写时默认使用文件名)、启用状态(status,可选,默认启用)、备注(remark,可选)</td><td>素材ID(id)、素材名称(assetName)、素材类型(assetType)、文件地址(fileUrl)、缩略图地址(thumbnailUrl)、文件大小(fileSize)、文件格式(fileFormat)、MIME类型(mimeType)、视频时长(durationSeconds)、分辨率(resolution)、启用状态(status)</td><td>robot_ops_media_asset</td></tr>
-<tr><td>/robot-ops/content/media</td><td>PUT</td><td>编辑素材</td><td>素材ID(id)、素材名称(assetName)、启用状态(status)、备注(remark)</td><td>无</td><td>robot_ops_media_asset</td></tr>
-<tr><td>/robot-ops/content/media/{id}</td><td>DELETE</td><td>删除素材</td><td>素材ID(id)</td><td>无;如素材已被播放方案引用,应返回不可删除原因和引用数量</td><td>robot_ops_media_asset、robot_ops_play_plan_item</td></tr>
-<tr><td>/robot-ops/content/media/export</td><td>GET</td><td>导出素材</td><td>同分页查询条件</td><td>Excel文件;建议导出素材名称、素材类型、文件格式、文件大小、视频时长、分辨率、引用状态、启用状态、上传时间、备注</td><td>robot_ops_media_asset</td></tr>
+<tr><td>/base/mediAasset/list</td><td>GET</td><td>素材分页</td><td>素材名称(assetName)、素材类型(assetType)、启用状态(status)、引用状态(quotedFlag)、上传时间范围、pageNum、pageSize</td><td>id、assetName、assetType、fileFormat、fileSize、durationSeconds、resolution、thumbnailUrl、fileUrl、quotedFlag、status、createTime</td><td>robot_ops_media_asset</td></tr>
+<tr><td>/base/mediAasset/{id}</td><td>GET</td><td>素材详情</td><td>素材ID(id)</td><td>id、assetName、assetType、fileUrl、thumbnailUrl、fileSize、fileFormat、mimeType、durationSeconds、resolution、quotedFlag、status、remark、createTime、updateTime</td><td>robot_ops_media_asset</td></tr>
+<tr><td>/common/uploadMediaFile</td><td>POST</td><td>上传素材文件</td><td>文件(file)</td><td>返回文件信息:fileUrl/url、assetType、thumbnailUrl、fileSize、fileFormat、mimeType、durationSeconds、resolution 等。前端上传成功后回填文件信息。</td><td>不直接等同于普通 CRUD 接口;是否直接入库以后端实现为准。当前前端兼容返回 id 或不返回 id 两种情况。</td></tr>
+<tr><td>/base/mediAasset</td><td>POST</td><td>新增素材记录</td><td>assetName、assetType、fileUrl、thumbnailUrl、fileSize、fileFormat、mimeType、durationSeconds、resolution、status、quotedFlag、remark</td><td>新增后的素材ID</td><td>robot_ops_media_asset</td></tr>
+<tr><td>/base/mediAasset</td><td>PUT</td><td>编辑素材</td><td>id、assetName、assetType、fileUrl、thumbnailUrl、fileSize、fileFormat、mimeType、durationSeconds、resolution、status、quotedFlag、remark。页面仅允许用户维护 assetName、status、remark;其他字段由上传接口回填或系统维护。</td><td>无</td><td>robot_ops_media_asset</td></tr>
+<tr><td>/base/mediAasset/{ids}</td><td>DELETE</td><td>删除素材</td><td>素材ID或ID数组(ids)</td><td>无;如素材已被播放方案引用,应返回不可删除原因和引用数量</td><td>robot_ops_media_asset、robot_ops_play_plan_item</td></tr>
+<tr><td>/base/mediAasset/export</td><td>GET</td><td>导出素材</td><td>同分页查询条件</td><td>Excel文件;建议导出素材名称、素材类型、文件格式、文件大小、视频时长、分辨率、引用状态、启用状态、上传时间、备注</td><td>robot_ops_media_asset</td></tr>
 </tbody></table>
-<div class="note">素材上传接口需要在 RuoYi 生成的基础 CRUD 之外进行二次开发。RuoYi 生成器可生成列表、详情、编辑、删除等基础接口,但不会自动解析上传文件信息、生成视频封面、解析视频时长和分辨率。</div>
-<div class="note">上传接口一期至少应返回 fileUrl、fileSize、fileFormat、mimeType、assetType。thumbnailUrl、resolution、durationSeconds 可根据后端解析能力逐步完善;前端需对这些字段为空的情况做兜底展示。</div>
-<div class="note">素材类型 assetType 是业务类型,用于页面筛选和播放方案选择;mimeType 是文件 MIME 类型,用于技术校验、预览和文件响应处理;fileFormat 是文件后缀/格式,用于页面展示和导出。</div>
+<div class="note">当前素材管理前端基于 RuoYi 生成接口落地,列表、详情、新增、编辑、删除、导出使用 <code class="inline">/base/mediAasset</code> 相关接口;素材文件上传使用 <code class="inline">/common/uploadMediaFile</code>。</div>
+<div class="note">上传接口返回素材文件信息后,前端会回填 fileUrl、thumbnailUrl、fileSize、fileFormat、mimeType、durationSeconds、resolution、assetType 等字段;用户点击“确定”后,再通过新增或编辑接口保存素材记录。前端已兼容上传接口直接返回 id 的情况。</div>
+<div class="note">素材类型 assetType 是业务类型,用于页面筛选、播放方案选择和预览方式判断;mimeType 是文件 MIME 类型,用于技术校验、预览和文件响应处理;fileFormat 是文件后缀/格式,用于页面展示和导出。</div>
+<div class="note">thumbnailUrl 用于列表缩略图展示。图片素材没有独立缩略图时,前端可使用 fileUrl 作为缩略图;视频素材只有后端返回 thumbnailUrl 时才展示封面,否则前端展示默认视频图标,避免将 mp4 文件地址误作为图片缩略图。</div>
+<div class="note">durationSeconds、resolution 支持为空,前端以“-”兜底展示。若后端可解析图片/视频信息,可返回对应值;若暂时无法解析,不影响素材列表和预览功能。</div>
 <div class="note">quotedFlag 由后端根据播放方案明细引用关系维护,前端只读展示。删除素材时,后端必须检查 robot_ops_play_plan_item 是否存在引用,不能仅依赖 quotedFlag。</div>
 
     <h4>7.4.4 播放方案接口</h4>
     <table><thead><tr><th>接口</th><th>方法</th><th>说明</th><th>请求/返回字段</th><th>数据库表</th></tr></thead><tbody>
-      <tr><td>/robot-ops/content/play-plan/page</td><td>GET</td><td>播放方案分页</td><td>方案名称(planName)、是否默认(isDefault)、启用状态(status)、pageNum、pageSize;返回 id、planName、assetCount、loopMode、isDefault、status、updateTime</td><td>robot_ops_play_plan、robot_ops_play_plan_item</td></tr>
-      <tr><td>/robot-ops/content/play-plan/{id}</td><td>GET</td><td>播放方案详情</td><td>返回 planName、loopMode、isDefault、status、remark、itemList</td><td>robot_ops_play_plan、robot_ops_play_plan_item、robot_ops_media_asset</td></tr>
-      <tr><td>/robot-ops/content/play-plan</td><td>POST</td><td>新增播放方案</td><td>planName、loopMode、isDefault、status、remark、itemList</td><td>robot_ops_play_plan、robot_ops_play_plan_item</td></tr>
-      <tr><td>/robot-ops/content/play-plan</td><td>PUT</td><td>编辑播放方案</td><td>id、planName、loopMode、isDefault、status、remark、itemList</td><td>robot_ops_play_plan、robot_ops_play_plan_item</td></tr>
-      <tr><td>/robot-ops/content/play-plan/{id}</td><td>DELETE</td><td>删除播放方案</td><td>方案ID(id)</td><td>robot_ops_play_plan、robot_ops_play_plan_item</td></tr>
-      <tr><td>/robot-ops/content/play-plan/{id}/set-default</td><td>POST</td><td>设为默认方案</td><td>方案ID(id)</td><td>robot_ops_play_plan</td></tr>
-          <tr><td>/robot-ops/content/play-plan/{id}/copy</td><td>POST</td><td>复制播放方案</td><td>方案ID(id)</td><td>robot_ops_play_plan、robot_ops_play_plan_item</td></tr>
-      <tr><td>/robot-ops/content/play-plan/{id}/preview</td><td>GET</td><td>预览播放方案</td><td>方案ID(id)</td><td>robot_ops_play_plan、robot_ops_play_plan_item、robot_ops_media_asset</td></tr>
+      <tr><td>/robot-ops/content/play-plan/page</td><td>GET</td><td>播放方案分页</td><td>请求:方案名称(planName)、默认方案标识(defaultFlag)、启用状态(status)、pageNum、pageSize;返回:id、planName、assetCount、loopMode、defaultFlag、status、updateTime</td><td>robot_ops_play_plan</td></tr>
+      <tr><td>/robot-ops/content/play-plan/{id}</td><td>GET</td><td>播放方案详情</td><td>返回:id、planName、assetCount、loopMode、defaultFlag、status、remark、itemList;itemList 从明细表返回 assetId、playOrder、staySeconds、transitionType,并通过关联 robot_ops_media_asset 补充 assetName、assetType、fileUrl、thumbnailUrl、durationSeconds、resolution、assetStatus 等展示字段。</td><td>robot_ops_play_plan、robot_ops_play_plan_item、robot_ops_media_asset</td></tr>
+      <tr><td>/robot-ops/content/play-plan</td><td>POST</td><td>新增播放方案</td><td>请求:planName、loopMode、defaultFlag、status、remark、itemList。itemList 只需提交 assetId、playOrder、staySeconds、transitionType;其中 transitionType 为预留字段,一期可不传,由后端默认写入 none;新增时至少包含一个素材明细;assetCount 由后端根据 itemList 数量计算。后端保存时需校验 assetId 对应素材是否存在且可用。</td><td>robot_ops_play_plan、robot_ops_play_plan_item、robot_ops_media_asset</td></tr>
+      <tr><td>/robot-ops/content/play-plan</td><td>PUT</td><td>编辑播放方案</td><td>请求:id、planName、loopMode、defaultFlag、status、remark、itemList。itemList 只需提交 assetId、playOrder、staySeconds、transitionType;其中 transitionType 为预留字段,一期可不传,由后端默认写入 none;保存时以后端接收的 itemList 为准重建或更新明细;assetCount 由后端重新计算。后端保存时需校验 assetId 对应素材是否存在。</td><td>robot_ops_play_plan、robot_ops_play_plan_item、robot_ops_media_asset</td></tr>
+      <tr><td>/robot-ops/content/play-plan/{id}</td><td>DELETE</td><td>删除播放方案</td><td>方案ID(id)。默认方案不允许直接删除;删除时同步删除明细并重新计算相关素材 quotedFlag。</td><td>robot_ops_play_plan、robot_ops_play_plan_item、robot_ops_media_asset</td></tr>
+      <tr><td>/robot-ops/content/play-plan/{id}/set-default</td><td>POST</td><td>设为默认方案</td><td>方案ID(id)。仅启用状态方案允许设为默认;后端事务中将其他方案 defaultFlag 置为 0,当前方案置为 1。</td><td>robot_ops_play_plan</td></tr>
+      <tr><td>/robot-ops/content/play-plan/{id}/copy</td><td>POST</td><td>复制播放方案</td><td>方案ID(id)。复制主表和明细表,复制后的方案 defaultFlag 默认 0,方案名称建议追加“副本”。</td><td>robot_ops_play_plan、robot_ops_play_plan_item</td></tr>
+      <tr><td>/robot-ops/content/play-plan/{id}/preview</td><td>GET</td><td>预览播放方案</td><td>方案ID(id)。返回方案基本信息和按 playOrder 排序后的 itemList;itemList 需关联素材表返回 assetName、assetType、fileUrl、thumbnailUrl、durationSeconds、resolution、assetStatus 等字段,用于前端预览。</td><td>robot_ops_play_plan、robot_ops_play_plan_item、robot_ops_media_asset</td></tr>
     </tbody></table>
-    <div class="code">itemList 字段示例:
+    <div class="code">新增/编辑播放方案请求 itemList 示例:
+[
+  {
+    "assetId": 1,
+    "playOrder": 1,
+    "staySeconds": 10,
+    "transitionType": "none"
+  },
+  {
+    "assetId": 2,
+    "playOrder": 2,
+    "staySeconds": null,
+    "transitionType": "none"
+  }
+]
+播放方案详情/预览返回 itemList 示例:
 [
   {
     "assetId": 1,
+    "assetName": "大厅欢迎背景图",
+    "assetType": "image",
+    "fileUrl": "/profile/upload/media/welcome-bg.jpg",
+    "thumbnailUrl": "/profile/upload/media/welcome-bg.jpg",
+    "durationSeconds": null,
+    "resolution": "1920x1080",
+    "assetStatus": "1",
     "playOrder": 1,
     "staySeconds": 10,
-    "transitionType": "fade"
+    "transitionType": "none"
+  },
+  {
+    "assetId": 2,
+    "assetName": "展厅轮播宣传视频",
+    "assetType": "video",
+    "fileUrl": "/profile/upload/media/showroom-video.mp4",
+    "thumbnailUrl": "/profile/upload/media/showroom-video.jpg",
+    "durationSeconds": 90,
+    "resolution": "1920x1080",
+    "assetStatus": "1",
+    "playOrder": 2,
+    "staySeconds": null,
+    "transitionType": "none"
   }
 ]</div>
+    <div class="note">播放方案保存时,后端应根据 itemList 数量更新主表 asset_count,并根据 itemList 顺序更新 play_order。</div>
+    <div class="note">说明:本节播放方案接口路径为规划接口路径。若本期基于 RuoYi 主子表生成播放方案代码,实际接口路径可能采用 RuoYi 生成风格,例如 /base/playPlan/list、/base/playPlan/{id}、/base/playPlan。前端开发和联调时应以实际生成的 API 文件路径为准,并在页面开发完成后同步本文档。</div>
+    <div class="note">播放方案明细表一期不保存素材快照字段,只保存 asset_id、play_order、stay_seconds、transition_type 等编排字段。播放方案详情、编辑回显和预览接口应根据 asset_id 关联 robot_ops_media_asset 返回 assetName、assetType、fileUrl、thumbnailUrl、durationSeconds、resolution、assetStatus 等展示字段。</div>
+    <div class="note">保存播放方案时,后端应根据 asset_id 校验素材是否存在。新增方案和新增明细时,仅允许选择启用状态的素材;历史方案中已引用的停用素材仍需支持详情回显和编辑回显,并提示素材已停用。</div>
+    <div class="note">新增、编辑、删除播放方案后,需要重新计算相关素材的 quoted_flag,确保素材管理列表中的引用状态准确。</div>
+    <div class="warn">默认方案唯一性由后端事务控制。设为默认时需将其他方案 default_flag 更新为 0,将当前方案 default_flag 更新为 1;默认方案不允许直接删除。</div>
+    <div class="note">transitionType / transition_type 为播放转场预留字段,一期前端不展示、不编辑。保存播放方案时,如前端未传 transitionType,后端默认写入 none。后续车端屏幕支持转场效果后,再开放 fade 等配置项。</div>
+    <div class="note">字典建议:loopMode 可使用 RuoYi 字典 play_plan_loop_mode,字典项:loop=循环播放,once=播放一次。transitionType 为预留字段,一期不开放配置。</div>
 
     <h4>7.4.5 播报内容与播报任务接口</h4>
     <table><thead><tr><th>接口</th><th>方法</th><th>说明</th><th>主要字段</th><th>数据库表</th></tr></thead><tbody>
@@ -455,7 +524,7 @@
     <div class="note">访客记录不设置 resultStatus 和 sourceType 作为页面字段。访客记录代表已完成登记的到访记录;登记失败、扫码失败、读卡失败、预约匹配失败等异常应进入日志中心或后续扩展的登记异常日志。</div>
     <div class="note">访客记录支持两类到访类型:APPOINTMENT=预约到访,WALK_IN=现场登记;登记方式支持 SCREEN=机器人端,H5=手机端。机器人端指访客在机器人屏幕完成登记;手机端指访客扫码后在 H5 页面完成登记。</div>
     <div class="note">预约记录由主控平台同步;预约到访访客在现场完成登记后生成访客记录,并通过 appointmentNo 关联预约记录。现场登记访客可不关联预约单号。</div>
-    <div class="note">白名单中的 whitelistType 字段在页面上显示为"人员类型",用于表示人员身份,如内部人员、访客、VIP、其他。人脸识别不作为人员类型,而是白名单匹配方式之一。</div>
+    <div class="note">白名单中的 whitelistType 字段在页面上显示为“人员类型”,用于表示人员身份,如内部人员、访客、VIP、其他。人脸识别不作为人员类型,而是白名单匹配方式之一。</div>
     <div class="note">白名单不单独设置识别方式字段。机器人侧可根据当前采集到的身份信息匹配白名单:人脸识别时通过 faceImageUrl 对应的人脸照片进行比对;刷身份证或输入身份证时匹配 idCardNo;输入手机号时匹配 mobile。任一方式匹配到启用且在有效期内的白名单人员,即视为白名单命中。</div>
     <div class="note">新增或编辑白名单时,mobile、idCardNo、faceImageUrl 三者至少填写一种;手机号填写时需符合大陆手机号格式,身份证号填写时需符合 18 位身份证基础格式;一期不保存人脸特征ID。</div>
     <div class="note">来源类型 sourceType 使用 RuoYi 字典 source_type,当前字典值为:1=本地录入,2=平台同步,3=机器人采集。sourceType 由系统自动赋值,运维后台新增和导入默认写入 1;主控平台同步写入 2;机器人采集写入 3。前端新增/编辑弹窗中只读展示来源类型,不允许人工选择。</div>
@@ -537,8 +606,8 @@
 
   <div class="section" id="s8"><h2>8. 数据库表设计</h2>
     <div class="note">说明:以下表结构按运维端主库设计,命名采用 <code class="inline">robot_ops_</code> 前缀。第 6 章括号内字段为前端/接口字段,采用 camelCase;第 8 章字段为数据库字段,采用 snake_case,例如 robotName 对应 robot_name。机器人基础信息、实时运行状态、资源占用状态、模块状态、视频流状态等由机器人侧实时接口提供,一期不建设 robot_ops_robot_info 和 robot_ops_device_status_snapshot 两张表。</div>
-    <div class="warn">数据库建表语句以 MySQL 8.x 为基线,字段中文说明通过 COMMENT 标注。若项目实际使用 RuoYi 默认字段规范,可在开发时结合 create_by、create_time、update_by、update_time、remark 等公共字段做统一封装。</div>
-    <div class="note">公共字段约定:除日志流水类表外,内容管理类、访客管理类、运维配置类、系统配置类等业务表统一包含 create_by、create_time、update_by、update_time 四个公共字段,用于记录创建人、创建时间、更新人和更新时间。日志流水类表以业务时间、操作人、处理人等字段为主,不强制补齐全部公共字段。</div>
+    <div class="warn">数据库建表语句以 MySQL 8.x 为基线,字段中文说明通过 COMMENT 标注。若项目实际使用 RuoYi 默认字段规范,可在开发时结合 create_by、create_time、update_by、update_time 等公共字段做统一封装;remark 作为业务备注字段,不要求所有表强制具备。</div>
+    <div class="note">公共字段约定:除日志流水类表外,内容管理类、访客管理类、运维配置类、系统配置类等业务表统一包含 create_by、create_time、update_by、update_time 四个公共字段,用于记录创建人、创建时间、更新人和更新时间。remark 为业务备注字段,不属于所有表强制必备字段;原则上用于后台可维护的主表、配置表和业务记录表。子表、明细表、日志流水表不强制添加 remark。日志类如需记录处理说明,应优先使用 result_msg、error_msg、handle_remark、description 等具备业务含义的字段。</div>
     <div class="note">问答分类使用 RuoYi 系统字典能力维护,不单独建设 robot_ops_faq_category 表。RuoYi 字典类型为 <code class="inline">robot_faq_category</code>,当前字典项为:1=问候寒暄,2=产品介绍,3=业务咨询,4=访客引导,5=场所引导,6=安防提示,7=设备使用,8=售后服务,9=常见问题,10=其他。</div>
 
     <h3>8.1 基础与权限表</h3>
@@ -591,7 +660,7 @@
   PRIMARY KEY (`id`),
   UNIQUE KEY `uk_robot_ops_welcome_config_key` (`config_key`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='欢迎语配置表';</div>
-    <div class="note">说明:欢迎语配置一期为单配置页,不作为多条数据列表管理。config_key 用于标识固定配置项,默认配置固定为 default;后端查询和保存时应以 config_key='default' 作为业务定位条件,避免因误插入多条数据导致无法判断当前配置。建议数据库初始化时预置一条 config_key='default' 的默认配置,同时保存接口应支持"有则更新,无则新增"。</div>
+    <div class="note">说明:欢迎语配置一期为单配置页,不作为多条数据列表管理。config_key 用于标识固定配置项,默认配置固定为 default;后端查询和保存时应以 config_key='default' 作为业务定位条件,避免因误插入多条数据导致无法判断当前配置。建议数据库初始化时预置一条 config_key='default' 的默认配置,同时保存接口应支持“有则更新,无则新增”。</div>
     <div class="note">说明:welcome_text 数据库预留 500 字长度,前端页面按产品规则限制最大 200 字,便于后续扩展欢迎语内容。</div>
     <div class="note">说明:status 控制欢迎语功能整体是否启用;voice_enabled 控制欢迎语触发后是否语音播报;cooldown_seconds 控制语音欢迎语的重复播报间隔。冷却期内再次检测到访客时,可仅做屏幕展示,不重复语音播报。</div>
     <div class="code">INSERT INTO `robot_ops_welcome_config`
@@ -657,16 +726,17 @@ VALUES
   KEY `idx_robot_ops_media_asset_quoted_flag` (`quoted_flag`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='素材资源表';</div>
 <div class="note">说明:asset_type 为业务素材类型,用于页面筛选、播放方案选择和预览方式判断;mime_type 为文件标准 MIME 类型,用于文件校验、响应和预览处理;file_format 为文件后缀/格式,用于页面展示和导出。</div>
-<div class="note">说明:file_url、thumbnail_url、file_size、file_format、mime_type、duration_seconds、resolution 等字段由上传接口自动生成或解析,前端不允许用户手动填写。duration_seconds 仅视频素材需要,图片素材为空。</div>
-<div class="note">说明:thumbnail_url 用于列表缩略图和视频封面展示。图片素材如未单独生成缩略图,可使用 file_url;视频素材如一期暂未生成封面图,可为空,前端展示默认视频图标。</div>
+<div class="note">说明:file_url、thumbnail_url、file_size、file_format、mime_type、duration_seconds、resolution 等字段由上传接口返回或后端解析生成,前端只读展示,不允许用户手动填写。duration_seconds 仅视频素材需要,图片素材为空;duration_seconds、resolution 允许为空,前端以“-”兜底展示。</div>
+<div class="note">说明:thumbnail_url 用于列表缩略图和视频封面展示。图片素材如未单独生成缩略图,可使用 file_url;视频素材只有后端返回 thumbnail_url 时才展示封面图,否则前端展示默认视频图标。</div>
 <div class="note">说明:quoted_flag 由系统维护,表示素材是否被播放方案引用。删除素材时,应以后端检查 robot_ops_play_plan_item 的实际引用关系为准。</div>
 
     <h4>8.2.5 播放方案表 robot_ops_play_plan</h4>
     <div class="code">CREATE TABLE `robot_ops_play_plan` (
   `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
   `plan_name` VARCHAR(100) NOT NULL COMMENT '播放方案名称',
-  `loop_mode` VARCHAR(20) DEFAULT 'loop' COMMENT '循环方式:loop循环播放,once播放一次',
-  `is_default` CHAR(1) NOT NULL DEFAULT '0' COMMENT '是否默认方案:0否,1是',
+  `loop_mode` VARCHAR(20) NOT NULL DEFAULT 'loop' COMMENT '循环方式:loop循环播放,once播放一次',
+  `default_flag` CHAR(1) NOT NULL DEFAULT '0' COMMENT '默认方案标识:0否,1是',
+  `asset_count` INT NOT NULL DEFAULT 0 COMMENT '素材数量',
   `status` CHAR(1) NOT NULL DEFAULT '1' COMMENT '启用状态:0停用,1启用',
   `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
   `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人',
@@ -674,26 +744,35 @@ VALUES
   `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人',
   `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   PRIMARY KEY (`id`),
-  KEY `idx_robot_ops_play_plan_default` (`is_default`),
+  KEY `idx_robot_ops_play_plan_default_flag` (`default_flag`),
   KEY `idx_robot_ops_play_plan_status` (`status`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='播放方案主表';</div>
+    <div class="note">说明:default_flag 替代 is_default,用于避免 Java/RuoYi 字段映射时与布尔 getter 命名产生混淆。前端字段为 defaultFlag。</div>
+    <div class="note">说明:asset_count 为素材数量,由后端根据播放方案明细 itemList 数量维护,前端不允许手动填写。</div>
+    <div class="note">说明:同一时间仅允许一个播放方案 default_flag=1,唯一性由后端事务控制,不依赖数据库唯一索引。</div>
 
     <h4>8.2.6 播放方案素材明细表 robot_ops_play_plan_item</h4>
     <div class="code">CREATE TABLE `robot_ops_play_plan_item` (
   `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
   `plan_id` BIGINT NOT NULL COMMENT '播放方案ID,关联robot_ops_play_plan.id',
   `asset_id` BIGINT NOT NULL COMMENT '素材ID,关联robot_ops_media_asset.id',
-  `play_order` INT DEFAULT 0 COMMENT '播放顺序,数字越小越靠前',
+  `play_order` INT NOT NULL DEFAULT 0 COMMENT '播放顺序,数字越小越靠前',
   `stay_seconds` INT DEFAULT NULL COMMENT '停留时长,图片必填,视频可为空',
-  `transition_type` VARCHAR(50) DEFAULT NULL COMMENT '转场方式',
+  `transition_type` VARCHAR(50) NOT NULL DEFAULT 'none' COMMENT '转场方式,预留字段:none无转场,fade淡入淡出',
   `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人',
   `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
   `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人',
   `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   PRIMARY KEY (`id`),
   KEY `idx_robot_ops_play_plan_item_plan_id` (`plan_id`),
-  KEY `idx_robot_ops_play_plan_item_asset_id` (`asset_id`)
+  KEY `idx_robot_ops_play_plan_item_asset_id` (`asset_id`),
+  KEY `idx_robot_ops_play_plan_item_order` (`plan_id`, `play_order`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='播放方案素材明细表';</div>
+    <div class="note">说明:播放方案明细表一期只保存素材引用关系和播放编排字段,不保存素材名称、类型、文件地址、缩略图、视频时长等快照字段。</div>
+    <div class="note">说明:asset_id 为素材真实关联字段;播放方案详情、编辑回显、预览接口需要通过 asset_id 关联 robot_ops_media_asset 返回素材名称、素材类型、文件地址、缩略图、视频时长、分辨率、素材状态等展示信息。</div>
+    <div class="note">说明:图片素材 stay_seconds 必填;视频素材 stay_seconds 可为空,表示按视频自身播放。一期不做视频截断播放逻辑。</div>
+    <div class="note">说明:transition_type 为预留字段,一期前端不展示、不编辑,默认值为 none。后续车端屏幕支持转场动画后,再开放 fade 等转场配置。</div>
+    <div class="note">设计取舍:一期不做播放方案发布版本冻结,不要求保存素材快照。这样表结构更简单,后端维护成本更低;后续如果需要"方案发布后素材内容不随素材库变更而变化",再扩展素材快照字段或播放方案发布版本表。</div>
 
     <h4>8.2.7 播报内容表 robot_ops_broadcast_content</h4>
     <div class="code">CREATE TABLE `robot_ops_broadcast_content` (
@@ -837,7 +916,7 @@ VALUES
   KEY `idx_robot_ops_whitelist_status` (`status`),
   KEY `idx_robot_ops_whitelist_valid_time` (`valid_start_time`, `valid_end_time`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='白名单表';</div>
-    <div class="note">说明:whitelist_type 当前页面显示为"人员类型",用于描述人员身份,不表示识别方式。建议字典项为 internal=内部人员、visitor=访客、vip=VIP、other=其他。</div>
+    <div class="note">说明:whitelist_type 当前页面显示为“人员类型”,用于描述人员身份,不表示识别方式。建议字典项为 internal=内部人员、visitor=访客、vip=VIP、other=其他。</div>
     <div class="note">说明:白名单不设置 recognition_type / auth_type 字段,因为同一人员可同时支持人脸照片、身份证号、手机号多种匹配方式。机器人侧根据实际采集到的信息选择对应字段进行匹配。</div>
     <div class="note">说明:一期不建设 face_feature_id 字段。当前人脸白名单采用照片比对方式,仅保存 face_image_url。</div>
     <div class="note">说明:source_type 使用 RuoYi 字典 source_type,当前字典项为 1=本地录入、2=平台同步、3=机器人采集。运维后台新增和导入的数据默认写入 1,平台同步和机器人采集数据由对应接口写入。</div>