Browse Source

跨site调拨

dev
常熟吴彦祖 2 weeks ago
parent
commit
a3ba1f7ac1
  1. 5
      src/main/java/com/gaotao/modules/autoWareHourceHaian/entity/vo/HaiAnSOIssueNotifyOrderMaterialListDetail.java
  2. 65
      src/main/java/com/gaotao/modules/warehouse/controller/CrossSiteHuTransferController.java
  3. 51
      src/main/java/com/gaotao/modules/warehouse/entity/CrossSiteHuTransferData.java
  4. 55
      src/main/java/com/gaotao/modules/warehouse/entity/CrossSiteHuTransferRequest.java
  5. 57
      src/main/java/com/gaotao/modules/warehouse/entity/CrossSiteHuTransferResult.java
  6. 27
      src/main/java/com/gaotao/modules/warehouse/entity/SiteSubOptionData.java
  7. 88
      src/main/java/com/gaotao/modules/warehouse/mapper/CrossSiteHuTransferMapper.java
  8. 41
      src/main/java/com/gaotao/modules/warehouse/service/CrossSiteHuTransferService.java
  9. 495
      src/main/java/com/gaotao/modules/warehouse/service/impl/CrossSiteHuTransferServiceImpl.java
  10. 185
      src/main/resources/mapper/warehouse/CrossSiteHuTransferMapper.xml

5
src/main/java/com/gaotao/modules/autoWareHourceHaian/entity/vo/HaiAnSOIssueNotifyOrderMaterialListDetail.java

@ -94,7 +94,10 @@ public class HaiAnSOIssueNotifyOrderMaterialListDetail {
private Integer allocSeq;
/**
* 栈板编码pallet_id - rqrq
* 领料栈板pallet_id
* <p>来源表字段haian_SOIssueNotifyOrderMaterialList_detail.pallet_id下达预留时写入对应本次领料占用的海安栈板号</p>
* <p>是否落库赋值时机海安领料匹配/下达写明细消费方查看领料申请单预留标签页签立库申请单完成情况预留标签清单表头展示为领料栈板</p>
* - rqrq
*/
private String palletId;
}

65
src/main/java/com/gaotao/modules/warehouse/controller/CrossSiteHuTransferController.java

@ -0,0 +1,65 @@
package com.gaotao.modules.warehouse.controller;
import com.gaotao.common.utils.PageUtils;
import com.gaotao.common.utils.R;
import com.gaotao.modules.warehouse.entity.CrossSiteHuTransferData;
import com.gaotao.modules.warehouse.entity.CrossSiteHuTransferRequest;
import com.gaotao.modules.warehouse.entity.CrossSiteHuTransferResult;
import com.gaotao.modules.warehouse.entity.SiteSubOptionData;
import com.gaotao.modules.warehouse.service.CrossSiteHuTransferService;
import org.springframework.beans.factory.annotation.Autowired;
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 java.util.List;
/**
* @author rqrq
* @Description 跨小site调拨控制器只负责收参调用Service返回统一R结构 - rqrq
* @date 2026/09/04
*/
@RestController
@RequestMapping("warehouse/crossSiteHuTransfer")
public class CrossSiteHuTransferController {
@Autowired
private CrossSiteHuTransferService crossSiteHuTransferService;
/**
* @author rqrq
* @Description 分页查询可跨小site调拨标签入参缺失site或无查询条件时由Service抛出业务异常 - rqrq
* @param query 查询对象site=登录大sitesearchSite=可选小site
* @return page 分页结果
*/
@PostMapping("/searchHandlingUnitList")
public R searchHandlingUnitList(@RequestBody CrossSiteHuTransferData query) {
PageUtils page = crossSiteHuTransferService.searchHandlingUnitList(query);
return R.ok().put("page", page);
}
/**
* @author rqrq
* @Description 查询目标小site下拉排除当前选中标签的来源site - rqrq
* @param query site=登录大siteexcludeSite=来源小site
* @return rows 目标小site列表
*/
@PostMapping("/listTargetSubSites")
public R listTargetSubSites(@RequestBody CrossSiteHuTransferData query) {
List<SiteSubOptionData> rows = crossSiteHuTransferService.listTargetSubSites(query);
return R.ok().put("rows", rows);
}
/**
* @author rqrq
* @Description 提交跨小site调拨二次校验后写STO/STI并改标签site - rqrq
* @param request 登录大site来源/目标小site操作人选中标签
* @return row 处理结果含STO/STI事务号
*/
@PostMapping("/transferHuSite")
public R transferHuSite(@RequestBody CrossSiteHuTransferRequest request) {
CrossSiteHuTransferResult result = crossSiteHuTransferService.transferHuSite(request);
return R.ok().put("row", result);
}
}

51
src/main/java/com/gaotao/modules/warehouse/entity/CrossSiteHuTransferData.java

@ -0,0 +1,51 @@
package com.gaotao.modules.warehouse.entity;
import com.gaotao.modules.warehouse.entity.vo.HandlingUnitQueryVo;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.ibatis.type.Alias;
/**
* @Description 跨小site调拨查询业务实体 - 用于分页查询可调拨标签 - rqrq
*
* <p><b>继承关系</b>继承 HandlingUnitQueryVo复用标签查询字段与分页参数</p>
*
* <p><b>用途</b></p>
* <ul>
* <li>跨小site调拨页查询入参/出参</li>
* <li>site 表示当前登录大site用于 site_sub 反查允许的小site集合</li>
* <li>searchSite 表示用户输入的标签小site过滤条件对应 handling_unit.site</li>
* </ul>
*
* <p><b>常用查询逻辑</b></p>
* <pre>
* WHERE h.in_stock_flag = 'Y'
* AND h.reserve_flag = 'N'
* AND h.site IN (SELECT ss.subsiteid FROM site_sub ss WHERE ss.siteid = #{query.site})
* AND h.site = #{query.searchSite} -- 仅当用户填写了site条件
* </pre>
*
* @author rqrq
* @date 2026/09/04
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Alias("CrossSiteHuTransferData")
public class CrossSiteHuTransferData extends HandlingUnitQueryVo {
/**
* 查询条件 - 标签小site精确查询对应 handling_unit.site
* <p>SQL示例AND h.site = #{query.searchSite}</p>
* <p>是否落库仅查询入参赋值时机前端筛选区输入消费方查询SQL</p>
* - rqrq
*/
private String searchSite;
/**
* 查询条件 - 需要排除的小site目标站点下拉用对应当前选中标签的site
* <p>SQL示例AND ss.subsiteid &lt;&gt; #{query.excludeSite}</p>
* <p>是否落库仅弹窗查询入参赋值时机点击跨site调拨后消费方目标site下拉SQL</p>
* - rqrq
*/
private String excludeSite;
}

