4 Commits

Author SHA1 Message Date
zhaoyf
bced456945 opt:语音控制功能-第一次联调测试修改 2026-04-22 10:47:24 +08:00
zhaoyf
5d83cceca5 opt:语音控制功能-添加vc回调接口 2026-04-20 10:12:55 +08:00
zhaoyf
6950c6c2c2 add:语音控制功能-初版待联调测试 2026-04-20 09:03:34 +08:00
Afly
f9ca8583fb add:语音控制功能-创建前端组件 2026-04-15 08:54:14 +08:00
20 changed files with 2146 additions and 7 deletions

View File

@@ -35,6 +35,9 @@
<if test="pcsn != null and pcsn != ''">
AND gro.pcsn = #{pcsn}
</if>
<if test="struct_code != null and struct_code != ''">
AND ivt.struct_code = #{struct_code}
</if>
<if test="stor_code != null and stor_code != ''">
AND ivt.stor_code = #{stor_code}
</if>

View File

@@ -0,0 +1,62 @@
package org.nl.wms.system_manage.controller.speech;
import lombok.extern.slf4j.Slf4j;
import org.nl.wms.system_manage.service.speech.SysSpeechService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.Map;
/**
* @author Zhao Ya Fei
* @date 2026-04-14
* 根据语音输入执行任务
*/
@Slf4j
@RestController
@RequestMapping("/api/speech")
public class SysSpeechController {
@Resource
private SysSpeechService sysSpeechService;
/**
* 健康检查
* @return
*/
@GetMapping("/health")
public ResponseEntity<Object> health(){
return ResponseEntity.noContent().build();
}
/**
* 语音转文字
* @return
*/
@PostMapping("/asr")
public ResponseEntity<Object> asr(){
return ResponseEntity.noContent().build();
}
/**
* 文字转语音
* @return
*/
@PostMapping("/tts")
public ResponseEntity<Object> tts(){
return ResponseEntity.noContent().build();
}
/**
* 用户确认文字
* @return
*/
@PostMapping("/confirm")
public ResponseEntity<Object> confirm(@RequestBody Map<String, Object> body){
Map<String, Object> res = sysSpeechService.confirm(body.getOrDefault("text", "").toString());
return ResponseEntity.ok(res);
}
}

View File

