Browse Source

新分拣看板

dev
常熟吴彦祖 3 days ago
parent
commit
6d4d7ffa81
  1. 312
      src/main/java/com/gaotao/modules/autoWareHourceHaian/HAIAN_AUTO_ISSUE_DOCKING_AND_PC.md
  2. 27
      src/main/java/com/gaotao/modules/autoWareHourceHaian/controller/HaiAnSortingBoardController.java
  3. 22
      src/main/java/com/gaotao/modules/autoWareHourceHaian/entity/dashboard/HaiAnSortingBoardItem.java
  4. 12
      src/main/java/com/gaotao/modules/autoWareHourceHaian/entity/dashboard/HaiAnSortingBoardQuery.java
  5. 22
      src/main/java/com/gaotao/modules/autoWareHourceHaian/entity/dashboard/HaiAnSortingBoardResult.java
  6. 14
      src/main/java/com/gaotao/modules/autoWareHourceHaian/mapper/HaiAnSortingBoardMapper.java
  7. 10
      src/main/java/com/gaotao/modules/autoWareHourceHaian/service/HaiAnSortingBoardService.java
  8. 167
      src/main/java/com/gaotao/modules/autoWareHourceHaian/service/impl/HaiAnSortingBoardServiceImpl.java
  9. 4
      src/main/java/com/gaotao/modules/autoWareHourceHaian/staticData/HaiAnStationStaticData.java
  10. 67
      src/main/resources/mapper/autoWareHourceHaian/HaiAnSortingBoardMapper.xml

312
src/main/java/com/gaotao/modules/autoWareHourceHaian/HAIAN_AUTO_ISSUE_DOCKING_AND_PC.md

