Merge remote-tracking branch 'origin/main'

This commit is contained in:
2026-08-20 10:55:34 +08:00
12 changed files with 209 additions and 57 deletions
@@ -2,10 +2,14 @@ package com.budwk.app.zhgh.club.controller.infoManage;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.json.JSONObject;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.sys.models.Sys_file;
import com.budwk.app.sys.utils.SysFileMinIoUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.club.param.ClubUserPageForm;
import com.budwk.app.zhgh.club.service.SysClubInfoManageService;
@@ -14,12 +18,24 @@ 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 org.nutz.json.Json;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* @Author: JyuHsin
@@ -32,6 +48,8 @@ import java.util.List;
@At("/platform/club/infoManage/schoolAuditReport")
public class ClubSchoolAuditReportController {
private static final Logger log = LoggerFactory.getLogger(ClubSchoolAuditReportController.class);
@Inject
private SysClubInfoManageService clubInfoManageService;
@@ -44,6 +62,71 @@ public class ClubSchoolAuditReportController {
@SaCheckPermission("club.infoManage.schoolAuditReport")
public Result pageData(@Valid ClubUserPageForm pageForm,
@Param(value = "approval") Boolean approval) {
Sql sql = buildPageDataSql(pageForm, approval);
Pagination pagination = clubInfoManageService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
}
/**
* 导出当前筛选范围内的换届报告附件压缩包。
*
* @param pageForm 页面查询条件
* @param approval 审核状态,与列表页保持一致
* @param response HTTP 下载响应
*/
@At
@Ok("void")
@SaCheckPermission("club.infoManage.schoolAuditReport")
public void exportRefreshReportZip(@Valid ClubUserPageForm pageForm,
@Param(value = "approval") Boolean approval,
HttpServletResponse response) throws IOException {
List<NutMap> reportList = clubInfoManageService.listMap(buildPageDataSql(pageForm, approval));
response.setContentType("application/zip");
response.setHeader("Content-Disposition", "attachment;filename=" + URLUtil.encode("换届报告.zip"));
// 显式指定 UTF-8,确保压缩包内中文目录和文件名正常显示。
try (ZipOutputStream zipOutputStream = new ZipOutputStream(new BufferedOutputStream(response.getOutputStream()), StandardCharsets.UTF_8)) {
Set<String> entryNames = new HashSet<>();
for (NutMap report : reportList) {
String filesJson = report.getString("files");
if (StrUtil.isBlank(filesJson)) {
continue;
}
List<JSONObject> files = Json.fromJsonAsList(JSONObject.class, filesJson);
String folderName = buildFolderName(report);
for (int index = 0; index < files.size(); index++) {
JSONObject fileInfo = files.get(index);
Sys_file sysFile = findSysFile(fileInfo);
if (sysFile == null) {
log.warn("换届报告附件不存在,报告ID:{},附件:{}", report.getString("id"), fileInfo);
continue;
}
byte[] fileBytes = SysFileMinIoUtil.getFileBytes(sysFile.getBucket(), sysFile.getStoragePath());
if (fileBytes == null || fileBytes.length == 0) {
log.warn("换届报告附件内容为空,报告ID:{},附件ID:{}", report.getString("id"), sysFile.getId());
continue;
}
String fileName = StrUtil.blankToDefault(fileInfo.getStr("name"), fileInfo.getStr("filename"));
if (StrUtil.isBlank(fileName)) {
fileName = sysFile.getName();
}
String entryName = buildUniqueEntryName(folderName + "/" + sanitizeFileName(fileName), entryNames);
zipOutputStream.putNextEntry(new ZipEntry(entryName));
zipOutputStream.write(fileBytes);
zipOutputStream.closeEntry();
}
}
}
}
/**
* 构建列表与导出共用的换届报告查询,确保“查什么导什么”。
*
* @param pageForm 页面查询条件
* @param approval 审核状态
* @return 已绑定筛选条件的查询对象
*/
private Sql buildPageDataSql(ClubUserPageForm pageForm, Boolean approval) {
Sql sql = Sqls.create("""
SELECT
info.*,
@@ -79,7 +162,7 @@ public class ClubSchoolAuditReportController {
cnd.and("t.taskName", "=", "d4323546-8d09-419e-88d4-7b15862ca29d");
cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId()));
if (approval) {
if (Boolean.TRUE.equals(approval)) {
cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode()));
} else {
cnd.and("t.taskState", "=", ProcessTaskStateEnum.DOING.getCode());
@@ -96,8 +179,67 @@ public class ClubSchoolAuditReportController {
cnd.groupBy("t.id");
sql.setCondition(cnd);
return sql;
}
Pagination pagination = clubInfoManageService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
/**
* 兼容新旧附件 JSON 结构,按附件 ID 或下载地址查找文件元数据。
*
* @param fileInfo 附件 JSON 信息
* @return 系统文件记录;未找到时返回 null
*/
private Sys_file findSysFile(JSONObject fileInfo) {
String fileId = fileInfo.getStr("id");
if (StrUtil.isNotBlank(fileId)) {
return clubInfoManageService.dao().fetch(Sys_file.class, fileId);
}
String fileUrl = fileInfo.getStr("url");
if (StrUtil.isBlank(fileUrl)) {
return null;
}
return clubInfoManageService.dao().fetch(Sys_file.class, Cnd.where(Sys_file::getDownloadPath, "=", fileUrl));
}
/**
* 使用社团名称建立目录,方便按换届报告来源查阅。
*
* @param report 换届报告记录
* @return Zip 内目录名称
*/
private String buildFolderName(NutMap report) {
return sanitizeFileName(report.getString("clubName"));
}
/**
* 保留附件原始文件名;同一社团目录存在同名文件时追加序号,避免覆盖。
*
* @param entryName 原始 Zip 条目名称
* @param entryNames 已写入的 Zip 条目名称
* @return 唯一的 Zip 条目名称
*/
private String buildUniqueEntryName(String entryName, Set<String> entryNames) {
if (entryNames.add(entryName)) {
return entryName;
}
int extensionIndex = entryName.lastIndexOf('.');
String name = extensionIndex > entryName.lastIndexOf('/') ? entryName.substring(0, extensionIndex) : entryName;
String extension = extensionIndex > entryName.lastIndexOf('/') ? entryName.substring(extensionIndex) : "";
int index = 2;
String uniqueEntryName;
do {
uniqueEntryName = name + "(" + index + ")" + extension;
index++;
} while (!entryNames.add(uniqueEntryName));
return uniqueEntryName;
}
/**
* 去除 Zip 条目路径分隔符,避免附件名影响压缩包目录结构。
*
* @param fileName 原始文件名
* @return 可安全写入 Zip 的文件名
*/
private String sanitizeFileName(String fileName) {
return StrUtil.blankToDefault(fileName, "未命名文件").replace("/", "_").replace("\\", "_");
}
}
@@ -9,9 +9,11 @@ import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.constant.RoleConstant;
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.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.dayofficework.asset.model.Asset;
import com.budwk.app.zhgh.dayofficework.asset.model.AssetDepreciationRecord;
@@ -27,6 +29,7 @@ import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
@@ -157,9 +160,16 @@ public class AssetManageController {
@SaCheckPermission(value = {"asset.manage", "asset.stocktaking", "h5.asset.stocktaking"}, mode = SaMode.OR)
public Result searchAssetUseUser(String query) {
Cnd cnd = Cnd.NEW();
cnd.where().orLike("username", query);
cnd.where().orLike("loginname", query);
cnd.where().orLike("id", query);
// 三个关键词条件必须整体分组,避免 OR 条件绕过分工会数据范围。
SqlExpressionGroup searchGroup = new SqlExpressionGroup();
searchGroup.orLike("username", query);
searchGroup.orLike("loginname", query);
searchGroup.orLike("id", query);
cnd.and(searchGroup);
// 分工会侧盘点人员只能选择本分工会成员,校工会和系统管理员保留全校查询范围。
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
cnd.and("unionId", "=", SecurityUtil.getUnionId());
}
Sql sql = Sqls.create("""
SELECT
id,
@@ -110,7 +110,9 @@ public class CadreTrainingMineController {
cnd.where().andLike(CadreTrainingAct::getName, pageForm.getSearchKeyword());
}
cnd.desc("info.applyTime");
cnd.and("info.userId", "=", SecurityUtil.getUserId());
// 本人报名和本人代他人报名的记录都应在“我的报名”中展示。
cnd.and(Cnd.exps("info.userId", "=", SecurityUtil.getUserId())
.or("info.applyUserId", "=", SecurityUtil.getUserId()));
sql.setCondition(cnd);
Pagination<NutMap> pagination = cadreTrainingSignUpService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -68,7 +68,8 @@ public class CadreTrainingSignUpController {
@ApiOperation("分页查询")
public Result pageData(@Valid PageForm pageForm,Integer year, boolean isEnrolled) {
Sql sql = Sqls.create("""
SELECT
-- 同一活动存在多条代报名记录时,活动列表仅展示一次。
SELECT DISTINCT
info.*,
CASE WHEN cts.id IS NOT NULL THEN 1 ELSE 0 END AS isEnrolled
FROM
@@ -101,6 +102,9 @@ public class CadreTrainingSignUpController {
List<ProcessInstance> instances = new ArrayList<>();
for (CadreTrainingSignUp signUp : cadreTrainingSignUps) {
// 代报名记录必须以当前登录人为报名操作人,供“已报名”和“我的报名”按操作人查询。
signUp.setApplyUserId(SecurityUtil.getUserId());
signUp.setApplyUserUserName(SecurityUtil.getUserUsername());
if (StrUtil.isBlank(signUp.getId())) {
signUp.setApplyTime(new Date());
}
@@ -101,7 +101,8 @@ public class ProposalExpeditingController {
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("t.taskName", "in", List.of("master_reply", "slave_reply", "opinion_master_reply"));
// 催办列表只展示现行 WF 承办答复环节中尚未完成的主办、二次答复及意见答复任务。
cnd.and("t.taskName", "in", List.of("unit_reply", "two_unit_reply", "opinion_unit_reply"));
if (!AuthUtil.hasRole(RoleConstant.SYSADMIN.name()) && !AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_ADMIN.name())) {
cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId()));
@@ -47,6 +47,7 @@ public class ProposalControlController {
@SaCheckPermission("proposal.control")
@ApiOperation("分页列表")
public Result pageData(@Valid ProposalSearchParam pageForm) {
// 状态调整属于提案业务,只保留能够关联到 proposal_info 的 WF 任务,排除其他业务流程产生的空白行。
Sql sql = Sqls.create("""
SELECT
info.*,
@@ -72,7 +73,7 @@ public class ProposalControlController {
LEFT JOIN wf_process_instance ins ON ins.id = t.processInstanceId
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id
LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10
LEFT JOIN proposal_info info ON info.id = ins.businessNo
INNER JOIN proposal_info info ON info.id = ins.businessNo
LEFT JOIN proposal_type type ON type.id = info.typeId
LEFT JOIN teacher_congress_session tcs ON tcs.id = info.sessionId
LEFT JOIN teacher_congress_delegation tcd ON tcd.id = info.delegationId