feat(inspect): 新增新聚检验订单编辑功能并优化报告生成流程
- 新增 updateNewJuInsOrder 接口支持新聚检验订单修改
- 增加文件上传大小限制至200MB,请求大小限制至400MB
- 在设备信息中新增出厂编号字段显示
- 优化温湿度处理逻辑,报告中仅显示温度信息
- 新增印章管理功能,支持MA标志章、报告章和骑缝章配置
- 完善检验报告生成流程,增加多种报告类型常量定义
- 优化附件上传功能,增加空值检查和返回值处理
- 调整委托编号生成逻辑,完善新聚订单可编辑性判断
- 更新CNAS流程中报告编号使用正式报告编号替代委托编号
已添加8个文件
已修改32个文件
1633 ■■■■■ 文件已修改
basic-server/src/main/java/com/ruoyi/basic/controller/SealConfigController.java 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
basic-server/src/main/java/com/ruoyi/basic/dto/SealConfigExport.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
basic-server/src/main/java/com/ruoyi/basic/mapper/SealConfigMapper.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
basic-server/src/main/java/com/ruoyi/basic/pojo/SealConfig.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
basic-server/src/main/java/com/ruoyi/basic/service/SealConfigService.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
basic-server/src/main/java/com/ruoyi/basic/service/impl/SealConfigServiceImpl.java 115 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
basic-server/src/main/resources/mapper/SealConfigMapper.xml 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-process/src/main/java/com/ruoyi/process/controller/ProcessReportController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-process/src/main/java/com/ruoyi/process/listener/NewJuOrderInspectionListener.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-process/src/main/java/com/ruoyi/process/pojo/ProcessReport.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-process/src/main/java/com/ruoyi/process/service/ProcessReportService.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-process/src/main/java/com/ruoyi/process/service/impl/InspectionOrderServiceImpl.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-process/src/main/java/com/ruoyi/process/service/impl/ProcessReportServiceImpl.java 153 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-process/src/main/resources/mapper/ProcessReportMapper.xml 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-process/src/main/resources/static/report-deal.docx 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/controller/InsOrderController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/controller/InsOrderPlanController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/controller/InsReportController.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/dto/InsPlaceOrderDto.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/dto/SampleOrderDto.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/dto/WriteInspectionConditionDto.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/pojo/InsReport.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/service/InsOrderService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/service/InsReportService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsOrderPlanServiceImpl.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsOrderServiceImpl.java 227 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsProductServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsReportServiceImpl.java 278 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/util/PreserveReportPicturePlaceholderHandler.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/util/ReportSealLayoutUtil.java 302 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/vo/InsOrderPrintingVo.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/vo/TemplateConditionVo.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/resources/mapper/InsOrderMapper.xml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/resources/mapper/InsReportMapper.xml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/resources/static/detection-report-template-v2.docx 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/resources/static/inspection-report-template-v2.docx 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application-xjxj.yml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application.yml 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/constant/InsOrderTypeConstants.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/PersonalScopeAspect.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
basic-server/src/main/java/com/ruoyi/basic/controller/SealConfigController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,120 @@
package com.ruoyi.basic.controller;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.basic.dto.SealConfigExport;
import com.ruoyi.basic.pojo.SealConfig;
import com.ruoyi.basic.service.SealConfigService;
import com.ruoyi.common.core.domain.Result;
import com.ruoyi.common.exception.base.BaseException;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.net.URLEncoder;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
/**
 * å°ç« ç®¡ç†(报告用印) æŽ§åˆ¶å±‚
 */
@Api(tags = "印章管理")
@RestController
@RequestMapping("/sealConfig")
public class SealConfigController {
    @Resource
    private SealConfigService sealConfigService;
    @Value("${file.path}")
    private String filePath;
    @ApiOperation(value = "查询印章列表")
    @GetMapping("/pageSeal")
    public Result<IPage<SealConfig>> pageSeal(Page page, SealConfig query) {
        return Result.success(sealConfigService.pageSeal(page, query));
    }
    @ApiOperation(value = "新增印章")
    @PostMapping("/addSeal")
    public Result addSeal(@RequestBody SealConfig seal) {
        return Result.success(sealConfigService.addSeal(seal));
    }
    @ApiOperation(value = "修改印章")
    @PostMapping("/updateSeal")
    public Result updateSeal(@RequestBody SealConfig seal) {
        return Result.success(sealConfigService.updateSeal(seal));
    }
    @ApiOperation(value = "删除印章")
    @DeleteMapping("/delSeal")
    public Result delSeal(Integer id) {
        return Result.success(sealConfigService.delSeal(id));
    }
    @ApiOperation(value = "启用印章下拉列表(供报告批准选择印章)")
    @GetMapping("/selectEnabled")
    public Result selectEnabled() {
        return Result.success(sealConfigService.selectEnabled());
    }
    @ApiOperation(value = "上传印章图片")
    @PostMapping("/upload")
    public Result upload(MultipartFile file) {
        if (file == null || file.isEmpty()) {
            throw new BaseException("请选择印章图片");
        }
        String originalName = file.getOriginalFilename() == null ? "" : file.getOriginalFilename();
        String suffix = originalName.substring(originalName.lastIndexOf(".") + 1).toLowerCase();
        if (!"jpg".equals(suffix) && !"jpeg".equals(suffix) && !"png".equals(suffix)) {
            throw new BaseException("仅支持 jpg / png å›¾ç‰‡");
        }
        String fileName = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyMMddHHmmssSSS")) + "." + suffix;
        try {
            File dir = new File(filePath);
            if (!dir.exists()) {
                dir.mkdirs();
            }
            file.transferTo(new File(dir.getPath() + File.separator + fileName));
        } catch (IOException e) {
            throw new BaseException("印章图片上传失败");
        }
        return Result.success(fileName);
    }
    @ApiOperation(value = "导出印章")
    @GetMapping("/exportSeal")
    public void exportSeal(SealConfig query, HttpServletResponse response) {
        List<SealConfigExport> list = sealConfigService.listForExport(query);
        response.setContentType("application/vnd.ms-excel");
        response.setCharacterEncoding("UTF-8");
        String fileName;
        try {
            fileName = URLEncoder.encode("印章管理", "UTF-8");
        } catch (Exception e) {
            throw new BaseException("导出失败");
        }
        response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
        try {
            ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream())
                    .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).build();
            WriteSheet mainSheet = EasyExcel.writerSheet(0, "印章管理").head(SealConfigExport.class).build();
            excelWriter.write(list, mainSheet);
            excelWriter.finish();
        } catch (IOException e) {
            throw new BaseException("导出失败");
        }
    }
}
basic-server/src/main/java/com/ruoyi/basic/dto/SealConfigExport.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
package com.ruoyi.basic.dto;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
/**
 * å°ç« ç®¡ç†å¯¼å‡ºå¯¹è±¡
 */
@Data
@ExcelIgnoreUnannotated
public class SealConfigExport {
    @ExcelProperty(value = "印章编号")
    private String code;
    @ExcelProperty(value = "印章名称")
    private String name;
    @ExcelProperty(value = "使用范围")
    private String useScope;
    @ExcelProperty(value = "状态")
    private String statusName;
    @ExcelProperty(value = "备注")
    private String remark;
    @ExcelProperty(value = "创建时间")
    private String createTime;
}
basic-server/src/main/java/com/ruoyi/basic/mapper/SealConfigMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
package com.ruoyi.basic.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.basic.pojo.SealConfig;
/**
 * å°ç« ç®¡ç†(报告用印) Mapper
 */
public interface SealConfigMapper extends BaseMapper<SealConfig> {
    /**
     * æŸ¥è¯¢å½“前最大的印章编号后缀数字(用于自动生成 YZ-00N)。
     */
    Integer selectMaxCodeSuffix();
}
basic-server/src/main/java/com/ruoyi/basic/pojo/SealConfig.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,64 @@
package com.ruoyi.basic.pojo;
import com.baomidou.mybatisplus.annotation.FieldFill;
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.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
 * å°ç« ç®¡ç†(报告用印)表对象
 *
 * <p>区别于按实验室维护的旧 seal è¡¨ï¼Œæœ¬è¡¨ç”¨äºŽé›†ä¸­ç»´æŠ¤å‡ºå…·æŠ¥å‘Šæ‰€éœ€çš„各类印章,
 * ä¾›â€œæŠ¥å‘Šç¼–制-批准”时的印章多选下拉使用(仅启用状态的印章可选)。</p>
 */