@ -0,0 +1,312 @@
# 海安自动发料对接同事 `AutoIssueService.autoIssue` + PC 查看页草图
> 只改海安侧适配与后续 PC 查询,**不改** `production.AutoIssueServiceImpl.autoIssue`
> 对接与 PC 查询已落地;2026-09-18 增加请求前持久化和结果待核实保护。下述事务拆分建议仍有待完成项,以 3.4/3.6 为准。 - rqrq
---
## 1. 同事方法实际给了什么(凑他,不能改)
入口:`AutoIssueService.autoIssue(AutoIssueRequestDto)`
实现:按工单 → 按标签循环,每张调 `IfsApiIssueAndReturnService.addShopOrderManualIssue`,标签之间 `sleep 20ms`
### 入参(已与海安侧对齐,2026-09-18)
工单层:标签先查 `haian_SOIssueNotifyOrderMaterialList_detail`,再用 `site + notify_no + item_no` 关联 `haian_SOIssueNotifyOrderList`,取出 `soorder_no / release_no / sequence_no`。按这些键分组后调用 `autoIssue`
```
AutoIssueShopOrderDto
site 发料任务 site(RFID 回调大 site,禁止登录态)
shopOrderNo OrderList.soorder_no
workOrderNo 固定空串,不传工单号
releaseNo OrderList.release_no,空则海安先填 *
sequenceNo OrderList.sequence_no,空则海安先填 *
handlingUnits[] 该组下全部待发标签
```
标签层:除 `lineItemNo` 外全部读 **实时 handling_unit**(移库成功后再查,库位已是目标库)。`labelCode` 固定空;同事 `getLabelCode()` 空时回退 `handlingUnitId`,失败清单也能对上 unit_id。
```
AutoIssueHandlingUnitDto
handlingUnitId handling_unit.unit_id
labelCode 固定空
partNo handling_unit.part_no
locationId handling_unit.location_id(移库后当前库位)
batchNo handling_unit.batch_no
serialNo handling_unit 无独立 IFS 序列字段,查不到传空,同事内部变 *
engChgLevel handling_unit.eng_chg_level(现海安查询 SQL 未选出,实施时补列)
wdrNo handling_unit.wdr
lineItemNo detail.BOM_item_no 转 Integer;转失败吞掉异常改 0(注意同事只在 null 时默认 1,0 会原样给 IFS)
issueQty handling_unit.qty,整标签全发
```
工单级缺 `site` / 工单号 / `handlingUnits` **会直接抛 `XJException`**,整次调用失败,前面已经发成功的标签也不会出现在回参里。
标签级缺字段或 IFS 失败 **不抛**,记入 `failures`,继续下一张。
### 回参(没有成功标签列表)
```
AutoIssueResultDto
totalCount / successCount / failCount
failures[] 只有失败
shopOrderNo, releaseNo, sequenceNo
handlingUnitId, labelCode, partNo, message
```
**成功标签不返回。** 海安只能用:
`本批送进去的标签 − failures.labelCode/handlingUnitId = 发料成功`
禁止把 `successCount == totalCount` 当成唯一依据去解绑:必须按标签对账,部分成功只解绑成功的。
不要走 `ProductionIssueController /autoIssue` HTTP。Controller 在 `failCount>0` 时返回 `R.error`,容易误判整单失败。海安定时任务直接注入 `AutoIssueService`
---
## 2. 海安现有定时链路(对接点)
```
RFID 复核通过
└ createFromRfidCheck 已有:建 haian_auto_issue_task + 标签快照 + 移库入队
HaianAutoTaskScheduler 每 4s
└ processOneAutoIssueTask 已有:等 IFS 队列
├ 队列未完结 本轮跳过
├ 队列 Y+F / 结果非 S finishByTransferFail → 不解绑,立库 type=2
└ 队列全 Y+S 或无需搬库 finishByIssue
└ finishByIssue
适配器调同事 autoIssue(已落地)
成功标签 deletePalletDetail + unbind=Y
全成功 type=1;有一张失败 type=2
```
`finishByTransferFail`、RFID 建单、移库入队 **都不用动**。发料段已改为 `issueMaterialsViaProduction` + `HaiAnProductionAutoIssueAdapter`
---
## 3. 推荐对接方案
### 3.1 适配器放哪
海安模块新建适配方法(名字示意):`HaiAnAutoIssueServiceImpl.issueMaterialsViaProduction`
- 入:任务 + 本任务标签列表
- 出:继续用现有 `HaiAnAutoIssueSimulateResult`(`serialNo + success + message`)
- 内部:组 `AutoIssueRequestDto` → 调 `autoIssue` → 按 failures 反推每张标签成败
这样 `finishByIssue` 后面的解绑 / 空托 / RFID 完结 / type=1|2 **原逻辑复用**
### 3.2 组单规则(移库成功之后再组)
1. 只处理 `issue_flag` 为空的标签;`S/F` 复用结果,`P/U` 进入待核实,均禁止再次发送。 - rqrq
2. 按标签再查一次 **当前** `handling_unit`(移库后 `location_id` 已变,禁止用建单快照)。现查询 XML 需补 `eng_chg_level`
3. 用任务标签 `serial_no``haian_SOIssueNotifyOrderMaterialList_detail`,再 `site + notify_no + item_no` 关联 `haian_SOIssueNotifyOrderList`,取 `soorder_no / release_no / sequence_no`
4. 按 `site + shopOrderNo + releaseNo + sequenceNo` 分组,每组一个 `AutoIssueShopOrderDto`,`workOrderNo=""`;`releaseNo/sequenceNo` 空则海安填 `*`(不要依赖同事默认,两边口径一致)。
5. 每张标签按第 1 节映射表填写;`labelCode` 空、`issueQty=qty` 全发、`lineItemNo` 由 `BOM_item_no` 转 Integer,转失败记 0。
6. 组完后 **本地先校验** site/工单号/handlingUnitId/partNo/locationId/issueQty,缺了不要调 `autoIssue`
7. 回参 `failCount>0` 或任一张失败:**成功的仍解绑,整托走异常位 type=2**(不要因为部分成功就 type=1)。
### 3.3 回参对账 → 解绑
```
sent = 本次实际送给 autoIssue 的 serial_no 集合
调用前:将 sent 的空 issue_flag 原子更新为 P,独立事务提交;任一条失败则整批不外呼。
failSet = failures 里 labelCode 或 handlingUnitId 命中的集合
收到回参后:先核对 total/success/fail 数量及失败标签唯一性、归属,再反推成功。
对每张 sent 标签:
在 failSet → issue_flag=F,unbind=N,写 error_msg
不在 failSet → issue_flag=S,deletePalletDetail,unbind=Y
同事抛异常(整单没回参):
本批全部记 U 待核实,禁止自动重发或解绑(无法证明哪些已进 IFS)
恢复时发现历史 P:
不再调用同事,转 U 待核实;先核实 IFS 发料记录,禁止直接补发。
```
全成功:任务已完成、RFID 已完成、立库 type=1。
有失败:任务已完成 + error_code=发料失败、失败标签留盘、立库 type=2。
有待核实:任务已完成 + error_code=发料待核实、U 标签留盘、立库 type=2;已完成仅指自动流程结束,不代表业务发料结果已确认。 - rqrq
### 3.4 事务(必须和同事方法错开)
`autoIssue` 内部是 IFS HTTP + 每标签 sleep,**不能包在海安长事务里**。
建议拆成以下阶段(尚未完整落地):
1. 移库判定 + 锁成「处理中」先提交。
2. **事务外**`autoIssue`
3. 新事务写标签结果、解绑、RFID、再调立库(立库仍须在本地终态之后)。
当前仍由外层任务事务保护锁单,适配器 `NOT_SUPPORTED` 只挂起外层事务,不提交或释放其锁;WCS 通知仍在外层提交前,长事务及通知提交顺序问题需另行改造。 - rqrq
超时回收仍把卡住的「处理中」退回「移库中」。重试只允许空发料状态调用,S/F 复用;P 转 U、U 保留。 - rqrq
### 3.5 幂等(同事方法没有)
同事每调一次就对 IFS 发一次。海安必须:
- 已 `issue_flag=S/F/P/U` 不再送;只有空状态可登记新请求。 - rqrq
- 先落库发料结果,再解绑;解绑失败也不要把已成功标签再送一遍
- 同一 RFID 明细已有任务则不重复建单(现网已有)
### 3.6 请求中断保护与上线要求(已落地)
- `HaiAnAutoIssueLabelFlagPersistService.markIssueRequestsStarted` 使用独立新事务,按 site+任务ID+标签ID+托盘+标签号将空状态改 P。全部提交后才调用同事;登记失败时不外呼。 - rqrq
- 同事返回后独立保存 S/F/U;结果落库失败或进程中断时,先前提交的 P 保留,恢复后只能转 U。空异常消息、空回参、数量或标签对账不一致也转 U。 - rqrq
- 保留按工单分组的整批调用;同事中途异常导致没有整批回参时,本批无法区分已执行和未执行标签,全部待核实,不伪造成功或自动补发。 - rqrq
- PC 展示 P=请求已开始、U=待核实,主表增加 unknownCount;S+N 仍显示留盘。页面保持只读,核实与人工处置入口不在本次范围。 - rqrq
- 上线前停止所有实例的海安自动任务,执行 `sql/20260918_haian_auto_issue_request_state.sql`,部署支持 P/U 的代码后恢复调度;禁止新旧消费者混跑。原建表脚本只处理新表,不能替代存量约束升级。 - rqrq
- 本补丁不能倒推出上线前空状态标签是否曾实际发料;历史可疑任务须先人工核对,禁止直接清空 P/U 重试。未执行真实 IFS 或数据库故障注入验证。 - rqrq
---
## 4. 已拍板(原待确认项)
1. `shopOrders.site`:发料任务大 site。
2. `lineItemNo`:`detail.BOM_item_no` 转 Integer,转失败为 0(不是同事的 null→1)。
3. `labelCode` 固定空,失败对账用 `handlingUnitId`(同事内部会把空 labelCode 回退成 handlingUnitId)。
4. 只要有一张失败:成功标签仍解绑,整托 type=2 去异常位。
5. `releaseNo/sequenceNo` 空则海安填 `*`
6. `workOrderNo` 固定空。
---
## 5. PC「自动发料任务」页面草图(后做,先定 UI)
风格对齐海安现有页:`palletScanCallback54` / `agvTask54`
目录建议:`wms-vue/src/views/modules/haianWarehouse/automatedWarehouse/autoIssueTask54.vue`
API:`wms-vue/src/api/haianWarehouse/`,后端独立 Controller,入参用已有 `HaiAnAutoIssueTaskData`,禁止 Map。
`site` 必须 `this.$store.state.user.site`。查询/刷新按钮 `xxxLoading + disabled + finally`
### 5.1 整页布局
```
┌──────────────────────────────────────────────────────────────────────────┐
│ 海安自动发料任务 │
├──────────────────────────────────────────────────────────────────────────┤
│ 工厂[54xx 禁用] 托盘[ ] 运输任务号[ ] │
│ 状态[全部▼] 失败码[全部▼] 创建从[日期] 至[日期] │
│ [查询] [重置] │
├──────────────────────────────────────────────────────────────────────────┤
│ 任务列表(主表 haian_auto_issue_task) 高亮当前行 │
│ ┌────┬────────┬──────────┬────────┬────────┬────────┬────────┬────────┐ │
│ │ID │托盘号 │运输任务号│状态 │失败码 │标签数 │成功/失败│创建时间│ │
│ │102 │P00021 │T20260917 │已完成 │发料失败│ 5 │ 4 / 1 │09-17 │ │
│ │101 │P00018 │T20260917 │移库中 │ │ 3 │ - / - │09-17 │ │
│ └────┴────────┴──────────┴────────┴────────┴────────┴────────┴────────┘ │
│ 共 2 条 [<] 1/1 [>] 每页 20 │
├──────────────────────────────────────────────────────────────────────────┤
│ 选中任务 #102 托盘 P00021 状态 已完成 失败码 发料失败 │
│ RFID明细ID 8891 完结时间 2026-09-17 16:20 │
│ 错误说明:LBL-005:IFS库位不足 │
│ │
│ [标签明细] [移库队列] │
│ │
│ 标签明细(haian_auto_issue_task_label) │
│ ┌────────┬────────┬────────┬────┬────┬────┬──────────────────────────┐ │
│ │标签号 │物料号 │申请单号│发料│解绑│留盘│失败原因 │ │
│ │LBL-001 │A100 │N54-01 │成功│已解│否 │ │ │
│ │LBL-005 │A100 │N54-01 │失败│未解│是 │IFS库位不足 │ │
│ └────────┴────────┴────────┴────┴────┴────┴──────────────────────────┘ │
│ │
│ 移库队列(haian_auto_issue_move_ref → haian_ifs_pallet_move_queue 只读) │
│ ┌────────┬────────┬────────┬──────────────────────────┐ │
│ │队列ID │status │结果 │result_msg │ │
│ │8801 │Y │S │ │ │
│ └────────┴────────┴────────┴──────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────┘
```
点击主表行立即刷下方明细,不必再开一层“查看”才能看到标签。需要对照原报文时,主表操作列保留「详情」弹窗(可后做)。
### 5.2 查询区
| 控件 | 绑定 | 说明 |
|---|---|---|
| 工厂编码 | site | 登录 site,禁用 |
| 托盘号 | searchPalletId | 精确 |
| 运输任务号 | searchTransportTaskNo | 精确 |
| 状态 | searchStatus | 全部 / 移库中 / 待发料 / 处理中 / 已完成 |
| 失败码 | searchErrorCode | 全部 / 空(成功) / 移库失败 / 发料失败 |
| 创建时间从/至 | startDate / endDate | 按 created_time |
本页只读,不做「重试发料」「手工解绑」。补发走异常位后的现有人工流程。
### 5.3 主表列
| 列 | 来源 | 展示 |
|---|---|---|
| ID | id | |
| 托盘号 | palletId | |
| 运输任务号 | transportTaskNo | |
| 状态 | status | Tag:移库中蓝 / 待发料橙 / 处理中灰 / 已完成绿;有 errorCode 则红 |
| 失败码 | errorCode | 空显示「-」 |
| 标签数 | 聚合 count(label) | 不落库 |
| 成功/失败 | 聚合 issue_flag=S / F | 未发显示 - / - |
| 已解绑数 | 聚合 unbind_flag=Y | 可选列 |
| 创建 / 更新 / 完结时间 | created/updated/processed | |
行颜色:`移库失败`/`发料失败` 浅红底,便于现场扫。
### 5.4 标签明细列
| 列 | 来源 | 展示 |
|---|---|---|
| 标签号 | serialNo | |
| 物料号 | partNo | |
| 申请单号 | notifyNo | |
| 预留2/3 | reserveOrderRef2/3 | 默认隐藏或详情里看 |
| 发料 | issueFlag | 空=未发 灰;S=成功 绿;F=失败 红 |
| 解绑 | unbindFlag | Y=已解绑;N=仍在托 |
| 是否留盘 | 计算 | 发料失败或未发且 N → 是 |
| 失败原因 | errorMsg | show-overflow-tooltip |
| 更新时间 | updatedTime | |
规则提示(页脚一句即可):发料成功才会解绑;移库失败整单标签都是未发+留盘。
### 5.5 移库队列表
只读展示本任务 `move_queue_id` 对应队列的 `status / result_flag / result_msg`
无关联行时显示:「无需搬库,视为 IFS 已完成」。
### 5.6 详情弹窗(可选,第二期)
左:任务字段(含 RFID 明细 id、error_msg 全文)。
右:标签表(同下方面板)。
底:关闭。不要在弹窗里放提交类按钮。
### 5.7 空状态 / 加载
- 未查询或无数据:主表空,下方「请选择一条任务」。
- 查询中:主表 `dataListLoading`
- 切行:仅明细 `labelListLoading`,不要整页转圈。
### 5.8 接口(已落地,走海安 PC 模块路径,不走 PDA `/haiAnWcsIntegration`
```
POST /autoWareHourceHaian/haiAnAutoIssueTask/getAutoIssueTaskPage
body: HaiAnAutoIssueTaskData(site/page/limit/search*)
回: R.page
POST /autoWareHourceHaian/haiAnAutoIssueTask/getAutoIssueTaskLabelList
body: HaiAnAutoIssueTaskLabelData(site + searchIssueTaskId)
回: R.rows
POST /autoWareHourceHaian/haiAnAutoIssueTask/getAutoIssueMoveRefList
body: HaiAnAutoIssueMoveRefData(site + searchIssueTaskId)
回: R.rows(带队列 queueStatus/resultFlag/resultMsg,JOIN 只读)
```
主表聚合字段(标签数、成功/失败)可在分页 SQL 一次查出,避免 N+1。
---
## 6. 建议实施顺序
1. 海安适配器按第 1/3.2 节组单,替换模拟发料(已落地)。
2. 联调:全成功解绑+type=1;有一张失败则成功解绑+失败留盘+type=2;移库失败不解绑+type=2。
3. PC 查询页(已落地):执行 `sql/20260918_haian_pc_auto_issue_task_menu.sql` 后按角色授权菜单。

