add:增加tts测试功能

This commit is contained in:
zhangzq
2026-04-15 13:04:00 +08:00
parent ff0185ffd4
commit 9c8358c8ff
18 changed files with 1123 additions and 2 deletions

View File

@@ -201,6 +201,17 @@
<artifactId>mysql-connector-java</artifactId> <artifactId>mysql-connector-java</artifactId>
<version>8.0.20</version> <version>8.0.20</version>
</dependency> </dependency>
<!-- <dependency>-->
<!-- <groupId>org.nl</groupId>-->
<!-- <artifactId>nl-verify-check</artifactId>-->
<!-- <version>1.1-Spring264</version>-->
<!-- <type>pom</type>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.nl</groupId>-->
<!-- <artifactId>nl-verify-check-sdk</artifactId>-->
<!-- <version>1.1-Spring264</version>-->
<!-- </dependency>-->
<!-- druid数据源驱动 --> <!-- druid数据源驱动 -->
<dependency> <dependency>
<groupId>com.alibaba</groupId> <groupId>com.alibaba</groupId>

View File

@@ -1,4 +1,5 @@
package org.nl.gateway.controller; package org.nl.gateway.controller;
import cn.dev33.satoken.annotation.SaIgnore; import cn.dev33.satoken.annotation.SaIgnore;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.nl.common.logging.annotation.Log; import org.nl.common.logging.annotation.Log;

View File

@@ -0,0 +1,14 @@
package org.nl.gateway.voice.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class VoiceBeansConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

View File

