feat(property):

- 新增入驻员工导入功能,支持导入员工信息和人脸数据
This commit is contained in:
zcxlsm 2025-07-29 02:04:15 +08:00
parent 5274fb8d64
commit e3e26f46c1
8 changed files with 478 additions and 13 deletions

View File

@ -1,11 +1,17 @@
package org.dromara.property.controller;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.dromara.common.excel.core.ExcelResult;
import org.dromara.property.domain.vo.ResidentPersonImportVo;
import org.dromara.property.listener.ResidentPersonImportListener;
import org.dromara.property.utils.UploadFaceUtil;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
@ -21,6 +27,7 @@ import org.dromara.property.domain.vo.ResidentPersonVo;
import org.dromara.property.domain.bo.ResidentPersonBo;
import org.dromara.property.service.IResidentPersonService;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.springframework.web.multipart.MultipartFile;
/**
* 入驻员工
@ -37,6 +44,8 @@ public class ResidentPersonController extends BaseController {
private final IResidentPersonService residentPersonService;
private final UploadFaceUtil uploadFaceUtil;
/**
* 查询入驻员工列表
*/
@ -65,7 +74,7 @@ public class ResidentPersonController extends BaseController {
@SaCheckPermission("property:person:query")
@GetMapping("/{id}")
public R<ResidentPersonVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable("id") Long id) {
@PathVariable("id") Long id) {
return R.ok(residentPersonService.queryById(id));
}
@ -103,4 +112,39 @@ public class ResidentPersonController extends BaseController {
@PathVariable("ids") Long[] ids) {
return toAjax(residentPersonService.deleteWithValidByIds(List.of(ids), true));
}
/**
* 导入数据
*
* @param file 导入文件
* @param updateSupport 是否更新已存在数据
* @param unitId 单位id
*/
@Log(title = "入驻员工", businessType = BusinessType.IMPORT)
@SaCheckPermission("property:person:import")
@PostMapping(value = "/importData", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public R<Void> importData(@RequestPart("file") MultipartFile file, boolean updateSupport, Long unitId) throws Exception {
ExcelResult<ResidentPersonImportVo> result = ExcelUtil.importExcel(file.getInputStream(), ResidentPersonImportVo.class, new ResidentPersonImportListener(updateSupport, unitId));
return R.ok(result.getAnalysis());
}
/**
* 获取导入模板
*/
@PostMapping("/importTemplate")
public void importTemplate(HttpServletResponse response) {
ExcelUtil.exportExcel(new ArrayList<>(), "入驻员工", ResidentPersonImportVo.class, response);
}
/**
* 导入人脸数据
*
* @param file 导入文件
* @param unitId 单位ID
*/
@PostMapping(value = "/importFace", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public R<Void> importFace(@RequestPart("file") MultipartFile file, Long unitId) {
uploadFaceUtil.processFaceZip(file, unitId);
return R.ok();
}
}

View File

@ -0,0 +1,75 @@
package org.dromara.property.domain.vo;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.dromara.common.excel.annotation.ExcelDictFormat;
import org.dromara.common.excel.convert.ExcelDictConvert;
import org.dromara.property.domain.ResidentPerson;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 入驻员工视图对象 resident_person
*
* @author mocheng
* @since 2025-06-19
*/
@Data
@NoArgsConstructor
public class ResidentPersonImportVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 用户名称
*/
@ExcelProperty(value = "用户名称")
private String userName;
/**
* 联系电话
*/
@ExcelProperty(value = "联系电话")
private String phone;
/**
* 用户性别
*/
@ExcelProperty(value = "性别", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "sys_user_sex")
private String gender;
/**
* 证件号
*/
@ExcelProperty(value = "证件号")
private String idCard;
/**
* 邮箱
*/
@ExcelProperty(value = "邮箱")
private String email;
/**
* 车牌号码
*/
@ExcelProperty(value = "车牌号码")
private String carNumber;
/**
* 备注
*/
@ExcelProperty(value = "备注")
private String remark;
}

View File