27
src/main/java/com/gaotao/modules/autoWareHourceHaian/controller/HaiAnSortingBoardController.java

@ -0,0 +1,27 @@
package com.gaotao.modules.autoWareHourceHaian.controller;
import com.gaotao.common.utils.R;
import com.gaotao.modules.autoWareHourceHaian.entity.dashboard.HaiAnSortingBoardQuery;
import com.gaotao.modules.autoWareHourceHaian.entity.dashboard.HaiAnSortingBoardResult;
import com.gaotao.modules.autoWareHourceHaian.service.HaiAnSortingBoardService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/** 海安只读分拣大屏入口,沿用/api/dashboard匿名展示策略;服务严格限制site=54及六个分拣位 - rqrq */
@RestController
@RequestMapping("/api/dashboard/haian/sorting")
public class HaiAnSortingBoardController {
@Autowired
private HaiAnSortingBoardService haiAnSortingBoardService;
/** 收取看板工厂和分拣位并返回row;参数校验与WCS/WMS查询均在服务层执行 - rqrq */
@PostMapping("/getSortingBoard")
public R getSortingBoard(@RequestBody HaiAnSortingBoardQuery query) {
// 查询对应分拣位快照site缺失或非海安/白名单分拣位时服务抛错禁止返回其他工厂数据 - rqrq
HaiAnSortingBoardResult row = haiAnSortingBoardService.getSortingBoard(query);
return R.ok().put("row", row);
}
}