@TableName(value = "cnas_seal")
@Data
public class SealConfig implements Serializable {
    @ApiModelProperty(value = "主键")
    @TableId(type = IdType.AUTO)
    private Integer id;
    @ApiModelProperty(value = "印章编号(保存时自动生成,如 YZ-001)")
    private String code;
    @ApiModelProperty(value = "印章名称")
    private String name;
    @ApiModelProperty(value = "印章图片(相对 file.path çš„æ–‡ä»¶åï¼‰")
    private String address;
    @ApiModelProperty(value = "使用范围")
    private String useScope;
    @ApiModelProperty(value = "状态:1启用 0停用")
    private Integer status;
    @ApiModelProperty(value = "备注")
    private String remark;
    @ApiModelProperty(value = "创建人id")
    @TableField(fill = FieldFill.INSERT)
    private Integer createUser;
    @ApiModelProperty(value = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    @ApiModelProperty(value = "更新人id")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Integer updateUser;
    @ApiModelProperty(value = "更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
}
basic-server/src/main/java/com/ruoyi/basic/service/SealConfigService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package com.ruoyi.basic.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.basic.dto.SealConfigExport;
import com.ruoyi.basic.pojo.SealConfig;
import java.util.List;
/**
 * å°ç« ç®¡ç†(报告用印) æœåŠ¡ç±»
 */
public interface SealConfigService extends IService<SealConfig> {
    IPage<SealConfig> pageSeal(Page page, SealConfig query);
    int addSeal(SealConfig seal);
    int updateSeal(SealConfig seal);
    int delSeal(Integer id);
    /**
     * æŸ¥è¯¢å¯ç”¨çŠ¶æ€çš„å°ç« ï¼ˆä¾›æŠ¥å‘Šæ‰¹å‡†å°ç« å¤šé€‰ä¸‹æ‹‰ä½¿ç”¨ï¼‰ã€‚
     */
    List<SealConfig> selectEnabled();
    /**
     * æŒ‰æ¡ä»¶æŸ¥è¯¢å¯¼å‡ºåˆ—表。
     */
    List<SealConfigExport> listForExport(SealConfig query);
}
basic-server/src/main/java/com/ruoyi/basic/service/impl/SealConfigServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,115 @@
package com.ruoyi.basic.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
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.basic.dto.SealConfigExport;
import com.ruoyi.basic.mapper.SealConfigMapper;
import com.ruoyi.basic.pojo.SealConfig;
import com.ruoyi.basic.service.SealConfigService;
import com.ruoyi.common.exception.base.BaseException;
import com.ruoyi.common.utils.QueryWrappers;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
/**
 * å°ç« ç®¡ç†(报告用印) æœåŠ¡å®žçŽ°
 */
@Service
public class SealConfigServiceImpl extends ServiceImpl<SealConfigMapper, SealConfig> implements SealConfigService {
    @Resource
    private SealConfigMapper sealConfigMapper;
    private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    @Override
    public IPage<SealConfig> pageSeal(Page page, SealConfig query) {
        QueryWrapper<SealConfig> wrapper = QueryWrappers.queryWrappers(query);
        if (wrapper == null) {
            wrapper = Wrappers.query();
        }
        wrapper.orderByDesc("create_time").orderByAsc("code");
        return this.page(page, wrapper);
    }
    @Override
    public int addSeal(SealConfig seal) {
        validate(seal);
        seal.setCode(generateCode());
        return sealConfigMapper.insert(seal);
    }
    @Override
    public int updateSeal(SealConfig seal) {
        if (seal.getId() == null) {
            throw new BaseException("印章id不能为空");
        }
        validate(seal);
        // ç¼–号不允许前端修改,保持自动生成值不变
        SealConfig db = sealConfigMapper.selectById(seal.getId());
        if (db != null) {
            seal.setCode(db.getCode());
        }
        return sealConfigMapper.updateById(seal);
    }
    @Override
    public int delSeal(Integer id) {
        return sealConfigMapper.deleteById(id);
    }
    @Override
    public List<SealConfig> selectEnabled() {
        return this.list(Wrappers.<SealConfig>lambdaQuery()
                .eq(SealConfig::getStatus, 1)
                .orderByAsc(SealConfig::getCode));
    }
    @Override
    public List<SealConfigExport> listForExport(SealConfig query) {
        QueryWrapper<SealConfig> wrapper = QueryWrappers.queryWrappers(query);
        if (wrapper == null) {
            wrapper = Wrappers.query();
        }
        wrapper.orderByAsc("code");
        List<SealConfig> list = this.list(wrapper);
        List<SealConfigExport> exports = new ArrayList<>();
        for (SealConfig seal : list) {
            SealConfigExport export = new SealConfigExport();
            export.setCode(seal.getCode());
            export.setName(seal.getName());
            export.setUseScope(seal.getUseScope());
            export.setStatusName(seal.getStatus() != null && seal.getStatus() == 1 ? "启用" : "停用");
            export.setRemark(seal.getRemark());
            export.setCreateTime(seal.getCreateTime() == null ? "" : seal.getCreateTime().format(DATE_FORMAT));
            exports.add(export);
        }
        return exports;
    }
    private void validate(SealConfig seal) {
        if (StringUtils.isBlank(seal.getName())) {
            throw new BaseException("印章名称不能为空");
        }
        if (StringUtils.isBlank(seal.getAddress())) {
            throw new BaseException("印章图片不能为空");
        }
    }
    /**
     * è‡ªåŠ¨ç”Ÿæˆå°ç« ç¼–å·ï¼šYZ-001、YZ-002 ...
     */
    private String generateCode() {
        Integer max = sealConfigMapper.selectMaxCodeSuffix();
        int next = (max == null ? 0 : max) + 1;
        return String.format("YZ-%03d", next);
    }
}
basic-server/src/main/resources/mapper/SealConfigMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,12 @@
<?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.ruoyi.basic.mapper.SealConfigMapper">
    <select id="selectMaxCodeSuffix" resultType="java.lang.Integer">
        select ifnull(max(cast(substring(code, 4) as unsigned)), 0)
        from cnas_seal
        where code like 'YZ-%'
    </select>
</mapper>
cnas-process/src/main/java/com/ruoyi/process/controller/ProcessReportController.java
@@ -68,4 +68,10 @@
        processReportService.exportProcessReport(dto, response);
    }
    @ApiOperation(value = "下载批准后的有章报告")
    @GetMapping("/downloadStampedReport")
    public void downloadStampedReport(Integer id, String format, HttpServletResponse response) {
        processReportService.downloadStampedReport(id, format, response);
    }
}
cnas-process/src/main/java/com/ruoyi/process/listener/NewJuOrderInspectionListener.java
@@ -30,9 +30,20 @@
            return;
        }
        // å¹‚等:同一成品订单已有检验委托单则跳过
        Long count = inspectionOrderMapper.selectCount(Wrappers.<InspectionOrder>lambdaQuery()
                .eq(InspectionOrder::getInsOrderId, order.getId()));
        if (count > 0) {
        InspectionOrder existingOrder = inspectionOrderMapper.selectOne(Wrappers.<InspectionOrder>lambdaQuery()
                .eq(InspectionOrder::getInsOrderId, order.getId()).last("limit 1"));
        if (existingOrder != null) {
            existingOrder.setEntrustCode(order.getEntrustCode());
            existingOrder.setSampleName(StringUtils.isNotBlank(order.getSampleView()) ? order.getSampleView() : order.getSample());
            existingOrder.setProduction(order.getProduction());
            existingOrder.setQuantity(order.getTestQuantity());
            existingOrder.setCommissionUnit(order.getCompany());
            existingOrder.setCommissionUser(order.getPrepareUser());
            existingOrder.setCommissionPhone(order.getPhone());
            existingOrder.setIsLeave(Integer.valueOf(1).equals(order.getIsLeave()) ? 1 : 2);
            existingOrder.setProcessing(Integer.valueOf(1).equals(order.getSampleDisposal()) ? 0 : 1);
            existingOrder.setSend(Integer.valueOf(1).equals(order.getReportDelivery()) ? 1 : 0);
            inspectionOrderMapper.updateById(existingOrder);
            return;
        }
        InspectionOrder inspectionOrder = new InspectionOrder();
cnas-process/src/main/java/com/ruoyi/process/pojo/ProcessReport.java
@@ -36,12 +36,6 @@
    @ApiModelProperty("检验报告编号")
    private String insReportCode;
    @ApiModelProperty("页数")
    private String pages;
    @ApiModelProperty("发送份数")
    private String number;
    @ApiModelProperty("发往何处")
    private String send;
@@ -74,4 +68,12 @@
    @ApiModelProperty("委托单id")
    private Integer inspectionOrderId;
    @ApiModelProperty("领报告日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate receiveDate;
    @ApiModelProperty("领报告人")
    private String receiveUser;
}
cnas-process/src/main/java/com/ruoyi/process/service/ProcessReportService.java
@@ -23,4 +23,7 @@
    IPage<ProcessReportDto> pageProcessReport(Page page, ProcessReportDto processReport);
    void exportProcessReport(ProcessReportDto dto, HttpServletResponse response);
    /** ä¸‹è½½æ‰¹å‡†åŽç”Ÿæˆçš„æœ‰ç« æŠ¥å‘Šï¼Œformat:pdf æˆ– word。 */
    void downloadStampedReport(Integer id, String format, HttpServletResponse response);
}
cnas-process/src/main/java/com/ruoyi/process/service/impl/InspectionOrderServiceImpl.java
@@ -146,14 +146,7 @@
        /*新增7.8报告结果*/
        ProcessReport processReport = new ProcessReport();
        processReport.setInspectionOrderId(inspectionOrder.getInsOrderId());
        processReport.setInsReportCode(inspectionOrder.getEntrustCode());//报告编号=委托编号
        //页数
        try {
            com.aspose.words.Document doc = new com.aspose.words.Document(path);
            processReport.setPages(doc.getPageCount()+"");
        } catch (Exception e) {
        }
        processReport.setNumber("1");//发送份数默认1
        processReport.setInsReportCode(insReport.getCode());//报告编号使用正式报告编号
        processReport.setSend(inspectionOrder.getCommissionUnit());//发往何处=委托单位
        processReport.setMethod(inspectionOrder.getSend()==1?"自取":"其他");//发送方式
        processReport.setSendTime(insReport.getRatifyTime().toLocalDate());//发送日期
@@ -247,9 +240,17 @@
        inspectionOrderDetailService.saveBatch(InspectionOrder.getOrderDetailList());
        //修改7.9报告结果
        ProcessReport processReport = processReportService.getOne(Wrappers.<ProcessReport>lambdaQuery().eq(ProcessReport::getInspectionOrderId, InspectionOrder.getInspectionOrderId()));
        processReport.setInsReportCode(InspectionOrder.getEntrustCode());//报告编号=委托编号
        ProcessReport processReport = processReportService.getOne(Wrappers.<ProcessReport>lambdaQuery()
                .eq(ProcessReport::getInspectionOrderId, InspectionOrder.getInsOrderId())
                .last("limit 1"));
        InsReport insReport = insReportService.getOne(Wrappers.<InsReport>lambdaQuery()
                .eq(InsReport::getInsOrderId, InspectionOrder.getInsOrderId())
                .orderByDesc(InsReport::getId)
                .last("limit 1"));
        if (processReport != null && insReport != null) {
            processReport.setInsReportCode(insReport.getCode());//报告编号使用正式报告编号
        processReportService.updateById(processReport);
        }
        return true;
    }
