#
zwl
2026-04-03 99d39b9533897c8bda6409d4637b245d86967da2
#
17个文件已添加
1134 ■■■■■ 已修改文件
src/main/java/com/zy/asrs/controller/BasCrnpController.java 189 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/BasCrnp.java 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/dto/BasCrnpMonitorDto.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/dto/WcsCrnDto.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/dto/WcsCrnSyncResult.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/enums/CrnModeType.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/enums/CrnStatusType.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/mapper/BasCrnpMapper.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/BasCrnpService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/WcsCrnSyncService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/BasCrnpServiceImpl.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/WcsCrnSyncServiceImpl.java 207 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/timer/WcsCrnTimer.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/utils/CrnUtils.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/sql/202604030001_add_bas_crnp.sql 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/static/js/basCrnp/basCrnp.js 275 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/basCrnp/basCrnp.html 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/controller/BasCrnpController.java
New file
@@ -0,0 +1,189 @@
package com.zy.asrs.controller;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.core.annotations.ManagerAuth;
import com.core.common.BaseRes;
import com.core.common.Cools;
import com.core.common.R;
import com.zy.asrs.entity.BasCrnp;
import com.zy.asrs.entity.dto.BasCrnpMonitorDto;
import com.zy.asrs.entity.dto.WcsCrnDto;
import com.zy.asrs.entity.dto.WcsCrnSyncResult;
import com.zy.asrs.service.BasCrnpService;
import com.zy.asrs.service.WcsCrnSyncService;
import com.zy.asrs.utils.CrnUtils;
import com.zy.common.web.BaseController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
public class BasCrnpController extends BaseController {
    @Autowired
    private BasCrnpService basCrnpService;
    @Autowired
    private WcsCrnSyncService wcsCrnSyncService;
    @Autowired
    private CrnUtils crnUtils;
    @RequestMapping(value = "/basCrnp/{id}/auth")
    @ManagerAuth
    public R get(@PathVariable("id") Integer id) {
        return R.ok(basCrnpService.getById(id));
    }
    @RequestMapping(value = "/basCrnp/list/auth")
    @ManagerAuth
    public R list(@RequestParam(defaultValue = "1") Integer curr,
                  @RequestParam(defaultValue = "10") Integer limit,
                  @RequestParam(required = false) Integer crnNo,
                  @RequestParam(required = false) String condition) {
        QueryWrapper<BasCrnp> wrapper = new QueryWrapper<>();
        if (crnNo != null) {
            wrapper.eq("crn_no", crnNo);
        }
        if (!Cools.isEmpty(condition)) {
            wrapper.and(inner -> inner.like("crn_no", condition).or().like("memo", condition));
        }
        wrapper.orderBy(true, true, "crn_no");
        Page<BasCrnp> page = basCrnpService.page(new Page<>(curr, limit), wrapper);
        List<BasCrnpMonitorDto> records = new ArrayList<>();
        for (BasCrnp basCrnp : page.getRecords()) {
            records.add(toMonitorDto(basCrnp));
        }
        Map<String, Object> data = new HashMap<>();
        data.put("total", page.getTotal());
        data.put("records", records);
        data.put("lastSyncTime", crnUtils.getLastSyncTime());
        data.put("syncError", crnUtils.getLastSyncError());
        return R.ok(data);
    }
    @RequestMapping(value = "/basCrnp/add/auth", method = RequestMethod.POST)
    @ManagerAuth
    public R add(BasCrnp basCrnp) {
        if (basCrnp == null || basCrnp.getCrnNo() == null) {
            return R.error("堆垛机编号不能为空");
        }
        if (basCrnpService.count(new QueryWrapper<BasCrnp>().eq("crn_no", basCrnp.getCrnNo())) > 0) {
            return R.parse(BaseRes.REPEAT).add("堆垛机编号");
        }
        Date now = new Date();
        basCrnp.setStatus(basCrnp.getStatus() == null ? 1 : basCrnp.getStatus());
        basCrnp.setCreateBy(getUserId());
        basCrnp.setCreateTime(now);
        basCrnp.setUpdateBy(getUserId());
        basCrnp.setUpdateTime(now);
        basCrnpService.save(basCrnp);
        return R.ok();
    }
    @RequestMapping(value = "/basCrnp/update/auth", method = RequestMethod.POST)
    @ManagerAuth
    public R update(BasCrnp basCrnp) {
        if (basCrnp == null || basCrnp.getCrnNo() == null) {
            return R.error("堆垛机编号不能为空");
        }
        BasCrnp old = basCrnpService.getById(basCrnp.getCrnNo());
        if (old == null) {
            return R.error("堆垛机配置不存在");
        }
        old.setStatus(basCrnp.getStatus() == null ? old.getStatus() : basCrnp.getStatus());
        old.setMemo(basCrnp.getMemo());
        old.setUpdateBy(getUserId());
        old.setUpdateTime(new Date());
        basCrnpService.updateById(old);
        return R.ok();
    }
    @RequestMapping(value = "/basCrnp/delete/auth", method = RequestMethod.POST)
    @ManagerAuth
    public R delete(@RequestParam(value = "ids[]") Integer[] ids) {
        if (ids == null || ids.length == 0) {
            return R.error("请选择要删除的数据");
        }
        basCrnpService.removeByIds(Arrays.asList(ids));
        for (Integer id : ids) {
            crnUtils.crnMap.remove(id);
        }
        return R.ok();
    }
    @RequestMapping(value = "/basCrnp/check/column/auth", method = RequestMethod.POST)
    @ManagerAuth
    public R checkColumn(@RequestBody JSONObject param) {
        String key = param == null ? null : param.getString("key");
        Object val = param == null ? null : param.get("val");
        if (!"crnNo".equals(key) || Cools.isEmpty(val)) {
            return R.ok();
        }
        BasCrnp basCrnp = basCrnpService.getOne(new QueryWrapper<BasCrnp>().eq("crn_no", val));
        if (basCrnp != null) {
            return R.parse(BaseRes.REPEAT).add(getComment(BasCrnp.class, key));
        }
        return R.ok();
    }
    @RequestMapping(value = "/basCrnp/refresh/auth", method = RequestMethod.POST)
    @ManagerAuth
    public R refresh() {
        WcsCrnSyncResult result = wcsCrnSyncService.sync();
        if (!result.isSuccess()) {
            return R.error(result.getMessage());
        }
        return R.ok(result);
    }
    private BasCrnpMonitorDto toMonitorDto(BasCrnp basCrnp) {
        BasCrnpMonitorDto dto = new BasCrnpMonitorDto();
        dto.setCrnNo(basCrnp.getCrnNo());
        dto.setLocalStatus(basCrnp.getStatus());
        dto.setLocalStatusDesc(basCrnp.getStatus$());
        dto.setMemo(basCrnp.getMemo());
        WcsCrnDto runtime = crnUtils.crnMap.get(basCrnp.getCrnNo());
        if (runtime == null) {
            dto.setOnline(0);
            dto.setOnlineDesc("离线");
            dto.setMode(null);
            dto.setModeDesc("离线");
            dto.setDeviceStatus(null);
            dto.setDeviceStatusDesc("离线");
            dto.setTaskNo(null);
            dto.setAlarm(null);
            dto.setLastSyncTime(crnUtils.getLastSyncTime());
            dto.setSyncError(crnUtils.getLastSyncError());
            return dto;
        }
        dto.setOnline(runtime.getOnline());
        dto.setOnlineDesc(runtime.getOnline() != null && runtime.getOnline() == 1 ? "在线" : "离线");
        dto.setMode(runtime.getMode());
        dto.setModeDesc(runtime.getModeDesc());
        dto.setDeviceStatus(runtime.getStatus());
        dto.setDeviceStatusDesc(runtime.getStatusDesc());
        dto.setTaskNo(runtime.getTaskNo());
        dto.setAlarm(runtime.getAlarm());
        dto.setLastSyncTime(runtime.getLastSyncTime());
        dto.setSyncError(runtime.getSyncError());
        return dto;
    }
}
src/main/java/com/zy/asrs/entity/BasCrnp.java
New file
@@ -0,0 +1,78 @@
package com.zy.asrs.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.core.common.Cools;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
@Data
@TableName("asr_bas_crnp")
public class BasCrnp implements Serializable {
    private static final long serialVersionUID = 1L;
    @Schema(description = "堆垛机编号")
    @TableId(value = "crn_no", type = IdType.INPUT)
    @TableField("crn_no")
    private Integer crnNo;
    @Schema(description = "状态 1: 正常  0: 禁用")
    private Integer status;
    @Schema(description = "创建人员")
    @TableField("create_by")
    private Long createBy;
    @Schema(description = "创建时间")
    @TableField("create_time")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;
    @Schema(description = "修改人员")
    @TableField("update_by")
    private Long updateBy;
    @Schema(description = "修改时间")
    @TableField("update_time")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date updateTime;
    @Schema(description = "备注")
    private String memo;
    public String getStatus$() {
        if (this.status == null) {
            return null;
        }
        switch (this.status) {
            case 1:
                return "正常";
            case 0:
                return "禁用";
            default:
                return String.valueOf(this.status);
        }
    }
    public String getCreateTime$() {
        if (Cools.isEmpty(this.createTime)) {
            return "";
        }
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.createTime);
    }
    public String getUpdateTime$() {
        if (Cools.isEmpty(this.updateTime)) {
            return "";
        }
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.updateTime);
    }
}
src/main/java/com/zy/asrs/entity/dto/BasCrnpMonitorDto.java
New file
@@ -0,0 +1,35 @@
package com.zy.asrs.entity.dto;
import lombok.Data;
@Data
public class BasCrnpMonitorDto {
    private Integer crnNo;
    private Integer localStatus;
    private String localStatusDesc;
    private Integer online;
    private String onlineDesc;
    private Integer mode;
    private String modeDesc;
    private Integer deviceStatus;
    private String deviceStatusDesc;
    private Integer taskNo;
    private Integer alarm;
    private String lastSyncTime;
    private String syncError;
    private String memo;
}
src/main/java/com/zy/asrs/entity/dto/WcsCrnDto.java
New file
@@ -0,0 +1,27 @@
package com.zy.asrs.entity.dto;
import lombok.Data;
@Data
public class WcsCrnDto {
    private Integer crnNo;
    private Integer online;
    private Integer mode;
    private String modeDesc;
    private Integer status;
    private String statusDesc;
    private Integer taskNo;
    private Integer alarm;
    private String lastSyncTime;
    private String syncError;
}
src/main/java/com/zy/asrs/entity/dto/WcsCrnSyncResult.java
New file
@@ -0,0 +1,17 @@
package com.zy.asrs.entity.dto;
import lombok.Data;
@Data
public class WcsCrnSyncResult {
    private boolean success;
    private String message;
    private int requestCount;
    private int updateCount;
    private String lastSyncTime;
}
src/main/java/com/zy/asrs/enums/CrnModeType.java
New file
@@ -0,0 +1,30 @@
package com.zy.asrs.enums;
public enum CrnModeType {
    NONE(-1, "离线"),
    STOP(0, "维修"),
    HAND(1, "手动"),
    HALF_AUTO(2, "半自动"),
    AUTO(3, "自动");
    public final Integer id;
    public final String desc;
    CrnModeType(Integer id, String desc) {
        this.id = id;
        this.desc = desc;
    }
    public static CrnModeType get(Integer id) {
        if (id == null) {
            return NONE;
        }
        for (CrnModeType type : CrnModeType.values()) {
            if (type.id.equals(id)) {
                return type;
            }
        }
        return NONE;
    }
}
src/main/java/com/zy/asrs/enums/CrnStatusType.java
New file
@@ -0,0 +1,40 @@
package com.zy.asrs.enums;
public enum CrnStatusType {
    NONE(-1, "离线"),
    IDLE(0, "空闲"),
    FETCH_MOVING(1, "取货行走"),
    FETCH_WAITING(2, "取货等待"),
    FETCHING(3, "取货中"),
    PUT_MOVING(4, "放货走行"),
    PUT_WAITING(5, "放货等待"),
    PUTTING(6, "放货中"),
    ORIGIN_GO(7, "回原点"),
    ORIGIN_BACK(8, "回反原点"),
    MOVING(9, "走行中"),
    WAITING(10, "任务完成等待WCS确认"),
    PAUSE(11, "任务暂停"),
    SOS(99, "报警"),
    UNKNOW(100, "其他");
    public final Integer id;
    public final String desc;
    CrnStatusType(Integer id, String desc) {
        this.id = id;
        this.desc = desc;
    }
    public static CrnStatusType get(Integer id) {
        if (id == null) {
            return NONE;
        }
        for (CrnStatusType type : CrnStatusType.values()) {
            if (type.id.equals(id)) {
                return type;
            }
        }
        return UNKNOW;
    }
}
src/main/java/com/zy/asrs/mapper/BasCrnpMapper.java
New file
@@ -0,0 +1,11 @@
package com.zy.asrs.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.zy.asrs.entity.BasCrnp;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
@Mapper
@Repository
public interface BasCrnpMapper extends BaseMapper<BasCrnp> {
}
src/main/java/com/zy/asrs/service/BasCrnpService.java
New file
@@ -0,0 +1,7 @@
package com.zy.asrs.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.zy.asrs.entity.BasCrnp;
public interface BasCrnpService extends IService<BasCrnp> {
}
src/main/java/com/zy/asrs/service/WcsCrnSyncService.java
New file
@@ -0,0 +1,8 @@
package com.zy.asrs.service;
import com.zy.asrs.entity.dto.WcsCrnSyncResult;
public interface WcsCrnSyncService {
    WcsCrnSyncResult sync();
}
src/main/java/com/zy/asrs/service/impl/BasCrnpServiceImpl.java
New file
@@ -0,0 +1,11 @@
package com.zy.asrs.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zy.asrs.entity.BasCrnp;
import com.zy.asrs.mapper.BasCrnpMapper;
import com.zy.asrs.service.BasCrnpService;
import org.springframework.stereotype.Service;
@Service("basCrnpService")
public class BasCrnpServiceImpl extends ServiceImpl<BasCrnpMapper, BasCrnp> implements BasCrnpService {
}
src/main/java/com/zy/asrs/service/impl/WcsCrnSyncServiceImpl.java
New file
@@ -0,0 +1,207 @@
package com.zy.asrs.service.impl;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.core.common.Cools;
import com.zy.asrs.entity.BasCrnp;
import com.zy.asrs.entity.dto.WcsCrnDto;
import com.zy.asrs.entity.dto.WcsCrnSyncResult;
import com.zy.asrs.enums.CrnModeType;
import com.zy.asrs.enums.CrnStatusType;
import com.zy.asrs.service.BasCrnpService;
import com.zy.asrs.service.WcsCrnSyncService;
import com.zy.asrs.utils.CrnUtils;
import com.zy.common.utils.HttpHandler;
import com.zy.system.entity.Config;
import com.zy.system.service.ConfigService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service("wcsCrnSyncService")
public class WcsCrnSyncServiceImpl implements WcsCrnSyncService {
    private static final String WCS_DEVICE_STATUS_URL_CODE = "wcsDeviceStatusUrl";
    @Autowired
    private ConfigService configService;
    @Autowired
    private BasCrnpService basCrnpService;
    @Autowired
    private CrnUtils crnUtils;
    @Override
    public synchronized WcsCrnSyncResult sync() {
        WcsCrnSyncResult result = new WcsCrnSyncResult();
        String now = nowText();
        List<BasCrnp> basCrnps = basCrnpService.list(new QueryWrapper<BasCrnp>()
                .eq("status", 1)
                .orderBy(true, true, "crn_no"));
        Set<Integer> enabledCrnNos = new HashSet<>();
        for (BasCrnp basCrnp : basCrnps) {
            if (basCrnp != null && basCrnp.getCrnNo() != null) {
                enabledCrnNos.add(basCrnp.getCrnNo());
            }
        }
        crnUtils.crnMap.keySet().removeIf(crnNo -> !enabledCrnNos.contains(crnNo));
        result.setRequestCount(enabledCrnNos.size());
        if (enabledCrnNos.isEmpty()) {
            crnUtils.setLastSyncTime(now);
            crnUtils.setLastSyncError(null);
            result.setSuccess(true);
            result.setMessage("暂无启用的堆垛机配置");
            result.setLastSyncTime(now);
            return result;
        }
        Config config = configService.getOne(new QueryWrapper<Config>().eq("code", WCS_DEVICE_STATUS_URL_CODE));
        String deviceStatusUrl = config == null ? null : config.getValue();
        if (Cools.isEmpty(deviceStatusUrl)) {
            return markSyncError(enabledCrnNos, now, result, "未配置WCS堆垛机状态接口地址");
        }
        HashMap<String, Object> requestParam = new HashMap<>();
        requestParam.put("crnNos", new ArrayList<>(enabledCrnNos));
        String response;
        try {
            response = new HttpHandler.Builder()
                    .setUri(deviceStatusUrl)
                    .setHttps(deviceStatusUrl.startsWith("https://"))
                    .setJson(JSON.toJSONString(requestParam))
                    .setTimeout(30, TimeUnit.SECONDS)
                    .build()
                    .doPost();
        } catch (Exception e) {
            log.error("同步堆垛机状态失败", e);
            return markSyncError(enabledCrnNos, now, result, "调用WCS接口失败: " + e.getMessage());
        }
        if (Cools.isEmpty(response)) {
            return markSyncError(enabledCrnNos, now, result, "WCS接口无响应");
        }
        JSONObject responseObj;
        try {
            responseObj = JSON.parseObject(response);
        } catch (Exception e) {
            log.error("解析堆垛机状态响应失败: {}", response, e);
            return markSyncError(enabledCrnNos, now, result, "WCS接口响应解析失败");
        }
        if (responseObj == null) {
            return markSyncError(enabledCrnNos, now, result, "WCS接口响应为空");
        }
        if (responseObj.getInteger("code") == null || responseObj.getInteger("code") != 200) {
            return markSyncError(enabledCrnNos, now, result,
                    Cools.isEmpty(responseObj.getString("msg")) ? "WCS接口返回失败" : responseObj.getString("msg"));
        }
        JSONObject dataObj = responseObj.getJSONObject("data");
        JSONArray crnList = dataObj == null ? null : dataObj.getJSONArray("crnList");
        Set<Integer> returnedCrnNos = new HashSet<>();
        int updateCount = 0;
        if (crnList != null) {
            for (Object item : crnList) {
                if (!(item instanceof JSONObject)) {
                    continue;
                }
                JSONObject crnObj = (JSONObject) item;
                Integer crnNo = crnObj.getInteger("crnNo");
                if (crnNo == null || !enabledCrnNos.contains(crnNo)) {
                    continue;
                }
                returnedCrnNos.add(crnNo);
                crnUtils.crnMap.put(crnNo, buildOnlineDto(crnObj, now));
                updateCount++;
            }
        }
        for (Integer crnNo : enabledCrnNos) {
            if (returnedCrnNos.contains(crnNo)) {
                continue;
            }
            crnUtils.crnMap.put(crnNo, buildOfflineDto(crnNo, now, null));
        }
        crnUtils.setLastSyncTime(now);
        crnUtils.setLastSyncError(null);
        result.setSuccess(true);
        result.setMessage("同步成功");
        result.setUpdateCount(updateCount);
        result.setLastSyncTime(now);
        return result;
    }
    private WcsCrnSyncResult markSyncError(Set<Integer> configuredCrnNos, String now, WcsCrnSyncResult result, String error) {
        crnUtils.setLastSyncTime(now);
        crnUtils.setLastSyncError(error);
        for (Integer crnNo : configuredCrnNos) {
            WcsCrnDto current = crnUtils.crnMap.get(crnNo);
            if (current == null) {
                current = buildOfflineDto(crnNo, now, error);
            } else {
                current.setLastSyncTime(now);
                current.setSyncError(error);
            }
            crnUtils.crnMap.put(crnNo, current);
        }
        result.setSuccess(false);
        result.setMessage(error);
        result.setLastSyncTime(now);
        return result;
    }
    private WcsCrnDto buildOnlineDto(JSONObject crnObj, String now) {
        WcsCrnDto dto = new WcsCrnDto();
        Integer mode = crnObj.getInteger("mode");
        Integer status = crnObj.getInteger("status");
        dto.setCrnNo(crnObj.getInteger("crnNo"));
        dto.setOnline(1);
        dto.setMode(mode);
        dto.setModeDesc(CrnModeType.get(mode).desc);
        dto.setStatus(status);
        dto.setStatusDesc(CrnStatusType.get(status).desc);
        dto.setTaskNo(crnObj.getInteger("taskNo"));
        dto.setAlarm(crnObj.getInteger("alarm"));
        dto.setLastSyncTime(now);
        dto.setSyncError(null);
        return dto;
    }
    private WcsCrnDto buildOfflineDto(Integer crnNo, String now, String syncError) {
        WcsCrnDto dto = new WcsCrnDto();
        dto.setCrnNo(crnNo);
        dto.setOnline(0);
        dto.setMode(null);
        dto.setModeDesc("离线");
        dto.setStatus(null);
        dto.setStatusDesc("离线");
        dto.setTaskNo(null);
        dto.setAlarm(null);
        dto.setLastSyncTime(now);
        dto.setSyncError(syncError);
        return dto;
    }
    private String nowText() {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
    }
}
src/main/java/com/zy/asrs/timer/WcsCrnTimer.java
New file
@@ -0,0 +1,26 @@
package com.zy.asrs.timer;
import com.zy.asrs.service.WcsCrnSyncService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Component
public class WcsCrnTimer {
    private static final Logger log = LoggerFactory.getLogger(WcsCrnTimer.class);
    @Autowired
    private WcsCrnSyncService wcsCrnSyncService;
    @Scheduled(cron = "0/3 * * * * ? ")
    public void execute() {
        try {
            wcsCrnSyncService.sync();
        } catch (Exception e) {
            log.error("堆垛机状态定时同步失败", e);
        }
    }
}
src/main/java/com/zy/asrs/utils/CrnUtils.java
New file
@@ -0,0 +1,33 @@
package com.zy.asrs.utils;
import com.zy.asrs.entity.dto.WcsCrnDto;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class CrnUtils {
    public Map<Integer, WcsCrnDto> crnMap = new ConcurrentHashMap<>();
    private volatile String lastSyncTime;
    private volatile String lastSyncError;
    public String getLastSyncTime() {
        return lastSyncTime;
    }
    public void setLastSyncTime(String lastSyncTime) {
        this.lastSyncTime = lastSyncTime;
    }
    public String getLastSyncError() {
        return lastSyncError;
    }
    public void setLastSyncError(String lastSyncError) {
        this.lastSyncError = lastSyncError;
    }
}
src/main/resources/sql/202604030001_add_bas_crnp.sql
New file
@@ -0,0 +1,38 @@
CREATE TABLE IF NOT EXISTS `asr_bas_crnp` (
  `crn_no` int NOT NULL COMMENT '堆垛机编号',
  `status` int DEFAULT 1 COMMENT '状态 1:正常 0:禁用',
  `create_by` bigint DEFAULT NULL COMMENT '创建人',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` bigint DEFAULT NULL COMMENT '修改人',
  `update_time` datetime DEFAULT NULL COMMENT '修改时间',
  `memo` varchar(255) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`crn_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='堆垛机配置';
INSERT INTO `sys_config` (`name`, `code`, `value`, `type`, `status`, `select_type`)
SELECT 'WCS堆垛机状态接口', 'wcsDeviceStatusUrl', '', 1, 1, NULL
WHERE NOT EXISTS (
    SELECT 1 FROM `sys_config` WHERE `code` = 'wcsDeviceStatusUrl'
);
INSERT INTO `sys_resource` (`code`, `name`, `resource_id`, `level`, `sort`, `status`)
SELECT 'views/basCrnp/basCrnp.html', '堆垛机状态监控', `resource_id`, `level`, `sort` + 1, `status`
FROM `sys_resource`
WHERE `code` = 'views/basStation/basStation.html'
  AND NOT EXISTS (
      SELECT 1 FROM `sys_resource` WHERE `code` = 'views/basCrnp/basCrnp.html'
  )
LIMIT 1;
INSERT INTO `sys_role_resource` (`role_id`, `resource_id`)
SELECT rr.`role_id`, new_res.`id`
FROM `sys_role_resource` rr
JOIN `sys_resource` old_res ON old_res.`id` = rr.`resource_id`
JOIN `sys_resource` new_res ON new_res.`code` = 'views/basCrnp/basCrnp.html'
WHERE old_res.`code` = 'views/basStation/basStation.html'
  AND NOT EXISTS (
      SELECT 1
      FROM `sys_role_resource` srr
      WHERE srr.`role_id` = rr.`role_id`
        AND srr.`resource_id` = new_res.`id`
  );
src/main/webapp/static/js/basCrnp/basCrnp.js
New file
@@ -0,0 +1,275 @@
var pageCurr = 1;
var tableIns;
var autoRefreshTimer = null;
var editDialogIndex = null;
var reloading = false;
layui.config({
    base: baseUrl + "/static/layui/lay/modules/"
}).use(['table', 'form', 'admin'], function () {
    var table = layui.table;
    var $ = layui.jquery;
    var layer = layui.layer;
    var form = layui.form;
    var admin = layui.admin;
    tableIns = table.render({
        elem: '#basCrnp',
        headers: {token: localStorage.getItem('token')},
        url: baseUrl + '/basCrnp/list/auth',
        page: true,
        limit: 15,
        limits: [15, 30, 50, 100, 200, 500],
        toolbar: '#toolbar',
        cellMinWidth: 80,
        height: 'full-160',
        cols: [[
            {type: 'checkbox'},
            {field: 'crnNo', align: 'center', title: '堆垛机号'},
            {field: 'localStatusDesc', align: 'center', title: '本地状态'},
            {field: 'onlineDesc', align: 'center', title: '在线状态'},
            {field: 'modeDesc', align: 'center', title: '模式'},
            {field: 'deviceStatusDesc', align: 'center', title: '设备状态'},
            {
                field: 'taskNo',
                align: 'center',
                title: '任务号',
                templet: function (d) {
                    return d.taskNo == null ? '-' : d.taskNo;
                }
            },
            {
                field: 'alarm',
                align: 'center',
                title: '报警码',
                templet: function (d) {
                    return d.alarm == null ? '-' : d.alarm;
                }
            },
            {
                field: 'lastSyncTime',
                align: 'center',
                title: '最后同步时间',
                templet: function (d) {
                    return isEmpty(d.lastSyncTime) ? '-' : d.lastSyncTime;
                }
            },
            {field: 'memo', align: 'center', title: '备注'},
            {fixed: 'right', title: '操作', align: 'center', toolbar: '#operate', width: 120}
        ]],
        request: {
            pageName: 'curr',
            pageSize: 'limit'
        },
        parseData: function (res) {
            return {
                code: res.code,
                msg: res.msg,
                count: res.data.total,
                data: res.data.records,
                syncError: res.data.syncError,
                lastSyncTime: res.data.lastSyncTime
            };
        },
        response: {
            statusCode: 200
        },
        done: function (res, curr) {
            reloading = false;
            if (res.code === 403) {
                top.location.href = baseUrl + "/";
                return;
            }
            pageCurr = curr;
            renderSyncStatus(res.lastSyncTime, res.syncError);
            limit();
        }
    });
    table.on('toolbar(basCrnp)', function (obj) {
        var checkStatus = table.checkStatus(obj.config.id).data;
        switch (obj.event) {
            case 'addData':
                showEditModel();
                break;
            case 'deleteData':
                if (checkStatus.length === 0) {
                    layer.msg('请选择要删除的数据', {icon: 2});
                    return;
                }
                del(checkStatus.map(function (d) {
                    return d.crnNo;
                }));
                break;
            case 'refreshData':
                refreshNow();
                break;
        }
    });
    table.on('tool(basCrnp)', function (obj) {
        var data = obj.data;
        switch (obj.event) {
            case 'edit':
                showEditModel(data);
                break;
            case 'del':
                del([data.crnNo]);
                break;
        }
    });
    form.on('submit(search)', function () {
        pageCurr = 1;
        tableReload();
        return false;
    });
    form.on('submit(reset)', function () {
        pageCurr = 1;
        clearFormVal($('#search-box'));
        tableReload();
        return false;
    });
    function showEditModel(mData) {
        editDialogIndex = admin.open({
            type: 1,
            area: '600px',
            title: (mData ? '修改' : '添加') + '堆垛机配置',
            content: $('#editDialog').html(),
            success: function (layero, dIndex) {
                form.val('detail', {
                    crnNo: mData ? mData.crnNo : '',
                    status: mData ? mData.localStatus : 1,
                    memo: mData ? mData.memo : ''
                });
                if (mData) {
                    $(layero).find('#crnNo').attr('readonly', true).addClass('layui-disabled');
                }
                form.on('submit(editSubmit)', function (data) {
                    var loadIndex = layer.load(2);
                    $.ajax({
                        url: baseUrl + "/basCrnp/" + (mData ? 'update' : 'add') + "/auth",
                        headers: {'token': localStorage.getItem('token')},
                        data: data.field,
                        method: 'POST',
                        success: function (res) {
                            layer.close(loadIndex);
                            if (res.code === 200) {
                                layer.close(dIndex);
                                editDialogIndex = null;
                                layer.msg('保存成功', {icon: 1});
                                tableReload();
                            } else if (res.code === 403) {
                                top.location.href = baseUrl + "/";
                            } else {
                                layer.msg(res.msg, {icon: 2});
                            }
                        }
                    });
                    return false;
                });
                $(layero).children('.layui-layer-content').css('overflow', 'visible');
                layui.form.render('select');
            },
            end: function () {
                editDialogIndex = null;
            }
        });
    }
    function del(ids) {
        layer.confirm('确定要删除选中数据吗?', {
            skin: 'layui-layer-admin',
            shade: .1
        }, function (i) {
            layer.close(i);
            var loadIndex = layer.load(2);
            $.ajax({
                url: baseUrl + "/basCrnp/delete/auth",
                headers: {'token': localStorage.getItem('token')},
                data: {ids: ids},
                method: 'POST',
                success: function (res) {
                    layer.close(loadIndex);
                    if (res.code === 200) {
                        layer.msg('删除成功', {icon: 1});
                        pageCurr = 1;
                        tableReload();
                    } else if (res.code === 403) {
                        top.location.href = baseUrl + "/";
                    } else {
                        layer.msg(res.msg, {icon: 2});
                    }
                }
            });
        });
    }
    function refreshNow() {
        var loadIndex = layer.load(2);
        $.ajax({
            url: baseUrl + '/basCrnp/refresh/auth',
            headers: {'token': localStorage.getItem('token')},
            method: 'POST',
            success: function (res) {
                layer.close(loadIndex);
                if (res.code === 200) {
                    layer.msg('同步成功', {icon: 1});
                    tableReload();
                } else if (res.code === 403) {
                    top.location.href = baseUrl + "/";
                } else {
                    layer.msg(res.msg, {icon: 2});
                }
            },
            error: function () {
                layer.close(loadIndex);
                layer.msg('同步请求异常', {icon: 2});
            }
        });
    }
    function renderSyncStatus(lastSyncTime, syncError) {
        var box = $('#syncStatusBox');
        if (isEmpty(lastSyncTime) && isEmpty(syncError)) {
            box.css('color', '#999').text('尚未同步');
            return;
        }
        if (!isEmpty(syncError)) {
            box.css('color', '#FF5722').text('数据非最新,最近同步时间:' + (lastSyncTime || '-') + ',原因:' + syncError);
            return;
        }
        box.css('color', '#16b777').text('最近同步成功时间:' + (lastSyncTime || '-'));
    }
    autoRefreshTimer = setInterval(function () {
        if (editDialogIndex != null || reloading) {
            return;
        }
        tableReload();
    }, 3000);
    $(window).on('beforeunload', function () {
        if (autoRefreshTimer != null) {
            clearInterval(autoRefreshTimer);
        }
    });
});
function tableReload() {
    if (reloading) {
        return;
    }
    reloading = true;
    var searchData = {};
    $.each($('#search-box [name]').serializeArray(), function () {
        searchData[this.name] = this.value;
    });
    tableIns.reload({
        where: searchData,
        page: {curr: pageCurr}
    });
}
src/main/webapp/views/basCrnp/basCrnp.html
New file
@@ -0,0 +1,102 @@
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>堆垛机状态监控</title>
    <meta name="renderer" content="webkit">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <link rel="stylesheet" href="../../static/layui/css/layui.css" media="all">
    <link rel="stylesheet" href="../../static/css/admin.css?v=318" media="all">
    <link rel="stylesheet" href="../../static/css/cool.css" media="all">
</head>
<body>
<div class="layui-fluid">
    <div class="layui-card">
        <div class="layui-card-body">
            <div class="layui-form toolbar" id="search-box">
                <div class="layui-form-item">
                    <div class="layui-inline">
                        <div class="layui-input-inline">
                            <input class="layui-input" type="text" name="crnNo" placeholder="堆垛机号" autocomplete="off">
                        </div>
                    </div>
                    <div class="layui-inline">
                        <div class="layui-input-inline">
                            <input class="layui-input" type="text" name="condition" placeholder="关键字" autocomplete="off">
                        </div>
                    </div>
                    <div class="layui-inline">&emsp;
                        <button class="layui-btn icon-btn" lay-filter="search" lay-submit>
                            <i class="layui-icon">&#xe615;</i>搜索
                        </button>
                        <button class="layui-btn icon-btn" lay-filter="reset" lay-submit>
                            <i class="layui-icon">&#xe666;</i>重置
                        </button>
                    </div>
                </div>
            </div>
            <div id="syncStatusBox" class="layui-word-aux" style="margin-bottom: 10px;">尚未同步</div>
            <table class="layui-hide" id="basCrnp" lay-filter="basCrnp"></table>
        </div>
    </div>
</div>
<script type="text/html" id="toolbar">
    <div class="layui-btn-container">
        <button class="layui-btn layui-btn-sm" id="btn-add" lay-event="addData">新增</button>
        <button class="layui-btn layui-btn-sm layui-btn-danger" id="btn-delete" lay-event="deleteData">删除</button>
        <button class="layui-btn layui-btn-sm layui-btn-normal" id="btn-refresh" lay-event="refreshData">立即同步</button>
    </div>
</script>
<script type="text/html" id="operate">
    <a class="layui-btn layui-btn-primary layui-btn-xs btn-edit" lay-event="edit">修改</a>
    <a class="layui-btn layui-btn-danger layui-btn-xs btn-edit" lay-event="del">删除</a>
</script>
<script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script>
<script type="text/javascript" src="../../static/layui/layui.js" charset="utf-8"></script>
<script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script>
<script type="text/javascript" src="../../static/js/cool.js" charset="utf-8"></script>
<script type="text/javascript" src="../../static/js/basCrnp/basCrnp.js" charset="utf-8"></script>
</body>
<script type="text/html" id="editDialog">
    <form id="detail" lay-filter="detail" class="layui-form admin-form model-form">
        <div class="layui-row">
            <div class="layui-col-md12">
                <div class="layui-form-item">
                    <label class="layui-form-label">堆垛机号: </label>
                    <div class="layui-input-block">
                        <input id="crnNo" class="layui-input" name="crnNo" placeholder="请输入堆垛机号" onkeyup="check(this.id, 'basCrnp')">
                    </div>
                </div>
                <div class="layui-form-item">
                    <label class="layui-form-label">本地状态: </label>
                    <div class="layui-input-block">
                        <select name="status">
                            <option value="1">正常</option>
                            <option value="0">禁用</option>
                        </select>
                    </div>
                </div>
                <div class="layui-form-item">
                    <label class="layui-form-label">备注: </label>
                    <div class="layui-input-block">
                        <input class="layui-input" name="memo" placeholder="请输入备注">
                    </div>
                </div>
            </div>
        </div>
        <hr class="layui-bg-gray">
        <div class="layui-form-item text-right">
            <button class="layui-btn" lay-filter="editSubmit" lay-submit>保存</button>
            <button class="layui-btn layui-btn-primary" type="button" ew-event="closeDialog">取消</button>
        </div>
    </form>
</script>
</html>