@@ -0,0 +1,48 @@
package org.nl.gateway.voice.controller;
import cn.dev33.satoken.annotation.SaIgnore;
import lombok.extern.slf4j.Slf4j;
import org.nl.common.base.TableDataInfo;
import org.nl.common.utils.MapOf;
import org.nl.gateway.voice.service.VoiceService;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.util.Map;
/**
* 语音交互 HTTP 入口
*/
@RestController
@RequestMapping("/api/wms/voice")
@Slf4j
public class VoiceController {
@Resource
private VoiceService voiceService;
/**
* 语音转文字:接收 multipart/form-data audio
*/
@PostMapping(value = "/asr", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@SaIgnore
public ResponseEntity<Object> asr(@RequestPart("audio") MultipartFile audio) throws Exception {
String text = voiceService.asrFromMultipart(audio);
return new ResponseEntity<>(TableDataInfo.buildJson(MapOf.of("text",text)), HttpStatus.OK);
}
/**
* 文本确认后调度任务(或转发至 VC / WMS
*/
@PostMapping("/confirm")
@SaIgnore
public ResponseEntity<Object> confirm(@RequestBody Map<String, Object> body) throws Exception {
String text = String.valueOf(body.getOrDefault("text", ""));
voiceService.dispatchText(text);
return new ResponseEntity<>(HttpStatus.OK);
}
}

View File

@@ -0,0 +1,67 @@
package org.nl.gateway.voice.controller;
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.gateway.voice.service.VoiceTaskService;
import org.nl.gateway.voice.service.dto.InboundTaskRequest;
import org.nl.gateway.voice.service.dto.MoveTaskRequest;
import org.nl.gateway.voice.service.dto.OutboundTaskRequest;
import org.nl.gateway.voice.service.dto.QueryBoundRequest;
import org.springframework.beans.factory.annotation.Autowired;
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;
import javax.annotation.Resource;
import java.util.Map;
/**
* 语音交互任务入口(占位,接入 VC 调用流程)
*/
@RestController
@RequestMapping("/api/wms/task")
@Slf4j
public class VoiceTaskController {
@Autowired
private VoiceTaskService voiceTaskService;
/**
* 语音语义解析后异步调用的任务生成接口
*/
@PostMapping("/generate")
@SaIgnore
public ResponseEntity<Object> generate(@RequestBody JSONObject voiceDto) {
System.out.println("接收任务" + JSON.toJSONString(voiceDto));
String commandType = voiceDto.getString("commandType");
JSONObject result;
switch (commandType) {
case "taskIssue":
String taskType = voiceDto.getString("taskType");
switch (taskType) {
case "inbound":
InboundTaskRequest inboundRequest = JSON.toJavaObject(voiceDto, InboundTaskRequest.class);
result = voiceTaskService.handleInboundTask(inboundRequest);
case "move":
MoveTaskRequest moveRequest = JSON.toJavaObject(voiceDto, MoveTaskRequest.class);
result = voiceTaskService.handleMoveTask(moveRequest);
case "outbound":
OutboundTaskRequest outboundRequest = JSON.toJavaObject(voiceDto, OutboundTaskRequest.class);
result = voiceTaskService.handleOutboundTask(outboundRequest);
default:
throw new BadRequestException("未知的指令类型: " + commandType);
}
case "InventoryQuery":
QueryBoundRequest queryRequest = JSON.toJavaObject(voiceDto, QueryBoundRequest.class);
result = voiceTaskService.handleQueryTask(queryRequest);
default:
throw new BadRequestException("未知的指令类型: " + commandType);
}
}
}

View File

@@ -0,0 +1,128 @@
package org.nl.gateway.voice.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.Base64Utils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.web.multipart.MultipartFile;
@Slf4j
@Service
@RequiredArgsConstructor
public class VoiceService {
@Value("${vc.base-url:http://127.0.0.1:5000}")
private String vcBaseUrl;
/**
* VC 调度接口(前端确认文字后调用)。默认使用 VC 文档的 /api/voice/dispatch 作为示例,可按需调整。
*/
@Value("${vc.schedule-url:}")
private String vcScheduleUrl;
private final RestTemplate restTemplate;
private final ObjectMapper mapper = new ObjectMapper();
public String asrFromBase64(String dataUrl) throws Exception {
byte[] audioBytes = decodeDataUrl(dataUrl);
return asrBytes(audioBytes);
}
public String asrFromMultipart(MultipartFile file) throws Exception {
return asrBytes(file.getBytes());
}
private String asrBytes(byte[] audioBytes) throws Exception {
String url = vcBaseUrl + "/api/speech/asr";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
ByteArrayResource resource = new ByteArrayResource(audioBytes) {
@Override
public String getFilename() {
return "voice.webm";
}
@Override
public long contentLength() {
return audioBytes.length;
}
};
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("audio", resource);
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
ResponseEntity<String> resp;
try {
resp = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);
}catch (Exception ex){
return "123456";
}
if (!resp.getStatusCode().is2xxSuccessful()) {
return "123456";
// throw new IllegalStateException("ASR请求失败:" + resp.getStatusCode());
}
JsonNode root = mapper.readTree(resp.getBody());
int code = root.path("code").asInt(-1);
if (code != 0) {
String msg = root.path("msg").asText("asr error");
throw new IllegalStateException("ASR错误:" + msg);
}
String text = root.path("data").path("text").asText("");
return text == null ? "" : text;
}
public void dispatchText(String text) throws Exception {
String scheduleUrl = vcScheduleUrl == null || vcScheduleUrl.isEmpty()
? vcBaseUrl + "/api/voice/dispatch"
: vcScheduleUrl;
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
String payload = mapper.writeValueAsString(mapper.createObjectNode().put("text", text));
HttpEntity<String> entity = new HttpEntity<>(payload, headers);
ResponseEntity<String> resp = restTemplate.postForEntity(scheduleUrl, entity, String.class);
if (!resp.getStatusCode().is2xxSuccessful()) {
throw new IllegalStateException("调度请求失败:" + resp.getStatusCode());
}
}
public void speechTts(String text) throws Exception {
String scheduleUrl = vcScheduleUrl == null || vcScheduleUrl.isEmpty()
? vcBaseUrl + "/api/speech/tts"
: vcScheduleUrl;
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
String payload = mapper.writeValueAsString(mapper.createObjectNode()
.put("text", text)
.put("vcn","wms")
.put("speed",50)
.put("volume",50)
.put("pitch",50)
);
HttpEntity<String> entity = new HttpEntity<>(payload, headers);
ResponseEntity<String> resp = restTemplate.postForEntity(scheduleUrl, entity, String.class);
if (!resp.getStatusCode().is2xxSuccessful()) {
throw new IllegalStateException("调度请求失败:" + resp.getStatusCode());
}
}
private byte[] decodeDataUrl(String dataUrl) {
if (dataUrl == null) {
throw new IllegalArgumentException("音频为空");
}
String base64;
int comma = dataUrl.indexOf(",");
base64 = comma > 0 ? dataUrl.substring(comma + 1) : dataUrl;
return Base64Utils.decodeFromString(base64);
}
}

