diff --git a/src/main/java/com/gaotao/modules/autoWareHourceHaian/entity/vo/HaiAnSOIssueNotifyOrderMaterialListDetail.java b/src/main/java/com/gaotao/modules/autoWareHourceHaian/entity/vo/HaiAnSOIssueNotifyOrderMaterialListDetail.java index 7977cb3f..fb827b41 100644 --- a/src/main/java/com/gaotao/modules/autoWareHourceHaian/entity/vo/HaiAnSOIssueNotifyOrderMaterialListDetail.java +++ b/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) + *

来源表字段:haian_SOIssueNotifyOrderMaterialList_detail.pallet_id;下达预留时写入,对应本次领料占用的海安栈板号

+ *

是否落库:是;赋值时机:海安领料匹配/下达写明细;消费方:查看领料申请单预留标签页签、立库申请单完成情况预留标签清单,表头展示为「领料栈板」

+ * - rqrq */ private String palletId; } diff --git a/src/main/java/com/gaotao/modules/warehouse/controller/CrossSiteHuTransferController.java b/src/main/java/com/gaotao/modules/warehouse/controller/CrossSiteHuTransferController.java new file mode 100644 index 00000000..7798b510 --- /dev/null +++ b/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 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); + } +} diff --git a/src/main/java/com/gaotao/modules/warehouse/entity/CrossSiteHuTransferData.java b/src/main/java/com/gaotao/modules/warehouse/entity/CrossSiteHuTransferData.java new file mode 100644 index 00000000..a177f748 --- /dev/null +++ b/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 + * + *

继承关系:继承 HandlingUnitQueryVo,复用标签查询字段与分页参数

+ * + *

用途:

+ * + * + *

常用查询逻辑:

+ *
+ * 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条件
+ * 
+ * + * @author rqrq + * @date 2026/09/04 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Alias("CrossSiteHuTransferData") +public class CrossSiteHuTransferData extends HandlingUnitQueryVo { + + /** + * 查询条件 - 标签小site(精确查询,对应 handling_unit.site) + *

SQL示例:AND h.site = #{query.searchSite}

+ *

是否落库:否,仅查询入参;赋值时机:前端筛选区输入;消费方:查询SQL

+ * - rqrq + */ + private String searchSite; + + /** + * 查询条件 - 需要排除的小site(目标站点下拉用,对应当前选中标签的site) + *

SQL示例:AND ss.subsiteid <> #{query.excludeSite}

+ *

是否落库:否,仅弹窗查询入参;赋值时机:点击跨site调拨后;消费方:目标site下拉SQL

+ * - rqrq + */ + private String excludeSite; +} diff --git a/src/main/java/com/gaotao/modules/warehouse/entity/CrossSiteHuTransferRequest.java b/src/main/java/com/gaotao/modules/warehouse/entity/CrossSiteHuTransferRequest.java new file mode 100644 index 00000000..5ac67a3f --- /dev/null +++ b/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 + * + *

用途:前端确认目标site后提交调拨。后台二次校验在库/未预留后,先按来源小site写STO出库,再按目标小site写STI入库,最后改 handling_unit.site。调拨外部接口后续接入。

+ * + * @author rqrq + * @date 2026/09/04 + */ +@Data +@Alias("CrossSiteHuTransferRequest") +public class CrossSiteHuTransferRequest { + + /** + * 当前登录大site,来源前端 this.$store.state.user.site,用于校验来源/目标小site都属于该大site + *

是否落库:否;赋值时机:前端提交;消费方:后端校验 site_sub 映射

+ * - rqrq + */ + private String site; + + /** + * 来源小site,必须等于选中标签的 handling_unit.site,且整批只能有一个来源 + *

是否落库:否;赋值时机:前端根据选中行汇总;消费方:更新WHERE条件

+ * - rqrq + */ + private String fromSite; + + /** + * 目标小site,必须来自 site_sub,且不能等于 fromSite + *

是否落库:是,写入 handling_unit.site;出库单site用 fromSite,入库单site用 toSite

+ * - rqrq + */ + private String toSite; + + /** + * 操作人,来源前端登录用户名,写入 modified_by + *

是否落库:是,写入 handling_unit.modified_by

+ * - rqrq + */ + private String createdBy; + + /** + * 选中的标签列表,至少1条;仅使用 site+unitId 作为更新主键 + *

是否落库:否,仅作为待处理清单

+ * - rqrq + */ + private List selectedItems; +} diff --git a/src/main/java/com/gaotao/modules/warehouse/entity/CrossSiteHuTransferResult.java b/src/main/java/com/gaotao/modules/warehouse/entity/CrossSiteHuTransferResult.java new file mode 100644 index 00000000..68298471 --- /dev/null +++ b/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 + * + *