cnas-process/src/main/java/com/ruoyi/process/service/impl/ProcessReportServiceImpl.java
@@ -15,7 +15,12 @@
import com.ruoyi.common.utils.QueryWrappers;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.SignatureImageUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.framework.exception.ErrorException;
import com.ruoyi.inspect.mapper.InsOrderMapper;
import com.ruoyi.inspect.mapper.InsReportMapper;
import com.ruoyi.inspect.pojo.InsOrder;
import com.ruoyi.inspect.pojo.InsReport;
import com.ruoyi.inspect.util.HackLoopTableRenderPolicy;
import com.ruoyi.process.dto.ProcessReportDto;
import com.ruoyi.process.mapper.ProcessReportMapper;
@@ -33,11 +38,14 @@
import java.io.*;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
@@ -56,17 +64,162 @@
    private ProcessReportMapper processReportMapper;
    @Value("${file.path}")
    private String imgUrl;
    @Value("${wordUrl}")
    private String wordUrl;
    @Resource
    private UserMapper userMapper;
    @Resource
    private InsReportMapper insReportMapper;
    @Resource
    private InsOrderMapper insOrderMapper;
    @Override
    public IPage<ProcessReportDto> pageProcessReport(Page page, ProcessReportDto processReport) {
        // #21 æ‰“开列表时,自动带出已通过审批(批准状态=批准)但尚未登记的检验单
        autoBringOutApproved();
        IPage<ProcessReportDto> processReportIPage = processReportMapper.pageProcessReport(page, QueryWrappers.queryWrappers(processReport));
        return processReportIPage;
    }
    /**
     * æ‰«æå·²æ‰¹å‡†ä½†å°šæœªè¿›å…¥æŠ¥å‘Šç»“果登记表的检验报告,自动生成待登记记录(以报告编号去重,幂等)。
     */
    private void autoBringOutApproved() {
        List<InsReport> approved = insReportMapper.selectList(Wrappers.<InsReport>lambdaQuery()
                .eq(InsReport::getIsRatify, 1)
                .eq(InsReport::getIsPass, 1));
        if (approved.isEmpty()) {
            return;
        }
        Set<String> existing = this.list(Wrappers.<ProcessReport>lambdaQuery()
                .select(ProcessReport::getInsReportCode)).stream()
                .map(ProcessReport::getInsReportCode)
                .filter(StringUtils::isNotBlank)
                .collect(Collectors.toCollection(HashSet::new));
        for (InsReport report : approved) {
            String code = report.getCode();
            if (StringUtils.isBlank(code) || existing.contains(code)) {
                continue;
            }
            ProcessReport pr = new ProcessReport();
            pr.setInsReportCode(code);
            pr.setInspectionOrderId(report.getInsOrderId());
            if (report.getRatifyTime() != null) {
                pr.setSendTime(report.getRatifyTime().toLocalDate());
            }
            if (report.getInsOrderId() != null) {
                InsOrder order = insOrderMapper.selectById(report.getInsOrderId());
                if (order != null) {
                    pr.setSend(order.getCompany());
                    pr.setMethod(getReportDeliveryName(order));
                }
            }
            this.save(pr);
            existing.add(code);
        }
    }
    private String getReportDeliveryName(InsOrder order) {
        if (order.getReportDelivery() != null) {
            switch (order.getReportDelivery()) {
                case 1:
                    return "自取";
                case 2:
                    return "快递";
                case 3:
                    return "电子传送";
                default:
                    break;
            }
        }
        // å…¼å®¹å°šæœªç»´æŠ¤ report_delivery çš„历史委托单。
        return order.getSend() != null && order.getSend() == 1 ? "自取" : "其他";
    }
    @Override
    public void downloadStampedReport(Integer id, String format, HttpServletResponse response) {
        if (id == null) {
            throw new ErrorException("报告结果不能为空");
        }
        ProcessReport processReport = getById(id);
        if (processReport == null) {
            throw new ErrorException("报告结果不存在");
        }
        if (StringUtils.isBlank(processReport.getInsReportCode())
                && processReport.getInspectionOrderId() == null) {
            throw new ErrorException("该记录未关联检验报告,无法下载");
        }
        String downloadFormat = StringUtils.isBlank(format) ? "pdf" : format.trim().toLowerCase();
        if (!"pdf".equals(downloadFormat) && !"word".equals(downloadFormat)) {
            throw new ErrorException("下载格式不正确");
        }
        com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<InsReport> query =
                Wrappers.<InsReport>lambdaQuery()
                        .eq(InsReport::getIsRatify, 1)
                        .eq(StringUtils.isNotBlank(processReport.getInsReportCode()),
                                InsReport::getCode, processReport.getInsReportCode())
                        .eq(processReport.getInspectionOrderId() != null,
                                InsReport::getInsOrderId, processReport.getInspectionOrderId())
                        .orderByDesc(InsReport::getId)
                        .last("limit 1");
        InsReport report = insReportMapper.selectOne(query);
        if (report == null) {
            throw new ErrorException("未找到已批准的报告,无法下载有章报告");
        }
        String fileUrl;
        String extension;
        String contentType;
        if ("word".equals(downloadFormat)) {
            fileUrl = StringUtils.isNotBlank(report.getUrlS()) ? report.getUrlS() : report.getUrl();
            extension = ".docx";
            contentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
        } else {
            fileUrl = report.getTempUrlPdf();
            extension = ".pdf";
            contentType = "application/pdf";
        }
        if ("pdf".equals(downloadFormat) && StringUtils.isBlank(fileUrl)) {
            String reportUrl = StringUtils.isNotBlank(report.getUrlS()) ? report.getUrlS() : report.getUrl();
            if (StringUtils.isNotBlank(reportUrl) && reportUrl.toLowerCase().endsWith(".docx")) {
                fileUrl = reportUrl.substring(0, reportUrl.length() - 5) + ".pdf";
            }
        }
        if (StringUtils.isBlank(fileUrl) || !fileUrl.replace('\\', '/').startsWith("/word/")) {
            throw new ErrorException("有章报告尚未生成,请先完成报告批准");
        }
        Path root = Paths.get(wordUrl).toAbsolutePath().normalize();
        Path file = root.resolve(fileUrl.replace('\\', '/').substring("/word/".length())).normalize();
        if (!file.startsWith(root) || !Files.isRegularFile(file)) {
            throw new ErrorException("有章报告文件不存在,请重新批准生成");
        }
        try {
            String filename = StringUtils.isNotBlank(processReport.getInsReportCode())
                    ? processReport.getInsReportCode() + extension : file.getFileName().toString();
            String encodedName = URLEncoder.encode(filename, "UTF-8").replace("+", "%20");
            response.setContentType(contentType);
            response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedName);
            response.setHeader("Access-Control-Expose-Headers", "Content-Disposition,download-filename");
            response.setHeader("download-filename", encodedName);
            response.setContentLengthLong(Files.size(file));
            try (InputStream inputStream = Files.newInputStream(file);
                 OutputStream outputStream = response.getOutputStream()) {
                IOUtils.copy(inputStream, outputStream);
                outputStream.flush();
            }
        } catch (IOException e) {
            throw new ErrorException("有章报告下载失败");
        }
    }
    @Override
    public void exportProcessReport(ProcessReportDto dto, HttpServletResponse response) {
        List<ProcessReportDto> processReports = processReportMapper.exportProcessReport();
cnas-process/src/main/resources/mapper/ProcessReportMapper.xml
@@ -5,8 +5,6 @@
    <resultMap id="BaseResultMap" type="com.ruoyi.process.pojo.ProcessReport">
        <id column="id" property="id"/>
        <result column="ins_report_code" property="insReportCode"/>
        <result column="pages" property="pages"/>
        <result column="number" property="number"/>
        <result column="send" property="send"/>
        <result column="method" property="method"/>
        <result column="send_time" property="sendTime"/>
@@ -19,19 +17,65 @@
        <result column="update_time" property="updateTime"/>
    </resultMap>
    <select id="pageProcessReport" resultType="com.ruoyi.process.dto.ProcessReportDto">
        select * from (select cpr.* ,
        select * from (select cpr.id,
        cpr.ins_report_code,
        cpr.send,
        case io.report_delivery
            when 1 then '自取'
            when 2 then '快递'
            when 3 then '电子传送'
            else cpr.method
        end method,
        cpr.send_time,
        cpr.send_user,
        cpr.signatory,
        cpr.remark,
        cpr.create_user,
        cpr.create_time,
        cpr.update_user,
        cpr.update_time,
        cpr.inspection_order_id,
        cpr.receive_date,
        cpr.receive_user,
        u1.name sendUserName
        from cnas_process_report cpr
        left join user u1 on send_user=u1.id )A
        left join user u1 on send_user=u1.id
        inner join ins_report ir on ir.code = cpr.ins_report_code
            and ir.ins_order_id = cpr.inspection_order_id
            and ir.is_ratify = 1 and ir.is_pass = 1
        left join ins_order io on io.id = ir.ins_order_id )A
        <if test="ew.customSqlSegment != null and ew.customSqlSegment != ''">
            ${ew.customSqlSegment}
        </if>
    </select>
    <select id="exportProcessReport" resultType="com.ruoyi.process.dto.ProcessReportDto">
        select ROW_NUMBER() OVER (ORDER BY cpr.id) AS indexs,
        cpr.* ,
        cpr.id,
        cpr.ins_report_code,
        cpr.send,
        case io.report_delivery
            when 1 then '自取'
            when 2 then '快递'
            when 3 then '电子传送'
            else cpr.method
        end method,
        cpr.send_time,
        cpr.send_user,
        cpr.signatory,
        cpr.remark,
        cpr.create_user,
        cpr.create_time,
        cpr.update_user,
        cpr.update_time,
        cpr.inspection_order_id,
        cpr.receive_date,
        cpr.receive_user,
        u1.signature_url sendUserUrl
        from cnas_process_report cpr
        left join user u1 on send_user=u1.id
        inner join ins_report ir on ir.code = cpr.ins_report_code
            and ir.ins_order_id = cpr.inspection_order_id
            and ir.is_ratify = 1 and ir.is_pass = 1
        left join ins_order io on io.id = ir.ins_order_id
    </select>
</mapper>
cnas-process/src/main/resources/static/report-deal.docx
Binary files differ
inspect-server/src/main/java/com/ruoyi/inspect/controller/InsOrderController.java
@@ -88,6 +88,12 @@
        return Result.success(insOrderService.addInsOrder(insPlaceOrderDto.getSampleList(), insPlaceOrderDto.getInsOrder()));
    }
    @ApiOperation(value = "修改新聚检验下单数据")
    @PostMapping("/updateNewJuInsOrder")
    public Result<?> updateNewJuInsOrder(@RequestBody InsPlaceOrderDto insPlaceOrderDto) {
        return Result.success(insOrderService.updateNewJuInsOrder(insPlaceOrderDto));
    }
    @ApiOperation(value = "查询订单最长预计时间")
    @GetMapping("/selectOrderManDay")
    public Result<?> selectOrderManDay(Integer id) {
inspect-server/src/main/java/com/ruoyi/inspect/controller/InsOrderPlanController.java
@@ -175,7 +175,7 @@
        return Result.success(map);
    }
    @ApiOperation(value = "填写温度与湿度")
    @ApiOperation(value = "填写温度")
    @PostMapping("/write")
    public Result<?> write(@RequestBody WriteInspectionConditionDto dto) {
        return Result.success(insProductService.write(dto));
inspect-server/src/main/java/com/ruoyi/inspect/controller/InsReportController.java
@@ -115,7 +115,20 @@
        Integer id = (Integer) param.get("id");
        Integer isRatify = (Integer) param.get("isRatify");
        String ratifyTell = (String) param.get("ratifyTell");
        return Result.success(insReportService.ratifyReport(id, isRatify, ratifyTell));
        Object sealIdsObj = param.get("sealIds");
        String sealIds = sealIdsObj == null ? null : String.valueOf(sealIdsObj);
        Integer maSealId = toInteger(param.get("maSealId"));
        Integer reportSealId = toInteger(param.get("reportSealId"));
        Integer seamSealId = toInteger(param.get("seamSealId"));
        return Result.success(insReportService.ratifyReport(
                id, isRatify, ratifyTell, sealIds, maSealId, reportSealId, seamSealId));
    }
    private Integer toInteger(Object value) {
        if (value == null || String.valueOf(value).trim().isEmpty()) {
            return null;
        }
        return value instanceof Number ? ((Number) value).intValue() : Integer.valueOf(String.valueOf(value));
    }
    @Anonymous