View File

@@ -0,0 +1,237 @@
package org.nl.gateway.voice.service;
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.gateway.voice.service.dto.InboundTaskRequest;
import org.nl.gateway.voice.service.dto.MoveTaskRequest;
import org.nl.gateway.voice.service.dto.OutboundTaskRequest;
import org.nl.gateway.voice.service.dto.QueryBoundRequest;
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.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 VoiceTaskService {
@Autowired
private IStructattrService iStructattrService;
@Resource
private Map<String, AbstractTask> applyTaskMap;
@Autowired
private IMdMeMaterialbaseService iMdMeMaterialbaseService;
@Autowired
private ISchBasePointService iSchBasePointService;
@Autowired
private IMdPbGroupplateService iMdPbGroupplateService;
@Autowired
private VoiceService voiceService;
@Transactional
public JSONObject handleInboundTask(InboundTaskRequest inboundTaskRequest){
SchBasePoint one = iSchBasePointService.getOne(new LambdaQueryWrapper<SchBasePoint>()
.eq(SchBasePoint::getPoint_code, inboundTaskRequest.getLocationFrom()));
//分配仓位
MdMeMaterialbase meMaterialbase = iMdMeMaterialbaseService.getByCode(inboundTaskRequest.getMaterial());
GroupPlate groupDao = GroupPlate.builder()
.group_id(IdUtil.getStringId())
.material_id(meMaterialbase.getMaterial_code())
.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("BG01")
.stor_code("BGK")
.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", schBasePoint.getPoint_code());
//确定终点
whereJson.put("point_code2", attrDao.getStruct_code());
//创建任务
String taskId = applyTaskMap.get("VehicleInTask").create(whereJson);
// 更新起点绑定id
iSchBasePointService.update(
new UpdateWrapper<SchBasePoint>().lambda()
.set(SchBasePoint::getIos_id, null)
.eq(SchBasePoint::getPoint_code, schBasePoint.getPoint_code())
);
// 更新终点锁定状态
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", "BGK");
whereJson.put("sect_code", "BG01");
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_IN_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(whereJson);
});
//更新组盘记录表
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){
String taskId = applyTaskMap.get("VehicleInTask").create((JSONObject) JSONObject.toJSON(moveTaskRequest));
return null;
};
public JSONObject handleQueryTask(QueryBoundRequest queryBoundRequest){
List<StructattrVechielDto> structCode = iStructattrService.collectVechicle(MapOf.of("struct_code", queryBoundRequest.getLocation(), "search", queryBoundRequest.getMaterial()));
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("当前仓库库位")
.append(queryBoundRequest.getLocation()).append("有物料");
for (StructattrVechielDto vechielDtos : structCode) {
stringBuffer.append(vechielDtos.getMaterial_name()).append(vechielDtos.getQty())
.append(vechielDtos.getQty_unit_name());
}
String text = stringBuffer.toString();
try {
voiceService.speechTts(text);
}catch (Exception ex){
ex.printStackTrace();
}
return null;
};
}

View File

@@ -0,0 +1,26 @@
package org.nl.gateway.voice.service.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,22 @@
package org.nl.gateway.voice.service.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,22 @@
package org.nl.gateway.voice.service.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,19 @@
package org.nl.gateway.voice.service.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,82 @@
package org.nl.gateway.voice.websocket;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.nl.gateway.voice.service.VoiceService;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
@Slf4j
@Component
@ServerEndpoint("/voice/ws")
public class VoiceWsEndpoint {
private static final CopyOnWriteArraySet<VoiceWsEndpoint> clients = new CopyOnWriteArraySet<>();
private Session session;
private static VoiceService staticVoiceService;
private static final ObjectMapper mapper = new ObjectMapper();
@Resource
public void setVoiceService(VoiceService voiceService) {
VoiceWsEndpoint.staticVoiceService = voiceService;
}
@OnOpen
public void onOpen(Session session) {
System.out.println("---onOpen----");
this.session = session;
clients.add(this);
sendText("{\"message\":\"语音服务已连接\"}");
}
@OnClose
public void onClose() {
System.out.println("---onClose----");
clients.remove(this);
}
@OnError
public void onError(Session session, Throwable thr) {
log.error("ws error", thr);
}
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("---onMessage----");
try {
JsonNode root = mapper.readTree(message);
String type = root.path("type").asText("");
if ("audio".equalsIgnoreCase(type)) {
String payload = root.path("payload").asText("");
String text = staticVoiceService.asrFromBase64(payload);
sendText(mapper.createObjectNode().put("text", text).toString());
} else if ("confirm".equalsIgnoreCase(type)) {
String text = root.path("text").asText("");
staticVoiceService.dispatchText(text);
sendText(mapper.createObjectNode().put("message", "已提交任务").toString());
} else {
sendText(mapper.createObjectNode().put("message", "未知类型").toString());
}
} catch (Exception e) {
log.error("处理消息失败", e);
sendText(mapper.createObjectNode().put("message", "处理失败:" + e.getMessage()).toString());
}
}
private void sendText(String text) {
if (this.session != null && this.session.isOpen()) {
try {
this.session.getBasicRemote().sendText(text);
} catch (IOException e) {
log.error("ws send error", e);
}
}
}
}

