困难帮扶阅览增加导出附件功能(分工会审核通后才能导出)

This commit is contained in:
2026-09-07 17:18:11 +08:00
parent 6088cbf4d2
commit facdb5f52a
5 changed files with 241 additions and 0 deletions
@@ -102,6 +102,8 @@ public class DifficultHelpMineController {
Cnd cnd = Cnd.NEW();
pageParam.buildSearch(cnd, "info.");
cnd.and("info.createdBy", "=", SecurityUtil.getUserId());
// 同一进行中任务存在多个参与人时,按困难补助申请主表 ID 去重,保证每条申请只展示一次。
cnd.groupBy("info.id");
if (StrUtil.isAllBlank(pageParam.getPageOrderName(), pageParam.getPageOrderBy())) {
cnd.desc("info.applyTime");
} else {
@@ -4,6 +4,7 @@ import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.flow.engine.FlowEngine;
import com.budwk.app.zhgh.staffbenefit.difficulthelp.service.DifficultHelpCommonService;
import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.DifficultHelpPageParam;
@@ -21,6 +22,9 @@ import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @version 1.0
* @Author zzr
@@ -94,6 +98,34 @@ public class DifficultHelpReadingController {
return Result.success().addData(pagination);
}
/**
* 导出当前阅览条件下困难补助申请人上传的附件。
*
* @param pageParam 阅览页筛选条件,包含年度、受助人关键字、所属工会和所属单位
* @param response HTTP 文件下载响应,返回名为“困难补助附件.zip”的 ZIP 二进制文件
*/
@At
@Ok("void")
@ApiOperation("导出困难补助附件压缩包")
@SaCheckPermission("difficultHelp.reading")
public void exportAttachmentZip(DifficultHelpPageParam pageParam, HttpServletResponse response) throws IOException {
byte[] zipBytes = difficultHelpCommonService.exportAttachmentZip(pageParam);
CommonDownloadUtil.download("困难补助附件.zip", zipBytes, response);
}
/**
* 校验当前筛选范围内是否存在可导出的困难补助附件。
*
* @param pageParam 阅览页筛选条件,包含年度、受助人关键字、所属工会和所属单位
* @return 布尔值;true 表示存在分工会审核已同意且已上传附件的申请
*/
@At
@ApiOperation("校验困难补助可导出附件")
@SaCheckPermission("difficultHelp.reading")
public Result hasExportableAttachment(DifficultHelpPageParam pageParam) {
return Result.success(difficultHelpCommonService.hasExportableAttachment(pageParam));
}
@At
@SaCheckLogin
@@ -2,6 +2,9 @@ package com.budwk.app.zhgh.staffbenefit.difficulthelp.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.staffbenefit.difficulthelp.models.DifficultHelpInfo;
import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.DifficultHelpPageParam;
import java.io.IOException;
/**
* @version 1.0
@@ -11,4 +14,21 @@ import com.budwk.app.zhgh.staffbenefit.difficulthelp.models.DifficultHelpInfo;
* @注释
*/
public interface DifficultHelpCommonService extends BaseService<DifficultHelpInfo> {
/**
* 按困难补助阅览页的查询条件生成分工会审核已同意申请的附件压缩包。
*
* @param pageParam 阅览页筛选参数,包含年度、工会、单位、受助人关键字及当前用户的数据范围
* @return ZIP 二进制内容,内部按“姓名(工号)”目录归档该受助人的上传附件
* @throws IOException 文件下载或 ZIP 写入失败时抛出
*/
byte[] exportAttachmentZip(DifficultHelpPageParam pageParam) throws IOException;
/**
* 判断当前阅览筛选范围内是否存在可导出的困难补助附件。
*
* @param pageParam 阅览页筛选参数,包含年度、工会、单位、受助人关键字及当前用户的数据范围
* @return 分工会审核已同意且已上传附件时返回 true,否则返回 false
*/
boolean hasExportableAttachment(DifficultHelpPageParam pageParam);
}
@@ -1,11 +1,30 @@
package com.budwk.app.zhgh.staffbenefit.difficulthelp.service.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.flow.enums.ProcessSubmitTypeEnum;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.sys.services.SysFileService;
import com.budwk.app.zhgh.staffbenefit.difficulthelp.models.DifficultHelpInfo;
import com.budwk.app.zhgh.staffbenefit.difficulthelp.service.DifficultHelpCommonService;
import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.DifficultHelpPageParam;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* @version 1.0
* @Author zzr
@@ -13,10 +32,168 @@ import org.nutz.ioc.loader.annotation.IocBean;
* @Date 2025/7/31 9:16
* @注释
*/
@Slf4j
@IocBean(args = {"refer:dao"})
public class DifficultHelpCommonServiceImpl extends BaseServiceImpl<DifficultHelpInfo> implements DifficultHelpCommonService {
/** 困难补助流程中“分工会审核”节点的实际 wf 节点编码。 */
private static final String BRANCH_UNION_AUDIT_TASK_NAME = "9d11798e-a0e4-4ff5-939b-e6c5519e0eb6";
@Inject
private SysFileService sysFileService;
public DifficultHelpCommonServiceImpl(Dao dao) {
super(dao);
}
/**
* 根据阅览页筛选结果下载分工会审核已同意的附件,并以受助人姓名和工号作为 ZIP 内目录。
* 无附件、文件记录不存在或文件下载失败的记录会跳过,避免单个异常附件影响其他附件导出。
*
* @param pageParam 阅览页筛选条件,数据权限由 {@link DifficultHelpPageParam#buildSearch(Cnd, String)} 统一处理
* @return 困难补助附件 ZIP 的二进制内容
* @throws IOException ZIP 输出流创建或关闭失败时抛出
*/
@Override
public byte[] exportAttachmentZip(DifficultHelpPageParam pageParam) throws IOException {
List<DifficultHelpInfo> helpInfoList = listBranchUnionApprovedHelpInfo(pageParam);
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) {
Set<String> entryNames = new HashSet<>();
for (DifficultHelpInfo helpInfo : helpInfoList) {
if (helpInfo.getFiles() == null || helpInfo.getFiles().isEmpty()) {
continue;
}
String folderName = buildFolderName(helpInfo);
for (JSONObject file : helpInfo.getFiles()) {
String fileId = getFileId(file);
if (StrUtil.isBlank(fileId)) {
continue;
}
try {
byte[] fileBytes = sysFileService.download(fileId);
String entryName = getUniqueEntryName(folderName, file.getStr("name"), entryNames);
zipOutputStream.putNextEntry(new ZipEntry(entryName));
zipOutputStream.write(fileBytes);
zipOutputStream.closeEntry();
} catch (IOException e) {
// 单个附件异常时保留其余可用附件,便于管理员完成批量导出。
log.warn("困难补助附件导出失败,申请记录ID:{},文件ID:{}", helpInfo.getId(), fileId, e);
}
}
}
zipOutputStream.finish();
return outputStream.toByteArray();
}
}
/**
* 判断当前筛选范围内是否有分工会审核已同意且已上传的附件。
*
* @param pageParam 阅览页筛选条件,数据权限由 {@link DifficultHelpPageParam#buildSearch(Cnd, String)} 统一处理
* @return 存在至少一条可导出附件时返回 true
*/
@Override
public boolean hasExportableAttachment(DifficultHelpPageParam pageParam) {
return listBranchUnionApprovedHelpInfo(pageParam).stream()
.anyMatch(helpInfo -> helpInfo.getFiles() != null && !helpInfo.getFiles().isEmpty());
}
/**
* 查询当前数据权限范围内,最新分工会审核任务已同意的困难补助申请。
* 同一申请被退回后再次审核时,仅认可最新一次分工会审核结果,防止历史同意记录绕过当前流程状态。
*
* @param pageParam 阅览页筛选条件,包含年度、工会、单位和受助人关键字
* @return 已通过分工会审核的困难补助申请列表
*/
private List<DifficultHelpInfo> listBranchUnionApprovedHelpInfo(DifficultHelpPageParam pageParam) {
Sql approvedBusinessSql = Sqls.create("""
SELECT ins.businessNo
FROM wf_process_instance ins
INNER JOIN wf_process_task branchTask ON branchTask.processInstanceId = ins.id
WHERE branchTask.taskName = @branchUnionAuditTaskName
AND branchTask.id = (
SELECT MAX(latestBranchTask.id)
FROM wf_process_task latestBranchTask
WHERE latestBranchTask.processInstanceId = ins.id
AND latestBranchTask.taskName = @branchUnionAuditTaskName
)
AND branchTask.taskState = @finishedTaskState
AND JSON_UNQUOTE(JSON_EXTRACT(branchTask.variable, '$.submitType')) = @agreeSubmitType
""");
approvedBusinessSql.setParam("branchUnionAuditTaskName", BRANCH_UNION_AUDIT_TASK_NAME);
approvedBusinessSql.setParam("finishedTaskState", ProcessTaskStateEnum.FINISHED.getCode());
approvedBusinessSql.setParam("agreeSubmitType", String.valueOf(ProcessSubmitTypeEnum.AGREE.getCode()));
Cnd cnd = Cnd.NEW();
pageParam.buildSearch(cnd, "info.");
cnd.and("info.id", "in", approvedBusinessSql);
cnd.desc("info.applyTime");
Sql sql = Sqls.create("""
SELECT info.*
FROM difficult_help_info info
$condition
""");
sql.setCondition(cnd);
return listEntity(sql);
}
/**
* 从 file-upload 保存的响应数据中提取系统文件 ID。
*
* @param file 上传组件保存的附件信息,响应数据格式为 /platform/sys/file/download?id=文件ID
* @return 系统文件 ID;附件信息不完整时返回空字符串
*/
private String getFileId(JSONObject file) {
if (file == null || file.getJSONObject("response") == null) {
return StrUtil.EMPTY;
}
String downloadUrl = file.getJSONObject("response").getStr("data");
String fileId = StrUtil.subAfter(downloadUrl, "id=", true);
return StrUtil.subBefore(fileId, "&", false);
}
/**
* 为同一受助人的附件创建稳定且不含路径分隔符的目录名称。
*
* @param helpInfo 困难补助申请信息,包含受助人姓名和工号
* @return ZIP 内的受助人目录名称
*/
private String buildFolderName(DifficultHelpInfo helpInfo) {
String userName = sanitizeEntryName(StrUtil.blankToDefault(helpInfo.getUserName(), "未命名受助人"));
String loginName = sanitizeEntryName(StrUtil.blankToDefault(helpInfo.getLoginName(), "无工号"));
return userName + "" + loginName + "";
}
/**
* 为附件生成不重复的 ZIP 条目名称,防止同一受助人上传同名文件时被覆盖。
*
* @param folderName 受助人目录名称
* @param fileName 原始附件名称
* @param entryNames 已写入 ZIP 的条目名称集合
* @return 可安全写入 ZIP 的唯一条目名称
*/
private String getUniqueEntryName(String folderName, String fileName, Set<String> entryNames) {
String safeFileName = sanitizeEntryName(StrUtil.blankToDefault(fileName, "未命名附件"));
String entryName = folderName + "/" + safeFileName;
int duplicateIndex = 1;
while (entryNames.contains(entryName)) {
entryName = folderName + "/" + duplicateIndex + "_" + safeFileName;
duplicateIndex++;
}
entryNames.add(entryName);
return entryName;
}
/**
* 清理 ZIP 条目中的路径和 Windows 非法字符,避免附件名称改变 ZIP 目录结构。
*
* @param entryName 用户姓名、工号或上传文件名
* @return 可用于 ZIP 单级目录或文件名的安全字符串
*/
private String sanitizeEntryName(String entryName) {
return entryName.replaceAll("[\\\\/:*?\"<>|\\r\\n]", "_");
}
}
@@ -42,6 +42,7 @@ layout("/layouts/platform.html"){
<el-card shadow="never" class="mt10">
<table-tool label="数据列表">
<el-button type="primary" icon="el-icon-download" size="small" @click="exportAttachmentZip">导出附件Zip</el-button>
</table-tool>
<el-table :data="tableData" :size="tableSize" @sort-change="pageOrder"
@@ -113,6 +114,15 @@ layout("/layouts/platform.html"){
'info': INFO
},
methods: {
exportAttachmentZip() {
this.$axios.post(loc() + "/hasExportableAttachment", this.pageForm).then((res) => {
if (res.code === 0 && res.data) {
this.$downLoad(loc() + "/exportAttachmentZip", this.pageForm)
} else {
this.$message.warning("当前筛选范围内没有可导出的附件")
}
})
},
openView(row) {
this.$refs.guava.view(() => {
this.$refs.infoRef.onOpen(row)