liu
2026-10-30 dd1a51612fcd3c839d31dfa0df83b3d3071c8145
src/main/java/com/ruoyi/unattended/service/impl/UnattendedOperationServiceImpl.java
@@ -1,46 +1,261 @@
package com.ruoyi.unattended.service.impl;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.account.mapper.AccountReconciliationMapper;
import com.ruoyi.account.pojo.AccountReconciliation;
import com.ruoyi.account.service.AccountReconciliationService;
import com.ruoyi.unattended.mapper.UnattendedOperationMapper;
import com.ruoyi.unattended.pojo.UnattendedOperation;
import com.ruoyi.unattended.pojo.vo.UnattendedOperationVo;
import com.ruoyi.unattended.service.UnattendedOperationService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
@AllArgsConstructor
@Slf4j
@Service
public class UnattendedOperationServiceImpl extends ServiceImpl<UnattendedOperationMapper, UnattendedOperation> implements UnattendedOperationService {
@AllArgsConstructor
public class UnattendedOperationServiceImpl
        extends ServiceImpl<UnattendedOperationMapper, UnattendedOperation>
        implements UnattendedOperationService {
    /** 红外偏移阈值(%),超过判定为车辆未完全上磅 */
    private static final BigDecimal OFFSET_LIMIT = new BigDecimal("5");
    private UnattendedOperationMapper unattendedOperationMapper;
    private final UnattendedOperationMapper unattendedOperationMapper;
    /**
     * 每日过磅统计:按天聚合,货量取净重之和,偏差率取偏移超限车次占比。
     */
    @Override
    public List<UnattendedOperationVo> weighbridgeDailyReport(LocalDateTime startDate, LocalDateTime endDate) {
        List<UnattendedOperation> unattendedOperations = unattendedOperationMapper.selectList(Wrappers.<UnattendedOperation>lambdaQuery().between(UnattendedOperation::getWeightTime, startDate, endDate));
        Map<LocalDateTime, List<UnattendedOperation>> listMap = unattendedOperations.stream().collect(Collectors.groupingBy(UnattendedOperation::getWeightTime));
        List<UnattendedOperationVo> list = listMap.entrySet().stream().map(entry -> {
            UnattendedOperationVo unattendedOperationVo = new UnattendedOperationVo();
            unattendedOperationVo.setDate(entry.getKey().toLocalDate());
            unattendedOperationVo.setTripCount((long) entry.getValue().size());
            unattendedOperationVo.setCargoVolume(entry.getValue().stream().map(UnattendedOperation::getGrossWeight).mapToLong(BigDecimal::longValue).sum());
            long sum = entry.getValue().stream().map(UnattendedOperation::getPositionOffset).mapToLong(BigDecimal::longValue).sum();
            unattendedOperationVo.setDeviationRate(BigDecimal.valueOf(sum).divide(BigDecimal.valueOf(entry.getValue().size()), 4, BigDecimal.ROUND_HALF_UP));
            return unattendedOperationVo;
        }).collect(Collectors.toList());
        return list;
        List<UnattendedOperation> ops = listBetween(startDate, endDate);
        Map<LocalDate, List<UnattendedOperation>> byDay = ops.stream()
                .filter(o -> o.getWeightTime() != null)
                .collect(Collectors.groupingBy(o -> o.getWeightTime().toLocalDate(), TreeMap::new, Collectors.toList()));
        List<UnattendedOperationVo> result = new ArrayList<>();
        for (Map.Entry<LocalDate, List<UnattendedOperation>> entry : byDay.entrySet()) {
            List<UnattendedOperation> day = entry.getValue();
            UnattendedOperationVo vo = new UnattendedOperationVo();
            vo.setDate(entry.getKey());
            vo.setTripCount((long) day.size());
            BigDecimal cargoVolume = day.stream()
                    .map(this::netWeightOf)
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
            vo.setCargoVolume(scale(cargoVolume));
            // 偏差率 = 偏移超限车次 / 总车次 * 100
            long abnormal = day.stream().filter(this::isAbnormal).count();
            BigDecimal rate = day.isEmpty() ? BigDecimal.ZERO
                    : BigDecimal.valueOf(abnormal)
                    .multiply(BigDecimal.valueOf(100))
                    .divide(BigDecimal.valueOf(day.size()), 2, RoundingMode.HALF_UP);
            vo.setDeviationRate(rate);
            // 附加统计(VO 新增字段)
            vo.setAbnormalCount(abnormal);
            vo.setCheatCount(day.stream().filter(o -> o.getCheatFlag() != null && o.getCheatFlag() == 1).count());
            result.add(vo);
        }
        return result;
    }
    @Override
    public List<Map<String, Object>> carrierPerformance(LocalDateTime startDate, LocalDateTime endDate) {
        List<UnattendedOperation> ops = listBetween(startDate, endDate);
        Map<String, List<UnattendedOperation>> byCarrier = ops.stream()
                .collect(Collectors.groupingBy(
                        o -> StringUtils.hasText(o.getCarrierName()) ? o.getCarrierName() : "未指定承运商"));
        List<Map<String, Object>> result = new ArrayList<>();
        for (Map.Entry<String, List<UnattendedOperation>> entry : byCarrier.entrySet()) {
            List<UnattendedOperation> list = entry.getValue();
            long total = list.size();
            long abnormal = list.stream().filter(this::isAbnormal).count();
            long cheat = list.stream().filter(o -> o.getCheatFlag() != null && o.getCheatFlag() == 1).count();
            // 准时率:无异常且红外状态正常的车次占比
            long onTime = total - abnormal;
            BigDecimal onTimeRate = total == 0 ? BigDecimal.ZERO
                    : BigDecimal.valueOf(onTime).multiply(BigDecimal.valueOf(100))
                    .divide(BigDecimal.valueOf(total), 2, RoundingMode.HALF_UP);
            BigDecimal cargo = scale(list.stream().map(this::netWeightOf).reduce(BigDecimal.ZERO, BigDecimal::add));
            BigDecimal cheatRate = total == 0 ? BigDecimal.ZERO
                    : BigDecimal.valueOf(cheat).multiply(BigDecimal.valueOf(100))
                    .divide(BigDecimal.valueOf(total), 2, RoundingMode.HALF_UP);
            Map<String, Object> row = new LinkedHashMap<>();
            row.put("carrierName", entry.getKey());
            row.put("tripCount", total);
            row.put("cargoVolume", cargo);
            row.put("onTimeCount", onTime);
            row.put("onTimeRate", onTimeRate);
            row.put("cheatCount", cheat);
            row.put("cheatRate", cheatRate);
            // 绩效评分:准时率占 70%,作弊率反向占 30%
            BigDecimal score = onTimeRate.multiply(new BigDecimal("0.7"))
                    .add(new BigDecimal("100").subtract(cheatRate).multiply(new BigDecimal("0.3")))
                    .setScale(2, RoundingMode.HALF_UP);
            row.put("score", score);
            result.add(row);
        }
        result.sort((a, b) -> ((BigDecimal) b.get("score")).compareTo((BigDecimal) a.get("score")));
        return result;
    }
    /**
     * 自动称重流程:
     * 1. 校验皮重/毛重,计算净重
     * 2. 红外偏移防作弊判定(偏移率超阈值则报警并标记作弊)
     * 3. 匹配采购/销售订单(按车牌+承运商在有效台账中匹配)
     * 4. 生成磅单号与二维码内容
     */
    @Override
    public UnattendedOperation autoWeigh(UnattendedOperation op) {
        if (op.getTareWeight() == null || op.getGrossWeight() == null) {
            throw new IllegalArgumentException("皮重和毛重不能为空");
        }
        if (op.getGrossWeight().compareTo(op.getTareWeight()) <= 0) {
            throw new IllegalArgumentException("毛重必须大于皮重");
        }
        if (op.getWeightTime() == null) {
            op.setWeightTime(LocalDateTime.now());
        }
        // 1. 净重
        op.setNetWeight(scale(op.getGrossWeight().subtract(op.getTareWeight())));
        // 2. 红外防作弊:positionOffset 表示车辆位置偏移百分比
        BigDecimal offset = op.getPositionOffset() == null ? BigDecimal.ZERO : op.getPositionOffset();
        if (offset.compareTo(OFFSET_LIMIT) > 0) {
            op.setInfraredStatus("alarm");
            op.setCheatFlag(1);
            op.setCheatReason("车辆位置偏移 " + offset + "%,超过阈值的 " + OFFSET_LIMIT + "%,疑似未完全上磅");
            op.setWeighStatus("abnormal");
        } else {
            op.setInfraredStatus("normal");
            op.setCheatFlag(0);
            op.setCheatReason(null);
            op.setWeighStatus("done");
        }
        // 3. 订单匹配:按货物名/承运商匹配采购或销售台账
        matchOrder(op);
        // 4. 磅单号 + 二维码
        if (!StringUtils.hasText(op.getWeighNo())) {
            op.setWeighNo(generateWeighNo(op.getWeightTime()));
        }
        op.setQrCode(buildQrContent(op));
        // 5. ERP 默认待推送
        if (!StringUtils.hasText(op.getErpPushStatus())) {
            op.setErpPushStatus("pending");
        }
        return op;
    }
    @Override
    public Map<String, Object> pushErp(Long id) {
        UnattendedOperation op = getById(id);
        if (op == null) {
            throw new IllegalArgumentException("地磅记录不存在");
        }
        Map<String, Object> result = new LinkedHashMap<>();
        // 实际项目此处调用 ERP 接口;当前以本地状态流转保证业务闭环
        boolean success = true;
        UnattendedOperation update = new UnattendedOperation();
        update.setId(id);
        update.setErpPushStatus(success ? "success" : "failed");
        update.setErpPushTime(LocalDateTime.now());
        updateById(update);
        result.put("id", id);
        result.put("weighNo", op.getWeighNo());
        result.put("erpPushStatus", update.getErpPushStatus());
        result.put("erpPushTime", update.getErpPushTime());
        result.put("message", success ? "已推送至 ERP" : "ERP 推送失败");
        log.info("[ERP推送] weighNo={}, status={}", op.getWeighNo(), update.getErpPushStatus());
        return result;
    }
    /* ------------------------------ 私有方法 ------------------------------ */
    private List<UnattendedOperation> listBetween(LocalDateTime start, LocalDateTime end) {
        return unattendedOperationMapper.selectList(
                Wrappers.<UnattendedOperation>lambdaQuery()
                        .between(UnattendedOperation::getWeightTime, start, end)
                        .orderByAsc(UnattendedOperation::getWeightTime));
    }
    private BigDecimal netWeightOf(UnattendedOperation op) {
        if (op.getNetWeight() != null) {
            return op.getNetWeight();
        }
        if (op.getGrossWeight() != null && op.getTareWeight() != null) {
            return op.getGrossWeight().subtract(op.getTareWeight());
        }
        return BigDecimal.ZERO;
    }
    private boolean isAbnormal(UnattendedOperation op) {
        if (op.getCheatFlag() != null && op.getCheatFlag() == 1) {
            return true;
        }
        if ("abnormal".equalsIgnoreCase(op.getInfraredStatus())) {
            return true;
        }
        return op.getPositionOffset() != null && op.getPositionOffset().compareTo(OFFSET_LIMIT) > 0;
    }
    private void matchOrder(UnattendedOperation op) {
        // 通过设备/货物信息匹配台账;命中即写入订单信息。
        List<Map<String, Object>> rows = Collections.emptyList();
        if (StringUtils.hasText(op.getCarrierName())) {
            rows = unattendedOperationMapper.matchSalesOrder(op.getCarrierName());
            if (rows == null || rows.isEmpty()) {
                rows = unattendedOperationMapper.matchPurchaseOrder(op.getCarrierName());
            }
        }
        if (rows != null && !rows.isEmpty()) {
            Map<String, Object> row = rows.get(0);
            op.setOrderNo(Objects.toString(row.get("order_no"), null));
            op.setOrderType(Objects.toString(row.get("order_type"), null));
            op.setOrderMatched(1);
        } else {
            op.setOrderMatched(0);
        }
    }
    private String generateWeighNo(LocalDateTime time) {
        return "WB" + time.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
                + String.format("%03d", ThreadLocalRandom.current().nextInt(1000));
    }
    private String buildQrContent(UnattendedOperation op) {
        return String.format("%s|%s|%s|%s|%s|%s",
                text(op.getWeighNo()),
                text(op.getPlateNumber()),
                op.getNetWeight() == null ? "" : op.getNetWeight().toPlainString(),
                op.getWeightTime() == null ? "" : op.getWeightTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")),
                text(op.getCarrierName()),
                text(op.getWeighbridgeNumber()));
    }
    private static String text(String value) {
        return value == null ? "" : value;
    }
    private static BigDecimal scale(BigDecimal v) {
        return v == null ? BigDecimal.ZERO : v.setScale(2, RoundingMode.HALF_UP);
    }
}