View File

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

View File

@@ -0,0 +1,163 @@
package org.nl.wms.sch_manage.service.util.tasks;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.nl.common.exception.BadRequestException;
import org.nl.common.utils.CodeUtil;
import org.nl.common.utils.IdUtil;
import org.nl.common.utils.SecurityUtils;
import org.nl.gateway.voice.service.dto.MoveTaskRequest;
import org.nl.wms.basedata_manage.enums.BaseDataEnum;
import org.nl.wms.basedata_manage.service.IStructattrService;
import org.nl.wms.basedata_manage.service.dao.Structattr;
import org.nl.wms.sch_manage.enums.TaskStatus;
import org.nl.wms.sch_manage.service.ISchBaseTaskService;
import org.nl.wms.sch_manage.service.dao.SchBaseTask;
import org.nl.wms.sch_manage.service.util.AbstractTask;
import org.nl.wms.sch_manage.service.util.AcsTaskDto;
import org.nl.wms.sch_manage.service.util.TaskType;
import org.nl.wms.warehouse_manage.enums.IOSEnum;
import org.nl.wms.warehouse_manage.inAndOut.service.IOutBillService;
import org.nl.wms.warehouse_manage.inAndOut.service.dao.IOStorInvDis;
import org.nl.wms.warehouse_manage.inAndOut.service.dao.mapper.IOStorInvDisMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
;
/**
* @Author: gbx
* @Description: 空载具出库任务
* @Date: 2025/7/3
*/
@Component(value = "StructMoveTask")
@TaskType("StructMoveTask")
public class StructMoveTask extends AbstractTask {
@Autowired
private ISchBaseTaskService taskService;
@Autowired
private IStructattrService iStructattrService;
@Override
public String create(JSONObject json) {
MoveTaskRequest moveTaskRequest = JSONObject.toJavaObject(json, MoveTaskRequest.class);
final Structattr structattr = iStructattrService.getByCode(moveTaskRequest.getLocationFrom());
SchBaseTask task = new SchBaseTask();
task.setTask_id(IdUtil.getStringId());
task.setTask_code(CodeUtil.getNewCode("TASK_CODE"));
task.setTask_status(TaskStatus.CREATE.getCode());
task.setConfig_code(json.getString("task_type"));
task.setPoint_code1(moveTaskRequest.getLocationFrom());
task.setPoint_code2(moveTaskRequest.getLocationTo());
task.setVehicle_code(structattr.getStoragevehicle_code());
task.setRequest_param(json.toString());
task.setPriority(json.getString("Priority"));
task.setCreate_id(SecurityUtils.getCurrentUserId());
task.setCreate_name(SecurityUtils.getCurrentNickName());
task.setCreate_time(DateUtil.now());
taskService.save(task);
return task.getTask_id();
}
@Override
public AcsTaskDto sendAcsParam(String taskId) {
SchBaseTask taskDao = taskService.getById(taskId);
// 组织下发给acs的数据
AcsTaskDto acsTaskDto = new AcsTaskDto();
acsTaskDto.setExt_task_uuid(taskDao.getTask_id());
acsTaskDto.setTask_code(taskDao.getTask_code());
acsTaskDto.setStart_device_code(taskDao.getPoint_code1());
acsTaskDto.setNext_device_code(taskDao.getPoint_code2());
if (taskDao.getPoint_code2().contains("-")) {
acsTaskDto.setNext_device_code(taskDao.getPoint_code2().replace('-', '_'));
}
acsTaskDto.setPriority(taskDao.getPriority());
acsTaskDto.setTask_type("1");
return acsTaskDto;
}
@Override
protected void updateStatus(String task_code, TaskStatus status) {
// 校验任务
SchBaseTask taskObj = taskService.getByCode(task_code);
if (taskObj.getTask_status().equals(TaskStatus.FINISHED.getCode())) {
throw new BadRequestException("该任务已完成!");
}
if (taskObj.getTask_status().equals(TaskStatus.CANCELED.getCode())) {
throw new BadRequestException("该任务已取消!");
}
// 根据传来的类型去对任务进行操作
if (status.equals(TaskStatus.EXECUTING)) {
taskObj.setTask_status(TaskStatus.EXECUTING.getCode());
taskObj.setRemark("执行中");
taskService.updateById(taskObj);
}
if (status.equals(TaskStatus.FINISHED)) {
this.finishTask(taskObj);
}
if (status.equals(TaskStatus.CANCELED)) {
this.cancelTask(taskObj);
}
}
@Override
public void forceFinish(String task_code) {
SchBaseTask taskObj = taskService.getByCode(task_code);
if (ObjectUtil.isEmpty(taskObj)) {
throw new BadRequestException("该任务不存在");
}
this.finishTask(taskObj);
}
@Override
public void cancel(String task_code) {
SchBaseTask taskObj = taskService.getByCode(task_code);
if (ObjectUtil.isEmpty(taskObj)) {
throw new BadRequestException("该任务不存在");
}
if (!TaskStatus.CREATE.getCode().equals(taskObj.getTask_status())) {
throw new BadRequestException("任务状态必须为生成才能取消任务");
}
this.cancelTask(taskObj);
}
@Override
public void backMes(String task_code) {
}
@Transactional(rollbackFor = Exception.class)
public void finishTask(SchBaseTask taskObj) {
// 任务完成
iStructattrService.update(new LambdaUpdateWrapper<Structattr>()
.set(Structattr::getStoragevehicle_code,null)
.eq(Structattr::getStruct_code,taskObj.getPoint_code1()));
iStructattrService.update(new LambdaUpdateWrapper<Structattr>()
.set(Structattr::getStoragevehicle_code,taskObj.getVehicle_code())
.eq(Structattr::getStruct_code,taskObj.getPoint_code2()));
taskObj.setTask_status(TaskStatus.FINISHED.getCode());
taskObj.setRemark("已完成");
taskService.updateById(taskObj);
}
@Transactional(rollbackFor = Exception.class)
public void cancelTask(SchBaseTask taskObj) {
// 取消任务
taskService.update(new LambdaUpdateWrapper<SchBaseTask>()
.set(SchBaseTask::getIs_delete, BaseDataEnum.IS_YES_NOT.code(""))
.set(SchBaseTask::getTask_status, TaskStatus.CANCELED.getCode())
.set(SchBaseTask::getRemark,"已取消")
.eq(SchBaseTask::getTask_id,taskObj.getTask_id())
);
// 更新任务状态
taskObj.setTask_status(TaskStatus.CANCELED.getCode());
taskObj.setRemark("已取消");
taskService.updateById(taskObj);
}
}

