feat:发货区AGV任务申请

This commit is contained in:
zhouz
2026-08-07 13:48:56 +08:00
parent a97f03f9ae
commit cc4e44f4e5
14 changed files with 524 additions and 0 deletions

View File

@@ -4,6 +4,9 @@ import cn.code.nl.framework.common.pojo.CommonResult;
import cn.code.nl.module.wms.controller.acs.vo.AcsApplyInboundReqVO;
import cn.code.nl.module.wms.controller.acs.vo.AcsApplyInboundRespVO;
import cn.code.nl.module.wms.controller.acs.vo.AcsCallTaskReqVO;
import cn.code.nl.module.wms.controller.acs.vo.AcsShippingApplyReqVO;
import cn.code.nl.module.wms.controller.acs.vo.AcsShippingApplyRespVO;
import cn.code.nl.module.wms.service.shippingarea.ShippingAreaService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.security.PermitAll;
@@ -13,6 +16,7 @@ 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 jakarta.annotation.Resource;
import static cn.code.nl.framework.common.pojo.CommonResult.success;
@@ -27,6 +31,20 @@ import static cn.code.nl.framework.common.pojo.CommonResult.success;
@Validated
public class CallTaskController {
@Resource
private ShippingAreaService shippingAreaService;
/**
* 申请发货区 AGV 任务。
*/
@PostMapping("/shipping/apply")
@Operation(summary = "申请发货区任务")
@PermitAll
public CommonResult<AcsShippingApplyRespVO> applyShippingTask(
@Valid @RequestBody AcsShippingApplyReqVO reqVO) {
return success(shippingAreaService.applyShippingTask(reqVO));
}
@PostMapping("/inbound/apply")
@Operation(summary = "请求入库")
@PermitAll

View File

@@ -0,0 +1,17 @@
package cn.code.nl.module.wms.controller.acs.vo;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* AGV 发货区任务申请请求。
*/
@Data
public class AcsShippingApplyReqVO {
@NotBlank(message = "设备号不能为空")
private String deviceCode;
@NotBlank(message = "木箱号不能为空")
private String boxNo;
}

View File

@@ -0,0 +1,20 @@
package cn.code.nl.module.wms.controller.acs.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* AGV 发货区任务申请结果。
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AcsShippingApplyRespVO {
private Long taskId;
private String boxNo;
private String pointCode;
private Long outboundBillId;
private String outboundBillCode;
}

View File

@@ -0,0 +1,13 @@
package cn.code.nl.module.wms.dal.dataobject.iostorinvdis;
import lombok.Data;
/**
* 木箱关联的出库单引用。
*/
@Data
public class OutboundBillRef {
private Long outboundBillId;
private String outboundBillCode;
}

View File

@@ -0,0 +1,12 @@
package cn.code.nl.module.wms.dal.dataobject.point;
import lombok.Data;
/**
* 发货区候选排。
*/
@Data
public class ShippingPointRowCandidate {
private Integer rowNum;
}

View File

@@ -0,0 +1,31 @@
package cn.code.nl.module.wms.dal.dataobject.point;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
/**
* WMS 点位数据对象。
*/
@Data
@TableName("wms_point")
public class WmsPointDO {
@TableId
private String pointCode;
private String pointName;
private String regionCode;
private String regionName;
private String pointType;
private String pointStatus;
private String vehicleCode;
private Integer vehicleQty;
private Integer rowNum;
private Integer colNum;
private String ingTaskCode;
private Boolean isUsed;
@TableField(exist = false)
private String taskVehicleCode;
}

View File

@@ -6,6 +6,7 @@ import cn.code.nl.framework.common.pojo.PageResult;
import cn.code.nl.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.code.nl.framework.mybatis.core.mapper.BaseMapperX;
import cn.code.nl.module.wms.dal.dataobject.iostorinvdis.IostorinvDisDO;
import cn.code.nl.module.wms.dal.dataobject.iostorinvdis.OutboundBillRef;
import org.apache.ibatis.annotations.Mapper;
import cn.code.nl.module.wms.controller.admin.iostorinvdis.vo.*;
import cn.code.nl.module.wms.controller.admin.iostorinv.vo.OutboundAllocationRespVO;
@@ -19,6 +20,9 @@ import org.apache.ibatis.annotations.Param;
@Mapper
public interface IostorinvDisMapper extends BaseMapperX<IostorinvDisDO> {
/** 根据木箱号查询最近一张未删除的出库单。 */
OutboundBillRef selectLatestOutboundBillByBoxNo(@Param("boxNo") String boxNo);
List<OutboundAllocationRespVO> selectAllocationList(
@Param("iostorinvId") Long iostorinvId,
@Param("iostorinvdtlId") Long iostorinvdtlId,

View File

@@ -0,0 +1,42 @@
package cn.code.nl.module.wms.dal.mysql.point;
import cn.code.nl.module.wms.dal.dataobject.point.ShippingPointRowCandidate;
import cn.code.nl.module.wms.dal.dataobject.point.WmsPointDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* WMS 点位 Mapper。
*/
@Mapper
public interface WmsPointMapper {
/** 查询当前出库单已使用且仍有空位的排。 */
List<ShippingPointRowCandidate> selectSameBillRows(@Param("billId") String billId);
/** 查询整排为空的候选排。 */
List<ShippingPointRowCandidate> selectEmptyRows();
/** 查询候选排内第一个空闲点位。 */
WmsPointDO selectFreePointInRow(@Param("rowNum") Integer rowNum);
/** 条件抢占发货点位。 */
int reserveShippingPoint(@Param("pointCode") String pointCode,
@Param("reservationCode") String reservationCode);
/** 将预占码替换为任务 ID。 */
int bindShippingTask(@Param("pointCode") String pointCode,
@Param("reservationCode") String reservationCode,
@Param("taskId") String taskId);
/** 查询任务占用的发货点位和任务木箱。 */
WmsPointDO selectShippingTaskPoint(@Param("taskId") String taskId);
/** 完成发货区任务。 */
int completeShippingTask(@Param("taskId") String taskId, @Param("boxNo") String boxNo);
/** 取消发货区任务。 */
int cancelShippingTask(@Param("taskId") String taskId);
}

View File

@@ -0,0 +1,19 @@
package cn.code.nl.module.wms.service.shippingarea;
import cn.code.nl.module.wms.controller.acs.vo.AcsShippingApplyReqVO;
import cn.code.nl.module.wms.controller.acs.vo.AcsShippingApplyRespVO;
/**
* 发货区任务服务。
*/
public interface ShippingAreaService {
/** 申请发货区 AGV 任务。 */
AcsShippingApplyRespVO applyShippingTask(AcsShippingApplyReqVO reqVO);
/** 完成发货区任务。 */
void completeTask(Long taskId);
/** 取消发货区任务。 */
void cancelTask(Long taskId);
}

View File

@@ -0,0 +1,131 @@
package cn.code.nl.module.wms.service.shippingarea;
import cn.code.nl.framework.common.exception.ServiceException;
import cn.code.nl.module.task.api.TransportTaskApi;
import cn.code.nl.module.task.dto.TransportTaskCreateReqDTO;
import cn.code.nl.module.wms.controller.acs.vo.AcsShippingApplyReqVO;
import cn.code.nl.module.wms.controller.acs.vo.AcsShippingApplyRespVO;
import cn.code.nl.module.wms.dal.dataobject.iostorinvdis.OutboundBillRef;
import cn.code.nl.module.wms.dal.dataobject.point.ShippingPointRowCandidate;
import cn.code.nl.module.wms.dal.dataobject.point.WmsPointDO;
import cn.code.nl.module.wms.dal.mysql.iostorinvdis.IostorinvDisMapper;
import cn.code.nl.module.wms.dal.mysql.point.WmsPointMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* 发货区任务服务实现。
*/
@Service
public class ShippingAreaServiceImpl implements ShippingAreaService {
private static final String SHIPPING_AREA_BIZ_TYPE = "SHIPPING_AREA";
private static final String SHIPPING_AREA_HANDLE_CODE = "SHIPAREATASK";
@Resource
private IostorinvDisMapper iostorinvDisMapper;
@Resource
private WmsPointMapper wmsPointMapper;
@Resource
private TransportTaskApi transportTaskApi;
/**
* 申请发货区 AGV 任务。
*/
@Override
@Transactional(rollbackFor = Exception.class)
public AcsShippingApplyRespVO applyShippingTask(AcsShippingApplyReqVO reqVO) {
OutboundBillRef bill = iostorinvDisMapper.selectLatestOutboundBillByBoxNo(reqVO.getBoxNo());
if (bill == null) {
throw new ServiceException(500, "木箱未关联到未删除的出库单");
}
String billId = String.valueOf(bill.getOutboundBillId());
String reservationCode = "B" + billId;
WmsPointDO point = reserveShippingPoint(billId, reservationCode);
if (point == null) {
throw new ServiceException(500, "发货区没有符合条件的可用点位");
}
Long taskId = createTransportTask(reqVO, point, billId);
if (wmsPointMapper.bindShippingTask(point.getPointCode(), reservationCode,
String.valueOf(taskId)) != 1) {
throw new ServiceException(500, "发货区点位预占状态异常,请重试");
}
transportTaskApi.issueTransportTaskRollBack(taskId).getCheckedData();
return new AcsShippingApplyRespVO(taskId, reqVO.getBoxNo(), point.getPointCode(),
bill.getOutboundBillId(), bill.getOutboundBillCode());
}
/**
* 完成发货区任务。
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void completeTask(Long taskId) {
String taskCode = String.valueOf(taskId);
WmsPointDO point = wmsPointMapper.selectShippingTaskPoint(taskCode);
if (point == null) {
throw new ServiceException(500, "未找到任务预占的发货区点位");
}
if (wmsPointMapper.completeShippingTask(taskCode, point.getTaskVehicleCode()) != 1) {
throw new ServiceException(500, "发货区任务完成状态异常");
}
}
/**
* 取消发货区任务。
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void cancelTask(Long taskId) {
if (wmsPointMapper.cancelShippingTask(String.valueOf(taskId)) != 1) {
throw new ServiceException(500, "未找到任务预占的发货区点位");
}
}
/**
* 按同单排、全空排的顺序抢占点位。
*/
private WmsPointDO reserveShippingPoint(String billId, String reservationCode) {
WmsPointDO point = reserveFromRows(wmsPointMapper.selectSameBillRows(billId), reservationCode);
return point != null ? point : reserveFromRows(wmsPointMapper.selectEmptyRows(), reservationCode);
}
/**
* 从候选排逐排尝试抢占空闲点位。
*/
private WmsPointDO reserveFromRows(List<ShippingPointRowCandidate> rows, String reservationCode) {
for (ShippingPointRowCandidate row : rows) {
WmsPointDO point = wmsPointMapper.selectFreePointInRow(row.getRowNum());
if (point != null && wmsPointMapper.reserveShippingPoint(
point.getPointCode(), reservationCode) == 1) {
return point;
}
}
return null;
}
/**
* 创建搬运任务。
*/
private Long createTransportTask(AcsShippingApplyReqVO reqVO, WmsPointDO point, String billId) {
TransportTaskCreateReqDTO task = new TransportTaskCreateReqDTO();
task.setTaskName("发货区搬运任务");
task.setOwnerService("WMS");
task.setBizType(SHIPPING_AREA_BIZ_TYPE);
task.setBizId(billId);
task.setHandleCode(SHIPPING_AREA_HANDLE_CODE);
task.setTaskType("010503");
task.setAcsTaskType("7");
task.setAgvSystemType("1");
task.setPointCode1(reqVO.getDeviceCode());
task.setPointCode2(point.getPointCode());
task.setVehicleCode(reqVO.getBoxNo());
task.setIsAutoIssue("0");
task.setProductArea("FH");
task.setIsCreateFinish(true);
return transportTaskApi.createTransportTask(task).getCheckedData();
}
}

View File

@@ -0,0 +1,29 @@
package cn.code.nl.module.wms.task;
import cn.code.nl.framework.execute.core.AbstractTask;
import cn.code.nl.framework.execute.core.dto.TaskExecuteDTO;
import cn.code.nl.module.wms.service.shippingarea.ShippingAreaService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
/**
* 发货区任务状态处理器。
*/
@Component("SHIPAREATASK")
public class ShipAreaTask extends AbstractTask {
@Resource
private ShippingAreaService shippingAreaService;
/** 转发任务完成事件。 */
@Override
public void doHandleFinish(TaskExecuteDTO dto) {
shippingAreaService.completeTask(dto.getTaskId());
}
/** 转发任务取消事件。 */
@Override
public void doHandleCancel(TaskExecuteDTO dto) {
shippingAreaService.cancelTask(dto.getTaskId());
}
}

View File

@@ -2,6 +2,19 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.code.nl.module.wms.dal.mysql.iostorinvdis.IostorinvDisMapper">
<select id="selectLatestOutboundBillByBoxNo"
resultType="cn.code.nl.module.wms.dal.dataobject.iostorinvdis.OutboundBillRef">
SELECT h.iostorinv_id AS outbound_bill_id,
h.bill_code AS outbound_bill_code
FROM wms_iostorinvdis d
JOIN wms_iostorinv h ON h.iostorinv_id = d.iostorinv_id
WHERE d.storagevehicle_code = #{boxNo}
AND h.io_type = '1'
AND h.deleted = 0
ORDER BY h.create_time DESC, h.iostorinv_id DESC
LIMIT 1
</select>
<select id="selectAllocationList"
resultType="cn.code.nl.module.wms.controller.admin.iostorinv.vo.OutboundAllocationRespVO">
SELECT dis.iostorinvdis_id,

View File

@@ -0,0 +1,110 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.code.nl.module.wms.dal.mysql.point.WmsPointMapper">
<sql id="shippingPointWithBill">
SELECT p.*,
COALESCE(
CASE WHEN p.ing_task_code LIKE 'B%'
THEN SUBSTRING(p.ing_task_code, 2) END,
t.biz_id,
(SELECT CAST(h.iostorinv_id AS CHAR)
FROM wms_iostorinvdis d
JOIN wms_iostorinv h ON h.iostorinv_id = d.iostorinv_id
WHERE d.storagevehicle_code = p.vehicle_code
AND h.io_type = '1'
AND h.deleted = 0
ORDER BY h.create_time DESC, h.iostorinv_id DESC
LIMIT 1)
) AS outbound_bill_id
FROM wms_point p
LEFT JOIN task_transport_job t
ON CAST(t.task_id AS CHAR) = p.ing_task_code
AND t.deleted = 0
AND t.biz_type = 'SHIPPING_AREA'
WHERE p.region_code = 'FH' AND p.is_used = 1
</sql>
<select id="selectSameBillRows"
resultType="cn.code.nl.module.wms.dal.dataobject.point.ShippingPointRowCandidate">
SELECT x.row_num
FROM (<include refid="shippingPointWithBill"/>) x
GROUP BY x.row_num
HAVING MAX(CASE WHEN (x.vehicle_code IS NOT NULL AND x.vehicle_code != '')
OR (x.ing_task_code IS NOT NULL AND x.ing_task_code != '')
THEN CASE WHEN x.outbound_bill_id = #{billId} THEN 1 ELSE 0 END
ELSE 0 END) = 1
AND MAX(CASE WHEN ((x.vehicle_code IS NOT NULL AND x.vehicle_code != '')
OR (x.ing_task_code IS NOT NULL AND x.ing_task_code != ''))
AND (x.outbound_bill_id IS NULL OR x.outbound_bill_id != #{billId})
THEN 1 ELSE 0 END) = 0
AND MAX(CASE WHEN (x.vehicle_code IS NULL OR x.vehicle_code = '')
AND (x.ing_task_code IS NULL OR x.ing_task_code = '')
THEN 1 ELSE 0 END) = 1
ORDER BY x.row_num
LIMIT 32
</select>
<select id="selectEmptyRows"
resultType="cn.code.nl.module.wms.dal.dataobject.point.ShippingPointRowCandidate">
SELECT row_num
FROM wms_point
WHERE region_code = 'FH' AND is_used = 1
GROUP BY row_num
HAVING MAX(CASE WHEN (vehicle_code IS NOT NULL AND vehicle_code != '')
OR (ing_task_code IS NOT NULL AND ing_task_code != '')
THEN 1 ELSE 0 END) = 0
ORDER BY row_num
LIMIT 32
</select>
<select id="selectFreePointInRow"
resultType="cn.code.nl.module.wms.dal.dataobject.point.WmsPointDO">
SELECT *
FROM wms_point
WHERE region_code = 'FH' AND is_used = 1 AND row_num = #{rowNum}
AND (vehicle_code IS NULL OR vehicle_code = '')
AND (ing_task_code IS NULL OR ing_task_code = '')
ORDER BY col_num, point_code
LIMIT 1
</select>
<update id="reserveShippingPoint">
UPDATE wms_point
SET ing_task_code = #{reservationCode}
WHERE point_code = #{pointCode} AND region_code = 'FH' AND is_used = 1
AND (vehicle_code IS NULL OR vehicle_code = '')
AND (ing_task_code IS NULL OR ing_task_code = '')
</update>
<update id="bindShippingTask">
UPDATE wms_point SET ing_task_code = #{taskId}
WHERE point_code = #{pointCode} AND region_code = 'FH'
AND ing_task_code = #{reservationCode}
</update>
<select id="selectShippingTaskPoint"
resultType="cn.code.nl.module.wms.dal.dataobject.point.WmsPointDO">
SELECT p.*, t.vehicle_code AS task_vehicle_code
FROM wms_point p
JOIN task_transport_job t
ON CAST(t.task_id AS CHAR) = p.ing_task_code
AND t.deleted = 0
AND t.biz_type = 'SHIPPING_AREA'
WHERE p.region_code = 'FH' AND p.ing_task_code = #{taskId}
</select>
<update id="completeShippingTask">
UPDATE wms_point
SET vehicle_code = #{boxNo}, vehicle_qty = 1, ing_task_code = NULL
WHERE region_code = 'FH' AND ing_task_code = #{taskId}
AND (vehicle_code IS NULL OR vehicle_code = '')
</update>
<update id="cancelShippingTask">
UPDATE wms_point SET ing_task_code = NULL
WHERE region_code = 'FH' AND ing_task_code = #{taskId}
</update>
</mapper>