@@ -0,0 +1,167 @@
package org.nl.wms.system_manage.controller.speech;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.nl.common.utils.MapOf;
import org.nl.wms.system_manage.service.speech.api.AsrTtsClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
@Slf4j
@ServerEndpoint("/ws/speech")
public class SysSpeechWebSocketEndpoint {
private static AsrTtsClient asrTtsClient;
private final ObjectMapper objectMapper = new ObjectMapper();
// 存储每个会话的上下文
private static final Map<String, SessionContext> sessionContexts = new ConcurrentHashMap<>();
private Session session;
//注入静态属性
@Autowired
public void setAsrTtsClient(AsrTtsClient asrTtsClient) {
SysSpeechWebSocketEndpoint.asrTtsClient = asrTtsClient;
}
@OnOpen
public void onOpen(Session session) {
sessionContexts.put(session.getId(), new SessionContext());
this.session = session;
log.info("WebSocket 监听: {}", session.getId());
}
@OnMessage
public void onTextMessage(String message, Session session) throws IOException {
SessionContext ctx = sessionContexts.get(session.getId());
if (ctx == null) return;
JsonNode json = objectMapper.readTree(message);
String type = json.get("type").asText();
if ("start".equals(type)) {
//TODO 健康检查
asrTtsClient.health().thenAccept(result -> {
log.info("健康检查结果: {}", result);
Integer resCode = JSON.parseObject(result.toString()).getInteger("code");
if (resCode != 0) {
sendTextBySys("语音控制服务异常");
}
}).exceptionally(ex -> {
log.error("健康检查异常: {}", session.getId(), ex);
sendTextBySys("语音控制服务异常");
return null;
});
} else if ("end".equals(type)) {
// 录音结束,将 PCM 数据发送给第三方 ASR
byte[] pcmData = ctx.audioBuffer.toByteArray();
// 异步调用第三方服务,避免阻塞 WebSocket 线程
asrTtsClient.recognizeAsync(pcmData, ctx.sampleRate, ctx.channels, ctx.bitDepth)
.thenAccept(result -> {
JSONObject jsonObject = JSON.parseObject(result.toString(), JSONObject.class);
log.info("ASR 输出参数为:-------------------" + jsonObject.toString());
Integer code = jsonObject.getInteger("code");
if (code == null || code != 0) {
String msg = jsonObject.getString("msg");
sendTextBySys(msg != null ? msg : "未知错误");
return;
}
JSONObject data = jsonObject.getJSONObject("data");
if (data != null) {
String text = data.getString("text");
sendTextByUser(text);
} else {
sendTextBySys("语音转文字发生错误");
}
})
.exceptionally(ex -> {
log.error("ASR 异常: {}", session.getId(), ex);
sendTextByUser("ASR 异常");
return null;
});
ctx.audioBuffer.reset();
// 清理上下文
// sessionContexts.remove(session.getId());
}
}
@OnMessage
public void onBinaryMessage(ByteBuffer byteBuffer, Session session) {
SessionContext ctx = sessionContexts.get(session.getId());
if (ctx != null) {
byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.get(bytes);
try {
ctx.audioBuffer.write(bytes);
System.out.println("已接收音频数据:" + ctx.audioBuffer.size());
} catch (IOException e) {
log.error("写入音频数据异常: {}", session.getId(), e);
}
}
}
@OnClose
public void onClose(Session session) {
sessionContexts.remove(session.getId());
log.info("连接关闭: {}", session.getId());
}
@OnError
public void onError(Session session, Throwable error) {
log.error("语音控制ws连接错误", error);
sessionContexts.remove(session.getId());
}
// 会话上下文内部类
private static class SessionContext {
ByteArrayOutputStream audioBuffer = new ByteArrayOutputStream();
int sampleRate;
int channels;
int bitDepth;
@Override
public String toString() {
return "sampleRate=" + sampleRate + ", channels=" + channels + ", bitDepth=" + bitDepth;
}
}
private void sendTextByUser(String message) {
try {
Map<String, Object> map = new HashMap<>();
map.put("type", "user");
map.put("message", message);
// 结果通过 WebSocket 返回给前端
String response = objectMapper.writeValueAsString(map);
session.getBasicRemote().sendText(response);
} catch (IOException e) {
log.error("发送文本消息异常: {}", session.getId(), e);
}
}
private void sendTextBySys(String message) {
try {
Map<String, String> map = new HashMap<>();
map.put("type", "system");
map.put("message", message);
session.getBasicRemote().sendText(
objectMapper.writeValueAsString(map));
} catch (IOException e) {
log.error("语音控制:发送错误消息异常: {}", session.getId(), e);
}
}
}

View File

@@ -0,0 +1,74 @@
package org.nl.wms.system_manage.controller.speech;
import cn.dev33.satoken.annotation.SaIgnore;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.nl.common.exception.BadRequestException;
import org.nl.wms.system_manage.service.speech.VoiceCallbackService;
import org.nl.wms.system_manage.service.speech.dto.InboundTaskRequest;
import org.nl.wms.system_manage.service.speech.dto.MoveTaskRequest;
import org.nl.wms.system_manage.service.speech.dto.OutboundTaskRequest;
import org.nl.wms.system_manage.service.speech.dto.QueryBoundRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
*
* vc回调接口
*/
@RestController
@RequestMapping("/api/wms/task")
@Slf4j
public class VoiceCallbackController {
@Autowired
private VoiceCallbackService voiceCallbackService;
/**
* 语音语义解析后异步调用的任务生成接口
*/
@PostMapping("/generate")
@SaIgnore
public ResponseEntity<Object> generate(@RequestBody JSONObject voiceDto) {
System.out.println("接收任务" + JSON.toJSONString(voiceDto));
String commandType = voiceDto.getString("commandType");
JSONObject commandParam = voiceDto.getJSONObject("commandParam");
JSONObject result;
switch (commandType) {
case "taskIssue":
final String taskType = commandParam.getString("taskType");
switch (taskType) {
case "inbound":
InboundTaskRequest inboundRequest = JSON.toJavaObject(commandParam, InboundTaskRequest.class);
result = voiceCallbackService.handleInboundTask(inboundRequest);
break;
case "move":
MoveTaskRequest moveRequest = JSON.toJavaObject(commandParam, MoveTaskRequest.class);
result = voiceCallbackService.handleMoveTask(moveRequest);
break;
case "outbound":
OutboundTaskRequest outboundRequest = JSON.toJavaObject(commandParam, OutboundTaskRequest.class);
result = voiceCallbackService.handleOutboundTask(outboundRequest);
break;
default:
throw new BadRequestException("未知的指令类型: " + commandType);
}
break;
case "InventoryQuery":
QueryBoundRequest queryRequest = JSON.toJavaObject(commandParam, QueryBoundRequest.class);
result = voiceCallbackService.handleQueryTask(queryRequest);
break;
default:
throw new BadRequestException("未知的指令类型: " + commandType);
}
return new ResponseEntity<>(result, HttpStatus.OK);
}
}