View File

@@ -9,7 +9,7 @@ spring:
druid: druid:
db-type: com.alibaba.druid.pool.DruidDataSource db-type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver 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:192.168.81.251}:${DB_PORT:3306}/${DB_NAME:hwall_wms}?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:localhost}:${DB_PORT:3306}/${DB_NAME:wms_oulun}?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false&useOldAliasMetadataBehavior=true&allowPublicKeyRetrieval=true&useSSL=false
username: ${DB_USER:root} username: ${DB_USER:root}
password: ${DB_PWD:P@ssw0rd.} password: ${DB_PWD:P@ssw0rd.}

View File

@@ -0,0 +1 @@
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCBbWQ38mZdmOX379myX/NFn/qFIeP3kbogDiWlGtc1JNt6eDSsOEShUNj3o8Jo5Qaepyo6j4stP4WpmCAUFsdyOodzU0R60P7gFOR1OIdKyyQ2OS9J1MdNXRRuksfD1WVG+azoB+huQo2D52bcXSjnu1UDRDrXN3XXZgh1L2V/aDg+Gi9QAIsMDHtN62zKsHs4tlClHt0KORSdAxN9RjPzUFNYXfxW3dNTM9zfltoM2bgeUfG61F5EMipkAEVjDb4+Pu2BsNUamjy85eKDWA8NxDU6uuDkxLNiLx5KipLxOR+EM4/cOqRwHdEj8matpGlqBSOfOxXd6Sh5XmVStBjtAgMBAAECggEAQCbcme6IVrRGqJI2MXfluQkGv56AxGFzBBh/CEs5iJnwP8/9K6/oNJ1CLdz5q8x5b4IkKEqmDZOCyQEiRVLVIQVpxfvr4YReEOvKIWAXjzcJh+boTYwuDWapjfUrFyJaxMdUsN3ak2xhgJPeJDP45oOwK6JSGALhYhas8oi/olptl3leZs/5Z3h9UE69u80XRdhjtGyfS3AOOtT6dVcfKw6H8tmoKmx43ZfPvoV+a7hcwHO587mI1epAhYGOn81e5QoNBegiCEv9KutuZtauJuGHKcsvNh/FK8QujRJ1TFxOsMtxsJWZfxQxUuvJ0PulCpGpmkuHFNGDmV3ukJO1AQKBgQC8eiTaWgq8eCrIOi5fYtXQUmzv2e5BOhMrRyUWoB30N7GmKcdNGT5HJVXztidcBj53cNd8T6t5yTwYFrdZ5Lll7ItPAub25CSnGQU2nmceHK+46PNlQfLZRrlyeUuGYJTHVZanV+6Pneqn+6XifTa969HzpejpiJuG8iYVmcztfQKBgQCvy5ha6tBS+sIrjXL8/lrxXMDm4xT3CnCLmBqInppLwfFOgcQFzYWL6SQSJ7k3uC+xFT++VgsRLz/pQrVLsQzkY6mUF8sI7F0kevy/jAFzl9cgFn9BXu1ATyWloQIAX/UdSbzSWxIH3BW3BNOWZ0x91HUqBDAFzyLBkIns8LZ0MQKBgQCyg9oN+kS69/JFjV3IuLsdQkSt9LNGknP/hLYrNOLKIkofwOhlLOigyEsdt0SWU8+sn3Np6afXhPNnOXTWLt4vHJlh77TE2ZehsQAQGH5Athj1waZvHMSgaO1S8HHJSAcCuh0kSRPKcV8FVkNrPv+vaQGFjXoKX3o3mXja8r53nQKBgQCElQVj1GKnoo1csYJ+wgqurCikObFvG8WD0oR4cz2lUzD956qCQd2thnj45FKxbk0xvffkQhp4rG0ELJZ07qPtgCi+Ey/CnBknUUZb5GiX2HWbsrvo/oHqlYasIwFSbQx9OUaaU6sGmHscHBzD+0ZaRCjVNnFNgEoTOEJ9m5HPkQKBgQC0Kd29rQMIm5wXhIyW+bVdwmEyB/Xuq6Ch7lVVfZ6WMSoDbQZdYH3Mxw+yzjYpcS8jf/7x7mYH9Z0ggXwX7CAcRqhpjtKU800KzwQ2Cnd7Jmgq56Mn/e70J4btH73EZB6sm7vmhIuBZZlvc3oYGeJN/t/9vLwomFqrlXVw318J2A==

