diff --git a/src/main/java/com/budwk/app/zhgh/democratic/opinion/mode/OpinionSchoolAuditImportExcelMode.java b/src/main/java/com/budwk/app/zhgh/democratic/opinion/mode/OpinionSchoolAuditImportExcelMode.java new file mode 100644 index 00000000..e0eb4960 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/democratic/opinion/mode/OpinionSchoolAuditImportExcelMode.java @@ -0,0 +1,44 @@ +package com.budwk.app.zhgh.democratic.opinion.mode; + +import cn.afterturn.easypoi.excel.annotation.Excel; +import com.budwk.app.base.model.ExcelImportError; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 校工会审核结果导入模板行数据。 + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class OpinionSchoolAuditImportExcelMode extends ExcelImportError { + + @Excel(name = "意见编号") + private String code; + + @Excel(name = "意见和建议") + private String name; + + @Excel(name = "意见类别") + private String typeName; + + @Excel(name = "建议人") + private String createUserName; + + @Excel(name = "建议人所属代表团") + private String delegationName; + + @Excel(name = "建议人所属单位") + private String unitName; + + @Excel(name = "主办") + private String masterUnitName; + + @Excel(name = "协办") + private String slaveUnitNames; + + @Excel(name = "审核结果") + private String auditResult; + + @Excel(name = "审核意见") + private String auditOpinion; +} diff --git a/src/main/java/com/budwk/app/zhgh/democratic/opinion/service/OpinionSchoolAuditService.java b/src/main/java/com/budwk/app/zhgh/democratic/opinion/service/OpinionSchoolAuditService.java new file mode 100644 index 00000000..af9ba234 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/democratic/opinion/service/OpinionSchoolAuditService.java @@ -0,0 +1,31 @@ +package com.budwk.app.zhgh.democratic.opinion.service; + +import com.budwk.app.base.model.ExcelImportRes; +import com.budwk.app.base.service.BaseService; +import com.budwk.app.zhgh.democratic.opinion.mode.OpinionSchoolAuditImportExcelMode; +import com.budwk.app.zhgh.democratic.opinion.models.OpinionInfo; +import org.nutz.mvc.upload.TempFile; + +import javax.servlet.http.HttpServletResponse; + +/** + * 校工会审核业务处理。 + */ +public interface OpinionSchoolAuditService extends BaseService { + + /** + * 下载校工会审核结果导入模板。 + * + * @param response 文件响应对象 + */ + void downloadAuditImportTemplate(HttpServletResponse response); + + /** + * 批量导入校工会审核结果,并按页面审核按钮的流程参数执行待办任务。 + * + * @param file 上传的 Excel 文件 + * @param sessionId 当前页面选中的教代会届次 ID,用于限定意见编号匹配范围 + * @return 导入审核结果,包含成功数、失败数和错误明细 + */ + ExcelImportRes importAuditResult(TempFile file, String sessionId); +} diff --git a/src/main/java/com/budwk/app/zhgh/democratic/opinion/service/impl/OpinionSchoolAuditServiceImpl.java b/src/main/java/com/budwk/app/zhgh/democratic/opinion/service/impl/OpinionSchoolAuditServiceImpl.java new file mode 100644 index 00000000..845a30cf --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/democratic/opinion/service/impl/OpinionSchoolAuditServiceImpl.java @@ -0,0 +1,337 @@ +package com.budwk.app.zhgh.democratic.opinion.service.impl; + +import cn.afterturn.easypoi.excel.ExcelExportUtil; +import cn.afterturn.easypoi.excel.ExcelImportUtil; +import cn.afterturn.easypoi.excel.entity.ExportParams; +import cn.afterturn.easypoi.excel.entity.ImportParams; +import cn.afterturn.easypoi.excel.entity.enmus.ExcelType; +import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity; +import cn.hutool.core.lang.Dict; +import cn.hutool.core.util.StrUtil; +import com.budwk.app.base.model.ExcelImportRes; +import com.budwk.app.base.service.impl.BaseServiceImpl; +import com.budwk.app.base.utils.CommonDownloadUtil; +import com.budwk.app.flow.constant.FlowConst; +import com.budwk.app.flow.engine.FlowEngine; +import com.budwk.app.flow.engine.util.FlowUtil; +import com.budwk.app.flow.entity.ProcessInstance; +import com.budwk.app.flow.entity.ProcessTask; +import com.budwk.app.flow.enums.ProcessSubmitTypeEnum; +import com.budwk.app.flow.enums.ProcessTaskStateEnum; +import com.budwk.app.flow.service.FlowCommonService; +import com.budwk.app.web.commons.auth.utils.SecurityUtil; +import com.budwk.app.zhgh.democratic.opinion.mode.OpinionSchoolAuditImportExcelMode; +import com.budwk.app.zhgh.democratic.opinion.models.OpinionInfo; +import com.budwk.app.zhgh.democratic.opinion.models.OpinionType; +import com.budwk.app.zhgh.democratic.opinion.service.OpinionSchoolAuditService; +import com.budwk.app.zhgh.democratic.proposal.models.ProposalUndertake; +import org.apache.poi.ss.usermodel.Workbook; +import org.nutz.dao.Chain; +import org.nutz.dao.Cnd; +import org.nutz.dao.Dao; +import org.nutz.dao.Sqls; +import org.nutz.dao.entity.Record; +import org.nutz.dao.sql.Sql; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.mvc.upload.TempFile; + +import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 校工会审核业务处理实现。 + */ +@IocBean(args = {"refer:dao"}) +public class OpinionSchoolAuditServiceImpl extends BaseServiceImpl implements OpinionSchoolAuditService { + + @Inject + private FlowEngine flowEngine; + @Inject + private FlowCommonService flowCommonService; + + public OpinionSchoolAuditServiceImpl(Dao dao) { + super(dao); + } + + @Override + public void downloadAuditImportTemplate(HttpServletResponse response) { + List entities = new ArrayList<>(); + entities.add(new ExcelExportEntity("序号", "index", 10)); + entities.add(new ExcelExportEntity("意见编号", "code", 20)); + entities.add(new ExcelExportEntity("意见和建议", "name", 50)); + entities.add(new ExcelExportEntity("意见类别", "typeName", 20)); + entities.add(new ExcelExportEntity("建议人", "createUserName", 20)); + entities.add(new ExcelExportEntity("建议人所属代表团", "delegationName", 25)); + entities.add(new ExcelExportEntity("建议人所属单位", "unitName", 25)); + entities.add(new ExcelExportEntity("主办", "masterUnitName", 35)); + entities.add(new ExcelExportEntity("协办", "slaveUnitNames", 35)); + entities.add(new ExcelExportEntity("审核结果", "auditResult", 20)); + entities.add(new ExcelExportEntity("审核意见", "auditOpinion", 35)); + + ExportParams exportParams = new ExportParams(); + exportParams.setType(ExcelType.XSSF); + Workbook workbook = ExcelExportUtil.exportExcel(exportParams, entities, Collections.emptyList()); + CommonDownloadUtil.download("校工会审核结果导入模板.xlsx", workbook, response); + } + + @Override + public ExcelImportRes importAuditResult(TempFile file, String sessionId) { + List importRows = ExcelImportUtil.importExcel(file.getFile(), OpinionSchoolAuditImportExcelMode.class, new ImportParams()); + ExcelImportRes excelImportRes = new ExcelImportRes<>(); + excelImportRes.setTotalRecords(importRows.size()); + + Set importedCodes = new HashSet<>(); + for (int i = 0; i < importRows.size(); i++) { + OpinionSchoolAuditImportExcelMode row = importRows.get(i); + if (isEmptyRow(row)) { + continue; + } + + // 逐行校验并执行流程,导入失败的行只记录错误,不影响后续行继续处理。 + handleAuditImportRow(row, i, sessionId, importedCodes); + } + + List errorDetails = importRows.stream() + .filter(row -> StrUtil.isNotBlank(row.getErrMsg())) + .collect(Collectors.toList()); + excelImportRes.setErrorDetails(errorDetails); + excelImportRes.setFailedCount(errorDetails.size()); + excelImportRes.setSuccessCount((int) importRows.stream() + .filter(row -> !isEmptyRow(row)) + .filter(row -> StrUtil.isBlank(row.getErrMsg())) + .count()); + return excelImportRes; + } + + private void handleAuditImportRow(OpinionSchoolAuditImportExcelMode row, int rowIndex, String sessionId, Set importedCodes) { + List errors = new ArrayList<>(); + String code = StrUtil.trim(row.getCode()); + if (StrUtil.isBlank(code)) { + errors.add("意见编号不能为空"); + } else if (!importedCodes.add(code)) { + errors.add("意见编号重复"); + } + + AuditResult auditResult = parseAuditResult(row.getAuditResult(), errors); + if (StrUtil.isBlank(row.getAuditOpinion())) { + errors.add("审核意见不能为空"); + } + + Record taskInfo = StrUtil.isBlank(code) ? null : fetchDoingSchoolAuditTask(code, sessionId); + if (taskInfo == null && StrUtil.isNotBlank(code)) { + errors.add("未找到当前届次下可审核的校工会待办意见"); + } + + OpinionType type = null; + ProposalUndertake masterUnit = null; + List slaveUnits = new ArrayList<>(); + if (auditResult == AuditResult.PASS) { + type = fetchOpinionType(row.getTypeName()); + if (type == null) { + errors.add("意见类别不存在"); + } + + masterUnit = fetchUndertake(row.getMasterUnitName()); + if (masterUnit == null) { + errors.add("主办单位不存在"); + } + + slaveUnits = parseSlaveUnits(row.getSlaveUnitNames(), errors); + String masterUnitId = masterUnit.getId(); + if (masterUnit != null && slaveUnits.stream().anyMatch(unit -> StrUtil.equals(unit.getId(), masterUnitId))) { + errors.add("协办单位不能包含主办单位"); + } + } + + if (!errors.isEmpty()) { + // Excel 行号按用户看到的表头后一行数据计算,便于定位错误。 + row.setErrInfo(StrUtil.join(";", errors), rowIndex + 2); + return; + } + + executeSchoolAuditTask(row, taskInfo, auditResult, type, masterUnit, slaveUnits); + } + + private void executeSchoolAuditTask(OpinionSchoolAuditImportExcelMode row, Record taskInfo, AuditResult auditResult, + OpinionType type, ProposalUndertake masterUnit, List slaveUnits) { + String opinionId = taskInfo.getString("opinionId"); + Long taskId = taskInfo.getLong("taskId"); + String taskName = taskInfo.getString("taskName"); + + Dict taskData = Dict.create(); + taskData.set("id", opinionId); + taskData.set(FlowConst.PROCESS_TASK_ID_KEY, taskId); + taskData.set(FlowConst.TASK_NAME, taskName); + taskData.set("roleCode", "OFFICE_MANAGER"); + taskData.set("tf_opinion", StrUtil.trim(row.getAuditOpinion())); + taskData.set(FlowConst.SUBMIT_TYPE, auditResult.submitType); + + if (auditResult != AuditResult.ROLLBACK) { + taskData.set("tf_schoolResult", auditResult.flowValue); + } + + if (auditResult == AuditResult.PASS) { + // 通过审核时与页面单条审核保持一致:先保存意见类别,再提交主办、协办等流程表单字段。 + updateOpinionType(opinionId, type.getId()); + taskData.set("typeId", type.getId()); + taskData.set("tf_masterUnitId", masterUnit.getId()); + taskData.set("tf_masterUnitName", masterUnit.getName()); + List slaveUnitIds = slaveUnits.stream().map(ProposalUndertake::getId).collect(Collectors.toList()); + List slaveUnitNames = slaveUnits.stream().map(ProposalUndertake::getName).collect(Collectors.toList()); + taskData.set("tf_slaveUnitIds", slaveUnitIds); + taskData.set("tf_slaveUnitNames", slaveUnitNames); + taskData.set("tf_slaveUnitNameStr", StrUtil.join(",", slaveUnitNames)); + } + + flowCommonService.executeTask(taskData); + } + + private Record fetchDoingSchoolAuditTask(String code, String sessionId) { + Sql sql = Sqls.create(""" + SELECT + info.id AS opinionId, + t.id AS taskId, + t.displayName AS taskName + FROM wf_process_task t + LEFT JOIN wf_process_instance ins ON ins.id = t.processInstanceId + LEFT JOIN opinion_info info ON info.id = ins.businessNo + LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id + $condition + """); + Cnd cnd = Cnd.NEW(); + cnd.and("info.code", "=", StrUtil.trim(code)); + cnd.andEX("info.sessionId", "=", sessionId); + cnd.and("t.taskName", "=", "schoolAudit"); + cnd.and("t.taskState", "=", ProcessTaskStateEnum.DOING.getCode()); + cnd.and("ta.actorId", "=", SecurityUtil.getUserId()); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.record()); + dao().execute(sql); + return sql.getObject(Record.class); + } + + private void updateOpinionType(String opinionId, Integer typeId) { + dao().update(OpinionInfo.class, Chain.make("typeId", typeId), Cnd.where("id", "=", opinionId)); + + OpinionInfo opinionInfo = new OpinionInfo(); + opinionInfo.setId(opinionId); + opinionInfo.setTypeId(typeId); + ProcessInstance instance = dao().fetch(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "=", opinionId)); + if (instance == null) { + return; + } + + // 同步流程实例和当前待办变量,保持后续节点读取到的表单数据与单条审核一致。 + Dict instanceArgs = FlowUtil.variableToDict(instance.getVariable()); + instanceArgs.set(FlowConst.FORM_DATA, opinionInfo); + dao().update(instance); + + List doingTaskList = flowEngine.processTaskService().getDoingTaskList(instance.getId(), null); + for (ProcessTask task : doingTaskList) { + Dict taskArgs = FlowUtil.variableToDict(task.getVariable()); + taskArgs.set(FlowConst.FORM_DATA, opinionInfo); + } + dao().update(doingTaskList); + } + + private OpinionType fetchOpinionType(String typeName) { + if (StrUtil.isBlank(typeName)) { + return null; + } + String name = StrUtil.trim(typeName); + List types = dao().query(OpinionType.class, + Cnd.where("name", "=", name).or("code", "=", name).and("delFlag", "=", false)); + return types.size() == 1 ? types.get(0) : null; + } + + private ProposalUndertake fetchUndertake(String unitName) { + if (StrUtil.isBlank(unitName)) { + return null; + } + String name = StrUtil.trim(unitName); + List undertakes = dao().query(ProposalUndertake.class, + Cnd.where("name", "=", name).or("code", "=", name).and("delFlag", "=", false)); + return undertakes.size() == 1 ? undertakes.get(0) : null; + } + + private List parseSlaveUnits(String slaveUnitText, List errors) { + List units = new ArrayList<>(); + if (StrUtil.isBlank(slaveUnitText)) { + return units; + } + String[] items = slaveUnitText.split("[,,、;;\\n\\r]+"); + Set names = new HashSet<>(); + for (String item : items) { + String unitName = StrUtil.trim(item); + if (StrUtil.isBlank(unitName)) { + continue; + } + if (!names.add(unitName)) { + errors.add("协办单位重复:" + unitName); + continue; + } + ProposalUndertake undertake = fetchUndertake(unitName); + if (undertake == null) { + errors.add("协办单位不存在:" + unitName); + continue; + } + units.add(undertake); + } + return units; + } + + private AuditResult parseAuditResult(String auditResultText, List errors) { + String value = StrUtil.trim(auditResultText); + if (StrUtil.isBlank(value)) { + errors.add("审核结果不能为空"); + return null; + } + if ("通过".equals(value) || "同意".equals(value) || "YES".equalsIgnoreCase(value)) { + return AuditResult.PASS; + } + if ("不通过".equals(value) || "不同意".equals(value) || "NO".equalsIgnoreCase(value)) { + return AuditResult.REJECT; + } + if ("退回发起人修改".equals(value) || "退回".equals(value) || "ROLLBACK".equalsIgnoreCase(value)) { + return AuditResult.ROLLBACK; + } + errors.add("审核结果只能填写通过、不通过或退回发起人修改"); + return null; + } + + private boolean isEmptyRow(OpinionSchoolAuditImportExcelMode row) { + return StrUtil.isBlank(row.getCode()) + && StrUtil.isBlank(row.getName()) + && StrUtil.isBlank(row.getTypeName()) + && StrUtil.isBlank(row.getCreateUserName()) + && StrUtil.isBlank(row.getDelegationName()) + && StrUtil.isBlank(row.getUnitName()) + && StrUtil.isBlank(row.getMasterUnitName()) + && StrUtil.isBlank(row.getSlaveUnitNames()) + && StrUtil.isBlank(row.getAuditResult()) + && StrUtil.isBlank(row.getAuditOpinion()); + } + + /** + * 导入审核结果与流程参数之间的映射。 + */ + private enum AuditResult { + PASS("YES", ProcessSubmitTypeEnum.AGREE.getCode()), + REJECT("NO", ProcessSubmitTypeEnum.AGREE.getCode()), + ROLLBACK("ROLLBACK", ProcessSubmitTypeEnum.ROLLBACK_TO_OPERATOR.getCode()); + + private final String flowValue; + private final Integer submitType; + + AuditResult(String flowValue, Integer submitType) { + this.flowValue = flowValue; + this.submitType = submitType; + } + } +}