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_