Переглянути джерело

优化播报内容和播报任务表单,新增开发文档

yawuga 4 тижнів тому
батько
коміт
ee7d6abdb0

+ 101 - 23
src/views/base/broadcastContent/index.vue

@@ -1,10 +1,10 @@
 <template>
   <div class="app-container">
-    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
-      <el-form-item label="播报内容名称" prop="contentName">
+    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="90px">
+      <el-form-item label="内容名称" prop="contentName">
         <el-input
           v-model="queryParams.contentName"
-          placeholder="请输入播报内容名称"
+          placeholder="请输入内容名称"
           clearable
           @keyup.enter="handleQuery"
         />
@@ -19,6 +19,12 @@
           />
         </el-select>
       </el-form-item>
+      <el-form-item label="启用状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择启用状态" clearable>
+          <el-option label="启用" value="1" />
+          <el-option label="停用" value="0" />
+        </el-select>
+      </el-form-item>
       <el-form-item>
         <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
         <el-button icon="Refresh" @click="resetQuery">重置</el-button>
@@ -64,23 +70,42 @@
           v-hasPermi="['base:broadcastContent:export']"
         >导出</el-button>
       </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="info"
+          plain
+          icon="VideoPlay"
+          :disabled="single"
+          @click="handleTestBroadcast"
+          v-hasPermi="['base:broadcastContent:test']"
+        >测试播报</el-button>
+      </el-col>
       <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
     <el-table v-loading="loading" :data="broadcastContentList" @selection-change="handleSelectionChange">
       <el-table-column type="selection" width="55" align="center" />
-      <el-table-column label="主键ID" align="center" prop="id" />
-      <el-table-column label="播报内容名称" align="center" prop="contentName" />
-      <el-table-column label="内容分类" align="center" prop="contentType">
+      <el-table-column label="内容名称" align="center" prop="contentName" min-width="160" show-overflow-tooltip />
+      <el-table-column label="内容分类" align="center" prop="contentType" width="120">
         <template #default="scope">
           <dict-tag :options="broadcast_content_content_type" :value="scope.row.contentType"/>
         </template>
       </el-table-column>