@ -0,0 +1,141 @@
package org.dromara.property.listener;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.http.HtmlUtil;
import cn.idev.excel.context.AnalysisContext;
import cn.idev.excel.event.AnalysisEventListener;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.ValidatorUtils;
import org.dromara.common.excel.core.ExcelListener;
import org.dromara.common.excel.core.ExcelResult;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.property.domain.bo.ResidentPersonBo;
import org.dromara.property.domain.vo.ResidentPersonImportVo;
import org.dromara.property.domain.vo.ResidentPersonVo;
import org.dromara.property.domain.vo.ResidentUnitVo;
import org.dromara.property.service.IResidentPersonService;
import org.dromara.property.service.IResidentUnitService;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* @author lsm
* @apiNote ResidentPersonImportListener
* @since 2025/7/28
*/
@Slf4j
public class ResidentPersonImportListener extends AnalysisEventListener<ResidentPersonImportVo> implements ExcelListener<ResidentPersonImportVo> {
private final IResidentPersonService residentPersonService;
private final IResidentUnitService residentUnitService;
private final Boolean isUpdateSupport;
private final Long unitId;
private int successNum = 0;
private int failureNum = 0;
private final StringBuilder successMsg = new StringBuilder();
private final StringBuilder failureMsg = new StringBuilder();
public ResidentPersonImportListener(Boolean isUpdateSupport, Long unitId) {
this.residentPersonService = SpringUtils.getBean(IResidentPersonService.class);
this.residentUnitService = SpringUtils.getBean(IResidentUnitService.class);
this.isUpdateSupport = isUpdateSupport;
this.unitId = unitId;
}
@Override
public void invoke(ResidentPersonImportVo personVo, AnalysisContext context) {
ResidentUnitVo unitVo = residentUnitService.queryById(unitId);
List<ResidentPersonVo> list = new ArrayList<>();
// 判断证件号是否为空
if (StringUtils.isEmpty(personVo.getIdCard())) {
failureNum++;
failureMsg.append("<br/>").append(failureNum).append("、账号 ").append(personVo.getUserName()).append(" 证件号不能为空!");
} else {
ResidentPersonBo personBo = new ResidentPersonBo();
personBo.setUnitId(unitId);
personBo.setIdCard(personVo.getIdCard());
list = residentPersonService.queryList(personBo);
}
try {
if (list.isEmpty()) { // 判断当前单位是否已存在该用户
ResidentPersonBo bo = BeanUtil.toBean(personVo, ResidentPersonBo.class);
ValidatorUtils.validate(bo);
bo.setState(1L);
bo.setUnitId(unitId);
bo.setTime(new Date());
bo.setUnitName(unitVo.getName());
bo.setAuthGroupId(unitVo.getAuthGroupId());
bo.setAuthBegDate(unitVo.getAuthBegDate());
bo.setAuthEndDate(unitVo.getAuthEndDate());
residentPersonService.insertByBo(bo);
successNum++;
successMsg.append("<br/>").append(successNum).append("、账号 ").append(bo.getUserName()).append(" 导入成功");
} else if (isUpdateSupport) {
Long id = list.get(0).getUserId();
ResidentPersonBo bo = BeanUtil.toBean(personVo, ResidentPersonBo.class);
bo.setId(id);
ValidatorUtils.validate(bo);
bo.setUpdateBy(LoginHelper.getUserId());
residentPersonService.updateByBo(bo);
successNum++;
successMsg.append("<br/>").append(successNum).append("、账号 ").append(bo.getUserName()).append(" 更新成功");
} else {
failureNum++;
failureMsg.append("<br/>").append(failureNum).append("、账号 ").append(list.get(0).getUserName()).append(" 已存在");
}
} catch (Exception e) {
failureNum++;
String msg = "<br/>" + failureNum + "、账号 " + HtmlUtil.cleanHtmlTag(personVo.getUserName()) + " 导入失败:";
String message = e.getMessage();
if (e instanceof ConstraintViolationException cvException) {
message = StreamUtils.join(cvException.getConstraintViolations(), ConstraintViolation::getMessage, ", ");
}
failureMsg.append(msg).append(message);
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
}
@Override
public ExcelResult<ResidentPersonImportVo> getExcelResult() {
return new ExcelResult<>() {
@Override
public String getAnalysis() {
if (failureNum > 0) {
failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
throw new ServiceException(failureMsg.toString());
} else {
successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");
}
return successMsg.toString();
}
@Override
public List<ResidentPersonImportVo> getList() {
return null;
}
@Override
public List<String> getErrorList() {
return null;
}
};
}
}

View File

@ -121,10 +121,13 @@ public class ResidentPersonServiceImpl implements IResidentPersonService {
// 首次入驻新用户权限组默认使用公司权限
ResidentUnitVo ruVo = residentUnitService.queryById(bo.getUnitId());
add.setAuthGroupId(ruVo.getAuthGroupId());
add.setAuthBegDate(ruVo.getAuthBegDate());
add.setAuthEndDate(ruVo.getAuthEndDate());
boolean flag = baseMapper.insert(add) > 0;
Assert.isTrue(flag, "员工入驻失败!");
if (flag) {
// 存在图片时才同步授权
if (flag && add.getImg() != null) {
log.info("开始写入授权记录, {}", bo.getUserName());
RemotePersonAuth personAuth = new RemotePersonAuth();
personAuth.setId(add.getId());
@ -190,7 +193,7 @@ public class ResidentPersonServiceImpl implements IResidentPersonService {
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if (isValid) {
LambdaQueryWrapper<ResidentPerson> lqw = new LambdaQueryWrapper<>();
lqw.eq(ResidentPerson::getId, ids);
lqw.in(ResidentPerson::getId, ids);
List<ResidentPersonVo> list = baseMapper.selectVoList(lqw);
boolean hasEnabled = list.stream()

View File

@ -0,0 +1,205 @@
package org.dromara.property.utils;
import cn.hutool.core.bean.BeanUtil;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboReference;
import org.dromara.property.domain.bo.ResidentPersonBo;
import org.dromara.property.domain.vo.ResidentPersonVo;
import org.dromara.property.service.IResidentPersonService;
import org.dromara.resource.api.RemoteFileService;
import org.dromara.resource.api.domain.RemoteFile;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* @author lsm
* @apiNote UploadFaceUtil
* @since 2025/7/29
*/
@Slf4j
@Service
public class UploadFaceUtil {
@DubboReference
private RemoteFileService remoteFileService;
@Resource
private IResidentPersonService residentPersonService;
// 安全配置参数实际项目中可以从配置文件读取
private static final int MAX_TOTAL_SIZE = 50 * 1024 * 1024; // 50MB 最大解压总大小
private static final int MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB 最大单个文件大小
private static final int MAX_FILE_COUNT = 30; // 最大文件数量
private static final Map<String, String> CONTENT_TYPE_MAP = new HashMap<>();
static {
CONTENT_TYPE_MAP.put("jpg", "image/jpeg");
CONTENT_TYPE_MAP.put("jpeg", "image/jpeg");
CONTENT_TYPE_MAP.put("png", "image/png");
CONTENT_TYPE_MAP.put("gif", "image/gif");
}
// 统计信息
private int totalFiles = 0;
private int processedFiles = 0;
private int successUploads = 0;
private int failedUploads = 0;
public void processFaceZip(MultipartFile zipFile, Long unitId) {
// 重置统计信息
resetStats();
try (ZipInputStream zis = new ZipInputStream(zipFile.getInputStream())) {
ZipEntry entry;
byte[] buffer = new byte[8192]; // 8KB缓冲区
long totalExtractedSize = 0;
int fileCount = 0;
while ((entry = zis.getNextEntry()) != null) {
// 跳过目录
if (entry.isDirectory()) {
zis.closeEntry();
continue;
}
// 1. 文件数量检查
if (++fileCount > MAX_FILE_COUNT) {
throw new SecurityException("ZIP炸弹防护文件数量超过限制 (" + MAX_FILE_COUNT + ")");
}
// 2. 单个文件大小检查
long entrySize = entry.getSize();
if (entrySize > MAX_FILE_SIZE) {
throw new SecurityException("ZIP炸弹防护文件 '" + entry.getName() +
"' 大小超过限制 (" + formatSize(MAX_FILE_SIZE) + ")");
}
// 3. 总大小检查
if (entrySize != -1) { // 有些ZIP实现可能返回-1
if (totalExtractedSize + entrySize > MAX_TOTAL_SIZE) {
throw new SecurityException("ZIP炸弹防护解压总大小超过限制 (" +
formatSize(MAX_TOTAL_SIZE) + ")");
}
totalExtractedSize += entrySize;
}
// 4. 文件类型验证
if (!isImageFile(entry.getName())) {
zis.closeEntry();
continue; // 跳过非图片文件
}
// 获取姓名移除文件扩展名
String name = extractName(entry.getName());
String contentType = getContentType(entry.getName());
// 读取图片数据使用安全方式
ByteArrayOutputStream bao = new ByteArrayOutputStream();
int len;
long actualSize = 0;
// 流式读取并检查实际大小
while ((len = zis.read(buffer)) > 0) {
// 检查实际读取大小是否超过限制
actualSize += len;
if (actualSize > MAX_FILE_SIZE) {
throw new SecurityException("ZIP炸弹防护文件 '" + entry.getName() +
"' 实际大小超过限制 (" + formatSize(MAX_FILE_SIZE) + ")");
}
// 检查总大小
if (totalExtractedSize + actualSize > MAX_TOTAL_SIZE) {
throw new SecurityException("ZIP炸弹防护解压总大小超过限制 (" +
formatSize(MAX_TOTAL_SIZE) + ")");
}
bao.write(buffer, 0, len);
}
ResidentPersonBo bo = new ResidentPersonBo();
bo.setUnitId(unitId);
bo.setUserName(name);
List<ResidentPersonVo> personVos = residentPersonService.queryList(bo);
// 判断当前姓名是否存在入驻单位
if (personVos.isEmpty()) continue;
byte[] imageData = bao.toByteArray();
RemoteFile remoteFile = remoteFileService.upload(name, name, contentType, imageData);
personVos.get(0).setImg(remoteFile.getOssId().toString());
ResidentPersonBo updateBo = BeanUtil.toBean(personVos.get(0), ResidentPersonBo.class);
residentPersonService.updateByBo(updateBo);
totalFiles++;
// 关闭当前entry
zis.closeEntry();
// 更新处理进度
processedFiles++;
}
// 打印统计信息
printStatistics();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private void resetStats() {
totalFiles = 0;
processedFiles = 0;
successUploads = 0;
failedUploads = 0;
}
private void printStatistics() {
System.out.println("\n===== ZIP处理统计 =====");
System.out.println("总文件数: " + totalFiles);
System.out.println("已处理文件数: " + processedFiles);
System.out.println("成功上传: " + successUploads);
System.out.println("失败上传: " + failedUploads);
System.out.println("=======================");
}
private String formatSize(long bytes) {
if (bytes < 1024) return bytes + " B";
int exp = (int) (Math.log(bytes) / Math.log(1024));
char unit = "KMGTPE".charAt(exp - 1);
return String.format("%.1f %sB", bytes / Math.pow(1024, exp), unit);
}
private String extractName(String fileName) {
// 移除路径和扩展名例如 "王五.jpg" -> "王五"
String baseName = new File(fileName).getName();
int dotIndex = baseName.lastIndexOf('.');
return (dotIndex == -1) ? baseName : baseName.substring(0, dotIndex);
}
private boolean isImageFile(String fileName) {
// 检查常见图片扩展名
String[] imgExtensions = {".jpg", ".jpeg", ".png", ".gif"};
String lowerName = fileName.toLowerCase();
for (String ext : imgExtensions) {
if (lowerName.endsWith(ext)) return true;
}
return false;
}
public String getContentType(String filename) {
String extension = filename.substring(filename.lastIndexOf(".") + 1).toLowerCase();
return CONTENT_TYPE_MAP.getOrDefault(extension, "application/octet-stream");
}
}

View File

@ -23,10 +23,7 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.*;
import static org.dromara.sis.sdk.hik.HCNetSDK.*;
@ -420,8 +417,8 @@ public class HikAlarmCallBack implements HCNetSDK.FMSGCallBack_V31 {
// try {
// Thread.sleep(10000L);
// List<Integer> ass = Arrays.asList(3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3);
// for (int i = 0; i < arrs.size(); i++) {
// HikApiService.getInstance().controlGateway("192.168.24.188", (i + 1), arrs.get(i));
// for (int i = 0; i < ass.size(); i++) {
// HikApiService.getInstance().controlGateway("192.168.24.188", (i + 1), ass.get(i));
// }
// } catch (InterruptedException e) {
// throw new RuntimeException(e);

View File

@ -17,7 +17,7 @@ public class FinaHWPersonReq {
/**
* 相似度
*/
private String similarityThreshold = "85";
private String similarityThreshold = "80";
/**
* page
*/

View File

@ -1,4 +1,4 @@
package org.dromara.sis.config.timer;
package org.dromara.sis.task;
import cn.dev33.satoken.context.mock.SaTokenContextMockUtil;
import cn.dev33.satoken.stp.StpUtil;
@ -35,14 +35,14 @@ import java.util.concurrent.atomic.AtomicReference;
/**
* @author lsm
* @apiNote AuthTimer
* @apiNote AuthSyncTask
* @since 2025/7/26
*/
@Slf4j
@Configuration
@EnableScheduling
@RequiredArgsConstructor
public class AuthTimer {
public class AuthSyncTask {
@DubboReference
private RemoteFileService remoteFileService;