View File

@@ -0,0 +1,34 @@
package org.nl.wms.system_manage.service.speech;
import java.util.Map;
public interface SysSpeechService {
/**
* 语音识别服务健康检查
*
* @return Boolean
*/
Boolean health();
/**
* 语音转文字
*
* @return Map<String, Object>
*/
Map<String, Object> asr();
/**
* 文字转语音
*
* @return Map<String, Object>
*/
Map<String, Object> tts();
/**
* 语音识别结果确认
*
* @return Map<String, Object>
*/
Map<String, Object> confirm(String text);
}

View File

@@ -0,0 +1,262 @@
package org.nl.wms.system_manage.service.speech;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.nl.common.exception.BadRequestException;
import org.nl.common.utils.CodeUtil;
import org.nl.common.utils.IdUtil;
import org.nl.common.utils.MapOf;
import org.nl.common.utils.SecurityUtils;
import org.nl.config.SpringContextHolder;
import org.nl.wms.basedata_manage.enums.BaseDataEnum;
import org.nl.wms.basedata_manage.service.IMdMeMaterialbaseService;
import org.nl.wms.basedata_manage.service.IStructattrService;
import org.nl.wms.basedata_manage.service.dao.*;
import org.nl.wms.basedata_manage.service.dto.StrategyMater;
import org.nl.wms.basedata_manage.service.dto.StrategyStructMaterialVO;
import org.nl.wms.basedata_manage.service.dto.StrategyStructParam;
import org.nl.wms.pda_manage.ios_manage.service.PdaIosInService;
import org.nl.wms.pda_manage.ios_manage.service.PdaIosOutService;
import org.nl.wms.sch_manage.enums.StatusEnum;
import org.nl.wms.sch_manage.enums.TaskEnum;
import org.nl.wms.sch_manage.service.ISchBasePointService;
import org.nl.wms.sch_manage.service.dao.SchBasePoint;
import org.nl.wms.sch_manage.service.util.AbstractTask;
import org.nl.wms.sch_manage.service.util.tasks.StInTask;
import org.nl.wms.sch_manage.service.util.tasks.VehicleOutTask;
import org.nl.wms.system_manage.service.speech.dto.InboundTaskRequest;
import org.nl.wms.system_manage.service.speech.dto.MoveTaskRequest;
import org.nl.wms.system_manage.service.speech.dto.OutboundTaskRequest;
import org.nl.wms.system_manage.service.speech.dto.QueryBoundRequest;
import org.nl.wms.warehouse_manage.enums.IOSConstant;
import org.nl.wms.warehouse_manage.enums.IOSEnum;
import org.nl.wms.warehouse_manage.service.IMdPbGroupplateService;
import org.nl.wms.warehouse_manage.service.IStIvtMoveinvService;
import org.nl.wms.warehouse_manage.service.dao.GroupPlate;
import org.nl.wms.warehouse_manage.service.dto.MoveInsertDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Base64Utils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import static org.nl.wms.warehouse_manage.enums.IOSEnum.GROUP_PLATE_STATUS;
@Slf4j
@Service
public class VoiceCallbackService {
@Autowired
private IStructattrService iStructattrService;
@Resource
private Map<String, AbstractTask> applyTaskMap;
@Autowired
private IMdMeMaterialbaseService iMdMeMaterialbaseService;
@Autowired
private ISchBasePointService iSchBasePointService;
@Autowired
private IMdPbGroupplateService iMdPbGroupplateService;
@Transactional
public JSONObject handleInboundTask(InboundTaskRequest inboundTaskRequest){
// SchBasePoint one = iSchBasePointService.getOne(new LambdaQueryWrapper<SchBasePoint>()
// .eq(SchBasePoint::getPoint_code, inboundTaskRequest.getLocationFrom()));
SchBasePoint one = iSchBasePointService.getOne(new LambdaQueryWrapper<SchBasePoint>()
.eq(SchBasePoint::getPoint_code, "RKD01"));
//分配仓位
MdMeMaterialbase meMaterialbase = iMdMeMaterialbaseService.getByCode(inboundTaskRequest.getMaterial());
GroupPlate groupDao = GroupPlate.builder()
.group_id(IdUtil.getStringId())
.material_id(meMaterialbase.getMaterial_id())
.storagevehicle_code(one.getVehicle_code())
.pcsn("111")
.qty_unit_id(meMaterialbase.getQty_unit_id())
.qty_unit_name(meMaterialbase.getQty_unit_name())
.qty(new BigDecimal(inboundTaskRequest.getQty()))
.status(GROUP_PLATE_STATUS.code("组盘"))
.create_id(SecurityUtils.getCurrentUserId())
.create_name(SecurityUtils.getCurrentNickName())
.create_time(DateUtil.now())
.build();
iMdPbGroupplateService.save(groupDao);
StrategyMater mater = new StrategyMater();
mater.setMaterial_code(inboundTaskRequest.getMaterial());
mater.setMaterial_id(meMaterialbase.getMaterial_id());
mater.setQty(new BigDecimal(inboundTaskRequest.getQty()));
mater.setPcsn("111");
List<StrategyMater> materss = new ArrayList<>();
materss.add(mater);
List<Structattr> structattrs = iStructattrService.inBoundSectDiv(
StrategyStructParam.builder()
.ioType(StatusEnum.STRATEGY_TYPE.code("入库"))
.sect_code("YL01")
.stor_code("YL")
.storagevehicle_code(one.getVehicle_code())
.strategyMaters(materss)
.build());
if (CollectionUtils.isEmpty(structattrs)) {
throw new BadRequestException("无可用货位");
}
Structattr attrDao = structattrs.get(0);
//确定起点
// SchBasePoint schBasePoint = iSchBasePointService.getOne(new LambdaQueryWrapper<SchBasePoint>()
// .eq(SchBasePoint::getPoint_code, inboundTaskRequest.getLocationFrom()));
// if (ObjectUtil.isEmpty(schBasePoint)) {
// throw new BadRequestException("未找到载具所在的点位信息,请检查");
// }
JSONObject whereJson = new JSONObject();
// whereJson.put("point_code1", "RKD01");
// whereJson.put("point_code1", schBasePoint.getPoint_code());
//确定终点
// whereJson.put("point_code2", attrDao.getStruct_code());
whereJson.put("vehicle_code", one.getVehicle_code());
whereJson.put("task_type", "STInTask");
whereJson.put("TaskCode", CodeUtil.getNewCode("TASK_CODE"));
whereJson.put("PickingLocation", "RKD01");
whereJson.put("PlacedLocation", attrDao.getStruct_code());
//创建任务
String taskId = applyTaskMap.get("STInTask").create(whereJson);
// 更新起点绑定id
iSchBasePointService.update(
new UpdateWrapper<SchBasePoint>().lambda()
.set(SchBasePoint::getIos_id, null)
.eq(SchBasePoint::getPoint_code, "RKD01")
);
// 更新终点锁定状态
JSONObject lock_map = new JSONObject();
lock_map.put("struct_code", attrDao.getStruct_code());
lock_map.put("inv_id", null);
lock_map.put("inv_code", null);
lock_map.put("inv_type", null);
lock_map.put("taskdtl_id", taskId);
lock_map.put("lock_type", IOSEnum.LOCK_TYPE.code("入库锁"));
iStructattrService.updateStatusByCode("0", lock_map);
return null;
};
@Transactional
public JSONObject handleOutboundTask(OutboundTaskRequest outboundTaskRequest){
MdMeMaterialbase meMaterialbase = iMdMeMaterialbaseService.getByCode(outboundTaskRequest.getMaterial());
JSONObject whereJson = new JSONObject();
whereJson.put("material_id", meMaterialbase.getMaterial_id());
whereJson.put("material_code", meMaterialbase.getMaterial_code());
whereJson.put("stor_code", "YL");
whereJson.put("sect_code", "YL01");
whereJson.put("siteCode", "CKD01");
StrategyMater mater = new StrategyMater();
mater.setQty(BigDecimal.valueOf(outboundTaskRequest.getQty()));
mater.setMaterial_id(meMaterialbase.getMaterial_id());
mater.setMaterial_code(meMaterialbase.getMaterial_code());
List<StrategyMater> maters = new ArrayList<>();
maters.add(mater);
StrategyStructParam strategyStructParam = StrategyStructParam.builder()
.ioType(whereJson.getString(StatusEnum.STRATEGY_TYPE.code("出库")))
.sect_code(whereJson.getString("sect_code"))
.stor_code(whereJson.getString("stor_code"))
.strategyMaters(maters)
.build();
List<StrategyStructMaterialVO> structList = iStructattrService.outBoundSectDiv(strategyStructParam);
if (CollectionUtils.isEmpty(structList)) {
throw new BadRequestException("无可用库存!");
}
structList.forEach(r -> {
//创建任务
JSONObject taskForm = new JSONObject();
taskForm.put("task_type", IOSConstant.VEHICLE_OUT_TASK);
taskForm.put("config_code", IOSConstant.VEHICLE_OUT_TASK);
taskForm.put("TaskCode", CodeUtil.getNewCode("TASK_CODE"));
taskForm.put("PickingLocation", r.getStruct_code());
taskForm.put("PlacedLocation", whereJson.getString("siteCode"));
taskForm.put("vehicle_code", r.getStoragevehicle_code());
applyTaskMap.get(IOSConstant.VEHICLE_OUT_TASK).create(taskForm);
});
//更新组盘记录表
Set<String> vehicleCodeSet = structList.stream()
.map(StrategyStructMaterialVO::getStoragevehicle_code)
.collect(Collectors.toSet());
iMdPbGroupplateService.update(
new GroupPlate(),
new LambdaUpdateWrapper<GroupPlate>()
.set(GroupPlate::getStatus, IOSEnum.GROUP_PLATE_STATUS.code("出库"))
.in(GroupPlate::getStoragevehicle_code, vehicleCodeSet)
);
//锁定仓位
Set<String> structCodeSet = structList.stream()
.map(StrategyStructMaterialVO::getStruct_code)
.collect(Collectors.toSet());
iStructattrService.update(
new LambdaUpdateWrapper<Structattr>()
.set(Structattr::getInv_id, null)
.set(Structattr::getInv_code, null)
.set(Structattr::getInv_type, null)
.set(Structattr::getLock_type, IOSEnum.LOCK_TYPE.code("出库锁"))
.in(Structattr::getStruct_code, structCodeSet)
);
return null;
};
public JSONObject handleMoveTask(MoveTaskRequest moveTaskRequest){
Structattr attrDao = iStructattrService.findByCode(moveTaskRequest.getLocationFrom());
JSONObject taskForm = new JSONObject();
taskForm.put("point_code1", moveTaskRequest.getLocationFrom());
taskForm.put("point_code", moveTaskRequest.getLocationTo());
taskForm.put("config_code", IOSConstant.MOVE_CONFIG_TASK);
taskForm.put("vehicle_code", attrDao.getStoragevehicle_code());
String taskId = applyTaskMap.get("MoveTask").create((JSONObject) JSONObject.toJSON(taskForm));
return null;
};
public JSONObject handleQueryTask(QueryBoundRequest queryBoundRequest){
List<StructattrVechielDto> structCode = iStructattrService.collectVechicle(MapOf.of("struct_code", queryBoundRequest.getLocation(), "search", queryBoundRequest.getMaterial()));
Map<String, BigDecimal> collect = structCode.stream()
.collect(Collectors.groupingBy(StructattrVechielDto::getMaterial_code,
Collectors.reducing(BigDecimal.ZERO, StructattrVechielDto::getQty, BigDecimal::add)));
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("当前仓库物料有物料");
collect.entrySet().forEach(r -> {
stringBuffer.append(r.getKey()).append("共计")
.append(r.getValue())
.append("");
});
// for (StructattrVechielDto vechielDtos : structCode) {
// stringBuffer.append(vechielDtos.getMaterial_name()).append("共计")
// .append(vechielDtos.getQty())
// .append("个");
// }
String text = stringBuffer.toString();
try {
// voiceService.speechTts(text);
}catch (Exception ex){
ex.printStackTrace();
}
JSONObject result = new JSONObject();
result.put("has",text);
return result ;
};
}