22
src/main/java/com/gaotao/modules/autoWareHourceHaian/entity/dashboard/HaiAnSortingBoardItem.java

@ -0,0 +1,22 @@
package com.gaotao.modules.autoWareHourceHaian.entity.dashboard;
import lombok.Data;
/** 海安分拣看板展示行;标签范围来自WCS,业务字段来自WMS无锁查询,禁止用于业务写入决策 - rqrq */
@Data
public class HaiAnSortingBoardItem {
/** WCS materials.rfidBarcode,对应handling_unit.unit_id,不落库,供页面显示标签号 - rqrq */
private String rfidBarcode;
/** 当前有效预留的handling_unit.reserve_order_ref3工单号;未预留或标签不唯一为空,不落库供看板展示 - rqrq */
private String orderNo;
/** 按标签预留申请单与行号关联海安订单的fgpart_no;未匹配为空,不落库供看板展示产品编码 - rqrq */
private String orderPartNo;
/** handling_unit.part_no物料编码;未找到或跨账套重码为空,不落库供看板展示 - rqrq */
private String partNo;
/** handling_unit.part_desc物料名称;未找到或跨账套重码为空,不落库供看板展示 - rqrq */
private String partDesc;
/** site+标签号下存在out_wcs_flag=N则未完成,否则已完成;不落库,仅为看板展示状态 - rqrq */
private String status;
/** 标签及预留匹配提示,缺失或重码时保留RFID并说明原因,不落库供现场核对 - rqrq */
private String message;
}

