add:添加真人数字人

This commit is contained in:
Maxchen
2025-10-27 14:04:42 +08:00
parent e12e4c4669
commit 0d403b6725
12 changed files with 1103 additions and 5 deletions

View File

@@ -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:
# 最大连接池数量

View File

@@ -18,6 +18,7 @@
<properties>
<easyexcel.version>3.2.1</easyexcel.version>
<jna.version>5.13.0</jna.version>
</properties>
<!-- 按照用户要求,不添加任何依赖 -->
@@ -70,5 +71,18 @@
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-excel</artifactId>
</dependency>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>${jna.version}</version>
</dependency>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna-platform</artifactId>
<version>${jna.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -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<AihumanRealConfigVo> 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<AihumanRealConfigVo> list = aihumanRealConfigService.queryList(bo);
ExcelUtil.exportExcel(list, "真人交互数字人配置", AihumanRealConfigVo.class, response);
}
/**
* 获取真人交互数字人配置详细信息
*
* @param id 主键
*/
@SaCheckPermission("aihuman:aihumanRealConfig:query")
@GetMapping("/{id}")
public R<AihumanRealConfigVo> 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<Void> 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<Void> 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<Void> 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<String> 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<String> stop(@Validated(EditGroup.class) @RequestBody AihumanRealConfigBo bo) {
boolean result = aihumanRealConfigService.stopByBo(bo);
if (result) {
// 运行状态
bo.setRunStatus("0");
return R.ok("真人交互数字人任务已停止");
} else {
return R.fail("停止真人交互数字人任务失败或没有正在运行的任务");
}
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<AihumanRealConfig, AihumanRealConfigVo> {
}

View File

@@ -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<AihumanRealConfigVo> queryPageList(AihumanRealConfigBo bo, PageQuery pageQuery);
/**
* 查询真人交互数字人配置列表
*/
List<AihumanRealConfigVo> queryList(AihumanRealConfigBo bo);
/**
* 新增真人交互数字人配置
*/
Boolean insertByBo(AihumanRealConfigBo bo);
/**
* 修改真人交互数字人配置
*/
Boolean updateByBo(AihumanRealConfigBo bo);
/**
* 执行真人交互数字人配置
*/
Boolean runByBo(AihumanRealConfigBo bo);
/**
* 校验并批量删除真人交互数字人配置信息
*/
Boolean deleteWithValidByIds(Collection<Integer> ids, Boolean isValid);
// 在AihumanRealConfigService接口中添加
Boolean stopByBo(AihumanRealConfigBo bo);
}

View File

@@ -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<AihumanRealConfigVo> queryPageList(AihumanRealConfigBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<AihumanRealConfig> lqw = buildQueryWrapper(bo);
Page<AihumanRealConfigVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
/**
* 查询真人交互数字人配置列表
*/
@Override
public List<AihumanRealConfigVo> queryList(AihumanRealConfigBo bo) {
LambdaQueryWrapper<AihumanRealConfig> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
private LambdaQueryWrapper<AihumanRealConfig> buildQueryWrapper(AihumanRealConfigBo bo) {
LambdaQueryWrapper<AihumanRealConfig> 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<Integer> 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到Rediskey={}, 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中删除进程IDkey={}", 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中删除进程IDkey={}", 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<AihumanRealConfig> lqw = Wrappers.lambdaQuery();
lqw.eq(AihumanRealConfig::getRunStatus, "1");
List<AihumanRealConfig> 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中删除进程IDkey={}", redisKey);
}
} catch (Exception e) {
log.error("停止数字人进程失败", e);
// 即使发生异常也尝试清理Redis中的进程ID记录
try {
LambdaQueryWrapper<AihumanRealConfig> lqw = Wrappers.lambdaQuery();
lqw.eq(AihumanRealConfig::getRunStatus, "1");
List<AihumanRealConfig> runningConfigs = baseMapper.selectList(lqw);
for (AihumanRealConfig config : runningConfigs) {
RedisUtils.deleteObject("aihuman:process:" + config.getId());
}
} catch (Exception ex) {
log.error("清理Redis中的进程ID记录失败", ex);
}
}
}
}
}

View File

@@ -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, '');

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.ruoyi.aihuman.mapper.AihumanRealConfigMapper">
</mapper>

View File

@@ -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_