View File

@@ -0,0 +1,88 @@
package org.nl.wms.system_manage.service.speech.api;
import cn.hutool.http.HttpRequest;
import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
/**
* 语音识别服务接口
*/
@Component
public class AsrTtsClient {
@Value("${vc.base-url:http://127.0.0.1:5000}")
private String vcBaseUrl;
/**
* 健康检查
*/
public CompletableFuture<Object> health() {
return CompletableFuture.supplyAsync(() -> {
return HttpRequest.get(vcBaseUrl + "/health")
.execute()
.body();
});
}
/**
* 语音转文字
*/
public static void asr() {
}
/**
* 文字转语音
*/
public CompletableFuture<byte[]> tts(String text) {
return CompletableFuture.supplyAsync(() -> {
Map<String, Object> params = new HashMap<>();
params.put("text", text);
params.put("vcn", "xiaoyan");
params.put("speed", 50);
params.put("volume", 50);
params.put("pitch", 50);
return HttpRequest.post(vcBaseUrl + "/api/speech/tts")
.body(JSONObject.toJSONString(params))
.contentType("application/json")
.execute()
.bodyBytes();
});
}
/**
* 用户确认文字
*/
public String confirm(String text, boolean confirmed) {
Map<String, Object> params = new HashMap<>();
params.put("text", text);
params.put("confirmed", confirmed);
return HttpRequest.post(vcBaseUrl + "/api/speech/confirm")
.body(JSONObject.toJSONString(params))
.contentType("application/json")
.execute()
.body();
}
public CompletionStage<Object> recognizeAsync(byte[] pcmData, int sampleRate, int channels, int bitDepth) {
return CompletableFuture.supplyAsync(() -> {
return HttpRequest.post(vcBaseUrl + "/api/speech/asr")
.form("audio", pcmData, "audio.pcm")
// .body(pcmData)
.contentType("multipart/form-data")
.execute()
.body();
});
}
}