55
src/main/java/com/gaotao/modules/warehouse/entity/CrossSiteHuTransferRequest.java

@ -0,0 +1,55 @@
package com.gaotao.modules.warehouse.entity;
import com.gaotao.modules.warehouse.entity.vo.HandlingUnitVo;
import lombok.Data;
import org.apache.ibatis.type.Alias;
import java.util.List;
/**
* @Description 跨小site调拨提交请求 - 承载登录大site来源/目标小site和选中标签 - rqrq
*
* <p><b>用途</b>前端确认目标site后提交调拨后台二次校验在库/未预留后先按来源小site写STO出库再按目标小site写STI入库最后改 handling_unit.site调拨外部接口后续接入</p>
*
* @author rqrq
* @date 2026/09/04
*/
@Data
@Alias("CrossSiteHuTransferRequest")
public class CrossSiteHuTransferRequest {
/**
* 当前登录大site来源前端 this.$store.state.user.site用于校验来源/目标小site都属于该大site
* <p>是否落库赋值时机前端提交消费方后端校验 site_sub 映射</p>
* - rqrq
*/
private String site;
/**
* 来源小site必须等于选中标签的 handling_unit.site且整批只能有一个来源
* <p>是否落库赋值时机前端根据选中行汇总消费方更新WHERE条件</p>
* - rqrq
*/
private String fromSite;
/**
* 目标小site必须来自 site_sub且不能等于 fromSite
* <p>是否落库写入 handling_unit.site出库单site用 fromSite入库单site用 toSite</p>
* - rqrq
*/
private String toSite;
/**
* 操作人来源前端登录用户名写入 modified_by
* <p>是否落库写入 handling_unit.modified_by</p>
* - rqrq
*/
private String createdBy;
/**
* 选中的标签列表至少1条仅使用 site+unitId 作为更新主键
* <p>是否落库仅作为待处理清单</p>
* - rqrq
*/
private List<HandlingUnitVo> selectedItems;
}

57
src/main/java/com/gaotao/modules/warehouse/entity/CrossSiteHuTransferResult.java

@ -0,0 +1,57 @@
package com.gaotao.modules.warehouse.entity;
import lombok.Data;
import org.apache.ibatis.type.Alias;
import java.util.ArrayList;
import java.util.List;
/**
* @Description 跨小site调拨处理结果 - 供前端弹窗展示成功/失败明细 - rqrq
*
* <p><b>数据来源</b>CrossSiteHuTransferServiceImpl.transferHuSite 汇总</p>
*
* @author rqrq
* @date 2026/09/04
*/
@Data
@Alias("CrossSiteHuTransferResult")
public class CrossSiteHuTransferResult {
/**
* 成功数量来源本次实际更新 handling_unit 的条数 - rqrq
*/
private int successCount;
/**
* 失败数量来源冲突校验或更新影响行数为0的标签数 - rqrq
*/
private int failCount;
/**
* 失败标签号列表前端用于展示失败序列号 - rqrq
*/
private List<String> failedUnitIds = new ArrayList<>();
/**
* 失败原因列表 failedUnitIds 顺序对应前端展示第一条即可定位 - rqrq
*/
private List<String> failedReasons = new ArrayList<>();
/**
* 处理结果摘要前端结果弹窗标题补充说明 - rqrq
*/
private String message;
/**
* 出库事务号来源登录大site的 transNoControl(STO)写入 trans_header.trans_no前端结果弹窗回显 - rqrq
* <p>是否落库对应 STO 出库单site=来源小site赋值时机校验通过后生成消费方前端结果展示后续调拨接口</p>
*/
private String stoTransNo;
/**
* 入库事务号来源登录大site的 transNoControl(STI)写入 trans_header.trans_no前端结果弹窗回显 - rqrq
* <p>是否落库对应 STI 入库单site=目标小site赋值时机校验通过后生成消费方前端结果展示后续调拨接口</p>
*/
private String stiTransNo;
}

27
src/main/java/com/gaotao/modules/warehouse/entity/SiteSubOptionData.java

@ -0,0 +1,27 @@
package com.gaotao.modules.warehouse.entity;
import lombok.Data;
import org.apache.ibatis.type.Alias;
/**
* @Description 跨小site调拨目标站点下拉结果 - 映射 site_sub - rqrq
*
* <p><b>数据来源</b>site_sub.siteid / site_sub.subsiteid</p>
*
* @author rqrq
* @date 2026/09/04
*/
@Data
@Alias("SiteSubOptionData")
public class SiteSubOptionData {
/**
* 大site编码来源 site_sub.siteid对应当前登录工厂 - rqrq
*/
private String siteId;
/**
* 小site编码来源 site_sub.subsiteid前端下拉展示并作为调拨目标 - rqrq
*/
private String subSiteId;
}

88
src/main/java/com/gaotao/modules/warehouse/mapper/CrossSiteHuTransferMapper.java

