소스 검색

完善运行日志

yawuga 1 주 전
부모
커밋
29d37f0cfd
3개의 변경된 파일215개의 추가작업 그리고 237개의 파일을 삭제
  1. 0 26
      src/api/base/sysLog.js
  2. 158 195
      src/views/base/sysLog/index.vue
  3. 57 16
      迎宾巡逻安防机器人运维端Web管理系统详细设计开发文档_V2.1.html

+ 0 - 26
src/api/base/sysLog.js

@@ -16,29 +16,3 @@ export function getSysLog(id) {
     method: 'get'
   })
 }
-
-// 新增运行日志
-export function addSysLog(data) {
-  return request({
-    url: '/base/sysLog',
-    method: 'post',
-    data: data
-  })
-}
-
-// 修改运行日志
-export function updateSysLog(data) {
-  return request({
-    url: '/base/sysLog',
-    method: 'put',
-    data: data
-  })
-}
-
-// 删除运行日志
-export function delSysLog(id) {
-  return request({
-    url: '/base/sysLog/' + id,
-    method: 'delete'
-  })
-}

+ 158 - 195
src/views/base/sysLog/index.vue

@@ -1,32 +1,27 @@
 <template>
   <div class="app-container">
-    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
+    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="90px">
       <el-form-item label="机器人编号" prop="robotCode">
         <el-input
           v-model="queryParams.robotCode"
           placeholder="请输入机器人编号"
           clearable
+          class="search-input"
           @keyup.enter="handleQuery"
         />
       </el-form-item>
-      <el-form-item label="日志时间,设备侧或服务侧日志发生时间" prop="logTime">
-        <el-date-picker clearable
-          v-model="queryParams.logTime"
-          type="date"
-          value-format="YYYY-MM-DD"
-          placeholder="请选择日志时间,设备侧或服务侧日志发生时间">
-        </el-date-picker>
-      </el-form-item>
-      <el-form-item label="日志接收时间" prop="receiveTime">
-        <el-date-picker clearable
-          v-model="queryParams.receiveTime"
-          type="date"
-          value-format="YYYY-MM-DD"
-          placeholder="请选择日志接收时间">
-        </el-date-picker>
+      <el-form-item label="日志级别" prop="logLevel">
+        <el-select v-model="queryParams.logLevel" placeholder="请选择日志级别" clearable class="search-select">
+          <el-option
+            v-for="dict in robot_log_level"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
       </el-form-item>
       <el-form-item label="日志类型" prop="logType">
-        <el-select v-model="queryParams.logType" placeholder="请选择日志类型" clearable>
+        <el-select v-model="queryParams.logType" placeholder="请选择日志类型" clearable class="search-select">
           <el-option
             v-for="dict in robot_log_type"
             :key="dict.value"
@@ -35,24 +30,26 @@
           />
         </el-select>
       </el-form-item>
-      <el-form-item label="日志级别" prop="logLevel">
+      <el-form-item label="模块名称" prop="moduleName">
         <el-input
-          v-model="queryParams.logLevel"
-          placeholder="请输入日志级别"
+          v-model="queryParams.moduleName"
+          placeholder="请输入模块名称"
           clearable
+          class="search-input"
           @keyup.enter="handleQuery"
         />
       </el-form-item>
-      <el-form-item label="模块名称,如 screen、audio、camera、network、ota" prop="moduleName">
+      <el-form-item label="关键字" prop="keyword">
         <el-input
-          v-model="queryParams.moduleName"
-          placeholder="请输入模块名称,如 screen、audio、camera、network、ota"
+          v-model="queryParams.keyword"
+          placeholder="请输入日志摘要/内容关键字"
           clearable
+          class="search-input"
           @keyup.enter="handleQuery"
         />
       </el-form-item>
       <el-form-item label="结果状态" prop="resultStatus">