inspect-server/src/main/java/com/ruoyi/inspect/dto/InsPlaceOrderDto.java
@@ -15,6 +15,9 @@
@Data
public class InsPlaceOrderDto{
    @ApiModelProperty("编辑的订单ID")
    private Integer orderId;
    @ApiModelProperty("样品")
    private List<SampleProductDto> sampleList;
inspect-server/src/main/java/com/ruoyi/inspect/dto/SampleOrderDto.java
@@ -17,6 +17,9 @@
@ExcelIgnoreUnannotated
public class SampleOrderDto extends InsOrder {
    @ApiModelProperty("是否允许编辑")
    private Boolean editable;
    @ApiModelProperty("检验对象")
    private String sampleType;
inspect-server/src/main/java/com/ruoyi/inspect/dto/WriteInspectionConditionDto.java
@@ -16,7 +16,6 @@
    private Integer templateId;
    private String remark;
    private String temperature;
    private String humidity;
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate detectionTime;
@@ -27,8 +26,6 @@
    @JsonIgnore
    private boolean temperaturePresent;
    @JsonIgnore
    private boolean humidityPresent;
    @JsonIgnore
    private boolean detectionTimePresent;
    @JsonIgnore
@@ -41,11 +38,6 @@
    public void setTemperature(String temperature) {
        this.temperature = temperature;
        this.temperaturePresent = true;
    }
    public void setHumidity(String humidity) {
        this.humidity = humidity;
        this.humidityPresent = true;
    }
    public void setDetectionTime(LocalDate detectionTime) {
inspect-server/src/main/java/com/ruoyi/inspect/pojo/InsReport.java
@@ -135,4 +135,28 @@
    @ApiModelProperty(value = "是否是通过的报告, 0:未通过, 1:通过")
    private Integer isPass;
    @ApiModelProperty(value = "批准时选用的印章id(多选,逗号分隔)")
    private String sealIds;
    @ApiModelProperty(value = "批准时选用的印章名称(多个,逗号分隔)")
    private String sealNames;
    @ApiModelProperty(value = "MA标志章id(PDF第1页左上角)")
    private Integer maSealId;
    @ApiModelProperty(value = "MA标志章名称")
    private String maSealName;
    @ApiModelProperty(value = "报告章id(对应模板 seal1、seal2)")
    private Integer reportSealId;
    @ApiModelProperty(value = "报告章名称")
    private String reportSealName;
    @ApiModelProperty(value = "PDF骑缝章的印章id")
    private Integer seamSealId;
    @ApiModelProperty(value = "PDF骑缝章的印章名称")
    private String seamSealName;
}
inspect-server/src/main/java/com/ruoyi/inspect/service/InsOrderService.java
@@ -37,6 +37,8 @@
    int addNewJuInsOrder(InsPlaceOrderDto insPlaceOrderDto);
    int updateNewJuInsOrder(InsPlaceOrderDto insPlaceOrderDto);
    Map<String, Object> getInsOrder(Integer id);
    int upInsOrderOfState(InsOrder insOrder);
inspect-server/src/main/java/com/ruoyi/inspect/service/InsReportService.java
@@ -22,7 +22,7 @@
    Map<String,Object> pageInsReport(Page page, ReportPageDto reportPageDto);
    void wordToPdf(String path,String sealUrl, boolean isRawMater);
    void wordToPdf(String path, String maSealUrl, String seamSealUrl, boolean isRawMater);
    int inReport(String url, Integer id);
@@ -35,7 +35,8 @@
    int examineReport(Integer id, Integer isExamine, String examineTell, Integer userId);
    //批准
    int ratifyReport(Integer id, Integer isRatify, String ratifyTell);
    int ratifyReport(Integer id, Integer isRatify, String ratifyTell, String sealIds,
                     Integer maSealId, Integer reportSealId, Integer seamSealId);
    int wordInsertUrl(Map<String, Object> map, String url);
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsOrderPlanServiceImpl.java
@@ -84,7 +84,6 @@
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
 * æ£€éªŒä»»åŠ¡-业务实现层
@@ -380,6 +379,9 @@
    @Override
    public int uploadFile(Integer orderId, MultipartFile file) {
        if (orderId == null || file == null || file.isEmpty()) {
            throw new ErrorException("请选择要上传的附件");
        }
        String urlString;
        String pathName;
        String path;
@@ -406,7 +408,7 @@
            urlString = realpath + "/" + pathName;
            file.transferTo(new File(urlString));
            insOrderFile.setFileUrl(pathName);
            return insOrderFileMapper.insert(insOrderFile);
            return insOrderFileMapper.insert(insOrderFile) > 0 ? insOrderFile.getId() : 0;
        } catch (Exception e) {
            e.printStackTrace();
            System.err.println("附件上传错误");
@@ -1684,8 +1686,9 @@
        boolean directTemplateOrder = isDirectTemplateOrder(orderId);
        if (currentState != null && Objects.equals(currentState.getInsState(), 5)
                && existingReport != null && StringUtils.isNotBlank(existingReport.getUrl())
                && Objects.equals(existingReport.getIsPass(), 1)
                && (!directTemplateOrder || StringUtils.isNotBlank(existingReport.getInspectionUrl()))) {
            // é˜²æ­¢å¤æ ¸é¡µé¢é‡å¤ç‚¹å‡»æˆ–网络重试重复生成报告、重复发起报告审批。
            // ä»…对仍在报告编制中的记录防重复;退回检验任务的报告需要继续执行并恢复 is_pass。
            return 1;
        }
        if (directTemplateOrder && Objects.equals(type, 1)
@@ -1964,10 +1967,12 @@
        if (StringUtils.isBlank(tempPdfName)) {
            throw new ErrorException("报告预览文件生成失败");
        }
        // generateReport ä¼šå†™å…¥å½“前时间;重新生成不应改变原审批记录时间。
        // generateReport ä¼šå†™å…¥å½“前时间,部分报告还会暂时写入 is_pass=0;
        // é‡æ–°ç”Ÿæˆä¸åº”改变原审批记录时间和可见状态。
        insReportMapper.update(null, Wrappers.<InsReport>lambdaUpdate()
                .eq(InsReport::getId, reportId)
                .set(InsReport::getWriteTime, report.getWriteTime())
                .set(InsReport::getIsPass, report.getIsPass())
                .set(InsReport::getTempUrlPdf, "/word/" + tempPdfName));
    }
@@ -3535,7 +3540,7 @@
        String insTime = formatReportTestDate(detectionDates);
        String insTimeEn = formatReportTestDateEn(detectionDates, monthNames);
        // æ£€éªŒçŽ¯å¢ƒï¼šä¼˜å…ˆæ±‡æ€»å„æ¨¡æ¿å¡«å†™çš„æ¸©æ¹¿åº¦ï¼ŒæŠ¥å‘Šå±•ç¤ºå…¨éƒ¨æ¨¡æ¿çš„æœ€å°å€¼åˆ°æœ€å¤§å€¼ã€‚
        // æ£€éªŒçŽ¯å¢ƒï¼šä¼˜å…ˆæ±‡æ€»å„æ¨¡æ¿å¡«å†™çš„æ¸©åº¦ï¼ŒæŠ¥å‘Šå±•ç¤ºå…¨éƒ¨æ¨¡æ¿çš„æœ€å°å€¼åˆ°æœ€å¤§å€¼ã€‚
        String environment = buildReportEnvironment(orderId, insOrder);
        String finalEnvironment = environment;
        LocalDateTime finalSendTime = sendTime;
@@ -3619,6 +3624,7 @@
        String finalResultRemark = resultRemark;
        String equipmentText = buildEquipmentText(finalDeviceList);
        String issueDate = now.format(DateTimeFormatter.ofPattern("yyyyå¹´MM月dd日"));
        String conclusionDate = now.format(DateTimeFormatter.ofPattern("yyyy å¹´ MM æœˆ dd æ—¥"));
        String finalReportModelStr = finalModelStr;
        LocalDateTime finalReportSendTime = finalSendTime;
        XWPFTemplate template = XWPFTemplate.compile(inputStream, configure).render(
@@ -3662,6 +3668,7 @@
                    put("customer", finalCustomerName);
                    put("category", finalDetectionCategory);
                    put("date", issueDate);
                    put("conclusionDate", conclusionDate);
                    put("maker", defaultReportText(insOrder.getProduction(), "/"));
                    put("env", finalEnvironment);
                    put("qty", defaultReportText(insOrder.getTestQuantity(), "/"));
@@ -3891,12 +3898,18 @@
        for (Map<String, String> device : deviceList) {
            String name = getMapText(device, "deviceName", "device_name");
            String managementNumber = getMapText(device, "managementNumber", "management_number");
            String factoryNo = getMapText(device, "factoryNo", "factory_no");
            if (StringUtils.isBlank(name) || "/".equals(name.trim())) {
                continue;
            }
            String number = StringUtils.isBlank(managementNumber) || "/".equals(managementNumber.trim())
                    ? "出厂编号" : managementNumber.trim();
            String item = name.trim() + ":" + number;
            String number = managementNumber;
            if (StringUtils.isBlank(number) || "/".equals(number.trim())) {
                number = factoryNo;
            }
            if (StringUtils.isBlank(number) || "/".equals(number.trim())) {
                continue;
            }
            String item = name.trim() + ":" + number.trim();
            equipmentItems.add(item);
        }
        if (equipmentItems.isEmpty()) {
@@ -3915,22 +3928,12 @@
        List<InsOrderStandardTemplate> snapshots = insOrderStandardTemplateService.list(
                Wrappers.<InsOrderStandardTemplate>lambdaQuery()
                        .eq(InsOrderStandardTemplate::getInsOrderId, orderId)
                        .select(InsOrderStandardTemplate::getTemperature,
                                InsOrderStandardTemplate::getHumidity));
                        .select(InsOrderStandardTemplate::getTemperature));
        List<String> temperatures = snapshots.stream()
                .map(InsOrderStandardTemplate::getTemperature)
                .filter(StringUtils::isNotBlank)
                .collect(Collectors.toList());
        List<String> humidities = snapshots.stream()
                .map(InsOrderStandardTemplate::getHumidity)
                .filter(StringUtils::isNotBlank)
                .collect(Collectors.toList());
        String temperature = formatConditionRange(temperatures, order.getTemperature(), "℃");
        String humidity = formatConditionRange(humidities, order.getHumidity(), "%RH");
        return Stream.of(temperature, humidity)
                .filter(StringUtils::isNotBlank)
                .collect(Collectors.joining(" "));
        return formatConditionRange(temperatures, order.getTemperature(), "℃");
    }
    private static String formatConditionRange(List<String> values, String fallback, String unit) {
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsOrderServiceImpl.java
@@ -125,8 +125,32 @@
        }
        // æ£€éªŒä¸­ï¼šæ–°èšä¸‹å•免审核即受理,需样品接收提交后(ins_sample_receipt.status=1)才展示
        String receiptSubmitted = sampleOrderDto.getState() != null && sampleOrderDto.getState() == 1 ? "1" : null;
        return insOrderMapper.selectInsOrderPage(page, QueryWrappers.queryWrappers(sampleOrderDto), laboratory,
                isOrderAll, receiptSubmitted);
        IPage<SampleOrderDto> result = insOrderMapper.selectInsOrderPage(page,
                QueryWrappers.queryWrappers(sampleOrderDto), laboratory, isOrderAll, receiptSubmitted);
        List<Integer> candidateIds = result.getRecords().stream()
                .filter(row -> Integer.valueOf(-1).equals(row.getTypeSource())
                        && Integer.valueOf(1).equals(row.getState()))
                .map(SampleOrderDto::getId)
                .collect(Collectors.toList());
        if (!candidateIds.isEmpty()) {
            Set<Integer> receivedIds = insSampleReceiptMapper.selectList(Wrappers.<InsSampleReceipt>lambdaQuery()
                            .in(InsSampleReceipt::getInsOrderId, candidateIds)
                            .eq(InsSampleReceipt::getStatus, 1)
                            .select(InsSampleReceipt::getInsOrderId)).stream()
                    .map(InsSampleReceipt::getInsOrderId).collect(Collectors.toSet());
            Set<Integer> taskIds = insOrderStateService.list(Wrappers.<InsOrderState>lambdaQuery()
                            .in(InsOrderState::getInsOrderId, candidateIds)
                            .select(InsOrderState::getInsOrderId)).stream()
                    .map(InsOrderState::getInsOrderId).collect(Collectors.toSet());
            Set<Integer> assignedIds = insSampleUserMapper.selectList(Wrappers.<InsSampleUser>lambdaQuery()
                            .in(InsSampleUser::getInsSampleId, candidateIds)
                            .select(InsSampleUser::getInsSampleId)).stream()
                    .map(InsSampleUser::getInsSampleId).collect(Collectors.toSet());
            result.getRecords().forEach(row -> row.setEditable(
                    candidateIds.contains(row.getId()) && !receivedIds.contains(row.getId())
                            && !taskIds.contains(row.getId()) && !assignedIds.contains(row.getId())));
        }
        return result;
    }
@@ -487,6 +511,9 @@
        }
        // å§”托要求字段校验:四组单选必选,分包/快递联动必填
        InsOrder reqOrder = dto.getInsOrder();
        if (reqOrder.getSampleDate() == null) {
            throw new ErrorException("送样日期不能为空");
        }
        if (reqOrder.getSubcontractType() == null || reqOrder.getSampleDisposal() == null
                || reqOrder.getResultEvaluation() == null || reqOrder.getReportDelivery() == null) {
            throw new ErrorException("请选择分包内容、样品处置、对结果评价和报告交付方式");
@@ -633,6 +660,170 @@
        return order.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int updateNewJuInsOrder(InsPlaceOrderDto dto) {
        if (dto == null || dto.getOrderId() == null || dto.getInsOrder() == null || dto.getStandardTreeId() == null) {
            throw new ErrorException("下单信息或标准不能为空");
        }
        InsOrder currentOrder = insOrderMapper.selectById(dto.getOrderId());
        if (currentOrder == null) {
            throw new ErrorException("检验单不存在");
        }
        if (!Integer.valueOf(1).equals(currentOrder.getState()) || !Integer.valueOf(-1).equals(currentOrder.getTypeSource())) {
            throw new ErrorException("当前委托不允许编辑");
        }
        assertNewJuOrderEditable(dto.getOrderId());
        String sampleCode = StringUtils.trim(dto.getSampleCode());
        if (StringUtils.isBlank(sampleCode) || sampleCode.length() < 2) {
            throw new ErrorException("样品编号不能为空且不能少于2位");
        }
        InsOrder order = dto.getInsOrder();
        if (order.getSampleDate() == null) {
            throw new ErrorException("送样日期不能为空");
        }
        if (order.getSubcontractType() == null || order.getSampleDisposal() == null
                || order.getResultEvaluation() == null || order.getReportDelivery() == null) {
            throw new ErrorException("请选择分包内容、样品处置、对结果评价和报告交付方式");
        }
        if (Integer.valueOf(1).equals(order.getSubcontractType()) && StringUtils.isBlank(order.getSubcontractCompany())) {
            throw new ErrorException("分包内容为有时,分包单位信息不能为空");
        }
        if (Integer.valueOf(2).equals(order.getReportDelivery())
                && (StringUtils.isBlank(order.getReportAddress()) || StringUtils.isBlank(order.getReportReceiver()))) {
            throw new ErrorException("报告交付方式为快递时,请填写报告寄送地址和收件人及电话");
        }
        StandardTree standard = standardTreeService.getById(dto.getStandardTreeId());
        if (standard == null || StringUtils.isBlank(standard.getSample())
                || standard.getSampleType() != null || standard.getModel() != null) {
            throw new ErrorException("请选择标准库二级标准");
        }
        List<StandardTreeTemplateBindingVo> bindings = standardTreeService.selectTemplateBindings(dto.getStandardTreeId());
        if (bindings.isEmpty()) {
            throw new ErrorException("该标准尚未绑定可用的原始记录模板");
        }
        Map<Integer, StandardTreeTemplateBindingVo> bindingMap = bindings.stream()
                .collect(Collectors.toMap(StandardTreeTemplateBindingVo::getTemplateId, binding -> binding, (first, ignored) -> first));
        List<OrderTemplateSelectionDto> selections = dto.getTemplateList();
        if (selections == null) {
            selections = bindings.stream().map(binding -> {
                OrderTemplateSelectionDto selection = new OrderTemplateSelectionDto();
                selection.setTemplateId(binding.getTemplateId());
                selection.setSort(binding.getSort());
                return selection;
            }).collect(Collectors.toList());
        }
        if (selections.isEmpty()) {
            throw new ErrorException("请至少保留一个原始记录模板");
        }
        Set<Integer> selectedTemplateIds = new HashSet<>();
        for (int index = 0; index < selections.size(); index++) {
            OrderTemplateSelectionDto selection = selections.get(index);
            if (selection == null || selection.getTemplateId() == null || !bindingMap.containsKey(selection.getTemplateId())) {
                throw new ErrorException("所选原始记录模板不属于当前标准");
            }
            if (!selectedTemplateIds.add(selection.getTemplateId())) {
                throw new ErrorException("原始记录模板不能重复选择");
            }
            if (selection.getSort() == null) {
                selection.setSort(index);
            }
        }
        selections.sort(Comparator.comparing(OrderTemplateSelectionDto::getSort)
                .thenComparing(OrderTemplateSelectionDto::getTemplateId));
        Map<Integer, Integer> templateSortMap = selections.stream().collect(Collectors.toMap(
                OrderTemplateSelectionDto::getTemplateId, OrderTemplateSelectionDto::getSort));
        List<TemplateInspectionItemDto> templateItems = standardTreeService.selectTemplateInspectionItems(dto.getStandardTreeId()).stream()
                .filter(item -> selectedTemplateIds.contains(item.getTemplateId()))
                .sorted(Comparator.comparing((TemplateInspectionItemDto item) -> templateSortMap.get(item.getTemplateId()))
                        .thenComparing(item -> item.getTemplateRowIndex() == null ? Integer.MAX_VALUE : item.getTemplateRowIndex()))
                .collect(Collectors.toList());
        StandardMethod standardMethod = standardMethodService.getById(standard.getStandardMethodId());
        if (standardMethod == null || StringUtils.isBlank(standardMethod.getCode())) {
            throw new ErrorException("标准编号不存在");
        }
        String standardCode = standardMethod.getCode().trim();
        order.setId(currentOrder.getId());
        order.setState(currentOrder.getState());
        order.setInsState(currentOrder.getInsState());
        order.setExamineTime(currentOrder.getExamineTime());
        order.setSendTime(currentOrder.getSendTime());
        order.setIsExemption(currentOrder.getIsExemption());
        order.setFactory(null);
        order.setLaboratory(standard.getLaboratory());
        order.setSampleType(null);
        order.setSample(standardCode);
        order.setPrepareUserEn(null);
        order.setPrepareCode(null);
        order.setRemarkEn(null);
        order.setTemperature(null);
        order.setHumidity(null);
        if (!Integer.valueOf(1).equals(order.getSubcontractType())) {
            order.setSubcontractCompany(null);
        }
        if (!Integer.valueOf(2).equals(order.getReportDelivery())) {
            order.setReportAddress(null);
            order.setReportReceiver(null);
        }
        insOrderMapper.updateById(order);
        List<InsSample> samples = insSampleMapper.selectList(Wrappers.<InsSample>lambdaQuery()
                .eq(InsSample::getInsOrderId, currentOrder.getId()).orderByAsc(InsSample::getId));
        if (samples.size() != 1) {
            throw new ErrorException("检验样品数据异常");
        }
        InsSample sample = samples.get(0);
        sample.setSampleCode(sampleCode);
        sample.setLaboratory(standard.getLaboratory());
        sample.setSampleType(null);
        sample.setSample(standardCode);
        sample.setModel(null);
        insSampleMapper.updateById(sample);
        insProductMapper.delete(Wrappers.<InsProduct>lambdaQuery().eq(InsProduct::getInsSampleId, sample.getId()));
        insOrderStandardTemplateMapper.delete(Wrappers.<InsOrderStandardTemplate>lambdaQuery()
                .eq(InsOrderStandardTemplate::getInsOrderId, currentOrder.getId()));
        insOrderTemplateDeviceMapper.delete(Wrappers.<InsOrderTemplateDevice>lambdaQuery()
                .eq(InsOrderTemplateDevice::getInsOrderId, currentOrder.getId()));
        int productSort = 0;
        for (TemplateInspectionItemDto item : templateItems) {
            InsProduct product = new InsProduct();
            product.setInsSampleId(sample.getId());
            product.setInspectionItem(item.getInspectionItem());
            product.setInspectionItemClass("");
            product.setInspectionItemSubclass("");
            product.setLaboratory(standard.getLaboratory());
            product.setSample(standardCode);
            product.setTemplateId(item.getTemplateId());
            product.setTemplateRowIndex(item.getTemplateRowIndex());
            product.setSonLaboratory(DIRECT_TEMPLATE_LABORATORY);
            product.setState(1);
            product.setSort(productSort++);
            insProductMapper.insert(product);
        }
        saveTemplateSnapshots(currentOrder.getId(), selections, bindingMap);
        String entrustCode = buildEntrustCode(order, Collections.singletonList(sample));
        assertEntrustCodeUnique(entrustCode, currentOrder.getId());
        insOrderMapper.update(null, Wrappers.<InsOrder>lambdaUpdate().eq(InsOrder::getId, currentOrder.getId())
                .set(InsOrder::getEntrustCode, entrustCode));
        applicationEventPublisher.publishEvent(new com.ruoyi.inspect.event.NewJuOrderPlacedEvent(currentOrder.getId()));
        return currentOrder.getId();
    }
    private void assertNewJuOrderEditable(Integer orderId) {
        Long receiptCount = insSampleReceiptMapper.selectCount(Wrappers.<InsSampleReceipt>lambdaQuery()
                .eq(InsSampleReceipt::getInsOrderId, orderId).eq(InsSampleReceipt::getStatus, 1));
        Long taskCount = insOrderStateService.count(Wrappers.<InsOrderState>lambdaQuery()
                .eq(InsOrderState::getInsOrderId, orderId));
        Long userCount = insSampleUserMapper.selectCount(Wrappers.<InsSampleUser>lambdaQuery()
                .eq(InsSampleUser::getInsSampleId, orderId));
        if (receiptCount > 0 || taskCount > 0 || userCount > 0) {
            throw new ErrorException("该委托已进入检验,不能编辑");
        }
    }
    private void saveTemplateSnapshots(Integer orderId, List<OrderTemplateSelectionDto> selections,
                                       Map<Integer, StandardTreeTemplateBindingVo> bindingMap) {
        for (OrderTemplateSelectionDto selection : selections) {
@@ -732,6 +923,36 @@
        map.put("sampleCode", orderSamples.isEmpty() ? null : orderSamples.get(0).getSampleCode());
        map.put("insOrderTemplate", map1);
        map.put("templateList", templateList);
        Set<Integer> orderTemplateIds = templateList.stream()
                .map(OrderTemplateSummaryVo::getTemplateId).collect(Collectors.toSet());
        List<Integer> methodIds = standardMethodService.list(Wrappers.<StandardMethod>lambdaQuery()
                        .eq(StandardMethod::getCode, insOrder.getSample())
                        .select(StandardMethod::getId)).stream()
                .map(StandardMethod::getId).collect(Collectors.toList());
        if (!methodIds.isEmpty() && !orderTemplateIds.isEmpty()) {
            List<StandardTree> candidates = standardTreeService.list(Wrappers.<StandardTree>lambdaQuery()
                    .in(StandardTree::getStandardMethodId, methodIds)
                    .isNull(StandardTree::getSampleType)
                    .isNull(StandardTree::getModel));
            Map<StandardTree, Set<Integer>> matches = candidates.stream()
                    .filter(tree -> StringUtils.isNotBlank(tree.getSample())
                            && Objects.equals(tree.getLaboratory(), insOrder.getLaboratory()))
                    .collect(Collectors.toMap(tree -> tree,
                            tree -> standardTreeService.selectTemplateBindings(tree.getId()).stream()
                                    .map(StandardTreeTemplateBindingVo::getTemplateId)
                                    .collect(Collectors.toSet())));
            matches.entrySet().removeIf(entry -> !entry.getValue().containsAll(orderTemplateIds));
            if (matches.size() == 1) {
                map.put("standardTreeId", matches.keySet().iterator().next().getId());
            } else if (matches.size() > 1) {
                List<StandardTree> exactMatches = matches.entrySet().stream()
                        .filter(entry -> entry.getValue().equals(orderTemplateIds))
                        .map(Map.Entry::getKey).collect(Collectors.toList());
                if (exactMatches.size() == 1) {
                    map.put("standardTreeId", exactMatches.get(0).getId());
                }
            }
        }
        return map;
    }
@@ -850,7 +1071,6 @@
                                .eq(InsOrderStandardTemplate::getInsOrderId, id)
                                .select(InsOrderStandardTemplate::getTemplateId,
                                        InsOrderStandardTemplate::getTemperature,
                                        InsOrderStandardTemplate::getHumidity,
                                        InsOrderStandardTemplate::getDetectionTime,
                                        InsOrderStandardTemplate::getDetectionPlace,
                                        InsOrderStandardTemplate::getInsResult)
@@ -860,7 +1080,6 @@
                    TemplateConditionVo condition = new TemplateConditionVo();
                    condition.setTemplateId(snapshot.getTemplateId());
                    condition.setTemperature(snapshot.getTemperature());
                    condition.setHumidity(snapshot.getHumidity());
                    condition.setDetectionTime(snapshot.getDetectionTime());
                    condition.setDetectionPlace(snapshot.getDetectionPlace());
                    condition.setInsResult(snapshot.getInsResult());
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsProductServiceImpl.java
@@ -77,8 +77,8 @@
            if (snapshot == null) {
                throw new ErrorException("未找到当前模板的原始记录快照");
            }
            boolean hasSnapshotField = dto.isTemperaturePresent() || dto.isHumidityPresent()
                    || dto.isDetectionTimePresent() || dto.isDetectionPlacePresent()
            boolean hasSnapshotField = dto.isTemperaturePresent() || dto.isDetectionTimePresent()
                    || dto.isDetectionPlacePresent()
                    || dto.isInsResultPresent();
            if (hasSnapshotField) {
                insOrderStandardTemplateMapper.update(null,
@@ -86,8 +86,6 @@
                                .eq(InsOrderStandardTemplate::getId, snapshot.getId())
                                .set(dto.isTemperaturePresent(),
                                        InsOrderStandardTemplate::getTemperature, dto.getTemperature())
                                .set(dto.isHumidityPresent(),
                                        InsOrderStandardTemplate::getHumidity, dto.getHumidity())
                                .set(dto.isDetectionTimePresent(),
                                        InsOrderStandardTemplate::getDetectionTime, dto.getDetectionTime())
                                .set(dto.isDetectionPlacePresent(),
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsReportServiceImpl.java
@@ -19,6 +19,8 @@
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.data.PictureRenderData;
import com.deepoove.poi.data.PictureType;
import com.deepoove.poi.data.Pictures;
import com.itextpdf.text.BadElementException;
import com.itextpdf.text.DocumentException;
@@ -26,7 +28,9 @@
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfStamper;
import com.ruoyi.basic.mapper.IfsInventoryQuantityMapper;
import com.ruoyi.basic.mapper.SealConfigMapper;
import com.ruoyi.basic.pojo.IfsInventoryQuantity;
import com.ruoyi.basic.pojo.SealConfig;
import com.ruoyi.basic.vo.IfsInventoryQuantityVO;
import com.ruoyi.common.config.WechatProperty;
import com.ruoyi.common.constant.InsOrderTypeConstants;
@@ -47,6 +51,7 @@
import com.ruoyi.inspect.service.InsReportService;
import com.ruoyi.inspect.service.RawMaterialOrderService;
import com.ruoyi.inspect.util.PreserveReportPicturePlaceholderHandler;
import com.ruoyi.inspect.util.ReportSealLayoutUtil;
import com.ruoyi.inspect.util.ReportSignaturePictureUtils;
import com.ruoyi.system.mapper.UserMapper;
import com.ruoyi.system.service.InformationNotificationService;
@@ -116,6 +121,8 @@
    private InsOrderMapper insOrderMapper;
    @Resource
    private IfsInventoryQuantityMapper ifsInventoryQuantityMapper;
    @Resource
    private SealConfigMapper sealConfigMapper;
    @Resource
    private InsUnqualifiedHandlerMapper insUnqualifiedHandlerMapper;
    @Resource
@@ -397,7 +404,8 @@
    //批准
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int ratifyReport(Integer id, Integer isRatify, String ratifyTell) {
    public int ratifyReport(Integer id, Integer isRatify, String ratifyTell, String sealIds,
                            Integer maSealId, Integer reportSealId, Integer seamSealId) {
        InsReport insReport = insReportMapper.selectById(id);
        if (Objects.equals(insReport.getExamineUserId(), SecurityUtils.getUserId().intValue())) {
            throw new ErrorException("审核人和批准人不能为同一人,请重新选择批准人");
@@ -471,33 +479,64 @@
        InsOrder order = insOrderMapper.selectById(insOrderId);
        boolean isRawMater = order.getTypeSource() != null && order.getTypeSource().equals(1);
        //获取场所的报告专用章
        String sealUrl;
        String laboratory = insOrderMapper.selectById(insReport.getInsOrderId()).getLaboratory();
        try {
            String type = "";
            if (isRawMater && order.getOrderType().equals(InsOrderTypeConstants.ENTER_THE_FACTORY)) {
                type = "进厂报告";
        // æ–°ç‰ˆæŒ‰ç”¨é€”选择:MA章、报告章(模板 seal1/seal2)、PDF骑缝章。
        // å…¼å®¹æ—§å‰ç«¯ï¼šåªä¼  sealIds æ—¶ï¼Œç¬¬ä¸€æžšç« ç»§ç»­ä½œä¸ºæŠ¥å‘Šç« å’Œéª‘缝章,MA章留空。
        SealConfig maSeal;
        SealConfig reportSeal;
        SealConfig seamSeal;
        boolean positionMode = maSealId != null || reportSealId != null || seamSealId != null;
        if (positionMode) {
            maSeal = resolveSeal(maSealId);
            reportSeal = resolveSeal(reportSealId);
            seamSeal = resolveSeal(seamSealId);
            } else {
                type = "委托报告";
            SealConfig legacySeal = resolveSeals(sealIds).get(0);
            maSeal = null;
            reportSeal = legacySeal;
            seamSeal = legacySeal;
            }
            sealUrl = insReportMapper.getLaboratoryByName(laboratory, type);
        } catch (Exception e) {
            throw new ErrorException(laboratory + "找不到报告专用章");
        LinkedHashMap<Integer, SealConfig> chosenById = new LinkedHashMap<>();
        Arrays.asList(maSeal, reportSeal, seamSeal).stream()
                .filter(Objects::nonNull)
                .forEach(seal -> chosenById.put(seal.getId(), seal));
        List<SealConfig> chosenSeals = new ArrayList<>(chosenById.values());
        if (chosenSeals.isEmpty()) {
            throw new ErrorException("请至少选择一种报告印章");
        }
        if (StringUtils.isBlank(sealUrl)) {
            throw new ErrorException(laboratory + "找不到报告专用章");
        }
        insReport.setSealIds(chosenSeals.stream().map(s -> String.valueOf(s.getId())).collect(Collectors.joining(",")));
        insReport.setSealNames(chosenSeals.stream().map(SealConfig::getName).collect(Collectors.joining(",")));
        insReport.setMaSealId(maSeal == null ? null : maSeal.getId());
        insReport.setMaSealName(maSeal == null ? null : maSeal.getName());
        insReport.setReportSealId(reportSeal == null ? null : reportSeal.getId());
        insReport.setReportSealName(reportSeal == null ? null : reportSeal.getName());
        insReport.setSeamSealId(seamSeal == null ? null : seamSeal.getId());
        insReport.setSeamSealName(seamSeal == null ? null : seamSeal.getName());
        String primaryReportUrl = getPrimaryReportUrl(insReport);
        String finalUrl = primaryReportUrl.replace("/word", wordUrl);
        Map<String, Object> ratifyMarks = new HashMap<String, Object>() {{
            put("ratifyUrl", ReportSignaturePictureUtils.create(imgUrl, signatureUrl));
            put("ratifyDateUrl", Pictures.ofStream(DateImageUtil.createDateImage(null)).create());
            put("seal1", Pictures.ofLocal(imgUrl + "/" + sealUrl).create());
            put("seal2", Pictures.ofLocal(imgUrl + "/" + sealUrl).create());
        }};
        writeMarksToReportFiles(insReport, ratifyMarks);
        Map<String, Object> ratifyMarks = new HashMap<>();
        ratifyMarks.put("ratifyUrl", ReportSignaturePictureUtils.create(imgUrl, signatureUrl));
        ratifyMarks.put("ratifyDateUrl", Pictures.ofStream(DateImageUtil.createDateImage(null)).create());
        if (reportSeal != null) {
            ratifyMarks.put("seal1", createSealPicture(reportSeal.getAddress()));
            ratifyMarks.put("seal2", createSealPicture(reportSeal.getAddress()));
        }
        writeApprovalMarksToReportFiles(insReport, ratifyMarks);
        // å°ç« å¿…须使用浮动定位,否则行内图片会撑高段落/表格并改变原报告分页。
        for (String reportUrl : getApprovalReportUrls(insReport)) {
            try {
                ReportSealLayoutUtil.apply(
                        reportUrl.replace("/word", wordUrl),
                        reportSeal == null ? null : Paths.get(imgUrl, reportSeal.getAddress()).toString(),
                        maSeal == null ? null : Paths.get(imgUrl, maSeal.getAddress()).toString());
            } catch (Exception e) {
                log.error("报告印章浮动定位失败: {}", reportUrl, e);
                throw new ErrorException("报告盖章失败,请检查印章图片和报告文件");
            }
        }
        // ä¿®æ”¹ä¸´æ—¶pdf
        insReport.setTempUrlPdf(primaryReportUrl.replace(".docx", ".pdf"));
@@ -508,7 +547,9 @@
        insOrderMapper.updateById(insOrder);
        for (String reportUrl : getApprovalReportUrls(insReport)) {
            wordToPdf(reportUrl.replace("/word", wordUrl), sealUrl,
            wordToPdf(reportUrl.replace("/word", wordUrl),
                    null,
                    seamSeal == null ? null : seamSeal.getAddress(),
                    isRawMater && order.getOrderType().equals(InsOrderTypeConstants.ENTER_THE_FACTORY));
        }
@@ -539,10 +580,97 @@
        return insReportMapper.updateById(insReport);
    }
    /**
     * è§£æžæ‰¹å‡†æ—¶é€‰æ‹©çš„印章(逗号分隔的印章id)。
     * æœªä¼ æˆ–全部无效时回退到第一个启用印章(默认“1号章”)。
     */
    private List<SealConfig> resolveSeals(String sealIds) {
        List<SealConfig> chosen = new ArrayList<>();
        if (StringUtils.isNotBlank(sealIds)) {
            List<Integer> ids = new ArrayList<>();
            for (String s : sealIds.split(",")) {
                if (StringUtils.isNotBlank(s)) {
                    ids.add(Integer.valueOf(s.trim()));
                }
            }
            if (!ids.isEmpty()) {
                Map<Integer, SealConfig> byId = sealConfigMapper.selectBatchIds(ids).stream()
                        .collect(Collectors.toMap(SealConfig::getId, x -> x));
                for (Integer sid : ids) {
                    SealConfig seal = byId.get(sid);
                    if (seal != null && Objects.equals(seal.getStatus(), 1)
                            && StringUtils.isNotBlank(seal.getAddress())) {
                        chosen.add(seal);
                    }
                }
            }
        }
        if (chosen.isEmpty()) {
            List<SealConfig> enabled = sealConfigMapper.selectList(Wrappers.<SealConfig>lambdaQuery()
                    .eq(SealConfig::getStatus, 1).orderByAsc(SealConfig::getCode));
            if (!enabled.isEmpty()) {
                chosen.add(enabled.get(0));
            }
        }
        if (chosen.isEmpty()) {
            throw new ErrorException("未找到可用印章,请先在印章管理中维护并启用印章");
        }
        return chosen;
    }
    private SealConfig resolveSeal(Integer sealId) {
        if (sealId == null) {
            return null;
        }
        SealConfig seal = sealConfigMapper.selectById(sealId);
        if (seal == null) {
            throw new ErrorException("所选印章不存在,请重新选择");
        }
        if (!Objects.equals(seal.getStatus(), 1)) {
            throw new ErrorException("印章“" + seal.getName() + "”已停用,请重新选择");
        }
        if (StringUtils.isBlank(seal.getAddress())) {
            throw new ErrorException("印章“" + seal.getName() + "”未上传图片,请重新选择");
        }
        return seal;
    }
    /**
     * æŒ‰å›¾ç‰‡å†…容识别印章格式,兼容历史数据中扩展名被保存为“_png”等非标准文件名的情况。
     */
    private PictureRenderData createSealPicture(String sealUrl) {
        Path sealPath = Paths.get(imgUrl, sealUrl);
        if (!Files.isRegularFile(sealPath)) {
            throw new ErrorException("印章图片文件不存在,请重新上传印章图片");
        }
        try {
            byte[] imageBytes = Files.readAllBytes(sealPath);
            PictureType pictureType = PictureType.suggestFileType(imageBytes);
            return Pictures.ofBytes(imageBytes, pictureType).create();
        } catch (Exception e) {
            log.error("读取印章图片失败: {}", sealPath, e);
            throw new ErrorException("印章图片无法识别,请重新上传 jpg æˆ– png å›¾ç‰‡");
        }
    }
    @Override
    public int wordInsertUrl(Map<String, Object> map, String url) {
        Configure configure = Configure.builder()
                .setValidErrorHandler(new PreserveReportPicturePlaceholderHandler())
                .build();
        XWPFTemplate template = XWPFTemplate.compile(url, configure).render(map);
        try {
            template.writeAndClose(Files.newOutputStream(Paths.get(url)));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return 1;
    }
    private int wordInsertApprovalMarks(Map<String, Object> map, String url) {
        Configure configure = Configure.builder()
                .setValidErrorHandler(new PreserveReportPicturePlaceholderHandler(
                        new HashSet<>(Arrays.asList("seal1", "seal2"))))
                .build();
        XWPFTemplate template = XWPFTemplate.compile(url, configure).render(map);
        try {
@@ -571,6 +699,12 @@
    private void writeMarksToReportFiles(InsReport report, Map<String, Object> marks) {
        for (String reportUrl : getApprovalReportUrls(report)) {
            wordInsertUrl(marks, reportUrl.replace("/word", wordUrl));
        }
    }
    private void writeApprovalMarksToReportFiles(InsReport report, Map<String, Object> marks) {
        for (String reportUrl : getApprovalReportUrls(report)) {
            wordInsertApprovalMarks(marks, reportUrl.replace("/word", wordUrl));
        }
    }
@@ -775,16 +909,17 @@
    }
    @Override
    public void wordToPdf(String path, String sealUrl, boolean isRawMater) {
    public void wordToPdf(String path, String maSealUrl, String seamSealUrl, boolean isRawMater) {
        try {
            wordToPdf(path, path.replace(".docx", ".pdf"), sealUrl, isRawMater);
            wordToPdf(path, path.replace(".docx", ".pdf"), maSealUrl, seamSealUrl, isRawMater);
        } catch (Exception e) {
            throw new ErrorException("转换失败");
        }
    }
    public String wordToPdf(String wordPath, String pdfPath, String sealUrl, boolean isRawMater) {
    public String wordToPdf(String wordPath, String pdfPath, String maSealUrl,
                            String seamSealUrl, boolean isRawMater) {
        FileOutputStream os = null;
        try {
            //凭证 ä¸ç„¶åˆ‡æ¢åŽæœ‰æ°´å°
@@ -795,27 +930,31 @@
                log.info("License验证不通过...");
                return null;
            }
            //生成一个空的PDF文件
            File file;
            //判断是否是进厂报告
            if (!isRawMater) {
                file = new File(pdfPath.replace(".pdf", "-1.pdf"));
            } else {
                file = new File(pdfPath.replace(".pdf", ".pdf"));
            }
            String effectiveSeamSealUrl = isRawMater ? null : seamSealUrl;
            boolean addPdfMarks = StringUtils.isNotBlank(maSealUrl)
                    || StringUtils.isNotBlank(effectiveSeamSealUrl);
            String sourcePdfPath = addPdfMarks ? pdfPath.replace(".pdf", "-stamp-source.pdf") : pdfPath;
            File file = new File(sourcePdfPath);
            os = new FileOutputStream(file);
            //要转换的word文件
            com.aspose.words.Document doc = new com.aspose.words.Document(wordPath);
            doc.save(os, SaveFormat.PDF);
            os.close();
            os = null;
            //添加骑缝章
            if (!isRawMater) {
                stamperCheckMarkPDF(pdfPath.replace(".pdf", "-1.pdf"), pdfPath, imgUrl + "/" + sealUrl);
            // MA章固定在第1页左上角;骑缝章按页切片固定在右侧中部。
            if (addPdfMarks) {
                stamperReportMarksPDF(
                        sourcePdfPath,
                        pdfPath,
                        StringUtils.isBlank(maSealUrl) ? null : Paths.get(imgUrl, maSealUrl).toString(),
                        StringUtils.isBlank(effectiveSeamSealUrl)
                                ? null : Paths.get(imgUrl, effectiveSeamSealUrl).toString());
            }
            return new File(pdfPath).getName();
        } catch (Exception e) {
            e.printStackTrace();
            log.error("生成盖章PDF失败: {}", wordPath, e);
            throw new ErrorException("报告转PDF或盖章失败");
        } finally {
            if (os != null) {
                try {
@@ -825,7 +964,6 @@
                }
            }
        }
        return null;
    }
    /**
@@ -850,7 +988,8 @@
                subImg = img.getSubimage(i * sw, 0, sw, h);
            }
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            ImageIO.write(subImg, Path.substring(Path.lastIndexOf('.') + 1), out);
            // ç»Ÿä¸€ç¼–码为 PNG,兼容历史“_png”等没有标准扩展名的图片文件。
            ImageIO.write(subImg, "png", out);
            nImage[i] = com.itextpdf.text.Image.getInstance(out.toByteArray());
        }
@@ -858,33 +997,46 @@
    }
    /**
     * ç›–骑缝章
     * æŒ‰å‚考报告盖 PDF æ ‡å¿—章:MA章位于第1页左上角,骑缝章位于每页右侧中部。
     *
     * @param infilePath  åŽŸPDF路径
     * @param outFilePath è¾“出PDF路径
     */
    public static void stamperCheckMarkPDF(String infilePath, String outFilePath, String picPath) throws IOException, DocumentException {
    public static void stamperReportMarksPDF(String infilePath, String outFilePath,
                                             String maPicPath, String seamPicPath)
            throws IOException, DocumentException {
        PdfReader reader = new PdfReader(infilePath);//选择需要印章的pdf
        PdfStamper stamp = new PdfStamper(reader, new FileOutputStream(outFilePath));//加完印章后的pdf
        com.itextpdf.text.Rectangle pageSize = reader.getPageSize(1);//获得第一页
        float height = pageSize.getHeight();
        float width = pageSize.getWidth();
        int nums = reader.getNumberOfPages();
        com.itextpdf.text.Image[] nImage = slicingImages(picPath, nums);//生成骑缝章切割图片
        boolean hasMaSeal = maPicPath != null && !maPicPath.trim().isEmpty();
        boolean hasSeamSeal = seamPicPath != null && !seamPicPath.trim().isEmpty();
        com.itextpdf.text.Image[] seamImages = hasSeamSeal ? slicingImages(seamPicPath, nums) : null;
        for (int n = 1; n <= nums; n++) {
            com.itextpdf.text.Rectangle pageSize = reader.getPageSize(n);
            float height = pageSize.getHeight();
            float width = pageSize.getWidth();
            PdfContentByte over = stamp.getOverContent(n);//设置在第几页打印印章
            com.itextpdf.text.Image img = nImage[n - 1];//选择图片
            if (n == 1 && hasMaSeal) {
                com.itextpdf.text.Image maImage = com.itextpdf.text.Image.getInstance(maPicPath);
                maImage.scaleToFit(100f, 90f);
                maImage.setAbsolutePosition(30f, height - maImage.getScaledHeight() - 30f);
                over.addImage(maImage);
            }
            if (hasSeamSeal) {
                com.itextpdf.text.Image seamImage = seamImages[n - 1];
            float newHeight = 100f;
            float newWidth = img.getWidth() / (img.getHeight() / 100);
            img.scaleAbsolute(newWidth, newHeight);//控制图片大小
            img.setAbsolutePosition(width - newWidth, height / 2 - newHeight / 2);//控制图片位置
            over.addImage(img);
                float newWidth = seamImage.getWidth() / (seamImage.getHeight() / newHeight);
                seamImage.scaleAbsolute(newWidth, newHeight);
                seamImage.setAbsolutePosition(width - newWidth, height / 2 - newHeight / 2);
                over.addImage(seamImage);
            }
        }
        stamp.close();
        reader.close();
    }
    @Override
@@ -1018,20 +1170,22 @@
    @Transactional(rollbackFor = Exception.class)
    public boolean sendBackTask(Integer id) {
        InsReport report = insReportMapper.selectById(id);
        // æ ¹æ®è®¢å•查询试验室
        String laboratory = insOrderMapper.selectLaboratoryByOrderId(report.getInsOrderId());
        if (report == null || report.getInsOrderId() == null) {
            throw new ErrorException("报告不存在或未关联检验单");
        }
        // ä¿®æ”¹è®¢å•状态
        // æŠ¥å‘Šç”±è®¢å•下各试验室共同形成;退回时全部恢复为待复核,避免只随机退回一个试验室。
        insOrderStateMapper.update(null, Wrappers.<InsOrderState>lambdaUpdate()
                .eq(InsOrderState::getInsOrderId, report.getInsOrderId())
                .eq(InsOrderState::getLaboratory, laboratory)
                .set(InsOrderState::getInsState, 4));
        Integer insSampleId = insSampleUserMapper.selectOne(Wrappers.<InsSampleUser>lambdaQuery()
        InsSampleUser sampleUser = insSampleUserMapper.selectOne(Wrappers.<InsSampleUser>lambdaQuery()
                .eq(InsSampleUser::getInsSampleId, report.getInsOrderId())
                .orderByDesc(InsSampleUser::getId)
                .last("limit 1")).getId();
        insSampleUserMapper.deleteById(insSampleId);
                .last("limit 1"));
        if (sampleUser != null) {
            insSampleUserMapper.deleteById(sampleUser.getId());
        }
        // ä¿®æ”¹æŠ¥å‘Šè¡Œä¸ºæœªæ˜¾ç¤º
        insReportMapper.update(null, Wrappers.<InsReport>lambdaUpdate()
inspect-server/src/main/java/com/ruoyi/inspect/util/PreserveReportPicturePlaceholderHandler.java
@@ -5,6 +5,7 @@
import com.deepoove.poi.template.ElementTemplate;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
@@ -27,13 +28,23 @@
    ));
    private final Configure.ClearHandler clearHandler = new Configure.ClearHandler();
    private final Set<String> clearPictureTags;
    public PreserveReportPicturePlaceholderHandler() {
        this(Collections.emptySet());
    }
    public PreserveReportPicturePlaceholderHandler(Set<String> clearPictureTags) {
        this.clearPictureTags = clearPictureTags == null ? Collections.emptySet() : clearPictureTags;
    }
    @Override
    public void handler(RenderContext<?> context) {
        ElementTemplate template = context.getEleTemplate();
        if (template != null
                && Character.valueOf('@').equals(template.getSign())
                && DEFERRED_PICTURE_TAGS.contains(template.getTagName())) {
                && DEFERRED_PICTURE_TAGS.contains(template.getTagName())
                && !clearPictureTags.contains(template.getTagName())) {
            return;
        }
        clearHandler.handler(context);
inspect-server/src/main/java/com/ruoyi/inspect/util/ReportSealLayoutUtil.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,302 @@
package com.ruoyi.inspect.util;
import com.aspose.words.Document;
import com.aspose.words.LayoutCollector;
import com.aspose.words.LayoutEnumerator;
import com.aspose.words.License;
import com.aspose.words.Node;
import com.aspose.words.NodeCollection;
import com.aspose.words.NodeType;
import com.aspose.words.PageSetup;
import com.aspose.words.Paragraph;
import com.aspose.words.RelativeHorizontalPosition;
import com.aspose.words.RelativeVerticalPosition;
import com.aspose.words.SaveFormat;
import com.aspose.words.Section;
import com.aspose.words.Shape;
import com.aspose.words.ShapeType;
import com.aspose.words.WrapType;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.awt.geom.Rectangle2D;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
 * å°†æŠ¥å‘Šä¸­çš„印章改为浮动图片,避免图片参与正文行高计算而改变原报告分页。
 */
public final class ReportSealLayoutUtil {
    private static final double MA_LEFT = 30D;
    private static final double MA_TOP = 30D;
    private static final double MA_MAX_WIDTH = 90D;
    private static final double MA_MAX_HEIGHT = 80D;
    private static final double REPORT_MAX_WIDTH = 105D;
    private static final double REPORT_MAX_HEIGHT = 105D;
    private static final double LAST_PAGE_MAX_WIDTH = 110D;
    private static final double LAST_PAGE_MAX_HEIGHT = 110D;
    private static final double POSITION_GAP = 14D;
    private ReportSealLayoutUtil() {
    }
    public static void apply(String wordPath, String reportSealPath, String maSealPath) throws Exception {
        applyAsposeLicense();
        Document document = new Document(wordPath);
        byte[] reportSealBytes = readImage(reportSealPath);
        byte[] maSealBytes = readImage(maSealPath);
        if (reportSealBytes != null) {
            removeMatchingImages(document, reportSealBytes);
            document.updatePageLayout();
            addCoverReportSeal(document, reportSealPath);
            addConclusionReportSeal(document, reportSealPath);
            addLastPageReportSeal(document, reportSealPath);
        }
        if (maSealBytes != null) {
            removeMatchingImages(document, maSealBytes);
            document.updatePageLayout();
            // reportSeal ä¸Ž maSeal å¯èƒ½é€‰æ‹©åŒä¸€å¼ å›¾ç‰‡ï¼Œç§»é™¤åŽéœ€è¦æ¢å¤ä¸‰ä¸ªæŠ¥å‘Šç« ä½ç½®ã€‚
            if (reportSealBytes != null && Arrays.equals(reportSealBytes, maSealBytes)) {
                addCoverReportSeal(document, reportSealPath);
                addConclusionReportSeal(document, reportSealPath);
                addLastPageReportSeal(document, reportSealPath);
            }
            addFirstPageMaSeal(document, maSealPath);
        }
        document.updatePageLayout();
        document.save(wordPath, SaveFormat.DOCX);
    }
    private static void applyAsposeLicense() throws Exception {
        try (InputStream inputStream = ReportSealLayoutUtil.class.getResourceAsStream("/lib/license.xml")) {
            if (inputStream == null) {
                throw new IllegalStateException("未找到 Aspose æŽˆæƒæ–‡ä»¶");
            }
            License license = new License();
            license.setLicense(inputStream);
            if (!license.getIsLicensed()) {
                throw new IllegalStateException("Aspose æŽˆæƒéªŒè¯å¤±è´¥");
            }
        }
    }
    private static byte[] readImage(String imagePath) throws Exception {
        if (imagePath == null || imagePath.trim().isEmpty()) {
            return null;
        }
        Path path = java.nio.file.Paths.get(imagePath);
        return Files.isRegularFile(path) ? Files.readAllBytes(path) : null;
    }
    private static void removeMatchingImages(Document document, byte[] imageBytes) throws Exception {
        for (Shape shape : imageShapes(document)) {
            if (sameImage(shape, imageBytes)) {
                shape.remove();
            }
        }
    }
    private static void addFirstPageMaSeal(Document document, String imagePath) throws Exception {
        Paragraph anchor = firstBodyParagraph(document);
        double[] size = fitImage(imagePath, MA_MAX_WIDTH, MA_MAX_HEIGHT);
        Shape shape = createFloatingImage(document, imagePath, size[0], size[1]);
        shape.setLeft(MA_LEFT);
        shape.setTop(MA_TOP);
        anchor.appendChild(shape);
    }
    private static void addCoverReportSeal(Document document, String imagePath) throws Exception {
        Paragraph anchor = firstBodyParagraph(document);
        PageSetup pageSetup = document.getFirstSection().getPageSetup();
        double[] size = fitImage(imagePath, REPORT_MAX_WIDTH, REPORT_MAX_HEIGHT);
        Shape shape = createFloatingImage(document, imagePath, size[0], size[1]);
        shape.setLeft(pageSetup.getLeftMargin() + 20D);
        shape.setTop(pageSetup.getPageHeight() - pageSetup.getBottomMargin()
                - size[1] - POSITION_GAP);
        shape.setBehindText(true);
        anchor.appendChild(shape);
    }
    private static void addConclusionReportSeal(Document document, String imagePath) throws Exception {
        document.updatePageLayout();
        LayoutCollector collector = new LayoutCollector(document);
        Paragraph anchor = findBodyParagraphContaining(document, "盖章", collector, null);
        if (anchor == null) {
            return;
        }
        Rectangle2D rectangle = getLayoutRectangle(document, collector, anchor);
        Section section = (Section) anchor.getAncestor(NodeType.SECTION);
        PageSetup pageSetup = section.getPageSetup();
        int page = collector.getStartPageIndex(anchor);
        Paragraph placementAnchor = findPageBodyAnchor(document, collector, page);
        if (placementAnchor == null) {
            placementAnchor = anchor;
        }
        double[] size = fitImage(imagePath, REPORT_MAX_WIDTH, REPORT_MAX_HEIGHT);
        Shape shape = createFloatingImage(document, imagePath, size[0], size[1]);
        shape.setLeft((pageSetup.getPageWidth() - size[0]) / 2D);
        double preferredTop = rectangle == null
                ? pageSetup.getPageHeight() - pageSetup.getBottomMargin() - size[1] - 120D
                : rectangle.getY() - size[1] + rectangle.getHeight() / 2D;
        shape.setTop(clamp(preferredTop,
                pageSetup.getTopMargin(),
                pageSetup.getPageHeight() - pageSetup.getBottomMargin() - size[1]));
        // ç»“论章位于表格单元格内,必须置于文字上方,否则会被单元格底纹遮住。
        shape.setBehindText(false);
        placementAnchor.appendChild(shape);
    }
    private static void addLastPageReportSeal(Document document, String imagePath) throws Exception {
        int lastPage = document.getPageCount();
        LayoutCollector collector = new LayoutCollector(document);
        Paragraph anchor = findLastPageAnchor(document, collector, lastPage);
        if (anchor == null) {
            return;
        }
        Section section = (Section) anchor.getAncestor(NodeType.SECTION);
        PageSetup pageSetup = section.getPageSetup();
        double[] size = fitImage(imagePath, LAST_PAGE_MAX_WIDTH, LAST_PAGE_MAX_HEIGHT);
        Shape shape = createFloatingImage(document, imagePath, size[0], size[1]);
        shape.setLeft((pageSetup.getPageWidth() - size[0]) / 2D);
        Rectangle2D rectangle = getLayoutRectangle(document, collector, anchor);
        double preferredTop = rectangle == null
                ? pageSetup.getPageHeight() - pageSetup.getBottomMargin() - size[1] - POSITION_GAP
                : rectangle.getY() + rectangle.getHeight() + POSITION_GAP;
        shape.setTop(clamp(preferredTop,
                pageSetup.getTopMargin(),
                pageSetup.getPageHeight() - pageSetup.getBottomMargin() - size[1]));
        shape.setBehindText(true);
        anchor.appendChild(shape);
    }
    private static Shape createFloatingImage(Document document, String imagePath,
                                              double width, double height) throws Exception {
        Shape shape = new Shape(document, ShapeType.IMAGE);
        shape.getImageData().setImage(imagePath);
        shape.setWidth(width);
        shape.setHeight(height);
        shape.setWrapType(WrapType.NONE);
        shape.setRelativeHorizontalPosition(RelativeHorizontalPosition.PAGE);
        shape.setRelativeVerticalPosition(RelativeVerticalPosition.PAGE);
        shape.setAllowOverlap(true);
        return shape;
    }
    private static Paragraph firstBodyParagraph(Document document) {
        Paragraph paragraph = document.getFirstSection().getBody().getFirstParagraph();
        if (paragraph != null) {
            return paragraph;
        }
        return document.getFirstSection().getBody().appendParagraph("");
    }
    private static Paragraph findLastPageAnchor(Document document, LayoutCollector collector, int lastPage)
            throws Exception {
        Paragraph fallback = null;
        NodeCollection paragraphs = document.getChildNodes(NodeType.PARAGRAPH, true);
        for (int i = paragraphs.getCount() - 1; i >= 0; i--) {
            Paragraph paragraph = (Paragraph) paragraphs.get(i);
            if (paragraph.getAncestor(NodeType.BODY) == null) {
                continue;
            }
            if (collector.getStartPageIndex(paragraph) != lastPage) {
                continue;
            }
            if (fallback == null) {
                fallback = paragraph;
            }
            if (paragraph.getText().contains("报告结束")) {
                return paragraph;
            }
        }
        return fallback;
    }
    private static Paragraph findBodyParagraphContaining(Document document, String text,
                                                          LayoutCollector collector, Integer page)
            throws Exception {
        NodeCollection paragraphs = document.getChildNodes(NodeType.PARAGRAPH, true);
        for (int i = 0; i < paragraphs.getCount(); i++) {
            Paragraph paragraph = (Paragraph) paragraphs.get(i);
            if (paragraph.getAncestor(NodeType.BODY) == null || !paragraph.getText().contains(text)) {
                continue;
            }
            if (page == null || collector.getStartPageIndex(paragraph) == page) {
                return paragraph;
            }
        }
        return null;
    }
    private static Paragraph findPageBodyAnchor(Document document, LayoutCollector collector, int page)
            throws Exception {
        NodeCollection paragraphs = document.getChildNodes(NodeType.PARAGRAPH, true);
        for (int i = 0; i < paragraphs.getCount(); i++) {
            Paragraph paragraph = (Paragraph) paragraphs.get(i);
            if (paragraph.getAncestor(NodeType.BODY) == null
                    || paragraph.getAncestor(NodeType.CELL) != null) {
                continue;
            }
            if (collector.getStartPageIndex(paragraph) == page) {
                return paragraph;
            }
        }
        return null;
    }
    private static Rectangle2D getLayoutRectangle(Document document, LayoutCollector collector,
                                                   Paragraph paragraph) throws Exception {
        Object entity = collector.getEntity(paragraph);
        if (entity == null) {
            NodeCollection runs = paragraph.getChildNodes(NodeType.RUN, true);
            for (int i = 0; i < runs.getCount() && entity == null; i++) {
                entity = collector.getEntity(runs.get(i));
            }
        }
        if (entity == null) {
            return null;
        }
        LayoutEnumerator enumerator = new LayoutEnumerator(document);
        enumerator.setCurrent(entity);
        return enumerator.getRectangle();
    }
    private static double clamp(double value, double min, double max) {
        return Math.max(min, Math.min(value, max));
    }
    private static List<Shape> imageShapes(Document document) throws Exception {
        List<Shape> result = new ArrayList<>();
        NodeCollection nodes = document.getChildNodes(NodeType.SHAPE, true);
        for (int i = 0; i < nodes.getCount(); i++) {
            Shape shape = (Shape) nodes.get(i);
            if (shape.hasImage()) {
                result.add(shape);
            }
        }
        return result;
    }
    private static boolean sameImage(Shape shape, byte[] imageBytes) throws Exception {
        return Arrays.equals(shape.getImageData().toByteArray(), imageBytes);
    }
    private static double[] fitImage(String imagePath, double maxWidth, double maxHeight) throws Exception {
        BufferedImage image = ImageIO.read(new ByteArrayInputStream(Files.readAllBytes(
                java.nio.file.Paths.get(imagePath))));
        if (image == null || image.getWidth() <= 0 || image.getHeight() <= 0) {
            return new double[]{maxWidth, maxHeight};
        }
        double scale = Math.min(maxWidth / image.getWidth(), maxHeight / image.getHeight());
        return new double[]{image.getWidth() * scale, image.getHeight() * scale};
    }
}
inspect-server/src/main/java/com/ruoyi/inspect/vo/InsOrderPrintingVo.java
@@ -4,7 +4,7 @@
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.time.LocalDateTime;
import java.time.LocalDate;
/**
 * @Author zhuo
@@ -25,9 +25,9 @@
    @ApiModelProperty("规格型号")
    private String sampleModel;
    @ApiModelProperty("委托日期")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime sendTime;
    @ApiModelProperty("送样日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate sampleDate;
    @ApiModelProperty("委托人")
    private String prepareUser;
inspect-server/src/main/java/com/ruoyi/inspect/vo/TemplateConditionVo.java
@@ -18,9 +18,6 @@
    @ApiModelProperty("温度")
    private String temperature;
    @ApiModelProperty("湿度")
    private String humidity;
    @ApiModelProperty("检测日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate detectionTime;
inspect-server/src/main/resources/mapper/InsOrderMapper.xml
@@ -246,6 +246,7 @@
               en_device_name,
               specification_model,
               management_number,
               factory_no,
               date_format(activation_date,'%Y-%m-%d')
        latest_traceability
        from device
@@ -416,7 +417,7 @@
        from (select io2.id insOrderId,
                     io2.sample_view,
                     io2.production,
                     io2.send_time,
                     io2.sample_date,
                     GROUP_CONCAT(DISTINCT isa.model SEPARATOR ' ') AS sample_model,
                     io2.prepare_user,
                     io2.entrust_code,
inspect-server/src/main/resources/mapper/InsReportMapper.xml
@@ -89,6 +89,9 @@
        case when type_source = 0 then '成品下单' else '原材料下单' end                    type_source,
        case
        when order_type = '抽检' then '抽检'
        when order_type = 'Commissioned testing' then '委托检测'
        when order_type = 'Inter-laboratory comparison' then '实验室间比对'
        when order_type = 'Proficiency testing' then '能力验证'
        when order_type = 'Customer-ordered test' then '委托检验'
        when order_type = '进厂检验' then '进厂检验'
        when order_type = 'Quarterly inspection' then '季度检验'
inspect-server/src/main/resources/static/detection-report-template-v2.docx
Binary files differ
inspect-server/src/main/resources/static/inspection-report-template-v2.docx
Binary files differ
ruoyi-admin/src/main/resources/application-xjxj.yml
@@ -74,7 +74,7 @@
#    port: 6379
    port: 11102
    # æ•°æ®åº“索引
    database: 0
    database: 6
    #    # å¯†ç 
    password:
    # è¿žæŽ¥è¶…æ—¶æ—¶é—´
ruoyi-admin/src/main/resources/application.yml
@@ -16,7 +16,9 @@
# å¼€å‘环境配置
server:
  # æœåŠ¡å™¨çš„HTTP端口
  port: 8001
  # æœåŠ¡å™¨ç«¯å£
#  port: 9509
  port: 9509
  servlet:
    # åº”用的访问路径
    context-path: /lims
@@ -55,9 +57,9 @@
  servlet:
    multipart:
      # å•个文件大小
      max-file-size: 10MB
      max-file-size: 200MB
      # è®¾ç½®æ€»ä¸Šä¼ çš„æ–‡ä»¶å¤§å°
      max-request-size: 20MB
      max-request-size: 400MB
  # æœåŠ¡æ¨¡å—
#  devtools:
#    restart:
ruoyi-common/src/main/java/com/ruoyi/common/constant/InsOrderTypeConstants.java
@@ -7,6 +7,12 @@
 * @Date 2024/8/11
 */
public class InsOrderTypeConstants {
    // å§”托检测
    public static final String COMMISSIONED_TEST = "Commissioned testing";
    // å®žéªŒå®¤é—´æ¯”对
    public static final String INTER_LABORATORY_COMPARISON = "Inter-laboratory comparison";
    // èƒ½åŠ›éªŒè¯
    public static final String PROFICIENCY_TESTING = "Proficiency testing";
    // æ£€éªŒç±»åž‹
    public static final String SPOT_CHECK = "抽检";
    // å§”托检验
ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/PersonalScopeAspect.java
@@ -41,6 +41,11 @@
        // èŽ·å–ç›®æ ‡æ–¹æ³•ä¸Šçš„æ³¨è§£
        PersonalScope annotation = signature.getMethod().getAnnotation(PersonalScope.class);
        // è¶…级管理员查看报告编制时不限制为“仅看我”
        if (SecurityUtils.isAdmin(userId) && "business:reportPreparation".equals(annotation.permsName())) {
            return;
        }
        // èŽ·å–æƒé™åˆ¤æ–­æ˜¯å¦æ˜¯ä»…çœ‹æˆ‘
        SysRoleVo sysRoleVo = sysRoleMenuMapper.selectRoleMenu(annotation.permsName(), userId);