@ -0,0 +1,88 @@
package com.gaotao.modules.warehouse.mapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.gaotao.modules.handlingunit.entity.HandlingUnit;
import com.gaotao.modules.warehouse.entity.CrossSiteHuTransferData;
import com.gaotao.modules.warehouse.entity.CrossSiteHuTransferRequest;
import com.gaotao.modules.warehouse.entity.SiteSubOptionData;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* @Description 跨小site调拨数据访问层负责可调拨标签查询小site清单和site改写 - rqrq
*
* @author rqrq
* @date 2026/09/04
*/
@Mapper
public interface CrossSiteHuTransferMapper {
/**
* @Description 分页查询可跨小site调拨的在库未预留标签 - rqrq
* @param page 分页对象
* @param query 查询条件site=登录大sitesearchSite=可选小site
* @return 标签分页结果
*/
IPage<CrossSiteHuTransferData> searchHandlingUnitList(Page<CrossSiteHuTransferData> page, @Param("query") CrossSiteHuTransferData query);
/**
* @Description 按登录大site查询可选小site排除当前标签来源site - rqrq
* @param query site=登录大siteexcludeSite=当前标签site
* @return 小site列表
*/
List<SiteSubOptionData> listTargetSubSites(@Param("query") CrossSiteHuTransferData query);
/**
* @Description 校验小site是否属于当前登录大site的 site_sub 映射 - rqrq
* @param site 登录大site
* @param subSite 待校验小site
* @return 命中数量0表示映射不存在
*/
int countSiteSubMapping(@Param("site") String site, @Param("subSite") String subSite);
/**
* @Description 按来源小site+标签号加锁查询待调拨标签供二次校验预留/在库状态 - rqrq
* @param fromSite 来源小site对应 handling_unit.site
* @param unitIds 待调拨标签号
* @return 当前库内标签清单查不到的标签表示已被删除或site已变化
*/
List<HandlingUnit> listTransferHandlingUnits(@Param("fromSite") String fromSite, @Param("unitIds") List<String> unitIds);
/**
* @Description 查询目标小site下已存在的同号标签避免联合主键冲突 - rqrq
* @param toSite 目标小site
* @param unitIds 待调拨标签号
* @return 已存在的标签号
*/
List<String> listConflictUnitIds(@Param("toSite") String toSite, @Param("unitIds") List<String> unitIds);
/**
* @Description 将符合在库未预留条件的标签site改写到目标小site - rqrq
* @param request fromSite/toSite/createdBy
* @param unitIds 待更新标签号
* @return 实际更新行数
*/
int updateHandlingUnitSite(@Param("request") CrossSiteHuTransferRequest request, @Param("unitIds") List<String> unitIds);
/**
* @Description 查询海安托盘明细中是否存在本次调拨标签serial_no对应handling_unit.unit_id - rqrq
* @param site 登录大site对应 haian_pallet_detail.site
* @param unitIds 待调拨标签号
* @return 已存在明细的条码号空列表表示这些标签不在海安托盘上
*/
List<String> listHaiAnPalletDetailSerialNos(@Param("site") String site, @Param("unitIds") List<String> unitIds);
/**
* @Description 将海安托盘明细的sub_site改到目标小site与handling_unit.site保持一致 - rqrq
* @param request toSite
* @param site 登录大site对应 haian_pallet_detail.site
* @param unitIds 已确认存在明细的条码号
* @return 实际更新行数
*/
int updateHaiAnPalletDetailSubSite(@Param("request") CrossSiteHuTransferRequest request,
@Param("site") String site,
@Param("unitIds") List<String> unitIds);
}

41
src/main/java/com/gaotao/modules/warehouse/service/CrossSiteHuTransferService.java

@ -0,0 +1,41 @@
package com.gaotao.modules.warehouse.service;
import com.gaotao.common.utils.PageUtils;
import com.gaotao.modules.warehouse.entity.CrossSiteHuTransferData;
import com.gaotao.modules.warehouse.entity.CrossSiteHuTransferRequest;
import com.gaotao.modules.warehouse.entity.CrossSiteHuTransferResult;
import com.gaotao.modules.warehouse.entity.SiteSubOptionData;
import java.util.List;
/**
* @author rqrq
* @Description 跨小site调拨业务接口查询可调拨标签目标site下拉校验后写STO/STI并改标签site - rqrq
* @date 2026/09/04
*/
public interface CrossSiteHuTransferService {
/**
* @author rqrq
* @Description 分页查询可跨小site调拨的在库未预留标签 - rqrq
* @param query site=登录大sitesearchSite=可选小site过滤必须至少有一个业务查询条件
* @return page 分页结果供前端表格展示
*/
PageUtils searchHandlingUnitList(CrossSiteHuTransferData query);
/**
* @author rqrq
* @Description 按登录大site查询可选目标小site排除当前标签来源site - rqrq
* @param query site=登录大siteexcludeSite=选中标签的来源小site
* @return 目标小site下拉列表
*/
List<SiteSubOptionData> listTargetSubSites(CrossSiteHuTransferData query);
/**
* @author rqrq
* @Description 二次校验预留/在库后生成STO出库STI入库再改写标签site调拨外部接口当前留空 - rqrq
* @param request 登录大site来源/目标小site操作人选中标签
* @return 成功数量出库/入库事务号
*/
CrossSiteHuTransferResult transferHuSite(CrossSiteHuTransferRequest request);
}

495
src/main/java/com/gaotao/modules/warehouse/service/impl/CrossSiteHuTransferServiceImpl.java