View File

@@ -0,0 +1,19 @@
package org.nl.wms.system_manage.service.speech.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class InboundTaskRequest {
@JsonProperty("commandType")
private String commandType; // 指令类型taskIssue
@JsonProperty("deviceId")
private String deviceId; // 语音设备ID
@JsonProperty("taskType")
private String taskType; // 任务类型inbound
@JsonProperty("locationFrom")
private String locationFrom; // 取货点编号:从哪个位置入库
@JsonProperty("material")
private String material; // 物料编码
@JsonProperty("qty")
private Integer qty; // 入库物料数量
}

View File

@@ -0,0 +1,17 @@
package org.nl.wms.system_manage.service.speech.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class MoveTaskRequest {
@JsonProperty("commandType")
private String commandType; // 指令类型taskIssue
@JsonProperty("deviceId")
private String deviceId; // 语音设备ID
@JsonProperty("taskType")
private String taskType; // 任务类型move
@JsonProperty("locationFrom")
private String locationFrom; // 取货点编号:从哪个库位取货
@JsonProperty("locationTo")
private String locationTo; // 放货点编号:移到哪个库位
}

View File

@@ -0,0 +1,17 @@
package org.nl.wms.system_manage.service.speech.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class OutboundTaskRequest {
@JsonProperty("commandType")
private String commandType; // 指令类型taskIssue
@JsonProperty("deviceId")
private String deviceId; // 语音设备ID
@JsonProperty("taskType")
private String taskType; // 任务类型outbound
@JsonProperty("material")
private String material; // 物料编码:需要出什么料
@JsonProperty("qty")
private Integer qty; // 出库数量
}

