待确认人员记录:导入变更调整

This commit is contained in:
2026-10-15 09:24:52 +08:00
parent 79ce184be3
commit b3c07b8bb5
5 changed files with 277 additions and 0 deletions
@@ -2,6 +2,7 @@ package com.budwk.app.zhgh.staffmanage.sourcechange.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.annotation.RepeatSubmit;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.zhgh.staffmanage.member.models.MemberChangeRecord;
@@ -13,7 +14,10 @@ import io.swagger.annotations.ApiOperation;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.AdaptBy;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.upload.TempFile;
import org.nutz.mvc.upload.UploadAdaptor;
import javax.servlet.http.HttpServletResponse;
@@ -66,6 +70,27 @@ public class PendingConfirmUserController {
pendingConfirmUserService.exportExcel(pageForm, response);
}
/**
* 导入人员类型和人员属性变更。
*
* @param file xls 或 xlsx 文件;表格中的“工号”定位人员,“编码”匹配人员属性字典配置
* @return JSON 结果;成功时 msg 包含实际更新人数
*/
@At
@Ok("json")
@RepeatSubmit
@SLog(tag = "待确认人员记录", msg = "导入人员类型和人员属性变更")
@ApiOperation("导入待确认人员变更")
@SaCheckPermission("staff.sourcechange.pending")
@AdaptBy(type = UploadAdaptor.class, args = {"ioc:fileUpload"})
public Result importChange(TempFile file) {
if (file == null || file.getFile() == null || !file.getFile().isFile()) {
throw new BaseException("请选择需要导入的Excel文件");
}
int changedCount = pendingConfirmUserService.importChange(file.getFile());
return Result.success("已成功导入变更" + changedCount + "人");
}
/**
* 直接提交待确认人员变更,不创建会员变更申请及审批流程。
*
@@ -0,0 +1,46 @@
package com.budwk.app.zhgh.staffmanage.sourcechange.mode;
import cn.afterturn.easypoi.excel.annotation.Excel;
import lombok.Data;
/**
* 待确认人员导入变更 Excel 行数据。
*
* 模板沿用列表导出字段,导入业务只采纳工号和编码,其他字段仅用于人工核对。
*/
@Data
public class PendingConfirmUserImportExcelMode {
@Excel(name = "序号")
private String index;
@Excel(name = "工号")
private String loginname;
@Excel(name = "姓名")
private String username;
@Excel(name = "性别")
private String sex;
@Excel(name = "所属工会")
private String unionName;
@Excel(name = "所属单位")
private String unitName;
@Excel(name = "人员类型")
private String personType;
@Excel(name = "人员属性")
private String userAttribute;
@Excel(name = "在职状态")
private String userState;
@Excel(name = "确认状态")
private String pendingConfirmStatusName;
@Excel(name = "编码")
private String code;
}
@@ -8,6 +8,7 @@ import com.budwk.app.zhgh.staffmanage.sourcechange.param.PendingConfirmUserBatch
import com.budwk.app.zhgh.staffmanage.sourcechange.param.PendingConfirmUserPageForm;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
/**
* 待确认人员记录业务服务。
@@ -30,6 +31,16 @@ public interface PendingConfirmUserService extends BaseService<Sys_user> {
*/
void exportExcel(PendingConfirmUserPageForm pageForm, HttpServletResponse response);
/**
* 导入待确认人员变更。
* Excel 必须包含“工号”和“编码”:工号定位系统人员,编码匹配人员属性字典的业务编码;
* 返回值为成功更新人员类型和人员属性的人员数量。
*
* @param importFile 用户上传的 xls 或 xlsx 文件
* @return 成功更新的人员数量
*/
int importChange(File importFile);
/**
* 直接提交待确认人员变更,不生成会员变更申请及流程数据。
*
@@ -1,7 +1,9 @@
package com.budwk.app.zhgh.staffmanage.sourcechange.service.impl;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.ExcelImportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.ImportParams;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.hutool.core.bean.BeanUtil;
@@ -23,8 +25,10 @@ import com.budwk.app.zhgh.staffmanage.member.service.MemberCommonService;
import com.budwk.app.zhgh.staffmanage.sourcechange.param.PendingConfirmUserBatchChangeParam;
import com.budwk.app.zhgh.staffmanage.sourcechange.param.PendingConfirmUserPageForm;
import com.budwk.app.zhgh.staffmanage.sourcechange.service.PendingConfirmUserService;
import com.budwk.app.zhgh.staffmanage.sourcechange.mode.PendingConfirmUserImportExcelMode;
import org.apache.poi.ss.usermodel.Workbook;
import org.nutz.dao.Cnd;
import org.nutz.dao.Chain;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
@@ -38,6 +42,10 @@ import org.nutz.lang.util.NutMap;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.io.File;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@@ -51,6 +59,7 @@ public class PendingConfirmUserServiceImpl extends BaseServiceImpl<Sys_user> imp
private static final int STATUS_PENDING = 1;
private static final int STATUS_CONFIRMED = 2;
private static final String USER_ATTRIBUTE_DICT_CODE = "USER_ATTRIBUTE";
/** 待确认人员只允许转为以下两个字典名称对应的编码。 */
private static final Set<String> ALLOWED_PERSON_TYPE_NAMES = Set.of("离退休人员", "其他");
@@ -103,6 +112,8 @@ public class PendingConfirmUserServiceImpl extends BaseServiceImpl<Sys_user> imp
exportEntities.add(new ExcelExportEntity("人员属性", "userAttribute", 20));
exportEntities.add(new ExcelExportEntity("在职状态", "userState", 15));
exportEntities.add(new ExcelExportEntity("确认状态", "pendingConfirmStatusName", 15));
// 编码仅作为导入填写列,导出时不设置行数据。
exportEntities.add(new ExcelExportEntity("编码", "changeCode", 20));
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
@@ -110,6 +121,147 @@ public class PendingConfirmUserServiceImpl extends BaseServiceImpl<Sys_user> imp
CommonDownloadUtil.download("待确认人员记录.xlsx", workbook, response);
}
/**
* 根据 Excel 工号和人员属性业务编码更新人员类型、人员属性。
* 所有行先统一校验,任一行异常时不执行更新;Excel 中的其他展示字段不参与变更。
*
* @param importFile 包含“工号”和“编码”列的 xls 或 xlsx 文件
* @return 实际更新的人员数量
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public int importChange(File importFile) {
if (importFile == null || !importFile.isFile()) {
throw new BaseException("请选择需要导入的Excel文件");
}
List<PendingConfirmUserImportExcelMode> importRows;
try {
ImportParams importParams = new ImportParams();
importRows = ExcelImportUtil.importExcel(importFile, PendingConfirmUserImportExcelMode.class, importParams);
} catch (Exception e) {
throw new BaseException("Excel文件解析失败,请使用待确认人员列表导出的模板");
}
if (importRows == null || importRows.isEmpty()) {
throw new BaseException("导入文件中没有人员数据");
}
// 导出的列表可能包含人工保留的空行;空工号行不参与人员查询、字典校验和更新。
List<PendingConfirmUserImportExcelMode> validImportRows = new ArrayList<>();
Map<PendingConfirmUserImportExcelMode, Integer> excelRowNumberByRow = new IdentityHashMap<>();
for (int index = 0; index < importRows.size(); index++) {
PendingConfirmUserImportExcelMode row = importRows.get(index);
if (row == null || StrUtil.isBlank(row.getLoginname())) {
continue;
}
validImportRows.add(row);
excelRowNumberByRow.put(row, index + 2);
}
if (validImportRows.isEmpty()) {
throw new BaseException("导入文件中没有有效工号数据");
}
Map<String, Sys_dict> attributeDictByBusinessCode = buildAttributeDictByBusinessCode();
Map<String, Sys_user> userByLoginname = buildImportUserMap(validImportRows);
Set<String> importedLoginnames = new HashSet<>();
// 全量校验必须在更新前完成,防止错误文件只修改其中一部分人员。
for (PendingConfirmUserImportExcelMode row : validImportRows) {
int excelRowNumber = excelRowNumberByRow.get(row);
String loginname = StrUtil.trim(row.getLoginname());
String changeCode = StrUtil.trim(row.getCode());
if (StrUtil.isBlank(changeCode)) {
throw new BaseException("第" + excelRowNumber + "行编码不能为空");
}
if (!importedLoginnames.add(loginname)) {
throw new BaseException("第" + excelRowNumber + "行工号重复:" + loginname);
}
if (!userByLoginname.containsKey(loginname)) {
throw new BaseException("第" + excelRowNumber + "行未查询到工号:" + loginname);
}
Sys_user importUser = userByLoginname.get(loginname);
if (!Integer.valueOf(STATUS_PENDING).equals(importUser.getPendingConfirmStatus())) {
throw new BaseException("第" + excelRowNumber + "行人员已不属于待确认状态:" + loginname);
}
Sys_dict attributeDict = attributeDictByBusinessCode.get(changeCode);
if (attributeDict == null) {
throw new BaseException("第" + excelRowNumber + "行编码未匹配到启用的人员属性:" + changeCode);
}
if (StrUtil.isBlank(attributeDict.getPersonType())) {
throw new BaseException("第" + excelRowNumber + "行编码未配置人员类型:" + changeCode);
}
if (StrUtil.isBlank(attributeDict.getCode())) {
throw new BaseException("第" + excelRowNumber + "行编码对应的人员属性字典值为空:" + changeCode);
}
}
int changedCount = 0;
for (PendingConfirmUserImportExcelMode row : validImportRows) {
String loginname = StrUtil.trim(row.getLoginname());
Sys_dict attributeDict = attributeDictByBusinessCode.get(StrUtil.trim(row.getCode()));
Sys_user user = userByLoginname.get(loginname);
Chain updateFields = Chain.make("personType", attributeDict.getPersonType())
.add("userAttribute", attributeDict.getCode())
.add("pendingConfirmStatus", STATUS_CONFIRMED);
int updated = dao().update(Sys_user.class, updateFields,
Cnd.where("id", "=", user.getId())
.and("pendingConfirmStatus", "=", STATUS_PENDING));
if (updated != 1) {
throw new BaseException("工号" + loginname + "的待确认状态已变化,请刷新列表后重新导出");
}
changedCount += updated;
}
return changedCount;
}
/**
* 构建人员属性业务编码映射,只接纳 USER_ATTRIBUTE 下启用且业务编码唯一的直属字典项。
*
* @return key 为 userAttributeCode,value 为人员属性字典项
*/
private Map<String, Sys_dict> buildAttributeDictByBusinessCode() {
List<Sys_dict> attributeDicts = sysDictService.getSubListByCode(USER_ATTRIBUTE_DICT_CODE);
Map<String, Sys_dict> result = new HashMap<>();
for (Sys_dict attributeDict : attributeDicts) {
String businessCode = StrUtil.trim(attributeDict.getUserAttributeCode());
if (StrUtil.isBlank(businessCode)) {
continue;
}
if (result.putIfAbsent(businessCode, attributeDict) != null) {
throw new BaseException("人员属性字典存在重复编码:" + businessCode);
}
}
return result;
}
/**
* 批量查询导入文件涉及的人员,避免逐行访问数据库。
*
* @param importRows Excel 解析后的数据行,仅使用工号字段
* @return key 为工号,value 为未删除的系统人员
*/
private Map<String, Sys_user> buildImportUserMap(List<PendingConfirmUserImportExcelMode> importRows) {
LinkedHashSet<String> loginnames = new LinkedHashSet<>();
for (PendingConfirmUserImportExcelMode row : importRows) {
if (StrUtil.isNotBlank(row.getLoginname())) {
loginnames.add(StrUtil.trim(row.getLoginname()));
}
}
if (loginnames.isEmpty()) {
return new HashMap<>();
}
// 历史人员的 delFlag 可能为空;与列表查询口径一致,将 0 和 NULL 都视为未删除。
SqlExpressionGroup activeUserCondition = Cnd.exps("delFlag", "=", false)
.or("delFlag", "is", null);
List<Sys_user> users = dao().query(Sys_user.class,
Cnd.where("loginname", "in", new ArrayList<>(loginnames)).and(activeUserCondition));
Map<String, Sys_user> result = new HashMap<>();
for (Sys_user user : users) {
result.put(user.getLoginname(), user);
}
return result;
}
/**
* 构建分页查询和导出共用的人员 SQL,确保筛选条件、确认状态及排序口径一致。
*
@@ -29,6 +29,11 @@ layout("/layouts/platform.html"){
margin-right: 8px;
}
.pending-list-toolbar-actions .pending-import-upload {
display: inline-block;
margin-right: 8px;
}
/* 在列表标题旁说明待确认人员的来源及可能情况。 */
.pending-confirm-tip {
color: #f56c6c;
@@ -128,6 +133,16 @@ layout("/layouts/platform.html"){
<el-button class="pending-batch-change-button" type="primary"
icon="el-icon-download" size="small"
@click="exportExcel">导出</el-button>
<el-upload class="pending-import-upload"
v-if="pageForm.pendingConfirmStatus === 1"
action="#"
accept=".xls,.xlsx"
:show-file-list="false"
:before-upload="validateImportFile"
:http-request="importChange">
<el-button type="primary" icon="el-icon-upload2" size="small"
:loading="importLoading" :disabled="importLoading">导入变更</el-button>
</el-upload>
<el-button class="pending-batch-change-button" type="primary"
icon="el-icon-edit" size="small"
v-if="pageForm.pendingConfirmStatus === 1"
@@ -275,6 +290,7 @@ layout("/layouts/platform.html"){
selectedPendingUsers: [],
batchDialogVisible: false,
batchSubmitting: false,
importLoading: false,
batchChangeForm: {
personType: "",
userState: "",
@@ -378,6 +394,33 @@ layout("/layouts/platform.html"){
exportExcel() {
this.$downLoad("/platform/sourcechange/pending/exportExcel", this.buildQueryPageForm())
},
/** 只接收 Excel 文件,避免无效文件进入后端解析。 */
validateImportFile(file) {
const fileName = file && file.name ? file.name.toLowerCase() : ""
const valid = fileName.endsWith(".xls") || fileName.endsWith(".xlsx")
if (!valid) {
this.$message.warning("只能上传xls或xlsx文件")
}
return valid
},
/** 上传填写了编码的列表文件,后端仅按工号和编码执行字典映射变更。 */
importChange(uploadRequest) {
const formData = new FormData()
formData.append("file", uploadRequest.file, uploadRequest.file.name)
this.importLoading = true
this.$axios.post("/platform/sourcechange/pending/importChange", formData).then((resp) => {
if (resp.code === 0) {
this.$message.success(resp.msg)
this.doSearch()
} else {
this.$message.error(resp.msg)
}
}).catch(() => {
this.$message.error("导入变更失败,请稍后重试")
}).finally(() => {
this.importLoading = false
})
},
/** 保存当前页勾选的待确认人员,供批量变更使用。 */
handleSelectionChange(selection) {
this.selectedPendingUsers = (selection || []).filter((item) => item.pendingConfirmStatus === 1)