@ -0,0 +1,495 @@
package com.gaotao.modules.warehouse.service.impl;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.gaotao.common.utils.PageUtils;
import com.gaotao.modules.handlingunit.entity.HandlingUnit;
import com.gaotao.modules.sys.entity.SysUserEntity;
import com.gaotao.modules.sys.service.SysUserService;
import com.gaotao.modules.trans.entity.TransDetail;
import com.gaotao.modules.trans.entity.TransDetailSub;
import com.gaotao.modules.trans.entity.TransHeader;
import com.gaotao.modules.trans.entity.TransNoControl;
import com.gaotao.modules.trans.service.TransDetailService;
import com.gaotao.modules.trans.service.TransDetailSubService;
import com.gaotao.modules.trans.service.TransHeaderService;
import com.gaotao.modules.trans.service.TransNoControlService;
import com.gaotao.modules.warehouse.entity.CrossSiteHuTransferData;
import com.gaotao.modules.warehouse.entity.CrossSiteHuTransferRequest;
import com.gaotao.modules.warehouse.entity.CrossSiteHuTransferResult;
import com.gaotao.modules.warehouse.entity.SiteSubOptionData;
import com.gaotao.modules.warehouse.entity.vo.HandlingUnitVo;
import com.gaotao.modules.warehouse.mapper.CrossSiteHuTransferMapper;
import com.gaotao.modules.warehouse.service.CrossSiteHuTransferService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author rqrq
* @Description 跨小site调拨实现查询限定大site下在库未预留标签提交时二次校验后先写STO再写STI再改标签site海安托盘明细有记录则同步sub_site - rqrq
* @date 2026/09/04
*/
@Slf4j
@Service
public class CrossSiteHuTransferServiceImpl implements CrossSiteHuTransferService {
@Autowired
private CrossSiteHuTransferMapper crossSiteHuTransferMapper;
@Autowired
private TransNoControlService transNoControlService;
@Autowired
private TransHeaderService transHeaderService;
@Autowired
private TransDetailService transDetailService;
@Autowired
private TransDetailSubService transDetailSubService;
@Autowired
private SysUserService sysUserService;
/**
* @author rqrq
* @Description 分页查询可调拨标签登录大site缺失无查询条件时直接失败避免全表扫描 - rqrq
* @param query site=登录大sitesearchSite=可选小site
* @return 分页结果
*/
@Override
public PageUtils searchHandlingUnitList(CrossSiteHuTransferData query) {
// 业务隔离必须使用请求中的登录大site禁止后端兜底登录态 - rqrq
if (query == null || !StringUtils.hasText(query.getSite())) {
throw new RuntimeException("登录工厂site不能为空");
}
// 至少填一个业务条件才能查防止无过滤把大site下全部在库标签拉出来 - rqrq
if (!hasAnySearchCondition(query)) {
throw new RuntimeException("请至少填写一个查询条件");
}
if (query.getPage() == null || query.getPage() < 1) {
query.setPage(1);
}
if (query.getLimit() == null || query.getLimit() < 1) {
query.setLimit(20);
}
// 用户填了小site时先校验属于当前大site避免查出无关工厂标签 - rqrq
if (StringUtils.hasText(query.getSearchSite())) {
validateSubSiteBelongToLoginSite(query.getSite(), query.getSearchSite(), "查询site");
}
log.info("开始查询跨小site调拨标签 - rqrq,loginSite={}, searchSite={}, unitId={}, partNo={}",
query.getSite(), query.getSearchSite(), query.getUnitId(), query.getPartNo());
// 调用分页查询SQL已固定 in_stock_flag=Yreserve_flag=N并限制 site_sub 范围失败则无列表可展示 - rqrq
Page<CrossSiteHuTransferData> page = new Page<>(query.getPage(), query.getLimit());
IPage<CrossSiteHuTransferData> pageResult = crossSiteHuTransferMapper.searchHandlingUnitList(page, query);
log.info("跨小site调拨标签查询完成 - rqrq,total={}", pageResult.getTotal());
return new PageUtils(pageResult);
}
/**
* @author rqrq
* @Description 查询目标小site下拉excludeSite用于排除当前选中标签的来源site - rqrq
* @param query site=登录大siteexcludeSite=来源小site
* @return 目标小site列表
*/
@Override
public List<SiteSubOptionData> listTargetSubSites(CrossSiteHuTransferData query) {
if (query == null || !StringUtils.hasText(query.getSite())) {
throw new RuntimeException("登录工厂site不能为空");
}
if (!StringUtils.hasText(query.getExcludeSite())) {
throw new RuntimeException("来源site不能为空");
}
// 来源site必须属于当前大site否则下拉结果会误导后续提交 - rqrq
validateSubSiteBelongToLoginSite(query.getSite(), query.getExcludeSite(), "来源site");
// 按大site查 site_sub排除来源site失败则弹窗无法选目标中断调拨 - rqrq
List<SiteSubOptionData> rows = crossSiteHuTransferMapper.listTargetSubSites(query);
return rows == null ? new ArrayList<>() : rows;
}
/**
* @author rqrq
* @Description 跨小site调拨主流程二次校验预留/在库 -> 生成STO出库 -> 生成STI入库 -> 改标签site -> 调拨接口留空 - rqrq
*
* <p>事务边界单据写入与标签site改写同一事务任一校验或写入失败整批回滚避免只出库不改site</p>
* <p>出入库记录site使用小siteSTO.site=fromSiteSTI.site=toSite</p>
*
* @param request 登录大site来源/目标小site操作人选中标签
* @return 成功数量与STO/STI事务号
*/
@Override
@Transactional(rollbackFor = Exception.class)
public CrossSiteHuTransferResult transferHuSite(CrossSiteHuTransferRequest request) {
// 先校验登录大site来源/目标归属和整批同一来源失败直接抛错不写任何单据 - rqrq
validateTransferRequest(request);
// 从选中行提取去重条码号空条码会失败避免后续IN条件无值 - rqrq
List<String> unitIds = collectUnitIds(request.getSelectedItems());
log.info("开始跨小site调拨 - rqrq,loginSite={}, fromSite={}, toSite={}, count={}",
request.getSite(), request.getFromSite(), request.getToSite(), unitIds.size());
// 加锁读取来源site下选中标签入参fromSite+unitIds失败或漏行则中断避免按过期列表写单 - rqrq
List<HandlingUnit> handlingUnits = crossSiteHuTransferMapper.listTransferHandlingUnits(request.getFromSite(), unitIds);
validateHandlingUnitStatus(unitIds, handlingUnits);
// 目标site已存在同号标签时禁止改联合主键避免唯一约束冲突导致半成品单据 - rqrq
List<String> conflictUnitIds = crossSiteHuTransferMapper.listConflictUnitIds(request.getToSite(), unitIds);
if (!CollectionUtils.isEmpty(conflictUnitIds)) {
throw new RuntimeException("目标site已存在相同条码,无法调拨:" + String.join(",", conflictUnitIds));
}
// 按操作人查用户档案写入出入库单 userId/userName查不到则仅用请求中的createdBy不阻断调拨 - rqrq
SysUserEntity operatorUser = null;
if (StringUtils.hasText(request.getCreatedBy())) {
operatorUser = sysUserService.queryByUserName(request.getCreatedBy());
}
String userId = operatorUser != null && operatorUser.getUserId() != null
? operatorUser.getUserId().toString() : "";
String userName = operatorUser != null && StringUtils.hasText(operatorUser.getUsername())
? operatorUser.getUsername() : request.getCreatedBy();
// 流水号按登录大site取号trans_no_control通常按大site维护单据site仍写小site取号失败则无法写单整批回滚 - rqrq
TransNoControl stoTransData = transNoControlService.getTransNo(request.getSite(), "STO", 10);
if (stoTransData == null || !StringUtils.hasText(stoTransData.getNewTransNo())) {
throw new RuntimeException("生成出库事务号失败,请检查当前工厂的STO流水配置:" + request.getSite());
}
String stoTransNo = stoTransData.getNewTransNo();
// STI流水同样按登录大site取号失败时STO号虽已占用独立事务但本事务回滚业务单据 - rqrq
TransNoControl stiTransData = transNoControlService.getTransNo(request.getSite(), "STI", 10);
if (stiTransData == null || !StringUtils.hasText(stiTransData.getNewTransNo())) {
throw new RuntimeException("生成入库事务号失败,请检查当前工厂的STI流水配置:" + request.getSite());
}
String stiTransNo = stiTransData.getNewTransNo();
String warehouseId = handlingUnits.get(0).getWarehouseId();
// 先写出库头site=来源小siteorderRef1互指入库单号便于后续对账 - rqrq
TransHeader stoHeader = buildTransHeader(request.getFromSite(), stoTransNo, "STO",
warehouseId, stiTransNo, "跨小site调拨出库 " + request.getFromSite() + " -> " + request.getToSite(),
userId, userName);
transHeaderService.save(stoHeader);
// 再写入库头site=目标小siteorderRef1互指出库单号 - rqrq
TransHeader stiHeader = buildTransHeader(request.getToSite(), stiTransNo, "STI",
warehouseId, stoTransNo, "跨小site调拨入库 " + request.getFromSite() + " -> " + request.getToSite(),
userId, userName);
transHeaderService.save(stiHeader);
// 按物料+批次+库位+WDR+工程变更级别汇总明细减少单据行数库位不改仅site变化 - rqrq
Map<String, List<HandlingUnit>> groupedHUs = handlingUnits.stream()
.collect(Collectors.groupingBy(hu ->
nvl(hu.getPartNo()) + "|" + nvl(hu.getBatchNo()) + "|" +
nvl(hu.getLocationId()) + "|" + nvl(hu.getWdr()) + "|" +
(StringUtils.hasText(hu.getEngChgLevel()) ? hu.getEngChgLevel() : "1")));
int itemNo = 1;
for (Map.Entry<String, List<HandlingUnit>> entry : groupedHUs.entrySet()) {
List<HandlingUnit> groupHUs = entry.getValue();
HandlingUnit firstHU = groupHUs.get(0);
BigDecimal totalQty = groupHUs.stream()
.map(hu -> hu.getQty() == null ? BigDecimal.ZERO : hu.getQty())
.reduce(BigDecimal.ZERO, BigDecimal::add);
// 写出库明细site=来源小sitedirection=-库位仍为标签当前库位 - rqrq
transDetailService.save(buildTransDetail(request.getFromSite(), stoTransNo, itemNo, firstHU,
totalQty, "-", firstHU.getLocationId()));
// 写入库明细site=目标小sitedirection=+库位不变本次只改site - rqrq
transDetailService.save(buildTransDetail(request.getToSite(), stiTransNo, itemNo, firstHU,
totalQty, "+", firstHU.getLocationId()));
int seqNo = 1;
for (HandlingUnit hu : groupHUs) {
// 出库子明细按标签落 subNo=unitIdsite用来源小site - rqrq
transDetailSubService.save(buildTransDetailSub(request.getFromSite(), stoTransNo, (double) itemNo,
seqNo, hu, "-"));
// 入库子明细同样按标签落行site用目标小site - rqrq
transDetailSubService.save(buildTransDetailSub(request.getToSite(), stiTransNo, (double) itemNo,
seqNo, hu, "+"));
seqNo++;
}
itemNo++;
}
// 单据写完后再改标签siteWHERE再次卡在库未预留影响行数必须等于选中数否则回滚 - rqrq
int updatedRows = crossSiteHuTransferMapper.updateHandlingUnitSite(request, unitIds);
if (updatedRows != unitIds.size()) {
throw new RuntimeException("更新标签site失败,期望更新" + unitIds.size() + "条,实际" + updatedRows
+ "条,可能标签已被预留或出库,请刷新后重试");
}
// 标签site改完后再查海安托盘明细有明细才改sub_site无明细视为普通仓标签直接跳过 - rqrq
syncHaiAnPalletDetailSubSite(request, unitIds);
// 调拨外部接口当前留空后续接入失败策略需与本事务一起评估避免先提交本地再调外部导致不一致 - rqrq
callCrossSiteTransferApi(request, handlingUnits, stoTransNo, stiTransNo);
CrossSiteHuTransferResult result = new CrossSiteHuTransferResult();
result.setSuccessCount(updatedRows);
result.setFailCount(0);
result.setStoTransNo(stoTransNo);
result.setStiTransNo(stiTransNo);
result.setMessage("跨site调拨完成,出库事务号:" + stoTransNo + ",入库事务号:" + stiTransNo);
log.info("跨小site调拨完成 - rqrq,fromSite={}, toSite={}, sto={}, sti={}, count={}",
request.getFromSite(), request.getToSite(), stoTransNo, stiTransNo, updatedRows);
return result;
}
/**
* @author rqrq
* @Description 校验提交参数登录大site来源/目标小site归属整批同一来源禁止同site调拨 - rqrq
* @param request 调拨请求
*/
private void validateTransferRequest(CrossSiteHuTransferRequest request) {
if (request == null) {
throw new RuntimeException("调拨请求不能为空");
}
if (!StringUtils.hasText(request.getSite())) {
throw new RuntimeException("登录工厂site不能为空");
}
if (!StringUtils.hasText(request.getFromSite())) {
throw new RuntimeException("来源site不能为空");
}
if (!StringUtils.hasText(request.getToSite())) {
throw new RuntimeException("目标site不能为空");
}
if (!StringUtils.hasText(request.getCreatedBy())) {
throw new RuntimeException("操作人不能为空");
}
if (request.getFromSite().equals(request.getToSite())) {
throw new RuntimeException("目标site不能与来源site相同");
}
if (CollectionUtils.isEmpty(request.getSelectedItems())) {
throw new RuntimeException("请先选择要调拨的标签");
}
// 前端可能漏传fromSite一致性这里以后端选中行再校验一遍避免跨多个小site混调 - rqrq
Set<String> selectedSites = request.getSelectedItems().stream()
.map(HandlingUnitVo::getSite)
.filter(StringUtils::hasText)
.collect(Collectors.toCollection(LinkedHashSet::new));
if (selectedSites.size() > 1) {
throw new RuntimeException("选中标签来自多个site,不允许一起调拨:" + String.join(",", selectedSites));
}
if (!selectedSites.isEmpty() && !selectedSites.contains(request.getFromSite())) {
throw new RuntimeException("来源site与选中标签site不一致");
}
// 来源/目标都必须落在当前登录大site的 site_sub 防止把标签调到无关工厂 - rqrq
validateSubSiteBelongToLoginSite(request.getSite(), request.getFromSite(), "来源site");
validateSubSiteBelongToLoginSite(request.getSite(), request.getToSite(), "目标site");
}
/**
* @author rqrq
* @Description 二次校验标签仍在库且未预留漏行已出库已预留均整批失败 - rqrq
* @param unitIds 前端提交的标签号
* @param handlingUnits 加锁查询到的当前库数据
*/
private void validateHandlingUnitStatus(List<String> unitIds, List<HandlingUnit> handlingUnits) {
if (CollectionUtils.isEmpty(handlingUnits)) {
throw new RuntimeException("未找到可调拨标签,请确认标签仍在来源site且在库未预留");
}
Map<String, HandlingUnit> huMap = handlingUnits.stream()
.collect(Collectors.toMap(HandlingUnit::getUnitId, hu -> hu, (a, b) -> a));
List<String> errors = new ArrayList<>();
for (String unitId : unitIds) {
HandlingUnit hu = huMap.get(unitId);
if (hu == null) {
errors.add(unitId + "不存在或不属于来源site");
continue;
}
if (!"Y".equals(hu.getInStockFlag())) {
errors.add(unitId + "不在库,当前在库状态=" + nvl(hu.getInStockFlag()));
}
if (!"N".equals(hu.getReserveFlag())) {
errors.add(unitId + "已被预留,当前预留标记=" + nvl(hu.getReserveFlag()));
}
}
if (!errors.isEmpty()) {
throw new RuntimeException("标签预留/在库校验失败:" + String.join(";", errors));
}
}
/**
* @author rqrq
* @Description 从选中行提取去重后的条码号空条码直接失败 - rqrq
* @param selectedItems 前端选中标签
* @return 去重后的unitId列表
*/
private List<String> collectUnitIds(List<HandlingUnitVo> selectedItems) {
List<String> unitIds = selectedItems.stream()
.map(HandlingUnitVo::getUnitId)
.filter(StringUtils::hasText)
.distinct()
.collect(Collectors.toList());
if (unitIds.isEmpty()) {
throw new RuntimeException("选中标签缺少条码号");
}
return unitIds;
}
/**
* @author rqrq
* @Description 校验小site是否属于登录大site的 site_sub 映射0条则拒绝 - rqrq
* @param loginSite 登录大site
* @param subSite 待校验小site
* @param fieldName 异常提示字段名
*/
private void validateSubSiteBelongToLoginSite(String loginSite, String subSite, String fieldName) {
// 按大site+小site查 site_sub0表示越权或录错必须中断避免改到无关工厂 - rqrq
int count = crossSiteHuTransferMapper.countSiteSubMapping(loginSite, subSite);
if (count <= 0) {
throw new RuntimeException(fieldName + "[" + subSite + "]不属于当前登录工厂[" + loginSite + "]");
}
}
/**
* @author rqrq
* @Description 判断是否至少填写一个业务查询条件含可选小site - rqrq
* @param query 查询对象
* @return true=允许查询
*/
private boolean hasAnySearchCondition(CrossSiteHuTransferData query) {
return StringUtils.hasText(query.getUnitId())
|| StringUtils.hasText(query.getPartNo())
|| StringUtils.hasText(query.getBatchNo())
|| StringUtils.hasText(query.getLocationId())
|| StringUtils.hasText(query.getWarehouseId())
|| StringUtils.hasText(query.getSearchSite());
}
/**
* @author rqrq
* @Description 组装出入库单头site必须是小sitestatus直接COMPLETED因为本地调拨同步完成 - rqrq
*/
private TransHeader buildTransHeader(String site, String transNo, String transTypeDb, String warehouseId,
String orderRef1, String remark, String userId, String userName) {
TransHeader header = new TransHeader();
header.setSite(site);
header.setTransNo(transNo);
header.setTransDate(Date.from(LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant()));
header.setTransTypeDb(transTypeDb);
header.setWarehouseId(warehouseId);
header.setUserId(userId);
header.setUserName(userName);
header.setRemark(remark);
header.setOrderRef1(orderRef1);
header.setStatus("COMPLETED");
header.setStatusDb("C");
header.setEnterDate(new Date());
header.setIfsFlag("N");
return header;
}
/**
* @author rqrq
* @Description 组装出入库明细site按出库/入库分别用来源或目标小site - rqrq
*/
private TransDetail buildTransDetail(String site, String transNo, int itemNo, HandlingUnit hu,
BigDecimal totalQty, String direction, String locationId) {
TransDetail detail = new TransDetail();
detail.setSite(site);
detail.setTransNo(transNo);
detail.setItemNo((double) itemNo);
detail.setPartNo(hu.getPartNo());
detail.setTransQty(totalQty);
detail.setBatchNo(hu.getBatchNo());
detail.setLocationId(locationId);
detail.setDirection(direction);
detail.setWdrNo(hu.getWdr());
detail.setEngChgLevel(hu.getEngChgLevel());
return detail;
}
/**
* @author rqrq
* @Description 组装出入库子明细一条标签一行subNo使用unitId便于反查 - rqrq
*/
private TransDetailSub buildTransDetailSub(String site, String transNo, Double itemNo, int seqNo,
HandlingUnit hu, String direction) {
TransDetailSub detailSub = new TransDetailSub();
detailSub.setSite(site);
detailSub.setTransNo(transNo);
detailSub.setItemNo(itemNo);
detailSub.setSubNo(hu.getUnitId());
detailSub.setSubQty(hu.getQty() == null ? 0D : hu.getQty().doubleValue());
detailSub.setDirection(direction);
detailSub.setSeqNo((double) seqNo);
detailSub.setPartNo(hu.getPartNo());
detailSub.setBatchNo(hu.getBatchNo());
detailSub.setLocationId(hu.getLocationId());
detailSub.setEngChgLevel(hu.getEngChgLevel());
return detailSub;
}
/**
* @author rqrq
* @Description 同步海安托盘明细sub_site先按条码查询是否存在明细没有则跳过有则改成目标小site - rqrq
*
* <p>haian_pallet_detail.serial_no 对应 handling_unit.unit_idsub_site 对应 handling_unit.site</p>
* <p>查不到明细不算失败因为跨site调拨页也会处理非海安托盘上的普通标签</p>
*
* @param request 含登录大site和目标小site
* @param unitIds 已改写handling_unit.site的标签号
*/
private void syncHaiAnPalletDetailSubSite(CrossSiteHuTransferRequest request, List<String> unitIds) {
// 按登录大site+条码查海安托盘明细失败则无法判断是否组盘中断并回滚以免HU与托盘明细site不一致 - rqrq
List<String> haiAnSerialNos = crossSiteHuTransferMapper.listHaiAnPalletDetailSerialNos(request.getSite(), unitIds);
if (CollectionUtils.isEmpty(haiAnSerialNos)) {
log.info("跨site调拨未命中海安托盘明细,跳过sub_site改写 - rqrq,loginSite={}, count={}",
request.getSite(), unitIds.size());
return;
}
// 仅改已存在的明细行sub_site改为目标小site与刚改完的handling_unit.site对齐 - rqrq
int haiAnUpdatedRows = crossSiteHuTransferMapper.updateHaiAnPalletDetailSubSite(request, request.getSite(), haiAnSerialNos);
log.info("跨site调拨已同步海安托盘明细sub_site - rqrq,loginSite={}, toSite={}, hit={}, updated={}",
request.getSite(), request.getToSite(), haiAnSerialNos.size(), haiAnUpdatedRows);
if (haiAnUpdatedRows <= 0) {
throw new RuntimeException("海安托盘明细已查到" + haiAnSerialNos.size() + "条标签,但更新sub_site影响行数为0,请刷新后重试");
}
}
/**
* @author rqrq
* @Description 调用跨site调拨外部接口的预留点当前不发请求只记日志后续补接口时在此接入 - rqrq
*
* <p>后续接入时需明确成功条件失败是否回滚本地STO/STI/标签site幂等键用哪张单据号</p>
*
* @param request 调拨请求
* @param handlingUnits 已校验通过的标签
* @param stoTransNo 出库事务号
* @param stiTransNo 入库事务号
*/
private void callCrossSiteTransferApi(CrossSiteHuTransferRequest request, List<HandlingUnit> handlingUnits,
String stoTransNo, String stiTransNo) {
log.info("跨site调拨外部接口暂未接入,本次仅完成本地STO/STI单据和标签site改写 - rqrq,fromSite={}, toSite={}, sto={}, sti={}, count={}",
request.getFromSite(), request.getToSite(), stoTransNo, stiTransNo,
handlingUnits == null ? 0 : handlingUnits.size());
}
/**
* @author rqrq
* @Description 空字符串转空避免分组key出现null导致NPE - rqrq
*/
private String nvl(String value) {
return value == null ? "" : value;
}
}