-        <el-select v-model="queryParams.resultStatus" placeholder="请选择结果状态" clearable>
+        <el-select v-model="queryParams.resultStatus" placeholder="请选择结果状态" clearable class="search-select">
           <el-option
             v-for="dict in common_result_status"
             :key="dict.value"
@@ -61,12 +58,15 @@
           />
         </el-select>
       </el-form-item>
-      <el-form-item label="链路追踪ID" prop="traceId">
-        <el-input
-          v-model="queryParams.traceId"
-          placeholder="请输入链路追踪ID"
-          clearable
-          @keyup.enter="handleQuery"
+      <el-form-item label="日志时间">
+        <el-date-picker
+          v-model="daterangeLogTime"
+          value-format="YYYY-MM-DD"
+          type="daterange"
+          range-separator="-"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          class="search-date"
         />
       </el-form-item>
       <el-form-item>
@@ -76,35 +76,6 @@
     </el-form>
 
     <el-row :gutter="10" class="mb8">
-      <el-col :span="1.5">
-        <el-button
-          type="primary"
-          plain
-          icon="Plus"
-          @click="handleAdd"
-          v-hasPermi="['base:sysLog:add']"
-        >新增</el-button>
-      </el-col>
-      <el-col :span="1.5">
-        <el-button
-          type="success"
-          plain
-          icon="Edit"
-          :disabled="single"
-          @click="handleUpdate"
-          v-hasPermi="['base:sysLog:edit']"
-        >修改</el-button>
-      </el-col>
-      <el-col :span="1.5">
-        <el-button
-          type="danger"
-          plain
-          icon="Delete"
-          :disabled="multiple"
-          @click="handleDelete"
-          v-hasPermi="['base:sysLog:remove']"
-        >删除</el-button>
-      </el-col>
       <el-col :span="1.5">
         <el-button
           type="warning"
@@ -117,48 +88,47 @@
       <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
-    <el-table v-loading="loading" :data="sysLogList" @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="robotCode" />
-      <el-table-column label="日志时间,设备侧或服务侧日志发生时间" align="center" prop="logTime" width="180">
+    <el-table v-loading="loading" :data="sysLogList">
+      <el-table-column label="日志时间" align="center" prop="logTime" width="170">
         <template #default="scope">