View File

@@ -0,0 +1,15 @@
package org.nl.wms.system_manage.service.speech.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class QueryBoundRequest {
@JsonProperty("commandType")
private String commandType; // 指令类型InventoryQuery
@JsonProperty("deviceId")
private String deviceId; // 语音设备ID
@JsonProperty("location")
private String location; // 货位编号:基于货位查看库存
@JsonProperty("material")
private String material; // 物料编码:基于物料查询库存
}

View File

@@ -0,0 +1,11 @@
package org.nl.wms.system_manage.service.speech.enums;
/**
* @author Zhao Ya Fei
* @date 2026-04-14
* @desc 语音识别URL
*/
public class SpeechUrlConstant {
}

View File

@@ -0,0 +1,59 @@
package org.nl.wms.system_manage.service.speech.impl;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.nl.common.utils.MapOf;
import org.nl.wms.system_manage.service.speech.SysSpeechService;
import org.nl.wms.system_manage.service.speech.api.AsrTtsClient;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.Map;
/**
* @author Zhao Ya Fei
* @date 2026-04-14
* 语音识别服务实现类
*/
@Slf4j
@Service
public class SysSpeechServiceImpl implements SysSpeechService {
@Resource
private AsrTtsClient asrTtsClient;
@Override
public Boolean health() {
return null;
}
@Override
public Map<String, Object> asr() {
return Collections.emptyMap();
}
@Override
public Map<String, Object> tts() {
return Collections.emptyMap();
}
@Override
public Map<String, Object> confirm(String text) {
if ("".equals(text)) {
throw new RuntimeException("确认文本为空");
}
String resJson = asrTtsClient.confirm(text, true);
log.info("语音控制:用户确认结果: {}", resJson);
//TODO
JSONObject jsonObject = JSONObject.parseObject(resJson);
JSONObject data = jsonObject.getJSONObject("data");
if (data != null) {
String resText = data.getString("text");
return MapOf.of("text", resText);
}
byte[] audio = asrTtsClient.tts(text).join();
//TODO 音频返回前端播放处理
return MapOf.of("text", text);
}
}

