吴羡
2026-10-22 202157cf81af12aaa9cf1b8e860a02dbb55bfe2a
feat: 完善直绑模板设备初始化与审批人权限校验

- 直绑模板订单打开时按标准库同步未人工调整的设备,人工修改后不再覆盖
- 未下发订单查看原始记录时不再提前创建任务状态
- 指定审核人、批准人时校验其具备「业务管理 → 报告编制」访问权限
- 更新标准模板未传价格时保留原价格
- 补充数据库增量脚本(标准库节点、资质颁发时间、满意度确认、培训讲师、设备初始化标记)
已修改5个文件
216 ■■■■■ 文件已修改
basic-server/src/main/java/com/ruoyi/basic/service/impl/StandardTemplateServiceImpl.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsOrderPlanServiceImpl.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsOrderServiceImpl.java 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsReportServiceImpl.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/lims_delta.sql 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
basic-server/src/main/java/com/ruoyi/basic/service/impl/StandardTemplateServiceImpl.java
@@ -63,9 +63,6 @@
    @Override
    public int upStandardTemplate(StandardTemplate standardTemplate) {
        if (standardTemplate.getPrice() == null) {
            standardTemplate.setPrice(java.math.BigDecimal.ZERO);
        }
        if (StringUtils.isNotBlank(standardTemplate.getThing())) {
            standardTemplate.setThing(GZipUtil.compress(standardTemplate.getThing()));
        } else {
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsOrderPlanServiceImpl.java
@@ -1659,7 +1659,7 @@
            refreshed.setRemark(standardTemplate == null ? oldSnapshot.getRemark() : standardTemplate.getRemark());
            refreshed.setThing(standardTemplate == null ? oldSnapshot.getThing() : standardTemplate.getThing());
            refreshed.setRecordValues(templateRecordValuesMap.get(templateId));
            refreshed.setDeviceDefaultsInitialized(oldSnapshot == null ? 1
            refreshed.setDeviceDefaultsInitialized(oldSnapshot == null ? 0
                    : oldSnapshot.getDeviceDefaultsInitialized());
            refreshed.setTemperature(templateTemperatureMap.get(templateId));
            refreshed.setHumidity(templateHumidityMap.get(templateId));
@@ -1947,6 +1947,11 @@
     * 兼容修复上线前已经下发、但因没有 ins_product 而缺少任务状态的直绑模板订单。
     */
    private void ensureDirectTemplateOrderState(Integer orderId) {
        InsOrder order = insOrderMapper.selectById(orderId);
        // 未下发的单还没进入检验任务,查看原始记录不能提前创建任务状态。
        if (order == null || order.getSendTime() == null) {
            return;
        }
        if (!isDirectTemplateOrder(orderId)) {
            return;
        }
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsOrderServiceImpl.java
@@ -55,6 +55,7 @@
import com.ruoyi.inspect.vo.TemplateConditionVo;
import com.ruoyi.system.mapper.UserMapper;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.context.ApplicationEventPublisher;
@@ -80,6 +81,7 @@
 * @description 针对表【ins_order(检验下单)】的数据库操作Service实现
 * @createDate 2024-03-12 16:17:55
 */
@Slf4j
@Service
@AllArgsConstructor
public class InsOrderServiceImpl extends ServiceImpl<InsOrderMapper, InsOrder>
@@ -1067,7 +1069,11 @@
    @Transactional(rollbackFor = Exception.class)
    public Map<String, Object> getInsOrderAndSample(Integer id, String laboratory) {
        Map<String, Object> map = new HashMap<>();
        initializeTemplateDevicesOnFirstOpen(id);
        try {
            initializeTemplateDevicesOnFirstOpen(id, laboratory);
        } catch (RuntimeException e) {
            log.warn("检验任务默认设备同步失败,orderId={}", id, e);
        }
//        InsOrder insOrder = insOrderMapper.selectById(id);
        IfsOrderVO ifsOrderVO = insOrderMapper.selectOrderInfoById(id);
        Map<Integer, List<Integer>> templateDeviceIds = insOrderTemplateDeviceMapper.selectList(
@@ -1110,18 +1116,34 @@
        return map;
    }
    /** 首次打开检验任务时,将仍为空的模板设备从对应标准补入订单。 */
    private void initializeTemplateDevicesOnFirstOpen(Integer orderId) {
    /**
     * 打开检验任务时,把尚未被人工改动过的模板设备同步为标准库当前绑定的设备;
     * 检验员手动增删过的模板以人工为准,不再自动填充。
     */
    private void initializeTemplateDevicesOnFirstOpen(Integer orderId, String laboratory) {
        InsOrder order = insOrderMapper.selectById(orderId);
        if (order == null || !DIRECT_TEMPLATE_LABORATORY.equals(order.getLaboratory())
                || !Objects.equals(order.getTypeSource(), -1)
        if (order == null || !Objects.equals(order.getTypeSource(), -1)
                || !Objects.equals(order.getState(), 1)
                || "QUALITY_MONITOR".equals(order.getTaskSource())) {
            return;
        }
        if (StringUtils.isNotBlank(laboratory)) {
            InsOrderState orderState = insOrderStateService.getOne(Wrappers.<InsOrderState>lambdaQuery()
                    .eq(InsOrderState::getInsOrderId, orderId)
                    .eq(InsOrderState::getLaboratory, laboratory)
                    .last("limit 1"));
            // 仅待检、在检阶段同步;进入复核等后续状态后以保存下来的设备为准。
            if (orderState != null && orderState.getInsState() != null
                    && orderState.getInsState() != 0 && orderState.getInsState() != 1) {
                return;
            }
        }
        List<InsOrderStandardTemplate> snapshots = insOrderStandardTemplateMapper.selectList(
                Wrappers.<InsOrderStandardTemplate>lambdaQuery()
                        .eq(InsOrderStandardTemplate::getInsOrderId, orderId)
                        .select(InsOrderStandardTemplate::getId,
                                InsOrderStandardTemplate::getTemplateId,
                                InsOrderStandardTemplate::getDeviceDefaultsInitialized)
                        .orderByAsc(InsOrderStandardTemplate::getSort, InsOrderStandardTemplate::getId)
                        .last("FOR UPDATE"));
        if (snapshots.isEmpty() || snapshots.stream().allMatch(
@@ -1129,41 +1151,57 @@
            return;
        }
        Set<Integer> existingTemplateIds = insOrderTemplateDeviceMapper.selectList(
                Wrappers.<InsOrderTemplateDevice>lambdaQuery()
                        .eq(InsOrderTemplateDevice::getInsOrderId, orderId))
                .stream().map(InsOrderTemplateDevice::getTemplateId)
                .filter(Objects::nonNull).collect(Collectors.toSet());
        boolean inspectionStarted = order.getFirstInspectDate() != null
                || (order.getInsState() != null && order.getInsState() > 0)
                || snapshots.stream().anyMatch(snapshot -> StringUtils.isNotBlank(snapshot.getRecordValues())
                        || snapshot.getDetectionTime() != null || snapshot.getTemperature() != null
                        || snapshot.getDetectionPlace() != null);
        Map<Integer, List<Integer>> defaults = inspectionStarted ? Collections.emptyMap()
                : findStandardDeviceDefaults(order, snapshots);
        Map<Integer, List<InsOrderTemplateDevice>> existingDevices = insOrderTemplateDeviceMapper.selectList(
                        Wrappers.<InsOrderTemplateDevice>lambdaQuery()
                                .eq(InsOrderTemplateDevice::getInsOrderId, orderId)
                                .orderByAsc(InsOrderTemplateDevice::getSort, InsOrderTemplateDevice::getId))
                .stream().filter(relation -> relation.getTemplateId() != null)
                .collect(Collectors.groupingBy(InsOrderTemplateDevice::getTemplateId));
        Map<Integer, List<Integer>> defaults = findStandardDeviceDefaults(order, snapshots);
        for (InsOrderStandardTemplate snapshot : snapshots) {
            // 检验员手动选择或清空过的模板以人工为准,不再套用标准库默认值。
            if (Objects.equals(snapshot.getDeviceDefaultsInitialized(), 1)) {
                continue;
            }
            Integer templateId = snapshot.getTemplateId();
            if (!inspectionStarted && !existingTemplateIds.contains(templateId)
                    && defaults.containsKey(templateId)) {
                List<Integer> deviceIds = defaults.get(templateId);
                for (int index = 0; index < deviceIds.size(); index++) {
                    InsOrderTemplateDevice relation = new InsOrderTemplateDevice();
                    relation.setInsOrderId(orderId);
                    relation.setTemplateId(templateId);
                    relation.setDeviceId(deviceIds.get(index));
                    relation.setSort(index);
                    insOrderTemplateDeviceMapper.insert(relation);
                }
            if (templateId == null) {
                continue;
            }
            // 无法唯一确定来源时也只尝试这一次,避免以后标准库调整时意外改动订单。
            insOrderStandardTemplateMapper.update(null,
                    Wrappers.<InsOrderStandardTemplate>lambdaUpdate()
                            .eq(InsOrderStandardTemplate::getId, snapshot.getId())
                            .set(InsOrderStandardTemplate::getDeviceDefaultsInitialized, 1));
            List<InsOrderTemplateDevice> currentDevices = existingDevices.getOrDefault(
                    templateId, Collections.emptyList());
            // 旧数据没有初始化标记,已有设备视为人工选择,标记后不再自动覆盖。
            if (snapshot.getDeviceDefaultsInitialized() == null && !currentDevices.isEmpty()) {
                insOrderStandardTemplateMapper.update(null,
                        Wrappers.<InsOrderStandardTemplate>lambdaUpdate()
                                .eq(InsOrderStandardTemplate::getId, snapshot.getId())
                                .set(InsOrderStandardTemplate::getDeviceDefaultsInitialized, 1));
                continue;
            }
            if (!defaults.containsKey(templateId)) {
                continue;
            }
            List<Integer> standardDeviceIds = defaults.get(templateId).stream()
                    .filter(Objects::nonNull).distinct().collect(Collectors.toList());
            List<Integer> currentDeviceIds = currentDevices.stream()
                    .map(InsOrderTemplateDevice::getDeviceId)
                    .filter(Objects::nonNull).distinct().collect(Collectors.toList());
            if (currentDeviceIds.equals(standardDeviceIds)) {
                continue;
            }
            // 未初始化过的模板完全对齐标准库绑定:绑定几个就显示几个。
            insOrderTemplateDeviceMapper.delete(
                    Wrappers.<InsOrderTemplateDevice>lambdaQuery()
                            .eq(InsOrderTemplateDevice::getInsOrderId, orderId)
                            .eq(InsOrderTemplateDevice::getTemplateId, templateId));
            for (int index = 0; index < standardDeviceIds.size(); index++) {
                InsOrderTemplateDevice relation = new InsOrderTemplateDevice();
                relation.setInsOrderId(orderId);
                relation.setTemplateId(templateId);
                relation.setDeviceId(standardDeviceIds.get(index));
                relation.setSort(index);
                insOrderTemplateDeviceMapper.insert(relation);
            }
        }
    }
@@ -1184,23 +1222,29 @@
                        .isNotNull(StandardTree::getSample)
                        .isNull(StandardTree::getSampleType)
                        .isNull(StandardTree::getModel));
                List<StandardTree> matches = candidates.stream()
                        .filter(tree -> StringUtils.isNotBlank(tree.getSample()))
                        .filter(tree -> {
                            Set<Integer> boundIds = standardTreeService.selectTemplateBindings(tree.getId()).stream()
                                    .map(StandardTreeTemplateBindingVo::getTemplateId).collect(Collectors.toSet());
                            return boundIds.containsAll(templateIds);
                        }).collect(Collectors.toList());
                if (matches.size() == 1) {
                    standardTreeId = matches.get(0).getId();
                } else {
                    List<StandardTree> exact = matches.stream().filter(tree ->
                            standardTreeService.selectTemplateBindings(tree.getId()).stream()
                                    .map(StandardTreeTemplateBindingVo::getTemplateId)
                                    .collect(Collectors.toSet()).equals(templateIds))
                            .collect(Collectors.toList());
                    if (exact.size() == 1) {
                        standardTreeId = exact.get(0).getId();
                if (candidates.size() == 1) {
                    // 同一标准编号、同一实验室只有一个二级节点时直接认定,
                    // 避免标准库调整过模板后旧订单匹配不上。
                    standardTreeId = candidates.get(0).getId();
                } else if (!candidates.isEmpty()) {
                    List<StandardTree> matches = candidates.stream()
                            .filter(tree -> StringUtils.isNotBlank(tree.getSample()))
                            .filter(tree -> {
                                Set<Integer> boundIds = standardTreeService.selectTemplateBindings(tree.getId()).stream()
                                        .map(StandardTreeTemplateBindingVo::getTemplateId).collect(Collectors.toSet());
                                return boundIds.containsAll(templateIds);
                            }).collect(Collectors.toList());
                    if (matches.size() == 1) {
                        standardTreeId = matches.get(0).getId();
                    } else if (!matches.isEmpty()) {
                        List<StandardTree> exact = matches.stream().filter(tree ->
                                standardTreeService.selectTemplateBindings(tree.getId()).stream()
                                        .map(StandardTreeTemplateBindingVo::getTemplateId)
                                        .collect(Collectors.toSet()).equals(templateIds))
                                .collect(Collectors.toList());
                        if (exact.size() == 1) {
                            standardTreeId = exact.get(0).getId();
                        }
                    }
                }
            }
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsReportServiceImpl.java
@@ -35,6 +35,7 @@
import com.ruoyi.common.constant.InsOrderTypeConstants;
import com.ruoyi.common.constant.MenuJumpPathConstants;
import com.ruoyi.common.core.domain.Result;
import com.ruoyi.common.core.domain.entity.SysMenu;
import com.ruoyi.common.core.domain.entity.InformationNotification;
import com.ruoyi.common.utils.DateImageUtil;
import com.ruoyi.common.utils.QueryWrappers;
@@ -53,6 +54,7 @@
import com.ruoyi.inspect.util.ReportSealLayoutUtil;
import com.ruoyi.inspect.util.ReportSignaturePictureUtils;
import com.ruoyi.system.mapper.UserMapper;
import com.ruoyi.system.service.ISysMenuService;
import com.ruoyi.system.service.InformationNotificationService;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
@@ -111,6 +113,8 @@
    private InsProductMapper insProductMapper;
    @Resource
    private InformationNotificationService informationNotificationService;
    @Resource
    private ISysMenuService menuService;
    @Value("${wordUrl}")
    private String wordUrl;
    @Value("${file.path}")
@@ -198,6 +202,7 @@
        if (userId == null) {
            throw new ErrorException("缺少审核人");
        }
        ensureReportPreparationPermission(userId, "审核人");
        insReport.setExamineUserId(userId);//审核人
        insReport.setWriteTime(LocalDateTime.now());//提交时间
        //获取提交人的签名地址
@@ -265,6 +270,33 @@
        return 1;
    }
    /**
     * 指定审核人/批准人时,确认对方登录后能看到「报告编制」页面,
     * 避免指定后登录只剩首页、无法继续审批。
     */
    private void ensureReportPreparationPermission(Integer userId, String userLabel) {
        List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId.longValue());
        if (!containsReportPreparation(menus)) {
            throw new ErrorException("所选" + userLabel + "没有「业务管理 → 报告编制」页面权限,请先在「系统管理 → 用户管理」为其分配对应角色");
        }
    }
    private boolean containsReportPreparation(List<SysMenu> menus) {
        if (CollectionUtils.isEmpty(menus)) {
            return false;
        }
        for (SysMenu menu : menus) {
            if (Long.valueOf(1129L).equals(menu.getMenuId())
                    || "reportPreparation".equals(menu.getPath())) {
                return true;
            }
            if (containsReportPreparation(menu.getChildren())) {
                return true;
            }
        }
        return false;
    }
    //审核
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -320,6 +352,7 @@
            if (userId == null) {
                throw new ErrorException("缺少批准人");
            }
            ensureReportPreparationPermission(userId, "批准人");
            if (Objects.equals(insReport.getExamineUserId(), userId)) {
                throw new ErrorException("审核人和批准人不能为同一人,请重新选择批准人");
            }
sql/lims_delta.sql
@@ -105,3 +105,32 @@
  add column search_new_time    datetime     default null comment '查新时间'                      after search_new_source,
  add column search_new_result  varchar(500) default null comment '查新结果'                      after search_new_time,
  add column search_new_website varchar(500) default null comment '来源为其他时的具体网址或网站名称' after search_new_result;
-- 22、检验订单表增加标准库二级节点ID(直绑模板下单)
-- 仅在目标库缺少该列时执行一次
alter table ins_order
  add column standard_tree_id int default null comment '新聚直绑模板下单时选择的标准库二级节点ID' after quality_monitor_details_id;
-- 23、资质表补齐颁发时间及创建人姓名
-- 仅在目标库缺少这些列时执行一次
alter table certification
  add column create_user_name     varchar(255) default null comment '创建人姓名'  after create_user,
  add column first_issuance_date  datetime     default null comment '首次颁发时间' after date_of_issuance,
  add column latest_issuance_date datetime     default null comment '最近颁发时间' after first_issuance_date;
-- 24、客户满意度表增加确认字段
-- 仅在目标库缺少这些列时执行一次
alter table cnas_client_satisfaction
  add column confirm_person    varchar(255) default null comment '确认人'                    after reasonable_fees_suggestion,
  add column confirm_person_id int          default null comment '确认人id'                  after confirm_person,
  add column confirm_status    int          default null comment '确认状态, 0:未确认, 1:确认' after confirm_person_id;
-- 25、人员培训明细表增加外聘/非系统讲师姓名
-- 仅在目标库缺少该列时执行一次
alter table cnas_person_training_detailed
  add column training_lecturer_name varchar(255) default null comment '外聘/非系统讲师姓名;内部讲师仍存 training_lecturer_id' after training_lecturer_id;
-- 26、检验订单模板快照表增加设备默认值初始化标记
-- 仅在目标库缺少该列时执行一次
alter table ins_order_standard_template
  add column device_defaults_initialized int default null comment '默认设备是否已在首次进入检验任务时初始化:0否,1是' after record_values;