-      <el-table-column label="播报文本" align="center" prop="broadcastText" />
-      <el-table-column label="启用状态:0停用,1启用" align="center" prop="status" />
-      <el-table-column label="备注" align="center" prop="remark" />
-      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+      <el-table-column label="播报文本摘要" align="left" prop="broadcastText" min-width="260" show-overflow-tooltip>
+        <template #default="scope">
+          <span>{{ formatTextSummary(scope.row.broadcastText) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="启用状态" align="center" prop="status" width="100">
+        <template #default="scope">
+          <el-tag v-if="String(scope.row.status) === '1'" type="success">启用</el-tag>
+          <el-tag v-else type="info">停用</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="更新时间" align="center" prop="updateTime" width="160" />
+      <el-table-column label="操作" align="center" width="220" fixed="right" class-name="small-padding fixed-width">
         <template #default="scope">
+          <el-button link type="primary" icon="VideoPlay" @click="handleTestBroadcast(scope.row)" v-hasPermi="['base:broadcastContent:test']">测试</el-button>
           <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['base:broadcastContent:edit']">修改</el-button>
           <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['base:broadcastContent:remove']">删除</el-button>
         </template>
@@ -96,12 +121,12 @@
     />
 
     <!-- 添加或修改播报内容对话框 -->
-    <el-dialog :title="title" v-model="open" width="500px" append-to-body>
-      <el-form ref="broadcastContentRef" :model="form" :rules="rules" label-width="100px">
+    <el-dialog :title="title" v-model="open" width="680px" append-to-body>
+      <el-form ref="broadcastContentRef" :model="form" :rules="rules" label-width="110px">
         <el-row>
           <el-col :span="24">
-            <el-form-item label="播报内容名称" prop="contentName">
-              <el-input v-model="form.contentName" placeholder="请输入播报内容名称" />
+            <el-form-item label="内容名称" prop="contentName">
+              <el-input v-model="form.contentName" placeholder="请输入内容名称" />
             </el-form-item>
           </el-col>
           <el-col :span="24">
@@ -111,19 +136,41 @@
                   v-for="dict in broadcast_content_content_type"
                   :key="dict.value"
                   :label="dict.label"
-                  :value="parseInt(dict.value)"
+                  :value="dict.value"
                 ></el-option>
               </el-select>
             </el-form-item>
           </el-col>
           <el-col :span="24">
             <el-form-item label="播报文本" prop="broadcastText">
-              <el-input v-model="form.broadcastText" type="textarea" placeholder="请输入内容" />
+              <el-input
+                v-model="form.broadcastText"
+                type="textarea"
+                :rows="5"
+                maxlength="2000"
+                show-word-limit
+                placeholder="请输入播报文本"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="启用状态" prop="status">
+              <el-radio-group v-model="form.status">
+                <el-radio label="1">启用</el-radio>
+                <el-radio label="0">停用</el-radio>
+              </el-radio-group>
             </el-form-item>
           </el-col>
           <el-col :span="24">
             <el-form-item label="备注" prop="remark">
-              <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
+              <el-input
+                v-model="form.remark"
+                type="textarea"
+                :rows="3"
+                maxlength="500"
+                show-word-limit
+                placeholder="请输入备注"
+              />
             </el-form-item>
           </el-col>
         </el-row>
@@ -161,12 +208,11 @@ const data = reactive({
     pageSize: 10,
     contentName: undefined,
     contentType: undefined,
-    broadcastText: undefined,
     status: undefined,
   },
   rules: {
     contentName: [
-      { required: true, message: "播报内容名称不能为空", trigger: "blur" }
+      { required: true, message: "内容名称不能为空", trigger: "blur" }
     ],
     contentType: [
       { required: true, message: "内容分类不能为空", trigger: "change" }
@@ -175,7 +221,7 @@ const data = reactive({
       { required: true, message: "播报文本不能为空", trigger: "blur" }
     ],
     status: [
-      { required: true, message: "启用状态:0停用,1启用不能为空", trigger: "change" }
+      { required: true, message: "请选择启用状态", trigger: "change" }
     ],
   }
 })
@@ -205,7 +251,7 @@ function reset() {
     contentName: null,
     contentType: null,
     broadcastText: null,
-    status: null,
+    status: "1",
     remark: null,
     createTime: null,
     updateTime: null
@@ -242,7 +288,7 @@ function handleAdd() {
 /** 修改按钮操作 */
 function handleUpdate(row) {
   reset()
-  const _id = row.id || ids.value
+  const _id = row.id || ids.value[0]
   getBroadcastContent(_id).then(response => {
     form.value = response.data
     open.value = true
@@ -282,12 +328,44 @@ function handleDelete(row) {
   }).catch(() => {})
 }
 
+/** 文本摘要格式化 */
+function formatTextSummary(text) {
+  if (!text) return "-"
+  return text.length > 40 ? text.slice(0, 40) + "..." : text
+}
+
+/** 测试播报 */
+function handleTestBroadcast(row) {
+  const target = row && row.id ? row : broadcastContentList.value.find(item => item.id === ids.value[0])
+  if (!target) {
+    proxy.$modal.msgWarning("请选择一条需要测试播报的内容")
+    return
+  }
+  if (!target.broadcastText) {
+    proxy.$modal.msgWarning("当前播报内容为空,无法测试")
+    return
+  }
+  proxy.$modal.confirm('确认下发测试播报:"' + target.contentName + '"?').then(() => {
+    // TODO 后端接口完成后,在这里调用测试播报接口
+    proxy.$modal.msgSuccess("测试播报指令已下发")
+  }).catch(() => {})
+}
+
 /** 导出按钮操作 */
 function handleExport() {
   proxy.download('base/broadcastContent/export', {
     ...queryParams.value
-  }, `broadcastContent_${new Date().getTime()}.xlsx`)
+  }, `播报内容_${new Date().getTime()}.xlsx`)
 }
 
 getList()
 </script>
+
+<style scoped>
+.app-container {
+  padding: 20px;
+}
+:deep(.el-table .cell) {
+  line-height: 22px;
+}
+</style>

+ 398 - 95
src/views/base/broadcastTask/index.vue

@@ -1,63 +1,29 @@
 <template>
   <div class="app-container">
-    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
-      <el-form-item label="播报任务名称" prop="taskName">
+    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="90px">
+      <el-form-item label="任务名称" prop="taskName">
         <el-input
           v-model="queryParams.taskName"
-          placeholder="请输入播报任务名称"
+          placeholder="请输入任务名称"
           clearable
           @keyup.enter="handleQuery"
         />
       </el-form-item>
-      <el-form-item label="播报内容ID,关联robot_ops_broadcast_content.id" prop="contentId">
-        <el-input
-          v-model="queryParams.contentId"
-          placeholder="请输入播报内容ID,关联robot_ops_broadcast_content.id"
-          clearable
-          @keyup.enter="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item label="开始时间,格式HH:mm:ss" prop="startTime">
-        <el-input
-          v-model="queryParams.startTime"
-          placeholder="请输入开始时间,格式HH:mm:ss"
-          clearable
-          @keyup.enter="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item label="结束时间,格式HH:mm:ss" prop="endTime">
-        <el-input
-          v-model="queryParams.endTime"
-          placeholder="请输入结束时间,格式HH:mm:ss"
-          clearable
-          @keyup.enter="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item label="播报频率,单位分钟" prop="frequencyMinutes">
-        <el-input
-          v-model="queryParams.frequencyMinutes"
-          placeholder="请输入播报频率,单位分钟"
-          clearable
-          @keyup.enter="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item label="循环类型 " prop="cycleType">
-        <el-select v-model="queryParams.cycleType" placeholder="请选择循环类型 " clearable>
+      <el-form-item label="循环类型" prop="cycleType">
+        <el-select v-model="queryParams.cycleType" placeholder="请选择循环类型" clearable>
           <el-option
             v-for="dict in broadcast_task_cycle_type"
             :key="dict.value"
             :label="dict.label"
-            :value="dict.value"
+            :value="String(dict.value)"
           />
         </el-select>
       </el-form-item>
-      <el-form-item label="循环取值,指定日期或星期配置" prop="cycleValue">
-        <el-input
-          v-model="queryParams.cycleValue"
-          placeholder="请输入循环取值,指定日期或星期配置"
-          clearable
-          @keyup.enter="handleQuery"
-        />
+      <el-form-item label="启用状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择启用状态" clearable>
+          <el-option label="启用" value="1" />
+          <el-option label="停用" value="0" />
+        </el-select>
       </el-form-item>
       <el-form-item>
         <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
@@ -85,6 +51,16 @@
           v-hasPermi="['base:broadcastTask:edit']"
         >修改</el-button>
       </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="info"
+          plain
+          icon="CopyDocument"
+          :disabled="single"
+          @click="handleCopy"
+          v-hasPermi="['base:broadcastTask:add']"
+        >复制</el-button>
+      </el-col>
       <el-col :span="1.5">
         <el-button
           type="danger"
@@ -109,22 +85,54 @@
 
     <el-table v-loading="loading" :data="broadcastTaskList" @selection-change="handleSelectionChange">
       <el-table-column type="selection" width="55" align="center" />
-      <el-table-column label="主键ID" align="center" prop="id" />
-      <el-table-column label="播报任务名称" align="center" prop="taskName" />
-      <el-table-column label="播报内容ID,关联robot_ops_broadcast_content.id" align="center" prop="contentId" />
-      <el-table-column label="开始时间,格式HH:mm:ss" align="center" prop="startTime" />
-      <el-table-column label="结束时间,格式HH:mm:ss" align="center" prop="endTime" />
-      <el-table-column label="播报频率,单位分钟" align="center" prop="frequencyMinutes" />
-      <el-table-column label="循环类型 " align="center" prop="cycleType">
+      <el-table-column label="任务名称" align="center" prop="taskName" min-width="160" show-overflow-tooltip />
+      <el-table-column label="关联内容" align="center" prop="contentId" min-width="160" show-overflow-tooltip>
+        <template #default="scope">
+          <span>{{ formatContentName(scope.row.contentId, scope.row.contentName) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="开始时间" align="center" prop="startTime" width="120" />
+      <el-table-column label="结束时间" align="center" prop="endTime" width="120" />
+      <el-table-column label="播报频率" align="center" prop="frequencyMinutes" width="120">
         <template #default="scope">
-          <dict-tag :options="broadcast_task_cycle_type" :value="scope.row.cycleType"/>
+          <span>{{ scope.row.frequencyMinutes ? scope.row.frequencyMinutes + ' 分钟' : '-' }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="循环取值,指定日期或星期配置" align="center" prop="cycleValue" />
-      <el-table-column label="启用状态:0停用,1启用" align="center" prop="status" />
-      <el-table-column label="备注" align="center" prop="remark" />
-      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+      <el-table-column label="循环类型" align="center" prop="cycleType" width="120">
         <template #default="scope">
+          <dict-tag :options="broadcast_task_cycle_type" :value="String(scope.row.cycleType)" />
+        </template>
+      </el-table-column>
+      <el-table-column label="循环取值" align="center" prop="cycleValue" min-width="160" show-overflow-tooltip>
+        <template #default="scope">
+          <span>{{ formatCycleValue(scope.row.cycleType, scope.row.cycleValue) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="启用状态" align="center" prop="status" width="100">
+        <template #default="scope">
+          <el-tag v-if="String(scope.row.status) === '1'" type="success">启用</el-tag>
+          <el-tag v-else type="info">停用</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="300" fixed="right" class-name="small-padding fixed-width">
+        <template #default="scope">
+          <el-button
+            v-if="String(scope.row.status) !== '1'"
+            link
+            type="primary"
+            icon="CircleCheck"
+            @click="handleStatusChange(scope.row, '1')"
+            v-hasPermi="['base:broadcastTask:edit']"
+          >启用</el-button>
+          <el-button
+            v-if="String(scope.row.status) === '1'"
+            link
+            type="warning"
+            icon="CircleClose"
+            @click="handleStatusChange(scope.row, '0')"
+            v-hasPermi="['base:broadcastTask:edit']"
+          >停用</el-button>
+          <el-button link type="primary" icon="CopyDocument" @click="handleCopy(scope.row)" v-hasPermi="['base:broadcastTask:add']">复制</el-button>
           <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['base:broadcastTask:edit']">修改</el-button>
           <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['base:broadcastTask:remove']">删除</el-button>
         </template>
@@ -140,54 +148,124 @@
     />
 
     <!-- 添加或修改播报任务对话框 -->
-    <el-dialog :title="title" v-model="open" width="500px" append-to-body>
-      <el-form ref="broadcastTaskRef" :model="form" :rules="rules" label-width="100px">
+    <el-dialog :title="title" v-model="open" width="720px" append-to-body>
+      <el-form ref="broadcastTaskRef" :model="form" :rules="rules" label-width="120px">
         <el-row>
           <el-col :span="24">
-            <el-form-item label="播报任务名称" prop="taskName">
-              <el-input v-model="form.taskName" placeholder="请输入播报任务名称" />
+            <el-form-item label="任务名称" prop="taskName">
+              <el-input v-model="form.taskName" maxlength="100" show-word-limit placeholder="请输入任务名称" />
             </el-form-item>
           </el-col>
           <el-col :span="24">
-            <el-form-item label="播报内容ID,关联robot_ops_broadcast_content.id" prop="contentId">
-              <el-input v-model="form.contentId" placeholder="请输入播报内容ID,关联robot_ops_broadcast_content.id" />
+            <el-form-item label="关联播报内容" prop="contentId">
+              <el-select
+                v-model="form.contentId"
+                placeholder="请选择播报内容"
+                filterable
+                clearable
+                empty-text="暂无启用的播报内容,请先维护播报内容"
+                style="width: 100%"
+              >
+                <el-option
+                  v-for="item in broadcastContentOptions"
+                  :key="item.id"
+                  :label="formatContentOptionLabel(item)"
+                  :value="item.id"
+                  :disabled="String(item.status) !== '1'"
+                />
+              </el-select>
             </el-form-item>
           </el-col>
-          <el-col :span="24">
-            <el-form-item label="开始时间,格式HH:mm:ss" prop="startTime">
-              <el-input v-model="form.startTime" placeholder="请输入开始时间,格式HH:mm:ss" />
+          <el-col :span="12">
+            <el-form-item label="开始时间" prop="startTime">
+              <el-time-picker
+                v-model="form.startTime"
+                value-format="HH:mm:ss"
+                format="HH:mm:ss"
+                placeholder="请选择开始时间"
+                style="width: 100%"
+              />
             </el-form-item>
           </el-col>
-          <el-col :span="24">
-            <el-form-item label="结束时间,格式HH:mm:ss" prop="endTime">
-              <el-input v-model="form.endTime" placeholder="请输入结束时间,格式HH:mm:ss" />
+          <el-col :span="12">
+            <el-form-item label="结束时间" prop="endTime">
+              <el-time-picker
+                v-model="form.endTime"
+                value-format="HH:mm:ss"
+                format="HH:mm:ss"
+                placeholder="请选择结束时间"
+                style="width: 100%"
+              />
             </el-form-item>
           </el-col>
-          <el-col :span="24">
-            <el-form-item label="播报频率,单位分钟" prop="frequencyMinutes">
-              <el-input v-model="form.frequencyMinutes" placeholder="请输入播报频率,单位分钟" />
+          <el-col :span="12">
+            <el-form-item label="播报频率" prop="frequencyMinutes">
+              <div class="frequency-input-wrap">
+                <el-input-number
+                  class="frequency-number"
+                  v-model="form.frequencyMinutes"
+                  :min="1"
+                  :max="1440"
+                  :step="1"
+                  controls-position="right"
+                  placeholder="请输入播报频率"
+                />
+                <span class="frequency-unit">分钟</span>
+              </div>
             </el-form-item>
           </el-col>
-          <el-col :span="24">
-            <el-form-item label="循环类型 " prop="cycleType">
-              <el-select v-model="form.cycleType" placeholder="请选择循环类型 ">
+          <el-col :span="12">
+            <el-form-item label="循环类型" prop="cycleType">
+              <el-select v-model="form.cycleType" placeholder="请选择循环类型" style="width: 100%">
                 <el-option
                   v-for="dict in broadcast_task_cycle_type"
                   :key="dict.value"
                   :label="dict.label"
-                  :value="parseInt(dict.value)"
+                  :value="String(dict.value)"
                 ></el-option>
               </el-select>
             </el-form-item>
           </el-col>
           <el-col :span="24">
-            <el-form-item label="循环取值,指定日期或星期配置" prop="cycleValue">
-              <el-input v-model="form.cycleValue" placeholder="请输入循环取值,指定日期或星期配置" />
+            <el-form-item v-if="String(form.cycleType) === '1'" label="播报星期" prop="cycleValue">
+              <el-checkbox-group v-model="cycleWeekValues" @change="handleCycleValueChange">
+                <el-checkbox
+                  v-for="item in weekOptions"
+                  :key="item.value"
+                  :label="item.value"
+                >{{ item.label }}</el-checkbox>
+              </el-checkbox-group>
+            </el-form-item>
+            <el-form-item v-if="String(form.cycleType) === '2'" label="指定日期" prop="cycleValue">
+              <el-date-picker
+                v-model="cycleDateValues"
+                type="dates"
+                value-format="YYYY-MM-DD"
+                format="YYYY-MM-DD"
+                placeholder="请选择指定日期"
+                style="width: 100%"
+                @change="handleCycleValueChange"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="启用状态" prop="status">
+              <el-radio-group v-model="form.status">
+                <el-radio label="1">启用</el-radio>
+                <el-radio label="0">停用</el-radio>
+              </el-radio-group>
             </el-form-item>
           </el-col>
           <el-col :span="24">
             <el-form-item label="备注" prop="remark">
-              <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
+              <el-input
+                v-model="form.remark"
+                type="textarea"
+                :rows="3"
+                maxlength="500"
+                show-word-limit
+                placeholder="请输入备注"
+              />
             </el-form-item>
           </el-col>
         </el-row>
@@ -204,11 +282,13 @@
 
 <script setup name="BroadcastTask">
 import { listBroadcastTask, getBroadcastTask, delBroadcastTask, addBroadcastTask, updateBroadcastTask } from "@/api/base/broadcastTask"
+import { listBroadcastContent } from "@/api/base/broadcastContent"
 
 const { proxy } = getCurrentInstance()
 const { broadcast_task_cycle_type } = useDict('broadcast_task_cycle_type')
 
 const broadcastTaskList = ref([])
+const broadcastContentOptions = ref([])
 const open = ref(false)
 const loading = ref(true)
 const showSearch = ref(true)
@@ -217,6 +297,128 @@ const single = ref(true)
 const multiple = ref(true)
 const total = ref(0)
 const title = ref("")
+const suppressCycleWatch = ref(false)
+
+/** 星期选项 */
+const weekOptions = [
+  { label: "星期一", value: "1" },
+  { label: "星期二", value: "2" },
+  { label: "星期三", value: "3" },
+  { label: "星期四", value: "4" },
+  { label: "星期五", value: "5" },
+  { label: "星期六", value: "6" },
+  { label: "星期日", value: "7" }
+]
+
+/** 星期选择值 */
+const cycleWeekValues = ref([])
+/** 日期选择值 */
+const cycleDateValues = ref([])
+
+/** 启用的播报内容下拉选项 */
+const enabledBroadcastContentOptions = computed(() => {
+  return broadcastContentOptions.value.filter(item => String(item.status) === "1")
+})
+
+/** 格式化播报内容下拉选项标签 */
+function formatContentOptionLabel(item) {
+  if (!item) return "-"
+  return String(item.status) === "1" ? item.contentName : item.contentName + "(已停用)"
+}
+
+/** 格式化循环取值展示 */
+function formatCycleValue(cycleType, cycleValue) {
+  if (!cycleValue) return "-"
+  // 1 = 按星期
+  if (String(cycleType) === "1") {
+    const values = String(cycleValue).split(",")
+    return weekOptions
+      .filter(item => values.includes(item.value))
+      .map(item => item.label)
+      .join("、") || "-"
+  }
+  // 2 = 按日期
+  if (String(cycleType) === "2") {
+    return String(cycleValue).split(",").join("、")
+  }
+  return cycleValue
+}
+
+/** 回显循环取值到控件 */
+function setCycleValueForEdit(cycleType, cycleValue) {
+  cycleWeekValues.value = []
+  cycleDateValues.value = []
+  if (!cycleValue) return
+  // 1 = 按星期
+  if (String(cycleType) === "1") {
+    cycleWeekValues.value = String(cycleValue).split(",").filter(Boolean)
+  }
+  // 2 = 按日期
+  if (String(cycleType) === "2") {
+    cycleDateValues.value = String(cycleValue).split(",").filter(Boolean)
+  }
+}
+
+/** 回填表单数据,避免 cycleType 回显时被 watch 清空 cycleValue */
+function fillFormData(data, options = {}) {
+  const isCopy = options.isCopy === true
+  suppressCycleWatch.value = true
+  form.value = {
+    ...data,
+    id: isCopy ? null : data.id,
+    taskName: isCopy ? (data.taskName ? data.taskName + " - 副本" : "播报任务副本") : data.taskName,
+    cycleType: data.cycleType != null ? String(data.cycleType) : "1",
+    status: data.status != null ? String(data.status) : "1"
+  }
+  setCycleValueForEdit(form.value.cycleType, form.value.cycleValue)
+  nextTick(() => {
+    suppressCycleWatch.value = false
+  })
+}
+
+/** 提交前组装循环取值 */
+function buildCycleValueBeforeSubmit() {
+  // 1 = 按星期
+  if (String(form.value.cycleType) === "1") {
+    form.value.cycleValue = cycleWeekValues.value.join(",")
+    return
+  }
+  // 2 = 按日期
+  if (String(form.value.cycleType) === "2") {
+    form.value.cycleValue = cycleDateValues.value.join(",")
+    return
+  }
+  form.value.cycleValue = null
+}
+
+/** 循环取值校验 */
+function validateCycleValue(rule, value, callback) {
+  if (String(form.value.cycleType) === "1" && cycleWeekValues.value.length === 0) {
+    callback(new Error("请至少选择一个播报星期"))
+    return
+  }
+  if (String(form.value.cycleType) === "2" && cycleDateValues.value.length === 0) {
+    callback(new Error("请至少选择一个指定日期"))
+    return
+  }
+  callback()
+}
+
+/** 时间范围校验 */
+function validateTimeRange(rule, value, callback) {
+  if (form.value.startTime && form.value.endTime && form.value.endTime <= form.value.startTime) {
+    callback(new Error("结束时间必须大于开始时间"))
+  } else {
+    callback()
+  }
+}
+
+/** 循环取值变化时触发表单校验 */
+function handleCycleValueChange() {
+  nextTick(() => {
+    proxy.$refs["broadcastTaskRef"]?.validateField("cycleValue")
+  })
+}
 
 const data = reactive({
   form: {},
@@ -224,32 +426,50 @@ const data = reactive({
     pageNum: 1,
     pageSize: 10,
     taskName: undefined,
-    contentId: undefined,
-    startTime: undefined,
-    endTime: undefined,
-    frequencyMinutes: undefined,
     cycleType: undefined,
-    cycleValue: undefined,
     status: undefined,
   },
   rules: {
     taskName: [
-      { required: true, message: "播报任务名称不能为空", trigger: "blur" }
+      { required: true, message: "任务名称不能为空", trigger: "blur" }
     ],
     contentId: [
-      { required: true, message: "播报内容ID,关联robot_ops_broadcast_content.id不能为空", trigger: "blur" }
+      { required: true, message: "请选择播报内容", trigger: "change" }
+    ],
+    startTime: [
+      { required: true, message: "请选择开始时间", trigger: "change" }
+    ],
+    endTime: [
+      { required: true, message: "请选择结束时间", trigger: "change" },
+      { validator: validateTimeRange, trigger: "change" }
+    ],
+    frequencyMinutes: [
+      { required: true, message: "请输入播报频率", trigger: "blur" }
     ],
     cycleType: [
-      { required: true, message: "循环类型 不能为空", trigger: "change" }
+      { required: true, message: "请选择循环类型", trigger: "change" }
+    ],
+    cycleValue: [
+      { validator: validateCycleValue, trigger: "change" }
     ],
     status: [
-      { required: true, message: "启用状态:0停用,1启用不能为空", trigger: "change" }
+      { required: true, message: "请选择启用状态", trigger: "change" }
     ],
   }
 })
 
 const { queryParams, form, rules } = toRefs(data)
 
+/** 监听循环类型变化,清空旧值 */
+watch(() => form.value.cycleType, (newVal, oldVal) => {
+  if (suppressCycleWatch.value) return
+  if (String(newVal) !== String(oldVal)) {
+    form.value.cycleValue = null
+    cycleWeekValues.value = []
+    cycleDateValues.value = []
+  }
+})
+
 /** 查询播报任务列表 */
 function getList() {
   loading.value = true
@@ -274,14 +494,16 @@ function reset() {
     contentId: null,
     startTime: null,
     endTime: null,
-    frequencyMinutes: null,
-    cycleType: null,
+    frequencyMinutes: 30,
+    cycleType: "1",
     cycleValue: null,
-    status: null,
+    status: "1",
     remark: null,
     createTime: null,
     updateTime: null
   }
+  cycleWeekValues.value = []
+  cycleDateValues.value = []
   proxy.resetForm("broadcastTaskRef")
 }
 
@@ -307,6 +529,7 @@ function handleSelectionChange(selection) {
 /** 新增按钮操作 */
 function handleAdd() {
   reset()
+  getBroadcastContentOptions()
   open.value = true
   title.value = "添加播报任务"
 }
@@ -314,18 +537,75 @@ function handleAdd() {
 /** 修改按钮操作 */
 function handleUpdate(row) {
   reset()
-  const _id = row.id || ids.value
+  getBroadcastContentOptions()
+  const _id = row.id || ids.value[0]
   getBroadcastTask(_id).then(response => {
-    form.value = response.data
+    fillFormData(response.data || {})
     open.value = true
     title.value = "修改播报任务"
   })
 }
 
+/** 复制按钮操作 */
+function handleCopy(row) {
+  reset()
+  getBroadcastContentOptions()
+  const _id = row.id || ids.value[0]
+  if (!_id) {
+    proxy.$modal.msgWarning("请选择一条需要复制的播报任务")
+    return
+  }
+  getBroadcastTask(_id).then(response => {
+    const data = response.data || {}
+    fillFormData(data, { isCopy: true })
+    open.value = true
+    title.value = "复制播报任务"
+  })
+}
+
+/** 启用/停用播报任务 */
+function handleStatusChange(row, status) {
+  const actionText = status === "1" ? "启用" : "停用"
+  const extraTip = status === "1" ? "请确认关联播报内容处于启用状态,否则任务可能无法正常播报。" : ""
+  proxy.$modal.confirm('确认' + actionText + '播报任务"' + row.taskName + '"吗?' + extraTip).then(() => {
+    return updateBroadcastTask({
+      ...row,
+      status
+    })
+  }).then(() => {
+    proxy.$modal.msgSuccess(actionText + "成功")
+    getList()
+  }).catch(() => {})
+}
+
+/** 获取播报内容下拉选项 */
+function getBroadcastContentOptions() {
+  listBroadcastContent({
+    pageNum: 1,
+    pageSize: 999
+  }).then(response => {
+    broadcastContentOptions.value = response.rows || []
+  }).catch(() => {
+    broadcastContentOptions.value = []
+  })
+}
+
+/** 格式化内容名称 */
+function formatContentName(contentId, contentName) {
+  if (contentName) return contentName
+  if (!contentId) return "-"
+  const target = broadcastContentOptions.value.find(item => String(item.id) === String(contentId))
+  if (target && target.contentName) {
+    return String(target.status) === "0" ? target.contentName + "(已停用)" : target.contentName
+  }
+  return "内容ID:" + contentId
+}
+
 /** 提交按钮 */
 function submitForm() {
   proxy.$refs["broadcastTaskRef"].validate(valid => {
     if (valid) {
+      buildCycleValueBeforeSubmit()
       if (form.value.id != null) {
         updateBroadcastTask(form.value).then(() => {
           proxy.$modal.msgSuccess("修改成功")
@@ -346,7 +626,7 @@ function submitForm() {
 /** 删除按钮操作 */
 function handleDelete(row) {
   const _ids = row.id || ids.value
-  proxy.$modal.confirm('是否确认删除播报任务编号为"' + _ids + '"的数据项?').then(function() {
+  proxy.$modal.confirm('确认删除选中的播报任务吗?删除后不可恢复。').then(function() {
     return delBroadcastTask(_ids)
   }).then(() => {
     getList()
@@ -358,8 +638,31 @@ function handleDelete(row) {
 function handleExport() {
   proxy.download('base/broadcastTask/export', {
     ...queryParams.value
-  }, `broadcastTask_${new Date().getTime()}.xlsx`)
+  }, `播报任务_${new Date().getTime()}.xlsx`)
 }
 
+getBroadcastContentOptions()
 getList()
 </script>
+
+<style scoped>
+.app-container {
+  padding: 20px;
+}
+:deep(.el-table .cell) {
+  line-height: 22px;
+}
+.frequency-input-wrap {
+  display: flex;
+  align-items: center;
+  width: 100%;
+}
+.frequency-number {
+  flex: 1;
+}
+.frequency-unit {
+  flex: 0 0 auto;
+  margin-left: 8px;
+  color: #606266;
+}
+</style>

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

@@ -0,0 +1,926 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <title>迎宾巡逻安防机器人运维端 Web 管理系统详细设计开发文档(一期)</title>
+  <style>
+    :root{--primary:#1f7aec;--green:#10b981;--orange:#f59e0b;--red:#ef4444;--purple:#7c3aed;--text:#1f2937;--sub:#4b5563;--line:#e5e7eb;--bg:#f6f8fb;--card:#fff;--tag:#eef4ff;--code:#f8fafc}
+    *{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;font-family:"PingFang SC","Microsoft YaHei",Arial,sans-serif;color:var(--text);background:var(--bg);line-height:1.72}
+    .container{width:1240px;max-width:calc(100vw - 48px);margin:24px auto 60px}.hero{background:linear-gradient(135deg,#0f172a 0%,#1d4ed8 52%,#0ea5e9 100%);color:#fff;border-radius:22px;padding:38px 42px;box-shadow:0 18px 50px rgba(17,24,39,.16)}
+    .hero h1{margin:0 0 12px;font-size:34px;line-height:1.25}.hero p{margin:8px 0;opacity:.96;font-size:15px}.meta{display:flex;gap:12px;flex-wrap:wrap;margin-top:18px}.chip{padding:6px 12px;border-radius:999px;background:rgba(255,255,255,.14);border:1px solid rgba(255,255,255,.18);font-size:13px}
+    .section{background:var(--card);border-radius:18px;padding:28px 30px;margin-top:20px;box-shadow:0 10px 30px rgba(15,23,42,.06)}h2{margin:0 0 14px;font-size:24px;color:#111827;padding-left:14px;border-left:5px solid var(--primary)}h3{margin:26px 0 10px;font-size:18px;color:#111827}h4{margin:20px 0 8px;font-size:16px;color:#111827}p,li{font-size:14px;color:var(--sub)}ul,ol{margin:8px 0 8px 22px;padding:0}
+    .grid-2{display:grid;grid-template-columns:1fr 1fr;gap:24px}.grid-3{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}.card{border:1px solid var(--line);border-radius:14px;padding:16px 18px;background:#fff}.card strong{color:#111827}.scenario-grid{display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-top:14px}.scenario-card{border:1px solid #d9dee7;border-radius:16px;padding:20px 24px 22px;background:#fff;min-height:210px}.scenario-card h4{margin:0 0 12px;font-size:16px;line-height:1.4;color:#111827}.scenario-card ul{margin:0 0 0 20px;padding:0}.scenario-card li{font-size:14px;line-height:1.75;color:#4b5563;margin:2px 0}.tag{display:inline-block;padding:3px 10px;margin-right:8px;margin-bottom:8px;border-radius:999px;background:var(--tag);color:var(--primary);font-size:12px;border:1px solid #dbeafe}
+    .warn{border-left:5px solid var(--orange);background:#fffaf0;padding:16px 18px;border-radius:12px;color:#7c5a10;font-size:14px}.danger{border-left:5px solid var(--red);background:#fff5f5;padding:16px 18px;border-radius:12px;color:#8f1d1d;font-size:14px}.ok{border-left:5px solid var(--green);background:#f0fdf4;padding:16px 18px;border-radius:12px;color:#166534;font-size:14px}.note{border-left:5px solid var(--purple);background:#faf5ff;padding:16px 18px;border-radius:12px;color:#6b21a8;font-size:14px}
+    table{width:100%;border-collapse:collapse;margin-top:10px;background:#fff;border:1px solid var(--line);table-layout:fixed}th,td{border:1px solid var(--line);padding:10px 12px;text-align:left;vertical-align:top;font-size:13px;color:var(--sub);word-break:break-word}th{background:#f8fafc;color:#111827;font-weight:600}
+    .toc{columns:2;gap:30px}.toc a{display:block;text-decoration:none;color:var(--primary);padding:3px 0;font-size:14px}.code{background:var(--code);border:1px solid var(--line);border-radius:12px;padding:14px 16px;font-family:Consolas,Monaco,monospace;font-size:12px;line-height:1.6;color:#0f172a;white-space:pre-wrap;overflow:auto}.footer{text-align:center;color:#94a3b8;margin-top:20px;font-size:12px}code.inline{background:#eff6ff;color:#1d4ed8;border:1px solid #dbeafe;padding:2px 6px;border-radius:6px;font-size:12px}
+    @media(max-width:900px){.grid-2,.grid-3,.scenario-grid{grid-template-columns:1fr}.scenario-card{padding:18px 20px}.scenario-card h4{font-size:16px}.scenario-card li{font-size:14px}.toc{columns:1}.container{max-width:calc(100vw - 24px)}.hero{padding:24px}.section{padding:22px}table{display:block;overflow:auto}}
+  </style>
+</head>
+<body>
+<div class="container">
+  <div class="hero">
+    <h1>迎宾巡逻安防机器人运维端 Web 管理系统详细设计开发文档(一期)</h1>
+    <p>文档用途:作为运维端、机器人端、主控平台侧的统一开发基线文档,直接用于产品评审、数据库设计、接口开发、前后端开发与测试联调。</p>
+    <p>文档原则:本版以运维端需求为主导进行定义,未定事项按本文直接定版,后续由机器人侧与其他团队按本文配合实现。</p>
+    <div class="meta"><span class="chip">技术基线:RuoYi + Vue3</span><span class="chip">文档级别:详细设计开发版</span><span class="chip">单机器人本地部署</span><span class="chip">局域网 IP + 端口访问</span></div>
+  </div>
+
+  <div class="section"><h2>目录</h2><div class="toc">
+    <a href="#s1">1. 项目定位与约束</a><a href="#s2">2. 用户角色与使用目标</a><a href="#s3">3. 系统边界与设计原则</a><a href="#s4">4. 总体功能架构</a><a href="#s5">5. 菜单结构设计</a><a href="#s6">6. 页面详细设计</a><a href="#s7">7. 后端接口设计</a><a href="#s8">8. 数据库表设计</a><a href="#s9">9. 状态、日志与控制规则</a><a href="#s10">10. 权限与账号设计</a><a href="#s11">11. 开发优先级与实施顺序</a><a href="#s12">12. 其他团队配合要求</a><a href="#s13">13. 测试验收要点</a>
+  </div></div>
+
+  <div class="section" id="s1"><h2>1. 项目定位与约束</h2>
+    <p>本系统为<strong>迎宾巡逻安防机器人本地运维 Web 管理系统</strong>。系统部署在单台机器人本体,运维人员通过局域网访问机器人 IP + 端口进入后台,进行机器人状态查看、内容配置、参数设置、日志诊断、版本升级与基础维护。</p>
+    <div class="ok">本系统不是云端总控平台,不承担多机器人统一调度,也不承担上级业务平台的任务编排、访客预约发起、巡逻策略制定等核心业务能力。</div>
+    <h3>1.1 核心建设目标</h3><div class="grid-3"><div class="card"><strong>本地可维护</strong><br>不依赖云端也可完成机器人基础配置、升级、日志查看与简单控制。</div><div class="card"><strong>内容可配置</strong><br>欢迎语、问答库、多媒体、播报任务、展示主题等均在本地后台完成维护。</div><div class="card"><strong>运维可闭环</strong><br>首页看状态、模块看详情、日志可排查、版本可升级、问题可定位。</div></div>
+    <h3>1.2 一期定版原则</h3><ul><li>凡是前期沟通中已经提到或建议过的功能,一期直接定版,不因外部团队尚未实现而弱化设计。</li><li>对机器人侧、主控平台侧、展示端存在依赖的地方,本文先定义目标能力,再要求对方配合提供接口或实现。</li><li>后续若因为硬件、算法或平台限制产生调整,应基于本文迭代版本,而不是回退为概要需求。</li></ul>
+  </div>
+
+  <div class="section" id="s2"><h2>2. 用户角色与使用目标</h2>
+    <table><thead><tr><th>角色</th><th>主要职责</th><th>核心使用模块</th></tr></thead><tbody><tr><td>管理员</td><td>系统初始化、账号维护、全量配置、升级控制</td><td>全部模块</td></tr><tr><td>运维人员</td><td>日常维护、配置调整、排障处理、日志查看</td><td>首页、内容管理、监控管理、运维管理</td></tr><tr><td>交付实施人员</td><td>现场交付、基础内容录入、播放方案配置、主题设置</td><td>首页、内容管理、系统设置</td></tr><tr><td>售后支持人员</td><td>远程辅助定位问题、指导升级、查看状态</td><td>首页、监控管理、运维管理</td></tr><tr><td>查看人员</td><td>仅查看运行状态和记录,不执行配置和控制操作</td><td>首页、访客记录、对话日志、告警日志</td></tr></tbody></table>
+    <h3>2.1 典型使用目标</h3>
+    <div class="scenario-grid">
+      <div class="scenario-card">
+        <h4>场景 A:现场交付初始化</h4>
+        <ul>
+          <li>接入机器人局域网</li>
+          <li>通过 IP + 端口访问后台</li>
+          <li>登录管理员账号</li>
+          <li>配置欢迎语、问答库、展示主题</li>
+          <li>上传多媒体素材并配置播放方案</li>
+          <li>配置播报内容与定时任务</li>
+        </ul>
+      </div>
+      <div class="scenario-card">
+        <h4>场景 B:日常运维排查</h4>
+        <ul>
+          <li>查看机器人电量、运行状态、告警信息</li>
+          <li>查看摄像头画面并进行远程喊话</li>
+          <li>查看对话日志、访客记录、安防告警记录</li>
+          <li>导出需要的记录或日志</li>
+          <li>按需进行重启、充电等简单操作</li>
+        </ul>
+      </div>
+    </div>
+  </div>
+
+  <div class="section" id="s3"><h2>3. 系统边界与设计原则</h2>
+    <h3>3.1 本系统负责内容</h3><div><span class="tag">本地登录</span><span class="tag">机器人状态查看</span><span class="tag">设备简单控制</span><span class="tag">欢迎语配置</span><span class="tag">问答库管理</span><span class="tag">素材管理</span><span class="tag">播放方案管理</span><span class="tag">播报内容管理</span><span class="tag">播报任务管理</span><span class="tag">展示主题配置</span><span class="tag">访客记录查看导出</span><span class="tag">预约记录查看</span><span class="tag">白名单管理</span><span class="tag">视频预览</span><span class="tag">远程喊话</span><span class="tag">对话日志</span><span class="tag">安防告警日志</span><span class="tag">参数配置</span><span class="tag">日志中心</span><span class="tag">系统诊断</span><span class="tag">版本管理与 OTA</span></div>
+    <h3>3.2 本系统不负责内容</h3><ul><li>多机器人管理和云端总控。</li><li>建图、路径编辑、巡逻任务编排等导航管理页面。</li><li>安防算法逻辑本身,例如跌倒检测、越界检测的算法实现。</li><li>访客预约发起、被访人确认、通行权限下发等上层业务流程。</li></ul><div class="warn">地图与路径功能虽然属于机器人能力的重要组成部分,但本期由其他团队负责,不纳入本 HTML 文档页面和表设计范围。</div>
+    <h3>3.3 设计原则</h3><ul><li>页面先按清晰可开发的业务模块定型,不采用“待定”“后续再说”的写法作为主体内容。</li><li>对动态参数、动态日志等场景,采用“页面结构固定 + 数据内容动态”的设计方式。</li><li>列表页统一遵循:查询区 + 操作区 + 表格区 + 分页区 + 详情弹窗 / 编辑弹窗。</li><li>高风险动作统一弹窗二次确认,并写入操作日志。</li></ul>
+  </div>
+
+  <div class="section" id="s4"><h2>4. 总体功能架构</h2><div class="grid-3">
+    <div class="card"><strong>首页</strong><ul><li>运行总览</li><li>统计信息</li><li>异常告警</li><li>快捷操作</li></ul></div><div class="card"><strong>内容管理</strong><ul><li>欢迎语配置</li><li>问答库管理</li><li>素材管理</li><li>播放方案管理</li><li>播报内容管理</li><li>播报任务管理</li><li>展示主题配置</li></ul></div><div class="card"><strong>访客管理</strong><ul><li>访客记录</li><li>预约记录</li><li>白名单管理</li></ul></div><div class="card"><strong>监控管理</strong><ul><li>视频预览</li><li>远程喊话</li><li>对话日志</li><li>安防告警日志</li></ul></div><div class="card"><strong>运维管理</strong><ul><li>设备状态</li><li>设备控制</li><li>运行参数配置</li><li>系统诊断</li><li>日志中心</li><li>软件版本 / OTA 升级</li></ul></div><div class="card"><strong>系统设置</strong><ul><li>账号管理</li><li>修改密码</li><li>基础设置</li></ul></div>
+  </div></div>
+
+  <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="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>
+      <tr><td rowspan="3">系统设置</td><td>账号管理</td><td>维护后台账号、角色、状态。</td><td>P1</td></tr><tr><td>修改密码</td><td>当前登录账号修改密码。</td><td>P0</td></tr><tr><td>基础设置</td><td>维护系统名称、Logo、页脚信息等后台基础配置。</td><td>P2</td></tr>
+    </tbody></table>
+
+    <h3>5.1 RuoYi 代码生成与定制开发划分</h3>
+    <div class="note">本项目基于 RuoYi + Vue3 开发。RuoYi 代码生成器适合根据数据库表快速生成常规列表页、表单页、Controller、Service、Mapper 等基础 CRUD 代码;但对于首页大屏式总览、实时视频、远程控制、OTA 进度、动态参数表单、播放编排等交互复杂页面,仍需要 Cursor 或开发人员进行定制化开发。</div>
+    <table>
+      <thead>
+        <tr>
+          <th>一级菜单</th>
+          <th>页面/模块</th>
+          <th>开发方式建议</th>
+          <th>是否适合 RuoYi 代码生成</th>
+          <th>说明</th>
+        </tr>
+      </thead>
+      <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>可基于 robot_ops_welcome_config 生成基础表单,但页面更接近单配置页,需定制保存、恢复默认、测试播报等操作。</td></tr>
+        <tr><td>问答库管理</td><td>RuoYi 生成后定制</td><td>部分适合</td><td>可基于 robot_ops_faq、robot_ops_faq_similar 生成基础 CRUD;问题分类使用 RuoYi 字典 robot_faq_category,不单独生成问答分类管理页面;需补充相似问多行编辑、导入导出、分类字典下拉等逻辑。</td></tr>
+        <tr><td>素材管理</td><td>RuoYi 生成后定制</td><td>部分适合</td><td>基础列表、查询、编辑可生成;上传、缩略图展示、图片/视频预览、引用保护需要定制。</td></tr>
+        <tr><td>播放方案管理</td><td>定制开发</td><td>否</td><td>涉及主子表、素材选择、拖拽排序、播放时长、复制方案、预览方案等复杂交互,建议 Cursor 或开发人员手工实现。</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>
+        <tr><td rowspan="3">访客管理</td><td>访客记录</td><td>RuoYi 生成后微调</td><td>适合</td><td>典型查询列表和详情页面,可基于 robot_ops_visitor_record 生成,导出和照片预览需轻微调整。</td></tr>
+        <tr><td>预约记录</td><td>RuoYi 生成后微调</td><td>适合</td><td>典型查询列表和详情页面,可基于 robot_ops_appointment_record 生成,数据来源为主控平台同步。</td></tr>
+        <tr><td>白名单管理</td><td>RuoYi 生成后微调</td><td>适合</td><td>典型 CRUD 页面,可基于 robot_ops_whitelist 生成,再补充导入导出和启用/停用快捷操作。</td></tr>
+        <tr><td rowspan="4">监控管理</td><td>视频预览</td><td>定制开发</td><td>否</td><td>实时视频播放、重连、全屏、状态提示依赖视频流接口和播放器组件,需要定制开发。</td></tr>
+        <tr><td>远程喊话</td><td>半定制开发</td><td>部分适合</td><td>喊话记录列表可基于 robot_ops_shout_record 生成;喊话输入、预置短语、立即喊话操作需要定制。</td></tr>
+        <tr><td>对话日志</td><td>RuoYi 生成后微调</td><td>适合</td><td>典型日志查询和详情页面,可基于 robot_ops_dialogue_log 生成,详情页需展示原始请求和原始响应。</td></tr>
+        <tr><td>安防告警日志</td><td>RuoYi 生成后微调</td><td>适合</td><td>典型日志查询页面,可基于 robot_ops_alarm_log 生成,再补充确认告警、忽略告警、抓拍图预览。</td></tr>
+        <tr><td rowspan="6">运维管理</td><td>设备状态</td><td>定制开发</td><td>否</td><td>数据来自机器人侧实时接口,不落库;页面为状态面板和模块状态展示,不能按数据库表生成。</td></tr>
+        <tr><td>设备控制</td><td>半定制开发</td><td>部分适合</td><td>控制记录列表可基于 robot_ops_control_record 生成;一键充电、重启、关机、服务重启等控制按钮和二次确认需要定制。</td></tr>
+        <tr><td>运行参数配置</td><td>定制开发</td><td>否</td><td>虽然有 robot_ops_param 表,但页面需要根据参数分组和值类型动态渲染表单控件,不能直接按固定 CRUD 页面生成。</td></tr>
+        <tr><td>系统诊断</td><td>半定制开发</td><td>部分适合</td><td>诊断项列表可基于 robot_ops_diagnosis_item 生成;立即诊断、诊断总览和诊断结果聚合需要定制。</td></tr>
+        <tr><td>日志中心</td><td>RuoYi 生成后定制</td><td>部分适合</td><td>单表日志可生成,但日志中心需要整合 robot_ops_sys_log 和 robot_ops_operate_log,并通过 sourceType 区分来源,需要定制查询逻辑。</td></tr>
+        <tr><td>软件版本 / OTA 升级</td><td>定制开发</td><td>否</td><td>升级包列表和升级记录可参考生成代码,但上传安装包、执行升级、进度刷新、结果展示属于流程型页面,需要定制开发。</td></tr>
+        <tr><td rowspan="3">系统设置</td><td>账号管理</td><td>优先复用 RuoYi</td><td>视权限方案而定</td><td>如果使用 RuoYi 原生权限体系,优先复用系统用户、角色、菜单管理;如果采用本文简化账号表,可基于 robot_ops_user 生成后微调。</td></tr>
+        <tr><td>修改密码</td><td>复用/定制均可</td><td>否</td><td>修改密码通常复用 RuoYi 用户中心能力,不建议按表生成单独 CRUD 页面。</td></tr>
+        <tr><td>基础设置</td><td>半定制开发</td><td>部分适合</td><td>可基于 robot_ops_system_config 生成基础表单,但页面更接近单配置页,需定制保存逻辑。</td></tr>
+      </tbody>
+    </table>
+    <div class="warn">开发建议:先使用 RuoYi 代码生成器生成“适合/部分适合”的基础列表、表单和后端 CRUD,再由 Cursor 或开发人员根据第 6 章页面设计、第 7 章接口设计进行二次调整。标记为“定制开发”的页面不要强行套用单表 CRUD 模板,否则后续返工成本较高。</div>
+  </div>
+
+  <div class="section" id="s6"><h2>6. 页面详细设计</h2>
+    <h3>6.1 登录页</h3><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>登录。</td></tr><tr><td>校验规则</td><td>账号不能为空;密码不能为空;连续输错密码 5 次锁定 10 分钟。</td></tr><tr><td>登录成功动作</td><td>写入 token / session,跳转首页。</td></tr><tr><td>登录失败动作</td><td>显示失败原因;账号或密码错误时不暴露具体哪个字段错误。</td></tr></tbody></table>
+
+    <h3>6.2 首页 / 工作台</h3><p>首页按<strong>顶部状态区 + 统计区 + 告警区 + 快捷操作区 + 最近记录区</strong>进行设计。</p><div class="note">首页中的机器人基础信息、实时运行状态、资源占用状态、模块状态等数据由机器人侧接口实时返回,运维端一期不建设机器人基础信息表和设备状态快照表。</div>
+    <h4>6.2.1 顶部状态区字段</h4><table><thead><tr><th>字段</th><th>说明</th><th>展示形式</th></tr></thead><tbody><tr><td>机器人名称(robotName)</td><td>机器人名称</td><td>文本</td></tr><tr><td>机器人编号(robotCode)</td><td>机器人编号</td><td>文本</td></tr><tr><td>在线状态(onlineStatus)</td><td>在线状态:在线/离线</td><td>状态标签</td></tr><tr><td>工作状态(workStatus)</td><td>工作状态:空闲/接待中/播报中/充电中/异常</td><td>状态标签</td></tr><tr><td>电量百分比(batteryLevel)</td><td>电量百分比</td><td>进度条 + 文本</td></tr><tr><td>充电状态(chargeStatus)</td><td>充电状态:未充电/充电中/充满</td><td>状态标签</td></tr><tr><td>网络状态(networkStatus)</td><td>网络状态:正常/异常</td><td>状态标签</td></tr><tr><td>设备本地 IP(ipAddress)</td><td>设备本地 IP</td><td>文本</td></tr><tr><td>存储占用情况(storageUsed)</td><td>存储占用情况</td><td>文本,例如 18.3GB / 64GB</td></tr><tr><td>系统主版本号(currentVersion)</td><td>系统主版本号</td><td>文本</td></tr></tbody></table>
+    <h4>6.2.2 统计区字段</h4><table><thead><tr><th>字段</th><th>说明</th><th>点击跳转</th></tr></thead><tbody><tr><td>今日访客登记数量(todayVisitorCount)</td><td>今日访客登记数量</td><td>访客记录</td></tr><tr><td>今日预约记录数量(todayAppointmentCount)</td><td>今日预约记录数量</td><td>预约记录</td></tr><tr><td>今日对话次数(todayDialogueCount)</td><td>今日对话次数</td><td>对话日志</td></tr><tr><td>今日播报次数(todayBroadcastCount)</td><td>今日播报次数</td><td>播报任务</td></tr><tr><td>今日安防告警数量(todayAlarmCount)</td><td>今日安防告警数量</td><td>安防告警日志</td></tr><tr><td>今日运维操作次数(todayOperateCount)</td><td>今日运维操作次数</td><td>日志中心</td></tr></tbody></table>
+    <h4>6.2.3 告警与快捷操作</h4><table><thead><tr><th>区块</th><th>内容</th></tr></thead><tbody><tr><td>最近系统异常</td><td>最近 5 条系统异常摘要,点击跳日志中心。</td></tr><tr><td>最近安防告警</td><td>最近 5 条安防告警摘要,点击跳安防告警日志。</td></tr><tr><td>最近升级失败</td><td>最近 5 条升级失败摘要,点击跳 OTA 升级页。</td></tr><tr><td>快捷按钮</td><td>查看摄像头、远程喊话、一键充电、停止充电、重启机器人、进入 OTA。</td></tr></tbody></table>
+
+    <h3>6.3 内容管理</h3>
+    <h4>6.3.1 欢迎语配置页面</h4><table><thead><tr><th>字段/功能</th><th>类型</th><th>详细设计</th></tr></thead><tbody><tr><td>欢迎语文本(welcomeText)</td><td>textarea</td><td>欢迎语文本,最大 200 字。</td></tr><tr><td>是否启用语音播报(voiceEnabled)</td><td>switch</td><td>是否启用语音播报。</td></tr><tr><td>触发冷却时间(cooldownSeconds)</td><td>number</td><td>触发冷却时间,单位秒,默认 30。</td></tr><tr><td>启用状态(status)</td><td>switch</td><td>启用/停用。</td></tr><tr><td>保存</td><td>button</td><td>保存当前配置。</td></tr><tr><td>恢复默认</td><td>button</td><td>恢复系统默认欢迎语配置。</td></tr><tr><td>测试播报</td><td>button</td><td>下发测试播报指令。</td></tr></tbody></table>
+    <h4>6.3.2 问答库管理页面</h4><table><thead><tr><th>模块</th><th>详细设计</th></tr></thead><tbody><tr><td>查询条件</td><td>问题分类(categoryType,数据来源:RuoYi 字典 robot_faq_category)、标准问题关键字、启用状态、更新时间范围。</td></tr><tr><td>列表字段</td><td>问题分类、标准问题、相似问数量、答案摘要、状态、排序、更新时间、操作。</td></tr><tr><td>操作按钮</td><td>新增、编辑、删除、启用/停用、导入、导出、批量删除。</td></tr><tr><td>编辑弹窗字段</td><td>问题分类(categoryType,RuoYi 字典 robot_faq_category)、标准问题(question)、相似问(similarQuestions)、答案内容(answer)、排序(sortNo)、启用状态(status)、备注(remark)。</td></tr><tr><td>交互规则</td><td>similarQuestions 用多行输入,一行一个;answer 最多 2000 字。</td></tr><tr><td>导入规则</td><td>Excel 字段:问题分类、标准问题、相似问、答案、排序、状态;问题分类按 RuoYi 字典 robot_faq_category 的字典标签或字典值匹配;相似问用英文分号分隔。</td></tr></tbody></table>
+    <h4>6.3.3 素材管理页面</h4><table><thead><tr><th>模块</th><th>详细设计</th></tr></thead><tbody><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>图片支持 jpg/png/webp;视频支持 mp4;单文件大小默认上限 500MB。</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.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.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>
+    <h4>6.4.1 访客记录页面</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>访客姓名(visitorName)、手机号(mobile)、证件号(idCardNo)、登记方式(registerType)、访客照片(visitorPhoto)、预约单号(appointmentNo)、被访对象(visitedPerson)、来访时间(visitTime)、登记结果(resultStatus)、来源类型(sourceType)、备注(remark)。</td></tr><tr><td>操作按钮</td><td>查看详情、导出。</td></tr><tr><td>导出字段</td><td>导出列表全部字段,不导出照片文件,仅导出照片链接。</td></tr></tbody></table>
+    <h4>6.4.2 预约记录页面</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>预约单号(appointmentNo)、访客姓名(visitorName)、手机号(mobile)、被访人(visitedPerson)、预约时间(appointmentTime)、预约状态(status)、同步时间(syncTime)、来源平台(sourcePlatform)、备注(remark)。</td></tr><tr><td>操作按钮</td><td>查看详情。</td></tr><tr><td>数据来源</td><td>主控平台同步;本地端仅展示,不发起预约流程。</td></tr></tbody></table>
+    <h4>6.4.3 白名单管理页面</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>姓名(name)、手机号(mobile)、白名单类型(whitelistType)、来源类型(sourceType)、启用状态(status)、备注(remark)。</td></tr><tr><td>操作按钮</td><td>新增、编辑、删除、导入、导出、启用/停用。</td></tr><tr><td>业务规则</td><td>一期即按可维护设计,不再仅停留在“预留按钮”。如后续对接平台同步,再增加只读同步标识。</td></tr></tbody></table>
+
+    <h3>6.5 监控管理</h3>
+    <h4>6.5.1 视频预览页面</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>显示码流类型、分辨率、最近更新时间。</td></tr><tr><td>操作按钮</td><td>刷新、全屏、重新连接。</td></tr><tr><td>异常提示</td><td>播放失败时展示错误码和建议操作:刷新、检查摄像头服务、检查网络。视频流地址、播放状态等由机器人侧实时接口返回,运维端一期不单独建设视频配置表。</td></tr></tbody></table>
+    <h4>6.5.2 远程喊话页面</h4><table><thead><tr><th>区域</th><th>详细设计</th></tr></thead><tbody><tr><td>输入区域</td><td>喊话文本输入框,最大 500 字。</td></tr><tr><td>参数区域</td><td>音量(0-100)、播放次数(1-5)、是否立即打断当前播报。</td></tr><tr><td>预置短语</td><td>展示常用喊话语句,点击自动填充。</td></tr><tr><td>结果区域</td><td>显示最近 10 次喊话记录与执行结果。</td></tr><tr><td>操作按钮</td><td>立即喊话、清空内容。</td></tr></tbody></table>
+    <h4>6.5.3 对话日志页面</h4><table><thead><tr><th>模块</th><th>详细设计</th></tr></thead><tbody><tr><td>查询条件</td><td>时间范围、会话 ID、用户问题关键字、命中状态、来源场景。</td></tr><tr><td>列表字段</td><td>时间、会话 ID、用户问题、机器人回答摘要、命中方式、来源场景、状态、操作。</td></tr><tr><td>详情字段</td><td>会话 ID(sessionId)、提问时间(askTime)、用户问题(question)、机器人回答(answer)、命中方式(hitType)、来源场景(sceneType)、结果状态(resultStatus)、原始请求(rawRequest)、原始响应(rawResponse)。</td></tr><tr><td>操作按钮</td><td>查看详情、导出。</td></tr></tbody></table>
+    <h4>6.5.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>告警时间(alarmTime)、告警类型(alarmType)、告警级别(alarmLevel)、来源位置(sourcePosition)、处理状态(handleStatus)、描述(description)、抓拍图地址(snapshotUrl)、备注(remark)。</td></tr><tr><td>操作按钮</td><td>查看详情、确认告警、忽略告警、导出。</td></tr></tbody></table>
+
+    <h3>6.6 运维管理</h3>
+    <h4>6.6.1 设备状态页面</h4><table><thead><tr><th>区域</th><th>字段</th></tr></thead><tbody><tr><td>基础信息</td><td>机器人名称(robotName)、机器人编号(robotCode)、序列号(serialNo)、IP 地址(ipAddress)、MAC 地址(macAddress)、运行时长(uptime)。</td></tr><tr><td>运行信息</td><td>在线状态(onlineStatus)、工作状态(workStatus)、电量百分比(batteryLevel)、充电状态(chargeStatus)、网络状态(networkStatus)。</td></tr><tr><td>系统资源</td><td>CPU 使用率(cpuUsage)、内存使用率(memoryUsage)、磁盘使用率(diskUsage)、温度(temperature)。</td></tr><tr><td>模块状态</td><td>摄像头状态(cameraStatus)、麦克风状态(micStatus)、扬声器状态(speakerStatus)、屏幕状态(screenStatus)、主服务状态(mainServiceStatus)。</td></tr><tr><td>时间信息</td><td>系统时间(serverTime)、最近心跳时间(lastHeartbeatTime)。</td></tr></tbody></table><p>以上设备状态字段均由机器人侧实时接口返回,运维端一期不做本地数据库持久化。</p>
+    <h4>6.6.2 设备控制页面</h4><table><thead><tr><th>控制项</th><th>说明</th><th>返回字段</th></tr></thead><tbody><tr><td>一键充电</td><td>触发机器人进入充电流程</td><td>任务ID(taskId)、执行状态(resultStatus)、结果信息(resultMsg)</td></tr>
+<tr><td>停止充电</td><td>停止当前充电动作</td><td>任务ID(taskId)、执行状态(resultStatus)、结果信息(resultMsg)</td></tr>
+<tr><td>重启机器人</td><td>执行整机重启</td><td>任务ID(taskId)、执行状态(resultStatus)、结果信息(resultMsg)</td></tr>
+<tr><td>关机</td><td>执行安全关机</td><td>任务ID(taskId)、执行状态(resultStatus)、结果信息(resultMsg)</td></tr>
+<tr><td>重启应用服务</td><td>重启指定服务</td><td>任务ID(taskId)、服务名称(serviceName)、执行状态(resultStatus)</td></tr>
+<tr><td>音频测试</td><td>播放测试音频或测试播报</td><td>执行状态(resultStatus)</td></tr>
+<tr><td>屏幕测试</td><td>切换测试画面</td><td>执行状态(resultStatus)</td></tr>
+<tr><td>控制记录</td><td>所有控制动作统一写入设备控制记录表和操作日志,便于追踪 taskId、执行结果和失败原因。</td><td>控制记录ID(controlRecordId)、任务ID(taskId)、执行状态(resultStatus)、结果信息(resultMsg)</td></tr></tbody></table>
+    <h4>6.6.3 运行参数配置页面</h4><p>页面采用<strong>模块 Tab + 模块参数表</strong>形式。</p><table><thead><tr><th>字段</th><th>说明</th></tr></thead><tbody><tr><td>参数分组编码(groupCode)</td><td>参数分组编码,例如 audio、screen、system、device、service。</td></tr><tr><td>参数分组名称(groupName)</td><td>参数分组名称。</td></tr><tr><td>参数编码(paramCode)</td><td>参数编码。</td></tr><tr><td>参数名称(paramName)</td><td>参数名称。</td></tr><tr><td>参数值(paramValue)</td><td>参数值。</td></tr><tr><td>值类型(valueType)</td><td>string、int、float、boolean、enum。</td></tr><tr><td>单位(unit)</td><td>单位。</td></tr><tr><td>是否可编辑(editable)</td><td>是否可编辑。</td></tr><tr><td>是否必填(requiredFlag)</td><td>是否必填。</td></tr><tr><td>数值边界(minValue / maxValue)</td><td>数值边界。</td></tr><tr><td>枚举项(enumOptions)</td><td>枚举项 JSON。</td></tr><tr><td>参数说明(remark)</td><td>参数说明。</td></tr></tbody></table><p>操作按钮:刷新、保存、重置默认值。保存时需按 valueType 校验值类型和取值范围。</p>
+    <h4>6.6.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>网络检查、摄像头检查、麦克风检查、扬声器检查、磁盘空间检查、服务状态检查。</td></tr><tr><td>操作按钮</td><td>立即诊断、刷新结果、导出诊断结果。</td></tr></tbody></table>
+    <h4>6.6.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>日志时间、日志类型、日志级别、模块名称、摘要、结果状态、操作。</td></tr><tr><td>详情字段</td><td>日志时间(logTime)、日志类型(logType)、日志级别(logLevel)、模块名称(moduleName)、日志内容(content)、结果状态(resultStatus)、追踪 ID(traceId)、备注(remark)。</td></tr><tr><td>操作按钮</td><td>查看详情、导出。</td></tr></tbody></table>
+    <h4>6.6.6 软件版本 / OTA 升级页面</h4><table><thead><tr><th>区块</th><th>详细设计</th></tr></thead><tbody><tr><td>软件版本列表</td><td>展示模块编码(moduleCode)、模块名称(moduleName)、当前版本(currentVersion)、安装时间(installTime)、运行状态(runStatus)。</td></tr><tr><td>安装包管理</td><td>展示安装包名称(packageName)、模块编码(moduleCode)、目标版本(targetVersion)、文件大小(fileSize)、上传时间(uploadTime)、上传人(uploadBy)。</td></tr><tr><td>升级操作</td><td>选择模块 + 安装包,执行升级,展示升级状态(resultStatus)、升级进度(progressPercent)与结果信息(resultMsg)。</td></tr><tr><td>升级记录</td><td>展示开始时间(startTime)、结束时间(endTime)、模块编码(moduleCode)、模块名称(moduleName)、原版本(currentVersion)、目标版本(targetVersion)、执行结果(resultStatus)、失败原因/结果信息(resultMsg)、升级进度(progressPercent)。</td></tr><tr><td>操作按钮</td><td>上传安装包、删除安装包、执行升级、刷新进度、查看升级详情。</td></tr></tbody></table>
+
+    <h3>6.7 系统设置</h3>
+    <h4>6.7.1 账号管理页面</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>登录账号(username)、用户姓名(nickName)、登录密码(password)、角色编码(roleCode)、启用状态(status)、备注(remark)。</td></tr><tr><td>操作按钮</td><td>新增、编辑、重置密码、启用/停用、删除。</td></tr><tr><td>业务规则</td><td>admin 默认账号不可删除;可重置密码。</td></tr></tbody></table>
+    <h4>6.7.2 修改密码页面</h4><table><thead><tr><th>字段</th><th>说明</th></tr></thead><tbody><tr><td>原密码(oldPassword)</td><td>原密码。</td></tr><tr><td>新密码(newPassword)</td><td>新密码,建议 8-32 位。</td></tr><tr><td>确认新密码(confirmPassword)</td><td>确认新密码,必须与 newPassword 一致。</td></tr></tbody></table>
+    <h4>6.7.3 基础设置页面</h4><table><thead><tr><th>字段</th><th>说明</th></tr></thead><tbody><tr><td>后台系统名称(systemName)</td><td>后台系统名称。</td></tr><tr><td>后台 Logo(systemLogo)</td><td>后台 Logo。</td></tr><tr><td>页脚文案(footerText)</td><td>页脚文案。</td></tr><tr><td>备案号/版权信息(recordNo)</td><td>备案号/版权信息,可选。</td></tr></tbody></table>
+  </div>
+
+  <div class="section" id="s7"><h2>7. 后端接口设计</h2>
+    <div class="note">说明:接口统一以 <code class="inline">/robot-ops</code> 为前缀,返回结构统一为 <code class="inline">{ code, msg, data, timestamp }</code>。第 7 章接口字段采用 camelCase;第 8 章数据库字段采用 snake_case。列表接口统一支持 pageNum、pageSize。</div>
+    <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>
+    </tbody></table>
+    <div class="note">通用文件上传接口默认不单独建设文件记录表,上传后的 fileUrl 由具体业务表保存;如后续需要统一文件管理,再扩展附件表。</div>
+
+
+    <h3>7.1 通用接口规范</h3>
+    <h4>7.1.1 通用返回结构</h4>
+    <div class="code">{
+  "code": 200,
+  "msg": "操作成功",
+  "data": {},
+  "timestamp": "2026-04-24 10:00:00"
+}</div>
+    <h4>7.1.2 分页返回结构</h4>
+    <div class="code">{
+  "code": 200,
+  "msg": "查询成功",
+  "data": {
+    "total": 100,
+    "pageNum": 1,
+    "pageSize": 10,
+    "rows": []
+  },
+  "timestamp": "2026-04-24 10:00:00"
+}</div>
+    <h4>7.1.3 通用状态码</h4>
+    <table><thead><tr><th>状态码</th><th>说明</th><th>处理建议</th></tr></thead><tbody>
+      <tr><td>200</td><td>成功</td><td>正常处理 data。</td></tr>
+      <tr><td>400</td><td>请求参数错误</td><td>前端提示 msg。</td></tr>
+      <tr><td>401</td><td>未登录或登录已过期</td><td>跳转登录页。</td></tr>
+      <tr><td>403</td><td>无权限</td><td>提示无权限。</td></tr>
+      <tr><td>500</td><td>服务异常</td><td>提示系统异常,并记录日志。</td></tr>
+      <tr><td>10001</td><td>机器人侧接口不可用</td><td>提示机器人服务异常。</td></tr>
+      <tr><td>10002</td><td>机器人执行失败</td><td>展示 resultMsg。</td></tr>
+    </tbody></table>
+
+    <h3>7.2 认证与系统接口</h3>
+    <h4>7.2.1 登录</h4>
+    <table><thead><tr><th>项</th><th>内容</th></tr></thead><tbody>
+      <tr><td>接口地址</td><td>POST /robot-ops/auth/login</td></tr>
+      <tr><td>请求参数</td><td>登录账号(username)、登录密码(password)</td></tr>
+      <tr><td>返回字段</td><td>访问令牌(token)、用户ID(userId)、登录账号(username)、用户姓名(nickName)、角色编码(roleCode)</td></tr>
+      <tr><td>数据库表</td><td>robot_ops_user</td></tr>
+      <tr><td>业务规则</td><td>账号停用不可登录;密码连续错误 5 次锁定 10 分钟;登录成功更新 last_login_time。</td></tr>
+    </tbody></table>
+    <div class="code">请求示例:
+{
+  "username": "admin",
+  "password": "******"
+}
+
+返回示例:
+{
+  "token": "xxxxxx",
+  "userId": 1,
+  "username": "admin",
+  "nickName": "管理员",
+  "roleCode": "ADMIN"
+}</div>
+
+    <h4>7.2.2 退出登录</h4>
+    <table><thead><tr><th>项</th><th>内容</th></tr></thead><tbody>
+      <tr><td>接口地址</td><td>POST /robot-ops/auth/logout</td></tr>
+      <tr><td>请求参数</td><td>无</td></tr>
+      <tr><td>返回字段</td><td>无</td></tr>
+      <tr><td>业务规则</td><td>清理当前登录 token 或 session。</td></tr>
+    </tbody></table>
+
+    <h4>7.2.3 修改密码</h4>
+    <table><thead><tr><th>项</th><th>内容</th></tr></thead><tbody>
+      <tr><td>接口地址</td><td>POST /robot-ops/auth/change-password</td></tr>
+      <tr><td>请求参数</td><td>原密码(oldPassword)、新密码(newPassword)、确认新密码(confirmPassword)</td></tr>
+      <tr><td>返回字段</td><td>无</td></tr>
+      <tr><td>数据库表</td><td>robot_ops_user</td></tr>
+      <tr><td>业务规则</td><td>原密码校验通过后更新;新密码和确认密码必须一致。</td></tr>
+    </tbody></table>
+
+    <h4>7.2.4 当前登录用户</h4>
+    <table><thead><tr><th>项</th><th>内容</th></tr></thead><tbody>
+      <tr><td>接口地址</td><td>GET /robot-ops/auth/current-user</td></tr>
+      <tr><td>返回字段</td><td>用户ID(userId)、登录账号(username)、用户姓名(nickName)、角色编码(roleCode)、权限标识(permissions)</td></tr>
+      <tr><td>数据库表</td><td>robot_ops_user 或 RuoYi 原生权限表</td></tr>
+    </tbody></table>
+
+    <h4>7.2.5 基础设置查询/保存</h4>
+    <table><thead><tr><th>接口</th><th>方法</th><th>说明</th><th>请求/返回字段</th><th>数据库表</th></tr></thead><tbody>
+      <tr><td>/robot-ops/system/base-config</td><td>GET</td><td>获取基础设置</td><td>后台系统名称(systemName)、后台Logo(systemLogo)、页脚文案(footerText)、备案号(recordNo)</td><td>robot_ops_system_config</td></tr>
+      <tr><td>/robot-ops/system/base-config</td><td>PUT</td><td>保存基础设置</td><td>后台系统名称(systemName)、后台Logo(systemLogo)、页脚文案(footerText)、备案号(recordNo)</td><td>robot_ops_system_config</td></tr>
+    </tbody></table>
+
+    <h3>7.3 首页接口</h3>
+    <h4>7.3.1 首页总览</h4>
+    <table><thead><tr><th>项</th><th>内容</th></tr></thead><tbody>
+      <tr><td>接口地址</td><td>GET /robot-ops/home/overview</td></tr>
+      <tr><td>请求参数</td><td>无</td></tr>
+      <tr><td>返回字段</td><td>机器人名称(robotName)、机器人编号(robotCode)、在线状态(onlineStatus)、工作状态(workStatus)、电量百分比(batteryLevel)、充电状态(chargeStatus)、网络状态(networkStatus)、IP地址(ipAddress)、存储占用(storageUsed)、当前版本(currentVersion)、今日访客数(todayVisitorCount)、今日预约数(todayAppointmentCount)、今日对话数(todayDialogueCount)、今日播报数(todayBroadcastCount)、今日告警数(todayAlarmCount)、今日操作数(todayOperateCount)</td></tr>
+      <tr><td>数据来源</td><td>机器人实时接口 + 本地访客/对话/告警/操作记录聚合。</td></tr>
+      <tr><td>数据库表</td><td>robot_ops_visitor_record、robot_ops_appointment_record、robot_ops_dialogue_log、robot_ops_alarm_log、robot_ops_operate_log;机器人实时状态不落库。</td></tr>
+    </tbody></table>
+
+    <h4>7.3.2 首页最近告警/最近记录</h4>
+    <table><thead><tr><th>接口</th><th>说明</th><th>请求参数</th><th>返回字段</th><th>数据库表</th></tr></thead><tbody>
+      <tr><td>GET /robot-ops/home/alarms</td><td>获取首页最近告警</td><td>数量(limit,默认5)</td><td>告警ID(id)、告警时间(alarmTime)、告警类型(alarmType)、告警级别(alarmLevel)、描述(description)</td><td>robot_ops_alarm_log</td></tr>
+      <tr><td>GET /robot-ops/home/quick-records</td><td>获取首页最近记录</td><td>数量(limit,默认5)</td><td>记录类型(recordType)、时间(recordTime)、标题(title)、摘要(summary)、跳转目标(targetUrl)</td><td>robot_ops_operate_log、robot_ops_upgrade_record、robot_ops_sys_log</td></tr>
+    </tbody></table>
+
+    <h3>7.4 内容管理接口</h3>
+    <h4>7.4.1 欢迎语配置接口</h4>
+    <table><thead><tr><th>接口</th><th>方法</th><th>说明</th><th>请求/返回字段</th><th>数据库表</th></tr></thead><tbody>
+      <tr><td>/robot-ops/content/welcome-config</td><td>GET</td><td>获取欢迎语配置</td><td>欢迎语文本(welcomeText)、是否启用语音播报(voiceEnabled)、触发冷却时间(cooldownSeconds)、启用状态(status)</td><td>robot_ops_welcome_config</td></tr>
+      <tr><td>/robot-ops/content/welcome-config</td><td>PUT</td><td>保存欢迎语配置</td><td>欢迎语文本(welcomeText)、是否启用语音播报(voiceEnabled)、触发冷却时间(cooldownSeconds)、启用状态(status)</td><td>robot_ops_welcome_config</td></tr>
+      <tr><td>/robot-ops/content/welcome-config/test</td><td>POST</td><td>测试欢迎语播报</td><td>欢迎语文本(welcomeText)</td><td>不新增业务表,可写入 robot_ops_operate_log</td></tr>
+    </tbody></table>
+
+    <h4>7.4.2 问答库接口</h4>
+    <table><thead><tr><th>接口</th><th>方法</th><th>说明</th><th>请求参数</th><th>返回/处理字段</th><th>数据库表</th></tr></thead><tbody>
+      <tr><td>/robot-ops/content/faq/page</td><td>GET</td><td>问答分页</td><td>问题分类(categoryType,字典值)、关键字(keyword)、启用状态(status)、pageNum、pageSize</td><td>问答ID(id)、问题分类名称(categoryName,按字典回显)、标准问题(question)、相似问数量(similarCount)、答案摘要(answerSummary)、排序(sortNo)、状态(status)、更新时间(updateTime)</td><td>robot_ops_faq、robot_ops_faq_similar;分类名称通过 RuoYi 字典 robot_faq_category 回显</td></tr>
+      <tr><td>/robot-ops/content/faq/{id}</td><td>GET</td><td>问答详情</td><td>问答ID(id)</td><td>categoryType、question、similarQuestions、answer、sortNo、status、remark</td><td>robot_ops_faq、robot_ops_faq_similar</td></tr>
+      <tr><td>/robot-ops/content/faq</td><td>POST</td><td>新增问答</td><td>categoryType、question、similarQuestions、answer、sortNo、status、remark</td><td>新增后的问答ID(id)</td><td>robot_ops_faq、robot_ops_faq_similar</td></tr>
+      <tr><td>/robot-ops/content/faq</td><td>PUT</td><td>编辑问答</td><td>id、categoryType、question、similarQuestions、answer、sortNo、status、remark</td><td>无</td><td>robot_ops_faq、robot_ops_faq_similar</td></tr>
+      <tr><td>/robot-ops/content/faq/{id}</td><td>DELETE</td><td>删除问答</td><td>问答ID(id)</td><td>无</td><td>robot_ops_faq、robot_ops_faq_similar</td></tr>
+      <tr><td>/robot-ops/content/faq/import</td><td>POST</td><td>导入问答</td><td>Excel文件(file)</td><td>导入总数(total)、成功数(successCount)、失败数(failCount)、失败明细(failList)</td><td>robot_ops_faq、robot_ops_faq_similar;问题分类按 RuoYi 字典 robot_faq_category 匹配</td></tr>
+      <tr><td>/robot-ops/content/faq/export</td><td>GET</td><td>导出问答</td><td>问题分类(categoryType)、关键字、状态</td><td>Excel文件</td><td>robot_ops_faq、robot_ops_faq_similar;分类名称按 RuoYi 字典 robot_faq_category 回显</td></tr>
+    </tbody></table>
+
+    <div class="note">问答分类不单独建设业务表和管理页面,统一使用 RuoYi 字典维护,字典类型建议为 <code class="inline">robot_faq_category</code>。导入时根据分类名称或分类字典值匹配该字典;如匹配失败,提示导入失败明细,不自动创建业务分类。</div>
+
+    <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、fileFormat、fileSize、durationSeconds、resolution、quotedFlag、status、remark、createTime</td><td>robot_ops_media_asset</td></tr>
+      <tr><td>/robot-ops/content/media/upload</td><td>POST</td><td>上传素材</td><td>文件(file)、素材名称(assetName)、素材类型(assetType)</td><td>素材ID(id)、文件地址(fileUrl)、缩略图地址(thumbnailUrl)、文件大小(fileSize)、文件格式(fileFormat)</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</td></tr>
+    </tbody></table>
+    <div class="warn">被播放方案引用的素材不可直接删除,需先解除播放方案引用。</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>
+    </tbody></table>
+    <div class="code">itemList 字段示例:
+[
+  {
+    "assetId": 1,
+    "playOrder": 1,
+    "staySeconds": 10,
+    "transitionType": "fade"
+  }
+]</div>
+
+    <h4>7.4.5 播报内容与播报任务接口</h4>
+    <table><thead><tr><th>接口</th><th>方法</th><th>说明</th><th>主要字段</th><th>数据库表</th></tr></thead><tbody>
+      <tr><td>/robot-ops/content/broadcast-content/page</td><td>GET</td><td>播报内容分页</td><td>contentName、contentType、broadcastText、status、updateTime</td><td>robot_ops_broadcast_content</td></tr>
+<tr><td>/robot-ops/content/broadcast-content/{id}</td><td>GET</td><td>播报内容详情</td><td>内容ID(id);返回 contentName、contentType、broadcastText、status、remark</td><td>robot_ops_broadcast_content</td></tr>
+      <tr><td>/robot-ops/content/broadcast-content</td><td>POST</td><td>新增播报内容</td><td>contentName、contentType、broadcastText、status、remark</td><td>robot_ops_broadcast_content</td></tr>
+      <tr><td>/robot-ops/content/broadcast-content</td><td>PUT</td><td>编辑播报内容</td><td>id、contentName、contentType、broadcastText、status、remark</td><td>robot_ops_broadcast_content</td></tr>
+      <tr><td>/robot-ops/content/broadcast-content/{id}</td><td>DELETE</td><td>删除播报内容</td><td>内容ID(id)</td><td>robot_ops_broadcast_content</td></tr>
+      <tr><td>/robot-ops/content/broadcast-content/{id}/test</td><td>POST</td><td>测试播报内容</td><td>内容ID(id)</td><td>robot_ops_broadcast_content、robot_ops_operate_log</td></tr>
+      <tr><td>/robot-ops/content/broadcast-task/page</td><td>GET</td><td>播报任务分页</td><td>请求字段:任务名称(taskName)、播报内容名称(contentName)、循环类型(cycleType)、启用状态(status)、pageNum、pageSize;返回字段:任务ID(id)、任务名称(taskName)、播报内容ID(contentId)、播报内容名称(contentName)、播报内容状态(contentStatus,建议返回)、开始时间(startTime)、结束时间(endTime)、播报频率(frequencyMinutes,单位:分钟)、循环类型(cycleType:1按星期,2按日期)、循环取值(cycleValue)、启用状态(status)、更新时间(updateTime)。</td><td>robot_ops_broadcast_task、robot_ops_broadcast_content</td></tr>
+<tr><td>/robot-ops/content/broadcast-task/{id}</td><td>GET</td><td>播报任务详情</td><td>任务ID(id);返回 taskName、contentId、contentName、contentStatus、startTime、endTime、frequencyMinutes、cycleType、cycleValue、status、remark</td><td>robot_ops_broadcast_task、robot_ops_broadcast_content</td></tr>
+      <tr><td>/robot-ops/content/broadcast-task</td><td>POST</td><td>新增播报任务</td><td>taskName、contentId、startTime、endTime、frequencyMinutes、cycleType、cycleValue、status、remark;cycleType=1 时 cycleValue 保存星期值,cycleType=2 时 cycleValue 保存日期值</td><td>robot_ops_broadcast_task</td></tr>
+      <tr><td>/robot-ops/content/broadcast-task</td><td>PUT</td><td>编辑播报任务</td><td>id、taskName、contentId、startTime、endTime、frequencyMinutes、cycleType、cycleValue、status、remark;cycleType=1 时 cycleValue 保存星期值,cycleType=2 时 cycleValue 保存日期值</td><td>robot_ops_broadcast_task</td></tr>
+      <tr><td>/robot-ops/content/broadcast-task/{id}</td><td>DELETE</td><td>删除播报任务</td><td>任务ID(id)</td><td>robot_ops_broadcast_task</td></tr>
+      <tr><td>/robot-ops/content/broadcast-task/{id}/copy</td><td>POST</td><td>复制播报任务</td><td>任务ID(id)</td><td>robot_ops_broadcast_task</td></tr>
+    </tbody></table>
+    <div class="note">播报任务循环类型使用 RuoYi 字典 <code class="inline">broadcast_task_cycle_type</code>,字典值约定为:1=按星期,2=按日期。按星期时,cycleValue 保存 1-7 的星期值,多个值用英文逗号分隔;按日期时,cycleValue 保存 YYYY-MM-DD 日期值,多个日期用英文逗号分隔。</div>
+    <div class="note">播报任务列表和详情接口建议返回 contentName 和 contentStatus,便于前端展示关联播报内容名称,以及识别关联内容是否已停用。新增/编辑播报任务时,前端仅允许选择启用状态的播报内容;历史任务关联的停用内容需要支持回显。</div>
+
+    <h4>7.4.6 展示主题接口</h4>
+    <table><thead><tr><th>接口</th><th>方法</th><th>说明</th><th>请求/返回字段</th><th>数据库表</th></tr></thead><tbody>
+      <tr><td>/robot-ops/content/theme/page</td><td>GET</td><td>主题分页</td><td>themeName、status、pageNum、pageSize;返回 logoUrl、backgroundType、backgroundUrl、primaryColor、currentEnabled、updateTime</td><td>robot_ops_theme</td></tr>
+      <tr><td>/robot-ops/content/theme/{id}</td><td>GET</td><td>主题详情</td><td>id、themeName、logoUrl、backgroundType、backgroundUrl、primaryColor、secondaryColor、welcomeTitle、welcomeSubTitle、status、currentEnabled</td><td>robot_ops_theme</td></tr>
+      <tr><td>/robot-ops/content/theme</td><td>POST</td><td>新增主题</td><td>themeName、logoUrl、backgroundType、backgroundUrl、primaryColor、secondaryColor、welcomeTitle、welcomeSubTitle、status</td><td>robot_ops_theme</td></tr>
+      <tr><td>/robot-ops/content/theme</td><td>PUT</td><td>编辑主题</td><td>id、themeName、logoUrl、backgroundType、backgroundUrl、primaryColor、secondaryColor、welcomeTitle、welcomeSubTitle、status</td><td>robot_ops_theme</td></tr>
+      <tr><td>/robot-ops/content/theme/{id}</td><td>DELETE</td><td>删除主题</td><td>主题ID(id)</td><td>robot_ops_theme</td></tr>
+      <tr><td>/robot-ops/content/theme/{id}/enable</td><td>POST</td><td>启用主题</td><td>主题ID(id)</td><td>robot_ops_theme</td></tr>
+    </tbody></table>
+    <div class="note">主题 Logo、背景资源可通过通用文件上传接口获取 fileUrl 后,再写入 logoUrl、backgroundUrl;也可根据项目实现复用素材上传接口。</div>
+    <div class="warn">启用主题时需要将其他主题 current_enabled 置为 0,当前主题置为 1,保证同一时间只有一个主题生效。</div>
+
+    <h3>7.5 访客管理接口</h3>
+    <table><thead><tr><th>接口</th><th>方法</th><th>说明</th><th>请求参数</th><th>返回/处理字段</th><th>数据库表</th></tr></thead><tbody>
+      <tr><td>/robot-ops/visitor/record/page</td><td>GET</td><td>访客记录分页</td><td>visitorName、mobile、idCardNo、registerType、resultStatus、visitTimeStart、visitTimeEnd、pageNum、pageSize</td><td>id、visitorName、mobile、idCardNo、registerType、visitedPerson、visitTime、resultStatus、sourceType</td><td>robot_ops_visitor_record</td></tr>
+      <tr><td>/robot-ops/visitor/record/{id}</td><td>GET</td><td>访客详情</td><td>记录ID(id)</td><td>visitorName、mobile、idCardNo、registerType、visitorPhoto、appointmentNo、visitedPerson、visitTime、resultStatus、sourceType、remark</td><td>robot_ops_visitor_record</td></tr>
+      <tr><td>/robot-ops/visitor/record/export</td><td>GET</td><td>导出访客记录</td><td>同分页查询条件</td><td>Excel文件</td><td>robot_ops_visitor_record</td></tr>
+      <tr><td>/robot-ops/visitor/appointment/page</td><td>GET</td><td>预约记录分页</td><td>appointmentNo、visitorName、mobile、status、appointmentTimeStart、appointmentTimeEnd、pageNum、pageSize</td><td>appointmentNo、visitorName、mobile、visitedPerson、appointmentTime、status、syncTime</td><td>robot_ops_appointment_record</td></tr>
+      <tr><td>/robot-ops/visitor/appointment/{id}</td><td>GET</td><td>预约详情</td><td>预约记录ID(id)</td><td>appointmentNo、visitorName、mobile、visitedPerson、appointmentTime、status、syncTime、sourcePlatform、remark</td><td>robot_ops_appointment_record</td></tr>
+      <tr><td>/robot-ops/visitor/whitelist/page</td><td>GET</td><td>白名单分页</td><td>name、mobile、status、sourceType、pageNum、pageSize</td><td>id、name、mobile、whitelistType、sourceType、status、updateTime</td><td>robot_ops_whitelist</td></tr>
+      <tr><td>/robot-ops/visitor/whitelist/{id}</td><td>GET</td><td>白名单详情</td><td>白名单ID(id)</td><td>name、mobile、whitelistType、sourceType、status、remark</td><td>robot_ops_whitelist</td></tr>
+      <tr><td>/robot-ops/visitor/whitelist</td><td>POST</td><td>新增白名单</td><td>name、mobile、whitelistType、sourceType、status、remark</td><td>新增ID(id)</td><td>robot_ops_whitelist</td></tr>
+      <tr><td>/robot-ops/visitor/whitelist</td><td>PUT</td><td>编辑白名单</td><td>id、name、mobile、whitelistType、sourceType、status、remark</td><td>无</td><td>robot_ops_whitelist</td></tr>
+<tr><td>/robot-ops/visitor/whitelist/{id}/status</td><td>PUT</td><td>启用/停用白名单</td><td>白名单ID(id)、启用状态(status)</td><td>无</td><td>robot_ops_whitelist</td></tr>
+      <tr><td>/robot-ops/visitor/whitelist/{id}</td><td>DELETE</td><td>删除白名单</td><td>白名单ID(id)</td><td>无</td><td>robot_ops_whitelist</td></tr>
+      <tr><td>/robot-ops/visitor/whitelist/import</td><td>POST</td><td>导入白名单</td><td>Excel文件(file)</td><td>导入总数、成功数、失败数、失败明细</td><td>robot_ops_whitelist</td></tr>
+      <tr><td>/robot-ops/visitor/whitelist/export</td><td>GET</td><td>导出白名单</td><td>同分页查询条件</td><td>Excel文件</td><td>robot_ops_whitelist</td></tr>
+    </tbody></table>
+
+    <h3>7.6 监控管理接口</h3>
+    <table><thead><tr><th>接口</th><th>方法</th><th>说明</th><th>请求参数</th><th>返回/处理字段</th><th>数据库表</th></tr></thead><tbody>
+      <tr><td>/robot-ops/monitor/video/stream-info</td><td>GET</td><td>获取视频流信息</td><td>无</td><td>视频地址(streamUrl)、播放协议(streamProtocol)、码流类型(streamType)、分辨率(resolution)、过期时间(expireTime)</td><td>实时接口返回,不落库</td></tr>
+      <tr><td>/robot-ops/monitor/video/status</td><td>GET</td><td>获取视频状态</td><td>无</td><td>播放状态(videoStatus)、错误码(errorCode)、错误信息(errorMsg)、更新时间(updateTime)</td><td>实时接口返回,不落库</td></tr>
+      <tr><td>/robot-ops/monitor/voice/shout</td><td>POST</td><td>执行远程喊话</td><td>喊话文本(shoutText)、音量(volume)、播放次数(playTimes)、是否打断(interruptFlag)</td><td>记录ID(id)、执行状态(resultStatus)、结果信息(resultMsg)</td><td>robot_ops_shout_record、robot_ops_operate_log</td></tr>
+      <tr><td>/robot-ops/monitor/voice/shout-record/page</td><td>GET</td><td>喊话记录分页</td><td>shoutText、resultStatus、createTimeStart、createTimeEnd、pageNum、pageSize</td><td>id、shoutText、volume、playTimes、interruptFlag、resultStatus、resultMsg、operateBy、createTime</td><td>robot_ops_shout_record</td></tr>
+      <tr><td>/robot-ops/monitor/dialogue/page</td><td>GET</td><td>对话日志分页</td><td>sessionId、keyword、hitType、sceneType、askTimeStart、askTimeEnd、pageNum、pageSize</td><td>id、sessionId、askTime、question、answerSummary、hitType、sceneType、resultStatus</td><td>robot_ops_dialogue_log</td></tr>
+      <tr><td>/robot-ops/monitor/dialogue/{id}</td><td>GET</td><td>对话详情</td><td>日志ID(id)</td><td>sessionId、askTime、question、answer、hitType、sceneType、resultStatus、rawRequest、rawResponse</td><td>robot_ops_dialogue_log</td></tr>
+      <tr><td>/robot-ops/monitor/dialogue/export</td><td>GET</td><td>导出对话日志</td><td>同分页查询条件</td><td>Excel文件</td><td>robot_ops_dialogue_log</td></tr>
+      <tr><td>/robot-ops/monitor/alarm/page</td><td>GET</td><td>安防告警分页</td><td>alarmType、alarmLevel、handleStatus、alarmTimeStart、alarmTimeEnd、pageNum、pageSize</td><td>id、alarmTime、alarmType、alarmLevel、sourcePosition、handleStatus、description</td><td>robot_ops_alarm_log</td></tr>
+      <tr><td>/robot-ops/monitor/alarm/{id}</td><td>GET</td><td>安防告警详情</td><td>告警ID(id)</td><td>alarmTime、alarmType、alarmLevel、sourcePosition、handleStatus、description、snapshotUrl、remark</td><td>robot_ops_alarm_log</td></tr>
+      <tr><td>/robot-ops/monitor/alarm/{id}/confirm</td><td>PUT</td><td>确认告警</td><td>告警ID(id)、备注(remark)</td><td>无</td><td>robot_ops_alarm_log</td></tr>
+      <tr><td>/robot-ops/monitor/alarm/{id}/ignore</td><td>PUT</td><td>忽略告警</td><td>告警ID(id)、备注(remark)</td><td>无</td><td>robot_ops_alarm_log</td></tr>
+      <tr><td>/robot-ops/monitor/alarm/export</td><td>GET</td><td>导出告警日志</td><td>同分页查询条件</td><td>Excel文件</td><td>robot_ops_alarm_log</td></tr>
+    </tbody></table>
+
+    <h3>7.7 运维管理接口</h3>
+    <h4>7.7.1 设备状态与设备控制接口</h4>
+    <table><thead><tr><th>接口</th><th>方法</th><th>说明</th><th>请求参数</th><th>返回/处理字段</th><th>数据库表</th></tr></thead><tbody>
+      <tr><td>/robot-ops/ops/device-status/detail</td><td>GET</td><td>设备状态详情</td><td>无</td><td>robotName、robotCode、serialNo、ipAddress、macAddress、uptime、onlineStatus、workStatus、batteryLevel、chargeStatus、networkStatus、cpuUsage、memoryUsage、diskUsage、temperature、cameraStatus、micStatus、speakerStatus、screenStatus、mainServiceStatus、serverTime、lastHeartbeatTime</td><td>实时接口返回,不落库</td></tr>
+      <tr><td>/robot-ops/ops/control/charge-start</td><td>POST</td><td>一键充电</td><td>无或扩展参数</td><td>taskId、resultStatus、resultMsg</td><td>robot_ops_control_record、robot_ops_operate_log</td></tr>
+      <tr><td>/robot-ops/ops/control/charge-stop</td><td>POST</td><td>停止充电</td><td>无或扩展参数</td><td>taskId、resultStatus、resultMsg</td><td>robot_ops_control_record、robot_ops_operate_log</td></tr>
+      <tr><td>/robot-ops/ops/control/reboot</td><td>POST</td><td>重启机器人</td><td>确认标识(confirmFlag)</td><td>taskId、resultStatus、resultMsg</td><td>robot_ops_control_record、robot_ops_operate_log</td></tr>
+      <tr><td>/robot-ops/ops/control/shutdown</td><td>POST</td><td>关机</td><td>确认标识(confirmFlag)</td><td>taskId、resultStatus、resultMsg</td><td>robot_ops_control_record、robot_ops_operate_log</td></tr>
+      <tr><td>/robot-ops/ops/control/restart-service</td><td>POST</td><td>重启服务</td><td>服务名称(serviceName)</td><td>taskId、serviceName、resultStatus、resultMsg</td><td>robot_ops_control_record、robot_ops_operate_log</td></tr>
+      <tr><td>/robot-ops/ops/control/audio-test</td><td>POST</td><td>音频测试</td><td>测试文本(testText,可选)</td><td>resultStatus、resultMsg</td><td>robot_ops_control_record、robot_ops_operate_log</td></tr>
+      <tr><td>/robot-ops/ops/control/screen-test</td><td>POST</td><td>屏幕测试</td><td>测试类型(testType,可选)</td><td>resultStatus、resultMsg</td><td>robot_ops_control_record、robot_ops_operate_log</td></tr>
+      <tr><td>/robot-ops/ops/control/record/page</td><td>GET</td><td>设备控制记录分页</td><td>controlType、resultStatus、createTimeStart、createTimeEnd、pageNum、pageSize</td><td>id、controlType、controlName、taskId、resultStatus、resultMsg、operateBy、createTime、finishTime</td><td>robot_ops_control_record</td></tr>
+      <tr><td>/robot-ops/ops/control/record/{id}</td><td>GET</td><td>设备控制记录详情</td><td>控制记录ID(id)</td><td>controlType、controlName、taskId、requestParam、resultStatus、resultMsg、operateBy、createTime、finishTime</td><td>robot_ops_control_record</td></tr>
+    </tbody></table>
+
+    <h4>7.7.2 运行参数接口</h4>
+    <table><thead><tr><th>接口</th><th>方法</th><th>说明</th><th>请求/返回字段</th><th>数据库表</th></tr></thead><tbody>
+      <tr><td>/robot-ops/ops/param/groups</td><td>GET</td><td>参数分组列表</td><td>groupCode、groupName、sortNo、status</td><td>robot_ops_param_group</td></tr>
+      <tr><td>/robot-ops/ops/param/page</td><td>GET</td><td>参数分页/分组参数</td><td>groupCode、keyword、pageNum、pageSize;返回 paramCode、paramName、paramValue、valueType、unit、editable、requiredFlag、minValue、maxValue、enumOptions、remark</td><td>robot_ops_param</td></tr>
+      <tr><td>/robot-ops/ops/param</td><td>PUT</td><td>保存参数</td><td>参数列表(params:paramCode、paramValue)</td><td>robot_ops_param、robot_ops_operate_log</td></tr>
+      <tr><td>/robot-ops/ops/param/reset</td><td>POST</td><td>重置参数</td><td>groupCode 或 paramCodes</td><td>robot_ops_param、robot_ops_operate_log</td></tr>
+    </tbody></table>
+    <div class="warn">参数保存时必须根据 valueType、requiredFlag、minValue、maxValue、enumOptions 做后端校验。</div>
+
+    <h4>7.7.3 系统诊断与日志接口</h4>
+    <table><thead><tr><th>接口</th><th>方法</th><th>说明</th><th>请求/返回字段</th><th>数据库表</th></tr></thead><tbody>
+      <tr><td>/robot-ops/ops/diagnosis/overview</td><td>GET</td><td>诊断总览</td><td>normalCount、warnCount、failCount、lastCheckTime</td><td>robot_ops_diagnosis_item</td></tr>
+      <tr><td>/robot-ops/ops/diagnosis/page</td><td>GET</td><td>诊断项分页</td><td>itemName、resultStatus、pageNum、pageSize;返回 itemCode、itemName、resultStatus、detailMsg、lastCheckTime</td><td>robot_ops_diagnosis_item</td></tr>
+      <tr><td>/robot-ops/ops/diagnosis/run</td><td>POST</td><td>执行诊断</td><td>诊断项编码(itemCodes,可选)</td><td>normalCount、warnCount、failCount、itemList</td><td>robot_ops_diagnosis_item、robot_ops_sys_log</td></tr>
+      <tr><td>/robot-ops/ops/diagnosis/export</td><td>GET</td><td>导出诊断结果</td><td>resultStatus</td><td>Excel文件</td><td>robot_ops_diagnosis_item</td></tr>
+      <tr><td>/robot-ops/ops/log/page</td><td>GET</td><td>日志分页</td><td>日志来源类型(sourceType:SYS/OPERATE,可选)、logType、logLevel、keyword、resultStatus、logTimeStart、logTimeEnd、pageNum、pageSize</td><td>id、日志来源类型(sourceType)、logTime、logType、logLevel、moduleName、contentSummary、resultStatus、traceId</td><td>robot_ops_sys_log、robot_ops_operate_log</td></tr>
+      <tr><td>/robot-ops/ops/log/{id}</td><td>GET</td><td>日志详情</td><td>日志ID(id)、日志来源类型(sourceType)</td><td>logTime、logType、logLevel、moduleName、content、resultStatus、traceId、remark</td><td>robot_ops_sys_log、robot_ops_operate_log</td></tr>
+      <tr><td>/robot-ops/ops/log/export</td><td>GET</td><td>导出日志</td><td>同日志分页查询条件</td><td>Excel文件</td><td>robot_ops_sys_log、robot_ops_operate_log</td></tr>
+    </tbody></table>
+
+    <h4>7.7.4 软件版本与 OTA 升级接口</h4>
+    <table><thead><tr><th>接口</th><th>方法</th><th>说明</th><th>请求/返回字段</th><th>数据库表</th></tr></thead><tbody>
+      <tr><td>/robot-ops/ops/version/page</td><td>GET</td><td>软件版本分页</td><td>moduleCode、moduleName、runStatus、pageNum、pageSize;返回 moduleCode、moduleName、currentVersion、installTime、runStatus</td><td>robot_ops_version_info</td></tr>
+      <tr><td>/robot-ops/ops/upgrade/package/upload</td><td>POST</td><td>上传升级包</td><td>文件(file)、模块编码(moduleCode)、目标版本(targetVersion)、安装包名称(packageName)</td><td>packageId、packageName、moduleCode、targetVersion、fileUrl、fileSize、uploadBy、uploadTime</td><td>robot_ops_upgrade_package</td></tr>
+      <tr><td>/robot-ops/ops/upgrade/package/page</td><td>GET</td><td>升级包分页</td><td>packageName、moduleCode、targetVersion、pageNum、pageSize</td><td>packageId、packageName、moduleCode、targetVersion、fileSize、uploadBy、uploadTime</td><td>robot_ops_upgrade_package</td></tr>
+      <tr><td>/robot-ops/ops/upgrade/package/{id}</td><td>DELETE</td><td>删除升级包</td><td>升级包ID(id)</td><td>无</td><td>robot_ops_upgrade_package</td></tr>
+      <tr><td>/robot-ops/ops/upgrade/execute</td><td>POST</td><td>执行升级</td><td>模块编码(moduleCode)、升级包ID(packageId)、确认标识(confirmFlag)</td><td>升级记录ID(recordId)、升级状态(resultStatus)、升级进度(progressPercent)、结果信息(resultMsg)</td><td>robot_ops_upgrade_record、robot_ops_operate_log</td></tr>
+      <tr><td>/robot-ops/ops/upgrade/record/page</td><td>GET</td><td>升级记录分页</td><td>moduleCode、resultStatus、startTimeStart、startTimeEnd、pageNum、pageSize</td><td>recordId、moduleCode、moduleName、currentVersion、targetVersion、startTime、endTime、resultStatus、resultMsg、progressPercent</td><td>robot_ops_upgrade_record</td></tr>
+      <tr><td>/robot-ops/ops/upgrade/record/{id}</td><td>GET</td><td>升级详情</td><td>升级记录ID(id)</td><td>moduleCode、moduleName、currentVersion、targetVersion、packageId、executeBy、startTime、endTime、resultStatus、resultMsg、progressPercent</td><td>robot_ops_upgrade_record</td></tr>
+    </tbody></table>
+
+    <h3>7.8 账号管理接口</h3>
+    <table><thead><tr><th>接口</th><th>方法</th><th>说明</th><th>请求/返回字段</th><th>数据库表</th></tr></thead><tbody>
+      <tr><td>/robot-ops/system/user/page</td><td>GET</td><td>账号分页</td><td>username、nickName、status、pageNum、pageSize;返回 userId、username、nickName、roleCode、status、lastLoginTime</td><td>robot_ops_user</td></tr>
+      <tr><td>/robot-ops/system/user/{id}</td><td>GET</td><td>账号详情</td><td>用户ID(id)</td><td>userId、username、nickName、roleCode、status、remark、lastLoginTime</td><td>robot_ops_user</td></tr>
+      <tr><td>/robot-ops/system/user</td><td>POST</td><td>新增账号</td><td>username、nickName、password、roleCode、status、remark</td><td>新增用户ID(userId)</td><td>robot_ops_user</td></tr>
+      <tr><td>/robot-ops/system/user</td><td>PUT</td><td>编辑账号</td><td>userId、nickName、roleCode、status、remark</td><td>无</td><td>robot_ops_user</td></tr>
+      <tr><td>/robot-ops/system/user/{id}</td><td>DELETE</td><td>删除账号</td><td>用户ID(id)</td><td>无</td><td>robot_ops_user</td></tr>
+      <tr><td>/robot-ops/system/user/{id}/reset-password</td><td>POST</td><td>重置密码</td><td>用户ID(id)、新密码(newPassword)</td><td>无</td><td>robot_ops_user</td></tr>
+    </tbody></table>
+    <div class="warn">admin 默认账号不可删除;账号停用后不可登录;所有账号新增、编辑、删除、重置密码操作均需写入操作日志。</div>
+  </div>
+
+  <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">问答分类使用 RuoYi 系统字典能力维护,不单独建设 robot_ops_faq_category 表。建议在 RuoYi 字典中新增字典类型 <code class="inline">robot_faq_category</code>,字典项可配置为:接待问候、业务咨询、场馆介绍、访客引导、安防提示、设备说明、其他。</div>
+
+    <h3>8.1 基础与权限表</h3>
+    <h4>8.1.1 本地账号表 robot_ops_user</h4>
+    <div class="code">CREATE TABLE `robot_ops_user` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `username` VARCHAR(50) NOT NULL COMMENT '登录账号',
+  `password` VARCHAR(100) NOT NULL COMMENT '登录密码,加密存储',
+  `nick_name` VARCHAR(50) DEFAULT NULL COMMENT '用户姓名/昵称',
+  `role_code` VARCHAR(50) DEFAULT NULL COMMENT '角色编码:ADMIN、OPS、VIEWER',
+  `status` CHAR(1) NOT NULL DEFAULT '1' COMMENT '账号状态:0停用,1启用',
+  `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
+  `last_login_time` DATETIME DEFAULT NULL COMMENT '最近登录时间',
+  `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`),
+  UNIQUE KEY `uk_robot_ops_user_username` (`username`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='本地后台账号表';</div>
+
+    <h4>8.1.2 系统基础配置表 robot_ops_system_config</h4>
+    <div class="code">CREATE TABLE `robot_ops_system_config` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `system_name` VARCHAR(100) DEFAULT NULL COMMENT '后台系统名称',
+  `system_logo` VARCHAR(255) DEFAULT NULL COMMENT '后台系统Logo地址',
+  `footer_text` VARCHAR(255) DEFAULT NULL COMMENT '页脚文案',
+  `record_no` VARCHAR(100) DEFAULT NULL COMMENT '备案号/版权信息',
+  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统基础配置表';</div>
+
+    <h3>8.2 内容管理表</h3>
+    <h4>8.2.1 欢迎语配置表 robot_ops_welcome_config</h4>
+    <div class="code">CREATE TABLE `robot_ops_welcome_config` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `welcome_text` VARCHAR(500) NOT NULL COMMENT '欢迎语文本',
+  `voice_enabled` CHAR(1) NOT NULL DEFAULT '1' COMMENT '是否启用语音播报:0否,1是',
+  `cooldown_seconds` INT DEFAULT 30 COMMENT '触发冷却时间,单位秒',
+  `status` CHAR(1) NOT NULL DEFAULT '1' COMMENT '启用状态:0停用,1启用',
+  `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
+  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='欢迎语配置表';</div>
+
+    <h4>8.2.2 问答库表 robot_ops_faq</h4>
+    <div class="code">CREATE TABLE `robot_ops_faq` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `category_type` VARCHAR(50) DEFAULT NULL COMMENT '问题分类字典值,字典类型:robot_faq_category',
+  `question` VARCHAR(500) NOT NULL COMMENT '标准问题',
+  `answer` TEXT NOT NULL COMMENT '答案内容',
+  `sort_no` INT DEFAULT 0 COMMENT '排序号,数字越小越靠前',
+  `status` CHAR(1) NOT NULL DEFAULT '1' COMMENT '启用状态:0停用,1启用',
+  `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
+  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_robot_ops_faq_category_type` (`category_type`),
+  KEY `idx_robot_ops_faq_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='问答库主表';</div>
+
+    <h4>8.2.3 相似问表 robot_ops_faq_similar</h4>
+    <div class="code">CREATE TABLE `robot_ops_faq_similar` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `faq_id` BIGINT NOT NULL COMMENT '问答ID,关联robot_ops_faq.id',
+  `similar_question` VARCHAR(500) NOT NULL COMMENT '相似问文本',
+  `sort_no` INT DEFAULT 0 COMMENT '排序号,数字越小越靠前',
+  PRIMARY KEY (`id`),
+  KEY `idx_robot_ops_faq_similar_faq_id` (`faq_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='问答相似问表';</div>
+
+    <h4>8.2.4 素材资源表 robot_ops_media_asset</h4>
+    <div class="code">CREATE TABLE `robot_ops_media_asset` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `asset_name` VARCHAR(100) NOT NULL COMMENT '素材名称',
+  `asset_type` VARCHAR(20) NOT NULL COMMENT '素材类型:image图片,video视频',
+  `file_url` VARCHAR(255) NOT NULL COMMENT '素材文件地址',
+  `thumbnail_url` VARCHAR(255) DEFAULT NULL COMMENT '缩略图地址',
+  `file_size` BIGINT DEFAULT NULL COMMENT '文件大小,单位字节',
+  `file_format` VARCHAR(20) DEFAULT NULL COMMENT '文件格式,如jpg、png、webp、mp4',
+  `duration_seconds` INT DEFAULT NULL COMMENT '视频时长,单位秒;图片为空',
+  `resolution` VARCHAR(50) DEFAULT NULL COMMENT '分辨率,如1920x1080',
+  `status` CHAR(1) NOT NULL DEFAULT '1' COMMENT '启用状态:0停用,1启用',
+  `quoted_flag` CHAR(1) NOT NULL DEFAULT '0' COMMENT '是否被播放方案引用:0否,1是',
+  `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
+  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间/上传时间',
+  `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_robot_ops_media_asset_type` (`asset_type`),
+  KEY `idx_robot_ops_media_asset_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='素材资源表';</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是',
+  `status` CHAR(1) NOT NULL DEFAULT '1' COMMENT '启用状态:0停用,1启用',
+  `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
+  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP 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_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='播放方案主表';</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 '播放顺序,数字越小越靠前',
+  `stay_seconds` INT DEFAULT NULL COMMENT '停留时长,图片必填,视频可为空',
+  `transition_type` VARCHAR(50) DEFAULT NULL 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`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='播放方案素材明细表';</div>
+
+    <h4>8.2.7 播报内容表 robot_ops_broadcast_content</h4>
+    <div class="code">CREATE TABLE `robot_ops_broadcast_content` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `content_name` VARCHAR(100) NOT NULL COMMENT '播报内容名称',
+  `content_type` VARCHAR(50) DEFAULT NULL COMMENT '内容分类:通知、宣传、提示、安防提醒、自定义',
+  `broadcast_text` VARCHAR(2000) NOT NULL COMMENT '播报文本',
+  `status` CHAR(1) NOT NULL DEFAULT '1' COMMENT '启用状态:0停用,1启用',
+  `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
+  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_robot_ops_broadcast_content_type` (`content_type`),
+  KEY `idx_robot_ops_broadcast_content_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='播报内容表';</div>
+
+    <h4>8.2.8 播报任务表 robot_ops_broadcast_task</h4>
+    <div class="code">CREATE TABLE `robot_ops_broadcast_task` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `task_name` VARCHAR(100) NOT NULL COMMENT '播报任务名称',
+  `content_id` BIGINT NOT NULL COMMENT '播报内容ID,关联robot_ops_broadcast_content.id',
+  `start_time` VARCHAR(8) DEFAULT NULL COMMENT '开始时间,格式HH:mm:ss',
+  `end_time` VARCHAR(8) DEFAULT NULL COMMENT '结束时间,格式HH:mm:ss',
+  `frequency_minutes` INT DEFAULT NULL COMMENT '播报频率,单位分钟',
+  `cycle_type` VARCHAR(20) DEFAULT NULL COMMENT '循环类型字典值,字典类型:broadcast_task_cycle_type,1按星期,2按日期',
+  `cycle_value` VARCHAR(255) DEFAULT NULL COMMENT '循环取值:cycle_type=1时保存星期值,如1,2,3,4,5;cycle_type=2时保存日期值,如2026-03-20,2026-03-21',
+  `status` CHAR(1) NOT NULL DEFAULT '1' COMMENT '启用状态:0停用,1启用',
+  `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
+  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_robot_ops_broadcast_task_content_id` (`content_id`),
+  KEY `idx_robot_ops_broadcast_task_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='播报任务表';
+    <div class="note">播报任务循环类型使用 RuoYi 字典 <code class="inline">broadcast_task_cycle_type</code>,字典项建议配置为:1=按星期,2=按日期。按星期时,cycle_value 中 1-7 分别代表星期一到星期日。</div></div>
+
+    <h4>8.2.9 展示主题表 robot_ops_theme</h4>
+    <div class="code">CREATE TABLE `robot_ops_theme` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `theme_name` VARCHAR(100) NOT NULL COMMENT '主题名称',
+  `logo_url` VARCHAR(255) DEFAULT NULL COMMENT 'Logo地址',
+  `background_type` VARCHAR(20) DEFAULT NULL COMMENT '背景类型:image图片,video视频,color纯色',
+  `background_url` VARCHAR(255) DEFAULT NULL COMMENT '背景资源地址',
+  `primary_color` VARCHAR(20) DEFAULT NULL COMMENT '主题主色',
+  `secondary_color` VARCHAR(20) DEFAULT NULL COMMENT '辅助色',
+  `welcome_title` VARCHAR(200) DEFAULT NULL COMMENT '欢迎标题',
+  `welcome_sub_title` VARCHAR(500) DEFAULT NULL COMMENT '欢迎副标题',
+  `status` CHAR(1) NOT NULL DEFAULT '1' COMMENT '启用状态:0停用,1启用',
+  `current_enabled` CHAR(1) NOT NULL DEFAULT '0' COMMENT '是否当前启用主题:0否,1是',
+  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_robot_ops_theme_current_enabled` (`current_enabled`),
+  KEY `idx_robot_ops_theme_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='机器人展示主题表';</div>
+
+    <h3>8.3 访客管理表</h3>
+    <h4>8.3.1 访客记录表 robot_ops_visitor_record</h4>
+    <div class="code">CREATE TABLE `robot_ops_visitor_record` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `visitor_name` VARCHAR(100) DEFAULT NULL COMMENT '访客姓名',
+  `mobile` VARCHAR(20) DEFAULT NULL COMMENT '访客手机号',
+  `id_card_no` VARCHAR(50) DEFAULT NULL COMMENT '证件号码',
+  `register_type` VARCHAR(50) DEFAULT NULL COMMENT '登记方式:读卡器、人工、本地同步等',
+  `visitor_photo` VARCHAR(255) DEFAULT NULL COMMENT '访客照片地址',
+  `appointment_no` VARCHAR(50) DEFAULT NULL COMMENT '关联预约单号',
+  `visited_person` VARCHAR(100) DEFAULT NULL COMMENT '被访人/被访对象',
+  `visit_time` DATETIME DEFAULT NULL COMMENT '来访时间/登记时间',
+  `result_status` VARCHAR(20) DEFAULT NULL COMMENT '登记结果:SUCCESS成功,FAIL失败,WHITELIST命中白名单',
+  `source_type` VARCHAR(20) DEFAULT NULL COMMENT '来源类型:LOCAL本地,PLATFORM平台同步',
+  `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
+  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_robot_ops_visitor_record_visit_time` (`visit_time`),
+  KEY `idx_robot_ops_visitor_record_mobile` (`mobile`),
+  KEY `idx_robot_ops_visitor_record_appointment_no` (`appointment_no`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='访客登记记录表';</div>
+
+    <h4>8.3.2 预约记录表 robot_ops_appointment_record</h4>
+    <div class="code">CREATE TABLE `robot_ops_appointment_record` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `appointment_no` VARCHAR(50) NOT NULL COMMENT '预约单号',
+  `visitor_name` VARCHAR(100) DEFAULT NULL COMMENT '访客姓名',
+  `mobile` VARCHAR(20) DEFAULT NULL COMMENT '访客手机号',
+  `visited_person` VARCHAR(100) DEFAULT NULL COMMENT '被访人/被访对象',
+  `appointment_time` DATETIME DEFAULT NULL COMMENT '预约到访时间',
+  `status` VARCHAR(20) DEFAULT NULL COMMENT '预约状态:待到访、已到访、已取消、已过期',
+  `sync_time` DATETIME DEFAULT NULL COMMENT '同步到本地时间',
+  `source_platform` VARCHAR(50) DEFAULT NULL COMMENT '来源平台',
+  `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
+  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_robot_ops_appointment_no` (`appointment_no`),
+  KEY `idx_robot_ops_appointment_time` (`appointment_time`),
+  KEY `idx_robot_ops_appointment_mobile` (`mobile`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='访客预约记录表';</div>
+
+    <h4>8.3.3 白名单表 robot_ops_whitelist</h4>
+    <div class="code">CREATE TABLE `robot_ops_whitelist` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `name` VARCHAR(100) NOT NULL COMMENT '人员姓名',
+  `mobile` VARCHAR(20) DEFAULT NULL COMMENT '手机号',
+  `whitelist_type` VARCHAR(50) DEFAULT NULL COMMENT '白名单类型:访客、内部人员、人脸白名单等',
+  `source_type` VARCHAR(20) DEFAULT NULL COMMENT '来源类型:LOCAL本地,PLATFORM平台同步',
+  `status` CHAR(1) NOT NULL DEFAULT '1' COMMENT '启用状态:0停用,1启用',
+  `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
+  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_robot_ops_whitelist_mobile` (`mobile`),
+  KEY `idx_robot_ops_whitelist_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='访客白名单表';</div>
+
+    <h3>8.4 监控与日志表</h3>
+    <h4>8.4.1 远程喊话记录表 robot_ops_shout_record</h4>
+    <div class="code">CREATE TABLE `robot_ops_shout_record` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `shout_text` VARCHAR(1000) NOT NULL COMMENT '喊话文本',
+  `volume` INT DEFAULT NULL COMMENT '音量,0-100',
+  `play_times` INT DEFAULT 1 COMMENT '播放次数',
+  `interrupt_flag` CHAR(1) DEFAULT '0' COMMENT '是否打断当前播报:0否,1是',
+  `result_status` VARCHAR(20) DEFAULT NULL COMMENT '执行状态:SUCCESS成功,FAIL失败,RUNNING执行中',
+  `result_msg` VARCHAR(500) DEFAULT NULL COMMENT '执行结果信息/失败原因',
+  `operate_by` VARCHAR(64) DEFAULT NULL COMMENT '操作人',
+  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间/喊话时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_robot_ops_shout_record_create_time` (`create_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='远程喊话记录表';</div>
+
+    <h4>8.4.2 对话日志表 robot_ops_dialogue_log</h4>
+    <div class="code">CREATE TABLE `robot_ops_dialogue_log` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `session_id` VARCHAR(100) DEFAULT NULL COMMENT '会话ID',
+  `ask_time` DATETIME DEFAULT NULL COMMENT '提问时间',
+  `question` VARCHAR(1000) DEFAULT NULL COMMENT '用户问题',
+  `answer` TEXT COMMENT '机器人回答',
+  `hit_type` VARCHAR(50) DEFAULT NULL COMMENT '命中方式:FAQ命中、未命中、其他',
+  `scene_type` VARCHAR(50) DEFAULT NULL COMMENT '来源场景:欢迎接待、咨询问答、其他',
+  `result_status` VARCHAR(20) DEFAULT NULL COMMENT '结果状态:SUCCESS成功,FAIL失败,TIMEOUT超时',
+  `raw_request` TEXT COMMENT '原始请求内容',
+  `raw_response` TEXT COMMENT '原始响应内容',
+  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_robot_ops_dialogue_log_session_id` (`session_id`),
+  KEY `idx_robot_ops_dialogue_log_ask_time` (`ask_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='人机对话日志表';</div>
+
+    <h4>8.4.3 安防告警日志表 robot_ops_alarm_log</h4>
+    <div class="code">CREATE TABLE `robot_ops_alarm_log` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `alarm_time` DATETIME DEFAULT NULL COMMENT '告警时间',
+  `alarm_type` VARCHAR(50) DEFAULT NULL COMMENT '告警类型',
+  `alarm_level` VARCHAR(20) DEFAULT NULL COMMENT '告警级别:LOW低,MEDIUM中,HIGH高,CRITICAL紧急',
+  `source_position` VARCHAR(100) DEFAULT NULL COMMENT '来源位置/区域/模块',
+  `handle_status` VARCHAR(20) DEFAULT 'UNHANDLED' COMMENT '处理状态:UNHANDLED未处理,CONFIRMED已确认,IGNORED已忽略',
+  `description` VARCHAR(1000) DEFAULT NULL COMMENT '告警描述',
+  `snapshot_url` VARCHAR(255) DEFAULT NULL COMMENT '抓拍图地址',
+  `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
+  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_robot_ops_alarm_log_time` (`alarm_time`),
+  KEY `idx_robot_ops_alarm_log_level` (`alarm_level`),
+  KEY `idx_robot_ops_alarm_log_status` (`handle_status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='安防告警日志表';</div>
+
+    <h4>8.4.4 系统日志表 robot_ops_sys_log</h4>
+    <div class="code">CREATE TABLE `robot_ops_sys_log` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `log_time` DATETIME DEFAULT NULL COMMENT '日志时间',
+  `log_type` VARCHAR(50) DEFAULT NULL COMMENT '日志类型:系统日志、设备日志、升级日志、服务日志',
+  `log_level` VARCHAR(20) DEFAULT NULL COMMENT '日志级别:INFO、WARN、ERROR',
+  `module_name` VARCHAR(100) DEFAULT NULL COMMENT '模块名称',
+  `content` TEXT COMMENT '日志内容',
+  `result_status` VARCHAR(20) DEFAULT NULL COMMENT '结果状态',
+  `trace_id` VARCHAR(100) DEFAULT NULL COMMENT '链路追踪ID',
+  `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
+  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_robot_ops_sys_log_time` (`log_time`),
+  KEY `idx_robot_ops_sys_log_type` (`log_type`),
+  KEY `idx_robot_ops_sys_log_level` (`log_level`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统日志表';</div>
+
+    <h4>8.4.5 操作日志表 robot_ops_operate_log</h4>
+    <div class="code">CREATE TABLE `robot_ops_operate_log` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `operate_time` DATETIME DEFAULT NULL COMMENT '操作时间',
+  `operate_user` VARCHAR(64) DEFAULT NULL COMMENT '操作人',
+  `module_name` VARCHAR(100) DEFAULT NULL COMMENT '操作模块',
+  `operate_type` VARCHAR(50) DEFAULT NULL COMMENT '操作类型:新增、编辑、删除、控制、升级等',
+  `operate_content` VARCHAR(1000) DEFAULT NULL COMMENT '操作内容',
+  `result_status` VARCHAR(20) DEFAULT NULL COMMENT '操作结果:SUCCESS成功,FAIL失败',
+  `result_msg` VARCHAR(500) DEFAULT NULL COMMENT '结果信息/失败原因',
+  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_robot_ops_operate_log_time` (`operate_time`),
+  KEY `idx_robot_ops_operate_log_user` (`operate_user`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='后台操作日志表';</div>
+
+    <h3>8.5 运维与升级表</h3>
+    <h4>8.5.1 参数分组表 robot_ops_param_group</h4>
+    <div class="code">CREATE TABLE `robot_ops_param_group` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `group_code` VARCHAR(50) NOT NULL COMMENT '参数分组编码',
+  `group_name` VARCHAR(100) NOT NULL COMMENT '参数分组名称',
+  `sort_no` INT DEFAULT 0 COMMENT '排序号,数字越小越靠前',
+  `status` CHAR(1) NOT NULL DEFAULT '1' COMMENT '启用状态:0停用,1启用',
+  `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_robot_ops_param_group_code` (`group_code`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='运行参数分组表';</div>
+
+    <h4>8.5.2 设备控制记录表 robot_ops_control_record</h4>
+    <div class="code">CREATE TABLE `robot_ops_control_record` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `control_type` VARCHAR(50) NOT NULL COMMENT '控制类型:充电、停止充电、重启、关机、重启服务、音频测试、屏幕测试等',
+  `control_name` VARCHAR(100) DEFAULT NULL COMMENT '控制名称',
+  `task_id` VARCHAR(100) DEFAULT NULL COMMENT '机器人侧返回的任务ID',
+  `request_param` TEXT COMMENT '请求参数JSON',
+  `result_status` VARCHAR(20) DEFAULT NULL COMMENT '执行状态:SUCCESS成功,FAIL失败,RUNNING执行中,PENDING等待中',
+  `result_msg` VARCHAR(500) DEFAULT NULL COMMENT '执行结果信息/失败原因',
+  `operate_by` VARCHAR(64) DEFAULT NULL COMMENT '操作人',
+  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间/下发时间',
+  `finish_time` DATETIME DEFAULT NULL COMMENT '完成时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_robot_ops_control_record_task_id` (`task_id`),
+  KEY `idx_robot_ops_control_record_type` (`control_type`),
+  KEY `idx_robot_ops_control_record_time` (`create_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设备控制记录表';</div>
+
+    <h4>8.5.3 运行参数表 robot_ops_param</h4>
+    <div class="code">CREATE TABLE `robot_ops_param` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `group_code` VARCHAR(50) NOT NULL COMMENT '参数分组编码,关联robot_ops_param_group.group_code',
+  `param_code` VARCHAR(100) NOT NULL COMMENT '参数编码',
+  `param_name` VARCHAR(100) NOT NULL COMMENT '参数名称',
+  `param_value` VARCHAR(2000) DEFAULT NULL COMMENT '参数值',
+  `value_type` VARCHAR(20) DEFAULT NULL COMMENT '值类型:string、int、float、boolean、enum、json',
+  `unit` VARCHAR(20) DEFAULT NULL COMMENT '单位',
+  `editable` CHAR(1) NOT NULL DEFAULT '1' COMMENT '是否可编辑:0否,1是',
+  `required_flag` CHAR(1) NOT NULL DEFAULT '0' COMMENT '是否必填:0否,1是',
+  `min_value` VARCHAR(50) DEFAULT NULL COMMENT '最小值',
+  `max_value` VARCHAR(50) DEFAULT NULL COMMENT '最大值',
+  `enum_options` VARCHAR(2000) DEFAULT NULL COMMENT '枚举项JSON',
+  `remark` VARCHAR(500) DEFAULT NULL COMMENT '参数说明/备注',
+  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_robot_ops_param_code` (`group_code`,`param_code`),
+  KEY `idx_robot_ops_param_group_code` (`group_code`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='运行参数配置表';</div>
+
+    <h4>8.5.4 系统诊断项表 robot_ops_diagnosis_item</h4>
+    <div class="code">CREATE TABLE `robot_ops_diagnosis_item` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `item_code` VARCHAR(50) NOT NULL COMMENT '诊断项编码',
+  `item_name` VARCHAR(100) NOT NULL COMMENT '诊断项名称',
+  `result_status` VARCHAR(20) DEFAULT NULL COMMENT '检查结果:NORMAL正常,WARN告警,FAIL失败',
+  `detail_msg` VARCHAR(1000) DEFAULT NULL COMMENT '详情描述',
+  `last_check_time` DATETIME DEFAULT NULL COMMENT '最后检查时间',
+  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_robot_ops_diagnosis_item_code` (`item_code`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统诊断项表';</div>
+    <div class="note">说明:一期仅保留各诊断项的最新诊断结果;如后续需要追踪每次诊断历史,可在二期扩展诊断记录表和诊断明细表。</div>
+
+    <h4>8.5.5 软件版本信息表 robot_ops_version_info</h4>
+    <div class="code">CREATE TABLE `robot_ops_version_info` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `module_code` VARCHAR(50) NOT NULL COMMENT '模块编码',
+  `module_name` VARCHAR(100) NOT NULL COMMENT '模块名称',
+  `current_version` VARCHAR(50) DEFAULT NULL COMMENT '当前版本号',
+  `install_time` DATETIME DEFAULT NULL COMMENT '安装时间',
+  `run_status` VARCHAR(20) DEFAULT NULL COMMENT '运行状态:RUNNING运行中,STOPPED已停止,ERROR异常',
+  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_robot_ops_version_module_code` (`module_code`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='软件版本信息表';</div>
+
+    <h4>8.5.6 升级包表 robot_ops_upgrade_package</h4>
+    <div class="code">CREATE TABLE `robot_ops_upgrade_package` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `package_name` VARCHAR(100) NOT NULL COMMENT '安装包名称',
+  `module_code` VARCHAR(50) NOT NULL COMMENT '升级模块编码',
+  `target_version` VARCHAR(50) NOT NULL COMMENT '目标版本号',
+  `file_url` VARCHAR(255) NOT NULL COMMENT '升级包文件地址',
+  `file_size` BIGINT DEFAULT NULL COMMENT '文件大小,单位字节',
+  `upload_by` VARCHAR(64) DEFAULT NULL COMMENT '上传人',
+  `upload_time` DATETIME DEFAULT NULL COMMENT '上传完成时间',
+  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_robot_ops_upgrade_package_module` (`module_code`),
+  KEY `idx_robot_ops_upgrade_package_version` (`target_version`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='OTA升级包表';</div>
+
+    <h4>8.5.7 升级记录表 robot_ops_upgrade_record</h4>
+    <div class="code">CREATE TABLE `robot_ops_upgrade_record` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `module_code` VARCHAR(50) NOT NULL COMMENT '模块编码',
+  `module_name` VARCHAR(100) DEFAULT NULL COMMENT '模块名称',
+  `current_version` VARCHAR(50) DEFAULT NULL COMMENT '原版本号',
+  `target_version` VARCHAR(50) DEFAULT NULL COMMENT '目标版本号',
+  `package_id` BIGINT DEFAULT NULL COMMENT '升级包ID,关联robot_ops_upgrade_package.id',
+  `execute_by` VARCHAR(64) DEFAULT NULL COMMENT '执行人',
+  `start_time` DATETIME DEFAULT NULL COMMENT '升级开始时间',
+  `end_time` DATETIME DEFAULT NULL COMMENT '升级结束时间',
+  `result_status` VARCHAR(20) DEFAULT NULL COMMENT '升级状态:SUCCESS成功,FAIL失败,RUNNING升级中,PENDING等待中',
+  `result_msg` VARCHAR(1000) DEFAULT NULL COMMENT '升级结果信息/失败原因',
+  `progress_percent` INT DEFAULT 0 COMMENT '升级进度百分比,0-100',
+  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_robot_ops_upgrade_record_module` (`module_code`),
+  KEY `idx_robot_ops_upgrade_record_status` (`result_status`),
+  KEY `idx_robot_ops_upgrade_record_start_time` (`start_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='OTA升级记录表';</div>
+
+    <div class="note">一期不建设机器人基础信息表和设备状态快照表。首页总览、设备状态页、视频预览页的数据以机器人侧实时接口为准;如二期需要状态历史趋势、离线查看最近状态、故障追溯统计,再扩展设备状态快照表。</div>
+  </div>
+
+  <div class="section" id="s9"><h2>9. 状态、日志与控制规则</h2>
+    <h3>9.1 状态枚举建议</h3><table><thead><tr><th>字段</th><th>建议值</th></tr></thead><tbody><tr><td>onlineStatus</td><td>ONLINE / OFFLINE</td></tr><tr><td>workStatus</td><td>IDLE / RECEIVING / BROADCASTING / CHARGING / ERROR</td></tr><tr><td>chargeStatus</td><td>NOT_CHARGING / CHARGING / FULL</td></tr><tr><td>resultStatus</td><td>SUCCESS / FAIL / RUNNING / PENDING</td></tr><tr><td>alarmLevel</td><td>LOW / MEDIUM / HIGH / CRITICAL</td></tr><tr><td>status</td><td>0 停用 / 1 启用</td></tr></tbody></table>
+    <h3>9.2 控制规则</h3><ul><li>重启、关机、充电、停止充电、升级等操作必须记录操作日志。</li><li>重启、关机、升级操作必须弹窗二次确认。</li><li>当机器人处于升级中时,除查看类操作外,禁止执行关机、重启、参数保存等高风险动作。</li><li>当设备离线时,控制按钮置灰并显示“设备离线,无法执行”。</li></ul>
+    <h3>9.3 日志规则</h3><ul><li>所有关键业务操作必须写入操作日志。</li><li>机器人端回传的异常和诊断结果,统一映射到日志中心或诊断中心展示。</li><li>日志保留时长一期默认 180 天,可由系统参数配置。</li></ul>
+  </div>
+
+  <div class="section" id="s10"><h2>10. 权限与账号设计</h2><table><thead><tr><th>角色</th><th>默认权限</th></tr></thead><tbody><tr><td>ADMIN</td><td>全量权限,包括账号管理、参数配置、设备控制、OTA 升级。</td></tr><tr><td>OPS</td><td>首页、内容管理、访客管理、监控管理、运维管理(除账号管理)。</td></tr><tr><td>VIEWER</td><td>仅查看权限,不可执行新增、编辑、删除、升级、控制等动作。</td></tr></tbody></table><p>RuoYi 菜单权限与按钮权限均需保留,避免后期返工。即使一期只有 admin,也要按标准权限框架开发。</p></div>
+
+  <div class="section" id="s11"><h2>11. 开发优先级与实施顺序</h2><table><thead><tr><th>阶段</th><th>模块</th><th>说明</th></tr></thead><tbody><tr><td>阶段一</td><td>登录、首页、设备状态、设备控制、参数配置、日志中心、版本/OTA</td><td>先打通基础运维闭环。</td></tr><tr><td>阶段二</td><td>欢迎语、问答库、素材管理、播放方案、播报内容、播报任务、主题配置</td><td>打通内容配置闭环。</td></tr><tr><td>阶段三</td><td>访客记录、预约记录、白名单、视频预览、远程喊话、对话日志、安防告警日志</td><td>补齐业务查询和监控能力。</td></tr><tr><td>阶段四</td><td>优化、导入导出、性能提升、操作审计完善</td><td>稳定化阶段。</td></tr></tbody></table></div>
+
+  <div class="section" id="s12"><h2>12. 对其他团队的配合要求</h2><div class="danger">以下内容不再作为“待定事项”,而是作为其他团队必须按本文配合实现的内容。</div><ul><li><strong>机器人侧:</strong>需提供首页状态接口、设备状态接口、控制接口、视频流信息接口、喊话接口、参数接口、日志接口、版本与升级接口。</li><li><strong>主控平台侧:</strong>需提供预约记录同步接口、可选白名单同步接口。</li><li><strong>展示端:</strong>需支持欢迎语、播放方案、播报任务、展示主题配置的读取与应用。</li><li><strong>算法 / 安防侧:</strong>需向运维端提供安防告警记录标准数据结构。</li></ul><div class="ok">结论:本文件已经作为一期开发基线文档定版。后续若有调整,应基于本文迭代版本,而不是推翻本文重新回到需求澄清阶段。</div></div>
+
+  <div class="section" id="s13"><h2>13. 测试验收要点</h2><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>欢迎语、问答库、素材、方案、播报任务、主题均可增删改查。</td></tr><tr><td>访客管理</td><td>访客记录、预约记录、白名单可查询,导出结果正确。</td></tr><tr><td>监控管理</td><td>视频可预览,喊话可执行,对话日志与告警日志可查询。</td></tr><tr><td>运维管理</td><td>设备状态可展示,控制操作有确认弹窗和结果反馈,参数保存有效,OTA 流程完整。</td></tr><tr><td>日志与权限</td><td>关键操作写入操作日志,不同角色权限生效。</td></tr></tbody></table></div>
+
+  <div class="footer">文档版本:V2.1(完整详细设计开发版)</div>
+</div>
+</body>
+</html>