package com.ruoyi.web.controller.api; import com.ruoyi.common.annotation.Log; import com.ruoyi.common.config.RuoYiConfig; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.enums.BusinessType; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.system.domain.SysDoorDevice; import com.ruoyi.system.dto.SysEmployeeAddRequest; import com.ruoyi.system.dto.SysEmployeeDeleteRequest; import com.ruoyi.system.dto.SysEmployeeDetailRequest; import com.ruoyi.system.dto.SysEmployeeEditRequest; import com.ruoyi.system.dto.SysEmployeePageRequest; import com.ruoyi.system.dto.SysEmployeeStatusRequest; import com.ruoyi.system.dto.SysEmployeeUnbindRequest; import com.ruoyi.system.mapper.SysDoorDeviceMapper; import com.ruoyi.system.service.ISysEmployeeService; import com.ruoyi.system.vo.SysEmployeeVO; import com.ruoyi.web.controller.tool.HikvisionAccessControlApi; import com.ruoyi.web.controller.tool.MiddlePlatformDataQuery; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.URLEncoder; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; /** * 员工管理接口 * * @author ruoyi */ @RestController @RequestMapping("/system/employee") public class SysEmployeeController extends BaseController { @Autowired private ISysEmployeeService employeeService; @Autowired private RuoYiConfig ruoyiConfig; @Autowired private SysDoorDeviceMapper doorDeviceMapper; /** * 分页查询员工列表 */ @Log(title = "员工管理", businessType = BusinessType.SELECT) @PreAuthorize("@ss.hasPermi('visitor:employee:view')") @PostMapping("/page") public AjaxResult page(@RequestBody(required = false) SysEmployeePageRequest request) { // 集群版对接中台时,员工属于中台人员主数据,本地只保留查询入口兼容前端。 if (ruoyiConfig.isConnectMiddlePlatform()) { try { return success(MiddlePlatformDataQuery.employeePage(request)); } catch (IOException e) { return middlePlatformError(e); } } return success(employeeService.pageList(request)); } /** * 查询员工详情 */ @Log(title = "员工管理", businessType = BusinessType.SELECT) @PreAuthorize("@ss.hasPermi('visitor:employee:view')") @PostMapping("/detail") public AjaxResult detail(@RequestBody SysEmployeeDetailRequest request) { // 员工详情跟随列表数据源,避免同一员工在列表和详情展示不同字段。 if (ruoyiConfig.isConnectMiddlePlatform()) { try { return success(MiddlePlatformDataQuery.employeeDetail(request.getEmployeeId())); } catch (IOException e) { return middlePlatformError(e); } } return success(employeeService.detail(request)); } /** * 新增员工 */ @Log(title = "员工管理", businessType = BusinessType.INSERT) @PreAuthorize("@ss.hasPermi('visitor:employee:add')") @PostMapping("/add") public AjaxResult add(@RequestBody SysEmployeeAddRequest request) { int rows = employeeService.add(request, getUsername()); AjaxResult ajax = toAjax(rows); if (rows > 0) { ajax.put("hikvisionIssue", issueEmployeeToHikvision(request)); } return ajax; } /** * 编辑员工 */ @Log(title = "员工管理", businessType = BusinessType.UPDATE) @PreAuthorize("@ss.hasPermi('visitor:employee:edit')") @PostMapping("/edit") public AjaxResult edit(@RequestBody SysEmployeeEditRequest request) { return toAjax(employeeService.edit(request, getUsername())); } /** * 启用或禁用员工 */ @Log(title = "员工管理", businessType = BusinessType.UPDATE) @PreAuthorize("@ss.hasPermi('visitor:employee:status')") @PostMapping("/changeStatus") public AjaxResult changeStatus(@RequestBody SysEmployeeStatusRequest request) { return toAjax(employeeService.changeStatus(request, getUsername())); } /** * 卡片解绑 */ @Log(title = "员工管理", businessType = BusinessType.UPDATE) @PreAuthorize("@ss.hasPermi('visitor:employee:unbind-card')") @PostMapping("/unbindCard") public AjaxResult unbindCard(@RequestBody SysEmployeeUnbindRequest request) { return toAjax(employeeService.unbindCard(request, getUsername())); } /** * 人脸解绑 */ @Log(title = "员工管理", businessType = BusinessType.UPDATE) @PreAuthorize("@ss.hasPermi('visitor:employee:unbind-face')") @PostMapping("/unbindFace") public AjaxResult unbindFace(@RequestBody SysEmployeeUnbindRequest request) { return toAjax(employeeService.unbindFace(request, getUsername())); } /** * 删除员工 */ @Log(title = "员工管理", businessType = BusinessType.DELETE) @PreAuthorize("@ss.hasPermi('visitor:employee:remove')") @PostMapping("/delete") public AjaxResult delete(@RequestBody SysEmployeeDeleteRequest request) { List employees = loadDeleteEmployees(request); Map hikvisionDelete = deleteEmployeesFromHikvision(employees); AjaxResult ajax = toAjax(employeeService.delete(request, getUsername())); ajax.put("hikvisionDelete", hikvisionDelete); return ajax; } /** * 下载员工导入模板 */ @Log(title = "员工管理", businessType = BusinessType.EXPORT) @PreAuthorize("@ss.hasPermi('visitor:employee:template')") @PostMapping("/importTemplate") public void importTemplate(HttpServletResponse response) throws IOException { response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); String fileName = URLEncoder.encode("员工导入模板", "UTF-8").replaceAll("\\+", "%20"); response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx"); try (Workbook workbook = new XSSFWorkbook()) { Sheet sheet = workbook.createSheet("员工导入"); String[] headers = {"员工名称", "员工编号", "联系电话", "所属部门", "岗位", "卡片"}; Row headerRow = sheet.createRow(0); for (int index = 0; index < headers.length; index++) { Cell cell = headerRow.createCell(index); cell.setCellValue(headers[index]); sheet.setColumnWidth(index, 20 * 256); } Row sampleRow = sheet.createRow(1); sampleRow.createCell(0).setCellValue("张三"); sampleRow.createCell(1).setCellValue("SY1001"); sampleRow.createCell(2).setCellValue("13800000000"); sampleRow.createCell(3).setCellValue("综合管理部"); sampleRow.createCell(4).setCellValue("员工"); sampleRow.createCell(5).setCellValue("15151665655"); workbook.write(response.getOutputStream()); } } /** * 批量导入员工 */ @Log(title = "员工管理", businessType = BusinessType.IMPORT) @PreAuthorize("@ss.hasPermi('visitor:employee:import')") @PostMapping("/importData") public AjaxResult importData(@RequestParam("file") MultipartFile file) throws IOException { if (file == null || file.isEmpty()) { return error("请选择导入文件"); } return success(employeeService.importData(file.getInputStream(), getUsername())); } /** * 目的:中台查询异常时返回统一业务错误,避免异常栈直接暴露给前端。 */ private List loadDeleteEmployees(SysEmployeeDeleteRequest request) { if (request == null || request.getEmployeeIds() == null || request.getEmployeeIds().isEmpty()) { throw new ServiceException("请选择员工"); } List employees = new ArrayList(); Set uniqueEmployeeIds = new HashSet(); for (Long employeeId : request.getEmployeeIds()) { if (employeeId == null || employeeId <= 0) { throw new ServiceException("员工主键不能为空"); } if (!uniqueEmployeeIds.add(employeeId)) { continue; } SysEmployeeDetailRequest detailRequest = new SysEmployeeDetailRequest(); detailRequest.setEmployeeId(employeeId); employees.add(employeeService.detail(detailRequest)); } return employees; } private Map deleteEmployeesFromHikvision(List employees) { Map result = new LinkedHashMap(); result.put("employees", employees.size()); List devices = doorDeviceMapper.selectEnabledHikvisionDevices(); result.put("total", devices.size()); if (devices.isEmpty()) { result.put("status", "skipped"); result.put("message", "未配置启用的海康门禁设备"); return result; } String username = readHikvisionConfig("hikvision.access.username", "HIKVISION_ACCESS_USERNAME", "admin"); String password = readHikvisionConfig("hikvision.access.password", "HIKVISION_ACCESS_PASSWORD", ""); if (StringUtils.isEmpty(password)) { result.put("status", "skipped"); result.put("message", "未配置海康门禁设备密码"); handleHikvisionDeleteFailure(result); return result; } List> records = new ArrayList>(); int successCount = 0; int totalCount = devices.size() * employees.size(); for (SysDoorDevice device : devices) { for (SysEmployeeVO employee : employees) { Map record = deleteEmployeeFromHikvisionDevice(device, employee, username, password); records.add(record); if (Boolean.TRUE.equals(record.get("success"))) { successCount++; } } } result.put("success", successCount); result.put("failed", totalCount - successCount); result.put("records", records); result.put("status", successCount == totalCount ? "success" : (successCount == 0 ? "failed" : "partial")); handleHikvisionDeleteFailure(result); return result; } private Map deleteEmployeeFromHikvisionDevice(SysDoorDevice device, SysEmployeeVO employee, String username, String password) { Map record = new LinkedHashMap(); record.put("doorId", device.getDoorId()); record.put("deviceName", device.getDeviceName()); record.put("deviceId", device.getDeviceId()); record.put("employeeId", employee.getEmployeeId()); record.put("employeeNo", employee.getEmployeeNo()); record.put("cardNo", employee.getCardNo()); try { HikvisionAccessControlApi.DeviceConfig deviceConfig = HikvisionAccessControlApi.DeviceConfig .http(device.getDeviceId(), username, password); deviceConfig.setPort(readHikvisionIntConfig("hikvision.access.port", "HIKVISION_ACCESS_PORT", 80)); deviceConfig.setHttps(readHikvisionBooleanConfig("hikvision.access.https", "HIKVISION_ACCESS_HTTPS", false)); HikvisionAccessControlApi.DeleteResult deleteResult = HikvisionAccessControlApi.deleteEmployee( deviceConfig, buildHikvisionDeleteEmployee(employee)); boolean success = isHikvisionDeleteSuccess(deleteResult); record.put("success", success); record.put("result", deleteResult); if (!success) { record.put("message", "海康门禁员工删除部分失败"); } } catch (Exception e) { record.put("success", false); record.put("message", e.getMessage()); } return record; } private HikvisionAccessControlApi.EmployeeCredential buildHikvisionDeleteEmployee(SysEmployeeVO employee) { return HikvisionAccessControlApi.EmployeeCredential.of( employee.getEmployeeNo(), employee.getEmployeeName(), employee.getCardNo()); } private boolean isHikvisionDeleteSuccess(HikvisionAccessControlApi.DeleteResult result) { return result != null && isHikvisionResultSuccess(result.getUserInfo()) && isHikvisionResultSuccess(result.getCard()) && isHikvisionResultSuccess(result.getFace()); } private boolean isHikvisionResultSuccess(HikvisionAccessControlApi.HikvisionResult result) { return result == null || result.isSuccess(); } private Map issueEmployeeToHikvision(SysEmployeeAddRequest request) { Map result = new LinkedHashMap(); if (request == null || Boolean.FALSE.equals(request.getEnabled())) { result.put("status", "skipped"); result.put("message", "员工未启用,跳过海康门禁下发"); return result; } List devices = doorDeviceMapper.selectEnabledHikvisionDevices(); result.put("total", devices.size()); if (devices.isEmpty()) { result.put("status", "skipped"); result.put("message", "未配置启用的海康门禁设备"); return result; } String username = readHikvisionConfig("hikvision.access.username", "HIKVISION_ACCESS_USERNAME", "admin"); String password = readHikvisionConfig("hikvision.access.password", "HIKVISION_ACCESS_PASSWORD", ""); if (StringUtils.isEmpty(password)) { result.put("status", "skipped"); result.put("message", "未配置海康门禁设备密码"); handleHikvisionIssueFailure(result); return result; } List> records = new ArrayList>(); int successCount = 0; for (SysDoorDevice device : devices) { Map record = issueEmployeeToHikvisionDevice(device, request, username, password); records.add(record); if (Boolean.TRUE.equals(record.get("success"))) { successCount++; } } result.put("success", successCount); result.put("failed", devices.size() - successCount); result.put("records", records); result.put("status", successCount == devices.size() ? "success" : (successCount == 0 ? "failed" : "partial")); handleHikvisionIssueFailure(result); return result; } private Map issueEmployeeToHikvisionDevice(SysDoorDevice device, SysEmployeeAddRequest request, String username, String password) { Map record = new LinkedHashMap(); record.put("doorId", device.getDoorId()); record.put("deviceName", device.getDeviceName()); record.put("deviceId", device.getDeviceId()); try { HikvisionAccessControlApi.DeviceConfig deviceConfig = HikvisionAccessControlApi.DeviceConfig .http(device.getDeviceId(), username, password); deviceConfig.setPort(readHikvisionIntConfig("hikvision.access.port", "HIKVISION_ACCESS_PORT", 80)); deviceConfig.setHttps(readHikvisionBooleanConfig("hikvision.access.https", "HIKVISION_ACCESS_HTTPS", false)); HikvisionAccessControlApi.IssueResult issueResult = HikvisionAccessControlApi.issueEmployee( deviceConfig, buildHikvisionEmployee(request)); record.put("success", true); record.put("result", issueResult); } catch (Exception e) { record.put("success", false); record.put("message", e.getMessage()); } return record; } private HikvisionAccessControlApi.EmployeeCredential buildHikvisionEmployee(SysEmployeeAddRequest request) throws IOException { HikvisionAccessControlApi.EmployeeCredential employee = HikvisionAccessControlApi.EmployeeCredential.of( request.getEmployeeNo(), request.getEmployeeName(), request.getCardNo()); String faceImage = StringUtils.trim(request.getFaceImage()); if (StringUtils.isEmpty(faceImage)) { return employee; } if (isHttpUrl(faceImage)) { employee.setFaceImageUrl(faceImage); return employee; } byte[] localFaceImage = readProfileFaceImage(faceImage); if (localFaceImage != null) { employee.setFaceImage(localFaceImage); return employee; } employee.setFaceImageBase64(faceImage); return employee; } private byte[] readProfileFaceImage(String faceImage) throws IOException { String relativePath = normalizeProfileRelativePath(faceImage); if (StringUtils.isEmpty(relativePath)) { return null; } Path profileRoot = Paths.get(RuoYiConfig.getProfile()).toAbsolutePath().normalize(); Path facePath = profileRoot.resolve(relativePath).normalize(); if (!facePath.startsWith(profileRoot)) { throw new ServiceException("人脸图片路径非法"); } if (!Files.exists(facePath) || !Files.isRegularFile(facePath)) { return null; } return Files.readAllBytes(facePath); } private String normalizeProfileRelativePath(String faceImage) { String value = StringUtils.trim(faceImage).replace("\\", "/"); if (StringUtils.isEmpty(value) || value.startsWith("data:")) { return ""; } if (value.startsWith("/profile/")) { return value.substring("/profile/".length()); } if (value.startsWith("profile/")) { return value.substring("profile/".length()); } if (value.startsWith("/")) { return ""; } return value; } private boolean isHttpUrl(String value) { return value != null && (value.startsWith("http://") || value.startsWith("https://")); } private String readHikvisionConfig(String propertyName, String envName, String defaultValue) { String value = System.getProperty(propertyName); if (StringUtils.isNotEmpty(StringUtils.trim(value))) { return StringUtils.trim(value); } value = System.getenv(envName); if (StringUtils.isNotEmpty(StringUtils.trim(value))) { return StringUtils.trim(value); } return defaultValue; } private int readHikvisionIntConfig(String propertyName, String envName, int defaultValue) { String value = readHikvisionConfig(propertyName, envName, ""); if (StringUtils.isEmpty(value)) { return defaultValue; } try { return Integer.parseInt(value); } catch (NumberFormatException e) { return defaultValue; } } private boolean readHikvisionBooleanConfig(String propertyName, String envName, boolean defaultValue) { String value = readHikvisionConfig(propertyName, envName, ""); return StringUtils.isEmpty(value) ? defaultValue : Boolean.parseBoolean(value); } private void handleHikvisionIssueFailure(Map result) { if (!readHikvisionBooleanConfig("hikvision.access.fail-on-error", "HIKVISION_ACCESS_FAIL_ON_ERROR", false)) { return; } if ("failed".equals(result.get("status")) || "partial".equals(result.get("status"))) { throw new ServiceException("海康门禁员工下发失败"); } } private void handleHikvisionDeleteFailure(Map result) { if (!readHikvisionBooleanConfig("hikvision.access.fail-on-error", "HIKVISION_ACCESS_FAIL_ON_ERROR", false)) { return; } if ("failed".equals(result.get("status")) || "partial".equals(result.get("status"))) { throw new ServiceException("海康门禁员工删除失败"); } } private AjaxResult middlePlatformError(IOException e) { return error("中台数据查询失败:" + e.getMessage()); } }