12
src/main/java/com/gaotao/modules/autoWareHourceHaian/entity/dashboard/HaiAnSortingBoardQuery.java

@ -0,0 +1,12 @@
package com.gaotao.modules.autoWareHourceHaian.entity.dashboard;
import lombok.Data;
/** 海安分拣看板请求,仅供查询,工厂与分拣位均由页面显式传入 - rqrq */
@Data
public class HaiAnSortingBoardQuery {
/** 请求工厂编码,当前仅允许54,不落库,用于服务校验与SQL隔离 - rqrq */
private String site;
/** 请求分拣位,允许J1H/J2H/R1H/R2H/R3H/R4H,不落库,用于选择对应看板 - rqrq */
private String sortingStation;
}

22
src/main/java/com/gaotao/modules/autoWareHourceHaian/entity/dashboard/HaiAnSortingBoardResult.java

@ -0,0 +1,22 @@
package com.gaotao.modules.autoWareHourceHaian.entity.dashboard;
import lombok.Data;
import java.util.Collections;
import java.util.List;
/** 海安单分拣位看板快照,与空清单区分查询异常;全部字段不落库 - rqrq */
@Data
public class HaiAnSortingBoardResult {
/** 已校验的请求工厂编码,不落库,前端核对响应所属工厂 - rqrq */
private String site;
/** 已校验的分拣位编码,不落库,前端核对响应所属屏幕 - rqrq */
private String sortingStation;
/** 本轮是否取得有效WCS及WMS数据;false时rows为空,前端显示异常而非暂无任务 - rqrq */
private boolean available;
/** 快照状态说明,来源服务校验/查询结果,不落库供前端提示 - rqrq */
private String message;
/** 本轮WCS及WMS查询结束时间,Unix毫秒,失败为0,不落库供页面显示数据时间 - rqrq */
private long updatedAt;
/** 当前分拣位标签明细,缺省空列表,不落库供前端表格显示 - rqrq */
private List<HaiAnSortingBoardItem> rows = Collections.emptyList();
}

14
src/main/java/com/gaotao/modules/autoWareHourceHaian/mapper/HaiAnSortingBoardMapper.java

@ -0,0 +1,14 @@
package com.gaotao.modules.autoWareHourceHaian.mapper;
import com.gaotao.modules.autoWareHourceHaian.entity.dashboard.HaiAnSortingBoardItem;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/** 海安看板专用只读Mapper,全部SQL使用NOLOCK,禁止被业务状态更新复用 - rqrq */
@Mapper
public interface HaiAnSortingBoardMapper {
/** 按工厂及最多500个RFID批量补齐预留工单、物料及完成状态;空列表不允许扫描全表 - rqrq */
List<HaiAnSortingBoardItem> listSortingBoardItems(@Param("site") String site,
@Param("rfidBarcodes") List<String> rfidBarcodes);
}

10
src/main/java/com/gaotao/modules/autoWareHourceHaian/service/HaiAnSortingBoardService.java

