Pārlūkot izejas kodu

样本送检-宠物食品检测Excel附件生成

陈长荣 3 nedēļas atpakaļ
vecāks
revīzija
2818713bdf

+ 247 - 6
jfcloud-gene-biz/src/main/java/com/github/jfcloud/gene/sample/service/biz/SampleFoodServiceImpl.java

@@ -1,15 +1,28 @@
 package com.github.jfcloud.gene.sample.service.biz;
 
 import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.date.DatePattern;
 import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.resource.ResourceUtil;
 import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.RandomUtil;
+import cn.hutool.crypto.digest.DigestUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.github.jfcloud.admin.api.sys.dto.message.MessageUserDTO;
+import com.github.jfcloud.common.holder.RequestHolder;
+import com.github.jfcloud.gene.cache.UserIdNameCache;
+import com.github.jfcloud.gene.common.constant.StrConstant;
 import com.github.jfcloud.gene.common.util.CustomIdGenerator;
 import com.github.jfcloud.gene.common.util.UserUtil;
 import com.github.jfcloud.gene.constants.GeneStatusEnum;
+import com.github.jfcloud.gene.file.service.FileInfoService;
+import com.github.jfcloud.gene.file.vo.FileVo;
+import com.github.jfcloud.gene.flow.entity.FlowAudit;
+import com.github.jfcloud.gene.flow.entity.FlowFileVersion;
+import com.github.jfcloud.gene.flow.service.FlowAuditService;
 import com.github.jfcloud.gene.flow.service.NotifyService;
 import com.github.jfcloud.gene.sample.entity.SampleFood;
 import com.github.jfcloud.gene.sample.entity.SampleFoodDetail;
@@ -23,14 +36,26 @@ import com.github.jfcloud.gene.sample.vo.SampleFoodDetailVo;
 import com.github.jfcloud.gene.sample.vo.SampleFoodVo;
 import com.github.jfcloud.gene.sample.vo.SampleSubmitVo;
 import com.github.jfcloud.gene.sys.service.DBSystemPropertiesService;
+import com.github.jfcloud.gene.util.WordDataService;
 import lombok.RequiredArgsConstructor;
+import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
+import org.docx4j.XmlUtils;
+import org.docx4j.dml.spreadsheetdrawing.CTDrawing;
+import org.docx4j.openpackaging.packages.SpreadsheetMLPackage;
+import org.docx4j.openpackaging.parts.DrawingML.Drawing;
+import org.docx4j.openpackaging.parts.PartName;
+import org.docx4j.openpackaging.parts.SpreadsheetML.JaxbSmlPart;
+import org.docx4j.openpackaging.parts.SpreadsheetML.WorksheetPart;
+import org.docx4j.openpackaging.parts.WordprocessingML.BinaryPartAbstractImage;
 import org.springframework.stereotype.Service;
 
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-import java.util.Set;
+import javax.servlet.http.HttpServletRequest;
+import java.io.File;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.time.LocalDateTime;
+import java.util.*;
 import java.util.stream.Collectors;
 
 @Slf4j
@@ -43,6 +68,10 @@ public class SampleFoodServiceImpl extends ServiceImpl<SampleFoodMapper, SampleF
     private final CommonSampleEditServiceImpl commonSampleEditService;
     private final DBSystemPropertiesService systemPropertiesService;
     private final NotifyService notifyService;