数据来源:CrossSiteHuTransferServiceImpl.transferHuSite 汇总

+ * + * @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 failedUnitIds = new ArrayList<>(); + + /** + * 失败原因列表,与 failedUnitIds 顺序对应,前端展示第一条即可定位 - rqrq + */ + private List failedReasons = new ArrayList<>(); + + /** + * 处理结果摘要,前端结果弹窗标题补充说明 - rqrq + */ + private String message; + + /** + * 出库事务号,来源登录大site的 transNoControl(STO),写入 trans_header.trans_no;前端结果弹窗回显 - rqrq + *

是否落库:是,对应 STO 出库单(site=来源小site);赋值时机:校验通过后生成;消费方:前端结果展示、后续调拨接口

+ */ + private String stoTransNo; + + /** + * 入库事务号,来源登录大site的 transNoControl(STI),写入 trans_header.trans_no;前端结果弹窗回显 - rqrq + *

是否落库:是,对应 STI 入库单(site=目标小site);赋值时机:校验通过后生成;消费方:前端结果展示、后续调拨接口

+ */ + private String stiTransNo; +} diff --git a/src/main/java/com/gaotao/modules/warehouse/entity/SiteSubOptionData.java b/src/main/java/com/gaotao/modules/warehouse/entity/SiteSubOptionData.java new file mode 100644 index 00000000..c1404e9c --- /dev/null +++ b/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 + * + *

数据来源:site_sub.siteid / site_sub.subsiteid

