医疗互助补助模块

This commit is contained in:
2026-07-23 13:57:28 +08:00
parent a17140fbd2
commit 076f17905d
4 changed files with 95 additions and 6 deletions
@@ -37,8 +37,15 @@ import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellRangeAddress;
import com.deepoove.poi.data.ByteArrayPictureRenderData;
import com.deepoove.poi.data.PictureRenderData;
import com.deepoove.poi.data.PictureType;
import com.deepoove.poi.data.style.PictureStyle;
import com.deepoove.poi.XWPFTemplate;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
@@ -141,6 +148,7 @@ public class MedicalMutualAidApplyServiceImpl extends BaseServiceImpl<MutualAppl
public NutMap exportMineApplyForm(Long applyId) {
MutualApply apply = applyDetail(applyId);
assertMineApplyOwner(apply);
assertMineApplyExportStatus(apply);
Map<String, Object> docData = buildMineApplyFormDocData(apply);
String fileName = StrUtil.blankToDefault(apply.getUserName(), "申请人") + "大病医疗互助金申请表.docx";
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
@@ -801,12 +809,83 @@ public class MedicalMutualAidApplyServiceImpl extends BaseServiceImpl<MutualAppl
docData.put("signDay", formatDate(signDate, "dd"));
docData.put("remark", emptyToBlank(apply.getRemark()));
docData.put("statusText", buildStatusText(apply.getStatus()));
// 签字图片由 pc-signature 组件保存为文件下载地址,模板中使用 {{@sign}} 渲染图片。
docData.put("sign", StrUtil.isBlank(apply.getSignFileId()) ? null
: sysOfficeTemplateUtil.createPictureRenderData(90, 35, apply.getSignFileId()));
// 签字图片导出前裁掉四周白边,模板中使用 {{@sign}} 渲染图片。
docData.put("sign", buildTrimmedSignPicture(apply.getSignFileId()));
return docData;
}
/**
* 构建申请人签字图片,优先裁剪签字图片四周白边;图片异常时回退为原图,避免影响申请单导出。
*/
private PictureRenderData buildTrimmedSignPicture(String signFileId) {
if (StrUtil.isBlank(signFileId)) {
return null;
}
try {
byte[] imageBytes = sysOfficeTemplateUtil.getFileBytesByUrl(signFileId);
byte[] trimmedBytes = trimImageWhiteBorder(imageBytes);
ByteArrayPictureRenderData pictureRenderData = new ByteArrayPictureRenderData(trimmedBytes, PictureType.PNG);
PictureStyle pictureStyle = new PictureStyle();
pictureStyle.setWidth(90);
pictureStyle.setHeight(35);
pictureRenderData.setPictureStyle(pictureStyle);
return pictureRenderData;
} catch (Exception e) {
return sysOfficeTemplateUtil.createPictureRenderData(90, 35, signFileId);
}
}
/**
* 裁剪图片四周白色或透明空白区域,保留真实签字笔画区域用于 Word 模板渲染。
*/
private byte[] trimImageWhiteBorder(byte[] imageBytes) throws IOException {
if (imageBytes == null || imageBytes.length == 0) {
throw new IOException("签字图片为空");
}
BufferedImage sourceImage = ImageIO.read(new ByteArrayInputStream(imageBytes));
if (sourceImage == null) {
throw new IOException("签字图片格式不支持");
}
int minX = sourceImage.getWidth();
int minY = sourceImage.getHeight();
int maxX = -1;
int maxY = -1;
for (int y = 0; y < sourceImage.getHeight(); y++) {
for (int x = 0; x < sourceImage.getWidth(); x++) {
if (!isBlankSignPixel(sourceImage.getRGB(x, y))) {
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
}
}
}
if (maxX < minX || maxY < minY) {
return imageBytes;
}
int padding = 2;
int cropX = Math.max(minX - padding, 0);
int cropY = Math.max(minY - padding, 0);
int cropWidth = Math.min(maxX + padding, sourceImage.getWidth() - 1) - cropX + 1;
int cropHeight = Math.min(maxY + padding, sourceImage.getHeight() - 1) - cropY + 1;
BufferedImage trimmedImage = sourceImage.getSubimage(cropX, cropY, cropWidth, cropHeight);
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
ImageIO.write(trimmedImage, "png", byteArrayOutputStream);
return byteArrayOutputStream.toByteArray();
}
}
/**
* 判断签字图片像素是否属于可裁剪空白,兼容透明底和接近白色的扫描底色。
*/
private boolean isBlankSignPixel(int rgb) {
int alpha = (rgb >> 24) & 0xff;
int red = (rgb >> 16) & 0xff;
int green = (rgb >> 8) & 0xff;
int blue = rgb & 0xff;
return alpha <= 15 || (red >= 245 && green >= 245 && blue >= 245);
}
/**
* 校验“我的申请”导出只能访问当前登录人的申请,避免通过 URL 参数导出他人申请。
*/
@@ -823,6 +902,15 @@ public class MedicalMutualAidApplyServiceImpl extends BaseServiceImpl<MutualAppl
}
}
/**
* 校验申请单导出状态,只有互助会已审核通过的数据才允许生成正式申请单。
*/
private void assertMineApplyExportStatus(MutualApply apply) {
if (apply == null || apply.getStatus() == null || apply.getStatus() != 1) {
throw new BaseException("只有已审核的申请才能导出申请单");
}
}
/**
* 将疾病名称和住院说明合并到模板“何时何病伤住何医院”栏,保留原 Word 模板结构。
*/
@@ -13,7 +13,8 @@ layout("/layouts/platform.html"){
.mine-table.el-table th { background: #fafafa; color: #303133; font-weight: 700; }
.mine-table.el-table td, .mine-table.el-table th { padding: 11px 0; }
.mine-actions { display: flex; align-items: center; justify-content: center; gap: 8px; }
.mine-actions .el-button { min-width: 54px; padding: 7px 14px; border-radius: 2px; margin-left: 0; }
.mine-actions .el-button { min-width: 54px; padding: 7px 12px; border-radius: 2px; margin-left: 0; }
.mine-actions .export-apply-btn { min-width: 86px; }
.mine-pager { display: flex; justify-content: flex-start; align-items: center; padding-top: 12px; flex-shrink: 0; }
.mine-pager .total-text { margin-right: 18px; color: #303133; }
</style>
@@ -58,11 +59,11 @@ layout("/layouts/platform.html"){
<el-tag v-else type="warning" size="mini">未审核</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="280" align="center" fixed="right">
<el-table-column label="操作" width="310" align="center" fixed="right">
<template slot-scope="{row}">
<div class="mine-actions">
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button>
<el-button @click="onExportApplyForm(row)" size="mini" type="primary">导出申请单</el-button>
<el-button v-if="isAudited(row)" class="export-apply-btn" @click="onExportApplyForm(row)" size="mini" type="primary">导出申请单</el-button>
<el-button v-if="isPending(row)" @click="onEdit(row)" size="mini" type="primary">编辑</el-button>
<el-button v-if="isPending(row)" @click="onDeleteTip" size="mini" type="danger">删除</el-button>
</div>