31 Commits
Author SHA1 Message Date
rcx 50423b4e4b feat(oms,wms): 入库/出库业务单执行单与WMS出入库各页面列表返回计划数量与实际数量总和(全量不分页合计)
- OMS入库业务单/执行单: list/list1返回totalPlanQuantity/totalInQuantity(与行quantity/in_quantity列同源)
- WMS入库/收货/上架: 返回totalPlanQuantity/totalInQuantity(上架计划数量=明细应上架量; 入库数量=收货/上架数量, 与行列同口径)
- WMS出库/拣货/复核/交接: 返回totalPlanQuantity/totalOutboundQuantity(各页实际量列同口径; 复核明细聚合不滤del_flag兼容已反审tab)
- 合计SQL与queryList同条件片段/同JOIN结构, 分页查询后同一DO查询, 响应与total同层级追加字段, 前端纯增量兼容
2026-09-14 18:02:57 +08:00
rcx e59173b3f9 feat(oms): 出库业务单/出库执行单列表计划数量与出库数量分开计算, 返回全量不分页的计划数量总数/出库数量总数
- 新增queryListTotal: 复用queryList同条件片段, LEFT JOIN一级明细汇总SUM(quantity)/SUM(outbound_quantity), 计划数量与出库数量分开计算
- list/list1响应在data/total/code/msg同层级追加totalPlanQuantity/totalOutboundQuantity(与出库排名页合计行模式一致, 前端兼容)
- 执行单列表顺带修复每行totalOutboundQuantity恒为null(补关联子查询+resultMap映射, 对齐业务单178b7697口径)
2026-09-14 16:24:53 +08:00
rcx b1d92fdda8 feat(oms,wms,system): OMS下单冻结库存(防超卖)——仓库级库存锁定模式
- 仓库新增stock_lock_mode开关(1-下单冻结 2-分配库存冻结默认),查询/独立切换接口+在途冻结单切换守卫
- WMS新增out_order_freeze_record冻结记录表:下单冻结/编辑原子重锁/取消解冻/分配承接/交接扣减/反审还原全状态机,条件更新防并发超卖
- OMS下单即冻结(失败阻断+落库失败补偿解冻)、已下发禁编辑、cancelIssueOrder补调WMS清理(修存量不同步)、cancelAuditState镜像还原修复
- 达梦SQL脚本3个(WAREHOUSE加列/冻结记录建表/主键列名修复),测试库已执行并全流程实测通过
2026-09-14 15:41:32 +08:00
rcx 888a293ebd feat(oms): 出/入仓委托单新增导入接口(委托单模板与W出入库管理导入一致)
出仓: POST /outEntrustOrderApi/import, EasyExcel两遍解析(前11行表头+第12行起明细), 客户名称解析货主, 明细按料号>SKU>品名匹配货主物料档案(料号不存在/规格/SKU不一致报错), 柜号/运输方式/监管方式字典转码, 校验通过复用新增链路落库(生成CKWT单号, 待审核/未下发); 入仓: POST /reservationStockInOrderApi/importData, 恢复原注释W式导入并按现行类型重写——POI标签扫描前12行表头+ExcelUtil读13行列名明细, 按委托编号NO分组建单, 料号必填且须在该货主下存在, 规格/SKU与档案一致性校验, 数量空行/容器行跳过, 计划数量=入库数量之和, 订单来源=数据导入; OutEntrustOrderMapper新增跨库物料直查(NGWL_TEST_WMS.MATERIAL_BASE_INFO, 货主+一级组织过滤)
2026-09-14 13:55:10 +08:00
rcx e60f22979c fix(wms): 补提交收货/上架单已反审归档详情接口(此前遗漏未入库)
getInfo 增加 archive 重载(归档主单已软删用getById放行+校验已反审标记,明细放行软删行)+ getReverseAuditInfo 接口,补齐对接文档承诺的4个归档详情接口中缺失的收货/上架两个;随附上架默认库位注释微调
2026-09-12 11:21:33 +08:00
rcx 84ff45b43d fix(oms): 批量审核运输业务单号重复(goods_number取号三层防线)
根因: 取号为「COUNT当日单量+1+查重后移」, 非原子、无锁、无唯一索引。
批量审核并发创建运输业务单时, 多请求在同一查重窗口取到同一序号并全部
插入成功, 且将同一单号回填到出库单business_number, 页面所见单号重复。

- GoodsNumberGenerator: 取号改为当日MAX+1+存在性校验后移; 按yyMMdd分段的
  JVM锁串行化"取号+插单"; 插单后自检同号自动改号自愈; 兼容唯一索引撞号重试
- BusinessDocumentOrderDomainService: 主创建入口改走原子"取号+插单",
  出库/入库单号回填移至插单成功之后
- BusinessDocumentOrderImpl: 审核拆单子单路径同改
- Mapper: countByGoodsNumberPrefix改为selectGoodsNumbersByPrefix
2026-09-12 10:09:11 +08:00
rcx 892cc30999 fix(oms): 出入库业务单下发执行单时订单来源继承业务单(数据导入/手动创建/系统生成)不再写死"客户预约"; 手动创建业务单来源为空兜底"手动创建"; 出库下发明细过滤del_flag, 修复编辑后软删旧明细参与仓库分组导致多生成执行单且数量叠加 2026-09-12 10:00:45 +08:00
rcx da76816533 fix(oms): 反审运输链路按拆单子单全量软删——执行单挂子单下(businessDocumentId=子单id), 主单+子单+执行单+TMS链全删; 执行单列表默认过滤归档行 2026-09-12 09:53:13 +08:00
rcx b86ab75dcf fix(oms,wms): WMS出库明细uniqueId撞键修复+推单返回值校验
- WMS反审/取消下发软删出库明细时置空uniqueId(表唯一约束不含del_flag)
- omsOrderAddDetail推单前自愈: 按本次推送uniqueId值跨执行单清归档行, 反审重发不再撞键
- OMS推单三处(单头/明细/配送明细)校验返回值, 失败报WMS真实原因, 不再静默吞掉后伪装成可用库存为0
- WmsServiceFeign三方法void改AjaxResult, 控制器与降级工厂同步
2026-09-11 16:07:21 +08:00
rcx 796bb99a8b fix(wms): 出库异常列表不再默认过滤已软删出库单, 反审生成的异常记录可见 2026-09-11 14:28:55 +08:00
rcx f9a50ae353 fix(oms): 出库反审核心改动(下发自愈/运输链路软删/issueStatus已驳回/uniqueId置空/仓库兜底切换) 2026-09-11 14:17:17 +08:00
rcx 2094ea31cd fix(oms): 仓库信息本地兜底查询去掉STOCK_LOCK_MODE列, 兜底不依赖冻结功能列, 修复无该列环境的下发报错 2026-09-11 13:52:28 +08:00
rcx 788bae9a16 fix(oms,wms): 出库反审补全——重新下发自愈/仓库信息本地兜底/运输链路反审联动/归档查询修复
- 反审后 issueStatus 置 3-已驳回(与入库侧同口径), 清空执行单号/货源单号
- 反审/取消下发软删执行明细时置空 uniqueId, 修复重新下发撞唯一约束
- 新增 WarehouseInfoApplicationService(Feign优先+本地跨库兜底), 出库侧8处调用点接入
- 反审联动软删运输链路: 运输业务单/执行单本地软删 + TMS deleteChainByExecuteOrderNumber
- WMS归档查询修复: 交接归档详情丢行/拣货归档数量恒0/复核列表归档汇总/OMS执行单已反审tab
2026-09-11 11:42:42 +08:00
rcx a65cca1cfa fix(oms,wms): 入库反审三项修复:反审记录不可见、反审后重新下发无法收货、异常列表JOIN过滤
1. 入库异常三个生成器(收货异常/上架异常/反审记录)插入时未显式设置delFlag,MyBatis-Plus insert不含该列,列无默认值落库为NULL,被列表del_flag=1过滤导致反审记录永远不可见;现显式setDelFlag(1)

2. 反审重置业务单status=1:首轮完成上架经WMS回传把业务单status写成5(已入库),不重置则重新下发时copy给执行单→W入库单→收货单,收货单带已完成状态导致前端不给收货按钮

3. 入库异常列表LEFT JOIN stock_in_order固定b.DEL_FLAG=1,反审记录关联的入库单已软删导致匹配不上,仓库等基于b表的过滤把行全部过滤掉、列表恒为空;改为按a.TYPE切换关联状态(type=3关联已软删入库单);收货/上架已反审tab同号多张入库单JOIN出重复行,限定只关联同状态最新一张(IN_ORDER_ID取max)
2026-09-11 09:41:21 +08:00
rcx cae62e83a5 fix(oms,wms): 入库反审修复:已反审tab列表查不到数据、未下发单反审补写反审记录
1. W收货单/W上架单【已反审】tab查不到数据:queryList 的 LEFT JOIN stock_in_order 固定带 a/b.DEL_FLAG=1,归档数据(反审后双方均已软删del_flag=2)关联不上b表,仓库/货主等基于b表的过滤条件把行全部过滤掉;改为按是否查反审归档动态切换关联条件(归档按del_flag=2关联,正常列表行为不变)

2. 未下发WMS的执行单反审不产生反审记录:reverseExecutionOrder 幂等跳过分支直接return导致入库异常无留痕;现补写1条反审记录(abnormalType=6,回退库存0,备注未下发WMS),同单号已有有效记录时防重,重试/重复反审幂等

3. OMS反审Feign参数透传组织三字段(organizationId/organizationName/topOrganizationId),供WMS跳过分支写反审记录使用(该场景无W入库单可取组织信息)
2026-09-10 17:25:07 +08:00
rcx e2da7177f0 fix(oms/wms): 出库明细出库时间过滤严格按出仓日期outbound_date(去除建档时间兜底),所见即所筛 2026-09-10 16:38:34 +08:00
rcx 0b45ad181f feat(oms/wms): 出库明细单据类型回显/查询条件/导出列补齐
- 出库明细查询新增orderTypeCode(精确)/orderTypeName(模糊)查询条件
- OMS出库明细SQL补查b.order_type_code/name,响应回显orderTypeCode/orderTypeName(原恒为null)
- WMS出库明细导出新增"业务单据类型"列(第31列)
2026-09-10 16:18:11 +08:00
rcx 260ec1f369 fix(oms): 出入库业务单反审补充清空执行单号/下发执行时间,修复反审后前端是否执行仍显示已执行 2026-09-10 14:24:43 +08:00
rcx b28575c34d fix(wms): 入库明细导出客户编号/客户名称改取货主编码与货主名称(与页面显示一致) 2026-09-10 14:04:06 +08:00
rcx 8af8197901 feat(oms/wms): 出入库明细货物种类改显物料分类名称、单据类型回显与查询条件
一、货物种类
- O/W出入库明细查询新增返回materialClassifyName(物料分类名称,多个逗号分隔)
- WMS入库导出"货物种类"列、出库导出"种类"列改为物料分类名称(原为goodsType编码转文字)
- OMS出入库导出新增"货物种类"列,货物类型列加字典转换不再显示0/1/2编码

二、单据类型
- O/W入库明细查询新增orderTypeCode/orderTypeName回显与查询条件(编码精确/名称模糊)
- WMS入库明细导出新增"业务单据类型"列(第31列)

三、时间字段
- OMS出库明细查询返回增加completeTime完成时间
- OMS orderOutboundDate出仓日期格式恢复为yyyy-MM-dd HH:mm:ss
- OMS出库时间过滤改为三级兜底:出库时间OUTBOUND_TIME→出仓日期→建档时间

四、修复
- WMS出库导出SQL修复TOTAL_PRICE列不存在(总价改用TOTAL_AMOUNT),总价值暂无来源占位空列
2026-09-10 11:35:30 +08:00
rcx 12f1d99e02 fix(wms): 出入库明细查询status字段改用独立SQL别名映射,避免同列双映射在增量构建下不可靠 2026-09-09 21:53:02 +08:00
rcx 244fbf9759 feat(wms): 出入库明细查询返回增加status字段(与outOrderStatus/inOrderStatus同值,便于前端绑定) 2026-09-09 18:20:44 +08:00
rcx e3826d6333 fix(oms): 出入库明细查询状态过滤口径修正-改为issue_status=4
问题:OMS执行出库单status不随WMS出库完成流转(回写代码被注释),推送创建的单据status也是默认值,
原过滤条件 status in(9,12,13)/status=5 会导致明细页查不到数据。

修复:
- 出库明细SQL:status in(9,12,13) 改为 issue_status=4(issueStatus: 0-已创建 2-已审核 3-已驳回 4-已出库)
- 入库明细SQL:status=5 改为 issue_status=4(WMS回写status=5时同步issueStatus=4,推送创建直接置4)
- 两个明细SQL新增返回 b.issue_status,前端状态列绑定issueStatus
- WithOrderPO新增issueStatus字段(导出"单据状态"列自动转文字),outOrderStatus/inOrderStatus保留返回但不再导出
2026-09-09 17:23:54 +08:00
rcx f217d6ec5c 发测试 2026-09-09 15:51:57 +08:00
rcx e58b6cb9ce feat(oms/wms): 出入库明细查询-已入库/已出库状态过滤、WMS新增出入库明细查询页面及专用导出
一、OMS 入库/出库明细查询(修复+导出)
- 出库明细SQL状态过滤由=9改为in(9,12,13):已出库/已复核/已交接均算已出库,与出库排名统计口径一致
- 入库明细SQL固定只统计已入库(status=5)单据
- 入/出库明细SQL新增实际入仓/出仓日期区间过滤(IFNULL(实际日期,建档时间)兜底),支持统计当天
- 新增Excel导出接口 /executionInMaterialDetailApi/exportWithOrder、/executionOutMaterialDetailApi/exportWithOrder(全量不分页)
- 退货回仓统计:通过入库单类型 inOrderTypeCode=tui_huo_ru_ku(退货入库)过滤

二、WMS 新增入库/出库明细查询页面
- 入库明细查询:GET /materialDetailApi/listWithOrder(明细关联入库单,固定已入库(5),level=1防父子行重复计数)
- 出库明细查询:GET /outMaterialDetailApi/listWithOrder(新建OutMaterialDetailApi,关联出库单,固定in(9,12,13))
- 返回单头字段:单据状态/审核状态/货主/供应商/客户/单据类型/实际入仓出仓日期/保额/货物类型等
- 入库客户编号/名称/客户预约名称经通知单号关联notice_order取得(入库单表无客户字段)

三、WMS 明细导出专用列集
- 出库导出/exportWithOrder:按客户要求30列导出,含出库单号/出库日期/客户信息/LOT号/费用组(单价、总价、折扣、仓租、合同收费)等
- 入库导出/exportWithOrder:按客户要求30列导出,含客户信息/入库日期/每件保额/收费标准/有效期等
- 单据状态、货物种类导出时自动转文字;导出列顺序与客户清单一致
- 火险保费、入库单件净重(KG)暂无数据来源,导出空列占位,待确认数据来源后补充

四、其他
- 查询条件不传日期时默认查全部日期,统计当天由前端传当天日期区间实现
- 数据权限:非平台超管按登录人一级组织过滤
2026-09-09 15:49:09 +08:00
rcx b7cacaeb8d feat(oms,wms): 入库业务单反审——业务单改已驳回+已反审tab,执行单/W入库/收货/上架软删打标归档,库存还原扣减,W入库异常反审记录
【需求】已入库/已出库的单子客户通知需要回退的场景,单子回退到最原始状态。反审入口在 OMS-入库业务单页面【反審單據】按钮,一次操作全链路回退。

【反审规则】
1. 按页面状态 issueStatus 判断:进行中(1)/已审核(2)/已入库(4)/部分完成(5) 可反审;已创建(0)无需反审、已驳回(3)可直接编辑;已取消/已关闭(status 6/7)兜底拦截
2. 已推送关务/NC(pushStatus=3)直接拦截,避免关务与仓内数据不一致
3. W入库单有出库记录就不能反审:按入库单号+动作(分配库存/出库)查询库存调整记录>0 即拦截,另校验库存冻结量>0
4. WMS无该单数据时幂等成功(未下发单反审),反审失败可安全重试

【反审动作】
1. 入库业务单:状态改为已驳回(issueStatus=3,与审核不通过同语义,可编辑→重新审核→重新下发),打已反审标记落入【已反审】tab;清空入库数量/入库时间/执行单号,明细 INBOUND_ITEMS/TOTAL_NET_WEIGHT 归零
2. 入库执行单+物料明细:软删(del_flag=2)+打已反审标记,【已反审】tab 按「已软删+已反审」查询归档数据,避免归档单被误审核/下发
3. WMS侧(逐执行单Feign omsReverseInOrder):库存还原扣减(校验库存行合计=已完成上架量后整行对称扣回,GREATEST(x-n,0)兜底)→ W入库单/W收货单/W上架单及明细、收货/上架作业单/任务/质检单整链软删+打已反审标记 → 写1条入库异常【反审记录】(type=3/abnormalType=6,FSYC单号)用于记录跟踪

【接口】
- POST /reservationStockInOrderApi/reverseAudit 反审(inOrderIds + reverseAuditRemark,支持批量)
- 已反审tab:/reservationStockInOrderApi/list?reverseAuditStatus=1(执行单/W入库单/W收货单/W上架单同参数,后端固定限定 del_flag=2+标记=1 归档数据)
- 归档详情:/executionStockInOrderApi、/stockInOrderApi、/stockReceiptOrderApi、/stockShelfOrderApi 各增 getReverseAuditInfo/{id}(原 getInfo 行为不变)
- 反审记录筛选:/inOrderAbnormalApi/list?abnormalType=6

【DDL】sql/20260908_oms_inbound_reverse_audit.sql:RESERVATION_STOCK_IN_ORDER / EXECUTION_STOCK_IN_ORDER / STOCK_IN_ORDER / STOCK_RECEIPT_ORDER / STOCK_SHELF_ORDER 各新增 REVERSE_AUDIT_STATUS/REMARK/BY/BY_NAME/TIME 五列,发服务前先执行