@ -0,0 +1,10 @@
package com.gaotao.modules.autoWareHourceHaian.service;
import com.gaotao.modules.autoWareHourceHaian.entity.dashboard.HaiAnSortingBoardQuery;
import com.gaotao.modules.autoWareHourceHaian.entity.dashboard.HaiAnSortingBoardResult;
/** 海安六个分拣看板统一查询服务,不修改分拣任务和库存 - rqrq */
public interface HaiAnSortingBoardService {
/** 按显式site及分拣位读取共享ALL快照;非法参数抛异常,外部查询失败返回不可用快照 - rqrq */
HaiAnSortingBoardResult getSortingBoard(HaiAnSortingBoardQuery query);
}

167
src/main/java/com/gaotao/modules/autoWareHourceHaian/service/impl/HaiAnSortingBoardServiceImpl.java

@ -0,0 +1,167 @@
package com.gaotao.modules.autoWareHourceHaian.service.impl;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.gaotao.modules.autoWareHourceHaian.api.entity.HaiAnAutoSortingInfoRequest;
import com.gaotao.modules.autoWareHourceHaian.api.service.HaiAnWcsApiService;
import com.gaotao.modules.autoWareHourceHaian.entity.dashboard.HaiAnSortingBoardItem;
import com.gaotao.modules.autoWareHourceHaian.entity.dashboard.HaiAnSortingBoardQuery;
import com.gaotao.modules.autoWareHourceHaian.entity.dashboard.HaiAnSortingBoardResult;
import com.gaotao.modules.autoWareHourceHaian.mapper.HaiAnSortingBoardMapper;
import com.gaotao.modules.autoWareHourceHaian.service.HaiAnSortingBoardService;
import com.gaotao.modules.autoWareHourceHaian.staticData.HaiAnSiteStaticData;
import com.gaotao.modules.autoWareHourceHaian.staticData.HaiAnStationStaticData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* 海安看板按需共用5秒ALL快照六屏并发只触发一次WCS及批量WMS查询无页面访问时不外呼
* 无业务写入无数据库事务WCS接口日志由既有接口层负责失败快照同样缓存5秒防止外部异常时请求风暴
* NOLOCK仅供展示缓存不允许用于预留/扣库存/发料决策多实例部署每个实例独立缓存- rqrq
*/
@Slf4j
@Service
public class HaiAnSortingBoardServiceImpl implements HaiAnSortingBoardService {
/** 成功和失败快照统一缓存5秒,使用单调时钟计算有效期,避免系统校时导致缓存永久有效 - rqrq */
private static final long CACHE_NANOS = TimeUnit.SECONDS.toNanos(5);
/** 单次SQL最多500个RFID,避免SQL Server的2100个参数限制 - rqrq */
private static final int BATCH_SIZE = 500;
@Autowired
private HaiAnWcsApiService haiAnWcsApiService;
@Autowired
private HaiAnSortingBoardMapper haiAnSortingBoardMapper;
/** 缓存只包含已校验工厂54的六个分拣位,读写均在同一实例锁内,禁止其他site进入 - rqrq */
private Map<String, HaiAnSortingBoardResult> cachedBoards = Collections.emptyMap();
/** 完整快照发布时的单调时钟值,0表示尚未加载 - rqrq */
private long cachedAtNanos;
/** 先校验site和点位,再串行刷新并返回快照;并发请求复用缓存,不重复调用ALL - rqrq */
@Override
public synchronized HaiAnSortingBoardResult getSortingBoard(HaiAnSortingBoardQuery query) {
if (query == null || !StringUtils.hasText(query.getSite())
|| !HaiAnSiteStaticData.HAIAN_SITE.equals(query.getSite().trim())) {
throw new IllegalArgumentException("海安分拣看板必须显式传入site=54");
}
String station = query.getSortingStation() == null ? ""
: query.getSortingStation().trim().toUpperCase(Locale.ROOT);
// 使用海安已有分拣位白名单排除被分拣源盘R5H-R8H等非看板位置非法输入不触发外呼 - rqrq
if (!HaiAnStationStaticData.isAutoSortStation(station)
&& !HaiAnStationStaticData.isManualSortStation(station)) {
throw new IllegalArgumentException("不支持的海安看板分拣位:" + station);
}
String site = query.getSite().trim();
if (cachedBoards.isEmpty() || System.nanoTime() - cachedAtNanos >= CACHE_NANOS) {
try {
// 请求ALL并批量补全WMS字段整体完成后才发布失败不得暴露部分新数据与旧数据混合的快照 - rqrq
cachedBoards = loadBoards(site);
} catch (Exception ex) {
log.error("海安分拣看板查询失败,site={},清空本轮六屏数据 - rqrq", site, ex);
// 构造明确不可用的六屏快照避免把WCS/SQL异常伪装成暂无分拣任务 - rqrq
cachedBoards = unavailableBoards(site, "分拣数据查询失败,请检查WCS/WMS服务");
}
cachedAtNanos = System.nanoTime();
}
return cachedBoards.get(station);
}
/** 拉取全部分拣位并补充WMS信息;仅接收合法数组/标签,缺失点位保留异常状态,空materials为正常空看板 - rqrq */
private Map<String, HaiAnSortingBoardResult> loadBoards(String site) throws Exception {
HaiAnAutoSortingInfoRequest request = new HaiAnAutoSortingInfoRequest();
request.setSite(site);
request.setLocationCode("ALL");
// 复用海安接口封装HTTP体由接口层序列化为JSON字符串"ALL"网络及业务失败抛错并写既有日志 - rqrq
String response = haiAnWcsApiService.queryAutoSortingInfo(request);
// 解析WCS响应支持根sortingStations及resData包裹报文错误必须中断不能沿用旧清单 - rqrq
JsonNode root = new ObjectMapper().readTree(response);
JsonNode stations = root == null ? null : root.get("sortingStations");
if (stations == null && root != null) {
stations = root.path("resData").get("sortingStations");
}
if (stations == null || !stations.isArray()) {
throw new IllegalStateException("WCS未返回sortingStations数组");
}
// 预置六个分拣位WCS没有返回的点位不能悄悄保留上轮数据 - rqrq
Map<String, HaiAnSortingBoardResult> boards = unavailableBoards(site, "WCS未返回该分拣位,请核对位置配置");
Map<String, Set<String>> stationLabels = new LinkedHashMap<>();
Set<String> allLabels = new LinkedHashSet<>();
for (JsonNode stationNode : stations) {
String station = stationNode.path("sortingStation").asText("").trim().toUpperCase(Locale.ROOT);
if (!boards.containsKey(station)) {
continue;
}
JsonNode materials = stationNode.get("materials");
if (stationLabels.containsKey(station) || materials == null || !materials.isArray()) {
throw new IllegalStateException("WCS分拣位重复或materials不是数组:" + station);
}
Set<String> labels = new LinkedHashSet<>();
for (JsonNode material : materials) {
JsonNode barcode = material.get("rfidBarcode");
String rfid = barcode == null ? "" : barcode.asText("").trim();
if (barcode == null || !barcode.isTextual() || rfid.isEmpty() || rfid.length() > 200) {
throw new IllegalStateException("WCS分拣位存在无效RFID:" + station);
}
labels.add(rfid);
allLabels.add(rfid);
}
stationLabels.put(station, labels);
}
List<String> labelList = new ArrayList<>(allLabels);
Map<String, HaiAnSortingBoardItem> details = new LinkedHashMap<>();
for (int start = 0; start < labelList.size(); start += BATCH_SIZE) {
List<String> batch = labelList.subList(start, Math.min(start + BATCH_SIZE, labelList.size()));
// 仅查本工厂及WCS实际返回的标签每批最多500个NOLOCK允许展示短暂不一致SQL失败整轮不可用 - rqrq
List<HaiAnSortingBoardItem> items = haiAnSortingBoardMapper.listSortingBoardItems(site, batch);
for (HaiAnSortingBoardItem item : items) {
details.put(item.getRfidBarcode(), item);
}
}
long updatedAt = System.currentTimeMillis();
for (Map.Entry<String, Set<String>> entry : stationLabels.entrySet()) {
List<HaiAnSortingBoardItem> rows = new ArrayList<>();
for (String rfid : entry.getValue()) {
HaiAnSortingBoardItem item = details.get(rfid);
if (item == null) {
throw new IllegalStateException("WMS看板查询未返回标签:" + rfid);
}
rows.add(item);
}
// 未完成优先同状态保持WCS清单顺序状态完全采用site+标签下out_wcs_flag=N存在性不用WCS完成标志猜测 - rqrq
rows.sort(Comparator.comparingInt(item -> "未完成".equals(item.getStatus()) ? 0 : 1));
HaiAnSortingBoardResult board = boards.get(entry.getKey());
board.setAvailable(true);
board.setMessage(rows.isEmpty() ? "当前分拣位暂无任务" : "数据正常");
board.setUpdatedAt(updatedAt);
board.setRows(Collections.unmodifiableList(rows));
}
return Collections.unmodifiableMap(boards);
}
/** 创建六个位置的不可用结果,site由已校验请求传递,rows为空防止过期任务继续展示 - rqrq */
private Map<String, HaiAnSortingBoardResult> unavailableBoards(String site, String message) {
List<String> stations = new ArrayList<>(HaiAnStationStaticData.AUTO_SORT_STATIONS);
stations.addAll(HaiAnStationStaticData.MANUAL_SORT_STATIONS);
Map<String, HaiAnSortingBoardResult> boards = new LinkedHashMap<>();
for (String station : stations) {
HaiAnSortingBoardResult board = new HaiAnSortingBoardResult();
board.setSite(site);
board.setSortingStation(station);
board.setAvailable(false);
board.setMessage(message);
boards.put(station, board);
}
return boards;
}
}

