|
|
@@ -0,0 +1,468 @@
|
|
|
+package com.ruoyi.web.controller.base;
|
|
|
+
|
|
|
+import com.ruoyi.base.domain.RobotOpsBroadcastContent;
|
|
|
+import com.ruoyi.base.domain.RobotOpsPlayPlan;
|
|
|
+import com.ruoyi.base.domain.RobotOpsPlayPlanItem;
|
|
|
+import com.ruoyi.base.domain.RobotOpsScreenThemeConfig;
|
|
|
+import com.ruoyi.base.domain.RobotOpsBroadcastTask;
|
|
|
+import com.ruoyi.base.service.IBroadcastRecordService;
|
|
|
+import com.ruoyi.base.service.IRobotOpsBroadcastContentService;
|
|
|
+import com.ruoyi.base.service.IRobotOpsBroadcastTaskService;
|
|
|
+import com.ruoyi.base.service.IRobotOpsPlayPlanService;
|
|
|
+import com.ruoyi.base.service.IRobotOpsScreenThemeConfigService;
|
|
|
+import com.ruoyi.common.annotation.Anonymous;
|
|
|
+import com.ruoyi.common.config.RuoYiConfig;
|
|
|
+import com.ruoyi.common.core.controller.BaseController;
|
|
|
+import com.ruoyi.common.core.domain.AjaxResult;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.web.bind.annotation.*;
|
|
|
+
|
|
|
+import java.text.SimpleDateFormat;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 机器人机身屏API接口
|
|
|
+ *
|
|
|
+ * 提供给 robot_screen 前端调用的接口
|
|
|
+ * 支持匿名访问,无需登录认证
|
|
|
+ *
|
|
|
+ * @author ruoyi
|
|
|
+ * @date 2026-05-14
|
|
|
+ */
|
|
|
+@Anonymous
|
|
|
+@RestController
|
|
|
+@RequestMapping("/robot-ops/screen")
|
|
|
+public class RobotOpsScreenApiController extends BaseController
|
|
|
+{
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(RobotOpsScreenApiController.class);
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private IRobotOpsPlayPlanService robotOpsPlayPlanService;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private IRobotOpsScreenThemeConfigService robotOpsScreenThemeConfigService;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private IRobotOpsBroadcastTaskService robotOpsBroadcastTaskService;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private IRobotOpsBroadcastContentService robotOpsBroadcastContentService;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private IBroadcastRecordService broadcastRecordService;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 资源访问基础URL,用于补全音频路径
|
|
|
+ */
|
|
|
+ @Value("${screen.resource-base-url:}")
|
|
|
+ private String resourceBaseUrl;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取当前播放方案
|
|
|
+ * 前端调用路径:/robot-ops/screen/play-plan/current
|
|
|
+ */
|
|
|
+ @GetMapping("/play-plan/current")
|
|
|
+ public AjaxResult getCurrentPlayPlan() {
|
|
|
+ try {
|
|
|
+ RobotOpsPlayPlan plan = robotOpsPlayPlanService.selectCurrentPlayingPlanWithItems();
|
|
|
+ if (plan == null) {
|
|
|
+ Map<String, Object> emptyResult = new HashMap<>();
|
|
|
+ emptyResult.put("enabled", false);
|
|
|
+ emptyResult.put("planId", null);
|
|
|
+ emptyResult.put("planName", null);
|
|
|
+ emptyResult.put("playMode", "loop");
|
|
|
+ emptyResult.put("defaultFitMode", "cover");
|
|
|
+ emptyResult.put("version", "");
|
|
|
+ emptyResult.put("items", new ArrayList<>());
|
|
|
+ return AjaxResult.success("当前没有正在播放的方案", emptyResult);
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Object> result = new HashMap<>();
|
|
|
+ result.put("enabled", true);
|
|
|
+ result.put("planId", plan.getId());
|
|
|
+ result.put("planName", plan.getPlanName());
|
|
|
+ result.put("playMode", plan.getLoopMode() != null ? plan.getLoopMode() : "loop");
|
|
|
+ result.put("defaultFitMode", "cover");
|
|
|
+
|
|
|
+ // 使用更新时间生成版本号,用于前端判断播放方案是否变化
|
|
|
+ // 格式:yyyyMMddHHmmss,如 20260515163000
|
|
|
+ String version = "";
|
|
|
+ if (plan.getUpdateTime() != null) {
|
|
|
+ java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyyMMddHHmmss");
|
|
|
+ version = sdf.format(plan.getUpdateTime());
|
|
|
+ }
|
|
|
+ result.put("version", version);
|
|
|
+
|
|
|
+ // 转换素材项
|
|
|
+ List<RobotOpsPlayPlanItem> items = plan.getRobotOpsPlayPlanItemList();
|
|
|
+ List<Map<String, Object>> convertedItems = new ArrayList<>();
|
|
|
+ if (items != null && !items.isEmpty()) {
|
|
|
+ items.stream()
|
|
|
+ .sorted((a, b) -> {
|
|
|
+ Long orderA = a.getPlayOrder() != null ? a.getPlayOrder() : 999L;
|
|
|
+ Long orderB = b.getPlayOrder() != null ? b.getPlayOrder() : 999L;
|
|
|
+ return orderA.compareTo(orderB);
|
|
|
+ })
|
|
|
+ .forEach(item -> {
|
|
|
+ Map<String, Object> itemMap = new HashMap<>();
|
|
|
+ itemMap.put("id", item.getId() != null ? String.valueOf(item.getId()) : "");
|
|
|
+ itemMap.put("type", convertAssetType(item.getAssetType()));
|
|
|
+ itemMap.put("title", item.getAssetName() != null ? item.getAssetName() : "");
|
|
|
+ itemMap.put("subtitle", "");
|
|
|
+ itemMap.put("url", item.getFileUrl() != null ? item.getFileUrl() : "");
|
|
|
+ itemMap.put("duration", item.getStaySeconds() != null ? item.getStaySeconds() * 1000 : 8000);
|
|
|
+ itemMap.put("fitMode", "cover");
|
|
|
+ itemMap.put("showTitle", false);
|
|
|
+ convertedItems.add(itemMap);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ result.put("items", convertedItems);
|
|
|
+
|
|
|
+ return AjaxResult.success(result);
|
|
|
+ } catch (Exception e) {
|
|
|
+ return AjaxResult.error("获取播放方案失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取屏幕配置
|
|
|
+ * 前端调用路径:/robot-ops/screen/config
|
|
|
+ */
|
|
|
+ @GetMapping("/config")
|
|
|
+ public AjaxResult getScreenConfig() {
|
|
|
+ Map<String, Object> config = new HashMap<>();
|
|
|
+ config.put("robotName", "智能迎宾机器人");
|
|
|
+ config.put("logoUrl", "");
|
|
|
+ config.put("idleTimeout", 60);
|
|
|
+ config.put("theme", "default");
|
|
|
+ config.put("version", "1.0.0");
|
|
|
+ return AjaxResult.success(config);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取机器人状态
|
|
|
+ * 前端调用路径:/robot-ops/robot/status
|
|
|
+ */
|
|
|
+ @GetMapping("/robot/status")
|
|
|
+ public AjaxResult getRobotStatus() {
|
|
|
+ Map<String, Object> status = new HashMap<>();
|
|
|
+ status.put("batteryLevel", 85);
|
|
|
+ status.put("networkStatus", "online");
|
|
|
+ status.put("workStatus", "idle");
|
|
|
+ status.put("chargeStatus", "not_charging");
|
|
|
+ status.put("faultFlag", false);
|
|
|
+ return AjaxResult.success(status);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取屏幕主题配置
|
|
|
+ * 前端调用路径:/robot-ops/screen/theme
|
|
|
+ */
|
|
|
+ @GetMapping("/theme")
|
|
|
+ public AjaxResult getScreenTheme() {
|
|
|
+ try {
|
|
|
+ // 从数据库查询默认主题配置(configKey 为 default)
|
|
|
+ RobotOpsScreenThemeConfig themeConfig = robotOpsScreenThemeConfigService.selectRobotOpsScreenThemeConfigByConfigKey("default");
|
|
|
+
|
|
|
+ Map<String, Object> theme = new HashMap<>();
|
|
|
+
|
|
|
+ if (themeConfig != null) {
|
|
|
+ // 使用数据库配置
|
|
|
+ theme.put("configKey", themeConfig.getConfigKey());
|
|
|
+ theme.put("logoUrl", themeConfig.getLogoUrl() != null ? themeConfig.getLogoUrl() : "");
|
|
|
+ theme.put("robotName", themeConfig.getRobotName() != null ? themeConfig.getRobotName() : "迎宾巡逻机器人");
|
|
|
+ theme.put("brandSubtitle", themeConfig.getBrandSubtitle() != null ? themeConfig.getBrandSubtitle() : "智能接待 · 路线引导 · 信息服务");
|
|
|
+ theme.put("backgroundImage", themeConfig.getBackgroundImage() != null ? themeConfig.getBackgroundImage() : "");
|
|
|
+ theme.put("welcomeTitle", themeConfig.getWelcomeTitle() != null ? themeConfig.getWelcomeTitle() : "您好,欢迎光临");
|
|
|
+ theme.put("welcomeSubtitle", themeConfig.getWelcomeSubtitle() != null ? themeConfig.getWelcomeSubtitle() : "我可以为您提供访客登记、路线引导、通知公告查询与现场帮助服务");
|
|
|
+ theme.put("touchText", themeConfig.getTouchText() != null ? themeConfig.getTouchText() : "触摸屏幕进入服务");
|
|
|
+ theme.put("buttonColor", themeConfig.getButtonColor() != null ? themeConfig.getButtonColor() : "#2f8ee5");
|
|
|
+ } else {
|
|
|
+ // 没有数据库配置,使用默认值兜底
|
|
|
+ theme.put("configKey", "default");
|
|
|
+ theme.put("logoUrl", "");
|
|
|
+ theme.put("robotName", "迎宾巡逻机器人");
|
|
|
+ theme.put("brandSubtitle", "智能接待 · 路线引导 · 信息服务");
|
|
|
+ theme.put("backgroundImage", "");
|
|
|
+ theme.put("welcomeTitle", "您好,欢迎光临");
|
|
|
+ theme.put("welcomeSubtitle", "我可以为您提供访客登记、路线引导、通知公告查询与现场帮助服务");
|
|
|
+ theme.put("touchText", "触摸屏幕进入服务");
|
|
|
+ theme.put("buttonColor", "#2f8ee5");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 固定的配置项
|
|
|
+ theme.put("primaryColor", "#2f8ee5");
|
|
|
+ theme.put("secondaryColor", "#20b7c7");
|
|
|
+ theme.put("backgroundType", "image");
|
|
|
+ theme.put("backgroundFit", "stretch");
|
|
|
+ theme.put("backgroundOverlay", true);
|
|
|
+ theme.put("showDecorations", false);
|
|
|
+ theme.put("showServicePanel", false);
|
|
|
+ theme.put("showStatusBar", true);
|
|
|
+ theme.put("showTime", true);
|
|
|
+ theme.put("showDebugSwitch", true);
|
|
|
+
|
|
|
+ return AjaxResult.success(theme);
|
|
|
+ } catch (Exception e) {
|
|
|
+ return AjaxResult.error("获取屏幕主题配置失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取欢迎页兜底内容
|
|
|
+ * 前端调用路径:/robot-ops/screen/welcome-content
|
|
|
+ */
|
|
|
+ @GetMapping("/welcome-content")
|
|
|
+ public AjaxResult getWelcomeContent() {
|
|
|
+ Map<String, Object> content = new HashMap<>();
|
|
|
+ content.put("title", "欢迎光临");
|
|
|
+ content.put("subtitle", "您好,请问有什么可以帮您?");
|
|
|
+ content.put("hint", "触摸屏幕开启服务");
|
|
|
+ content.put("logo", "");
|
|
|
+ return AjaxResult.success(content);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取当前播报状态
|
|
|
+ * 前端调用路径:/robot-ops/screen/broadcast/current
|
|
|
+ *
|
|
|
+ * 根据播报任务的循环类型、开始时间、结束时间判断当前是否需要播报
|
|
|
+ * 如果需要播报,返回播报内容和音频地址
|
|
|
+ */
|
|
|
+ @GetMapping("/broadcast/current")
|
|
|
+ public AjaxResult getCurrentBroadcast()
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ // 1. 获取当前命中的播报任务
|
|
|
+ RobotOpsBroadcastTask task = robotOpsBroadcastTaskService.selectCurrentBroadcastTask();
|
|
|
+
|
|
|
+ // 构建返回结果
|
|
|
+ Map<String, Object> result = new HashMap<>();
|
|
|
+ result.put("broadcasting", false);
|
|
|
+ result.put("taskId", null);
|
|
|
+ result.put("contentId", null);
|
|
|
+ result.put("taskName", "");
|
|
|
+ result.put("title", "");
|
|
|
+ result.put("content", "");
|
|
|
+ result.put("contentType", "");
|
|
|
+ result.put("level", "normal");
|
|
|
+ result.put("audioUrl", "");
|
|
|
+ result.put("audioDuration", null);
|
|
|
+ result.put("playMode", "once");
|
|
|
+ result.put("startTime", null);
|
|
|
+ result.put("estimatedEndTime", null);
|
|
|
+ result.put("version", generateVersion());
|
|
|
+
|
|
|
+ // 如果没有命中的任务,返回无播报状态
|
|
|
+ if (task == null)
|
|
|
+ {
|
|
|
+ return AjaxResult.success("当前无播报", result);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 获取关联的播报内容
|
|
|
+ RobotOpsBroadcastContent content = robotOpsBroadcastContentService.selectRobotOpsBroadcastContentById(task.getContentId());
|
|
|
+ if (content == null)
|
|
|
+ {
|
|
|
+ log.warn("[Broadcast] 播报任务关联的内容不存在,taskId: {}", task.getId());
|
|
|
+ return AjaxResult.success("当前无播报", result);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 检查内容是否启用
|
|
|
+ if (!"1".equals(content.getStatus()))
|
|
|
+ {
|
|
|
+ log.info("[Broadcast] 播报内容未启用,contentId: {}", content.getId());
|
|
|
+ return AjaxResult.success("当前无播报", result);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 检查音频是否合成成功
|
|
|
+ if (!"1".equals(content.getAudioStatus()) || content.getAudioPath() == null || content.getAudioPath().isEmpty())
|
|
|
+ {
|
|
|
+ log.info("[Broadcast] 播报内容音频未就绪,contentId: {}, audioStatus: {}",
|
|
|
+ content.getId(), content.getAudioStatus());
|
|
|
+ return AjaxResult.success("当前无播报", result);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 构建播报结果
|
|
|
+ result.put("broadcasting", true);
|
|
|
+ result.put("taskId", task.getId());
|
|
|
+ result.put("contentId", content.getId());
|
|
|
+ result.put("taskName", task.getTaskName() != null ? task.getTaskName() : "");
|
|
|
+ result.put("title", content.getContentName() != null ? content.getContentName() : "");
|
|
|
+ result.put("content", content.getBroadcastText() != null ? content.getBroadcastText() : "");
|
|
|
+ result.put("contentType", convertContentType(content.getContentType()));
|
|
|
+ result.put("level", "normal");
|
|
|
+
|
|
|
+ // 补全音频 URL
|
|
|
+ String audioUrl = completeAudioUrl(content.getAudioPath());
|
|
|
+ result.put("audioUrl", audioUrl);
|
|
|
+ result.put("audioDuration", content.getAudioDuration() != null ? content.getAudioDuration() : null);
|
|
|
+ result.put("startTime", formatDateTime(LocalDateTime.now()));
|
|
|
+
|
|
|
+ // 计算预计结束时间
|
|
|
+ if (content.getAudioDuration() != null)
|
|
|
+ {
|
|
|
+ LocalDateTime estimatedEnd = LocalDateTime.now().plusSeconds(content.getAudioDuration());
|
|
|
+ result.put("estimatedEndTime", formatDateTime(estimatedEnd));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 6. 记录播报时间(用于下次播报频率控制)
|
|
|
+ broadcastRecordService.recordBroadcast(task.getId());
|
|
|
+
|
|
|
+ log.info("[Broadcast] 返回播报状态,taskId: {}, contentId: {}", task.getId(), content.getId());
|
|
|
+ return AjaxResult.success("查询成功", result);
|
|
|
+ }
|
|
|
+ catch (Exception e)
|
|
|
+ {
|
|
|
+ log.error("[Broadcast] 获取当前播报状态异常: {}", e.getMessage(), e);
|
|
|
+ Map<String, Object> result = new HashMap<>();
|
|
|
+ result.put("broadcasting", false);
|
|
|
+ result.put("version", generateVersion());
|
|
|
+ return AjaxResult.success("当前无播报", result);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 播报播放完成回执
|
|
|
+ * 前端调用路径:/robot-ops/screen/broadcast/ack
|
|
|
+ */
|
|
|
+ @PostMapping("/broadcast/ack")
|
|
|
+ public AjaxResult broadcastAck(@RequestBody Map<String, Object> params)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ String taskId = params.get("taskId") != null ? params.get("taskId").toString() : null;
|
|
|
+ String contentId = params.get("contentId") != null ? params.get("contentId").toString() : null;
|
|
|
+ String resultStatus = params.get("resultStatus") != null ? params.get("resultStatus").toString() : "success";
|
|
|
+ String playTime = params.get("playTime") != null ? params.get("playTime").toString() : null;
|
|
|
+
|
|
|
+ log.info("[Broadcast] 收到播报回执,taskId: {}, contentId: {}, status: {}, playTime: {}",
|
|
|
+ taskId, contentId, resultStatus, playTime);
|
|
|
+
|
|
|
+ // 一期暂不处理回执逻辑,后续可扩展
|
|
|
+ return AjaxResult.success("回执已接收");
|
|
|
+ }
|
|
|
+ catch (Exception e)
|
|
|
+ {
|
|
|
+ log.error("[Broadcast] 处理播报回执异常: {}", e.getMessage(), e);
|
|
|
+ return AjaxResult.error("处理回执失败");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 补全音频 URL
|
|
|
+ * 如果 audioPath 已经是完整 URL 则直接返回
|
|
|
+ * 否则拼接资源基础 URL
|
|
|
+ */
|
|
|
+ private String completeAudioUrl(String audioPath)
|
|
|
+ {
|
|
|
+ if (audioPath == null || audioPath.isEmpty())
|
|
|
+ {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果已经是完整 URL(包含 http 或 /profile)
|
|
|
+ if (audioPath.startsWith("http://") || audioPath.startsWith("https://"))
|
|
|
+ {
|
|
|
+ return audioPath;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (audioPath.startsWith("/profile/"))
|
|
|
+ {
|
|
|
+ return audioPath;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果配置了资源基础 URL
|
|
|
+ if (resourceBaseUrl != null && !resourceBaseUrl.isEmpty())
|
|
|
+ {
|
|
|
+ // 避免重复拼接
|
|
|
+ if (audioPath.startsWith("/"))
|
|
|
+ {
|
|
|
+ return resourceBaseUrl + audioPath;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ return resourceBaseUrl + "/" + audioPath;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 默认拼接 /profile/
|
|
|
+ if (audioPath.startsWith("/"))
|
|
|
+ {
|
|
|
+ return "/profile" + audioPath;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ return "/profile/" + audioPath;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 转换内容类型
|
|
|
+ */
|
|
|
+ private String convertContentType(Long contentType)
|
|
|
+ {
|
|
|
+ if (contentType == null)
|
|
|
+ {
|
|
|
+ return "custom";
|
|
|
+ }
|
|
|
+ switch (contentType.intValue())
|
|
|
+ {
|
|
|
+ case 1:
|
|
|
+ return "notice";
|
|
|
+ case 2:
|
|
|
+ return "promotion";
|
|
|
+ case 3:
|
|
|
+ return "tip";
|
|
|
+ case 4:
|
|
|
+ return "security";
|
|
|
+ default:
|
|
|
+ return "custom";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成版本号
|
|
|
+ */
|
|
|
+ private String generateVersion()
|
|
|
+ {
|
|
|
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
|
|
|
+ return sdf.format(new Date());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 格式化日期时间
|
|
|
+ */
|
|
|
+ private String formatDateTime(LocalDateTime dateTime)
|
|
|
+ {
|
|
|
+ if (dateTime == null)
|
|
|
+ {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
|
|
+ return dateTime.format(formatter);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 转换素材类型
|
|
|
+ * 后端:image/video
|
|
|
+ * 前端期望:image/video
|
|
|
+ */
|
|
|
+ private String convertAssetType(String assetType)
|
|
|
+ {
|
|
|
+ if (assetType == null)
|
|
|
+ {
|
|
|
+ return "image";
|
|
|
+ }
|
|
|
+ return assetType;
|
|
|
+ }
|
|
|
+}
|