View File

@@ -0,0 +1,98 @@
package org.nl.wms.system_manage.service.speech.session;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.Session;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 暂时先不用
* 后续如果需要实时展示任务结果可能用到
*/
@Slf4j
//@Component
public class SessionManager {
// userId -> WebSocket Session
private final Map<String, Session> sessionMap = new ConcurrentHashMap<>();
private final AtomicInteger onlineCount = new AtomicInteger(0);
/**
* 注册会话(支持单端登录策略:新连接踢掉旧连接)
*/
public void register(String userId, Session session) {
Session oldSession = sessionMap.put(userId, session);
if (oldSession != null && oldSession.isOpen()) {
try { oldSession.close(); } catch (IOException ignored) {}
log.info("语音控制:🔄 用户 {} 旧连接已被踢出", userId);
}
onlineCount.incrementAndGet();
log.info("语音控制:✅ 用户 {} 已上线 | 当前在线: {}", userId, onlineCount.get());
}
/**
* 注销会话
*/
public void unregister(String userId) {
sessionMap.remove(userId);
onlineCount.decrementAndGet();
log.info("语音控制:❌ 用户 {} 已下线 | 当前在线: {}", userId, onlineCount.get());
}
/**
* 主动推送文本消息
*/
public boolean pushText(String userId, String message) {
Session session = sessionMap.get(userId);
if (session == null || !session.isOpen()) {
log.warn("⚠️ 推送失败: 用户 {} 不在线", userId);
return false;
}
try {
session.getBasicRemote().sendText(message);
return true;
} catch (IOException e) {
log.error("📤 推送文本异常: {}", userId, e);
// 连接已失效,自动清理
unregister(userId);
return false;
}
}
/**
* 主动推送二进制数据TTS音频、文件流等
*/
public boolean pushBinary(String userId, byte[] data) {
Session session = sessionMap.get(userId);
if (session == null || !session.isOpen()) {
log.warn("⚠️ 推送失败: 用户 {} 不在线", userId);
return false;
}
try {
session.getBasicRemote().sendBinary(ByteBuffer.wrap(data));
return true;
} catch (IOException e) {
log.error("📤 推送二进制异常: {}", userId, e);
unregister(userId);
return false;
}
}
/**
* 广播文本(如系统公告、全局状态)
*/
public void broadcastText(String message) {
sessionMap.values().forEach(session -> {
if (session.isOpen()) {
try { session.getBasicRemote().sendText(message); } catch (IOException ignored) {}
}
});
}
public int getOnlineCount() { return onlineCount.get(); }
}

