diff --git a/src/main/java/com/budwk/app/sys/controller/SysUnionCadreImportController.java b/src/main/java/com/budwk/app/sys/controller/SysUnionCadreImportController.java new file mode 100644 index 0000000..59df764 --- /dev/null +++ b/src/main/java/com/budwk/app/sys/controller/SysUnionCadreImportController.java @@ -0,0 +1,275 @@ +package com.budwk.app.sys.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.annotation.SaCheckRole; +import cn.hutool.core.util.StrUtil; +import com.budwk.app.base.constant.RoleConstant; +import com.budwk.app.base.result.Result; +import com.budwk.app.base.utils.CommonDownloadUtil; +import com.budwk.app.sys.models.*; +import com.budwk.app.sys.services.SysDictService; +import com.budwk.app.sys.views.View_user; +import lombok.extern.slf4j.Slf4j; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.nutz.dao.Cnd; +import org.nutz.dao.Dao; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.util.NutMap; +import org.nutz.mvc.annotation.AdaptBy; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; +import org.nutz.mvc.annotation.Param; +import org.nutz.mvc.upload.TempFile; +import org.nutz.mvc.upload.UploadAdaptor; + +import javax.servlet.http.HttpServletResponse; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@IocBean +@At("/platform/sys/unionCadreImport") +@Ok("json:full") +public class SysUnionCadreImportController { + private static final String[] HEADERS = {"工会编码", "工号", "角色编码", "届次编码(选填)", "所属单位编码(书记必填)"}; + private static final int MAX_ROWS = 1000; + + @Inject + private Dao dao; + @Inject + private SysDictService sysDictService; + @Inject + private SysUnionController sysUnionController; + + @SuppressWarnings("unchecked") + private List roleOptions() { + return (List) sysUnionController.branchUnionRoleOptions().getData(); + } + + @At + @Ok("void") + @SaCheckRole("SYSADMIN") + @SaCheckPermission("sys.manager.union.branchOfficer") + public void downloadTemplate(HttpServletResponse response) { + Workbook workbook = new XSSFWorkbook(); + Sheet data = createSheet(workbook, "干部导入", HEADERS); + CellStyle textStyle = workbook.createCellStyle(); + textStyle.setDataFormat(workbook.createDataFormat().getFormat("@")); + for (int i = 0; i < HEADERS.length; i++) { + data.setDefaultColumnStyle(i, textStyle); + } + Sheet notes = createSheet(workbook, "填写说明", "字段", "填写要求"); + notes.setColumnWidth(1, 100 * 256); + append(notes, "导入范围", "仅导入第一个工作表;每行一个人员角色,最多1000行;请保留表头,工号和编码按文本填写。"); + append(notes, "工会编码", "必填,参考工会对照表。人员必须属于该分工会。"); + append(notes, "工号", "必填,填写系统中已存在的人员工号,不填写姓名。"); + append(notes, "角色编码", "必填,参考角色对照表,不填写角色名称。"); + append(notes, "届次编码(选填)", "可留空;填写时使用届次对照表中的编码,与单个添加一致。"); + append(notes, "所属单位编码(书记必填)", "仅二级党委书记填写,多个单位用英文逗号分隔,必须为当前分工会的二级单位;其他角色留空。"); + append(notes, "重复记录", "普通干部按人员和角色跳过;二级党委书记按人员、角色和工会跳过。禁用的已有关系也跳过,不覆盖。"); + append(notes, "保存方式", "系统管理员直接授权,写入干部及角色记录并清理权限缓存,不发起审批。逐行保存,失败行不影响其他行。"); + append(notes, "已有干部记录", "已有在任或待审核干部记录但未分配角色时跳过,避免重复创建或绕过已有审批。"); + Sheet unions = createSheet(workbook, "工会对照", "工会编码", "工会名称"); + List unionList = dao.query(Sys_union.class, Cnd.NEW().asc("unionCode")); + for (Sys_union union : unionList) { + append(unions, union.getUnionCode(), union.getName()); + } + Sheet roles = createSheet(workbook, "角色对照", "角色编码", "角色名称"); + for (NutMap role : roleOptions()) { + append(roles, role.getString("code"), role.getString("name")); + } + Sheet terms = createSheet(workbook, "届次对照", "届次编码", "届次名称"); + for (Sys_dict term : sysDictService.getSubListByCode("TEACHER_CONGRESS_J")) { + append(terms, term.getCode(), term.getName()); + } + Sheet units = createSheet(workbook, "所属单位对照", "单位编码", "单位名称", "工会编码", "工会名称"); + for (Sys_unit unit : dao.query(Sys_unit.class, Cnd.where("unitLevel", "=", 2).asc("unitcode"))) { + for (Sys_union union : unionList) { + if (union.getId().equals(unit.getUnionId())) { + append(units, unit.getUnitcode(), unit.getName(), union.getUnionCode(), union.getName()); + break; + } + } + } + CommonDownloadUtil.download("批量添加分工会干部模板.xlsx", workbook, response); + } + + @At + @SaCheckRole("SYSADMIN") + @SaCheckPermission("sys.manager.union.branchOfficer") + @AdaptBy(type = UploadAdaptor.class, args = {"ioc:fileUpload"}) + public Result doImport(@Param("file") TempFile file) { + if (file == null) { + return Result.error("请选择Excel文件"); + } + List rows = new ArrayList<>(); + // 先完整读取文件,避免文件格式错误时已写入部分数据却无法返回逐行结果。 + try (InputStream input = file.getInputStream(); Workbook workbook = WorkbookFactory.create(input)) { + if (workbook.getNumberOfSheets() == 0) { + return Result.error("文件没有工作表,请使用下载的模板"); + } + Sheet sheet = workbook.getSheetAt(0); + DataFormatter formatter = new DataFormatter(); + for (int i = 0; i < HEADERS.length; i++) { + if (!HEADERS[i].equals(cellText(sheet.getRow(0), i, formatter))) { + return Result.error("模板表头不正确,请使用下载的模板并保留第一行表头"); + } + } + for (Row row : sheet) { + if (row.getRowNum() == 0) { + continue; + } + String[] values = new String[HEADERS.length]; + for (int j = 0; j < values.length; j++) { + values[j] = cellText(row, j, formatter); + } + if (Arrays.stream(values).allMatch(StrUtil::isBlank)) { + continue; + } + if (rows.size() >= MAX_ROWS) { + return Result.error("每次最多导入1000行有效数据,请拆分文件后导入"); + } + rows.add(NutMap.NEW().addv("rowNumber", row.getRowNum() + 1).addv("unionCode", values[0]) + .addv("loginname", values[1]).addv("roleCode", values[2]) + .addv("j", values[3]).addv("unitCodes", values[4])); + } + } catch (Exception e) { + log.warn("读取分工会干部导入文件失败", e); + return Result.error("无法读取文件,请上传未加密的xls或xlsx模板文件,编码列不要使用公式"); + } + if (rows.isEmpty()) { + return Result.error("文件中没有需要导入的数据"); + } + List roleCodes = roleOptions().stream().map(item -> item.getString("code")).toList(); + List terms = sysDictService.getSubListByCode("TEACHER_CONGRESS_J").stream() + .map(Sys_dict::getCode).toList(); + int success = 0; + int skipped = 0; + for (NutMap row : rows) { + try { + importRow(row, roleCodes, terms); + } catch (Exception e) { + log.error("导入分工会干部失败,行号:{}", row.getInt("rowNumber"), e); + outcome(row, "失败", "保存失败,请检查该行数据或联系管理员"); + } + if ("成功".equals(row.getString("status"))) { + success++; + } else if ("跳过".equals(row.getString("status"))) { + skipped++; + } + } + return Result.success(NutMap.NEW().addv("success", success).addv("skipped", skipped) + .addv("failed", rows.size() - success - skipped).addv("rows", rows)); + } + + private void importRow(NutMap row, List roleCodes, List terms) { + String unionCode = row.getString("unionCode"); + String loginname = row.getString("loginname"); + String roleCode = row.getString("roleCode"); + String j = row.getString("j"); + if (StrUtil.isBlank(unionCode) || StrUtil.isBlank(loginname) || StrUtil.isBlank(roleCode)) { + outcome(row, "失败", "工会编码、工号、角色编码不能为空"); + return; + } + List unions = dao.query(Sys_union.class, Cnd.where("unionCode", "=", unionCode)); + List users = dao.query(View_user.class, Cnd.where("loginname", "=", loginname)); + List roles = dao.query(Sys_role.class, Cnd.where("code", "=", roleCode)); + if (unions.size() != 1 || users.size() != 1 || roles.size() != 1 || !roleCodes.contains(roleCode)) { + outcome(row, "失败", "工会、工号或分工会角色编码不存在或不唯一,请按模板对照表填写"); + return; + } + Sys_union union = unions.get(0); + View_user user = users.get(0); + Sys_role role = roles.get(0); + boolean secretary = RoleConstant.UNIT_PARTY_SECRETARY.name().equals(roleCode); + Cnd existing = Cnd.where("userId", "=", user.getId()).and("roleId", "=", role.getId()); + if (secretary) { + existing.and("unionId", "=", union.getId()); + } + if (dao.count(Sys_user_role.class, existing) > 0) { + outcome(row, "跳过", "已分配该角色,保留原有记录(含禁用关系)"); + return; + } + if (!union.getId().equals(user.getUnionId())) { + outcome(row, "失败", "所选人员不属于该分工会"); + return; + } + if (StrUtil.isNotBlank(j) && !terms.contains(j)) { + outcome(row, "失败", "届次编码不存在,请参照届次对照表或留空"); + return; + } + if (dao.count(Sys_union_cadre.class, Cnd.where("userId", "=", user.getId()) + .and("roleCode", "=", roleCode).and("unionId", "=", union.getId()).and("isServing", "=", true)) > 0) { + outcome(row, "跳过", "已有在任或待审核干部记录,请通过原记录处理"); + return; + } + List unitIds = new ArrayList<>(); + String unitCodes = row.getString("unitCodes"); + if (secretary) { + List codes = Arrays.stream(unitCodes.split("[,,]")) + .map(String::trim).filter(StrUtil::isNotBlank).distinct().collect(Collectors.toList()); + if (codes.isEmpty()) { + outcome(row, "失败", "二级党委书记必须填写所属单位编码"); + return; + } + for (String code : codes) { + List units = dao.query(Sys_unit.class, Cnd.where("unitcode", "=", code) + .and("unionId", "=", union.getId()).and("unitLevel", "=", 2)); + if (units.size() != 1) { + outcome(row, "失败", "所属单位编码不存在、不唯一或不是该分工会的二级单位:" + code); + return; + } + unitIds.add(units.get(0).getId()); + } + } else if (StrUtil.isNotBlank(unitCodes)) { + outcome(row, "失败", "普通干部角色的所属单位编码应留空"); + return; + } + // 经由注入的原控制器代理调用,保留单行事务、直接授权及缓存清理逻辑。 + Result result = secretary + ? sysUnionController.insertBranchUnionPartySecretaryRole(user.getId(), union.getId(), unitIds.toArray(new String[0]), j) + : sysUnionController.insertBranchUnionUserRole(user.getId(), roleCode, union.getId(), j); + if (result.isSuccess()) { + outcome(row, "成功", "添加成功"); + } else if ("请勿重复添加".equals(result.getMsg()) || "该人员已设置二级党委书记,请先删除后再重新设置".equals(result.getMsg())) { + outcome(row, "跳过", result.getMsg()); + } else { + outcome(row, "失败", result.getMsg()); + } + } + + private void outcome(NutMap row, String status, String message) { + row.setv("status", status).setv("message", message); + } + + private String cellText(Row row, int column, DataFormatter formatter) { + Cell cell = row == null ? null : row.getCell(column); + if (cell != null && cell.getCellType() == CellType.FORMULA) { + throw new IllegalArgumentException("导入内容不支持公式"); + } + return formatter.formatCellValue(cell).trim(); + } + + private Sheet createSheet(Workbook workbook, String name, String... headers) { + Sheet sheet = workbook.createSheet(name); + Row header = sheet.createRow(0); + for (int i = 0; i < headers.length; i++) { + header.createCell(i).setCellValue(headers[i]); + sheet.setColumnWidth(i, 30 * 256); + } + sheet.createFreezePane(0, 1); + return sheet; + } + + private void append(Sheet sheet, String... values) { + Row row = sheet.createRow(sheet.getLastRowNum() + 1); + for (int i = 0; i < values.length; i++) { + row.createCell(i).setCellValue(values[i] == null ? "" : values[i]); + } + } +} diff --git a/src/main/java/com/budwk/app/zhgh/club/controller/infoManage/ClubRegisterImportController.java b/src/main/java/com/budwk/app/zhgh/club/controller/infoManage/ClubRegisterImportController.java new file mode 100644 index 0000000..1cf8ff4 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/club/controller/infoManage/ClubRegisterImportController.java @@ -0,0 +1,150 @@ +package com.budwk.app.zhgh.club.controller.infoManage; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.annotation.SaCheckRole; +import cn.hutool.core.util.StrUtil; +import com.budwk.app.base.result.Result; +import com.budwk.app.base.utils.CommonDownloadUtil; +import com.budwk.app.sys.models.Sys_dict; +import com.budwk.app.sys.services.SysDictService; +import com.budwk.app.zhgh.club.service.ClubRegisterImportService; +import lombok.extern.slf4j.Slf4j; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.util.NutMap; +import org.nutz.mvc.annotation.AdaptBy; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; +import org.nutz.mvc.annotation.Param; +import org.nutz.mvc.upload.TempFile; +import org.nutz.mvc.upload.UploadAdaptor; + +import javax.servlet.http.HttpServletResponse; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Slf4j +@IocBean +@At("/platform/club/infoManage/registerImport") +@Ok("json:full") +public class ClubRegisterImportController { + private static final String[] HEADERS = {"社团名称", "社团编码(选填)", "社团类型编码", "成立日期", "会长工号", "秘书长工号", + "发起人工号", "副会长工号(选填)", "副秘书长工号(选填)", "联系人工号(选填)", "会费标准(选填)", + "联系电话(选填)", "联系邮箱(选填)", "社团介绍及宗旨(选填)", "活动形式(选填)", "缩略图地址(选填)", + "申请成立报告文件ID(选填)", "章程草案文件ID(选填)", "经费管理办法文件ID(选填)", "年度计划文件ID(选填)", "其他附件文件ID(选填)"}; + @Inject + private ClubRegisterImportService clubRegisterImportService; + @Inject + private SysDictService sysDictService; + + @At + @Ok("void") + @SaCheckRole("SYSADMIN") + @SaCheckPermission("club.infoManage.manage") + public void downloadTemplate(HttpServletResponse response) { + Workbook workbook = new XSSFWorkbook(); + Sheet data = sheet(workbook, "社团导入", HEADERS); + CellStyle style = workbook.createCellStyle(); + style.setDataFormat(workbook.createDataFormat().getFormat("@")); + for (int i = 0; i < HEADERS.length; i++) data.setDefaultColumnStyle(i, style); + Sheet notes = sheet(workbook, "填写说明", "参数", "填写要求"); + notes.setColumnWidth(1, 100 * 256); + append(notes, "导入规则", "仅读取第一个工作表,每行一个社团,最多1000行有效数据;保留表头,工号与编码按文本填写。"); + append(notes, "必填", "社团名称、社团类型编码、成立日期、会长工号、秘书长工号、至少3名不同发起人工号。"); + append(notes, "社团编码", "选填,留空按单个注册的年度编号规则生成;已有名称或编码直接跳过,不覆盖草稿、审批中或已解散记录。"); + append(notes, "成立日期", "按yyyy-MM-dd填写,如2026-10-23;支持Excel日期单元格。"); + append(notes, "人员", "使用系统内已有工号。会长和秘书长各1人;副职可选多人,用逗号分隔,理事机构人员不得重复。"); + append(notes, "发起人", "至少3名不同人员,用逗号分隔;可与理事机构人员重合。"); + append(notes, "联系人", "选填工号,电话及邮箱默认取人员资料;单独填写电话、邮箱时使用填写值。"); + append(notes, "社团类型", "参照社团类型工作表填写编码,不填写名称。"); + append(notes, "其他资料", "会费标准、介绍、活动形式、缩略图地址均选填;介绍及活动形式按纯文本导入。"); + append(notes, "附件", "选填系统已上传文件的ID,多文件用逗号分隔,每类最多5个;Excel不上传附件本身,留空可后续在维护页面补充。"); + append(notes, "直接通过", "保存社团、发起人、理事会员、入会时间和年度缴费记录,授予社团角色;直接通过,不生成审批任务。"); + append(notes, "导入结果", "逐行独立保存,已存在的跳过,失败行回滚并显示原因,不影响其他行。"); + Sheet types = sheet(workbook, "社团类型", "类型编码", "类型名称"); + for (Sys_dict type : sysDictService.getSubListByCode("CLUB_REGISTER_TYPE")) append(types, type.getCode(), type.getName()); + CommonDownloadUtil.download("社团批量注册模板.xlsx", workbook, response); + } + + @At + @SaCheckRole("SYSADMIN") + @SaCheckPermission("club.infoManage.manage") + @AdaptBy(type = UploadAdaptor.class, args = {"ioc:fileUpload"}) + public Result doImport(@Param("file") TempFile file) { + if (file == null) return Result.error("请选择Excel文件"); + List rows = new ArrayList<>(); + List numbers = new ArrayList<>(); + try (InputStream input = file.getInputStream(); Workbook workbook = WorkbookFactory.create(input)) { + if (workbook.getNumberOfSheets() == 0) return Result.error("文件中没有工作表"); + Sheet sheet = workbook.getSheetAt(0); + DataFormatter formatter = new DataFormatter(); + for (int i = 0; i < HEADERS.length; i++) { + if (!HEADERS[i].equals(text(sheet.getRow(0), i, formatter))) return Result.error("表头不匹配,请使用下载的模板并保留完整表头"); + } + for (Row row : sheet) { + if (row.getRowNum() == 0) continue; + String[] values = new String[HEADERS.length]; + for (int i = 0; i < values.length; i++) values[i] = text(row, i, formatter); + if (Arrays.stream(values).allMatch(StrUtil::isBlank)) continue; + if (rows.size() >= 1000) return Result.error("每次最多导入1000行有效数据"); + rows.add(values); + numbers.add(row.getRowNum() + 1); + } + } catch (Exception e) { + log.warn("读取社团注册导入文件失败", e); + return Result.error("文件读取失败,请使用未加密的xls或xlsx模板,不要使用公式"); + } + if (rows.isEmpty()) return Result.error("文件中没有需要导入的数据"); + List results = new ArrayList<>(); + int success = 0; + int skipped = 0; + for (int i = 0; i < rows.size(); i++) { + String[] values = rows.get(i); + NutMap result; + try { + result = clubRegisterImportService.importRow(values); + } catch (IllegalArgumentException e) { + result = NutMap.NEW().addv("status", "失败").addv("message", e.getMessage()); + } catch (Exception e) { + log.error("社团批量注册失败,行号:{}", numbers.get(i), e); + result = NutMap.NEW().addv("status", "失败").addv("message", "保存失败,该行已回滚,请检查数据或联系管理员"); + } + result.setv("rowNumber", numbers.get(i)).setv("clubName", values[0]); + if (!result.containsKey("clubCode")) result.setv("clubCode", values[1]); + if ("成功".equals(result.getString("status"))) success++; + if ("跳过".equals(result.getString("status"))) skipped++; + results.add(result); + } + return Result.success(NutMap.NEW().addv("success", success).addv("skipped", skipped) + .addv("failed", results.size() - success - skipped).addv("rows", results)); + } + + private String text(Row row, int index, DataFormatter formatter) { + Cell cell = row == null ? null : row.getCell(index); + if (cell != null && cell.getCellType() == CellType.FORMULA) throw new IllegalArgumentException("不支持公式"); + if (index == 3 && cell != null && cell.getCellType() == CellType.NUMERIC && DateUtil.isCellDateFormatted(cell)) { + return cell.getLocalDateTimeCellValue().toLocalDate().toString(); + } + return formatter.formatCellValue(cell).trim(); + } + + private Sheet sheet(Workbook workbook, String name, String... headers) { + Sheet sheet = workbook.createSheet(name); + Row row = sheet.createRow(0); + for (int i = 0; i < headers.length; i++) { + row.createCell(i).setCellValue(headers[i]); + sheet.setColumnWidth(i, 28 * 256); + } + sheet.createFreezePane(0, 1); + return sheet; + } + + private void append(Sheet sheet, String... values) { + Row row = sheet.createRow(sheet.getLastRowNum() + 1); + for (int i = 0; i < values.length; i++) row.createCell(i).setCellValue(values[i]); + } +} diff --git a/src/main/java/com/budwk/app/zhgh/club/service/ClubRegisterImportService.java b/src/main/java/com/budwk/app/zhgh/club/service/ClubRegisterImportService.java new file mode 100644 index 0000000..159424b --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/club/service/ClubRegisterImportService.java @@ -0,0 +1,186 @@ +package com.budwk.app.zhgh.club.service; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.budwk.app.flow.constant.FlowConst; +import com.budwk.app.flow.entity.ProcessDefine; +import com.budwk.app.flow.entity.ProcessInstance; +import com.budwk.app.flow.enums.ProcessInstanceStateEnum; +import com.budwk.app.sys.models.*; +import com.budwk.app.sys.services.SysDictService; +import com.budwk.app.sys.services.SysRoleService; +import com.budwk.app.sys.services.SysUserService; +import com.budwk.app.web.commons.auth.utils.SecurityUtil; +import com.budwk.app.zhgh.club.model.SysClub; +import com.budwk.app.zhgh.club.model.SysClubSponsor; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.dao.Cnd; +import org.nutz.dao.Dao; +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 java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@IocBean +public class ClubRegisterImportService { + @Inject + private Dao dao; + @Inject + private SysClubService sysClubService; + @Inject + private SysDictService sysDictService; + @Inject + private SysRoleService sysRoleService; + @Inject + private SysUserService sysUserService; + + @Aop(TransAop.READ_COMMITTED) + public NutMap importRow(String[] values) { + String name = values[0]; + String code = values[1]; + if (StrUtil.isBlank(name)) { + throw new IllegalArgumentException("社团名称不能为空"); + } + if (dao.count(SysClub.class, Cnd.where("clubName", "=", name)) > 0 + || (StrUtil.isNotBlank(code) && dao.count(SysClub.class, Cnd.where("clubCode", "=", code)) > 0)) { + return NutMap.NEW().addv("status", "跳过").addv("message", "社团名称或编码已存在,保留原记录(含草稿、审批中及已解散社团)"); + } + if (name.length() > 100 || code.length() > 100) { + throw new IllegalArgumentException("社团名称和编码不能超过100个字符"); + } + if (sysDictService.getSubListByCode("CLUB_REGISTER_TYPE").stream().noneMatch(item -> values[2].equals(item.getCode()))) { + throw new IllegalArgumentException("请选择类型对照表中的社团类型编码"); + } + try { + LocalDate.parse(values[3]); + } catch (Exception e) { + throw new IllegalArgumentException("成立日期必填,格式为yyyy-MM-dd"); + } + List managers = new ArrayList<>(); + Set managerIds = new HashSet<>(); + addManagers(managers, managerIds, values[4], "CLUB_PRESIDENT", true); + addManagers(managers, managerIds, values[5], "CLUB_SECRETARY", true); + addManagers(managers, managerIds, values[7], "CLUB_VICE_PRESIDENT", false); + addManagers(managers, managerIds, values[8], "CLUB_VICE_SECRETARY", false); + List sponsors = new ArrayList<>(); + for (String loginname : split(values[6])) { + SysClubSponsor sponsor = new SysClubSponsor(); + sponsor.setSponsorId(findUser(loginname).getId()); + sponsors.add(sponsor); + } + if (sponsors.size() < 3) { + throw new IllegalArgumentException("发起人至少需要3名不同人员,多个工号用逗号分隔"); + } + ProcessDefine define = dao.fetch(ProcessDefine.class, Cnd.where("name", "=", "XHZC").and("state", "=", 1).desc("version")); + if (define == null) { + throw new IllegalArgumentException("未配置可用的社团注册定义,无法记录直接通过状态"); + } + if (StrUtil.isBlank(code)) { + int sequence = dao.count(SysClub.class, Cnd.where("year(createTime)", "=", DateUtil.thisYear())) + 1; + do { + code = DateUtil.thisYear() + String.format("%02d", sequence++); + } while (dao.count(SysClub.class, Cnd.where("clubCode", "=", code)) > 0); + } + SysClub club = new SysClub(); + club.setClubName(name); + club.setClubCode(code); + club.setClubType(values[2]); + club.setFoundTime(values[3]); + club.setSponsors(sponsors); + club.setDue(values[10]); + if (values[10].length() > 50 || values[11].length() > 20 || values[12].length() > 100 || values[15].length() > 1000) { + throw new IllegalArgumentException("会费标准、联系电话、邮箱或缩略图地址超出字段长度限制"); + } + if (StrUtil.isNotBlank(values[9])) { + Sys_user contact = findUser(values[9]); + club.setConcatPerson(contact.getId()); + club.setConcatPersonMobile(contact.getMobile()); + club.setConcatPersonEmail(contact.getEmail()); + } + if (StrUtil.isNotBlank(values[11])) club.setConcatPersonMobile(values[11]); + if (StrUtil.isNotBlank(values[12])) club.setConcatPersonEmail(values[12]); + club.setIntroduce(escapeText(values[13])); + club.setPurpose(escapeText(values[14])); + club.setThumbnail(values[15]); + club.setEstablishReport(files(values[16])); + club.setRulesFile(files(values[17])); + club.setManageFile(files(values[18])); + club.setYearPlanFile(files(values[19])); + club.setFiles(files(values[20])); + // 复用单个注册:社团、发起人、理事会员资料及入会时间、年度缴费记录和快照。 + sysClubService.doAdd(club, managers); + // 与注册通过后的角色授权一致,但不启动流程或创建审批任务。 + for (NutMap manager : managers) { + Sys_role role = sysRoleService.getByCode(manager.getString("roleCode")); + if (role == null) throw new IllegalArgumentException("社团角色不存在:" + manager.getString("roleCode")); + Sys_user_role relation = new Sys_user_role(); + relation.setUserId(manager.getString("userId")); + relation.setRoleId(role.getId()); + relation.setClubId(club.getId()); + dao.insert(relation); + } + // 现有社团查询通过已完成实例识别有效社团;保留可追溯的管理员直接通过记录。 + ProcessInstance instance = new ProcessInstance(); + instance.setProcessDefineId(define.getId()); + instance.setBusinessNo(club.getId()); + instance.setOperator(SecurityUtil.getUserId()); + instance.setState(ProcessInstanceStateEnum.FINISHED.getCode()); + instance.setVariable(JSONUtil.toJsonStr(NutMap.NEW() + .addv(FlowConst.FORM_DATA, JSONUtil.toJsonStr(club)) + .addv(FlowConst.AUTO_GEN_TITLE, name + "(系统管理员批量导入直接通过)") + .addv("directImport", true))); + dao.insert(instance); + sysUserService.clearCache(); + sysRoleService.clearCache(); + return NutMap.NEW().addv("status", "成功").addv("message", "注册成功,已直接通过").addv("clubCode", code); + } + + private void addManagers(List managers, Set ids, String text, String roleCode, boolean required) { + List names = split(text); + if (required && names.size() != 1) throw new IllegalArgumentException("会长、秘书长各需填写1名人员工号"); + if (!names.isEmpty() && sysRoleService.getByCode(roleCode) == null) { + throw new IllegalArgumentException("系统未配置社团角色:" + roleCode); + } + for (String name : names) { + Sys_user user = findUser(name); + if (!ids.add(user.getId())) throw new IllegalArgumentException("理事机构人员不能重复:" + name); + managers.add(NutMap.NEW().addv("userId", user.getId()).addv("roleCode", roleCode)); + } + } + + private Sys_user findUser(String loginname) { + List users = dao.query(Sys_user.class, Cnd.where("loginname", "=", loginname)); + if (users.size() != 1) throw new IllegalArgumentException("工号不存在或不唯一:" + loginname); + return users.get(0); + } + + private List split(String text) { + return Arrays.stream(text.split("[,,]|;|;")).map(String::trim).filter(StrUtil::isNotBlank).distinct().toList(); + } + + private List files(String text) { + List result = new ArrayList<>(); + List ids = split(text); + if (ids.size() > 5) throw new IllegalArgumentException("每类附件最多5个文件"); + for (String id : ids) { + Sys_file file = dao.fetch(Sys_file.class, id); + if (file == null || Boolean.TRUE.equals(file.getDelFlag())) throw new IllegalArgumentException("附件文件不存在:" + id); + result.add(JSONUtil.parseObj(file)); + } + return result; + } + + private String escapeText(String text) { + return text.replace("&", "&").replace("<", "<").replace(">", ">") + .replace("\r\n", "\n").replace("\n", "
"); + } +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 9590810..c220c03 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -97,6 +97,7 @@ xsssql: urls: /platform/cms/content/article/addDo,/platform/sys/app/conf/addDo,/platform/sys/app/conf/editDo beetl: + RESOURCE_LOADER: com.budwk.app.web.commons.ext.beetl.BeetlCustomResourceLoader RESOURCE: #本地路径,开发时设置本地路径,便于调试 rootLocal: '${framework.beetl.RESOURCE.rootLocal}' diff --git a/src/main/resources/views/platform/sys/union/branchUnionManage.js b/src/main/resources/views/platform/sys/union/branchUnionManage.js index 295ea60..1a57b09 100644 --- a/src/main/resources/views/platform/sys/union/branchUnionManage.js +++ b/src/main/resources/views/platform/sys/union/branchUnionManage.js @@ -8,6 +8,7 @@ const branchUnionManage = { 基层干部审核 新建分工会 + 批量添加分工会干部 @@ -39,6 +40,75 @@ const branchUnionManage = { > + + +
+
+
1下载并填写模板
+

模板内附当前工会、角色、届次及所属单位对照表。

+ 下载模板及参数对照表 +
+
必填参数
工会编码、工号、角色编码
+
选填参数
届次编码,可留空
+
书记角色
所属单位编码必填,多个二级单位用逗号分隔;其他角色留空
+
+
人员须属于对应分工会;工号与编码按文本填写,请保留模板表头。
+
+
+
2上传填写好的文件
+

支持 .xls / .xlsx,每次一个文件,最多1000行有效数据。

+ + +
正在导入,请稍候…
+
将文件拖到此处,或点击选择文件
+
+
选择文件后点击“开始导入”。逐行保存,一行失败不影响其他行。
+
+
+
+
导入结果
+
+
成功添加{{ cadreImportResult.success }}行
+
已跳过{{ cadreImportResult.skipped }}行
+
导入失败{{ cadreImportResult.failed }}行
+
+ + 全部 + 成功 + 跳过 + 失败 + + + + + + + + + + + +
+ +
@@ -61,6 +131,11 @@ const branchUnionManage = { mixins: [initTableMixins], data() { return { + cadreImportVisible: false, + cadreImportLoading: false, + cadreImportFiles: [], + cadreImportResult: null, + cadreImportFilter: "全部", dialogFormVisible: false, formData: {}, formRules: { @@ -70,6 +145,12 @@ const branchUnionManage = { } } }, + computed: { + cadreImportRows() { + const rows = this.cadreImportResult ? this.cadreImportResult.rows : [] + return this.cadreImportFilter === "全部" ? rows : rows.filter(row => row.status === this.cadreImportFilter) + } + }, methods: { pageData() { $.get("/platform/sys/union/pageData", this.pageForm).then((res) => { @@ -77,6 +158,67 @@ const branchUnionManage = { this.pageForm.totalCount = res.data.totalCount }) }, + openCadreImport() { + this.cadreImportFiles = [] + this.cadreImportResult = null + this.cadreImportVisible = true + this.$nextTick(() => this.$refs.cadreImportUpload.clearFiles()) + }, + downloadCadreTemplate() { + commonUtil.downLoadService("/platform/sys/unionCadreImport/downloadTemplate", {}) + }, + cadreImportChange(file, fileList) { + if (file.status === "ready") { + this.cadreImportFiles = fileList + this.cadreImportResult = null + } + }, + cadreImportRemove() { + this.cadreImportFiles = [] + }, + cadreImportExceed() { + this.$message.warning("每次只能导入一个文件,请先移除已选文件") + }, + beforeCadreImport(file) { + if (!/\.(xls|xlsx)$/i.test(file.name)) { + this.$message.error("请选择xls或xlsx文件") + return false + } + this.cadreImportLoading = true + return true + }, + submitCadreImport() { + if (this.cadreImportLoading || !this.cadreImportFiles.length) { + return + } + this.$refs.cadreImportUpload.submit() + }, + cadreImportSuccess(res) { + this.cadreImportLoading = false + this.$refs.cadreImportUpload.clearFiles() + this.cadreImportFiles = [] + if (!res || res.code !== 0 || !res.data) { + this.$message.error((res && res.msg) || "导入失败,请重新选择文件") + return + } + this.cadreImportFilter = "全部" + this.cadreImportResult = res.data + if (res.data.failed) { + this.$message.warning("导入完成,部分行失败,请查看结果说明") + } else { + this.$message.success("导入完成,请查看结果") + } + if (res.data.success) { + this.pageData() + this.$emit("refresh", null) + } + }, + cadreImportError() { + this.cadreImportLoading = false + this.$refs.cadreImportUpload.clearFiles() + this.cadreImportFiles = [] + this.$message.error("导入请求失败,可重新选择文件重试,已分配的角色将自动跳过") + }, // 基层干部审核 openAudit() { this.$emit('union-cadre') diff --git a/src/main/resources/views/platform/sys/union/index.html b/src/main/resources/views/platform/sys/union/index.html index 573822e..0b6d75e 100644 --- a/src/main/resources/views/platform/sys/union/index.html +++ b/src/main/resources/views/platform/sys/union/index.html @@ -49,6 +49,37 @@ layout("/layouts/platform.html"){ .union-branch-list-panel .el-pagination-container { flex-shrink: 0; } + .union-cadre-import-dialog { max-width: calc(100vw - 32px); } + .union-cadre-import-dialog .el-dialog__body { padding: 16px 24px; max-height: 72vh; overflow-y: auto; } + .union-cadre-import-dialog .el-dialog__header { padding: 20px 24px 16px; border-bottom: 1px solid #ebeef5; } + .union-cadre-import-dialog .el-dialog__footer { padding: 16px 24px; border-top: 1px solid #ebeef5; } + .union-cadre-import-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 20px; } + .union-cadre-import-panel { min-width: 0; padding: 20px; border: 1px solid #e4e7ed; border-radius: 8px; } + .union-cadre-import-heading { display: flex; align-items: center; gap: 10px; font-size: 15px; font-weight: 600; color: #303133; } + .union-cadre-import-number { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 50%; background: #ecf5ff; color: #409eff; font-size: 13px; } + .union-cadre-import-muted { color: #606266; font-size: 12px; line-height: 1.7; } + .union-cadre-import-fields { display: grid; grid-template-columns: 68px 1fr; gap: 10px; margin: 20px 0 14px; font-size: 12px; line-height: 1.7; } + .union-cadre-import-fields dt { color: #909399; } + .union-cadre-import-fields dd { margin: 0; color: #303133; } + .union-cadre-import-note { margin-top: 14px; color: #606266; font-size: 12px; line-height: 1.8; } + .union-cadre-import-panel .el-upload { display: block; width: 100%; } + .union-cadre-import-panel .el-upload-dragger { width: 100%; height: 180px; background: #fafcff; } + .union-cadre-import-panel .el-upload-dragger .el-icon-loading { display: block; margin: 40px auto 20px; font-size: 40px; color: #409eff; } + .union-cadre-import-results { margin-top: 24px; } + .union-cadre-import-summary { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin: 14px 0; } + .union-cadre-import-stat { display: flex; align-items: center; justify-content: space-between; padding: 14px 18px; border-radius: 6px; font-size: 13px; } + .union-cadre-import-stat strong { font-size: 24px; } + .union-cadre-import-stat small { margin-left: 6px; font-size: 12px; font-weight: normal; } + .union-cadre-import-stat.is-success { color: #529b2e; background: #f0f9eb; } + .union-cadre-import-stat.is-skipped { color: #606266; background: #f4f4f5; } + .union-cadre-import-stat.is-failed { color: #c45656; background: #fef0f0; } + .union-cadre-import-filters { margin-bottom: 12px; } + .union-cadre-import-footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; text-align: left; } + @media (max-width: 720px) { + .union-cadre-import-grid { grid-template-columns: 1fr; } + .union-cadre-import-dialog .el-dialog__body { padding: 16px; } + .union-cadre-import-stat { padding: 12px 8px; flex-direction: column; gap: 6px; } + }
diff --git a/src/main/resources/views/platform/zhgh/club/infoManage/manage/clubRegisterImport.js b/src/main/resources/views/platform/zhgh/club/infoManage/manage/clubRegisterImport.js new file mode 100644 index 0000000..67f713c --- /dev/null +++ b/src/main/resources/views/platform/zhgh/club/infoManage/manage/clubRegisterImport.js @@ -0,0 +1,118 @@ +const CLUB_REGISTER_IMPORT_TEMPLATE = { + template: ` + + +
+
+

1. 下载并填写模板

+ 下载模板及参数说明 +

必填:名称、类型编码、成立日期、会长和秘书长工号、至少3名发起人工号。

+

人员:会长和秘书长各1人,副职选填。多人工号用逗号分隔,理事人员不能重复。

+

选填:社团编码留空自动生成;联系人、会费、介绍、缩略图及附件等详见模板。

+

社团类型来自系统字典,模板内附当前类型对照表。

+
+
+

2. 上传Excel文件

+ + +
{{ loading ? '正在导入,请稍候…' : '拖拽文件到此处,或点击选择文件' }}
+
+

支持xls、xlsx,每次最多1000行有效数据。保留完整表头,工号与编码按文本填写。

+

附件列可留空;填写时使用已上传文件的ID,模板本身不上传附件。

+
+
+
+
+ 成功 {{ result.success }} 行 + 跳过 {{ result.skipped }} 行 + 失败 {{ result.failed }} 行 +
+ + {{ item }} + + + + + + + + + + +
+ +
+ `, + data() { + return { visible: false, loading: false, files: [], result: null, filter: "全部" } + }, + computed: { + rows() { + const rows = this.result ? this.result.rows : [] + return this.filter === "全部" ? rows : rows.filter(row => row.status === this.filter) + } + }, + methods: { + open() { + this.files = [] + this.result = null + this.filter = "全部" + this.visible = true + this.$nextTick(() => this.$refs.upload.clearFiles()) + }, + download() { + commonUtil.downLoadService("/platform/club/infoManage/registerImport/downloadTemplate", {}) + }, + change(file, files) { + if (file.status === "ready") { + this.files = files + this.result = null + } + }, + remove() { this.files = [] }, + exceed() { this.$message.warning("每次选择一个文件,请先移除已选文件") }, + beforeUpload(file) { + if (!/\.(xls|xlsx)$/i.test(file.name)) { + this.$message.error("请选择xls或xlsx文件") + return false + } + this.loading = true + return true + }, + submit() { + if (!this.loading && this.files.length) this.$refs.upload.submit() + }, + success(res) { + this.loading = false + this.files = [] + this.$refs.upload.clearFiles() + if (!res || res.code !== 0 || !res.data) { + this.$message.error((res && res.msg) || "导入失败,请重新选择文件") + return + } + this.result = res.data + this.filter = "全部" + this.$message[res.data.failed ? 'warning' : 'success']("导入完成,请查看逐行处理结果") + if (res.data.success) this.$emit("completed") + }, + error() { + this.loading = false + this.files = [] + this.$refs.upload.clearFiles() + this.$message.error("导入请求失败,可重新选择文件重试,已存在的社团会自动跳过") + } + } +} diff --git a/src/main/resources/views/platform/zhgh/club/infoManage/manage/index.html b/src/main/resources/views/platform/zhgh/club/infoManage/manage/index.html index 9ffe55f..e92ac3f 100644 --- a/src/main/resources/views/platform/zhgh/club/infoManage/manage/index.html +++ b/src/main/resources/views/platform/zhgh/club/infoManage/manage/index.html @@ -69,6 +69,24 @@ layout("/layouts/platform.html"){ #app .club-manage-list-card .el-pagination-container { flex: 0 0 auto; } + .club-register-import-dialog { max-width: calc(100vw - 32px); } + .club-register-import-dialog .el-dialog__body { padding: 16px 24px; max-height: 72vh; overflow-y: auto; } + .club-register-import-dialog .el-dialog__header { padding: 20px 24px 16px; border-bottom: 1px solid #ebeef5; } + .club-register-import-dialog .el-dialog__footer { padding: 16px 24px; border-top: 1px solid #ebeef5; } + .club-register-import-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin: 20px 0; } + .club-register-import-panel { min-width: 0; padding: 18px; border: 1px solid #e4e7ed; border-radius: 8px; } + .club-register-import-panel h4 { margin: 0 0 16px; color: #303133; } + .club-register-import-panel p { font-size: 12px; line-height: 1.8; } + .club-register-import-muted { color: #606266; font-size: 12px; line-height: 1.7; } + .club-register-import-panel .el-upload, .club-register-import-panel .el-upload-dragger { width: 100%; } + .club-register-import-panel .el-upload-dragger { background: #fafcff; } + .club-register-import-panel .el-icon-loading { display: block; margin: 40px auto 20px; font-size: 40px; color: #409eff; } + .club-register-import-summary { display: flex; gap: 12px; margin-bottom: 16px; } + .club-register-import-footer { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; } + @media (max-width: 720px) { + .club-register-import-grid { grid-template-columns: 1fr; } + .club-register-import-dialog .el-dialog__body { padding: 16px; } + }
@@ -192,6 +210,7 @@ layout("/layouts/platform.html"){ + new Vue({ diff --git a/src/main/resources/views/platform/zhgh/club/infoManage/manage/schoolClubManage.js b/src/main/resources/views/platform/zhgh/club/infoManage/manage/schoolClubManage.js index cf07a87..098e06a 100644 --- a/src/main/resources/views/platform/zhgh/club/infoManage/manage/schoolClubManage.js +++ b/src/main/resources/views/platform/zhgh/club/infoManage/manage/schoolClubManage.js @@ -22,6 +22,8 @@ const SCHOOL_CLUB_MANAGE_TEMPLATE = {