+ * + * @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; +} diff --git a/src/main/java/com/gaotao/modules/warehouse/mapper/CrossSiteHuTransferMapper.java b/src/main/java/com/gaotao/modules/warehouse/mapper/CrossSiteHuTransferMapper.java new file mode 100644 index 00000000..27fc9ea7 --- /dev/null +++ b/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 searchHandlingUnitList(Page page, @Param("query") CrossSiteHuTransferData query); + + /** + * @Description 按登录大site查询可选小site,排除当前标签来源site - rqrq + * @param query site=登录大site,excludeSite=当前标签site + * @return 小site列表 + */ + List 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 listTransferHandlingUnits(@Param("fromSite") String fromSite, @Param("unitIds") List unitIds); + + /** + * @Description 查询目标小site下已存在的同号标签,避免联合主键冲突 - rqrq + * @param toSite 目标小site + * @param unitIds 待调拨标签号 + * @return 已存在的标签号 + */ + List listConflictUnitIds(@Param("toSite") String toSite, @Param("unitIds") List unitIds); + + /** + * @Description 将符合在库未预留条件的标签site改写到目标小site - rqrq + * @param request 含 fromSite/toSite/createdBy + * @param unitIds 待更新标签号 + * @return 实际更新行数 + */ + int updateHandlingUnitSite(@Param("request") CrossSiteHuTransferRequest request, @Param("unitIds") List unitIds); + + /** + * @Description 查询海安托盘明细中是否存在本次调拨标签,serial_no对应handling_unit.unit_id - rqrq + * @param site 登录大site,对应 haian_pallet_detail.site + * @param unitIds 待调拨标签号 + * @return 已存在明细的条码号;空列表表示这些标签不在海安托盘上 + */ + List listHaiAnPalletDetailSerialNos(@Param("site") String site, @Param("unitIds") List 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 unitIds); +} diff --git a/src/main/java/com/gaotao/modules/warehouse/service/CrossSiteHuTransferService.java b/src/main/java/com/gaotao/modules/warehouse/service/CrossSiteHuTransferService.java new file mode 100644 index 00000000..bbff02f4 --- /dev/null +++ b/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 listTargetSubSites(CrossSiteHuTransferData query); + + /** + * @author rqrq + * @Description 二次校验预留/在库后生成STO出库、STI入库,再改写标签site;调拨外部接口当前留空 - rqrq + * @param request 登录大site、来源/目标小site、操作人、选中标签 + * @return 成功数量、出库/入库事务号 + */ + CrossSiteHuTransferResult transferHuSite(CrossSiteHuTransferRequest request); +} diff --git a/src/main/java/com/gaotao/modules/warehouse/service/impl/CrossSiteHuTransferServiceImpl.java b/src/main/java/com/gaotao/modules/warehouse/service/impl/CrossSiteHuTransferServiceImpl.java new file mode 100644 index 00000000..39fb4867 --- /dev/null +++ b/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 page = new Page<>(query.getPage(), query.getLimit()); + IPage 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 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 rows = crossSiteHuTransferMapper.listTargetSubSites(query); + return rows == null ? new ArrayList<>() : rows; + } + + /** + * @author rqrq + * @Description 跨小site调拨主流程:二次校验预留/在库 -> 生成STO出库 -> 生成STI入库 -> 改标签site -> 调拨接口留空 - rqrq + * + *

事务边界:单据写入与标签site改写同一事务;任一校验或写入失败整批回滚,避免只出库不改site。

+ *

出入库记录site使用小site:STO.site=fromSite,STI.site=toSite。

+ * + * @param request 登录大site、来源/目标小site、操作人、选中标签 + * @return 成功数量与STO/STI事务号 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public CrossSiteHuTransferResult transferHuSite(CrossSiteHuTransferRequest request) { + // 先校验登录大site、来源/目标归属和整批同一来源;失败直接抛错,不写任何单据 - rqrq + validateTransferRequest(request); + + // 从选中行提取去重条码号,空条码会失败,避免后续IN条件无值 - rqrq + List 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 handlingUnits = crossSiteHuTransferMapper.listTransferHandlingUnits(request.getFromSite(), unitIds); + validateHandlingUnitStatus(unitIds, handlingUnits); + + // 目标site已存在同号标签时禁止改联合主键,避免唯一约束冲突导致半成品单据 - rqrq + List 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> 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> entry : groupedHUs.entrySet()) { + List 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 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 unitIds, List handlingUnits) { + if (CollectionUtils.isEmpty(handlingUnits)) { + throw new RuntimeException("未找到可调拨标签,请确认标签仍在来源site且在库未预留"); + } + Map huMap = handlingUnits.stream() + .collect(Collectors.toMap(HandlingUnit::getUnitId, hu -> hu, (a, b) -> a)); + List 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 collectUnitIds(List selectedItems) { + List 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 + * + *

haian_pallet_detail.serial_no 对应 handling_unit.unit_id;sub_site 对应 handling_unit.site。

+ *

查不到明细不算失败,因为跨site调拨页也会处理非海安托盘上的普通标签。

+ * + * @param request 含登录大site和目标小site + * @param unitIds 已改写handling_unit.site的标签号 + */ + private void syncHaiAnPalletDetailSubSite(CrossSiteHuTransferRequest request, List unitIds) { + // 按登录大site+条码查海安托盘明细;失败则无法判断是否组盘,中断并回滚以免HU与托盘明细site不一致 - rqrq + List 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 + * + *

后续接入时需明确:成功条件、失败是否回滚本地STO/STI/标签site、幂等键用哪张单据号。

+ * + * @param request 调拨请求 + * @param handlingUnits 已校验通过的标签 + * @param stoTransNo 出库事务号 + * @param stiTransNo 入库事务号 + */ + private void callCrossSiteTransferApi(CrossSiteHuTransferRequest request, List 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; + } +} diff --git a/src/main/resources/mapper/warehouse/CrossSiteHuTransferMapper.xml b/src/main/resources/mapper/warehouse/CrossSiteHuTransferMapper.xml new file mode 100644 index 00000000..d21b821d --- /dev/null +++ b/src/main/resources/mapper/warehouse/CrossSiteHuTransferMapper.xml @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + 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 + + #{unitId} + + + + + + + + + UPDATE haian_pallet_detail WITH (ROWLOCK) + SET sub_site = #{request.toSite} + WHERE site = #{site} + AND serial_no IN + + #{unitId} + + + +