View File

@@ -9,10 +9,11 @@ spring:
druid:
db-type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://${DB_HOST:192.168.81.251}:${DB_PORT:3306}/${DB_NAME:wms_standardv2}?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false&useOldAliasMetadataBehavior=true&allowPublicKeyRetrieval=true&useSSL=false
# url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:wms_oulun}?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false&useOldAliasMetadataBehavior=true&allowPublicKeyRetrieval=true&useSSL=false
# url: jdbc:mysql://${DB_HOST:192.168.81.251}:${DB_PORT:3306}/${DB_NAME:wms_standardv2}?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false&useOldAliasMetadataBehavior=true&allowPublicKeyRetrieval=true&useSSL=false
url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:wms_standardv2}?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false&useOldAliasMetadataBehavior=true&allowPublicKeyRetrieval=true&useSSL=false
username: ${DB_USER:root}
password: ${DB_PWD:P@ssw0rd.}
password: ${DB_PWD:123456}
# password: ${DB_PWD:P@ssw0rd.}
# 初始连接数
initial-size: 15
# 最小连接数
@@ -137,3 +138,5 @@ sa-token:
lucene:
index:
path: D:\lms\lucene\index
vc:
base-url: http://192.168.20.156:5000

View File

@@ -0,0 +1,9 @@
import request from "@/utils/request";
export function confirm(data) {
return request({
url: '/api/speech/confirm',
method: 'post',
data
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -79,6 +79,7 @@ export default {
'moreMenu': 'MoreMenu',
'browses': 'browse',
'fz': 'Full screen zoom',
'speech': 'Voice Control',
'submit': 'Submit Success',
'add': 'Add Success',
'edit': 'Edit Success',

View File

@@ -79,6 +79,7 @@ export default {
'moreMenu': '更多菜单',
'browses': '浏览',
'fz': '全屏缩放',
'speech': '语音控制',
'submit': '提交成功',
'add': '新增成功',
'edit': '编辑成功',

View File

@@ -6,6 +6,13 @@
<div class="right-menu">
<template v-if="device!=='mobile'">
<!-- 语音输入按钮 -->
<el-tooltip :content="$t('common.speech')" effect="dark" placement="bottom">
<div class="right-menu-item hover-effect voice-btn" @click="openVoiceInput">
<i class="el-icon-microphone" />
</div>
</el-tooltip>
<search id="header-search" class="right-menu-item" />
<!-- <el-tooltip content="项目文档" effect="dark" placement="bottom">
@@ -15,10 +22,13 @@
<el-tooltip :content="$t('common.fz')" effect="dark" placement="bottom">
<screenfull id="screenfull" class="right-menu-item hover-effect" />
</el-tooltip>
<notice-icon class="right-menu-item" />
<notice-icon-reader ref="noticeIconReader" />
<notice-icon class="right-menu-item" />
<notice-icon-reader ref="noticeIconReader" />
<voice-input-dialog ref="voiceInputDialog" />
<!-- <el-tooltip content="布局设置" effect="dark" placement="bottom">
<!-- <el-tooltip content="布局设置" effect="dark" placement="bottom">
<size-select id="size-select" class="right-menu-item hover-effect" />
</el-tooltip>-->
@@ -73,6 +83,7 @@ import Search from '@/components/HeaderSearch'
import Avatar from '@/assets/images/avatar.png'
import NoticeIcon from '@/views/system/notice/NoticeIcon.vue'
import NoticeIconReader from '@/views/system/notice/NoticeIconReader.vue'
import VoiceInputDialog from '@/components/VoiceInputDialog'
export default {
components: {
@@ -84,7 +95,8 @@ export default {
SizeSelect,
Search,
Doc,
TopNav
TopNav,
VoiceInputDialog
},
data() {
return {
@@ -162,6 +174,9 @@ export default {
this.$store.dispatch('LogOut').then(() => {
location.reload()
})
},
openVoiceInput() {
this.$refs.voiceInputDialog && this.$refs.voiceInputDialog.show()
}
}
}