185
src/main/resources/mapper/warehouse/CrossSiteHuTransferMapper.xml

@ -0,0 +1,185 @@
<?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="com.gaotao.modules.warehouse.mapper.CrossSiteHuTransferMapper">
<!-- rqrq - 分页查询可跨小site调拨标签:固定在库且未预留,site限定在当前登录大site的 site_sub 范围内 -->
<select id="searchHandlingUnitList" parameterType="CrossSiteHuTransferData" resultType="CrossSiteHuTransferData">
SELECT
h.unit_id AS unitId,
h.site,
h.parent_unit_id AS parentUnitId,
h.unit_type AS unitType,
h.unit_type_db AS unitTypeDb,
h.part_no AS partNo,
h.part_desc AS partDesc,
h.qty,
h.batch_no AS batchNo,
h.location_id AS locationId,
h.warehouse_id AS warehouseId,
h.wdr,
h.availability_control_id AS availabilityControlId,
h.status,
h.status_db AS statusDb,
h.freeze_flag AS freezeFlag,
h.merged_flag AS mergedFlag,
h.in_stock_flag AS inStockFlag,
h.order_ref1 AS orderRef1,
h.order_ref2 AS orderRef2,
h.order_ref3 AS orderRef3,
h.supplier_id AS supplierId,
h.customer_id AS customerId,
h.manufacture_date AS manufactureDate,
h.expired_date AS expiredDate,
h.created_date AS createdDate,
h.created_by AS createdBy,
h.modified_date AS modifiedDate,
h.modified_by AS modifiedBy,
h.remark,
h.source_type AS sourceType,
h.source_ref AS sourceRef,
h.gross_weight AS grossWeight,
h.net_weight AS netWeight,
h.weight_unit AS weightUnit,
h.volume,
h.volume_unit AS volumeUnit,
h.qr_code AS qrCode,
h.bar_code AS barCode,
h.printer_name AS printerName,
h.last_print_date AS lastPrintDate,
h.print_count AS printCount,
h.receive_date AS receiveDate,
h.original_qty AS originalQty,
h.tid,
h.epc,
h.length,
h.width,
h.um_id AS umId,
h.reserve_flag AS reserveFlag,
h.reserve_order_ref1 AS reserveOrderRef1,
h.reserve_order_ref2 AS reserveOrderRef2,
h.reserve_order_ref3 AS reserveOrderRef3,
h.height,
h.eng_chg_level AS engChgLevel
FROM handling_unit h WITH (NOLOCK)
WHERE h.in_stock_flag = 'Y'
AND h.reserve_flag = 'N'
AND h.site IN (
SELECT ss.subsiteid
FROM site_sub ss WITH (NOLOCK)
WHERE ss.siteid = #{query.site}
)
<if test="query.searchSite != null and query.searchSite != ''">
AND h.site = #{query.searchSite}
</if>
<if test="query.unitId != null and query.unitId != ''">
AND h.unit_id LIKE '%' + #{query.unitId} + '%'
</if>
<if test="query.partNo != null and query.partNo != ''">
AND h.part_no = #{query.partNo}
</if>
<if test="query.batchNo != null and query.batchNo != ''">
AND h.batch_no LIKE '%' + #{query.batchNo} + '%'
</if>
<if test="query.locationId != null and query.locationId != ''">
AND h.location_id LIKE '%' + #{query.locationId} + '%'
</if>
<if test="query.warehouseId != null and query.warehouseId != ''">
AND h.warehouse_id = #{query.warehouseId}
</if>
ORDER BY h.created_date DESC
</select>
<!-- rqrq - 按登录大site查询可选目标小site,排除当前标签来源site;NOLOCK仅用于下拉展示,允许脏读 -->
<select id="listTargetSubSites" parameterType="CrossSiteHuTransferData" resultType="SiteSubOptionData">
SELECT
ss.siteid AS siteId,
ss.subsiteid AS subSiteId
FROM site_sub ss WITH (NOLOCK)
WHERE ss.siteid = #{query.site}
<if test="query.excludeSite != null and query.excludeSite != ''">
AND ss.subsiteid &lt;&gt; #{query.excludeSite}
</if>
ORDER BY ss.subsiteid
</select>
<!-- rqrq - 校验小site是否属于当前登录大site;用于拒绝越权调拨 -->
<select id="countSiteSubMapping" resultType="int">
SELECT COUNT(1)
FROM site_sub WITH (NOLOCK)
WHERE siteid = #{site}
AND subsiteid = #{subSite}
</select>
<!-- rqrq - 加锁读取待调拨标签,避免并发改预留/出库后仍按旧状态写单 -->
<select id="listTransferHandlingUnits" resultType="com.gaotao.modules.handlingunit.entity.HandlingUnit">
SELECT
h.unit_id,
h.site,
h.part_no,
h.part_desc,
h.qty,
h.batch_no,
h.location_id,
h.warehouse_id,
h.wdr,
h.eng_chg_level,
h.in_stock_flag,
h.reserve_flag
FROM handling_unit h WITH (UPDLOCK, ROWLOCK)
WHERE h.site = #{fromSite}
AND h.unit_id IN
<foreach collection="unitIds" item="unitId" open="(" separator="," close=")">
#{unitId}
</foreach>
</select>
<!-- rqrq - 目标小site已存在同 unit_id 时禁止改主键,避免联合主键冲突 -->
<select id="listConflictUnitIds" resultType="java.lang.String">
SELECT h.unit_id
FROM handling_unit h WITH (NOLOCK)
WHERE h.site = #{toSite}
AND h.unit_id IN
<foreach collection="unitIds" item="unitId" open="(" separator="," close=")">
#{unitId}
</foreach>
</select>
<!-- rqrq - 仅改写标签site;WHERE再次限定在库且未预留,防止校验后状态被并发改写 -->
<update id="updateHandlingUnitSite">
UPDATE handling_unit WITH (ROWLOCK)
SET site = #{request.toSite},
modified_by = #{request.createdBy},
modified_date = GETDATE()
WHERE site = #{request.fromSite}
AND in_stock_flag = 'Y'
AND reserve_flag = 'N'
AND unit_id IN
<foreach collection="unitIds" item="unitId" open="(" separator="," close=")">
#{unitId}
</foreach>
</update>
<!-- rqrq - 按大site+条码查海安托盘明细,确认标签是否组在海安托盘上;无记录则跳过sub_site改写 -->
<select id="listHaiAnPalletDetailSerialNos" resultType="java.lang.String">
SELECT DISTINCT d.serial_no
FROM haian_pallet_detail d WITH (NOLOCK)
WHERE d.site = #{site}
AND d.serial_no IN
<foreach collection="unitIds" item="unitId" open="(" separator="," close=")">
#{unitId}
</foreach>
</select>
<!-- rqrq - 仅改已存在的海安托盘明细sub_site;site用登录大site隔离,serial_no对应标签号 -->
<update id="updateHaiAnPalletDetailSubSite">
UPDATE haian_pallet_detail WITH (ROWLOCK)
SET sub_site = #{request.toSite}
WHERE site = #{site}
AND serial_no IN
<foreach collection="unitIds" item="unitId" open="(" separator="," close=")">
#{unitId}
</foreach>
</update>
</mapper>
Loading…
Cancel
Save