+    private final WordDataService wordDataService;
+    private final FlowAuditService flowAuditService;
+    private final UserIdNameCache userIdNameCache;
+    private final FileInfoService fileInfoService;
 
     @Override
     public void removeBySampleId(Long sampleId) {
@@ -87,7 +116,218 @@ public class SampleFoodServiceImpl extends ServiceImpl<SampleFoodMapper, SampleF
 
     @Override
     public void generate(Long sampleId) {
+        SampleInfo sampleInfo = sampleInfoMapper.selectById(sampleId);
+        SampleFoodVo detail = getDetail(sampleId);
+
+        if (sampleInfo.getApplyTime() == null) {
+            sampleInfo.setApplyTime(LocalDateTime.now());
+        }
+        String applyDate = DateUtil.format(sampleInfo.getApplyTime(), DatePattern.CHINESE_DATE_PATTERN);
+
+        try (InputStream stream = ResourceUtil.getStream("officeTemplate/sampleFood.xlsx")) {
+            //替换excel中的变量,勾选框使用字符串代替
+            Map<String, String> mappings = new HashMap<>();
+            mappings.put("送检目的", detail.getPurpose());
+            mappings.put("送检方名称", detail.getSenderName());
+            mappings.put("送检方地址", detail.getSenderAddress());
+            mappings.put("送检方联系人", detail.getSenderContactName());
+            mappings.put("送检方联系电话", detail.getSenderPhone());
+            mappings.put("送检方电子邮箱", detail.getSenderEmail());
+            mappings.put("报告方式", "☐一个样品一个报告  ☐多个样品一个报告");
+            mappings.put("报告类型", "☐电子报告    ☐纸质报告");
+            mappings.put("是否评判结果", "☐是      ☐否");
+            mappings.put("是否以干基计", "☐是      ☐否");
+            mappings.put("其他要求", "☐CNAS    ☐CMA    ☐保密数据");
+            mappings.put("自定义", "☐自定义");
+            mappings.put("样品照片", "☐是(需要)   ☐外观   ☐内容物   ☐其他");
+            mappings.put("样品照片-其他", "");
+            mappings.put("样品照片-否", "   ☐否(不需要)");
+            mappings.put("样品储存要求", "☐室温   ☐冷藏(0℃~8℃)   ☐冷冻(≤18℃)   ☐避光   ☐干燥   ☐其他");
+            mappings.put("样品储存要求-其他", "");
+            mappings.put("样品保留期限", "☐常规样品一个月(默认)   ☐新鲜产品一周   ☐冷冻产品三周");
+            mappings.put("非危险性样品", "☐非危险性样品");
+            mappings.put("危险性样品", "☐易燃易爆  ☐腐蚀性  ☐毒性(非剧毒) ☐氧化剂  ☐其他");
+            mappings.put("危险性样品-其他", "");
+            mappings.put("服务时限", "☐标准时间(7个工作日)   ☐5个工作日\n\n☐3个工作日   ☐1个工作日\n\n☐加急");
+
+            List<SampleFoodDetailVo> detailList = detail.getDetailList();
+            SampleFoodDetailVo itemVo = CollUtil.isEmpty(detailList) ? new SampleFoodDetailVo() : detailList.get(0);
+            mappings.put("样品名称", itemVo.getSampleName());
+            mappings.put("生产商", itemVo.getManufacturer());
+            mappings.put("生产日期", "");
+            mappings.put("批号", itemVo.getBatchNumber());
+            mappings.put("数量", itemVo.getQuantity());
+            mappings.put("样品状态", "☐固体  ☐半固体\n☐液体  ☐其他");
+            mappings.put("样品状态-其他", "");
+            mappings.put("包装方式", "☐袋装  ☐罐装\n☐瓶装  ☐其他");
+            mappings.put("包装方式-其他", "");
+            mappings.put("检测项目", "常规指标,矿物质,维生素");
+            mappings.put("样品-其他", "");
+
+            mappings.put("常规指标条目", "☐水分  ☐灰分  ☐粗纤维  ☐粗脂肪   ☐粗蛋白   ☐水溶性氯化物  ☐pH  ☐酸价  ☐过氧化值  ☐淀粉  ☐挥发性盐基氮  ☐还原糖  ☐SOD  ☐总黄酮  ☐Ω-3脂肪酸  ☐Ω-6脂肪酸  ☐混合均匀度  ☐碳水化合物  ☐硬度  ☐不溶性杂质  ☐粘稠度  ☐杂质  ☐容重  ☐镜检  ☐嫩度(剪切力)  ☐水分活度  ☐膳食纤维  ☐可溶性膳食纤维  ☐不可溶性膳食纤维  ☐尿素酶活性  ☐其他");
+            mappings.put("常规指标条目-其他", "");
+            mappings.put("矿物质条目", "☐硒  ☐总磷  ☐氟  ☐铜  ☐铁  ☐锌  ☐锰  ☐钙  ☐钾  ☐钠  ☐镁  ☐其他");
+            mappings.put("矿物质条目-其他", "");
+            mappings.put("污染物条目", "☐亚硝酸盐  ☐铅  ☐镉  ☐铬  ☐总砷  ☐汞  ☐游离棉酚  ☐组胺  ☐苯并(a)芘  ☐其他");
+            mappings.put("污染物条目-其他", "");
+            mappings.put("维生素条目", "☐维生素D3  ☐维生素E  ☐维生素B1  ☐维生素B6  ☐泛酸  ☐维生素C  ☐生物素  ☐维生素A  ☐维生素K3  ☐维生素B2  ☐烟酸  ☐维生素B12  ☐叶酸  ☐左旋肉碱  ☐其他");
+            mappings.put("维生素条目-其他", "");
+            mappings.put("真菌毒素条目", "☐黄曲霉毒素B1  ☐玉米赤霉烯酮  ☐T2毒素  ☐脱氧雪腐镰刀菌烯醇  ☐赭曲霉毒素A  ☐伏马毒素  ☐其他");
+            mappings.put("真菌毒素条目-其他", "");
+            mappings.put("微生物条目", "☐沙门氏菌  ☐细菌总数  ☐酵母菌  ☐金黄色葡萄球菌  ☐志贺氏菌  ☐霉菌总数  ☐大肠菌群  ☐蜡样芽孢杆菌  ☐阪崎肠杆菌  ☐单核细胞增生李斯特氏菌  ☐其他");
+            mappings.put("微生物条目-其他", "");
+            mappings.put("其他条目", "☐17种氨基酸  ☐牛磺酸  ☐六六六  ☐滴滴涕  ☐BHA  ☐BHT  ☐TBHQ  ☐山梨酸  ☐苯甲酸  ☐其他");
+            mappings.put("其他条目-其他", "");
+            mappings.put("套餐条目", "☐套餐①基础九项  ☐套餐②国标全检  ☐套餐③农业部20号公告全检  ☐套餐④AAFCO套餐全检");
+            mappings.put("自定义条目", "");
+
+            mappings.put("日期", applyDate);
+            //将null替换为空字符串
+            mappings.forEach((key, value) -> {
+                if (value == null) {
+                    mappings.put(key, "");
+                }
+            });
+
+            //加载excel模板,并替换变量
+            SpreadsheetMLPackage opcPackagePkg = SpreadsheetMLPackage.load(stream);
+            JaxbSmlPart<?> smlPart = (JaxbSmlPart) opcPackagePkg.getParts().get(new PartName("/xl/sharedStrings.xml"));
+            smlPart.variableReplace(mappings);
+
+            //查询审核记录,添加签字图片
+            List<FlowAudit> flowAudits = flowAuditService.auditList(sampleId,"sample." + sampleInfo.getType(), true);
+            //项目管理部审核
+            flowAudits.stream()
+                    .filter(audit -> GeneStatusEnum.PROJECT_MANAGEMENT.getStatus().equals(audit.getFlowStatus()) && StrConstant.YES.equals(audit.getAuditResult()))
+                    .findFirst()
+                    .ifPresent(audit -> addImageDrawingPart(opcPackagePkg, audit.getCreateSign(), "G26", 3));
+            //部门负责人审核
+            flowAudits.stream()
+                    .filter(audit -> GeneStatusEnum.DEPART_LEADER.getStatus().equals(audit.getFlowStatus()) && StrConstant.YES.equals(audit.getAuditResult()))
+                    .findFirst()
+                    .ifPresent(audit -> addImageDrawingPart(opcPackagePkg, audit.getCreateSign(), "D26", 2));
+
+            //提交之后,显示送检人签字
+            if (!GeneStatusEnum.SUBMIT_STATUS.contains(sampleInfo.getStatus())) {
+                String signPic = userIdNameCache.getSignPic(detail.getSenderContactId());
+                addImageDrawingPart(opcPackagePkg, signPic, "B26", 1);
+                signPic = userIdNameCache.getSignPic(detail.getReceiverId());
+                addImageDrawingPart(opcPackagePkg, signPic, "I26", 4);
+            }
+
+            //保存为excel
+            String targetFileName = String.format("食品检测部门送检单(详细)-%s-%s.xlsx", applyDate, RandomUtil.randomString(RandomUtil.BASE_CHAR_NUMBER_LOWER, 4));
+            File tmpFile = new File(System.getProperty("user.dir"), targetFileName);
+            opcPackagePkg.save(tmpFile);
+
+            //上传服务器
+            FileVo fileVo = fileInfoService.uploadFileWithFileName(Files.newInputStream(tmpFile.toPath()), targetFileName);
+            HttpServletRequest request = RequestHolder.getRequest();
+            new FlowFileVersion()
+                    .setFlowId(sampleInfo.getId())
+                    .setFlowStatus(sampleInfo.getStatus())
+                    .setFileMd5(DigestUtil.md5Hex(tmpFile))
+                    .setFilename(fileVo.getName())
+                    .setFilepath(fileVo.getUrl())
+                    .setUri(request.getMethod() + " " + request.getRequestURI())
+                    .setTemplateName("sampleFood.xlsx")
+                    .insert();
+
+            //5s后删除临时文件
+            new Timer().schedule(new TimerTask() {
+                @Override
+                public void run() {
+                    FileUtil.del(tmpFile);
+                    log.info("删除临时文件 {}", tmpFile.getAbsolutePath());
+                }
+            }, 5000);
+        } catch (Exception e) {
+            log.error("宠物食品附件生成失败 id={} error={}", sampleId, e.getMessage());
+        }
+
+    }
+
+    /**
+     * 添加图片
+     *
+     * @param imageUrl 图片链接
+     * @param location 图片插入位置,如B23
+     * @param index    图片下标
+     */
+    @SneakyThrows
+    private void addImageDrawingPart(SpreadsheetMLPackage pkg, String imageUrl, String location, int index) {
+        byte[] bytes = wordDataService.getBytes(imageUrl);
+        if (bytes == null) {
+            //获取图片字节数组失败,不添加图片
+            return;
+        }
+
+        //在第一个sheet添加图片
+        WorksheetPart worksheet = pkg.getWorkbookPart().getWorksheet(0);
+
+        // Add anchor XML to worksheet
+        org.xlsx4j.sml.CTDrawing smlCtDrawing = org.xlsx4j.jaxb.Context.getsmlObjectFactory().createCTDrawing();
+        worksheet.getJaxbElement().setDrawing(smlCtDrawing);
+
+        // Create Drawing part and add to sheet
+        Drawing drawing = new Drawing();
+        smlCtDrawing.setId(worksheet.addTargetPart(drawing).getId());
+
+        BinaryPartAbstractImage imagePart = BinaryPartAbstractImage.createImagePart(pkg, drawing, bytes);
+        String imageRelID = imagePart.getSourceRelationships().get(0).getId();
+
+        int col = location.charAt(0) - 'A';
+        int row = Integer.parseInt(location.substring(1)) - 1;
+
+        String openXML = "<xdr:wsDr xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\" xmlns:a14=\"http://schemas.microsoft.com/office/drawing/2010/main\" xmlns:xdr=\"http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing\" xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">"
+                + "<xdr:twoCellAnchor editAs=\"oneCell\">"
+                + "<xdr:from>"
+                + "<xdr:col>" + col + "</xdr:col>"
+                + "<xdr:colOff>0</xdr:colOff>"
+                + "<xdr:row>" + row + "</xdr:row>"
+                + "<xdr:rowOff>0</xdr:rowOff>"
+                + "</xdr:from>"
+                + "<xdr:to>"
+                + "<xdr:col>" + col + "</xdr:col>"
+                + "<xdr:colOff>603250</xdr:colOff>"
+                + "<xdr:row>" + row + "</xdr:row>"
+                + "<xdr:rowOff>796500</xdr:rowOff>"
+                + "</xdr:to>"
+                + "<xdr:pic>"
+                + "<xdr:nvPicPr>"
+                + "<xdr:cNvPr id=\"" + index + "\" name=\"Picture " + index + "\"/>"
+                + "<xdr:cNvPicPr>"
+                + "<a:picLocks noChangeAspect=\"1\"/>"
+                + "</xdr:cNvPicPr>"
+                + "</xdr:nvPicPr>"
+                + "<xdr:blipFill>"
+                + "<a:blip r:embed=\"" + imageRelID + "\">"
+                + "<a:extLst>"
+                + "<a:ext uri=\"{" + UUID.randomUUID() + "}\">"
+                + "<a14:useLocalDpi val=\"0\"/>"
+                + "</a:ext>"
+                + "</a:extLst>"
+                + "</a:blip>"
+                + "<a:stretch>"
+                + "<a:fillRect/>"
+                + "</a:stretch>"
+                + "</xdr:blipFill>"
+                + "<xdr:spPr bwMode=\"auto\">"
+                + "<a:xfrm>"
+                + "<a:off x=\"3794124\" y=\"9294813\"/>"
+                + "<a:ext cx=\"603251\" cy=\"357215\"/>"
+                + "</a:xfrm>"
+                + "<a:prstGeom prst=\"rect\">"
+                + "<a:avLst/>"
+                + "</a:prstGeom>"
+                + "</xdr:spPr>"
+                + "</xdr:pic>"
+                + "<xdr:clientData/>"
+                + "</xdr:twoCellAnchor>"
+                + "</xdr:wsDr>";
 
+        CTDrawing dmlCtDrawing = (CTDrawing) XmlUtils.unwrap(XmlUtils.unmarshalString(openXML));
+        drawing.setJaxbElement(dmlCtDrawing);
     }
 
     @Override
@@ -127,11 +367,12 @@ public class SampleFoodServiceImpl extends ServiceImpl<SampleFoodMapper, SampleF
 
     @Override
     public SampleFoodVo getDetail(Long id) {
-        SampleFood sampleFood = getById(id);
+        SampleFood sampleFood = getOne(new LambdaQueryWrapper<>(SampleFood.class).eq(SampleFood::getSampleId, id));
         if (sampleFood == null) {
             return null;
         }
-        List<SampleFoodDetail> detailList = sampleFoodDetailService.list(new LambdaQueryWrapper<>(SampleFoodDetail.class).eq(SampleFoodDetail::getSampleFoodId, id));
+        List<SampleFoodDetail> detailList = sampleFoodDetailService.list(new LambdaQueryWrapper<>(SampleFoodDetail.class)
+                .eq(SampleFoodDetail::getSampleFoodId, sampleFood.getId()));
 
         SampleFoodVo sampleFoodVo = BeanUtil.copyProperties(sampleFood, SampleFoodVo.class);
         sampleFoodVo.setDetailList(BeanUtil.copyToList(detailList, SampleFoodDetailVo.class));

+ 18 - 5
jfcloud-gene-biz/src/main/java/com/github/jfcloud/gene/util/WordDataService.java

@@ -89,9 +89,23 @@ public class WordDataService {
      * @return base64字符串
      */
     public String getBase64(String fileUrl) {
-        if (StrUtil.isBlank(fileUrl) || !fileUrl.startsWith("/admin/sys-file/")) {
+        byte[] bytes = getBytes(fileUrl);
+        if (bytes == null) {
             return "";
         }
+        return Base64.getEncoder().encodeToString(bytes);
+    }
+
+    /**
+     * 获取图片字节
+     *
+     * @param fileUrl 图片地址
+     * @return 字节数组
+     */
+    public byte[] getBytes(String fileUrl) {
+        if (StrUtil.isBlank(fileUrl) || !fileUrl.startsWith("/admin/sys-file/")) {
+            return null;
+        }
 
         //存储桶图片
         String bucketName = fileUrl.substring(16);
@@ -100,12 +114,11 @@ public class WordDataService {
 
         try (S3Object s3Object = fileTemplate.getObject(bucketName, filename);
              InputStream inputStream = s3Object.getObjectContent()) {
-            byte[] bytes = IoUtil.readBytes(inputStream);
-            return Base64.getEncoder().encodeToString(bytes);
+            return IoUtil.readBytes(inputStream);
 
         } catch (Exception e) {
-            log.error("图片读取异常: {}", e.getLocalizedMessage());
-            return "";
+            log.error("图片 [{}] 读取异常: {}", fileUrl, e.getLocalizedMessage());
+            return null;
         }
     }
 

BIN
jfcloud-gene-biz/src/main/resources/officeTemplate/sampleFood.xlsx


+ 1 - 1
jfcloud-gene-biz/src/main/resources/sql/ddl-20250509.sql

@@ -42,7 +42,7 @@ CREATE TABLE sample_food
 CREATE TABLE sample_food_detail
 (
     id                  BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '样品ID',
-    sample_food_id      INT COMMENT '关联的宠物食品检测表ID',
+    sample_food_id      BIGINT COMMENT '关联的宠物食品检测表ID',
     sample_name         VARCHAR(255) NOT NULL COMMENT '样品名称',
     manufacturer        VARCHAR(255) COMMENT '生产商',
     production_date     DATE COMMENT '生产日期',