diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml
index 1fd51a25..33f583cd 100644
--- a/ruoyi-admin/src/main/resources/application-dev.yml
+++ b/ruoyi-admin/src/main/resources/application-dev.yml
@@ -16,9 +16,9 @@ spring:
master:
type: ${spring.datasource.type}
driverClassName: com.mysql.cj.jdbc.Driver
- url: jdbc:mysql://127.0.0.1:3306/ruoyi-ai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true
- username: root
- password: root
+ url: jdbc:mysql://127.0.0.1:3306/aihumanv2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true
+ username: aihumanv2
+ password: 123456
hikari:
# 最大连接池数量
diff --git a/ruoyi-modules/ruoyi-aihuman/pom.xml b/ruoyi-modules/ruoyi-aihuman/pom.xml
index d594c634..b86f7de5 100644
--- a/ruoyi-modules/ruoyi-aihuman/pom.xml
+++ b/ruoyi-modules/ruoyi-aihuman/pom.xml
@@ -18,6 +18,7 @@
3.2.1
+ 5.13.0
@@ -70,5 +71,18 @@
org.ruoyi
ruoyi-common-excel
+
+
+ net.java.dev.jna
+ jna
+ ${jna.version}
+
+
+
+ net.java.dev.jna
+ jna-platform
+ ${jna.version}
+
+
\ No newline at end of file
diff --git a/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/controller/AihumanRealConfigController.java b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/controller/AihumanRealConfigController.java
new file mode 100644
index 00000000..e1732db0
--- /dev/null
+++ b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/controller/AihumanRealConfigController.java
@@ -0,0 +1,157 @@
+package org.ruoyi.aihuman.controller;
+
+import java.util.List;
+
+import cn.dev33.satoken.annotation.SaIgnore;
+import lombok.RequiredArgsConstructor;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.constraints.*;
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import org.ruoyi.common.log.enums.OperatorType;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.validation.annotation.Validated;
+import org.ruoyi.common.idempotent.annotation.RepeatSubmit;
+import org.ruoyi.common.log.annotation.Log;
+import org.ruoyi.common.web.core.BaseController;
+import org.ruoyi.core.page.PageQuery;
+import org.ruoyi.common.core.domain.R;
+import org.ruoyi.common.core.validate.AddGroup;
+import org.ruoyi.common.core.validate.EditGroup;
+import org.ruoyi.common.log.enums.BusinessType;
+import org.ruoyi.common.excel.utils.ExcelUtil;
+import org.ruoyi.aihuman.domain.vo.AihumanRealConfigVo;
+import org.ruoyi.aihuman.domain.bo.AihumanRealConfigBo;
+import org.ruoyi.aihuman.service.AihumanRealConfigService;
+import org.ruoyi.core.page.TableDataInfo;
+
+/**
+ * 真人交互数字人配置
+ *
+ * @author ageerle
+ * @date Tue Oct 21 11:46:52 GMT+08:00 2025
+ */
+//临时免登录
+@SaIgnore
+
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/aihuman/aihumanRealConfig")
+public class AihumanRealConfigController extends BaseController {
+
+ private final AihumanRealConfigService aihumanRealConfigService;
+
+/**
+ * 查询真人交互数字人配置列表
+ */
+@SaCheckPermission("aihuman:aihumanRealConfig:list")
+@GetMapping("/list")
+ public TableDataInfo list(AihumanRealConfigBo bo, PageQuery pageQuery) {
+ return aihumanRealConfigService.queryPageList(bo, pageQuery);
+ }
+
+ /**
+ * 导出真人交互数字人配置列表
+ */
+ @SaCheckPermission("aihuman:aihumanRealConfig:export")
+ @Log(title = "真人交互数字人配置", businessType = BusinessType.EXPORT)
+ @PostMapping("/export")
+ public void export(AihumanRealConfigBo bo, HttpServletResponse response) {
+ List list = aihumanRealConfigService.queryList(bo);
+ ExcelUtil.exportExcel(list, "真人交互数字人配置", AihumanRealConfigVo.class, response);
+ }
+
+ /**
+ * 获取真人交互数字人配置详细信息
+ *
+ * @param id 主键
+ */
+ @SaCheckPermission("aihuman:aihumanRealConfig:query")
+ @GetMapping("/{id}")
+ public R getInfo(@NotNull(message = "主键不能为空")
+ @PathVariable Integer id) {
+ return R.ok(aihumanRealConfigService.queryById(id));
+ }
+
+ /**
+ * 新增真人交互数字人配置
+ */
+ @SaCheckPermission("aihuman:aihumanRealConfig:add")
+ @Log(title = "真人交互数字人配置", businessType = BusinessType.INSERT)
+ @RepeatSubmit()
+ @PostMapping()
+ public R add(@Validated(AddGroup.class) @RequestBody AihumanRealConfigBo bo) {
+ return toAjax(aihumanRealConfigService.insertByBo(bo));
+ }
+
+ /**
+ * 修改真人交互数字人配置
+ */
+ @SaCheckPermission("aihuman:aihumanRealConfig:edit")
+ @Log(title = "真人交互数字人配置", businessType = BusinessType.UPDATE)
+ @RepeatSubmit()
+ @PutMapping()
+ public R edit(@Validated(EditGroup.class) @RequestBody AihumanRealConfigBo bo) {
+ return toAjax(aihumanRealConfigService.updateByBo(bo));
+ }
+
+ /**
+ * 删除真人交互数字人配置
+ *
+ * @param ids 主键串
+ */
+ @SaCheckPermission("aihuman:aihumanRealConfig:remove")
+ @Log(title = "真人交互数字人配置", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{ids}")
+ public R remove(@NotEmpty(message = "主键不能为空")
+ @PathVariable Integer[] ids) {
+ return toAjax(aihumanRealConfigService.deleteWithValidByIds(List.of(ids), true));
+ }
+
+ /**
+ * 1.执行以下命令:
+ * cd F:\Projects\AI-Human\LiveTalking
+ * conda activate D:\zg117\C\Users\zg117\.conda\envs\livetalking_new
+ * python app.py --transport webrtc --model wav2lip --avatar_id wav2lip256_avatar1
+ *
+ * 2.监听 python app.py --transport webrtc --model wav2lip --avatar_id wav2lip256_avatar1 执行情况
+ *
+ * 3.返回执行结果并打开页面
+ * http://127.0.0.1:8010/webrtcapi-diy.html
+ */
+ @SaCheckPermission("aihuman:aihumanRealConfig:run")
+ //@Log(title = "真人交互数字人配置", businessType = BusinessType.UPDATE, operatorType = OperatorType.OTHER)
+ @RepeatSubmit()
+ @PutMapping("/run")
+ public R run(@Validated(EditGroup.class) @RequestBody AihumanRealConfigBo bo) {
+ boolean result = aihumanRealConfigService.runByBo(bo);
+ if (result) {
+ // 返回前端页面URL,前端可以根据这个URL跳转或打开新页面
+ // http://127.0.0.1:8010/webrtcapi-diy.html 其中的 http://127.0.0.1 获取当前java服务的IP地址
+ // return R.ok("http://127.0.0.1:8010/webrtcapi-diy.html");
+ // 运行状态
+ bo.setRunStatus("1");
+ return R.ok("http://127.0.0.1:8010/webrtcapi-diy.html");
+ } else {
+ return R.fail("启动真人交互数字人失败");
+ }
+ }
+
+ /**
+ * 停止真人交互数字人配置任务
+ */
+ @SaCheckPermission("aihuman:aihumanRealConfig:stop")
+ //@Log(title = "真人交互数字人配置", businessType = BusinessType.UPDATE, operatorType = OperatorType.OTHER)
+ @RepeatSubmit()
+ @PutMapping("/stop")
+ public R stop(@Validated(EditGroup.class) @RequestBody AihumanRealConfigBo bo) {
+ boolean result = aihumanRealConfigService.stopByBo(bo);
+ if (result) {
+ // 运行状态
+ bo.setRunStatus("0");
+ return R.ok("真人交互数字人任务已停止");
+ } else {
+ return R.fail("停止真人交互数字人任务失败或没有正在运行的任务");
+ }
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/domain/AihumanRealConfig.java b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/domain/AihumanRealConfig.java
new file mode 100644
index 00000000..2c9c207d
--- /dev/null
+++ b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/domain/AihumanRealConfig.java
@@ -0,0 +1,101 @@
+package org.ruoyi.aihuman.domain;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+import java.time.LocalDateTime;
+import java.io.Serializable;
+
+/**
+ * 真人交互数字人配置对象 aihuman_real_config
+ *
+ * @author ageerle
+ * @date Tue Oct 21 11:46:52 GMT+08:00 2025
+ */
+@Data
+@TableName("aihuman_real_config")
+public class AihumanRealConfig implements Serializable {
+
+
+ /**
+ * 主键id
+ */
+ @TableId(value = "id", type = IdType.AUTO)
+ private Integer id;
+
+ /**
+ * 场景名称
+ */
+ private String name;
+
+ /**
+ * 真人形象名称
+ */
+ private String avatars;
+
+ /**
+ * 模型名称
+ */
+ private String models;
+
+ /**
+ * 形象参数(预留)
+ */
+ private String avatarsParams;
+
+ /**
+ * 模型参数(预留)
+ */
+ private String modelsParams;
+
+ /**
+ * 智能体参数(扣子)
+ */
+ private String agentParams;
+
+ /**
+ * 创建时间
+ */
+ private LocalDateTime createTime;
+
+ /**
+ * 更新时间
+ */
+ private LocalDateTime updateTime;
+
+ /**
+ * 状态
+ */
+ private Integer status;
+
+ /**
+ * 发布状态
+ */
+ private Integer publish;
+
+ /**
+ * 运行参数
+ */
+ private String runParams;
+
+ /**
+ * 运行状态
+ */
+ private String runStatus;
+
+ /**
+ * 创建部门
+ */
+ private String createDept;
+
+ /**
+ * 创建用户
+ */
+ private String createBy;
+
+ /**
+ * 更新用户
+ */
+ private String updateBy;
+
+
+}
\ No newline at end of file
diff --git a/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/domain/bo/AihumanRealConfigBo.java b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/domain/bo/AihumanRealConfigBo.java
new file mode 100644
index 00000000..e162f81a
--- /dev/null
+++ b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/domain/bo/AihumanRealConfigBo.java
@@ -0,0 +1,87 @@
+package org.ruoyi.aihuman.domain.bo;
+
+import org.ruoyi.aihuman.domain.AihumanRealConfig;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.io.Serializable;
+
+/**
+ * 真人交互数字人配置业务对象 aihuman_real_config
+ *
+ * @author ageerle
+ * @date Tue Oct 21 11:46:52 GMT+08:00 2025
+ */
+@Data
+
+@AutoMapper(target = AihumanRealConfig.class, reverseConvertGenerate = false)
+public class AihumanRealConfigBo implements Serializable {
+
+ private Integer id;
+
+ /**
+ * 场景名称
+ */
+ private String name;
+ /**
+ * 真人形象名称
+ */
+ private String avatars;
+ /**
+ * 模型名称
+ */
+ private String models;
+ /**
+ * 形象参数(预留)
+ */
+ private String avatarsParams;
+ /**
+ * 模型参数(预留)
+ */
+ private String modelsParams;
+ /**
+ * 智能体参数(扣子)
+ */
+ private String agentParams;
+ /**
+ * 创建时间
+ */
+ private LocalDateTime createTime;
+ /**
+ * 更新时间
+ */
+ private LocalDateTime updateTime;
+ /**
+ * 状态
+ */
+ private Integer status;
+ /**
+ * 发布状态
+ */
+ private Integer publish;
+
+ /**
+ * 运行参数
+ */
+ private String runParams;
+
+ /**
+ * 运行状态
+ */
+ private String runStatus;
+
+ /**
+ * 创建部门
+ */
+ private String createDept;
+ /**
+ * 创建用户
+ */
+ private String createBy;
+ /**
+ * 更新用户
+ */
+ private String updateBy;
+
+}
\ No newline at end of file
diff --git a/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/domain/vo/AihumanRealConfigVo.java b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/domain/vo/AihumanRealConfigVo.java
new file mode 100644
index 00000000..da71fc70
--- /dev/null
+++ b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/domain/vo/AihumanRealConfigVo.java
@@ -0,0 +1,108 @@
+package org.ruoyi.aihuman.domain.vo;
+
+import java.time.LocalDateTime;
+import java.io.Serializable;
+import org.ruoyi.aihuman.domain.AihumanRealConfig;
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import org.ruoyi.common.excel.annotation.ExcelDictFormat;
+import org.ruoyi.common.excel.convert.ExcelDictConvert;
+
+
+/**
+ * 真人交互数字人配置视图对象 aihuman_real_config
+ *
+ * @author ageerle
+ * @date Tue Oct 21 11:46:52 GMT+08:00 2025
+ */
+@Data
+@ExcelIgnoreUnannotated
+@AutoMapper(target = AihumanRealConfig.class)
+public class AihumanRealConfigVo implements Serializable {
+
+ private Integer id;
+ /**
+ * 场景名称
+ */
+ @ExcelProperty(value = "场景名称")
+ private String name;
+ /**
+ * 真人形象名称
+ */
+ @ExcelProperty(value = "真人形象名称")
+ private String avatars;
+ /**
+ * 模型名称
+ */
+ @ExcelProperty(value = "模型名称")
+ private String models;
+ /**
+ * 形象参数(预留)
+ */
+ @ExcelProperty(value = "形象参数", converter = ExcelDictConvert.class)
+ @ExcelDictFormat(readConverterExp = "$column.readConverterExp()")
+ private String avatarsParams;
+ /**
+ * 模型参数(预留)
+ */
+ @ExcelProperty(value = "模型参数", converter = ExcelDictConvert.class)
+ @ExcelDictFormat(readConverterExp = "$column.readConverterExp()")
+ private String modelsParams;
+ /**
+ * 智能体参数(扣子)
+ */
+ @ExcelProperty(value = "智能体参数", converter = ExcelDictConvert.class)
+ @ExcelDictFormat(readConverterExp = "$column.readConverterExp()")
+ private String agentParams;
+ /**
+ * 创建时间
+ */
+ @ExcelProperty(value = "创建时间")
+ private LocalDateTime createTime;
+ /**
+ * 更新时间
+ */
+ @ExcelProperty(value = "更新时间")
+ private LocalDateTime updateTime;
+ /**
+ * 状态
+ */
+ @ExcelProperty(value = "状态")
+ private Integer status;
+ /**
+ * 发布状态
+ */
+ @ExcelProperty(value = "发布状态")
+ private Integer publish;
+
+ /**
+ * 运行参数
+ */
+ @ExcelProperty(value = "运行参数")
+ private String runParams;
+
+ /**
+ * 运行状态
+ */
+ @ExcelProperty(value = "运行状态")
+ private String runStatus;
+
+ /**
+ * 创建部门
+ */
+ @ExcelProperty(value = "创建部门")
+ private String createDept;
+ /**
+ * 创建用户
+ */
+ @ExcelProperty(value = "创建用户")
+ private String createBy;
+ /**
+ * 更新用户
+ */
+ @ExcelProperty(value = "更新用户")
+ private String updateBy;
+
+}
\ No newline at end of file
diff --git a/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/mapper/AihumanRealConfigMapper.java b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/mapper/AihumanRealConfigMapper.java
new file mode 100644
index 00000000..9aae74d7
--- /dev/null
+++ b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/mapper/AihumanRealConfigMapper.java
@@ -0,0 +1,17 @@
+package org.ruoyi.aihuman.mapper;
+
+import org.ruoyi.aihuman.domain.AihumanRealConfig;
+import org.ruoyi.aihuman.domain.vo.AihumanRealConfigVo;
+import org.ruoyi.core.mapper.BaseMapperPlus;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 真人交互数字人配置Mapper接口
+ *
+ * @author ageerle
+ * @date Tue Oct 21 11:46:52 GMT+08:00 2025
+ */
+@Mapper
+public interface AihumanRealConfigMapper extends BaseMapperPlus {
+
+}
diff --git a/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/service/AihumanRealConfigService.java b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/service/AihumanRealConfigService.java
new file mode 100644
index 00000000..0f258ffa
--- /dev/null
+++ b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/service/AihumanRealConfigService.java
@@ -0,0 +1,56 @@
+package org.ruoyi.aihuman.service;
+
+import org.ruoyi.aihuman.domain.vo.AihumanRealConfigVo;
+import org.ruoyi.aihuman.domain.bo.AihumanRealConfigBo;
+ import org.ruoyi.core.page.TableDataInfo;
+ import org.ruoyi.core.page.PageQuery;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 真人交互数字人配置Service接口
+ *
+ * @author ageerle
+ * @date Tue Oct 21 11:46:52 GMT+08:00 2025
+ */
+public interface AihumanRealConfigService {
+
+ /**
+ * 查询真人交互数字人配置
+ */
+ AihumanRealConfigVo queryById(Integer id);
+
+ /**
+ * 查询真人交互数字人配置列表
+ */
+ TableDataInfo queryPageList(AihumanRealConfigBo bo, PageQuery pageQuery);
+
+ /**
+ * 查询真人交互数字人配置列表
+ */
+ List queryList(AihumanRealConfigBo bo);
+
+ /**
+ * 新增真人交互数字人配置
+ */
+ Boolean insertByBo(AihumanRealConfigBo bo);
+
+ /**
+ * 修改真人交互数字人配置
+ */
+ Boolean updateByBo(AihumanRealConfigBo bo);
+
+ /**
+ * 执行真人交互数字人配置
+ */
+ Boolean runByBo(AihumanRealConfigBo bo);
+
+ /**
+ * 校验并批量删除真人交互数字人配置信息
+ */
+ Boolean deleteWithValidByIds(Collection ids, Boolean isValid);
+
+ // 在AihumanRealConfigService接口中添加
+ Boolean stopByBo(AihumanRealConfigBo bo);
+}
\ No newline at end of file
diff --git a/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/service/impl/AihumanRealConfigServiceImpl.java b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/service/impl/AihumanRealConfigServiceImpl.java
new file mode 100644
index 00000000..e1a09f6f
--- /dev/null
+++ b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/service/impl/AihumanRealConfigServiceImpl.java
@@ -0,0 +1,532 @@
+package org.ruoyi.aihuman.service.impl;
+
+import com.sun.jna.Library;
+import com.sun.jna.Native;
+import jakarta.annotation.PreDestroy;
+import org.ruoyi.common.core.utils.MapstructUtils;
+import org.ruoyi.core.page.TableDataInfo;
+import org.ruoyi.core.page.PageQuery;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.ruoyi.aihuman.domain.bo.AihumanRealConfigBo;
+import org.ruoyi.aihuman.domain.vo.AihumanRealConfigVo;
+import org.ruoyi.aihuman.domain.AihumanRealConfig;
+import org.ruoyi.aihuman.mapper.AihumanRealConfigMapper;
+import org.ruoyi.aihuman.service.AihumanRealConfigService;
+import org.ruoyi.common.core.utils.StringUtils;
+import org.ruoyi.common.redis.utils.RedisUtils;
+
+import java.lang.reflect.Field;
+import java.util.List;
+import java.util.Collection;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.sun.jna.platform.win32.WinNT;
+import com.sun.jna.Pointer;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 真人交互数字人配置Service业务层处理
+ *
+ * @author ageerle
+ * @date Tue Oct 21 11:46:52 GMT+08:00 2025
+ */
+@RequiredArgsConstructor
+@Service
+public class AihumanRealConfigServiceImpl implements AihumanRealConfigService {
+
+ private final AihumanRealConfigMapper baseMapper;
+ // 存储当前运行的进程,用于停止操作
+ private volatile Process runningProcess = null;
+
+ /**
+ * 查询真人交互数字人配置
+ */
+ @Override
+ public AihumanRealConfigVo queryById(Integer id) {
+ return baseMapper.selectVoById(id);
+ }
+
+ /**
+ * 查询真人交互数字人配置列表
+ */
+ @Override
+ public TableDataInfo queryPageList(AihumanRealConfigBo bo, PageQuery pageQuery) {
+ LambdaQueryWrapper lqw = buildQueryWrapper(bo);
+ Page result = baseMapper.selectVoPage(pageQuery.build(), lqw);
+ return TableDataInfo.build(result);
+ }
+
+ /**
+ * 查询真人交互数字人配置列表
+ */
+ @Override
+ public List queryList(AihumanRealConfigBo bo) {
+ LambdaQueryWrapper lqw = buildQueryWrapper(bo);
+ return baseMapper.selectVoList(lqw);
+ }
+
+ private LambdaQueryWrapper buildQueryWrapper(AihumanRealConfigBo bo) {
+ LambdaQueryWrapper lqw = Wrappers.lambdaQuery();
+ lqw.like(StringUtils.isNotBlank(bo.getName()), AihumanRealConfig::getName, bo.getName());
+ lqw.like(StringUtils.isNotBlank(bo.getAvatars()), AihumanRealConfig::getAvatars, bo.getAvatars());
+ lqw.like(StringUtils.isNotBlank(bo.getModels()), AihumanRealConfig::getModels, bo.getModels());
+ lqw.eq(StringUtils.isNotBlank(bo.getAvatarsParams()), AihumanRealConfig::getAvatarsParams, bo.getAvatarsParams());
+ lqw.eq(StringUtils.isNotBlank(bo.getModelsParams()), AihumanRealConfig::getModelsParams, bo.getModelsParams());
+ lqw.eq(StringUtils.isNotBlank(bo.getAgentParams()), AihumanRealConfig::getAgentParams, bo.getAgentParams());
+ lqw.eq(bo.getCreateTime() != null, AihumanRealConfig::getCreateTime, bo.getCreateTime());
+ lqw.eq(bo.getUpdateTime() != null, AihumanRealConfig::getUpdateTime, bo.getUpdateTime());
+ lqw.eq(bo.getStatus() != null, AihumanRealConfig::getStatus, bo.getStatus());
+ lqw.eq(bo.getPublish() != null, AihumanRealConfig::getPublish, bo.getPublish());
+ lqw.eq(StringUtils.isNotBlank(bo.getRunParams()), AihumanRealConfig::getRunParams, bo.getRunParams());
+ // 添加runStatus字段的查询条件
+ lqw.eq(StringUtils.isNotBlank(bo.getRunStatus()), AihumanRealConfig::getRunStatus, bo.getRunStatus());
+ lqw.eq(StringUtils.isNotBlank(bo.getCreateDept()), AihumanRealConfig::getCreateDept, bo.getCreateDept());
+ lqw.eq(StringUtils.isNotBlank(bo.getCreateBy()), AihumanRealConfig::getCreateBy, bo.getCreateBy());
+ lqw.eq(StringUtils.isNotBlank(bo.getUpdateBy()), AihumanRealConfig::getUpdateBy, bo.getUpdateBy());
+ return lqw;
+ }
+
+ /**
+ * 新增真人交互数字人配置
+ */
+ @Override
+ public Boolean insertByBo(AihumanRealConfigBo bo) {
+ AihumanRealConfig add = MapstructUtils.convert(bo, AihumanRealConfig. class);
+ validEntityBeforeSave(add);
+ boolean flag = baseMapper.insert(add) > 0;
+ if (flag) {
+ bo.setId(add.getId());
+ }
+ return flag;
+ }
+
+ /**
+ * 修改真人交互数字人配置
+ */
+ @Override
+ public Boolean updateByBo(AihumanRealConfigBo bo) {
+ AihumanRealConfig update = MapstructUtils.convert(bo, AihumanRealConfig. class);
+ validEntityBeforeSave(update);
+ return baseMapper.updateById(update) > 0;
+ }
+
+ /**
+ * 保存前的数据校验
+ */
+ private void validEntityBeforeSave(AihumanRealConfig entity) {
+ //TODO 做一些数据校验,如唯一约束
+ }
+
+ /**
+ * 批量删除真人交互数字人配置
+ */
+ @Override
+ public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) {
+ if (isValid) {
+ //TODO 做一些业务上的校验,判断是否需要校验
+ }
+ return baseMapper.deleteBatchIds(ids) > 0;
+ }
+
+ private static final Logger log = LoggerFactory.getLogger(AihumanRealConfigServiceImpl.class);
+
+ /**
+ * 执行真人交互数字人配置
+ * 通过主键获取数据库记录,然后从run_params字段读取命令并执行
+ */
+ @Override
+ public Boolean runByBo(AihumanRealConfigBo bo) {
+ try {
+ // 1. 通过主键获取数据库记录
+ Integer id = bo.getId();
+ if (id == null) {
+ log.error("执行命令失败:主键ID为空");
+ throw new RuntimeException("执行命令失败:主键ID为空");
+ }
+
+ // 检查是否已经有对应的进程在运行
+ String redisKey = "aihuman:process:" + id;
+ String existingPid = RedisUtils.getCacheObject(redisKey);
+ if (StringUtils.isNotEmpty(existingPid) && isProcessRunning(existingPid)) {
+ log.warn("ID为{}的配置已有进程在运行,进程ID: {}", id, existingPid);
+ // 刷新run_status状态为运行中
+ AihumanRealConfig updateStatus = new AihumanRealConfig();
+ updateStatus.setId(id);
+ updateStatus.setRunStatus("1"); // 1表示运行中
+ baseMapper.updateById(updateStatus);
+ return true;
+ }
+
+ // 查询数据库记录
+ AihumanRealConfig config = baseMapper.selectById(id);
+ if (config == null) {
+ log.error("执行命令失败:未找到ID为{}的配置记录", id);
+ throw new RuntimeException("执行命令失败:未找到对应的配置记录");
+ }
+
+ // 2. 从记录中获取run_params字段
+ String runParams = config.getRunParams();
+ if (StringUtils.isBlank(runParams)) {
+ log.error("执行命令失败:ID为{}的记录中run_params字段为空", id);
+ throw new RuntimeException("执行命令失败:run_params字段为空");
+ }
+
+ // 3. 解析并执行命令
+ // 将多行命令合并为一个命令字符串
+ String[] commands = runParams.split("\\r?\\n");
+ if (commands.length == 0) {
+ log.error("执行命令失败:runParams中没有有效的命令");
+ throw new RuntimeException("执行命令失败:runParams中没有有效的命令");
+ }
+
+ // 将所有命令合并到一个命令字符串中,使用&&连接,确保在同一个进程中执行
+ StringBuilder mergedCmd = new StringBuilder();
+ for (int i = 0; i < commands.length; i++) {
+ String command = commands[i].trim();
+ if (command.isEmpty()) {
+ continue;
+ }
+
+ if (mergedCmd.length() > 0) {
+ mergedCmd.append(" && ");
+ }
+
+ mergedCmd.append(command);
+ }
+
+ String cmd = "cmd.exe /c " + mergedCmd.toString();
+ log.info("准备执行合并命令:{}", cmd);
+
+ // 更新数据库中的运行状态为运行中
+ AihumanRealConfig updateStatus = new AihumanRealConfig();
+ updateStatus.setId(id);
+ updateStatus.setRunStatus("1"); // 1表示运行中
+ baseMapper.updateById(updateStatus);
+
+ // 使用线程池执行命令并监听输出
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ executor.submit(() -> {
+ try {
+ Process process = Runtime.getRuntime().exec(cmd);
+ // 保存进程引用,用于后续停止操作
+ runningProcess = process;
+
+ // 获取进程ID并保存到Redis
+ String pid = getProcessId(process);
+ if (!"unknown".equals(pid)) {
+ RedisUtils.setCacheObject(redisKey, pid);
+ log.info("保存进程ID到Redis:key={}, pid={}", redisKey, pid);
+ }
+
+ // 读取标准输出
+ new Thread(() -> {
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ log.info("[LiveTalking] {}", line);
+ }
+ } catch (IOException e) {
+ log.error("读取命令输出失败", e);
+ }
+ }).start();
+
+ // 读取debug输出
+ new Thread(() -> {
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ log.debug("[LiveTalking DEBUG] {}", line);
+ }
+ } catch (IOException e) {
+ log.error("读取命令debug输出失败", e);
+ }
+ }).start();
+
+ // 等待进程结束
+ int exitCode = process.waitFor();
+ log.info("LiveTalking进程结束,退出码: {}", exitCode);
+
+ // 进程结束后更新数据库状态为已停止
+ AihumanRealConfig endStatus = new AihumanRealConfig();
+ endStatus.setId(id);
+ endStatus.setRunStatus("0"); // 0表示已停止
+ baseMapper.updateById(endStatus);
+
+ // 进程结束后从Redis中删除进程ID
+ RedisUtils.deleteObject(redisKey);
+ log.info("从Redis中删除进程ID:key={}", redisKey);
+
+ // 进程结束后清空引用
+ runningProcess = null;
+ } catch (Exception e) {
+ log.error("执行命令失败", e);
+ // 发生异常时更新数据库状态为失败
+ try {
+ AihumanRealConfig errorStatus = new AihumanRealConfig();
+ errorStatus.setId(id);
+ errorStatus.setRunStatus("2"); // 2表示启动失败
+ baseMapper.updateById(errorStatus);
+ } catch (Exception ex) {
+ log.error("更新状态失败", ex);
+ }
+ // 发生异常时从Redis中删除进程ID
+ RedisUtils.deleteObject(redisKey);
+ // 发生异常时清空引用
+ runningProcess = null;
+ }
+ });
+
+ executor.shutdown();
+ return true;
+ } catch (Exception e) {
+ log.error("执行命令过程中发生异常", e);
+ return false;
+ }
+ }
+
+ /**
+ * 检查进程是否正在运行
+ * @param pid 进程ID
+ * @return 是否正在运行
+ */
+ private boolean isProcessRunning(String pid) {
+ if (StringUtils.isEmpty(pid) || "unknown".equals(pid)) {
+ return false;
+ }
+
+ try {
+ boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win");
+ ProcessBuilder processBuilder;
+
+ if (isWindows) {
+ processBuilder = new ProcessBuilder("tasklist", "/FI", "PID eq " + pid);
+ } else {
+ processBuilder = new ProcessBuilder("ps", "-p", pid);
+ }
+
+ Process process = processBuilder.start();
+ int exitCode = process.waitFor();
+
+ // 在Windows上,tasklist命令如果找不到进程,退出码也是0,但输出中不会包含PID
+ if (isWindows) {
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ if (line.contains(pid)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ } else {
+ // 在Linux/Mac上,ps命令如果找不到进程,退出码不为0
+ return exitCode == 0;
+ }
+ } catch (Exception e) {
+ log.error("检查进程是否运行失败, pid={}", pid, e);
+ return false;
+ }
+ }
+
+ /**
+ * 停止正在运行的真人交互数字人配置任务
+ */
+ @Override
+ public Boolean stopByBo(AihumanRealConfigBo bo) {
+ try {
+ Integer id = bo.getId();
+ String redisKey = "aihuman:process:" + id;
+
+ // 首先检查Redis中是否有对应的进程ID
+ String pid = RedisUtils.getCacheObject(redisKey);
+ if (StringUtils.isNotEmpty(pid)) {
+ // 如果Redis中有进程ID,先尝试通过进程ID停止进程
+ try {
+ // 根据操作系统类型,使用不同的命令终止进程树
+ if (System.getProperty("os.name").toLowerCase().contains("win")) {
+ // Windows系统使用taskkill命令终止进程树
+ log.info("通过Redis中的PID停止进程: taskkill /F /T /PID {}", pid);
+ Process killProcess = Runtime.getRuntime().exec("taskkill /F /T /PID " + pid);
+ // 等待kill命令执行完成
+ killProcess.waitFor(5, TimeUnit.SECONDS);
+ } else {
+ // Linux/Mac系统使用pkill命令终止进程树
+ Runtime.getRuntime().exec("pkill -P " + pid);
+ }
+ } catch (Exception e) {
+ log.error("通过Redis中的PID停止进程失败", e);
+ }
+ }
+
+ // 然后检查本地runningProcess引用
+ if (runningProcess != null && runningProcess.isAlive()) {
+ log.info("正在停止LiveTalking进程...");
+ // 强制销毁进程树,确保完全停止
+ destroyProcessTree(runningProcess);
+
+ // 更新数据库中的运行状态为已停止
+ AihumanRealConfig updateStatus = new AihumanRealConfig();
+ updateStatus.setId(id);
+ updateStatus.setRunStatus("0"); // 0表示已停止
+ baseMapper.updateById(updateStatus);
+
+ runningProcess = null;
+ log.info("LiveTalking进程已停止");
+ } else {
+ log.warn("没有正在运行的LiveTalking进程");
+ // 确保数据库中的状态也是已停止
+ AihumanRealConfig updateStatus = new AihumanRealConfig();
+ updateStatus.setId(id);
+ updateStatus.setRunStatus("0"); // 0表示已停止
+ baseMapper.updateById(updateStatus);
+ }
+
+ // 无论如何都从Redis中删除进程ID
+ RedisUtils.deleteObject(redisKey);
+ log.info("从Redis中删除进程ID:key={}", redisKey);
+
+ return true;
+ } catch (Exception e) {
+ log.error("停止进程时发生异常", e);
+ // 发生异常时也尝试从Redis中删除进程ID
+ try {
+ RedisUtils.deleteObject("aihuman:process:" + bo.getId());
+ } catch (Exception ex) {
+ log.error("从Redis中删除进程ID失败", ex);
+ }
+ return false;
+ }
+ }
+
+ /**
+ * 销毁进程及其子进程(进程树)
+ * @param process 要销毁的进程
+ */
+ private void destroyProcessTree(Process process) {
+ try {
+ if (process.isAlive()) {
+ // 获取进程ID
+ String pid = getProcessId(process);
+ log.info("获取到进程ID: {}", pid);
+
+ // 根据操作系统类型,使用不同的命令终止进程树
+ if (System.getProperty("os.name").toLowerCase().contains("win")) {
+ // Windows系统使用taskkill命令终止进程树
+ log.info("执行taskkill命令终止进程树: taskkill /F /T /PID {}", pid);
+ Process killProcess = Runtime.getRuntime().exec("taskkill /F /T /PID " + pid);
+ // 等待kill命令执行完成
+ killProcess.waitFor(5, TimeUnit.SECONDS);
+ } else {
+ // Linux/Mac系统使用pkill命令终止进程树
+ Runtime.getRuntime().exec("pkill -P " + pid);
+ process.destroy();
+ }
+ }
+ } catch (Exception e) {
+ log.error("销毁进程树时发生异常", e);
+ // 如果出现异常,尝试使用普通销毁方法
+ process.destroy();
+ try {
+ // 强制销毁
+ if (process.isAlive()) {
+ process.destroyForcibly();
+ }
+ } catch (Exception ex) {
+ log.error("强制销毁进程失败", ex);
+ }
+ }
+ }
+
+ /**
+ * 获取进程ID
+ * @param process 进程对象
+ * @return 进程ID
+ */
+ private String getProcessId(Process process) {
+ try {
+ // 不同JVM实现可能有所不同,这里尝试通过反射获取
+ if (process.getClass().getName().equals("java.lang.Win32Process") ||
+ process.getClass().getName().equals("java.lang.ProcessImpl")) {
+ Field f = process.getClass().getDeclaredField("handle");
+ f.setAccessible(true);
+ long handl = f.getLong(process);
+ Kernel32 kernel = Kernel32.INSTANCE;
+ WinNT.HANDLE handle = new WinNT.HANDLE();
+ handle.setPointer(Pointer.createConstant(handl));
+ return String.valueOf(kernel.GetProcessId(handle));
+ } else if (process.getClass().getName().equals("java.lang.UNIXProcess")) {
+ Field f = process.getClass().getDeclaredField("pid");
+ f.setAccessible(true);
+ return String.valueOf(f.getInt(process));
+ }
+ } catch (Exception e) {
+ log.error("获取进程ID失败", e);
+ }
+
+ // 如果反射获取失败,尝试通过wmic命令获取
+ try {
+ // 对于Windows系统,可以尝试使用wmic命令获取进程ID
+ if (System.getProperty("os.name").toLowerCase().contains("win")) {
+ ProcessHandle.Info info = process.toHandle().info();
+ return String.valueOf(process.toHandle().pid());
+ }
+ } catch (Exception e) {
+ log.error("通过ProcessHandle获取进程ID失败", e);
+ }
+
+ return "unknown";
+ }
+
+ // JNA接口定义,用于Windows系统获取进程ID
+ interface Kernel32 extends Library {
+ Kernel32 INSTANCE = (Kernel32) Native.loadLibrary("kernel32", Kernel32.class);
+ int GetProcessId(WinNT.HANDLE hProcess);
+ }
+
+ @PreDestroy
+ public void onDestroy() {
+ if (runningProcess != null && runningProcess.isAlive()) {
+ try {
+ log.info("应用关闭,正在停止数字人进程");
+ destroyProcessTree(runningProcess);
+
+ // 查找所有运行状态为运行中的配置,并更新为已停止
+ LambdaQueryWrapper lqw = Wrappers.lambdaQuery();
+ lqw.eq(AihumanRealConfig::getRunStatus, "1");
+ List runningConfigs = baseMapper.selectList(lqw);
+ for (AihumanRealConfig config : runningConfigs) {
+ config.setRunStatus("0");
+ baseMapper.updateById(config);
+
+ // 从Redis中删除对应的进程ID记录
+ String redisKey = "aihuman:process:" + config.getId();
+ RedisUtils.deleteObject(redisKey);
+ log.info("应用关闭,从Redis中删除进程ID:key={}", redisKey);
+ }
+ } catch (Exception e) {
+ log.error("停止数字人进程失败", e);
+ // 即使发生异常,也尝试清理Redis中的进程ID记录
+ try {
+ LambdaQueryWrapper lqw = Wrappers.lambdaQuery();
+ lqw.eq(AihumanRealConfig::getRunStatus, "1");
+ List runningConfigs = baseMapper.selectList(lqw);
+ for (AihumanRealConfig config : runningConfigs) {
+ RedisUtils.deleteObject("aihuman:process:" + config.getId());
+ }
+ } catch (Exception ex) {
+ log.error("清理Redis中的进程ID记录失败", ex);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/sql/aihuman_real_config_menu.sql b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/sql/aihuman_real_config_menu.sql
new file mode 100644
index 00000000..c1ef85a6
--- /dev/null
+++ b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/sql/aihuman_real_config_menu.sql
@@ -0,0 +1,19 @@
+-- 菜单 SQL
+insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
+values(1980480880138051584, '真人交互数字人配置', '2000', '1', 'aihumanRealConfig', 'aihuman/aihumanRealConfig/index', 1, 0, 'C', '0', '0', 'aihuman:aihumanRealConfig:list', '#', 103, 1, sysdate(), null, null, '真人交互数字人配置菜单');
+
+-- 按钮 SQL
+insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
+values(1980480880138051585, '真人交互数字人配置查询', 1980480880138051584, '1', '#', '', 1, 0, 'F', '0', '0', 'aihuman:aihumanRealConfig:query', '#', 103, 1, sysdate(), null, null, '');
+
+insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
+values(1980480880138051586, '真人交互数字人配置新增', 1980480880138051584, '2', '#', '', 1, 0, 'F', '0', '0', 'aihuman:aihumanRealConfig:add', '#', 103, 1, sysdate(), null, null, '');
+
+insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
+values(1980480880138051587, '真人交互数字人配置修改', 1980480880138051584, '3', '#', '', 1, 0, 'F', '0', '0', 'aihuman:aihumanRealConfig:edit', '#', 103, 1, sysdate(), null, null, '');
+
+insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
+values(1980480880138051588, '真人交互数字人配置删除', 1980480880138051584, '4', '#', '', 1, 0, 'F', '0', '0', 'aihuman:aihumanRealConfig:remove', '#', 103, 1, sysdate(), null, null, '');
+
+insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
+values(1980480880138051589, '真人交互数字人配置导出', 1980480880138051584, '5', '#', '', 1, 0, 'F', '0', '0', 'aihuman:aihumanRealConfig:export', '#', 103, 1, sysdate(), null, null, '');
diff --git a/ruoyi-modules/ruoyi-aihuman/src/main/resources/mapper/aihuman/AihumanRealConfigMapper.xml b/ruoyi-modules/ruoyi-aihuman/src/main/resources/mapper/aihuman/AihumanRealConfigMapper.xml
new file mode 100644
index 00000000..8c7011ca
--- /dev/null
+++ b/ruoyi-modules/ruoyi-aihuman/src/main/resources/mapper/aihuman/AihumanRealConfigMapper.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/ruoyi-modules/ruoyi-generator/src/main/resources/generator.yml b/ruoyi-modules/ruoyi-generator/src/main/resources/generator.yml
index fea39ec6..7f5955fb 100644
--- a/ruoyi-modules/ruoyi-generator/src/main/resources/generator.yml
+++ b/ruoyi-modules/ruoyi-generator/src/main/resources/generator.yml
@@ -3,8 +3,8 @@ gen:
# 作者
author: ageerle
# 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool
- packageName: org.ruoyi.system
+ packageName: org.ruoyi.aihuman
# 自动去除表前缀,默认是false
autoRemovePre: false
# 表前缀(生成类名不会包含表前缀,多个用逗号分隔)
- tablePrefix: sys_
+ tablePrefix: aihuman_