-          <span>{{ parseTime(scope.row.logTime, '{y}-{m}-{d}') }}</span>
+          <span>{{ parseTime(scope.row.logTime) }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="日志接收时间" align="center" prop="receiveTime" width="180">
+      <el-table-column label="日志级别" align="center" prop="logLevel" width="100">
         <template #default="scope">
-          <span>{{ parseTime(scope.row.receiveTime, '{y}-{m}-{d}') }}</span>
+          <dict-tag :options="robot_log_level" :value="scope.row.logLevel"/>
         </template>
       </el-table-column>
-      <el-table-column label="日志类型" align="center" prop="logType">
+      <el-table-column label="日志类型" align="center" prop="logType" width="110">
         <template #default="scope">
           <dict-tag :options="robot_log_type" :value="scope.row.logType"/>
         </template>
       </el-table-column>
-      <el-table-column label="日志级别" align="center" prop="logLevel">
+      <el-table-column label="模块名称" align="center" prop="moduleName" width="120" show-overflow-tooltip />
+      <el-table-column label="日志摘要" align="left" prop="contentSummary" min-width="260" show-overflow-tooltip>
         <template #default="scope">
-          <dict-tag :options="robot_log_level" :value="scope.row.logLevel"/>
+          <span>{{ scope.row.contentSummary || formatSummary(scope.row.content) }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="模块名称,如 screen、audio、camera、network、ota" align="center" prop="moduleName" />
-      <el-table-column label="日志摘要" align="center" prop="contentSummary" />
-      <el-table-column label="日志内容" align="center" prop="content" />
-      <el-table-column label="结果状态" align="center" prop="resultStatus">
+      <el-table-column label="结果状态" align="center" prop="resultStatus" width="100">
         <template #default="scope">
           <dict-tag :options="common_result_status" :value="scope.row.resultStatus"/>
         </template>
       </el-table-column>
-      <el-table-column label="链路追踪ID" align="center" prop="traceId" />
-      <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="robotCode" width="130" show-overflow-tooltip />
+      <el-table-column label="追踪ID" align="center" prop="traceId" width="150" show-overflow-tooltip />
+      <el-table-column label="接收时间" align="center" prop="receiveTime" width="170">
         <template #default="scope">
-          <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['base:sysLog:edit']">修改</el-button>
-          <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['base:sysLog:remove']">删除</el-button>
+          <span>{{ parseTime(scope.row.receiveTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" fixed="right" width="100">
+        <template #default="scope">
+          <el-button link type="primary" icon="View" @click="handleDetail(scope.row)">详情</el-button>
         </template>
       </el-table-column>
     </el-table>
-    
+
     <pagination
       v-show="total>0"
       :total="total"
@@ -167,16 +137,41 @@
       @pagination="getList"
     />
 
-    <!-- 添加或修改运行日志对话框 -->
-    <el-dialog :title="title" v-model="open" width="500px" append-to-body>
-      <el-form ref="sysLogRef" :model="form" :rules="rules" label-width="100px">
-        <el-row>
-        </el-row>
-      </el-form>
+    <el-dialog title="运行日志详情" v-model="detailOpen" width="760px" append-to-body>
+      <el-descriptions :column="2" border>
+        <el-descriptions-item label="机器人编号">{{ detail.robotCode || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="日志时间">{{ parseTime(detail.logTime) || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="接收时间">{{ parseTime(detail.receiveTime) || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="日志级别">
+          <dict-tag :options="robot_log_level" :value="detail.logLevel"/>
+        </el-descriptions-item>
+        <el-descriptions-item label="日志类型">
+          <dict-tag :options="robot_log_type" :value="detail.logType"/>
+        </el-descriptions-item>
+        <el-descriptions-item label="模块名称">{{ detail.moduleName || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="结果状态">
+          <dict-tag :options="common_result_status" :value="detail.resultStatus"/>
+        </el-descriptions-item>
+        <el-descriptions-item label="追踪ID">
+          <span>{{ detail.traceId || '-' }}</span>
+          <el-button
+            v-if="detail.traceId"
+            link
+            type="primary"
+            size="small"
+            @click="copyTraceId"
+          >复制</el-button>
+        </el-descriptions-item>
+        <el-descriptions-item label="日志摘要" :span="2">{{ detail.contentSummary || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="备注" :span="2">{{ detail.remark || '-' }}</el-descriptions-item>
+      </el-descriptions>
+      <div class="log-content-block">
+        <div class="log-content-title">日志内容</div>
+        <pre class="log-content">{{ detail.content || '-' }}</pre>
+      </div>
       <template #footer>
         <div class="dialog-footer">
-          <el-button type="primary" @click="submitForm">确 定</el-button>
-          <el-button @click="cancel">取 消</el-button>
+          <el-button @click="detailOpen = false">关 闭</el-button>
         </div>
       </template>
     </el-dialog>
@@ -184,79 +179,44 @@
 </template>
 
 <script setup name="SysLog">
-import { listSysLog, getSysLog, delSysLog, addSysLog, updateSysLog } from "@/api/base/sysLog"
+import { listSysLog, getSysLog } from "@/api/base/sysLog"
 
 const { proxy } = getCurrentInstance()
-const { robot_log_type, common_result_status } = useDict('robot_log_type', 'common_result_status')
+const { robot_log_type, robot_log_level, common_result_status } = useDict('robot_log_type', 'robot_log_level', 'common_result_status')
 
 const sysLogList = ref([])
-const open = ref(false)
 const loading = ref(true)
 const showSearch = ref(true)
-const ids = ref([])
-const single = ref(true)
-const multiple = ref(true)
 const total = ref(0)
-const title = ref("")
 
-const data = reactive({
-  form: {},
-  queryParams: {
-    pageNum: 1,
-    pageSize: 10,
-    robotCode: undefined,
-    logTime: undefined,
-    receiveTime: undefined,
-    logType: undefined,
-    logLevel: undefined,
-    moduleName: undefined,
-    contentSummary: undefined,
-    content: undefined,
-    resultStatus: undefined,
-    traceId: undefined,
-  },
-  rules: {
-  }
+const daterangeLogTime = ref([])
+
+const queryParams = ref({
+  pageNum: 1,
+  pageSize: 10,
+  robotCode: undefined,
+  logLevel: undefined,
+  logType: undefined,
+  moduleName: undefined,
+  keyword: undefined,
+  resultStatus: undefined
 })
 
-const { queryParams, form, rules } = toRefs(data)
+const detailOpen = ref(false)
+const detail = ref({})
 
 /** 查询运行日志列表 */
 function getList() {
   loading.value = true
-  listSysLog(queryParams.value).then(response => {
-    sysLogList.value = response.rows
-    total.value = response.total
+  const params = proxy.addDateRange(queryParams.value, daterangeLogTime.value, 'LogTime')
+  listSysLog(params).then(response => {
+    sysLogList.value = response.rows || []
+    total.value = response.total || 0
+  }).finally(() => {
     loading.value = false
   })
 }
 
-/** 取消按钮 */
-function cancel() {
-  open.value = false
-  reset()
-}
-
-/** 表单重置 */
-function reset() {
-  form.value = {
-    id: null,
-    robotCode: null,
-    logTime: null,
-    receiveTime: null,
-    logType: null,
-    logLevel: null,
-    moduleName: null,
-    contentSummary: null,
-    content: null,
-    resultStatus: null,
-    traceId: null,
-    remark: null,
-    createTime: null
-  }
-  proxy.resetForm("sysLogRef")
-}
-
 /** 搜索按钮操作 */
 function handleQuery() {
   queryParams.value.pageNum = 1
@@ -265,73 +225,76 @@ function handleQuery() {
 
 /** 重置按钮操作 */
 function resetQuery() {
+  daterangeLogTime.value = []
   proxy.resetForm("queryRef")
   handleQuery()
 }
 
-/** 多选框选中数据 */
-function handleSelectionChange(selection) {
-  ids.value = selection.map(item => item.id)
-  single.value = selection.length != 1
-  multiple.value = !selection.length
-}
-
-/** 新增按钮操作 */
-function handleAdd() {
-  reset()
-  open.value = true
-  title.value = "添加运行日志"
-}
-
-/** 修改按钮操作 */
-function handleUpdate(row) {
-  reset()
-  const _id = row.id || ids.value
-  getSysLog(_id).then(response => {
-    form.value = response.data
-    open.value = true
-    title.value = "修改运行日志"
+/** 详情按钮操作 */
+function handleDetail(row) {
+  getSysLog(row.id).then(response => {
+    detail.value = response.data || row
+    detailOpen.value = true
   })
 }
 
-/** 提交按钮 */
-function submitForm() {
-  proxy.$refs["sysLogRef"].validate(valid => {
-    if (valid) {
-      if (form.value.id != null) {
-        updateSysLog(form.value).then(() => {
-          proxy.$modal.msgSuccess("修改成功")
-          open.value = false
-          getList()
-        })
-      } else {
-        addSysLog(form.value).then(() => {
-          proxy.$modal.msgSuccess("新增成功")
-          open.value = false
-          getList()
-        })
-      }
-    }
+/** 复制追踪ID */
+function copyTraceId() {
+  if (!detail.value.traceId) return
+  navigator.clipboard.writeText(detail.value.traceId).then(() => {
+    proxy.$modal.msgSuccess('追踪ID已复制')
+  }).catch(() => {
+    proxy.$modal.msgError('复制失败,请手动复制')
   })
 }
 
-/** 删除按钮操作 */
-function handleDelete(row) {
-  const _ids = row.id || ids.value
-  proxy.$modal.confirm('是否确认删除运行日志编号为"' + _ids + '"的数据项?').then(function() {
-    return delSysLog(_ids)
-  }).then(() => {
-    getList()
-    proxy.$modal.msgSuccess("删除成功")
-  }).catch(() => {})
+/** 摘要兜底方法 */
+function formatSummary(content) {
+  if (!content) return '-'
+  return content.length > 80 ? content.substring(0, 80) + '...' : content
 }
 
 /** 导出按钮操作 */
 function handleExport() {
-  proxy.download('base/sysLog/export', {
-    ...queryParams.value
-  }, `sysLog_${new Date().getTime()}.xlsx`)
+  proxy.$modal.confirm('确认导出当前查询条件下的运行日志数据吗?').then(() => {
+    const params = proxy.addDateRange(queryParams.value, daterangeLogTime.value, 'LogTime')
+    proxy.download('base/sysLog/export', {
+      ...params
+    }, `运行日志_${new Date().getTime()}.xlsx`)
+  }).catch(() => {})
 }
 
 getList()
 </script>
+
+<style scoped>
+.search-select,
+.search-input {
+  width: 220px;
+}
+.search-date {
+  width: 260px;
+}
+.log-content-block {
+  margin-top: 16px;
+}
+.log-content-title {
+  margin-bottom: 8px;
+  font-weight: 600;
+  color: #303133;
+}
+.log-content {
+  min-height: 120px;
+  max-height: 320px;
+  overflow: auto;
+  padding: 12px;
+  margin: 0;
+  line-height: 1.6;
+  color: #303133;
+  background: #f8fafc;
+  border: 1px solid #ebeef5;
+  border-radius: 6px;
+  white-space: pre-wrap;
+  word-break: break-all;
+}
+</style>

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

@@ -207,7 +207,13 @@
 </tbody></table>
     <div class="note">字典建议:loopMode 可使用 RuoYi 字典 play_plan_loop_mode,字典项:loop=循环播放,once=播放一次。transitionType 为预留字段,一期不开放配置。</div>
     <h4>6.3.5 播报内容管理页面</h4><table><thead><tr><th>模块</th><th>详细设计</th></tr></thead><tbody><tr><td>查询条件</td><td>内容名称、内容分类、启用状态。</td></tr><tr><td>列表字段</td><td>内容名称、内容分类、播报文本摘要、启用状态、更新时间、操作。</td></tr><tr><td>编辑字段</td><td>内容名称(contentName)、内容分类(contentType)、播报文本(broadcastText)、启用状态(status)、备注(remark)。</td></tr><tr><td>操作按钮</td><td>新增、编辑、删除、启用/停用、测试播报。</td></tr><tr><td>内容分类</td><td>通知、宣传、提示、安防提醒、自定义。</td></tr></tbody></table>
+<div class="note">播报内容由运维端维护,机身屏不提供新增、编辑、删除能力。播报内容被播报任务命中后,由后端调用第三方 TTS 生成或复用 MP3 文件,并通过当前播报状态接口返回给机身屏;机身屏根据 audioUrl 播放 MP3、展示播报浮层,并暂停/恢复当前素材播放。</div>
+
+    <div class="note">一期 TTS 采用非流式方案:播报文本首次使用时生成 MP3;后续相同文本和相同语音参数重复播报时复用已生成 MP3。若播报文本、音色、语速、音量、TTS 供应商或模型版本发生变化,应重新生成 MP3。</div>
     <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>
+<div class="note">播报任务的时间判断、频率控制、循环规则和重复触发保护由后端负责。任务命中后,后端应更新当前播报运行态,并通过 <code class="inline">/robot-ops/screen/broadcast/current</code> 提供给机身屏前端轮询消费;机身屏只负责根据 current 接口返回的 audioUrl 播放 MP3、暂停当前素材、显示播报浮层,并在 MP3 播放结束后恢复播放。</div>
+
+    <div class="note">播报任务执行时,后端优先检查播报内容是否已有可用 MP3 文件;若已存在且 Hash 未变化,则直接返回该 MP3 的 audioUrl;若不存在、文件丢失或 Hash 已变化,则先调用第三方 TTS 生成 MP3,再返回新的 audioUrl 给机身屏播放。</div>
     <h4>6.3.7 展示主题配置页面</h4>
 <table><thead><tr><th>模块</th><th>详细设计</th></tr></thead><tbody>
 <tr><td>页面目标</td><td>维护机器人屏幕端待机欢迎页的展示内容。该页面为单配置页,不提供列表、新增、普通删除、多主题切换和启用状态;仅提供“清空配置”能力,用于删除当前 config_key=default 的运维端覆盖配置。页面打开后直接加载 config_key=default 的配置。</td></tr>
@@ -279,11 +285,16 @@
 </tbody></table>
 <div class="note">运行日志定位为通用运维排查页面,主要用于查看机器人设备侧和本地服务侧上报的运行日志、服务日志、接口调用日志、异常报错日志和关键模块日志。</div>
 
-    <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>
+    <h4>6.5.3 对话日志页面</h4>
+<table><thead><tr><th>模块</th><th>详细设计</th></tr></thead><tbody>
+<tr><td>页面目标</td><td>查看机器人与用户之间的人机对话记录,用于排查用户问题、机器人回答、命中方式、来源场景、执行结果和原始请求响应。</td></tr>
+<tr><td>查询条件</td><td>会话 ID、用户问题关键字、命中方式、来源场景、结果状态、提问时间范围。</td></tr>
+<tr><td>列表字段</td><td>提问时间、会话 ID、用户问题、回答摘要、命中方式、来源场景、结果状态、操作。</td></tr>
+<tr><td>详情字段</td><td>机器人编号(robotCode)、会话 ID(sessionId)、提问时间(askTime)、用户问题(question)、机器人回答(answer)、回答摘要(answerSummary)、命中方式(hitType)、来源场景(sceneType)、结果状态(resultStatus)、错误信息(errorMsg)、原始请求(rawRequest)、原始响应(rawResponse)。</td></tr>
 <tr><td>操作按钮</td><td>查看详情、导出。</td></tr>
+<tr><td>业务规则</td><td>对话日志由机器人端或后端问答服务写入,运维端只读查看,不提供新增、编辑、删除。列表中只展示用户问题和回答摘要,完整回答、错误信息、原始请求和原始响应在详情弹窗中查看。</td></tr>
+<tr><td>边界说明</td><td>对话日志只记录人机问答过程,不替代运行日志和安防告警日志。接口异常、模型调用失败、知识库未命中等可在对话日志中记录结果状态和错误信息,同时也可在运行日志中记录系统级异常。</td></tr>
+<tr><td>扩展说明</td><td>一期不设计响应耗时、命中问答库 ID、命中标准问题和匹配置信度等字段;后续如问答能力、知识库命中逻辑或上位平台统计需求明确后再扩展。</td></tr>
 </tbody></table>
 
     <h4>6.5.4 安防告警日志页面</h4><table><thead><tr><th>模块</th><th>详细设计</th></tr></thead><tbody>
@@ -302,6 +313,11 @@
 
   <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>
+    <div class="note">播报插播对接补充:运维端播报内容和播报任务完成维护后,后端需提供 <code class="inline">GET /robot-ops/screen/broadcast/current</code> 给机身屏消费。该接口不用于运维端页面管理,而用于机身屏待机页判断是否正在播报、获取播报文字和 audioUrl,并暂停/恢复素材播放。</div>
+
+    <div class="note">TTS 实现补充:一期不采用流式 TTS。后端应将第三方 TTS 封装为统一 <code class="inline">TtsService</code>。播报文本转换为 MP3 后保存到小主机本地目录,例如 <code class="inline">/data/robot/audio/broadcast/</code>,并记录音频路径、访问地址、时长、Hash、生成状态和失败原因。当前播报状态接口应返回 <code class="inline">audioUrl</code>,供机身屏前端播放。</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>
@@ -537,6 +553,11 @@
     </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>
+    <div class="note">播报内容被播报任务命中后,由后端调用第三方 TTS 生成或复用 MP3 文件,并通过当前播报状态接口返回给机身屏;机身屏根据 audioUrl 播放 MP3、展示播报浮层,并暂停/恢复当前素材播放。</div>
+    <div class="note">一期 TTS 采用非流式方案:播报文本首次使用时生成 MP3;后续相同文本和相同语音参数重复播报时复用已生成 MP3。若播报文本、音色、语速、音量、TTS 供应商或模型版本发生变化,应重新生成 MP3。</div>
+    <div class="note">机身屏只负责根据 current 接口返回的 audioUrl 播放 MP3、暂停当前素材、显示播报浮层,并在 MP3 播放结束后恢复播放。</div>
+    <div class="note">若已存在且 Hash 未变化,则直接返回该 MP3 的 audioUrl;若不存在、文件丢失或 Hash 已变化,则先调用第三方 TTS 生成 MP3,再返回新的 audioUrl 给机身屏播放。</div>
+    <div class="note">TTS 实现补充:一期不采用流式 TTS。后端应将第三方 TTS 封装为统一 TtsService。播报文本转换为 MP3 后保存到小主机本地目录,例如 <code class="inline">/data/robot/audio/broadcast/</code>,并记录音频路径、访问地址、时长、Hash、生成状态和失败原因。当前播报状态接口应返回 audioUrl,供机身屏前端播放。</div>
 
     <h4>7.4.6 展示主题配置接口</h4>
 <table><thead><tr><th>接口</th><th>方法</th><th>说明</th><th>请求/返回字段</th><th>数据库表</th></tr></thead><tbody>
@@ -588,9 +609,12 @@
     </tbody></table>
     <div class="note">远程喊话为麦克风实时语音喊话,不是文本转语音播报。浏览器端后续需要对接麦克风采集能力,机器人端需要提供音频通道建立、音频推送、播放和结束能力。文本播报、定时播报和固定内容播报仍由播报内容与播报任务模块负责。</div>
     <table><thead><tr><th>接口</th><th>方法</th><th>说明</th><th>请求参数</th><th>返回/处理字段</th><th>数据库表</th></tr></thead><tbody>
-      <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/dialogue/page</td><td>GET</td><td>对话日志分页</td><td>sessionId、keyword、hitType、sceneType、resultStatus、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>id、robotCode、sessionId、askTime、question、answer、answerSummary、hitType、sceneType、resultStatus、errorMsg、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文件;建议导出提问时间、机器人编号、会话ID、用户问题、回答摘要、命中方式、来源场景、结果状态、错误信息</td><td>robot_ops_dialogue_log</td></tr>
+    </tbody></table>
+    <div class="note">对话日志为只读日志,不提供新增、编辑、删除接口。robotCode 用于后续上传上位平台或多机器人汇聚识别;本地运维端当前不作为主要查询条件和列表字段展示。</div>
+    <table><thead><tr><th>接口</th><th>方法</th><th>说明</th><th>请求参数</th><th>返回/处理字段</th><th>数据库表</th></tr></thead><tbody>
       <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>
@@ -832,6 +856,13 @@ VALUES
   `content_type` VARCHAR(50) DEFAULT NULL COMMENT '内容分类:通知、宣传、提示、安防提醒、自定义',
   `broadcast_text` VARCHAR(2000) NOT NULL COMMENT '播报文本',
   `status` CHAR(1) NOT NULL DEFAULT '1' COMMENT '启用状态:0停用,1启用',
+  `audio_file_url` VARCHAR(500) DEFAULT NULL COMMENT 'MP3文件访问地址,供机身屏前端播放,例如 http://192.168.0.30/profile/audio/broadcast/xxx.mp3',
+  `audio_local_path` VARCHAR(500) DEFAULT NULL COMMENT 'MP3文件在小主机本地的保存路径',
+  `audio_duration` DECIMAL(10,2) DEFAULT NULL COMMENT 'MP3音频时长,单位秒',
+  `audio_hash` VARCHAR(64) DEFAULT NULL COMMENT 'MP3生成Hash,用于判断是否需要重新生成',
+  `tts_status` CHAR(1) DEFAULT '0' COMMENT 'TTS生成状态:0待生成,1生成成功,2生成失败',
+  `tts_error_msg` VARCHAR(500) DEFAULT NULL COMMENT 'TTS生成失败错误信息',
+  `tts_update_time` DATETIME DEFAULT NULL COMMENT 'TTS生成或更新时间',
   `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
   `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人',
   `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
@@ -841,6 +872,7 @@ VALUES
   KEY `idx_robot_ops_broadcast_content_type` (`content_type`),
   KEY `idx_robot_ops_broadcast_content_status` (`status`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='播报内容表';</div>
+    <div class="note">说明:audio_file_url 字段用于返回 MP3 访问地址,供机身屏前端播放;audio_local_path 字段记录本地保存路径;audio_duration 记录音频时长;audio_hash 用于判断播报文本和语音参数变化后是否需要重新生成 MP3;tts_status、tts_error_msg、tts_update_time 用于记录 TTS 生成状态和失败原因。建议 Hash 计算口径为:md5(播报文本 + 音色 + 语速 + 音量 + TTS供应商 + 模型版本)。</div>
 
     <h4>8.2.8 播报任务表 robot_ops_broadcast_task</h4>
     <div class="code">CREATE TABLE `robot_ops_broadcast_task` (
@@ -995,22 +1027,31 @@ VALUES
 ) 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` (
+<div class="code">CREATE TABLE `robot_ops_dialogue_log` (
   `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `robot_code` VARCHAR(64) DEFAULT NULL COMMENT '机器人编号,用于后续上传上位平台或多机器人汇聚识别',
   `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 '原始响应内容',
+  `answer_summary` VARCHAR(500) DEFAULT NULL COMMENT '回答摘要',
+  `hit_type` VARCHAR(50) DEFAULT NULL COMMENT '命中方式:FAQ/AI/NONE/ERROR',
+  `scene_type` VARCHAR(50) DEFAULT NULL COMMENT '来源场景:SCREEN/VOICE/H5/OTHER',
+  `result_status` VARCHAR(20) DEFAULT NULL COMMENT '结果状态:SUCCESS/FAIL/UNKNOWN',
+  `error_msg` VARCHAR(1000) DEFAULT NULL COMMENT '失败原因或错误信息',
+  `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>
+  KEY `idx_dialogue_robot` (`robot_code`),
+  KEY `idx_dialogue_session` (`session_id`),
+  KEY `idx_dialogue_ask_time` (`ask_time`),
+  KEY `idx_dialogue_hit_type` (`hit_type`),
+  KEY `idx_dialogue_scene_type` (`scene_type`),
+  KEY `idx_dialogue_result` (`result_status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='对话日志表';</div>
+<div class="note">说明:对话日志表用于保存机器人端或后端问答服务产生的人机对话记录。robot_code 用于后续日志上传上位平台或多机器人汇聚识别;本地端当前仍按单机器人后台使用。</div>
+<div class="note">字典建议:hitType 使用 dialogue_hit_type,字典项 FAQ=命中问答库、AI=AI生成、NONE=未命中、ERROR=异常;sceneType 使用 dialogue_scene_type,字典项 SCREEN=机器人屏幕、VOICE=语音交互、H5=扫码H5、OTHER=其他;resultStatus 复用 common_result_status。</div>
 
     <h4>8.4.3 安防告警日志表 robot_ops_alarm_log</h4>
     <div class="code">CREATE TABLE `robot_ops_alarm_log` (