Browse Source

跨site调拨

dev
常熟吴彦祖 3 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=登录大site,searchSite=可选小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=登录大site,excludeSite=来源小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=登录大site,searchSite=可选小site
* @return 标签分页结果
*/
IPage<CrossSiteHuTransferData> searchHandlingUnitList(Page<CrossSiteHuTransferData> page, @Param("query") CrossSiteHuTransferData query);
/**
* @Description 按登录大site查询可选小site,排除当前标签来源site - rqrq
* @param query site=登录大site,excludeSite=当前标签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=登录大site,searchSite=可选小site过滤;必须至少有一个业务查询条件
* @return page 分页结果,供前端表格展示
*/
PageUtils searchHandlingUnitList(CrossSiteHuTransferData query);
/**
* @author rqrq
* @Description 按登录大site查询可选目标小site,排除当前标签来源site - rqrq
* @param query site=登录大site,excludeSite=选中标签的来源小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=登录大site,searchSite=可选小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=Y、reserve_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=登录大site,excludeSite=来源小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使用小site:STO.site=fromSite,STI.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=来源小site,orderRef1互指入库单号,便于后续对账 - rqrq
TransHeader stoHeader = buildTransHeader(request.getFromSite(), stoTransNo, "STO",
warehouseId, stiTransNo, "跨小site调拨出库 " + request.getFromSite() + " -> " + request.getToSite(),
userId, userName);
transHeaderService.save(stoHeader);
// 再写入库头:site=目标小site,orderRef1互指出库单号 - 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=来源小site,direction=-,库位仍为标签当前库位 - rqrq
transDetailService.save(buildTransDetail(request.getFromSite(), stoTransNo, itemNo, firstHU,
totalQty, "-", firstHU.getLocationId()));
// 写入库明细:site=目标小site,direction=+,库位不变(本次只改site) - rqrq
transDetailService.save(buildTransDetail(request.getToSite(), stiTransNo, itemNo, firstHU,
totalQty, "+", firstHU.getLocationId()));
int seqNo = 1;
for (HandlingUnit hu : groupHUs) {
// 出库子明细按标签落 subNo=unitId,site用来源小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++;
}
// 单据写完后再改标签site;WHERE再次卡在库未预留,影响行数必须等于选中数,否则回滚 - 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_sub;0表示越权或录错,必须中断避免改到无关工厂 - 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必须是小site,status直接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_id;sub_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