【其他】
- 库存扣减走 batchSubtractInventoryQuantity 并显式传更新人,规避 Feign 链路无登录态时 SecurityUtils 抛异常
- omsReverseInOrder 的 Feign 方法与降级已随出库反审提交(715883f1)入库,本次不再重复提交
- 归档行只读,详情走 getReverseAuditInfo;正常数据与归档数据互不干扰
2026-09-09 15:38:21 +08:00
rcx 715883f1d4 feat(oms,wms): 出库业务单反审——已出库/已审核单据回退已创建
- 新增 POST /reservationStockOutOrderApi/reverseAudit,按issueStatus(2-已审核/4-已出库)判断可反审
- 主单回退已创建并打reverseStatus标记,进【已反审】tab(list status=-2),重新审核通过自动复位
- 执行单/明细软删+reverseFlag=1,预约库存调整记录软删
- WMS新增/stockOutOrderApi/omsReverseExecutionOrder:已出库还原实物库存+推OMS,未出库仅还原分配占用,WMS无数据幂等成功
- W出库/拣货/复核/交接单软删+reverseFlag=1,列表支持delFlag=2&reverseFlag=1【已反审】tab
- 四页面新增归档详情接口getReverseInfo*
- 生成出库异常记录【反审记录】(abnormalType=3,前缀FSYC)
- 需先执行SQL: 20260908两个反审脚本(OMS库+WMS库)
2026-09-09 15:20:13 +08:00
rcx 139a3a82da feat(wms): 盘点单详情investigetionManageDetailList按物料编码排序,相同编码相邻展示(仅排序不聚合) 2026-09-09 14:18:48 +08:00
rcx fcc8102d9e feat(wms): 库存调整记录列表新增按入库单号(inOrderNumber)查询 2026-09-09 14:16:32 +08:00
rcx 7649585715 发测试 2026-09-09 09:27:57 +08:00
rcx 7511d211c6 fix(wms): 修复盘点详情批次属性attributeValue为空的问题
1. 标签映射表改LinkedHashMap固定优先级,补全批次号/入库日期/生产日期/过期日期及英文标签映射
2. fieldName反射取不到值时回退按batchLabels映射再取,修复fieldName配置失效导致值永远为空
3. batchDetailId为空的批次属性配置行不再丢弃,改为batchDetailId+标签双重去重
2026-09-09 09:14:01 +08:00
269 changed files with 9082 additions and 2051 deletions
@@ -19,8 +19,7 @@ import java.util.Set;
/**
* bms财务结算系统feign调用接口
*/
// 目标地址由 nacos 配置项 feign.url.mhd-bms-service 指定,未配置时按服务名走服务发现
@FeignClient(contextId = "BmsService", value = ServiceNameConstants.BMS_SERVICE, url = "http://10.102.192.32:8019",fallbackFactory = RemoteBmsFeignFallbackFactory.class, configuration = FeignAutoConfiguration.class)
@FeignClient(contextId = "BmsService", value = ServiceNameConstants.BMS_SERVICE, url = "http://10.33.0.109:8012",fallbackFactory = RemoteBmsFeignFallbackFactory.class, configuration = FeignAutoConfiguration.class)
public interface BmsServiceFeign {
@@ -17,8 +17,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
import java.util.concurrent.CompletableFuture;
// 目标地址由 nacos 配置项 feign.url.mhd-oms-service 指定,未配置时按服务名走服务发现
@FeignClient(contextId = "OmsService", value = ServiceNameConstants.OMS_SERVICE,url = "http://10.102.192.33:8017",fallbackFactory = RemoteOmsFeignFallbackFactory.class, configuration = FeignAutoConfiguration.class)
@FeignClient(contextId = "OmsService", value = ServiceNameConstants.OMS_SERVICE,url = "http://10.33.0.99:8017",fallbackFactory = RemoteOmsFeignFallbackFactory.class, configuration = FeignAutoConfiguration.class)
public interface OmsServiceFeign {
@@ -26,8 +26,7 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
// 目标地址由 nacos 配置项 feign.url.mhd-wlhy-service 指定,未配置时按服务名走服务发现
@FeignClient(contextId = "commonWlhyServiceFeign",value = ServiceNameConstants.WLHY_SERVICE,url = "http://10.102.192.31:8014",configuration = FeignAutoConfiguration.class)
@FeignClient(contextId = "commonWlhyServiceFeign",value = ServiceNameConstants.WLHY_SERVICE,url = "http://10.33.0.129:8014",configuration = FeignAutoConfiguration.class)
public interface WlhyServiceFeign {
/**
@@ -540,6 +539,13 @@ public interface WlhyServiceFeign {
@GetMapping("/feign/order/cancelIssueDocuments")
public AjaxResult cancelIssueDocuments(@RequestParam(name="executeOrderId") String executeOrderId);
/**
* OMS出库业务单反审联动: 按执行单货源编号软删TMS整条链路(货源单->运单->运输作业单, is_delete=1), 幂等,
* 与 cancelIssueDocuments(仅关闭货源单且有有效运单即拒绝)不同, 反审口径不留TMS数据
*/
@PostMapping("/feign/order/deleteChainByExecuteOrderNumber")
public AjaxResult deleteChainByExecuteOrderNumber(@RequestParam(name="executeOrderNumber") String executeOrderNumber);
@GetMapping(value = "/ntocc/tmsSettlementExchangeRate/queryList")
public AjaxResult<?> queryList(@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
@@ -21,8 +21,7 @@ import java.util.Map;
* @description: TODO
* @date 2024/5/10 8:58
**/
// 目标地址由 nacos 配置项 feign.url.mhd-wms-service 指定,未配置时按服务名走服务发现
@FeignClient(contextId = "remoteWmsServiceFeign",value = ServiceNameConstants.WMS_SERVICE,url = "http://10.102.192.32:8016",fallbackFactory = RemoteWmsFallbackFactory.class, configuration = FeignAutoConfiguration.class)
@FeignClient(contextId = "remoteWmsServiceFeign",value = ServiceNameConstants.WMS_SERVICE,fallbackFactory = RemoteWmsFallbackFactory.class, configuration = FeignAutoConfiguration.class)
public interface WmsServiceFeign {
/**
@@ -75,21 +74,21 @@ public interface WmsServiceFeign {
*/
@ApiOperation("oms下发单保存")
@PostMapping(value = "/stockOutOrderApi/omsOutOrderAdd")
public void omsOutOrderAdd(@RequestBody StockOutOrder stockOutOrder);
public AjaxResult omsOutOrderAdd(@RequestBody StockOutOrder stockOutOrder);
/**
* oms下发单明细保存
*/
@ApiOperation("oms下发单明细保存")
@PostMapping(value = "/stockOutOrderApi/omsOutOrderAddDetail")
public void omsOutOrderAddDetail(@RequestBody List<OutMaterialDetail> outMaterialDetails);
public AjaxResult omsOutOrderAddDetail(@RequestBody List<OutMaterialDetail> outMaterialDetails);
/**
* oms下发单明细保存
*/
@ApiOperation("oms下发单明细保存")
@PostMapping(value = "/stockOutOrderApi/omsOutOrderAddDeliveryDetailsLink")
public void omsOutOrderAddDeliveryDetailsLink(@RequestBody List<DeliveryDetailsLink> deliveryDetailsLinks);
public AjaxResult omsOutOrderAddDeliveryDetailsLink(@RequestBody List<DeliveryDetailsLink> deliveryDetailsLinks);
/**
@@ -113,11 +112,46 @@ public interface WmsServiceFeign {
/**
* oms下发前库存校验:可用库存不足时返回错误,OMS据此在推送前拦截下发,
* 避免单据先落库、分配才失败而在WMS残留"已审核"状态的垃圾单
* 避免单据先落库、分配才失败而在WMS残留"已审核"状态的垃圾单
* businessOrderNumber(可空):OMS出库业务单号,下单冻结模式下校验可用量=库存可用+本单冻结记录量
*/
@ApiOperation("oms下发前库存校验")
@PostMapping(value = "/stockOutOrderApi/checkOmsAssignInventory")
public AjaxResult checkOmsAssignInventory(@RequestBody List<OutMaterialDetail> outMaterialDetails);
public AjaxResult checkOmsAssignInventory(@RequestBody List<OutMaterialDetail> outMaterialDetails,
@RequestParam(value = "businessOrderNumber", required = false) String businessOrderNumber);
/**
* oms出库业务单下单冻结库存(防超卖):WMS按明细冻结库存(可用-、冻结+,库存数量不变),
* 条件更新防并发超卖,任一行可用不足整体失败并返回错误;同业务单号幂等。
* 冻结失败必须阻断OMS下单(返回非200由OMS整体回滚)。
*/
@ApiOperation("oms出库业务单下单冻结库存")
@PostMapping(value = "/stockOutOrderApi/omsFreezeInventory")
public AjaxResult omsFreezeInventory(@RequestBody OmsFreezeInventoryParam param);
/**
* oms出库业务单取消/删除-释放冻结库存:可用+、冻结-(库存数量不变);按业务单号幂等;
* 冻结已被执行单承接时拒绝(须先取消下发)
*/
@ApiOperation("oms出库业务单释放冻结库存")
@PostMapping(value = "/stockOutOrderApi/omsUnfreezeInventory")
public AjaxResult omsUnfreezeInventory(@RequestBody OmsFreezeInventoryParam param);
/**
* oms出库业务单编辑-原子重锁:WMS同一事务内先释放原冻结再按新明细重新冻结,
* 任一新明细可用不足整体回滚(旧冻结同样还原),满足"先释放再重新锁定"原子性要求
*/
@ApiOperation("oms出库业务单编辑重锁库存")
@PostMapping(value = "/stockOutOrderApi/omsRelockInventory")
public AjaxResult omsRelockInventory(@RequestBody OmsFreezeInventoryParam param);
/**
* 仓库是否存在活跃冻结记录(status in 1-业务单冻结 2-执行单承接):
* 仓库"库存锁定模式"(stock_lock_mode)切换守卫,存在在途冻结单时禁止切换
*/
@ApiOperation("仓库是否存在活跃冻结记录")
@GetMapping(value = "/stockOutOrderApi/hasActiveFreezeByWarehouse/{warehouseId}")
public AjaxResult hasActiveFreezeByWarehouse(@PathVariable("warehouseId") Long warehouseId);
/**
* oms下发单明细保存(返回执行结果,明细保存含自动审核链路,失败时OMS据此回滚)
@@ -134,6 +168,27 @@ public interface WmsServiceFeign {
@PostMapping(value = "/stockInOrderApi/omsCancelInOrder")
public AjaxResult omsCancelInOrder(@RequestBody StockInOrder stockInOrder);
/**
* oms出库执行单反审(已出库/已复核单据客户通知回退场景):WMS侧还原实物库存并推送OMS库存、
* 逻辑删除出库链路单据、生成1条出库异常【反审记录】;反审原因经 remark 透传。
* 已产生下游不可逆数据时返回错误,OMS据此整体回滚。
*/
@ApiOperation("oms出库执行单反审WMS回退")
@PostMapping(value = "/stockOutOrderApi/omsReverseExecutionOrder")
public AjaxResult omsReverseExecutionOrder(@RequestBody StockOutOrder stockOutOrder);
/**
* oms入库业务单反审:WMS侧一个事务内回退已入库/收货中/上架中单据——
* 校验未产生出库(按入库单号+动作"分配库存/出库"查库存调整记录>0即有出库) ->
* 按库存行整行回退库存扣减 -> 软删W入库单/W收货单/W上架单全链并打已反审标记
* (各页面【已反审】tab按"已软删+已反审"查询归档数据)-> 写1条入库异常【反审记录】(type=3)。
* 入参: inOrderNumber=执行入库单号, remark=业务单号+反审原因, updateBy/updateByName=反审人;
* WMS无该单数据时幂等返回成功(未下发单反审不产生WMS数据)。
*/
@ApiOperation("oms入库单反审WMS回退")
@PostMapping(value = "/stockInOrderApi/omsReverseInOrder")
public AjaxResult omsReverseInOrder(@RequestBody StockInOrder stockInOrder);
@ApiOperation("oms查询物料列表")
@GetMapping("/materialBaseInfoApi/queryList")
public TableDataInfo getMaterialBaseInfoList(MaterialInfoDTO materialInfoDTO);
@@ -13,8 +13,7 @@ import org.springframework.web.bind.annotation.*;
import java.util.Map;
// 目标地址由 nacos 配置项 feign.url.xxl-job-service 指定,未配置时按服务名走服务发现
@FeignClient(contextId = "XxlJobService", value = ServiceNameConstants.XXL_JOB_SERVICE,url = "http://10.102.192.33:8020",fallbackFactory = RemoteXxlJobFeignFallbackFactory.class, configuration = FeignAutoConfiguration.class)
@FeignClient(contextId = "XxlJobService", value = ServiceNameConstants.XXL_JOB_SERVICE,url = "http://127.0.0.1:8020",fallbackFactory = RemoteXxlJobFeignFallbackFactory.class, configuration = FeignAutoConfiguration.class)
public interface XxlJobServiceFeign {
@@ -9,8 +9,7 @@ import com.mhd.system.api.feign.FeignAutoConfiguration;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
// 目标地址由 nacos 配置项 feign.url.mhd-yms-service 指定,未配置时按服务名走服务发现
@FeignClient(contextId = "YmsService", value = ServiceNameConstants.YMS_SERVICE,url = "http://10.102.192.33:8018",fallbackFactory = RemoteBmsFeignFallbackFactory.class, configuration = FeignAutoConfiguration.class)
@FeignClient(contextId = "YmsService", value = ServiceNameConstants.YMS_SERVICE,url = "http://10.33.0.99:8018",fallbackFactory = RemoteBmsFeignFallbackFactory.class, configuration = FeignAutoConfiguration.class)
public interface YmsServiceFeign {
/**
@@ -0,0 +1,42 @@
package com.mhd.system.api.domain;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* OMS出库业务单冻结库存请求参数
*
* @description OMS下单冻结库存模式(防超卖):OMS出库业务单「下单」即冻结WMS库存
* (可用-、冻结+,库存数量不变,库存数量=可用数量+冻结数量);
* 「编辑」原子释放旧冻结+按新明细重锁;「取消」释放冻结。
* 冻结/解冻以业务单号为幂等键。
* @date 2026-09-12
*/
@Data
public class OmsFreezeInventoryParam implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("OMS出库业务单号(幂等键)")
private String outOrderNumber;
@ApiModelProperty("执行出库单号(预留,冻结/解冻/重锁时不使用)")
private String executionOrderNumber;
@ApiModelProperty("库存行ID集合(预留)")
private List<Long> materialInventoryIds;
@ApiModelProperty("出库明细列表(须携带 materialInventoryId 定位库存行)")
private List<OutMaterialDetail> details;
@ApiModelProperty("操作人ID(Feign调用可能无登录上下文时的兜底)")
private Long operatorId;
@ApiModelProperty("操作人姓名(Feign调用可能无登录上下文时的兜底)")
private String operatorName;
@ApiModelProperty("备注")
private String remark;
}
@@ -124,6 +124,10 @@ public class WarehouseFeignPO extends BaseVOEntity{
@Excel(name = "作业模式")
private String workMode;
@ApiModelProperty("库存锁定模式: 1-下单冻结 2-分配库存冻结")
@Excel(name = "库存锁定模式: 1-下单冻结 2-分配库存冻结")
private Integer stockLockMode;
@ApiModelProperty("订单号生成规则(0-日期累加 1-历史累加)")
@Excel(name = "单号生成规则")
private String orderNoGenerateRule;
@@ -551,6 +551,12 @@ public class RemoteWlhyFallbackFactory implements FallbackFactory<WlhyServiceFei
return AjaxResult.error(cause.getMessage());
}
@Override
public AjaxResult deleteChainByExecuteOrderNumber(String executeOrderNumber) {
//返回错误而非静默成功, 让OMS反审事务能感知失败并整体回滚
return AjaxResult.error("TMS反审联动软删链路失败(服务降级):" + cause.getMessage());
}
@Override
public AjaxResult<?> queryList(Integer pageNo, Integer pageSize, String pushStatus, String organizationId) {
return AjaxResult.error(cause.getMessage());
@@ -57,16 +57,19 @@ public class RemoteWmsFallbackFactory implements FallbackFactory<WmsServiceFeign
}
@Override
public void omsOutOrderAdd(StockOutOrder stockOutOrder) {
public AjaxResult omsOutOrderAdd(StockOutOrder stockOutOrder) {
//返回错误而非静默成功: 降级吞掉会让OMS继续走分配, 残留"有单无明细"的垃圾单
return AjaxResult.error("WMS下发单保存失败(服务降级):" + cause.getMessage());
}
@Override
public void omsOutOrderAddDetail(List<OutMaterialDetail> outMaterialDetails) {
public AjaxResult omsOutOrderAddDetail(List<OutMaterialDetail> outMaterialDetails) {
return AjaxResult.error("WMS下发单明细保存失败(服务降级):" + cause.getMessage());
}
@Override
public void omsOutOrderAddDeliveryDetailsLink(List<DeliveryDetailsLink> deliveryDetailsLinks) {
public AjaxResult omsOutOrderAddDeliveryDetailsLink(List<DeliveryDetailsLink> deliveryDetailsLinks) {
return AjaxResult.error("WMS下发单配送明细保存失败(服务降级):" + cause.getMessage());
}
@Override
@@ -82,6 +85,12 @@ public class RemoteWmsFallbackFactory implements FallbackFactory<WmsServiceFeign
return AjaxResult.error("WMS取消下发清理失败:" + cause.getMessage());
}
@Override
public AjaxResult omsReverseExecutionOrder(StockOutOrder stockOutOrder) {
//返回错误而非静默成功,让OMS反审事务能感知失败并整体回滚
return AjaxResult.error("WMS出库执行单反审失败(服务降级):" + cause.getMessage());
}
@Override
public void omsAutoAssign(StockOutOrderDTO stockOutOrderDTO) {
@@ -93,11 +102,35 @@ public class RemoteWmsFallbackFactory implements FallbackFactory<WmsServiceFeign
}
@Override
public AjaxResult checkOmsAssignInventory(List<OutMaterialDetail> outMaterialDetails) {
public AjaxResult checkOmsAssignInventory(List<OutMaterialDetail> outMaterialDetails, String businessOrderNumber) {
//校验接口本身不可用时返回错误,OMS会阻止下发(不能在WMS状态未知时盲目推送)
return AjaxResult.error("WMS库存校验服务不可用:" + cause.getMessage());
}
@Override
public AjaxResult omsFreezeInventory(OmsFreezeInventoryParam param) {
//返回错误而非静默成功:下单冻结失败必须阻断OMS下单落库(冻结成功与否状态未知时不允许继续)
return AjaxResult.error("WMS下单冻结库存失败(服务降级):" + cause.getMessage());
}
@Override
public AjaxResult omsUnfreezeInventory(OmsFreezeInventoryParam param) {
//返回错误而非静默成功:解冻失败让OMS取消/删除感知(解冻可重试,WMS解冻幂等)
return AjaxResult.error("WMS释放冻结库存失败(服务降级):" + cause.getMessage());
}
@Override
public AjaxResult omsRelockInventory(OmsFreezeInventoryParam param) {
//返回错误而非静默成功:编辑重锁是原子操作,失败必须让OMS编辑整体回滚
return AjaxResult.error("WMS编辑重锁库存失败(服务降级):" + cause.getMessage());
}
@Override
public AjaxResult hasActiveFreezeByWarehouse(Long warehouseId) {
//返回错误(fail-closed):模式切换守卫校验不可用时拒绝切换,避免在途冻结单语义错乱
return AjaxResult.error("WMS冻结记录校验服务不可用:" + cause.getMessage());
}
@Override
public AjaxResult omsInOrderAddDetail(List<InMaterialDetail> inMaterialDetails) {
//返回错误而非静默成功:明细保存(含自动审核)失败必须让OMS感知并回滚下发标记
@@ -110,6 +143,12 @@ public class RemoteWmsFallbackFactory implements FallbackFactory<WmsServiceFeign
return AjaxResult.error("WMS取消下发清理失败:" + cause.getMessage());
}
@Override
public AjaxResult omsReverseInOrder(StockInOrder stockInOrder) {
//返回错误而非静默成功,让OMS侧反审能感知回退失败并整体回滚(反审可重试,WMS回退幂等)
return AjaxResult.error("WMS入库单反审回退失败(服务降级):" + cause.getMessage());
}
@Override
public TableDataInfo getMaterialBaseInfoList(MaterialInfoDTO materialInfoDTO) {
TableDataInfo tableDataInfo = new TableDataInfo();
@@ -130,11 +130,9 @@ public class TokenLoginApplicationService {
//验证码登录校验验证码
String key = CacheConstants.SMS_LOGIN + userPhone;
Object object = redisService.getCacheObject(key) == null ? "":redisService.getCacheObject(key);
if (loginDTO.getWechatFlag() != 1) {
// if (!ObjectUtil.equal(String.valueOf(object), verificationCode) && !ObjectUtil.equal(verificationCode, SmsConstants.universalCode)) {
// recordLogininfor(userPhone, Constants.LOGIN_FAIL, "验证码错误", 1);
if(!ObjectUtil.equal(String.valueOf(object),verificationCode) && !ObjectUtil.equal(verificationCode, SmsConstants.universalCode)){
recordLogininfor(userPhone, Constants.LOGIN_FAIL, "验证码错误", 1);
throw new ServiceException("验证码错误");
// }
}
//获取包名对应的业务(存在需要查询业务,不存在保存为无业务模式)
@@ -170,13 +168,7 @@ public class TokenLoginApplicationService {
throw new ServiceException("对不起,您的账号:" + userPhone + " 已停用");
}
//校验验证码是否正确
if (loginDTO.getWechatFlag() != 1) {
// if (!ObjectUtil.equal(String.valueOf(object), verificationCode) && !ObjectUtil.equal(verificationCode, SmsConstants.universalCode)) {
// recordLogininfor(userPhone, Constants.LOGIN_FAIL, "验证码错误", 1);
throw new ServiceException("验证码错误");
// }
}
// sysPasswordService.validate(userPo, verificationCode, 1, null);
sysPasswordService.validate(userPo, verificationCode, 1, null);
}
return userInfo;
}
@@ -40,7 +40,4 @@ public class LoginDTO {
@ApiModelProperty(name = "组织名称(司机选择组织时传入)")
private String organizationName;
@ApiModelProperty(name = "微信标识")
private Integer wechatFlag;
}
+4 -4
View File
@@ -12,10 +12,10 @@ spring:
cloud:
nacos:
discovery:
# server-addr: 127.0.0.1:8848
server-addr: 127.0.0.1:8848
# 线上测试环境配置 容器名+端口号
# server-addr: 10.33.0.129:6010
server-addr: 10.102.192.31:6848
# server-addr: 10.102.192.31:6848
username: nacos
password: manhuoda@2023
#线上正式环境
@@ -23,8 +23,8 @@ spring:
# username: nacos
# password: manhuoda@2023
config:
# server-addr: 127.0.0.1:8848
server-addr: 10.102.192.31:6848
server-addr: 127.0.0.1:8848
# server-addr: 10.102.192.31:6848
# 测试环境配置 容器名+端口号
# server-addr: 10.33.0.129:6010
# 线上测试环境配置 容器名+端口号
@@ -7,9 +7,6 @@ import com.mhd.bi.domain.biPlatformRealData.repository.po.RealTaskLoadPO;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealBusinessTypeStatPO;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealPlatformFloorStatPO;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealPlatformInfoPO;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealOnTimeStatPO;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealTaskTypeStatPO;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealMonthlyTaskDurationPO;
import java.util.List;
@@ -29,27 +26,18 @@ public interface BiPlatformRealDataMapper {
/** 今日作业任务数 */
Long countTodayTasks();
/** 车辆状态统计(今日,含叫号状态合计 callStatusTotal = 已签到+已叫号+已过号 */
/** 车辆状态统计(今日) */
RealVehicleStatusPO queryVehicleStatusToday();
/** 本月任务量 */
Long countMonthTasks();
/** 本月任务平均时长 / 完成率统计 */
RealMonthlyTaskDurationPO queryMonthTaskStats();
/** 任务负荷(今日 24 小时) */
List<RealTaskLoadPO> queryTaskLoadToday();
/** 今日预约-业务类型统计(按 businessName 分组统计今日预约数量) */
List<RealBusinessTypeStatPO> queryTodayBusinessTypeStats();
/** 今日作业准时率统计(total 今日预约总数;onTimeCount 今日准时车辆数) */
RealOnTimeStatPO queryTodayOnTimeStats();
/** 任务类型统计(本月 YMS QMS_MAIN_BUSINESS 预约按业务类型 BUSINESS_NAME 分组) */
List<RealTaskTypeStatPO> queryMonthTransportOrderByType();
/** 月台监控-按楼层统计月台车辆(楼层、月台数量、今日预约车辆、当前作业车辆) */
List<RealPlatformFloorStatPO> queryPlatformFloorStats();
@@ -1,29 +0,0 @@
package com.mhd.bi.domain.biPlatformRealData.repository.po;
import lombok.Data;
import java.io.Serializable;
/**
* 本月任务平均时长 / 完成率统计(真实数据,来自 TEST_NGWL_YMS.QMS_MAIN_BUSINESS
* avgDurationHours:累计 (EXIT_TIME - ENTRY_TIME) 小时数(仅 ENTRY_TIME/EXIT_TIME 都不为空时参与)
* durationCount:参与平均时长统计的有效条数
* completedCount:已出场(EXIT_TIME IS NOT NULL)且预约日期在本月的数量
* totalCount:本月(TARGET_DATE)预约总数量
*/
@Data
public class RealMonthlyTaskDurationPO implements Serializable {
private static final long serialVersionUID = 1L;
/** 平均作业时长(小时) */
private Double avgDurationHours;
/** 参与平均时长统计的有效条数 */
private Long durationCount;
/** 本月已出场数量 */
private Long completedCount;
/** 本月预约总数量(按 TARGET_DATE 统计) */
private Long totalCount;
}
@@ -1,20 +0,0 @@
package com.mhd.bi.domain.biPlatformRealData.repository.po;
import lombok.Data;
import java.io.Serializable;
/**
* 今日作业准时率统计(真实数据,来自 TEST_NGWL_YMS.QMS_MAIN_BUSINESS
* 准时率 = 今日实际进场时间在预约日期当天的车辆数 / 今日预约总车辆数 × 100%
*/
@Data
public class RealOnTimeStatPO implements Serializable {
private static final long serialVersionUID = 1L;
/** 今日预约总车辆数 */
private Long total;
/** 今日准时车辆数(已进场且实际进场日期 = 预约日期) */
private Long onTimeCount;
}
@@ -1,20 +0,0 @@
package com.mhd.bi.domain.biPlatformRealData.repository.po;
import lombok.Data;
import java.io.Serializable;
/**
* 任务类型统计(真实数据,来自 NGWL_TEST_YMS.QMS_MAIN_BUSINESS 预约登记表)
* 按本月预约车辆数(TARGET_DATE 在当前月)以 BUSINESS_NAME 业务类型分组
*/
@Data
public class RealTaskTypeStatPO implements Serializable {
private static final long serialVersionUID = 1L;
/** 业务类型名称(qms_main_business.BUSINESS_NAME */
private String businessName;
/** 该业务类型本月预约车辆数 */
private Long count;
}
@@ -16,12 +16,6 @@ public class RealVehicleStatusPO implements Serializable {
/** 当前未签到 */
private Long notCheckedIn;
/** 当前等待作业(FLOW_STATUS IN '1','2','3' 已预约/已签到/已叫号) */
private Long waiting;
/** 叫号状态合计:已签到 + 已叫号 + 已过号(FLOW_STATUS IN '2','3','4',预约日期为今天) */
private Long callStatusTotal;
/** 当前作业中 */
private Long operatingVehicles;
@@ -9,19 +9,12 @@ import com.mhd.bi.domain.biPlatformRealData.repository.po.RealTaskLoadPO;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealBusinessTypeStatPO;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealPlatformFloorStatPO;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealPlatformInfoPO;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealOnTimeStatPO;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealTaskTypeStatPO;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealMonthlyTaskDurationPO;
import com.mhd.bi.domain.biPlatformSurveillance.repository.mapper.BiPlatformSurveillanceReportMapper;
import com.mhd.bi.domain.biSnapshotSync.repository.mapper.BiSnapshotWriteMapper;
import com.mhd.bi.domain.biSnapshotSync.support.BiSnapshotWeeklyRandomDataBuilder;
import com.mhd.bi.interfaces.facadeApi.biReport.vo.platform.PlatformSurveillanceVO;
import com.mhd.bi.interfaces.facadeApi.biReport.vo.platform.PlatformVehicleItemVO;
import com.mhd.bi.interfaces.facadeApi.biReport.vo.platform.TaskLoadItemVO;
import com.mhd.bi.interfaces.facadeApi.biReport.vo.platform.TaskTypeStatisticsVO;
import com.mhd.bi.interfaces.facadeApi.biReport.vo.platform.TaskTypeCountItemVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
@@ -98,27 +91,16 @@ public class BiPlatformRealDataService {
if (vo.getPlatformOverview() != null) {
fp.put("totalPlatforms", vo.getPlatformOverview().getTotalPlatforms());
fp.put("operatingPlatforms", vo.getPlatformOverview().getOperatingPlatforms());
fp.put("currentOccupancyRate", vo.getPlatformOverview().getCurrentOccupancyRate());
fp.put("todayTasks", vo.getPlatformOverview().getTodayTasks());
}
if (vo.getEfficiencyStatistics() != null) {
fp.put("platformOnTimeRate", vo.getEfficiencyStatistics().getPlatformOnTimeRate());
}
if (vo.getVehicleStatus() != null) {
fp.put("todayReservedVehicles", vo.getVehicleStatus().getTodayReservedVehicles());
fp.put("notCheckedIn", vo.getVehicleStatus().getNotCheckedIn());
fp.put("waiting", vo.getVehicleStatus().getWaiting());
fp.put("callStatusTotal", vo.getVehicleStatus().getCallStatusTotal());
fp.put("operatingVehicles", vo.getVehicleStatus().getOperatingVehicles());
fp.put("completedVehicles", vo.getVehicleStatus().getCompletedVehicles());
}
if (vo.getMonthlyTaskStatistics() != null) {
fp.put("taskTotal", vo.getMonthlyTaskStatistics().getTaskTotal());
fp.put("avgTaskDuration", vo.getMonthlyTaskStatistics().getAvgTaskDuration());
fp.put("taskCompletionRate", vo.getMonthlyTaskStatistics().getTaskCompletionRate());
}
if (vo.getVehicles() != null) {
List<Map<String, Object>> vehicles = new ArrayList<>();
@@ -144,27 +126,6 @@ public class BiPlatformRealDataService {
}
fp.put("taskLoadStatistics", loads);
}
if (vo.getTaskTypeStatistics() != null) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("totalTasks", vo.getTaskTypeStatistics().getTotalTasks());
if (vo.getTaskTypeStatistics().getTaskTypes() != null) {
List<Map<String, Object>> types = new ArrayList<>();
for (TaskTypeCountItemVO it : vo.getTaskTypeStatistics().getTaskTypes()) {
Map<String, Object> tm = new LinkedHashMap<>();
tm.put("type", it.getType());
tm.put("count", it.getCount());
types.add(tm);
}
m.put("taskTypes", types);
}
fp.put("taskTypeStatistics", m);
}
// 月台告警统计:按业务要求恒为 0,纳入指纹以确保旧快照(可能含非0写死值)能被覆盖更新为 0
if (vo.getPlatformAlarmStatistics() != null) {
fp.put("todayAlarmTotal", vo.getPlatformAlarmStatistics().getTodayAlarmTotal());
fp.put("handled", vo.getPlatformAlarmStatistics().getHandled());
fp.put("unhandled", vo.getPlatformAlarmStatistics().getUnhandled());
}
return objectMapper.writeValueAsString(fp);
}
@@ -205,34 +166,12 @@ public class BiPlatformRealDataService {
}
}
/**
* 今日作业准时率 = 今日准时车辆数 / 今日有入场时间的车辆数 × 100%(四舍五入取整)
* 准时:实际进入时间 < 预约日期(TARGET_DATE) + 预约结束时间(BOOKING_END_TIME)。
* 分子分母都只统计有入场时间(ENTRY_TIME 不为空)的车辆;分母为 0 时返回 0。
*/
public int calcTodayOnTimeRate() {
try {
RealOnTimeStatPO po = biPlatformRealDataMapper.queryTodayOnTimeStats();
if (po == null) return 0;
long total = po.getTotal() == null ? 0L : po.getTotal();
long onTime = po.getOnTimeCount() == null ? 0L : po.getOnTimeCount();
if (total <= 0) return 0;
// 四舍五入到整数百分比(取整)
return (int) Math.round(((double) onTime / (double) total) * 100.0);
} catch (Exception e) {
log.error("【YMS】计算今日作业准时率失败: {}", e.getMessage(), e);
return 0;
}
}
public RealVehicleStatusPO queryVehicleStatusToday() {
try {
RealVehicleStatusPO po = biPlatformRealDataMapper.queryVehicleStatusToday();
if (po == null) po = new RealVehicleStatusPO();
if (po.getTodayReservedVehicles() == null) po.setTodayReservedVehicles(0L);
if (po.getNotCheckedIn() == null) po.setNotCheckedIn(0L);
if (po.getWaiting() == null) po.setWaiting(0L);
if (po.getCallStatusTotal() == null) po.setCallStatusTotal(0L);
if (po.getOperatingVehicles() == null) po.setOperatingVehicles(0L);
if (po.getCompletedVehicles() == null) po.setCompletedVehicles(0L);
return po;
@@ -241,14 +180,10 @@ public class BiPlatformRealDataService {
RealVehicleStatusPO po = new RealVehicleStatusPO();
po.setTodayReservedVehicles(0L);
po.setNotCheckedIn(0L);
po.setWaiting(0L);
po.setCallStatusTotal(0L);
po.setOperatingVehicles(0L);
po.setCompletedVehicles(0L);
return po;
}
}
public long countMonthTasks() {
@@ -261,21 +196,6 @@ public class BiPlatformRealDataService {
}
}
/**
* 本月任务平均时长 / 完成率统计:
* - avgDurationHours:累计(EXIT_TIME - ENTRY_TIME)小时数 / 参与数;任一为空的行不参与
* - taskCompletionRate:已完成(EXIT_TIME 不为空 且 预约日期在本月)/ 本月预约总数量 ×100
* 任一查询失败均返回 null,由调用方决定是否覆盖模板值。
*/
public RealMonthlyTaskDurationPO queryMonthTaskStats() {
try {
return biPlatformRealDataMapper.queryMonthTaskStats();
} catch (Exception e) {
log.error("【YMS】统计本月任务平均时长/完成率失败: {}", e.getMessage(), e);
return null;
}
}
public List<RealTaskLoadPO> queryTaskLoadToday() {
try {
return biPlatformRealDataMapper.queryTaskLoadToday();
@@ -285,35 +205,6 @@ public class BiPlatformRealDataService {
}
}
/**
* 任务类型统计:本月(YMS QMS_MAIN_BUSINESS)预约车辆按业务类型 BUSINESS_NAME 分组;
* 返回 {totalTasks: 当月总预约车辆数, taskTypes: [{type: 业务类型名称, count: 该类型预约数}]}
* 按真实 BUSINESS_NAME 分组输出,不固定兜底类别;当月无数据或查询失败时返回空列表。
*/
public TaskTypeStatisticsVO queryMonthTransportOrderByType() {
List<TaskTypeCountItemVO> items = new ArrayList<>();
int total = 0;
try {
List<RealTaskTypeStatPO> rows = biPlatformRealDataMapper.queryMonthTransportOrderByType();
if (rows != null) {
for (RealTaskTypeStatPO r : rows) {
if (r == null || r.getBusinessName() == null || r.getCount() == null) {
continue;
}
int c = r.getCount().intValue();
items.add(TaskTypeCountItemVO.builder().type(r.getBusinessName()).count(c).build());
total += c;
}
}
} catch (Exception e) {
log.error("【YMS】统计当月预约业务类型失败: {}", e.getMessage(), e);
// 查询失败返回空列表(前端会显示空)
items = new ArrayList<>();
total = 0;
}
return TaskTypeStatisticsVO.builder().totalTasks(total).taskTypes(items).build();
}
/**
* 今日预约-业务类型统计:按业务类型分组统计今日预约数量。
* 例:今日预约航空打板 3 条 → 返回 {businessName:"航空打板", count:3}
@@ -380,68 +271,29 @@ public class BiPlatformRealDataService {
template.setVehicles(vehicleVos);
// 月台实时概览:真实数据源,无条件覆盖
// 总月台 / 占用月台 来源:YMS 月台管理列表 QMS_WINDOW_INFOJOB_STATUS=1 表示占用)
// 月台占用率 = 占用 / 全部月台
// 今日作业任务 来源:QMS_MAIN_BUSINESS 按预约时间 TARGET_DATE = 今天 的全部数量
// 作业准时率 = 今日(实际进场时间在预约日期当天的车辆数) / 今日预约车辆总数 × 100%
if (template.getPlatformOverview() != null) {
List<RealPlatformInfoPO> platformList = queryPlatformInfoList();
int total = platformList == null ? 0 : platformList.size();
int occ = 0;
if (platformList != null) {
for (RealPlatformInfoPO p : platformList) {
if (p != null && p.getJobStatus() != null && p.getJobStatus() == 1) {
occ++;
}
}
}
RealPlatformOverviewPO ov = queryPlatformOverview();
int total = ov.getTotalPlatforms().intValue();
int occ = ov.getOperatingPlatforms().intValue();
double rate = total > 0 ? Math.round(((double) occ / total) * 100.0 * 100.0) / 100.0 : 0d;
int todayTasks = (int) countTodayTasks();
template.getPlatformOverview().setTotalPlatforms(total);
template.getPlatformOverview().setOperatingPlatforms(occ);
template.getPlatformOverview().setCurrentOccupancyRate(rate);
template.getPlatformOverview().setTodayTasks(todayTasks);
// 作业准时率(实际进入时间 < 预约日期+预约结束时间 的车辆 / 今日预约总数 × 100% 取整)
if (template.getEfficiencyStatistics() != null) {
template.getEfficiencyStatistics().setPlatformOnTimeRate(calcTodayOnTimeRate());
}
template.getPlatformOverview().setTodayTasks((int) countTodayTasks());
}
// 作业车辆状态统计:真实数据源,无条件覆盖(含等待作业 waiting = 今日预约且 已预约/已签到/已叫号)
// 作业车辆状态统计:真实数据源,无条件覆盖
if (template.getVehicleStatus() != null) {
RealVehicleStatusPO vs = queryVehicleStatusToday();
template.getVehicleStatus().setTodayReservedVehicles(vs.getTodayReservedVehicles().intValue());
template.getVehicleStatus().setNotCheckedIn(vs.getNotCheckedIn().intValue());
template.getVehicleStatus().setWaiting(vs.getWaiting().intValue());
// 叫号状态合计:已签到 + 已叫号 + 已过号(一个字段表示三个状态的总数量)
template.getVehicleStatus().setCallStatusTotal(vs.getCallStatusTotal().intValue());
template.getVehicleStatus().setOperatingVehicles(vs.getOperatingVehicles().intValue());
template.getVehicleStatus().setCompletedVehicles(vs.getCompletedVehicles().intValue());
}
// 本月任务统计:taskTotal / avgTaskDuration / taskCompletionRate 均有真实数据源,无条件覆盖;YoY 保留模板写死值
// 本月任务统计:taskTotal 有真实数据源无条件覆盖;YoY 等无真实数据源保留模板写死值
if (template.getMonthlyTaskStatistics() != null) {
template.getMonthlyTaskStatistics().setTaskTotal((int) countMonthTasks());
RealMonthlyTaskDurationPO monthStats = queryMonthTaskStats();
if (monthStats != null) {
if (monthStats.getAvgDurationHours() != null) {
// 保留两位小数
double v = monthStats.getAvgDurationHours();
template.getMonthlyTaskStatistics().setAvgTaskDuration(Math.round(v * 100.0) / 100.0);
}
Long total = monthStats.getTotalCount();
Long completed = monthStats.getCompletedCount();
if (total != null && total > 0 && completed != null) {
int rate = (int) Math.round(((double) completed.longValue() / total.longValue()) * 100.0);
template.getMonthlyTaskStatistics().setTaskCompletionRate(rate);
} else {
template.getMonthlyTaskStatistics().setTaskCompletionRate(0);
}
}
}
// 任务负荷:有真实数据源;真实为空则清空不用模板模拟,有数据则按真实小时覆盖
@@ -461,9 +313,6 @@ public class BiPlatformRealDataService {
}
}
// 任务类型统计:本月 YMS QMS_MAIN_BUSINESS 按业务类型 BUSINESS_NAME 分组(真实业务数据)
template.setTaskTypeStatistics(queryMonthTransportOrderByType());
return template;
}
@@ -87,9 +87,7 @@ public final class BiReportSnapshotMapperSupport {
.platformOverview(PlatformOverviewVO.builder()
.totalPlatforms(0).currentOccupancyRate(0d).operatingPlatforms(0).todayTasks(0).build())
.vehicleStatus(VehicleStatusVO.builder()
.todayReservedVehicles(0).notCheckedIn(0).waiting(0)
.callStatusTotal(0)
.operatingVehicles(0).completedVehicles(0).build())
.todayReservedVehicles(0).notCheckedIn(0).operatingVehicles(0).completedVehicles(0).build())
.avgOperationDurationTrend(Collections.emptyList())
.monthlyTaskStatistics(MonthlyTaskStatisticsVO.builder()
.taskTotal(0).taskTotalYoY(0).avgTaskDuration(0d).avgDurationYoY(0)
@@ -16,7 +16,7 @@ import com.mhd.bi.interfaces.facadeApi.biReport.vo.warehouseZone.WarehouseOperat
import com.mhd.bi.interfaces.facadeApi.biReport.vo.platform.PlatformVehicleItemVO;
import com.mhd.bi.domain.biPlatformRealData.service.BiPlatformRealDataService;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealPlatformVehiclePO;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealPlatformInfoPO;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealPlatformOverviewPO;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealVehicleStatusPO;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealTaskLoadPO;
import lombok.RequiredArgsConstructor;
@@ -173,31 +173,18 @@ public class BiReportSnapshotSyncService {
}
// ====== 真实数据:月台实时概览 ======
// 总月台 / 占用月台 来源:YMS 月台管理列表 QMS_WINDOW_INFOJOB_STATUS=1 表示占用)
// 月台占用率 = 占用 / 全部月台
// 今日作业任务 来源:QMS_MAIN_BUSINESS 按预约时间 TARGET_DATE = 今天 的全部数量
RealPlatformOverviewPO ov = biPlatformRealDataService.queryPlatformOverview();
if (template.getPlatformOverview() != null) {
List<RealPlatformInfoPO> platformList = biPlatformRealDataService.queryPlatformInfoList();
int total = platformList == null ? 0 : platformList.size();
int occ = 0;
if (platformList != null) {
for (RealPlatformInfoPO p : platformList) {
if (p != null && p.getJobStatus() != null && p.getJobStatus() == 1) {
occ++;
}
}
}
template.getPlatformOverview().setTotalPlatforms(ov.getTotalPlatforms().intValue());
template.getPlatformOverview().setOperatingPlatforms(ov.getOperatingPlatforms().intValue());
int total = ov.getTotalPlatforms().intValue();
int occ = ov.getOperatingPlatforms().intValue();
double rate = total > 0 ? Math.round(((double) occ / total) * 100.0 * 100.0) / 100.0 : 0d;
int todayTasks = (int) biPlatformRealDataService.countTodayTasks();
template.getPlatformOverview().setTotalPlatforms(total);
template.getPlatformOverview().setOperatingPlatforms(occ);
template.getPlatformOverview().setCurrentOccupancyRate(rate);
template.getPlatformOverview().setTodayTasks(todayTasks);
// 作业准时率 = 实际进入时间 < 预约日期+预约结束时间 的车辆 / 今日预约总数 × 100%(取整)
if (template.getEfficiencyStatistics() != null) {
template.getEfficiencyStatistics().setPlatformOnTimeRate(biPlatformRealDataService.calcTodayOnTimeRate());
}
long todayTasks = biPlatformRealDataService.countTodayTasks();
if (template.getPlatformOverview() != null) {
template.getPlatformOverview().setTodayTasks((int) todayTasks);
}
// ====== 真实数据:作业车辆状态统计 ======
@@ -205,36 +192,14 @@ public class BiReportSnapshotSyncService {
if (template.getVehicleStatus() != null) {
template.getVehicleStatus().setTodayReservedVehicles(vs.getTodayReservedVehicles().intValue());
template.getVehicleStatus().setNotCheckedIn(vs.getNotCheckedIn().intValue());
template.getVehicleStatus().setWaiting(vs.getWaiting().intValue());
// 叫号状态合计:已签到 + 已叫号 + 已过号(一个字段表示三个状态的总数量)
template.getVehicleStatus().setCallStatusTotal(vs.getCallStatusTotal() == null ? 0 : vs.getCallStatusTotal().intValue());
template.getVehicleStatus().setOperatingVehicles(vs.getOperatingVehicles().intValue());
template.getVehicleStatus().setCompletedVehicles(vs.getCompletedVehicles().intValue());
}
// ====== 真实数据:本月任务统计 taskTotal / avgTaskDuration / taskCompletionRate ======
// ====== 真实数据:本月任务统计 taskTotal ======
long monthTasks = biPlatformRealDataService.countMonthTasks();
if (template.getMonthlyTaskStatistics() != null) {
template.getMonthlyTaskStatistics().setTaskTotal((int) monthTasks);
com.mhd.bi.domain.biPlatformRealData.repository.po.RealMonthlyTaskDurationPO monthStats =
biPlatformRealDataService.queryMonthTaskStats();
if (monthStats != null) {
if (monthStats.getAvgDurationHours() != null) {
double v = monthStats.getAvgDurationHours();
template.getMonthlyTaskStatistics().setAvgTaskDuration(Math.round(v * 100.0) / 100.0);
}
Long total = monthStats.getTotalCount();
Long completed = monthStats.getCompletedCount();
if (total != null && total > 0 && completed != null) {
int rate = (int) Math.round(((double) completed.longValue() / total.longValue()) * 100.0);
template.getMonthlyTaskStatistics().setTaskCompletionRate(rate);
} else {
template.getMonthlyTaskStatistics().setTaskCompletionRate(0);
}
}
}
// ====== 真实数据:任务负荷(今日 24 小时) ======
@@ -253,9 +218,6 @@ public class BiReportSnapshotSyncService {
}
}
// ====== 真实数据:任务类型统计(本月 YMS QMS_MAIN_BUSINESS 按业务类型 BUSINESS_NAME 分组) ======
template.setTaskTypeStatistics(biPlatformRealDataService.queryMonthTransportOrderByType());
return template;
}
@@ -155,13 +155,10 @@ public final class BiSnapshotWeeklyRandomDataBuilder {
VehicleStatusVO vs = VehicleStatusVO.builder()
.todayReservedVehicles(Math.max(0, jitterInt(Baseline.P_RESV, r)))
.notCheckedIn(Math.max(0, jitterInt(Baseline.P_NC, r)))
.waiting(Math.max(0, jitterInt(Baseline.P_WAIT, r)))
.callStatusTotal(Math.max(0, jitterInt(Baseline.P_CALL_STATUS_TOTAL, r)))
.operatingVehicles(Math.max(0, jitterInt(Baseline.P_OPV, r)))
.completedVehicles(Math.max(0, jitterInt(Baseline.P_DONE, r)))
.build();
List<AvgOperationDurationTrendItemVO> avgTrend = new ArrayList<>();
for (int i = 0; i < Baseline.P_MO_LABELS.length; i++) {
avgTrend.add(AvgOperationDurationTrendItemVO.builder()
@@ -188,8 +185,13 @@ public final class BiSnapshotWeeklyRandomDataBuilder {
.build());
}
// 月台告警:按业务要求,全部置 0(趋势/统计/分布均不生成写死数据)
List<PlatformAlarmTrendItemVO> alarmTrend = new ArrayList<>();
for (int i = 0; i < Baseline.P_AL_D.length; i++) {
alarmTrend.add(PlatformAlarmTrendItemVO.builder()
.date(Baseline.P_AL_D[i])
.alarmCount(Math.max(0, jitterInt(Baseline.P_AL_C[i], r)))
.build());
}
List<TaskTypeCountItemVO> typeCounts = new ArrayList<>();
for (int i = 0; i < Baseline.P_TT_TYPES.length; i++) {
@@ -199,11 +201,18 @@ public final class BiSnapshotWeeklyRandomDataBuilder {
.build());
}
int todayAl = 0;
int handled = 0;
int unhandled = 0;
int todayAl = Math.max(0, jitterInt(Baseline.P_AL_TOT, r));
int handled = Math.max(0, jitterInt(Baseline.P_AL_H, r));
handled = Math.min(handled, todayAl);
int unhandled = Math.max(0, todayAl - handled);
List<PlatformAlarmDistItemVO> dist = new ArrayList<>();
for (int i = 0; i < Baseline.P_DIST_T.length; i++) {
dist.add(PlatformAlarmDistItemVO.builder()
.type(Baseline.P_DIST_T[i])
.count(Math.max(0, jitterInt(Baseline.P_DIST_C[i], r)))
.build());
}
return PlatformSurveillanceVO.builder()
.vehicles(vehicles)
@@ -406,8 +415,6 @@ public final class BiSnapshotWeeklyRandomDataBuilder {
static final int P_TASKS = 418;
static final int P_RESV = 2_850;
static final int P_NC = 920;
static final int P_WAIT = 680;
static final int P_CALL_STATUS_TOTAL = 680;
static final int P_OPV = 1_180;
static final int P_DONE = 1_420;
static final String[] P_MO_LABELS = {"5月", "6月", "7月"};
@@ -20,12 +20,6 @@ public class VehicleStatusVO {
@ApiModelProperty("当前未签到,单位:辆")
private Integer notCheckedIn;
@ApiModelProperty("当前等待作业(已预约/已签到/已叫号),单位:辆")
private Integer waiting;
@ApiModelProperty("已签到+已叫号+已过号(FLOW_STATUS IN 2/3/4),单位:辆")
private Integer callStatusTotal;
@ApiModelProperty("当前作业中,单位:辆")
private Integer operatingVehicles;
+2 -2
View File
@@ -19,14 +19,14 @@ spring:
# username: nacos
# password: manhuoda@2023
#线上正式环境
server-addr: 10.102.192.31:6848
server-addr: 10.102.192.30:6848
username: nacos
password: manhuoda@2023
config:
# server-addr: 127.0.0.1:8848
# 线上测试环境配置 容器名+端口号
# server-addr: 10.33.0.129:6010
server-addr: 10.102.192.31:6848
server-addr: 10.102.192.30:6848
username: nacos
password: manhuoda@2023
#线上正式环境
@@ -40,85 +40,35 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
FROM NGWL_TEST_YMS.QMS_WINDOW_INFO
</select>
<!-- 今日作业任务:按预约时间 TARGET_DATE(今日)统计主业务记录数 -->
<!-- 今日作业任务:实际进入时间是今日的 -->
<select id="countTodayTasks" resultType="java.lang.Long">
SELECT COUNT(1)
FROM NGWL_TEST_YMS.QMS_MAIN_BUSINESS
WHERE TARGET_DATE IS NOT NULL
AND TRUNC(TARGET_DATE) = TRUNC(SYSDATE)
</select>
<!-- 今日作业准时率统计:
total 今日有入场时间的预约车辆数(ENTRY_TIME 不为空 且 TARGET_DATE = 今天)
onTimeCount 今日准时车辆数(实际进入时间 < 预约日期 + 预约结束时间 BOOKING_END_TIME,即 ENTRY_TIME < TRUNC(TARGET_DATE) + 预约时段结束时分)
「准时率 = 今日准时车辆数 / 今日有入场时间的车辆数 × 100%(四舍五入取整);分子分母都只统计有入场时间的车辆」
BOOKING_END_TIME 存纯时分(如 18:30),故用 TO_DATE 解析后取其时分间隔加到 TARGET_DATE 上
-->
<select id="queryTodayOnTimeStats" resultType="com.mhd.bi.domain.biPlatformRealData.repository.po.RealOnTimeStatPO">
SELECT
SUM(CASE WHEN ENTRY_TIME IS NOT NULL
AND TARGET_DATE IS NOT NULL
AND TRUNC(TARGET_DATE) = TRUNC(SYSDATE) THEN 1 ELSE 0 END) AS total,
SUM(CASE WHEN ENTRY_TIME IS NOT NULL
AND TRUNC(TARGET_DATE) = TRUNC(SYSDATE)
AND ENTRY_TIME &lt; TRUNC(TARGET_DATE)
+ (TO_DATE(BOOKING_END_TIME, 'HH24:MI') - TRUNC(TO_DATE(BOOKING_END_TIME, 'HH24:MI')))
THEN 1 ELSE 0 END) AS onTimeCount
FROM NGWL_TEST_YMS.QMS_MAIN_BUSINESS
WHERE ENTRY_TIME IS NOT NULL
AND TRUNC(ENTRY_TIME) = TRUNC(SYSDATE)
</select>
<!-- 车辆状态统计:今日的统计(按创建时间 CREATE_TIME / 流程状态)
todayReservedVehicles 改为按预约日期 TARGET_DATE(其它 3 项保持 CREATE_TIME
waiting 等待作业:FLOW_STATUS IN ('1','2','3') 已预约/已签到/已叫号 且 预约日期 = 今天
callStatusTotal 叫号状态合计:已签到 + 已叫号 + 已过号 = FLOW_STATUS IN ('2','3','4') 且 预约日期 = 今天
FLOW_STATUS: 1已预约 2已签到 3已叫号 4已过号 5已进场 6已称重 7已装车 8二次称重 9已结算 10已还卡 11已出场 12已取消 -->
<select id="queryVehicleStatusToday" resultType="com.mhd.bi.domain.biPlatformRealData.repository.po.RealVehicleStatusPO">
SELECT
SUM(CASE WHEN TRUNC(TARGET_DATE) = TRUNC(SYSDATE) THEN 1 ELSE 0 END) AS todayReservedVehicles,
SUM(CASE WHEN FLOW_STATUS = '1' AND TRUNC(CREATE_TIME) = TRUNC(SYSDATE) THEN 1 ELSE 0 END) AS notCheckedIn,
SUM(CASE WHEN FLOW_STATUS IN ('1','2','3') AND TRUNC(TARGET_DATE) = TRUNC(SYSDATE) THEN 1 ELSE 0 END) AS waiting,
SUM(CASE WHEN FLOW_STATUS IN ('2','3','4') AND TRUNC(TARGET_DATE) = TRUNC(SYSDATE) THEN 1 ELSE 0 END) AS callStatusTotal,
SUM(CASE WHEN FLOW_STATUS IN ('5','6','7','8','9','10') AND TRUNC(CREATE_TIME) = TRUNC(SYSDATE) THEN 1 ELSE 0 END) AS operatingVehicles,
SUM(CASE WHEN FLOW_STATUS = '11' AND TRUNC(CREATE_TIME) = TRUNC(SYSDATE) THEN 1 ELSE 0 END) AS completedVehicles
FROM NGWL_TEST_YMS.QMS_MAIN_BUSINESS
</select>
<!-- 本月任务量:按预约日期 TARGET_DATE(Y预约日期)统计本月预约总数量 -->
<!-- 本月任务量:实际进入时间是本月的 -->
<select id="countMonthTasks" resultType="java.lang.Long">
SELECT COUNT(1)
FROM NGWL_TEST_YMS.QMS_MAIN_BUSINESS
WHERE TARGET_DATE IS NOT NULL
AND TO_CHAR(TARGET_DATE, 'YYYY-MM') = TO_CHAR(SYSDATE, 'YYYY-MM')
</select>
<!-- 本月任务平均作业时长 / 完成率统计:
avgDurationHours 平均时长(小时)= AVG((EXIT_TIME - ENTRY_TIME) * 24)
仅 ENTRY_TIME 与 EXIT_TIME 都不为空时参与;
durationCount 参与平均时长统计的有效条数;
completedCount 本月(TARGET_DATE)已出场(EXIT_TIME IS NOT NULL)的数量;
totalCount 本月(TARGET_DATE)预约总数量。
-->
<select id="queryMonthTaskStats" resultType="com.mhd.bi.domain.biPlatformRealData.repository.po.RealMonthlyTaskDurationPO">
SELECT
AVG(CASE WHEN ENTRY_TIME IS NOT NULL AND EXIT_TIME IS NOT NULL
THEN (EXIT_TIME - ENTRY_TIME) * 24
END) AS avgDurationHours,
SUM(CASE WHEN ENTRY_TIME IS NOT NULL AND EXIT_TIME IS NOT NULL
WHERE ENTRY_TIME IS NOT NULL
AND TO_CHAR(ENTRY_TIME, 'YYYY-MM') = TO_CHAR(SYSDATE, 'YYYY-MM')
THEN 1 ELSE 0 END) AS durationCount,
SUM(CASE WHEN EXIT_TIME IS NOT NULL
AND TARGET_DATE IS NOT NULL
AND TO_CHAR(TARGET_DATE, 'YYYY-MM') = TO_CHAR(SYSDATE, 'YYYY-MM')
THEN 1 ELSE 0 END) AS completedCount,
SUM(CASE WHEN TARGET_DATE IS NOT NULL
AND TO_CHAR(TARGET_DATE, 'YYYY-MM') = TO_CHAR(SYSDATE, 'YYYY-MM')
THEN 1 ELSE 0 END) AS totalCount
FROM NGWL_TEST_YMS.QMS_MAIN_BUSINESS
</select>
<!-- 今日预约-业务类型统计:按 BUSINESS_NAME(业务类型名称)分组统计今日预约数量,按预约日期 TARGET_DATE 过滤 -->
<!-- 今日预约-业务名称统计:按 BUSINESS_NAME(业务名称)分组统计今日预约数量,按预约日期 TARGET_DATE 过滤 -->
<select id="queryTodayBusinessTypeStats" resultType="com.mhd.bi.domain.biPlatformRealData.repository.po.RealBusinessTypeStatPO">
SELECT
BUSINESS_NAME AS businessName,
@@ -152,20 +102,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
ORDER BY 1 DESC
</select>
<!-- 任务类型统计(来自 NGWL_TEST_YMS.QMS_MAIN_BUSINESS 预约登记表):
按本月预约车辆数(TARGET_DATE 在当前月)按 BUSINESS_NAME 业务类型分组统计 -->
<select id="queryMonthTransportOrderByType" resultType="com.mhd.bi.domain.biPlatformRealData.repository.po.RealTaskTypeStatPO">
SELECT
BUSINESS_NAME AS businessName,
COUNT(1) AS count
FROM NGWL_TEST_YMS.QMS_MAIN_BUSINESS
WHERE BUSINESS_NAME IS NOT NULL
AND TARGET_DATE IS NOT NULL
AND TO_CHAR(TARGET_DATE, 'YYYY-MM') = TO_CHAR(SYSDATE, 'YYYY-MM')
GROUP BY BUSINESS_NAME
ORDER BY COUNT(1) DESC
</select>
<!-- 任务负荷(今日 24 小时时段):按 EXTRACT(HOUR FROM ENTRY_TIME) 统计 -->
<select id="queryTaskLoadToday" resultType="com.mhd.bi.domain.biPlatformRealData.repository.po.RealTaskLoadPO">
SELECT
@@ -35,4 +35,7 @@ public class WarehousePO implements Serializable {
@Excel(name = "是否启用: 1-否 2-是")
private Integer isEnable;
@ApiModelProperty("库存锁定模式: 1-下单冻结 2-分配库存冻结")
private Integer stockLockMode;
}
@@ -26,8 +26,7 @@ import java.io.InputStream;
import java.util.List;
import java.util.Map;
// 目标地址由 nacos 配置项 feign.url.mhd-third-party-service 指定,未配置时按服务名走服务发现
@FeignClient(value = "mhd-third-party-service",url = "http://10.102.192.31:8012", configuration = FeignAutoConfiguration.class)
@FeignClient(value = "mhd-third-party-service",url = "http://10.33.0.129:8012", configuration = FeignAutoConfiguration.class)
public interface ThirdPartyServiceFeign {
//查询当前组织所有三方接口信息
@@ -12,7 +12,9 @@ import com.mhd.basic.domain.warehouse.repository.todo.WarehouseDO;
import com.mhd.basic.domain.warehouse.service.WarehouseDomainService;
import com.mhd.common.core.enums.DictCode;
import com.mhd.common.core.exception.ServiceException;
import com.mhd.common.core.web.domain.AjaxResult;
import com.mhd.common.security.utils.SecurityUtils;
import com.mhd.system.api.WmsServiceFeign;
import com.mhd.system.api.domain.SysDictData;
import com.mhd.system.api.model.LoginUser;
import com.mhd.system.service.ISysDictTypeService;
@@ -43,6 +45,8 @@ public class WarehouseApplicationService {
private ISysDictTypeService dictTypeService;
@Autowired
private IAssociationWarehouseService associationWarehouseService;
@Autowired
private WmsServiceFeign wmsServiceFeign;
/**
@@ -239,12 +243,78 @@ public class WarehouseApplicationService {
/**
* 修改仓库
*/
/**
* @description 查询仓库库存锁定模式(OMS下单冻结库存/防超卖功能的仓库级开关)
* 1-下单冻结:OMS出库业务单「下单」即冻结WMS库存(可用-、冻结+,库存数量不变)
* 2-分配库存冻结:现有模式,WMS「分配库存」时才冻结
* 仓库不存在或未配置(null)时返回默认值 2(与代码侧降级口径一致)
* @author gen
* @date 2026-09-12
*/
public Integer getStockLockMode(Long warehouseId) {
WarehousePO warehousePO = warehouseDomainService.getInfo(warehouseId);
return warehousePO == null ? 2
: cn.hutool.core.util.ObjectUtil.defaultIfNull(warehousePO.getStockLockMode(), 2);
}
public Boolean update(WarehouseDO warehouseDO) {
// 库存锁定模式切换守卫:模式发生变化时,存在在途冻结单(OMS下单冻结未释放)则禁止切换,
// 否则在途单的解冻/承接逻辑会按新模式错乱。fail-closed:校验服务不可用时拒绝切换
if (warehouseDO.getWarehouseId() != null && warehouseDO.getStockLockMode() != null) {
WarehousePO existingPO = warehouseDomainService.getInfo(warehouseDO.getWarehouseId());
if (existingPO != null) {
checkStockLockModeSwitchable(warehouseDO.getWarehouseId(), warehouseDO.getStockLockMode(), existingPO);
}
}
//设置数据字典键值
setDataDict(warehouseDO);
return warehouseDomainService.update(warehouseDO);
}
/**
* @description 单独切换仓库库存锁定模式(不改动仓库其他信息)
* 1-下单冻结:OMS出库业务单「下单」即冻结WMS库存
* 2-分配库存冻结(默认):WMS「分配库存」时才冻结
* 切换守卫与通用编辑一致:模式变化且存在在途冻结单时禁止切换,fail-closed
* @author gen
* @date 2026-09-14
*/
public Boolean switchStockLockMode(Long warehouseId, Integer stockLockMode) {
if (warehouseId == null) {
throw new ServiceException("仓库ID不能为空");
}
if (stockLockMode == null || (stockLockMode.intValue() != 1 && stockLockMode.intValue() != 2)) {
throw new ServiceException("库存锁定模式不合法,仅支持 1-下单冻结 2-分配库存冻结");
}
WarehousePO existingPO = warehouseDomainService.getInfo(warehouseId);
if (existingPO == null) {
throw new ServiceException("仓库不存在");
}
checkStockLockModeSwitchable(warehouseId, stockLockMode, existingPO);
return warehouseDomainService.switchStockLockMode(warehouseId, stockLockMode);
}
/**
* @description 库存锁定模式切换守卫:模式未变化直接放行;模式变化时查询WMS是否存在
* 在途下单冻结单(freeze_status 1/2),存在则禁止切换。
* fail-closed:WMS服务不可用/返回异常时一律拒绝切换,防止切换后冻结语义错乱
* @author gen
* @date 2026-09-14
*/
private void checkStockLockModeSwitchable(Long warehouseId, Integer newMode, WarehousePO existingPO) {
if (newMode.compareTo(cn.hutool.core.util.ObjectUtil.defaultIfNull(existingPO.getStockLockMode(), 2)) == 0) {
return;
}
AjaxResult freezeCheck = wmsServiceFeign.hasActiveFreezeByWarehouse(warehouseId);
if (freezeCheck == null || !"200".equals(String.valueOf(freezeCheck.get("code")))) {
throw new ServiceException("库存锁定模式切换校验失败,请稍后重试:"
+ (freezeCheck == null ? "WMS服务不可用" : freezeCheck.get("msg")));
}
if (Boolean.TRUE.equals(freezeCheck.get("data"))) {
throw new ServiceException("该仓库存在下单冻结中的出库单,不可切换库存锁定模式");
}
}
/**
* @description 设置数据字典键值
* @author ZhouGY
@@ -130,6 +130,10 @@ public class Warehouse extends BaseVOEntity{
@Excel(name = "作业模式")
private String workMode;
@ApiModelProperty("库存锁定模式: 1-下单冻结 2-分配库存冻结")
@Excel(name = "库存锁定模式: 1-下单冻结 2-分配库存冻结")
private Integer stockLockMode;
@ApiModelProperty("订单号生成规则0日期累加1历史累加")
@Excel(name = "单号生成规则")
private String orderNoGenerateRule;
@@ -132,6 +132,10 @@ public class WarehousePO extends BaseVOEntity{
@Excel(name = "作业模式")
private String workMode;
@ApiModelProperty("库存锁定模式: 1-下单冻结 2-分配库存冻结")
@Excel(name = "库存锁定模式: 1-下单冻结 2-分配库存冻结")
private Integer stockLockMode;
@ApiModelProperty("订单号生成规则")
@Excel(name = "单号生成规则")
@@ -140,6 +140,10 @@ public class WarehouseDO extends BaseVOEntity{
@Excel(name = "作业模式")
private String workMode;
@ApiModelProperty("库存锁定模式: 1-下单冻结 2-分配库存冻结")
@Excel(name = "库存锁定模式: 1-下单冻结 2-分配库存冻结")
private Integer stockLockMode;
@ApiModelProperty("订单号生成规则")
@Excel(name = "单号生成规则")
@@ -142,6 +142,29 @@ public class WarehouseDomainService {
.eq(Warehouse::getDelFlag, 1));
}
/**
* @description 单独切换仓库库存锁定模式(仅更新 STOCK_LOCK_MODE 与审计字段,不触碰仓库其他数据)
* 1-下单冻结:OMS出库业务单「下单」即冻结WMS库存
* 2-分配库存冻结(默认):WMS「分配库存」时才冻结
* @author gen
* @date 2026-09-14
*/
public Boolean switchStockLockMode(Long warehouseId, Integer stockLockMode) {
Long updateBy = null;
try {
LoginUser loginUser = SecurityUtils.getLoginUser();
updateBy = loginUser == null ? null : loginUser.getUserid();
} catch (Exception ignore) {
}
Long finalUpdateBy = updateBy;
return warehouseService.update(new UpdateWrapper<Warehouse>().lambda()
.set(Warehouse::getStockLockMode, stockLockMode)
.set(finalUpdateBy != null, Warehouse::getUpdateBy, finalUpdateBy)
.set(Warehouse::getUpdateTime, new Date())
.eq(Warehouse::getWarehouseId, warehouseId)
.eq(Warehouse::getDelFlag, 1));
}
/**
* @description 获取指定仓库下的所有库区及库区下所有库位
* @author ZhouGY
@@ -145,6 +145,10 @@ public class WarehouseDTO extends BaseVOEntity{
@Excel(name = "作业模式")
private String workMode;
@ApiModelProperty("库存锁定模式: 1-下单冻结 2-分配库存冻结")
@Excel(name = "库存锁定模式: 1-下单冻结 2-分配库存冻结")
private Integer stockLockMode;
@ApiModelProperty("订单号生成规则")
@Excel(name = "单号生成规则")
@@ -104,6 +104,33 @@ public class WarehouseApi extends BaseController{
return AjaxResult.success(warehouseApplicationService.getInfo(warehouseId));
}
/**
* 查询仓库库存锁定模式(OMS下单冻结库存/防超卖功能的仓库级开关):
* 1-下单冻结(OMS出库业务单「下单」即冻结WMS库存:可用-、冻结+,库存数量不变;
* 分配不动数量;编辑=原子重锁;取消=释放;交接=库存-冻结-)
* 2-分配库存冻结(默认,现有行为:WMS「分配库存」时才 可用-冻结+)
* 仓库不存在或未配置时返回 2(与 OMS/WMS 代码侧降级口径一致)
*/
@ApiOperation("查询仓库库存锁定模式: 1-下单冻结 2-分配库存冻结")
@GetMapping(value = "/getStockLockMode/{warehouseId}")
public AjaxResult getStockLockMode(@PathVariable("warehouseId") Long warehouseId)
{
return AjaxResult.success(warehouseApplicationService.getStockLockMode(warehouseId));
}
/**
* 单独切换仓库库存锁定模式(不改动仓库其他信息):
* 模式变化且该仓库存在在途下单冻结出库单(freeze_status 1/2)时拒绝切换;
* WMS校验服务不可用时同样拒绝(fail-closed
*/
@ApiOperation("切换仓库库存锁定模式: 1-下单冻结 2-分配库存冻结")
@PostMapping(value = "/switchStockLockMode/{warehouseId}/{stockLockMode}")
public AjaxResult switchStockLockMode(@PathVariable("warehouseId") Long warehouseId,
@PathVariable("stockLockMode") Integer stockLockMode)
{
return toAjax(warehouseApplicationService.switchStockLockMode(warehouseId, stockLockMode));
}
@ApiOperation("批量作废仓库")
@GetMapping("/nullifyByIds/{warehouseIds}")
@@ -13,16 +13,16 @@ spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
# server-addr: 10.33.0.129:6010
# server-addr: 127.0.0.1:8848
server-addr: 10.33.0.129:6010
# server-addr: 10.102.192.31:6848
username: nacos
password: manhuoda@2023
#线上正式环境
# server-addr: 10.102.192.105:6848
config:
server-addr: 127.0.0.1:8848
# server-addr: 10.33.0.129:6010
# server-addr: 127.0.0.1:8848
server-addr: 10.33.0.129:6010
# server-addr: 10.102.192.31:6848
username: nacos
password: manhuoda@2023
@@ -43,6 +43,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="orderNoGenerateRule" column="ORDER_NO_GENERATE_RULE" />
<result property="inOrderNoPrefix" column="IN_ORDER_NO_PREFIX" />
<result property="outOrderNoPrefix" column="OUT_ORDER_NO_PREFIX" />
<result property="stockLockMode" column="STOCK_LOCK_MODE" />
</resultMap>
@@ -51,7 +52,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
warehouse_type, warehouse_type_name, warehouse_structure, warehouse_nature, warm_layer_code, warm_layer_name,
director_name, director_phone, region_code, region_name, detail_address, longitude, latitude, building_region,
usage_region, remark, opening_up, nullify, create_time, create_by, create_by_name, update_time, update_by,
update_by_name, del_flag, IS_ENABLE ,WORK_MODE,TASK_MODE,SAMPLE_OUT_ORDER_NO_PREFIX,ORDER_NO_GENERATE_RULE,IN_ORDER_NO_PREFIX,
update_by_name, del_flag, IS_ENABLE ,WORK_MODE,TASK_MODE,STOCK_LOCK_MODE,SAMPLE_OUT_ORDER_NO_PREFIX,ORDER_NO_GENERATE_RULE,IN_ORDER_NO_PREFIX,
OUT_ORDER_NO_PREFIX
from warehouse
</sql>
@@ -97,7 +97,6 @@ import com.mhd.system.api.model.LoginUser;
import com.mhd.user.interfaces.dto.addDTO.UserDriverDTO;
import com.mhd.user.interfaces.dto.queryDTO.UserDocumentWarningQueryDTO;
import com.mhd.user.interfaces.dto.updateDTO.UpdateHasDefaultAddressDTO;
import com.mhd.user.interfaces.dto.updateDTO.PasswordChangeDTO;
import com.mhd.user.interfaces.facadeApi.Po.UserDriverCountPo;
import com.mhd.user.interfaces.vo.QueryAllShipperInfoVo;
import lombok.extern.slf4j.Slf4j;
@@ -215,61 +214,6 @@ public class UserApplicationService {
@Autowired
private UserShipperMapper userShipperMapper;
/**
* 修改密码 - 同步 wlhy yms
*/
@Transactional(rollbackFor = Exception.class)
public void changePassword(PasswordChangeDTO passwordChangeDTO) {
// 1. 获取当前登录用户
LoginUser loginUser = SecurityUtils.getLoginUser();
if (ObjectUtil.isNull(loginUser)) {
throw new DigitalLogisticsException(UserError.TIMEOUT);
}
Long userId = loginUser.getUserPo().getUserId();
if (userId == null) {
throw new ServiceException("用户未登录");
}
// 2. 查询用户信息
UserPo userPO = userDomainService.selectByUserId(userId);
UserEntity userEntity = new UserEntity();
BeanUtils.copyProperties(userPO,userEntity);
if (ObjectUtil.isNull(userEntity)) {
throw new ServiceException("用户不存在");
}
// 3. 解密并加密新密码
String salt = userEntity.getUserSalt();
String newPassword = decrypt(passwordChangeDTO.getNewPassword());
String encodedPassword = PasswordUtil.encrypt(userEntity.getUserAccount(), newPassword, salt);
// 4. 更新本地密码
UserDO userDO = new UserDO();
userDO.setUserId(userId);
userDO.setUserPassword(encodedPassword);
userDomainService.updateUser(userDO);
// 5. 同步到网货平台wlhy
try {
userEntity.setUserPassword(encodedPassword);
wlhyDomainService.addOrEditPlatformUser(userEntity);
} catch (Exception e) {
log.warn("同步密码到网货平台失败,但不影响主流程:{}", e.getMessage());
}
// 6. 同步到 YMS 平台
try {
Integer isQueue = userMapper.getOrdIsQueue(userEntity.getOrganizationId());
if (isQueue != null && isQueue == 1) {
userEntity.setUserPassword(newPassword);
ymsDomainService.addOrEditYmsUser(userEntity);
}
} catch (Exception e) {
log.warn("同步密码到 YMS 平台失败,但不影响主流程:{}", e.getMessage());
}
}
/**
* @Description 查询用户列表
* @Author Alex
@@ -2027,11 +1971,6 @@ public class UserApplicationService {
if (StrUtil.isEmpty(userDO.getUserName())) {
userDO.setUserName("用户" + userDO.getUserPhone().substring(userDO.getUserPhone().length() - 4));
}
//密码为空则给默认密码
if (StrUtil.isEmpty(userDO.getUserPassword())) {
String defaultPassword = "Aa123456";
userDO.setUserPassword(PasswordUtil.encrypt(userDO.getUserAccount(), defaultPassword, userDO.getUserSalt()));
}
UserEntity userEntity = userDomainService.addUser(userDO);
//给小程序注册用户分配默认普通用户角色must本组织没有则找上级
userEntity.setUserPassword("Aa123456"); // 三方同步需要密码
@@ -100,7 +100,6 @@ public class UserYmsDomainService {
ymsSysUserDTO.setPhonenumber(userEntity.getUserPhone());
ymsSysUserDTO.setAvatar(userEntity.getAvatar());
ymsSysUserDTO.setUserAccountType(userEntity.getUserAccountType());
ymsSysUserDTO.setPassword(userEntity.getUserPassword());
if (StringUtils.isNotBlank(userEntity.getRoleCode())){
ymsSysUserDTO.setRoleCodes(Arrays.asList(userEntity.getRoleCode().split(",")));
@@ -1,20 +0,0 @@
package com.mhd.user.interfaces.dto.updateDTO;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* 修改密码 DTO
* @author AI
* @date 2026/09/10
*/
@Data
public class PasswordChangeDTO {
/**
* 新密码
*/
@NotBlank(message = "新密码不能为空")
private String newPassword;
}
@@ -32,7 +32,6 @@ import com.mhd.common.security.annotation.InnerAuth;
import com.mhd.common.security.service.TokenService;
import com.mhd.system.api.model.LoginUser;
import com.mhd.user.interfaces.dto.queryDTO.UserDocumentWarningQueryDTO;
import com.mhd.user.interfaces.dto.updateDTO.PasswordChangeDTO;
import com.mhd.user.interfaces.dto.updateDTO.UpdateHasDefaultAddressDTO;
import com.mhd.user.interfaces.vo.QueryAllShipperInfoVo;
import io.swagger.annotations.Api;
@@ -1038,16 +1037,4 @@ public class UserAPI extends BaseController {
public R<String> finShipperEnterpriseNamedByUserId(@PathVariable("userId") Long userId) {
return R.ok(userApplicationService.finShipperEnterpriseNamedByUserId(userId));
}
/**
* 修改密码 - 同步 wlhy 与 yms
* 根据当前登录用户判断是谁修改,只传新密码,不需要校验旧密码
*/
@Log(title = "用户中台管理 - 修改密码", description ="修改当前登录用户密码",businessType = BusinessType.UPDATE)
@ApiOperation("修改密码")
@PostMapping("/changePassword")
public AjaxResult changePassword(@RequestBody PasswordChangeDTO passwordChangeDTO) {
userApplicationService.changePassword(passwordChangeDTO);
return AjaxResult.success();
}
}
@@ -18,15 +18,15 @@ spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
# server-addr: 127.0.0.1:8848
# server-addr: 10.33.0.129:6010
# server-addr: 10.102.192.31:6848
server-addr: 10.102.192.31:6848
username: nacos
password: manhuoda@2023
config:
server-addr: 127.0.0.1:8848
# server-addr: 127.0.0.1:8848
# server-addr: 10.33.0.129:6010
# server-addr: 10.102.192.31:6848
server-addr: 10.102.192.31:6848
username: nacos
password: manhuoda@2023
group: DEFAULT_GROUP # 默认分组就是DEFAULT_GROUP,如果使用默认分组可以不配置
@@ -50,7 +50,3 @@ feign:
mhd-third-party-service:
connectTimeout: 10000
readTimeout: 120000
#wechat:
# miniapp:
# appId: wxc9dd2810a77e4e30
# appSecret: 9354ff1215a82785ef14f2355fbf2185
@@ -0,0 +1,58 @@
package com.mhd.oms.application.service;
import com.mhd.common.core.exception.ServiceException;
import com.mhd.common.core.web.domain.AjaxResult;
import com.mhd.oms.domain.reservationStockInOrder.repository.mapper.ReservationStockInOrderMapper;
import com.mhd.system.api.SystemServiceFeign;
import com.mhd.system.api.domain.WarehouseFeignPO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* 仓库信息获取(带本地兜底): 下发/下单冻结/保存等流程原先直接 Feign system 服务取仓库信息,
* system 服务未启动/降级/返回非200时整个流程被"获取仓库信息失败"阻断
* 本组件统一收口: 优先 Feign, 失败时回退本地跨库直查 "NGWL_TEST_SYSTEM".WAREHOUSE( Feign 同库同表, OMS 库账号可读),
* 两侧都取不到才抛错并带出 Feign 真实返回原因
*
* @date 2026/9/11
*/
@Slf4j
@Service
public class WarehouseInfoApplicationService {
@Autowired
private SystemServiceFeign systemServiceFeign;
@Autowired
private ReservationStockInOrderMapper reservationStockInOrderMapper;
/**
* 获取仓库信息: Feign 优先, 失败走本地兜底; 两边都取不到抛 ServiceException
*
* @param warehouseId 仓库id
* @return 仓库信息(含编码/名称/单号生成规则/库存锁定模式等)
*/
public WarehouseFeignPO getWarehouseInfo(Long warehouseId) {
if (warehouseId == null) {
throw new ServiceException("仓库ID不能为空");
}
AjaxResult ajaxResult = null;
try {
ajaxResult = systemServiceFeign.getWarehouseInfoByWarehouseId(warehouseId);
} catch (Exception e) {
log.warn("获取仓库信息Feign调用异常, 走本地兜底查询, warehouseId={}, error={}", warehouseId, e.getMessage());
}
if (ajaxResult != null && "200".equals(String.valueOf(ajaxResult.get("code"))) && ajaxResult.get("data") != null) {
return com.alibaba.fastjson.JSON.parseObject(
com.alibaba.fastjson2.JSONObject.toJSONString(ajaxResult.get("data")), WarehouseFeignPO.class);
}
String remoteMsg = ajaxResult == null ? "Feign调用异常" : String.valueOf(ajaxResult.get("msg"));
log.warn("获取仓库信息Feign失败(返回: {}), 走本地兜底查询, warehouseId={}", remoteMsg, warehouseId);
WarehouseFeignPO local = reservationStockInOrderMapper.getWarehouseInfoFallback(warehouseId);
if (local == null) {
throw new ServiceException("获取仓库信息失败(Feign返回:" + remoteMsg + "), 且本地未查到仓库[warehouseId="
+ warehouseId + "], 请检查仓库是否存在/已启用");
}
return local;
}
}
@@ -1,554 +0,0 @@
package com.mhd.oms.application.service.businessDocumentOrder;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.mhd.common.core.exception.ServiceException;
import com.mhd.common.core.web.domain.AjaxResult;
import com.mhd.common.security.utils.SecurityUtils;
import com.mhd.oms.domain.businessDocumentOrder.entity.BusinessDocumentOrder;
import com.mhd.oms.domain.businessDocumentOrder.repository.mapper.BusinessDocumentOrderMapper;
import com.mhd.oms.domain.businessDocumentOrderAdjust.entity.BusinessDocumentOrderAdjust;
import com.mhd.oms.domain.businessDocumentOrderAdjust.repository.mapper.BusinessDocumentOrderAdjustMapper;
import com.mhd.oms.domain.businessDocumentOrderAdjustDetail.entity.BusinessDocumentOrderAdjustDetail;
import com.mhd.oms.domain.businessDocumentOrderAdjustDetail.repository.mapper.BusinessDocumentOrderAdjustDetailMapper;
import com.mhd.oms.domain.businessOrderAccount.BusinessOrderAccount;
import com.mhd.oms.domain.businessOrderAccount.repository.mapper.BusinessOrderAccountMapper;
import com.mhd.oms.domain.executeOrder.entity.ExecuteOrder;
import com.mhd.oms.domain.executeOrder.repository.mapper.ExecuteOrderMapper;
import com.mhd.oms.interfaces.dto.businessDocumentOrderAdjust.BusinessDocumentOrderAdjustAuditDTO;
import com.mhd.oms.interfaces.dto.businessDocumentOrderAdjust.BusinessDocumentOrderAdjustDTO;
import com.mhd.oms.interfaces.dto.businessDocumentOrderAdjust.BusinessDocumentOrderAdjustQueryDTO;
import com.mhd.oms.interfaces.dto.businessDocumentOrderAdjust.BusinessDocumentOrderAdjustRecordDTO;
import com.mhd.system.api.WlhyServiceFeign;
import com.mhd.system.api.model.LoginUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
/**
* 运输业务单订单调整应用服务
*
* 规则(定稿)
* 1. 每次调整都生成1 条主记录 + 1 条明细快照
* 2. 费用数据源 = 前端回传的费用明细 business_order_account(feeDetailList)
* 主表金额 deliveryFreight 为其 amount 合计(冗余汇总后端自动算)
* 3. 是否需审核一旦任一金额发生变化
* = feeDetailList 明细 amount 变化或主表金额列变化 -> 走业务主管审核
* 否则(仅改非金额字段) -> 直通
* 4. 涉及金额提交 -> 仅把业务单 review_status 置为调整待审核(3)主记录 AUDIT_STATUS=0(待审核)
* 不更新业务单数据不影响其它状态(order_status / execute_status 不动)
* 5. 审核通过 -> 覆盖业务单(金额) + 替换费用明细review_status 一律置为 1(已审核)
* 不影响 order_status / execute_status
* 审核驳回 -> 不更新业务单数据review_status 还原为调整前值(主记录 before_review_status)可再次调整
* 6. 直通(未改金额) -> 覆盖业务单可编辑字段 + deliveryFreight(明细合计)并替换 business_order_account
* (逻辑删旧 is_delete=1 + 插新 is_delete=0)不改动任何审核/执行状态
* 7. 前端以 amountChanged 告知是否改金额(默认前端判定)未传时后端兜底比对
*
* 说明review_status 原枚举 0未审/1通过/2驳回新增 3=调整待审核不改变原有用例
*
* @author dev
* @date 2026-09-09
*/
@Slf4j
@Service
public class BusinessDocumentOrderAdjustService {
private static final Integer AUDIT_WAIT = 0; // 调整记录待审核
private static final Integer AUDIT_PASS = 1; // 调整记录审核通过
private static final Integer AUDIT_REJECT = 2; // 调整记录驳回
/** 主表金额(费用)字段,任一变化触发审核 */
private static final List<String> FEE_FIELDS = Arrays.asList(
"deliveryFreight", "collectedFreight", "uncollectedFreight", "collectLessFreight",
"transportationUnitPrice", "infoFee", "freightUnitPrice", "cargoInsuranceAmount",
"freightCharges", "freightFee", "unloadingFee", "customsFee", "shortageFee",
"miscellaneousFee", "otherFee", "detentionFee");
/** 不允许被前端调整覆盖的系统字段 */
private static final List<String> PROTECTED_FIELDS = Arrays.asList(
"id", "goodsNumber",
"orderStatus", "reviewStatus", "reviewId", "review", "reviewTime", "reviewOpinion",
"salesManager", "auditStatus", "auditBy", "auditTime",
"executeStatus", "executeId", "executeTime", "executor",
"executeOrderId", "executeOrderNumber", "adjustStatus",
"isDelete", "delFlag", "createBy", "createByName", "createTime",
"updateBy", "updateByName", "updateTime",
"pushStatus", "pushTime", "pushBy", "pushByName",
"reviseApplyType", "reviseApplyStatus", "reviseApplyBy", "reviseApplyByName",
"reviseApplyReason", "reviseApplyTime",
"reservationOrderId", "reservationOrderNumber",
"mainOrderId", "mainOrderNum", "mergeOrderId", "mergeOrderNum",
"splicingOrderType", "splicingStatus", "deliveryProgress",
"orgId", "orgName", "organizationId", "organizationName", "topOrganizationId",
"realCreateBy", "realCreateName", "createCompanyId", "createUserId",
"szwlUserId"
);
@Autowired
private BusinessDocumentOrderMapper businessDocumentOrderMapper;
@Autowired
private BusinessOrderAccountMapper businessOrderAccountMapper;
@Autowired
private BusinessDocumentOrderAdjustMapper adjustMapper;
@Autowired
private BusinessDocumentOrderAdjustDetailMapper adjustDetailMapper;
@Autowired
private ExecuteOrderMapper executeOrderMapper;
@Autowired
private WlhyServiceFeign wlhyServiceFeign;
/**
* 提交调整
* 返回 data = [是否需审核, 调整记录ID]
*/
@Transactional(rollbackFor = Exception.class)
public AjaxResult adjust(BusinessDocumentOrderAdjustDTO dto) {
if (dto == null || dto.getOrder() == null || dto.getOrder().getId() == null) {
throw new ServiceException("参数错误:缺少业务单ID(order.id)");
}
BusinessDocumentOrder dbOrder = businessDocumentOrderMapper.selectById(dto.getOrder().getId());
if (ObjectUtil.isNull(dbOrder)) {
throw new ServiceException("业务单不存在或已被删除");
}
BusinessDocumentOrder front = dto.getOrder();
// 1. 当前库中费用明细
List<BusinessOrderAccount> dbFeeList = businessOrderAccountMapper.selectList(
new LambdaQueryWrapper<BusinessOrderAccount>()
.eq(BusinessOrderAccount::getOrderId, dbOrder.getId())
.eq(BusinessOrderAccount::getIsDelete, 0));
// 2. 调整后的费用明细(前端) null 则视为保持库中不变
List<BusinessOrderAccount> newFeeList = dto.getFeeDetailList();
// 3. 生成"调整后"业务单对象DB原值 + 前端可编辑字段覆盖(忽略null)
BusinessDocumentOrder afterOrder = buildAfterOrder(dbOrder, front);
// 4. 若提供了费用明细 deliveryFreight(合计)=明细 amount 之和并联动到 afterOrder
boolean hasFeeDetail = newFeeList != null && !newFeeList.isEmpty();
if (hasFeeDetail) {
BigDecimal sum = sumAmount(newFeeList);
afterOrder.setDeliveryFreight(sum);
}
// 5. 判定是否修改了金额以前端 amountChanged 为准(前端整单回传已自行判定)
// 若前端未传则后端兜底比对(主表金额列 明细 amount 变化)
boolean amountChanged;
if (dto.getAmountChanged() != null) {
amountChanged = dto.getAmountChanged();
} else {
amountChanged = isAmountChanged(dbOrder, front, hasFeeDetail, dbFeeList, newFeeList);
}
// 6. 写主记录
LoginUser loginUser = SecurityUtils.getLoginUser();
Date now = new Date();
BusinessDocumentOrderAdjust adjust = buildAdjust(dbOrder, loginUser, now, amountChanged, dto.getAdjustReason());
adjustMapper.insert(adjust);
// 7. 写明细快照(调整后业务单整行 + 费用明细JSON)
BusinessDocumentOrderAdjustDetail detail = new BusinessDocumentOrderAdjustDetail();
copySnapshot(afterOrder, detail);
if (hasFeeDetail) {
detail.setFeeDetailJson(JSONUtil.toJsonStr(newFeeList));
}
detail.setAdjustId(adjust.getId());
detail.setOrderId(dbOrder.getId());
detail.setGoodsNumber(dbOrder.getGoodsNumber());
detail.setCreateBy(adjust.getAdjustBy());
detail.setCreateName(adjust.getAdjustByName());
detail.setCreateTime(now);
detail.setDelFlag(1);
adjustDetailMapper.insert(detail);
if (!amountChanged) {
// 8a. 直通覆盖主表可编辑字段( deliveryFreight 合计) + 替换费用明细不改动任何审核/执行状态
saveOrderAndFee(dbOrder.getId(), afterOrder, hasFeeDetail ? newFeeList : dbFeeList, loginUser, now, false, null);
updateAdjustPass(adjust.getId(), adjust.getAdjustBy(), adjust.getAdjustByName(), now, null);
syncTmsAsync(dbOrder.getGoodsNumber(), afterOrder);
} else {
// 8b. 涉及金额仅把业务单 review_status 置为"调整待审核(3)"其余状态(exec/orderStatus)不动不更新业务单数据
BusinessDocumentOrder statusOnly = new BusinessDocumentOrder();
statusOnly.setId(dbOrder.getId());
statusOnly.setReviewStatus(3L); // 调整待审核
businessDocumentOrderMapper.updateById(statusOnly);
}
return AjaxResult.success(new Object[]{amountChanged, adjust.getId()});
}
/**
* 调整记录列表(管理页查全部可选按订单/业务单号/审核状态等筛选配合分页使用)
*/
public List<BusinessDocumentOrderAdjustRecordDTO> listAdjustRecord(BusinessDocumentOrderAdjustQueryDTO query) {
LambdaQueryWrapper<BusinessDocumentOrderAdjust> wrapper = new LambdaQueryWrapper<BusinessDocumentOrderAdjust>()
.eq(BusinessDocumentOrderAdjust::getDelFlag, 1);
// ===== 组织隔离(参考业务单 queryList 口径) =====
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null && loginUser.getUserPo() != null) {
Long loginOrgId = loginUser.getUserPo().getOrganizationId();
Long loginTopOrgId = loginUser.getUserPo().getTopOrganizationId();
// 一级组织非1(集团)按一级组织过滤(可看本一级组织下所有)
if (loginTopOrgId != null && loginTopOrgId != 1) {
wrapper.eq(BusinessDocumentOrderAdjust::getTopOrgId, String.valueOf(loginTopOrgId));
} else if (loginOrgId != null) {
// 否则按当前组织过滤若查询方显式指定了 organizationId 则以其为准
Long effOrgId = (query != null && query.getOrganizationId() != null)
? query.getOrganizationId() : loginOrgId;
if (effOrgId != null) {
wrapper.eq(BusinessDocumentOrderAdjust::getOrgId, String.valueOf(effOrgId));
}
}
} else if (query != null && query.getOrganizationId() != null) {
wrapper.eq(BusinessDocumentOrderAdjust::getOrgId, String.valueOf(query.getOrganizationId()));
}
if (query != null) {
if (query.getOrderId() != null) {
wrapper.eq(BusinessDocumentOrderAdjust::getOrderId, query.getOrderId());
}
if (StrUtil.isNotBlank(query.getGoodsNumber())) {
wrapper.like(BusinessDocumentOrderAdjust::getGoodsNumber, query.getGoodsNumber());
}
if (query.getIsAudit() != null) {
wrapper.eq(BusinessDocumentOrderAdjust::getIsAudit, query.getIsAudit());
}
if (query.getAuditStatus() != null) {
wrapper.eq(BusinessDocumentOrderAdjust::getAuditStatus, query.getAuditStatus());
}
if (StrUtil.isNotBlank(query.getAdjustByName())) {
wrapper.like(BusinessDocumentOrderAdjust::getAdjustByName, query.getAdjustByName());
}
}
wrapper.orderByDesc(BusinessDocumentOrderAdjust::getAdjustTime);
List<BusinessDocumentOrderAdjust> list = adjustMapper.selectList(wrapper);
return list.stream().map(a -> {
BusinessDocumentOrderAdjustRecordDTO vo = new BusinessDocumentOrderAdjustRecordDTO();
BeanUtil.copyProperties(a, vo);
return vo;
}).collect(Collectors.toList());
}
/**
* 按业务单查询调整记录(供订单详情页"调整记录"Tab用)
*/
public List<BusinessDocumentOrderAdjustRecordDTO> listAdjustRecordByOrder(Long orderId) {
BusinessDocumentOrderAdjustQueryDTO query = new BusinessDocumentOrderAdjustQueryDTO();
query.setOrderId(orderId);
return listAdjustRecord(query);
}
/**
* 调整记录详情(主记录 + 调整后明细快照)
*/
public BusinessDocumentOrderAdjustRecordDTO getAdjustRecord(Long adjustId) {
BusinessDocumentOrderAdjust adjust = adjustMapper.selectById(adjustId);
if (adjust == null) {
throw new ServiceException("调整记录不存在");
}
BusinessDocumentOrderAdjustRecordDTO vo = new BusinessDocumentOrderAdjustRecordDTO();
BeanUtil.copyProperties(adjust, vo);
BusinessDocumentOrderAdjustDetail detail = adjustDetailMapper.selectOne(
new LambdaQueryWrapper<BusinessDocumentOrderAdjustDetail>()
.eq(BusinessDocumentOrderAdjustDetail::getAdjustId, adjustId)
.eq(BusinessDocumentOrderAdjustDetail::getDelFlag, 1)
.last("LIMIT 1"));
vo.setDetail(detail);
return vo;
}
/**
* 调整审核(通过/驳回)业务主管操作
*/
@Transactional(rollbackFor = Exception.class)
public AjaxResult auditAdjust(BusinessDocumentOrderAdjustAuditDTO auditDTO) {
if (auditDTO == null || auditDTO.getAdjustId() == null) {
throw new ServiceException("参数错误:缺少调整记录ID");
}
BusinessDocumentOrderAdjust adjust = adjustMapper.selectById(auditDTO.getAdjustId());
if (adjust == null) {
throw new ServiceException("调整记录不存在");
}
if (adjust.getAuditStatus() == null || adjust.getAuditStatus() != AUDIT_WAIT) {
throw new ServiceException("该调整记录非待审核状态,无法审核");
}
Integer result = auditDTO.getAuditResult();
if (result == null || (result != AUDIT_PASS && result != AUDIT_REJECT)) {
throw new ServiceException("审核结果非法");
}
LoginUser loginUser = SecurityUtils.getLoginUser();
String auditBy = loginUser != null ? String.valueOf(loginUser.getUserid()) : null;
String auditByName = loginUser != null && loginUser.getWlhyLoginUser() != null ? loginUser.getWlhyLoginUser().getRealname() : null;
Date now = new Date();
if (result == AUDIT_PASS) {
BusinessDocumentOrder order = businessDocumentOrderMapper.selectById(adjust.getOrderId());
if (ObjectUtil.isNull(order)) {
throw new ServiceException("业务单不存在");
}
BusinessDocumentOrderAdjustDetail detail = adjustDetailMapper.selectOne(
new LambdaQueryWrapper<BusinessDocumentOrderAdjustDetail>()
.eq(BusinessDocumentOrderAdjustDetail::getAdjustId, adjust.getId())
.eq(BusinessDocumentOrderAdjustDetail::getDelFlag, 1)
.last("LIMIT 1"));
// 还原调整后业务单(从JSON整行快照)
BusinessDocumentOrder snapshot = null;
if (detail != null && StrUtil.isNotBlank(detail.getOrderSnapshotJson())) {
try {
snapshot = JSONUtil.toBean(detail.getOrderSnapshotJson(), BusinessDocumentOrder.class);
} catch (Exception e) {
log.warn("解析调整快照JSON失败", e);
}
}
BusinessDocumentOrder toSave = new BusinessDocumentOrder();
toSave.setId(order.getId());
if (snapshot != null) {
BeanUtil.copyProperties(snapshot, toSave, CopyOptions.create().setIgnoreNullValue(true)
.setIgnoreProperties(toArray()));
} else if (detail != null) {
BeanUtil.copyProperties(detail, toSave, CopyOptions.create().setIgnoreNullValue(true)
.setIgnoreProperties(toArray()));
}
// 还原调整后费用明细( feeDetailJson)
List<BusinessOrderAccount> newFeeList = new ArrayList<>();
if (detail != null && StrUtil.isNotBlank(detail.getFeeDetailJson())) {
try {
newFeeList = JSONUtil.toList(detail.getFeeDetailJson(), BusinessOrderAccount.class);
} catch (Exception e) {
log.warn("解析调整费用明细JSON失败", e);
}
}
// 若快照中有明细合计则保证 deliveryFreight 一致
if (!newFeeList.isEmpty()) {
toSave.setDeliveryFreight(sumAmount(newFeeList));
}
// 审核通过后 review_status 一律置为 1(已审核)不影响 order_status / execute_status
toSave.setReviewStatus(1L);
// 覆盖业务单 + 替换费用明细
saveOrderAndFee(order.getId(), toSave, newFeeList, loginUser, now, false, null);
updateAdjustPass(adjust.getId(), auditBy, auditByName, now, auditDTO.getAuditRemark());
syncTmsAsync(order.getGoodsNumber(), toSave);
} else {
adjust.setAuditStatus(AUDIT_REJECT);
adjust.setAuditBy(auditBy);
adjust.setAuditByName(auditByName);
adjust.setAuditTime(now);
adjust.setAuditRemark(auditDTO.getAuditRemark());
adjust.setUpdateBy(auditBy);
adjust.setUpdateTime(now);
adjustMapper.updateById(adjust);
// 驳回不更新业务单数据仅把 review_status 还原为调整前值(不影响其它状态)
BusinessDocumentOrder statusOnly = new BusinessDocumentOrder();
statusOnly.setId(adjust.getOrderId());
statusOnly.setReviewStatus(adjust.getBeforeReviewStatus() == null ? 0L : adjust.getBeforeReviewStatus());
businessDocumentOrderMapper.updateById(statusOnly);
}
return AjaxResult.success();
}
// =====================================================
// 私有工具
// =====================================================
private BusinessDocumentOrder buildAfterOrder(BusinessDocumentOrder dbOrder, BusinessDocumentOrder front) {
BusinessDocumentOrder after = new BusinessDocumentOrder();
BeanUtil.copyProperties(dbOrder, after, CopyOptions.create().setIgnoreNullValue(true));
BeanUtil.copyProperties(front, after, CopyOptions.create().setIgnoreNullValue(true)
.setIgnoreProperties(toArray()));
after.setId(dbOrder.getId());
return after;
}
private BusinessDocumentOrderAdjust buildAdjust(BusinessDocumentOrder dbOrder, LoginUser u, Date now,
boolean amountChanged, String reason) {
BusinessDocumentOrderAdjust adjust = new BusinessDocumentOrderAdjust();
adjust.setOrderId(dbOrder.getId());
adjust.setGoodsNumber(dbOrder.getGoodsNumber());
adjust.setAdjustBy(u != null ? String.valueOf(u.getUserid()) : null);
adjust.setAdjustByName(u != null && u.getWlhyLoginUser() != null ? u.getWlhyLoginUser().getRealname() : null);
adjust.setAdjustTime(now);
adjust.setAdjustReason(reason);
adjust.setIsAudit(amountChanged ? 1 : 0);
adjust.setAuditStatus(amountChanged ? AUDIT_WAIT : AUDIT_PASS);
adjust.setBeforeReviewStatus(dbOrder.getReviewStatus());
// 组织信息参考业务单口径取当前登录用户所属组织(调整人归属)
if (u != null && u.getUserPo() != null) {
if (u.getUserPo().getOrganizationId() != null) {
adjust.setOrgId(String.valueOf(u.getUserPo().getOrganizationId()));
} else {
adjust.setOrgId(dbOrder.getOrgId());
}
if (u.getUserPo().getTopOrganizationId() != null) {
adjust.setTopOrgId(String.valueOf(u.getUserPo().getTopOrganizationId()));
} else if (dbOrder.getTopOrganizationId() != null) {
adjust.setTopOrgId(String.valueOf(dbOrder.getTopOrganizationId()));
}
} else {
adjust.setOrgId(dbOrder.getOrgId());
if (dbOrder.getTopOrganizationId() != null) {
adjust.setTopOrgId(String.valueOf(dbOrder.getTopOrganizationId()));
}
}
adjust.setCreateBy(adjust.getAdjustBy());
adjust.setCreateName(adjust.getAdjustByName());
adjust.setCreateTime(now);
adjust.setDelFlag(1);
return adjust;
}
/**
* 是否修改了金额主表金额列变化 明细 amount 变化
*/
private boolean isAmountChanged(BusinessDocumentOrder dbOrder, BusinessDocumentOrder front,
boolean hasFeeDetail, List<BusinessOrderAccount> dbFeeList,
List<BusinessOrderAccount> newFeeList) {
// a) 主表金额列比对
boolean mainChanged = FEE_FIELDS.stream().anyMatch(field -> {
Object newVal = BeanUtil.getFieldValue(front, field);
if (newVal == null) {
return false;
}
Object oldVal = BeanUtil.getFieldValue(dbOrder, field);
BigDecimal old = oldVal == null ? BigDecimal.ZERO : (BigDecimal) oldVal;
BigDecimal now = (BigDecimal) newVal;
return old.compareTo(now) != 0;
});
if (mainChanged) {
return true;
}
// b) 若前端未传明细则只看主表若传了明细比较明细金额
if (!hasFeeDetail) {
return false;
}
// 明细合计对比
BigDecimal oldSum = sumAmount(dbFeeList);
BigDecimal newSum = sumAmount(newFeeList);
return oldSum.compareTo(newSum) != 0;
}
private BigDecimal sumAmount(List<BusinessOrderAccount> list) {
BigDecimal sum = BigDecimal.ZERO;
if (list == null) {
return sum;
}
for (BusinessOrderAccount acc : list) {
if (acc.getAmount() != null) {
sum = sum.add(acc.getAmount());
}
}
return sum;
}
/**
* 覆盖业务单可编辑字段 + 替换费用明细(逻辑删旧 + 插新)
*/
/**
* 覆盖业务单可编辑字段 + 替换费用明细(逻辑删旧 + 插新)
* @param restoreReview 是否还原审核状态(审核通过/驳回后 true直通 false 不改任何审核状态)
* @param beforeReviewStatus 调整前 review_status用于还原
*/
private void saveOrderAndFee(Long orderId, BusinessDocumentOrder toSave,
List<BusinessOrderAccount> newFeeList,
LoginUser loginUser, Date now,
boolean restoreReview, Long beforeReviewStatus) {
if (restoreReview) {
// 仅还原 review_status 到调整前值不影响 order_status / execute_status 等其它状态
toSave.setReviewStatus(beforeReviewStatus == null ? 0L : beforeReviewStatus);
}
businessDocumentOrderMapper.updateById(toSave);
// 逻辑删旧明细
List<BusinessOrderAccount> oldFeeList = businessOrderAccountMapper.selectList(
new LambdaQueryWrapper<BusinessOrderAccount>()
.eq(BusinessOrderAccount::getOrderId, orderId)
.eq(BusinessOrderAccount::getIsDelete, 0));
if (oldFeeList != null) {
for (BusinessOrderAccount oldAcc : oldFeeList) {
oldAcc.setIsDelete(1);
oldAcc.setUpdateBy(loginUser != null ? String.valueOf(loginUser.getUserid()) : null);
oldAcc.setUpdateTime(now);
businessOrderAccountMapper.updateById(oldAcc);
}
}
// 插新明细
if (newFeeList != null && !newFeeList.isEmpty()) {
for (BusinessOrderAccount acc : newFeeList) {
acc.setId(IdUtil.randomUUID());
acc.setOrderId(String.valueOf(orderId));
acc.setIsDelete(0);
acc.setCreateBy(loginUser != null ? String.valueOf(loginUser.getUserid()) : null);
acc.setCreateTime(now);
businessOrderAccountMapper.insert(acc);
}
}
}
private void copySnapshot(BusinessDocumentOrder order, BusinessDocumentOrderAdjustDetail detail) {
BeanUtil.copyProperties(order, detail, CopyOptions.create().setIgnoreNullValue(true)
.setIgnoreProperties("id", "adjustId", "orderId", "createBy", "createName",
"createTime", "updateBy", "updateTime", "delFlag",
"orderSnapshotJson", "feeDetailJson"));
try {
detail.setOrderSnapshotJson(JSONUtil.toJsonStr(order));
} catch (Exception e) {
log.warn("生成调整快照JSON失败", e);
}
}
private void updateAdjustPass(Long adjustId, String auditBy, String auditByName, Date auditTime, String remark) {
BusinessDocumentOrderAdjust adjust = new BusinessDocumentOrderAdjust();
adjust.setId(adjustId);
adjust.setAuditStatus(AUDIT_PASS);
adjust.setAuditBy(auditBy);
adjust.setAuditByName(auditByName);
adjust.setAuditTime(auditTime);
adjust.setAuditRemark(remark);
adjust.setUpdateBy(auditBy);
adjust.setUpdateTime(auditTime);
adjustMapper.updateById(adjust);
}
private String[] toArray() {
return PROTECTED_FIELDS.toArray(new String[0]);
}
private void syncTmsAsync(String goodsNumber, BusinessDocumentOrder after) {
try {
List<ExecuteOrder> executeOrders = executeOrderMapper.selectList(
new LambdaQueryWrapper<ExecuteOrder>()
.eq(ExecuteOrder::getGoodsNumber, goodsNumber)
.eq(ExecuteOrder::getDelFlag, 1));
if (executeOrders == null || executeOrders.isEmpty()) {
return;
}
log.info("业务单[{}]调整完成,关联执行单[{}]。如需回写TMS请接入TMS改费Feign。", goodsNumber,
executeOrders.stream().map(ExecuteOrder::getGoodsNumber).collect(Collectors.toList()));
} catch (Exception e) {
log.error("同步TMS失败,不影响主流程", e);
}
}
}
@@ -11,6 +11,7 @@ import com.mhd.oms.domain.outEntrustOrder.repository.po.OutEntrustOrderPO;
import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDO;
import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDetailDO;
import com.mhd.oms.domain.outEntrustOrder.service.OutEntrustOrderDomainService;
import com.mhd.oms.domain.outEntrustOrder.repository.listener.OutEntrustExcelParseService;
import com.mhd.system.api.SystemServiceFeign;
import com.mhd.system.api.WmsServiceFeign;
import com.mhd.system.api.domain.NoticeMaterialDetailDTO;
@@ -20,8 +21,10 @@ import com.mhd.system.api.model.LoginUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
@@ -46,9 +49,15 @@ public class OutEntrustOrderApplicationService {
@Resource
private SystemServiceFeign systemServiceFeign;
@Resource
private com.mhd.oms.application.service.WarehouseInfoApplicationService warehouseInfoApplicationService;
@Resource
private WmsServiceFeign wmsServiceFeign;
@Resource
private OutEntrustExcelParseService outEntrustExcelParseService;
/**
* 分页查询出仓委托单列表(不做默认状态过滤,显示全部数据)
*/
@@ -105,6 +114,45 @@ public class OutEntrustOrderApplicationService {
return outEntrustOrderDomainService.insert(outEntrustOrderDO);
}
/**
* 导入出仓委托单(模板与WMS出库管理导入一致前11行表头+第12行起明细)
* 解析校验通过后复用新增链路落库生成CKWT委托单号状态待审核/未下发
*
* @return 校验错误信息空串表示导入成功
*/
public StringBuilder importOutEntrustOrder(MultipartFile file, Long warehouseId, String warehouseCode, String warehouseName) throws IOException {
//解析+校验(客户解析/物料匹配/字典转换/必填项)
OutEntrustExcelParseService.ParseResult parseResult =
outEntrustExcelParseService.parseOutEntrustOrderExcel(file, warehouseName, warehouseCode, warehouseId);
if (parseResult.getErrorMsg().length() > 0) {
return parseResult.getErrorMsg();
}
OutEntrustOrderDO outEntrustOrderDO = parseResult.getOrder();
//审计信息(与WMS出库导入一致导入人=当前登录人)
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null) {
String userRealName;
if (loginUser.getUserPo() != null && loginUser.getUserPo().getUserName() != null
&& !loginUser.getUserPo().getUserName().trim().isEmpty()) {
userRealName = loginUser.getUserPo().getUserName();
} else if (loginUser.getRealname() != null && !loginUser.getRealname().trim().isEmpty()) {
userRealName = loginUser.getRealname();
} else {
userRealName = loginUser.getUsername();
}
Date now = new Date();
outEntrustOrderDO.setCreateBy(loginUser.getUserid());
outEntrustOrderDO.setCreateByName(userRealName);
outEntrustOrderDO.setCreateTime(now);
outEntrustOrderDO.setUpdateBy(loginUser.getUserid());
outEntrustOrderDO.setUpdateByName(userRealName);
outEntrustOrderDO.setUpdateTime(now);
}
//复用新增链路必填校验默认普通出库仓库校验组织信息汇总申报数量生成CKWT委托单号(待审核/未下发)
insert(outEntrustOrderDO);
return new StringBuilder();
}
/**
* 修改出仓委托单(仅待审核/已驳回且未下发可修改已驳回单编辑后状态重置为待审核)
*/
@@ -352,14 +400,10 @@ public class OutEntrustOrderApplicationService {
}
/**
* 仓库信息校验并回填编码/名称
* 仓库信息校验并回填编码/名称 (Feign失败走本地跨库兜底)
*/
private void setWarehouseInfo(OutEntrustOrderDO outEntrustOrderDO) {
AjaxResult ajaxResult = systemServiceFeign.getWarehouseInfoByWarehouseId(outEntrustOrderDO.getWarehouseId());
if (ajaxResult == null || !"200".equals(String.valueOf(ajaxResult.get("code")))) {
throw new ServiceException("获取仓库信息失败,请检查仓库是否有效");
}
WarehouseFeignPO warehouseFeignPO = JSONUtil.toBean(JSONUtil.toJsonStr(ajaxResult.get("data")), WarehouseFeignPO.class);
WarehouseFeignPO warehouseFeignPO = warehouseInfoApplicationService.getWarehouseInfo(outEntrustOrderDO.getWarehouseId());
if (warehouseFeignPO == null || Objects.isNull(warehouseFeignPO.getWarehouseId())) {
throw new ServiceException("仓库不存在,请重新选择");
}
@@ -659,7 +659,7 @@ public class BusinessDocumentOrder extends BaseVOEntity{
@Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()")
private Date documentDate;
@ApiModelProperty( "审核状态 0-未审核 1-审核通过 2-已驳回 3-调整待审核")
@ApiModelProperty( "审核状态 0-未审核 1-审核通过 2-已驳回")
private Long reviewStatus;
@ApiModelProperty("审核时间")
@@ -825,7 +825,4 @@ public class BusinessDocumentOrder extends BaseVOEntity{
@ApiModelProperty("车次")
@Excel(name = "车次")
private String shuttleNo;
@ApiModelProperty("业务单调整状态 0-无 1-调整待审核 2-调整驳回 3-调整已通过")
private Integer adjustStatus;
}
@@ -39,8 +39,8 @@ public interface BusinessDocumentOrderMapper extends BaseMapper<BusinessDocument
String getPushVerifyByOrgId(@Param("organizationId") Long organizationId);
/**
* 统计指定日期前缀(yyMMdd)下已生成的新格式业务单号数量
* 统计新格式(长度<=12)过滤历史16位旧数据保证序号每日从001正确累计
* 查询指定日期前缀(yyMMdd)下已生成的新格式业务单号列表
* 新格式(长度<=12)过滤历史16位旧数据供取号器解析当日最大序号
*/
int countByGoodsNumberPrefix(@Param("prefix") String prefix);
List<String> selectGoodsNumbersByPrefix(@Param("prefix") String prefix);
}
@@ -237,11 +237,8 @@ public class BusinessDocumentOrderImpl extends ServiceImpl<BusinessDocumentOrder
subOrder.setRecipientName(user.getUserName());
//生成子单货源单号yyMMdd + 当日顺序号
String orderCode = goodsNumberGenerator.generateUniqueGoodsNumber(subOrder.getDocumentDate());
subOrder.setGoodsNumber(orderCode);
businessDocumentOrderMapper.insert(subOrder);
//生成子单货源单号yyMMdd + 当日顺序号并落库, 撞唯一索引自动重新取号重试
goodsNumberGenerator.insertWithUniqueGoodsNumber(subOrder);
subOrderCount++; // 增加子单计数
// 保存子单的地址信息到BUSINESS_DOCUMENT_ADDRESS_MULTI表
@@ -285,24 +285,9 @@ public class BusinessDocumentOrderDomainService {
}
}
//生成货源单号yyMMdd + 当日顺序号每日重置
String orderCode = goodsNumberGenerator.generateUniqueGoodsNumber(tmsOrder.getDocumentDate());
tmsOrder.setGoodsNumber(orderCode);
ReservationStockOutOrder reservationStockOutOrder = reservationStockOutOrderMapper.selectOne(new LambdaQueryWrapper<ReservationStockOutOrder>()
.eq(ReservationStockOutOrder::getOutOrderNumber, tmsOrder.getInOrderNumber())
.eq(ReservationStockOutOrder::getDelFlag, 1));
if (reservationStockOutOrder != null){
reservationStockOutOrder.setBusinessNumber(tmsOrder.getGoodsNumber());
reservationStockOutOrderMapper.updateById(reservationStockOutOrder);
}
ReservationStockInOrder reservationStockInOrder = reservationStockInOrderMapper.selectOne(new LambdaQueryWrapper<ReservationStockInOrder>()
.eq(ReservationStockInOrder::getInOrderNumber, tmsOrder.getInOrderNumber())
.eq(ReservationStockInOrder::getDelFlag, 1));
if (reservationStockInOrder != null){
reservationStockInOrder.setBusinessNumber(tmsOrder.getGoodsNumber());
reservationStockInOrderMapper.updateById(reservationStockInOrder);
}
//生成货源单号并落库见下方 insertWithUniqueGoodsNumber:
//取号与插单合并为原子重试(撞goods_number唯一索引自动重新取号),
//出库/入库业务单的单号回填也移至插单成功之后,避免插单失败留下脏回填
List<BusinessDocumentAddressMultiDTO> loadingAddressList = businessDocumentOrderDO.getLoadingAddressList();//装货地信息集合
List<BusinessDocumentAddressMultiDTO> unloadAddressList = businessDocumentOrderDO.getUnloadAddressList();//卸货地信息集合
@@ -426,7 +411,24 @@ public class BusinessDocumentOrderDomainService {
//一天后
tmsOrder.setDocumentDate(businessDocumentOrderDO.getDocumentDate());
boolean istrue = businessDocumentOrderMapper.insert(tmsOrder) > 0;
//生成货源单号yyMMdd + 当日顺序号每日重置并落库, 撞唯一索引自动重新取号重试
boolean istrue = goodsNumberGenerator.insertWithUniqueGoodsNumber(tmsOrder);
//落库成功后将运输业务单号回填至关联的出库/入库业务单
ReservationStockOutOrder reservationStockOutOrder = reservationStockOutOrderMapper.selectOne(new LambdaQueryWrapper<ReservationStockOutOrder>()
.eq(ReservationStockOutOrder::getOutOrderNumber, tmsOrder.getInOrderNumber())
.eq(ReservationStockOutOrder::getDelFlag, 1));
if (reservationStockOutOrder != null){
reservationStockOutOrder.setBusinessNumber(tmsOrder.getGoodsNumber());
reservationStockOutOrderMapper.updateById(reservationStockOutOrder);
}
ReservationStockInOrder reservationStockInOrder = reservationStockInOrderMapper.selectOne(new LambdaQueryWrapper<ReservationStockInOrder>()
.eq(ReservationStockInOrder::getInOrderNumber, tmsOrder.getInOrderNumber())
.eq(ReservationStockInOrder::getDelFlag, 1));
if (reservationStockInOrder != null){
reservationStockInOrder.setBusinessNumber(tmsOrder.getGoodsNumber());
reservationStockInOrderMapper.updateById(reservationStockInOrder);
}
if (hasDetails) {
for (BusinessDeliveryDetailsLinkDTO businessDeliveryDetailsLinkDTO : businessDeliveryDetailsLinkDTOList) {
@@ -5,23 +5,44 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.mhd.common.core.exception.ServiceException;
import com.mhd.oms.domain.businessDocumentOrder.entity.BusinessDocumentOrder;
import com.mhd.oms.domain.businessDocumentOrder.repository.mapper.BusinessDocumentOrderMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 业务单据订单(goodsNumber)单号生成器
* 规则: yyMMdd(取自单据日期) + 当日顺序号(每日重置, 3位起步, 超999自动扩位)
* 示例: 2608040012608049992608041000
*
* 唯一性保障(不依赖唯一索引, 三层防线):
* 1) 取号: 当日已生成单号 MAX+1 作为基线 + 候选号存在性校验后移收敛
* (对齐入库单号修复先例 ReservationStockInOrderDomainService#generateReserveOrderNumber)
* 2) 落库: 按日期前缀分段的JVM锁串行化"取号+插单", 消除本实例内并发审核/
* 批量取号的check-then-act竞态(多实例部署时本层只覆盖各自实例)
* 3) 自检自愈: 插单后立即校验同号数量, >1 说明与并发插入撞号, 给本单重新
* 取号改号, 兜住跨实例残余竞态
* 若日后补建goods_number唯一索引( sql/20260906_oms_business_goods_number_unique.sql),
* 撞号将抛DuplicateKeyException由重试逻辑接住, 与上述防线叠加互不冲突
*/
@Slf4j
@Component
public class GoodsNumberGenerator {
private static final int MAX_RETRY = 100;
/**
* 取号+插单分段锁: 按单号日期前缀(yyMMdd)分段, 同日串行跨日并行,
* 锁对象常驻(每日新增1个, 增量可忽略)
*/
private static final Map<String, Object> LOCK_STRIPES = new ConcurrentHashMap<>();
@Autowired
private BusinessDocumentOrderMapper businessDocumentOrderMapper;
@@ -33,8 +54,21 @@ public class GoodsNumberGenerator {
Date date = (documentDate == null) ? new Date() : documentDate;
String datePart = DateUtil.format(date, "yyMMdd");
// count 只查一次作为序号基线
long seq = (long) businessDocumentOrderMapper.countByGoodsNumberPrefix(datePart) + 1;
// 当日已生成单号的最大序号作为基线(MAX+1而非COUNT+1, 防脏数据/序号错位导致回退复用)
List<String> existNumbers = businessDocumentOrderMapper.selectGoodsNumbersByPrefix(datePart);
long seq = 0;
for (String existNumber : existNumbers) {
if (existNumber == null || existNumber.length() <= datePart.length()) {
continue;
}
String numStr = existNumber.substring(datePart.length());
if (!numStr.matches("\\d+")) {
continue; // 防脏数据(非纯数字后缀)导致解析失败
}
seq = Math.max(seq, Long.parseLong(numStr));
}
seq = seq + 1;
int retryCount = 0;
while (retryCount < MAX_RETRY) {
String orderCode = datePart + String.format("%03d", seq);
@@ -46,8 +80,60 @@ public class GoodsNumberGenerator {
if (CollectionUtils.isEmpty(exist)) {
return orderCode;
}
seq++; // 关键查重失败序号往后递增找空位而不是重新count
seq++; // 关键查重失败序号往后递增找空位而不是重新取基线
}
throw new ServiceException("生成唯一业务单号失败,已达到最大重试次数:" + MAX_RETRY);
}
/**
* 生成唯一业务单号并落库: JVM分段锁内完成"取号+插单",
* 插入撞goods_number唯一索引时自动重新取号重试(索引为可选兜底),
* 插单成功后自检同号并自愈改号
* 供业务单创建入口(手动/审核新增审核拆单预约确认)统一使用
* @return 是否插入成功(mapper.insert结果)
*/
public boolean insertWithUniqueGoodsNumber(BusinessDocumentOrder order) {
Date date = (order.getDocumentDate() == null) ? new Date() : order.getDocumentDate();
String lockKey = DateUtil.format(date, "yyMMdd");
synchronized (LOCK_STRIPES.computeIfAbsent(lockKey, k -> new Object())) {
int retryCount = 0;
while (retryCount < MAX_RETRY) {
retryCount++;
order.setGoodsNumber(generateUniqueGoodsNumber(order.getDocumentDate()));
try {
boolean inserted = businessDocumentOrderMapper.insert(order) > 0;
// 插单后自检自愈: 无唯一索引时跨实例并发窗口内的撞号双方都能落库,
// 同号数量>1说明本单撞号, 给本单重新取号改号, 防止单号重复扩散
healIfDuplicated(order);
return inserted;
} catch (DuplicateKeyException e) {
// 已建唯一索引时的兜底: 仅业务单号撞唯一索引时重试, 其他唯一约束冲突原样抛出
String message = e.getMessage();
if (message == null || !message.toLowerCase().contains("goods_number")) {
throw e;
}
log.warn("业务单号[{}]撞号, 自动重新取号重试(第{}次): {}", order.getGoodsNumber(), retryCount, message);
}
}
throw new ServiceException("生成唯一业务单号失败,已达到最大重试次数:" + MAX_RETRY);
}
}
/**
* 插单后自检自愈: 查同号数量, >1 说明有并发插入撞号(本单为后插入方),
* 保持先落库单据单号不变, 重新取号给本单改号
*/
private void healIfDuplicated(BusinessDocumentOrder order) {
List<BusinessDocumentOrder> sameNumber = businessDocumentOrderMapper.selectList(
new QueryWrapper<BusinessDocumentOrder>().eq("goods_number", order.getGoodsNumber())
);
if (sameNumber != null && sameNumber.size() > 1) {
String oldNumber = order.getGoodsNumber();
String newNumber = generateUniqueGoodsNumber(order.getDocumentDate());
order.setGoodsNumber(newNumber);
businessDocumentOrderMapper.updateById(order);
log.warn("业务单号[{}]与并发插入撞号(无唯一索引兜底), 已自愈改号为[{}], orderId={}",
oldNumber, newNumber, order.getId());
}
}
}
@@ -1,93 +0,0 @@
package com.mhd.oms.domain.businessDocumentOrderAdjust.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 运输业务单 调整主记录
*
* @author dev
* @date 2026-09-09
*/
@Data
@TableName("business_document_order_adjust")
public class BusinessDocumentOrderAdjust implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("主键")
@TableId(type = IdType.ASSIGN_ID)
private Long id;
@ApiModelProperty("业务单ID")
private Long orderId;
@ApiModelProperty("业务单号")
private String goodsNumber;
@ApiModelProperty("调整人账号")
private String adjustBy;
@ApiModelProperty("调整人姓名")
private String adjustByName;
@ApiModelProperty("调整时间")
private Date adjustTime;
@ApiModelProperty("调整原因")
private String adjustReason;
@ApiModelProperty("是否需要审核 0-否 1-是")
private Integer isAudit;
@ApiModelProperty("调整前业务单审核状态(review_status原值,用于调整审核通过/驳回后还原,避免影响其它)")
private Long beforeReviewStatus;
@ApiModelProperty("审核状态 0-待审核 1-审核通过 2-驳回")
private Integer auditStatus;
@ApiModelProperty("审核人账号")
private String auditBy;
@ApiModelProperty("审核人姓名")
private String auditByName;
@ApiModelProperty("审核时间")
private Date auditTime;
@ApiModelProperty("审核/驳回意见")
private String auditRemark;
@ApiModelProperty("组织ID")
private String orgId;
@ApiModelProperty("一级组织ID")
private String topOrgId;
@ApiModelProperty("创建人")
private String createBy;
@ApiModelProperty("创建人姓名")
private String createName;
@ApiModelProperty("创建时间")
private Date createTime;
@ApiModelProperty("更新人")
private String updateBy;
@ApiModelProperty("更新人姓名")
private String updateName;
@ApiModelProperty("更新时间")
private Date updateTime;
@ApiModelProperty("删除标记 1-正常 0-删除")
private Integer delFlag;
}
@@ -1,12 +0,0 @@
package com.mhd.oms.domain.businessDocumentOrderAdjust.repository.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mhd.oms.domain.businessDocumentOrderAdjust.entity.BusinessDocumentOrderAdjust;
import org.apache.ibatis.annotations.Mapper;
/**
* 运输业务单 调整主记录 Mapper
*/
@Mapper
public interface BusinessDocumentOrderAdjustMapper extends BaseMapper<BusinessDocumentOrderAdjust> {
}
@@ -1,173 +0,0 @@
package com.mhd.oms.domain.businessDocumentOrderAdjustDetail.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 运输业务单 调整明细(调整后业务单整行快照)
*
* @author dev
* @date 2026-09-09
*/
@Data
@TableName("business_document_order_adjust_detail")
public class BusinessDocumentOrderAdjustDetail implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("主键")
@TableId(type = IdType.ASSIGN_ID)
private Long id;
@ApiModelProperty("关联调整主记录ID")
private Long adjustId;
@ApiModelProperty("业务单ID")
private Long orderId;
@ApiModelProperty("业务单号")
private String goodsNumber;
@ApiModelProperty("货物类型ID")
private String goodsTypeId;
@ApiModelProperty("货物名称")
private String goodsName;
@ApiModelProperty("车型")
private String carType;
@ApiModelProperty("装货地")
private String loadingName;
@ApiModelProperty("装货地址")
private String loadingAddress;
@ApiModelProperty("装货时间")
private Date loadingDate;
@ApiModelProperty("卸货地")
private String unloadName;
@ApiModelProperty("卸货地址")
private String unloadAddress;
@ApiModelProperty("卸货时间")
private Date unloadingData;
@ApiModelProperty("发货人")
private String freighterName;
@ApiModelProperty("装货电话")
private String loadingPhone;
@ApiModelProperty("收货人")
private String dischargerName;
@ApiModelProperty("卸货电话")
private String unloadPhone;
@ApiModelProperty("备注")
private String remarks;
@ApiModelProperty("线路")
private String route;
@ApiModelProperty("单据日期")
private Date documentDate;
@ApiModelProperty("组织ID")
private String orgId;
@ApiModelProperty("组织ID")
private Long organizationId;
@ApiModelProperty("组织名称")
private String organizationName;
@ApiModelProperty("一级组织ID")
private Long topOrganizationId;
// ==================== 金额/费用快照 ====================
@ApiModelProperty("发货运费")
private BigDecimal deliveryFreight;
@ApiModelProperty("已收运费")
private BigDecimal collectedFreight;
@ApiModelProperty("未收运费")
private BigDecimal uncollectedFreight;
@ApiModelProperty("少收运费")
private BigDecimal collectLessFreight;
@ApiModelProperty("运费单价(元/吨)")
private BigDecimal transportationUnitPrice;
@ApiModelProperty("信息费")
private BigDecimal infoFee;
@ApiModelProperty("计费单价")
private BigDecimal freightUnitPrice;
@ApiModelProperty("货物保险金额")
private BigDecimal cargoInsuranceAmount;
@ApiModelProperty("运费")
private BigDecimal freightCharges;
@ApiModelProperty("运费")
private BigDecimal freightFee;
@ApiModelProperty("卸货费")
private BigDecimal unloadingFee;
@ApiModelProperty("报关费")
private BigDecimal customsFee;
@ApiModelProperty("差货费")
private BigDecimal shortageFee;
@ApiModelProperty("杂费")
private BigDecimal miscellaneousFee;
@ApiModelProperty("其他费用")
private BigDecimal otherFee;
@ApiModelProperty("滞车费")
private BigDecimal detentionFee;
@ApiModelProperty("税率")
private BigDecimal taxRate;
@ApiModelProperty("创建人")
private String createBy;
@ApiModelProperty("创建人姓名")
private String createName;
@ApiModelProperty("创建时间")
private Date createTime;
@ApiModelProperty("更新人")
private String updateBy;
@ApiModelProperty("更新时间")
private Date updateTime;
@ApiModelProperty("删除标记 1-正常 0-删除")
private Integer delFlag;
@ApiModelProperty("调整后业务单整行JSON快照(用于全字段精确还原)")
private String orderSnapshotJson;
@ApiModelProperty("调整后费用明细JSON快照(business_order_account列表)")
private String feeDetailJson;
}
@@ -1,12 +0,0 @@
package com.mhd.oms.domain.businessDocumentOrderAdjustDetail.repository.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mhd.oms.domain.businessDocumentOrderAdjustDetail.entity.BusinessDocumentOrderAdjustDetail;
import org.apache.ibatis.annotations.Mapper;
/**
* 运输业务单 调整明细 Mapper
*/
@Mapper
public interface BusinessDocumentOrderAdjustDetailMapper extends BaseMapper<BusinessDocumentOrderAdjustDetail> {
}
@@ -501,6 +501,6 @@ public class ExecutionInMaterialDetailPO extends ExecutionStockInOrderBasePO {
private BigDecimal insureAmount;
@ApiModelProperty("货物类型:0-物料 1-半成品 2-成品")
@Excel(name = "货物类型")
@Excel(name = "货物类型", readConverterExp = "0=物料,1=半成品,2=成品")
private Integer goodsType;
}
@@ -1,5 +1,7 @@
package com.mhd.oms.domain.executionInMaterialDetail.repository.po;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.mhd.common.core.annotation.Excel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@@ -15,21 +17,34 @@ import java.util.Date;
public class ExecutionInMaterialDetailWithOrderPO extends ExecutionInMaterialDetailPO {
private static final long serialVersionUID = 1L;
@ApiModelProperty("入库单状态: 1-已创建 2-已审核 3-收货中 4-上架中 5-已入库 6-已取消 7-已关闭")
@ApiModelProperty("入库单流转状态(参考): 1-已创建 2-已审核 3-收货中 4-上架中 5-已入库 6-已取消 7-已关闭")
private Integer inOrderStatus;
@ApiModelProperty("入库单展示状态: 0-已创建 2-已审核 3-已驳回 4-已入库(WMS回写status=5时同步/推送创建时置4,前端状态列绑定此字段)")
@Excel(name = "单据状态", readConverterExp = "0=已创建,2=已审核,3=已驳回,4=已入库")
private Integer issueStatus;
@ApiModelProperty("货物种类(物料分类名称,多个逗号分隔)")
@Excel(name = "货物种类")
private String materialClassifyName;
@ApiModelProperty("入库单审核状态: 1-未审核 2-审核不通过 3-审核通过")
@Excel(name = "入库单审核状态: 1-未审核 2-审核不通过 3-审核通过")
private Integer inOrderAuditStatus;
@ApiModelProperty("货主ID")
private Long orderShipperId;
@ApiModelProperty("货主编码")
@Excel(name = "货主编码")
private String orderShipperCode;
@ApiModelProperty("货主名称")
@Excel(name = "货主名称")
private String orderShipperName;
@ApiModelProperty("入库日期")
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
@Excel(name = "入仓日期", width = 30, dateFormat = "yyyy-MM-dd")
private Date warehouseDate;
}
@@ -45,6 +45,12 @@ public class ExecutionInMaterialDetailQueryDTO extends BaseVOEntity {
@ApiModelProperty("入库单类型编码")
private String inOrderTypeCode;
@ApiModelProperty("入库单类型编码(与inOrderTypeCode同义)")
private String orderTypeCode;
@ApiModelProperty("入库单类型名称(模糊查询)")
private String orderTypeName;
@ApiModelProperty("入库状态: 1-已创建 2-已审核 3-收货中 4-上架中 5-已入库 6-已取消 7-已关闭")
private Integer inStatus;
@@ -66,4 +72,10 @@ public class ExecutionInMaterialDetailQueryDTO extends BaseVOEntity {
@ApiModelProperty("物料名称")
private String materialName;
@ApiModelProperty("实际入仓日期 开始(yyyy-MM-dd,为空按建档时间兜底)")
private String warehouseDateStart;
@ApiModelProperty("实际入仓日期 结束(yyyy-MM-dd,为空按建档时间兜底)")
private String warehouseDateEnd;
}
@@ -78,6 +78,12 @@ public class QueryOrderOverStockDO extends BaseVOEntity {
@Excel(name = "入库单类型编码")
private String inOrderTypeCode;
@ApiModelProperty("入库单类型编码(与inOrderTypeCode同义)")
private String orderTypeCode;
@ApiModelProperty("入库单类型名称")
private String orderTypeName;
@ApiModelProperty("入库单类型名称")
@Excel(name = "入库单类型名称")
private String inOrderTypeName;
@@ -137,4 +143,10 @@ public class QueryOrderOverStockDO extends BaseVOEntity {
@ApiModelProperty("越库单状态 0:无状态 1:处理中 2:越库完成")
@Excel(name = "越库单状态 0:无状态 1:处理中 2:越库完成")
private Integer overStockStatus;
@ApiModelProperty("实际入仓日期 开始(yyyy-MM-dd,为空按建档时间兜底)")
private String warehouseDateStart;
@ApiModelProperty("实际入仓日期 结束(yyyy-MM-dd,为空按建档时间兜底)")
private String warehouseDateEnd;
}
@@ -85,4 +85,21 @@ public class ExecutionInMaterialDetailApplicationService {
return inMaterialDetailDomainService.queryInOrderDetailList(queryOrderOverStockDO);
}
/**
* 入库明细导出全字段口径同 queryInOrderDetailList不分页
*/
public List<ExecutionInMaterialDetailWithOrderPO> exportInOrderDetailList(QueryOrderOverStockDO queryOrderOverStockDO) {
List<ExecutionInMaterialDetailWithOrderPO> list = queryInOrderDetailList(queryOrderOverStockDO);
// 货物种类=挂载分类的父级分类名称material_classify_name为分类路径("食品,饮料")取倒数第二段
// 仅一级分类(无父级)时取自身名称
for (ExecutionInMaterialDetailWithOrderPO po : list) {
String names = po.getMaterialClassifyName();
if (names != null && names.contains(",")) {
String[] parts = names.split(",");
po.setMaterialClassifyName(parts[parts.length - 2].trim());
}
}
return list;
}
}
@@ -518,4 +518,7 @@ public class ExecutionOutMaterialDetail extends BaseVOEntity {
private String inLineNumGw;
private String opWhLocGw;
private String warehouseGw;
@ApiModelProperty("反审标记: 0-无 1-已反审(业务单反审时软删del_flag=2并打标记)")
private Integer reverseFlag;
}
@@ -1,6 +1,7 @@
package com.mhd.oms.domain.executionOutMaterialDetail.repository.po;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.mhd.common.core.annotation.Excel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
@@ -18,34 +19,49 @@ import java.util.Date;
public class ExecutionOutMaterialDetailWithOrderPO extends ExecutionOutMaterialDetailPO {
private static final long serialVersionUID = 1L;
@ApiModelProperty("出库单状态: 1-已创建 2-已审核 3-部分分配 4-已分配 5-拣货中 6-波次中 7-拣货完成 8-复核中 9-已出库 10-已取消 11-已关闭")
@ApiModelProperty("出库单流转状态(参考): 1-已创建 2-已审核 3-部分分配 4-已分配 5-拣货中 6-波次中 7-拣货完成 8-复核中 9-已出库 10-已取消 11-已关闭 12-已复核 13-已交接;WMS出库完成不回写此字段")
private Integer outOrderStatus;
@ApiModelProperty("出库单展示状态: 0-已创建 2-已审核 3-已驳回 4-已出库(WMS出库完成/推送创建时回写,前端状态列绑定此字段)")
@Excel(name = "单据状态", readConverterExp = "0=已创建,2=已审核,3=已驳回,4=已出库")
private Integer issueStatus;
@ApiModelProperty("出库单审核状态: 1-未审核 2-审核不通过 3-审核通过")
@Excel(name = "出库单审核状态: 1-未审核 2-审核不通过 3-审核通过")
private Integer outOrderAuditStatus;
@ApiModelProperty("货主ID")
private Long orderShipperId;
@ApiModelProperty("货主编码")
@Excel(name = "货主编码")
private String orderShipperCode;
@ApiModelProperty("货主名称")
@Excel(name = "货主名称")
private String orderShipperName;
@ApiModelProperty("出仓日期")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "出仓日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date orderOutboundDate;
@ApiModelProperty("出库时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "出库时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date orderOutboundTime;
@ApiModelProperty("保额")
@Excel(name = "保额")
private BigDecimal insureAmount;
@ApiModelProperty("货物类型:0-物料 1-半成品 2-成品")
@Excel(name = "货物类型", readConverterExp = "0=物料,1=半成品,2=成品")
private Integer goodsType;
@ApiModelProperty("货物种类(物料分类名称,多个逗号分隔)")
@Excel(name = "货物种类")
private String materialClassifyName;
}
@@ -45,6 +45,12 @@ public class ExecutionOutMaterialDetailQueryDTO extends BaseVOEntity {
@ApiModelProperty("出库单类型编码")
private String outOrderTypeCode;
@ApiModelProperty("出库单类型编码(与outOrderTypeCode同义)")
private String orderTypeCode;
@ApiModelProperty("出库单类型名称(模糊查询)")
private String orderTypeName;
@ApiModelProperty("出库状态: 1-已创建 2-已审核 3-部分分配 4-已分配 5-拣货中 6-波次中 7-拣货完成 8-复核中 9-已出库 10-已取消 11-已关闭")
private Integer outStatus;
@@ -66,4 +72,10 @@ public class ExecutionOutMaterialDetailQueryDTO extends BaseVOEntity {
@ApiModelProperty("物料名称")
private String materialName;
@ApiModelProperty("实际出仓日期 开始(yyyy-MM-dd,为空按建档时间兜底)")
private String outboundDateStart;
@ApiModelProperty("实际出仓日期 结束(yyyy-MM-dd,为空按建档时间兜底)")
private String outboundDateEnd;
}
@@ -459,4 +459,21 @@ public class ExecutionStockInOrder extends BaseVOEntity {
@ApiModelProperty("通知单备注")
private String noticeRemark;
@ApiModelProperty("反审状态: 0-无 1-已反审")
private Integer reverseAuditStatus;
@ApiModelProperty("反审原因")
private String reverseAuditRemark;
@ApiModelProperty("反审人id")
private Long reverseAuditBy;
@ApiModelProperty("反审人姓名")
private String reverseAuditByName;
@ApiModelProperty("反审时间")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date reverseAuditTime;
}
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
import com.mhd.oms.domain.executionInMaterialDetail.repository.po.ExecutionInMaterialDetailPO;
import com.mhd.oms.domain.executionStockInOrder.entity.ExecutionStockInOrder;
import com.mhd.oms.domain.executionStockInOrder.repository.po.ExecutionStockInOrderPO;
import com.mhd.oms.domain.executionStockInOrder.repository.po.ExecutionStockInOrderTotalPO;
import com.mhd.oms.domain.executionStockInOrder.repository.todo.ExecutionStockInOrderDO;
import com.mhd.oms.domain.executionStockInOrder.repository.vo.DeliveryDataDTO;
import com.mhd.oms.domain.executionStockInOrder.repository.vo.StockInDataDTO;
@@ -25,6 +26,11 @@ public interface IExecutionStockInOrderService extends IService<ExecutionStockIn
*/
public List<ExecutionStockInOrderPO> queryList(ExecutionStockInOrderDO stockInOrderDO);
/**
* 列表合计按查询条件全量(不分页)汇总计划数量与入库数量
*/
public ExecutionStockInOrderTotalPO queryListTotal(ExecutionStockInOrderDO stockInOrderDO);
/**
* 新增入库单
*/
@@ -7,6 +7,7 @@ import com.mhd.oms.domain.executionStockInOrder.repository.po.MaterialBaseInfoPO
import com.mhd.oms.domain.executionStockInOrder.repository.po.MaterialGoodsRulePO;
import com.mhd.oms.domain.executionStockInOrder.repository.po.MaterialWarehouseControlPO;
import com.mhd.oms.domain.executionStockInOrder.repository.po.ExecutionStockInOrderPO;
import com.mhd.oms.domain.executionStockInOrder.repository.po.ExecutionStockInOrderTotalPO;
import com.mhd.oms.domain.executionStockInOrder.repository.todo.ExecutionStockInOrderDO;
//import com.mhd.oms.domain.reservationInMaterialDetail.repository.po.ExecutionInMaterialDetailPO;
import org.apache.ibatis.annotations.Param;
@@ -27,6 +28,11 @@ public interface ExecutionStockInOrderMapper extends BaseMapper<ExecutionStockIn
*/
public List<ExecutionStockInOrderPO> queryList(ExecutionStockInOrderDO stockInOrderDO);
/**
* 列表合计按查询条件全量(不分页)汇总计划数量与入库数量
*/
public ExecutionStockInOrderTotalPO queryListTotal(ExecutionStockInOrderDO stockInOrderDO);
public MaterialBaseInfoPO getinfo(@Param("id") Long id);
public MaterialBaseInfoPO getByName(@Param("name") String name);
@@ -26,6 +26,7 @@ import com.mhd.oms.domain.executionStockInOrder.repository.listener.StockInExcel
import com.mhd.oms.domain.executionStockInOrder.repository.mapper.ExecutionStockInOrderMapper;
import com.mhd.oms.domain.executionStockInOrder.repository.po.MaterialBaseInfoPO;
import com.mhd.oms.domain.executionStockInOrder.repository.po.ExecutionStockInOrderPO;
import com.mhd.oms.domain.executionStockInOrder.repository.po.ExecutionStockInOrderTotalPO;
import com.mhd.oms.domain.executionStockInOrder.repository.todo.ExecutionStockInOrderDO;
import com.mhd.oms.domain.executionStockInOrder.repository.vo.*;
import com.mhd.oms.domain.executionStockOutOrder.entity.ExecutionStockOutOrder;
@@ -93,6 +94,15 @@ public class ExecutionStockInOrderImpl extends ServiceImpl<ExecutionStockInOrder
return stockInOrderMapper.queryList(stockInOrderDO);
}
/**
* 列表合计按查询条件全量(不分页)汇总计划数量与入库数量
*/
@Override
public ExecutionStockInOrderTotalPO queryListTotal(ExecutionStockInOrderDO stockInOrderDO)
{
return stockInOrderMapper.queryListTotal(stockInOrderDO);
}
/**
* 新增入库单
*/
@@ -482,4 +482,21 @@ public class ExecutionStockInOrderPO extends BaseVOEntity {
private String khdm;
@ApiModelProperty("通知单备注")
private String noticeRemark;
@ApiModelProperty("反审状态: 0-无 1-已反审")
private Integer reverseAuditStatus;
@ApiModelProperty("反审原因")
private String reverseAuditRemark;
@ApiModelProperty("反审人id")
private Long reverseAuditBy;
@ApiModelProperty("反审人姓名")
private String reverseAuditByName;
@ApiModelProperty("反审时间")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date reverseAuditTime;
}
@@ -0,0 +1,30 @@
package com.mhd.oms.domain.executionStockInOrder.repository.po;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 入库执行单列表合计PO
*
* <p>当前筛选条件下不分页的全量合计随分页列表一起返回
* totalPlanQuantity/totalInQuantity计划数量与入库数量分开计算</p>
*
* @author rcx
* @date 2026-09-14
*/
@Data
@ApiModel(value = "入库执行单列表合计对象", description = "入库执行单列表合计响应对象")
public class ExecutionStockInOrderTotalPO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("计划数量总数")
private BigDecimal totalPlanQuantity;
@ApiModelProperty("入库数量总数")
private BigDecimal totalInQuantity;
}
@@ -513,4 +513,21 @@ public class ExecutionStockInOrderDO extends BaseVOEntity {
private String businessPriceWay;
@ApiModelProperty("通知单备注")
private String noticeRemark;
@ApiModelProperty("反审状态: 0-无 1-已反审")
private Integer reverseAuditStatus;
@ApiModelProperty("反审原因")
private String reverseAuditRemark;
@ApiModelProperty("反审人id")
private Long reverseAuditBy;
@ApiModelProperty("反审人姓名")
private String reverseAuditByName;
@ApiModelProperty("反审时间")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date reverseAuditTime;
}
@@ -510,4 +510,21 @@ public class ExecutionStockInOrderDTO extends ExecutionStockInOrderBaseDTO {
private String businessPriceWay;
@ApiModelProperty("通知单备注")
private String noticeRemark;
@ApiModelProperty("反审状态: 0-无 1-已反审")
private Integer reverseAuditStatus;
@ApiModelProperty("反审原因")
private String reverseAuditRemark;
@ApiModelProperty("反审人id")
private Long reverseAuditBy;
@ApiModelProperty("反审人姓名")
private String reverseAuditByName;
@ApiModelProperty("反审时间")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date reverseAuditTime;
}
@@ -18,6 +18,7 @@ import com.mhd.oms.domain.executionStockInOrder.entity.ExecutionStockInOrder;
import com.mhd.oms.domain.executionStockInOrder.repository.mapper.ExecutionStockInOrderMapper;
import com.mhd.oms.domain.executionStockInOrder.repository.po.MaterialBaseInfoPO;
import com.mhd.oms.domain.executionStockInOrder.repository.po.ExecutionStockInOrderPO;
import com.mhd.oms.domain.executionStockInOrder.repository.po.ExecutionStockInOrderTotalPO;
import com.mhd.oms.domain.executionStockInOrder.repository.todo.ExecutionStockInOrderDO;
import com.mhd.oms.domain.executionStockInOrder.repository.todo.ExecutionStockInOrderDTO;
import com.mhd.oms.domain.executionStockInOrder.repository.vo.GwInDetail;
@@ -172,6 +173,15 @@ public class ExecutionStockInOrderApplicationService {
return stockInOrderDomainService.queryList(stockInOrderDO);
}
/**
* 列表合计按查询条件全量(不分页)汇总计划数量与入库数量
* 须在 queryList/queryList1 之后用同一个DO调用数据权限条件由列表查询原地写入DO
* 合计查询才能继承生效后的筛选 PageHelper startPage 只拦截下一条SQL本查询不会被分页
*/
public ExecutionStockInOrderTotalPO queryListTotal(ExecutionStockInOrderDO stockInOrderDO) {
return stockInOrderDomainService.queryListTotal(stockInOrderDO);
}
/**
* 新增入库单
@@ -263,7 +273,16 @@ public class ExecutionStockInOrderApplicationService {
*/
public ExecutionStockInOrderPO getInfo(Long inOrderId)
{
ExecutionStockInOrderPO info = stockInOrderDomainService.getInfo(inOrderId);
return getInfo(inOrderId, false);
}
/**
* 获取入库执行单详情getReverseAuditInfo 归档详情复用
* @param archive true-查询已反审tab 归档数据
*/
public ExecutionStockInOrderPO getInfo(Long inOrderId, boolean archive)
{
ExecutionStockInOrderPO info = stockInOrderDomainService.getInfo(inOrderId, archive);
String userNcCode = reservationStockInOrderMapper.getUserNcCode(info.getShipperId());
info.setKhdm(userNcCode);
List<ExecutionInMaterialDetailPO> materialDetailList = info.getMaterialDetailList();
@@ -276,6 +295,14 @@ public class ExecutionStockInOrderApplicationService {
return info;
}
/**
* 查询入库执行单已反审tab 归档详情已软删+已反审标记的数据含软删明细
*/
public ExecutionStockInOrderPO getReverseAuditInfo(Long inOrderId)
{
return getInfo(inOrderId, true);
}
/**
@@ -30,6 +30,7 @@ import com.mhd.oms.domain.executionStockInOrder.repository.po.MaterialBaseInfoPO
import com.mhd.oms.domain.executionStockInOrder.repository.po.MaterialGoodsRulePO;
import com.mhd.oms.domain.executionStockInOrder.repository.po.MaterialWarehouseControlPO;
import com.mhd.oms.domain.executionStockInOrder.repository.po.ExecutionStockInOrderPO;
import com.mhd.oms.domain.executionStockInOrder.repository.po.ExecutionStockInOrderTotalPO;
import com.mhd.oms.domain.executionStockInOrder.repository.todo.ExecutionStockInOrderDO;
import com.mhd.oms.domain.executionStockOutOrder.entity.ExecutionStockOutOrder;
import com.mhd.oms.domain.executionStockOutOrder.repository.facade.IExecutionStockOutOrderService;
@@ -113,6 +114,13 @@ public class ExecutionStockInOrderDomainService {
return stockInOrderService.queryList(stockInOrderDO);
}
/**
* 列表合计按查询条件全量(不分页)汇总计划数量与入库数量
*/
public ExecutionStockInOrderTotalPO queryListTotal(ExecutionStockInOrderDO stockInOrderDO) {
return stockInOrderService.queryListTotal(stockInOrderDO);
}
/**
* 从单号列表中提取最大序号
* 单号格式: prefix + 数字序号 RK260807001 prefix=RK260807, 序号=001
@@ -314,14 +322,31 @@ public class ExecutionStockInOrderDomainService {
* 获取入库单详细信息
*/
public ExecutionStockInOrderPO getInfo(Long inOrderId)
{
return getInfo(inOrderId, false);
}
/**
* 获取入库执行单详情
* @param archive true-查询已反审tab 归档数据主单须为已软删+已反审标记明细放行软删行
*/
public ExecutionStockInOrderPO getInfo(Long inOrderId, boolean archive)
{
ExecutionStockInOrderPO stockInOrderPO = stockInOrderService.getInfo(inOrderId);
if (archive && (stockInOrderPO.getDelFlag() == null || stockInOrderPO.getDelFlag() != 2
|| !Integer.valueOf(1).equals(stockInOrderPO.getReverseAuditStatus()))) {
throw new ServiceException("入库执行单非已反审归档数据,无法查看归档详情");
}
ExecutionInMaterialDetailDO inMaterialDetailDO = new ExecutionInMaterialDetailDO();
String inOrderNumber = stockInOrderPO.getInOrderNumber();
List<BusinessDocumentOrder> businessDocumentOrders = businessDocumentOrderMapper.selectList(new LambdaQueryWrapper<BusinessDocumentOrder>().eq(BusinessDocumentOrder::getInOrderNumber, inOrderNumber).notIn(BusinessDocumentOrder::getOrderStatus,4,5));
stockInOrderPO.setBusinessDocumentOrderDOList(businessDocumentOrders);
inMaterialDetailDO.setInOrderNumber(inOrderNumber);
inMaterialDetailDO.setLevel(1);
if (archive) {
//归档明细已随主单软删(del_flag=2)放行软删明细行
inMaterialDetailDO.setDelFlag(2);
}
List<ExecutionInMaterialDetailPO> inMaterialDetailPOList = materialDetailService.queryList(inMaterialDetailDO);
// 先通过入库单号查询收货单列表
@@ -516,4 +516,7 @@ public class ExecutionStockOutOrder extends BaseVOEntity {
@ApiModelProperty("折扣")
private BigDecimal discount;
@ApiModelProperty("反审标记: 0-无 1-已反审(业务单反审时软删del_flag=2并打标记)")
private Integer reverseFlag;
}
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
import com.mhd.oms.domain.executionOutMaterialDetail.repository.po.ExecutionOutMaterialDetailPO;
import com.mhd.oms.domain.executionStockOutOrder.entity.ExecutionStockOutOrder;
import com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderPO;
import com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderTotalPO;
import com.mhd.oms.domain.executionStockOutOrder.repository.todo.ExecutionStockOutOrderDO;
import java.util.List;
@@ -21,6 +22,11 @@ public interface IExecutionStockOutOrderService extends IService<ExecutionStockO
*/
public List<ExecutionStockOutOrderPO> queryList(ExecutionStockOutOrderDO stockOutOrderDO);
/**
* 列表合计按查询条件全量(不分页)汇总一级明细的计划数量与出库数量
*/
public ExecutionStockOutOrderTotalPO queryListTotal(ExecutionStockOutOrderDO stockOutOrderDO);
/**
* 新增出库单
*/
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mhd.oms.domain.executionOutMaterialDetail.repository.po.ExecutionOutMaterialDetailPO;
import com.mhd.oms.domain.executionStockOutOrder.entity.ExecutionStockOutOrder;
import com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderPO;
import com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderTotalPO;
import com.mhd.oms.domain.executionStockOutOrder.repository.todo.ExecutionStockOutOrderDO;
import org.apache.ibatis.annotations.Param;
//import com.mhd.oms.domain.reservationOutMaterialDetail.repository.po.ExecutionOutMaterialDetailPO;
@@ -23,6 +24,11 @@ public interface ExecutionStockOutOrderMapper extends BaseMapper<ExecutionStockO
*/
public List<ExecutionStockOutOrderPO> queryList(ExecutionStockOutOrderDO stockOutOrderDO);
/**
* 列表合计按查询条件全量(不分页)汇总一级明细的计划数量与出库数量
*/
public ExecutionStockOutOrderTotalPO queryListTotal(ExecutionStockOutOrderDO stockOutOrderDO);
/**
* 出库单提货单打印-表头
*/
@@ -6,6 +6,7 @@ import com.mhd.oms.domain.executionStockOutOrder.entity.ExecutionStockOutOrder;
import com.mhd.oms.domain.executionStockOutOrder.repository.facade.IExecutionStockOutOrderService;
import com.mhd.oms.domain.executionStockOutOrder.repository.mapper.ExecutionStockOutOrderMapper;
import com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderPO;
import com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderTotalPO;
import com.mhd.oms.domain.executionStockOutOrder.repository.todo.ExecutionStockOutOrderDO;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
@@ -34,6 +35,15 @@ public class ExecutionStockOutOrderImpl extends ServiceImpl<ExecutionStockOutOrd
return reservationStockOutOrderMapper.queryList(stockOutOrderDO);
}
/**
* 列表合计按查询条件全量(不分页)汇总一级明细的计划数量与出库数量
*/
@Override
public ExecutionStockOutOrderTotalPO queryListTotal(ExecutionStockOutOrderDO stockOutOrderDO)
{
return reservationStockOutOrderMapper.queryListTotal(stockOutOrderDO);
}
/**
* 新增出库单
*/
@@ -29,6 +29,8 @@ public class ExecutionStockOutOrderPO extends BaseVOEntity {
@ApiModelProperty("出库单id")
private Long outOrderId;
@ApiModelProperty("反审标记: 1-已反审(反审软删归档, 【已反审】tab按 delFlag=2+reverseFlag=1 查询)")
private Integer reverseFlag;
@ApiModelProperty("总价")
@Excel(name = "总价")
private BigDecimal amount;
@@ -0,0 +1,30 @@
package com.mhd.oms.domain.executionStockOutOrder.repository.po;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 出库执行单列表合计PO
*
* <p>当前筛选条件下不分页的全量合计随分页列表一起返回
* totalPlanQuantity/totalOutboundQuantity计划数量与出库数量分开计算</p>
*
* @author rcx
* @date 2026-09-14
*/
@Data
@ApiModel(value = "出库执行单列表合计对象", description = "出库执行单列表合计响应对象")
public class ExecutionStockOutOrderTotalPO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("计划数量总数(一级明细计划数量之和)")
private BigDecimal totalPlanQuantity;
@ApiModelProperty("出库数量总数(一级明细出库数量之和)")
private BigDecimal totalOutboundQuantity;
}
@@ -29,6 +29,9 @@ public class ExecutionStockOutOrderDO extends BaseVOEntity {
@ApiModelProperty("出库单id")
private Long outOrderId;
@ApiModelProperty("反审标记查询: 1-已反审(与delFlag=2成对传, 查【已反审】tab归档数据)")
private Integer reverseFlag;
@ApiModelProperty("组织表ID")
@Excel(name = "组织表ID")
private Long organizationId;
@@ -27,6 +27,9 @@ public class ExecutionStockOutOrderDTO extends ExecutionStockOutOrderBaseDTO {
@ApiModelProperty("出库单id")
private Long outOrderId;
@ApiModelProperty("反审标记查询: 1-已反审(与delFlag=2成对传, 查【已反审】tab归档数据)")
private Integer reverseFlag;
@ApiModelProperty("组织表ID")
@Excel(name = "组织表ID")
private Long organizationId;
@@ -26,6 +26,7 @@ import com.mhd.oms.domain.executionStockOutOrder.entity.ExecutionStockOutOrder;
import com.mhd.oms.domain.executionStockOutOrder.repository.facade.IExecutionStockOutOrderService;
import com.mhd.oms.domain.executionStockOutOrder.repository.mapper.ExecutionStockOutOrderMapper;
import com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderPO;
import com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderTotalPO;
import com.mhd.oms.domain.executionStockOutOrder.repository.todo.ExecutionStockOutOrderDO;
import com.mhd.oms.domain.executionStockOutOrder.repository.todo.ExecutionStockOutOrderDTO;
import com.mhd.oms.domain.gwLog.entity.GwLog;
@@ -66,6 +67,8 @@ import java.util.stream.Collectors;
public class ExecutionStockOutOrderApplicationService {
@Autowired
private ExecutionStockOutOrderDomainService stockOutOrderDomainService;
@Autowired
private com.mhd.oms.application.service.WarehouseInfoApplicationService warehouseInfoApplicationService;
// @Autowired
// private ExecutionOutMaterialDetailDomainService outMaterialDetailDomainService;
// @Autowired
@@ -223,6 +226,15 @@ public class ExecutionStockOutOrderApplicationService {
return stockOutOrderPOS;
}
/**
* 列表合计按查询条件全量(不分页)汇总一级明细的计划数量与出库数量
* 须在 queryList/queryList1 之后用同一个DO调用数据权限条件由列表查询原地写入DO
* 合计查询才能继承生效后的筛选 PageHelper startPage 只拦截下一条SQL本查询不会被分页
*/
public ExecutionStockOutOrderTotalPO queryListTotal(ExecutionStockOutOrderDO stockOutOrderDO) {
return stockOutOrderDomainService.queryListTotal(stockOutOrderDO);
}
/**
* 新增出库单
@@ -862,11 +874,8 @@ public class ExecutionStockOutOrderApplicationService {
if (stockOutOrderDO.getWarehouseId() == null) {
throw new ServiceException("仓库ID不能为空");
}
AjaxResult ajaxResult = systemServiceFeign.getWarehouseInfoByWarehouseId(stockOutOrderDO.getWarehouseId());
if(!"200".equals(String.valueOf(ajaxResult.get("code")))){
throw new ServiceException("获取仓库信息失败");
}
WarehouseFeignPO warehouseFeignPO = JSON.parseObject(JSONObject.toJSONString(ajaxResult.get("data")), WarehouseFeignPO.class);
// Feign失败走本地跨库兜底, 避免保存被"获取仓库信息失败"阻断
WarehouseFeignPO warehouseFeignPO = warehouseInfoApplicationService.getWarehouseInfo(stockOutOrderDO.getWarehouseId());
stockOutOrderDO.setWarehouseCode(warehouseFeignPO.getWarehouseCode());
stockOutOrderDO.setWarehouseName(warehouseFeignPO.getWarehouseName());
}
@@ -30,6 +30,7 @@ import com.mhd.oms.domain.executionStockOutOrder.entity.ExecutionStockOutOrder;
import com.mhd.oms.domain.executionStockOutOrder.repository.facade.IExecutionStockOutOrderService;
import com.mhd.oms.domain.executionStockOutOrder.repository.mapper.ExecutionStockOutOrderMapper;
import com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderPO;
import com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderTotalPO;
import com.mhd.oms.domain.executionStockOutOrder.repository.todo.ExecutionStockOutOrderDO;
import com.mhd.oms.domain.materialBaseInfo.service.MaterialBaseInfoResolveService;
import com.mhd.oms.domain.reservationDeliveryDetailsLink.entity.ReservationDeliveryDetailsLink;
@@ -71,6 +72,8 @@ public class ExecutionStockOutOrderDomainService {
@Autowired
private IExecutionStockOutOrderService stockOutOrderService;
@Autowired
private com.mhd.oms.application.service.WarehouseInfoApplicationService warehouseInfoApplicationService;
@Autowired
private IExecutionOutMaterialDetailService outMaterialDetailService;
@Autowired
private IReservationDeliveryDetailsLinkService deliveryDetailsLinkService;
@@ -112,6 +115,13 @@ public class ExecutionStockOutOrderDomainService {
return stockOutOrderService.queryList(stockOutOrderDO);
}
/**
* 列表合计按查询条件全量(不分页)汇总一级明细的计划数量与出库数量
*/
public ExecutionStockOutOrderTotalPO queryListTotal(ExecutionStockOutOrderDO stockOutOrderDO) {
return stockOutOrderService.queryListTotal(stockOutOrderDO);
}
/**
* 从单号列表中提取最大序号
* 单号格式: prefix + 数字序号 RK260807001 prefix=RK260807, 序号=001
@@ -148,14 +158,9 @@ public class ExecutionStockOutOrderDomainService {
public Boolean insert(ExecutionStockOutOrderDO stockOutOrderDO) {
//建单校验明细数量必须大于00数量单下发WMS后自动分配必失败出库单会卡死在已审核状态
validateMaterialDetailQuantity(stockOutOrderDO.getMaterialDetailList());
//设置单号
//设置单号(仓库信息: Feign优先, system服务失败/降级时本地跨库兜底)
Long warehouseId = stockOutOrderDO.getWarehouseId();
AjaxResult ajaxResult = systemServiceFeign.getWarehouseInfoByWarehouseId(warehouseId);
if (!"200".equals(String.valueOf(ajaxResult.get("code")))) {
throw new ServiceException("获取仓库信息失败");
}
//仓库信息
WarehouseFeignPO warehouseFeignPO = com.alibaba.fastjson.JSON.parseObject(com.alibaba.fastjson2.JSONObject.toJSONString(ajaxResult.get("data")), WarehouseFeignPO.class);
WarehouseFeignPO warehouseFeignPO = warehouseInfoApplicationService.getWarehouseInfo(warehouseId);
String orderNoGenerateRule = warehouseFeignPO.getOrderNoGenerateRule();
String prefix = warehouseFeignPO.getOutOrderNoPrefix();
if ("yang_pin_chu_ku".equals(stockOutOrderDO.getBusinessType())) {
@@ -218,14 +223,9 @@ public class ExecutionStockOutOrderDomainService {
@Transactional(rollbackFor = Exception.class)
public Boolean batchInsert(List<ExecutionStockOutOrderDO> stockOutOrderDOList) {
for (ExecutionStockOutOrderDO stockOutOrderDO : stockOutOrderDOList){
//设置单号
//设置单号(仓库信息: Feign优先, system服务失败/降级时本地跨库兜底)
Long warehouseId = stockOutOrderDO.getWarehouseId();
AjaxResult ajaxResult = systemServiceFeign.getWarehouseInfoByWarehouseId(warehouseId);
if (!"200".equals(String.valueOf(ajaxResult.get("code")))) {
throw new ServiceException("获取仓库信息失败");
}
//仓库信息
WarehouseFeignPO warehouseFeignPO = com.alibaba.fastjson.JSON.parseObject(com.alibaba.fastjson2.JSONObject.toJSONString(ajaxResult.get("data")), WarehouseFeignPO.class);
WarehouseFeignPO warehouseFeignPO = warehouseInfoApplicationService.getWarehouseInfo(warehouseId);
String orderNoGenerateRule = warehouseFeignPO.getOrderNoGenerateRule();
String prefix = warehouseFeignPO.getOutOrderNoPrefix();
if ("yang_pin_chu_ku".equals(stockOutOrderDO.getBusinessType())) {
@@ -448,8 +448,9 @@ public class ExecutionStockOutOrderDomainService {
BigDecimal freeze = freezeAfterQuantity.subtract(freezeBeforeQuantity);
BigDecimal allocationQuantity = reservationMaterialInventory.getAllocationQuantity();
BigDecimal freezeQuantity = reservationMaterialInventory.getFreezeQuantity();
allocationQuantity.add(allocation);
freezeQuantity.subtract(freeze);
// 修复BigDecimal 不可变add/subtract 返回新对象必须回赋原实现未回赋取消下发的镜像库存还原实际不生效
allocationQuantity = (allocationQuantity == null ? BigDecimal.ZERO : allocationQuantity).add(allocation);
freezeQuantity = (freezeQuantity == null ? BigDecimal.ZERO : freezeQuantity).subtract(freeze);
reservationMaterialInventory.setAllocationQuantity(allocationQuantity);
reservationMaterialInventory.setFreezeQuantity(freezeQuantity);
reservationMaterialInventory.setUpdateTime(new Date());
@@ -513,7 +514,7 @@ public class ExecutionStockOutOrderDomainService {
checkDetail.setMaterialName(detail.getMaterialName());
checkList.add(checkDetail);
}
AjaxResult checkResult = wmsServiceFeign.checkOmsAssignInventory(checkList);
AjaxResult checkResult = wmsServiceFeign.checkOmsAssignInventory(checkList, byId.getBusinessOutOrderNumber());
if (checkResult == null || !"200".equals(String.valueOf(checkResult.get("code")))) {
validateErrors.add("出库单[" + byId.getOutOrderNumber() + "]库存校验失败:"
+ (checkResult == null ? "WMS服务不可用,已阻止下发" : checkResult.get("msg")));
@@ -535,6 +536,9 @@ public class ExecutionStockOutOrderDomainService {
try {
StockOutOrder stockOutOrder = new StockOutOrder();
BeanUtils.copyProperties(byId, stockOutOrder,"outOrderId");
// 下单冻结模式WMS单头带上业务单号字段名不同copyProperties不会复制必须显式set
// 供WMS分配承接/下发校验做单据级冻结记录过滤双单冻结同行防误消费
stockOutOrder.setBusinessOrderNo(byId.getBusinessOutOrderNumber());
Long warehouseId = stockOutOrder.getWarehouseId();
Long topOrgId = stockInOrderMapper.getTopOrgId(warehouseId);
Long orgId = stockInOrderMapper.getOrgId(warehouseId);
@@ -542,7 +546,12 @@ public class ExecutionStockOutOrderDomainService {
stockOutOrder.setOrganizationId(orgId);
stockOutOrder.setOrganizationName(orgName);
stockOutOrder.setTopOrganizationId(topOrgId);
wmsServiceFeign.omsOutOrderAdd(stockOutOrder);
//单头/配送明细推送同样校验返回值: 降级吞掉会残留"有单无明细"等半截单
AjaxResult orderAddResult = wmsServiceFeign.omsOutOrderAdd(stockOutOrder);
if (orderAddResult == null || !"200".equals(String.valueOf(orderAddResult.get("code")))) {
throw new ServiceException("出库单[" + currentOrderNumber + "]WMS出库单推送失败:"
+ (orderAddResult == null ? "WMS服务不可用或已降级" : orderAddResult.get("msg")));
}
List<ReservationDeliveryDetailsLink> list = deliveryDetailsLinkService.list(new QueryWrapper<ReservationDeliveryDetailsLink>().lambda().eq(ReservationDeliveryDetailsLink::getOrderNumber, byId.getOutOrderNumber()));
List<DeliveryDetailsLink> deliveryDetailsLinkList = new ArrayList<>();
@@ -552,7 +561,11 @@ public class ExecutionStockOutOrderDomainService {
deliveryDetailsLink.setOmsId(reservationDeliveryDetailsLink.getId());
deliveryDetailsLinkList.add(deliveryDetailsLink);
}
wmsServiceFeign.omsOutOrderAddDeliveryDetailsLink(deliveryDetailsLinkList);
AjaxResult linkAddResult = wmsServiceFeign.omsOutOrderAddDeliveryDetailsLink(deliveryDetailsLinkList);
if (linkAddResult == null || !"200".equals(String.valueOf(linkAddResult.get("code")))) {
throw new ServiceException("出库单[" + currentOrderNumber + "]WMS配送明细推送失败:"
+ (linkAddResult == null ? "WMS服务不可用或已降级" : linkAddResult.get("msg")));
}
String inOrderNumber = stockOutOrder.getOutOrderNumber();
@@ -566,7 +579,12 @@ public class ExecutionStockOutOrderDomainService {
outMaterialDetail.setTopOrganizationId(topOrgId);
outMaterialDetailList.add(outMaterialDetail);
}
wmsServiceFeign.omsOutOrderAddDetail(outMaterialDetailList);
//明细推送必须校验返回值: 失败被降级吞掉时WMS会残留"有单无明细"的出库单, 自动分配报"可用库存为0"难定位
AjaxResult detailAddResult = wmsServiceFeign.omsOutOrderAddDetail(outMaterialDetailList);
if (detailAddResult == null || !"200".equals(String.valueOf(detailAddResult.get("code")))) {
throw new ServiceException("出库单[" + currentOrderNumber + "]WMS出库明细推送失败:"
+ (detailAddResult == null ? "WMS服务不可用或已降级" : detailAddResult.get("msg")));
}
String outOrderNumber = byId.getOutOrderNumber();
com.mhd.system.api.domain.StockOutOrderDTO stockOutOrderDTO = new com.mhd.system.api.domain.StockOutOrderDTO();
@@ -0,0 +1,58 @@
package com.mhd.oms.domain.outEntrustOrder.repository.listener;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
/**
* 自定义转换器不进行任何类型转换直接返回原始值
* 覆盖EasyExcel默认的StringNumberConverter避免数字格式化错误
*
* @author rcx
* @date 2026-09-14
*/
public class NoConvertConverter implements Converter<String> {
@Override
public Class<?> supportJavaTypeKey() {
return String.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
// 关键返回NUMBER类型覆盖StringNumberConverter
return CellDataTypeEnum.NUMBER;
}
@Override
public String convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
// 不进行任何格式化直接返回原始值的字符串表示
if (cellData == null) {
return "";
}
// 根据单元格类型获取原始值
try {
CellDataTypeEnum type = cellData.getType();
if (type == CellDataTypeEnum.STRING) {
return cellData.getStringValue() != null ? cellData.getStringValue() : "";
} else if (type == CellDataTypeEnum.NUMBER) {
// 直接转换数字为字符串不进行格式化
return cellData.getNumberValue() != null ? cellData.getNumberValue().toString() : "";
} else if (type == CellDataTypeEnum.BOOLEAN) {
return cellData.getBooleanValue() != null ? cellData.getBooleanValue().toString() : "";
} else if (type == CellDataTypeEnum.EMPTY) {
return "";
} else {
// 其他类型尝试获取字符串值
return cellData.getStringValue() != null ? cellData.getStringValue() : "";
}
} catch (Exception e) {
// 如果获取失败返回空字符串
return "";
}
}
}
@@ -0,0 +1,481 @@
package com.mhd.oms.domain.outEntrustOrder.repository.listener;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelReader;
import com.alibaba.excel.read.metadata.ReadSheet;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.TypeReference;
import com.mhd.common.core.domain.po.SysDictDataVo;
import com.mhd.common.core.domain.po.UserPo;
import com.mhd.common.core.exception.ServiceException;
import com.mhd.common.core.utils.StringUtils;
import com.mhd.common.core.web.domain.AjaxResult;
import com.mhd.common.security.utils.SecurityUtils;
import com.mhd.oms.domain.outEntrustOrder.repository.mapper.OutEntrustOrderMapper;
import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDO;
import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDetailDO;
import com.mhd.oms.domain.reservationStockInOrder.repository.po.MaterialBaseInfoPO;
import com.mhd.system.api.SystemServiceFeign;
import com.mhd.system.api.UserServiceFeign;
import com.mhd.system.api.model.LoginUser;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 出仓委托单Excel解析服务
* 模板与WMS出库管理导入一致前11行为表头第12行起为明细
*
* @author rcx
* @date 2026-09-14
*/
@Slf4j
@Service
public class OutEntrustExcelParseService {
@Resource
private SystemServiceFeign systemServiceFeign;
@Resource
private UserServiceFeign userServiceFeign;
@Autowired
private OutEntrustOrderMapper outEntrustOrderMapper;
/**
* 解析结果委托单对象 + 校验错误信息(空串表示通过)
*/
@Getter
public static class ParseResult {
private final OutEntrustOrderDO order;
private final StringBuilder errorMsg;
public ParseResult(OutEntrustOrderDO order, StringBuilder errorMsg) {
this.order = order;
this.errorMsg = errorMsg;
}
}
/**
* 解析出仓委托单Excel(不落库落库由应用服务走新增链路)
*
* @param file 上传的Excel文件
* @param warehouseName 仓库名称(导入弹窗选择)
* @param warehouseCode 仓库编码
* @param warehouseId 仓库ID
* @return 解析结果委托单对象 + 校验错误信息错误信息非空时不应落库
*/
public ParseResult parseOutEntrustOrderExcel(MultipartFile file, String warehouseName, String warehouseCode, Long warehouseId) throws IOException {
OutEntrustHeaderListener headerListener = new OutEntrustHeaderListener();
ReadSheet headerSheet = EasyExcel.readSheet(0)
.headRowNumber(0)
.build();
ExcelReader headerReader = EasyExcel.read(file.getInputStream())
.autoCloseStream(false)
.registerReadListener(headerListener)
.registerConverter(new NoConvertConverter())
.headRowNumber(0)
.build();
// 通过监听器控制只读取前11行
headerReader.read(headerSheet);
InputStream itemInputStream = file.getInputStream();
OutEntrustItemListener itemListener = new OutEntrustItemListener();
// 商品明细需要跳过前11行,从第12行开始读取
ExcelReader itemReader = EasyExcel.read(itemInputStream)
.autoCloseStream(false)
.registerReadListener(itemListener)
.registerConverter(new NoConvertConverter())
.headRowNumber(12)
.build();
ReadSheet itemSheet = EasyExcel.readSheet(0).build();
itemReader.read(itemSheet);
itemReader.finish();
List<OutEntrustOrderDetailDO> itemList = itemListener.getItemList();
OutEntrustOrderDO outEntrustOrder = headerListener.getOutEntrustOrder();
// 提前获取货主ID用于查询物料基础信息时过滤
String customerName = outEntrustOrder.getShipperName();
UserPo shipperUser = resolveCustomerShipperUserPoForImport(customerName);
Long shipperId = shipperUser != null ? shipperUser.getUserId() : null;
LoginUser loginUser = SecurityUtils.getLoginUser();
Long topOrganizationId = loginUser != null && loginUser.getUserPo() != null
? loginUser.getUserPo().getTopOrganizationId() : null;
initList(itemList, shipperId, topOrganizationId);
outEntrustOrder.setMaterialDetailList(itemList);
outEntrustOrder.setWarehouseName(warehouseName);
outEntrustOrder.setWarehouseCode(warehouseCode);
outEntrustOrder.setWarehouseId(warehouseId);
//业务员默认当前登录人
if (loginUser != null) {
outEntrustOrder.setSalesmanId(String.valueOf(loginUser.getUserid()));
outEntrustOrder.setSalesmanName(loginUser.getUserPo() != null ? loginUser.getUserPo().getUserName() : loginUser.getUsername());
}
//货主信息回填(名称/编号以用户中心为准)
if (shipperUser != null) {
outEntrustOrder.setShipperId(shipperUser.getUserId());
outEntrustOrder.setShipperCode(shipperUser.getUserMemberCode());
outEntrustOrder.setShipperName(shipperUser.getUserName());
}
initDict(outEntrustOrder);
//校验必填字段
StringBuilder errorMsg = check(outEntrustOrder);
// 日期单元格有值但解析失败与物料一致性错误同等对待阻断导入并明确提示杜绝静默丢日期
for (String dateError : headerListener.getDateParseErrors()) {
errorMsg.append(dateError).append("; ");
}
return new ParseResult(outEntrustOrder, errorMsg);
}
/**
* 校验导入明细是否已在当前货主下维护物料基础信息
*/
private StringBuilder validateShipperMaterialDetails(OutEntrustOrderDO outEntrustOrder) {
StringBuilder sb = new StringBuilder();
List<OutEntrustOrderDetailDO> details = outEntrustOrder.getMaterialDetailList();
if (details == null || details.isEmpty()) {
return sb;
}
Long shipperId = outEntrustOrder.getShipperId();
for (OutEntrustOrderDetailDO row : details) {
if (row.getMaterialBaseInfoId() != null) {
continue;
}
boolean hasIdentifier = StringUtils.isNotBlank(row.getMaterialNo())
|| StringUtils.isNotBlank(row.getSkucode())
|| StringUtils.isNotBlank(row.getCommodityName());
if (!hasIdentifier) {
continue;
}
String desc = buildImportMaterialRowDesc(row);
if (ObjectUtil.isEmpty(shipperId)) {
sb.append("请先确认客户(货主)信息;无法校验以下物料是否已在货主下维护:").append(desc).append("; ");
} else {
sb.append("当前货主下未维护以下物料:").append(desc).append("; ");
}
}
return sb;
}
/**
* 提示文案只强调 Excel 导入列商品名称商品名称为空时用导入料号/SKU 辅助说明
*/
private String buildImportMaterialRowDesc(OutEntrustOrderDetailDO row) {
String commodityName = StringUtils.trimToEmpty(row.getCommodityName());
if (StringUtils.isNotBlank(commodityName)) {
return "商品名称「" + commodityName + "";
}
String materialNo = StringUtils.trimToEmpty(row.getMaterialNo());
String sku = StringUtils.trimToEmpty(row.getSkucode());
StringBuilder b = new StringBuilder("导入商品名称为空");
if (StringUtils.isNotBlank(materialNo)) {
b.append(",料号「").append(materialNo).append("");
}
if (StringUtils.isNotBlank(sku)) {
b.append("SKU「").append(sku).append("");
}
return b.toString();
}
private StringBuilder check(OutEntrustOrderDO outEntrustOrder) {
StringBuilder errorMsg = new StringBuilder();
errorMsg.append(validateShipperMaterialDetails(outEntrustOrder));
if (outEntrustOrder.getMaterialDetailList() == null || outEntrustOrder.getMaterialDetailList().isEmpty()) {
errorMsg.append("物料明细列表不能为空或匹配不上物料信息; ");
}
if (StringUtils.isEmpty(outEntrustOrder.getToReceiver())) errorMsg.append("To不能为空; ");
if (StringUtils.isEmpty(outEntrustOrder.getCarrier())) errorMsg.append("承运商不能为空; ");
if (StringUtils.isEmpty(outEntrustOrder.getTransportModeCode())) errorMsg.append("运输方式不能为空; ");
if (StringUtils.isEmpty(outEntrustOrder.getSupervisionModeCode())) errorMsg.append("监督方式不能为空; ");
if (StringUtils.isEmpty(outEntrustOrder.getDepartureArrivalCountry())) errorMsg.append("出发/到达国家不能为空; ");
if (StringUtils.isEmpty(outEntrustOrder.getContactPerson())) errorMsg.append("联系人不能为空; ");
if (ObjectUtil.isEmpty(outEntrustOrder.getShipperId())) errorMsg.append("客户名称不能为空或匹配不上客户信息; ");
if (StringUtils.isEmpty(outEntrustOrder.getDeclareCustoms())) errorMsg.append("申报地关区不能为空; ");
if (StringUtils.isEmpty(outEntrustOrder.getEntryExitCustoms())) errorMsg.append("进出境关别不能为空; ");
return errorMsg;
}
/**
* @description 初始化物料明细列表查询物料基础信息并设置物料基础信息ID(与WMS出库管理导入逻辑一致料号 > SKU码 > 商品名称)
* @param itemList 出仓委托单明细列表
* @param shipperId 货主ID用于过滤物料基础信息确保查询到的是该货主绑定的物料
* @param topOrganizationId 一级组织ID用于过滤物料基础信息
*/
private void initList(List<OutEntrustOrderDetailDO> itemList, Long shipperId, Long topOrganizationId) {
for (OutEntrustOrderDetailDO detail : itemList) {
// 未识别货主时不查物料避免匹配到其他货主的物料基础信息
if (shipperId == null) {
continue;
}
// 根据明细表中的字段查询物料基础信息优先料号 > 商品SKU码 > 商品名称
MaterialBaseInfoPO materialBaseInfoPO = null;
// 优先使用料号查询materialNo对应materialCode
String materialNo = detail.getMaterialNo();
if (StringUtils.isNotBlank(materialNo)) {
List<MaterialBaseInfoPO> materialList = outEntrustOrderMapper.getMaterialByMaterialCode(materialNo, shipperId, topOrganizationId);
if (materialList != null && !materialList.isEmpty()) {
materialBaseInfoPO = materialList.get(0);
// ==== 料号与规格型号/SKU码一致性校验任何一项不符直接报对应错误 ====
// 双方trim后必须完全一致Excel留空而档案有值或Excel填了值而档案为空均视为不一致报错
String excelSpec = StringUtils.trimToEmpty(detail.getSpecificationModel());
String dbSpec = StringUtils.trimToEmpty(materialBaseInfoPO.getSpecificationModel());
if (!excelSpec.equals(dbSpec)) {
throw new ServiceException(String.format("导入失败:料号「%s」规格型号与物料档案不一致(Excel:%s / 档案:%s",
materialNo, excelSpec.isEmpty() ? "" : excelSpec, dbSpec.isEmpty() ? "" : dbSpec));
}
String excelSku = StringUtils.trimToEmpty(detail.getSkucode());
String dbSku = StringUtils.trimToEmpty(materialBaseInfoPO.getBarCode());
if (!excelSku.equals(dbSku)) {
throw new ServiceException(String.format("导入失败:料号「%s」SKU码与物料档案不一致(Excel:%s / 档案:%s",
materialNo, excelSku.isEmpty() ? "" : excelSku, dbSku.isEmpty() ? "" : dbSku));
}
} else {
// 料号已填写但在该货主下查不到 直接报错与WMS出库导入一致不再被SKU码/商品名称静默兜底
throw new ServiceException(String.format("导入失败:料号「%s」在当前客户(货主)下不存在,请核对料号或在物料管理中维护", materialNo));
}
}
// 如果料号查询不到使用商品SKU码查询skucode对应barCode
if (materialBaseInfoPO == null && StringUtils.isNotBlank(detail.getSkucode())) {
List<MaterialBaseInfoPO> materialList = outEntrustOrderMapper.getMaterialByBarCode(detail.getSkucode(), shipperId, topOrganizationId);
if (materialList != null && !materialList.isEmpty()) {
materialBaseInfoPO = materialList.get(0);
}
}
// 如果料号和SKU码都查询不到使用商品名称查询commodityName对应materialName
if (materialBaseInfoPO == null && StringUtils.isNotBlank(detail.getCommodityName())) {
List<MaterialBaseInfoPO> materialList = outEntrustOrderMapper.getMaterialByMaterialName(detail.getCommodityName(), shipperId, topOrganizationId);
if (materialList != null && !materialList.isEmpty()) {
materialBaseInfoPO = materialList.get(0);
}
}
// 设置物料基础信息ID并从物料基础信息中回填料号物料编码物料名称等信息
if (materialBaseInfoPO != null) {
detail.setMaterialBaseInfoId(materialBaseInfoPO.getMaterialBaseInfoId());
// 单价统一从物料档案回填
detail.setUnitPrice(materialBaseInfoPO.getUnitPrice());
// 如果Excel中没有料号从物料基础信息中回填
if (StringUtils.isBlank(detail.getMaterialNo())) {
detail.setMaterialNo(materialBaseInfoPO.getMaterialCode());
}
// 回填物料编码
if (StringUtils.isBlank(detail.getMaterialCode())) {
detail.setMaterialCode(materialBaseInfoPO.getMaterialCode());
}
// 回填商品编号
if (StringUtils.isBlank(detail.getCommodityNo())) {
detail.setCommodityNo(materialBaseInfoPO.getMaterialCode());
}
// 回填物料名称如果Excel中的商品名称为空使用物料基础信息中的名称
if (StringUtils.isBlank(detail.getCommodityName())) {
detail.setCommodityName(materialBaseInfoPO.getMaterialName());
}
// 回填物料名称到materialName字段
detail.setMaterialName(materialBaseInfoPO.getMaterialName());
// 回填条形码
detail.setBarCode(materialBaseInfoPO.getBarCode());
// 如果Excel中没有SKU码从物料基础信息中回填
if (StringUtils.isBlank(detail.getSkucode())) {
detail.setSkucode(materialBaseInfoPO.getBarCode());
}
}
// 单位优先使用Excel中的"单位"字段如果为空则使用"申报单位"如果还为空则从物料主数据获取与WMS出库导入处理一致
String unit = StringUtils.isNotBlank(detail.getUnit()) ? detail.getUnit() : null;
if (StringUtils.isBlank(unit) && StringUtils.isNotBlank(detail.getDeclareUnit())) {
unit = detail.getDeclareUnit();
}
if (StringUtils.isBlank(unit) && materialBaseInfoPO != null && StringUtils.isNotBlank(materialBaseInfoPO.getUnitName())) {
unit = materialBaseInfoPO.getUnitName();
}
detail.setUnit(unit);
}
}
private void initDict(OutEntrustOrderDO outEntrustOrder) {
if (ObjectUtil.isNotNull(outEntrustOrder.getContainerNoName())) {
AjaxResult containerNo = systemServiceFeign.selectListByDictType("cabinetNo");
if (ObjectUtil.isNotNull(containerNo) && "200".equals(String.valueOf(containerNo.get("code")))) {
List<SysDictDataVo> containerNoList = JSON.parseArray(JSONObject.toJSONString(containerNo.get("data")), SysDictDataVo.class);
for (SysDictDataVo sysDictDataVo : containerNoList) {
if (outEntrustOrder.getContainerNoName().equals(sysDictDataVo.getDictLabel())) {
outEntrustOrder.setContainerNo(sysDictDataVo.getDictValue());
break;
}
}
}
}
if (ObjectUtil.isNotNull(outEntrustOrder.getTransportModeName())) {
AjaxResult transportMode = systemServiceFeign.selectListByDictType("transportMode");
if (ObjectUtil.isNotNull(transportMode) && "200".equals(String.valueOf(transportMode.get("code")))) {
List<SysDictDataVo> transportModeList = JSON.parseArray(JSONObject.toJSONString(transportMode.get("data")), SysDictDataVo.class);
for (SysDictDataVo sysDictDataVo : transportModeList) {
if (outEntrustOrder.getTransportModeName().equals(sysDictDataVo.getDictLabel())) {
outEntrustOrder.setTransportModeCode(sysDictDataVo.getDictValue());
break;
}
}
}
}
if (ObjectUtil.isNotNull(outEntrustOrder.getSupervisionModeName())) {
AjaxResult supervisionMode = systemServiceFeign.selectListByDictType("controlType");
if (ObjectUtil.isNotNull(supervisionMode) && "200".equals(String.valueOf(supervisionMode.get("code")))) {
List<SysDictDataVo> supervisionModeList = JSON.parseArray(JSONObject.toJSONString(supervisionMode.get("data")), SysDictDataVo.class);
for (SysDictDataVo sysDictDataVo : supervisionModeList) {
if (outEntrustOrder.getSupervisionModeName().equals(sysDictDataVo.getDictLabel())) {
outEntrustOrder.setSupervisionModeCode(sysDictDataVo.getDictValue());
break;
}
}
}
}
}
/**
* 导入出库单解析出的货主用户是否属于当前登录用户同一组织organizationId
* 仅当登录用户带有 organizationId 时启用过滤否则保持与旧行为一致
*/
private boolean shipperMatchesImporterOrganization(UserPo userPo, Long loginOrganizationId) {
if (loginOrganizationId == null) {
return true;
}
if (userPo == null) {
return false;
}
Long oid = userPo.getOrganizationId();
return oid != null && loginOrganizationId.equals(oid);
}
/**
* 按客户名称或数字ID解析货主用户逻辑与WMS出库管理导入一致getInfoByName 货主列表兜底均做 organizationId 校验
*/
private UserPo resolveCustomerShipperUserPoForImport(String customerName) {
if (StringUtils.isEmpty(customerName)) {
return null;
}
Long loginOrganizationId = null;
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null && loginUser.getUserPo() != null) {
loginOrganizationId = loginUser.getUserPo().getOrganizationId();
}
String shipperName = customerName.trim();
if (StringUtils.isEmpty(shipperName)) {
return null;
}
// 纯数字按用户ID查询
if (shipperName.matches("\\d+")) {
try {
Long possibleId = Long.valueOf(shipperName);
AjaxResult ajaxResult = userServiceFeign.getInfo(possibleId);
if ("200".equals(String.valueOf(ajaxResult.get("code"))) && ajaxResult.get("data") != null) {
UserPo userPo = JSON.parseObject(JSONObject.toJSONString(ajaxResult.get("data")), UserPo.class);
if (userPo != null && shipperMatchesImporterOrganization(userPo, loginOrganizationId)) {
return userPo;
}
}
} catch (Exception e) {
// ignore
}
return null;
}
// 按名称查询用户中心
try {
AjaxResult ajaxResult = userServiceFeign.getInfoByName(shipperName);
if ("200".equals(String.valueOf(ajaxResult.get("code"))) && ajaxResult.get("data") != null) {
UserPo userPo = JSON.parseObject(JSONObject.toJSONString(ajaxResult.get("data")), UserPo.class);
if (userPo != null && shipperMatchesImporterOrganization(userPo, loginOrganizationId)) {
return userPo;
}
}
} catch (Exception e) {
// 继续货主列表兜底
}
// 货主列表兜底与WMS出库导入一致
try {
AjaxResult shipperListResult = userServiceFeign.userShipperListAll();
if ("200".equals(String.valueOf(shipperListResult.get("code"))) && shipperListResult.get("data") != null) {
Object data = shipperListResult.get("data");
List<Map<String, Object>> shipperList = null;
if (data instanceof List) {
List<Object> rawList = (List<Object>) data;
shipperList = new ArrayList<>();
for (Object item : rawList) {
if (item instanceof Map) {
Map<String, Object> mapItem = (Map<String, Object>) item;
shipperList.add(mapItem);
} else {
Map<String, Object> mapItem = JSON.parseObject(JSONObject.toJSONString(item), new TypeReference<Map<String, Object>>() {
});
shipperList.add(mapItem);
}
}
} else {
String jsonStr = JSONObject.toJSONString(data);
shipperList = JSON.parseObject(jsonStr, new TypeReference<List<Map<String, Object>>>() {
});
}
if (shipperList != null && !shipperList.isEmpty()) {
for (Map<String, Object> shipper : shipperList) {
Object userId = shipper.get("userId");
Object userName = shipper.get("userName");
if (userName == null) {
userName = shipper.get("name");
}
if (userName == null) {
userName = shipper.get("shipperName");
}
boolean matched = false;
if (StringUtils.isNotEmpty(shipperName) && userName != null) {
String normalizedUserName = String.valueOf(userName).trim();
if (shipperName.equals(normalizedUserName) || normalizedUserName.contains(shipperName)) {
matched = true;
}
}
if (matched && userId != null) {
Long candidateId = Long.valueOf(String.valueOf(userId));
AjaxResult infoRes = userServiceFeign.getInfo(candidateId);
if ("200".equals(String.valueOf(infoRes.get("code"))) && infoRes.get("data") != null) {
UserPo up = JSON.parseObject(JSONObject.toJSONString(infoRes.get("data")), UserPo.class);
if (shipperMatchesImporterOrganization(up, loginOrganizationId)) {
return up;
}
}
}
}
}
}
} catch (Exception e) {
// ignore
}
return null;
}
}
@@ -0,0 +1,201 @@
package com.mhd.oms.domain.outEntrustOrder.repository.listener;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDO;
import lombok.Getter;
import org.apache.poi.ss.usermodel.DateUtil; // Excel序列号转日期POI已随EasyExcel引入
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 出仓委托单导入-表头信息监听器处理前11行与WMS出库管理导入模板一致
*
* @author rcx
* @date 2026-09-14
*/
@Getter
public class OutEntrustHeaderListener extends AnalysisEventListener<Map<Integer, Object>> {
private final OutEntrustOrderDO outEntrustOrder = new OutEntrustOrderDO();
private int rowNum = 0;
/** 日期单元格有值但格式无法识别的收集器(类上已有@Getter,自动生成getDateParseErrors() */
private final List<String> dateParseErrors = new ArrayList<>();
private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@Override
public void invoke(Map<Integer, Object> data, AnalysisContext context) {
// 将Map<Integer, Object>转换为Map<Integer, String>
Map<Integer, String> stringData = convertToStringMap(data);
// 将Map转换为List再转为数组
List<String> dataList = mapToList(stringData);
Object[] rowData = listToArray(dataList);
String stringValue1 = getStringValue(rowData[0]);
if (stringValue1 != null && !stringValue1.isEmpty()) {
if ("备注:".equals(stringValue1)) {
outEntrustOrder.setRemark(getStringValue(rowData[1]));
}
}
// 根据行号提取对应信息
switch (rowNum) {
case 4: // 第5行to仓库联系人车型及车牌委托编码
outEntrustOrder.setToReceiver(getStringValue(rowData[2]));
outEntrustOrder.setContactPerson(getStringValue(rowData[5]));
outEntrustOrder.setVehicleModelPlate(getStringValue(rowData[12]));
outEntrustOrder.setEntrustNo(getStringValue(rowData[16]));
break;
case 5: // 第6行承运方tel柜号司机签名
outEntrustOrder.setCarrier(getStringValue(rowData[2]));
outEntrustOrder.setTel(getStringValue(rowData[5]));
outEntrustOrder.setContainerNoName(getStringValue(rowData[12]));
outEntrustOrder.setDriverSign(getStringValue(rowData[16]));
break;
case 6: // 第7行出仓日期fax运输方式申报地关区
Date outboundDate = parseDateSmart(getStringValue(rowData[2]), "出仓日期");
if (outboundDate != null) {
outEntrustOrder.setOutboundDate(outboundDate);
}
outEntrustOrder.setFax(getStringValue(rowData[5]));
outEntrustOrder.setTransportModeName(getStringValue(rowData[12]));
outEntrustOrder.setDeclareCustoms(getStringValue(rowData[16]));
break;
case 7: // 第8行生产商客户名称报关姓名及联系方式完成时间
outEntrustOrder.setManufacturer(getStringValue(rowData[2]));
outEntrustOrder.setShipperName(getStringValue(rowData[5]));
outEntrustOrder.setCustomsDeclarerInfo(getStringValue(rowData[12]));
Date completeTime = parseDateSmart(getStringValue(rowData[16]), "完成时间");
if (completeTime != null) {
outEntrustOrder.setCompleteTime(completeTime);
}
break;
case 8: // 第9行监管方式送货地址海关是否查验货物进出境关别
outEntrustOrder.setSupervisionModeName(getStringValue(rowData[2]));
outEntrustOrder.setDeliveryAddr(getStringValue(rowData[5]));
String stringValue = getStringValue(rowData[12]);
outEntrustOrder.setCustomsInspectionFlag(stringValue);
outEntrustOrder.setEntryExitCustoms(getStringValue(rowData[16]));
break;
case 9: // 第10行下单日期起运国
Date orderDate = parseDateSmart(getStringValue(rowData[2]), "下单日期");
if (orderDate != null) {
outEntrustOrder.setOrderDate(orderDate);
}
outEntrustOrder.setDepartureArrivalCountry(getStringValue(rowData[5]));
break;
}
rowNum++;
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 解析完成后的处理
}
/**
* 将List转换为数组
*/
private Object[] listToArray(List<String> list) {
// 创建一个足够大的数组来容纳最多30列数据
Object[] array = new Object[30];
for (int i = 0; i < 30 && i < list.size(); i++) {
array[i] = list.get(i);
}
return array;
}
/**
* 将Object的Map转换为String的Map
*/
private Map<Integer, String> convertToStringMap(Map<Integer, Object> dataMap) {
if (dataMap == null) {
return new HashMap<>();
}
Map<Integer, String> stringMap = new HashMap<>();
for (Map.Entry<Integer, Object> entry : dataMap.entrySet()) {
Object value = entry.getValue();
String stringValue = value != null ? value.toString() : "";
stringMap.put(entry.getKey(), stringValue);
}
return stringMap;
}
/**
* 将Map转换为List
*/
private List<String> mapToList(Map<Integer, String> map) {
if (map == null) {
return new ArrayList<>();
}
// 找到最大索引以确定列表大小
int maxSize = map.keySet().stream().mapToInt(Integer::intValue).max().orElse(-1) + 1;
maxSize = Math.max(maxSize, 30); // 至少30个位置
List<String> list = new ArrayList<>(Collections.nCopies(maxSize, ""));
for (Map.Entry<Integer, String> entry : map.entrySet()) {
if (entry.getKey() >= 0 && entry.getKey() < maxSize) {
String value = entry.getValue();
list.set(entry.getKey(), value != null ? value : "");
}
}
return list;
}
/**
* 转换为字符串处理null值
*/
private String getStringValue(Object value) {
return value == null ? "" : value.toString().trim();
}
/**
* 智能解析日期兼容 Excel 日期单元格序列号 + 常见文本格式
* 解析失败记入 dateParseErrors由解析服务汇总返回给用户不再静默丢失
* @param raw 单元格原始字符串
* @param fieldLabel 字段中文名用于错误提示
*/
private Date parseDateSmart(String raw, String fieldLabel) {
String s = raw == null ? "" : raw.trim();
if (s.isEmpty()) {
return null; // 单元格为空保持原行为不算错误
}
// 1) Excel 日期序列号NoConvertConverter 会把真日期单元格转成纯数字字符串 46234 = 2026-08-20
// 区间 20000~60000 对应 1954~2064 避免把普通数字误判成日期
if (s.matches("^\\d{1,5}(\\.\\d+)?$")) {
try {
double serial = Double.parseDouble(s);
if (serial >= 20000 && serial <= 60000) {
return DateUtil.getJavaDate(serial);
}
} catch (NumberFormatException ignore) {
}
}
// 2) 常见文本格式逐个尝试
String[] patterns = {
"yyyy-MM-dd", "yyyy/M/d", "yyyy.M.d", "yyyy年M月d日",
"yyyy-MM-dd HH:mm:ss", "yyyy/M/d H:mm:ss", "yyyy-MM-dd H:mm"
};
for (String p : patterns) {
try {
SimpleDateFormat sdf = new SimpleDateFormat(p);
sdf.setLenient(false);
return sdf.parse(s);
} catch (ParseException ignore) {
}
}
dateParseErrors.add(fieldLabel + "" + s + "」格式无法识别,请改为日期格式或 2026-08-20 / 2026/8/20 / 2026年8月20日 等格式");
return null;
}
}
@@ -0,0 +1,282 @@
package com.mhd.oms.domain.outEntrustOrder.repository.listener;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDetailDO;
import lombok.Getter;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 出仓委托单导入-商品明细监听器从第12行开始与WMS出库管理导入模板一致
*
* @author rcx
* @date 2026-09-14
*/
@Getter
public class OutEntrustItemListener extends AnalysisEventListener<Map<Integer, Object>> {
private final List<OutEntrustOrderDetailDO> itemList = new ArrayList<>();
@Override
public void invoke(Map<Integer, Object> data, AnalysisContext context) {
// 将Map<Integer, Object>转换为Map<Integer, String>
Map<Integer, String> stringData = convertToStringMap(data);
// 将Map转换为List再转为数组
List<String> dataList = mapToList(stringData);
Object[] rowData = listToArray(dataList);
// 提取序号第一列过滤空行和标题行
String serialNoStr = getStringValue(rowData[0]);
String commodityName = getStringValue(rowData[1]);
String materialNo = getStringValue(rowData[2]);
if (commodityName.isEmpty() && materialNo.isEmpty()) {
return;
}
if (serialNoStr.isEmpty() || "序号".equals(serialNoStr) || !isSequenceNumber(serialNoStr)) {
return;
}
OutEntrustOrderDetailDO item = new OutEntrustOrderDetailDO();
// 填充商品明细数据
//商品名称 料号 规格型号/ITEM 商品SKU码 Invoice No. 申报数量 申报单位 箱数 件数 尺寸 出库数量 "总净重
//(KG)" 总毛重 (KG) "总体积
//(CBM)" 总面积(SQM) Batch Ref NO's Sheet Ref NO's 原产国 箱号/卡板号 总价 币制 备注
item.setCommodityName(getStringValue(rowData[1]));
item.setMaterialNo(getStringValue(rowData[2]));
item.setSpecificationModel(getStringValue(rowData[3]));
item.setSkucode(getStringValue(rowData[4]));
item.setInvoiceNo(getStringValue(rowData[5]));
item.setDeclareQuantity(parseBigDecimal(rowData[6]));
item.setDeclareUnit(getStringValue(rowData[7]));
item.setLiter(normalizeNumeric(getStringValue(rowData[8])));
item.setCaseCount(parseBigDecimal(rowData[9]));
item.setPieceCount(parseBigDecimal(rowData[10]));
item.setSize(getStringValue(rowData[11]));
item.setOutboundQuantity(parseBigDecimal(rowData[12]));
item.setTotalNetWeight(parseBigDecimal(rowData[13]));
item.setTotalGrossWeight(parseBigDecimal(rowData[14]));
item.setTotalVolume(parseBigDecimal(rowData[15]));
item.setTotalArea(parseBigDecimal(rowData[16]));
item.setBatchRefNo(getStringValue(rowData[17]));
item.setSheetRefNo(getStringValue(rowData[18]));
item.setCountryOfOrigin(getStringValue(rowData[19]));
item.setBoxPalletNo(getStringValue(rowData[20]));
item.setTotalAmount(parseBigDecimal(rowData[21]));
item.setCurrency(getStringValue(rowData[22]));
item.setRemark(getStringValue(rowData[23]));
itemList.add(item);
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 解析完成后的处理
}
/**
* 将List转换为数组
*/
private Object[] listToArray(List<String> list) {
// 创建一个足够大的数组来容纳最多30列数据
Object[] array = new Object[30];
for (int i = 0; i < 30 && i < list.size(); i++) {
array[i] = list.get(i);
}
return array;
}
/**
* 将Object的Map转换为String的Map
*/
private Map<Integer, String> convertToStringMap(Map<Integer, Object> dataMap) {
if (dataMap == null) {
return new HashMap<>();
}
Map<Integer, String> stringMap = new HashMap<>();
for (Map.Entry<Integer, Object> entry : dataMap.entrySet()) {
Object value = entry.getValue();
String stringValue = convertToString(value);
stringMap.put(entry.getKey(), stringValue);
}
return stringMap;
}
/**
* 将Object转换为String处理BigDecimal等数字类型去除小数点后的零
*/
private String convertToString(Object value) {
if (value == null) {
return "";
}
// 如果是BigDecimal类型需要特殊处理去除小数点后的零
if (value instanceof BigDecimal) {
BigDecimal bd = (BigDecimal) value;
// 如果小数部分为0转换为整数形式
if (bd.scale() > 0 && bd.stripTrailingZeros().scale() <= 0) {
return String.valueOf(bd.longValue());
}
// 否则去除尾部零
return bd.stripTrailingZeros().toPlainString();
}
// 如果是Double或Float类型也需要处理
if (value instanceof Double) {
Double d = (Double) value;
if (d == d.longValue()) {
return String.valueOf(d.longValue());
}
return d.toString();
}
if (value instanceof Float) {
Float f = (Float) value;
if (f == f.longValue()) {
return String.valueOf(f.longValue());
}
return f.toString();
}
if (value instanceof String) {
String str = value.toString().trim();
if (str.isEmpty()) {
return "";
}
return str;
}
return value.toString().trim();
}
/**
* 将Map转换为List
*/
private List<String> mapToList(Map<Integer, String> map) {
if (map == null) {
return new ArrayList<>();
}
// 找到最大索引以确定列表大小
int maxSize = map.keySet().stream().mapToInt(Integer::intValue).max().orElse(-1) + 1;
maxSize = Math.max(maxSize, 30); // 至少30个位置
List<String> list = new ArrayList<>(Collections.nCopies(maxSize, ""));
for (Map.Entry<Integer, String> entry : map.entrySet()) {
if (entry.getKey() >= 0 && entry.getKey() < maxSize) {
String value = entry.getValue();
list.set(entry.getKey(), value != null ? value : "");
}
}
return list;
}
private String getStringValue(Object value) {
if (value == null) {
return "";
}
// 如果是BigDecimal类型需要特殊处理去除小数点后的零
if (value instanceof BigDecimal) {
BigDecimal bd = (BigDecimal) value;
// 如果小数部分为0转换为整数形式
if (bd.scale() > 0 && bd.stripTrailingZeros().scale() <= 0) {
return String.valueOf(bd.longValue());
}
// 否则去除尾部零
return bd.stripTrailingZeros().toPlainString();
}
// 如果是Double或Float类型也需要处理
if (value instanceof Double) {
Double d = (Double) value;
if (d == d.longValue()) {
return String.valueOf(d.longValue());
}
return d.toString();
}
if (value instanceof Float) {
Float f = (Float) value;
if (f == f.longValue()) {
return String.valueOf(f.longValue());
}
return f.toString();
}
if (value instanceof String) {
String str = value.toString().trim();
if (str.isEmpty()) {
return "";
}
// 尝试解析为数字如果是整数形式的小数 "123.0"转换为整数
try {
BigDecimal bd = new BigDecimal(str);
// 如果小数部分为0转换为整数形式
if (bd.scale() > 0 && bd.stripTrailingZeros().scale() <= 0) {
return String.valueOf(bd.longValue());
}
// 否则去除尾部零
return bd.stripTrailingZeros().toPlainString();
} catch (NumberFormatException e) {
// 不是数字字符串直接返回
return str;
}
}
return value.toString().trim();
}
/**
* 解析BigDecimal失败返回0
*/
private BigDecimal parseBigDecimal(Object value) {
try {
String strValue = getStringValue(value);
if (strValue.isEmpty()) {
return BigDecimal.ZERO;
}
return new BigDecimal(strValue);
} catch (Exception e) {
return BigDecimal.ZERO;
}
}
/**
* 升列归一化去千分位逗号去空白空值统一存 null避免前端合计时对空串求和
* 纯数字归一为无多余零的字符串非数字内容原样保留
*/
private String normalizeNumeric(String raw) {
if (raw == null) {
return null;
}
String s = raw.trim().replace(",", "");
if (s.isEmpty()) {
return null;
}
try {
return new BigDecimal(s).stripTrailingZeros().toPlainString();
} catch (NumberFormatException e) {
return s;
}
}
public static boolean isSequenceNumber(String str) {
if (str == null || str.trim().isEmpty()) {
return false;
}
str = str.trim();
return str.matches("^[1-9]\\d*$") || str.matches("^[1-9]\\d*\\.\\d+$") || str.matches("^0\\.\\d+$");
}
}
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mhd.oms.domain.outEntrustOrder.entity.OutEntrustOrder;
import com.mhd.oms.domain.outEntrustOrder.repository.po.OutEntrustOrderPO;
import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDO;
import com.mhd.oms.domain.reservationStockInOrder.repository.po.MaterialBaseInfoPO;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@@ -25,4 +26,25 @@ public interface OutEntrustOrderMapper extends BaseMapper<OutEntrustOrder> {
* 根据编号集合查询出仓委托单列表
*/
List<OutEntrustOrderPO> getInfoByIds(@Param("idList") List<Long> idList);
/**
* 导入用按料号查询货主下的物料基础信息(跨库WMS物料表)
*/
List<MaterialBaseInfoPO> getMaterialByMaterialCode(@Param("materialCode") String materialCode,
@Param("shipperId") Long shipperId,
@Param("topOrganizationId") Long topOrganizationId);
/**
* 导入用按SKU码(条形码)查询货主下的物料基础信息(跨库WMS物料表)
*/
List<MaterialBaseInfoPO> getMaterialByBarCode(@Param("barCode") String barCode,
@Param("shipperId") Long shipperId,
@Param("topOrganizationId") Long topOrganizationId);
/**
* 导入用按物料名称查询货主下的物料基础信息(跨库WMS物料表)
*/
List<MaterialBaseInfoPO> getMaterialByMaterialName(@Param("materialName") String materialName,
@Param("shipperId") Long shipperId,
@Param("topOrganizationId") Long topOrganizationId);
}
@@ -102,6 +102,12 @@ public class QueryOrderOverStockDO extends BaseVOEntity {
@Excel(name = "出库单类型编码")
private String outOrderTypeCode;
@ApiModelProperty("出库单类型编码(与outOrderTypeCode同义)")
private String orderTypeCode;
@ApiModelProperty("出库单类型名称")
private String orderTypeName;
@ApiModelProperty("出库单类型名称")
@Excel(name = "出库单类型名称")
private String outOrderTypeName;
@@ -137,4 +143,10 @@ public class QueryOrderOverStockDO extends BaseVOEntity {
@ApiModelProperty("越库单状态 0:无状态 1:处理中 2:越库完成")
@Excel(name = "越库单状态 0:无状态 1:处理中 2:越库完成")
private Integer overStockStatus;
@ApiModelProperty("实际出仓日期 开始(yyyy-MM-dd,为空按建档时间兜底)")
private String outboundDateStart;
@ApiModelProperty("实际出仓日期 结束(yyyy-MM-dd,为空按建档时间兜底)")
private String outboundDateEnd;
}
@@ -456,4 +456,21 @@ public class ReservationStockInOrder extends BaseVOEntity {
@ApiModelProperty("业务计价方式")
private String businessPriceWay;
@ApiModelProperty("反审状态: 0-无 1-已反审")
private Integer reverseAuditStatus;
@ApiModelProperty("反审原因")
private String reverseAuditRemark;
@ApiModelProperty("反审人id")
private Long reverseAuditBy;
@ApiModelProperty("反审人姓名")
private String reverseAuditByName;
@ApiModelProperty("反审时间")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date reverseAuditTime;
}
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
import com.mhd.oms.domain.reservationInMaterialDetail.repository.po.ReservationInMaterialDetailPO;
import com.mhd.oms.domain.reservationStockInOrder.entity.ReservationStockInOrder;
import com.mhd.oms.domain.reservationStockInOrder.repository.po.ReservationStockInOrderPO;
import com.mhd.oms.domain.reservationStockInOrder.repository.po.ReservationStockInOrderTotalPO;
import com.mhd.oms.domain.reservationStockInOrder.repository.todo.ReservationStockInOrderDO;
import com.mhd.oms.domain.reservationStockInOrder.repository.vo.DeliveryDataDTO;
import com.mhd.oms.domain.reservationStockInOrder.repository.vo.SheetImportResultDTO;
@@ -26,6 +27,11 @@ public interface IReservationStockInOrderService extends IService<ReservationSto
*/
public List<ReservationStockInOrderPO> queryList(ReservationStockInOrderDO stockInOrderDO);
/**
* 列表合计按查询条件全量(不分页)汇总计划数量与入库数量
*/
public ReservationStockInOrderTotalPO queryListTotal(ReservationStockInOrderDO stockInOrderDO);
/**
* 新增入库单
*/
@@ -8,6 +8,7 @@ import com.mhd.oms.domain.reservationStockInOrder.repository.po.MaterialBaseInfo
import com.mhd.oms.domain.reservationStockInOrder.repository.po.MaterialGoodsRulePO;
import com.mhd.oms.domain.reservationStockInOrder.repository.po.MaterialWarehouseControlPO;
import com.mhd.oms.domain.reservationStockInOrder.repository.po.ReservationStockInOrderPO;
import com.mhd.oms.domain.reservationStockInOrder.repository.po.ReservationStockInOrderTotalPO;
import com.mhd.oms.domain.reservationStockInOrder.repository.todo.ReservationStockInOrderDO;
import org.apache.ibatis.annotations.Param;
@@ -27,6 +28,11 @@ public interface ReservationStockInOrderMapper extends BaseMapper<ReservationSto
*/
public List<ReservationStockInOrderPO> queryList(ReservationStockInOrderDO stockInOrderDO);
/**
* 列表合计按查询条件全量(不分页)汇总计划数量与入库数量
*/
public ReservationStockInOrderTotalPO queryListTotal(ReservationStockInOrderDO stockInOrderDO);
public MaterialBaseInfoPO getinfo(@Param("id") Long id);
public MaterialBaseInfoPO getByName(@Param("name") String name);
@@ -54,6 +60,13 @@ public interface ReservationStockInOrderMapper extends BaseMapper<ReservationSto
public String getWarehouseName(@Param("id") Long id);
/**
* 本地跨库直查仓库信息(编码/名称/单号生成规则),
* 供下发等流程在 system 服务 Feign 失败/降级时兜底, getWarehouseInfoByWarehouseId Feign 接口同数据源
* 不含 STOCK_LOCK_MODE(冻结功能列, 各环境库结构不一致), 兜底时 stockLockMode null 即默认分配库存冻结模式
*/
public com.mhd.system.api.domain.WarehouseFeignPO getWarehouseInfoFallback(@Param("id") Long id);
List<ReservationStockInOrderPO> queryNoticePrint(ReservationStockInOrderDO stockInOrderDO);
List<ReservationInMaterialDetailPO> queryNoticePrintDetail(ReservationStockInOrderDO stockInOrderDO);
public void updateByTransportationNumber(@Param("transportationNumber") String transportationNumber);
@@ -34,6 +34,7 @@ import com.mhd.oms.domain.reservationStockInOrder.repository.listener.StockInExc
import com.mhd.oms.domain.reservationStockInOrder.repository.mapper.ReservationStockInOrderMapper;
import com.mhd.oms.domain.reservationStockInOrder.repository.po.MaterialBaseInfoPO;
import com.mhd.oms.domain.reservationStockInOrder.repository.po.ReservationStockInOrderPO;
import com.mhd.oms.domain.reservationStockInOrder.repository.po.ReservationStockInOrderTotalPO;
import com.mhd.oms.domain.reservationStockInOrder.repository.todo.ReservationStockInOrderDO;
import com.mhd.oms.domain.reservationStockInOrder.repository.vo.*;
import com.mhd.oms.domain.reservationStockInOrder.service.ReservationStockInOrderDomainService;
@@ -106,6 +107,15 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
return stockInOrderMapper.queryList(stockInOrderDO);
}
/**
* 列表合计按查询条件全量(不分页)汇总计划数量与入库数量
*/
@Override
public ReservationStockInOrderTotalPO queryListTotal(ReservationStockInOrderDO stockInOrderDO)
{
return stockInOrderMapper.queryListTotal(stockInOrderDO);
}
/**
* 新增入库单
*/
@@ -502,4 +502,21 @@ public class ReservationStockInOrderPO extends BaseVOEntity {
@ApiModelProperty("业务计价方式")
private String businessPriceWay;
private String khdm;
@ApiModelProperty("反审状态: 0-无 1-已反审")
private Integer reverseAuditStatus;
@ApiModelProperty("反审原因")
private String reverseAuditRemark;
@ApiModelProperty("反审人id")
private Long reverseAuditBy;
@ApiModelProperty("反审人姓名")
private String reverseAuditByName;
@ApiModelProperty("反审时间")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date reverseAuditTime;
}
@@ -0,0 +1,30 @@
package com.mhd.oms.domain.reservationStockInOrder.repository.po;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 入库业务单列表合计PO
*
* <p>当前筛选条件下不分页的全量合计随分页列表一起返回
* totalPlanQuantity/totalInQuantity计划数量与入库数量分开计算</p>
*
* @author rcx
* @date 2026-09-14
*/
@Data
@ApiModel(value = "入库业务单列表合计对象", description = "入库业务单列表合计响应对象")
public class ReservationStockInOrderTotalPO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("计划数量总数")
private BigDecimal totalPlanQuantity;
@ApiModelProperty("入库数量总数")
private BigDecimal totalInQuantity;
}
@@ -513,4 +513,21 @@ public class ReservationStockInOrderDO extends BaseVOEntity {
@ApiModelProperty("业务计价方式")
private String businessPriceWay;
@ApiModelProperty("反审状态: 0-无 1-已反审")
private Integer reverseAuditStatus;
@ApiModelProperty("反审原因")
private String reverseAuditRemark;
@ApiModelProperty("反审人id")
private Long reverseAuditBy;
@ApiModelProperty("反审人姓名")
private String reverseAuditByName;
@ApiModelProperty("反审时间")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date reverseAuditTime;
}
@@ -507,4 +507,21 @@ public class ReservationStockInOrderDTO extends ReservationStockInOrderBaseDTO {
@ApiModelProperty("业务计价方式")
private String businessPriceWay;
@ApiModelProperty("反审状态: 0-无 1-已反审")
private Integer reverseAuditStatus;
@ApiModelProperty("反审原因")
private String reverseAuditRemark;
@ApiModelProperty("反审人id")
private Long reverseAuditBy;
@ApiModelProperty("反审人姓名")
private String reverseAuditByName;
@ApiModelProperty("反审时间")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date reverseAuditTime;
}
@@ -10,6 +10,7 @@ import com.mhd.common.core.domain.po.UserPo;
import com.mhd.common.core.enums.DictCode;
import com.mhd.common.core.exception.ServiceException;
import com.mhd.common.core.utils.StringUtils;
import com.mhd.common.core.utils.poi.ExcelUtil;
import com.mhd.common.core.web.domain.AjaxResult;
import com.mhd.common.security.utils.SecurityUtils;
import com.mhd.oms.domain.erpCountry.entity.ErpCountry;
@@ -18,14 +19,17 @@ import com.mhd.oms.domain.gwLog.repository.mapper.GwLogMapper;
import com.mhd.oms.domain.reservationInMaterialDetail.entity.ReservationInMaterialDetail;
import com.mhd.oms.domain.reservationInMaterialDetail.repository.mapper.ReservationInMaterialDetailMapper;
import com.mhd.oms.domain.reservationInMaterialDetail.repository.po.ReservationInMaterialDetailPO;
import com.mhd.oms.domain.reservationInMaterialDetail.repository.todo.ReservationInMaterialDetailDO;
import com.mhd.oms.domain.reservationMaterialInventory.repository.facade.IReservationMaterialInventoryService;
import com.mhd.oms.domain.reservationMaterialInventory.repository.mapper.ReservationMaterialInventoryMapper;
import com.mhd.oms.domain.reservationStockInOrder.entity.ReservationStockInOrder;
import com.mhd.oms.domain.reservationStockInOrder.repository.mapper.ReservationStockInOrderMapper;
import com.mhd.oms.domain.reservationStockInOrder.repository.po.MaterialBaseInfoPO;
import com.mhd.oms.domain.reservationStockInOrder.repository.po.ReservationStockInOrderPO;
import com.mhd.oms.domain.reservationStockInOrder.repository.po.ReservationStockInOrderTotalPO;
import com.mhd.oms.domain.reservationStockInOrder.repository.todo.ReservationStockInOrderDO;
import com.mhd.oms.domain.reservationStockInOrder.repository.todo.ReservationStockInOrderDTO;
import com.mhd.oms.domain.reservationStockInOrder.repository.todo.ReservationStockInOrderImportRowDTO;
import com.mhd.oms.domain.reservationStockInOrder.repository.vo.GwInDetail;
import com.mhd.oms.domain.reservationStockInOrder.repository.vo.GwStockInOrder;
import com.mhd.oms.interfaces.dto.erpCountry.ErpCountryDTO;
@@ -47,6 +51,7 @@ import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.IOException;
@@ -180,6 +185,15 @@ public class ReservationStockInOrderApplicationService {
return stockInOrderDomainService.queryList(stockInOrderDO);
}
/**
* 列表合计按查询条件全量(不分页)汇总计划数量与入库数量
* 须在 queryList/queryList1 之后用同一个DO调用数据权限条件由列表查询原地写入DO
* 合计查询才能继承生效后的筛选 PageHelper startPage 只拦截下一条SQL本查询不会被分页
*/
public ReservationStockInOrderTotalPO queryListTotal(ReservationStockInOrderDO stockInOrderDO) {
return stockInOrderDomainService.queryListTotal(stockInOrderDO);
}
/**
* 新增入库单
@@ -282,8 +296,7 @@ public class ReservationStockInOrderApplicationService {
* @return Boolean
*/
public Boolean auditState(ReservationStockInOrderDO stockInOrderDO){
Boolean b = stockInOrderDomainService.auditState(stockInOrderDO);
// List<Long> inOrderIds = stockInOrderDO.getInOrderIds();
Boolean b = stockInOrderDomainService.auditState(stockInOrderDO);// List<Long> inOrderIds = stockInOrderDO.getInOrderIds();
// if (inOrderIds != null && !inOrderIds.isEmpty()) {
// ReservationStockInOrder stockInOrder = reservationStockInOrderMapper.selectById(inOrderIds.get(0));
// Integer needCustomsDeclaration = stockInOrder.getNeedCustomsDeclaration();
@@ -302,6 +315,15 @@ public class ReservationStockInOrderApplicationService {
return b;
}
/**
* 反审入库业务单业务单状态改为已驳回(issueStatus=3)并打已反审标记落入已反审tab
* 入库执行单+明细逻辑删除WMS侧整链回退库存扣减+单据清理+反审异常记录
* W入库单有出库记录时WMS侧拦截已推送关务直接拦截
*/
public Boolean reverseAudit(ReservationStockInOrderDO stockInOrderDO){
return stockInOrderDomainService.reverseAudit(stockInOrderDO);
}
/**
* @description 下发入库单
* @author ZhouGY
@@ -1582,6 +1604,635 @@ public class ReservationStockInOrderApplicationService {
// }
// }
/**
* 导入入仓委托单模板与WMS入库管理导入一致第1-12行表头第13行列名第14行起明细
* 委托编号NO作为分组键同一委托编号的多条明细归为一张入库单
*/
public Boolean importData(MultipartFile file, Long warehouseId, String warehouseCode, String warehouseName) {
if (file == null || file.isEmpty()) {
throw new ServiceException("导入文件不能为空");
}
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser == null) {
throw new ServiceException("登录已失效,请重新登录");
}
Long topOrganizationId = loginUser.getUserPo() != null ? loginUser.getUserPo().getTopOrganizationId() : null;
try (InputStream is = file.getInputStream()) {
// 明细表的列名在第13行索引12数据从第14行开始
int detailTitleRowIndex = 12;
// 1. 读取表头信息第1-12行按标签扫描提取字段
ReservationStockInOrderImportRowDTO headerInfo = new ReservationStockInOrderImportRowDTO();
try (InputStream isForHeader = file.getInputStream()) {
Workbook wb = WorkbookFactory.create(isForHeader);
Sheet sheet = wb.getSheetAt(0);
fillHeaderInfoByLabels(sheet, headerInfo);
} catch (Exception e) {
log.error("读取Excel表头信息失败", e);
// 不抛出异常继续尝试从明细行读取
}
log.info("表头信息读取完成 - 委托编号NO: {}, 客户名称: {}, To: {}, 联系人: {}, 承运商: {}, 入仓日期: {}, 完成时间: {}, 下单日期: {}",
headerInfo.getConsignmentNo(), headerInfo.getShipperName(), headerInfo.getTo(),
headerInfo.getContactPerson(), headerInfo.getCarrier(), headerInfo.getWarehouseDate(),
headerInfo.getCompletionTime(), headerInfo.getOrderDate());
// 2. 读取明细数据ExcelUtil从第13行列名开始
ExcelUtil<ReservationStockInOrderImportRowDTO> util = new ExcelUtil<>(ReservationStockInOrderImportRowDTO.class);
List<ReservationStockInOrderImportRowDTO> rows = util.importExcel(is, detailTitleRowIndex);
if (rows == null || rows.isEmpty()) {
throw new ServiceException("导入数据为空。请检查Excel第13行是否为明细表列名(包含'商品名称'列),第14行开始是否为数据行");
}
log.info("成功读取 {} 行明细数据", rows.size());
// 3. 将表头信息应用到每一行明细行值为空时使用表头值
applyHeaderToRows(headerInfo, rows);
// 4. 过滤掉商品名称为空的空行
List<ReservationStockInOrderImportRowDTO> validRows = rows.stream()
.filter(r -> r != null && StringUtils.isNotBlank(r.getMaterialName()))
.collect(Collectors.toList());
if (validRows.isEmpty()) {
throw new ServiceException("导入数据为空(可能全为空行)。共读取 " + rows.size()
+ " 行数据,但商品名称都为空。请检查Excel第13行是否为明细表列名(包含'商品名称'列),第14行开始是否为数据行");
}
// 5. 仓库必填校验与出库单导入保持一致仓库由导入弹窗选择传入
if (warehouseId == null) {
throw new ServiceException("导入失败:仓库ID不能为空");
}
if (StringUtils.isBlank(warehouseCode)) {
throw new ServiceException("导入失败:仓库编码不能为空");
}
if (StringUtils.isBlank(warehouseName)) {
throw new ServiceException("导入失败:仓库名称不能为空");
}
// 6. 以委托编号NO分组若为空则归到同一组
Map<String, List<ReservationStockInOrderImportRowDTO>> groupMap = validRows.stream()
.collect(Collectors.groupingBy(r -> StringUtils.isBlank(r.getConsignmentNo()) ? "__EMPTY_CONSIGNMENT__" : r.getConsignmentNo()));
List<ReservationStockInOrderDO> orderList = new ArrayList<>();
for (Map.Entry<String, List<ReservationStockInOrderImportRowDTO>> entry : groupMap.entrySet()) {
ReservationStockInOrderDO order = buildStockInOrderFromImport(
entry.getValue(), headerInfo, loginUser, topOrganizationId,
warehouseId, warehouseCode, warehouseName);
if (order != null) {
orderList.add(order);
}
}
// 如果所有订单都被跳过orderList为空抛出异常提示用户
if (orderList.isEmpty()) {
throw new ServiceException("导入失败:所有入库单的明细行都因数据不完整(数量为空或物料基础信息缺失)被跳过,请检查Excel数据");
}
return stockInOrderDomainService.batchInsert(orderList);
} catch (ServiceException e) {
throw e;
} catch (Exception e) {
log.error("导入入库单失败", e);
throw new ServiceException("导入入库单失败:" + e.getMessage());
}
}
/**
* 单个委托编号分组的入库单组装解析+校验不落库
* 明细行数量为空/容器类型行跳过料号必填且必须在当前货主下存在规格型号/SKU码与物料档案不一致直接报错
*
* @return 组装完成的入库单该组明细全部被跳过时返回null
*/
private ReservationStockInOrderDO buildStockInOrderFromImport(List<ReservationStockInOrderImportRowDTO> groupRows,
ReservationStockInOrderImportRowDTO headerInfo,
LoginUser loginUser, Long topOrganizationId,
Long warehouseId, String warehouseCode, String warehouseName) {
ReservationStockInOrderImportRowDTO h = groupRows.get(0);
// 客户名称优先使用表头表头为空时使用明细行
String customerName = StringUtils.isNotBlank(headerInfo.getShipperName()) ? headerInfo.getShipperName() : h.getShipperName();
if (StringUtils.isBlank(customerName)) {
throw new ServiceException("导入失败:客户名称不能为空(委托编号NO=" + h.getConsignmentNo()
+ ")。请确保Excel表头(第1-12行)中包含'客户名称'字段");
}
ReservationStockInOrderDO order = new ReservationStockInOrderDO();
// 组织/审计字段
if (loginUser.getUserPo() != null) {
order.setOrganizationId(loginUser.getUserPo().getOrganizationId());
order.setOrganizationName(loginUser.getUserPo().getOrganizationName());
order.setTopOrganizationId(loginUser.getUserPo().getTopOrganizationId());
}
order.setCreateBy(loginUser.getUserid());
order.setCreateByName(resolveUserRealName(loginUser));
order.setCreateTime(new Date());
// 仓库导入弹窗选择传入
order.setWarehouseId(warehouseId);
order.setWarehouseCode(warehouseCode);
order.setWarehouseName(warehouseName);
// 货主信息名称/编号解析货主ID
String shipperCode = StringUtils.isNotBlank(headerInfo.getShipperCode()) ? headerInfo.getShipperCode() : h.getShipperCode();
Long shipperId = getShipperIdByNameOrCode(customerName, shipperCode);
order.setShipperId(shipperId);
if (StringUtils.isNotBlank(customerName)) {
order.setShipperName(customerName);
}
if (StringUtils.isNotBlank(shipperCode)) {
order.setShipperCode(shipperCode);
}
order.setConsignmentNo(h.getConsignmentNo());
order.setRemark(h.getRemark());
// 导入入库单时如果入库单类型为空默认设置为普通入库
order.setOrderTypeCode("pu_tong_ru_ku");
order.setOrderTypeName("普通入库单");
// 订单来源数据导入与入库业务单多页签导入保持一致
order.setReservationOrderSource("数据导入");
// 报关表头字段优先使用表头字段表头为空则使用明细行字段
order.setTo(firstNotBlank(headerInfo.getTo(), h.getTo()));
order.setContactPerson(firstNotBlank(headerInfo.getContactPerson(), h.getContactPerson()));
order.setVehicleInfo(firstNotBlank(headerInfo.getVehicleInfo(), h.getVehicleInfo()));
order.setTel(firstNotBlank(headerInfo.getTel(), h.getTel()));
order.setDriverSignature(firstNotBlank(headerInfo.getDriverSignature(), h.getDriverSignature()));
order.setFax(firstNotBlank(headerInfo.getFax(), h.getFax()));
order.setDeclarationArea(firstNotBlank(headerInfo.getDeclarationArea(), h.getDeclarationArea()));
order.setManufacturer(firstNotBlank(headerInfo.getManufacturer(), h.getManufacturer()));
order.setCustomsBrokerInfo(firstNotBlank(headerInfo.getCustomsBrokerInfo(), h.getCustomsBrokerInfo()));
order.setDeliveryAddress(firstNotBlank(headerInfo.getDeliveryAddress(), h.getDeliveryAddress()));
order.setEntryExitCustoms(firstNotBlank(headerInfo.getEntryExitCustoms(), h.getEntryExitCustoms()));
order.setOriginDestinationCountry(firstNotBlank(headerInfo.getOriginDestinationCountry(), h.getOriginDestinationCountry()));
order.setCarrier(firstNotBlank(headerInfo.getCarrier(), h.getCarrier()));
order.setContainerNo(firstNotBlank(headerInfo.getContainerNo(), h.getContainerNo()));
order.setTransportMethod(firstNotBlank(headerInfo.getTransportMethod(), h.getTransportMethod()));
order.setSupervisionMethod(firstNotBlank(headerInfo.getSupervisionMethod(), h.getSupervisionMethod()));
// 报关字段默认值优先使用表头字段
Integer customsInspection = headerInfo.getCustomsInspection() != null ? headerInfo.getCustomsInspection() : h.getCustomsInspection();
order.setCustomsInspection(customsInspection == null ? 0 : customsInspection); // 默认
Integer needCustomsDeclaration = headerInfo.getNeedCustomsDeclaration() != null ? headerInfo.getNeedCustomsDeclaration() : h.getNeedCustomsDeclaration();
order.setNeedCustomsDeclaration(needCustomsDeclaration == null ? 1 : needCustomsDeclaration); // 默认
Integer needTransport = headerInfo.getNeedTransport() != null ? headerInfo.getNeedTransport() : h.getNeedTransport();
order.setNeedTransport(needTransport == null ? 0 : needTransport); // 默认
// 日期字段优先使用表头字段
String warehouseDateStr = firstNotBlank(headerInfo.getWarehouseDate(), h.getWarehouseDate());
if (StringUtils.isNotBlank(warehouseDateStr)) {
Date warehouseDate = parseDateString(warehouseDateStr);
if (warehouseDate != null) {
order.setWarehouseDate(warehouseDate);
}
}
String completionTimeStr = firstNotBlank(headerInfo.getCompletionTime(), h.getCompletionTime());
if (StringUtils.isNotBlank(completionTimeStr)) {
Date completionTime = parseDateString(completionTimeStr);
if (completionTime != null) {
order.setCompletionTime(completionTime);
}
}
String orderDateStr = firstNotBlank(headerInfo.getOrderDate(), h.getOrderDate());
if (StringUtils.isNotBlank(orderDateStr)) {
Date orderDate = parseDateString(orderDateStr);
if (orderDate != null) {
order.setOrderDate(orderDate);
}
}
// 明细
List<ReservationInMaterialDetailDO> details = new ArrayList<>();
for (ReservationStockInOrderImportRowDTO r : groupRows) {
// 先检查数量仓库数量优先入库数量兜底数量为空直接跳过该明细行
BigDecimal quantity = r.getWarehouseQuantity();
if (quantity == null) {
quantity = r.getInboundQuantity();
}
if (quantity == null) {
log.warn("跳过明细行:仓库数量或入库数量不能为空 - 委托编号NO: [{}], 商品名称: [{}], 料号: [{}]",
h.getConsignmentNo(), r.getMaterialName(), r.getMaterialCode());
continue;
}
// 检查是否为容器类型等非物料信息如果是则跳过
String materialName = r.getMaterialName();
if (StringUtils.isNotBlank(materialName)) {
String lowerName = materialName.trim();
if (lowerName.contains("尺柜") || lowerName.contains("尺集装箱")
|| lowerName.contains("Container") || lowerName.contains("container")
|| (lowerName.contains("") && (lowerName.contains("20") || lowerName.contains("40") || lowerName.contains("45")))) {
log.info("跳过明细行:识别为容器类型信息,非实际物料 - 委托编号NO: [{}], 商品名称: [{}]",
h.getConsignmentNo(), materialName);
continue;
}
}
// 导入文件中必须填写料号根据料号获取商品信息必须是货主下有的才可以匹配
if (StringUtils.isBlank(r.getMaterialCode())) {
throw new ServiceException("导入失败:请在导入文件中填写正确的料号");
}
// 根据料号跨库查询物料基础信息getByCode按组织过滤查到后需在Java侧核对货主归属
MaterialBaseInfoPO materialBaseInfoPO = safeGetMaterialByCode(r.getMaterialCode(), loginUser);
if (materialBaseInfoPO != null && shipperId != null
&& (materialBaseInfoPO.getShipperId() == null || !materialBaseInfoPO.getShipperId().equals(shipperId))) {
// 查到但不属于该货主等同于不存在不允许导入
materialBaseInfoPO = null;
}
if (materialBaseInfoPO == null) {
// 如果提供了料号但查询不到或不属于该货主直接报错不允许导入
throw new ServiceException(String.format("导入失败:该货主下不存在该料号(货主名称=%s,料号=%s)。请确保该料号已在物料管理中创建,且属于该货主",
order.getShipperName(), r.getMaterialCode()));
}
// 校验料号与规格型号/SKU码一致性与WMS入库管理导入一致任何一项不符直接报对应错误
String excelSpec = StringUtils.trimToEmpty(r.getSpecificationModel());
String dbSpec = StringUtils.trimToEmpty(materialBaseInfoPO.getSpecificationModel());
if (!excelSpec.equals(dbSpec)) {
throw new ServiceException(String.format("导入失败:料号[%s](商品名称:%s)规格型号与物料档案不一致(Excel:%s / 档案:%s",
r.getMaterialCode(), r.getMaterialName(),
excelSpec.isEmpty() ? "" : excelSpec, dbSpec.isEmpty() ? "" : dbSpec));
}
String excelSku = StringUtils.trimToEmpty(r.getSkuCode());
String dbSku = StringUtils.trimToEmpty(materialBaseInfoPO.getBarCode());
if (!excelSku.equals(dbSku)) {
throw new ServiceException(String.format("导入失败:料号[%s](商品名称:%s)SKU码与物料档案不一致(Excel:%s / 档案:%s",
r.getMaterialCode(), r.getMaterialName(),
excelSku.isEmpty() ? "" : excelSku, dbSku.isEmpty() ? "" : dbSku));
}
ReservationInMaterialDetailDO d = new ReservationInMaterialDetailDO();
d.setMaterialBaseInfoId(materialBaseInfoPO.getMaterialBaseInfoId());
// 从物料档案获取名称与WMS入库导入一致
d.setMaterialName(materialBaseInfoPO.getMaterialName());
d.setMaterialCode(r.getMaterialCode());
d.setQuantity(quantity);
d.setSpecificationModel(r.getSpecificationModel());
d.setSkuCode(r.getSkuCode());
d.setInvoiceNo(r.getInvoiceNo());
d.setDeclaredQuantity(r.getDeclaredQuantity());
d.setDeclarationUnit(r.getDeclarationUnit());
d.setLiter(r.getLiter());
d.setBoxCount(r.getBoxCount());
d.setPieceCount(r.getPieceCount());
d.setDimensions(r.getDimensions());
// 入库数量优先使用入库数量如果为空则使用仓库数量
BigDecimal inboundQuantity = r.getInboundQuantity();
if (inboundQuantity == null) {
inboundQuantity = r.getWarehouseQuantity();
}
d.setInboundQuantity(inboundQuantity);
// 单位优先使用Excel中的"单位"字段如果为空则使用"申报单位"如果还为空则从物料主数据获取
String unit = StringUtils.isNotBlank(r.getUnit()) ? r.getUnit() : null;
if (StringUtils.isBlank(unit) && StringUtils.isNotBlank(r.getDeclarationUnit())) {
unit = r.getDeclarationUnit();
}
if (StringUtils.isBlank(unit) && StringUtils.isNotBlank(materialBaseInfoPO.getUnitName())) {
unit = materialBaseInfoPO.getUnitName();
}
d.setUnit(unit);
// 总净重/总毛重/总体积/总面积优先使用主字段如果为空则使用备用字段
d.setTotalNetWeight(r.getTotalNetWeight() != null ? r.getTotalNetWeight() : r.getTotalNetWeight2());
d.setTotalGrossWeight(r.getTotalGrossWeight() != null ? r.getTotalGrossWeight() : r.getTotalGrossWeight2());
d.setTotalVolume(r.getTotalVolume() != null ? r.getTotalVolume() : r.getTotalVolume2());
d.setTotalArea(r.getTotalArea() != null ? r.getTotalArea() : r.getTotalArea2());
d.setBatchRefNo(r.getBatchRefNo());
d.setSheetRefNo(r.getSheetRefNo());
// 原产国优先使用Excel中的数据如果为空则从物料主数据获取
String originCountry = StringUtils.isNotBlank(r.getOriginCountry()) ? r.getOriginCountry() : null;
if (StringUtils.isBlank(originCountry) && StringUtils.isNotBlank(materialBaseInfoPO.getOriginCountry())) {
originCountry = materialBaseInfoPO.getOriginCountry();
}
d.setOriginCountry(originCountry);
d.setBoxPalletNo(r.getBoxPalletNo());
d.setTotalPrice(r.getTotalPrice());
// 币制优先使用Excel中的数据如果为空则从物料主数据获取
String currency = StringUtils.isNotBlank(r.getCurrency()) ? r.getCurrency() : null;
if (StringUtils.isBlank(currency) && StringUtils.isNotBlank(materialBaseInfoPO.getCurrency())) {
currency = materialBaseInfoPO.getCurrency();
}
d.setCurrency(currency);
d.setRemark(r.getDetailRemark());
details.add(d);
}
// 如果该订单的所有明细行都被跳过details为空跳过该订单
if (details.isEmpty()) {
log.warn("跳过入库单:委托编号NO [{}] 的所有明细行都因数据不完整被跳过,该入库单将不会被导入", h.getConsignmentNo());
return null;
}
order.setMaterialDetailList(details);
// 导入入库单时计划数量取所有明细行的入库数量之和
BigDecimal totalInboundQuantity = details.stream()
.map(detail -> detail.getInboundQuantity() != null ? detail.getInboundQuantity() : BigDecimal.ZERO)
.reduce(BigDecimal.ZERO, BigDecimal::add);
order.setQuantity(totalInboundQuantity);
// 导入时补全货主/供应商/数据字典信息与手动新增链路一致
try {
shipperParam(order);
} catch (Exception e) {
log.error("导入入库单时设置货主信息失败,委托编号NO:{},货主名称:{},错误:{}",
h.getConsignmentNo(), order.getShipperName(), e.getMessage());
throw new ServiceException("导入失败:设置货主信息失败(委托编号NO=" + h.getConsignmentNo()
+ ",货主名称=" + order.getShipperName() + ")。" + e.getMessage());
}
supplierParam(order);
setDataDict(order);
return order;
}
/**
* 导入用按料号跨库查询物料基础信息查询结果需在Java侧核对货主归属
*/
private MaterialBaseInfoPO safeGetMaterialByCode(String materialCode, LoginUser loginUser) {
try {
Long orgId = loginUser.getUserPo() != null ? loginUser.getUserPo().getOrganizationId() : null;
return reservationStockInOrderMapper.getByCode(materialCode, orgId);
} catch (Exception e) {
log.warn("按料号查询物料基础信息失败,料号: [{}], 错误: {}", materialCode, e.getMessage());
return null;
}
}
/**
* 将表头信息应用到每一行明细明细行值为空时使用表头值客户名称/货主编号表头有值时强制应用
*/
private void applyHeaderToRows(ReservationStockInOrderImportRowDTO headerInfo, List<ReservationStockInOrderImportRowDTO> rows) {
for (ReservationStockInOrderImportRowDTO row : rows) {
if (row == null) {
continue;
}
// 应用表头中的委托编号NO
if (StringUtils.isBlank(row.getConsignmentNo()) && StringUtils.isNotBlank(headerInfo.getConsignmentNo())) {
row.setConsignmentNo(headerInfo.getConsignmentNo());
}
// 表头有客户名称/货主编号时强制应用到所有明细行
if (StringUtils.isNotBlank(headerInfo.getShipperName())) {
row.setShipperName(headerInfo.getShipperName());
}
if (StringUtils.isNotBlank(headerInfo.getShipperCode())) {
row.setShipperCode(headerInfo.getShipperCode());
}
// 应用其他表头字段如果明细行为空则使用表头值
row.setTo(firstNotBlank(row.getTo(), headerInfo.getTo()));
row.setContactPerson(firstNotBlank(row.getContactPerson(), headerInfo.getContactPerson()));
row.setVehicleInfo(firstNotBlank(row.getVehicleInfo(), headerInfo.getVehicleInfo()));
row.setCarrier(firstNotBlank(row.getCarrier(), headerInfo.getCarrier()));
row.setTel(firstNotBlank(row.getTel(), headerInfo.getTel()));
row.setDriverSignature(firstNotBlank(row.getDriverSignature(), headerInfo.getDriverSignature()));
row.setFax(firstNotBlank(row.getFax(), headerInfo.getFax()));
row.setDeclarationArea(firstNotBlank(row.getDeclarationArea(), headerInfo.getDeclarationArea()));
row.setManufacturer(firstNotBlank(row.getManufacturer(), headerInfo.getManufacturer()));
row.setCustomsBrokerInfo(firstNotBlank(row.getCustomsBrokerInfo(), headerInfo.getCustomsBrokerInfo()));
row.setDeliveryAddress(firstNotBlank(row.getDeliveryAddress(), headerInfo.getDeliveryAddress()));
row.setEntryExitCustoms(firstNotBlank(row.getEntryExitCustoms(), headerInfo.getEntryExitCustoms()));
row.setOriginDestinationCountry(firstNotBlank(row.getOriginDestinationCountry(), headerInfo.getOriginDestinationCountry()));
row.setContainerNo(firstNotBlank(row.getContainerNo(), headerInfo.getContainerNo()));
row.setTransportMethod(firstNotBlank(row.getTransportMethod(), headerInfo.getTransportMethod()));
row.setSupervisionMethod(firstNotBlank(row.getSupervisionMethod(), headerInfo.getSupervisionMethod()));
if (row.getCustomsInspection() == null) {
row.setCustomsInspection(headerInfo.getCustomsInspection());
}
if (row.getNeedCustomsDeclaration() == null) {
row.setNeedCustomsDeclaration(headerInfo.getNeedCustomsDeclaration());
}
if (row.getNeedTransport() == null) {
row.setNeedTransport(headerInfo.getNeedTransport());
}
row.setWarehouseDate(firstNotBlank(row.getWarehouseDate(), headerInfo.getWarehouseDate()));
row.setCompletionTime(firstNotBlank(row.getCompletionTime(), headerInfo.getCompletionTime()));
row.setOrderDate(firstNotBlank(row.getOrderDate(), headerInfo.getOrderDate()));
row.setRemark(firstNotBlank(row.getRemark(), headerInfo.getRemark()));
}
}
/**
* 扫描表头区域第1-12行按标签提取字段值
* 值优先取标签同行右侧最近的合法单元格其次取下一行同列最后取标签文本冒号后的内容
* 看起来仍是标签文本含冒号或命中标签关键词的值会被跳过
*/
private void fillHeaderInfoByLabels(Sheet sheet, ReservationStockInOrderImportRowDTO headerInfo) {
for (int rowIdx = 0; rowIdx < 12; rowIdx++) {
Row row = sheet.getRow(rowIdx);
if (row == null) {
continue;
}
for (int colIdx = 0; colIdx < row.getPhysicalNumberOfCells(); colIdx++) {
Cell labelCell = row.getCell(colIdx);
if (labelCell == null) {
continue;
}
String labelValue = getCellValueAsString(labelCell);
if (StringUtils.isBlank(labelValue)) {
continue;
}
// 标签文本冒号后的内容兜底值
String extractedFromSameCell = null;
if (labelValue.contains(":")) {
String[] parts = labelValue.split(":", 2);
if (parts.length > 1) {
extractedFromSameCell = parts[1].trim();
}
}
String value = findValueRightwards(row, colIdx);
if (StringUtils.isBlank(value)) {
value = findValueBelow(sheet, rowIdx, colIdx);
}
if (StringUtils.isBlank(value)) {
value = extractedFromSameCell;
}
if (StringUtils.isBlank(value)) {
continue;
}
dispatchHeaderField(labelValue, value, headerInfo);
}
}
}
/**
* 在同行右侧最多10列查找第一个合法值
*/
private String findValueRightwards(Row row, int colIdx) {
for (int searchColIdx = colIdx + 1; searchColIdx < colIdx + 10 && searchColIdx < row.getPhysicalNumberOfCells(); searchColIdx++) {
Cell valueCell = row.getCell(searchColIdx);
if (valueCell == null) {
continue;
}
String cellValue = getCellValueAsString(valueCell);
if (StringUtils.isNotBlank(cellValue)) {
return cellValue;
}
}
return null;
}
/**
* 在下一行同列查找合法值模板变体值写在标签下一行
*/
private String findValueBelow(Sheet sheet, int rowIdx, int colIdx) {
Row nextRow = sheet.getRow(rowIdx + 1);
if (nextRow == null) {
return null;
}
Cell nextRowCell = nextRow.getCell(colIdx);
if (nextRowCell == null) {
return null;
}
String nextRowValue = getCellValueAsString(nextRowCell);
return StringUtils.isNotBlank(nextRowValue) ? nextRowValue : null;
}
/**
* 按标签关键词分发到表头字段
*/
private void dispatchHeaderField(String labelValue, String value, ReservationStockInOrderImportRowDTO headerInfo) {
// 备注值内容任意不做标签过滤
if (labelValue.contains("备注")) {
if (StringUtils.isBlank(headerInfo.getRemark())) {
headerInfo.setRemark(value);
}
return;
}
// 委托编号NO支持"委托编号NO""委托编号"等格式排除标签本身
if (labelValue.contains("委托编号")) {
if (StringUtils.isBlank(headerInfo.getConsignmentNo())
&& !value.contains("委托编号") && !value.contains("NO")
&& !"委托编号NO".equals(value) && !"委托编号".equals(value)) {
headerInfo.setConsignmentNo(value);
}
return;
}
// 客户名称
if (labelValue.contains("客户名称")) {
if (StringUtils.isBlank(headerInfo.getShipperName())
&& !value.contains("客户") && !"客户名称".equals(value)) {
headerInfo.setShipperName(value);
}
return;
}
// To需要精确匹配避免误匹配
if (isToLabel(labelValue)) {
if (StringUtils.isBlank(headerInfo.getTo()) && !isToLabel(value) && !looksLikeLabel(value)) {
headerInfo.setTo(value);
}
return;
}
// 值是标签文本含冒号或命中标签关键词时跳过
if (value.contains("") || value.contains(":") || looksLikeLabel(value)) {
return;
}
if (labelValue.contains("联系人") && StringUtils.isBlank(headerInfo.getContactPerson())) {
headerInfo.setContactPerson(value);
} else if ((labelValue.contains("车型") || labelValue.contains("车牌")) && StringUtils.isBlank(headerInfo.getVehicleInfo())) {
headerInfo.setVehicleInfo(value);
} else if ((labelValue.contains("承运方") || labelValue.contains("承运商")) && StringUtils.isBlank(headerInfo.getCarrier())) {
headerInfo.setCarrier(value);
} else if (labelValue.contains("Tel") && StringUtils.isBlank(headerInfo.getTel())) {
headerInfo.setTel(value);
} else if (labelValue.contains("司机签名") && StringUtils.isBlank(headerInfo.getDriverSignature())) {
headerInfo.setDriverSignature(value);
} else if (labelValue.contains("入仓日期") && StringUtils.isBlank(headerInfo.getWarehouseDate())) {
headerInfo.setWarehouseDate(value);
} else if (labelValue.contains("Fax") && StringUtils.isBlank(headerInfo.getFax())) {
headerInfo.setFax(value);
} else if (labelValue.contains("申报地关区") && StringUtils.isBlank(headerInfo.getDeclarationArea())) {
headerInfo.setDeclarationArea(value);
} else if (labelValue.contains("生产商") && StringUtils.isBlank(headerInfo.getManufacturer())) {
headerInfo.setManufacturer(value);
} else if (labelValue.contains("报关员") && StringUtils.isBlank(headerInfo.getCustomsBrokerInfo())) {
headerInfo.setCustomsBrokerInfo(value);
} else if (labelValue.contains("送货地址") && StringUtils.isBlank(headerInfo.getDeliveryAddress())) {
headerInfo.setDeliveryAddress(value);
} else if (labelValue.contains("进出境关别") && StringUtils.isBlank(headerInfo.getEntryExitCustoms())) {
headerInfo.setEntryExitCustoms(value);
} else if ((labelValue.contains("起运") || labelValue.contains("运抵国")) && StringUtils.isBlank(headerInfo.getOriginDestinationCountry())) {
headerInfo.setOriginDestinationCountry(value);
} else if (labelValue.contains("柜号") && StringUtils.isBlank(headerInfo.getContainerNo())) {
headerInfo.setContainerNo(value);
} else if (labelValue.contains("运输方式") && StringUtils.isBlank(headerInfo.getTransportMethod())) {
headerInfo.setTransportMethod(value);
} else if (labelValue.contains("监管方式") && StringUtils.isBlank(headerInfo.getSupervisionMethod())) {
headerInfo.setSupervisionMethod(value);
} else if (labelValue.contains("海关是否查验货物") && headerInfo.getCustomsInspection() == null) {
headerInfo.setCustomsInspection(parseYesNo(value));
} else if (labelValue.contains("是否需要报关") && headerInfo.getNeedCustomsDeclaration() == null) {
headerInfo.setNeedCustomsDeclaration(parseYesNo(value));
} else if (labelValue.contains("是否需要运输") && headerInfo.getNeedTransport() == null) {
headerInfo.setNeedTransport(parseYesNo(value));
} else if (labelValue.contains("完成时间") && StringUtils.isBlank(headerInfo.getCompletionTime())) {
headerInfo.setCompletionTime(value);
} else if (labelValue.contains("下单日期") && StringUtils.isBlank(headerInfo.getOrderDate())) {
headerInfo.setOrderDate(value);
}
}
/**
* 是否为To标签精确匹配避免把Value/Total等英文词误认为To
*/
private boolean isToLabel(String text) {
if (text == null) {
return false;
}
String t = text.trim();
return "To".equals(t) || "To:".equals(t) || "To".equals(t) || t.startsWith("To:") || t.startsWith("To");
}
/**
* 值是否看起来仍是标签文本命中常见标签关键词
*/
private boolean looksLikeLabel(String value) {
if (value == null) {
return true;
}
String v = value.trim();
return v.contains("委托编号") || v.contains("司机签名") || v.contains("完成时间") || v.contains("下单日期")
|| v.contains("入仓日期") || v.contains("联系人") || v.contains("Tel") || v.contains("Fax")
|| v.contains("客户") || v.contains("地址") || v.contains("日期") || v.contains("方式")
|| v.contains("关区") || v.contains("关别") || v.contains("报关员") || v.contains("生产商")
|| v.contains("承运方") || v.contains("承运商") || v.contains("起运") || v.contains("运抵国")
|| v.contains("地区") || v.contains("车型") || v.contains("车牌") || v.contains("柜号")
|| v.contains("海关是否查验货物") || v.contains("是否需要报关") || v.contains("是否需要运输");
}
/**
* /或1/0转Integer无法识别返回null
*/
private Integer parseYesNo(String value) {
if (value == null) {
return null;
}
String v = value.trim();
if (v.contains("") || "1".equals(v)) {
return 1;
}
if (v.contains("") || "0".equals(v)) {
return 0;
}
return null;
}
/**
* 取第一个非空字符串
*/
private String firstNotBlank(String a, String b) {
return StringUtils.isNotBlank(a) ? a : b;
}
/**
* 获取登录人姓名优先UserPo.userName其次realname最后username兜底
*/
private String resolveUserRealName(LoginUser loginUser) {
if (loginUser.getUserPo() != null && StringUtils.isNotBlank(loginUser.getUserPo().getUserName())) {
return loginUser.getUserPo().getUserName();
}
if (StringUtils.isNotBlank(loginUser.getRealname())) {
return loginUser.getRealname();
}
return loginUser.getUsername();
}
/**
* 解析日期字符串为Date对象
* 支持多种日期格式yyyy-MM-dd, yyyy/MM/dd, yyyy年MM月dd日, Date.toString()格式等
@@ -13,6 +13,9 @@ import com.mhd.common.core.web.domain.AjaxResult;
import com.mhd.common.security.utils.SecurityUtils;
import com.mhd.oms.domain.businessDocumentOrder.entity.BusinessDocumentOrder;
import com.mhd.oms.domain.businessDocumentOrder.repository.mapper.BusinessDocumentOrderMapper;
import com.mhd.oms.domain.electronicSignature.entity.ElectronicSignature;
import com.mhd.oms.domain.electronicSignature.repository.facade.IElectronicSignatureMaterialDetailService;
import com.mhd.oms.domain.electronicSignature.repository.facade.IElectronicSignatureService;
import com.mhd.oms.domain.executionInMaterialDetail.entity.ExecutionInMaterialDetail;
import com.mhd.oms.domain.executionInMaterialDetail.repository.facade.IExecutionInMaterialDetailService;
import com.mhd.oms.domain.executionStockInOrder.entity.ExecutionStockInOrder;
@@ -33,6 +36,7 @@ import com.mhd.oms.domain.reservationStockInOrder.repository.po.MaterialBaseInfo
import com.mhd.oms.domain.reservationStockInOrder.repository.po.MaterialGoodsRulePO;
import com.mhd.oms.domain.reservationStockInOrder.repository.po.MaterialWarehouseControlPO;
import com.mhd.oms.domain.reservationStockInOrder.repository.po.ReservationStockInOrderPO;
import com.mhd.oms.domain.reservationStockInOrder.repository.po.ReservationStockInOrderTotalPO;
import com.mhd.oms.domain.reservationStockInOrder.repository.todo.ReservationStockInOrderDO;
import com.mhd.oms.domain.reservationStockOutOrder.entity.ReservationStockOutOrder;
import com.mhd.oms.domain.reservationStockOutOrder.repository.facade.IReservationStockOutOrderService;
@@ -91,6 +95,10 @@ public class ReservationStockInOrderDomainService {
private ExecutionStockInOrderMapper executionStockInOrderMapper;
@Autowired
private ReserveStockInOrderMapper reserveStockInOrderMapper;
@Autowired
private IElectronicSignatureService electronicSignatureService;
@Autowired
private IElectronicSignatureMaterialDetailService electronicSignatureMaterialDetailService;
/**
* 分页查询入库单列表
@@ -99,6 +107,13 @@ public class ReservationStockInOrderDomainService {
return stockInOrderService.queryList(stockInOrderDO);
}
/**
* 列表合计按查询条件全量(不分页)汇总计划数量与入库数量
*/
public ReservationStockInOrderTotalPO queryListTotal(ReservationStockInOrderDO stockInOrderDO) {
return stockInOrderService.queryListTotal(stockInOrderDO);
}
/**
* 生成入库预约单号
*/
@@ -169,6 +184,10 @@ public class ReservationStockInOrderDomainService {
// //统计总体积
// BigDecimal volumeLimit = stockInOrderDO.getMaterialDetailList().stream().map(ReservationInMaterialDetailDO::getVolumeLimit).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
// stockInOrderDO.setVolumeLimit(volumeLimit);
// 订单来源为空时兜底为手动创建导入/预约下发等路径已显式设置不受影响下发执行单时随业务单继承
if (stockInOrderDO.getReservationOrderSource() == null || stockInOrderDO.getReservationOrderSource().isEmpty()) {
stockInOrderDO.setReservationOrderSource("手动创建");
}
stockInOrderService.insert(stockInOrderDO);
return materialDetailService.batchInsert(stockInOrderDO.getInOrderNumber(), stockInOrderDO.getMaterialDetailList());
}
@@ -607,7 +626,6 @@ public class ReservationStockInOrderDomainService {
reservationStockInOrder.setOrderTypeName("普通入库");
reservationStockInOrder.setBusinessType("pu_tong_ru_ku");
}
reservationStockInOrder.setReservationOrderSource("客户预约");
reservationStockInOrder.setWarehouseId(warehouseId);
if (warehouseId == null || warehouseId == 0L){
reservationStockInOrder.setWarehouseId(byId.getWarehouseId());
@@ -767,6 +785,171 @@ public class ReservationStockInOrderDomainService {
}
}
/**
* 反审入库业务单需求已入库/已出库的单子客户通知需要回退的场景单子回退到最原始状态
* 页面状态列按 issueStatus0-已创建 1-进行中 2-已审核 3-已驳回 4-已入库 5-部分完成展示
* 反审条件按 issueStatus 判断进行中(1)/已审核(2)/已入库(4)/部分完成(5)可反审
* 已创建(0)无需反审已驳回(3)可直接编辑已取消/已关闭(status 6/7)兜底拦截
* 反审动作"审核不通过"同语义 + 全链路回退补齐 auditState 审核不通过缺失的下游联动
* <ul>
* <li>业务单状态改为"已驳回"(issueStatus=3)打已反审标记(REVERSE_AUDIT_STATUS=1)落入已反审tab
* 之后可编辑重新审核重新下发生成全新执行单</li>
* <li>入库执行单+物料明细软删(del_flag=2)并打已反审标记执行单已反审tab按
* "已软删+已反审"查询归档数据软删后不出现在正常tab避免归档单被误审核/下发</li>
* <li>清空WMS完成上架跨库回写的完成数据业务单 IN_QUANTITY/STORAGE_TIME明细 INBOUND_ITEMS/TOTAL_NET_WEIGHT</li>
* <li>WMS侧逐执行单回退omsReverseInOrderW入库单有出库记录则拦截按入库单号+动作"分配库存/出库"
* 查库存调整记录&gt;0 库存还原扣减 W入库单/W收货单/W上架单整链软删并打已反审标记
* 数据划归W入库单/W收货单/W上架单已反审tab归档
* 写1条W入库异常反审记录用于记录跟踪</li>
* </ul>
* 已推送关务(pushStatus=3)直接拦截WMS回退幂等反审失败可安全重试
*/
@Transactional(rollbackFor = Exception.class)
public Boolean reverseAudit(ReservationStockInOrderDO stockInOrderDO) {
LoginUser loginUser = SecurityUtils.getLoginUser();
// 获取用户姓名优先从UserPo获取如果为空则使用realname最后使用username作为兜底
String userRealName = loginUser.getUserPo() != null && loginUser.getUserPo().getUserName() != null
? loginUser.getUserPo().getUserName()
: (loginUser.getRealname() != null ? loginUser.getRealname() : loginUser.getUsername());
List<Long> inOrderIds = stockInOrderDO.getInOrderIds();
if (inOrderIds == null || inOrderIds.isEmpty()) {
throw new ServiceException("未选择反审的入库业务单");
}
Date now = new Date();
for (Long inOrderId : inOrderIds) {
ReservationStockInOrder businessOrder = stockInOrderService.getById(inOrderId);
if (businessOrder == null
|| (businessOrder.getDelFlag() != null && businessOrder.getDelFlag() != 1)) {
throw new ServiceException("入库业务单不存在或已被删除,无法反审");
}
String businessNumber = businessOrder.getInOrderNumber();
Integer issueStatus = businessOrder.getIssueStatus();
if (issueStatus == null || issueStatus == 0) {
throw new ServiceException("入库业务单[" + businessNumber + "]为已创建状态,无需反审");
}
if (issueStatus == 3) {
throw new ServiceException("入库业务单[" + businessNumber + "]已驳回,无需反审,可直接编辑后重新提交审核");
}
if (issueStatus != 1 && issueStatus != 2 && issueStatus != 4 && issueStatus != 5) {
throw new ServiceException("入库业务单[" + businessNumber + "]仅进行中/已审核/已入库状态可反审,当前状态值:" + issueStatus);
}
if (businessOrder.getStatus() != null && (businessOrder.getStatus() == 6 || businessOrder.getStatus() == 7)) {
throw new ServiceException("入库业务单[" + businessNumber + "]已取消/已关闭,无法反审");
}
if (businessOrder.getPushStatus() != null && businessOrder.getPushStatus() == 3) {
throw new ServiceException("入库业务单[" + businessNumber + "]已推送关务/NC,无法反审,请先在关务侧处理");
}
//该业务单下全部有效执行单逐张调WMS回退后软删并打已反审标记
List<ExecutionStockInOrder> executionOrders = executionStockInOrderService.list(
new LambdaQueryWrapper<ExecutionStockInOrder>()
.eq(ExecutionStockInOrder::getBusinessInOrderNumber, businessNumber)
.eq(ExecutionStockInOrder::getDelFlag, 1));
for (ExecutionStockInOrder executionOrder : executionOrders) {
String executionNumber = executionOrder.getInOrderNumber();
if (executionOrder.getPushStatus() != null && executionOrder.getPushStatus() == 3) {
throw new ServiceException("执行入库单[" + executionNumber + "]已推送关务/NC,无法反审,请先在关务侧处理");
}
//WMS侧回退对称收货/上架完成链路 WMS StockInOrderDomainService.reverseExecutionOrder
//有出库/库存已被分配时WMS报错此处抛错整体回滚WMS无数据时幂等成功未下发执行单反审
StockInOrder reverseParam = new StockInOrder();
reverseParam.setInOrderNumber(executionNumber);
reverseParam.setRemark("业务单号:" + businessNumber
+ (StringUtils.isNotEmpty(stockInOrderDO.getReverseAuditRemark())
? ",反审原因:" + stockInOrderDO.getReverseAuditRemark() : ""));
//组织三字段WMS幂等跳过分支未下发单反审写反审记录时取不到W入库单需经Feign透传
reverseParam.setOrganizationId(executionOrder.getOrganizationId());
reverseParam.setOrganizationName(executionOrder.getOrganizationName());
reverseParam.setTopOrganizationId(executionOrder.getTopOrganizationId());
reverseParam.setUpdateBy(loginUser.getUserid());
reverseParam.setUpdateByName(userRealName);
AjaxResult reverseResult = wmsServiceFeign.omsReverseInOrder(reverseParam);
if (reverseResult == null || !"200".equals(String.valueOf(reverseResult.get("code")))) {
throw new ServiceException("反审失败:执行入库单[" + executionNumber + "]WMS回退失败:"
+ (reverseResult == null ? "WMS服务不可用或已降级" : reverseResult.get("msg")));
}
//清理下发自动审核生成的待签名电子签名记录signatureStatus=1已签名记录保留作凭证
cancelPendingSignature(executionNumber);
//执行单+物料明细软删(del_flag=2)并打已反审标记
//执行单已反审tab按"已软删+已反审"(del_flag=2 and REVERSE_AUDIT_STATUS=1)查询归档数据
//软删后不出现在正常tab避免归档单被误审核/下发业务单重新下发时生成全新执行单
executionInMaterialDetailService.update(new UpdateWrapper<ExecutionInMaterialDetail>().lambda()
.set(ExecutionInMaterialDetail::getDelFlag, 2)
.set(ExecutionInMaterialDetail::getUpdateBy, loginUser.getUserid())
.set(ExecutionInMaterialDetail::getUpdateByName, userRealName)
.set(ExecutionInMaterialDetail::getUpdateTime, now)
.eq(ExecutionInMaterialDetail::getInOrderNumber, executionNumber)
.eq(ExecutionInMaterialDetail::getDelFlag, 1));
executionStockInOrderService.update(new UpdateWrapper<ExecutionStockInOrder>().lambda()
.set(ExecutionStockInOrder::getDelFlag, 2)
.set(ExecutionStockInOrder::getReverseAuditStatus, 1)
.set(ExecutionStockInOrder::getReverseAuditRemark, stockInOrderDO.getReverseAuditRemark())
.set(ExecutionStockInOrder::getReverseAuditBy, loginUser.getUserid())
.set(ExecutionStockInOrder::getReverseAuditByName, userRealName)
.set(ExecutionStockInOrder::getReverseAuditTime, now)
.set(ExecutionStockInOrder::getUpdateBy, loginUser.getUserid())
.set(ExecutionStockInOrder::getUpdateByName, userRealName)
.set(ExecutionStockInOrder::getUpdateTime, now)
.eq(ExecutionStockInOrder::getInOrderId, executionOrder.getInOrderId())
.eq(ExecutionStockInOrder::getDelFlag, 1));
}
//清空WMS完成上架跨库回写的业务单明细入库数量INBOUND_ITEMS/TOTAL_NET_WEIGHT 上架时按上架量覆写过
materialDetailService.update(new UpdateWrapper<ReservationInMaterialDetail>().lambda()
.set(ReservationInMaterialDetail::getInboundQuantity, BigDecimal.ZERO)
.set(ReservationInMaterialDetail::getTotalNetWeight, BigDecimal.ZERO)
.eq(ReservationInMaterialDetail::getInOrderNumber, businessNumber)
.eq(ReservationInMaterialDetail::getDelFlag, 1));
}
//业务单状态改为"已驳回"与审核不通过同语义+ 已反审标记落入已反审tab + 清空完成回写数据
return stockInOrderService.update(null, new UpdateWrapper<ReservationStockInOrder>().lambda()
.set(ReservationStockInOrder::getIssueStatus, 3)
//清空下发/执行时间前端"是否执行"列按执行时间判断不清会仍显示已执行与取消下发 cancelIssueOrder 同口径
.set(ReservationStockInOrder::getIssueTime, null)
//入库进度状态(1-已创建 2-已审核 3-收货中 4-上架中 5-已入库)首轮"完成上架"会经WMS回传写成5
//不重置的话重新下发时copy给执行单W入库单收货单收货单带着"已完成"状态导致前端不给收货按钮
//重置为1(已创建)与手工新单首次下发链路一致(W入库单/收货单起点=1-待收货)
.set(ReservationStockInOrder::getStatus, 1)
.set(ReservationStockInOrder::getInQuantity, BigDecimal.ZERO)
.set(ReservationStockInOrder::getStorageTime, null)
.set(ReservationStockInOrder::getExecutionInOrderNumber, null)
//反审标记与反审痕迹已反审tab reverseAuditStatus=1 过滤业务单本身不软删保持可编辑重新下发
.set(ReservationStockInOrder::getReverseAuditStatus, 1)
.set(ReservationStockInOrder::getReverseAuditRemark, stockInOrderDO.getReverseAuditRemark())
.set(ReservationStockInOrder::getReverseAuditBy, loginUser.getUserid())
.set(ReservationStockInOrder::getReverseAuditByName, userRealName)
.set(ReservationStockInOrder::getReverseAuditTime, now)
.set(ReservationStockInOrder::getUpdateBy, loginUser.getUserid())
.set(ReservationStockInOrder::getUpdateByName, userRealName)
.set(ReservationStockInOrder::getUpdateTime, now)
.in(ReservationStockInOrder::getInOrderId, inOrderIds));
}
/**
* 反审时清理下发自动审核生成的待签名电子签名记录signatureStatus=1及其明细
* 已签名记录保留作凭证清理失败仅记日志不阻断反审主流程同取消下发
*/
private void cancelPendingSignature(String inOrderNumber) {
try {
List<ElectronicSignature> signatures = electronicSignatureService.list(
new LambdaQueryWrapper<ElectronicSignature>()
.eq(ElectronicSignature::getOrderNumber, inOrderNumber)
.eq(ElectronicSignature::getOrderType, "IN")
.eq(ElectronicSignature::getSignatureStatus, 1)
.eq(ElectronicSignature::getDelFlag, 1));
for (ElectronicSignature signature : signatures) {
electronicSignatureMaterialDetailService.deleteBySignatureId(signature.getSignatureId());
electronicSignatureService.update(new UpdateWrapper<ElectronicSignature>().lambda()
.set(ElectronicSignature::getDelFlag, 2)
.eq(ElectronicSignature::getSignatureId, signature.getSignatureId()));
}
} catch (Exception e) {
log.error("反审清理电子签名记录失败,入库单号:{}", inOrderNumber, e);
}
}
@Transactional(rollbackFor = Exception.class)
public Boolean executeAndIssue(ReservationStockInOrderDO stockInOrderDO) {
// 防重复执行校验 issueOrder 的防重逻辑对齐issueTime 有值表示已下发/执行过
@@ -870,7 +1053,6 @@ public class ReservationStockInOrderDomainService {
reservationStockInOrder.setOrderTypeName("普通入库");
reservationStockInOrder.setBusinessType("pu_tong_ru_ku");
}
reservationStockInOrder.setReservationOrderSource("客户预约");
reservationStockInOrder.setWarehouseId(warehouseId);
// reservationStockInOrder.setOrganizationId(orgId);
// reservationStockInOrder.setOrganizationName(orgName);
@@ -515,4 +515,21 @@ public class ReservationStockOutOrder extends BaseVOEntity {
@ApiModelProperty("折扣")
private BigDecimal discount;
@ApiModelProperty("反审状态: 0-无 1-已反审(反审后status回退为1-已创建,重新审核通过自动复位0)")
private Integer reverseStatus;
@ApiModelProperty("反审人id")
private Long reverseBy;
@ApiModelProperty("反审人姓名")
private String reverseByName;
@ApiModelProperty("反审时间")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date reverseTime;
@ApiModelProperty("反审原因")
private String reverseRemark;
}
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
import com.mhd.oms.domain.reservationOutMaterialDetail.repository.po.ReservationOutMaterialDetailPO;
import com.mhd.oms.domain.reservationStockOutOrder.entity.ReservationStockOutOrder;
import com.mhd.oms.domain.reservationStockOutOrder.repository.po.ReservationStockOutOrderPO;
import com.mhd.oms.domain.reservationStockOutOrder.repository.po.ReservationStockOutOrderTotalPO;
import com.mhd.oms.domain.reservationStockOutOrder.repository.todo.ReservationStockOutOrderDO;
@@ -22,6 +23,11 @@ public interface IReservationStockOutOrderService extends IService<ReservationSt
*/
public List<ReservationStockOutOrderPO> queryList(ReservationStockOutOrderDO stockOutOrderDO);
/**
* 列表合计按查询条件全量(不分页)汇总一级明细的计划数量与出库数量
*/
public ReservationStockOutOrderTotalPO queryListTotal(ReservationStockOutOrderDO stockOutOrderDO);
/**
* 新增出库单
*/
@@ -6,6 +6,7 @@ import com.mhd.common.core.domain.po.UserPo;
import com.mhd.oms.domain.reservationOutMaterialDetail.repository.po.ReservationOutMaterialDetailPO;
import com.mhd.oms.domain.reservationStockOutOrder.entity.ReservationStockOutOrder;
import com.mhd.oms.domain.reservationStockOutOrder.repository.po.ReservationStockOutOrderPO;
import com.mhd.oms.domain.reservationStockOutOrder.repository.po.ReservationStockOutOrderTotalPO;
import com.mhd.oms.domain.reservationStockOutOrder.repository.todo.ReservationStockOutOrderDO;
import org.apache.ibatis.annotations.Param;
//import com.mhd.wms.domain.stockOutOrder.entity.ReservationStockOutOrder;
@@ -27,6 +28,11 @@ public interface ReservationStockOutOrderMapper extends BaseMapper<ReservationSt
*/
public List<ReservationStockOutOrderPO> queryList(ReservationStockOutOrderDO stockOutOrderDO);
/**
* 列表合计按查询条件全量(不分页)汇总一级明细的计划数量与出库数量
*/
public ReservationStockOutOrderTotalPO queryListTotal(ReservationStockOutOrderDO stockOutOrderDO);
/**
* 出库单提货单打印-表头
*/

Some files were not shown because too many files have changed in this diff Show More