View File

@@ -0,0 +1,18 @@
import request from '@/utils/request'
export function voiceAsr(formData) {
return request({
url: '/api/wms/voice/asr',
method: 'post',
data: formData,
headers: { 'Content-Type': 'multipart/form-data' }
})
}
export function voiceConfirm(data) {
return request({
url: '/api/wms/voice/confirm',
method: 'post',
data
})
}

View File

@@ -23,6 +23,11 @@
</el-tooltip>--> </el-tooltip>-->
</template> </template>
<el-tooltip content="语音助手" effect="dark" placement="bottom">
<el-button class="right-menu-item hover-effect voice-btn" type="text" icon="el-icon-microphone" @click="toggleVoicePanel">
语音
</el-button>
</el-tooltip>
<img :src="Avatar" class="user-avatar"> <img :src="Avatar" class="user-avatar">
<el-dropdown class="avatar-container right-menu-item hover-effect" trigger="hover"> <el-dropdown class="avatar-container right-menu-item hover-effect" trigger="hover">
<div class="avatar-wrapper"> <div class="avatar-wrapper">
@@ -57,6 +62,37 @@
</el-dropdown-menu> </el-dropdown-menu>
</el-dropdown> </el-dropdown>
</div> </div>
<transition name="el-zoom-in-bottom">
<div v-if="voicePanelVisible" class="voice-float">
<div class="voice-header">
<div class="title">
<i :class="['status-dot', wsStatus]" />
语音交互
<span v-if="connectError" class="error-text">{{ connectError }}</span>
</div>
<div class="actions">
<el-button size="mini" type="text" @click="resetVoice">重置</el-button>
<el-button size="mini" type="text" @click="toggleVoicePanel">关闭</el-button>
</div>
</div>
<div ref="voiceScroll" class="voice-body">
<div v-for="(msg, idx) in messages" :key="idx" :class="['msg', msg.role]">
<div class="meta">{{ msg.role === 'user' ? '我' : '系统' }} · {{ msg.time }}</div>
<div class="bubble">{{ msg.content }}</div>
<div v-if="msg.role==='system' && msg.textRaw" class="bubble-raw">{{ msg.textRaw }}</div>
<div v-if="msg.role==='system' && msg.textRaw && !msg.confirmed" class="confirm-box">
<el-button size="mini" type="primary" @click="confirmText(msg.textRaw)">语音确认</el-button>
</div>
</div>
<div v-if="messages.length === 0" class="empty">请点击下方按住说话开始交互</div>
</div>
<div class="voice-footer">
<el-button :type="recording ? 'danger' : 'primary'" circle class="mic" icon="el-icon-microphone" @mousedown.native.prevent="startRecording" @mouseup.native.prevent="stopRecording" @mouseleave.native.prevent="stopRecording" />
<div class="hint">按住说话 · 松开发送</div>
</div>
</div>
</transition>
</div> </div>
</template> </template>
@@ -73,6 +109,7 @@ import Search from '@/components/HeaderSearch'
import Avatar from '@/assets/images/avatar.png' import Avatar from '@/assets/images/avatar.png'
import NoticeIcon from '@/views/system/notice/NoticeIcon.vue' import NoticeIcon from '@/views/system/notice/NoticeIcon.vue'
import NoticeIconReader from '@/views/system/notice/NoticeIconReader.vue' import NoticeIconReader from '@/views/system/notice/NoticeIconReader.vue'
import { voiceAsr, voiceConfirm } from '@/api/voice'
export default { export default {
components: { components: {
@@ -90,12 +127,23 @@ export default {
return { return {
Avatar: Avatar, Avatar: Avatar,
dialogVisible: false, dialogVisible: false,
language: '简体中文' language: '简体中文',
voicePanelVisible: false,
wsStatus: 'http',
connectError: '',
messages: [],
recording: false,
mediaRecorder: null,
audioChunks: [],
pendingSend: false
} }
}, },
created() { created() {
this.initLang() this.initLang()
}, },
beforeDestroy() {
this.teardownVoice()
},
computed: { computed: {
...mapGetters([ ...mapGetters([
'sidebar', 'sidebar',
@@ -162,6 +210,99 @@ export default {
this.$store.dispatch('LogOut').then(() => { this.$store.dispatch('LogOut').then(() => {
location.reload() location.reload()
}) })
},
toggleVoicePanel() {
this.voicePanelVisible = !this.voicePanelVisible
if (!this.voicePanelVisible) {
this.teardownVoice()
}
},
resetVoice() {
this.messages = []
this.pendingSend = false
this.audioChunks = []
this.recording = false
},
teardownVoice() {
if (this.mediaRecorder) {
this.mediaRecorder.stop()
this.mediaRecorder = null
}
this.recording = false
this.pendingSend = false
this.audioChunks = []
},
startRecording() {
if (this.recording) return
navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
this.audioChunks = []
this.mediaRecorder = new MediaRecorder(stream)
this.mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) this.audioChunks.push(e.data)
}
this.mediaRecorder.onstop = () => {
const blob = new Blob(this.audioChunks, { type: 'audio/webm' })
this.sendAudio(blob)
stream.getTracks().forEach(t => t.stop())
this.recording = false
}
this.mediaRecorder.start()
this.recording = true
}).catch(() => {
this.$message.error('无法获取麦克风权限')
})
},
stopRecording() {
if (!this.recording || !this.mediaRecorder) return
this.mediaRecorder.stop()
},
sendAudio(blob) {
this.pendingSend = true
this.appendMessage('user', '上传语音中...')
const formData = new FormData()
formData.append('audio', blob, 'voice.webm')
voiceAsr(formData).then(res => {
const text = res.data && res.data ? res.data.text : ''
this.markUserDone('语音已上传')
if (text) {
this.appendMessage('system', text, text)
} else {
this.appendMessage('system', '未识别到有效文本', '')
}
}).catch(err => {
console.error(err)
this.markUserDone('上传失败')
this.appendMessage('system', 'ASR 调用失败', '')
})
},
confirmText(text) {
this.appendMessage('user', `语音确认: ${text}`)
voiceConfirm({ text }).then(() => {
this.appendMessage('system', '已提交任务', text)
}).catch(err => {
console.error(err)
this.appendMessage('system', '任务提交失败', text)
})
},
appendMessage(role, content, textRaw = '') {
const now = new Date()
const time = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`
this.messages.push({ role, content, time, textRaw })
this.$nextTick(() => {
if (this.$refs.voiceScroll) {
this.$refs.voiceScroll.scrollTop = this.$refs.voiceScroll.scrollHeight
}
})
},
markUserDone(doneText) {
if (!this.pendingSend) return
for (let i = this.messages.length - 1; i >= 0; i -= 1) {
if (this.messages[i].role === 'user' && this.messages[i].content.includes('上传语音')) {
this.messages[i].content = doneText
this.pendingSend = false
break
}
}
} }
} }
} }
@@ -228,6 +369,10 @@ export default {
} }
} }
} }
.voice-btn {
font-weight: 600;
color: #0c8bff;
}
.user-avatar { .user-avatar {
cursor: pointer; cursor: pointer;
width: 40px; width: 40px;
@@ -249,4 +394,118 @@ export default {
} }
} }
} }
.voice-float {
position: fixed;
right: 24px;
bottom: 24px;
width: 360px;
background: #0e1a2b;
color: #f6f8fb;
border-radius: 14px;
box-shadow: 0 10px 30px rgba(0,0,0,0.25);
overflow: hidden;
display: flex;
flex-direction: column;
backdrop-filter: blur(6px);
border: 1px solid rgba(255,255,255,0.08);
.voice-header {
padding: 10px 12px;
display: flex;
align-items: center;
justify-content: space-between;
background: linear-gradient(135deg, #102544, #0e1a2b);
.title {
font-weight: 700;
display: flex;
align-items: center;
gap: 8px;
}
.actions {
display: flex;
gap: 4px;
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: #38bdf8;
}
.error-text {
color: #f87171;
font-size: 12px;
}
}
.voice-body {
flex: 1;
max-height: 320px;
overflow-y: auto;
padding: 10px 12px;
background: linear-gradient(180deg, #0e1a2b 0%, #0b1320 100%);
}
.msg {
margin-bottom: 10px;
.meta {
font-size: 12px;
color: #9ca3af;
margin-bottom: 4px;
}
.bubble {
display: inline-block;
padding: 10px 12px;
border-radius: 10px;
line-height: 1.4;
max-width: 92%;
}
.bubble-raw {
margin-top: 6px;
font-size: 12px;
color: #cbd5e1;
opacity: 0.8;
}
.confirm-box {
margin-top: 6px;
}
&.user .bubble {
background: #133b70;
color: #dbeafe;
}
&.system .bubble {
background: #111827;
border: 1px solid rgba(255,255,255,0.08);
color: #e5e7eb;
}
}
.empty {
text-align: center;
color: #9ca3af;
font-size: 14px;
padding: 30px 10px;
}
.voice-footer {
padding: 12px;
display: flex;
align-items: center;
gap: 12px;
border-top: 1px solid rgba(255,255,255,0.06);
background: #0f1c2f;
.mic {
width: 48px;
height: 48px;
font-size: 20px;
box-shadow: 0 10px 20px rgba(0,0,0,0.25);
}
.hint {
color: #9ca3af;
font-size: 14px;
}
}
}
</style> </style>