4
src/main/java/com/gaotao/modules/autoWareHourceHaian/staticData/HaiAnStationStaticData.java

@ -94,10 +94,10 @@ public final class HaiAnStationStaticData {
public static final Integer GROUND_LOCATION_REMOVE = 2; public static final Integer GROUND_LOCATION_REMOVE = 2;
/** /**
* 自动分拣组盘位白名单pickingFeedback 命中记自动分拣与手工分拣位被分拣位都不是同一组待确认站点实际编码当前为旧暂定值 J1J4 - rqrq
* 自动分拣组盘位白名单pickingFeedback 命中记自动分拣与手工分拣位被分拣位都不是同一组待确认站点实际编码当前为旧暂定值 J1J4 - rqrq 海安自动分拣就2个位置
*/ */
public static final List<String> AUTO_SORT_STATIONS = Collections.unmodifiableList( public static final List<String> AUTO_SORT_STATIONS = Collections.unmodifiableList(
Arrays.asList("J1H", "J2H", "J3H", "J4H")
Arrays.asList("J1H", "J2H")
); );
/** /**

67
src/main/resources/mapper/autoWareHourceHaian/HaiAnSortingBoardMapper.xml

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gaotao.modules.autoWareHourceHaian.mapper.HaiAnSortingBoardMapper">
<!-- 全部表含关联和EXISTS均使用NOLOCK,可能脏读、漏读或重复读,仅供看板,不作为发料/预留/库存决策;以WCS标签驱动,WMS缺失也保留标签 - rqrq -->
<select id="listSortingBoardItems" resultType="com.gaotao.modules.autoWareHourceHaian.entity.dashboard.HaiAnSortingBoardItem">
SELECT requested.rfidBarcode AS rfidBarcode,
CASE WHEN hu.matchCount = 1 AND hu.reserveFlag = 'Y'
THEN ISNULL(hu.orderNo, '') ELSE '' END AS orderNo,
CASE WHEN hu.matchCount = 1 AND hu.reserveFlag = 'Y'
THEN ISNULL(ord.orderPartNo, '') ELSE '' END AS orderPartNo,
CASE WHEN hu.matchCount = 1 THEN ISNULL(hu.partNo, '') ELSE '' END AS partNo,
CASE WHEN hu.matchCount = 1 THEN ISNULL(hu.partDesc, '') ELSE '' END AS partDesc,
CASE WHEN EXISTS (
SELECT 1
FROM haian_SOIssueNotifyOrderMaterialList_detail d WITH (NOLOCK)
WHERE d.site = #{site}
AND d.serialNo = requested.rfidBarcode
AND d.out_wcs_flag = 'N'
) THEN N'未完成' ELSE N'已完成' END AS status,
CASE WHEN hu.matchCount = 0 THEN N'WMS未找到该工厂标签'
WHEN hu.matchCount &gt; 1 THEN N'标签存在多条记录,请核对账套'
WHEN ISNULL(hu.reserveFlag, '') != 'Y' THEN N'标签当前未预留'
WHEN NULLIF(LTRIM(RTRIM(hu.orderNo)), '') IS NULL THEN N'标签预留工单号为空'
WHEN NULLIF(LTRIM(RTRIM(ord.orderPartNo)), '') IS NULL THEN N'预留订单未匹配产品编码'
ELSE '' END AS message
FROM (
<choose>
<when test="rfidBarcodes != null and rfidBarcodes.size() &gt; 0">
<foreach collection="rfidBarcodes" item="rfid" separator=" UNION ALL ">
SELECT CAST(#{rfid} AS NVARCHAR(200)) AS rfidBarcode
</foreach>
</when>
<otherwise>
SELECT CAST(NULL AS NVARCHAR(200)) AS rfidBarcode WHERE 1 = 0
</otherwise>
</choose>
) requested
<!-- 聚合只用于检测同码多账套,不从多条标签中随意挑选工单或物料;工厂边界使用site_sub映射 - rqrq -->
OUTER APPLY (
SELECT COUNT(1) AS matchCount,
MAX(h.site) AS subSite,
MAX(h.part_no) AS partNo,
MAX(h.part_desc) AS partDesc,
MAX(h.reserve_flag) AS reserveFlag,
MAX(h.reserve_order_ref1) AS notifyNo,
MAX(h.reserve_order_ref2) AS itemNo,
MAX(h.reserve_order_ref3) AS orderNo
FROM handling_unit h WITH (NOLOCK)
WHERE h.unit_id = requested.rfidBarcode
AND (h.site = #{site} OR EXISTS (
SELECT 1 FROM site_sub ss WITH (NOLOCK)
WHERE ss.siteid = #{site} AND ss.subsiteid = h.site
))
) hu
<!-- 仅当前有效预留按完整工厂/子账套/申请单/订单行关联产品,不反查标签历史任务,避免工单串单 - rqrq -->
OUTER APPLY (
SELECT CASE WHEN COUNT(1) = 1 THEN MAX(o.fgpart_no) ELSE '' END AS orderPartNo
FROM haian_SOIssueNotifyOrderList o WITH (NOLOCK)
WHERE hu.matchCount = 1 AND hu.reserveFlag = 'Y'
AND o.site = #{site}
AND o.sub_site = hu.subSite
AND o.notify_no = hu.notifyNo
AND CONVERT(VARCHAR(50), o.item_no) = hu.itemNo
AND o.soorder_no = hu.orderNo
) ord
</select>
</mapper>
Loading…
Cancel
Save