From eaac21eebcbdf8b2c95d8c19407a5b4e1e7a23e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=9C=E5=AD=A6=E6=88=90?= <1274337408@qq.com> Date: Wed, 14 Oct 2026 16:26:16 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8D=97=E4=BA=AC=E7=90=86=E5=B7=A5-=E5=88=86?= =?UTF-8?q?=E7=BA=A7=E6=A0=B8=E5=AF=B9=E9=9C=80=E6=B1=82=EF=BC=9A=201.?= =?UTF-8?q?=E5=B7=A5=E4=BC=9A=E7=BB=84=E7=BB=87=E7=AE=A1=E7=90=86=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E4=BB=85=E7=BB=99=E2=80=98=E6=9C=BA=E5=85=B3=E5=85=9A?= =?UTF-8?q?=E5=A7=94=E5=B7=A5=E4=BC=9A=E2=80=99=E7=9A=84=E6=88=90=E5=91=98?= =?UTF-8?q?=E5=8D=95=E4=BD=8D=E5=88=97=E8=A1=A8=E5=8A=A0=E4=B8=89=E7=BA=A7?= =?UTF-8?q?=E5=8D=95=E4=BD=8D=E7=AE=A1=E7=90=86=E5=91=98=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E6=8C=89=E9=92=AE=EF=BC=9B=202.=E6=96=B0=E5=A2=9E=E2=80=98?= =?UTF-8?q?=E5=88=86=E7=BA=A7=E6=A0=B8=E5=AF=B9=E7=AE=A1=E7=90=86=E2=80=99?= =?UTF-8?q?=EF=BC=88=E6=A0=A1=E5=B7=A5=E4=BC=9A=E7=AE=A1=E7=90=86=E5=91=98?= =?UTF-8?q?=E5=8F=AF=E8=A7=81=EF=BC=89=E5=92=8C=E2=80=98=E5=8D=95=E4=BD=8D?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=A0=B8=E5=AF=B9=E2=80=99=EF=BC=88=E4=B8=89?= =?UTF-8?q?=E7=BA=A7=E5=8D=95=E4=BD=8D=E7=AE=A1=E7=90=86=E5=91=98=E5=92=8C?= =?UTF-8?q?=E5=88=86=E5=B7=A5=E4=BC=9A=E7=AE=A1=E7=90=86=E5=91=98=E5=8F=AF?= =?UTF-8?q?=E8=A7=81=EF=BC=89=E8=8F=9C=E5=8D=95=E9=A1=B5=E9=9D=A2=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/io/v/nutz/base/utils/Roles.java | 5 + .../levelcheck/constant/LevelCheckAction.java | 12 + .../levelcheck/constant/LevelCheckState.java | 12 + .../LevelCheckSchoolController.java | 184 +++ .../controller/LevelCheckUnitController.java | 97 ++ .../levelcheck/model/LevelCheckActionLog.java | 49 + .../levelcheck/model/LevelCheckDataRow.java | 54 + .../model/LevelCheckFileVersion.java | 53 + .../nutz/levelcheck/model/LevelCheckTask.java | 41 + .../levelcheck/model/LevelCheckUnitTask.java | 85 ++ .../levelcheck/service/LevelCheckService.java | 1125 +++++++++++++++++ .../listener/ProposalUnderTakeListener.java | 13 +- .../platform/sys/SysUnionMangeCon.java | 110 +- src/main/resources/db/init_3_level_check.sql | 72 ++ .../views/platform/levelCheck/school.html | 549 ++++++++ .../views/platform/levelCheck/unit.html | 318 +++++ .../views/platform/sys/union/unionMange.html | 78 +- 17 files changed, 2853 insertions(+), 4 deletions(-) create mode 100644 src/main/java/io/v/nutz/levelcheck/constant/LevelCheckAction.java create mode 100644 src/main/java/io/v/nutz/levelcheck/constant/LevelCheckState.java create mode 100644 src/main/java/io/v/nutz/levelcheck/controller/LevelCheckSchoolController.java create mode 100644 src/main/java/io/v/nutz/levelcheck/controller/LevelCheckUnitController.java create mode 100644 src/main/java/io/v/nutz/levelcheck/model/LevelCheckActionLog.java create mode 100644 src/main/java/io/v/nutz/levelcheck/model/LevelCheckDataRow.java create mode 100644 src/main/java/io/v/nutz/levelcheck/model/LevelCheckFileVersion.java create mode 100644 src/main/java/io/v/nutz/levelcheck/model/LevelCheckTask.java create mode 100644 src/main/java/io/v/nutz/levelcheck/model/LevelCheckUnitTask.java create mode 100644 src/main/java/io/v/nutz/levelcheck/service/LevelCheckService.java create mode 100644 src/main/resources/db/init_3_level_check.sql create mode 100644 src/main/resources/views/platform/levelCheck/school.html create mode 100644 src/main/resources/views/platform/levelCheck/unit.html diff --git a/src/main/java/io/v/nutz/base/utils/Roles.java b/src/main/java/io/v/nutz/base/utils/Roles.java index 8a139b5..6626cfa 100644 --- a/src/main/java/io/v/nutz/base/utils/Roles.java +++ b/src/main/java/io/v/nutz/base/utils/Roles.java @@ -16,6 +16,11 @@ public interface Roles { */ String UNION_MANGER = "5d342e614e9a48288d50154bcdcc75d3"; + /** + * 三级单位管理员 + */ + String THIRD_UNIT_MANGER = "e8d957d1ab9f4e56a60b5a53929d18bb"; + /** * 体育活动联合工会管理员 */ diff --git a/src/main/java/io/v/nutz/levelcheck/constant/LevelCheckAction.java b/src/main/java/io/v/nutz/levelcheck/constant/LevelCheckAction.java new file mode 100644 index 0000000..bf46188 --- /dev/null +++ b/src/main/java/io/v/nutz/levelcheck/constant/LevelCheckAction.java @@ -0,0 +1,12 @@ +package io.v.nutz.levelcheck.constant; + +/** + * 分级核对操作类型 + */ +public interface LevelCheckAction { + String DISTRIBUTE = "DISTRIBUTE"; + String CONFIRM = "CONFIRM"; + String SUBMIT_REVISION = "SUBMIT_REVISION"; + String APPROVE = "APPROVE"; + String REJECT = "REJECT"; +} diff --git a/src/main/java/io/v/nutz/levelcheck/constant/LevelCheckState.java b/src/main/java/io/v/nutz/levelcheck/constant/LevelCheckState.java new file mode 100644 index 0000000..f556ba3 --- /dev/null +++ b/src/main/java/io/v/nutz/levelcheck/constant/LevelCheckState.java @@ -0,0 +1,12 @@ +package io.v.nutz.levelcheck.constant; + +/** + * 分级核对状态 + */ +public interface LevelCheckState { + int PENDING_CHECK = 0; + int CONFIRMED = 1; + int PENDING_AUDIT = 2; + int REJECTED = 3; + int APPROVED = 4; +} diff --git a/src/main/java/io/v/nutz/levelcheck/controller/LevelCheckSchoolController.java b/src/main/java/io/v/nutz/levelcheck/controller/LevelCheckSchoolController.java new file mode 100644 index 0000000..ad45e8b --- /dev/null +++ b/src/main/java/io/v/nutz/levelcheck/controller/LevelCheckSchoolController.java @@ -0,0 +1,184 @@ +package io.v.nutz.levelcheck.controller; + +import cn.wizzer.framework.base.Result; +import io.v.nutz.annontation.ViReturn; +import io.v.nutz.levelcheck.service.LevelCheckService; +import org.nutz.ioc.aop.Aop; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.util.NutMap; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.mvc.annotation.*; +import org.nutz.mvc.upload.TempFile; +import org.nutz.mvc.upload.UploadAdaptor; +import org.apache.shiro.authz.annotation.RequiresAuthentication; + +import javax.servlet.http.HttpServletResponse; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +/** + * 校工会分级核对管理 + */ +@IocBean +@At("/platform/levelCheck/school") +@Ok("json:full") +@RequiresAuthentication +public class LevelCheckSchoolController { + @Inject + private LevelCheckService levelCheckService; + + @At("") + @Ok("beetl:/platform/levelCheck/school.html") + public void index() { + levelCheckService.requireSchoolAdmin(); + } + + @At + @ViReturn + public Object taskData(String taskName, String taskDate, int pageNumber, int pageSize) { + try { + levelCheckService.requireSchoolAdmin(); + return Result.success().addData(levelCheckService.schoolTasks( + taskName, taskDate, pageNumber, pageSize)); + } catch (Exception e) { + return Result.error(e.getMessage()); + } + } + + @At + @ViReturn + public Object unitTaskData(String taskId, String unitName, Integer state, Integer managerState, + int pageNumber, int pageSize) { + try { + levelCheckService.requireSchoolAdmin(); + return Result.success().addData(levelCheckService.schoolUnitTasks( + taskId, unitName, state, managerState, pageNumber, pageSize)); + } catch (Exception e) { + return Result.error(e.getMessage()); + } + } + + @At + @ViReturn + public Object unitDataPage(String unitTaskId, String versionType, String searchName, String searchKeyword, + Integer state, + int pageNumber, int pageSize) { + try { + return Result.success().addData(levelCheckService.unitDataPage( + unitTaskId, versionType, searchName, searchKeyword, state, pageNumber, pageSize)); + } catch (Exception e) { + return Result.error(e.getMessage()); + } + } + + @At + @ViReturn + public Object currentDataPage(String taskId, String searchName, String searchKeyword, Integer state, + int pageNumber, int pageSize) { + try { + return Result.success().addData(levelCheckService.currentDataPage( + taskId, searchName, searchKeyword, state, pageNumber, pageSize)); + } catch (Exception e) { + return Result.error(e.getMessage()); + } + } + + @At + @POST + @ViReturn + @Aop(TransAop.READ_COMMITTED) + public Object doDelete(String taskId) { + try { + levelCheckService.deleteTask(taskId); + return null; + } catch (Exception e) { + return Result.error(e.getMessage()); + } + } + + @At + @POST + @ViReturn + @Aop(TransAop.READ_COMMITTED) + @AdaptBy(type = UploadAdaptor.class, args = {"ioc:fileUpload"}) + public Object doUpload(String taskName, String description, + @Param("files") TempFile[] files) { + try { + levelCheckService.requireSchoolAdmin(); + NutMap result = levelCheckService.createTask(taskName, description, files); + String message = result.getInt("errorCount") > 0 ? "导入完成,部分数据未分发" : "分发成功"; + return Result.success(message).addData(result); + } catch (Exception e) { + return Result.error(e.getMessage()); + } + } + + @At + public void exportImportErrors(String cacheKey, HttpServletResponse response) { + try { + levelCheckService.requireSchoolAdmin(); + byte[] bytes = levelCheckService.getImportErrorFile(cacheKey); + String fileName = "分级核对导入错误.xlsx"; + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setHeader("Content-Disposition", "attachment;filename=" + + URLEncoder.encode(fileName, StandardCharsets.UTF_8.name())); + response.getOutputStream().write(bytes); + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + } + } + + @At + public void downloadTemplate(HttpServletResponse response) { + try { + byte[] bytes = levelCheckService.getImportTemplate(); + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setHeader("Content-Disposition", "attachment;filename=" + + URLEncoder.encode("分级核对导入模板.xlsx", StandardCharsets.UTF_8.name())); + response.getOutputStream().write(bytes); + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + } + } + + @At + @ViReturn + @Aop(TransAop.READ_COMMITTED) + public Object doAudit(String unitTaskId, boolean approved, String opinion) { + try { + levelCheckService.audit(unitTaskId, approved, opinion); + return null; + } catch (Exception e) { + return Result.error(e.getMessage()); + } + } + + @At + public void download(String unitTaskId, String versionType, HttpServletResponse response) { + try { + byte[] bytes = levelCheckService.getDownloadBytes(unitTaskId, versionType, true); + String fileName = levelCheckService.getDownloadFileName(unitTaskId, versionType, true); + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setHeader("Content-Disposition", "attachment;filename=" + + URLEncoder.encode(fileName, StandardCharsets.UTF_8.name())); + response.getOutputStream().write(bytes); + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + } + } + + @At + public void exportCurrent(String taskId, HttpServletResponse response) { + try { + byte[] bytes = levelCheckService.exportCurrent(taskId); + String fileName = "分级核对当前生效数据.xlsx"; + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setHeader("Content-Disposition", "attachment;filename=" + + URLEncoder.encode(fileName, StandardCharsets.UTF_8.name())); + response.getOutputStream().write(bytes); + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + } + } +} diff --git a/src/main/java/io/v/nutz/levelcheck/controller/LevelCheckUnitController.java b/src/main/java/io/v/nutz/levelcheck/controller/LevelCheckUnitController.java new file mode 100644 index 0000000..41b2e71 --- /dev/null +++ b/src/main/java/io/v/nutz/levelcheck/controller/LevelCheckUnitController.java @@ -0,0 +1,97 @@ +package io.v.nutz.levelcheck.controller; + +import cn.wizzer.framework.base.Result; +import io.v.nutz.annontation.ViReturn; +import io.v.nutz.levelcheck.service.LevelCheckService; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.ioc.aop.Aop; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.mvc.annotation.*; +import org.nutz.mvc.upload.TempFile; +import org.nutz.mvc.upload.UploadAdaptor; +import org.apache.shiro.authz.annotation.RequiresAuthentication; + +import javax.servlet.http.HttpServletResponse; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +/** + * 单位分级核对 + */ +@IocBean +@At("/platform/levelCheck/unit") +@Ok("json:full") +@RequiresAuthentication +public class LevelCheckUnitController { + @Inject + private LevelCheckService levelCheckService; + + @At("") + @Ok("beetl:/platform/levelCheck/unit.html") + public void index() { + } + + @At + @ViReturn + public Object taskData(String keyword, Integer state, int pageNumber, int pageSize) { + try { + return Result.success().addData(levelCheckService.myUnitTasks( + keyword, state, pageNumber, pageSize)); + } catch (Exception e) { + return Result.error(e.getMessage()); + } + } + + @At + @ViReturn + @Aop(TransAop.READ_COMMITTED) + public Object doConfirm(String unitTaskId) { + try { + levelCheckService.confirm(unitTaskId); + return null; + } catch (Exception e) { + return Result.error(e.getMessage()); + } + } + + @At + @ViReturn + public Object dataPage(String unitTaskId, String searchName, String searchKeyword, Integer state, + int pageNumber, int pageSize) { + try { + return Result.success().addData(levelCheckService.myUnitDataPage( + unitTaskId, searchName, searchKeyword, state, pageNumber, pageSize)); + } catch (Exception e) { + return Result.error(e.getMessage()); + } + } + + @At + @POST + @ViReturn + @Aop(TransAop.READ_COMMITTED) + @AdaptBy(type = UploadAdaptor.class, args = {"ioc:fileUpload"}) + public Object doUploadRevision(String unitTaskId, @Param("file") TempFile file) { + try { + levelCheckService.submitRevision(unitTaskId, file); + return null; + } catch (Exception e) { + return Result.error(e.getMessage()); + } + } + + @At + public void download(String unitTaskId, HttpServletResponse response) { + try { + byte[] bytes = levelCheckService.getDownloadBytes(unitTaskId, "current", false); + String fileName = levelCheckService.getDownloadFileName(unitTaskId, "current", false); + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setHeader("Content-Disposition", "attachment;filename=" + + URLEncoder.encode(fileName, StandardCharsets.UTF_8.name())); + response.getOutputStream().write(bytes); + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + } + } +} diff --git a/src/main/java/io/v/nutz/levelcheck/model/LevelCheckActionLog.java b/src/main/java/io/v/nutz/levelcheck/model/LevelCheckActionLog.java new file mode 100644 index 0000000..618c794 --- /dev/null +++ b/src/main/java/io/v/nutz/levelcheck/model/LevelCheckActionLog.java @@ -0,0 +1,49 @@ +package io.v.nutz.levelcheck.model; + +import io.v.nutz.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.nutz.dao.entity.annotation.*; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +@Data +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +@Table("level_check_action_log") +@TableIndexes({@Index(name = "IDX_LEVEL_CHECK_LOG_UNIT", fields = {"unitTaskId"}, unique = false)}) +public class LevelCheckActionLog extends BaseModel { + @Name + @PrevInsert(uu32 = true) + @ColDefine(type = ColType.VARCHAR, width = 32) + private String id; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 32) + private String unitTaskId; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 30) + private String action; + + @Column + private Integer fromState; + + @Column + private Integer toState; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 32) + private String operatorId; + + @Column + private Long operateAt; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 500) + private String opinion; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 32) + private String fileVersionId; +} diff --git a/src/main/java/io/v/nutz/levelcheck/model/LevelCheckDataRow.java b/src/main/java/io/v/nutz/levelcheck/model/LevelCheckDataRow.java new file mode 100644 index 0000000..f09a01d --- /dev/null +++ b/src/main/java/io/v/nutz/levelcheck/model/LevelCheckDataRow.java @@ -0,0 +1,54 @@ +package io.v.nutz.levelcheck.model; + +import io.v.nutz.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.nutz.dao.entity.annotation.*; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +/** + * 分级核对固定模板数据明细 + */ +@Data +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +@Table("level_check_data_row") +@TableIndexes({@Index(name = "IDX_LEVEL_CHECK_DATA_VERSION", fields = {"versionId", "sequenceNo"}, unique = true)}) +public class LevelCheckDataRow extends BaseModel { + @Name + @PrevInsert(uu32 = true) + @ColDefine(type = ColType.VARCHAR, width = 32) + private String id; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 32) + private String versionId; + + @Column + private Integer sequenceNo; + + @Column + @ColDefine(customType = "text") + private String salaryUnitCode; + + @Column + @ColDefine(customType = "text") + private String employeeNo; + + @Column + @ColDefine(customType = "text") + private String employeeName; + + @Column + @ColDefine(customType = "text") + private String unionFee; + + @Column + @ColDefine(customType = "text") + private String yearMonth; + + @Column + @ColDefine(customType = "text") + private String unitName; +} diff --git a/src/main/java/io/v/nutz/levelcheck/model/LevelCheckFileVersion.java b/src/main/java/io/v/nutz/levelcheck/model/LevelCheckFileVersion.java new file mode 100644 index 0000000..aed3dfa --- /dev/null +++ b/src/main/java/io/v/nutz/levelcheck/model/LevelCheckFileVersion.java @@ -0,0 +1,53 @@ +package io.v.nutz.levelcheck.model; + +import io.v.nutz.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.nutz.dao.entity.annotation.*; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +@Data +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +@Table("level_check_file_version") +@TableIndexes({@Index(name = "IDX_LEVEL_CHECK_FILE_UNIT", fields = {"unitTaskId", "versionNo"}, unique = true)}) +public class LevelCheckFileVersion extends BaseModel { + @Name + @PrevInsert(uu32 = true) + @ColDefine(type = ColType.VARCHAR, width = 32) + private String id; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 32) + private String unitTaskId; + + @Column + private Integer versionNo; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 20) + private String versionType; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 32) + private String sysFileId; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 200) + private String originalFileName; + + @Column + private Integer rowCount; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 64) + private String sha256; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 32) + private String uploadedBy; + + @Column + private Long uploadedAt; +} diff --git a/src/main/java/io/v/nutz/levelcheck/model/LevelCheckTask.java b/src/main/java/io/v/nutz/levelcheck/model/LevelCheckTask.java new file mode 100644 index 0000000..b6156b4 --- /dev/null +++ b/src/main/java/io/v/nutz/levelcheck/model/LevelCheckTask.java @@ -0,0 +1,41 @@ +package io.v.nutz.levelcheck.model; + +import io.v.nutz.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.nutz.dao.entity.annotation.*; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +@Data +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +@Table("level_check_task") +public class LevelCheckTask extends BaseModel { + @Name + @PrevInsert(uu32 = true) + @ColDefine(type = ColType.VARCHAR, width = 32) + private String id; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 100) + @Comment("任务名称") + private String name; + + @Column + @Comment("任务日期") + private Long taskDate; + + @Column + @Comment("总单位数") + private Integer totalUnits; + + @Column + @Comment("已完成单位数") + private Integer finishedUnits; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 500) + @Comment("任务说明") + private String description; +} diff --git a/src/main/java/io/v/nutz/levelcheck/model/LevelCheckUnitTask.java b/src/main/java/io/v/nutz/levelcheck/model/LevelCheckUnitTask.java new file mode 100644 index 0000000..54dbb60 --- /dev/null +++ b/src/main/java/io/v/nutz/levelcheck/model/LevelCheckUnitTask.java @@ -0,0 +1,85 @@ +package io.v.nutz.levelcheck.model; + +import io.v.nutz.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.nutz.dao.entity.annotation.*; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +@Data +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +@Table("level_check_unit_task") +@TableIndexes({ + @Index(name = "IDX_LEVEL_CHECK_UNIT_TASK", fields = {"taskId", "unitId"}, unique = true), + @Index(name = "IDX_LEVEL_CHECK_UNIT_STATE", fields = {"state"}, unique = false) +}) +public class LevelCheckUnitTask extends BaseModel { + @Name + @PrevInsert(uu32 = true) + @ColDefine(type = ColType.VARCHAR, width = 32) + private String id; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 32) + private String taskId; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 32) + private String unitId; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 100) + private String unitName; + + @Column + private Integer unitLevel; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 20) + private String managerType; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 32) + private String unionId; + + @Column + private Integer state; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 32) + private String originalVersionId; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 32) + private String currentVersionId; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 32) + private String pendingVersionId; + + @Column + private Integer rowCount; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 32) + private String checkedBy; + + @Column + private Long checkedAt; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 32) + private String auditedBy; + + @Column + private Long auditedAt; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 500) + private String auditOpinion; + + @Column + private Integer dataVersion; +} diff --git a/src/main/java/io/v/nutz/levelcheck/service/LevelCheckService.java b/src/main/java/io/v/nutz/levelcheck/service/LevelCheckService.java new file mode 100644 index 0000000..ab41512 --- /dev/null +++ b/src/main/java/io/v/nutz/levelcheck/service/LevelCheckService.java @@ -0,0 +1,1125 @@ +package io.v.nutz.levelcheck.service; + +import io.v.nutz.base.utils.Roles; +import io.v.nutz.base.page.Pagination; +import io.v.nutz.levelcheck.constant.LevelCheckAction; +import io.v.nutz.levelcheck.constant.LevelCheckState; +import io.v.nutz.levelcheck.model.LevelCheckActionLog; +import io.v.nutz.levelcheck.model.LevelCheckDataRow; +import io.v.nutz.levelcheck.model.LevelCheckFileVersion; +import io.v.nutz.levelcheck.model.LevelCheckTask; +import io.v.nutz.levelcheck.model.LevelCheckUnitTask; +import io.v.nutz.sys.models.Sys_config; +import io.v.nutz.sys.models.Sys_file; +import io.v.nutz.sys.models.Sys_unit; +import io.v.nutz.sys.models.Sys_union; +import io.v.nutz.sys.models.Sys_user_role; +import io.v.nutz.web.commons.utils.ShiroUtil; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.nutz.boot.starter.ftp.FtpService; +import org.nutz.dao.Chain; +import org.nutz.dao.Cnd; +import org.nutz.dao.Dao; +import org.nutz.dao.Sqls; +import org.nutz.dao.sql.Sql; +import org.nutz.integration.jedis.RedisService; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.ioc.aop.Aop; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.lang.Strings; +import org.nutz.lang.random.R; +import org.nutz.lang.util.NutMap; +import org.nutz.mvc.upload.TempFile; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.security.MessageDigest; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.*; + +/** + * 分级核对业务服务 + */ +@IocBean +public class LevelCheckService { + private static final List REQUIRED_HEADERS = Arrays.asList( + "薪酬单位代码", "职工号", "姓名", "扣工会会费", "年月", "所在单位" + ); + private static final String UNIT_HEADER = "所在单位"; + private static final ZoneId TASK_ZONE_ID = ZoneId.of("Asia/Shanghai"); + + @Inject + private Dao dao; + + @Inject + private FtpService ftpService; + + @Inject + private RedisService redisService; + + public Pagination schoolTasks(String taskName, String taskDate, int pageNumber, int pageSize) { + int currentPage = Math.max(pageNumber, 1); + int currentPageSize = Math.min(Math.max(pageSize, 1), 100); + Cnd cnd = Cnd.NEW(); + if (Strings.isNotBlank(taskName)) { + cnd.and("name", "like", "%" + Strings.trim(taskName) + "%"); + } + if (Strings.isNotBlank(taskDate)) { + long[] taskDateRange = taskDateRange(Strings.trim(taskDate)); + cnd.and("taskDate", ">=", taskDateRange[0]).and("taskDate", "<", taskDateRange[1]); + } + int totalCount = dao.count(LevelCheckTask.class, cnd); + cnd.desc("createdAt"); + List tasks = dao.query(LevelCheckTask.class, cnd, + dao.createPager(currentPage, currentPageSize)); + return new Pagination(currentPage, currentPageSize, totalCount, tasks); + } + + public Pagination schoolUnitTasks(String taskId, String unitName, Integer state, Integer managerState, + int pageNumber, int pageSize) { + int currentPage = Math.max(pageNumber, 1); + int currentPageSize = Math.min(Math.max(pageSize, 1), 100); + String unitKeyword = Strings.trim(Strings.sNull(unitName)); + int stateValue = state == null ? -1 : state; + int managerStateValue = managerState == null ? -1 : managerState; + String specialUnionId = specialUnionId(); + String fromWhere = """ + FROM level_check_unit_task ut + LEFT JOIN level_check_task t ON t.id = ut.taskId + LEFT JOIN sys_unit currentUnit ON currentUnit.id = ut.unitId + WHERE ut.taskId = @taskId + AND (@unitName = '' OR ut.unitName LIKE @unitNameLike) + AND (@state < 0 OR ut.state = @state) + AND (@managerState < 0 OR @managerState = CASE WHEN EXISTS ( + SELECT 1 + FROM sys_user_role managerRole + WHERE ((currentUnit.unionid = @specialUnionId AND @specialUnionId <> '' + AND managerRole.roleId = @thirdRoleId AND managerRole.unitid = ut.unitId) + OR ((currentUnit.unionid <> @specialUnionId OR @specialUnionId = '') + AND currentUnit.unionid IS NOT NULL AND currentUnit.unionid <> '' + AND managerRole.roleId = @unionRoleId + AND managerRole.unionid = currentUnit.unionid)) + ) THEN 1 ELSE 0 END) + """; + Sql sql = Sqls.create(""" + SELECT ut.*, t.name taskName, t.taskDate, + CASE WHEN currentUnit.unionid = @specialUnionId AND @specialUnionId <> '' THEN ( + SELECT GROUP_CONCAT(DISTINCT CONCAT(u.username, '(', u.loginname, ')') SEPARATOR '、') + FROM sys_user_role ur + LEFT JOIN sys_user u ON u.id = ur.userId + WHERE ur.roleId = @thirdRoleId AND ur.unitid = ut.unitId + ) ELSE ( + SELECT GROUP_CONCAT(DISTINCT CONCAT(u.username, '(', u.loginname, ')') SEPARATOR '、') + FROM sys_user_role ur + LEFT JOIN sys_user u ON u.id = ur.userId + WHERE ur.roleId = @unionRoleId AND ur.unionid = currentUnit.unionid + ) END managerNames + """ + fromWhere + """ + ORDER BY ut.unitName + """); + Sql countSql = Sqls.create("SELECT COUNT(1) " + fromWhere); + for (Sql currentSql : Arrays.asList(sql, countSql)) { + currentSql.setParam("taskId", taskId); + currentSql.setParam("unitName", unitKeyword); + currentSql.setParam("unitNameLike", "%" + unitKeyword + "%"); + currentSql.setParam("state", stateValue); + currentSql.setParam("managerState", managerStateValue); + currentSql.setParam("specialUnionId", specialUnionId); + currentSql.setParam("unionRoleId", Roles.UNION_MANGER); + currentSql.setParam("thirdRoleId", Roles.THIRD_UNIT_MANGER); + } + sql.setPager(dao.createPager(currentPage, currentPageSize)); + sql.setCallback(Sqls.callback.maps()); + dao.execute(sql); + List unitTasks = sql.getList(NutMap.class); + for (NutMap unitTask : unitTasks) { + if (!unitTask.containsKey("managerNames")) { + unitTask.setv("managerNames", unitTask.get("managernames")); + } + } + countSql.setCallback(Sqls.callback.integer()); + dao.execute(countSql); + return new Pagination(currentPage, currentPageSize, countSql.getInt(), unitTasks); + } + + /** + * 分页查看单个单位的生效数据或待审核修改版数据 + */ + public Pagination unitDataPage(String unitTaskId, String versionType, String searchName, String searchKeyword, + Integer state, + int pageNumber, int pageSize) throws Exception { + requireSchoolAdmin(); + LevelCheckUnitTask unitTask = dao.fetch(LevelCheckUnitTask.class, unitTaskId); + if (unitTask == null) { + throw new IllegalArgumentException("单位任务不存在"); + } + boolean pending = "pending".equals(versionType); + String versionId = pending ? unitTask.getPendingVersionId() : unitTask.getCurrentVersionId(); + if (Strings.isBlank(versionId)) { + throw new IllegalArgumentException(pending ? "单位修改版数据不存在" : "单位生效数据不存在"); + } + return dataPage(Collections.singletonList(versionId), + searchName, searchKeyword, state, pageNumber, pageSize); + } + + /** + * 单位管理员分页查看本单位当前生效数据 + */ + public Pagination myUnitDataPage(String unitTaskId, String searchName, String searchKeyword, Integer state, + int pageNumber, int pageSize) throws Exception { + LevelCheckUnitTask unitTask = requireManagedUnitTask(unitTaskId); + if (Strings.isBlank(unitTask.getCurrentVersionId())) { + throw new IllegalArgumentException("单位生效数据不存在"); + } + return dataPage(Collections.singletonList(unitTask.getCurrentVersionId()), + searchName, searchKeyword, state, pageNumber, pageSize); + } + + /** + * 获取分级核对固定导入模板 + */ + public byte[] getImportTemplate() throws Exception { + requireSchoolAdmin(); + return createUnitWorkbook(REQUIRED_HEADERS, Collections.emptyList()); + } + + /** + * 分页查看任务内所有单位的当前生效数据 + */ + public Pagination currentDataPage(String taskId, String searchName, String searchKeyword, Integer state, + int pageNumber, int pageSize) throws Exception { + requireSchoolAdmin(); + if (dao.fetch(LevelCheckTask.class, taskId) == null) { + throw new IllegalArgumentException("任务不存在"); + } + List unitTasks = dao.query(LevelCheckUnitTask.class, + Cnd.where("taskId", "=", taskId).asc("unitName")); + List versionIds = new ArrayList<>(); + for (LevelCheckUnitTask unitTask : unitTasks) { + if (Strings.isNotBlank(unitTask.getCurrentVersionId())) { + versionIds.add(unitTask.getCurrentVersionId()); + } + } + return dataPage(versionIds, searchName, searchKeyword, state, pageNumber, pageSize); + } + + public Pagination myUnitTasks(String keyword, Integer state, int pageNumber, int pageSize) { + int currentPage = Math.max(pageNumber, 1); + int currentPageSize = Math.min(Math.max(pageSize, 1), 100); + String userId = currentUserId(); + String searchKeyword = Strings.trim(Strings.sNull(keyword)); + long[] taskDateRange = optionalTaskDateRange(searchKeyword); + int stateValue = state == null ? -1 : state; + String specialUnionId = specialUnionId(); + String fromWhere = """ + FROM level_check_unit_task ut + LEFT JOIN level_check_task t ON t.id = ut.taskId + LEFT JOIN sys_unit currentUnit ON currentUnit.id = ut.unitId + WHERE EXISTS ( + SELECT 1 FROM sys_user_role ur + WHERE ur.userId = @userId + AND (((currentUnit.unionid <> @specialUnionId OR @specialUnionId = '') + AND currentUnit.unionid IS NOT NULL + AND currentUnit.unionid <> '' AND ur.roleId = @unionRoleId + AND ur.unionid = currentUnit.unionid) + OR (currentUnit.unionid = @specialUnionId AND @specialUnionId <> '' + AND ur.roleId = @thirdRoleId AND ur.unitid = ut.unitId)) + ) + AND (@keyword = '' OR t.name LIKE @keywordLike OR ut.unitName LIKE @keywordLike + OR (@taskDateStart > 0 AND t.taskDate >= @taskDateStart AND t.taskDate < @taskDateEnd)) + AND (@state < 0 OR ut.state = @state) + """; + Sql sql = Sqls.create(""" + SELECT ut.*, t.name taskName, t.taskDate, t.description + """ + fromWhere + """ + ORDER BY t.createdAt DESC, ut.unitName + """); + Sql countSql = Sqls.create("SELECT COUNT(1) " + fromWhere); + for (Sql currentSql : Arrays.asList(sql, countSql)) { + currentSql.setParam("userId", userId); + currentSql.setParam("specialUnionId", specialUnionId); + currentSql.setParam("unionRoleId", Roles.UNION_MANGER); + currentSql.setParam("thirdRoleId", Roles.THIRD_UNIT_MANGER); + currentSql.setParam("keyword", searchKeyword); + currentSql.setParam("keywordLike", "%" + searchKeyword + "%"); + currentSql.setParam("taskDateStart", taskDateRange[0]); + currentSql.setParam("taskDateEnd", taskDateRange[1]); + currentSql.setParam("state", stateValue); + } + sql.setPager(dao.createPager(currentPage, currentPageSize)); + sql.setCallback(Sqls.callback.maps()); + dao.execute(sql); + countSql.setCallback(Sqls.callback.integer()); + dao.execute(countSql); + return new Pagination(currentPage, currentPageSize, countSql.getInt(), sql.getList(NutMap.class)); + } + + /** + * 删除分级核对任务及其全部业务数据 + */ + @Aop(TransAop.READ_COMMITTED) + public void deleteTask(String taskId) { + requireSchoolAdmin(); + if (Strings.isBlank(taskId)) { + throw new IllegalArgumentException("任务ID不能为空"); + } + LevelCheckTask task = dao.fetch(LevelCheckTask.class, taskId); + if (task == null) { + throw new IllegalArgumentException("任务不存在或已删除"); + } + + List unitTasks = dao.query(LevelCheckUnitTask.class, + Cnd.where("taskId", "=", taskId)); + if (!unitTasks.isEmpty()) { + List unitTaskIds = new ArrayList<>(); + for (LevelCheckUnitTask unitTask : unitTasks) { + unitTaskIds.add(unitTask.getId()); + } + List versions = dao.query(LevelCheckFileVersion.class, + Cnd.where("unitTaskId", "in", unitTaskIds)); + if (!versions.isEmpty()) { + List versionIds = new ArrayList<>(); + for (LevelCheckFileVersion version : versions) { + versionIds.add(version.getId()); + } + dao.clear(LevelCheckDataRow.class, Cnd.where("versionId", "in", versionIds)); + } + dao.clear(LevelCheckActionLog.class, Cnd.where("unitTaskId", "in", unitTaskIds)); + dao.clear(LevelCheckFileVersion.class, Cnd.where("unitTaskId", "in", unitTaskIds)); + dao.clear(LevelCheckUnitTask.class, Cnd.where("taskId", "=", taskId)); + } + dao.delete(LevelCheckTask.class, taskId); + } + + /** + * 上传文件并按每行“所在单位”分组,文件名不参与单位匹配 + */ + @Aop(TransAop.READ_COMMITTED) + public NutMap createTask(String taskName, String description, TempFile[] files) throws Exception { + if (Strings.isBlank(taskName)) { + throw new IllegalArgumentException("任务名称不能为空"); + } + long currentTaskDate = System.currentTimeMillis(); + if (files == null || files.length == 0) { + throw new IllegalArgumentException("请至少上传一个 Excel 文件"); + } + if (files.length > 20) { + throw new IllegalArgumentException("一次最多上传 20 个 Excel 文件"); + } + + ParsedBatch batch = parseFiles(files); + if (batch.totalRows == 0) { + throw new IllegalArgumentException("Excel 中没有可分发的数据"); + } + Map matchedUnits = new LinkedHashMap<>(); + Iterator>>> iterator = batch.unitRows.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry>> entry = iterator.next(); + try { + matchedUnits.put(entry.getKey(), matchUnit(entry.getKey())); + } catch (IllegalArgumentException e) { + for (LinkedHashMap values : entry.getValue()) { + addImportError(batch, values, e.getMessage()); + } + iterator.remove(); + } + } + + String errorCacheKey = cacheImportErrors(batch.errorRows); + if (batch.unitRows.isEmpty()) { + return new NutMap().setv("taskId", "").setv("taskDate", currentTaskDate) + .setv("units", new ArrayList<>()).setv("totalCount", batch.totalRows) + .setv("successCount", 0).setv("errorCount", batch.errorRows.size()) + .setv("errorCacheKey", errorCacheKey); + } + + LevelCheckTask task = new LevelCheckTask() + .setId(R.UU32()) + .setName(Strings.trim(taskName)) + .setTaskDate(currentTaskDate) + .setDescription(Strings.trim(Strings.sNull(description))) + .setTotalUnits(batch.unitRows.size()) + .setFinishedUnits(0); + dao.insert(task); + + List resultUnits = new ArrayList<>(); + for (Map.Entry>> entry : batch.unitRows.entrySet()) { + Sys_unit unit = matchedUnits.get(entry.getKey()); + + LevelCheckUnitTask unitTask = new LevelCheckUnitTask() + .setId(R.UU32()) + .setTaskId(task.getId()) + .setUnitId(unit.getId()) + .setUnitName(unit.getName()) + .setUnitLevel(unit.getUnitlevel()) + .setManagerType(isSpecialUnionUnit(unit) ? "THIRD" : "UNION") + .setUnionId(unit.getUnionid()) + .setState(LevelCheckState.PENDING_CHECK) + .setRowCount(entry.getValue().size()) + .setDataVersion(1); + dao.insert(unitTask); + + String sourceNames = String.join("、", batch.sourceNames.get(entry.getKey())); + LevelCheckFileVersion version = saveVersion(unitTask, batch.headers, entry.getValue(), + sourceNames, "ORIGINAL", 1); + unitTask.setOriginalVersionId(version.getId()); + unitTask.setCurrentVersionId(version.getId()); + dao.update(LevelCheckUnitTask.class, + Chain.make("originalVersionId", version.getId()).add("currentVersionId", version.getId()), + Cnd.where("id", "=", unitTask.getId())); + addLog(unitTask.getId(), LevelCheckAction.DISTRIBUTE, null, + LevelCheckState.PENDING_CHECK, null, version.getId()); + + resultUnits.add(new NutMap().setv("unitId", unit.getId()) + .setv("unitName", unit.getName()).setv("rowCount", entry.getValue().size())); + } + return new NutMap().setv("taskId", task.getId()).setv("taskDate", currentTaskDate) + .setv("units", resultUnits).setv("totalCount", batch.totalRows) + .setv("successCount", batch.totalRows - batch.errorRows.size()) + .setv("errorCount", batch.errorRows.size()).setv("errorCacheKey", errorCacheKey); + } + + @Aop(TransAop.READ_COMMITTED) + public void confirm(String unitTaskId) { + LevelCheckUnitTask task = requireManagedUnitTask(unitTaskId); + if (task.getState() != LevelCheckState.PENDING_CHECK) { + throw new IllegalStateException("当前状态不能确认无误"); + } + int count = dao.update(LevelCheckUnitTask.class, + Chain.make("state", LevelCheckState.CONFIRMED) + .add("checkedBy", currentUserId()).add("checkedAt", System.currentTimeMillis()), + Cnd.where("id", "=", unitTaskId).and("state", "=", LevelCheckState.PENDING_CHECK)); + if (count != 1) { + throw new IllegalStateException("任务状态已发生变化,请刷新后重试"); + } + addLog(unitTaskId, LevelCheckAction.CONFIRM, LevelCheckState.PENDING_CHECK, + LevelCheckState.CONFIRMED, null, task.getCurrentVersionId()); + refreshProgress(task.getTaskId()); + } + + @Aop(TransAop.READ_COMMITTED) + public void submitRevision(String unitTaskId, TempFile file) throws Exception { + if (file == null) { + throw new IllegalArgumentException("请选择修改后的 Excel 文件"); + } + LevelCheckUnitTask unitTask = requireManagedUnitTask(unitTaskId); + if (unitTask.getState() != LevelCheckState.PENDING_CHECK + && unitTask.getState() != LevelCheckState.REJECTED) { + throw new IllegalStateException("当前状态不能上传修改版"); + } + + ParsedBatch batch = parseFiles(new TempFile[]{file}); + if (!batch.errorRows.isEmpty() || batch.unitRows.size() != 1 + || !batch.unitRows.containsKey(normalizeUnitName(unitTask.getUnitName()))) { + throw new IllegalArgumentException("修改版只能包含当前单位的数据"); + } + List originalHeaders = readVersionHeaders(unitTask.getOriginalVersionId()); + if (!originalHeaders.equals(new ArrayList<>(batch.headers))) { + throw new IllegalArgumentException("修改版表头必须与原单位数据一致"); + } + + List> rows = batch.unitRows.get(normalizeUnitName(unitTask.getUnitName())); + int versionNo = unitTask.getDataVersion() == null ? 2 : unitTask.getDataVersion() + 1; + LevelCheckFileVersion version = saveVersion(unitTask, batch.headers, rows, + file.getSubmittedFileName(), "REVISION", versionNo); + int fromState = unitTask.getState(); + int count = dao.update(LevelCheckUnitTask.class, + Chain.make("state", LevelCheckState.PENDING_AUDIT) + .add("pendingVersionId", version.getId()) + .add("checkedBy", currentUserId()).add("checkedAt", System.currentTimeMillis()) + .add("dataVersion", versionNo).add("auditOpinion", null), + Cnd.where("id", "=", unitTaskId).and("state", "=", fromState)); + if (count != 1) { + throw new IllegalStateException("任务状态已发生变化,请刷新后重试"); + } + addLog(unitTaskId, LevelCheckAction.SUBMIT_REVISION, fromState, + LevelCheckState.PENDING_AUDIT, null, version.getId()); + refreshProgress(unitTask.getTaskId()); + } + + @Aop(TransAop.READ_COMMITTED) + public void audit(String unitTaskId, boolean approved, String opinion) { + requireSchoolAdmin(); + LevelCheckUnitTask unitTask = dao.fetch(LevelCheckUnitTask.class, unitTaskId); + if (unitTask == null || unitTask.getState() != LevelCheckState.PENDING_AUDIT + || Strings.isBlank(unitTask.getPendingVersionId())) { + throw new IllegalStateException("当前任务没有待审核修改版"); + } + String auditOpinion = Strings.trim(Strings.sNull(opinion)); + if (!approved && Strings.isBlank(auditOpinion)) { + throw new IllegalArgumentException("退回时必须填写原因"); + } + if (auditOpinion.length() > 500) { + throw new IllegalArgumentException("退回原因不能超过500字"); + } + + int toState = approved ? LevelCheckState.APPROVED : LevelCheckState.REJECTED; + Chain chain = Chain.make("state", toState) + .add("auditedBy", currentUserId()).add("auditedAt", System.currentTimeMillis()) + .add("auditOpinion", auditOpinion); + if (approved) { + LevelCheckFileVersion pendingVersion = dao.fetch( + LevelCheckFileVersion.class, unitTask.getPendingVersionId()); + if (pendingVersion == null || !unitTaskId.equals(pendingVersion.getUnitTaskId())) { + throw new IllegalStateException("待审核修改版不存在"); + } + chain.add("currentVersionId", pendingVersion.getId()) + .add("rowCount", pendingVersion.getRowCount()); + } + int count = dao.update(LevelCheckUnitTask.class, chain, + Cnd.where("id", "=", unitTaskId) + .and("state", "=", LevelCheckState.PENDING_AUDIT) + .and("pendingVersionId", "=", unitTask.getPendingVersionId())); + if (count != 1) { + throw new IllegalStateException("任务状态已发生变化,请刷新后重试"); + } + addLog(unitTaskId, approved ? LevelCheckAction.APPROVE : LevelCheckAction.REJECT, + LevelCheckState.PENDING_AUDIT, toState, auditOpinion, unitTask.getPendingVersionId()); + refreshProgress(unitTask.getTaskId()); + } + + public byte[] getDownloadBytes(String unitTaskId, String versionType, boolean schoolSide) throws Exception { + LevelCheckUnitTask unitTask = requireDownloadUnitTask(unitTaskId, schoolSide); + String versionId; + if ("original".equals(versionType)) { + versionId = unitTask.getOriginalVersionId(); + } else if ("pending".equals(versionType)) { + versionId = unitTask.getPendingVersionId(); + } else { + versionId = unitTask.getCurrentVersionId(); + } + LevelCheckFileVersion version = dao.fetch(LevelCheckFileVersion.class, versionId); + if (version == null || !unitTaskId.equals(version.getUnitTaskId())) { + throw new IllegalArgumentException("数据版本不存在"); + } + LevelCheckData data = loadVersionData(version); + return createUnitWorkbook(data.headers, data.rows); + } + + public String getDownloadFileName(String unitTaskId, String versionType, boolean schoolSide) { + LevelCheckUnitTask unitTask = requireDownloadUnitTask(unitTaskId, schoolSide); + LevelCheckTask task = dao.fetch(LevelCheckTask.class, unitTask.getTaskId()); + if (task == null) { + throw new IllegalArgumentException("核对任务不存在"); + } + String versionName = switch (Strings.sNull(versionType)) { + case "original" -> "原始数据"; + case "pending" -> "修改版"; + default -> "生效数据"; + }; + return safeFileName(task.getName()) + "_" + safeFileName(unitTask.getUnitName()) + + "_" + versionName + ".xlsx"; + } + + private LevelCheckUnitTask requireDownloadUnitTask(String unitTaskId, boolean schoolSide) { + if (!schoolSide) { + return requireManagedUnitTask(unitTaskId); + } + requireSchoolAdmin(); + LevelCheckUnitTask unitTask = dao.fetch(LevelCheckUnitTask.class, unitTaskId); + if (unitTask == null) { + throw new IllegalArgumentException("单位任务不存在"); + } + return unitTask; + } + + /** + * 汇总任务当前生效版本 + */ + public byte[] exportCurrent(String taskId) throws Exception { + requireSchoolAdmin(); + LevelCheckTask task = dao.fetch(LevelCheckTask.class, taskId); + if (task == null) { + throw new IllegalArgumentException("任务不存在"); + } + List unitTasks = dao.query(LevelCheckUnitTask.class, + Cnd.where("taskId", "=", taskId).asc("unitName")); + LinkedHashSet headers = new LinkedHashSet<>(); + List> rows = new ArrayList<>(); + for (LevelCheckUnitTask unitTask : unitTasks) { + LevelCheckFileVersion version = dao.fetch(LevelCheckFileVersion.class, unitTask.getCurrentVersionId()); + if (version == null) { + throw new IllegalStateException(unitTask.getUnitName() + " 的生效数据不存在"); + } + LevelCheckData data = loadVersionData(version); + headers.addAll(data.headers); + rows.addAll(data.rows); + } + return createUnitWorkbook(headers, rows); + } + + private ParsedBatch parseFiles(TempFile[] files) throws Exception { + ParsedBatch batch = new ParsedBatch(); + DataFormatter formatter = new DataFormatter(); + for (TempFile file : files) { + String fileName = Strings.sNull(file.getSubmittedFileName()); + String lowerName = fileName.toLowerCase(Locale.ROOT); + if (!lowerName.endsWith(".xls") && !lowerName.endsWith(".xlsx")) { + throw new IllegalArgumentException(fileName + " 不是 Excel 文件"); + } + if (file.getSize() > 20L * 1024 * 1024) { + throw new IllegalArgumentException(fileName + " 超过 20MB 限制"); + } + byte[] sourceBytes; + try (InputStream input = file.getInputStream()) { + sourceBytes = input.readAllBytes(); + } + try (Workbook workbook = WorkbookFactory.create(new ByteArrayInputStream(sourceBytes))) { + for (int sheetIndex = 0; sheetIndex < workbook.getNumberOfSheets(); sheetIndex++) { + Sheet sheet = workbook.getSheetAt(sheetIndex); + if (sheet.getPhysicalNumberOfRows() == 0) { + continue; + } + Row headerRow = sheet.getRow(sheet.getFirstRowNum()); + Map headerIndex = readHeader(headerRow, formatter, fileName); + batch.headers.addAll(headerIndex.keySet()); + for (int rowIndex = headerRow.getRowNum() + 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) { + Row row = sheet.getRow(rowIndex); + if (row == null || isBlankRow(row, headerIndex, formatter)) { + continue; + } + batch.totalRows++; + if (batch.totalRows > 100000) { + throw new IllegalArgumentException("单批数据不能超过 100000 行"); + } + LinkedHashMap values = new LinkedHashMap<>(); + for (Map.Entry header : headerIndex.entrySet()) { + Cell cell = row.getCell(header.getValue()); + values.put(header.getKey(), cell == null ? "" : Strings.trim(formatter.formatCellValue(cell))); + } + String unitName = normalizeUnitName(values.get(UNIT_HEADER)); + if (Strings.isBlank(unitName)) { + addImportError(batch, values, fileName + " 第" + (rowIndex + 1) + "行所在单位为空"); + continue; + } + List> unitRows = batch.unitRows.computeIfAbsent(unitName, k -> new ArrayList<>()); + unitRows.add(values); + batch.sourceNames.computeIfAbsent(unitName, k -> new LinkedHashSet<>()).add(fileName); + } + } + } catch (IllegalArgumentException e) { + throw e; + } catch (Exception e) { + throw new IllegalArgumentException(fileName + " 无法读取,请确认文件格式正确", e); + } + } + return batch; + } + + private void addImportError(ParsedBatch batch, Map values, String message) { + batch.errorRows.add(new ImportErrorRow(new LinkedHashMap<>(values), message)); + } + + private String cacheImportErrors(List errorRows) throws Exception { + if (errorRows.isEmpty()) { + return ""; + } + String cacheKey = "levelCheckImportError:" + R.UU32(); + redisService.setex(cacheKey, 60 * 60 * 5, + Base64.getEncoder().encodeToString(createImportErrorWorkbook(errorRows))); + return cacheKey; + } + + private byte[] createImportErrorWorkbook(List errorRows) throws Exception { + try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream output = new ByteArrayOutputStream()) { + Sheet sheet = workbook.createSheet("导入错误"); + Row header = sheet.createRow(0); + LinkedHashSet headers = new LinkedHashSet<>(); + for (ImportErrorRow errorRow : errorRows) { + headers.addAll(errorRow.values.keySet()); + } + List headerList = new ArrayList<>(headers); + for (int i = 0; i < headerList.size(); i++) { + header.createCell(i, CellType.STRING).setCellValue(headerList.get(i)); + } + header.createCell(headerList.size(), CellType.STRING).setCellValue("错误信息"); + for (int rowIndex = 0; rowIndex < errorRows.size(); rowIndex++) { + Row row = sheet.createRow(rowIndex + 1); + ImportErrorRow errorRow = errorRows.get(rowIndex); + for (int column = 0; column < headerList.size(); column++) { + row.createCell(column, CellType.STRING) + .setCellValue(Strings.sNull(errorRow.values.get(headerList.get(column)))); + } + row.createCell(headerList.size(), CellType.STRING).setCellValue(errorRow.message); + } + workbook.write(output); + return output.toByteArray(); + } + } + + public byte[] getImportErrorFile(String cacheKey) { + if (Strings.isBlank(cacheKey) || !cacheKey.startsWith("levelCheckImportError:")) { + throw new IllegalArgumentException("错误文件不存在或已过期"); + } + String value = redisService.get(cacheKey); + if (Strings.isBlank(value)) { + throw new IllegalArgumentException("错误文件不存在或已过期"); + } + return Base64.getDecoder().decode(value); + } + + private Map readHeader(Row row, DataFormatter formatter, String fileName) { + if (row == null) { + throw new IllegalArgumentException(fileName + " 缺少表头"); + } + Map actualHeaders = new LinkedHashMap<>(); + for (Cell cell : row) { + String header = Strings.trim(formatter.formatCellValue(cell)); + if (Strings.isBlank(header)) { + continue; + } + if (actualHeaders.containsKey(header)) { + throw new IllegalArgumentException(fileName + " 表头重复:" + header); + } + actualHeaders.put(header, cell.getColumnIndex()); + } + List missing = new ArrayList<>(); + Map result = new LinkedHashMap<>(); + for (String required : REQUIRED_HEADERS) { + if (actualHeaders.containsKey(required)) { + result.put(required, actualHeaders.get(required)); + } else { + missing.add(required); + } + } + if (!missing.isEmpty()) { + throw new IllegalArgumentException(fileName + " 缺少列:" + String.join("、", missing)); + } + return result; + } + + private boolean isBlankRow(Row row, Map headerIndex, DataFormatter formatter) { + for (Integer columnIndex : headerIndex.values()) { + Cell cell = row.getCell(columnIndex); + if (cell != null && Strings.isNotBlank(formatter.formatCellValue(cell))) { + return false; + } + } + return true; + } + + public String normalizeUnitName(String value) { + String result = Strings.sNull(value).replace('/', '/').replace('\u00A0', ' ').trim(); + while (result.startsWith(" ")) { + result = result.substring(1); + } + while (result.endsWith(" ")) { + result = result.substring(0, result.length() - 1); + } + int slash = result.indexOf('/'); + return Strings.trim(slash >= 0 ? result.substring(0, slash) : result); + } + + private Sys_unit matchUnit(String unitName) { + List units = dao.query(Sys_unit.class, Cnd.where("name", "=", unitName)); + if (units.isEmpty()) { + units = dao.query(Sys_unit.class, Cnd.where("aliasName", "=", unitName)); + } + List validUnits = new ArrayList<>(); + for (Sys_unit unit : units) { + if (unit.getUnitlevel() == 2) { + validUnits.add(unit); + } + } + if (validUnits.isEmpty()) { + throw new IllegalArgumentException("未找到可分发单位:" + unitName); + } + if (validUnits.size() > 1) { + throw new IllegalArgumentException("单位名称匹配到多条记录:" + unitName); + } + return validUnits.get(0); + } + + private boolean isSpecialUnionUnit(Sys_unit unit) { + return unit != null && Strings.isNotBlank(unit.getUnionid()) + && unit.getUnionid().equals(specialUnionId()); + } + + private String specialUnionId() { + Sys_config config = dao.fetch(Sys_config.class, "LevelCheckThirdAdminUnionId"); + if (config != null && Strings.isNotBlank(config.getConfigValue())) { + String unionId = Strings.trim(config.getConfigValue()); + Sys_union configuredUnion = dao.fetch(Sys_union.class, unionId); + if (configuredUnion != null + && "机关党委工会".equals(Strings.trim(configuredUnion.getUnionname()))) { + return unionId; + } + } + Sys_union union = dao.fetch(Sys_union.class, Cnd.where("unionname", "=", "机关党委工会")); + return union == null ? "" : union.getId(); + } + + private byte[] createUnitWorkbook(Collection headers, + List> rows) throws Exception { + try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream output = new ByteArrayOutputStream()) { + Sheet sheet = workbook.createSheet("sheet"); + Row header = sheet.createRow(0); + List headerList = new ArrayList<>(headers); + for (int i = 0; i < headerList.size(); i++) { + header.createCell(i, CellType.STRING).setCellValue(headerList.get(i)); + } + for (int rowIndex = 0; rowIndex < rows.size(); rowIndex++) { + Row row = sheet.createRow(rowIndex + 1); + Map values = rows.get(rowIndex); + for (int column = 0; column < headerList.size(); column++) { + row.createCell(column, CellType.STRING) + .setCellValue(Strings.sNull(values.get(headerList.get(column)))); + } + } + workbook.write(output); + return output.toByteArray(); + } + } + + private Pagination dataPage(List versionIds, String searchName, String searchKeyword, Integer state, + int pageNumber, int pageSize) throws Exception { + int currentPage = Math.max(pageNumber, 1); + int currentPageSize = Math.min(Math.max(pageSize, 1), 200); + if (versionIds.isEmpty()) { + return new Pagination(currentPage, currentPageSize, 0, new ArrayList<>()); + } + Map contextMap = dataContextMap(versionIds); + String keyword = Strings.trim(Strings.sNull(searchKeyword)); + String searchField = dataSearchField(searchName); + int stateValue = state == null ? -1 : state; + List matchedVersionIds = new ArrayList<>(versionIds); + if (stateValue >= 0) { + matchedVersionIds.removeIf(versionId -> { + NutMap context = contextMap.get(versionId); + return context == null || context.getInt("state") != stateValue; + }); + } + if (matchedVersionIds.isEmpty()) { + return new Pagination(currentPage, currentPageSize, 0, new ArrayList<>()); + } + Cnd cnd = Cnd.where("versionId", "in", matchedVersionIds); + int storedCount = dao.count(LevelCheckDataRow.class, cnd); + if (Strings.isNotBlank(keyword)) { + cnd.and(searchField, "like", "%" + keyword + "%"); + } + cnd.asc("unitName").asc("sequenceNo"); + int totalCount = dao.count(LevelCheckDataRow.class, cnd); + List versions = dao.query(LevelCheckFileVersion.class, + Cnd.where("id", "in", matchedVersionIds)); + int expectedCount = 0; + for (LevelCheckFileVersion version : versions) { + expectedCount += version.getRowCount() == null ? 0 : version.getRowCount(); + } + if (storedCount < expectedCount) { + Map versionMap = new HashMap<>(); + for (LevelCheckFileVersion version : versions) { + versionMap.put(version.getId(), version); + } + List allRows = new ArrayList<>(); + for (String versionId : matchedVersionIds) { + LevelCheckFileVersion version = versionMap.get(versionId); + if (version != null) { + for (LinkedHashMap row : loadVersionData(version).rows) { + if (matchesDataSearch(row, searchField, keyword)) { + allRows.add(toPageValues(row, contextMap.get(versionId))); + } + } + } + } + int fromIndex = Math.min((currentPage - 1) * currentPageSize, allRows.size()); + int toIndex = Math.min(fromIndex + currentPageSize, allRows.size()); + return new Pagination(currentPage, currentPageSize, allRows.size(), + new ArrayList<>(allRows.subList(fromIndex, toIndex))); + } + List dataRows = dao.query(LevelCheckDataRow.class, cnd, + dao.createPager(currentPage, currentPageSize)); + List rows = new ArrayList<>(); + for (LevelCheckDataRow dataRow : dataRows) { + rows.add(toPageValues(toDataValues(dataRow), contextMap.get(dataRow.getVersionId()))); + } + return new Pagination(currentPage, currentPageSize, totalCount, rows); + } + + private String dataSearchField(String searchName) { + return switch (Strings.sNull(searchName)) { + case "salaryUnitCode" -> "salaryUnitCode"; + case "employeeName" -> "employeeName"; + case "unionFee" -> "unionFee"; + case "yearMonth" -> "yearMonth"; + case "unitName" -> "unitName"; + default -> "employeeNo"; + }; + } + + private boolean matchesDataSearch(Map row, String searchField, String keyword) { + if (Strings.isBlank(keyword)) { + return true; + } + String header = switch (searchField) { + case "salaryUnitCode" -> "薪酬单位代码"; + case "employeeName" -> "姓名"; + case "unionFee" -> "扣工会会费"; + case "yearMonth" -> "年月"; + case "unitName" -> "所在单位"; + default -> "职工号"; + }; + return Strings.sNull(row.get(header)).contains(keyword); + } + + private Map dataContextMap(List versionIds) { + String specialUnionId = specialUnionId(); + Sql sql = Sqls.create(""" + SELECT CASE WHEN ut.pendingVersionId IN (@versionIds) + THEN ut.pendingVersionId ELSE ut.currentVersionId END versionId, + ut.state, ut.checkedAt, + CASE WHEN currentUnit.unionid = @specialUnionId AND @specialUnionId <> '' THEN ( + SELECT GROUP_CONCAT(DISTINCT CONCAT(u.username, '(', u.loginname, ')') SEPARATOR '、') + FROM sys_user_role ur + LEFT JOIN sys_user u ON u.id = ur.userId + WHERE ur.roleId = @thirdRoleId AND ur.unitid = ut.unitId + ) ELSE ( + SELECT GROUP_CONCAT(DISTINCT CONCAT(u.username, '(', u.loginname, ')') SEPARATOR '、') + FROM sys_user_role ur + LEFT JOIN sys_user u ON u.id = ur.userId + WHERE ur.roleId = @unionRoleId AND ur.unionid = currentUnit.unionid + ) END managerNames + FROM level_check_unit_task ut + LEFT JOIN sys_unit currentUnit ON currentUnit.id = ut.unitId + WHERE ut.currentVersionId IN (@versionIds) OR ut.pendingVersionId IN (@versionIds) + """); + sql.setParam("versionIds", versionIds); + sql.setParam("specialUnionId", specialUnionId); + sql.setParam("unionRoleId", Roles.UNION_MANGER); + sql.setParam("thirdRoleId", Roles.THIRD_UNIT_MANGER); + sql.setCallback(Sqls.callback.maps()); + dao.execute(sql); + Map result = new HashMap<>(); + for (NutMap context : sql.getList(NutMap.class)) { + String versionId = context.getString("versionId"); + if (Strings.isBlank(versionId)) { + versionId = context.getString("versionid"); + } + String managerNames = context.getString("managerNames"); + if (Strings.isBlank(managerNames)) { + managerNames = context.getString("managernames"); + } + Object checkedAt = context.containsKey("checkedAt") + ? context.get("checkedAt") : context.get("checkedat"); + result.put(versionId, new NutMap() + .setv("state", context.get("state")) + .setv("checkedAt", checkedAt) + .setv("managerNames", Strings.sNull(managerNames))); + } + return result; + } + + private NutMap toPageValues(LinkedHashMap values, NutMap context) { + NutMap result = new NutMap(); + result.putAll(values); + result.setv("state", context == null ? null : context.get("state")); + result.setv("checkedAt", context == null ? null : context.get("checkedAt")); + result.setv("managerNames", context == null ? "" : context.getString("managerNames")); + return result; + } + + private LinkedHashMap toDataValues(LevelCheckDataRow dataRow) { + LinkedHashMap values = new LinkedHashMap<>(); + values.put("薪酬单位代码", dataRow.getSalaryUnitCode()); + values.put("职工号", dataRow.getEmployeeNo()); + values.put("姓名", dataRow.getEmployeeName()); + values.put("扣工会会费", dataRow.getUnionFee()); + values.put("年月", dataRow.getYearMonth()); + values.put("所在单位", dataRow.getUnitName()); + return values; + } + + private LevelCheckFileVersion saveVersion(LevelCheckUnitTask unitTask, Collection headers, + List> rows, + String originalFileName, String type, int versionNo) throws Exception { + LevelCheckFileVersion version = new LevelCheckFileVersion() + .setId(R.UU32()).setUnitTaskId(unitTask.getId()).setVersionNo(versionNo) + .setVersionType(type) + .setOriginalFileName(cut(originalFileName, 200)).setRowCount(rows.size()) + .setSha256(sha256(createUnitWorkbook(headers, rows))) + .setUploadedBy(currentUserId()).setUploadedAt(System.currentTimeMillis()); + dao.insert(version); + List dataRows = new ArrayList<>(); + for (int i = 0; i < rows.size(); i++) { + Map row = rows.get(i); + dataRows.add(new LevelCheckDataRow() + .setId(R.UU32()).setVersionId(version.getId()).setSequenceNo(i + 1) + .setSalaryUnitCode(row.get("薪酬单位代码")) + .setEmployeeNo(row.get("职工号")) + .setEmployeeName(row.get("姓名")) + .setUnionFee(row.get("扣工会会费")) + .setYearMonth(row.get("年月")) + .setUnitName(row.get("所在单位"))); + } + dao.fastInsert(dataRows); + return version; + } + + private List readVersionHeaders(String versionId) throws Exception { + LevelCheckFileVersion version = dao.fetch(LevelCheckFileVersion.class, versionId); + if (version == null) { + throw new IllegalArgumentException("原单位数据不存在"); + } + return new ArrayList<>(loadVersionData(version).headers); + } + + private LevelCheckData loadVersionData(LevelCheckFileVersion version) throws Exception { + List dataRows = dao.query(LevelCheckDataRow.class, + Cnd.where("versionId", "=", version.getId()).asc("sequenceNo")); + if (!dataRows.isEmpty()) { + LevelCheckData data = new LevelCheckData(); + data.headers.addAll(REQUIRED_HEADERS); + for (LevelCheckDataRow dataRow : dataRows) { + data.rows.add(toDataValues(dataRow)); + } + return data; + } + if (Strings.isBlank(version.getSysFileId())) { + throw new IllegalArgumentException("数据版本内容不存在"); + } + Sys_file file = dao.fetch(Sys_file.class, version.getSysFileId()); + if (file == null) { + throw new IllegalArgumentException("历史数据文件不存在"); + } + ByteArrayOutputStream output = new ByteArrayOutputStream(); + ftpService.download(file.getFilepath(), output); + return readWorkbookData(output.toByteArray()); + } + + private LevelCheckData readWorkbookData(byte[] bytes) throws Exception { + LevelCheckData data = new LevelCheckData(); + DataFormatter formatter = new DataFormatter(); + try (Workbook workbook = WorkbookFactory.create(new ByteArrayInputStream(bytes))) { + Sheet sheet = workbook.getSheetAt(0); + Row headerRow = sheet.getRow(sheet.getFirstRowNum()); + Map headerIndex = readHeader(headerRow, formatter, "历史数据文件"); + data.headers.addAll(headerIndex.keySet()); + for (int rowIndex = headerRow.getRowNum() + 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) { + Row row = sheet.getRow(rowIndex); + if (row == null || isBlankRow(row, headerIndex, formatter)) { + continue; + } + LinkedHashMap values = new LinkedHashMap<>(); + for (Map.Entry header : headerIndex.entrySet()) { + Cell cell = row.getCell(header.getValue()); + values.put(header.getKey(), cell == null ? "" : Strings.trim(formatter.formatCellValue(cell))); + } + data.rows.add(values); + } + } + return data; + } + + private LevelCheckUnitTask requireManagedUnitTask(String unitTaskId) { + LevelCheckUnitTask task = dao.fetch(LevelCheckUnitTask.class, unitTaskId); + if (task == null) { + throw new IllegalArgumentException("单位任务不存在"); + } + if (ShiroUtil.hasAnyRoles("sysadmin,A06")) { + return task; + } + Cnd cnd = Cnd.where("userId", "=", currentUserId()); + Sys_unit currentUnit = dao.fetch(Sys_unit.class, task.getUnitId()); + boolean specialUnit = currentUnit == null + ? "THIRD".equals(task.getManagerType()) : isSpecialUnionUnit(currentUnit); + if (!specialUnit) { + String unionId = currentUnit == null ? task.getUnionId() : currentUnit.getUnionid(); + if (Strings.isBlank(unionId)) { + throw new SecurityException("该单位尚未分配核对人员"); + } + cnd.and("roleId", "=", Roles.UNION_MANGER).and("unionid", "=", unionId); + } else { + cnd.and("roleId", "=", Roles.THIRD_UNIT_MANGER).and("unitid", "=", task.getUnitId()); + } + if (dao.count(Sys_user_role.class, cnd) == 0) { + throw new SecurityException("无权访问该单位任务"); + } + return task; + } + + public void requireSchoolAdmin() { + if (!ShiroUtil.hasAnyRoles("sysadmin,A06")) { + throw new SecurityException("仅校工会管理员可执行此操作"); + } + } + + private void addLog(String unitTaskId, String action, Integer fromState, Integer toState, + String opinion, String fileVersionId) { + dao.insert(new LevelCheckActionLog().setId(R.UU32()).setUnitTaskId(unitTaskId) + .setAction(action).setFromState(fromState).setToState(toState) + .setOperatorId(currentUserId()).setOperateAt(System.currentTimeMillis()) + .setOpinion(Strings.trim(Strings.sNull(opinion))).setFileVersionId(fileVersionId)); + } + + private void refreshProgress(String taskId) { + int finished = dao.count(LevelCheckUnitTask.class, Cnd.where("taskId", "=", taskId) + .and("state", "in", new Integer[]{LevelCheckState.CONFIRMED, LevelCheckState.APPROVED})); + dao.update(LevelCheckTask.class, Chain.make("finishedUnits", finished), Cnd.where("id", "=", taskId)); + } + + private String currentUserId() { + return String.valueOf(ShiroUtil.getPrincipalProperty("id")); + } + + private String cut(String value, int length) { + String result = Strings.sNull(value); + return result.length() > length ? result.substring(0, length) : result; + } + + private String safeFileName(String value) { + return Strings.sNull(value).replaceAll("[\\\\/:*?\"<>|]", "_"); + } + + private long[] taskDateRange(String taskDate) { + try { + LocalDate date = LocalDate.parse(taskDate); + return new long[]{date.atStartOfDay(TASK_ZONE_ID).toInstant().toEpochMilli(), + date.plusDays(1).atStartOfDay(TASK_ZONE_ID).toInstant().toEpochMilli()}; + } catch (Exception e) { + throw new IllegalArgumentException("任务日期格式不正确"); + } + } + + private long[] optionalTaskDateRange(String taskDate) { + try { + return taskDateRange(taskDate); + } catch (IllegalArgumentException e) { + return new long[]{-1L, -1L}; + } + } + + private String sha256(byte[] bytes) throws Exception { + byte[] digest = MessageDigest.getInstance("SHA-256").digest(bytes); + StringBuilder result = new StringBuilder(); + for (byte value : digest) { + result.append(String.format("%02x", value)); + } + return result.toString(); + } + + private static class ParsedBatch { + private int totalRows; + private final LinkedHashSet headers = new LinkedHashSet<>(); + private final Map>> unitRows = new LinkedHashMap<>(); + private final Map> sourceNames = new LinkedHashMap<>(); + private final List errorRows = new ArrayList<>(); + } + + private static class ImportErrorRow { + private final LinkedHashMap values; + private final String message; + + private ImportErrorRow(LinkedHashMap values, String message) { + this.values = values; + this.message = message; + } + } + + private static class LevelCheckData { + public List headers = new ArrayList<>(); + public List> rows = new ArrayList<>(); + } +} diff --git a/src/main/java/io/v/nutz/proposal/listener/ProposalUnderTakeListener.java b/src/main/java/io/v/nutz/proposal/listener/ProposalUnderTakeListener.java index da6bd69..9cf0929 100644 --- a/src/main/java/io/v/nutz/proposal/listener/ProposalUnderTakeListener.java +++ b/src/main/java/io/v/nutz/proposal/listener/ProposalUnderTakeListener.java @@ -33,8 +33,17 @@ public class ProposalUnderTakeListener implements SysCommonDataChange { case UPDATE -> { Sys_unit sys_unit = dao.fetch(Sys_unit.class, bizId); ProposalUndertake undertake = dao.fetch(ProposalUndertake.class, Cnd.where("unitCode", "=", sys_unit.getUnitcode())); - undertake.setUnitName(sys_unit.getName()); - dao.updateIgnoreNull(undertake); + if (undertake == null) { + // 兼容历史单位缺少提案承办单位记录的情况 + undertake = new ProposalUndertake(); + undertake.setUnitCode(sys_unit.getUnitcode()); + undertake.setUnitName(sys_unit.getName()); + undertake.setIsEnable(Boolean.TRUE); + dao.insert(undertake); + } else { + undertake.setUnitName(sys_unit.getName()); + dao.updateIgnoreNull(undertake); + } } case DELETE -> { dao.clear(ProposalUndertake.class, Cnd.where("id", "=", bizId)); diff --git a/src/main/java/io/v/nutz/web/controllers/platform/sys/SysUnionMangeCon.java b/src/main/java/io/v/nutz/web/controllers/platform/sys/SysUnionMangeCon.java index a62860e..afe8995 100644 --- a/src/main/java/io/v/nutz/web/controllers/platform/sys/SysUnionMangeCon.java +++ b/src/main/java/io/v/nutz/web/controllers/platform/sys/SysUnionMangeCon.java @@ -347,12 +347,120 @@ public class SysUnionMangeCon { @At public Object getUnitsByUnion(String unionid) { try { - return Result.success().addData(sysUnitService.query(Cnd.where("unionid", "=", unionid).and("unitlevel", "=", 2))); + Sql sql = Sqls.create(""" + SELECT un.*, + GROUP_CONCAT(DISTINCT CONCAT(u.username, '(', u.loginname, ')')) thirdManagerNames + FROM sys_unit un + LEFT JOIN sys_user_role sur ON sur.unitid = un.id AND sur.roleId = @roleId + LEFT JOIN sys_user u ON u.id = sur.userId + WHERE un.unionid = @unionId AND un.unitlevel = 2 + GROUP BY un.id + ORDER BY un.location + """); + sql.setParam("roleId", Roles.THIRD_UNIT_MANGER); + sql.setParam("unionId", unionid); + return Result.success().addData(sysUnitService.list(sql)); } catch (Exception e) { return Result.error(); } } + /** + * 查询机关党委工会成员单位的可选人员和已设置管理员 + */ + @At + public Object findThirdUnitManagerUsers(String unionId, String unitId) { + try { + checkThirdUnitManagerScope(unionId, unitId); + Sql userSql = Sqls.create(""" + SELECT id, CONCAT(username, '(', loginname, ')') name + FROM `user` + WHERE unionid = @unionId + AND unitid = @unitId + ORDER BY username + """); + userSql.setParam("unionId", unionId); + userSql.setParam("unitId", unitId); + Sql selectedSql = Sqls.create(""" + SELECT userId + FROM sys_user_role + WHERE roleId = @roleId AND unitid = @unitId + """); + selectedSql.setParam("roleId", Roles.THIRD_UNIT_MANGER); + selectedSql.setParam("unitId", unitId); + List selectedUsers = sysUnionService.list(selectedSql); + List selectedUserIds = selectedUsers.stream() + .map(v -> v.getString("userId")).collect(Collectors.toList()); + return Result.success().addData(Record.create() + .set("allUsers", sysUnionService.list(userSql)) + .set("selectedUserIds", selectedUserIds)); + } catch (Exception e) { + return Result.error(e.getMessage()); + } + } + + /** + * 设置机关党委工会成员单位管理员 + */ + @At + @Aop(TransAop.READ_COMMITTED) + public Object doSetThirdUnitManagerUsers(String unionId, String unitId, String userIds) { + try { + checkThirdUnitManagerScope(unionId, unitId); + List ids = Strings.isBlank(userIds) + ? new java.util.ArrayList<>() : Json.fromJsonAsList(String.class, userIds); + for (String userId : ids) { + Sql userCheckSql = Sqls.create(""" + SELECT id + FROM `user` + WHERE id = @userId + AND unionid = @unionId + AND unitid = @unitId + LIMIT 1 + """); + userCheckSql.setParam("userId", userId); + userCheckSql.setParam("unionId", unionId); + userCheckSql.setParam("unitId", unitId); + if (sysUnionService.list(userCheckSql).isEmpty()) { + return Result.error("所选人员不属于当前成员单位"); + } + } + List userRoles = new java.util.ArrayList<>(); + for (String userId : ids) { + Sys_user_role userRole = new Sys_user_role(); + userRole.setRoleId(Roles.THIRD_UNIT_MANGER); + userRole.setUserId(userId); + userRole.setUnitid(unitId); + userRoles.add(userRole); + } + sysUserRoleService.clear(Cnd.where("roleId", "=", Roles.THIRD_UNIT_MANGER) + .and("unitid", "=", unitId)); + if (!userRoles.isEmpty()) { + sysUserRoleService.dao().insert(userRoles); + } + sysUserService.clearCache(); + sysRoleService.clearCache(); + return Result.success(); + } catch (Exception e) { + log.error(e); + return Result.error(e.getMessage()); + } + } + + private void checkThirdUnitManagerScope(String unionId, String unitId) { + if (!checkRole()) { + throw new SecurityException("仅校工会管理员可设置三级管理员"); + } + Sys_union union = sysUnionService.fetch(unionId); + if (union == null || !"机关党委工会".equals(Strings.trim(union.getUnionname()))) { + throw new IllegalArgumentException("仅机关党委工会的成员单位可设置三级管理员"); + } + Sys_unit unit = sysUnitService.fetch(unitId); + if (unit == null || !unionId.equals(unit.getUnionid()) || unit.getUnitlevel() != 2) { + throw new IllegalArgumentException("成员单位不属于机关党委工会"); + } + } + @At // 会员档案 public Object hyPageData(String unitid, String unionid, String searchName, String searchKeyword, int pageNumber, int pageSize, String pageOrderName, String pageOrderBy) { diff --git a/src/main/resources/db/init_3_level_check.sql b/src/main/resources/db/init_3_level_check.sql new file mode 100644 index 0000000..804139f --- /dev/null +++ b/src/main/resources/db/init_3_level_check.sql @@ -0,0 +1,72 @@ +/* + 分级核对与审核初始化脚本 + 说明:脚本可重复执行,不自动给任何用户分配三级单位管理员角色。 +*/ + +SET @third_unit_manager_role_id = 'e8d957d1ab9f4e56a60b5a53929d18bb'; +SET @school_union_admin_role_id = '9b01918f873645048f8a85a4fc06d135'; +SET @union_manager_role_id = '5d342e614e9a48288d50154bcdcc75d3'; +SET @sysadmin_role_id = (SELECT id FROM sys_role WHERE code = 'sysadmin' LIMIT 1); +SET @level_check_root_menu_id = 'a63f7fd56f594b29b2b5616d0d7f0101'; +SET @level_check_school_menu_id = 'a63f7fd56f594b29b2b5616d0d7f0102'; +SET @level_check_unit_menu_id = 'a63f7fd56f594b29b2b5616d0d7f0103'; + +INSERT INTO sys_role + (id, name, code, aliasName, disabled, note, createdBy, createdAt, updatedBy, updatedAt, delFlag) +SELECT @third_unit_manager_role_id, '三级单位管理员', 'ThirdUnitCheckAdmin', '三级单位管理员', + 0, '机关党委工会成员单位的数据核对管理员', '', 0, '', 0, 0 +WHERE NOT EXISTS ( + SELECT 1 FROM sys_role + WHERE id = @third_unit_manager_role_id OR code = 'ThirdUnitCheckAdmin' +); + +INSERT INTO sys_menu + (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, + permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag) +SELECT @level_check_root_menu_id, '', '0098', '分级核对', 'LevelCheck', 'menu', '', '', + 'ti-check-box', 1, 0, 'levelCheck', NULL, 980, 1, '', 0, '', 0, 0 +WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE permission = 'levelCheck'); + +INSERT INTO sys_menu + (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, + permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag) +SELECT @level_check_school_menu_id, @level_check_root_menu_id, '00980001', '分级核对管理', + 'LevelCheckSchool', 'menu', '/platform/levelCheck/school', 'data-pjax', '', 1, 0, + 'levelCheck.school', NULL, 981, 0, '', 0, '', 0, 0 +WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE permission = 'levelCheck.school'); + +INSERT INTO sys_menu + (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, + permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag) +SELECT @level_check_unit_menu_id, @level_check_root_menu_id, '00980002', '单位数据核对', + 'LevelCheckUnit', 'menu', '/platform/levelCheck/unit', 'data-pjax', '', 1, 0, + 'levelCheck.unit', NULL, 982, 0, '', 0, '', 0, 0 +WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE permission = 'levelCheck.unit'); + +INSERT INTO sys_role_menu (roleId, menuId) +SELECT role_menu.roleId, role_menu.menuId +FROM ( + SELECT @sysadmin_role_id roleId, @level_check_root_menu_id menuId + UNION ALL SELECT @sysadmin_role_id, @level_check_school_menu_id + UNION ALL SELECT @sysadmin_role_id, @level_check_unit_menu_id + UNION ALL SELECT @school_union_admin_role_id, @level_check_root_menu_id + UNION ALL SELECT @school_union_admin_role_id, @level_check_school_menu_id + UNION ALL SELECT @union_manager_role_id, @level_check_root_menu_id + UNION ALL SELECT @union_manager_role_id, @level_check_unit_menu_id + UNION ALL SELECT @third_unit_manager_role_id, @level_check_root_menu_id + UNION ALL SELECT @third_unit_manager_role_id, @level_check_unit_menu_id +) role_menu +WHERE NOT EXISTS ( + SELECT 1 FROM sys_role_menu current_role_menu + WHERE current_role_menu.roleId = role_menu.roleId + AND current_role_menu.menuId = role_menu.menuId +); + +INSERT INTO sys_config + (configKey, configValue, note, createdBy, createdAt, updatedBy, updatedAt, delFlag) +SELECT 'LevelCheckThirdAdminUnionId', IFNULL(( + SELECT id FROM sys_union WHERE unionname = '机关党委工会' ORDER BY id LIMIT 1 + ), ''), '允许设置三级管理员的特殊工会ID', '', 0, '', 0, 0 +WHERE NOT EXISTS ( + SELECT 1 FROM sys_config WHERE configKey = 'LevelCheckThirdAdminUnionId' +); diff --git a/src/main/resources/views/platform/levelCheck/school.html b/src/main/resources/views/platform/levelCheck/school.html new file mode 100644 index 0000000..5f438a7 --- /dev/null +++ b/src/main/resources/views/platform/levelCheck/school.html @@ -0,0 +1,549 @@ + +
+ + + + + + + + + + + + + + + + + + + 选择文件 +
可同时选择多个文件,数据按“所在单位”首段归组。
+
+ 下载导入模板 +
+
+ + 取 消 + 上传并分发 + +
+ + + + + + + + + 取 消 + 确 定 + + + +
+ + diff --git a/src/main/resources/views/platform/levelCheck/unit.html b/src/main/resources/views/platform/levelCheck/unit.html new file mode 100644 index 0000000..eeb928d --- /dev/null +++ b/src/main/resources/views/platform/levelCheck/unit.html @@ -0,0 +1,318 @@ + +
+ + + + + + + + + + + + 选择修改版 + + + 取 消 + 提 交 + + +
+ + diff --git a/src/main/resources/views/platform/sys/union/unionMange.html b/src/main/resources/views/platform/sys/union/unionMange.html index 64fc919..28f8970 100644 --- a/src/main/resources/views/platform/sys/union/unionMange.html +++ b/src/main/resources/views/platform/sys/union/unionMange.html @@ -306,6 +306,22 @@ layout("/layouts/platform.html"){ + + + + + + @@ -433,6 +449,26 @@ layout("/layouts/platform.html"){ + + + + + + + + + + + + + 取 消 + 确 定 + + + @@ -630,6 +666,11 @@ layout("/layouts/platform.html"){ fzrValue: [], data: [], zcdwTableData: [], + thirdManagerDialogVisible: false, + thirdManagerQuick: false, + thirdManagerUnit: {}, + thirdManagerUsers: [], + thirdManagerUserIds: [], lxrTableData: [], defaultProps: { children: 'childrens', @@ -762,7 +803,42 @@ layout("/layouts/platform.html"){ return val }, }, + computed: { + isSpecialUnion() { + return this.sysadmin && this.node && this.node.data + && this.node.data.unionname === '机关党委工会' + } + }, methods: { + async openThirdManager(row, quick) { + const resp = await $.post(loc() + '/findThirdUnitManagerUsers', { + unionId: this.node.data.id, + unitId: row.id + }) + if (resp.code === 0) { + this.thirdManagerQuick = quick + this.thirdManagerUnit = row + this.thirdManagerUsers = resp.data.allusers + this.thirdManagerUserIds = resp.data.selecteduserids + this.thirdManagerDialogVisible = true + } else { + this.$notify.warning(resp.msg) + } + }, + async doSetThirdManager() { + const resp = await $.post(loc() + '/doSetThirdUnitManagerUsers', { + unionId: this.node.data.id, + unitId: this.thirdManagerUnit.id, + userIds: JSON.stringify(this.thirdManagerUserIds) + }) + if (resp.code === 0) { + this.thirdManagerDialogVisible = false + this.$notify.success(resp.msg) + this.getZcdwTabData() + } else { + this.$notify.warning(resp.msg) + } + }, doExport() { window.location.href = loc() + "/doExport" }, @@ -1405,4 +1481,4 @@ layout("/layouts/platform.html"){ \ No newline at end of file +#-->